@livestore/common-cf 0.4.0-dev.21 → 0.4.0-dev.23
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/cf-declare.d.ts +53 -6
- package/dist/declare/cf-declare.d.ts.map +1 -1
- package/dist/declare/cf-declare.js +9 -10
- package/dist/declare/cf-declare.js.map +1 -1
- package/dist/do-rpc/client.d.ts +1 -1
- package/dist/do-rpc/client.d.ts.map +1 -1
- package/dist/do-rpc/client.js +10 -17
- package/dist/do-rpc/client.js.map +1 -1
- package/dist/do-rpc/rpc.test.js +3 -3
- package/dist/do-rpc/rpc.test.js.map +1 -1
- package/dist/do-rpc/server.d.ts +1 -1
- package/dist/do-rpc/server.d.ts.map +1 -1
- package/dist/do-rpc/server.js +31 -21
- package/dist/do-rpc/server.js.map +1 -1
- package/dist/do-rpc/test-fixtures/worker.d.ts.map +1 -1
- package/dist/do-rpc/test-fixtures/worker.js +1 -1
- package/dist/do-rpc/test-fixtures/worker.js.map +1 -1
- 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/test-fixtures/worker.js.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.d.ts +45 -5
- package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.js +16 -6
- package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
- package/dist/ws-rpc/ws-rpc.test.js +3 -3
- package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
- package/package.json +46 -13
- package/src/declare/cf-declare.ts +18 -10
- package/src/do-rpc/README.md +1 -2
- package/src/do-rpc/client.ts +15 -19
- package/src/do-rpc/rpc.test.ts +5 -3
- package/src/do-rpc/server.ts +45 -32
- package/src/do-rpc/test-fixtures/worker.ts +4 -2
- package/src/ws-rpc/README.md +1 -1
- package/src/ws-rpc/test-fixtures/worker.ts +6 -4
- package/src/ws-rpc/ws-rpc-server.ts +38 -12
- package/src/ws-rpc/ws-rpc.test.ts +5 -3
- package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
- package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
- package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
- package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
- package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
- package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
package/src/do-rpc/server.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type Scope,
|
|
16
16
|
Stream,
|
|
17
17
|
} from '@livestore/utils/effect'
|
|
18
|
+
|
|
18
19
|
import type * as CfTypes from '../cf-types.ts'
|
|
19
20
|
|
|
20
21
|
export interface ClientDoWithRpcCallback {
|
|
@@ -47,10 +48,10 @@ export const toDurableObjectHandler =
|
|
|
47
48
|
|
|
48
49
|
// Handle potential nested array from client serialization
|
|
49
50
|
let requests: RpcMessage.FromClient<Rpcs>[]
|
|
50
|
-
if (Array.isArray(decoded) && decoded.length === 1 && Array.isArray(decoded[0])) {
|
|
51
|
+
if (Array.isArray(decoded) === true && decoded.length === 1 && Array.isArray(decoded[0]) === true) {
|
|
51
52
|
// Double-wrapped array [[{...}]] -> [{...}]
|
|
52
53
|
requests = decoded[0]
|
|
53
|
-
} else if (Array.isArray(decoded)) {
|
|
54
|
+
} else if (Array.isArray(decoded) === true) {
|
|
54
55
|
// Single array [{...}]
|
|
55
56
|
requests = decoded
|
|
56
57
|
} else {
|
|
@@ -69,10 +70,12 @@ export const toDurableObjectHandler =
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
// Find the RPC handler
|
|
73
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RpcGroup.requests map returns Rpc.Any; narrowing to AnyWithProps for property access
|
|
72
74
|
const rpc = group.requests.get(request.tag)! as unknown as Rpc.AnyWithProps
|
|
75
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- context.unsafeMap dynamic lookup; type safety ensured by RpcGroup registration
|
|
73
76
|
const entry = context.unsafeMap.get(rpc.key) as Rpc.Handler<Rpcs['_tag']>
|
|
74
77
|
|
|
75
|
-
if (
|
|
78
|
+
if (rpc == null || entry == null) {
|
|
76
79
|
responses.push({
|
|
77
80
|
_tag: 'Exit',
|
|
78
81
|
requestId: request.id,
|
|
@@ -82,10 +85,11 @@ export const toDurableObjectHandler =
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
// Check if this is a streaming RPC
|
|
88
|
+
// 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
|
|
85
89
|
const isStream = RpcSchema.isStreamSchema((rpc as any).successSchema)
|
|
86
90
|
|
|
87
91
|
// For streaming RPCs with only one request, return ReadableStream directly
|
|
88
|
-
if (isStream && requests.length === 1) {
|
|
92
|
+
if (isStream === true && requests.length === 1) {
|
|
89
93
|
return yield* createStreamingResponse(rpc, entry, request, parser, options.layer)
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -99,17 +103,19 @@ export const toDurableObjectHandler =
|
|
|
99
103
|
})
|
|
100
104
|
|
|
101
105
|
let value: any
|
|
102
|
-
if (Effect.isEffect(handlerResult)) {
|
|
106
|
+
if (Effect.isEffect(handlerResult) === true) {
|
|
107
|
+
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch
|
|
103
108
|
value = yield* handlerResult
|
|
104
109
|
} else {
|
|
105
110
|
value = handlerResult
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
// Get the exit schema for this RPC
|
|
114
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
109
115
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
|
|
110
116
|
|
|
111
117
|
let encodedExit: any
|
|
112
|
-
if (exitSchema) {
|
|
118
|
+
if (exitSchema !== undefined) {
|
|
113
119
|
// Use schema encoding for proper serialization
|
|
114
120
|
const rawExit = Exit.succeed(value)
|
|
115
121
|
encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
|
|
@@ -126,11 +132,12 @@ export const toDurableObjectHandler =
|
|
|
126
132
|
}).pipe(
|
|
127
133
|
Effect.catchAllCause((cause) => {
|
|
128
134
|
// Get the exit schema for this RPC
|
|
135
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
129
136
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
|
|
130
137
|
|
|
131
138
|
return Effect.gen(function* () {
|
|
132
139
|
let encodedExit: any
|
|
133
|
-
if (exitSchema) {
|
|
140
|
+
if (exitSchema !== undefined) {
|
|
134
141
|
// Use schema encoding for proper serialization
|
|
135
142
|
const rawExit = Exit.failCause(cause)
|
|
136
143
|
encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
|
|
@@ -151,12 +158,13 @@ export const toDurableObjectHandler =
|
|
|
151
158
|
responses.push(result)
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
|
|
154
162
|
const encoded = parser.encode(responses) as Uint8Array<ArrayBuffer>
|
|
155
163
|
return encoded
|
|
156
164
|
}).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie)
|
|
157
165
|
|
|
158
166
|
/** Out-of-band RPC stream response emission back to the caller DO */
|
|
159
|
-
export const emitStreamResponse = ({
|
|
167
|
+
export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(function* ({
|
|
160
168
|
callerContext,
|
|
161
169
|
env,
|
|
162
170
|
requestId,
|
|
@@ -166,22 +174,22 @@ export const emitStreamResponse = ({
|
|
|
166
174
|
callerContext: { bindingName: string; durableObjectId: string }
|
|
167
175
|
requestId: string
|
|
168
176
|
values: NonEmptyArray<any>
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
}) {
|
|
178
|
+
// 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
|
|
179
|
+
const clientDoNamespace = env[callerContext.bindingName] as
|
|
180
|
+
| CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>
|
|
181
|
+
| undefined
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
if (clientDoNamespace === undefined) {
|
|
184
|
+
throw new Error(`Client DO namespace not found: ${callerContext.bindingName}`)
|
|
185
|
+
}
|
|
178
186
|
|
|
179
|
-
|
|
187
|
+
const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
const res: RpcMessage.ResponseChunkEncoded = { _tag: 'Chunk', requestId, values }
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(res))
|
|
192
|
+
})
|
|
185
193
|
|
|
186
194
|
/**
|
|
187
195
|
* Creates a ReadableStream response for streaming RPCs.
|
|
@@ -193,7 +201,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
193
201
|
request: any,
|
|
194
202
|
parser: ReturnType<typeof RpcSerialization.msgPack.unsafeMake>,
|
|
195
203
|
layer: Layer.Layer<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>, LE>,
|
|
196
|
-
): Effect.Effect<CfTypes.ReadableStream,
|
|
204
|
+
): Effect.Effect<CfTypes.ReadableStream, never, Scope.Scope> =>
|
|
197
205
|
Effect.gen(function* () {
|
|
198
206
|
// Execute the handler to get the stream
|
|
199
207
|
const handlerResult = entry.handler(request.payload, {
|
|
@@ -203,20 +211,19 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
203
211
|
}),
|
|
204
212
|
})
|
|
205
213
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
stream = yield* handlerResult
|
|
210
|
-
} else {
|
|
211
|
-
// Direct stream
|
|
212
|
-
stream = handlerResult
|
|
213
|
-
}
|
|
214
|
+
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch; orDie converts the error to a defect handled by the downstream catchAllCause
|
|
215
|
+
const stream: Stream.Stream<any, any> =
|
|
216
|
+
Effect.isEffect(handlerResult) === true ? yield* Effect.orDie(handlerResult) : handlerResult
|
|
214
217
|
|
|
215
218
|
// Get the stream schemas for proper chunk-level encoding
|
|
219
|
+
// 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
|
|
216
220
|
const streamSchemas = RpcSchema.getStreamSchemas((rpc as any).successSchema.ast)
|
|
217
|
-
const chunkEncoder =
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
const chunkEncoder =
|
|
222
|
+
Option.isSome(streamSchemas) === true
|
|
223
|
+
? // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- stream schema success type is inferred as unknown; cast needed for encodeUnknown
|
|
224
|
+
Schema.encodeUnknown(Schema.Array(streamSchemas.value.success as Schema.Schema<any>))
|
|
225
|
+
: // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Schema.Any needs explicit cast for Schema.Array compatibility
|
|
226
|
+
Schema.encodeUnknown(Schema.Array(Schema.Any as Schema.Schema<any>))
|
|
220
227
|
|
|
221
228
|
// Convert stream to ReadableStream
|
|
222
229
|
const readableStream = new ReadableStream({
|
|
@@ -238,6 +245,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
238
245
|
values: encodedValues,
|
|
239
246
|
}
|
|
240
247
|
|
|
248
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
|
|
241
249
|
const serialized = parser.encode([chunkMessage]) as Uint8Array<ArrayBuffer>
|
|
242
250
|
controller.enqueue(serialized)
|
|
243
251
|
}),
|
|
@@ -245,6 +253,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
245
253
|
|
|
246
254
|
// Send final exit message with proper schema encoding
|
|
247
255
|
const rawExit = Exit.void
|
|
256
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
248
257
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
|
|
249
258
|
const encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
|
|
250
259
|
|
|
@@ -254,6 +263,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
254
263
|
exit: encodedExit,
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
|
|
257
267
|
const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
|
|
258
268
|
controller.enqueue(exitSerialized)
|
|
259
269
|
controller.close()
|
|
@@ -262,6 +272,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
262
272
|
Effect.gen(function* () {
|
|
263
273
|
// Send error exit with proper schema encoding
|
|
264
274
|
const rawExit = Exit.failCause(cause)
|
|
275
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
265
276
|
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
|
|
266
277
|
const encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
|
|
267
278
|
|
|
@@ -271,6 +282,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
271
282
|
exit: encodedExit,
|
|
272
283
|
}
|
|
273
284
|
|
|
285
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
|
|
274
286
|
const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
|
|
275
287
|
controller.enqueue(exitSerialized)
|
|
276
288
|
controller.close()
|
|
@@ -281,6 +293,7 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
281
293
|
// Run the stream processing
|
|
282
294
|
runStream.pipe(Effect.provide(layer), Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
283
295
|
},
|
|
296
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridging standard Web API ReadableStream to Cloudflare Worker ReadableStream type
|
|
284
297
|
}) as any as CfTypes.ReadableStream
|
|
285
298
|
|
|
286
299
|
// yield* Effect.addFinalizer(() => Effect.promise(() => readableStream.cancel()))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
2
|
|
|
3
3
|
import { DurableObject } from 'cloudflare:workers'
|
|
4
|
+
|
|
4
5
|
import { layerProtocolDurableObject, toDurableObjectHandler } from '@livestore/common-cf'
|
|
5
6
|
import {
|
|
6
7
|
Effect,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
Schedule,
|
|
14
15
|
Stream,
|
|
15
16
|
} from '@livestore/utils/effect'
|
|
17
|
+
|
|
16
18
|
import { TestRpcs } from './rpc-schema.ts'
|
|
17
19
|
|
|
18
20
|
export interface Env {
|
|
@@ -20,7 +22,7 @@ export interface Env {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export class TestRpcDurableObject extends DurableObject {
|
|
23
|
-
__DURABLE_OBJECT_BRAND = 'TestRpcDurableObject' as never
|
|
25
|
+
override __DURABLE_OBJECT_BRAND = 'TestRpcDurableObject' as never
|
|
24
26
|
|
|
25
27
|
async rpc(payload: unknown): Promise<unknown> {
|
|
26
28
|
const TestRpcsLive = TestRpcs.toLayer({
|
|
@@ -139,7 +141,7 @@ export default {
|
|
|
139
141
|
headers: { 'Content-Type': 'text/plain' },
|
|
140
142
|
})
|
|
141
143
|
} catch (error) {
|
|
142
|
-
return new Response(`Error: ${error}`, { status: 500 })
|
|
144
|
+
return new Response(`Error: ${String(error)}`, { status: 500 })
|
|
143
145
|
}
|
|
144
146
|
},
|
|
145
147
|
}
|
package/src/ws-rpc/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
2
|
|
|
3
3
|
import { DurableObject } from 'cloudflare:workers'
|
|
4
|
+
|
|
4
5
|
import { Effect, Layer, Option, RpcServer, Schedule, Stream } from '@livestore/utils/effect'
|
|
6
|
+
|
|
5
7
|
import type * as CfTypes from '../../cf-types.ts'
|
|
6
8
|
import { setupDurableObjectWebSocketRpc } from '../ws-rpc-server.ts'
|
|
7
9
|
import { TestRpcs } from './rpc-schema.ts'
|
|
@@ -11,7 +13,7 @@ export interface Env {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export class TestRpcDurableObject extends DurableObject<Env, unknown> {
|
|
14
|
-
__DURABLE_OBJECT_BRAND = 'TestRpcDurableObject' as never
|
|
16
|
+
override __DURABLE_OBJECT_BRAND = 'TestRpcDurableObject' as never
|
|
15
17
|
|
|
16
18
|
constructor(state: DurableObjectState, env: Env) {
|
|
17
19
|
super(state, env)
|
|
@@ -59,7 +61,7 @@ export class TestRpcDurableObject extends DurableObject<Env, unknown> {
|
|
|
59
61
|
})
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
async fetch(request: Request): Promise<Response> {
|
|
64
|
+
override async fetch(request: Request): Promise<Response> {
|
|
63
65
|
const upgradeHeader = request.headers.get('Upgrade')
|
|
64
66
|
if (upgradeHeader === undefined || upgradeHeader !== 'websocket') {
|
|
65
67
|
return new Response('Durable Object expected Upgrade: websocket', { status: 426 })
|
|
@@ -81,7 +83,7 @@ export default {
|
|
|
81
83
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
82
84
|
try {
|
|
83
85
|
const upgradeHeader = request.headers.get('Upgrade')
|
|
84
|
-
if (
|
|
86
|
+
if (upgradeHeader == null || upgradeHeader !== 'websocket') {
|
|
85
87
|
return new Response('Durable Object expected Upgrade: websocket', { status: 426 })
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -89,7 +91,7 @@ export default {
|
|
|
89
91
|
|
|
90
92
|
return serverDO.fetch(request)
|
|
91
93
|
} catch (error) {
|
|
92
|
-
return new Response(`Error: ${error}`, { status: 500 })
|
|
94
|
+
return new Response(`Error: ${String(error)}`, { status: 500 })
|
|
93
95
|
}
|
|
94
96
|
},
|
|
95
97
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { notYetImplemented, omitUndefineds } from '@livestore/utils'
|
|
19
19
|
import {
|
|
20
|
+
Context,
|
|
20
21
|
constVoid,
|
|
21
22
|
Effect,
|
|
22
23
|
Exit,
|
|
@@ -30,8 +31,16 @@ import {
|
|
|
30
31
|
Scope,
|
|
31
32
|
Stream,
|
|
32
33
|
} from '@livestore/utils/effect'
|
|
34
|
+
|
|
33
35
|
import type * as CfTypes from '../cf-types.ts'
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Context service providing access to the current WebSocket.
|
|
39
|
+
* This is useful for reading WebSocket attachment data (e.g., forwarded headers)
|
|
40
|
+
* inside RPC handlers.
|
|
41
|
+
*/
|
|
42
|
+
export class WsContext extends Context.Tag('WsContext')<WsContext, { readonly ws: CfTypes.WebSocket }>() {}
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* Configuration options for setting up WebSocket RPC on a Durable Object.
|
|
37
46
|
*/
|
|
@@ -44,11 +53,25 @@ export interface DurableObjectWebSocketRpcConfig {
|
|
|
44
53
|
* - 'accept': Use traditional WebSocket handling (not yet implemented)
|
|
45
54
|
*/
|
|
46
55
|
webSocketMode: 'hibernate' | 'accept'
|
|
47
|
-
/**
|
|
48
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Effect RPC layer that requires `RpcServer.Protocol` (and `WsContext` if used)
|
|
58
|
+
* and provides the RPC server runtime.
|
|
59
|
+
*
|
|
60
|
+
* This is typically created by:
|
|
61
|
+
* ```typescript
|
|
62
|
+
* RpcServer.layer(MyRpcs).pipe(Layer.provide(handlersLayer))
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* `WsContext` is provided by the WebSocket protocol layer, so handlers can access
|
|
66
|
+
* WebSocket attachment data (e.g., forwarded headers stored after WebSocket upgrade).
|
|
67
|
+
*
|
|
68
|
+
* The layer requirements (`RIn`) must be a subset of `RpcServer.Protocol | WsContext`,
|
|
69
|
+
* which are both provided by the WebSocket protocol layer.
|
|
70
|
+
*/
|
|
71
|
+
rpcLayer: Layer.Layer<never, never, RpcServer.Protocol | WsContext>
|
|
49
72
|
/** Function to get access to incoming requests */
|
|
50
73
|
onMessage?: (msg: RpcMessage.FromClientEncoded, ws: CfTypes.WebSocket) => void
|
|
51
|
-
mainLayer?: Layer.Layer<never
|
|
74
|
+
mainLayer?: Layer.Layer<never>
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
/**
|
|
@@ -132,7 +155,7 @@ export const setupDurableObjectWebSocketRpc = ({
|
|
|
132
155
|
|
|
133
156
|
const launchServer = (ws: CfTypes.WebSocket) =>
|
|
134
157
|
Effect.gen(function* () {
|
|
135
|
-
if (serverCtxMap.has(ws)) {
|
|
158
|
+
if (serverCtxMap.has(ws) === true) {
|
|
136
159
|
return serverCtxMap.get(ws)!
|
|
137
160
|
}
|
|
138
161
|
|
|
@@ -140,7 +163,7 @@ export const setupDurableObjectWebSocketRpc = ({
|
|
|
140
163
|
|
|
141
164
|
const scope = yield* Scope.make()
|
|
142
165
|
|
|
143
|
-
const incomingQueue = yield* Mailbox.make<Uint8Array
|
|
166
|
+
const incomingQueue = yield* Mailbox.make<Uint8Array | string>()
|
|
144
167
|
|
|
145
168
|
yield* Scope.addFinalizer(scope, incomingQueue.shutdown)
|
|
146
169
|
|
|
@@ -160,7 +183,7 @@ export const setupDurableObjectWebSocketRpc = ({
|
|
|
160
183
|
scope,
|
|
161
184
|
onMessage: (message: string | ArrayBuffer) =>
|
|
162
185
|
incomingQueue
|
|
163
|
-
.offer(message as Uint8Array
|
|
186
|
+
.offer(message as Uint8Array | string)
|
|
164
187
|
.pipe(
|
|
165
188
|
Effect.asVoid,
|
|
166
189
|
Effect.withSpan('ws-rpc-server/onMessage', { root: true }),
|
|
@@ -190,7 +213,7 @@ export const setupDurableObjectWebSocketRpc = ({
|
|
|
190
213
|
const webSocketClose: CfTypes.DurableObject['webSocketClose'] = async (ws, _code, _reason, _wasClean) => {
|
|
191
214
|
const ctx = serverCtxMap.get(ws)
|
|
192
215
|
// console.log('webSocketClose', ctx, ws)
|
|
193
|
-
if (ctx) {
|
|
216
|
+
if (ctx !== undefined) {
|
|
194
217
|
await Scope.close(ctx.scope, Exit.void).pipe(Effect.runPromise)
|
|
195
218
|
serverCtxMap.delete(ws)
|
|
196
219
|
}
|
|
@@ -212,7 +235,7 @@ export interface WsRpcServerArgs {
|
|
|
212
235
|
ws: CfTypes.WebSocket
|
|
213
236
|
onMessage?: (message: RpcMessage.FromClientEncoded, ws: CfTypes.WebSocket) => void
|
|
214
237
|
/** Mailbox queue for receiving incoming messages from the WebSocket */
|
|
215
|
-
incomingQueue: Mailbox.Mailbox<Uint8Array
|
|
238
|
+
incomingQueue: Mailbox.Mailbox<Uint8Array | string>
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
/**
|
|
@@ -221,13 +244,16 @@ export interface WsRpcServerArgs {
|
|
|
221
244
|
* This layer handles the low-level WebSocket protocol details for RPC communication,
|
|
222
245
|
* including message serialization, routing, and error handling.
|
|
223
246
|
*
|
|
247
|
+
* Also provides `WsContext` with the current WebSocket so handlers can access
|
|
248
|
+
* WebSocket attachment data (e.g., forwarded headers).
|
|
249
|
+
*
|
|
224
250
|
* @param args Configuration for WebSocket RPC protocol
|
|
225
|
-
* @returns Effect layer that provides RPC server protocol functionality
|
|
251
|
+
* @returns Effect layer that provides RPC server protocol functionality and WsContext
|
|
226
252
|
*
|
|
227
253
|
* @internal This is typically used internally by `setupDurableObjectWebSocketRpc`
|
|
228
254
|
*/
|
|
229
255
|
export const layerRpcServerWebsocket = (args: WsRpcServerArgs) =>
|
|
230
|
-
Layer.scoped(RpcServer.Protocol, makeSocketProtocol(args))
|
|
256
|
+
Layer.mergeAll(Layer.scoped(RpcServer.Protocol, makeSocketProtocol(args)), Layer.succeed(WsContext, { ws: args.ws }))
|
|
231
257
|
|
|
232
258
|
/**
|
|
233
259
|
* Creates the low-level RPC protocol implementation for WebSocket communication.
|
|
@@ -245,7 +271,7 @@ const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =
|
|
|
245
271
|
const serialization = yield* RpcSerialization.RpcSerialization
|
|
246
272
|
const disconnects = yield* Mailbox.make<number>()
|
|
247
273
|
|
|
248
|
-
const writeRaw = (msg: Uint8Array
|
|
274
|
+
const writeRaw = (msg: Uint8Array | string) => Effect.succeed(ws.send(msg))
|
|
249
275
|
|
|
250
276
|
let writeRequest!: (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect<void>
|
|
251
277
|
|
|
@@ -278,7 +304,7 @@ const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =
|
|
|
278
304
|
while: () => i < decoded.length,
|
|
279
305
|
body: () => {
|
|
280
306
|
const request = decoded[i++]!
|
|
281
|
-
if (onMessage) {
|
|
307
|
+
if (onMessage !== undefined) {
|
|
282
308
|
onMessage(request, ws)
|
|
283
309
|
}
|
|
284
310
|
return writeRequest(id, request)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
import { WranglerDevServerService } from '@livestore/utils-dev/wrangler'
|
|
1
5
|
import {
|
|
2
6
|
Chunk,
|
|
3
7
|
Effect,
|
|
@@ -12,9 +16,7 @@ import {
|
|
|
12
16
|
Stream,
|
|
13
17
|
} from '@livestore/utils/effect'
|
|
14
18
|
import { PlatformNode } from '@livestore/utils/node'
|
|
15
|
-
|
|
16
|
-
import { WranglerDevServerService } from '@livestore/utils-dev/wrangler'
|
|
17
|
-
import { expect } from 'vitest'
|
|
19
|
+
|
|
18
20
|
import { TestRpcs } from './test-fixtures/rpc-schema.ts'
|
|
19
21
|
|
|
20
22
|
const testTimeout = 60_000
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|