@livestore/sync-cf 0.3.1 → 0.3.2-dev.1
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/durable-object.d.ts +11 -5
- package/dist/cf-worker/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/durable-object.js +14 -16
- package/dist/cf-worker/durable-object.js.map +1 -1
- package/dist/cf-worker/mod.d.ts +2 -2
- package/dist/cf-worker/mod.js +2 -2
- package/dist/cf-worker/worker.d.ts +43 -13
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +15 -4
- package/dist/cf-worker/worker.js.map +1 -1
- package/dist/common/mod.d.ts +1 -1
- package/dist/common/mod.js +1 -1
- package/dist/sync-impl/mod.d.ts +1 -1
- package/dist/sync-impl/mod.js +1 -1
- package/dist/sync-impl/ws-impl.d.ts +1 -1
- package/dist/sync-impl/ws-impl.js +1 -3
- package/dist/sync-impl/ws-impl.js.map +1 -1
- package/package.json +12 -4
- package/src/cf-worker/durable-object.ts +23 -19
- package/src/cf-worker/mod.ts +2 -2
- package/src/cf-worker/worker.ts +87 -23
- package/src/common/mod.ts +1 -1
- package/src/sync-impl/mod.ts +1 -1
- package/src/sync-impl/ws-impl.ts +4 -4
package/src/cf-worker/mod.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './durable-object.
|
|
2
|
-
export * from './worker.
|
|
1
|
+
export * from './durable-object.ts'
|
|
2
|
+
export * from './worker.ts'
|
package/src/cf-worker/worker.ts
CHANGED
|
@@ -1,16 +1,57 @@
|
|
|
1
|
+
import type * as CfWorker from '@cloudflare/workers-types'
|
|
1
2
|
import { UnexpectedError } from '@livestore/common'
|
|
2
3
|
import type { Schema } from '@livestore/utils/effect'
|
|
3
4
|
import { Effect, UrlParams } from '@livestore/utils/effect'
|
|
4
5
|
|
|
5
|
-
import { SearchParamsSchema } from '../common/mod.
|
|
6
|
-
import type { Env } from './durable-object.
|
|
6
|
+
import { SearchParamsSchema } from '../common/mod.ts'
|
|
7
|
+
import type { Env } from './durable-object.ts'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// Redeclaring Response to Cloudflare Worker Response type to avoid lib.dom type clashing
|
|
10
|
+
declare const Response: typeof CfWorker.Response
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper type to extract DurableObject keys from Env to give consumer type safety.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* type PlatformEnv = {
|
|
18
|
+
* DB: D1Database
|
|
19
|
+
* ADMIN_TOKEN: string
|
|
20
|
+
* WEBSOCKET_SERVER: DurableObjectNamespace<WebSocketServer>
|
|
21
|
+
* }
|
|
22
|
+
* export default makeWorker<PlatformEnv>({
|
|
23
|
+
* durableObject: { name: "WEBSOCKET_SERVER" },
|
|
24
|
+
* // ^ (property) name?: "WEBSOCKET_SERVER" | undefined
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
type ExtractDurableObjectKeys<TEnv = Env> = TEnv extends Env
|
|
28
|
+
? [keyof TEnv] extends [keyof Env]
|
|
29
|
+
? string
|
|
30
|
+
: keyof {
|
|
31
|
+
[K in keyof TEnv as K extends keyof Env
|
|
32
|
+
? never
|
|
33
|
+
: TEnv[K] extends CfWorker.DurableObjectNamespace<any>
|
|
34
|
+
? K
|
|
35
|
+
: never]: TEnv[K]
|
|
36
|
+
}
|
|
37
|
+
: never
|
|
38
|
+
|
|
39
|
+
// HINT: If we ever extend user's custom worker RPC, type T can help here with expected return type safety. Currently unused.
|
|
40
|
+
export type CFWorker<TEnv extends Env = Env, _T extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined> = {
|
|
41
|
+
fetch: <CFHostMetada = unknown>(
|
|
42
|
+
request: CfWorker.Request<CFHostMetada>,
|
|
43
|
+
env: TEnv,
|
|
44
|
+
ctx: CfWorker.ExecutionContext,
|
|
45
|
+
) => Promise<CfWorker.Response>
|
|
10
46
|
}
|
|
11
47
|
|
|
12
|
-
export type MakeWorkerOptions = {
|
|
13
|
-
|
|
48
|
+
export type MakeWorkerOptions<TEnv extends Env = Env> = {
|
|
49
|
+
/**
|
|
50
|
+
* Validates the payload during WebSocket connection establishment.
|
|
51
|
+
* Note: This runs only at connection time, not for individual push events.
|
|
52
|
+
* For push event validation, use the `onPush` callback in the durable object.
|
|
53
|
+
*/
|
|
54
|
+
validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
|
|
14
55
|
/** @default false */
|
|
15
56
|
enableCORS?: boolean
|
|
16
57
|
durableObject?: {
|
|
@@ -19,11 +60,16 @@ export type MakeWorkerOptions = {
|
|
|
19
60
|
*
|
|
20
61
|
* @default 'WEBSOCKET_SERVER'
|
|
21
62
|
*/
|
|
22
|
-
name?:
|
|
63
|
+
name?: ExtractDurableObjectKeys<TEnv>
|
|
23
64
|
}
|
|
24
65
|
}
|
|
25
66
|
|
|
26
|
-
export const makeWorker =
|
|
67
|
+
export const makeWorker = <
|
|
68
|
+
TEnv extends Env = Env,
|
|
69
|
+
TDurableObjectRpc extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined,
|
|
70
|
+
>(
|
|
71
|
+
options: MakeWorkerOptions<TEnv> = {},
|
|
72
|
+
): CFWorker<TEnv, TDurableObjectRpc> => {
|
|
27
73
|
return {
|
|
28
74
|
fetch: async (request, env, _ctx) => {
|
|
29
75
|
const url = new URL(request.url)
|
|
@@ -37,7 +83,7 @@ export const makeWorker = (options: MakeWorkerOptions = {}): CFWorker => {
|
|
|
37
83
|
})
|
|
38
84
|
}
|
|
39
85
|
|
|
40
|
-
const corsHeaders: HeadersInit = options.enableCORS
|
|
86
|
+
const corsHeaders: CfWorker.HeadersInit = options.enableCORS
|
|
41
87
|
? {
|
|
42
88
|
'Access-Control-Allow-Origin': '*',
|
|
43
89
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
@@ -53,7 +99,7 @@ export const makeWorker = (options: MakeWorkerOptions = {}): CFWorker => {
|
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
if (url.pathname.endsWith('/websocket')) {
|
|
56
|
-
return handleWebSocket(request, env, _ctx, {
|
|
102
|
+
return handleWebSocket<TEnv, TDurableObjectRpc>(request, env, _ctx, {
|
|
57
103
|
headers: corsHeaders,
|
|
58
104
|
validatePayload: options.validatePayload,
|
|
59
105
|
durableObject: options.durableObject,
|
|
@@ -79,7 +125,8 @@ export const makeWorker = (options: MakeWorkerOptions = {}): CFWorker => {
|
|
|
79
125
|
*
|
|
80
126
|
* @example
|
|
81
127
|
* ```ts
|
|
82
|
-
* const validatePayload = (payload: Schema.JsonValue | undefined) => {
|
|
128
|
+
* const validatePayload = (payload: Schema.JsonValue | undefined, context: { storeId: string }) => {
|
|
129
|
+
* console.log(`Validating connection for store: ${context.storeId}`)
|
|
83
130
|
* if (payload?.authToken !== 'insecure-token-change-me') {
|
|
84
131
|
* throw new Error('Invalid auth token')
|
|
85
132
|
* }
|
|
@@ -98,16 +145,20 @@ export const makeWorker = (options: MakeWorkerOptions = {}): CFWorker => {
|
|
|
98
145
|
*
|
|
99
146
|
* @throws {UnexpectedError} If the payload is invalid
|
|
100
147
|
*/
|
|
101
|
-
export const handleWebSocket =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
148
|
+
export const handleWebSocket = <
|
|
149
|
+
TEnv extends Env = Env,
|
|
150
|
+
TDurableObjectRpc extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined,
|
|
151
|
+
CFHostMetada = unknown,
|
|
152
|
+
>(
|
|
153
|
+
request: CfWorker.Request<CFHostMetada>,
|
|
154
|
+
env: TEnv,
|
|
155
|
+
_ctx: CfWorker.ExecutionContext,
|
|
105
156
|
options: {
|
|
106
|
-
headers?: HeadersInit
|
|
107
|
-
durableObject?: MakeWorkerOptions['durableObject']
|
|
108
|
-
validatePayload?: (payload: Schema.JsonValue | undefined) => void | Promise<void>
|
|
157
|
+
headers?: CfWorker.HeadersInit
|
|
158
|
+
durableObject?: MakeWorkerOptions<TEnv>['durableObject']
|
|
159
|
+
validatePayload?: (payload: Schema.JsonValue | undefined, context: { storeId: string }) => void | Promise<void>
|
|
109
160
|
},
|
|
110
|
-
): Promise<Response> =>
|
|
161
|
+
): Promise<CfWorker.Response> =>
|
|
111
162
|
Effect.gen(function* () {
|
|
112
163
|
const url = new URL(request.url)
|
|
113
164
|
|
|
@@ -124,7 +175,7 @@ export const handleWebSocket = (
|
|
|
124
175
|
const { storeId, payload } = paramsResult.right
|
|
125
176
|
|
|
126
177
|
if (options.validatePayload !== undefined) {
|
|
127
|
-
const result = yield* Effect.promise(async () => options.validatePayload!(payload)).pipe(
|
|
178
|
+
const result = yield* Effect.promise(async () => options.validatePayload!(payload, { storeId })).pipe(
|
|
128
179
|
UnexpectedError.mapToUnexpectedError,
|
|
129
180
|
Effect.either,
|
|
130
181
|
)
|
|
@@ -136,15 +187,28 @@ export const handleWebSocket = (
|
|
|
136
187
|
}
|
|
137
188
|
|
|
138
189
|
const durableObjectName = options.durableObject?.name ?? 'WEBSOCKET_SERVER'
|
|
139
|
-
|
|
190
|
+
if (!(durableObjectName in env)) {
|
|
191
|
+
return new Response(`Failed dependency: Required Durable Object binding '${durableObjectName}' not available`, {
|
|
192
|
+
status: 424,
|
|
193
|
+
headers: options.headers,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const durableObjectNamespace = env[
|
|
198
|
+
durableObjectName as keyof TEnv
|
|
199
|
+
] as CfWorker.DurableObjectNamespace<TDurableObjectRpc>
|
|
140
200
|
|
|
141
201
|
const id = durableObjectNamespace.idFromName(storeId)
|
|
142
202
|
const durableObject = durableObjectNamespace.get(id)
|
|
143
203
|
|
|
144
204
|
const upgradeHeader = request.headers.get('Upgrade')
|
|
145
205
|
if (!upgradeHeader || upgradeHeader !== 'websocket') {
|
|
146
|
-
return new Response('Durable Object expected Upgrade: websocket', {
|
|
206
|
+
return new Response('Durable Object expected Upgrade: websocket', {
|
|
207
|
+
status: 426,
|
|
208
|
+
headers: options?.headers,
|
|
209
|
+
})
|
|
147
210
|
}
|
|
148
211
|
|
|
149
|
-
|
|
212
|
+
// Cloudflare Durable Object type clashing with lib.dom Response type, which is why we need the casts here.
|
|
213
|
+
return yield* Effect.promise(() => durableObject.fetch(request as any) as unknown as Promise<CfWorker.Response>)
|
|
150
214
|
}).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
|
package/src/common/mod.ts
CHANGED
package/src/sync-impl/mod.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './ws-impl.
|
|
1
|
+
export * from './ws-impl.ts'
|
package/src/sync-impl/ws-impl.ts
CHANGED
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
} from '@livestore/utils/effect'
|
|
20
20
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
21
21
|
|
|
22
|
-
import { SearchParamsSchema, WSMessage } from '../common/mod.
|
|
23
|
-
import type { SyncMetadata } from '../common/ws-message-types.
|
|
22
|
+
import { SearchParamsSchema, WSMessage } from '../common/mod.ts'
|
|
23
|
+
import type { SyncMetadata } from '../common/ws-message-types.ts'
|
|
24
24
|
|
|
25
25
|
export interface WsSyncOptions {
|
|
26
26
|
url: string
|
|
@@ -235,10 +235,10 @@ const connect = (wsUrl: string) =>
|
|
|
235
235
|
|
|
236
236
|
// NOTE it seems that this callback doesn't work reliably on a worker but only via `window.addEventListener`
|
|
237
237
|
// We might need to proxy the event from the main thread to the worker if we want this to work reliably.
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
if (typeof self !== 'undefined' && typeof self.addEventListener === 'function') {
|
|
240
240
|
// TODO support an Expo equivalent for this
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
yield* Effect.eventListener(self, 'offline', () => Deferred.succeed(connectionClosed, void 0))
|
|
243
243
|
}
|
|
244
244
|
|