@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.
Files changed (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/declare/cf-declare.d.ts +53 -6
  3. package/dist/declare/cf-declare.d.ts.map +1 -1
  4. package/dist/declare/cf-declare.js +9 -10
  5. package/dist/declare/cf-declare.js.map +1 -1
  6. package/dist/do-rpc/client.d.ts +1 -1
  7. package/dist/do-rpc/client.d.ts.map +1 -1
  8. package/dist/do-rpc/client.js +10 -17
  9. package/dist/do-rpc/client.js.map +1 -1
  10. package/dist/do-rpc/rpc.test.js +3 -3
  11. package/dist/do-rpc/rpc.test.js.map +1 -1
  12. package/dist/do-rpc/server.d.ts +1 -1
  13. package/dist/do-rpc/server.d.ts.map +1 -1
  14. package/dist/do-rpc/server.js +31 -21
  15. package/dist/do-rpc/server.js.map +1 -1
  16. package/dist/do-rpc/test-fixtures/worker.d.ts.map +1 -1
  17. package/dist/do-rpc/test-fixtures/worker.js +1 -1
  18. package/dist/do-rpc/test-fixtures/worker.js.map +1 -1
  19. package/dist/ws-rpc/test-fixtures/worker.d.ts.map +1 -1
  20. package/dist/ws-rpc/test-fixtures/worker.js +2 -2
  21. package/dist/ws-rpc/test-fixtures/worker.js.map +1 -1
  22. package/dist/ws-rpc/ws-rpc-server.d.ts +45 -5
  23. package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
  24. package/dist/ws-rpc/ws-rpc-server.js +16 -6
  25. package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
  26. package/dist/ws-rpc/ws-rpc.test.js +3 -3
  27. package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
  28. package/package.json +46 -13
  29. package/src/declare/cf-declare.ts +18 -10
  30. package/src/do-rpc/README.md +1 -2
  31. package/src/do-rpc/client.ts +15 -19
  32. package/src/do-rpc/rpc.test.ts +5 -3
  33. package/src/do-rpc/server.ts +45 -32
  34. package/src/do-rpc/test-fixtures/worker.ts +4 -2
  35. package/src/ws-rpc/README.md +1 -1
  36. package/src/ws-rpc/test-fixtures/worker.ts +6 -4
  37. package/src/ws-rpc/ws-rpc-server.ts +38 -12
  38. package/src/ws-rpc/ws-rpc.test.ts +5 -3
  39. package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
  40. package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
  41. package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
  42. package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
  43. package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
  44. package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
@@ -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 (!rpc || !entry) {
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
- Effect.gen(function* () {
171
- const clientDoNamespace = env[callerContext.bindingName] as
172
- | CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>
173
- | undefined
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
- if (clientDoNamespace === undefined) {
176
- throw new Error(`Client DO namespace not found: ${callerContext.bindingName}`)
177
- }
183
+ if (clientDoNamespace === undefined) {
184
+ throw new Error(`Client DO namespace not found: ${callerContext.bindingName}`)
185
+ }
178
186
 
179
- const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
187
+ const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
180
188
 
181
- const res: RpcMessage.ResponseChunkEncoded = { _tag: 'Chunk', requestId, values }
189
+ const res: RpcMessage.ResponseChunkEncoded = { _tag: 'Chunk', requestId, values }
182
190
 
183
- yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(res))
184
- }).pipe(Effect.withSpan('do-rpc/emitStreamResponse'))
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, any, Scope.Scope> =>
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
- let stream: Stream.Stream<any, any, never>
207
- if (Effect.isEffect(handlerResult)) {
208
- // If handler returns Effect<Stream>, we need to run it to get the stream
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 = Option.isSome(streamSchemas)
218
- ? Schema.encodeUnknown(Schema.Array(streamSchemas.value.success as Schema.Schema<any>))
219
- : Schema.encodeUnknown(Schema.Array(Schema.Any as Schema.Schema<any>))
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
  }
@@ -19,4 +19,4 @@ This module provides a Effect RPC implementation for WebSocket connections hoste
19
19
 
20
20
  ```ts
21
21
  // See test-fixtures/worker.ts for an example
22
- ```
22
+ ```
@@ -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 (!upgradeHeader || upgradeHeader !== 'websocket') {
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
- /** Effect RPC layer that defines the available RPC methods and handlers */
48
- rpcLayer: Layer.Layer<never, never, RpcServer.Protocol>
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, never, 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<ArrayBufferLike> | string>()
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<ArrayBufferLike> | string)
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<ArrayBufferLike> | string>
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<ArrayBufferLike> | string) => Effect.succeed(ws.send(msg))
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
- import { Vitest } from '@livestore/utils-dev/node-vitest'
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