147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
import { BaseStore } from './base-store.js';
|
|
|
|
/**
|
|
* A store that accepts multiple stores and syncs them by key.
|
|
*
|
|
* @remarks
|
|
*
|
|
* Syncing is done by key - if values differ, then this will remain undetected.
|
|
* This is by design - local stores will sometimes prune irrelevant values as an optimization (while retaining the key).
|
|
* However, this cannot be done on the encrypted stores as the encrypted stores cannot inspect the data to know whether relevant/irrelevant.
|
|
* UUIDv7 keys are used to ensure uniqueness of keys.
|
|
*/
|
|
export class StoreComposite extends BaseStore {
|
|
static async from(localStore: BaseStore, remoteStores: Array<BaseStore>) {
|
|
const compositeStore = new this(localStore, remoteStores);
|
|
await compositeStore.sync();
|
|
return compositeStore;
|
|
}
|
|
|
|
constructor(
|
|
private localStore: BaseStore,
|
|
private remoteStores: Array<BaseStore>,
|
|
) {
|
|
super();
|
|
|
|
// Local store events propagate to remote stores
|
|
this.localStore.on('set', async (payload) => {
|
|
// Propagate to all remote stores in parallel
|
|
await Promise.all(
|
|
this.remoteStores.map((remoteStore) =>
|
|
remoteStore.set(payload.key, payload.value),
|
|
),
|
|
);
|
|
});
|
|
|
|
// Local store events propagate to remote stores
|
|
this.localStore.on('delete', async (payload) => {
|
|
// Propagate to all remote stores in parallel
|
|
await Promise.all(
|
|
this.remoteStores.map((remoteStore) => remoteStore.delete(payload.key)),
|
|
);
|
|
});
|
|
|
|
// Local store events propagate to remote stores
|
|
this.localStore.on('clear', async () => {
|
|
// Propagate to all remote stores in parallel
|
|
await Promise.all(
|
|
this.remoteStores.map((remoteStore) => remoteStore.clear()),
|
|
);
|
|
});
|
|
|
|
// Remote store events propagate to local store
|
|
for (const remoteStore of this.remoteStores) {
|
|
remoteStore.on('set', async (payload) => {
|
|
// Skip if the local store already has the key
|
|
if (await this.localStore.has(payload.key)) return;
|
|
|
|
await this.localStore.set(payload.key, payload.value);
|
|
});
|
|
|
|
remoteStore.on('delete', async (payload) => {
|
|
if (await this.localStore.has(payload.key)) {
|
|
await this.localStore.delete(payload.key);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async all<T>() {
|
|
return await this.localStore.all<T>();
|
|
}
|
|
|
|
async keys() {
|
|
return await this.localStore.keys();
|
|
}
|
|
|
|
async get<T>(key: string) {
|
|
return await this.localStore.get<T>(key);
|
|
}
|
|
|
|
async set<T>(key: string, value: T) {
|
|
await this.localStore.set(key, value);
|
|
this.emit('set', { key, value });
|
|
}
|
|
|
|
async delete(key: string) {
|
|
await this.localStore.delete(key);
|
|
this.emit('delete', { key });
|
|
}
|
|
|
|
async clear() {
|
|
await this.localStore.clear();
|
|
this.emit('clear', undefined);
|
|
}
|
|
|
|
async has(key: string) {
|
|
return await this.localStore.has(key);
|
|
}
|
|
|
|
async close() {
|
|
await Promise.all([
|
|
this.localStore.close(),
|
|
...this.remoteStores.map((store) => store.close()),
|
|
]);
|
|
}
|
|
|
|
async sync(): Promise<void> {
|
|
// Sync all remote stores in parallel
|
|
await Promise.all(
|
|
this.remoteStores.map(async (remoteStore) => {
|
|
// TODO: We want to negotiate using an Inverse Bloom Filter first to optimize the sync.
|
|
// Currently, this is doing a naive set comparison.
|
|
|
|
// Get all keys from the remote store
|
|
const remoteKeys = await remoteStore.keys();
|
|
const localKeys = await this.localStore.keys();
|
|
|
|
// Find keys missing in local store
|
|
const missingLocalKeys = remoteKeys.filter(
|
|
(key) => !localKeys.includes(key),
|
|
);
|
|
|
|
// Find keys missing in remote store
|
|
const missingRemoteKeys = localKeys.filter(
|
|
(key) => !remoteKeys.includes(key),
|
|
);
|
|
|
|
// Sync missing keys from remote to local (in parallel)
|
|
await Promise.all(
|
|
missingLocalKeys.map(async (key) => {
|
|
const value = await remoteStore.get(key);
|
|
await this.localStore.set(key, value);
|
|
}),
|
|
);
|
|
|
|
// Sync missing keys from local to remote (in parallel)
|
|
await Promise.all(
|
|
missingRemoteKeys.map(async (key) => {
|
|
const value = await this.localStore.get(key);
|
|
await remoteStore.set(key, value);
|
|
}),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|