@livestore/common-cf 0.4.0 โ†’ 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 (40) 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/do-rpc/client.d.ts.map +1 -1
  5. package/dist/do-rpc/client.js +17 -27
  6. package/dist/do-rpc/client.js.map +1 -1
  7. package/dist/do-rpc/client.test.d.ts +2 -0
  8. package/dist/do-rpc/client.test.d.ts.map +1 -0
  9. package/dist/do-rpc/client.test.js +99 -0
  10. package/dist/do-rpc/client.test.js.map +1 -0
  11. package/dist/do-rpc/rpc.test.js +25 -57
  12. package/dist/do-rpc/rpc.test.js.map +1 -1
  13. package/dist/do-rpc/server.d.ts +9 -4
  14. package/dist/do-rpc/server.d.ts.map +1 -1
  15. package/dist/do-rpc/server.js +84 -43
  16. package/dist/do-rpc/server.js.map +1 -1
  17. package/dist/do-rpc/test-fixtures/rpc-schema.d.ts +27 -27
  18. package/dist/do-rpc/test-fixtures/rpc-schema.js +11 -11
  19. package/dist/do-rpc/test-fixtures/worker.d.ts.map +1 -1
  20. package/dist/do-rpc/test-fixtures/worker.js +9 -9
  21. package/dist/do-rpc/test-fixtures/worker.js.map +1 -1
  22. package/dist/ws-rpc/test-fixtures/rpc-schema.d.ts +27 -27
  23. package/dist/ws-rpc/test-fixtures/rpc-schema.js +9 -9
  24. package/dist/ws-rpc/ws-rpc-server.d.ts +6 -5
  25. package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
  26. package/dist/ws-rpc/ws-rpc-server.js +17 -17
  27. package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
  28. package/dist/ws-rpc/ws-rpc.test.js +27 -59
  29. package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
  30. package/package.json +11 -20
  31. package/src/declare/cf-declare.ts +1 -1
  32. package/src/do-rpc/client.test.ts +122 -0
  33. package/src/do-rpc/client.ts +21 -31
  34. package/src/do-rpc/rpc.test.ts +29 -59
  35. package/src/do-rpc/server.ts +119 -54
  36. package/src/do-rpc/test-fixtures/rpc-schema.ts +11 -11
  37. package/src/do-rpc/test-fixtures/worker.ts +9 -12
  38. package/src/ws-rpc/test-fixtures/rpc-schema.ts +9 -9
  39. package/src/ws-rpc/ws-rpc-server.ts +37 -28
  40. package/src/ws-rpc/ws-rpc.test.ts +31 -61
@@ -18,13 +18,13 @@
18
18
  import { notYetImplemented, omitUndefineds } from '@livestore/utils'
