@livestore/sync-cf 0.4.0-dev.14 → 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/shared.d.ts +2 -2
- 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 +1 -1
- package/package.json +4 -4
- package/src/cf-worker/worker.ts +49 -16
|
@@ -91,8 +91,8 @@ export declare const encodeOutgoingMessage: (a: {
|
|
|
91
91
|
readonly backendId: string;
|
|
92
92
|
readonly batch: readonly {
|
|
93
93
|
readonly metadata: import("effect/Option").Option<{
|
|
94
|
-
readonly createdAt: string;
|
|
95
94
|
readonly _tag: "SyncMessage.SyncMetadata";
|
|
95
|
+
readonly createdAt: string;
|
|
96
96
|
}>;
|
|
97
97
|
readonly eventEncoded: {
|
|
98
98
|
readonly name: string;
|
|
@@ -116,10 +116,10 @@ export declare const encodeOutgoingMessage: (a: {
|
|
|
116
116
|
} | {
|
|
117
117
|
readonly _tag: "SyncMessage.AdminResetRoomResponse";
|
|
118
118
|
} | {
|
|
119
|
+
readonly _tag: "SyncMessage.AdminInfoResponse";
|
|
119
120
|
readonly info: {
|
|
120
121
|
readonly durableObjectId: string;
|
|
121
122
|
};
|
|
122
|
-
readonly _tag: "SyncMessage.AdminInfoResponse";
|
|
123
123
|
}, overrideOptions?: import("effect/SchemaAST").ParseOptions) => string;
|
|
124
124
|
export declare const encodeIncomingMessage: (a: {
|
|
125
125
|
readonly cursor: import("effect/Option").Option<{
|
|
@@ -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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"}
|
|
@@ -45,8 +45,8 @@ export declare const emptyPullResponse: (backendId: string) => {
|
|
|
45
45
|
readonly backendId: string;
|
|
46
46
|
readonly batch: readonly {
|
|
47
47
|
readonly metadata: import("effect/Option").Option<{
|
|
48
|
-
readonly createdAt: string;
|
|
49
48
|
readonly _tag: "SyncMessage.SyncMetadata";
|
|
49
|
+
readonly createdAt: string;
|
|
50
50
|
}>;
|
|
51
51
|
readonly eventEncoded: {
|
|
52
52
|
readonly name: string;
|
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/
|
|
15
|
-
"@livestore/
|
|
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
|
|