@livestore/webmesh 0.4.0-dev.9 → 0.5.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +5 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/direct-channel-internal.d.ts +2 -3
  4. package/dist/channel/direct-channel-internal.d.ts.map +1 -1
  5. package/dist/channel/direct-channel-internal.js +35 -38
  6. package/dist/channel/direct-channel-internal.js.map +1 -1
  7. package/dist/channel/direct-channel.d.ts.map +1 -1
  8. package/dist/channel/direct-channel.js +22 -23
  9. package/dist/channel/direct-channel.js.map +1 -1
  10. package/dist/channel/proxy-channel.d.ts +29 -5
  11. package/dist/channel/proxy-channel.d.ts.map +1 -1
  12. package/dist/channel/proxy-channel.js +87 -37
  13. package/dist/channel/proxy-channel.js.map +1 -1
  14. package/dist/common.d.ts +33 -35
  15. package/dist/common.d.ts.map +1 -1
  16. package/dist/common.js +7 -7
  17. package/dist/common.js.map +1 -1
  18. package/dist/mesh-schema.d.ts +262 -136
  19. package/dist/mesh-schema.d.ts.map +1 -1
  20. package/dist/mesh-schema.js +45 -44
  21. package/dist/mesh-schema.js.map +1 -1
  22. package/dist/node.d.ts +12 -6
  23. package/dist/node.d.ts.map +1 -1
  24. package/dist/node.js +47 -44
  25. package/dist/node.js.map +1 -1
  26. package/dist/node.test.js +253 -63
  27. package/dist/node.test.js.map +1 -1
  28. package/dist/utils.d.ts +1 -1
  29. package/dist/utils.d.ts.map +1 -1
  30. package/dist/utils.js.map +1 -1
  31. package/dist/websocket-edge.d.ts +20 -18
  32. package/dist/websocket-edge.d.ts.map +1 -1
  33. package/dist/websocket-edge.js +33 -41
  34. package/dist/websocket-edge.js.map +1 -1
  35. package/dist/websocket-edge.test.d.ts +7 -0
  36. package/dist/websocket-edge.test.d.ts.map +1 -0
  37. package/dist/websocket-edge.test.js +74 -0
  38. package/dist/websocket-edge.test.js.map +1 -0
  39. package/dist/worker/mod.d.ts +24 -0
  40. package/dist/worker/mod.d.ts.map +1 -0
  41. package/dist/worker/mod.js +44 -0
  42. package/dist/worker/mod.js.map +1 -0
  43. package/dist/worker/schema.d.ts +10 -0
  44. package/dist/worker/schema.d.ts.map +1 -0
  45. package/dist/worker/schema.js +7 -0
  46. package/dist/worker/schema.js.map +1 -0
  47. package/package.json +48 -12
  48. package/src/channel/direct-channel-internal.ts +41 -47
  49. package/src/channel/direct-channel.ts +28 -25
  50. package/src/channel/proxy-channel.ts +120 -57
  51. package/src/common.ts +9 -7
  52. package/src/mesh-schema.ts +34 -33
  53. package/src/node.test.ts +356 -80
  54. package/src/node.ts +63 -55
  55. package/src/utils.ts +3 -3
  56. package/src/websocket-edge.test.ts +98 -0
  57. package/src/websocket-edge.ts +52 -48
  58. package/src/worker/mod.ts +89 -0
  59. 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,26 +1,62 @@
1
1
  {
2
2
  "name": "@livestore/webmesh",
3
- "version": "0.4.0-dev.9",
3
+ "version": "0.5.0-dev.0",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/livestorejs/livestore.git"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "package.json",
12
+ "src"
13
+ ],
4
14
  "type": "module",
5
15
  "sideEffects": false,
6
16
  "exports": {
7
17
  ".": "./dist/mod.js",
8
- "./websocket-server": "./dist/websocket-server.js"
18
+ "./worker": "./dist/worker/mod.js"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
9
22
  },
10
23
  "dependencies": {
11
- "@livestore/utils": "0.4.0-dev.9"
24
+ "@livestore/utils": "^0.5.0-dev.0"
12
25
  },
13
26
  "devDependencies": {
14
- "vitest": "3.2.4",
15
- "@livestore/utils-dev": "0.4.0-dev.9"
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",
33
+ "@opentelemetry/api": "1.9.0",
34
+ "@opentelemetry/resources": "2.2.0",
35
+ "@standard-schema/spec": "1.1.0",
36
+ "effect": "4.0.0-rc.111",
37
+ "vitest": "4.1.9",
38
+ "@livestore/utils-dev": "^0.5.0-dev.0"
16
39
  },
