rwsdk 1.0.0-beta.3 → 1.0.0-beta.30
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/constants.d.mts +1 -0
- package/dist/lib/constants.mjs +7 -4
- package/dist/lib/e2e/browser.mjs +6 -2
- package/dist/lib/e2e/constants.d.mts +16 -0
- package/dist/lib/e2e/constants.mjs +77 -0
- package/dist/lib/e2e/dev.mjs +37 -49
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +202 -65
- package/dist/lib/e2e/index.d.mts +1 -0
- package/dist/lib/e2e/index.mjs +1 -0
- package/dist/lib/e2e/poll.d.mts +1 -1
- package/dist/lib/e2e/release.d.mts +1 -0
- package/dist/lib/e2e/release.mjs +16 -32
- package/dist/lib/e2e/tarball.mjs +2 -34
- package/dist/lib/e2e/testHarness.d.mts +36 -4
- package/dist/lib/e2e/testHarness.mjs +216 -128
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/runtime/client/client.d.ts +35 -0
- package/dist/runtime/client/client.js +35 -0
- package/dist/runtime/client/navigation.d.ts +49 -0
- package/dist/runtime/client/navigation.js +80 -31
- package/dist/runtime/entries/clientSSR.d.ts +1 -0
- package/dist/runtime/entries/clientSSR.js +3 -0
- package/dist/runtime/entries/router.d.ts +1 -0
- package/dist/runtime/entries/routerClient.d.ts +1 -0
- package/dist/runtime/entries/routerClient.js +1 -0
- package/dist/runtime/entries/worker.d.ts +2 -0
- package/dist/runtime/entries/worker.js +2 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
- package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
- package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
- package/dist/runtime/lib/db/createDb.d.ts +1 -2
- package/dist/runtime/lib/db/createDb.js +4 -0
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/links.d.ts +18 -7
- package/dist/runtime/lib/links.js +70 -24
- package/dist/runtime/lib/links.test.js +20 -0
- package/dist/runtime/lib/manifest.d.ts +1 -1
- package/dist/runtime/lib/manifest.js +7 -4
- package/dist/runtime/lib/realtime/client.js +8 -2
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +147 -33
- package/dist/runtime/lib/router.js +169 -20
- package/dist/runtime/lib/router.test.js +241 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
- package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
- package/dist/runtime/lib/types.js +1 -0
- package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
- package/dist/runtime/render/renderToStream.d.ts +4 -2
- package/dist/runtime/render/renderToStream.js +53 -24
- package/dist/runtime/render/renderToString.d.ts +3 -1
- package/dist/runtime/requestInfo/types.d.ts +4 -1
- package/dist/runtime/requestInfo/utils.d.ts +9 -0
- package/dist/runtime/requestInfo/utils.js +44 -0
- package/dist/runtime/requestInfo/worker.js +3 -2
- package/dist/runtime/script.d.ts +1 -3
- package/dist/runtime/script.js +1 -10
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +13 -0
- package/dist/runtime/worker.d.ts +3 -1
- package/dist/runtime/worker.js +26 -0
- package/dist/scripts/debug-sync.mjs +18 -20
- package/dist/scripts/worker-run.d.mts +1 -1
- package/dist/scripts/worker-run.mjs +52 -113
- package/dist/use-synced-state/SyncStateServer.d.mts +20 -0
- package/dist/use-synced-state/SyncStateServer.mjs +124 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
- package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
- package/dist/use-synced-state/client.d.ts +28 -0
- package/dist/use-synced-state/client.js +39 -0
- package/dist/use-synced-state/constants.d.mts +1 -0
- package/dist/use-synced-state/constants.mjs +1 -0
- package/dist/use-synced-state/useSyncState.d.ts +20 -0
- package/dist/use-synced-state/useSyncState.js +58 -0
- package/dist/use-synced-state/useSyncedState.d.ts +20 -0
- package/dist/use-synced-state/useSyncedState.js +58 -0
- package/dist/use-synced-state/worker.d.mts +14 -0
- package/dist/use-synced-state/worker.mjs +73 -0
- package/dist/vite/buildApp.mjs +34 -2
- package/dist/vite/configPlugin.mjs +8 -14
- package/dist/vite/constants.d.mts +1 -0
- package/dist/vite/constants.mjs +1 -0
- package/dist/vite/directiveModulesDevPlugin.mjs +1 -1
- package/dist/vite/envResolvers.d.mts +11 -0
- package/dist/vite/envResolvers.mjs +20 -0
- package/dist/vite/getViteEsbuild.mjs +2 -1
- package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
- package/dist/vite/hmrStabilityPlugin.mjs +68 -0
- package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
- package/dist/vite/knownDepsResolverPlugin.mjs +1 -12
- package/dist/vite/linkerPlugin.d.mts +2 -1
- package/dist/vite/linkerPlugin.mjs +11 -3
- package/dist/vite/linkerPlugin.test.mjs +15 -0
- package/dist/vite/miniflareHMRPlugin.mjs +1 -38
- package/dist/vite/moveStaticAssetsPlugin.mjs +14 -4
- package/dist/vite/redwoodPlugin.mjs +6 -10
- package/dist/vite/runDirectivesScan.mjs +59 -14
- package/dist/vite/ssrBridgePlugin.mjs +122 -34
- package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
- package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
- package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
- package/dist/vite/staleDepRetryPlugin.mjs +69 -0
- package/dist/vite/statePlugin.d.mts +4 -0
- package/dist/vite/statePlugin.mjs +62 -0
- package/package.json +26 -10
- package/dist/vite/manifestPlugin.d.mts +0 -4
- package/dist/vite/manifestPlugin.mjs +0 -63
- /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { setSyncStateClientForTesting } from "../client";
|
|
3
|
+
import { createSyncStateHook, } from "../useSyncedState";
|
|
4
|
+
const createStateHarness = () => {
|
|
5
|
+
let currentState;
|
|
6
|
+
const cleanups = [];
|
|
7
|
+
const useStateImpl = ((initialValue) => {
|
|
8
|
+
const resolved = typeof initialValue === "function"
|
|
9
|
+
? initialValue()
|
|
10
|
+
: initialValue;
|
|
11
|
+
currentState = resolved;
|
|
12
|
+
const setState = (next) => {
|
|
13
|
+
currentState =
|
|
14
|
+
typeof next === "function"
|
|
15
|
+
? next(currentState)
|
|
16
|
+
: next;
|
|
17
|
+
};
|
|
18
|
+
return [currentState, setState];
|
|
19
|
+
});
|
|
20
|
+
const useEffectImpl = (callback) => {
|
|
21
|
+
const cleanup = callback();
|
|
22
|
+
if (typeof cleanup === "function") {
|
|
23
|
+
cleanups.push(cleanup);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const useRefImpl = ((value) => ({
|
|
27
|
+
current: value,
|
|
28
|
+
}));
|
|
29
|
+
const useCallbackImpl = (fn) => fn;
|
|
30
|
+
const deps = {
|
|
31
|
+
useState: useStateImpl,
|
|
32
|
+
useEffect: useEffectImpl,
|
|
33
|
+
useRef: useRefImpl,
|
|
34
|
+
useCallback: useCallbackImpl,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
deps,
|
|
38
|
+
getState: () => currentState,
|
|
39
|
+
runCleanups: () => cleanups.forEach((fn) => fn()),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
describe("createSyncStateHook", () => {
|
|
43
|
+
const subscribeHandlers = new Map();
|
|
44
|
+
const client = {
|
|
45
|
+
async getState() {
|
|
46
|
+
return 5;
|
|
47
|
+
},
|
|
48
|
+
async setState(_value, _key) { },
|
|
49
|
+
async subscribe(key, handler) {
|
|
50
|
+
subscribeHandlers.set(key, handler);
|
|
51
|
+
},
|
|
52
|
+
async unsubscribe(key) {
|
|
53
|
+
subscribeHandlers.delete(key);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const resetClient = () => {
|
|
57
|
+
client.getState = async () => 5;
|
|
58
|
+
client.setState = async (_value, _key) => { };
|
|
59
|
+
client.subscribe = async (key, handler) => {
|
|
60
|
+
subscribeHandlers.set(key, handler);
|
|
61
|
+
};
|
|
62
|
+
client.unsubscribe = async (key) => {
|
|
63
|
+
subscribeHandlers.delete(key);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
resetClient();
|
|
68
|
+
setSyncStateClientForTesting(client);
|
|
69
|
+
subscribeHandlers.clear();
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
setSyncStateClientForTesting(null);
|
|
73
|
+
});
|
|
74
|
+
it("loads remote state and updates local value", async () => {
|
|
75
|
+
const harness = createStateHarness();
|
|
76
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
77
|
+
const [value] = useSyncedState(0, "counter");
|
|
78
|
+
expect(value).toBe(0);
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
expect(harness.getState()).toBe(5);
|
|
81
|
+
});
|
|
82
|
+
it("sends updates through the client and applies optimistic value", async () => {
|
|
83
|
+
const harness = createStateHarness();
|
|
84
|
+
const setCalls = [];
|
|
85
|
+
client.setState = async (value, key) => {
|
|
86
|
+
setCalls.push({ key, value });
|
|
87
|
+
};
|
|
88
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
89
|
+
const [, setSyncValue] = useSyncedState(0, "counter");
|
|
90
|
+
setSyncValue(9);
|
|
91
|
+
expect(harness.getState()).toBe(9);
|
|
92
|
+
expect(setCalls).toEqual([{ key: "counter", value: 9 }]);
|
|
93
|
+
});
|
|
94
|
+
it("applies remote updates from the subscription handler", async () => {
|
|
95
|
+
const harness = createStateHarness();
|
|
96
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
97
|
+
useSyncedState(0, "counter");
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
const handler = subscribeHandlers.get("counter");
|
|
100
|
+
handler?.(7);
|
|
101
|
+
expect(harness.getState()).toBe(7);
|
|
102
|
+
});
|
|
103
|
+
it("unsubscribes during cleanup", () => {
|
|
104
|
+
const harness = createStateHarness();
|
|
105
|
+
const unsubscribed = [];
|
|
106
|
+
client.unsubscribe = async (key) => {
|
|
107
|
+
unsubscribed.push({ key });
|
|
108
|
+
subscribeHandlers.delete(key);
|
|
109
|
+
};
|
|
110
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
111
|
+
useSyncedState(0, "counter");
|
|
112
|
+
harness.runCleanups();
|
|
113
|
+
expect(unsubscribed).toEqual([{ key: "counter" }]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { setSyncStateClientForTesting, } from "../client";
|
|
3
|
+
import { createSyncStateHook, } from "../useSyncedState";
|
|
4
|
+
const createStateHarness = () => {
|
|
5
|
+
let currentState;
|
|
6
|
+
const cleanups = [];
|
|
7
|
+
const useStateImpl = ((initialValue) => {
|
|
8
|
+
const resolved = typeof initialValue === "function"
|
|
9
|
+
? initialValue()
|
|
10
|
+
: initialValue;
|
|
11
|
+
currentState = resolved;
|
|
12
|
+
const setState = (next) => {
|
|
13
|
+
currentState =
|
|
14
|
+
typeof next === "function"
|
|
15
|
+
? next(currentState)
|
|
16
|
+
: next;
|
|
17
|
+
};
|
|
18
|
+
return [currentState, setState];
|
|
19
|
+
});
|
|
20
|
+
const useEffectImpl = (callback) => {
|
|
21
|
+
const cleanup = callback();
|
|
22
|
+
if (typeof cleanup === "function") {
|
|
23
|
+
cleanups.push(cleanup);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const useRefImpl = ((value) => ({
|
|
27
|
+
current: value,
|
|
28
|
+
}));
|
|
29
|
+
const useCallbackImpl = (fn) => fn;
|
|
30
|
+
const deps = {
|
|
31
|
+
useState: useStateImpl,
|
|
32
|
+
useEffect: useEffectImpl,
|
|
33
|
+
useRef: useRefImpl,
|
|
34
|
+
useCallback: useCallbackImpl,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
deps,
|
|
38
|
+
getState: () => currentState,
|
|
39
|
+
runCleanups: () => cleanups.forEach((fn) => fn()),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
describe("createSyncStateHook", () => {
|
|
43
|
+
const subscribeHandlers = new Map();
|
|
44
|
+
const client = {
|
|
45
|
+
async getState() {
|
|
46
|
+
return 5;
|
|
47
|
+
},
|
|
48
|
+
async setState(_value, _key) { },
|
|
49
|
+
async subscribe(key, handler) {
|
|
50
|
+
subscribeHandlers.set(key, handler);
|
|
51
|
+
},
|
|
52
|
+
async unsubscribe(key) {
|
|
53
|
+
subscribeHandlers.delete(key);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const resetClient = () => {
|
|
57
|
+
client.getState = async () => 5;
|
|
58
|
+
client.setState = async (_value, _key) => { };
|
|
59
|
+
client.subscribe = async (key, handler) => {
|
|
60
|
+
subscribeHandlers.set(key, handler);
|
|
61
|
+
};
|
|
62
|
+
client.unsubscribe = async (key) => {
|
|
63
|
+
subscribeHandlers.delete(key);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
resetClient();
|
|
68
|
+
setSyncStateClientForTesting(client);
|
|
69
|
+
subscribeHandlers.clear();
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
setSyncStateClientForTesting(null);
|
|
73
|
+
});
|
|
74
|
+
it("loads remote state and updates local value", async () => {
|
|
75
|
+
const harness = createStateHarness();
|
|
76
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
77
|
+
const [value] = useSyncedState(0, "counter");
|
|
78
|
+
expect(value).toBe(0);
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
expect(harness.getState()).toBe(5);
|
|
81
|
+
});
|
|
82
|
+
it("sends updates through the client and applies optimistic value", async () => {
|
|
83
|
+
const harness = createStateHarness();
|
|
84
|
+
const setCalls = [];
|
|
85
|
+
client.setState = async (value, key) => {
|
|
86
|
+
setCalls.push({ key, value });
|
|
87
|
+
};
|
|
88
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
89
|
+
const [, setSyncValue] = useSyncedState(0, "counter");
|
|
90
|
+
setSyncValue(9);
|
|
91
|
+
expect(harness.getState()).toBe(9);
|
|
92
|
+
expect(setCalls).toEqual([{ key: "counter", value: 9 }]);
|
|
93
|
+
});
|
|
94
|
+
it("applies remote updates from the subscription handler", async () => {
|
|
95
|
+
const harness = createStateHarness();
|
|
96
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
97
|
+
useSyncedState(0, "counter");
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
const handler = subscribeHandlers.get("counter");
|
|
100
|
+
handler?.(7);
|
|
101
|
+
expect(harness.getState()).toBe(7);
|
|
102
|
+
});
|
|
103
|
+
it("unsubscribes during cleanup", () => {
|
|
104
|
+
const harness = createStateHarness();
|
|
105
|
+
const unsubscribed = [];
|
|
106
|
+
client.unsubscribe = async (key) => {
|
|
107
|
+
unsubscribed.push({ key });
|
|
108
|
+
subscribeHandlers.delete(key);
|
|
109
|
+
};
|
|
110
|
+
const useSyncedState = createSyncStateHook({ hooks: harness.deps });
|
|
111
|
+
useSyncedState(0, "counter");
|
|
112
|
+
harness.runCleanups();
|
|
113
|
+
expect(unsubscribed).toEqual([{ key: "counter" }]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
vi.mock("cloudflare:workers", () => {
|
|
3
|
+
class DurableObject {
|
|
4
|
+
}
|
|
5
|
+
return { DurableObject, env: {} };
|
|
6
|
+
});
|
|
7
|
+
vi.mock("capnweb", () => ({
|
|
8
|
+
RpcTarget: class RpcTarget {
|
|
9
|
+
},
|
|
10
|
+
newWorkersRpcResponse: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("../runtime/entries/router", () => ({
|
|
13
|
+
route: vi.fn((path, handler) => ({ path, handler })),
|
|
14
|
+
}));
|
|
15
|
+
import { SyncStateServer } from "../SyncStateServer.mjs";
|
|
16
|
+
describe("SyncStateProxy", () => {
|
|
17
|
+
let mockCoordinator;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockCoordinator = new SyncStateServer({}, {});
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
SyncStateServer.registerKeyHandler(async (key) => key);
|
|
23
|
+
});
|
|
24
|
+
it("transforms keys before calling coordinator methods when handler is registered", async () => {
|
|
25
|
+
const handler = async (key) => `transformed:${key}`;
|
|
26
|
+
SyncStateServer.registerKeyHandler(handler);
|
|
27
|
+
const transformedKey = await handler("counter");
|
|
28
|
+
expect(transformedKey).toBe("transformed:counter");
|
|
29
|
+
mockCoordinator.setState(5, transformedKey);
|
|
30
|
+
const value = mockCoordinator.getState(transformedKey);
|
|
31
|
+
expect(value).toBe(5);
|
|
32
|
+
});
|
|
33
|
+
it("does not transform keys when no handler is registered", () => {
|
|
34
|
+
SyncStateServer.registerKeyHandler(async (key) => key);
|
|
35
|
+
const handler = SyncStateServer.getKeyHandler();
|
|
36
|
+
expect(handler).not.toBeNull();
|
|
37
|
+
});
|
|
38
|
+
it("passes through original key when handler returns it unchanged", async () => {
|
|
39
|
+
const handler = async (key) => key;
|
|
40
|
+
SyncStateServer.registerKeyHandler(handler);
|
|
41
|
+
const result = await handler("counter");
|
|
42
|
+
expect(result).toBe("counter");
|
|
43
|
+
});
|
|
44
|
+
it("handler can scope keys per user", async () => {
|
|
45
|
+
const handler = async (key) => {
|
|
46
|
+
const userId = "user123";
|
|
47
|
+
return `user:${userId}:${key}`;
|
|
48
|
+
};
|
|
49
|
+
SyncStateServer.registerKeyHandler(handler);
|
|
50
|
+
const result = await handler("settings");
|
|
51
|
+
expect(result).toBe("user:user123:settings");
|
|
52
|
+
});
|
|
53
|
+
it("allows errors from handler to propagate", async () => {
|
|
54
|
+
const handler = async (_key) => {
|
|
55
|
+
throw new Error("Handler error");
|
|
56
|
+
};
|
|
57
|
+
SyncStateServer.registerKeyHandler(handler);
|
|
58
|
+
await expect(handler("test")).rejects.toThrow("Handler error");
|
|
59
|
+
});
|
|
60
|
+
it("handles async operations in handler", async () => {
|
|
61
|
+
const handler = async (key) => {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
63
|
+
return `async:${key}`;
|
|
64
|
+
};
|
|
65
|
+
SyncStateServer.registerKeyHandler(handler);
|
|
66
|
+
const result = await handler("data");
|
|
67
|
+
expect(result).toBe("async:data");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SyncStateClient = {
|
|
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
|
+
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 {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
2
|
+
import { DEFAULT_SYNC_STATE_PATH } from "./constants.mjs";
|
|
3
|
+
let cachedClient = null;
|
|
4
|
+
let cachedEndpoint = DEFAULT_SYNC_STATE_PATH;
|
|
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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_SYNC_STATE_PATH = "/__sync-state";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_SYNC_STATE_PATH = "/__sync-state";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { React } from "../runtime/client/client";
|
|
2
|
+
type HookDeps = {
|
|
3
|
+
useState: typeof React.useState;
|
|
4
|
+
useEffect: typeof React.useEffect;
|
|
5
|
+
useRef: typeof React.useRef;
|
|
6
|
+
useCallback: typeof React.useCallback;
|
|
7
|
+
};
|
|
8
|
+
type Setter<T> = (value: T | ((previous: T) => T)) => void;
|
|
9
|
+
export type CreateSyncStateHookOptions = {
|
|
10
|
+
url?: string;
|
|
11
|
+
hooks?: HookDeps;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
15
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
16
|
+
* @returns Hook that syncs state through the sync state service.
|
|
17
|
+
*/
|
|
18
|
+
export declare const createSyncStateHook: (options?: CreateSyncStateHookOptions) => <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
19
|
+
export declare const useSyncedState: <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { React } from "../runtime/client/client";
|
|
2
|
+
import { getSyncStateClient } from "./client";
|
|
3
|
+
import { DEFAULT_SYNC_STATE_PATH } from "./constants.mjs";
|
|
4
|
+
const defaultDeps = {
|
|
5
|
+
useState: React.useState,
|
|
6
|
+
useEffect: React.useEffect,
|
|
7
|
+
useRef: React.useRef,
|
|
8
|
+
useCallback: React.useCallback,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
12
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
13
|
+
* @returns Hook that syncs state through the sync state service.
|
|
14
|
+
*/
|
|
15
|
+
export const createSyncStateHook = (options = {}) => {
|
|
16
|
+
const resolvedUrl = options.url ?? DEFAULT_SYNC_STATE_PATH;
|
|
17
|
+
const deps = options.hooks ?? defaultDeps;
|
|
18
|
+
const { useState, useEffect, useRef, useCallback } = deps;
|
|
19
|
+
return function useSyncedState(initialValue, key) {
|
|
20
|
+
if (typeof window === "undefined" && !options.hooks) {
|
|
21
|
+
return [initialValue, () => { }];
|
|
22
|
+
}
|
|
23
|
+
const client = getSyncStateClient(resolvedUrl);
|
|
24
|
+
const [value, setValue] = useState(initialValue);
|
|
25
|
+
const valueRef = useRef(value);
|
|
26
|
+
valueRef.current = value;
|
|
27
|
+
const setSyncValue = useCallback((nextValue) => {
|
|
28
|
+
const resolved = typeof nextValue === "function"
|
|
29
|
+
? nextValue(valueRef.current)
|
|
30
|
+
: nextValue;
|
|
31
|
+
setValue(resolved);
|
|
32
|
+
valueRef.current = resolved;
|
|
33
|
+
void client.setState(resolved, key);
|
|
34
|
+
}, [client, key, setValue, valueRef]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let isActive = true;
|
|
37
|
+
const handleUpdate = (next) => {
|
|
38
|
+
if (isActive) {
|
|
39
|
+
setValue(next);
|
|
40
|
+
valueRef.current = next;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
void client.getState(key).then((existing) => {
|
|
44
|
+
if (existing !== undefined && isActive) {
|
|
45
|
+
setValue(existing);
|
|
46
|
+
valueRef.current = existing;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
void client.subscribe(key, handleUpdate);
|
|
50
|
+
return () => {
|
|
51
|
+
isActive = false;
|
|
52
|
+
void client.unsubscribe(key, handleUpdate);
|
|
53
|
+
};
|
|
54
|
+
}, [client, key, setValue, valueRef]);
|
|
55
|
+
return [value, setSyncValue];
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const useSyncedState = createSyncStateHook();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { React } from "../runtime/client/client";
|
|
2
|
+
type HookDeps = {
|
|
3
|
+
useState: typeof React.useState;
|
|
4
|
+
useEffect: typeof React.useEffect;
|
|
5
|
+
useRef: typeof React.useRef;
|
|
6
|
+
useCallback: typeof React.useCallback;
|
|
7
|
+
};
|
|
8
|
+
type Setter<T> = (value: T | ((previous: T) => T)) => void;
|
|
9
|
+
export type CreateSyncStateHookOptions = {
|
|
10
|
+
url?: string;
|
|
11
|
+
hooks?: HookDeps;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
15
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
16
|
+
* @returns Hook that syncs state through the sync state service.
|
|
17
|
+
*/
|
|
18
|
+
export declare const createSyncStateHook: (options?: CreateSyncStateHookOptions) => <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
19
|
+
export declare const useSyncedState: <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { React } from "../runtime/client/client";
|
|
2
|
+
import { getSyncStateClient } from "./client";
|
|
3
|
+
import { DEFAULT_SYNC_STATE_PATH } from "./constants.mjs";
|
|
4
|
+
const defaultDeps = {
|
|
5
|
+
useState: React.useState,
|
|
6
|
+
useEffect: React.useEffect,
|
|
7
|
+
useRef: React.useRef,
|
|
8
|
+
useCallback: React.useCallback,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
12
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
13
|
+
* @returns Hook that syncs state through the sync state service.
|
|
14
|
+
*/
|
|
15
|
+
export const createSyncStateHook = (options = {}) => {
|
|
16
|
+
const resolvedUrl = options.url ?? DEFAULT_SYNC_STATE_PATH;
|
|
17
|
+
const deps = options.hooks ?? defaultDeps;
|
|
18
|
+
const { useState, useEffect, useRef, useCallback } = deps;
|
|
19
|
+
return function useSyncedState(initialValue, key) {
|
|
20
|
+
if (typeof window === "undefined" && !options.hooks) {
|
|
21
|
+
return [initialValue, () => { }];
|
|
22
|
+
}
|
|
23
|
+
const client = getSyncStateClient(resolvedUrl);
|
|
24
|
+
const [value, setValue] = useState(initialValue);
|
|
25
|
+
const valueRef = useRef(value);
|
|
26
|
+
valueRef.current = value;
|
|
27
|
+
const setSyncValue = useCallback((nextValue) => {
|
|
28
|
+
const resolved = typeof nextValue === "function"
|
|
29
|
+
? nextValue(valueRef.current)
|
|
30
|
+
: nextValue;
|
|
31
|
+
setValue(resolved);
|
|
32
|
+
valueRef.current = resolved;
|
|
33
|
+
void client.setState(resolved, key);
|
|
34
|
+
}, [client, key, setValue, valueRef]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let isActive = true;
|
|
37
|
+
const handleUpdate = (next) => {
|
|
38
|
+
if (isActive) {
|
|
39
|
+
setValue(next);
|
|
40
|
+
valueRef.current = next;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
void client.getState(key).then((existing) => {
|
|
44
|
+
if (existing !== undefined && isActive) {
|
|
45
|
+
setValue(existing);
|
|
46
|
+
valueRef.current = existing;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
void client.subscribe(key, handleUpdate);
|
|
50
|
+
return () => {
|
|
51
|
+
isActive = false;
|
|
52
|
+
void client.unsubscribe(key, handleUpdate);
|
|
53
|
+
};
|
|
54
|
+
}, [client, key, setValue, valueRef]);
|
|
55
|
+
return [value, setSyncValue];
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const useSyncedState = createSyncStateHook();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SyncStateServer } from "./SyncStateServer.mjs";
|
|
2
|
+
export { SyncStateServer } from "./SyncStateServer.mjs";
|
|
3
|
+
export type SyncStateRouteOptions = {
|
|
4
|
+
basePath?: string;
|
|
5
|
+
resetPath?: string;
|
|
6
|
+
durableObjectName?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
10
|
+
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
11
|
+
* @param options Optional overrides for base path, reset path, and object name.
|
|
12
|
+
* @returns Router entries for the sync state API and reset endpoint.
|
|
13
|
+
*/
|
|
14
|
+
export declare const syncStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncStateServer>, options?: SyncStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition<`/${string}`, import("../runtime/worker.js").RequestInfo<any, import("../runtime/worker.js").DefaultAppContext>>[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
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
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _SyncStateProxy_stub, _SyncStateProxy_keyHandler;
|
|
13
|
+
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
|
|
14
|
+
import { env } from "cloudflare:workers";
|
|
15
|
+
import { route } from "../runtime/entries/router";
|
|
16
|
+
import { SyncStateServer } from "./SyncStateServer.mjs";
|
|
17
|
+
import { DEFAULT_SYNC_STATE_PATH } from "./constants.mjs";
|
|
18
|
+
export { SyncStateServer } from "./SyncStateServer.mjs";
|
|
19
|
+
const DEFAULT_SYNC_STATE_NAME = "syncedState";
|
|
20
|
+
class SyncStateProxy extends RpcTarget {
|
|
21
|
+
constructor(stub, keyHandler) {
|
|
22
|
+
super();
|
|
23
|
+
_SyncStateProxy_stub.set(this, void 0);
|
|
24
|
+
_SyncStateProxy_keyHandler.set(this, void 0);
|
|
25
|
+
__classPrivateFieldSet(this, _SyncStateProxy_stub, stub, "f");
|
|
26
|
+
__classPrivateFieldSet(this, _SyncStateProxy_keyHandler, keyHandler, "f");
|
|
27
|
+
}
|
|
28
|
+
async getState(key) {
|
|
29
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f").call(this, key) : key;
|
|
30
|
+
return __classPrivateFieldGet(this, _SyncStateProxy_stub, "f").getState(transformedKey);
|
|
31
|
+
}
|
|
32
|
+
async setState(value, key) {
|
|
33
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f").call(this, key) : key;
|
|
34
|
+
return __classPrivateFieldGet(this, _SyncStateProxy_stub, "f").setState(value, transformedKey);
|
|
35
|
+
}
|
|
36
|
+
async subscribe(key, client) {
|
|
37
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f").call(this, key) : key;
|
|
38
|
+
return __classPrivateFieldGet(this, _SyncStateProxy_stub, "f").subscribe(transformedKey, client);
|
|
39
|
+
}
|
|
40
|
+
async unsubscribe(key, client) {
|
|
41
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncStateProxy_keyHandler, "f").call(this, key) : key;
|
|
42
|
+
return __classPrivateFieldGet(this, _SyncStateProxy_stub, "f").unsubscribe(transformedKey, client);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_SyncStateProxy_stub = new WeakMap(), _SyncStateProxy_keyHandler = new WeakMap();
|
|
46
|
+
/**
|
|
47
|
+
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
48
|
+
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
49
|
+
* @param options Optional overrides for base path, reset path, and object name.
|
|
50
|
+
* @returns Router entries for the sync state API and reset endpoint.
|
|
51
|
+
*/
|
|
52
|
+
export const syncStateRoutes = (getNamespace, options = {}) => {
|
|
53
|
+
const basePath = options.basePath ?? DEFAULT_SYNC_STATE_PATH;
|
|
54
|
+
const resetPath = options.resetPath ?? `${basePath}/reset`;
|
|
55
|
+
const durableObjectName = options.durableObjectName ?? DEFAULT_SYNC_STATE_NAME;
|
|
56
|
+
const forwardRequest = async (request) => {
|
|
57
|
+
const keyHandler = SyncStateServer.getKeyHandler();
|
|
58
|
+
if (!keyHandler) {
|
|
59
|
+
const namespace = getNamespace(env);
|
|
60
|
+
const id = namespace.idFromName(durableObjectName);
|
|
61
|
+
return namespace.get(id).fetch(request);
|
|
62
|
+
}
|
|
63
|
+
const namespace = getNamespace(env);
|
|
64
|
+
const id = namespace.idFromName(durableObjectName);
|
|
65
|
+
const coordinator = namespace.get(id);
|
|
66
|
+
const proxy = new SyncStateProxy(coordinator, keyHandler);
|
|
67
|
+
return newWorkersRpcResponse(request, proxy);
|
|
68
|
+
};
|
|
69
|
+
return [
|
|
70
|
+
route(basePath, ({ request }) => forwardRequest(request)),
|
|
71
|
+
route(resetPath, ({ request }) => forwardRequest(request)),
|
|
72
|
+
];
|
|
73
|
+
};
|