rwsdk 1.0.0-beta.27-test.20251111115809 → 1.0.0-beta.27-test.20251115092208

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.
Files changed (30) hide show
  1. package/dist/runtime/client/client.d.ts +1 -0
  2. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  3. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  4. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +2 -2
  5. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +20 -20
  6. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +1 -2
  7. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +3 -78
  8. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +1 -51
  9. package/dist/runtime/lib/db/typeInference/utils.d.ts +7 -9
  10. package/dist/use-synced-state/SyncStateServer.d.mts +20 -0
  11. package/dist/use-synced-state/SyncStateServer.mjs +124 -0
  12. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  13. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
  14. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  15. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  16. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  17. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  18. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  19. package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
  20. package/dist/use-synced-state/client.d.ts +28 -0
  21. package/dist/use-synced-state/client.js +39 -0
  22. package/dist/use-synced-state/constants.d.mts +1 -0
  23. package/dist/use-synced-state/constants.mjs +1 -0
  24. package/dist/use-synced-state/useSyncState.d.ts +20 -0
  25. package/dist/use-synced-state/useSyncState.js +58 -0
  26. package/dist/use-synced-state/useSyncedState.d.ts +20 -0
  27. package/dist/use-synced-state/useSyncedState.js +58 -0
  28. package/dist/use-synced-state/worker.d.mts +14 -0
  29. package/dist/use-synced-state/worker.mjs +73 -0
  30. package/package.json +12 -3
@@ -1,5 +1,6 @@
1
1
  import "./setWebpackRequire";
2
2
  export { default as React } from "react";
3
+ export type { Dispatch, MutableRefObject, SetStateAction } from "react";
3
4
  export { ClientOnly } from "./ClientOnly.js";
4
5
  export { initClientNavigation, navigate } from "./navigation.js";
5
6
  import type { HydrationOptions, Transport } from "./types";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mock implementation of the virtual:use-client-lookup.js module for tests.
3
+ * This provides an empty lookup object since tests don't need to actually
4
+ * load client modules - they use dependency injection for React hooks.
5
+ */
6
+ export declare const useClientLookup: Record<string, () => Promise<any>>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mock implementation of the virtual:use-client-lookup.js module for tests.
3
+ * This provides an empty lookup object since tests don't need to actually
4
+ * load client modules - they use dependency injection for React hooks.
5
+ */
6
+ export const useClientLookup = {};
@@ -31,14 +31,14 @@ export interface AlterTableBuilder<TName extends string, TOps extends AlterOpera
31
31
  readonly __renamedFrom: TName;
32
32
  };
33
33
  setSchema(newSchema: string): AlterTableBuilder<TName, TOps>;
34
- addColumn<K extends string, T extends DataTypeExpression, TNullable extends boolean = true>(name: K, type: T, build?: (col: ColumnDefinitionBuilder<SqlToTsType<T>>) => ColumnDefinitionBuilder<SqlToTsType<T>, TNullable>): AlterTableBuilder<TName, [...TOps, AddColumnOp<K, T, TNullable>]>;
34
+ addColumn<K extends string, T extends DataTypeExpression>(name: K, type: T, build?: (col: ColumnDefinitionBuilder<SqlToTsType<T>>) => ColumnDefinitionBuilder<SqlToTsType<T>>): AlterTableBuilder<TName, [...TOps, AddColumnOp<K, T>]>;
35
35
  dropColumn<K extends string>(name: K): AlterTableBuilder<TName, [...TOps, DropColumnOp<K>]>;
36
36
  renameColumn<KFrom extends string, KTo extends string>(from: KFrom, to: KTo): AlterTableBuilder<TName, [...TOps, RenameColumnOp<KFrom, KTo>]>;
37
37
  alterColumn<K extends string, const TCallback extends AlterColumnBuilderCallback>(column: K, alteration: TCallback): AlterTableBuilder<TName, [
38
38
  ...TOps,
39
39
  AlterColumnOp<K, ReturnType<TCallback>["__alteration"]>
40
40
  ]>;
