@livestore/common-cf 0.4.0-dev.9 → 0.5.0-dev.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.
Files changed (58) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cf-types.js +2 -0
  3. package/dist/cf-types.js.map +1 -1
  4. package/dist/declare/cf-declare.d.ts +32 -5
  5. package/dist/declare/cf-declare.d.ts.map +1 -1
  6. package/dist/declare/cf-declare.js +9 -10
  7. package/dist/declare/cf-declare.js.map +1 -1
  8. package/dist/do-rpc/client.d.ts +1 -1
  9. package/dist/do-rpc/client.d.ts.map +1 -1
  10. package/dist/do-rpc/client.js +23 -46
  11. package/dist/do-rpc/client.js.map +1 -1
  12. package/dist/do-rpc/client.test.d.ts +2 -0
  13. package/dist/do-rpc/client.test.d.ts.map +1 -0
  14. package/dist/do-rpc/client.test.js +99 -0
  15. package/dist/do-rpc/client.test.js.map +1 -0
  16. package/dist/do-rpc/rpc.test.js +27 -59
  17. package/dist/do-rpc/rpc.test.js.map +1 -1
  18. package/dist/do-rpc/server.d.ts +10 -5
  19. package/dist/do-rpc/server.d.ts.map +1 -1
  20. package/dist/do-rpc/server.js +104 -54
  21. package/dist/do-rpc/server.js.map +1 -1
  22. package/dist/do-rpc/test-fixtures/rpc-schema.d.ts +27 -27
  23. package/dist/do-rpc/test-fixtures/rpc-schema.js +11 -11
  24. package/dist/do-rpc/test-fixtures/worker.d.ts.map +1 -1
  25. package/dist/do-rpc/test-fixtures/worker.js +10 -10
  26. package/dist/do-rpc/test-fixtures/worker.js.map +1 -1
  27. package/dist/ws-rpc/test-fixtures/rpc-schema.d.ts +27 -27
  28. package/dist/ws-rpc/test-fixtures/rpc-schema.js +9 -9
  29. package/dist/ws-rpc/test-fixtures/worker.d.ts.map +1 -1
  30. package/dist/ws-rpc/test-fixtures/worker.js +2 -2
  31. package/dist/ws-rpc/test-fixtures/worker.js.map +1 -1
  32. package/dist/ws-rpc/ws-rpc-server.d.ts +47 -6
  33. package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
  34. package/dist/ws-rpc/ws-rpc-server.js +30 -20
  35. package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
  36. package/dist/ws-rpc/ws-rpc.test.js +29 -61
  37. package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
  38. package/package.json +40 -16
  39. package/src/cf-types.ts +2 -0
  40. package/src/declare/cf-declare.ts +18 -10
  41. package/src/do-rpc/README.md +1 -2
  42. package/src/do-rpc/client.test.ts +122 -0
  43. package/src/do-rpc/client.ts +30 -50
  44. package/src/do-rpc/rpc.test.ts +33 -61
  45. package/src/do-rpc/server.ts +157 -80
  46. package/src/do-rpc/test-fixtures/rpc-schema.ts +11 -11
  47. package/src/do-rpc/test-fixtures/worker.ts +13 -14
  48. package/src/ws-rpc/README.md +1 -1
  49. package/src/ws-rpc/test-fixtures/rpc-schema.ts +9 -9
  50. package/src/ws-rpc/test-fixtures/worker.ts +6 -4
  51. package/src/ws-rpc/ws-rpc-server.ts +69 -34
  52. package/src/ws-rpc/ws-rpc.test.ts +35 -63
  53. package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
  54. package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
  55. package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
  56. package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
  57. package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
  58. package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
@@ -1,25 +1,43 @@
1
1
  import {
2
- Chunk,
3
2
  Effect,
4
3
  Exit,
5
4
  Headers,
6
5
  type Layer,
7
- type NonEmptyArray,
8
6
  Option,
7
+ type ReadonlyArray,
9
8
  Rpc,
10
9
  type RpcGroup,
11
- type RpcMessage,
10
+ RpcMessage,
12
11
  RpcSchema,
13
12
  RpcSerialization,
13
+ Result,
14
14
  Schema,
15
+ SchemaIssue,
15
16
  type Scope,
16
17
  Stream,
17
18
  } from '@livestore/utils/effect'
