Skip to content

Commit e3fbeae

Browse files
committed
Refactor CallbackRegistry to support typed arguments
Updated CallbackRegistry to be generic, allowing registration and execution of callbacks with typed arguments. Refactored Evolu to use separate registries for different callback types and simplified Promise.withResolvers usage. Added comprehensive tests for typed and no-argument callback scenarios.
1 parent 6606888 commit e3fbeae

3 files changed

Lines changed: 88 additions & 37 deletions

File tree

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Brand } from "./Brand.js";
22
import { NanoIdLibDep } from "./NanoId.js";
33
import { Result } from "./Result.js";
4+
import { createId, Id } from "./Type.js";
45

56
/**
67
* A registry for one-time callback functions.
@@ -15,35 +16,69 @@ import { Result } from "./Result.js";
1516
* because it's the callback's responsibility to handle its own errors. The
1617
* registry is just a correlation mechanism and should not interfere with error
1718
* handling or debugging by masking the original error location.
19+
*
20+
* ### Example
21+
*
22+
* ```ts
23+
* // No-argument callbacks
24+
* const registry = createCallbackRegistry(deps);
25+
* const id = registry.register(() => console.log("called"));
26+
* registry.execute(id);
27+
*
28+
* // With argument callbacks
29+
* const stringRegistry = createCallbackRegistry<string>(deps);
30+
* const id = stringRegistry.register((value) => {
31+
* console.log(value);
32+
* });
33+
* stringRegistry.execute(id, "hello");
34+
*
35+
* // Promise.withResolvers pattern
36+
* const promiseRegistry = createCallbackRegistry<string>(deps);
37+
* const { promise, resolve } = Promise.withResolvers<string>();
38+
* const id = promiseRegistry.register(resolve);
39+
* promiseRegistry.execute(id, "resolved value");
40+
* await promise; // "resolved value"
41+
* ```
42+
*
43+
* @template T - The type of argument passed to callbacks (defaults to undefined
44+
* for no-argument callbacks)
1845
*/
19-
export interface CallbackRegistry {
46+
export interface CallbackRegistry<T = undefined> {
2047
/** Registers a callback function and returns a unique ID. */
21-
readonly register: (callback: (arg?: unknown) => void) => CallbackId;
48+
readonly register: (callback: (arg: T) => void) => CallbackId;
2249

2350
/** Executes and removes a callback associated with the given ID. */
24-
readonly execute: (id: CallbackId, arg?: unknown) => void;
51+
readonly execute: T extends undefined
52+
? (id: CallbackId) => undefined
53+
: (id: CallbackId, arg: T) => undefined;
2554
}
2655

27-
export type CallbackId = string & Brand<"CallbackId">;
56+
export type CallbackId = Id & Brand<"Callback">;
2857

29-
export const createCallbackRegistry = (
58+
/** Creates a new {@link CallbackRegistry} for one-time callback functions. */
59+
export const createCallbackRegistry = <T = undefined>(
3060
deps: NanoIdLibDep,
31-
): CallbackRegistry => {
32-
const callbackMap = new Map<CallbackId, (arg?: unknown) => void>();
61+
): CallbackRegistry<T> => {
62+
const callbackMap = new Map<CallbackId, (arg: T) => void>();
3363

3464
return {
3565
register: (callback) => {
36-
const id = deps.nanoIdLib.nanoid() as CallbackId;
66+
const id = createId<"Callback">(deps);
3767
callbackMap.set(id, callback);
3868
return id;
3969
},
4070

41-
execute: (id, arg) => {
71+
execute: (id: CallbackId, ...args: T extends undefined ? [] : [T]) => {
4272
const callback = callbackMap.get(id);
4373
if (callback) {
4474
callbackMap.delete(id);
45-
callback(arg);
75+
if (args.length === 0) {
76+
// Called without argument (undefined case)
77+
(callback as () => void)();
78+
} else {
79+
callback(args[0]);
80+
}
4681
}
4782
},
48-
};
83+
} as CallbackRegistry<T>;
4984
};

packages/common/src/Evolu/Evolu.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,8 @@ const createEvoluInstance =
673673

674674
const subscribedQueries = createSubscribedQueries(rowsStore);
675675
const loadingPromises = createLoadingPromises(subscribedQueries);
676-
const callbackRegistry = createCallbackRegistry(deps);
676+
const onCompleteRegistry = createCallbackRegistry(deps);
677+
const exportRegistry = createCallbackRegistry<Uint8Array>(deps);
677678

678679
const appState = deps.createAppState(config);
679680
const dbWorker = deps.createDbWorker(config.name);
@@ -726,7 +727,7 @@ const createEvoluInstance =
726727
}
727728

728729
for (const id of message.onCompleteIds) {
729-
callbackRegistry.execute(id);
730+
onCompleteRegistry.execute(id);
730731
}
731732
break;
732733
}
@@ -746,13 +747,13 @@ const createEvoluInstance =
746747
if (message.reload) {
747748
appState.reset();
748749
} else {
749-
callbackRegistry.execute(message.onCompleteId);
750+
onCompleteRegistry.execute(message.onCompleteId);
750751
}
751752
break;
752753
}
753754