41
- modifyColumn<K extends string, T extends DataTypeExpression, TNullable extends boolean = true>(column: K, type: T, build?: (col: ColumnDefinitionBuilder<SqlToTsType<T>>) => ColumnDefinitionBuilder<SqlToTsType<T>, TNullable>): AlterTableBuilder<TName, [...TOps, ModifyColumnOp<K, T, TNullable>]>;
41
+ modifyColumn<K extends string, T extends DataTypeExpression>(column: K, type: T, build?: (col: ColumnDefinitionBuilder<SqlToTsType<T>>) => ColumnDefinitionBuilder<SqlToTsType<T>>): AlterTableBuilder<TName, [...TOps, ModifyColumnOp<K, T>]>;
42
42
  addUniqueConstraint(constraintName: string, columns: string[], build?: (builder: UniqueConstraintBuilder) => UniqueConstraintBuilder): AlterTableBuilder<TName, TOps>;
43
43
  addPrimaryKeyConstraint(constraintName: string, columns: string[], build?: (builder: PrimaryKeyConstraintBuilder) => PrimaryKeyConstraintBuilder): AlterTableBuilder<TName, TOps>;
44
44
  addCheckConstraint(constraintName: string, checkExpression: Expression<any>, build?: (builder: CheckConstraintBuilder) => CheckConstraintBuilder): AlterTableBuilder<TName, TOps>;
@@ -1,25 +1,25 @@
1
1
  import { ColumnDefinitionNode, Expression, sql } from "kysely";
2
2
  type DefaultValueExpression = string | number | boolean | null | ReturnType<typeof sql>;