19
+
18
20
  import type * as CfTypes from '../cf-types.ts'
19
21
 
22
+ /**
23
+ * The erased dynamic RPC schemas (`Rpc.exitSchema(...) as Schema.Top`, `rpc.payloadSchema`) are
24
+ * plain data — they need NO decoding/encoding services. `Schema.Top` types those service channels
25
+ * as `unknown`, which surfaces as `unknown` in the requirements channel of `encode/decodeUnknownEffect`
26
+ * (anyUnknownInErrorContext / TS377030). Assert the true `never` services in ONE place.
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) =>
31
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- erased RPC data schemas require no codec services
32
+ Schema.toCodecJson(schema as Schema.Codec<unknown, unknown, never, never>)
33
+
20
34
  export interface ClientDoWithRpcCallback {
21
35
  __DURABLE_OBJECT_BRAND: never
22
- syncUpdateRpc: (payload: RpcMessage.ResponseChunkEncoded) => Promise<void>
36
+ /**
37
+ * The sync backend calls this to deliver a live update; `storeId` lets a rebuilt DO reload its
38
+ * store before delivering. See the Cloudflare Durable Object adapter docs for the recovery options.
39
+ */
40
+ syncUpdateRpc: (payload: Uint8Array<ArrayBuffer>, storeId: string) => Promise<void>
23
41
  }
24
42
 
