@livestore/common-cf 0.4.0 → 0.5.0-dev.1

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