19
19
  import {
20
20
  Context,
21
- constVoid,
22
21
  Effect,
22
+ Function,
23
23
  Exit,
24
24
  Layer,
25
25
  Logger,
26
- LogLevel,
27
- Mailbox,
26
+ Queue,
27
+ References,
28
28
  RpcMessage,
29
29
  RpcSerialization,
30
30
  RpcServer,
@@ -39,7 +39,7 @@ import type * as CfTypes from '../cf-types.ts'
39
39
  * This is useful for reading WebSocket attachment data (e.g., forwarded headers)
40
40
  * inside RPC handlers.
41
41
  */
42
- export class WsContext extends Context.Tag('WsContext')<WsContext, { readonly ws: CfTypes.WebSocket }>() {}
42
+ export class WsContext extends Context.Service<WsContext, { readonly ws: CfTypes.WebSocket }>()('WsContext') {}
43
43
 
44
44
  /**
45
45
  * Configuration options for setting up WebSocket RPC on a Durable Object.
@@ -55,7 +55,7 @@ export interface DurableObjectWebSocketRpcConfig {
55
55
  webSocketMode: 'hibernate' | 'accept'
56
56
  /**
57
57
  * Effect RPC layer that requires `RpcServer.Protocol` (and `WsContext` if used)
58
- * and provides the RPC server runtime.
58
+ * and provides the RPC server services.
59
59
  *
60
60
  * This is typically created by:
61
61
  * ```typescript
@@ -148,7 +148,7 @@ export const setupDurableObjectWebSocketRpc = ({
148
148
  const serverCtxMap = new Map<
149
149
  CfTypes.WebSocket,
150
150
  {
151
- scope: Scope.CloseableScope
151
+ scope: Scope.Closeable
152
152
  onMessage: (message: string | ArrayBuffer) => Promise<void>
153
153
  }
154
154
  >()
@@ -163,12 +163,13 @@ export const setupDurableObjectWebSocketRpc = ({
163
163
 
164
164
  const scope = yield* Scope.make()
165
165
 
166
- const incomingQueue = yield* Mailbox.make<Uint8Array | string>()
166
+ const incomingQueue = yield* Queue.unbounded<Uint8Array | string>()
167
167
 
168
- yield* Scope.addFinalizer(scope, incomingQueue.shutdown)
168
+ yield* Scope.addFinalizer(scope, Queue.shutdown(incomingQueue).pipe(Effect.asVoid))
169
169
 
170
170
  const ProtocolLive = layerRpcServerWebsocket({
171
171
  ws,
172
+ scope,
172
173
  incomingQueue,
173
174
  ...omitUndefineds({ onMessage }),
174
175
  }).pipe(Layer.provide(RpcSerialization.layerJson))
@@ -177,19 +178,16 @@ export const setupDurableObjectWebSocketRpc = ({
177
178
 
178
179
  yield* Layer.launch(ServerLive).pipe(Effect.tapCauseLogPretty, Effect.forkIn(scope))
179
180
 
180
- const runtime = yield* Effect.runtime()
181
+ const services = yield* Effect.context()
181
182
 
182
183
  const ctx = {
183
184
  scope,
184
185
  onMessage: (message: string | ArrayBuffer) =>
185
- incomingQueue
186
- .offer(message as Uint8Array | string)
187
- .pipe(
188
- Effect.asVoid,
189
- Effect.withSpan('ws-rpc-server/onMessage', { root: true }),
190
- Effect.provide(runtime),
191
- Effect.runPromise,
192
- ),
186
+ Queue.offer(incomingQueue, message as Uint8Array | string).pipe(
187
+ Effect.asVoid,
188
+ Effect.withSpan('ws-rpc-server/onMessage', { root: true }),
189
+ Effect.runPromiseWith(services),
190
+ ),
193
191
  }
194
192
 
195
193
  serverCtxMap.set(ws, ctx)
@@ -197,8 +195,14 @@ export const setupDurableObjectWebSocketRpc = ({
197
195
  return ctx
198
196
  }).pipe(
199
197
  Effect.tapCauseLogPretty,
200
- Logger.withMinimumLogLevel(LogLevel.Debug), // Useful for debugging
201
- Effect.provide(Layer.mergeAll(Logger.consoleWithThread('ws-rpc-server'), mainLayer ?? Layer.empty)),
198
+ Effect.annotateLogs({ thread: 'ws-rpc-server' }),
199
+ Effect.provide(
200
+ Layer.mergeAll(
201
+ Logger.layer([Logger.consoleStructured]),
202
+ Layer.succeed(References.MinimumLogLevel, 'Debug'), // Useful for debugging
203
+ mainLayer ?? Layer.empty,
204
+ ),
205
+ ),
202
206
  Effect.withSpan('effect-ws-rpc-server'),
203
207
  Effect.runPromise,
204
208
  )
@@ -233,9 +237,10 @@ export const setupDurableObjectWebSocketRpc = ({
233
237
  */
234
238
  export interface WsRpcServerArgs {
235
239
  ws: CfTypes.WebSocket
240
+ scope: Scope.Scope
236
241
  onMessage?: (message: RpcMessage.FromClientEncoded, ws: CfTypes.WebSocket) => void
237
- /** Mailbox queue for receiving incoming messages from the WebSocket */
238
- incomingQueue: Mailbox.Mailbox<Uint8Array | string>
242
+ /** Queue for receiving incoming messages from the WebSocket */
243
+ incomingQueue: Queue.Queue<Uint8Array | string>
239
244
  }
240
245
 
241
246
  /**
@@ -253,7 +258,10 @@ export interface WsRpcServerArgs {
253
258
  * @internal This is typically used internally by `setupDurableObjectWebSocketRpc`
254
259
  */
255
260
  export const layerRpcServerWebsocket = (args: WsRpcServerArgs) =>
256
- Layer.mergeAll(Layer.scoped(RpcServer.Protocol, makeSocketProtocol(args)), Layer.succeed(WsContext, { ws: args.ws }))
261
+ Layer.mergeAll(
262
+ Layer.effect(RpcServer.Protocol, makeSocketProtocol(args)),
263
+ Layer.succeed(WsContext, WsContext.of({ ws: args.ws })),
264
+ )
257
265
 
258
266
  /**
259
267
  * Creates the low-level RPC protocol implementation for WebSocket communication.
@@ -266,16 +274,16 @@ export const layerRpcServerWebsocket = (args: WsRpcServerArgs) =>
266
274
  *
267
275
  * @internal Used internally by `layerRpcServerWebsocket`
268
276
  */
269
- const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =>
277
+ const makeSocketProtocol = ({ incomingQueue, scope, ws, onMessage }: WsRpcServerArgs) =>
270
278
  Effect.gen(function* () {
271
279
  const serialization = yield* RpcSerialization.RpcSerialization
272
- const disconnects = yield* Mailbox.make<number>()
280
+ const disconnects = yield* Queue.unbounded<number>()
273
281
 
274
282
  const writeRaw = (msg: Uint8Array | string) => Effect.succeed(ws.send(msg))
275
283
 
276
284
  let writeRequest!: (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect<void>
277
285
 
278
- const parser = serialization.unsafeMake()
286
+ const parser = serialization.makeUnsafe()
279
287
  const id = 0
280
288
 
281
289
  const write = (response: RpcMessage.FromServerEncoded) => {
@@ -294,7 +302,7 @@ const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =
294
302
  writeRequest = writeRequest_
295
303
 
296
304
  // Start processing messages now that writeRequest is available
297
- const startProcessing = Mailbox.toStream(incomingQueue).pipe(
305
+ const startProcessing = Stream.fromQueue(incomingQueue).pipe(
298
306
  Stream.tap((data) => {
299
307
  try {
300
308
  const decoded = parser.decode(data) as ReadonlyArray<RpcMessage.FromClientEncoded>
@@ -309,7 +317,7 @@ const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =
309
317
  }
310
318
  return writeRequest(id, request)
311
319
  },
312
- step: constVoid,
320
+ step: Function.constVoid,
313
321
  })
314
322
  } catch (cause) {
315
323
  return Effect.orDie(writeRaw(parser.encode(RpcMessage.ResponseDefectEncoded(cause))!))
@@ -317,7 +325,7 @@ const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =
317
325
  }),
318
326
  Stream.runDrain,
319
327
  Effect.tapCauseLogPretty,
320
- Effect.fork,
328
+ Effect.forkIn(scope),
321
329
  )
322
330
 
323
331
  // Start the message processing
@@ -333,6 +341,7 @@ const makeSocketProtocol = ({ incomingQueue, ws, onMessage }: WsRpcServerArgs) =
333
341
  supportsAck: true,
334
342
  supportsTransferables: false,
335
343
  supportsSpanPropagation: true,
344
+ supportsNotifications: true,
336
345
  }))