3
- export interface ColumnDefinitionBuilder<TType, TNullable extends boolean = true> {
4
- autoIncrement(): ColumnDefinitionBuilder<TType, TNullable>;
5
- identity(): ColumnDefinitionBuilder<TType, TNullable>;
6
- primaryKey(): ColumnDefinitionBuilder<TType, false>;
7
- references(ref: string): ColumnDefinitionBuilder<TType, TNullable>;
8
- onDelete(onDelete: "no action" | "restrict" | "cascade" | "set null" | "set default"): ColumnDefinitionBuilder<TType, TNullable>;
9
- onUpdate(onUpdate: "no action" | "restrict" | "cascade" | "set null" | "set default"): ColumnDefinitionBuilder<TType, TNullable>;
10
- unique(): ColumnDefinitionBuilder<TType, TNullable>;
11
- notNull(): ColumnDefinitionBuilder<TType, false>;
12
- unsigned(): ColumnDefinitionBuilder<TType, TNullable>;
13
- defaultTo(value: DefaultValueExpression): ColumnDefinitionBuilder<TType, false>;
14
- check(expression: Expression<any>): ColumnDefinitionBuilder<TType, TNullable>;
15
- generatedAlwaysAs(expression: Expression<any>): ColumnDefinitionBuilder<TType, TNullable>;
16
- generatedAlwaysAsIdentity(): ColumnDefinitionBuilder<TType, TNullable>;
17
- generatedByDefaultAsIdentity(): ColumnDefinitionBuilder<TType, TNullable>;
18
- stored(): ColumnDefinitionBuilder<TType, TNullable>;
19
- modifyFront(modifier: Expression<any>): ColumnDefinitionBuilder<TType, TNullable>;
20
- nullsNotDistinct(): ColumnDefinitionBuilder<TType, TNullable>;
21
- ifNotExists(): ColumnDefinitionBuilder<TType, TNullable>;
22
- modifyEnd(modifier: Expression<any>): ColumnDefinitionBuilder<TType, TNullable>;
3
+ export interface ColumnDefinitionBuilder<TType> {
4
+ autoIncrement(): ColumnDefinitionBuilder<TType>;
5
+ identity(): ColumnDefinitionBuilder<TType>;
6
+ primaryKey(): ColumnDefinitionBuilder<TType>;
7
+ references(ref: string): ColumnDefinitionBuilder<TType>;
8
+ onDelete(onDelete: "no action" | "restrict" | "cascade" | "set null" | "set default"): ColumnDefinitionBuilder<TType>;
9
+ onUpdate(onUpdate: "no action" | "restrict" | "cascade" | "set null" | "set default"): ColumnDefinitionBuilder<TType>;
10
+ unique(): ColumnDefinitionBuilder<TType>;
11
+ notNull(): ColumnDefinitionBuilder<TType>;
12
+ unsigned(): ColumnDefinitionBuilder<TType>;
13
+ defaultTo(value: DefaultValueExpression): ColumnDefinitionBuilder<TType>;
14
+ check(expression: Expression<any>): ColumnDefinitionBuilder<TType>;
15
+ generatedAlwaysAs(expression: Expression<any>): ColumnDefinitionBuilder<TType>;
16
+ generatedAlwaysAsIdentity(): ColumnDefinitionBuilder<TType>;
17
+ generatedByDefaultAsIdentity(): ColumnDefinitionBuilder<TType>;
18
+ stored(): ColumnDefinitionBuilder<TType>;
19
+ modifyFront(modifier: Expression<any>): ColumnDefinitionBuilder<TType>;
20
+ nullsNotDistinct(): ColumnDefinitionBuilder<TType>;
21
+ ifNotExists(): ColumnDefinitionBuilder<TType>;
22
+ modifyEnd(modifier: Expression<any>): ColumnDefinitionBuilder<TType>;
23
23
  $call<T>(func: (qb: this) => T): T;
24
24
  toOperationNode(): ColumnDefinitionNode;
25
25
  }
@@ -28,8 +28,7 @@ export interface CreateTableBuilder<TName extends string, TSchema extends Record
28
28
  temporary(): CreateTableBuilder<TName, TSchema>;
29
29
  onCommit(onCommit: "preserve rows" | "delete rows" | "drop"): CreateTableBuilder<TName, TSchema>;
30
30
  ifNotExists(): CreateTableBuilder<TName, TSchema>;
31
- addColumn<K extends string, T extends string>(name: K, type: T): CreateTableBuilder<TName, Prettify<(TSchema extends Record<string, any> ? TSchema : {}) & Record<K, SqlToTsType<T> | null>>>;
32
- addColumn<K extends string, T extends string, TNullable extends boolean>(name: K, type: T, build: (col: ColumnDefinitionBuilder<SqlToTsType<T>>) => ColumnDefinitionBuilder<SqlToTsType<T>, TNullable>): CreateTableBuilder<TName, Prettify<(TSchema extends Record<string, any> ? TSchema : {}) & Record<K, TNullable extends true ? SqlToTsType<T> | null : SqlToTsType<T>>>>;
31
+ addColumn<K extends string, T extends string>(name: K, type: T, build?: (col: ColumnDefinitionBuilder<SqlToTsType<T>>) => ColumnDefinitionBuilder<SqlToTsType<T>>): CreateTableBuilder<TName, Prettify<(TSchema extends Record<string, any> ? TSchema : {}) & Record<K, SqlToTsType<T>>>>;
33
32
  addUniqueConstraint(constraintName: string, columns: (keyof TSchema)[], build?: (builder: UniqueConstraintBuilder) => UniqueConstraintBuilder): CreateTableBuilder<TName, TSchema>;
34
33
  addPrimaryKeyConstraint(constraintName: string, columns: (keyof TSchema)[], build?: (builder: PrimaryKeyConstraintBuilder) => PrimaryKeyConstraintBuilder): CreateTableBuilder<TName, TSchema>;
35
34
  addCheckConstraint(constraintName: string, checkExpression: Expression<any>, build?: (builder: CheckConstraintBuilder) => CheckConstraintBuilder): CreateTableBuilder<TName, TSchema>;
@@ -153,7 +153,7 @@
153
153
  up: async (db) => [
154
154
  await db.schema
155
155
  .createTable("users")
156
- .addColumn("id", "integer", (c) => c.primaryKey().autoIncrement())
156
+ .addColumn("id", "integer")
157
157
  .execute(),
158
158
  ],
159
159
  },
@@ -242,7 +242,7 @@
242
242
  return [
243
243
  await db.schema
244
244
  .createTable("users")
245
- .addColumn("id", "integer", (col) => col.notNull())
245
+ .addColumn("id", "integer")
246
246
  .execute(),
247
247
  ];
248
248
  },
@@ -271,7 +271,7 @@
271
271
  return [
272
272
  await db.schema
273
273
  .createTable("users")
274
- .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
274
+ .addColumn("id", "integer")
275
275
  .execute(),
276
276
  ];