25
43
  /**
@@ -40,17 +58,17 @@ export const toDurableObjectHandler =
40
58
  ) => Effect.Effect<Uint8Array<ArrayBuffer> | CfTypes.ReadableStream>) =>
41
59
  (serializedPayload) =>
42
60
  Effect.gen(function* () {
43
- const parser = RpcSerialization.msgPack.unsafeMake()
61
+ const parser = RpcSerialization.msgPack.makeUnsafe()
44
62
 
45
63
  // Decode incoming requests - client sends array of requests
46
64
  const decoded = parser.decode(serializedPayload)
47
65
 
48
66
  // Handle potential nested array from client serialization
49
- let requests: RpcMessage.FromClient<Rpcs>[]
50
- if (Array.isArray(decoded) && decoded.length === 1 && Array.isArray(decoded[0])) {
67
+ let requests: RpcMessage.FromClientEncoded[]
68
+ if (Array.isArray(decoded) === true && decoded.length === 1 && Array.isArray(decoded[0]) === true) {
51
69
  // Double-wrapped array [[{...}]] -> [{...}]
52
70
  requests = decoded[0]
53
- } else if (Array.isArray(decoded)) {
71
+ } else if (Array.isArray(decoded) === true) {
54
72
  // Single array [{...}]
55
73
  requests = decoded
56
74
  } else {
@@ -67,52 +85,83 @@ export const toDurableObjectHandler =
67
85
  if (request._tag !== 'Request') {
68
86
  continue
69
87
  }
88
+ const requestId = RpcMessage.RequestId(request.id)
70
89
 
71
90
  // Find the RPC handler
91
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RpcGroup.requests map returns Rpc.Any; narrowing to AnyWithProps for property access
72
92
  const rpc = group.requests.get(request.tag)! as unknown as Rpc.AnyWithProps
73
- const entry = context.unsafeMap.get(rpc.key) as Rpc.Handler<Rpcs['_tag']>
93
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- context.mapUnsafe dynamic lookup; type safety ensured by RpcGroup registration
94
+ const entry = context.mapUnsafe.get(rpc.key) as Rpc.Handler<Rpcs['_tag']>
74
95
 
75
- if (!rpc || !entry) {
96
+ if (rpc == null || entry == null) {
76
97
  responses.push({
77
98
  _tag: 'Exit',
78
- requestId: request.id,
99
+ requestId,
79
100
  exit: Exit.die(`Unknown request tag: ${request.tag}`),
80
101
  })
81
102
  continue
82
103
  }
83
104
 
105
+ const payloadResult = yield* Schema.decodeUnknownEffect(erasedJsonCodec(rpc.payloadSchema))(
106
+ request.payload,
107
+ ).pipe(Effect.provideContext(entry.context), Effect.result)
108
+
109
+ 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
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
113
+ const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
114
+ const rawExit = Exit.die(SchemaIssue.makeFormatterDefault()(payloadResult.failure.issue))
115
+ const encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit).pipe(
116
+ Effect.provideContext(entry.context),
117
+ )
118
+ responses.push({
119
+ _tag: 'Exit',
120
+ requestId,
121
+ exit: encodedExit,
122
+ })
123
+ continue
124
+ }
125
+
126
+ const payload = payloadResult.success
127
+
84
128
  // Check if this is a streaming RPC
85
- const isStream = RpcSchema.isStreamSchema((rpc as any).successSchema)
129
+ const isStream = RpcSchema.isStreamSchema(rpc.successSchema)
86
130
 
87
131
  // For streaming RPCs with only one request, return ReadableStream directly
88
- if (isStream && requests.length === 1) {
89
- return yield* createStreamingResponse(rpc, entry, request, parser, options.layer)
132
+ if (isStream === true && requests.length === 1) {
133
+ return yield* createStreamingResponse(rpc, entry, requestId, payload, parser, options.layer)
90
134
  }
91
135
 
92
136
  // Execute the handler
93
137
  const result = yield* Effect.gen(function* () {
94
- const handlerResult = entry.handler(request.payload, {
95
- clientId: 0, // TODO: add proper clientId if needed
138
+ const handlerResult = entry.handler(payload, {
139
+ client: new Rpc.ServerClient(0), // TODO: add proper clientId if needed
140
+ requestId,
96
141
  headers: Headers.fromInput({
97
- 'x-rpc-request-id': request.id.toString(),
142
+ 'x-rpc-request-id': requestId.toString(),
98
143
  }),
144
+ rpc,
99
145
  })
146
+ const effectOrStream = Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
100
147
 
101
148
  let value: any
102
- if (Effect.isEffect(handlerResult)) {
103
- value = yield* handlerResult
149
+ if (Effect.isEffect(effectOrStream) === true) {
150
+ // @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch
151
+ value = yield* effectOrStream
104
152
  } else {
105
- value = handlerResult
153
+ value = effectOrStream
106
154
  }
107
155
 
108
156
  // Get the exit schema for this RPC
109
- const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
157
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
158
+ const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
110
159
 
111
160
  let encodedExit: any
112
- if (exitSchema) {
161
+ if (exitSchema !== undefined) {
113
162
  // Use schema encoding for proper serialization
114
163
  const rawExit = Exit.succeed(value)
115
- encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
164
+ encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
116
165
  } else {
117
166
  // Fallback to direct exit
118
167
  encodedExit = Exit.succeed(value)
@@ -120,20 +169,21 @@ export const toDurableObjectHandler =
120
169
 
121
170
  return {
122
171
  _tag: 'Exit' as const,
123
- requestId: request.id,
172
+ requestId,
124
173
  exit: encodedExit,
125
174
  }
126
175
  }).pipe(
127
- Effect.catchAllCause((cause) => {
176
+ Effect.catchCause((cause) => {
128
177
  // Get the exit schema for this RPC
129
- const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
178
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
179
+ const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
130
180
 
131
181
  return Effect.gen(function* () {
132
182
  let encodedExit: any
133
- if (exitSchema) {
183
+ if (exitSchema !== undefined) {
134
184
  // Use schema encoding for proper serialization
135
185
  const rawExit = Exit.failCause(cause)
136
- encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
186
+ encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
137
187
  } else {
138
188
  // Fallback to direct exit
139
189
  encodedExit = Exit.failCause(cause)
@@ -141,7 +191,7 @@ export const toDurableObjectHandler =
141
191
 
142
192
  return {
143
193
  _tag: 'Exit' as const,
144
- requestId: request.id,
194
+ requestId,
145
195
  exit: encodedExit,
146
196
  }
147
197
  })
@@ -151,37 +201,47 @@ export const toDurableObjectHandler =
151
201
  responses.push(result)
152
202
  }
153
203
 
204
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
154
205
  const encoded = parser.encode(responses) as Uint8Array<ArrayBuffer>
155
206
  return encoded
156
- }).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie)
207
+ }).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie) as Effect.Effect<
208
+ Uint8Array<ArrayBuffer> | CfTypes.ReadableStream
209
+ >
157
210
 
158
211
  /** Out-of-band RPC stream response emission back to the caller DO */
