@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
@@ -3,6 +3,7 @@ import {
3
3
  Fiber,
4
4
  FiberMap,
5
5
  Layer,
6
+ Option,
6
7
  RpcClient,
7
8
  type RpcMessage,
8
9
  RpcSerialization,
@@ -11,13 +12,10 @@ import {
11
12
 
12
13
  import type * as CfTypes from '../cf-types.ts'
13
14
 
14
- /**
15
- * Processes a ReadableStream response from streaming RPCs.
16
- * Reads chunks from the stream and writes them as RPC responses.
17
- */
15
+ /** Decodes a streaming-RPC `ReadableStream`'s msgpack frames, writing each out as it arrives. */
18
16
  const processReadableStream = (
19
17
  stream: CfTypes.ReadableStream,
20
- parser: ReturnType<typeof RpcSerialization.msgPack.unsafeMake>,
18
+ parser: ReturnType<typeof RpcSerialization.msgPack.makeUnsafe>,
21
19
  writeResponse: (response: any) => Effect.Effect<void>,
22
20
  ): Effect.Effect<void> =>
23
21
  Effect.gen(function* () {
@@ -31,21 +29,10 @@ const processReadableStream = (
31
29
  break
32
30
  }
33
31
 
34
- // Decode the chunk
32
+ // Server encodes `[message]` per enqueue; merged enqueues arrive as
33
+ // `[[msg1], [msg2], ...]`. `flat(1)` normalizes both to `[msg1, ...]`.
35
34
  const decoded = parser.decode(value as Uint8Array)
36
-
37
- // Handle array of messages from server.
38
- // Server sends `parser.encode([message])` per enqueue, so each decoded value is `[message]`.
39
- // When CF DO RPC merges enqueues in production, we get `[[msg1], [msg2], ...]`.
40
- // `flat(1)` normalizes both single and merged cases to `[msg1, msg2, ...]`.
41
- let messages: any[]
42
- if (Array.isArray(decoded) === true) {
43
- messages = decoded.flat(1)
44
- } else {
45
- messages = [decoded]
46
- }
47
-
48
- // Write each message
35
+ const messages = Array.isArray(decoded) === true ? decoded.flat(1) : [decoded]
49
36
  for (const message of messages) {
50
37
  yield* writeResponse(message)
51
38
  }
@@ -70,9 +57,8 @@ interface MakeDoRpcProtocolArgs {
70
57
  * Creates a Protocol layer that uses Cloudflare Durable Object RPC calls.
71
58
  * This enables direct RPC communication with Durable Objects using Cloudflare's native RPC.
72
59
  */
73
- export const layerProtocolDurableObject = (
74
- args: MakeDoRpcProtocolArgs,
75
- ): Layer.Layer<RpcClient.Protocol> => Layer.scoped(RpcClient.Protocol, makeProtocolDurableObject(args))
60
+ export const layerProtocolDurableObject = (args: MakeDoRpcProtocolArgs): Layer.Layer<RpcClient.Protocol> =>
61
+ Layer.effect(RpcClient.Protocol, makeProtocolDurableObject(args))
76
62
 
77
63
  /**
78
64
  * Implementation of the RPC Protocol interface using Cloudflare Durable Object RPC calls.
@@ -80,26 +66,30 @@ export const layerProtocolDurableObject = (
80
66
  */
81
67
  const makeProtocolDurableObject = ({
82
68
  callRpc,
83
- }: MakeDoRpcProtocolArgs): Effect.Effect<RpcClient.Protocol['Type'], never, Scope.Scope> =>
69
+ }: MakeDoRpcProtocolArgs): Effect.Effect<RpcClient.Protocol['Service'], never, Scope.Scope> =>
84
70
  RpcClient.Protocol.make(
85
71
  Effect.fnUntraced(function* (writeResponse) {
86
- const parser = RpcSerialization.msgPack.unsafeMake()
87
72
  // Not using an actual `FiberMap` here because it seems to shutdown to early
88
- // const fiberMap = new Map<string, Fiber.RuntimeFiber<void, never>>()
73
+ // const fiberMap = new Map<string, Fiber.Fiber<void, never>>()
89
74
  const fiberMap = yield* FiberMap.make<string, void, never>()
90
75
 
91
- const send = (message: RpcMessage.FromClientEncoded): Effect.Effect<void> => {
76
+ const send = (clientId: number, message: RpcMessage.FromClientEncoded): Effect.Effect<void> => {
92
77
  if (message._tag !== 'Request') {
93
78
  if (message._tag === 'Interrupt') {
94
79
  return Effect.gen(function* () {
95
80
  const fiber = yield* FiberMap.get(fiberMap, message.requestId)
96
- yield* Fiber.interrupt(fiber)
81
+ if (Option.isSome(fiber) === true) {
82
+ yield* Fiber.interrupt(fiber.value)
83
+ }
97
84
  }).pipe(Effect.orDie)
98
85
  }
99
86
 
100
87
  return Effect.void
101
88
  }
102
89
 
90
+ // MessagePack parsers buffer incomplete frames, so scope one parser to one DO RPC call.
91
+ const parser = RpcSerialization.msgPack.makeUnsafe()
92
+
103
93
  // Wrap single Request in array to match server expected format
104
94
  const serializedPayload = parser.encode([message]) as Uint8Array
105
95
 
@@ -111,16 +101,16 @@ const makeProtocolDurableObject = ({
111
101
  const fiber = yield* processReadableStream(
112
102
  serializedResponse as CfTypes.ReadableStream,
113
103
  parser,
114
- writeResponse,
104
+ (response) => writeResponse(clientId, response),
115
105
  ).pipe(
116
106
  // Effect.tapCauseLogPretty,
117
- Effect.fork,
107
+ Effect.forkChild,
118
108
  )
119
109
 
120
110
  // fiberMap.set(message.id, fiber)
121
111
  yield* FiberMap.set(fiberMap, message.id, fiber)
122
112
 
123
- yield* fiber
113
+ yield* Fiber.join(fiber)
124
114
 
125
115
  return
126
116
  }
@@ -138,7 +128,7 @@ const makeProtocolDurableObject = ({
138
128
 
139
129
  // Process each response
140
130
  for (const response of responseArray) {
141
- yield* writeResponse(response)
131
+ yield* writeResponse(clientId, response)
142
132
  }
143
133
  }).pipe(Effect.withSpan('do-rpc-client:send'), Effect.orDie) // Ensure never error type
144
134
  }
@@ -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
  Stream,
@@ -23,22 +21,26 @@ const testTimeout = 60_000
23
21
  const withWranglerTest = Vitest.makeWithTestCtx({
24
22
  timeout: testTimeout,
25
23
  makeLayer: () =>
26
- WranglerDevServerService.Default({
24
+ WranglerDevServer.layer({
27
25
  cwd: `${import.meta.dirname}/test-fixtures`,
28
26
  }).pipe(
29
27
  Layer.provide(
30
- Layer.mergeAll(PlatformNode.NodeContext.layer, FetchHttpClient.layer, Logger.minimumLogLevel(LogLevel.Debug)),
28
+ Layer.mergeAll(
29
+ PlatformNode.NodeServices.layer,
30
+ FetchHttpClient.layer,
31
+ Layer.succeed(References.MinimumLogLevel, 'Debug'),
32
+ ),
31
33
  ),
32
34
  ),
33
35
  })
34
36
 
35
37
  const ProtocolLive = Layer.suspend(() =>
36
38
  Effect.gen(function* () {
37
- const server = yield* WranglerDevServerService
39
+ const server = yield* WranglerDevServer.WranglerDevServer
38
40
  return RpcClient.layerProtocolHttp({
39
41
  url: `${server.url}/rpc`,
40
42
  }).pipe(Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]))
41
- }).pipe(Layer.unwrapEffect),
43
+ }).pipe(Layer.unwrap),
42
44
  )
43
45
 
44
46
  /**
@@ -61,7 +63,7 @@ const ProtocolLive = Layer.suspend(() =>
61
63
 
62
64
  Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
63
65
  // Direct HTTP RPC client tests
64
- Vitest.scopedLive('should call ping method', (test) =>
66
+ Vitest.live('should call ping method', (test) =>
65
67
  Effect.gen(function* () {
66
68
  const client = yield* RpcClient.make(TestRpcs)
67
69
  const result = yield* client.Ping({ message: 'Hello HTTP RPC' })
@@ -69,7 +71,7 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
69
71
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
70
72
  )
71
73
 
72
- Vitest.scopedLive('should call echo method', (test) =>
74
+ Vitest.live('should call echo method', (test) =>
73
75
  Effect.gen(function* () {
74
76
  const client = yield* RpcClient.make(TestRpcs)
75
77
  const result = yield* client.Echo({ text: 'Echo' })
@@ -77,7 +79,7 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
77
79
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
78
80
  )
79
81
 
80
- Vitest.scopedLive('should call add method', (test) =>
82
+ Vitest.live('should call add method', (test) =>
81
83
  Effect.gen(function* () {
82
84
  const client = yield* RpcClient.make(TestRpcs)
83
85
  const result = yield* client.Add({ a: 15, b: 25 })
@@ -85,103 +87,71 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
85
87
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
86
88
  )
87
89
 
88
- Vitest.scopedLive('should handle RPC fail method', (test) =>
90
+ Vitest.live('should handle RPC fail method', (test) =>
89
91
  Effect.gen(function* () {
90
92
  const client = yield* RpcClient.make(TestRpcs)
91
93
  const error = yield* client.Fail({ message: 'test http failure' }).pipe(Effect.exit)
92
94
  expect(error.toString()).toMatchInlineSnapshot(`
93
- "{
94
- "_id": "Exit",
95
- "_tag": "Failure",
96
- "cause": {
97
- "_id": "Cause",
98
- "_tag": "Die",
99
- "defect": "RPC failure: test http failure"
100
- }
101
- }"
95
+ "Failure(Cause([Die("RPC failure: test http failure")]))"
102
96
  `)
103
97
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
104
98
  )
105
99
 
106
- Vitest.scopedLive('should handle defect method', (test) =>
100
+ Vitest.live('should handle defect method', (test) =>
107
101
  Effect.gen(function* () {
108
102
  const client = yield* RpcClient.make(TestRpcs)
109
103
  const error = yield* client.Defect({ message: 'test http defect' }).pipe(Effect.exit)
110
104
  expect(error.toString()).toMatchInlineSnapshot(`
111
- "{
112
- "_id": "Exit",
113
- "_tag": "Failure",
114
- "cause": {
115
- "_id": "Cause",
116
- "_tag": "Die",
117
- "defect": "some defect: test http defect"
118
- }
119
- }"
105
+ "Failure(Cause([Die("some defect: test http defect")]))"
120
106
  `)
121
107
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
122
108
  )
123
109
 
124
- Vitest.scopedLive('should handle streaming RPC via HTTP', (test) =>
110
+ Vitest.live('should handle streaming RPC via HTTP', (test) =>
125
111
  Effect.gen(function* () {
126
112
  const client = yield* RpcClient.make(TestRpcs)
127
113
  const stream = client.Stream({}).pipe(
128
114
  Stream.take(4),
129
115
  Stream.map((c) => c.maybeNumber.pipe(Option.getOrUndefined)),
130
116
  )
131
- const chunks = yield* Stream.runCollect(stream)
132
- expect(Chunk.toReadonlyArray(chunks)).toEqual([1, 4, 9, 16]) // squares of 1,2,3,4
117
+ const result = yield* Stream.runCollect(stream)
118
+ expect(result).toEqual([1, 4, 9, 16]) // squares of 1,2,3,4
133
119
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
134
120
  )
135
121
 
136
- Vitest.scopedLive('should handle streaming RPC with error via HTTP', (test) =>
122
+ Vitest.live('should handle streaming RPC with error via HTTP', (test) =>
137
123
  Effect.gen(function* () {
138
124
  const client = yield* RpcClient.make(TestRpcs)
139
125
  const stream = client.StreamError({ count: 5, errorAfter: 4 })
140
126
  const error = yield* Stream.runCollect(stream).pipe(Effect.exit)
141
127
  expect(error.toString()).toMatchInlineSnapshot(`
142
- "{
143
- "_id": "Exit",
144
- "_tag": "Failure",
145
- "cause": {
146
- "_id": "Cause",
147
- "_tag": "Fail",
148
- "failure": "Stream error after 4: got 9"
149
- }
150
- }"
128
+ "Failure(Cause([Fail("Stream error after 4: got 9")]))"
151
129
  `)
152
130
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
153
131
  )
154
132
 
155
- Vitest.scopedLive('should handle streaming RPC with defect via HTTP', (test) =>
133
+ Vitest.live('should handle streaming RPC with defect via HTTP', (test) =>
156
134
  Effect.gen(function* () {
157
135
  const client = yield* RpcClient.make(TestRpcs)
158
136
  const stream = client.StreamDefect({ count: 4, defectAfter: 1 })
159
137
  const error = yield* Stream.runCollect(stream).pipe(Effect.exit)
160
138
  expect(error.toString()).toMatchInlineSnapshot(`
161
- "{
162
- "_id": "Exit",
163
- "_tag": "Failure",
164
- "cause": {
165
- "_id": "Cause",
166
- "_tag": "Die",
167
- "defect": "Stream defect after 1: got 4"
168
- }
169
- }"
139
+ "Failure(Cause([Die("Stream defect after 1: got 4")]))"
170
140
  `)
171
141
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
172
142
  )
173
143
 
174
- Vitest.scopedLive('should handle stream interruption via HTTP', (test) =>
144
+ Vitest.live('should handle stream interruption via HTTP', (test) =>
175
145
  Effect.gen(function* () {
176
146
  const client = yield* RpcClient.make(TestRpcs)
177
147
  const stream = client.StreamInterruptible({ delay: 50, interruptAfterCount: 3 }).pipe(Stream.take(3))
178
- const chunks = yield* Stream.runCollect(stream)
179
- expect(Chunk.toReadonlyArray(chunks)).toEqual([1, 2, 3])
148
+ const result = yield* Stream.runCollect(stream)
149
+ expect(result).toEqual([1, 2, 3])
180
150
  }).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
181
151
  )
182
152
 
183
153
  // TODO @IMax153
184
- Vitest.scopedLive.skip('should handle streaming RPC bug scenario', (test) =>
154
+ Vitest.live.skip('should handle streaming RPC bug scenario', (test) =>
185
155
  Effect.gen(function* () {
186
156
  const client = yield* RpcClient.make(TestRpcs)
187
157
  yield* client.StreamBugScenarioDoClient({})
@@ -225,7 +195,7 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
225
195
  errorSchema: [Function: Never],
226
196
  annotations: { _id: 'Context', services: [] },
227
197
  middlewares: Set(0) {},
228
- key: '@effect/rpc/Rpc/StreamBugScenarioDoServer'
198
+ key: 'effect/unstable/rpc/Rpc/StreamBugScenarioDoServer'
229
199
  } {}
230
200
 
231
201
  Killing wrangler process...