@livestore/sync-cf 0.4.0-dev.9 → 0.5.0-dev.0

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 (113) hide show
  1. package/README.md +7 -8
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cf-worker/do/durable-object.d.ts +1 -1
  4. package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
  5. package/dist/cf-worker/do/durable-object.js +36 -18
  6. package/dist/cf-worker/do/durable-object.js.map +1 -1
  7. package/dist/cf-worker/do/layer.d.ts +24 -20
  8. package/dist/cf-worker/do/layer.d.ts.map +1 -1
  9. package/dist/cf-worker/do/layer.js +57 -24
  10. package/dist/cf-worker/do/layer.js.map +1 -1
  11. package/dist/cf-worker/do/pull.d.ts +8 -3
  12. package/dist/cf-worker/do/pull.d.ts.map +1 -1
  13. package/dist/cf-worker/do/pull.js +26 -21
  14. package/dist/cf-worker/do/pull.js.map +1 -1
  15. package/dist/cf-worker/do/push.d.ts +20 -6
  16. package/dist/cf-worker/do/push.d.ts.map +1 -1
  17. package/dist/cf-worker/do/push.js +84 -56
  18. package/dist/cf-worker/do/push.js.map +1 -1
  19. package/dist/cf-worker/do/push.test.d.ts +2 -0
  20. package/dist/cf-worker/do/push.test.d.ts.map +1 -0
  21. package/dist/cf-worker/do/push.test.js +30 -0
  22. package/dist/cf-worker/do/push.test.js.map +1 -0
  23. package/dist/cf-worker/do/sqlite.d.ts +49 -52
  24. package/dist/cf-worker/do/sqlite.d.ts.map +1 -1
  25. package/dist/cf-worker/do/sqlite.js +14 -5
  26. package/dist/cf-worker/do/sqlite.js.map +1 -1
  27. package/dist/cf-worker/do/sync-storage.d.ts +15 -9
  28. package/dist/cf-worker/do/sync-storage.d.ts.map +1 -1
  29. package/dist/cf-worker/do/sync-storage.js +111 -25
  30. package/dist/cf-worker/do/sync-storage.js.map +1 -1
  31. package/dist/cf-worker/do/transport/do-rpc-server.d.ts +2 -2
  32. package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
  33. package/dist/cf-worker/do/transport/do-rpc-server.js +31 -23
  34. package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
  35. package/dist/cf-worker/do/transport/http-rpc-server.d.ts +5 -3
  36. package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
  37. package/dist/cf-worker/do/transport/http-rpc-server.js +28 -19
  38. package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
  39. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +3 -2
  40. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
  41. package/dist/cf-worker/do/transport/ws-rpc-server.js +32 -10
  42. package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
  43. package/dist/cf-worker/shared.d.ts +124 -34
  44. package/dist/cf-worker/shared.d.ts.map +1 -1
  45. package/dist/cf-worker/shared.js +52 -17
  46. package/dist/cf-worker/shared.js.map +1 -1
  47. package/dist/cf-worker/worker.d.ts +47 -39
  48. package/dist/cf-worker/worker.d.ts.map +1 -1
  49. package/dist/cf-worker/worker.js +54 -35
  50. package/dist/cf-worker/worker.js.map +1 -1
  51. package/dist/client/transport/do-rpc-client.d.ts +13 -4
  52. package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
  53. package/dist/client/transport/do-rpc-client.js +74 -28
  54. package/dist/client/transport/do-rpc-client.js.map +1 -1
  55. package/dist/client/transport/http-rpc-client.d.ts +3 -3
  56. package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
  57. package/dist/client/transport/http-rpc-client.js +51 -22
  58. package/dist/client/transport/http-rpc-client.js.map +1 -1
  59. package/dist/client/transport/ws-rpc-client.d.ts +4 -3
  60. package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
  61. package/dist/client/transport/ws-rpc-client.js +40 -22
  62. package/dist/client/transport/ws-rpc-client.js.map +1 -1
  63. package/dist/common/constants.d.ts +7 -0
  64. package/dist/common/constants.d.ts.map +1 -0
  65. package/dist/common/constants.js +17 -0
  66. package/dist/common/constants.js.map +1 -0
  67. package/dist/common/do-rpc-schema.d.ts +42 -43
  68. package/dist/common/do-rpc-schema.d.ts.map +1 -1
  69. package/dist/common/do-rpc-schema.js +5 -5
  70. package/dist/common/do-rpc-schema.js.map +1 -1
  71. package/dist/common/http-rpc-schema.d.ts +37 -38
  72. package/dist/common/http-rpc-schema.d.ts.map +1 -1
  73. package/dist/common/http-rpc-schema.js +7 -7
  74. package/dist/common/http-rpc-schema.js.map +1 -1
  75. package/dist/common/mod.d.ts +5 -3
  76. package/dist/common/mod.d.ts.map +1 -1
  77. package/dist/common/mod.js +4 -2
  78. package/dist/common/mod.js.map +1 -1
  79. package/dist/common/sync-message-types.d.ts +113 -130
  80. package/dist/common/sync-message-types.d.ts.map +1 -1
  81. package/dist/common/sync-message-types.js +29 -17
  82. package/dist/common/sync-message-types.js.map +1 -1
  83. package/dist/common/ws-rpc-schema.d.ts +34 -35
  84. package/dist/common/ws-rpc-schema.d.ts.map +1 -1
  85. package/dist/common/ws-rpc-schema.js +5 -5
  86. package/dist/common/ws-rpc-schema.js.map +1 -1
  87. package/package.json +57 -15
  88. package/src/cf-worker/do/durable-object.ts +59 -41
  89. package/src/cf-worker/do/layer.ts +153 -88
  90. package/src/cf-worker/do/pull.ts +48 -31
  91. package/src/cf-worker/do/push.test.ts +40 -0
  92. package/src/cf-worker/do/push.ts +163 -125
  93. package/src/cf-worker/do/sqlite.ts +15 -5
  94. package/src/cf-worker/do/sync-storage.ts +171 -38
  95. package/src/cf-worker/do/transport/do-rpc-server.ts +40 -40
  96. package/src/cf-worker/do/transport/http-rpc-server.ts +37 -20
  97. package/src/cf-worker/do/transport/ws-rpc-server.ts +44 -16
  98. package/src/cf-worker/shared.ts +152 -36
  99. package/src/cf-worker/worker.ts +110 -58
  100. package/src/client/transport/do-rpc-client.ts +106 -40
  101. package/src/client/transport/http-rpc-client.ts +74 -38
  102. package/src/client/transport/ws-rpc-client.ts +55 -29
  103. package/src/common/constants.ts +18 -0
  104. package/src/common/do-rpc-schema.ts +6 -5
  105. package/src/common/http-rpc-schema.ts +8 -7
  106. package/src/common/mod.ts +4 -3
  107. package/src/common/sync-message-types.ts +19 -19
  108. package/src/common/ws-rpc-schema.ts +6 -5
  109. package/dist/cf-worker/do/ws-chunking.d.ts +0 -22
  110. package/dist/cf-worker/do/ws-chunking.d.ts.map +0 -1
  111. package/dist/cf-worker/do/ws-chunking.js +0 -49
  112. package/dist/cf-worker/do/ws-chunking.js.map +0 -1
  113. package/src/cf-worker/do/ws-chunking.ts +0 -76