159
- export const emitStreamResponse = ({
212
+ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(function* ({
160
213
  callerContext,
161
214
  env,
162
215
  requestId,
216
+ storeId,
163
217
  values,
164
218
  }: {
165
219
  env: Record<string, any>
166
220
  callerContext: { bindingName: string; durableObjectId: string }
167
221
  requestId: string
168
- values: NonEmptyArray<any>
169
- }) =>
170
- Effect.gen(function* () {
171
- const clientDoNamespace = env[callerContext.bindingName] as
172
- | CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>
173
- | undefined
174
-
175
- if (clientDoNamespace === undefined) {
176
- throw new Error(`Client DO namespace not found: ${callerContext.bindingName}`)
177
- }
178
-
179
- const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
180
-
181
- const res: RpcMessage.ResponseChunkEncoded = { _tag: 'Chunk', requestId, values }
182
-
183
- yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(res))
184
- }).pipe(Effect.withSpan('do-rpc/emitStreamResponse'))
222
+ storeId: string
223
+ values: ReadonlyArray.NonEmptyReadonlyArray<any>
224
+ }) {
225
+ // 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
+ const clientDoNamespace = env[callerContext.bindingName] as
227
+ | CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>
228
+ | undefined
229
+
230
+ if (clientDoNamespace === undefined) {
231
+ throw new Error(`Client DO namespace not found: ${callerContext.bindingName}`)
232
+ }
233
+
234
+ const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
235
+
236
+ const res: RpcMessage.ResponseChunkEncoded = { _tag: 'Chunk', requestId, values }
237
+ const parser = RpcSerialization.msgPack.makeUnsafe()
238
+ // Native Cloudflare RPC rejects schema values with custom prototypes. Keep the callback
239
+ // boundary clone-safe by sending the already-encoded Effect RPC message as bytes.
240
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; the encoded result is a byte payload
241
+ const serializedRes = parser.encode(res) as Uint8Array<ArrayBuffer>
242
+
243
+ yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(serializedRes, storeId))
244
+ })
185
245
 