754755
case "onExport": {
755-
callbackRegistry.execute(message.onCompleteId, message.file);
756+
exportRegistry.execute(message.onCompleteId, message.file);
756757
break;
757758
}
758759

@@ -850,11 +851,11 @@ const createEvoluInstance =
850851
if (mutateMicrotaskQueue.length === 1)
851852
queueMicrotask(() => {
852853
const changes: Array<MutationChange> = [];
853-
const onCompletes = [];
854+
const onCompleteCallbacks = [];
854855

855856
for (const [change, onComplete] of mutateMicrotaskQueue) {
856857
if (change !== null) changes.push(change);
857-
if (onComplete) onCompletes.push(onComplete);
858+
if (onComplete) onCompleteCallbacks.push(onComplete);
858859
}
859860

860861
const queueLength = mutateMicrotaskQueue.length;
@@ -866,8 +867,8 @@ const createEvoluInstance =
866867
return;
867868
}
868869

869-
const onCompleteIds = onCompletes.map((onComplete) =>
870-
callbackRegistry.register(onComplete),
870+
const onCompleteIds = onCompleteCallbacks.map(
871+
onCompleteRegistry.register,
871872
);
872873

873874
loadingPromises.releaseUnsubscribed();
@@ -957,12 +958,8 @@ const createEvoluInstance =
957958
upsert: createMutation("upsert"),
958959

959960
resetAppOwner: (options) => {
960-
// Eslint bug, Promise<void> is correct by docs.
961-
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
962-
const { promise, resolve } = Promise.withResolvers<void>();
963-
const onCompleteId = callbackRegistry.register(() => {
964-
resolve();
965-
});
961+
const { promise, resolve } = Promise.withResolvers<undefined>();
962+
const onCompleteId = onCompleteRegistry.register(resolve);
966963
dbWorker.postMessage({
967964
type: "reset",
968965
onCompleteId,
@@ -972,13 +969,8 @@ const createEvoluInstance =
972969
},
973970

974971
restoreAppOwner: (mnemonic, options) => {
975-
// Eslint bug, Promise<void> is correct by docs.
976-
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
977-
const { promise, resolve } = Promise.withResolvers<void>();
978-
const onCompleteId = callbackRegistry.register(() => {
979-
resolve();
980-
});
981-
972+
const { promise, resolve } = Promise.withResolvers<undefined>();
973+
const onCompleteId = onCompleteRegistry.register(resolve);
982974
dbWorker.postMessage({
983975
type: "reset",
984976
onCompleteId,
@@ -1000,9 +992,7 @@ const createEvoluInstance =
1000992

1001993
exportDatabase: () => {
1002994
const { promise, resolve } = Promise.withResolvers<Uint8Array>();
1003-
const onCompleteId = callbackRegistry.register((arg) => {
1004-
if (arg instanceof Uint8Array) resolve(arg);
1005-
});
995+
const onCompleteId = exportRegistry.register(resolve);
1006996
dbWorker.postMessage({ type: "export", onCompleteId });
1007997
return promise;
1008998
},

packages/common/test/CallbackRegistry.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,45 @@ import { expect, test } from "vitest";
22
import { createCallbackRegistry } from "../src/CallbackRegistry.js";
33
import { testNanoIdLibDep } from "./_deps.js";
44

5-
test("CallbackRegistry", () => {
5+
test("CallbackRegistry with no argument", () => {
66
const registry = createCallbackRegistry(testNanoIdLibDep);
77

88
let called = false;
99
const id = registry.register(() => {
1010
called = true;
1111
});
1212

13-
expect(id).toBeDefined();
1413
registry.execute(id);
1514
expect(called).toBe(true);
1615

1716
called = false;
1817
registry.execute(id);
1918
expect(called).toBe(false);
2019
});
20+
21+
test("CallbackRegistry with string type", () => {
22+
const registry = createCallbackRegistry<string>(testNanoIdLibDep);
23+
24+
let receivedValue: string | null = null;
25+
const id = registry.register((value) => {
26+
receivedValue = value;
27+
});
28+
29+
registry.execute(id, "test value");
30+
expect(receivedValue).toBe("test value");
31+
32+
receivedValue = null;
33+
registry.execute(id, "should not execute");
34+
expect(receivedValue).toBe(null);
35+
});
36+
37+
test("CallbackRegistry with Promise.withResolvers pattern", () => {
38+
const registry = createCallbackRegistry<string>(testNanoIdLibDep);
39+
40+
const { promise, resolve } = Promise.withResolvers<string>();
41+
const id = registry.register(resolve);
42+
43+
registry.execute(id, "resolved value");
44+
45+
return expect(promise).resolves.toBe("resolved value");
46+
});

0 commit comments

Comments
 (0)