@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.
- package/README.md +7 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/cf-worker/do/durable-object.d.ts +1 -1
- package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/do/durable-object.js +36 -18
- package/dist/cf-worker/do/durable-object.js.map +1 -1
- package/dist/cf-worker/do/layer.d.ts +24 -20
- package/dist/cf-worker/do/layer.d.ts.map +1 -1
- package/dist/cf-worker/do/layer.js +57 -24
- package/dist/cf-worker/do/layer.js.map +1 -1
- package/dist/cf-worker/do/pull.d.ts +8 -3
- package/dist/cf-worker/do/pull.d.ts.map +1 -1
- package/dist/cf-worker/do/pull.js +26 -21
- package/dist/cf-worker/do/pull.js.map +1 -1
- package/dist/cf-worker/do/push.d.ts +20 -6
- package/dist/cf-worker/do/push.d.ts.map +1 -1
- package/dist/cf-worker/do/push.js +84 -56
- package/dist/cf-worker/do/push.js.map +1 -1
- package/dist/cf-worker/do/push.test.d.ts +2 -0
- package/dist/cf-worker/do/push.test.d.ts.map +1 -0
- package/dist/cf-worker/do/push.test.js +30 -0
- package/dist/cf-worker/do/push.test.js.map +1 -0
- package/dist/cf-worker/do/sqlite.d.ts +49 -52
- package/dist/cf-worker/do/sqlite.d.ts.map +1 -1
- package/dist/cf-worker/do/sqlite.js +14 -5
- package/dist/cf-worker/do/sqlite.js.map +1 -1
- package/dist/cf-worker/do/sync-storage.d.ts +15 -9
- package/dist/cf-worker/do/sync-storage.d.ts.map +1 -1
- package/dist/cf-worker/do/sync-storage.js +111 -25
- package/dist/cf-worker/do/sync-storage.js.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts +2 -2
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.js +31 -23
- package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts +5 -3
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.js +28 -19
- package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +3 -2
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.js +32 -10
- package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
- package/dist/cf-worker/shared.d.ts +124 -34
- package/dist/cf-worker/shared.d.ts.map +1 -1
- package/dist/cf-worker/shared.js +52 -17
- package/dist/cf-worker/shared.js.map +1 -1
- package/dist/cf-worker/worker.d.ts +47 -39
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +54 -35
- package/dist/cf-worker/worker.js.map +1 -1
- package/dist/client/transport/do-rpc-client.d.ts +13 -4
- package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/do-rpc-client.js +74 -28
- package/dist/client/transport/do-rpc-client.js.map +1 -1
- package/dist/client/transport/http-rpc-client.d.ts +3 -3
- package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/http-rpc-client.js +51 -22
- package/dist/client/transport/http-rpc-client.js.map +1 -1
- package/dist/client/transport/ws-rpc-client.d.ts +4 -3
- package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/ws-rpc-client.js +40 -22
- package/dist/client/transport/ws-rpc-client.js.map +1 -1
- package/dist/common/constants.d.ts +7 -0
- package/dist/common/constants.d.ts.map +1 -0
- package/dist/common/constants.js +17 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/do-rpc-schema.d.ts +42 -43
- package/dist/common/do-rpc-schema.d.ts.map +1 -1
- package/dist/common/do-rpc-schema.js +5 -5
- package/dist/common/do-rpc-schema.js.map +1 -1
- package/dist/common/http-rpc-schema.d.ts +37 -38
- package/dist/common/http-rpc-schema.d.ts.map +1 -1
- package/dist/common/http-rpc-schema.js +7 -7
- package/dist/common/http-rpc-schema.js.map +1 -1
- package/dist/common/mod.d.ts +5 -3
- package/dist/common/mod.d.ts.map +1 -1
- package/dist/common/mod.js +4 -2
- package/dist/common/mod.js.map +1 -1
- package/dist/common/sync-message-types.d.ts +113 -130
- package/dist/common/sync-message-types.d.ts.map +1 -1
- package/dist/common/sync-message-types.js +29 -17
- package/dist/common/sync-message-types.js.map +1 -1
- package/dist/common/ws-rpc-schema.d.ts +34 -35
- package/dist/common/ws-rpc-schema.d.ts.map +1 -1
- package/dist/common/ws-rpc-schema.js +5 -5
- package/dist/common/ws-rpc-schema.js.map +1 -1
- package/package.json +57 -15
- package/src/cf-worker/do/durable-object.ts +59 -41
- package/src/cf-worker/do/layer.ts +153 -88
- package/src/cf-worker/do/pull.ts +48 -31
- package/src/cf-worker/do/push.test.ts +40 -0
- package/src/cf-worker/do/push.ts +163 -125
- package/src/cf-worker/do/sqlite.ts +15 -5
- package/src/cf-worker/do/sync-storage.ts +171 -38
- package/src/cf-worker/do/transport/do-rpc-server.ts +40 -40
- package/src/cf-worker/do/transport/http-rpc-server.ts +37 -20
- package/src/cf-worker/do/transport/ws-rpc-server.ts +44 -16
- package/src/cf-worker/shared.ts +152 -36
- package/src/cf-worker/worker.ts +110 -58
- package/src/client/transport/do-rpc-client.ts +106 -40
- package/src/client/transport/http-rpc-client.ts +74 -38
- package/src/client/transport/ws-rpc-client.ts +55 -29
- package/src/common/constants.ts +18 -0
- package/src/common/do-rpc-schema.ts +6 -5
- package/src/common/http-rpc-schema.ts +8 -7
- package/src/common/mod.ts +4 -3
- package/src/common/sync-message-types.ts +19 -19
- package/src/common/ws-rpc-schema.ts +6 -5
- package/dist/cf-worker/do/ws-chunking.d.ts +0 -22
- package/dist/cf-worker/do/ws-chunking.d.ts.map +0 -1
- package/dist/cf-worker/do/ws-chunking.js +0 -49
- package/dist/cf-worker/do/ws-chunking.js.map +0 -1
- package/src/cf-worker/do/ws-chunking.ts +0 -76
|
@@ -1,24 +1,79 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UnknownError } from '@livestore/common';
|
|
2
2
|
import type { CfTypes } from '@livestore/common-cf';
|
|
3
|
-
import { 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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
*
|
|
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
|
|
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: (
|
|
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:
|
|
50
|
-
readonly parentSeqNum:
|
|
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
|
-
},
|
|
78
|
-
export declare const encodeIncomingMessage: (
|
|
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:
|
|
88
|
-
readonly parentSeqNum:
|
|
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
|
-
},
|
|
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.
|
|
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.
|
|
129
|
-
storeId:
|
|
130
|
-
payload: Schema.optional<Schema.
|
|
131
|
-
pullRequestIds: Schema.
|
|
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,
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/cf-worker/shared.ts"],"names":[],"mappings":"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"}
|
package/dist/cf-worker/shared.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Result, Schema } from '@livestore/utils/effect';
|
|
2
2
|
import { SearchParamsSchema, SyncMessage } from "../common/mod.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* CRITICAL: Increment this version whenever you modify the database schema structure.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
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.
|
|
10
|
-
export const encodeIncomingMessage = Schema.encodeSync(Schema.
|
|
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
|
|
19
|
-
|
|
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.
|
|
34
|
+
return paramsResult.success;
|
|
24
35
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
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":"
|
|
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
|
|
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
|
-
*
|
|
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
|
|
44
|
+
* For push event validation, use the `onPush` callback in the Durable Object.
|
|
21
45
|
*/
|
|
22
|
-
validatePayload?: (payload:
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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 {
|
|
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
|
|
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?:
|
|
82
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"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"}
|
package/dist/cf-worker/worker.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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 {
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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));
|