186
246
  /**
187
247
  * Creates a ReadableStream response for streaming RPCs.
@@ -190,33 +250,40 @@ export const emitStreamResponse = ({
190
250
  const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
191
251
  rpc: Rpc.AnyWithProps,
192
252
  entry: Rpc.Handler<Rpcs['_tag']>,
193
- request: any,
194
- parser: ReturnType<typeof RpcSerialization.msgPack.unsafeMake>,
253
+ requestId: RpcMessage.RequestId,
254
+ payload: unknown,
255
+ parser: ReturnType<typeof RpcSerialization.msgPack.makeUnsafe>,
195
256
  layer: Layer.Layer<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>, LE>,
196
- ): Effect.Effect<CfTypes.ReadableStream, any, Scope.Scope> =>
257
+ ): Effect.Effect<CfTypes.ReadableStream, never, Scope.Scope> =>
197
258
  Effect.gen(function* () {
198
259
  // Execute the handler to get the stream
199
- const handlerResult = entry.handler(request.payload, {
200
- clientId: 0, // TODO: add proper clientId if needed
260
+ const handlerResult = entry.handler(payload, {
261
+ client: new Rpc.ServerClient(0), // TODO: add proper clientId if needed
262
+ requestId,
201
263
  headers: Headers.fromInput({
202
- 'x-rpc-request-id': request.id.toString(),
264
+ 'x-rpc-request-id': requestId.toString(),
203
265
  }),
266
+ rpc,
204
267
  })
268
+ const effectOrStream = Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
205
269
 
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
- }
270
+ const stream: Stream.Stream<any, any> =
271
+ // @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 catchCause
272
+ Effect.isEffect(effectOrStream) === true ? yield* Effect.orDie(effectOrStream) : effectOrStream
214
273
 
215
274
  // Get the stream schemas for proper chunk-level encoding
216
- const streamSchemas = RpcSchema.getStreamSchemas((rpc as any).successSchema.ast)
217
- const chunkEncoder = Option.isSome(streamSchemas)
218
- ? Schema.encodeUnknown(Schema.Array(streamSchemas.value.success))
219
- : Schema.encodeUnknown(Schema.Array(Schema.Any))
275
+ // 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
276
+ const streamSchemas =
277
+ RpcSchema.isStreamSchema(rpc.successSchema) === true
278
+ ? Option.some({
279
+ success: rpc.successSchema.success,
280
+ error: rpc.successSchema.error,
281
+ })
282
+ : Option.none()
283
+ const arrayEncoder =
284
+ Option.isSome(streamSchemas) === true
285
+ ? Schema.encodeUnknownEffect(Schema.toCodecJson(Schema.Array(streamSchemas.value.success)))
286
+ : Schema.encodeUnknownEffect(Schema.toCodecJson(Schema.Array(Schema.Any)))
220
287
 
221
288
  // Convert stream to ReadableStream
222
289
  const readableStream = new ReadableStream({
@@ -224,20 +291,20 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
224
291
  // Run the stream and send chunks + final exit
225
292
  const runStream = Effect.gen(function* () {
226
293
  // Process stream chunks - let chunk encoder handle Effect objects properly
227
- yield* Stream.runForEachChunk(stream, (chunk) =>
294
+ yield* Stream.runForEachArray(stream, (array) =>
228
295
  Effect.gen(function* () {
229
- const chunkArray = Chunk.toReadonlyArray(chunk)
230
- if (chunkArray.length === 0) return
296
+ if (array.length === 0) return
231
297
 
232
298
  // Encode the chunk using the proper chunk encoder (like official RPC)
233
- const encodedValues = yield* chunkEncoder(chunkArray)
299
+ const encodedValues = yield* arrayEncoder(array)
234
300
 
235
301
  const chunkMessage = {
236
302
  _tag: 'Chunk' as const,
237
- requestId: request.id,
303
+ requestId,
238
304
  values: encodedValues,
239
305
  }
240
306
 
307
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
241
308
  const serialized = parser.encode([chunkMessage]) as Uint8Array<ArrayBuffer>
242
309
  controller.enqueue(serialized)
243
310
  }),
@@ -245,32 +312,36 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
245
312
 
246
313
  // Send final exit message with proper schema encoding
247
314
  const rawExit = Exit.void
248
- const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
249
- const encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
315
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
316
+ const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
317
+ const encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
250
318
 
251
319
  const exitMessage = {
252
320
  _tag: 'Exit' as const,
253
- requestId: request.id,
321
+ requestId,
254
322
  exit: encodedExit,
255
323
  }
256
324
 
325
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
257
326
  const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
258
327
  controller.enqueue(exitSerialized)
259
328
  controller.close()
260
329
  }).pipe(
261
- Effect.catchAllCause((cause) =>
330
+ Effect.catchCause((cause) =>
262
331
  Effect.gen(function* () {
263
332
  // Send error exit with proper schema encoding
264
333
  const rawExit = Exit.failCause(cause)
265
- const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Schema<any>
266
- const encodedExit = yield* Schema.encodeUnknown(exitSchema)(rawExit)
334
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
335
+ const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
336
+ const encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
267
337
 
268
338
  const exitMessage = {
269
339
  _tag: 'Exit' as const,
270
- requestId: request.id,
340
+ requestId,
271
341
  exit: encodedExit,
272
342
  }
273
343
 
344
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
274
345
  const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
275
346
  controller.enqueue(exitSerialized)
276
347
  controller.close()
@@ -279,9 +350,15 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
279
350
  )
280
351
 
281
352
  // Run the stream processing
282
- // @ts-expect-error - Complex context requirements but functionality works correctly
283
- runStream.pipe(Effect.provide(layer), Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
353
+ runStream.pipe(
354
+ Effect.provide(layer),
355
+ Effect.scoped,
356
+ Effect.tapCauseLogPretty,
357
+ (_) => _ as Effect.Effect<void>,
358
+ Effect.runPromise,
359
+ )
284
360
  },
361
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridging standard Web API ReadableStream to Cloudflare Worker ReadableStream type
285
362
  }) as any as CfTypes.ReadableStream
286
363
 
287
364
  // yield* Effect.addFinalizer(() => Effect.promise(() => readableStream.cancel()))
@@ -10,8 +10,8 @@ export class TestRpcs extends RpcGroup.make(
10
10
  success: Schema.Struct({ echo: Schema.String }),
11
11
  }),
12
12
  Rpc.make('Add', {
13
- payload: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
14
- success: Schema.Struct({ result: Schema.Number }),
13
+ payload: Schema.Struct({ a: Schema.Finite, b: Schema.Finite }),
14
+ success: Schema.Struct({ result: Schema.Finite }),
15
15
  }),
16
16
  Rpc.make('Defect', {
17
17
  payload: Schema.Struct({ message: Schema.String }),
@@ -25,34 +25,34 @@ export class TestRpcs extends RpcGroup.make(
25
25
  Rpc.make('Stream', {
26
26
  payload: Schema.Struct({}),
27
27
  success: Schema.Struct({
28
- maybeNumber: Schema.Option(Schema.Number),
28
+ maybeNumber: Schema.Option(Schema.Finite),
29
29
  }),
30
30
  stream: true,
31
31
  }),
32
32
  Rpc.make('StreamError', {
33
- payload: Schema.Struct({ count: Schema.Number, errorAfter: Schema.Number }),
34
- success: Schema.Number,
33
+ payload: Schema.Struct({ count: Schema.Finite, errorAfter: Schema.Finite }),
34
+ success: Schema.Finite,
35
35
  error: Schema.String,
36
36
  stream: true,
37
37
  }),
38
38
  Rpc.make('StreamDefect', {
39
- payload: Schema.Struct({ count: Schema.Number, defectAfter: Schema.Number }),
40
- success: Schema.Number,
39
+ payload: Schema.Struct({ count: Schema.Finite, defectAfter: Schema.Finite }),
40
+ success: Schema.Finite,
41
41
  stream: true,
42
42
  }),
43
43
  Rpc.make('StreamInterruptible', {
44
- payload: Schema.Struct({ delay: Schema.Number, interruptAfterCount: Schema.Number }),
45
- success: Schema.Number,
44
+ payload: Schema.Struct({ delay: Schema.Finite, interruptAfterCount: Schema.Finite }),
45
+ success: Schema.Finite,
46
46
  stream: true,
47
47
  }),
48
48
  Rpc.make('StreamBugScenarioDoServer', {
49
49
  payload: Schema.Struct({}),
50
- success: Schema.Number,
50
+ success: Schema.Finite,
51
51
  stream: true,
52
52
  }),
53
53
  Rpc.make('StreamBugScenarioDoClient', {
54
54
  payload: Schema.Struct({}),
55
- success: Schema.Number,
55
+ success: Schema.Finite,
56
56
  }),
57
57
  ) {}
58
58
  export type TestRpcsI = RpcGroup.Rpcs<typeof TestRpcs>
@@ -1,10 +1,11 @@
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,
7
- HttpApp,
8
+ HttpEffect,
8
9
  Layer,
9
10
  Option,
10
11
  RpcClient,
@@ -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({
@@ -74,7 +76,7 @@ export default {
74
76
  const url = new URL(request.url)
75
77
 
76
78
  // Handle HTTP RPC endpoint
77
- if (url.pathname === '/rpc') {
79
+ if (url.pathname === '/rpc/') {
78
80
  // Get the test server DO instance
79
81
  const doId = env.TEST_RPC_DO.idFromName('test-server')
80
82
  const serverDO = env.TEST_RPC_DO.get(doId)
@@ -104,26 +106,23 @@ export default {
104
106
  doRpcClient.StreamBugScenarioDoServer(msg).pipe(
105
107
  Stream.tap(() => Effect.fail('doh')),
106
108
  // observed behaviour: `log1` is still logged
107
- Stream.tapErrorCause((cause) => Effect.log('log1', cause)),
109
+ Stream.tapCause((cause) => Effect.log('log1', cause)),
108
110
  Stream.mapError((cause) => cause.toString()),
109
111
  // observed behaviour: after this error mapping `log2` is never logged
110
- Stream.tapErrorCause((cause) => Effect.log('log2', cause)),
112
+ Stream.tapCause((cause) => Effect.log('log2', cause)),
111
113
  Stream.tapLogWithLabel('stream'),
112
114
  Stream.runCount,
113
115
  Effect.orDie,
114
116
  // observed behaviour: `log3` is also never logged
115
- Effect.tapErrorCause((cause) => Effect.log('log3', cause)),
117
+ Effect.tapCause((cause) => Effect.log('log3', cause)),
116
118
  ),
117
- }).pipe(
118
- Layer.provideMerge(RpcServer.layerProtocolHttp({ path: '/rpc' })),
119
- Layer.provideMerge(RpcSerialization.layerJson),
120
- )
119
+ }).pipe(Layer.provideMerge(RpcSerialization.layerJson))
121
120
 
122
- // Create the HTTP RPC app
123
- const httpApp = RpcServer.toHttpApp(TestRpcs).pipe(Effect.provide(handlersLayer))
121
+ // Create the HTTP RPC effect
122
+ const httpEffect = yield* RpcServer.toHttpEffect(TestRpcs).pipe(Effect.provide(handlersLayer))
124
123
 
125
124
  // Run the app and convert to web handler
126
- const webHandler = yield* httpApp.pipe(Effect.map(HttpApp.toWebHandler))
125
+ const webHandler = HttpEffect.toWebHandler(httpEffect)
127
126
 
128
127
  return yield* Effect.promise(() => webHandler(request))
129
128
  }).pipe(
@@ -139,7 +138,7 @@ export default {
139
138
  headers: { 'Content-Type': 'text/plain' },
140
139
  })
141
140
  } catch (error) {
142
- return new Response(`Error: ${error}`, { status: 500 })
141
+ return new Response(`Error: ${String(error)}`, { status: 500 })
143
142
  }
144
143
  },
145
144
  }
@@ -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
+ ```
@@ -10,8 +10,8 @@ export class TestRpcs extends RpcGroup.make(
10
10
  success: Schema.Struct({ echo: Schema.String }),
11
11
  }),
12
12
  Rpc.make('Add', {
13
- payload: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
14
- success: Schema.Struct({ result: Schema.Number }),
13
+ payload: Schema.Struct({ a: Schema.Finite, b: Schema.Finite }),
14
+ success: Schema.Struct({ result: Schema.Finite }),
15
15
  }),
16
16
  Rpc.make('Defect', {
17
17
  payload: Schema.Struct({ message: Schema.String }),
@@ -25,24 +25,24 @@ export class TestRpcs extends RpcGroup.make(
25
25
  Rpc.make('Stream', {
26
26
  payload: Schema.Struct({}),
27
27
  success: Schema.Struct({
28
- maybeNumber: Schema.Option(Schema.Number),
28
+ maybeNumber: Schema.Option(Schema.Finite),
29
29
  }),
30
30
  stream: true,
31
31
  }),
32
32
  Rpc.make('StreamError', {
33
- payload: Schema.Struct({ count: Schema.Number, errorAfter: Schema.Number }),
34
- success: Schema.Number,
33
+ payload: Schema.Struct({ count: Schema.Finite, errorAfter: Schema.Finite }),
34
+ success: Schema.Finite,
35
35
  error: Schema.String,
36
36
  stream: true,
37
37
  }),
38
38
  Rpc.make('StreamDefect', {
39
- payload: Schema.Struct({ count: Schema.Number, defectAfter: Schema.Number }),
40
- success: Schema.Number,
39
+ payload: Schema.Struct({ count: Schema.Finite, defectAfter: Schema.Finite }),
40
+ success: Schema.Finite,
41
41
  stream: true,
42
42
  }),
43
43
  Rpc.make('StreamInterruptible', {
44
- payload: Schema.Struct({ delay: Schema.Number, interruptAfterCount: Schema.Number }),
45
- success: Schema.Number,
44
+ payload: Schema.Struct({ delay: Schema.Finite, interruptAfterCount: Schema.Finite }),
45
+ success: Schema.Finite,
46
46
  stream: true,
47
47
  }),
48
48
  ) {}
@@ -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
  }