@livestore/common-cf 0.5.0-dev.0 → 0.5.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/dist/.tsbuildinfo +1 -1
- package/dist/declare/mod.js +1 -1
- package/dist/do-rpc/client.d.ts +8 -0
- package/dist/do-rpc/client.d.ts.map +1 -1
- package/dist/do-rpc/client.js +67 -37
- package/dist/do-rpc/client.js.map +1 -1
- package/dist/do-rpc/client.test.js +97 -7
- package/dist/do-rpc/client.test.js.map +1 -1
- package/dist/do-rpc/mod.d.ts +1 -1
- package/dist/do-rpc/mod.d.ts.map +1 -1
- package/dist/do-rpc/mod.js +2 -2
- package/dist/do-rpc/mod.js.map +1 -1
- package/dist/do-rpc/rpc.test.js +2 -2
- package/dist/do-rpc/rpc.test.js.map +1 -1
- package/dist/do-rpc/server.d.ts +4 -3
- package/dist/do-rpc/server.d.ts.map +1 -1
- package/dist/do-rpc/server.js +55 -63
- package/dist/do-rpc/server.js.map +1 -1
- package/dist/do-rpc/test-fixtures/rpc-schema.d.ts +16 -16
- package/dist/do-rpc/test-fixtures/worker.d.ts.map +1 -1
- package/dist/do-rpc/test-fixtures/worker.js +1 -1
- package/dist/mod.js +3 -3
- package/dist/ws-rpc/test-fixtures/rpc-schema.d.ts +16 -16
- package/dist/ws-rpc/test-fixtures/worker.d.ts.map +1 -1
- package/dist/ws-rpc/test-fixtures/worker.js +2 -2
- package/dist/ws-rpc/ws-rpc-server.d.ts +8 -2
- package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.js +45 -13
- package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
- package/dist/ws-rpc/ws-rpc.test.js +1 -1
- package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
- package/package.json +28 -28
- package/src/do-rpc/README.md +2 -2
- package/src/do-rpc/client.test.ts +148 -7
- package/src/do-rpc/client.ts +94 -53
- package/src/do-rpc/mod.ts +1 -1
- package/src/do-rpc/rpc.test.ts +1 -1
- package/src/do-rpc/server.ts +84 -67
- package/src/ws-rpc/ws-rpc-server.ts +78 -12
package/src/do-rpc/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Effect,
|
|
3
3
|
Exit,
|
|
4
|
+
Fiber,
|
|
4
5
|
Headers,
|
|
5
6
|
type Layer,
|
|
6
7
|
Option,
|
|
@@ -19,17 +20,29 @@ import {
|
|
|
19
20
|
|
|
20
21
|
import type * as CfTypes from '../cf-types.ts'
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* TODO(effect): revisit if a cleaner/idiomatic API lands — https://github.com/Effect-TS/effect/issues/6489
|
|
29
|
-
*/
|
|
30
|
-
const erasedJsonCodec = (schema: Schema.Top) =>
|
|
23
|
+
const isEncodedRpcMessage = Schema.is(RpcMessage.EncodedSchema)
|
|
24
|
+
|
|
25
|
+
const isRequestEncoded = (message: unknown): message is RpcMessage.RequestEncoded =>
|
|
26
|
+
isEncodedRpcMessage(message) === true && message._tag === 'Request'
|
|
27
|
+
|
|
28
|
+
const erasedCodec = (codecFor: RpcSerialization.CodecFor, schema: Schema.Top) =>
|
|
31
29
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- erased RPC data schemas require no codec services
|
|
32
|
-
|
|
30
|
+
codecFor(schema as Schema.Codec<unknown, unknown, never, never>)
|
|
31
|
+
|
|
32
|
+
const schemaBinarySerialization = RpcSerialization.RpcSerialization.pipe(
|
|
33
|
+
Effect.provide(RpcSerialization.layerSchemaBinary()),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const encodeBytes = (parser: RpcSerialization.Parser, value: unknown): Uint8Array<ArrayBuffer> => {
|
|
37
|
+
const encoded = parser.encode(value)
|
|
38
|
+
if (encoded instanceof Uint8Array === false) {
|
|
39
|
+
throw new TypeError('SchemaBinary RPC serialization did not produce bytes')
|
|
40
|
+
}
|
|
41
|
+
if (encoded.buffer instanceof ArrayBuffer === false) {
|
|
42
|
+
throw new TypeError('SchemaBinary RPC serialization produced shared bytes')
|
|
43
|
+
}
|
|
44
|
+
return new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength)
|
|
45
|
+
}
|
|
33
46
|
|
|
34
47
|
export interface ClientDoWithRpcCallback {
|
|
35
48
|
__DURABLE_OBJECT_BRAND: never
|
|
@@ -58,22 +71,10 @@ export const toDurableObjectHandler =
|
|
|
58
71
|
) => Effect.Effect<Uint8Array<ArrayBuffer> | CfTypes.ReadableStream>) =>
|
|
59
72
|
(serializedPayload) =>
|
|
60
73
|
Effect.gen(function* () {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
// Handle potential nested array from client serialization
|
|
67
|
-
let requests: RpcMessage.FromClientEncoded[]
|
|
68
|
-
if (Array.isArray(decoded) === true && decoded.length === 1 && Array.isArray(decoded[0]) === true) {
|
|
69
|
-
// Double-wrapped array [[{...}]] -> [{...}]
|
|
70
|
-
requests = decoded[0]
|
|
71
|
-
} else if (Array.isArray(decoded) === true) {
|
|
72
|
-
// Single array [{...}]
|
|
73
|
-
requests = decoded
|
|
74
|
-
} else {
|
|
75
|
-
requests = []
|
|
76
|
-
}
|
|
74
|
+
const serialization = yield* schemaBinarySerialization
|
|
75
|
+
const parser = serialization.makeUnsafe()
|
|
76
|
+
|
|
77
|
+
const requests = parser.decode(serializedPayload).filter(isRequestEncoded)
|
|
77
78
|
|
|
78
79
|
// Get the context with handlers
|
|
79
80
|
const context = yield* Effect.context<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>>()
|
|
@@ -82,9 +83,6 @@ export const toDurableObjectHandler =
|
|
|
82
83
|
const responses: any[] = []
|
|
83
84
|
|
|
84
85
|
for (const request of requests) {
|
|
85
|
-
if (request._tag !== 'Request') {
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
86
|
const requestId = RpcMessage.RequestId(request.id)
|
|
89
87
|
|
|
90
88
|
// Find the RPC handler
|
|
@@ -102,19 +100,17 @@ export const toDurableObjectHandler =
|
|
|
102
100
|
continue
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
const payloadResult = yield* Schema.decodeUnknownEffect(
|
|
103
|
+
const payloadResult = yield* Schema.decodeUnknownEffect(erasedCodec(serialization.codecFor, rpc.payloadSchema))(
|
|
106
104
|
request.payload,
|
|
107
105
|
).pipe(Effect.provideContext(entry.context), Effect.result)
|
|
108
106
|
|
|
109
107
|
if (Result.isFailure(payloadResult) === true) {
|
|
110
|
-
// Request payloads are encoded with the JSON codec by Effect's RPC client. Decode them
|
|
111
|
-
// before dispatch so JSON-only representations such as `null` become their schema values.
|
|
112
108
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
113
109
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
114
110
|
const rawExit = Exit.die(SchemaIssue.makeFormatterDefault()(payloadResult.failure.issue))
|
|
115
|
-
const encodedExit = yield* Schema.encodeUnknownEffect(
|
|
116
|
-
|
|
117
|
-
)
|
|
111
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(serialization.codecFor, exitSchema))(
|
|
112
|
+
rawExit,
|
|
113
|
+
).pipe(Effect.provideContext(entry.context))
|
|
118
114
|
responses.push({
|
|
119
115
|
_tag: 'Exit',
|
|
120
116
|
requestId,
|
|
@@ -130,7 +126,15 @@ export const toDurableObjectHandler =
|
|
|
130
126
|
|
|
131
127
|
// For streaming RPCs with only one request, return ReadableStream directly
|
|
132
128
|
if (isStream === true && requests.length === 1) {
|
|
133
|
-
return yield* createStreamingResponse(
|
|
129
|
+
return yield* createStreamingResponse(
|
|
130
|
+
rpc,
|
|
131
|
+
entry,
|
|
132
|
+
requestId,
|
|
133
|
+
payload,
|
|
134
|
+
parser,
|
|
135
|
+
serialization.codecFor,
|
|
136
|
+
options.layer,
|
|
137
|
+
)
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
// Execute the handler
|
|
@@ -143,7 +147,8 @@ export const toDurableObjectHandler =
|
|
|
143
147
|
}),
|
|
144
148
|
rpc,
|
|
145
149
|
})
|
|
146
|
-
const effectOrStream
|
|
150
|
+
const effectOrStream: Effect.Effect<unknown, unknown> | Stream.Stream<unknown, unknown> =
|
|
151
|
+
Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
|
|
147
152
|
|
|
148
153
|
let value: any
|
|
149
154
|
if (Effect.isEffect(effectOrStream) === true) {
|
|
@@ -161,7 +166,7 @@ export const toDurableObjectHandler =
|
|
|
161
166
|
if (exitSchema !== undefined) {
|
|
162
167
|
// Use schema encoding for proper serialization
|
|
163
168
|
const rawExit = Exit.succeed(value)
|
|
164
|
-
encodedExit = yield* Schema.encodeUnknownEffect(
|
|
169
|
+
encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(serialization.codecFor, exitSchema))(rawExit)
|
|
165
170
|
} else {
|
|
166
171
|
// Fallback to direct exit
|
|
167
172
|
encodedExit = Exit.succeed(value)
|
|
@@ -183,7 +188,9 @@ export const toDurableObjectHandler =
|
|
|
183
188
|
if (exitSchema !== undefined) {
|
|
184
189
|
// Use schema encoding for proper serialization
|
|
185
190
|
const rawExit = Exit.failCause(cause)
|
|
186
|
-
encodedExit = yield* Schema.encodeUnknownEffect(
|
|
191
|
+
encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(serialization.codecFor, exitSchema))(
|
|
192
|
+
rawExit,
|
|
193
|
+
)
|
|
187
194
|
} else {
|
|
188
195
|
// Fallback to direct exit
|
|
189
196
|
encodedExit = Exit.failCause(cause)
|
|
@@ -201,9 +208,7 @@ export const toDurableObjectHandler =
|
|
|
201
208
|
responses.push(result)
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
const encoded = parser.encode(responses) as Uint8Array<ArrayBuffer>
|
|
206
|
-
return encoded
|
|
211
|
+
return encodeBytes(parser, responses)
|
|
207
212
|
}).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie) as Effect.Effect<
|
|
208
213
|
Uint8Array<ArrayBuffer> | CfTypes.ReadableStream
|
|
209
214
|
>
|
|
@@ -213,6 +218,7 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
213
218
|
callerContext,
|
|
214
219
|
env,
|
|
215
220
|
requestId,
|
|
221
|
+
schema,
|
|
216
222
|
storeId,
|
|
217
223
|
values,
|
|
218
224
|
}: {
|
|
@@ -220,7 +226,8 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
220
226
|
callerContext: { bindingName: string; durableObjectId: string }
|
|
221
227
|
requestId: string
|
|
222
228
|
storeId: string
|
|
223
|
-
|
|
229
|
+
schema: Schema.Top
|
|
230
|
+
values: ReadonlyArray.NonEmptyReadonlyArray<unknown>
|
|
224
231
|
}) {
|
|
225
232
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CF worker env bindings are typed as Record<string, any>; narrowing to known DO namespace
|
|
226
233
|
const clientDoNamespace = env[callerContext.bindingName] as
|
|
@@ -233,12 +240,15 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
233
240
|
|
|
234
241
|
const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
|
|
235
242
|
|
|
236
|
-
const
|
|
237
|
-
const parser =
|
|
243
|
+
const serialization = yield* schemaBinarySerialization
|
|
244
|
+
const parser = serialization.makeUnsafe()
|
|
245
|
+
const encodedValues = yield* Schema.encodeUnknownEffect(
|
|
246
|
+
erasedCodec(serialization.codecFor, Schema.NonEmptyArray(schema)),
|
|
247
|
+
)(values)
|
|
248
|
+
const res = { _tag: 'Chunk', requestId, values: encodedValues }
|
|
238
249
|
// Native Cloudflare RPC rejects schema values with custom prototypes. Keep the callback
|
|
239
250
|
// boundary clone-safe by sending the already-encoded Effect RPC message as bytes.
|
|
240
|
-
|
|
241
|
-
const serializedRes = parser.encode(res) as Uint8Array<ArrayBuffer>
|
|
251
|
+
const serializedRes = encodeBytes(parser, res)
|
|
242
252
|
|
|
243
253
|
yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(serializedRes, storeId))
|
|
244
254
|
})
|
|
@@ -252,7 +262,8 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
252
262
|
entry: Rpc.Handler<Rpcs['_tag']>,
|
|
253
263
|
requestId: RpcMessage.RequestId,
|
|
254
264
|
payload: unknown,
|
|
255
|
-
parser:
|
|
265
|
+
parser: RpcSerialization.Parser,
|
|
266
|
+
codecFor: RpcSerialization.CodecFor,
|
|
256
267
|
layer: Layer.Layer<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>, LE>,
|
|
257
268
|
): Effect.Effect<CfTypes.ReadableStream, never, Scope.Scope> =>
|
|
258
269
|
Effect.gen(function* () {
|
|
@@ -265,11 +276,15 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
265
276
|
}),
|
|
266
277
|
rpc,
|
|
267
278
|
})
|
|
268
|
-
const effectOrStream
|
|
279
|
+
const effectOrStream: Effect.Effect<unknown, unknown> | Stream.Stream<unknown, unknown> =
|
|
280
|
+
Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
|
|
269
281
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- dynamically dispatched handlers have an unknown error; orDie converts it to a defect handled by the downstream catchCause
|
|
283
|
+
const result = Effect.isEffect(effectOrStream) === true ? yield* Effect.orDie(effectOrStream) : effectOrStream
|
|
284
|
+
if (Stream.isStream(result) === false) {
|
|
285
|
+
return yield* Effect.die(`Streaming RPC ${rpc._tag} did not produce a Stream`)
|
|
286
|
+
}
|
|
287
|
+
const stream = result
|
|
273
288
|
|
|
274
289
|
// Get the stream schemas for proper chunk-level encoding
|
|
275
290
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.Handler doesn't expose successSchema publicly; see https://github.com/Effect-TS/effect/issues/6064
|
|
@@ -282,10 +297,12 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
282
297
|
: Option.none()
|
|
283
298
|
const arrayEncoder =
|
|
284
299
|
Option.isSome(streamSchemas) === true
|
|
285
|
-
? Schema.encodeUnknownEffect(
|
|
286
|
-
: Schema.encodeUnknownEffect(
|
|
300
|
+
? Schema.encodeUnknownEffect(erasedCodec(codecFor, Schema.NonEmptyArray(streamSchemas.value.success)))
|
|
301
|
+
: Schema.encodeUnknownEffect(erasedCodec(codecFor, Schema.NonEmptyArray(Schema.Any)))
|
|
287
302
|
|
|
288
303
|
// Convert stream to ReadableStream
|
|
304
|
+
let producer: Fiber.Fiber<void> | undefined
|
|
305
|
+
let cancelled = false
|
|
289
306
|
const readableStream = new ReadableStream({
|
|
290
307
|
start(controller) {
|
|
291
308
|
// Run the stream and send chunks + final exit
|
|
@@ -304,9 +321,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
304
321
|
values: encodedValues,
|
|
305
322
|
}
|
|
306
323
|
|
|
307
|
-
|
|
308
|
-
const serialized = parser.encode([chunkMessage]) as Uint8Array<ArrayBuffer>
|
|
309
|
-
controller.enqueue(serialized)
|
|
324
|
+
controller.enqueue(encodeBytes(parser, [chunkMessage]))
|
|
310
325
|
}),
|
|
311
326
|
)
|
|
312
327
|
|
|
@@ -314,7 +329,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
314
329
|
const rawExit = Exit.void
|
|
315
330
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
316
331
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
317
|
-
const encodedExit = yield* Schema.encodeUnknownEffect(
|
|
332
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(codecFor, exitSchema))(rawExit)
|
|
318
333
|
|
|
319
334
|
const exitMessage = {
|
|
320
335
|
_tag: 'Exit' as const,
|
|
@@ -322,18 +337,17 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
322
337
|
exit: encodedExit,
|
|
323
338
|
}
|
|
324
339
|
|
|
325
|
-
|
|
326
|
-
const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
|
|
327
|
-
controller.enqueue(exitSerialized)
|
|
340
|
+
controller.enqueue(encodeBytes(parser, [exitMessage]))
|
|
328
341
|
controller.close()
|
|
329
342
|
}).pipe(
|
|
330
343
|
Effect.catchCause((cause) =>
|
|
331
344
|
Effect.gen(function* () {
|
|
345
|
+
if (cancelled === true) return
|
|
332
346
|
// Send error exit with proper schema encoding
|
|
333
347
|
const rawExit = Exit.failCause(cause)
|
|
334
348
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
335
349
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
336
|
-
const encodedExit = yield* Schema.encodeUnknownEffect(
|
|
350
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(codecFor, exitSchema))(rawExit)
|
|
337
351
|
|
|
338
352
|
const exitMessage = {
|
|
339
353
|
_tag: 'Exit' as const,
|
|
@@ -341,23 +355,26 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
341
355
|
exit: encodedExit,
|
|
342
356
|
}
|
|
343
357
|
|
|
344
|
-
|
|
345
|
-
const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
|
|
346
|
-
controller.enqueue(exitSerialized)
|
|
358
|
+
controller.enqueue(encodeBytes(parser, [exitMessage]))
|
|
347
359
|
controller.close()
|
|
348
360
|
}),
|
|
349
361
|
),
|
|
350
362
|
)
|
|
351
363
|
|
|
352
364
|
// Run the stream processing
|
|
353
|
-
runStream.pipe(
|
|
365
|
+
producer = runStream.pipe(
|
|
354
366
|
Effect.provide(layer),
|
|
355
367
|
Effect.scoped,
|
|
356
368
|
Effect.tapCauseLogPretty,
|
|
357
369
|
(_) => _ as Effect.Effect<void>,
|
|
358
|
-
Effect.
|
|
370
|
+
Effect.runFork,
|
|
359
371
|
)
|
|
360
372
|
},
|
|
373
|
+
cancel() {
|
|
374
|
+
cancelled = true
|
|
375
|
+
// @effect-diagnostics-next-line runEffectInsideEffect:off -- native stream callback runs outside Effect; interruption needs no services
|
|
376
|
+
return producer === undefined ? undefined : Effect.runPromise(Fiber.interrupt(producer))
|
|
377
|
+
},
|
|
361
378
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridging standard Web API ReadableStream to Cloudflare Worker ReadableStream type
|
|
362
379
|
}) as any as CfTypes.ReadableStream
|
|
363
380
|
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
RpcMessage,
|
|
29
29
|
RpcSerialization,
|
|
30
30
|
RpcServer,
|
|
31
|
+
Schema,
|
|
31
32
|
Scope,
|
|
32
33
|
Stream,
|
|
33
34
|
} from '@livestore/utils/effect'
|
|
@@ -71,6 +72,10 @@ export interface DurableObjectWebSocketRpcConfig {
|
|
|
71
72
|
rpcLayer: Layer.Layer<never, never, RpcServer.Protocol | WsContext>
|
|
72
73
|
/** Function to get access to incoming requests */
|
|
73
74
|
onMessage?: (msg: RpcMessage.FromClientEncoded, ws: CfTypes.WebSocket) => void
|
|
75
|
+
/** Called after a terminal RPC response has been accepted by the serializer. */
|
|
76
|
+
onRequestExit?: (requestId: string | number, ws: CfTypes.WebSocket) => void
|
|
77
|
+
/** Called after a connection-level RPC defect has been accepted by the serializer. */
|
|
78
|
+
onProtocolDefect?: (ws: CfTypes.WebSocket) => void
|
|
74
79
|
mainLayer?: Layer.Layer<never>
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -139,6 +144,8 @@ export const setupDurableObjectWebSocketRpc = ({
|
|
|
139
144
|
rpcLayer,
|
|
140
145
|
webSocketMode,
|
|
141
146
|
onMessage,
|
|
147
|
+
onRequestExit,
|
|
148
|
+
onProtocolDefect,
|
|
142
149
|
mainLayer,
|
|
143
150
|
}: DurableObjectWebSocketRpcConfig) => {
|
|
144
151
|
if (webSocketMode === 'accept') {
|
|
@@ -171,7 +178,7 @@ export const setupDurableObjectWebSocketRpc = ({
|
|
|
171
178
|
ws,
|
|
172
179
|
scope,
|
|
173
180
|
incomingQueue,
|
|
174
|
-
...omitUndefineds({ onMessage }),
|
|
181
|
+
...omitUndefineds({ onMessage, onRequestExit, onProtocolDefect }),
|
|
175
182
|
}).pipe(Layer.provide(RpcSerialization.layerJson))
|
|
176
183
|
|
|
177
184
|
const ServerLive = rpcLayer.pipe(Layer.provide(ProtocolLive))
|
|
@@ -239,6 +246,8 @@ export interface WsRpcServerArgs {
|
|
|
239
246
|
ws: CfTypes.WebSocket
|
|
240
247
|
scope: Scope.Scope
|
|
241
248
|
onMessage?: (message: RpcMessage.FromClientEncoded, ws: CfTypes.WebSocket) => void
|
|
249
|
+
onRequestExit?: (requestId: string | number, ws: CfTypes.WebSocket) => void
|
|
250
|
+
onProtocolDefect?: (ws: CfTypes.WebSocket) => void
|
|
242
251
|
/** Queue for receiving incoming messages from the WebSocket */
|
|
243
252
|
incomingQueue: Queue.Queue<Uint8Array | string>
|
|
244
253
|
}
|
|
@@ -274,30 +283,79 @@ export const layerRpcServerWebsocket = (args: WsRpcServerArgs) =>
|
|
|
274
283
|
*
|
|
275
284
|
* @internal Used internally by `layerRpcServerWebsocket`
|
|
276
285
|
*/
|
|
277
|
-
const makeSocketProtocol = ({
|
|
286
|
+
const makeSocketProtocol = ({
|
|
287
|
+
incomingQueue,
|
|
288
|
+
scope,
|
|
289
|
+
ws,
|
|
290
|
+
onMessage,
|
|
291
|
+
onRequestExit,
|
|
292
|
+
onProtocolDefect,
|
|
293
|
+
}: WsRpcServerArgs) =>
|
|
278
294
|
Effect.gen(function* () {
|
|
279
295
|
const serialization = yield* RpcSerialization.RpcSerialization
|
|
280
296
|
const disconnects = yield* Queue.unbounded<number>()
|
|
297
|
+
const requestIdsWithSchemas = new Set<string | number>()
|
|
298
|
+
const interruptedExitEncoded = yield* Schema.encodeUnknownEffect(
|
|
299
|
+
serialization.codecFor(Schema.Exit(Schema.Void, Schema.Never, Schema.Never)),
|
|
300
|
+
)(Exit.interrupt()).pipe(Effect.orDie)
|
|
281
301
|
|
|
282
|
-
const writeRaw = (msg: Uint8Array | string) => Effect.
|
|
302
|
+
const writeRaw = (msg: Uint8Array | string) => Effect.sync(() => ws.send(msg))
|
|
283
303
|
|
|
284
304
|
let writeRequest!: (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect<void>
|
|
285
305
|
|
|
286
306
|
const parser = serialization.makeUnsafe()
|
|
287
307
|
const id = 0
|
|
288
308
|
|
|
289
|
-
const
|
|
309
|
+
const runLifecycleHook = (name: string, hook: () => void) =>
|
|
310
|
+
Effect.sync(hook).pipe(
|
|
311
|
+
Effect.catchCause((cause) => Effect.logError(`WebSocket RPC ${name} hook failed`, { cause })),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
const writeDefect = (defect: unknown) => {
|
|
315
|
+
try {
|
|
316
|
+
const encoded = parser.encode(RpcMessage.ResponseDefectEncoded(defect))
|
|
317
|
+
const responseEffect = encoded === undefined ? Effect.void : Effect.orDie(writeRaw(encoded))
|
|
318
|
+
const onAccepted = Effect.sync(() => requestIdsWithSchemas.clear()).pipe(
|
|
319
|
+
Effect.andThen(
|
|
320
|
+
onProtocolDefect !== undefined
|
|
321
|
+
? runLifecycleHook('onProtocolDefect', () => onProtocolDefect(ws))
|
|
322
|
+
: Effect.void,
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
return Effect.ensuring(responseEffect, onAccepted)
|
|
326
|
+
} catch (cause) {
|
|
327
|
+
return Effect.die(cause)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const writeResponse = (response: unknown, onAccepted?: Effect.Effect<void>) => {
|
|
290
332
|
try {
|
|
291
333
|
const encoded = parser.encode(response)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
return Effect.orDie(writeRaw(encoded))
|
|
334
|
+
const responseEffect = encoded === undefined ? Effect.void : Effect.orDie(writeRaw(encoded))
|
|
335
|
+
return onAccepted === undefined ? responseEffect : Effect.ensuring(responseEffect, onAccepted)
|
|
296
336
|
} catch (cause) {
|
|
297
|
-
return
|
|
337
|
+
return writeDefect(cause)
|
|
298
338
|
}
|
|
299
339
|
}
|
|
300
340
|
|
|
341
|
+
const writeExit = (requestId: string | number, exit: unknown) =>
|
|
342
|
+
writeResponse(
|
|
343
|
+
{ _tag: 'Exit', requestId, exit },
|
|
344
|
+
Effect.sync(() => requestIdsWithSchemas.delete(requestId)).pipe(
|
|
345
|
+
Effect.andThen(
|
|
346
|
+
onRequestExit !== undefined
|
|
347
|
+
? runLifecycleHook('onRequestExit', () => onRequestExit(requestId, ws))
|
|
348
|
+
: Effect.void,
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
const write = (response: RpcMessage.FromServerEncoded) => {
|
|
354
|
+
if (response._tag === 'Exit') return writeExit(response.requestId, response.exit)
|
|
355
|
+
if (response._tag === 'Defect') return writeDefect(response.defect)
|
|
356
|
+
return writeResponse(response)
|
|
357
|
+
}
|
|
358
|
+
|
|
301
359
|
const protocol = yield* RpcServer.Protocol.make((writeRequest_) => {
|
|
302
360
|
writeRequest = writeRequest_
|
|
303
361
|
|
|
@@ -312,15 +370,22 @@ const makeSocketProtocol = ({ incomingQueue, scope, ws, onMessage }: WsRpcServer
|
|
|
312
370
|
while: () => i < decoded.length,
|
|
313
371
|
body: () => {
|
|
314
372
|
const request = decoded[i++]!
|
|
315
|
-
if (onMessage !== undefined)
|
|
316
|
-
|
|
373
|
+
if (onMessage !== undefined) onMessage(request, ws)
|
|
374
|
+
|
|
375
|
+
if (request._tag === 'Request') {
|
|
376
|
+
requestIdsWithSchemas.add(request.id)
|
|
377
|
+
} else if (request._tag === 'Interrupt' && requestIdsWithSchemas.has(request.requestId) === false) {
|
|
378
|
+
// Effect creates this Exit for an unknown request, but its encoder drops it when hibernation has
|
|
379
|
+
// discarded the request schema.
|
|
380
|
+
return writeExit(request.requestId, interruptedExitEncoded)
|
|
317
381
|
}
|
|
382
|
+
|
|
318
383
|
return writeRequest(id, request)
|
|
319
384
|
},
|
|
320
385
|
step: Function.constVoid,
|
|
321
386
|
})
|
|
322
387
|
} catch (cause) {
|
|
323
|
-
return
|
|
388
|
+
return writeDefect(cause)
|
|
324
389
|
}
|
|
325
390
|
}),
|
|
326
391
|
Stream.runDrain,
|
|
@@ -342,6 +407,7 @@ const makeSocketProtocol = ({ incomingQueue, scope, ws, onMessage }: WsRpcServer
|
|
|
342
407
|
supportsTransferables: false,
|
|
343
408
|
supportsSpanPropagation: true,
|
|
344
409
|
supportsNotifications: true,
|
|
410
|
+
codecFor: serialization.codecFor,
|
|
345
411
|
}))
|
|
346
412
|
})
|
|
347
413
|
|