277
277
  },
@@ -357,79 +357,4 @@
357
357
  };
358
358
  (_test) => { };
359
359
  };
360
- (_it = "alterTable addColumn with notNull") => {
361
- const migrations = {
362
- "0": {
363
- async up(db) {
364
- return [
365
- await db.schema
366
- .createTable("users")
367
- .addColumn("id", "integer", (col) => col.primaryKey())
368
- .execute(),
369
- ];
370
- },
371
- },
372
- "1": {
373
- async up(db) {
374
- return [
375
- await db.schema
376
- .alterTable("users")
377
- .addColumn("email", "text", (col) => col.notNull())
378
- .execute(),
379
- ];
380
- },
381
- },
382
- };
383
- (_test) => { };
384
- };
385
- (_it = "alterTable modifyColumn with notNull") => {
386
- const migrations = {
387
- "0": {
388
- async up(db) {
389
- return [
390
- await db.schema
391
- .createTable("products")
392
- .addColumn("price", "real")
393
- .execute(),
394
- ];
395
- },
396
- },
397
- "1": {
398
- async up(db) {
399
- return [
400
- await db.schema
401
- .alterTable("products")
402
- .modifyColumn("price", "real", (col) => col.notNull())
403
- .execute(),
404
- ];
405
- },
406
- },
407
- };
408
- (_test) => { };
409
- };
410
- (_it = "alterTable modifyColumn nullable to non-nullable") => {
411
- const migrations = {
412
- "0": {
413
- async up(db) {
414
- return [
415
- await db.schema
416
- .createTable("orders")
417
- .addColumn("status", "text")
418
- .execute(),
419
- ];
420
- },
421
- },
422
- "1": {
423
- async up(db) {
424
- return [
425
- await db.schema
426
- .alterTable("orders")
427
- .modifyColumn("status", "text", (col) => col.notNull().defaultTo("pending"))
428
- .execute(),
429
- ];
430
- },
431
- },
432
- };
433
- (_test) => { };
434
- };
435
360
  export {};
@@ -22,59 +22,9 @@ import { sql } from "kysely";
22
22
  await db.schema
23
23
  .createTable("users")
24
24
  .addColumn("username", "text", (col) => col.notNull())
25
+ .addColumn("age", "integer", (col) => col.defaultTo(18))
25
26
  .addColumn("active", "boolean", (col) => col.defaultTo(true))
26
27
  .addColumn("anotherBoolean", "boolean", (col) => col.defaultTo(sql `true`))
