repo_description: The flexible backend for all your projects 🐰 Turn your DB into a headless CMS, admin panels, or apps with a custom UI, instant APIs, auth & more.
repo_topics: api app cms composable data-visualization database directus graphql headless-cms javascript mariadb mssql mysql no-code node postgresql sql sqlite typescript vue
code: import { useEnv } from '@directus/env';
  import { Redis } from 'ioredis';
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
  
  interface SynchronizationManager {
  	set(key: string, value: string | number): Promise<void>;
  	get(key: string): Promise<string | null>;
  	delete(key: string): Promise<void>;
  	exists(key: string): Promise<boolean>;
  	setGreaterThan(key: string, value: number): Promise<boolean>;
  }
  
  let synchronizationManager: SynchronizationManager;
  
  function getSynchronizationManager() {
  	if (synchronizationManager) return synchronizationManager;
  
  	const env = useEnv();
  
  	if (env['SYNCHRONIZATION_STORE'] === 'redis') {
  		synchronizationManager = new SynchronizationManagerRedis();
  	} else {
  		synchronizationManager = new SynchronizationManagerMemory();
  	}
  
  	return synchronizationManager;
  }
  
  class SynchronizationManagerMemory implements SynchronizationManager {
  	private store: Record<string, string>;
  
  	constructor() {
  		this.store = {};
  	}
  
  	public async set(key: string, value: string | number): Promise<void> {
  		this.setSync(key, value);
  	}
  
  	public async get(key: string): Promise<string | null> {
  		return this.getSync(key);
  	}
  
  	public async delete(key: string): Promise<void> {
  		this.deleteSync(key);
  	}
  
  	public async exists(key: string): Promise<boolean> {
  		return this.existsSync(key);
  	}
  
  	public async setGreaterThan(key: string, value: number): Promise<boolean> {
  		if (this.existsSync(key)) {
  			const oldValue = Number(this.getSync(key));
  
  			if (value <= oldValue) {
  				return false;
  			}
  		}
  
  		this.setSync(key, value);
  
  		return true;
  	}
  
  	private setSync(key: string, value: string | number): void {
  		this.store[key] = String(value);
  	}
  
  	private getSync(key: string): string | null {
  		return this.store[key] ?? null;
  	}
  
  	private deleteSync(key: string): void {
  		delete this.store[key];
  	}
  
  	private existsSync(key: string): boolean {
  		return key in this.store;
  	}
  }
  
  const SET_GREATER_THAN_SCRIPT = `
    local key = KEYS[1]
    local value = tonumber(ARGV[1])
  
    if redis.call("EXISTS", key) == 1 then
      local oldValue = tonumber(redis.call('GET', key))
  
      if value <= oldValue then
        return false
      end
    end
  
    redis.call('SET', key, value)
  
    return true
  `;
  
  class SynchronizationManagerRedis implements SynchronizationManager {
  	private namespace: string;
  	private client: Redis;
  
  	constructor() {
  		const env = useEnv();
  		const config = getConfigFromEnv('REDIS');
  
  		this.client = new Redis(env['REDIS'] ?? config);
  		this.namespace = (env['SYNCHRONIZATION_NAMESPACE'] as string) ?? 'directus-sync';
  
  		this.client.defineCommand('setGreaterThan', {
  			numberOfKeys: 1,
  			lua: SET_GREATER_THAN_SCRIPT,
  		});
  	}
  
  	public async set(key: string, value: string | number): Promise<void> {
  		await this.client.set(this.getNamespacedKey(key), value);
  	}
  
  	public get(key: string): Promise<string | null> {
  		return this.client.get(this.getNamespacedKey(key));
  	}
  
  	public async delete(key: string): Promise<void> {
  		await this.client.del(this.getNamespacedKey(key));
  	}
  
  	public async exists(key: string): Promise<boolean> {
  		const doesExist = await this.client.exists(this.getNamespacedKey(key));
  
  		return doesExist === 1;
  	}
  
  	public async setGreaterThan(key: string, value: number): Promise<boolean> {
  		const client = this.client as Redis & {
  			setGreaterThan(key: string, value: number): Promise<number>;
  		};
  
  		const wasSet = await client.setGreaterThan(this.getNamespacedKey(key), value);
  
  		return wasSet === 1;
  	}
  
  	private getNamespacedKey(key: string): string {
  		return `${this.namespace}:${key}`;
  	}
  }
  
  export class SynchronizedClock {
  	private key: string;
  	private synchronizationManager: SynchronizationManager;
  
  	constructor(id: string) {
  		this.key = `clock:${id}`;
  		this.synchronizationManager = getSynchronizationManager();
  	}
  
  	public async set(timestamp: number): Promise<boolean> {
  		const wasSet = await this.synchronizationManager.setGreaterThan(this.key, timestamp);
  
  		return wasSet;
  	}
  
  	public async reset(): Promise<void> {
  		await this.synchronizationManager.delete(this.key);
  	}
  }
  