337
346
  })
338
347
 
@@ -1,15 +1,13 @@
1
1
  import { expect } from 'vitest'
2
2
 
3
3
  import { Vitest } from '@livestore/utils-dev/node-vitest'
4
- import { WranglerDevServerService } from '@livestore/utils-dev/wrangler'
4
+ import { WranglerDevServer } from '@livestore/utils-dev/wrangler'
5
5
  import {
6
- Chunk,
7
6
  Effect,
8
7
  FetchHttpClient,
9
8
  Layer,
10
- Logger,
11
- LogLevel,
12
9
  Option,
10
+ References,
13
11
  RpcClient,
14
12
  RpcSerialization,
15
13
  Socket,
@@ -24,29 +22,33 @@ const testTimeout = 60_000
24
22
  const withWranglerTest = Vitest.makeWithTestCtx({
25
23
  timeout: testTimeout,
26
24
  makeLayer: () =>
27
- WranglerDevServerService.Default({
25
+ WranglerDevServer.layer({
28
26
  cwd: `${import.meta.dirname}/test-fixtures`,
29
27
  }).pipe(
30
28
  Layer.provide(
31
- Layer.mergeAll(PlatformNode.NodeContext.layer, FetchHttpClient.layer, Logger.minimumLogLevel(LogLevel.Debug)),
29
+ Layer.mergeAll(
30
+ PlatformNode.NodeServices.layer,
31
+ FetchHttpClient.layer,
32
+ Layer.succeed(References.MinimumLogLevel, 'Debug'),
33
+ ),
32
34
  ),
33
35
  ),
34
36
  })
35
37
 
36
38
  const ProtocolLive = Layer.suspend(() =>
37
39
  Effect.gen(function* () {
38
- const server = yield* WranglerDevServerService
40
+ const server = yield* WranglerDevServer.WranglerDevServer
39
41
  return RpcClient.layerProtocolSocket().pipe(
40
42
  Layer.provide(Socket.layerWebSocket(`ws://localhost:${server.port}`)),
41
43
  Layer.provide(Socket.layerWebSocketConstructorGlobal),
42
44
  Layer.provide(RpcSerialization.layerJson),
43
45
  )
44
- }).pipe(Layer.unwrapEffect),
46
+ }).pipe(Layer.unwrap),
45
47
  )
46
48
 
47
49
  Vitest.describe('Durable Object WebSocket RPC', { timeout: testTimeout }, () => {
48
50
  // Direct HTTP RPC client tests
49
- Vitest.scopedLive('should call ping method', (test) =>
51
+ Vitest.live('should call ping method', (test) =>
50
52
  Effect.gen(function* () {
51
53
  const client = yield* RpcClient.make(TestRpcs)
52
54
  const result = yield* client.Ping({ message: 'Hello HTTP RPC' })
@@ -54,7 +56,7 @@ Vitest.describe('Durable Object WebSocket RPC', { timeout: testTimeout }, () =>
54
56
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
55
57
  )
56
58
 
57
- Vitest.scopedLive('should call echo method', (test) =>
59
+ Vitest.live('should call echo method', (test) =>
58
60
  Effect.gen(function* () {
59
61
  const client = yield* RpcClient.make(TestRpcs)
60
62
  const result = yield* client.Echo({ text: 'Echo' })
@@ -62,7 +64,7 @@ Vitest.describe('Durable Object WebSocket RPC', { timeout: testTimeout }, () =>
62
64
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
63
65
  )
64
66
 
65
- Vitest.scopedLive('should call add method', (test) =>
67
+ Vitest.live('should call add method', (test) =>
66
68
  Effect.gen(function* () {
67
69
  const client = yield* RpcClient.make(TestRpcs)
68
70
  const result = yield* client.Add({ a: 15, b: 25 })
@@ -70,104 +72,72 @@ Vitest.describe('Durable Object WebSocket RPC', { timeout: testTimeout }, () =>
70
72
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
71
73
  )
72
74
 
73
- Vitest.scopedLive('should handle RPC fail method', (test) =>
75
+ Vitest.live('should handle RPC fail method', (test) =>
74
76
  Effect.gen(function* () {
75
77
  const client = yield* RpcClient.make(TestRpcs)
76
78
  const error = yield* client.Fail({ message: 'test http failure' }).pipe(Effect.exit)
77
79
  expect(error.toString()).toMatchInlineSnapshot(`
78
- "{
79
- "_id": "Exit",
80
- "_tag": "Failure",
81
- "cause": {
82
- "_id": "Cause",
83
- "_tag": "Fail",
84
- "failure": "RPC failure: test http failure"
85
- }
86
- }"
80
+ "Failure(Cause([Fail("RPC failure: test http failure")]))"
87
81
  `)
88
82
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
89
83
  )
90
84
 
91
- Vitest.scopedLive('should handle defect method', (test) =>
85
+ Vitest.live('should handle defect method', (test) =>
92
86
  Effect.gen(function* () {
93
87
  const client = yield* RpcClient.make(TestRpcs)
94
88
  const error = yield* client.Defect({ message: 'test http defect' }).pipe(Effect.exit)
95
89
  expect(error.toString()).toMatchInlineSnapshot(`
96
- "{
97
- "_id": "Exit",
98
- "_tag": "Failure",
99
- "cause": {
100
- "_id": "Cause",
101
- "_tag": "Die",
102
- "defect": "some defect: test http defect"
103
- }
104
- }"
90
+ "Failure(Cause([Die("some defect: test http defect")]))"
105
91
  `)
106
92
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
107
93
  )
108
94
 
109
- Vitest.scopedLive('should handle streaming RPC via HTTP', (test) =>
95
+ Vitest.live('should handle streaming RPC via HTTP', (test) =>
110
96
  Effect.gen(function* () {
111
97
  const client = yield* RpcClient.make(TestRpcs)
112
98
  const stream = client.Stream({}).pipe(
113
99
  Stream.take(4),
114
100
  Stream.map((c) => c.maybeNumber.pipe(Option.getOrUndefined)),
115
101
  )
116
- const chunks = yield* Stream.runCollect(stream)
117
- expect(Chunk.toReadonlyArray(chunks)).toEqual([1, 4, 9, 16]) // squares of 1,2,3,4
102
+ const result = yield* Stream.runCollect(stream)
103
+ expect(result).toEqual([1, 4, 9, 16]) // squares of 1,2,3,4
118
104
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
119
105
  )
120
106
 
121
- Vitest.scopedLive('should handle streaming RPC with error via HTTP', (test) =>
107
+ Vitest.live('should handle streaming RPC with error via HTTP', (test) =>
122
108
  Effect.gen(function* () {
123
109
  const client = yield* RpcClient.make(TestRpcs)
124
110
  const stream = client.StreamError({ count: 5, errorAfter: 4 })
125
111
  const error = yield* Stream.runCollect(stream).pipe(Effect.exit)
126
112
  expect(error.toString()).toMatchInlineSnapshot(`
127
- "{
128
- "_id": "Exit",
129
- "_tag": "Failure",
130
- "cause": {
131
- "_id": "Cause",
132
- "_tag": "Fail",
133
- "failure": "Stream error after 4: got 9"
134
- }
135
- }"
113
+ "Failure(Cause([Fail("Stream error after 4: got 9")]))"
136
114
  `)
137
115
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
138
116
  )
139
117
 
140
- Vitest.scopedLive('should handle streaming RPC with defect via HTTP', (test) =>
118
+ Vitest.live('should handle streaming RPC with defect via HTTP', (test) =>
141
119
  Effect.gen(function* () {
142
120
  const client = yield* RpcClient.make(TestRpcs)
143
121
  const stream = client.StreamDefect({ count: 4, defectAfter: 1 })
144
122
  const error = yield* Stream.runCollect(stream).pipe(Effect.exit)
145
123
  expect(error.toString()).toMatchInlineSnapshot(`
146
- "{
147
- "_id": "Exit",
148
- "_tag": "Failure",
149
- "cause": {
150
- "_id": "Cause",
151
- "_tag": "Die",
152
- "defect": "Stream defect after 1: got 4"
153
- }
154
- }"
124
+ "Failure(Cause([Die("Stream defect after 1: got 4")]))"
155
125
  `)
156
126
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
157
127
  )
158
128
 
159
- Vitest.scopedLive('should handle stream interruption via HTTP', (test) =>
129
+ Vitest.live('should handle stream interruption via HTTP', (test) =>
160
130
  Effect.gen(function* () {
161
131
  const client = yield* RpcClient.make(TestRpcs)
162
132
  const stream = client.StreamInterruptible({ delay: 50, interruptAfterCount: 3 }).pipe(Stream.take(3))
163
- const chunks = yield* Stream.runCollect(stream)
164
- expect(Chunk.toReadonlyArray(chunks)).toEqual([1, 2, 3])
133
+ const result = yield* Stream.runCollect(stream)
134
+ expect(result).toEqual([1, 2, 3])
165
135
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
166
136
  )
167
137
  })
168
138
 
169
139
  Vitest.describe('Hibernation Tests', { timeout: 25000 }, () => {
170
- Vitest.scopedLive('should maintain RPC functionality after hibernation', (test) =>
140
+ Vitest.live('should maintain RPC functionality after hibernation', (test) =>
171
141
  Effect.gen(function* () {
172
142
  console.log('๐Ÿงช Testing RPC server persistence across hibernation...')
173
143
 
@@ -222,15 +192,15 @@ Vitest.describe('Hibernation Tests', { timeout: 25000 }, () => {
222
192
  Stream.take(3),
223
193
  Stream.map((c) => c.maybeNumber.pipe(Option.getOrUndefined)),
224
194
  )
225
- const chunks = yield* Stream.runCollect(stream)
226
- expect(Chunk.toReadonlyArray(chunks)).toEqual([1, 4, 9]) // squares of 1,2,3
195
+ const result = yield* Stream.runCollect(stream)
196
+ expect(result).toEqual([1, 4, 9]) // squares of 1,2,3
227
197
  console.log('โœ… Streaming after hibernation successful')
228
198
 
229
199
  console.log('๐ŸŽ‰ All RPC operations successful after hibernation!')
230
200
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
231
201
  )
232
202
 
233
- Vitest.scopedLive('should handle rapid operations after hibernation', (test) =>
203
+ Vitest.live('should handle rapid operations after hibernation', (test) =>
234
204
  Effect.gen(function* () {
235
205
  console.log('๐Ÿงช Testing rapid operations after hibernation...')
236
206