27
- .addColumn("email", "text", (col) => col)
28
- .addColumn("favoriteColor", "text", (col) => col.unique())
29
- .addColumn("name", "text", (col) => col.defaultTo("John Doe"))
30
- .execute(),
31
- ];
32
- },
33
- },
34
- };
35
- (_test) => { };
36
- };
37
- (_it = "createTable column without callback is nullable") => {
38
- const migrations = {
39
- "001_init": {
40
- async up(db) {
41
- return [
42
- await db.schema
43
- .createTable("posts")
44
- .addColumn("title", "text")
45
- .addColumn("body", "text")
46
- .execute(),
47
- ];
48
- },
49
- },
50
- };
51
- (_test) => { };
52
- };
53
- (_it = "createTable with primaryKey is non-nullable") => {
54
- const migrations = {
55
- "001_init": {
56
- async up(db) {
57
- return [
58
- await db.schema
59
- .createTable("users")
60
- .addColumn("id", "integer", (col) => col.primaryKey())
61
- .addColumn("email", "text", (col) => col.notNull())
62
- .execute(),
63
- ];
64
- },
65
- },
66
- };
67
- (_test) => { };
68
- };
69
- (_it = "createTable with unique but no notNull is nullable") => {
70
- const migrations = {
71
- "001_init": {
72
- async up(db) {
73
- return [
74
- await db.schema
75
- .createTable("products")
76
- .addColumn("sku", "text", (col) => col.unique())
77
- .addColumn("name", "text", (col) => col)
78
28
  .execute(),
79
29
  ];
80
30
  },
@@ -1,10 +1,9 @@
1
1
  import { sql } from "kysely";
2
2
  type DataTypeExpression = string | typeof sql;
3
- export type AddColumnOp<K extends string, T extends DataTypeExpression, TNullable extends boolean = true> = {
3
+ export type AddColumnOp<K extends string, T extends DataTypeExpression> = {
4
4
  op: "addColumn";
5
5
  name: K;
6
6
  type: T;
7
- nullable: TNullable;
8
7
  };
9
8
  export type DropColumnOp<K extends string> = {
10
9
  op: "dropColumn";
@@ -15,11 +14,10 @@ export type RenameColumnOp<KFrom extends string, KTo extends string> = {
15
14
  from: KFrom;
16
15
  to: KTo;
17
16
  };
18
- export type ModifyColumnOp<K extends string, T extends DataTypeExpression, TNullable extends boolean = true> = {
17
+ export type ModifyColumnOp<K extends string, T extends DataTypeExpression> = {
19
18
  op: "modifyColumn";
20
19
  name: K;
21
20
  type: T;
22
- nullable: TNullable;
23
21
  };
24
22
  export type Alteration = {
25
23
  kind: "setDataType";
@@ -39,7 +37,7 @@ export type AlterColumnOp<K extends string, TAlteration extends Alteration> = {
39
37
  name: K;
40
38
  alteration: TAlteration;
41
39
  };
42
- export type AlterOperation = AddColumnOp<any, any, any> | DropColumnOp<any> | RenameColumnOp<any, any> | AlterColumnOp<any, any> | ModifyColumnOp<any, any, any>;
40
+ export type AlterOperation = AddColumnOp<any, any> | DropColumnOp<any> | RenameColumnOp<any, any> | AlterColumnOp<any, any> | ModifyColumnOp<any, any>;
43
41
  export type SqlToTsType<T extends string | typeof sql> = T extends "text" ? string : T extends "integer" ? number : T extends "blob" ? Uint8Array : T extends "real" ? number : T extends "boolean" ? boolean : T extends typeof sql ? any : never;
44
42
  export type Prettify<T> = {
45
43
  [K in keyof T]: T[K];
@@ -59,8 +57,8 @@ export type Cast<A, B> = A extends B ? A : B;
59
57
  /**
60
58
  * Applies a single alteration operation to a schema.
61
59
  */
62
- type ApplyOp<TSchema, THeadOp> = THeadOp extends AddColumnOp<infer K, infer T, infer TNullable> ? Prettify<TSchema & {
63
- [P in K]: TNullable extends true ? SqlToTsType<T> | null : SqlToTsType<T>;
60
+ type ApplyOp<TSchema, THeadOp> = THeadOp extends AddColumnOp<infer K, infer T> ? Prettify<TSchema & {
61
+ [P in K]: SqlToTsType<T>;
64
62
  }> : THeadOp extends DropColumnOp<infer K> ? Omit<TSchema, K> : THeadOp extends RenameColumnOp<infer KFrom, infer KTo> ? KFrom extends keyof TSchema ? Prettify<Omit<TSchema, KFrom> & {
65
63
  [P in KTo]: TSchema[KFrom];
66
64
  }> : TSchema : THeadOp extends AlterColumnOp<infer K, infer TAlt> ? TAlt extends {
@@ -68,8 +66,8 @@ type ApplyOp<TSchema, THeadOp> = THeadOp extends AddColumnOp<infer K, infer T, i
68
66
  dataType: infer DT extends string;
69
67
  } ? Prettify<Omit<TSchema, K> & {
70
68
  [P in K]: SqlToTsType<DT>;
71
- }> : TSchema : THeadOp extends ModifyColumnOp<infer K, infer T, infer TNullable> ? Prettify<Omit<TSchema, K> & {
72
- [P in K]: TNullable extends true ? SqlToTsType<T> | null : SqlToTsType<T>;
69
+ }> : TSchema : THeadOp extends ModifyColumnOp<infer K, infer T> ? Prettify<Omit<TSchema, K> & {
70
+ [P in K]: SqlToTsType<T>;
73
71
  }> : TSchema;
74
72
  /**
75
73
  * Recursively processes a list of alteration operations (AST)
@@ -0,0 +1,20 @@
1
+ import { RpcStub } from "capnweb";
2
+ import { DurableObject } from "cloudflare:workers";
3
+ export type SyncStateValue = unknown;
4
+ type OnSetHandler = (key: string, value: SyncStateValue) => void;
5
+ type OnGetHandler = (key: string, value: SyncStateValue | undefined) => void;
6
+ /**
7
+ * Durable Object that keeps shared state for multiple clients and notifies subscribers.
8
+ */
9
+ export declare class SyncStateServer 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): SyncStateValue;
16
+ setState(value: SyncStateValue, key: string): void;
17
+ subscribe(key: string, client: RpcStub<(value: SyncStateValue) => void>): void;
18
+ unsubscribe(key: string, client: RpcStub<(value: SyncStateValue) => void>): void;
19
+ }
20
+ export {};
@@ -0,0 +1,124 @@
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 _a, _SyncStateServer_keyHandler, _SyncStateServer_setStateHandler, _SyncStateServer_getStateHandler, _SyncStateServer_stateStore, _SyncStateServer_subscriptions, _SyncStateServer_subscriptionRefs, _CoordinatorApi_coordinator;
13
+ import { RpcTarget } from "capnweb";
14
+ import { DurableObject } from "cloudflare:workers";
15
+ /**
16
+ * Durable Object that keeps shared state for multiple clients and notifies subscribers.
17
+ */
18
+ export class SyncStateServer extends DurableObject {
19
+ constructor() {
20
+ super(...arguments);
21
+ _SyncStateServer_stateStore.set(this, new Map());
22
+ _SyncStateServer_subscriptions.set(this, new Map());
23
+ _SyncStateServer_subscriptionRefs.set(this, new Map());
24
+ }
25
+ static registerKeyHandler(handler) {
26
+ __classPrivateFieldSet(_a, _a, handler, "f", _SyncStateServer_keyHandler);
27
+ }
28
+ static getKeyHandler() {
29
+ return __classPrivateFieldGet(_a, _a, "f", _SyncStateServer_keyHandler);
30
+ }
31
+ static registerSetStateHandler(handler) {
32
+ __classPrivateFieldSet(_a, _a, handler, "f", _SyncStateServer_setStateHandler);
33
+ }
34
+ static registerGetStateHandler(handler) {
35
+ __classPrivateFieldSet(_a, _a, handler, "f", _SyncStateServer_getStateHandler);
36
+ }
37
+ getState(key) {
38
+ const value = __classPrivateFieldGet(this, _SyncStateServer_stateStore, "f").get(key);
39
+ if (__classPrivateFieldGet(_a, _a, "f", _SyncStateServer_getStateHandler)) {
40
+ __classPrivateFieldGet(_a, _a, "f", _SyncStateServer_getStateHandler).call(_a, key, value);
41
+ }
42
+ return value;
43
+ }
44
+ setState(value, key) {
45
+ __classPrivateFieldGet(this, _SyncStateServer_stateStore, "f").set(key, value);
46
+ if (__classPrivateFieldGet(_a, _a, "f", _SyncStateServer_setStateHandler)) {
47
+ __classPrivateFieldGet(_a, _a, "f", _SyncStateServer_setStateHandler).call(_a, key, value);
48
+ }
49
+ const subscribers = __classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").get(key);
50
+ if (!subscribers) {
51
+ return;
52
+ }
53
+ for (const subscriber of subscribers) {
54
+ subscriber(value).catch(() => {
55
+ subscribers.delete(subscriber);
56
+ const refs = __classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").get(key);
57
+ if (refs) {
58
+ for (const [original, duplicate] of refs) {
59
+ if (duplicate === subscriber) {
60
+ refs.delete(original);
61
+ break;
62
+ }
63
+ }
64
+ if (refs.size === 0) {
65
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").delete(key);
66
+ }
67
+ }
68
+ });
69
+ }
70
+ if (subscribers.size === 0) {
71
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").delete(key);
72
+ }
73
+ }
74
+ subscribe(key, client) {
75
+ if (!__classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").has(key)) {
76
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").set(key, new Set());
77
+ }
78
+ if (!__classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").has(key)) {
79
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").set(key, new Map());
80
+ }
81
+ const duplicate = client.dup();
82
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").get(key).add(duplicate);
83
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").get(key).set(client, duplicate);
84
+ }
85
+ unsubscribe(key, client) {
86
+ const duplicates = __classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").get(key);
87
+ const duplicate = duplicates?.get(client);
88
+ const subscribers = __classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").get(key);
89
+ if (duplicate && subscribers) {
90
+ subscribers.delete(duplicate);
91
+ duplicates.delete(client);
92
+ if (subscribers.size === 0) {
93
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptions, "f").delete(key);
94
+ }
95
+ if (duplicates.size === 0) {
96
+ __classPrivateFieldGet(this, _SyncStateServer_subscriptionRefs, "f").delete(key);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ _a = SyncStateServer, _SyncStateServer_stateStore = new WeakMap(), _SyncStateServer_subscriptions = new WeakMap(), _SyncStateServer_subscriptionRefs = new WeakMap();
102
+ _SyncStateServer_keyHandler = { value: null };
103
+ _SyncStateServer_setStateHandler = { value: null };
104
+ _SyncStateServer_getStateHandler = { value: null };
105
+ class CoordinatorApi extends RpcTarget {
106
+ constructor(coordinator) {
107
+ super();
108
+ _CoordinatorApi_coordinator.set(this, void 0);
109
+ __classPrivateFieldSet(this, _CoordinatorApi_coordinator, coordinator, "f");
110
+ }
111
+ getState(key) {
112
+ return __classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").getState(key);
113
+ }
114
+ setState(value, key) {
115
+ __classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").setState(value, key);
116
+ }
117
+ subscribe(key, client) {
118
+ __classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").subscribe(key, client);
119
+ }
120
+ unsubscribe(key, client) {
121
+ __classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").unsubscribe(key, client);
122
+ }
123
+ }
124
+ _CoordinatorApi_coordinator = new WeakMap();
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ vi.mock("cloudflare:workers", () => {
3
+ class DurableObject {
4
+ }
5
+ return { DurableObject };
6
+ });
7
+ import { SyncStateServer } from "../SyncStateServer.mjs";
8
+ const createStub = (onInvoke) => {
9
+ const fn = Object.assign(async (value) => {
10
+ await onInvoke(value);
11
+ }, {
12
+ dup: () => fn,
13
+ });
14
+ return fn;
15
+ };
16
+ describe("SyncStateServer", () => {
17
+ it("notifies subscribers when state changes", async () => {
18
+ const coordinator = new SyncStateServer({}, {});
19
+ const received = [];
20
+ const stub = createStub((value) => {
21
+ received.push(value);
22
+ });
23
+ coordinator.subscribe("counter", stub);
24
+ coordinator.setState(5, "counter");
25
+ expect(coordinator.getState("counter")).toBe(5);
26
+ expect(received).toEqual([5]);
27
+ });
28
+ it("removes subscriptions on unsubscribe", () => {
29
+ const coordinator = new SyncStateServer({}, {});
30
+ const stub = createStub(() => { });
31
+ coordinator.subscribe("counter", stub);
32
+ coordinator.unsubscribe("counter", stub);
33
+ coordinator.setState(1, "counter");
34
+ expect(coordinator.getState("counter")).toBe(1);
35
+ });
36
+ it("drops failing subscribers", async () => {
37
+ const coordinator = new SyncStateServer({}, {});
38
+ const stub = createStub(async () => {
39
+ throw new Error("fail");
40
+ });
41
+ coordinator.subscribe("counter", stub);
42
+ coordinator.setState(3, "counter");
43
+ await Promise.resolve();
44
+ coordinator.setState(4, "counter");
45
+ expect(coordinator.getState("counter")).toBe(4);
46
+ });
47
+ it("invokes registered onSet handler", () => {
48
+ const coordinator = new SyncStateServer({}, {});
49
+ const calls = [];
50
+ SyncStateServer.registerSetStateHandler((key, value) => {
51
+ calls.push({ key, value });
52
+ });
53
+ coordinator.setState(2, "counter");
54
+ expect(calls).toEqual([{ key: "counter", value: 2 }]);
55
+ SyncStateServer.registerSetStateHandler(null);
56
+ });
57
+ it("invokes registered onGet handler", () => {
58
+ const coordinator = new SyncStateServer({}, {});
59
+ const calls = [];
60
+ SyncStateServer.registerGetStateHandler((key, value) => {
61
+ calls.push({ key, value });
62
+ });
63
+ coordinator.setState(4, "counter");
64
+ expect(coordinator.getState("counter")).toBe(4);
65
+ expect(calls).toEqual([{ key: "counter", value: 4 }]);
66
+ SyncStateServer.registerGetStateHandler(null);
67
+ });
68
+ describe("registerKeyHandler", () => {
69
+ afterEach(() => {
70
+ SyncStateServer.registerKeyHandler(async (key) => key);
71
+ });
72
+ it("stores and retrieves the registered handler", async () => {
73
+ const handler = async (key) => `transformed:${key}`;
74
+ SyncStateServer.registerKeyHandler(handler);
75
+ const retrievedHandler = SyncStateServer.getKeyHandler();
76
+ expect(retrievedHandler).toBe(handler);
77
+ });
78
+ it("transforms keys using the registered handler", async () => {
79
+ const handler = async (key) => `user:123:${key}`;
80
+ SyncStateServer.registerKeyHandler(handler);
81
+ const result = await handler("counter");
82
+ expect(result).toBe("user:123:counter");
83
+ });
84
+ it("returns null when no handler is registered", () => {
85
+ SyncStateServer.registerKeyHandler(async (key) => key);
86
+ const handler = SyncStateServer.getKeyHandler();
87
+ expect(handler).not.toBeNull();
88
+ });
89
+ it("allows handler to be async", async () => {
90
+ const handler = async (key) => {
91
+ await new Promise((resolve) => setTimeout(resolve, 10));
92
+ return `async:${key}`;
93
+ };
94
+ SyncStateServer.registerKeyHandler(handler);
95
+ const result = await handler("test");
96
+ expect(result).toBe("async:test");
97
+ });
98
+ it("handler receives the correct key parameter", async () => {
99
+ let receivedKey = "";
100
+ const handler = async (key) => {
101
+ receivedKey = key;
102
+ return key;
103
+ };
104
+ SyncStateServer.registerKeyHandler(handler);
105
+ await handler("myKey");
106
+ expect(receivedKey).toBe("myKey");
107
+ });
108
+ });
109
+ });
@@ -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,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<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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-beta.27-test.20251111115809",
3
+ "version": "1.0.0-beta.27-test.20251115092208",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -104,6 +104,14 @@
104
104
  "./realtime/durableObject": {
105
105
  "types": "./dist/runtime/lib/realtime/durableObject.d.ts",
106
106
  "default": "./dist/runtime/lib/realtime/durableObject.js"
107
+ },
108
+ "./use-synced-state/client": {
109
+ "types": "./dist/use-synced-state/client.d.ts",
110
+ "default": "./dist/use-synced-state/client.js"
111
+ },
112
+ "./use-synced-state/worker": {
113
+ "types": "./dist/use-synced-state/worker.d.ts",
114
+ "default": "./dist/use-synced-state/worker.js"
107
115
  }
108
116
  },
109
117
  "keywords": [
@@ -144,10 +152,12 @@
144
152
  "@puppeteer/browsers": "~2.10.0",
145
153
  "@types/decompress": "~4.2.7",
146
154
  "@types/fs-extra": "~11.0.4",
155
+ "@types/glob": "^8.1.0",
147
156
  "@types/react": "~19.1.2",
148
157
  "@types/react-dom": "~19.1.2",
149
158
  "@types/react-is": "~19.0.0",
150
159
  "@vitejs/plugin-react": "~5.0.0",
160
+ "capnweb": "~0.2.0",
151
161
  "chokidar": "~4.0.0",
152
162
  "debug": "~4.4.0",
153
163
  "decompress": "~4.2.1",
@@ -173,8 +183,7 @@
173
183
  "ts-morph": "~27.0.0",
174
184
  "unique-names-generator": "~4.7.1",
175
185
  "vibe-rules": "~0.3.0",
176
- "vite-tsconfig-paths": "~5.1.4",
177
- "@types/glob": "^8.1.0"
186
+ "vite-tsconfig-paths": "~5.1.4"
178
187
  },
179
188
  "peerDependencies": {
180
189
  "@cloudflare/vite-plugin": "^1.13.10",