@livestore/sync-cf 0.4.0-dev.21 → 0.4.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/do/durable-object.js +12 -8
- package/dist/cf-worker/do/durable-object.js.map +1 -1
- package/dist/cf-worker/do/layer.d.ts +1 -1
- package/dist/cf-worker/do/layer.d.ts.map +1 -1
- package/dist/cf-worker/do/layer.js +2 -2
- package/dist/cf-worker/do/layer.js.map +1 -1
- package/dist/cf-worker/do/pull.d.ts +7 -2
- package/dist/cf-worker/do/pull.d.ts.map +1 -1
- package/dist/cf-worker/do/pull.js +12 -6
- package/dist/cf-worker/do/pull.js.map +1 -1
- package/dist/cf-worker/do/push.d.ts +5 -4
- package/dist/cf-worker/do/push.d.ts.map +1 -1
- package/dist/cf-worker/do/push.js +18 -11
- package/dist/cf-worker/do/push.js.map +1 -1
- package/dist/cf-worker/do/sqlite.d.ts.map +1 -1
- package/dist/cf-worker/do/sqlite.js.map +1 -1
- package/dist/cf-worker/do/sync-storage.d.ts +1 -1
- package/dist/cf-worker/do/sync-storage.d.ts.map +1 -1
- package/dist/cf-worker/do/sync-storage.js +2 -1
- package/dist/cf-worker/do/sync-storage.js.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.js +13 -7
- package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts +2 -1
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.js +17 -14
- package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +2 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.js +30 -8
- package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
- package/dist/cf-worker/shared.d.ts +60 -15
- package/dist/cf-worker/shared.d.ts.map +1 -1
- package/dist/cf-worker/shared.js +27 -0
- package/dist/cf-worker/shared.js.map +1 -1
- package/dist/cf-worker/worker.d.ts +31 -31
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +27 -30
- package/dist/cf-worker/worker.js.map +1 -1
- package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/do-rpc-client.js +11 -7
- package/dist/client/transport/do-rpc-client.js.map +1 -1
- package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/http-rpc-client.js +10 -6
- package/dist/client/transport/http-rpc-client.js.map +1 -1
- package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/ws-rpc-client.js +11 -11
- package/dist/client/transport/ws-rpc-client.js.map +1 -1
- package/dist/common/do-rpc-schema.d.ts +3 -3
- package/dist/common/do-rpc-schema.d.ts.map +1 -1
- package/dist/common/do-rpc-schema.js +3 -3
- package/dist/common/do-rpc-schema.js.map +1 -1
- package/dist/common/http-rpc-schema.d.ts +3 -3
- package/dist/common/http-rpc-schema.d.ts.map +1 -1
- package/dist/common/http-rpc-schema.js +3 -3
- package/dist/common/http-rpc-schema.js.map +1 -1
- package/dist/common/sync-message-types.d.ts +2 -2
- package/dist/common/ws-rpc-schema.d.ts +3 -3
- package/dist/common/ws-rpc-schema.d.ts.map +1 -1
- package/dist/common/ws-rpc-schema.js +3 -3
- package/dist/common/ws-rpc-schema.js.map +1 -1
- package/package.json +71 -13
- package/src/cf-worker/do/durable-object.ts +18 -10
- package/src/cf-worker/do/layer.ts +4 -3
- package/src/cf-worker/do/pull.ts +28 -9
- package/src/cf-worker/do/push.ts +29 -10
- package/src/cf-worker/do/sqlite.ts +1 -0
- package/src/cf-worker/do/sync-storage.ts +4 -2
- package/src/cf-worker/do/transport/do-rpc-server.ts +18 -7
- package/src/cf-worker/do/transport/http-rpc-server.ts +27 -21
- package/src/cf-worker/do/transport/ws-rpc-server.ts +40 -12
- package/src/cf-worker/shared.ts +89 -11
- package/src/cf-worker/worker.ts +64 -47
- package/src/client/transport/do-rpc-client.ts +20 -14
- package/src/client/transport/http-rpc-client.ts +19 -13
- package/src/client/transport/ws-rpc-client.ts +39 -36
- package/src/common/do-rpc-schema.ts +4 -3
- package/src/common/http-rpc-schema.ts +4 -3
- package/src/common/ws-rpc-schema.ts +4 -3
package/src/cf-worker/shared.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UnknownError } from '@livestore/common'
|
|
2
2
|
import type { CfTypes } from '@livestore/common-cf'
|
|
3
3
|
import { Effect, Schema, UrlParams } from '@livestore/utils/effect'
|
|
4
4
|
|
|
@@ -7,17 +7,61 @@ import { SearchParamsSchema, SyncMessage } from '../common/mod.ts'
|
|
|
7
7
|
|
|
8
8
|
export type Env = {}
|
|
9
9
|
|
|
10
|
+
/** Headers forwarded from the request to callbacks */
|
|
11
|
+
export type ForwardedHeaders = ReadonlyMap<string, string>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for forwarding request headers to DO callbacks.
|
|
15
|
+
* - `string[]`: List of header names to forward (case-insensitive)
|
|
16
|
+
* - `(request) => Record<string, string>`: Custom extraction function (sync)
|
|
17
|
+
*/
|
|
18
|
+
export type ForwardHeadersOption = readonly string[] | ((request: CfTypes.Request) => Record<string, string>)
|
|
19
|
+
|
|
20
|
+
/** Context passed to onPush/onPull callbacks */
|
|
21
|
+
export type CallbackContext = {
|
|
22
|
+
storeId: StoreId
|
|
23
|
+
payload?: Schema.JsonValue
|
|
24
|
+
/** Headers forwarded from the request (only present if `forwardHeaders` is configured) */
|
|
25
|
+
headers?: ForwardedHeaders
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
export type MakeDurableObjectClassOptions = {
|
|
11
|
-
onPush?: (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
) => Effect.SyncOrPromiseOrEffect<void>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
onPush?: (message: SyncMessage.PushRequest, context: CallbackContext) => Effect.SyncOrPromiseOrEffect<void>
|
|
30
|
+
onPushRes?: (message: SyncMessage.PushAck | UnknownError) => Effect.SyncOrPromiseOrEffect<void>
|
|
31
|
+
onPull?: (message: SyncMessage.PullRequest, context: CallbackContext) => Effect.SyncOrPromiseOrEffect<void>
|
|
32
|
+
onPullRes?: (message: SyncMessage.PullResponse | UnknownError) => Effect.SyncOrPromiseOrEffect<void>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Forward request headers to `onPush`/`onPull` callbacks for authentication.
|
|
36
|
+
*
|
|
37
|
+
* This enables cookie-based or header-based authentication patterns where
|
|
38
|
+
* you need access to request headers inside the Durable Object.
|
|
39
|
+
*
|
|
40
|
+
* @example Forward specific headers by name (case-insensitive)
|
|
41
|
+
* ```ts
|
|
42
|
+
* makeDurableObject({
|
|
43
|
+
* forwardHeaders: ['cookie', 'authorization'],
|
|
44
|
+
* onPush: async (message, { headers }) => {
|
|
45
|
+
* const cookie = headers?.get('cookie')
|
|
46
|
+
* const session = await validateSession(cookie)
|
|
47
|
+
* },
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Custom extraction function for derived values
|
|
52
|
+
* ```ts
|
|
53
|
+
* makeDurableObject({
|
|
54
|
+
* forwardHeaders: (request) => ({
|
|
55
|
+
* 'x-user-id': request.headers.get('x-user-id') ?? '',
|
|
56
|
+
* 'x-session': request.headers.get('cookie')?.split('session=')[1]?.split(';')[0] ?? '',
|
|
57
|
+
* }),
|
|
58
|
+
* onPush: async (message, { headers }) => {
|
|
59
|
+
* const userId = headers?.get('x-user-id')
|
|
60
|
+
* },
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
forwardHeaders?: ForwardHeadersOption
|
|
21
65
|
/**
|
|
22
66
|
* Storage engine for event persistence.
|
|
23
67
|
* - Default: `{ _tag: 'do-sqlite' }` (Durable Object SQLite)
|
|
@@ -137,5 +181,39 @@ export const WebSocketAttachmentSchema = Schema.parseJson(
|
|
|
137
181
|
// Different for each websocket connection
|
|
138
182
|
payload: Schema.optional(Schema.JsonValue),
|
|
139
183
|
pullRequestIds: Schema.Array(Schema.String),
|
|
184
|
+
// Headers forwarded from the initial request (via forwardHeaders option)
|
|
185
|
+
headers: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
140
186
|
}),
|
|
141
187
|
)
|
|
188
|
+
|
|
189
|
+
/** Helper to extract headers from a request based on the forwardHeaders option */
|
|
190
|
+
export const extractForwardedHeaders = (
|
|
191
|
+
request: CfTypes.Request,
|
|
192
|
+
forwardHeaders: ForwardHeadersOption | undefined,
|
|
193
|
+
): Record<string, string> | undefined => {
|
|
194
|
+
if (forwardHeaders === undefined) {
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (typeof forwardHeaders === 'function') {
|
|
199
|
+
return forwardHeaders(request)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Array of header names - extract them case-insensitively
|
|
203
|
+
const result: Record<string, string> = {}
|
|
204
|
+
for (const name of forwardHeaders) {
|
|
205
|
+
const value = request.headers.get(name)
|
|
206
|
+
if (value !== null) {
|
|
207
|
+
result[name.toLowerCase()] = value
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Convert a headers record to a ReadonlyMap */
|
|
214
|
+
export const headersRecordToMap = (headers: Record<string, string> | undefined): ForwardedHeaders | undefined => {
|
|
215
|
+
if (headers === undefined) {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
return new Map(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]))
|
|
219
|
+
}
|
package/src/cf-worker/worker.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { env as importedEnv } from 'cloudflare:workers'
|
|
2
|
+
|
|
2
3
|
import { UnknownError } from '@livestore/common'
|
|
3
4
|
import type { HelperTypes } from '@livestore/common-cf'
|
|
4
5
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
6
|
+
|
|
5
7
|
import type { CfTypes, SearchParams } from '../common/mod.ts'
|
|
6
8
|
import type { CfDeclare } from './mod.ts'
|
|
7
|
-
import { type Env, matchSyncRequest } from './shared.ts'
|
|
9
|
+
import { type Env, type ForwardedHeaders, matchSyncRequest } from './shared.ts'
|
|
8
10
|
|
|
9
11
|
// NOTE We need to redeclare runtime types here to avoid type conflicts with the lib.dom Response type.
|
|
10
12
|
declare class Response extends CfDeclare.Response {}
|
|
@@ -18,6 +20,13 @@ export type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjec
|
|
|
18
20
|
) => Promise<CfTypes.Response>
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/** Context passed to validatePayload callback */
|
|
24
|
+
export type ValidatePayloadContext = {
|
|
25
|
+
storeId: string
|
|
26
|
+
/** Request headers (raw, not filtered by forwardHeaders) */
|
|
27
|
+
headers: ForwardedHeaders
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
/**
|
|
22
31
|
* Options accepted by {@link makeWorker}. The Durable Object binding has to be
|
|
23
32
|
* supplied explicitly so we never fall back to deprecated defaults when Cloudflare config changes.
|
|
@@ -27,11 +36,6 @@ export type MakeWorkerOptions<TEnv extends Env = Env, TSyncPayload = Schema.Json
|
|
|
27
36
|
* Binding name of the sync Durable Object declared in wrangler config.
|
|
28
37
|
*/
|
|
29
38
|
syncBackendBinding: HelperTypes.ExtractDurableObjectKeys<TEnv>
|
|
30
|
-
/**
|
|
31
|
-
* Validates the payload during WebSocket connection establishment.
|
|
32
|
-
* Note: This runs only at connection time, not for individual push events.
|
|
33
|
-
* For push event validation, use the `onPush` callback in the Durable Object.
|
|
34
|
-
*/
|
|
35
39
|
/**
|
|
36
40
|
* Optionally pass a schema to decode the client-provided payload into a typed object
|
|
37
41
|
* before calling {@link validatePayload}. If omitted, the raw JSON value is forwarded.
|
|
@@ -39,9 +43,23 @@ export type MakeWorkerOptions<TEnv extends Env = Env, TSyncPayload = Schema.Json
|
|
|
39
43
|
syncPayloadSchema?: Schema.Schema<TSyncPayload>
|
|
40
44
|
/**
|
|
41
45
|
* Validates the (optionally decoded) payload during WebSocket connection establishment.
|
|
42
|
-
* If {@link syncPayloadSchema} is provided, `payload` will be of the schema
|
|
46
|
+
* If {@link syncPayloadSchema} is provided, `payload` will be of the schema's inferred type.
|
|
47
|
+
*
|
|
48
|
+
* The context includes request headers for cookie-based or header-based authentication.
|
|
49
|
+
*
|
|
50
|
+
* @example Cookie-based authentication
|
|
51
|
+
* ```ts
|
|
52
|
+
* validatePayload: async (payload, { storeId, headers }) => {
|
|
53
|
+
* const cookie = headers.get('cookie')
|
|
54
|
+
* const session = await validateSessionFromCookie(cookie)
|
|
55
|
+
* if (!session) throw new Error('Unauthorized')
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Note: This runs only at connection time, not for individual push events.
|
|
60
|
+
* For push event validation, use the `onPush` callback in the Durable Object.
|
|
43
61
|
*/
|
|
44
|
-
validatePayload?: (payload: TSyncPayload, context:
|
|
62
|
+
validatePayload?: (payload: TSyncPayload, context: ValidatePayloadContext) => void | Promise<void>
|
|
45
63
|
/** @default false */
|
|
46
64
|
enableCORS?: boolean
|
|
47
65
|
}
|
|
@@ -64,15 +82,16 @@ export const makeWorker = <
|
|
|
64
82
|
fetch: async (request, env, _ctx) => {
|
|
65
83
|
const url = new URL(request.url)
|
|
66
84
|
|
|
67
|
-
const corsHeaders: CfTypes.HeadersInit =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
const corsHeaders: CfTypes.HeadersInit =
|
|
86
|
+
options.enableCORS === true
|
|
87
|
+
? {
|
|
88
|
+
'Access-Control-Allow-Origin': '*',
|
|
89
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
90
|
+
'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers') ?? '*',
|
|
91
|
+
}
|
|
92
|
+
: {}
|
|
74
93
|
|
|
75
|
-
if (request.method === 'OPTIONS' && options.enableCORS) {
|
|
94
|
+
if (request.method === 'OPTIONS' && options.enableCORS === true) {
|
|
76
95
|
return new Response(null, {
|
|
77
96
|
status: 204,
|
|
78
97
|
headers: corsHeaders,
|
|
@@ -117,37 +136,33 @@ export const makeWorker = <
|
|
|
117
136
|
}
|
|
118
137
|
}
|
|
119
138
|
|
|
139
|
+
/** Convert CF Request headers to a ForwardedHeaders map */
|
|
140
|
+
const requestHeadersToMap = (request: CfTypes.Request): ForwardedHeaders => {
|
|
141
|
+
const result = new Map<string, string>()
|
|
142
|
+
request.headers.forEach((value, key) => {
|
|
143
|
+
result.set(key.toLowerCase(), value)
|
|
144
|
+
})
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
120
148
|
/**
|
|
121
149
|
* Handles LiveStore sync requests (e.g. with search params `?storeId=...&transport=...`).
|
|
122
150
|
*
|
|
123
|
-
* @example
|
|
151
|
+
* @example Token-based authentication
|
|
124
152
|
* ```ts
|
|
125
153
|
* const validatePayload = (payload: Schema.JsonValue | undefined, context: { storeId: string }) => {
|
|
126
|
-
* console.log(`Validating connection for store: ${context.storeId}`)
|
|
127
154
|
* if (payload?.authToken !== 'insecure-token-change-me') {
|
|
128
155
|
* throw new Error('Invalid auth token')
|
|
129
156
|
* }
|
|
130
157
|
* }
|
|
158
|
+
* ```
|
|
131
159
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* return handleSyncRequest({
|
|
139
|
-
* request,
|
|
140
|
-
* searchParams,
|
|
141
|
-
* env,
|
|
142
|
-
* ctx,
|
|
143
|
-
* syncBackendBinding: 'SYNC_BACKEND_DO',
|
|
144
|
-
* headers: {},
|
|
145
|
-
* validatePayload,
|
|
146
|
-
* })
|
|
147
|
-
* }
|
|
148
|
-
*
|
|
149
|
-
* return new Response('Invalid path', { status: 400 })
|
|
150
|
-
* }
|
|
160
|
+
* @example Cookie-based authentication
|
|
161
|
+
* ```ts
|
|
162
|
+
* const validatePayload = async (payload: Schema.JsonValue | undefined, { storeId, headers }) => {
|
|
163
|
+
* const cookie = headers.get('cookie')
|
|
164
|
+
* const session = await validateSessionFromCookie(cookie)
|
|
165
|
+
* if (!session) throw new Error('Unauthorized')
|
|
151
166
|
* }
|
|
152
167
|
* ```
|
|
153
168
|
*
|
|
@@ -180,6 +195,9 @@ export const handleSyncRequest = <
|
|
|
180
195
|
}): Promise<CfTypes.Response> =>
|
|
181
196
|
Effect.gen(function* () {
|
|
182
197
|
if (validatePayload !== undefined) {
|
|
198
|
+
// Convert request headers to a Map for the validation context
|
|
199
|
+
const requestHeaders = requestHeadersToMap(request)
|
|
200
|
+
|
|
183
201
|
// Always decode with the supplied schema when present, even if payload is undefined.
|
|
184
202
|
// This ensures required payloads are enforced by the schema.
|
|
185
203
|
if (syncPayloadSchema !== undefined) {
|
|
@@ -187,26 +205,25 @@ export const handleSyncRequest = <
|
|
|
187
205
|
if (decodedEither._tag === 'Left') {
|
|
188
206
|
const message = decodedEither.left.toString()
|
|
189
207
|
console.error('Invalid payload (decode failed)', message)
|
|
190
|
-
return new Response(message, { status: 400, headers })
|
|
208
|
+
return new Response(message, { status: 400, ...(headers !== undefined ? { headers } : {}) })
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
const result = yield* Effect.promise(async () =>
|
|
194
|
-
validatePayload(decodedEither.right
|
|
212
|
+
validatePayload(decodedEither.right, { storeId, headers: requestHeaders }),
|
|
195
213
|
).pipe(UnknownError.mapToUnknownError, Effect.either)
|
|
196
214
|
|
|
197
215
|
if (result._tag === 'Left') {
|
|
198
216
|
console.error('Invalid payload (validation failed)', result.left)
|
|
199
|
-
return new Response(result.left.toString(), { status: 400, headers })
|
|
217
|
+
return new Response(result.left.toString(), { status: 400, ...(headers !== undefined ? { headers } : {}) })
|
|
200
218
|
}
|
|
201
219
|
} else {
|
|
202
|
-
const result = yield* Effect.promise(async () =>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
220
|
+
const result = yield* Effect.promise(async () =>
|
|
221
|
+
validatePayload(payload as TSyncPayload, { storeId, headers: requestHeaders }),
|
|
222
|
+
).pipe(UnknownError.mapToUnknownError, Effect.either)
|
|
206
223
|
|
|
207
224
|
if (result._tag === 'Left') {
|
|
208
225
|
console.error('Invalid payload (validation failed)', result.left)
|
|
209
|
-
return new Response(result.left.toString(), { status: 400, headers })
|
|
226
|
+
return new Response(result.left.toString(), { status: 400, ...(headers !== undefined ? { headers } : {}) })
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
}
|
|
@@ -218,7 +235,7 @@ export const handleSyncRequest = <
|
|
|
218
235
|
`Failed dependency: Required Durable Object binding '${syncBackendBinding as string}' not available`,
|
|
219
236
|
{
|
|
220
237
|
status: 424,
|
|
221
|
-
headers,
|
|
238
|
+
...(headers !== undefined ? { headers } : {}),
|
|
222
239
|
},
|
|
223
240
|
)
|
|
224
241
|
}
|
|
@@ -235,7 +252,7 @@ export const handleSyncRequest = <
|
|
|
235
252
|
if (transport === 'ws' && (upgradeHeader === null || upgradeHeader !== 'websocket')) {
|
|
236
253
|
return new Response('Durable Object expected Upgrade: websocket', {
|
|
237
254
|
status: 426,
|
|
238
|
-
headers,
|
|
255
|
+
...(headers !== undefined ? { headers } : {}),
|
|
239
256
|
})
|
|
240
257
|
}
|
|
241
258
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SyncBackend, UnknownError } from '@livestore/common'
|
|
2
2
|
import { splitChunkBySize } from '@livestore/common/sync'
|
|
3
3
|
import { type CfTypes, layerProtocolDurableObject } from '@livestore/common-cf'
|
|
4
4
|
import { omit, shouldNeverHappen } from '@livestore/utils'
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Stream,
|
|
16
16
|
SubscriptionRef,
|
|
17
17
|
} from '@livestore/utils/effect'
|
|
18
|
+
|
|
18
19
|
import type { SyncBackendRpcInterface } from '../../cf-worker/shared.ts'
|
|
19
20
|
import { MAX_DO_RPC_REQUEST_BYTES, MAX_PUSH_EVENTS_PER_REQUEST } from '../../common/constants.ts'
|
|
20
21
|
import { SyncDoRpc } from '../../common/do-rpc-schema.ts'
|
|
@@ -73,9 +74,9 @@ export const makeDoRpcSync =
|
|
|
73
74
|
})),
|
|
74
75
|
),
|
|
75
76
|
storeId,
|
|
76
|
-
rpcContext: options?.live ? { callerContext: durableObjectContext } : undefined,
|
|
77
|
+
rpcContext: options?.live === true ? { callerContext: durableObjectContext } : undefined,
|
|
77
78
|
}).pipe(
|
|
78
|
-
options?.live
|
|
79
|
+
options?.live === true
|
|
79
80
|
? Stream.concatWithLastElement((res) =>
|
|
80
81
|
Effect.gen(function* () {
|
|
81
82
|
if (res._tag === 'None')
|
|
@@ -93,12 +94,16 @@ export const makeDoRpcSync =
|
|
|
93
94
|
: identity,
|
|
94
95
|
Stream.tap((res) => backendIdHelper.lazySet(res.backendId)),
|
|
95
96
|
Stream.map((res) => omit(res, ['backendId'])),
|
|
96
|
-
Stream.mapError((cause) =>
|
|
97
|
+
Stream.mapError((cause) =>
|
|
98
|
+
cause._tag === 'UnknownError' || cause._tag === 'BackendIdMismatchError'
|
|
99
|
+
? cause
|
|
100
|
+
: new UnknownError({ cause }),
|
|
101
|
+
),
|
|
97
102
|
Stream.withSpan('rpc-sync-client:pull'),
|
|
98
103
|
)
|
|
99
104
|
|
|
100
|
-
const push: SyncBackend.SyncBackend<{ createdAt: string }>['push'] = (
|
|
101
|
-
|
|
105
|
+
const push: SyncBackend.SyncBackend<{ createdAt: string }>['push'] = Effect.fn('rpc-sync-client:push')(
|
|
106
|
+
function* (batch) {
|
|
102
107
|
if (batch.length === 0) {
|
|
103
108
|
return
|
|
104
109
|
}
|
|
@@ -114,19 +119,20 @@ export const makeDoRpcSync =
|
|
|
114
119
|
backendId,
|
|
115
120
|
}),
|
|
116
121
|
}),
|
|
117
|
-
Effect.mapError((cause) => new
|
|
122
|
+
Effect.mapError((cause) => new UnknownError({ cause })),
|
|
118
123
|
)
|
|
119
124
|
|
|
120
125
|
for (const chunk of Chunk.toReadonlyArray(batchChunks)) {
|
|
121
126
|
const chunkArray = Chunk.toReadonlyArray(chunk)
|
|
122
127
|
yield* rpcClient.SyncDoRpc.Push({ batch: chunkArray, storeId, backendId })
|
|
123
128
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
129
|
+
},
|
|
130
|
+
Effect.mapError((cause) =>
|
|
131
|
+
cause._tag === 'UnknownError' || cause._tag === 'ServerAheadError' || cause._tag === 'BackendIdMismatchError'
|
|
132
|
+
? cause
|
|
133
|
+
: new UnknownError({ cause }),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
130
136
|
|
|
131
137
|
const ping: SyncBackend.SyncBackend<{ createdAt: string }>['ping'] = rpcClient.SyncDoRpc.Ping({
|
|
132
138
|
storeId,
|
|
@@ -170,7 +176,7 @@ export const makeDoRpcSync =
|
|
|
170
176
|
export const handleSyncUpdateRpc = (payload: unknown) =>
|
|
171
177
|
Effect.gen(function* () {
|
|
172
178
|
const decodedPayload = yield* Schema.decodeUnknown(ResponseChunkEncoded)(payload)
|
|
173
|
-
const decoded = yield* Schema.decodeUnknown(SyncMessage.PullResponse)(decodedPayload.values[0]
|
|
179
|
+
const decoded = yield* Schema.decodeUnknown(SyncMessage.PullResponse)(decodedPayload.values[0])
|
|
174
180
|
|
|
175
181
|
const pullStreamMailbox = requestIdMailboxMap.get(decodedPayload.requestId)
|
|
176
182
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SyncBackend, UnknownError } from '@livestore/common'
|
|
2
2
|
import type { EventSequenceNumber } from '@livestore/common/schema'
|
|
3
3
|
import { splitChunkBySize } from '@livestore/common/sync'
|
|
4
4
|
import { omit } from '@livestore/utils'
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SubscriptionRef,
|
|
20
20
|
UrlParams,
|
|
21
21
|
} from '@livestore/utils/effect'
|
|
22
|
+
|
|
22
23
|
import { MAX_HTTP_REQUEST_BYTES, MAX_PUSH_EVENTS_PER_REQUEST } from '../../common/constants.ts'
|
|
23
24
|
import { SyncHttpRpc } from '../../common/http-rpc-schema.ts'
|
|
24
25
|
import { SearchParamsSchema } from '../../common/mod.ts'
|
|
@@ -134,7 +135,7 @@ export const makeHttpSync =
|
|
|
134
135
|
payload,
|
|
135
136
|
cursor: mapCursor(cursor),
|
|
136
137
|
}).pipe(
|
|
137
|
-
options?.live
|
|
138
|
+
options?.live === true
|
|
138
139
|
? // Phase 2: Simulate `live` pull by polling for new events
|
|
139
140
|
Stream.concatWithLastElement((lastElement) => {
|
|
140
141
|
const initialPhase2Cursor = lastElement.pipe(
|
|
@@ -166,14 +167,18 @@ export const makeHttpSync =
|
|
|
166
167
|
: identity,
|
|
167
168
|
Stream.tap((res) => backendIdHelper.lazySet(res.backendId)),
|
|
168
169
|
Stream.map((res) => omit(res, ['backendId'])),
|
|
169
|
-
Stream.mapError((cause) =>
|
|
170
|
+
Stream.mapError((cause) =>
|
|
171
|
+
cause._tag === 'UnknownError' || cause._tag === 'BackendIdMismatchError'
|
|
172
|
+
? cause
|
|
173
|
+
: new UnknownError({ cause }),
|
|
174
|
+
),
|
|
170
175
|
Stream.withSpan('http-sync-client:pull'),
|
|
171
176
|
)
|
|
172
177
|
|
|
173
178
|
const pushSemaphore = yield* Effect.makeSemaphore(1)
|
|
174
179
|
|
|
175
|
-
const push: SyncBackend.SyncBackend<SyncMetadata>['push'] = (
|
|
176
|
-
|
|
180
|
+
const push: SyncBackend.SyncBackend<SyncMetadata>['push'] = Effect.fn('http-sync-client:push')(
|
|
181
|
+
function* (batch) {
|
|
177
182
|
if (batch.length === 0) {
|
|
178
183
|
return
|
|
179
184
|
}
|
|
@@ -190,20 +195,21 @@ export const makeHttpSync =
|
|
|
190
195
|
backendId,
|
|
191
196
|
}),
|
|
192
197
|
}),
|
|
193
|
-
Effect.mapError((cause) => new
|
|
198
|
+
Effect.mapError((cause) => new UnknownError({ cause })),
|
|
194
199
|
)
|
|
195
200
|
|
|
196
201
|
for (const chunk of Chunk.toReadonlyArray(batchChunks)) {
|
|
197
202
|
const chunkArray = Chunk.toReadonlyArray(chunk)
|
|
198
203
|
yield* rpcClient.SyncHttpRpc.Push({ storeId, payload, batch: chunkArray, backendId })
|
|
199
204
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
205
|
+
},
|
|
206
|
+
pushSemaphore.withPermits(1),
|
|
207
|
+
Effect.mapError((cause) =>
|
|
208
|
+
cause._tag === 'UnknownError' || cause._tag === 'ServerAheadError' || cause._tag === 'BackendIdMismatchError'
|
|
209
|
+
? cause
|
|
210
|
+
: new UnknownError({ cause }),
|
|
211
|
+
),
|
|
212
|
+
)
|
|
207
213
|
|
|
208
214
|
return SyncBackend.of({
|
|
209
215
|
connect,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IsOfflineError, SyncBackend, UnknownError } from '@livestore/common'
|
|
2
2
|
import type { LiveStoreEvent } from '@livestore/common/schema'
|
|
3
3
|
import { splitChunkBySize } from '@livestore/common/sync'
|
|
4
4
|
import { omit } from '@livestore/utils'
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
UrlParams,
|
|
20
20
|
} from '@livestore/utils/effect'
|
|
21
21
|
import type { WebSocket } from '@livestore/utils/effect/browser'
|
|
22
|
+
|
|
22
23
|
import { MAX_PUSH_EVENTS_PER_REQUEST, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
|
|
23
24
|
import { SearchParamsSchema } from '../../common/mod.ts'
|
|
24
25
|
import type { SyncMetadata } from '../../common/sync-message-types.ts'
|
|
@@ -95,7 +96,10 @@ export const makeWsSync =
|
|
|
95
96
|
|
|
96
97
|
const ProtocolLive = RpcClient.layerProtocolSocketWithIsConnected({
|
|
97
98
|
isConnected,
|
|
98
|
-
retryTransientErrors: Schedule.
|
|
99
|
+
retryTransientErrors: Schedule.exponential('1 seconds').pipe(
|
|
100
|
+
Schedule.union(Schedule.fixed('30 seconds')),
|
|
101
|
+
Schedule.jittered,
|
|
102
|
+
),
|
|
99
103
|
pingSchedule: Schedule.once.pipe(Schedule.andThen(Schedule.fixed(pingInterval))),
|
|
100
104
|
url: wsUrl,
|
|
101
105
|
}).pipe(
|
|
@@ -138,55 +142,54 @@ export const makeWsSync =
|
|
|
138
142
|
backendId: backendIdHelper.get().pipe(Option.getOrThrow),
|
|
139
143
|
})),
|
|
140
144
|
),
|
|
141
|
-
live: options?.live
|
|
145
|
+
live: options?.live === true,
|
|
142
146
|
}).pipe(
|
|
143
147
|
Stream.tap((res) => backendIdHelper.lazySet(res.backendId)),
|
|
144
148
|
Stream.map((res) => omit(res, ['backendId'])),
|
|
145
149
|
Stream.mapError((cause) =>
|
|
146
|
-
cause._tag === 'RpcClientError' && Socket.isSocketError(cause.cause)
|
|
150
|
+
cause._tag === 'RpcClientError' && Socket.isSocketError(cause.cause) === true
|
|
147
151
|
? new IsOfflineError({ cause: cause.cause })
|
|
148
|
-
: cause._tag === '
|
|
152
|
+
: cause._tag === 'UnknownError' || cause._tag === 'BackendIdMismatchError'
|
|
149
153
|
? cause
|
|
150
|
-
:
|
|
154
|
+
: new UnknownError({ cause }),
|
|
151
155
|
),
|
|
152
156
|
Stream.withSpan('pull'),
|
|
153
157
|
),
|
|
154
158
|
|
|
155
|
-
push: (batch)
|
|
156
|
-
|
|
157
|
-
if (batch.length === 0) return
|
|
159
|
+
push: Effect.fn('push')(function* (batch) {
|
|
160
|
+
if (batch.length === 0) return
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
const encodePayload = (batch: ReadonlyArray<LiveStoreEvent.Global.Encoded>) => ({
|
|
163
|
+
storeId,
|
|
164
|
+
payload,
|
|
165
|
+
batch,
|
|
166
|
+
backendId: backendIdHelper.get(),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const chunksChunk = yield* Chunk.fromIterable(batch).pipe(
|
|
170
|
+
splitChunkBySize({
|
|
171
|
+
maxItems: MAX_PUSH_EVENTS_PER_REQUEST,
|
|
172
|
+
maxBytes: MAX_WS_MESSAGE_BYTES,
|
|
173
|
+
encode: encodePayload,
|
|
174
|
+
}),
|
|
175
|
+
Effect.mapError((cause) => new UnknownError({ cause })),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
for (const sub of chunksChunk) {
|
|
179
|
+
yield* rpcClient.SyncWsRpc.Push({
|
|
160
180
|
storeId,
|
|
161
181
|
payload,
|
|
162
|
-
batch,
|
|
182
|
+
batch: Chunk.toReadonlyArray(sub),
|
|
163
183
|
backendId: backendIdHelper.get(),
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
encode: encodePayload,
|
|
171
|
-
}),
|
|
172
|
-
Effect.mapError((cause) => new InvalidPushError({ cause: new UnknownError({ cause }) })),
|
|
184
|
+
}).pipe(
|
|
185
|
+
Effect.mapError((cause) =>
|
|
186
|
+
cause._tag === 'UnknownError' || cause._tag === 'ServerAheadError' || cause._tag === 'BackendIdMismatchError'
|
|
187
|
+
? cause
|
|
188
|
+
: new UnknownError({ cause }),
|
|
189
|
+
),
|
|
173
190
|
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
yield* rpcClient.SyncWsRpc.Push({
|
|
177
|
-
storeId,
|
|
178
|
-
payload,
|
|
179
|
-
batch: Chunk.toReadonlyArray(sub),
|
|
180
|
-
backendId: backendIdHelper.get(),
|
|
181
|
-
}).pipe(
|
|
182
|
-
Effect.mapError((cause) =>
|
|
183
|
-
cause._tag === 'InvalidPushError'
|
|
184
|
-
? cause
|
|
185
|
-
: new InvalidPushError({ cause: new UnknownError({ cause }) }),
|
|
186
|
-
),
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
}).pipe(Effect.withSpan('push')),
|
|
191
|
+
}
|
|
192
|
+
}),
|
|
190
193
|
ping,
|
|
191
194
|
metadata: {
|
|
192
195
|
name: '@livestore/cf-sync',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BackendIdMismatchError, ServerAheadError, UnknownError } from '@livestore/common'
|
|
2
2
|
import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect'
|
|
3
|
+
|
|
3
4
|
import * as SyncMessage from './sync-message-types.ts'
|
|
4
5
|
|
|
5
6
|
const commonPayloadFields = {
|
|
@@ -34,7 +35,7 @@ export class SyncDoRpc extends RpcGroup.make(
|
|
|
34
35
|
rpcRequestId: Schema.String,
|
|
35
36
|
...SyncMessage.PullResponse.fields,
|
|
36
37
|
}),
|
|
37
|
-
error:
|
|
38
|
+
error: Schema.Union(UnknownError, BackendIdMismatchError),
|
|
38
39
|
stream: true,
|
|
39
40
|
}),
|
|
40
41
|
Rpc.make('SyncDoRpc.Push', {
|
|
@@ -43,7 +44,7 @@ export class SyncDoRpc extends RpcGroup.make(
|
|
|
43
44
|
...commonPayloadFields,
|
|
44
45
|
},
|
|
45
46
|
success: SyncMessage.PushAck,
|
|
46
|
-
error:
|
|
47
|
+
error: Schema.Union(UnknownError, ServerAheadError, BackendIdMismatchError),
|
|
47
48
|
}),
|
|
48
49
|
Rpc.make('SyncDoRpc.Ping', {
|
|
49
50
|
payload: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BackendIdMismatchError, ServerAheadError, UnknownError } from '@livestore/common'
|
|
2
2
|
import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect'
|
|
3
|
+
|
|
3
4
|
import * as SyncMessage from './sync-message-types.ts'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -17,7 +18,7 @@ export class SyncHttpRpc extends RpcGroup.make(
|
|
|
17
18
|
...SyncMessage.PullRequest.fields,
|
|
18
19
|
}),
|
|
19
20
|
success: SyncMessage.PullResponse,
|
|
20
|
-
error:
|
|
21
|
+
error: Schema.Union(UnknownError, BackendIdMismatchError),
|
|
21
22
|
stream: true,
|
|
22
23
|
}),
|
|
23
24
|
Rpc.make('SyncHttpRpc.Push', {
|
|
@@ -27,7 +28,7 @@ export class SyncHttpRpc extends RpcGroup.make(
|
|
|
27
28
|
...SyncMessage.PushRequest.fields,
|
|
28
29
|
}),
|
|
29
30
|
success: SyncMessage.PushAck,
|
|
30
|
-
error:
|
|
31
|
+
error: Schema.Union(UnknownError, ServerAheadError, BackendIdMismatchError),
|
|
31
32
|
}),
|
|
32
33
|
Rpc.make('SyncHttpRpc.Ping', {
|
|
33
34
|
payload: Schema.Struct({
|