@livestore/sync-cf 0.4.0-dev.8 → 0.4.0-dev.9

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 (37) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
  3. package/dist/cf-worker/do/durable-object.js +5 -9
  4. package/dist/cf-worker/do/durable-object.js.map +1 -1
  5. package/dist/cf-worker/do/layer.d.ts +1 -1
  6. package/dist/cf-worker/do/pull.d.ts +1 -1
  7. package/dist/cf-worker/do/pull.d.ts.map +1 -1
  8. package/dist/cf-worker/do/pull.js +9 -3
  9. package/dist/cf-worker/do/pull.js.map +1 -1
  10. package/dist/cf-worker/do/push.d.ts.map +1 -1
  11. package/dist/cf-worker/do/push.js +65 -34
  12. package/dist/cf-worker/do/push.js.map +1 -1
  13. package/dist/cf-worker/do/transport/do-rpc-server.d.ts +2 -1
  14. package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
  15. package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
  16. package/dist/cf-worker/do/transport/http-rpc-server.d.ts +1 -1
  17. package/dist/cf-worker/do/ws-chunking.d.ts +22 -0
  18. package/dist/cf-worker/do/ws-chunking.d.ts.map +1 -0
  19. package/dist/cf-worker/do/ws-chunking.js +49 -0
  20. package/dist/cf-worker/do/ws-chunking.js.map +1 -0
  21. package/dist/cf-worker/shared.d.ts +19 -13
  22. package/dist/cf-worker/shared.d.ts.map +1 -1
  23. package/dist/cf-worker/shared.js +15 -4
  24. package/dist/cf-worker/shared.js.map +1 -1
  25. package/dist/cf-worker/worker.d.ts +30 -45
  26. package/dist/cf-worker/worker.d.ts.map +1 -1
  27. package/dist/cf-worker/worker.js +30 -25
  28. package/dist/cf-worker/worker.js.map +1 -1
  29. package/dist/common/sync-message-types.d.ts +5 -5
  30. package/package.json +5 -5
  31. package/src/cf-worker/do/durable-object.ts +6 -10
  32. package/src/cf-worker/do/pull.ts +15 -3
  33. package/src/cf-worker/do/push.ts +84 -38
  34. package/src/cf-worker/do/transport/do-rpc-server.ts +4 -2
  35. package/src/cf-worker/do/ws-chunking.ts +76 -0
  36. package/src/cf-worker/shared.ts +19 -6
  37. package/src/cf-worker/worker.ts +46 -69
@@ -1,6 +1,7 @@
1
1
  import type { InvalidPullError, InvalidPushError } from '@livestore/common'
2
2
  import type { CfTypes } from '@livestore/common-cf'
3
- import { Effect, type Option, Schema, UrlParams } from '@livestore/utils/effect'
3
+ import { Effect, Schema, UrlParams } from '@livestore/utils/effect'
4
+ import type { SearchParams } from '../common/mod.ts'
4
5
  import { SearchParamsSchema, SyncMessage } from '../common/mod.ts'
5
6
 
