rwsdk 1.0.0-beta.30-test.20251119220440 → 1.0.0-beta.30-test.20251120213828
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/e2e/testHarness.mjs +6 -1
- package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
- package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
- package/dist/use-synced-state/SyncedStateServer.d.mts +21 -0
- package/dist/use-synced-state/{SyncStateServer.mjs → SyncedStateServer.mjs} +38 -34
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +19 -19
- package/dist/use-synced-state/__tests__/useSyncState.test.js +9 -9
- package/dist/use-synced-state/__tests__/useSyncedState.test.js +9 -9
- package/dist/use-synced-state/__tests__/worker.test.mjs +11 -11
- package/dist/use-synced-state/client-core.d.ts +26 -0
- package/dist/use-synced-state/client-core.js +39 -0
- package/dist/use-synced-state/client.d.ts +3 -28
- package/dist/use-synced-state/client.js +4 -39
- package/dist/use-synced-state/constants.d.mts +1 -1
- package/dist/use-synced-state/constants.mjs +1 -1
- package/dist/use-synced-state/useSyncedState.d.ts +3 -3
- package/dist/use-synced-state/useSyncedState.js +7 -7
- package/dist/use-synced-state/worker.d.mts +6 -7
- package/dist/use-synced-state/worker.mjs +25 -29
- package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
- package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
- package/dist/vite/createDirectiveLookupPlugin.mjs +6 -7
- package/dist/vite/directivesPlugin.mjs +0 -4
- package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
- package/dist/vite/knownDepsResolverPlugin.mjs +6 -16
- package/dist/vite/redwoodPlugin.mjs +2 -0
- package/dist/vite/runDirectivesScan.mjs +13 -4
- package/dist/vite/ssrBridgePlugin.mjs +10 -7
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -4
- package/dist/vite/virtualPlugin.mjs +6 -7
- package/package.json +5 -4
- package/dist/use-synced-state/SyncStateServer.d.mts +0 -20
- package/dist/use-synced-state/useSyncState.d.ts +0 -20
- package/dist/use-synced-state/useSyncState.js +0 -58
|
@@ -413,7 +413,12 @@ function createSDKTestRunner() {
|
|
|
413
413
|
console.warn("Failed to connect to existing browser instance. " +
|
|
414
414
|
"This might happen if you are running a single test file. " +
|
|
415
415
|
"Launching a new browser instance instead.");
|
|
416
|
-
|
|
416
|
+
// Check for RWSDK_HEADLESS environment variable (default to true if not set)
|
|
417
|
+
// Set RWSDK_HEADLESS=0 or RWSDK_HEADLESS=false to run in headed mode
|
|
418
|
+
const headless = process.env.RWSDK_HEADLESS === undefined ||
|
|
419
|
+
process.env.RWSDK_HEADLESS === "1" ||
|
|
420
|
+
process.env.RWSDK_HEADLESS === "true";
|
|
421
|
+
browser = await launchBrowser(undefined, headless);
|
|
417
422
|
}
|
|
418
423
|
}, SETUP_WAIT_TIMEOUT);
|
|
419
424
|
afterAll(async () => {
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RpcStub } from "capnweb";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
export type SyncedStateValue = unknown;
|
|
4
|
+
type OnSetHandler = (key: string, value: SyncedStateValue) => void;
|
|
5
|
+
type OnGetHandler = (key: string, value: SyncedStateValue | undefined) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Durable Object that keeps shared state for multiple clients and notifies subscribers.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SyncedStateServer extends DurableObject {
|
|
10
|
+
#private;
|
|
11
|
+
static registerKeyHandler(handler: (key: string) => Promise<string>): void;
|
|
12
|
+
static getKeyHandler(): ((key: string) => Promise<string>) | null;
|
|
13
|
+
static registerSetStateHandler(handler: OnSetHandler | null): void;
|
|
14
|
+
static registerGetStateHandler(handler: OnGetHandler | null): void;
|
|
15
|
+
getState(key: string): SyncedStateValue;
|
|
16
|
+
setState(value: SyncedStateValue, key: string): void;
|
|
17
|
+
subscribe(key: string, client: RpcStub<(value: SyncedStateValue) => void>): void;
|
|
18
|
+
unsubscribe(key: string, client: RpcStub<(value: SyncedStateValue) => void>): void;
|
|
19
|
+
fetch(request: Request): Promise<Response>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -9,51 +9,51 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var _a,
|
|
13
|
-
import { RpcTarget } from "capnweb";
|
|
12
|
+
var _a, _SyncedStateServer_keyHandler, _SyncedStateServer_setStateHandler, _SyncedStateServer_getStateHandler, _SyncedStateServer_stateStore, _SyncedStateServer_subscriptions, _SyncedStateServer_subscriptionRefs, _CoordinatorApi_coordinator;
|
|
13
|
+
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
|
|
14
14
|
import { DurableObject } from "cloudflare:workers";
|
|
15
15
|
/**
|
|
16
16
|
* Durable Object that keeps shared state for multiple clients and notifies subscribers.
|
|
17
17
|
*/
|
|
18
|
-
export class
|
|
18
|
+
export class SyncedStateServer extends DurableObject {
|
|
19
19
|
constructor() {
|
|
20
20
|
super(...arguments);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
_SyncedStateServer_stateStore.set(this, new Map());
|
|
22
|
+
_SyncedStateServer_subscriptions.set(this, new Map());
|
|
23
|
+
_SyncedStateServer_subscriptionRefs.set(this, new Map());
|
|
24
24
|
}
|
|
25
25
|
static registerKeyHandler(handler) {
|
|
26
|
-
__classPrivateFieldSet(_a, _a, handler, "f",
|
|
26
|
+
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_keyHandler);
|
|
27
27
|
}
|
|
28
28
|
static getKeyHandler() {
|
|
29
|
-
return __classPrivateFieldGet(_a, _a, "f",
|
|
29
|
+
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_keyHandler);
|
|
30
30
|
}
|
|
31
31
|
static registerSetStateHandler(handler) {
|
|
32
|
-
__classPrivateFieldSet(_a, _a, handler, "f",
|
|
32
|
+
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_setStateHandler);
|
|
33
33
|
}
|
|
34
34
|
static registerGetStateHandler(handler) {
|
|
35
|
-
__classPrivateFieldSet(_a, _a, handler, "f",
|
|
35
|
+
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_getStateHandler);
|
|
36
36
|
}
|
|
37
37
|
getState(key) {
|
|
38
|
-
const value = __classPrivateFieldGet(this,
|
|
39
|
-
if (__classPrivateFieldGet(_a, _a, "f",
|
|
40
|
-
__classPrivateFieldGet(_a, _a, "f",
|
|
38
|
+
const value = __classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").get(key);
|
|
39
|
+
if (__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_getStateHandler)) {
|
|
40
|
+
__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_getStateHandler).call(_a, key, value);
|
|
41
41
|
}
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
44
|
setState(value, key) {
|
|
45
|
-
__classPrivateFieldGet(this,
|
|
46
|
-
if (__classPrivateFieldGet(_a, _a, "f",
|
|
47
|
-
__classPrivateFieldGet(_a, _a, "f",
|
|
45
|
+
__classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").set(key, value);
|
|
46
|
+
if (__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_setStateHandler)) {
|
|
47
|
+
__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_setStateHandler).call(_a, key, value);
|
|
48
48
|
}
|
|
49
|
-
const subscribers = __classPrivateFieldGet(this,
|
|
49
|
+
const subscribers = __classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(key);
|
|
50
50
|
if (!subscribers) {
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
for (const subscriber of subscribers) {
|
|
54
54
|
subscriber(value).catch(() => {
|
|
55
55
|
subscribers.delete(subscriber);
|
|
56
|
-
const refs = __classPrivateFieldGet(this,
|
|
56
|
+
const refs = __classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").get(key);
|
|
57
57
|
if (refs) {
|
|
58
58
|
for (const [original, duplicate] of refs) {
|
|
59
59
|
if (duplicate === subscriber) {
|
|
@@ -62,46 +62,50 @@ export class SyncStateServer extends DurableObject {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
if (refs.size === 0) {
|
|
65
|
-
__classPrivateFieldGet(this,
|
|
65
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").delete(key);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
if (subscribers.size === 0) {
|
|
71
|
-
__classPrivateFieldGet(this,
|
|
71
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").delete(key);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
subscribe(key, client) {
|
|
75
|
-
if (!__classPrivateFieldGet(this,
|
|
76
|
-
__classPrivateFieldGet(this,
|
|
75
|
+
if (!__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").has(key)) {
|
|
76
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").set(key, new Set());
|
|
77
77
|
}
|
|
78
|
-
if (!__classPrivateFieldGet(this,
|
|
79
|
-
__classPrivateFieldGet(this,
|
|
78
|
+
if (!__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").has(key)) {
|
|
79
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").set(key, new Map());
|
|
80
80
|
}
|
|
81
81
|
const duplicate = client.dup();
|
|
82
|
-
__classPrivateFieldGet(this,
|
|
83
|
-
__classPrivateFieldGet(this,
|
|
82
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(key).add(duplicate);
|
|
83
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").get(key).set(client, duplicate);
|
|
84
84
|
}
|
|
85
85
|
unsubscribe(key, client) {
|
|
86
|
-
const duplicates = __classPrivateFieldGet(this,
|
|
86
|
+
const duplicates = __classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").get(key);
|
|
87
87
|
const duplicate = duplicates?.get(client);
|
|
88
|
-
const subscribers = __classPrivateFieldGet(this,
|
|
88
|
+
const subscribers = __classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(key);
|
|
89
89
|
if (duplicate && subscribers) {
|
|
90
90
|
subscribers.delete(duplicate);
|
|
91
91
|
duplicates.delete(client);
|
|
92
92
|
if (subscribers.size === 0) {
|
|
93
|
-
__classPrivateFieldGet(this,
|
|
93
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").delete(key);
|
|
94
94
|
}
|
|
95
95
|
if (duplicates.size === 0) {
|
|
96
|
-
__classPrivateFieldGet(this,
|
|
96
|
+
__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").delete(key);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
async fetch(request) {
|
|
101
|
+
const api = new CoordinatorApi(this);
|
|
102
|
+
return newWorkersRpcResponse(request, api);
|
|
103
|
+
}
|
|
100
104
|
}
|
|
101
|
-
_a =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
_a = SyncedStateServer, _SyncedStateServer_stateStore = new WeakMap(), _SyncedStateServer_subscriptions = new WeakMap(), _SyncedStateServer_subscriptionRefs = new WeakMap();
|
|
106
|
+
_SyncedStateServer_keyHandler = { value: null };
|
|
107
|
+
_SyncedStateServer_setStateHandler = { value: null };
|
|
108
|
+
_SyncedStateServer_getStateHandler = { value: null };
|
|
105
109
|
class CoordinatorApi extends RpcTarget {
|
|
106
110
|
constructor(coordinator) {
|
|
107
111
|
super();
|
|
@@ -4,7 +4,7 @@ vi.mock("cloudflare:workers", () => {
|
|
|
4
4
|
}
|
|
5
5
|
return { DurableObject };
|
|
6
6
|
});
|
|
7
|
-
import {
|
|
7
|
+
import { SyncedStateServer } from "../SyncedStateServer.mjs";
|
|
8
8
|
const createStub = (onInvoke) => {
|
|
9
9
|
const fn = Object.assign(async (value) => {
|
|
10
10
|
await onInvoke(value);
|
|
@@ -13,9 +13,9 @@ const createStub = (onInvoke) => {
|
|
|
13
13
|
});
|
|
14
14
|
return fn;
|
|
15
15
|
};
|
|
16
|
-
describe("
|
|
16
|
+
describe("SyncedStateServer", () => {
|
|
17
17
|
it("notifies subscribers when state changes", async () => {
|
|
18
|
-
const coordinator = new
|
|
18
|
+
const coordinator = new SyncedStateServer({}, {});
|
|
19
19
|
const received = [];
|
|
20
20
|
const stub = createStub((value) => {
|
|
21
21
|
received.push(value);
|
|
@@ -26,7 +26,7 @@ describe("SyncStateServer", () => {
|
|
|
26
26
|
expect(received).toEqual([5]);
|
|
27
27
|
});
|
|
28
28
|
it("removes subscriptions on unsubscribe", () => {
|
|
29
|
-
const coordinator = new
|
|
29
|
+
const coordinator = new SyncedStateServer({}, {});
|
|
30
30
|
const stub = createStub(() => { });
|
|
31
31
|
coordinator.subscribe("counter", stub);
|
|
32
32
|
coordinator.unsubscribe("counter", stub);
|
|
@@ -34,7 +34,7 @@ describe("SyncStateServer", () => {
|
|
|
34
34
|
expect(coordinator.getState("counter")).toBe(1);
|
|
35
35
|
});
|
|
36
36
|
it("drops failing subscribers", async () => {
|
|
37
|
-
const coordinator = new
|
|
37
|
+
const coordinator = new SyncedStateServer({}, {});
|
|
38
38
|
const stub = createStub(async () => {
|
|
39
39
|
throw new Error("fail");
|
|
40
40
|
});
|
|
@@ -45,45 +45,45 @@ describe("SyncStateServer", () => {
|
|
|
45
45
|
expect(coordinator.getState("counter")).toBe(4);
|
|
46
46
|
});
|
|
47
47
|
it("invokes registered onSet handler", () => {
|
|
48
|
-
const coordinator = new
|
|
48
|
+
const coordinator = new SyncedStateServer({}, {});
|
|
49
49
|
const calls = [];
|
|
50
|
-
|
|
50
|
+
SyncedStateServer.registerSetStateHandler((key, value) => {
|
|
51
51
|
calls.push({ key, value });
|
|
52
52
|
});
|
|
53
53
|
coordinator.setState(2, "counter");
|
|
54
54
|
expect(calls).toEqual([{ key: "counter", value: 2 }]);
|
|
55
|
-
|
|
55
|
+
SyncedStateServer.registerSetStateHandler(null);
|
|
56
56
|
});
|
|
57
57
|
it("invokes registered onGet handler", () => {
|
|
58
|
-
const coordinator = new
|
|
58
|
+
const coordinator = new SyncedStateServer({}, {});
|
|
59
59
|
const calls = [];
|
|
60
|
-
|
|
60
|
+
SyncedStateServer.registerGetStateHandler((key, value) => {
|
|
61
61
|
calls.push({ key, value });
|
|
62
62
|
});
|
|
63
63
|
coordinator.setState(4, "counter");
|
|
64
64
|
expect(coordinator.getState("counter")).toBe(4);
|
|
65
65
|
expect(calls).toEqual([{ key: "counter", value: 4 }]);
|
|
66
|
-
|
|
66
|
+
SyncedStateServer.registerGetStateHandler(null);
|
|
67
67
|
});
|
|
68
68
|
describe("registerKeyHandler", () => {
|
|
69
69
|
afterEach(() => {
|
|
70
|
-
|
|
70
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
71
71
|
});
|
|
72
72
|
it("stores and retrieves the registered handler", async () => {
|
|
73
73
|
const handler = async (key) => `transformed:${key}`;
|
|
74
|
-
|
|
75
|
-
const retrievedHandler =
|
|
74
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
75
|
+
const retrievedHandler = SyncedStateServer.getKeyHandler();
|
|
76
76
|
expect(retrievedHandler).toBe(handler);
|
|
77
77
|
});
|
|
78
78
|
it("transforms keys using the registered handler", async () => {
|
|
79
79
|
const handler = async (key) => `user:123:${key}`;
|
|
80
|
-
|
|
80
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
81
81
|
const result = await handler("counter");
|
|
82
82
|
expect(result).toBe("user:123:counter");
|
|
83
83
|
});
|
|
84
84
|
it("returns null when no handler is registered", () => {
|
|
85
|
-
|
|
86
|
-
const handler =
|
|
85
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
86
|
+
const handler = SyncedStateServer.getKeyHandler();
|
|
87
87
|
expect(handler).not.toBeNull();
|
|
88
88
|
});
|
|
89
89
|
it("allows handler to be async", async () => {
|
|
@@ -91,7 +91,7 @@ describe("SyncStateServer", () => {
|
|
|
91
91
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
92
92
|
return `async:${key}`;
|
|
93
93
|
};
|
|
94
|
-
|
|
94
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
95
95
|
const result = await handler("test");
|
|
96
96
|
expect(result).toBe("async:test");
|
|
97
97
|
});
|
|
@@ -101,7 +101,7 @@ describe("SyncStateServer", () => {
|
|
|
101
101
|
receivedKey = key;
|
|
102
102
|
return key;
|
|
103
103
|
};
|
|
104
|
-
|
|
104
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
105
105
|
await handler("myKey");
|
|
106
106
|
expect(receivedKey).toBe("myKey");
|
|
107
107
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { setSyncedStateClientForTesting, } from "../client";
|
|
3
|
+
import { createSyncedStateHook, } from "../useSyncedState";
|
|
4
4
|
const createStateHarness = () => {
|
|
5
5
|
let currentState;
|
|
6
6
|
const cleanups = [];
|
|
@@ -39,7 +39,7 @@ const createStateHarness = () => {
|
|
|
39
39
|
runCleanups: () => cleanups.forEach((fn) => fn()),
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
-
describe("
|
|
42
|
+
describe("createSyncedStateHook", () => {
|
|
43
43
|
const subscribeHandlers = new Map();
|
|
44
44
|
const client = {
|
|
45
45
|
async getState() {
|
|
@@ -65,15 +65,15 @@ describe("createSyncStateHook", () => {
|
|
|
65
65
|
};
|
|
66
66
|
beforeEach(() => {
|
|
67
67
|
resetClient();
|
|
68
|
-
|
|
68
|
+
setSyncedStateClientForTesting(client);
|
|
69
69
|
subscribeHandlers.clear();
|
|
70
70
|
});
|
|
71
71
|
afterEach(() => {
|
|
72
|
-
|
|
72
|
+
setSyncedStateClientForTesting(null);
|
|
73
73
|
});
|
|
74
74
|
it("loads remote state and updates local value", async () => {
|
|
75
75
|
const harness = createStateHarness();
|
|
76
|
-
const useSyncedState =
|
|
76
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
77
77
|
const [value] = useSyncedState(0, "counter");
|
|
78
78
|
expect(value).toBe(0);
|
|
79
79
|
await Promise.resolve();
|
|
@@ -85,7 +85,7 @@ describe("createSyncStateHook", () => {
|
|
|
85
85
|
client.setState = async (value, key) => {
|
|
86
86
|
setCalls.push({ key, value });
|
|
87
87
|
};
|
|
88
|
-
const useSyncedState =
|
|
88
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
89
89
|
const [, setSyncValue] = useSyncedState(0, "counter");
|
|
90
90
|
setSyncValue(9);
|
|
91
91
|
expect(harness.getState()).toBe(9);
|
|
@@ -93,7 +93,7 @@ describe("createSyncStateHook", () => {
|
|
|
93
93
|
});
|
|
94
94
|
it("applies remote updates from the subscription handler", async () => {
|
|
95
95
|
const harness = createStateHarness();
|
|
96
|
-
const useSyncedState =
|
|
96
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
97
97
|
useSyncedState(0, "counter");
|
|
98
98
|
await Promise.resolve();
|
|
99
99
|
const handler = subscribeHandlers.get("counter");
|
|
@@ -107,7 +107,7 @@ describe("createSyncStateHook", () => {
|
|
|
107
107
|
unsubscribed.push({ key });
|
|
108
108
|
subscribeHandlers.delete(key);
|
|
109
109
|
};
|
|
110
|
-
const useSyncedState =
|
|
110
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
111
111
|
useSyncedState(0, "counter");
|
|
112
112
|
harness.runCleanups();
|
|
113
113
|
expect(unsubscribed).toEqual([{ key: "counter" }]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { setSyncedStateClientForTesting, } from "../client";
|
|
3
|
+
import { createSyncedStateHook, } from "../useSyncedState";
|
|
4
4
|
const createStateHarness = () => {
|
|
5
5
|
let currentState;
|
|
6
6
|
const cleanups = [];
|
|
@@ -39,7 +39,7 @@ const createStateHarness = () => {
|
|
|
39
39
|
runCleanups: () => cleanups.forEach((fn) => fn()),
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
-
describe("
|
|
42
|
+
describe("createSyncedStateHook", () => {
|
|
43
43
|
const subscribeHandlers = new Map();
|
|
44
44
|
const client = {
|
|
45
45
|
async getState() {
|
|
@@ -65,15 +65,15 @@ describe("createSyncStateHook", () => {
|
|
|
65
65
|
};
|
|
66
66
|
beforeEach(() => {
|
|
67
67
|
resetClient();
|
|
68
|
-
|
|
68
|
+
setSyncedStateClientForTesting(client);
|
|
69
69
|
subscribeHandlers.clear();
|
|
70
70
|
});
|
|
71
71
|
afterEach(() => {
|
|
72
|
-
|
|
72
|
+
setSyncedStateClientForTesting(null);
|
|
73
73
|
});
|
|
74
74
|
it("loads remote state and updates local value", async () => {
|
|
75
75
|
const harness = createStateHarness();
|
|
76
|
-
const useSyncedState =
|
|
76
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
77
77
|
const [value] = useSyncedState(0, "counter");
|
|
78
78
|
expect(value).toBe(0);
|
|
79
79
|
await Promise.resolve();
|
|
@@ -85,7 +85,7 @@ describe("createSyncStateHook", () => {
|
|
|
85
85
|
client.setState = async (value, key) => {
|
|
86
86
|
setCalls.push({ key, value });
|
|
87
87
|
};
|
|
88
|
-
const useSyncedState =
|
|
88
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
89
89
|
const [, setSyncValue] = useSyncedState(0, "counter");
|
|
90
90
|
setSyncValue(9);
|
|
91
91
|
expect(harness.getState()).toBe(9);
|
|
@@ -93,7 +93,7 @@ describe("createSyncStateHook", () => {
|
|
|
93
93
|
});
|
|
94
94
|
it("applies remote updates from the subscription handler", async () => {
|
|
95
95
|
const harness = createStateHarness();
|
|
96
|
-
const useSyncedState =
|
|
96
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
97
97
|
useSyncedState(0, "counter");
|
|
98
98
|
await Promise.resolve();
|
|
99
99
|
const handler = subscribeHandlers.get("counter");
|
|
@@ -107,7 +107,7 @@ describe("createSyncStateHook", () => {
|
|
|
107
107
|
unsubscribed.push({ key });
|
|
108
108
|
subscribeHandlers.delete(key);
|
|
109
109
|
};
|
|
110
|
-
const useSyncedState =
|
|
110
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
111
111
|
useSyncedState(0, "counter");
|
|
112
112
|
harness.runCleanups();
|
|
113
113
|
expect(unsubscribed).toEqual([{ key: "counter" }]);
|
|
@@ -12,18 +12,18 @@ vi.mock("capnweb", () => ({
|
|
|
12
12
|
vi.mock("../runtime/entries/router", () => ({
|
|
13
13
|
route: vi.fn((path, handler) => ({ path, handler })),
|
|
14
14
|
}));
|
|
15
|
-
import {
|
|
16
|
-
describe("
|
|
15
|
+
import { SyncedStateServer } from "../SyncedStateServer.mjs";
|
|
16
|
+
describe("SyncedStateProxy", () => {
|
|
17
17
|
let mockCoordinator;
|
|
18
18
|
beforeEach(() => {
|
|
19
|
-
mockCoordinator = new
|
|
19
|
+
mockCoordinator = new SyncedStateServer({}, {});
|
|
20
20
|
});
|
|
21
21
|
afterEach(() => {
|
|
22
|
-
|
|
22
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
23
23
|
});
|
|
24
24
|
it("transforms keys before calling coordinator methods when handler is registered", async () => {
|
|
25
25
|
const handler = async (key) => `transformed:${key}`;
|
|
26
|
-
|
|
26
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
27
27
|
const transformedKey = await handler("counter");
|
|
28
28
|
expect(transformedKey).toBe("transformed:counter");
|
|
29
29
|
mockCoordinator.setState(5, transformedKey);
|
|
@@ -31,13 +31,13 @@ describe("SyncStateProxy", () => {
|
|
|
31
31
|
expect(value).toBe(5);
|
|
32
32
|
});
|
|
33
33
|
it("does not transform keys when no handler is registered", () => {
|
|
34
|
-
|
|
35
|
-
const handler =
|
|
34
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
35
|
+
const handler = SyncedStateServer.getKeyHandler();
|
|
36
36
|
expect(handler).not.toBeNull();
|
|
37
37
|
});
|
|
38
38
|
it("passes through original key when handler returns it unchanged", async () => {
|
|
39
39
|
const handler = async (key) => key;
|
|
40
|
-
|
|
40
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
41
41
|
const result = await handler("counter");
|
|
42
42
|
expect(result).toBe("counter");
|
|
43
43
|
});
|
|
@@ -46,7 +46,7 @@ describe("SyncStateProxy", () => {
|
|
|
46
46
|
const userId = "user123";
|
|
47
47
|
return `user:${userId}:${key}`;
|
|
48
48
|
};
|
|
49
|
-
|
|
49
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
50
50
|
const result = await handler("settings");
|
|
51
51
|
expect(result).toBe("user:user123:settings");
|
|
52
52
|
});
|
|
@@ -54,7 +54,7 @@ describe("SyncStateProxy", () => {
|
|
|
54
54
|
const handler = async (_key) => {
|
|
55
55
|
throw new Error("Handler error");
|
|
56
56
|
};
|
|
57
|
-
|
|
57
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
58
58
|
await expect(handler("test")).rejects.toThrow("Handler error");
|
|
59
59
|
});
|
|
60
60
|
it("handles async operations in handler", async () => {
|
|
@@ -62,7 +62,7 @@ describe("SyncStateProxy", () => {
|
|
|
62
62
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
63
63
|
return `async:${key}`;
|
|
64
64
|
};
|
|
65
|
-
|
|
65
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
66
66
|
const result = await handler("data");
|
|
67
67
|
expect(result).toBe("async:data");
|
|
68
68
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SyncedStateClient = {
|
|
2
|
+
getState(key: string): Promise<unknown>;
|
|
3
|
+
setState(value: unknown, key: string): Promise<void>;
|
|
4
|
+
subscribe(key: string, handler: (value: unknown) => void): Promise<void>;
|
|
5
|
+
unsubscribe(key: string, handler: (value: unknown) => void): Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
9
|
+
* @param endpoint Endpoint to connect to.
|
|
10
|
+
* @returns RPC client instance.
|
|
11
|
+
*/
|
|
12
|
+
export declare const getSyncedStateClient: (endpoint?: string) => SyncedStateClient;
|
|
13
|
+
/**
|
|
14
|
+
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
15
|
+
* @param options Optional endpoint override.
|
|
16
|
+
* @returns Cached client instance or `null` when running without `window`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const initSyncedStateClient: (options?: {
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
}) => SyncedStateClient | null;
|
|
21
|
+
/**
|
|
22
|
+
* Injects a client instance for tests and updates the cached endpoint.
|
|
23
|
+
* @param client Stub client instance or `null` to clear the cache.
|
|
24
|
+
* @param endpoint Endpoint associated with the injected client.
|
|
25
|
+
*/
|
|
26
|
+
export declare const setSyncedStateClientForTesting: (client: SyncedStateClient | null, endpoint?: string) => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
2
|
+
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
3
|
+
let cachedClient = null;
|
|
4
|
+
let cachedEndpoint = DEFAULT_SYNCED_STATE_PATH;
|
|
5
|
+
/**
|
|
6
|
+
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
7
|
+
* @param endpoint Endpoint to connect to.
|
|
8
|
+
* @returns RPC client instance.
|
|
9
|
+
*/
|
|
10
|
+
export const getSyncedStateClient = (endpoint = cachedEndpoint) => {
|
|
11
|
+
if (cachedClient && endpoint === cachedEndpoint) {
|
|
12
|
+
return cachedClient;
|
|
13
|
+
}
|
|
14
|
+
cachedEndpoint = endpoint;
|
|
15
|
+
cachedClient = newWebSocketRpcSession(cachedEndpoint);
|
|
16
|
+
return cachedClient;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
20
|
+
* @param options Optional endpoint override.
|
|
21
|
+
* @returns Cached client instance or `null` when running without `window`.
|
|
22
|
+
*/
|
|
23
|
+
export const initSyncedStateClient = (options = {}) => {
|
|
24
|
+
cachedEndpoint = options.endpoint ?? DEFAULT_SYNCED_STATE_PATH;
|
|
25
|
+
if (typeof window === "undefined") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
cachedClient = newWebSocketRpcSession(cachedEndpoint);
|
|
29
|
+
return cachedClient;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Injects a client instance for tests and updates the cached endpoint.
|
|
33
|
+
* @param client Stub client instance or `null` to clear the cache.
|
|
34
|
+
* @param endpoint Endpoint associated with the injected client.
|
|
35
|
+
*/
|
|
36
|
+
export const setSyncedStateClientForTesting = (client, endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
37
|
+
cachedClient = client;
|
|
38
|
+
cachedEndpoint = endpoint;
|
|
39
|
+
};
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
subscribe(key: string, handler: (value: unknown) => void): Promise<void>;
|
|
5
|
-
unsubscribe(key: string, handler: (value: unknown) => void): Promise<void>;
|
|
6
|
-
};
|
|
7
|
-
type InitOptions = {
|
|
8
|
-
endpoint?: string;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
12
|
-
* @param options Optional endpoint override.
|
|
13
|
-
* @returns Cached client instance or `null` when running without `window`.
|
|
14
|
-
*/
|
|
15
|
-
export declare const initSyncStateClient: (options?: InitOptions) => SyncStateClient | null;
|
|
16
|
-
/**
|
|
17
|
-
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
18
|
-
* @param endpoint Endpoint to connect to.
|
|
19
|
-
* @returns RPC client instance.
|
|
20
|
-
*/
|
|
21
|
-
export declare const getSyncStateClient: (endpoint?: string) => SyncStateClient;
|
|
22
|
-
/**
|
|
23
|
-
* Injects a client instance for tests and updates the cached endpoint.
|
|
24
|
-
* @param client Stub client instance or `null` to clear the cache.
|
|
25
|
-
* @param endpoint Endpoint associated with the injected client.
|
|
26
|
-
*/
|
|
27
|
-
export declare const setSyncStateClientForTesting: (client: SyncStateClient | null, endpoint?: string) => void;
|
|
28
|
-
export {};
|
|
1
|
+
export { getSyncedStateClient, initSyncedStateClient, setSyncedStateClientForTesting, } from "./client-core.js";
|
|
2
|
+
export type { SyncedStateClient } from "./client-core.js";
|
|
3
|
+
export { useSyncedState } from "./useSyncedState.js";
|
|
@@ -1,39 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
7
|
-
* @param options Optional endpoint override.
|
|
8
|
-
* @returns Cached client instance or `null` when running without `window`.
|
|
9
|
-
*/
|
|
10
|
-
export const initSyncStateClient = (options = {}) => {
|
|
11
|
-
cachedEndpoint = options.endpoint ?? DEFAULT_SYNC_STATE_PATH;
|
|
12
|
-
if (typeof window === "undefined") {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
cachedClient = newWebSocketRpcSession(cachedEndpoint);
|
|
16
|
-
return cachedClient;
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
20
|
-
* @param endpoint Endpoint to connect to.
|
|
21
|
-
* @returns RPC client instance.
|
|
22
|
-
*/
|
|
23
|
-
export const getSyncStateClient = (endpoint = cachedEndpoint) => {
|
|
24
|
-
if (cachedClient && endpoint === cachedEndpoint) {
|
|
25
|
-
return cachedClient;
|
|
26
|
-
}
|
|
27
|
-
cachedEndpoint = endpoint;
|
|
28
|
-
cachedClient = newWebSocketRpcSession(cachedEndpoint);
|
|
29
|
-
return cachedClient;
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Injects a client instance for tests and updates the cached endpoint.
|
|
33
|
-
* @param client Stub client instance or `null` to clear the cache.
|
|
34
|
-
* @param endpoint Endpoint associated with the injected client.
|
|
35
|
-
*/
|
|
36
|
-
export const setSyncStateClientForTesting = (client, endpoint = DEFAULT_SYNC_STATE_PATH) => {
|
|
37
|
-
cachedClient = client;
|
|
38
|
-
cachedEndpoint = endpoint;
|
|
39
|
-
};
|
|
1
|
+
// Re-export everything from client-core to maintain the public API
|
|
2
|
+
export { getSyncedStateClient, initSyncedStateClient, setSyncedStateClientForTesting, } from "./client-core.js";
|
|
3
|
+
// Re-export useSyncedState (no circular dependency since useSyncedState imports from client-core, not client)
|
|
4
|
+
export { useSyncedState } from "./useSyncedState.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const DEFAULT_SYNCED_STATE_PATH = "/__synced-state";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const DEFAULT_SYNCED_STATE_PATH = "/__synced-state";
|