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