17
- "files": [
18
- "package.json",
19
- "src",
20
- "dist"
21
- ],
22
- "publishConfig": {
23
- "access": "public"
40
+ "peerDependencies": {
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",
47
+ "@opentelemetry/api": "^1.9.0",
48
+ "@opentelemetry/resources": "^2.2.0",
49
+ "@standard-schema/spec": "^1.1.0",
50
+ "effect": "^4.0.0-rc.111"
51
+ },
52
+ "$genie": {
53
+ "source": "package.json.genie.ts",
54
+ "warning": "DO NOT EDIT - changes will be overwritten",
55
+ "workspaceClosureDirs": [
56
+ "packages/@livestore/utils",
57
+ "packages/@livestore/utils-dev",
58
+ "packages/@livestore/webmesh"
59
+ ]
24
60
  },
25
61
  "scripts": {
26
62
  "test": "vitest"
@@ -1,10 +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
- OtelTracer,
7
+ Fiber,
8
8
  Predicate,
9
9
  Queue,
10
10
  Schema,
@@ -53,7 +53,7 @@ export const makeDirectChannelInternal = ({
53
53
  }: MakeDirectChannelArgs & {
54
54
  channelVersion: number
55
55
  /** We're passing in the closeable scope from the wrapping direct channel */
56
- scope: Scope.CloseableScope
56
+ scope: Scope.Closeable
57
57
  sourceId: string
58
58
  }): Effect.Effect<
59
59
  WebChannel.WebChannel<any, any>,
@@ -89,14 +89,9 @@ export const makeDirectChannelInternal = ({
89
89
 
90
90
  const deferred = yield* makeDeferredResult()
91
91
 
92
- const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
93
- // const span = {
94
- // addEvent: (...msg: any[]) => console.log(`${nodeName}→${channelName}→${target}[${channelVersion}]`, ...msg),
95
- // }
96
-
97
92
  const schema = {
98
- send: Schema.Union(schema_.send, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong),
99
- 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]),
100
95
  }
101
96
 
102
97
  const channelStateRef: { current: ChannelState } = {
@@ -107,11 +102,13 @@ export const makeDirectChannelInternal = ({
107
102
  Effect.gen(function* () {
108
103
  const channelState = channelStateRef.current
109
104
 
110
- span?.addEvent(`process:${packet._tag}`, {
105
+ yield* Effect.spanEvent(`process:${packet._tag}`, {
111
106
  channelState: channelState._tag,
112
107
  packetId: packet.id,
113
108
  packetReqId: packet.reqId,
114
- packetChannelVersion: Predicate.hasProperty('channelVersion')(packet) ? packet.channelVersion : undefined,
109
+ ...(Predicate.hasProperty('channelVersion')(packet) === true
110
+ ? { packetChannelVersion: packet.channelVersion }
111
+ : {}),
115
112
  })
116
113
 
117
114
  // const reqIdStr =
@@ -130,7 +127,7 @@ export const makeDirectChannelInternal = ({
130
127
  // If the other side has a higher version, we need to close this channel and
131
128
  // recreate it with the new version
132
129
  if (packet.channelVersion > channelVersion) {
133
- span?.addEvent(`incoming packet has higher version (${packet.channelVersion}), closing channel`)
130
+ yield* Effect.spanEvent(`incoming packet has higher version (${packet.channelVersion}), closing channel`)
134
131
  yield* Scope.close(scope, Exit.succeed('higher-version-expected'))
135
132
  // TODO include expected version in the error so the channel gets recreated with the new version
136
133
  return 'close'
@@ -149,7 +146,7 @@ export const makeDirectChannelInternal = ({
149
146
  remainingHops: packet.hops,
150
147
  reqId: undefined,
151
148
  })
152
- span?.addEvent(
149
+ yield* Effect.spanEvent(
153
150
  `incoming packet has lower version (${packet.channelVersion}), sending request to reconnect (${newPacket.id})`,
154
151
  )
155
152
 
@@ -164,7 +161,7 @@ export const makeDirectChannelInternal = ({
164
161
  } else {
165
162
  // In case the instance of the source has changed, we need to close the channel
166
163
  // and reconnect with a new channel
167
- span?.addEvent(`force-new-channel`)
164
+ yield* Effect.spanEvent(`force-new-channel`)
168
165
  yield* Scope.close(scope, Exit.succeed('force-new-channel'))
169
166
  return 'close'
170
167
  }
@@ -191,15 +188,17 @@ export const makeDirectChannelInternal = ({
191
188
  remainingHops: packet.hops,
192
189
  reqId: packet.id,
193
190
  })
194
- span?.addEvent(`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
+ )
195
194
 
196
195
  yield* sendPacket(newRequestPacket)
197
196
  }
198
197
 
199
198
  const isWinner = nodeName > target
200
199
 
201
- if (isWinner) {
202
- span?.addEvent(`winner side: creating direct channel and sending response`)
200
+ if (isWinner === true) {
201
+ yield* Effect.spanEvent(`winner side: creating direct channel and sending response`)
203
202
  const mc = new MessageChannel()
204
203
 
205
204
  // We're using a direct channel with acks here to make sure messages are not lost
@@ -227,31 +226,27 @@ export const makeDirectChannelInternal = ({
227
226
 
228
227
  channelStateRef.current = { _tag: 'winner:ResponseSent', channel, otherSourceId: packet.sourceId }
229
228
 
230
- // span?.addEvent(`winner side: waiting for ping`)
231
-
232
229
  // Now we wait for the other side to respond via the channel
233
230
  yield* channel.listen.pipe(
234
- Stream.flatten(),
231
+ Stream.mapEffect(Effect.fromResult),
235
232
  Stream.filter(Schema.is(MeshSchema.DirectChannelPing)),
236
233
  Stream.take(1),
237
234
  Stream.runDrain,
238
235
  )
239
236
 
240
- // span?.addEvent(`winner side: sending pong`)
241
-
242
237
  yield* channel.send(MeshSchema.DirectChannelPong.make({}))
243
238
 
244
- span?.addEvent(`winner side: established`)
239
+ yield* Effect.spanEvent(`winner side: established`)
245
240
  channelStateRef.current = { _tag: 'Established', otherSourceId: packet.sourceId }
246
241
 
247
242
  yield* Deferred.succeed(deferred, channel)
248
243
  } else {
249
- span?.addEvent(`loser side: waiting for response`)
244
+ yield* Effect.spanEvent(`loser side: waiting for response`)
250
245
  // Wait for `DirectChannelResponseSuccess` packet
251
246
  channelStateRef.current = { _tag: 'loser:WaitingForResponse', otherSourceId: packet.sourceId }
252
247
  }
253
248
 
254
- break
249
+ return
255
250
  }
256
251
  case 'DirectChannelResponseSuccess': {
257
252
  if (channelState._tag !== 'loser:WaitingForResponse') {
@@ -268,15 +263,13 @@ export const makeDirectChannelInternal = ({
268
263
  }).pipe(Effect.andThen(WebChannel.toOpenChannel))
269
264
 
270
265
  const waitForPongFiber = yield* channel.listen.pipe(
271
- Stream.flatten(),
266
+ Stream.mapEffect(Effect.fromResult),
272
267
  Stream.filter(Schema.is(MeshSchema.DirectChannelPong)),
273
268
  Stream.take(1),
274
269
  Stream.runDrain,
275
- Effect.fork,
270
+ Effect.forkChild,
276
271
  )
277
272
 
278
- // span?.addEvent(`loser side: sending ping`)
279
-
280
273
  // There seems to be some scenario where the initial ping message is lost.
281
274
  // As a workaround until we find the root cause, we're retrying the ping a few times.
282
275
  // TODO write a test that reproduces this issue and fix the root cause ()
@@ -285,11 +278,9 @@ export const makeDirectChannelInternal = ({
285
278
  .send(MeshSchema.DirectChannelPing.make({}))
286
279
  .pipe(Effect.timeout(10), Effect.retry({ times: 2 }))
287
280
 
288
- // span?.addEvent(`loser side: waiting for pong`)
281
+ yield* Fiber.join(waitForPongFiber)
289
282
 
290
- yield* waitForPongFiber
291
-
292
- span?.addEvent(`loser side: established`)
283
+ yield* Effect.spanEvent(`loser side: established`)
293
284
  channelStateRef.current = { _tag: 'Established', otherSourceId: channelState.otherSourceId }
294
285
 
295
286
  yield* Deferred.succeed(deferred, channel)
@@ -306,17 +297,6 @@ export const makeDirectChannelInternal = ({
306
297
  }),
307
298
  )
308
299
 
309
- yield* Effect.gen(function* () {
310
- while (true) {
311
- const packet = yield* Queue.take(incomingPacketsQueue)
312
- const res = yield* processMessagePacket(packet)
313
- // We want to give requests another chance to be processed
314
- if (res === 'close') {
315
- return
316
- }
317
- }
318
- }).pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
319
-
320
300
  const channelState = channelStateRef.current
321
301
 
322
302
  if (channelState._tag !== 'Initial') {
@@ -345,12 +325,26 @@ export const makeDirectChannelInternal = ({
345
325
  }
346
326
 
347
327
  yield* sendPacket(packet)
348
- span?.addEvent(`initial edge request sent (${packet.id})`)
328
+ yield* Effect.spanEvent(`initial edge request sent (${packet.id})`)
349
329
  })
350
330
 
351
331
  yield* edgeRequest
352
332
 
353
- 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)
354
348
 
355
349
  return channel
356
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
 
@@ -61,12 +63,12 @@ export const makeDirectChannel = ({
61
63
  innerChannelRef: { current: undefined as WebChannel.WebChannel<any, any> | undefined },
62
64
  }
63
65
 
64
- // #region reconnect-loop
66
+ //#region reconnect-loop
65
67
  yield* Effect.gen(function* () {
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) &&
137
- Schema.is(WebmeshSchema.DirectChannelResponseNoTransferables)(channelExit.cause.error)
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
 
@@ -193,7 +196,7 @@ export const makeDirectChannel = ({
193
196
  Effect.tapCauseLogPretty,
194
197
  Effect.forkScoped,
195
198
  )
196
- // #endregion reconnect-loop
199
+ //#endregion reconnect-loop
197
200
 
198
201
  const parentSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
199
202
 
@@ -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,