@livestore/webmesh 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 (56) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/channel/direct-channel-internal.d.ts +2 -3
  3. package/dist/channel/direct-channel-internal.d.ts.map +1 -1
  4. package/dist/channel/direct-channel-internal.js +23 -18
  5. package/dist/channel/direct-channel-internal.js.map +1 -1
  6. package/dist/channel/direct-channel.d.ts.map +1 -1
  7. package/dist/channel/direct-channel.js +20 -21
  8. package/dist/channel/direct-channel.js.map +1 -1
  9. package/dist/channel/proxy-channel.d.ts +7 -8
  10. package/dist/channel/proxy-channel.d.ts.map +1 -1
  11. package/dist/channel/proxy-channel.js +26 -22
  12. package/dist/channel/proxy-channel.js.map +1 -1
  13. package/dist/common.d.ts +33 -35
  14. package/dist/common.d.ts.map +1 -1
  15. package/dist/common.js +3 -2
  16. package/dist/common.js.map +1 -1
  17. package/dist/mesh-schema.d.ts +262 -136
  18. package/dist/mesh-schema.d.ts.map +1 -1
  19. package/dist/mesh-schema.js +45 -44
  20. package/dist/mesh-schema.js.map +1 -1
  21. package/dist/node.d.ts +6 -6
  22. package/dist/node.d.ts.map +1 -1
  23. package/dist/node.js +22 -21
  24. package/dist/node.js.map +1 -1
  25. package/dist/node.test.js +74 -71
  26. package/dist/node.test.js.map +1 -1
  27. package/dist/utils.d.ts +1 -1
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js.map +1 -1
  30. package/dist/websocket-edge.d.ts +18 -16
  31. package/dist/websocket-edge.d.ts.map +1 -1
  32. package/dist/websocket-edge.js +30 -36
  33. package/dist/websocket-edge.js.map +1 -1
  34. package/dist/websocket-edge.test.js +14 -14
  35. package/dist/websocket-edge.test.js.map +1 -1
  36. package/dist/worker/mod.d.ts +24 -0
  37. package/dist/worker/mod.d.ts.map +1 -0
  38. package/dist/worker/mod.js +44 -0
  39. package/dist/worker/mod.js.map +1 -0
  40. package/dist/worker/schema.d.ts +10 -0
  41. package/dist/worker/schema.d.ts.map +1 -0
  42. package/dist/worker/schema.js +7 -0
  43. package/dist/worker/schema.js.map +1 -0
  44. package/package.json +20 -37
  45. package/src/channel/direct-channel-internal.ts +30 -22
  46. package/src/channel/direct-channel.ts +26 -23
  47. package/src/channel/proxy-channel.ts +39 -36
  48. package/src/common.ts +6 -3
  49. package/src/mesh-schema.ts +34 -33
  50. package/src/node.test.ts +99 -86
  51. package/src/node.ts +32 -32
  52. package/src/utils.ts +3 -3
  53. package/src/websocket-edge.test.ts +14 -14
  54. package/src/websocket-edge.ts +47 -41
  55. package/src/worker/mod.ts +89 -0
  56. package/src/worker/schema.ts +8 -0
