XO wallet integration
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { BaseStore } from './base-store.js';
|
||||
import { StoreComposite } from './store-composite.js';
|
||||
|
||||
import { UUIDV7 } from '../utils/index.js';
|
||||
import { EventEmitter, UUIDV7 } from '../utils/index.js';
|
||||
|
||||
// Make the given property in a type optional.
|
||||
// TODO: This is an extremely useful utility. If we use it elsewhere, move to own "type-utils.ts" file.
|
||||
@@ -9,10 +9,17 @@ type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
// Base Activity interface
|
||||
export interface Activity {
|
||||
entityId: string;
|
||||
name: string;
|
||||
/** Key (Type) of this activity. */
|
||||
key: string;
|
||||
|
||||
/** Value for this activity. */
|
||||
value: unknown;
|
||||
|
||||
/** Timestamp for this activity. */
|
||||
timestamp: bigint;
|
||||
|
||||
/** Entity ID may refer to a transaction id or other specific identifier. */
|
||||
entityId?: string;
|
||||
}
|
||||
|
||||
// Define the snapshot structure
|
||||
@@ -28,6 +35,10 @@ export type PolicyFunction = (
|
||||
newActivity: Activity,
|
||||
) => Activity;
|
||||
|
||||
export type ActivitiesEvents = {
|
||||
[key: `updated:${string}`]: { activity: Activity };
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an event sourcing pattern for managing application state
|
||||
* with a simplified interface focused on the current snapshot.
|
||||
@@ -51,7 +62,14 @@ export type PolicyFunction = (
|
||||
* const savedActivity = await activities.add(walletName);
|
||||
* ```
|
||||
*/
|
||||
export class Activities {
|
||||
export class Activities extends EventEmitter<ActivitiesEvents> {
|
||||
// Dependencies.
|
||||
public readonly store: StoreComposite;
|
||||
|
||||
// State.
|
||||
// TODO: If you can refactor this around your new Storage mechanism, things become a lot simpler:
|
||||
// 1. You don't need to define such a clunky snapshot format because you have indexes.
|
||||
// 2. Caching of snapshots likely becomes possible (because you can use a storage adapter).
|
||||
public snapshot: SnapshotByType = {};
|
||||
private policies = new Map<string, PolicyFunction>();
|
||||
private storeKeyMap = new Map<string, string>();
|
||||
@@ -59,27 +77,20 @@ export class Activities {
|
||||
/**
|
||||
* Create an Activities instance from the given composite store.
|
||||
*/
|
||||
static async from(compositeStore: StoreComposite) {
|
||||
static async from(stores?: Array<BaseStore>) {
|
||||
const compositeStore = await StoreComposite.from(stores);
|
||||
const activities = new Activities(compositeStore);
|
||||
await activities.compileSnapshot();
|
||||
return activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Activities instance from the given primary and secondary stores
|
||||
*/
|
||||
static async fromStores(
|
||||
primaryStore: BaseStore,
|
||||
secondaryStores?: Array<BaseStore>,
|
||||
): Promise<Activities> {
|
||||
const compositeStore = await StoreComposite.from(
|
||||
primaryStore,
|
||||
secondaryStores,
|
||||
);
|
||||
return Activities.from(compositeStore);
|
||||
}
|
||||
private constructor(store: StoreComposite) {
|
||||
super();
|
||||
|
||||
private constructor(public store: StoreComposite) {
|
||||
// Assign our dependencies.
|
||||
this.store = store;
|
||||
|
||||
// Register callbacks.
|
||||
this.store.on(
|
||||
'set',
|
||||
async (payload: { key: string; value: Activity | null }) => {
|
||||
@@ -90,47 +101,78 @@ export class Activities {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Additional argument to "add": Something like a quarum.
|
||||
// But, instead of a quorum, it's a function that takes the resolve of all "set" promises.
|
||||
// That way, we can implement custom storage policies?
|
||||
|
||||
/**
|
||||
* Add a new activity to the store
|
||||
* @param activity The activity to add (id and timestamp are optional)
|
||||
* @returns The stored activity with all fields populated
|
||||
*/
|
||||
public async add<T extends Optional<Activity, 'timestamp'>>(
|
||||
activity: T,
|
||||
public async add<T extends Activity>(
|
||||
activity: Optional<T, 'entityId' | 'timestamp'>,
|
||||
): Promise<Activity> {
|
||||
// Generate timestamp if not provided
|
||||
const timestamp = activity.timestamp ?? BigInt(Date.now());
|
||||
|
||||
// Generate a UUID based on the timestamp
|
||||
const key = UUIDV7.fromTimestamp(timestamp).toString();
|
||||
const uuid = UUIDV7.fromTimestamp(timestamp).toString();
|
||||
|
||||
// Generate an ID if not provided
|
||||
const id = activity.entityId ?? key;
|
||||
// If this doesn't reference a specific entity id, put it at root level (empty string: "")
|
||||
const id = activity.entityId ?? '';
|
||||
|
||||
const completeActivity: Activity = {
|
||||
name: activity.name,
|
||||
key: activity.key,
|
||||
value: activity.value,
|
||||
entityId: id,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
await this.store.set(key, completeActivity);
|
||||
await this.store.set(uuid, completeActivity);
|
||||
|
||||
return completeActivity;
|
||||
}
|
||||
|
||||
public async remove<T extends Activity>(
|
||||
activity: Pick<T, 'key' | 'entityId'>,
|
||||
) {
|
||||
// Generate timestamp.
|
||||
const timestamp = BigInt(Date.now());
|
||||
|
||||
// Generate a UUID based on the timestamp
|
||||
const uuid = UUIDV7.fromTimestamp(timestamp).toString();
|
||||
|
||||
// If this doesn't reference a specific entity id, put it at root level (empty string: "")
|
||||
const id = activity.entityId ?? '';
|
||||
|
||||
const removedActivity: Activity = {
|
||||
key: activity.key,
|
||||
value: null,
|
||||
entityId: id,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
await this.store.set(uuid, removedActivity);
|
||||
|
||||
return removedActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific activity by type and id with type inference
|
||||
*/
|
||||
public get<T extends Activity>(type: T['name'], id: string): T | undefined {
|
||||
const activity = this.snapshot[type]?.[id];
|
||||
public get<T extends Activity>(
|
||||
type: T['key'],
|
||||
entityId: string = '',
|
||||
): T | undefined {
|
||||
const activity = this.snapshot[type]?.[entityId];
|
||||
return activity as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all activities of a specific type
|
||||
*/
|
||||
public getByType<T extends Activity>(type: T['name']): T[] {
|
||||
public getByType<T extends Activity>(type: T['key']): T[] {
|
||||
const activities = this.snapshot[type];
|
||||
if (!activities) {
|
||||
return [];
|
||||
@@ -157,20 +199,14 @@ export class Activities {
|
||||
if (!activity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure timestamp exists by extracting from UUID if needed
|
||||
if (!activity.timestamp) {
|
||||
activity.timestamp = UUIDV7.from(storeKey).getTimestamp();
|
||||
}
|
||||
|
||||
// Ensure ID exists
|
||||
if (!activity.entityId) {
|
||||
activity.entityId = storeKey;
|
||||
}
|
||||
|
||||
const snapshotKey = this.getSnapshotKey(activity);
|
||||
const existingActivity = this.getActivityFromSnapshot(
|
||||
activity.name,
|
||||
activity.key,
|
||||
activity.entityId,
|
||||
);
|
||||
|
||||
@@ -185,12 +221,15 @@ export class Activities {
|
||||
const oldStoreKey = this.storeKeyMap.get(snapshotKey);
|
||||
if (oldStoreKey && oldStoreKey !== storeKey) {
|
||||
// Delete the old activity from the store
|
||||
await this.store.set(oldStoreKey, null);
|
||||
// await this.store.set(oldStoreKey, null);
|
||||
}
|
||||
this.storeKeyMap.set(snapshotKey, storeKey);
|
||||
|
||||
// Emit the updated activity.
|
||||
this.emit(`updated:${activity.key}`, { activity });
|
||||
} else if (storeKey !== this.storeKeyMap.get(snapshotKey)) {
|
||||
// If we kept the existing activity, delete the new activity from the store
|
||||
await this.store.set(storeKey, null);
|
||||
// await this.store.set(storeKey, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,27 +269,27 @@ export class Activities {
|
||||
*/
|
||||
private getActivityFromSnapshot(
|
||||
type: string,
|
||||
id: string,
|
||||
entityId: string = '',
|
||||
): Activity | undefined {
|
||||
return this.snapshot[type]?.[id];
|
||||
return this.snapshot[type]?.[entityId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an activity in the snapshot, creating necessary object structure
|
||||
*/
|
||||
private setActivityInSnapshot(activity: Activity): void {
|
||||
if (!this.snapshot[activity.name]) {
|
||||
this.snapshot[activity.name] = {};
|
||||
if (!this.snapshot[activity.key]) {
|
||||
this.snapshot[activity.key] = {};
|
||||
}
|
||||
|
||||
this.snapshot[activity.name][activity.entityId] = activity;
|
||||
this.snapshot[activity.key][activity.entityId || ''] = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the snapshot key for an activity
|
||||
*/
|
||||
private getSnapshotKey(activity: Activity): string {
|
||||
return `${activity.name}:${activity.entityId}`;
|
||||
return `${activity.key}:${activity.entityId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,7 +299,7 @@ export class Activities {
|
||||
existingActivity: Activity | undefined,
|
||||
newActivity: Activity,
|
||||
): Activity {
|
||||
const policy = this.policies.get(newActivity.name);
|
||||
const policy = this.policies.get(newActivity.key);
|
||||
|
||||
if (!policy) {
|
||||
// Default policy: compare timestamps
|
||||
|
||||
Reference in New Issue
Block a user