@@ -1,24 +1,79 @@
1
- import type { InvalidPullError, InvalidPushError } from '@livestore/common';
1
+ import type { UnknownError } from '@livestore/common';
2
2
  import type { CfTypes } from '@livestore/common-cf';
3
- import { Effect, Schema } from '@livestore/utils/effect';
3
+ import type { Effect } from '@livestore/utils/effect';
4
+ import { Schema } from '@livestore/utils/effect';
4
5
  import type { SearchParams } from '../common/mod.ts';
5
6
  import { SyncMessage } from '../common/mod.ts';
6
- export interface Env {
7
- /** Eventlog database */
8
- DB: CfTypes.D1Database;
9
- ADMIN_SECRET: string;
10
- }
7
+ export type Env = {};
8
+ /** Headers forwarded from the request to callbacks */
9
+ export type ForwardedHeaders = ReadonlyMap<string, string>;
10
+ /**
11
+ * Configuration for forwarding request headers to DO callbacks.
12
+ * - `string[]`: List of header names to forward (case-insensitive)
13
+ * - `(request) => Record<string, string>`: Custom extraction function (sync)
14
+ */
15
+ export type ForwardHeadersOption = readonly string[] | ((request: CfTypes.Request) => Record<string, string>);
16
+ /** Context passed to onPush/onPull callbacks */
17
+ export type CallbackContext = {
18
+ storeId: StoreId;
19
+ payload?: Schema.Json;
20
+ /** Headers forwarded from the request (only present if `forwardHeaders` is configured) */
21
+ headers?: ForwardedHeaders;
22
+ };
11
23
  export type MakeDurableObjectClassOptions = {
12
- onPush?: (message: SyncMessage.PushRequest, context: {
13
- storeId: StoreId;
14
- payload?: Schema.JsonValue;
15
- }) => Effect.SyncOrPromiseOrEffect<void>;
16
- onPushRes?: (message: SyncMessage.PushAck | InvalidPushError) => Effect.SyncOrPromiseOrEffect<void>;
17
- onPull?: (message: SyncMessage.PullRequest, context: {
18
- storeId: StoreId;
19
- payload?: Schema.JsonValue;
20
- }) => Effect.SyncOrPromiseOrEffect<void>;
21
- onPullRes?: (message: SyncMessage.PullResponse | InvalidPullError) => Effect.SyncOrPromiseOrEffect<void>;
24
+ onPush?: (message: SyncMessage.PushRequest, context: CallbackContext) => Effect.SyncOrPromiseOrEffect<void>;
25
+ onPushRes?: (message: SyncMessage.PushAck | UnknownError) => Effect.SyncOrPromiseOrEffect<void>;
26
+ onPull?: (message: SyncMessage.PullRequest, context: CallbackContext) => Effect.SyncOrPromiseOrEffect<void>;
27
+ onPullRes?: (message: SyncMessage.PullResponse | UnknownError) => Effect.SyncOrPromiseOrEffect<void>;
28
+ /**
29
+ * Forward request headers to `onPush`/`onPull` callbacks for authentication.
30
+ *
31
+ * This enables cookie-based or header-based authentication patterns where
32
+ * you need access to request headers inside the Durable Object.
33
+ *
34
+ * @example Forward specific headers by name (case-insensitive)
35
+ * ```ts
36
+ * makeDurableObject({
37
+ * forwardHeaders: ['cookie', 'authorization'],
38
+ * onPush: async (message, { headers }) => {
39
+ * const cookie = headers?.get('cookie')
40
+ * const session = await validateSession(cookie)
41
+ * },
42
+ * })
43
+ * ```
44
+ *
45
+ * @example Custom extraction function for derived values
46
+ * ```ts
47
+ * makeDurableObject({
48
+ * forwardHeaders: (request) => ({
49
+ * 'x-user-id': request.headers.get('x-user-id') ?? '',
50
+ * 'x-session': request.headers.get('cookie')?.split('session=')[1]?.split(';')[0] ?? '',
51
+ * }),
52
+ * onPush: async (message, { headers }) => {
53
+ * const userId = headers?.get('x-user-id')
54
+ * },
55
+ * })
56
+ * ```
57
+ */
58
+ forwardHeaders?: ForwardHeadersOption;
59
+ /**
60
+ * Storage engine for event persistence.
61
+ * - Default: `{ _tag: 'do-sqlite' }` (Durable Object SQLite)
62
+ * - D1: `{ _tag: 'd1', binding: string }` where `binding` is the D1 binding name in wrangler.toml.
63
+ *
64
+ * If omitted, the runtime defaults to DO SQLite. For backwards-compatibility, if an env binding named
65
+ * `DB` exists and looks like a D1Database, D1 will be used.
66
+ *
67
+ * Trade-offs:
68
+ * - DO SQLite: simpler deploy, data co-located with DO, not externally queryable
69
+ * - D1: centralized DB, inspectable with DB tools, extra network hop and JSON size limits
70
+ */
71
+ storage?: {
72
+ _tag: 'do-sqlite';
73
+ } | {
74
+ _tag: 'd1';
75
+ binding: string;
76
+ };
22
77
  /**
23
78
  * Enabled transports for sync backend
24
79
  * - `http`: HTTP JSON-RPC
@@ -28,6 +83,25 @@ export type MakeDurableObjectClassOptions = {
28
83
  * @default Set(['http', 'ws', 'do-rpc'])
29
84
  */
30
85
  enabledTransports?: Set<'http' | 'ws' | 'do-rpc'>;
86
+ /**
87
+ * Custom HTTP response headers for HTTP transport
88
+ * These headers will be added to all HTTP RPC responses (Pull, Push, Ping)
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * {
93
+ * http: {
94
+ * responseHeaders: {
95
+ * 'Access-Control-Allow-Origin': '*',
96
+ * 'Cache-Control': 'no-cache'
97
+ * }
98
+ * }
99
+ * }
100
+ * ```
101
+ */
102
+ http?: {
103
+ responseHeaders?: Record<string, string>;
104
+ };
31
105
  otel?: {
32
106
  baseUrl?: string;
33
107
  serviceName?: string;
@@ -36,18 +110,29 @@ export type MakeDurableObjectClassOptions = {
36
110
  export type StoreId = string;
37
111
  export type DurableObjectId = string;
38
112
  /**
39
- * Needs to be bumped when the storage format changes (e.g. eventlogTable schema changes)
113
+ * CRITICAL: Increment this version whenever you modify the database schema structure.
114
+ *
115
+ * Bump required when:
116
+ * - Adding/removing/renaming columns in eventlogTable or contextTable (see sqlite.ts)
117
+ * - Changing column types or constraints
118
+ * - Modifying primary keys or indexes
119
+ *
120
+ * Bump NOT required when:
121
+ * - Changing query patterns, pagination logic, or streaming behavior
122
+ * - Adding new tables (as long as existing table schemas remain unchanged)
123
+ * - Updating implementation details in sync-storage.ts
40
124
  *
41
- * Changing this version number will lead to a "soft reset".
125
+ * Impact: Changing this version triggers a "soft reset" - new table names are created
126
+ * and old data becomes inaccessible (but remains in storage).
42
127
  */
43
128
  export declare const PERSISTENCE_FORMAT_VERSION = 7;
44
- export declare const encodeOutgoingMessage: (a: {
129
+ export declare const encodeOutgoingMessage: (input: {
45
130
  readonly batch: readonly {
46
131
  readonly eventEncoded: {
47
132
  readonly name: string;
48
133
  readonly args: any;
49
- readonly seqNum: any;
50
- readonly parentSeqNum: any;
134
+ readonly seqNum: number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">;
135
+ readonly parentSeqNum: number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">;
51
136
  readonly clientId: string;
52
137
  readonly sessionId: string;
53
138
  };
@@ -74,8 +159,8 @@ export declare const encodeOutgoingMessage: (a: {
74
159
  readonly info: {
75
160
  readonly durableObjectId: string;
76
161
  };
77
- }, overrideOptions?: import("effect/SchemaAST").ParseOptions) => string;
78
- export declare const encodeIncomingMessage: (a: {
162
+ }, options?: import("effect/SchemaAST").ParseOptions) => string;
163
+ export declare const encodeIncomingMessage: (input: {
79
164
  readonly cursor: import("effect/Option").Option<{
80
165
  readonly backendId: string;
81
166
  readonly eventSequenceNumber: number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">;
@@ -84,8 +169,8 @@ export declare const encodeIncomingMessage: (a: {
84
169
  readonly batch: readonly {
85
170
  readonly name: string;
86
171
  readonly args: any;
87
- readonly seqNum: any;
88
- readonly parentSeqNum: any;
172
+ readonly seqNum: number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">;
173
+ readonly parentSeqNum: number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">;
89
174
  readonly clientId: string;
90
175
  readonly sessionId: string;
91
176
  }[];
@@ -98,18 +183,16 @@ export declare const encodeIncomingMessage: (a: {
98
183
  } | {
99
184
  readonly _tag: "SyncMessage.AdminInfoRequest";
100
185
  readonly adminSecret: string;
101
- }, overrideOptions?: import("effect/SchemaAST").ParseOptions) => string;
186
+ }, options?: import("effect/SchemaAST").ParseOptions) => string;
102
187
  /**
103
188
  * Extracts the LiveStore sync search parameters from a request. Returns
104
189
  * `undefined` when the request does not carry valid sync metadata so callers
105
190
  * can fall back to custom routing.
106
191
  */
107
192
  export declare const matchSyncRequest: (request: CfTypes.Request) => SearchParams | undefined;
108
- export declare const MAX_PULL_EVENTS_PER_MESSAGE = 100;
109
- export declare const MAX_WS_MESSAGE_BYTES = 900000;
110
193
  export type RpcSubscription = {
111
194
  storeId: StoreId;
112
- payload?: Schema.JsonValue;
195
+ payload?: Schema.Json;
113
196
  subscribedAt: number;
114
197
  /** Effect RPC request ID */
115
198
  requestId: string;
@@ -118,6 +201,8 @@ export type RpcSubscription = {
118
201
  durableObjectId: string;
119
202
  };
120
203
  };
204
+ /** Key prefix for DO-RPC subscriptions persisted in the DO's synchronous KV storage. */
205
+ export declare const rpcSubscriptionKeyPrefix = "rpc-sub:";
121
206
  /**
122
207
  * Durable Object interface supporting the DO RPC protocol for DO <> DO syncing.
123
208
  */
@@ -125,9 +210,14 @@ export interface SyncBackendRpcInterface {
125
210
  __DURABLE_OBJECT_BRAND: never;
126
211
  rpc(payload: Uint8Array): Promise<Uint8Array | CfTypes.ReadableStream>;
127
212
  }
128
- export declare const WebSocketAttachmentSchema: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
129
- storeId: typeof Schema.String;
130
- payload: Schema.optional<Schema.Schema<Schema.JsonValue, Schema.JsonValue, never>>;
131
- pullRequestIds: Schema.Array$<typeof Schema.String>;
213
+ export declare const WebSocketAttachmentSchema: Schema.fromJsonString<Schema.Struct<{
214
+ readonly storeId: Schema.String;
215
+ readonly payload: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
216
+ readonly pullRequestIds: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
217
+ readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
132
218
  }>>;
219
+ /** Helper to extract headers from a request based on the forwardHeaders option */
220
+ export declare const extractForwardedHeaders: (request: CfTypes.Request, forwardHeaders: ForwardHeadersOption | undefined) => Record<string, string> | undefined;
221
+ /** Convert a headers record to a ReadonlyMap */
222
+ export declare const headersRecordToMap: (headers: Record<string, string> | undefined) => ForwardedHeaders | undefined;
133
223
  //# sourceMappingURL=shared.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/cf-worker/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAa,MAAM,yBAAyB,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAsB,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAElE,MAAM,WAAW,GAAG;IAClB,wBAAwB;IACxB,EAAE,EAAE,OAAO,CAAC,UAAU,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,CAAC,EAAE,CACP,OAAO,EAAE,WAAW,CAAC,WAAW,EAChC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAA;KAAE,KACtD,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACvC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,gBAAgB,KAAK,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACnG,MAAM,CAAC,EAAE,CACP,OAAO,EAAE,WAAW,CAAC,WAAW,EAChC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAA;KAAE,KACtD,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACvC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,YAAY,GAAG,gBAAgB,KAAK,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAA;IAGxG;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC,CAAA;IAEjD,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACF,CAAA;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,CAAA;AAC5B,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uEAA0E,CAAA;AAC5G,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;uEAA0E,CAAA;AAE5G;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,CAAC,OAAO,KAAG,YAAY,GAAG,SAU1E,CAAA;AAED,eAAO,MAAM,2BAA2B,MAAM,CAAA;AAK9C,eAAO,MAAM,oBAAoB,SAAU,CAAA;AAG3C,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE;QACb,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;KACxB,CAAA;CACF,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sBAAsB,EAAE,KAAK,CAAA;IAC7B,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CACvE;AAED,eAAO,MAAM,yBAAyB;;;;GAQrC,CAAA"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/cf-worker/shared.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,25 @@
1
- import { Effect, Schema, UrlParams } from '@livestore/utils/effect';
1
+ import { Result, Schema } from '@livestore/utils/effect';
2
2
  import { SearchParamsSchema, SyncMessage } from "../common/mod.js";
3
3
  /**
4
- * Needs to be bumped when the storage format changes (e.g. eventlogTable schema changes)
4
+ * CRITICAL: Increment this version whenever you modify the database schema structure.
5
5
  *
6
- * Changing this version number will lead to a "soft reset".
6
+ * Bump required when:
7
+ * - Adding/removing/renaming columns in eventlogTable or contextTable (see sqlite.ts)
8
+ * - Changing column types or constraints
9
+ * - Modifying primary keys or indexes
10
+ *
11
+ * Bump NOT required when:
12
+ * - Changing query patterns, pagination logic, or streaming behavior
13
+ * - Adding new tables (as long as existing table schemas remain unchanged)
14
+ * - Updating implementation details in sync-storage.ts
15
+ *
16
+ * Impact: Changing this version triggers a "soft reset" - new table names are created
17
+ * and old data becomes inaccessible (but remains in storage).
7
18
  */
8
19
  export const PERSISTENCE_FORMAT_VERSION = 7;
9
- export const encodeOutgoingMessage = Schema.encodeSync(Schema.parseJson(SyncMessage.BackendToClientMessage));
10
- export const encodeIncomingMessage = Schema.encodeSync(Schema.parseJson(SyncMessage.ClientToBackendMessage));
20
+ export const encodeOutgoingMessage = Schema.encodeSync(Schema.fromJsonString(SyncMessage.BackendToClientMessage));
21
+ export const encodeIncomingMessage = Schema.encodeSync(Schema.fromJsonString(SyncMessage.ClientToBackendMessage));
22
+ const SearchParamsFromUrlSearchParams = Schema.fromURLSearchParams(SearchParamsSchema);
11
23
  /**
12
24
  * Extracts the LiveStore sync search parameters from a request. Returns
13
25
  * `undefined` when the request does not carry valid sync metadata so callers
@@ -15,23 +27,46 @@ export const encodeIncomingMessage = Schema.encodeSync(Schema.parseJson(SyncMess
15
27
  */
16
28
  export const matchSyncRequest = (request) => {
17
29
  const url = new URL(request.url);
18
- const urlParams = UrlParams.fromInput(url.searchParams);
19
- const paramsResult = UrlParams.schemaStruct(SearchParamsSchema)(urlParams).pipe(Effect.option, Effect.runSync);
20
- if (paramsResult._tag === 'None') {
30
+ const paramsResult = Schema.decodeUnknownResult(SearchParamsFromUrlSearchParams)(url.searchParams);
31
+ if (Result.isFailure(paramsResult) === true) {
21
32
  return undefined;
22
33
  }
23
- return paramsResult.value;
34
+ return paramsResult.success;
24
35
  };
25
- export const MAX_PULL_EVENTS_PER_MESSAGE = 100;
26
- // Cloudflare hibernated WebSocket frames begin failing just below 1MB. Keep our
27
- // payloads comfortably beneath that ceiling so we don't rely on implementation
28
- // quirks of local dev servers.
29
- export const MAX_WS_MESSAGE_BYTES = 900_000;
30
- export const WebSocketAttachmentSchema = Schema.parseJson(Schema.Struct({
36
+ /** Key prefix for DO-RPC subscriptions persisted in the DO's synchronous KV storage. */
37
+ export const rpcSubscriptionKeyPrefix = 'rpc-sub:';
38
+ export const WebSocketAttachmentSchema = Schema.fromJsonString(Schema.Struct({
31
39
  // Same across all websocket connections
32
40
  storeId: Schema.String,
33
41
  // Different for each websocket connection
34
- payload: Schema.optional(Schema.JsonValue),
35
- pullRequestIds: Schema.Array(Schema.String),
42
+ payload: Schema.optional(Schema.Json),
43
+ pullRequestIds: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
44
+ // Headers forwarded from the initial request (via forwardHeaders option)
45
+ headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
36
46
  }));
47
+ /** Helper to extract headers from a request based on the forwardHeaders option */
48
+ export const extractForwardedHeaders = (request, forwardHeaders) => {
49
+ if (forwardHeaders === undefined) {
50
+ return undefined;
51
+ }
52
+ if (typeof forwardHeaders === 'function') {
53
+ return forwardHeaders(request);
54
+ }
55
+ // Array of header names - extract them case-insensitively
56
+ const result = {};
57
+ for (const name of forwardHeaders) {
58
+ const value = request.headers.get(name);
59
+ if (value !== null) {
60
+ result[name.toLowerCase()] = value;
61
+ }
62
+ }
63
+ return Object.keys(result).length > 0 ? result : undefined;
64
+ };
65
+ /** Convert a headers record to a ReadonlyMap */
66
+ export const headersRecordToMap = (headers) => {
67
+ if (headers === undefined) {
68
+ return undefined;
69
+ }
70
+ return new Map(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
71
+ };
37
72
  //# sourceMappingURL=shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/cf-worker/shared.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAEnE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAwClE;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAA;AAE3C,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAC5G,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAE5G;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAwB,EAA4B,EAAE;IACrF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IACvD,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,CAAC,kBAAkB,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IAE9G,IAAI,YAAY,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACjC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO,YAAY,CAAC,KAAK,CAAA;AAC3B,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAA;AAE9C,gFAAgF;AAChF,+EAA+E;AAC/E,+BAA+B;AAC/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAA;AAuB3C,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC,SAAS,CACvD,MAAM,CAAC,MAAM,CAAC;IACZ,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;IAC1C,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;CAC5C,CAAC,CACH,CAAA"}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/cf-worker/shared.ts"],"names":[],"mappings":"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"}
@@ -1,27 +1,49 @@
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
- import { type Env } from './shared.ts';
4
+ import { type Env, type ForwardedHeaders } from './shared.ts';
5
5
  export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
6
6
  fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada>, env: TEnv, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>;
7
7
  };
8
+ /** Context passed to validatePayload callback */
9
+ export type ValidatePayloadContext = {
10
+ storeId: string;
11
+ /** Request headers (raw, not filtered by forwardHeaders) */
12
+ headers: ForwardedHeaders;
13
+ };
8
14
  /**
9
15
  * Options accepted by {@link makeWorker}. The Durable Object binding has to be
10
16
  * supplied explicitly so we never fall back to deprecated defaults when Cloudflare config changes.
11
17
  */
12
- export type MakeWorkerOptions<TEnv extends Env = Env> = {
18
+ export type MakeWorkerOptions<TEnv extends Env = Env, TSyncPayload = Schema.Json> = {
13
19
  /**
14
20
  * Binding name of the sync Durable Object declared in wrangler config.
15
21
  */
16
22
  syncBackendBinding: HelperTypes.ExtractDurableObjectKeys<TEnv>;
17
23
  /**
18
- * Validates the payload during WebSocket connection establishment.
24
+ * Optionally pass a schema to decode the client-provided payload into a typed object
25
+ * before calling {@link validatePayload}. If omitted, the raw JSON value is forwarded.
26
+ */
27
+ syncPayloadSchema?: Schema.Decoder<TSyncPayload>;
28
+ /**
29
+ * Validates the (optionally decoded) payload during WebSocket connection establishment.
30
+ * If {@link syncPayloadSchema} is provided, `payload` will be of the schema's inferred type.
31
+ *
32
+ * The context includes request headers for cookie-based or header-based authentication.
33
+ *
34
+ * @example Cookie-based authentication
35
+ * ```ts
36
+ * validatePayload: async (payload, { storeId, headers }) => {
37
+ * const cookie = headers.get('cookie')
38
+ * const session = await validateSessionFromCookie(cookie)
39
+ * if (!session) throw new Error('Unauthorized')
40
+ * }
41
+ * ```
42
+ *
19
43
  * Note: This runs only at connection time, not for individual push events.
20
- * For push event validation, use the `onPush` callback in the durable object.
44
+ * For push event validation, use the `onPush` callback in the Durable Object.
21
45
  */
22
- validatePayload?: (payload: Schema.JsonValue | undefined, context: {
23
- storeId: string;
24
- }) => void | Promise<void>;
46
+ validatePayload?: (payload: TSyncPayload, context: ValidatePayloadContext) => void | Promise<void>;
25
47
  /** @default false */
26
48
  enableCORS?: boolean;
27
49
  };
@@ -32,54 +54,40 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
32
54
  * For more complex setups prefer implementing a custom `fetch` and call {@link handleSyncRequest}
33
55
  * from the branch that handles LiveStore sync requests.
34
56
  */
35
- export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined>(options: MakeWorkerOptions<TEnv>) => CFWorker<TEnv, TDurableObjectRpc>;
57
+ export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, TSyncPayload = Schema.Json>(options: MakeWorkerOptions<TEnv, TSyncPayload>) => CFWorker<TEnv, TDurableObjectRpc>;
36
58
  /**
37
- * Handles `/sync` endpoint.
59
+ * Handles LiveStore sync requests (e.g. with search params `?storeId=...&transport=...`).
38
60
  *
39
- * @example
61
+ * @example Token-based authentication
40
62
  * ```ts
41
- * const validatePayload = (payload: Schema.JsonValue | undefined, context: { storeId: string }) => {
42
- * console.log(`Validating connection for store: ${context.storeId}`)
63
+ * const validatePayload = (payload: Schema.Json | undefined, context: { storeId: string }) => {
43
64
  * if (payload?.authToken !== 'insecure-token-change-me') {
44
65
  * throw new Error('Invalid auth token')
45
66
  * }
46
67
  * }
68
+ * ```
47
69
  *
48
- * export default {
49
- * fetch: async (request, env, ctx) => {
50
- * const searchParams = matchSyncRequest(request)
51
- *
52
- * // Is LiveStore sync request
53
- * if (searchParams !== undefined) {
54
- * return handleSyncRequest({
55
- * request,
56
- * searchParams,
57
- * env,
58
- * ctx,
59
- * syncBackendBinding: 'SYNC_BACKEND_DO',
60
- * headers: {},
61
- * validatePayload,
62
- * })
63
- * }
64
- *
65
- * return new Response('Invalid path', { status: 400 })
66
- * }
70
+ * @example Cookie-based authentication
71
+ * ```ts
72
+ * const validatePayload = async (payload: Schema.Json | undefined, { storeId, headers }) => {
73
+ * const cookie = headers.get('cookie')
74
+ * const session = await validateSessionFromCookie(cookie)
75
+ * if (!session) throw new Error('Unauthorized')
67
76
  * }
68
77
  * ```
69
78
  *
70
- * @throws {UnexpectedError} If the payload is invalid
79
+ * @throws {UnknownError} 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, syncBackendBinding, headers, validatePayload, }: {
81
+ export declare const handleSyncRequest: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown, TSyncPayload = Schema.Json>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }: {
73
82
  request: CfTypes.Request<CFHostMetada>;
74
83
  searchParams: SearchParams;
75
- env: TEnv;
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":"AACA,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,gHAOC;IACD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IACtC,YAAY,EAAE,YAAY,CAAA;IAC1B,GAAG,EAAE,IAAI,CAAA;IACT,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,CAyC0B,CAAA"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
- import { UnexpectedError } from '@livestore/common';
2
- import { Effect } from '@livestore/utils/effect';
1
+ import { env as importedEnv } from 'cloudflare:workers';
2
+ import { UnknownError } from '@livestore/common';
3
+ import { CfDeclare } from '@livestore/common-cf/declare';
4
+ import { Effect, Result, Schema } from '@livestore/utils/effect';
3
5
  import { matchSyncRequest } from "./shared.js";
6
+ const Response = CfDeclare.Response;
4
7
  /**
5
8
  * Produces a Cloudflare Worker `fetch` handler that delegates sync traffic to the
6
9
  * Durable Object identified by `syncBackendBinding`.
@@ -12,14 +15,14 @@ export const makeWorker = (options) => {
12
15
  return {
13
16
  fetch: async (request, env, _ctx) => {
14
17
  const url = new URL(request.url);
15
- const corsHeaders = options.enableCORS
18
+ const corsHeaders = options.enableCORS === true
16
19
  ? {
17
20
  'Access-Control-Allow-Origin': '*',
18
21
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
19
22
  'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers') ?? '*',
20
23
  }
21
24
  : {};
22
- if (request.method === 'OPTIONS' && options.enableCORS) {
25
+ if (request.method === 'OPTIONS' && options.enableCORS === true) {
23
26
  return new Response(null, {
24
27
  status: 204,
25
28
  headers: corsHeaders,
@@ -36,6 +39,7 @@ export const makeWorker = (options) => {
36
39
  syncBackendBinding: options.syncBackendBinding,
37
40
  headers: corsHeaders,
38
41
  validatePayload: options.validatePayload,
42
+ syncPayloadSchema: options.syncPayloadSchema,
39
43
  });
40
44
  }
41
45
  // Only show info message for GET requests to / without sync parameters
@@ -57,54 +61,69 @@ export const makeWorker = (options) => {
57
61
  },
58
62
  };
59
63
  };
64
+ /** Convert CF Request headers to a ForwardedHeaders map */
65
+ const requestHeadersToMap = (request) => {
66
+ const result = new Map();
67
+ request.headers.forEach((value, key) => {
68
+ result.set(key.toLowerCase(), value);
69
+ });
70
+ return result;
71
+ };
60
72
  /**
61
- * Handles `/sync` endpoint.
73
+ * Handles LiveStore sync requests (e.g. with search params `?storeId=...&transport=...`).
62
74
  *
63
- * @example
75
+ * @example Token-based authentication
64
76
  * ```ts
65
- * const validatePayload = (payload: Schema.JsonValue | undefined, context: { storeId: string }) => {
66
- * console.log(`Validating connection for store: ${context.storeId}`)
77
+ * const validatePayload = (payload: Schema.Json | undefined, context: { storeId: string }) => {
67
78
  * if (payload?.authToken !== 'insecure-token-change-me') {
68
79
  * throw new Error('Invalid auth token')
69
80
  * }
70
81
  * }
82
+ * ```
71
83
  *
72
- * export default {
73
- * fetch: async (request, env, ctx) => {
74
- * const searchParams = matchSyncRequest(request)
75
- *
76
- * // Is LiveStore sync request
77
- * if (searchParams !== undefined) {
78
- * return handleSyncRequest({
79
- * request,
80
- * searchParams,
81
- * env,
82
- * ctx,
83
- * syncBackendBinding: 'SYNC_BACKEND_DO',
84
- * headers: {},
85
- * validatePayload,
86
- * })
87
- * }
88
- *
89
- * return new Response('Invalid path', { status: 400 })
90
- * }
84
+ * @example Cookie-based authentication
85
+ * ```ts
86
+ * const validatePayload = async (payload: Schema.Json | undefined, { storeId, headers }) => {
87
+ * const cookie = headers.get('cookie')
88
+ * const session = await validateSessionFromCookie(cookie)
89
+ * if (!session) throw new Error('Unauthorized')
91
90
  * }
92
91
  * ```
93
92
  *
94
- * @throws {UnexpectedError} If the payload is invalid
93
+ * @throws {UnknownError} If the payload is invalid
95
94
  */
96
- export const handleSyncRequest = ({ request, searchParams: { storeId, payload, transport }, env, syncBackendBinding, headers, validatePayload, }) => Effect.gen(function* () {
95
+ export const handleSyncRequest = ({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }) => Effect.gen(function* () {
97
96
  if (validatePayload !== undefined) {
98
- const result = yield* Effect.promise(async () => validatePayload(payload, { storeId })).pipe(UnexpectedError.mapToUnexpectedError, Effect.either);
99
- if (result._tag === 'Left') {
100
- console.error('Invalid payload', result.left);
101
- return new Response(result.left.toString(), { status: 400, headers });
97
+ // Convert request headers to a Map for the validation context
98
+ const requestHeaders = requestHeadersToMap(request);
99
+ // Always decode with the supplied schema when present, even if payload is undefined.
100
+ // This ensures required payloads are enforced by the schema.
101
+ if (syncPayloadSchema !== undefined) {
102
+ const decodedResult = Schema.decodeUnknownResult(syncPayloadSchema)(payload);
103
+ if (Result.isFailure(decodedResult) === true) {
104
+ const message = decodedResult.failure.toString();
105
+ console.error('Invalid payload (decode failed)', message);
106
+ return new Response(message, { status: 400, ...(headers !== undefined ? { headers } : {}) });
107
+ }
108
+ const result = yield* Effect.promise(async () => validatePayload(decodedResult.success, { storeId, headers: requestHeaders })).pipe(UnknownError.mapToUnknownError, Effect.result);
109
+ if (Result.isFailure(result) === true) {
110
+ console.error('Invalid payload (validation failed)', result.failure);
111
+ return new Response(result.failure.toString(), { status: 400, ...(headers !== undefined ? { headers } : {}) });
112
+ }
113
+ }
114
+ else {
115
+ const result = yield* Effect.promise(async () => validatePayload(payload, { storeId, headers: requestHeaders })).pipe(UnknownError.mapToUnknownError, Effect.result);
116
+ if (Result.isFailure(result) === true) {
117
+ console.error('Invalid payload (validation failed)', result.failure);
118
+ return new Response(result.failure.toString(), { status: 400, ...(headers !== undefined ? { headers } : {}) });
119
+ }
102
120
  }
103
121
  }
122
+ const env = explicitlyProvidedEnv ?? importedEnv;
104
123
  if (!(syncBackendBinding in env)) {
105
124
  return new Response(`Failed dependency: Required Durable Object binding '${syncBackendBinding}' not available`, {
106
125
  status: 424,
107
- headers,
126
+ ...(headers !== undefined ? { headers } : {}),
108
127
  });
109
128
  }
110
129
  const durableObjectNamespace = env[syncBackendBinding];
@@ -115,7 +134,7 @@ export const handleSyncRequest = ({ request, searchParams: { storeId, payload, t
115
134
  if (transport === 'ws' && (upgradeHeader === null || upgradeHeader !== 'websocket')) {
116
135
  return new Response('Durable Object expected Upgrade: websocket', {
117
136
  status: 426,
118
- headers,
137
+ ...(headers !== undefined ? { headers } : {}),
119
138
  });
120
139
  }
121
140
  return yield* Effect.promise(() => durableObject.fetch(request));