@livestore/sync-cf 0.4.0-dev.13 → 0.4.0-dev.15

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.
@@ -4,5 +4,5 @@ import { DoCtx } from '../layer.ts';
4
4
  export declare const createHttpRpcHandler: ({ request, responseHeaders, }: {
5
5
  request: CfTypes.Request;
6
6
  responseHeaders?: Record<string, string>;
7
- }) => Effect.Effect<CfTypes.Response, import("effect/Cause").TimeoutException, import("effect/Scope").Scope | DoCtx>;
7
+ }) => Effect.Effect<CfTypes.Response, import("effect/Cause").TimeoutException, DoCtx | import("effect/Scope").Scope>;
8
8
  //# sourceMappingURL=http-rpc-server.d.ts.map
@@ -88,7 +88,12 @@ export type DurableObjectId = string;
88
88
  */
89
89
  export declare const PERSISTENCE_FORMAT_VERSION = 7;
90
90
  export declare const encodeOutgoingMessage: (a: {
91
+ readonly backendId: string;
91
92
  readonly batch: readonly {
93
+ readonly metadata: import("effect/Option").Option<{
94
+ readonly _tag: "SyncMessage.SyncMetadata";
95
+ readonly createdAt: string;
96
+ }>;
92
97
  readonly eventEncoded: {
93
98
  readonly name: string;
94
99
  readonly args: any;
@@ -97,10 +102,6 @@ export declare const encodeOutgoingMessage: (a: {
97
102
  readonly clientId: string;
98
103
  readonly sessionId: string;
99
104
  };
100
- readonly metadata: import("effect/Option").Option<{
101
- readonly _tag: "SyncMessage.SyncMetadata";
102
- readonly createdAt: string;
103
- }>;
104
105
  }[];
105
106
  readonly pageInfo: {
106
107
  readonly _tag: "MoreUnknown";
@@ -110,7 +111,6 @@ export declare const encodeOutgoingMessage: (a: {
110
111
  } | {
111
112
  readonly _tag: "NoMore";
112
113
  };
113
- readonly backendId: string;
114
114
  } | {} | {
115
115
  readonly _tag: "SyncMessage.Pong";
116
116
  } | {
@@ -127,6 +127,7 @@ export declare const encodeIncomingMessage: (a: {
127
127
  readonly eventSequenceNumber: number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">;
128
128
  }>;
129
129
  } | {
130
+ readonly backendId: import("effect/Option").Option<string>;
130
131
  readonly batch: readonly {
131
132
  readonly name: string;
132
133
  readonly args: any;
@@ -135,7 +136,6 @@ export declare const encodeIncomingMessage: (a: {
135
136
  readonly clientId: string;
136
137
  readonly sessionId: string;
137
138
  }[];
138
- readonly backendId: import("effect/Option").Option<string>;
139
139
  } | {
140
140
  readonly _tag: "SyncMessage.Ping";
141
141
  } | {
@@ -1,5 +1,5 @@
1
1
  import type { HelperTypes } from '@livestore/common-cf';
2
- import type { Schema } from '@livestore/utils/effect';
2
+ import { Schema } from '@livestore/utils/effect';
3
3
  import type { CfTypes, SearchParams } from '../common/mod.ts';
4
4
  import { type Env } from './shared.ts';
5
5
  export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
@@ -9,7 +9,7 @@ export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjec
9
9
  * Options accepted by {@link makeWorker}. The Durable Object binding has to be
10
10
  * supplied explicitly so we never fall back to deprecated defaults when Cloudflare config changes.
11
11
  */
12
- export type MakeWorkerOptions<TEnv extends Env = Env> = {
12
+ export type MakeWorkerOptions<TEnv extends Env = Env, TSyncPayload = Schema.JsonValue> = {
13
13
  /**
14
14
  * Binding name of the sync Durable Object declared in wrangler config.
15
15
  */
@@ -19,7 +19,16 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
19
19
  * Note: This runs only at connection time, not for individual push events.
20
20
  * For push event validation, use the `onPush` callback in the durable object.
21
21
  */
22
- validatePayload?: (payload: Schema.JsonValue | undefined, context: {
22
+ /**
23
+ * Optionally pass a schema to decode the client-provided payload into a typed object
24
+ * before calling {@link validatePayload}. If omitted, the raw JSON value is forwarded.
25
+ */
26
+ syncPayloadSchema?: Schema.Schema<TSyncPayload>;
27
+ /**
28
+ * Validates the (optionally decoded) payload during WebSocket connection establishment.
29
+ * If {@link syncPayloadSchema} is provided, `payload` will be of the schema’s inferred type.
30
+ */
31
+ validatePayload?: (payload: TSyncPayload, context: {
23
32
  storeId: string;
24
33
  }) => void | Promise<void>;
25
34
  /** @default false */
@@ -32,7 +41,7 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
32
41
  * For more complex setups prefer implementing a custom `fetch` and call {@link handleSyncRequest}
33
42
  * from the branch that handles LiveStore sync requests.
34
43
  */
35
- export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined>(options: MakeWorkerOptions<TEnv>) => CFWorker<TEnv, TDurableObjectRpc>;
44
+ export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, TSyncPayload = Schema.JsonValue>(options: MakeWorkerOptions<TEnv, TSyncPayload>) => CFWorker<TEnv, TDurableObjectRpc>;
36
45
  /**
37
46
  * Handles LiveStore sync requests (e.g. with search params `?storeId=...&transport=...`).
38
47
  *
@@ -69,17 +78,16 @@ export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc exte
69
78
  *
70
79
  * @throws {UnexpectedError} If the payload is invalid
71
80
  */
72
- export declare const handleSyncRequest: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, }: {
81
+ export declare const handleSyncRequest: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown, TSyncPayload = Schema.JsonValue>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }: {
73
82
  request: CfTypes.Request<CFHostMetada>;
74
83
  searchParams: SearchParams;
75
84
  env?: TEnv | undefined;
76
85
  /** Only there for type-level reasons */
77
86
  ctx: CfTypes.ExecutionContext;
78
87
  /** Binding name of the sync backend Durable Object */
79
- syncBackendBinding: MakeWorkerOptions<TEnv>["syncBackendBinding"];
88
+ syncBackendBinding: MakeWorkerOptions<TEnv, TSyncPayload>["syncBackendBinding"];
80
89
  headers?: CfTypes.HeadersInit | undefined;
81
- validatePayload?: (payload: Schema.JsonValue | undefined, context: {
82
- storeId: string;
83
- }) => void | Promise<void>;
90
+ validatePayload?: MakeWorkerOptions<TEnv, TSyncPayload>["validatePayload"];
91
+ syncPayloadSchema?: MakeWorkerOptions<TEnv, TSyncPayload>["syncPayloadSchema"];
84
92
  }) => Promise<CfTypes.Response>;
85
93
  //# sourceMappingURL=worker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE7D,OAAO,EAAE,KAAK,GAAG,EAAoB,MAAM,aAAa,CAAA;AAMxD,MAAM,MAAM,QAAQ,CAAC,IAAI,SAAS,GAAG,GAAG,GAAG,EAAE,EAAE,SAAS,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,SAAS,GAAG,SAAS,IAAI;IAClH,KAAK,EAAE,CAAC,YAAY,GAAG,OAAO,EAC5B,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,EACtC,GAAG,EAAE,IAAI,EACT,GAAG,EAAE,OAAO,CAAC,gBAAgB,KAC1B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC/B,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,CAAC,IAAI,SAAS,GAAG,GAAG,GAAG,IAAI;IACtD;;OAEG;IACH,kBAAkB,EAAE,WAAW,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAA;IAC9D;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,SAAS,GAAG,SAAS,EAAE,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/G,qBAAqB;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,IAAI,SAAS,GAAG,GAAG,GAAG,EACtB,iBAAiB,SAAS,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,SAAS,GAAG,SAAS,EAElF,SAAS,iBAAiB,CAAC,IAAI,CAAC,KAC/B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAuDlC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,iBAAiB,GAC5B,IAAI,SAAS,GAAG,GAAG,GAAG,EACtB,iBAAiB,SAAS,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,SAAS,GAAG,SAAS,EAClF,YAAY,GAAG,OAAO,EACtB,uIAOC;IACD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IACtC,YAAY,EAAE,YAAY,CAAA;IAC1B,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,CAAA;IACtB,wCAAwC;IACxC,GAAG,EAAE,OAAO,CAAC,gBAAgB,CAAA;IAC7B,sDAAsD;IACtD,kBAAkB,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,CAAA;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,GAAG,SAAS,CAAA;IACzC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,SAAS,GAAG,SAAS,EAAE,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChH,KAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CA2C0B,CAAA"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAU,MAAM,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE7D,OAAO,EAAE,KAAK,GAAG,EAAoB,MAAM,aAAa,CAAA;AAMxD,MAAM,MAAM,QAAQ,CAAC,IAAI,SAAS,GAAG,GAAG,GAAG,EAAE,EAAE,SAAS,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,SAAS,GAAG,SAAS,IAAI;IAClH,KAAK,EAAE,CAAC,YAAY,GAAG,OAAO,EAC5B,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,EACtC,GAAG,EAAE,IAAI,EACT,GAAG,EAAE,OAAO,CAAC,gBAAgB,KAC1B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC/B,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,CAAC,IAAI,SAAS,GAAG,GAAG,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,SAAS,IAAI;IACvF;;OAEG;IACH,kBAAkB,EAAE,WAAW,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAA;IAC9D;;;;OAIG;IACH;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IAC/C;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/F,qBAAqB;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,IAAI,SAAS,GAAG,GAAG,GAAG,EACtB,iBAAiB,SAAS,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,SAAS,GAAG,SAAS,EAClF,YAAY,GAAG,MAAM,CAAC,SAAS,EAE/B,SAAS,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,KAC7C,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAwDlC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,iBAAiB,GAC5B,IAAI,SAAS,GAAG,GAAG,GAAG,EACtB,iBAAiB,SAAS,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,SAAS,GAAG,SAAS,EAClF,YAAY,GAAG,OAAO,EACtB,YAAY,GAAG,MAAM,CAAC,SAAS,EAC/B,0JAQC;IACD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IACtC,YAAY,EAAE,YAAY,CAAA;IAC1B,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,CAAA;IACtB,wCAAwC;IACxC,GAAG,EAAE,OAAO,CAAC,gBAAgB,CAAA;IAC7B,sDAAsD;IACtD,kBAAkB,EAAE,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,oBAAoB,CAAC,CAAA;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,GAAG,SAAS,CAAA;IACzC,eAAe,CAAC,EAAE,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,iBAAiB,CAAC,CAAA;IAC1E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,mBAAmB,CAAC,CAAA;CAC/E,KAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CA+D0B,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { env as importedEnv } from 'cloudflare:workers';
2
2
  import { UnexpectedError } from '@livestore/common';
3
- import { Effect } from '@livestore/utils/effect';
3
+ import { Effect, Schema } from '@livestore/utils/effect';
4
4
  import { matchSyncRequest } from "./shared.js";
5
5
  /**
6
6
  * Produces a Cloudflare Worker `fetch` handler that delegates sync traffic to the
@@ -37,6 +37,7 @@ export const makeWorker = (options) => {
37
37
  syncBackendBinding: options.syncBackendBinding,
38
38
  headers: corsHeaders,
39
39
  validatePayload: options.validatePayload,
40
+ syncPayloadSchema: options.syncPayloadSchema,
40
41
  });
41
42
  }
42
43
  // Only show info message for GET requests to / without sync parameters
@@ -94,12 +95,29 @@ export const makeWorker = (options) => {
94
95
  *
95
96
  * @throws {UnexpectedError} If the payload is invalid
96
97
  */
97
- export const handleSyncRequest = ({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, }) => Effect.gen(function* () {
98
+ export const handleSyncRequest = ({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }) => Effect.gen(function* () {
98
99
  if (validatePayload !== undefined) {
99
- const result = yield* Effect.promise(async () => validatePayload(payload, { storeId })).pipe(UnexpectedError.mapToUnexpectedError, Effect.either);
100
- if (result._tag === 'Left') {
101
- console.error('Invalid payload', result.left);
102
- return new Response(result.left.toString(), { status: 400, headers });
100
+ // Always decode with the supplied schema when present, even if payload is undefined.
101
+ // This ensures required payloads are enforced by the schema.
102
+ if (syncPayloadSchema !== undefined) {
103
+ const decodedEither = Schema.decodeUnknownEither(syncPayloadSchema)(payload);
104
+ if (decodedEither._tag === 'Left') {
105
+ const message = decodedEither.left.toString();
106
+ console.error('Invalid payload (decode failed)', message);
107
+ return new Response(message, { status: 400, headers });
108
+ }
109
+ const result = yield* Effect.promise(async () => validatePayload(decodedEither.right, { storeId })).pipe(UnexpectedError.mapToUnexpectedError, Effect.either);
110
+ if (result._tag === 'Left') {
111
+ console.error('Invalid payload (validation failed)', result.left);
112
+ return new Response(result.left.toString(), { status: 400, headers });
113
+ }
114
+ }
115
+ else {
116
+ const result = yield* Effect.promise(async () => validatePayload(payload, { storeId })).pipe(UnexpectedError.mapToUnexpectedError, Effect.either);
117
+ if (result._tag === 'Left') {
118
+ console.error('Invalid payload (validation failed)', result.left);
119
+ return new Response(result.left.toString(), { status: 400, headers });
120
+ }
103
121
  }
104
122
  }
105
123
  const env = explicitlyProvidedEnv ?? importedEnv;
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"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"}
@@ -42,7 +42,12 @@ export declare const PullResponse: Schema.Struct<{
42
42
  backendId: Schema.SchemaClass<string, string, never>;
43
43
  }>;
44
44
  export declare const emptyPullResponse: (backendId: string) => {
45
+ readonly backendId: string;
45
46
  readonly batch: readonly {
47
+ readonly metadata: import("effect/Option").Option<{
48
+ readonly _tag: "SyncMessage.SyncMetadata";
49
+ readonly createdAt: string;
50
+ }>;
46
51
  readonly eventEncoded: {
47
52
  readonly name: string;
48
53
  readonly args: any;
@@ -51,10 +56,6 @@ export declare const emptyPullResponse: (backendId: string) => {
51
56
  readonly clientId: string;
52
57
  readonly sessionId: string;
53
58
  };
54
- readonly metadata: import("effect/Option").Option<{
55
- readonly _tag: "SyncMessage.SyncMetadata";
56
- readonly createdAt: string;
57
- }>;
58
59
  }[];
59
60
  readonly pageInfo: {
60
61
  readonly _tag: "MoreUnknown";
@@ -64,7 +65,6 @@ export declare const emptyPullResponse: (backendId: string) => {
64
65
  } | {
65
66
  readonly _tag: "NoMore";
66
67
  };
67
- readonly backendId: string;
68
68
  };
69
69
  export type PullResponse = typeof PullResponse.Type;
70
70
  export declare const PushRequest: Schema.Struct<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/sync-cf",
3
- "version": "0.4.0-dev.13",
3
+ "version": "0.4.0-dev.15",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -10,9 +10,9 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@cloudflare/workers-types": "4.20250923.0",
13
- "@livestore/common": "0.4.0-dev.13",
14
- "@livestore/common-cf": "0.4.0-dev.13",
15
- "@livestore/utils": "0.4.0-dev.13"
13
+ "@livestore/common-cf": "0.4.0-dev.15",
14
+ "@livestore/common": "0.4.0-dev.15",
15
+ "@livestore/utils": "0.4.0-dev.15"
16
16
  },
17
17
  "files": [
18
18
  "dist",
@@ -1,8 +1,7 @@
1
1
  import { env as importedEnv } from 'cloudflare:workers'
2
2
  import { UnexpectedError } from '@livestore/common'
3
3
  import type { HelperTypes } from '@livestore/common-cf'
4
- import type { Schema } from '@livestore/utils/effect'
5
- import { Effect } from '@livestore/utils/effect'
4
+ import { Effect, Schema } from '@livestore/utils/effect'
6
5
  import type { CfTypes, SearchParams } from '../common/mod.ts'
7
6
  import type { CfDeclare } from './mod.ts'
8
7
  import { type Env, matchSyncRequest } from './shared.ts'
@@ -23,7 +22,7 @@ export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjec
23
22
  * Options accepted by {@link makeWorker}. The Durable Object binding has to be
24
23
  * supplied explicitly so we never fall back to deprecated defaults when Cloudflare config changes.
25
24
  */
26
- export type MakeWorkerOptions<TEnv extends Env = Env> = {
25
+ export type MakeWorkerOptions<TEnv extends Env = Env, TSyncPayload = Schema.JsonValue> = {
27
26
  /**
28
27
  * Binding name of the sync Durable Object declared in wrangler config.
29
28
  */
@@ -33,7 +32,16 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
33
32
  * Note: This runs only at connection time, not for individual push events.
34
33
  * For push event validation, use the `onPush` callback in the durable object.
35
34
  */
36
- validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
35
+ /**
36
+ * Optionally pass a schema to decode the client-provided payload into a typed object
37
+ * before calling {@link validatePayload}. If omitted, the raw JSON value is forwarded.
38
+ */
39
+ syncPayloadSchema?: Schema.Schema<TSyncPayload>
40
+ /**
41
+ * Validates the (optionally decoded) payload during WebSocket connection establishment.
42
+ * If {@link syncPayloadSchema} is provided, `payload` will be of the schema’s inferred type.
43
+ */
44
+ validatePayload?: (payload: TSyncPayload, context: { storeId: string }) => void | Promise<void>
37
45
  /** @default false */
38
46
  enableCORS?: boolean
39
47
  }
@@ -48,8 +56,9 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
48
56
  export const makeWorker = <
49
57
  TEnv extends Env = Env,
50
58
  TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined,
59
+ TSyncPayload = Schema.JsonValue,
51
60
  >(
52
- options: MakeWorkerOptions<TEnv>,
61
+ options: MakeWorkerOptions<TEnv, TSyncPayload>,
53
62
  ): CFWorker<TEnv, TDurableObjectRpc> => {
54
63
  return {
55
64
  fetch: async (request, env, _ctx) => {
@@ -74,7 +83,7 @@ export const makeWorker = <
74
83
 
75
84
  // Check if this is a sync request first, before showing info message
76
85
  if (searchParams !== undefined) {
77
- return handleSyncRequest<TEnv, TDurableObjectRpc>({
86
+ return handleSyncRequest<TEnv, TDurableObjectRpc, unknown, TSyncPayload>({
78
87
  request,
79
88
  searchParams,
80
89
  env,
@@ -82,6 +91,7 @@ export const makeWorker = <
82
91
  syncBackendBinding: options.syncBackendBinding,
83
92
  headers: corsHeaders,
84
93
  validatePayload: options.validatePayload,
94
+ syncPayloadSchema: options.syncPayloadSchema,
85
95
  })
86
96
  }
87
97
 
@@ -147,6 +157,7 @@ export const handleSyncRequest = <
147
157
  TEnv extends Env = Env,
148
158
  TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined,
149
159
  CFHostMetada = unknown,
160
+ TSyncPayload = Schema.JsonValue,
150
161
  >({
151
162
  request,
152
163
  searchParams: { storeId, payload, transport },
@@ -154,6 +165,7 @@ export const handleSyncRequest = <
154
165
  syncBackendBinding,
155
166
  headers,
156
167
  validatePayload,
168
+ syncPayloadSchema,
157
169
  }: {
158
170
  request: CfTypes.Request<CFHostMetada>
159
171
  searchParams: SearchParams
@@ -161,20 +173,41 @@ export const handleSyncRequest = <
161
173
  /** Only there for type-level reasons */
162
174
  ctx: CfTypes.ExecutionContext
163
175
  /** Binding name of the sync backend Durable Object */
164
- syncBackendBinding: MakeWorkerOptions<TEnv>['syncBackendBinding']
176
+ syncBackendBinding: MakeWorkerOptions<TEnv, TSyncPayload>['syncBackendBinding']
165
177
  headers?: CfTypes.HeadersInit | undefined
166
- validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
178
+ validatePayload?: MakeWorkerOptions<TEnv, TSyncPayload>['validatePayload']
179
+ syncPayloadSchema?: MakeWorkerOptions<TEnv, TSyncPayload>['syncPayloadSchema']
167
180
  }): Promise<CfTypes.Response> =>
168
181
  Effect.gen(function* () {
169
182
  if (validatePayload !== undefined) {
170
- const result = yield* Effect.promise(async () => validatePayload!(payload, { storeId })).pipe(
171
- UnexpectedError.mapToUnexpectedError,
172
- Effect.either,
173
- )
174
-
175
- if (result._tag === 'Left') {
176
- console.error('Invalid payload', result.left)
177
- return new Response(result.left.toString(), { status: 400, headers })
183
+ // Always decode with the supplied schema when present, even if payload is undefined.
184
+ // This ensures required payloads are enforced by the schema.
185
+ if (syncPayloadSchema !== undefined) {
186
+ const decodedEither = Schema.decodeUnknownEither(syncPayloadSchema)(payload)
187
+ if (decodedEither._tag === 'Left') {
188
+ const message = decodedEither.left.toString()
189
+ console.error('Invalid payload (decode failed)', message)
190
+ return new Response(message, { status: 400, headers })
191
+ }
192
+
193
+ const result = yield* Effect.promise(async () =>
194
+ validatePayload(decodedEither.right as TSyncPayload, { storeId }),
195
+ ).pipe(UnexpectedError.mapToUnexpectedError, Effect.either)
196
+
197
+ if (result._tag === 'Left') {
198
+ console.error('Invalid payload (validation failed)', result.left)
199
+ return new Response(result.left.toString(), { status: 400, headers })
200
+ }
201
+ } else {
202
+ const result = yield* Effect.promise(async () => validatePayload(payload as TSyncPayload, { storeId })).pipe(
203
+ UnexpectedError.mapToUnexpectedError,
204
+ Effect.either,
205
+ )
206
+
207
+ if (result._tag === 'Left') {
208
+ console.error('Invalid payload (validation failed)', result.left)
209
+ return new Response(result.left.toString(), { status: 400, headers })
210
+ }
178
211
  }
179
212
  }
180
213