6
7
  export interface Env {
@@ -48,20 +49,32 @@ export type DurableObjectId = string
48
49
  */
49
50
  export const PERSISTENCE_FORMAT_VERSION = 7
50
51
 
51
- export const DEFAULT_SYNC_DURABLE_OBJECT_NAME = 'SYNC_BACKEND_DO'
52
-
53
52
  export const encodeOutgoingMessage = Schema.encodeSync(Schema.parseJson(SyncMessage.BackendToClientMessage))
54
53
  export const encodeIncomingMessage = Schema.encodeSync(Schema.parseJson(SyncMessage.ClientToBackendMessage))
55
54
 
56
- export const getSyncRequestSearchParams = (request: CfTypes.Request): Option.Option<typeof SearchParamsSchema.Type> => {
55
+ /**
56
+ * Extracts the LiveStore sync search parameters from a request. Returns
57
+ * `undefined` when the request does not carry valid sync metadata so callers
58
+ * can fall back to custom routing.
59
+ */
60
+ export const matchSyncRequest = (request: CfTypes.Request): SearchParams | undefined => {
57
61
  const url = new URL(request.url)
58
62
  const urlParams = UrlParams.fromInput(url.searchParams)
59
63
  const paramsResult = UrlParams.schemaStruct(SearchParamsSchema)(urlParams).pipe(Effect.option, Effect.runSync)
60
64
 
61
- return paramsResult
65
+ if (paramsResult._tag === 'None') {
66
+ return undefined
67
+ }
68
+
69
+ return paramsResult.value
62
70
  }
63
71
 
64
- export const PULL_CHUNK_SIZE = 100
72
+ export const MAX_PULL_EVENTS_PER_MESSAGE = 100
73
+
74
+ // Cloudflare hibernated WebSocket frames begin failing just below 1MB. Keep our
75
+ // payloads comfortably beneath that ceiling so we don't rely on implementation
76
+ // quirks of local dev servers.
77
+ export const MAX_WS_MESSAGE_BYTES = 900_000
65
78
 
66
79
  // RPC subscription storage (TODO refactor)
67
80
  export type RpcSubscription = {
@@ -1,42 +1,14 @@
1
1
  import { UnexpectedError } from '@livestore/common'
2
+ import type { HelperTypes } from '@livestore/common-cf'
2
3
  import type { Schema } from '@livestore/utils/effect'
3
4
  import { Effect } from '@livestore/utils/effect'
4
5
  import type { CfTypes, SearchParams } from '../common/mod.ts'
5
6
  import type { CfDeclare } from './mod.ts'
6
- import { DEFAULT_SYNC_DURABLE_OBJECT_NAME, type Env, getSyncRequestSearchParams } from './shared.ts'
7
+ import { type Env, matchSyncRequest } from './shared.ts'
7
8
 
8
9
  // NOTE We need to redeclare runtime types here to avoid type conflicts with the lib.dom Response type.
9
10
  declare class Response extends CfDeclare.Response {}
10
11
 
11
- export namespace HelperTypes {
12
- type AnyDON = CfTypes.DurableObjectNamespace<undefined>
13
-
14
- type DOKeys<T> = {
15
- [K in keyof T]-?: T[K] extends AnyDON ? K : never
16
- }[keyof T]
17
-
18
- type NonBuiltins<T> = Omit<T, keyof Env>
19
-
20
- /**
21
- * Helper type to extract DurableObject keys from Env to give consumer type safety.
22
- *
23
- * @example
24
- * ```ts
25
- * type PlatformEnv = {
26
- * DB: D1Database
27
- * ADMIN_TOKEN: string
28
- * SYNC_BACKEND_DO: DurableObjectNamespace<SyncBackendDO>
29
- * }
30
- * export default makeWorker<PlatformEnv>({
31
- * durableObject: { name: "SYNC_BACKEND_DO" },
32
- * // ^ (property) name?: "SYNC_BACKEND_DO" | undefined
33
- * });
34
- */
35
- export type ExtractDurableObjectKeys<TEnv = Env> = DOKeys<NonBuiltins<TEnv>> extends never
36
- ? string
37
- : DOKeys<NonBuiltins<TEnv>>
38
- }
39
-
40
12
  // HINT: If we ever extend user's custom worker RPC, type T can help here with expected return type safety. Currently unused.
41
13
  export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
42
14
  fetch: <CFHostMetada = unknown>(
@@ -46,7 +18,15 @@ export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjec
46
18
  ) => Promise<CfTypes.Response>
47
19
  }
48
20
 
21
+ /**
22
+ * Options accepted by {@link makeWorker}. The Durable Object binding has to be
23
+ * supplied explicitly so we never fall back to deprecated defaults when Cloudflare config changes.
24
+ */
49
25
  export type MakeWorkerOptions<TEnv extends Env = Env> = {
26
+ /**
27
+ * Binding name of the sync Durable Object declared in wrangler config.
28
+ */
29
+ syncBackendBinding: HelperTypes.ExtractDurableObjectKeys<TEnv>
50
30
  /**
51
31
  * Validates the payload during WebSocket connection establishment.
52
32
  * Note: This runs only at connection time, not for individual push events.
@@ -55,21 +35,20 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
55
35
  validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
56
36
  /** @default false */
57
37
  enableCORS?: boolean
58
- durableObject?: {
59
- /**
60
- * Needs to match the binding name from the wrangler config
61
- *
62
- * @default 'SYNC_BACKEND_DO'
63
- */
64
- name?: HelperTypes.ExtractDurableObjectKeys<TEnv>
65
- }
66
38
  }
67
39
 
40
+ /**
41
+ * Produces a Cloudflare Worker `fetch` handler that delegates sync traffic to the
42
+ * Durable Object identified by `syncBackendBinding`.
43
+ *
44
+ * For more complex setups prefer implementing a custom `fetch` and call {@link handleSyncRequest}
45
+ * from the branch that handles LiveStore sync requests.
46
+ */
68
47
  export const makeWorker = <
69
48
  TEnv extends Env = Env,
70
49
  TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined,
71
50
  >(
72
- options: MakeWorkerOptions<TEnv> = {},
51
+ options: MakeWorkerOptions<TEnv>,
73
52
  ): CFWorker<TEnv, TDurableObjectRpc> => {
74
53
  return {
75
54
  fetch: async (request, env, _ctx) => {
@@ -90,20 +69,18 @@ export const makeWorker = <
90
69
  })
91
70
  }
92
71
 
93
- const requestParamsResult = getSyncRequestSearchParams(request)
72
+ const searchParams = matchSyncRequest(request)
94
73
 
95
74
  // Check if this is a sync request first, before showing info message
96
- if (requestParamsResult._tag === 'Some') {
75
+ if (searchParams !== undefined) {
97
76
  return handleSyncRequest<TEnv, TDurableObjectRpc>({
98
77
  request,
99
- searchParams: requestParamsResult.value,
78
+ searchParams,
100
79
  env,
101
80
  ctx: _ctx,
102
- options: {
103
- headers: corsHeaders,
104
- validatePayload: options.validatePayload,
105
- durableObject: options.durableObject,
106
- },
81
+ syncBackendBinding: options.syncBackendBinding,
82
+ headers: corsHeaders,
83
+ validatePayload: options.validatePayload,
107
84
  })
108
85
  }
109
86
 
@@ -143,16 +120,18 @@ export const makeWorker = <
143
120
  *
144
121
  * export default {
145
122
  * fetch: async (request, env, ctx) => {
146
- * const requestParamsResult = getSyncRequestSearchParams(request)
123
+ * const searchParams = matchSyncRequest(request)
147
124
  *
148
125
  * // Is LiveStore sync request
149
- * if (requestParamsResult._tag === 'Some') {
126
+ * if (searchParams !== undefined) {
150
127
  * return handleSyncRequest({
151
128
  * request,
152
- * searchParams: requestParamsResult.value,
129
+ * searchParams,
153
130
  * env,
154
131
  * ctx,
155
- * options: { headers: {}, validatePayload }
132
+ * syncBackendBinding: 'SYNC_BACKEND_DO',
133
+ * headers: {},
134
+ * validatePayload,
156
135
  * })
157
136
  * }
158
137
  *
@@ -169,49 +148,47 @@ export const handleSyncRequest = <
169
148
  CFHostMetada = unknown,
170
149
  >({
171
150
  request,
172
- searchParams,
151
+ searchParams: { storeId, payload, transport },
173
152
  env,
174
- options = {},
153
+ syncBackendBinding,
154
+ headers,
155
+ validatePayload,
175
156
  }: {
176
157
  request: CfTypes.Request<CFHostMetada>
177
158
  searchParams: SearchParams
178
159
  env: TEnv
179
160
  /** Only there for type-level reasons */
180
161
  ctx: CfTypes.ExecutionContext
181
- options?: {
182
- headers?: CfTypes.HeadersInit
183
- durableObject?: MakeWorkerOptions<TEnv>['durableObject']
184
- validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
185
- }
162
+ /** Binding name of the sync backend Durable Object */
163
+ syncBackendBinding: MakeWorkerOptions<TEnv>['syncBackendBinding']
164
+ headers?: CfTypes.HeadersInit | undefined
165
+ validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
186
166
  }): Promise<CfTypes.Response> =>
187
167
  Effect.gen(function* () {
188
- const { storeId, payload, transport } = searchParams
189
-
190
- if (options.validatePayload !== undefined) {
191
- const result = yield* Effect.promise(async () => options.validatePayload!(payload, { storeId })).pipe(
168
+ if (validatePayload !== undefined) {
169
+ const result = yield* Effect.promise(async () => validatePayload!(payload, { storeId })).pipe(
192
170
  UnexpectedError.mapToUnexpectedError,
193
171
  Effect.either,
194
172
  )
195
173
 
196
174
  if (result._tag === 'Left') {
197
175
  console.error('Invalid payload', result.left)
198
- return new Response(result.left.toString(), { status: 400, headers: options.headers })
176
+ return new Response(result.left.toString(), { status: 400, headers })
199
177
  }
200
178
  }
201
179
 
202
- const durableObjectName = options.durableObject?.name ?? DEFAULT_SYNC_DURABLE_OBJECT_NAME
203
- if (!(durableObjectName in env)) {
180
+ if (!(syncBackendBinding in env)) {
204
181
  return new Response(
205
- `Failed dependency: Required Durable Object binding '${durableObjectName as string}' not available`,
182
+ `Failed dependency: Required Durable Object binding '${syncBackendBinding as string}' not available`,
206
183
  {
207
184
  status: 424,
208
- headers: options.headers,
185
+ headers,
209
186
  },
210
187
  )
211
188
  }
212
189
 
213
190
  const durableObjectNamespace = env[
214
- durableObjectName as keyof TEnv
191
+ syncBackendBinding as keyof TEnv
215
192
  ] as CfTypes.DurableObjectNamespace<TDurableObjectRpc>
216
193
 
217
194
  const id = durableObjectNamespace.idFromName(storeId)
@@ -222,7 +199,7 @@ export const handleSyncRequest = <
222
199
  if (transport === 'ws' && (upgradeHeader === null || upgradeHeader !== 'websocket')) {
223
200
  return new Response('Durable Object expected Upgrade: websocket', {
224
201
  status: 426,
225
- headers: options?.headers,
202
+ headers,
226
203
  })
227
204
  }
228
205