Activities/Wallet Refactor

This commit is contained in:
Jim Hamill
2025-04-18 05:17:20 +00:00
parent 408d3b8d8c
commit 7e4508cf5a
18 changed files with 706 additions and 1111 deletions
+70 -82
View File
@@ -11,7 +11,10 @@ import { BaseStore } from './base-store.js';
* UUIDv7 keys are used to ensure uniqueness of keys.
*/
export class StoreComposite extends BaseStore {
static async from(localStore: BaseStore, remoteStores: Array<BaseStore>) {
static async from(
localStore: BaseStore,
remoteStores: Array<BaseStore> = [],
) {
const compositeStore = new this(localStore, remoteStores);
await compositeStore.sync();
return compositeStore;
@@ -19,55 +22,9 @@ export class StoreComposite extends BaseStore {
constructor(
private localStore: BaseStore,
private remoteStores: Array<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
// NOTE: It's important we DO NOT repropagate events that have already occurred.
// If we do, we can end up in infinite loops.
if (await this.localStore.has(payload.key)) return;
await this.localStore.set(payload.key, payload.value);
});
remoteStore.on('delete', async (payload) => {
// NOTE: It's important we DO NOT repropagate events that have already occurred.
// If we do, we can end up in infinite loops.
if (await this.localStore.has(payload.key)) {
await this.localStore.delete(payload.key);
}
});
}
}
async all<T>() {
@@ -108,43 +65,74 @@ export class StoreComposite extends BaseStore {
]);
}
/**
* Add a new remote store and set up synchronization
*/
public async addRemoteStore(remoteStore: BaseStore): Promise<void> {
this.remoteStores.push(remoteStore);
// Propagate existing local events to the new remote
this.localStore.on('set', async (payload) => {
await remoteStore.set(payload.key, payload.value);
});
this.localStore.on('delete', async (payload) => {
await remoteStore.delete(payload.key);
});
this.localStore.on('clear', async () => {
await remoteStore.clear();
});
// Set up remote to local propagation
remoteStore.on('set', async (payload) => {
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);
}
});
remoteStore.on('clear', async () => {
await this.localStore.clear();
});
// Sync with the new remote
await this.syncWithRemote(remoteStore);
}
private async syncWithRemote(remoteStore: BaseStore): Promise<void> {
const remoteKeys = await remoteStore.keys();
const localKeys = await this.localStore.keys();
const missingLocalKeys = remoteKeys.filter(
(key) => !localKeys.includes(key),
);
const missingRemoteKeys = localKeys.filter(
(key) => !remoteKeys.includes(key),
);
await Promise.all([
...missingLocalKeys.map(async (key) => {
const value = await remoteStore.get(key);
await this.localStore.set(key, value);
}),
...missingRemoteKeys.map(async (key) => {
const value = await this.localStore.get(key);
await remoteStore.set(key, value);
}),
]);
}
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);
}),
);
}),
this.remoteStores.map(async (remoteStore) =>
this.syncWithRemote(remoteStore),
),
);
}
}