@@ -0,0 +1,44 @@
1
+ import { LS_DEV } from '@livestore/utils';
2
+ import { Context, Deferred, Effect, Layer, Queue, Stream, WebChannel } from '@livestore/utils/effect';
3
+ import * as WebmeshSchema from "../mesh-schema.js";
4
+ import { makeMeshNode } from "../node.js";
5
+ import * as WorkerSchema from "./schema.js";
6
+ export * as Schema from "./schema.js";
7
+ export class CacheService extends Context.Service()('@livestore/webmesh:worker:CacheService') {
8
+ static layer = ({ nodeName }) => Effect.gen(function* () {
9
+ const node = yield* makeMeshNode(nodeName);
10
+ globalThis.__debugWebmeshNode = node;
11
+ return CacheService.of({ node });
12
+ }).pipe(Layer.effect(CacheService));
13
+ }
14
+ export const CreateConnection = ({ from, port }) => Stream.callback((emit) => Effect.gen(function* () {
15
+ const { node } = yield* CacheService;
16
+ const messagePortChannel = yield* WebChannel.messagePortChannel({ port, schema: WebmeshSchema.Packet });
17
+ yield* node.addEdge({ target: from, edgeChannel: messagePortChannel, replaceIfExists: true });
18
+ if (LS_DEV === true) {
19
+ yield* Effect.logDebug(`@livestore/webmesh:worker: accepted edge: ${node.nodeName} <- ${from}`);
20
+ }
21
+ // `Stream.callback` exposes a queue-like emitter in Effect v4. Emit the single success value
22
+ // and then end the stream to preserve the old one-response worker command behavior.
23
+ yield* Queue.offer(emit, {});
24
+ yield* Queue.end(emit);
25
+ yield* Effect.spanEvent({ connectedTo: [...node.edgeKeys] });
26
+ }).pipe(Effect.orDie)).pipe(Stream.withSpan(`@livestore/webmesh:worker:create-connection:${from}`));
27
+ export const connectViaWorker = ({ node, target, worker, }) => Effect.gen(function* () {
28
+ const mc = new MessageChannel();
29
+ const isConnected = yield* Deferred.make();
30
+ if (LS_DEV === true) {
31
+ yield* Effect.addFinalizerLog(`@livestore/webmesh:worker: closing message channel ${node.nodeName} -> ${target}`);
32
+ }
33
+ yield* worker.execute(WorkerSchema.CreateConnection.make({ from: node.nodeName, port: mc.port1 })).pipe(Stream.tap(() => Deferred.succeed(isConnected, true)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkScoped);
34
+ yield* Deferred.await(isConnected);
35
+ const workerConnection = yield* WebChannel.messagePortChannel({
36
+ port: mc.port2,
37
+ schema: WebmeshSchema.Packet,
38
+ });
39
+ yield* node.addEdge({ target, edgeChannel: workerConnection, replaceIfExists: true });
40
+ if (LS_DEV === true) {
41
+ yield* Effect.logDebug(`@livestore/webmesh:worker: initiated connection: ${node.nodeName} -> ${target}`);
42
+ }
43
+ });
44
+ //# sourceMappingURL=mod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.js","sourceRoot":"","sources":["../../src/worker/mod.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { Schema, Transferable } from '@livestore/utils/effect';
2
+ export declare const CreateConnection: Schema.TaggedStruct<"WebmeshWorker.CreateConnection", {
3
+ readonly from: Schema.String;
4
+ readonly port: Transferable.Transferable<Schema.declare<MessagePort, MessagePort>>;
5
+ }>;
6
+ export declare const Request: Schema.Union<readonly [Schema.TaggedStruct<"WebmeshWorker.CreateConnection", {
7
+ readonly from: Schema.String;
8
+ readonly port: Transferable.Transferable<Schema.declare<MessagePort, MessagePort>>;
9
+ }>]>;
10
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/worker/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAE9D,eAAO,MAAM,gBAAgB;;;EAG3B,CAAA;AAEF,eAAO,MAAM,OAAO;;;IAAmC,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { Schema, Transferable } from '@livestore/utils/effect';
2
+ export const CreateConnection = Schema.TaggedStruct('WebmeshWorker.CreateConnection', {
3
+ from: Schema.String,
4
+ port: Transferable.MessagePort,
5
+ });
6
+ export const Request = Schema.Union([CreateConnection]);
7
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/worker/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAE9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,YAAY,CAAC,gCAAgC,EAAE;IACpF,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,IAAI,EAAE,YAAY,CAAC,WAAW;CAC/B,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/webmesh",
3
- "version": "0.4.0",
3
+ "version": "0.5.0-dev.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,57 +14,40 @@
14
14
  "type": "module",
15
15
  "sideEffects": false,
16
16
  "exports": {
17
- ".": "./dist/mod.js"
17
+ ".": "./dist/mod.js",
18
+ "./worker": "./dist/worker/mod.js"
18
19
  },
19
20
  "publishConfig": {
20
21
  "access": "public"
21
22
  },
22
23
  "dependencies": {
23
- "@livestore/utils": "^0.4.0"
24
+ "@livestore/utils": "^0.5.0-dev.0"
24
25
  },
25
26
  "devDependencies": {
26
- "@effect/ai": "0.35.0",
27
- "@effect/cli": "0.75.1",
28
- "@effect/cluster": "0.58.2",
29
- "@effect/experimental": "0.60.0",
30
- "@effect/opentelemetry": "0.63.0",
31
- "@effect/platform": "0.96.1",
32
- "@effect/platform-browser": "0.76.0",
33
- "@effect/platform-bun": "0.89.0",
34
- "@effect/platform-node": "0.106.0",
35
- "@effect/printer": "0.49.0",
36
- "@effect/printer-ansi": "0.49.0",
37
- "@effect/rpc": "0.75.1",
38
- "@effect/sql": "0.51.1",
39
- "@effect/typeclass": "0.40.0",
40
- "@effect/vitest": "0.29.0",
27
+ "@effect/opentelemetry": "4.0.0-rc.111",
28
+ "@effect/platform-browser": "4.0.0-rc.111",
29
+ "@effect/platform-bun": "4.0.0-rc.111",
30
+ "@effect/platform-node": "4.0.0-rc.111",
31
+ "@effect/platform-node-shared": "4.0.0-rc.111",
32
+ "@effect/vitest": "4.0.0-rc.111",
41
33
  "@opentelemetry/api": "1.9.0",
42
34
  "@opentelemetry/resources": "2.2.0",
43
35
  "@standard-schema/spec": "1.1.0",
44
- "effect": "3.21.2",
45
- "vitest": "3.2.4",
46
- "@livestore/utils-dev": "^0.4.0"
36
+ "effect": "4.0.0-rc.111",
37
+ "vitest": "4.1.9",
38
+ "@livestore/utils-dev": "^0.5.0-dev.0"
47
39
  },
48
40
  "peerDependencies": {
49
- "@effect/ai": "^0.35.0",
50
- "@effect/cli": "^0.75.1",
51
- "@effect/cluster": "^0.58.2",
52
- "@effect/experimental": "^0.60.0",
53
- "@effect/opentelemetry": "^0.63.0",
54
- "@effect/platform": "^0.96.1",
55
- "@effect/platform-browser": "^0.76.0",
56
- "@effect/platform-bun": "^0.89.0",
57
- "@effect/platform-node": "^0.106.0",
58
- "@effect/printer": "^0.49.0",
59
- "@effect/printer-ansi": "^0.49.0",
60
- "@effect/rpc": "^0.75.1",
61
- "@effect/sql": "^0.51.1",
62
- "@effect/typeclass": "^0.40.0",
63
- "@effect/vitest": "^0.29.0",
41
+ "@effect/opentelemetry": "^4.0.0-rc.111",
42
+ "@effect/platform-browser": "^4.0.0-rc.111",
43
+ "@effect/platform-bun": "^4.0.0-rc.111",
44
+ "@effect/platform-node": "^4.0.0-rc.111",
45
+ "@effect/platform-node-shared": "^4.0.0-rc.111",
46
+ "@effect/vitest": "^4.0.0-rc.111",
64
47
  "@opentelemetry/api": "^1.9.0",
65
48
  "@opentelemetry/resources": "^2.2.0",
66
49
  "@standard-schema/spec": "^1.1.0",
67
- "effect": "^3.21.2"
50
+ "effect": "^4.0.0-rc.111"
68
51
  },
69
52
  "$genie": {
70
53
  "source": "package.json.genie.ts",
@@ -1,9 +1,10 @@
1
1
  import { casesHandled, shouldNeverHappen } from '@livestore/utils'
2
- import type { PubSub } from '@livestore/utils/effect'
3
2
  import {
3
+ type PubSub,
4
4
  Deferred,
5
5
  Effect,
6
6
  Exit,
7
+ Fiber,
7
8
  Predicate,
8
9
  Queue,
9
10
  Schema,
@@ -52,7 +53,7 @@ export const makeDirectChannelInternal = ({
52
53
  }: MakeDirectChannelArgs & {
53
54
  channelVersion: number
54
55
  /** We're passing in the closeable scope from the wrapping direct channel */
55
- scope: Scope.CloseableScope
56
+ scope: Scope.Closeable
56
57
  sourceId: string
57
58
  }): Effect.Effect<
58
59
  WebChannel.WebChannel<any, any>,
@@ -89,8 +90,8 @@ export const makeDirectChannelInternal = ({
89
90
  const deferred = yield* makeDeferredResult()
90
91
 
91
92
  const schema = {
92
- send: Schema.Union(schema_.send, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong),
93
- listen: Schema.Union(schema_.listen, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong),
93
+ send: Schema.Union([schema_.send, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong]),
94
+ listen: Schema.Union([schema_.listen, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong]),
94
95
  }
95
96
 
96
97
  const channelStateRef: { current: ChannelState } = {
@@ -105,7 +106,9 @@ export const makeDirectChannelInternal = ({
105
106
  channelState: channelState._tag,
106
107
  packetId: packet.id,
107
108
  packetReqId: packet.reqId,
108
- ...(Predicate.hasProperty('channelVersion')(packet) === true ? { packetChannelVersion: packet.channelVersion } : {}),
109
+ ...(Predicate.hasProperty('channelVersion')(packet) === true
110
+ ? { packetChannelVersion: packet.channelVersion }
111
+ : {}),
109
112
  })
110
113
 
111
114
  // const reqIdStr =
@@ -185,7 +188,9 @@ export const makeDirectChannelInternal = ({
185
188
  remainingHops: packet.hops,
186
189
  reqId: packet.id,
187
190
  })
188
- yield* Effect.spanEvent(`Re-sending new request (${newRequestPacket.id}) for incoming request (${packet.id})`)
191
+ yield* Effect.spanEvent(
192
+ `Re-sending new request (${newRequestPacket.id}) for incoming request (${packet.id})`,
193
+ )
189
194
 
190
195
  yield* sendPacket(newRequestPacket)
191
196
  }
@@ -223,7 +228,7 @@ export const makeDirectChannelInternal = ({
223
228
 
224
229
  // Now we wait for the other side to respond via the channel
225
230
  yield* channel.listen.pipe(
226
- Stream.flatten(),
231
+ Stream.mapEffect(Effect.fromResult),
227
232
  Stream.filter(Schema.is(MeshSchema.DirectChannelPing)),
228
233
  Stream.take(1),
229
234
  Stream.runDrain,
@@ -258,11 +263,11 @@ export const makeDirectChannelInternal = ({
258
263
  }).pipe(Effect.andThen(WebChannel.toOpenChannel))
259
264
 
260
265
  const waitForPongFiber = yield* channel.listen.pipe(
261
- Stream.flatten(),
266
+ Stream.mapEffect(Effect.fromResult),
262
267
  Stream.filter(Schema.is(MeshSchema.DirectChannelPong)),
263
268
  Stream.take(1),
264
269
  Stream.runDrain,
265
- Effect.fork,
270
+ Effect.forkChild,
266
271
  )
267
272
 
268
273
  // There seems to be some scenario where the initial ping message is lost.
@@ -273,7 +278,7 @@ export const makeDirectChannelInternal = ({
273
278
  .send(MeshSchema.DirectChannelPing.make({}))
274
279
  .pipe(Effect.timeout(10), Effect.retry({ times: 2 }))
275
280
 
276
- yield* waitForPongFiber
281
+ yield* Fiber.join(waitForPongFiber)
277
282
 
278
283
  yield* Effect.spanEvent(`loser side: established`)
279
284
  channelStateRef.current = { _tag: 'Established', otherSourceId: channelState.otherSourceId }
@@ -292,17 +297,6 @@ export const makeDirectChannelInternal = ({
292
297
  }),
293
298
  )
294
299
 
295
- yield* Effect.gen(function* () {
296
- while (true) {
297
- const packet = yield* Queue.take(incomingPacketsQueue)
298
- const res = yield* processMessagePacket(packet)
299
- // We want to give requests another chance to be processed
300
- if (res === 'close') {
301
- return
302
- }
303
- }
304
- }).pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
305
-
306
300
  const channelState = channelStateRef.current
307
301
 
308
302
  if (channelState._tag !== 'Initial') {
@@ -336,7 +330,21 @@ export const makeDirectChannelInternal = ({
336
330
 
337
331
  yield* edgeRequest
338
332
 
339
- const channel = yield* deferred
333
+ // Start draining only after `edgeRequest` moved the channel to `RequestSent`. Under Effect v4
334
+ // an eagerly forked processor can otherwise handle an already-buffered packet while still
335
+ // `Initial`.
336
+ yield* Effect.gen(function* () {
337
+ while (true) {
338
+ const packet = yield* Queue.take(incomingPacketsQueue)
339
+ const res = yield* processMessagePacket(packet)
340
+ // We want to give requests another chance to be processed
341
+ if (res === 'close') {
342
+ return
343
+ }
344
+ }
345
+ }).pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
346
+
347
+ const channel = yield* Deferred.await(deferred)
340
348
 
341
349
  return channel
342
350
  }).pipe(Effect.withSpanScoped(`makeDirectChannel:${channelVersion}`))
@@ -2,14 +2,16 @@ import {
2
2
  Cause,
3
3
  Deferred,
4
4
  Effect,
5
- Either,
5
+ Fiber,
6
+ Result,
6
7
  Exit,
7
8
  Option,
8
9
  Queue,
10
+ References,
9
11
  Schema,
10
12
  Scope,
11
13
  Stream,
12
- TQueue,
14
+ TxQueue,
13
15
  WebChannel,
14
16
  } from '@livestore/utils/effect'
15
17
  import { nanoid } from '@livestore/utils/nanoid'
@@ -49,7 +51,7 @@ export const makeDirectChannel = ({
49
51
  const sourceId = nanoid()
50
52
 
51
53
  const listenQueue = yield* Queue.unbounded<any>()
52
- const sendQueue = yield* TQueue.unbounded<[msg: any, deferred: Deferred.Deferred<void>]>()
54
+ const sendQueue = yield* TxQueue.unbounded<[msg: any, deferred: Deferred.Deferred<void>]>()
53
55
 
54
56
  const initialEdgeDeferred = yield* Deferred.make<void>()
55
57
 
@@ -66,7 +68,7 @@ export const makeDirectChannel = ({
66
68
  const resultDeferred = yield* Deferred.make<{
67
69
  channel: WebChannel.WebChannel<any, any>
68
70
  channelVersion: number
69
- makeDirectChannelScope: Scope.CloseableScope
71
+ makeDirectChannelScope: Scope.Closeable
70
72
  }>()
71
73
 
72
74
  while (true) {
@@ -99,7 +101,7 @@ export const makeDirectChannel = ({
99
101
  Stream.take(1),
100
102
  Stream.runDrain,
101
103
  Effect.as('new-edge' as const),
102
- Effect.fork,
104
+ Effect.forkChild,
103
105
  )
104
106
 
105
107
  const makeChannel = makeDirectChannelInternal({
@@ -115,29 +117,30 @@ export const makeDirectChannel = ({
115
117
  sendPacket,
116
118
  scope: makeDirectChannelScope,
117
119
  }).pipe(
118
- Scope.extend(makeDirectChannelScope),
120
+ Scope.provide(makeDirectChannelScope),
119
121
  Effect.forkIn(makeDirectChannelScope),
120
- // Given we only call `Effect.exit` later when joining the fiber,
122
+ // Given we only await the exit later when observing the fiber,
121
123
  // we don't want Effect to produce a "unhandled error" log message
122
- Effect.withUnhandledErrorLogLevel(Option.none()),
124
+ Effect.provideService(References.UnhandledLogLevel, undefined),
123
125
  )
124
126
 
125
- const raceResult = yield* Effect.raceFirst(makeChannel, waitForNewEdgeFiber.pipe(Effect.disconnect))
127
+ const raceResult = yield* Effect.raceFirst(makeChannel, Fiber.join(waitForNewEdgeFiber))
126
128
 
127
129
  if (raceResult === 'new-edge') {
128
130
  yield* Scope.close(makeDirectChannelScope, Exit.fail('new-edge'))
129
131
  // We'll try again
130
132
  } else {
131
- const channelExit = yield* raceResult.pipe(Effect.exit)
133
+ const channelExit = yield* Fiber.await(raceResult)
132
134
  if (channelExit._tag === 'Failure') {
133
135
  yield* Scope.close(makeDirectChannelScope, channelExit)
134
136
 
135
137
  if (
136
- Cause.isFailType(channelExit.cause) === true &&
137
- Schema.is(WebmeshSchema.DirectChannelResponseNoTransferables)(channelExit.cause.error) === true
138
+ Option.exists(Cause.findErrorOption(channelExit.cause), (error) =>
139
+ Schema.is(WebmeshSchema.DirectChannelResponseNoTransferables)(error),
140
+ ) === true
138
141
  ) {
139
142
  // Only retry when there is a new edge available
140
- yield* waitForNewEdgeFiber.pipe(Effect.exit)
143
+ yield* Effect.exit(Fiber.join(waitForNewEdgeFiber))
141
144
  }
142
145
  } else {
143
146
  const channel = channelExit.value
@@ -149,7 +152,7 @@ export const makeDirectChannel = ({
149
152
  }
150
153
 
151
154
  // Now we wait until the first channel is established
152
- const { channel, makeDirectChannelScope, channelVersion } = yield* resultDeferred
155
+ const { channel, makeDirectChannelScope, channelVersion } = yield* Deferred.await(resultDeferred)
153
156
 
154
157
  yield* Effect.spanEvent(`Connected#${channelVersion}`)
155
158
  debugInfo.isConnected = true
@@ -159,9 +162,9 @@ export const makeDirectChannel = ({
159
162
 
160
163
  // We'll now forward all incoming messages to the listen queue
161
164
  yield* channel.listen.pipe(
162
- Stream.flatten(),
165
+ Stream.mapEffect(Effect.fromResult),
163
166
  // Stream.tap((msg) => Effect.log(`${target}→${channelName}→${nodeName}:message:${msg.message}`)),
164
- Stream.tapChunk((chunk) => Queue.offerAll(listenQueue, chunk)),
167
+ Stream.tapArray((array) => Queue.offerAll(listenQueue, array)),
165
168
  Stream.runDrain,
166
169
  Effect.tapCauseLogPretty,
167
170
  Effect.forkIn(makeDirectChannelScope),
@@ -169,18 +172,18 @@ export const makeDirectChannel = ({
169
172
 
170
173
  yield* Effect.gen(function* () {
171
174
  while (true) {
172
- const [msg, deferred] = yield* TQueue.peek(sendQueue)
175
+ const [msg, deferred] = yield* TxQueue.peek(sendQueue)
173
176
  // NOTE we don't need an explicit retry flow here since in case of the channel being closed,
174
177
  // the send will never succeed. Meanwhile the send-loop fiber will be interrupted and
175
178
  // given we only peeked at the queue, the message to send is still there.
176
179
  yield* channel.send(msg)
177
180
  yield* Deferred.succeed(deferred, void 0)
178
- yield* TQueue.take(sendQueue) // Remove the message from the queue
181
+ yield* TxQueue.take(sendQueue) // Remove the message from the queue
179
182
  }
180
183
  }).pipe(Effect.forkIn(makeDirectChannelScope))
181
184
 
182
185
  // Wait until the channel is closed and then try to reconnect
183
- yield* channel.closedDeferred
186
+ yield* Deferred.await(channel.closedDeferred)
184
187
 
185
188
  yield* Scope.close(makeDirectChannelScope, Exit.succeed('channel-closed'))
186
189
 
@@ -204,16 +207,16 @@ export const makeDirectChannel = ({
204
207
  debugInfo.pendingSends++
205
208
  debugInfo.totalSends++
206
209
 
207
- yield* TQueue.offer(sendQueue, [message, sentDeferred])
210
+ yield* TxQueue.offer(sendQueue, [message, sentDeferred])
208
211
 
209
- yield* sentDeferred
212
+ yield* Deferred.await(sentDeferred)
210
213
 
211
214
  debugInfo.pendingSends--
212
215
  }).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
213
216
 
214
- const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right))
217
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.rechunk(1), Stream.map(Result.succeed))
215
218
 
216
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
219
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
217
220
 
218
221
  const webChannel = {
219
222
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
@@ -1,9 +1,9 @@
1
1
  import { casesHandled, shouldNeverHappen } from '@livestore/utils'
2
- import type { PubSub } from '@livestore/utils/effect'
3
2
  import {
3
+ type PubSub,
4
4
  Deferred,
5
5
  Effect,
6
- Either,
6
+ Result,
7
7
  Exit,
8
8
  Fiber,
9
9
  FiberHandle,
@@ -39,11 +39,11 @@ export const ProxyChannelSimulationParams = Schema.Struct({
39
39
  */
40
40
  onPayload: Schema.Struct({
41
41
  /** Delay before sending the ACK response (simulates slow ACK send) */
42
- beforeAckSend: Schema.Int.pipe(Schema.between(0, 500)),
42
+ beforeAckSend: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 500 })),
43
43
  /** Delay after forking the ACK send, before adding message to listen queue */
44
- afterAckFork: Schema.Int.pipe(Schema.between(0, 500)),
44
+ afterAckFork: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 500 })),
45
45
  /** Delay after adding message to listen queue */
46
- afterListenQueueOffer: Schema.Int.pipe(Schema.between(0, 500)),
46
+ afterListenQueueOffer: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 500 })),
47
47
  }),
48
48
  })
49
49
 
@@ -66,8 +66,8 @@ interface MakeProxyChannelArgs {
66
66
  channelName: ChannelName
67
67
  target: MeshNodeName
68
68
  schema: {
69
- send: Schema.Schema<any, any>
70
- listen: Schema.Schema<any, any>
69
+ send: Schema.Codec<any, any>
70
+ listen: Schema.Codec<any, any>
71
71
  }
72
72
  /** Optional simulation parameters for testing timing-sensitive behavior */
73
73
  simulation?: ProxyChannelSimulationParams
@@ -105,7 +105,7 @@ export const makeProxyChannel = ({
105
105
 
106
106
  type ProxiedChannelStateEstablished = {
107
107
  _tag: 'Established'
108
- listenSchema: Schema.Schema<any, any>
108
+ listenSchema: Schema.Codec<any, any>
109
109
  listenQueue: Queue.Queue<any>
110
110
  ackMap: Map<string, Deferred.Deferred<void>>
111
111
  combinedChannelId: string
@@ -131,6 +131,10 @@ export const makeProxyChannel = ({
131
131
  const channelSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
132
132
 
133
133
  const connectedStateRef = yield* SubscriptionRef.make<ProxiedChannelStateEstablished | false>(false)
134
+ // Incoming packets can be processed as soon as the queue-draining fiber starts, so state
135
+ // referenced by established channels must exist before that processor is forked.
136
+ const listenQueue = yield* Queue.unbounded<any>()
137
+ const ackMap = new Map<string, Deferred.Deferred<void>>()
134
138
 
135
139
  const waitForEstablished = Effect.gen(function* () {
136
140
  const state = yield* SubscriptionRef.waitUntil(connectedStateRef, (state) => state !== false)
@@ -162,8 +166,9 @@ export const makeProxyChannel = ({
162
166
  const getCombinedChannelId = (otherSideChannelIdCandidate: string) =>
163
167
  [channelIdCandidate, otherSideChannelIdCandidate].toSorted().join('_')
164
168
 
165
- const earlyPayloadBuffer = yield* Queue.unbounded<typeof MeshSchema.ProxyChannelPayload.Type>().pipe(
166
- Effect.acquireRelease(Queue.shutdown),
169
+ const earlyPayloadBuffer = yield* Effect.acquireRelease(
170
+ Queue.unbounded<typeof MeshSchema.ProxyChannelPayload.Type>(),
171
+ Queue.shutdown,
167
172
  )
168
173
 
169
174
  const processProxyPacket = ({ packet, respondToSender }: ProxyQueueItem) =>
@@ -267,7 +272,7 @@ export const makeProxyChannel = ({
267
272
  const establishedState = channelStateRef.current
268
273
  if (establishedState._tag === 'Established') {
269
274
  //
270
- const bufferedPackets = yield* Queue.takeAll(earlyPayloadBuffer)
275
+ const bufferedPackets = yield* Queue.clear(earlyPayloadBuffer)
271
276
  // yield* Effect.logDebug(
272
277
  // `[${nodeName}] Draining early payload buffer (${bufferedPackets.length}) after ResponseSuccess`,
273
278
  // )
@@ -278,10 +283,10 @@ export const makeProxyChannel = ({
278
283
  )
279
284
  continue
280
285
  }
281
- const decodedMessage = yield* Schema.decodeUnknown(establishedState.listenSchema)(
286
+ const decodedMessage = yield* Schema.decodeUnknownEffect(establishedState.listenSchema)(
282
287
  bufferedPacket.payload,
283
288
  )
284
- yield* establishedState.listenQueue.pipe(Queue.offer(decodedMessage))
289
+ yield* Queue.offer(establishedState.listenQueue, decodedMessage)
285
290
  }
286
291
  } else {
287
292
  yield* Effect.logError(
@@ -327,8 +332,8 @@ export const makeProxyChannel = ({
327
332
  yield* simSleep('onPayload', 'afterAckFork')
328
333
 
329
334
  if (channelState._tag === 'Established') {
330
- const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
331
- yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
335
+ const decodedMessage = yield* Schema.decodeUnknownEffect(channelState.listenSchema)(packet.payload)
336
+ yield* Queue.offer(channelState.listenQueue, decodedMessage)
332
337
 
333
338
  // Simulation point: delay after adding to listen queue
334
339
  yield* simSleep('onPayload', 'afterListenQueueOffer')
@@ -370,19 +375,8 @@ export const makeProxyChannel = ({
370
375
  }),
371
376
  )
372
377
 
373
- yield* Stream.fromQueue(queue).pipe(
374
- Stream.tap(processProxyPacket),
375
- Stream.runDrain,
376
- Effect.tapCauseLogPretty,
377
- Effect.forkScoped,
378
- )
379
-
380
- const listenQueue = yield* Queue.unbounded<any>()
381
-
382
378
  yield* Effect.spanEvent(`Connecting`)
383
379
 
384
- const ackMap = new Map<string, Deferred.Deferred<void>>()
385
-
386
380
  // check if already established via incoming `ProxyChannelRequest` from other side
387
381
  // which indicates we already have a edge to the target node
388
382
  // const channelState = channelStateRef.current
@@ -401,6 +395,15 @@ export const makeProxyChannel = ({
401
395
  Effect.forkScoped,
402
396
  )
403
397
 
398
+ // Drain queued proxy packets only after the local side entered `Pending` and sent its
399
+ // request. Otherwise an already-buffered response can race initialization.
400
+ yield* Stream.fromQueue(queue).pipe(
401
+ Stream.tap(processProxyPacket),
402
+ Stream.runDrain,
403
+ Effect.tapCauseLogPretty,
404
+ Effect.forkScoped,
405
+ )
406
+
404
407
  const { combinedChannelId: channelId } = yield* waitForEstablished
405
408
 
406
409
  yield* Fiber.interrupt(retryOnNewEdgeFiber)
@@ -410,7 +413,7 @@ export const makeProxyChannel = ({
410
413
 
411
414
  const send = (message: any) =>
412
415
  Effect.gen(function* () {
413
- const payload = yield* Schema.encodeUnknown(schema.send)(message)
416
+ const payload = yield* Schema.encodeUnknownEffect(schema.send)(message)
414
417
  const sendFiberHandle = yield* FiberHandle.make<void, never>()
415
418
 
416
419
  const sentDeferred = yield* Deferred.make<void>()
@@ -441,7 +444,7 @@ export const makeProxyChannel = ({
441
444
 
442
445
  yield* sendPacket(packet)
443
446
 
444
- yield* ack
447
+ yield* Deferred.await(ack)
445
448
  yield* Deferred.succeed(sentDeferred, void 0)
446
449
 
447
450
  debugInfo.pendingSends--
@@ -450,18 +453,18 @@ export const makeProxyChannel = ({
450
453
  // TODO make this configurable
451
454
  // Schedule.exponential(10): 10, 20, 40, 80, 160, 320, ...
452
455
  yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(10)), Effect.orDie)
453
- }).pipe(Effect.tapErrorCause(Effect.logError))
456
+ }).pipe(Effect.tapCause(Effect.logError))
454
457
 
455
- const rerunOnNewChannelFiber = yield* connectedStateRef.changes.pipe(
458
+ const rerunOnNewChannelFiber = yield* SubscriptionRef.changes(connectedStateRef).pipe(
456
459
  Stream.filter((_) => _ === false),
457
460
  Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
458
461
  Stream.runDrain,
459
- Effect.fork,
462
+ Effect.forkChild,
460
463
  )
461
464
 
462
465
  yield* FiberHandle.run(sendFiberHandle, trySend)
463
466
 
464
- yield* sentDeferred
467
+ yield* Deferred.await(sentDeferred)
465
468
 
466
469
  yield* Fiber.interrupt(rerunOnNewChannelFiber)
467
470
  }).pipe(
@@ -470,11 +473,11 @@ export const makeProxyChannel = ({
470
473
  Effect.withParentSpan(channelSpan),
471
474
  )
472
475
 
473
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
476
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Result.succeed))
474
477
 
475
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
478
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
476
479
 
477
- const runtime = yield* Effect.runtime()
480
+ const services = yield* Effect.context()
478
481
 
479
482
  const webChannel = {
480
483
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
@@ -489,7 +492,7 @@ export const makeProxyChannel = ({
489
492
  debug: {
490
493
  ping: (message = 'ping') =>
491
494
  send(WebChannel.DebugPingMessage.make({ message })).pipe(
492
- Effect.provide(runtime),
495
+ Effect.provide(services),
493
496
  Effect.tapCauseLogPretty,
494
497
  Effect.runFork,
495
498
  ),