@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
package/src/node.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  Effect,
7
7
  Exit,
8
8
  Fiber,
9
- Option,
10
9
  PubSub,
11
10
  Queue,
12
11
  Schema,
@@ -16,7 +15,7 @@ import {
16
15
  } from '@livestore/utils/effect'
17
16
 
18
17
  import { makeDirectChannel } from './channel/direct-channel.ts'
19
- import { makeProxyChannel } from './channel/proxy-channel.ts'
18
+ import { makeProxyChannel, type ProxyChannelSimulationParams } from './channel/proxy-channel.ts'
20
19
  import type { ChannelKey, ListenForChannelResult, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.ts'
21
20
  import { EdgeAlreadyExistsError, packetAsOtelAttributes } from './common.ts'
22
21
  import * as WebmeshSchema from './mesh-schema.ts'
@@ -61,7 +60,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
61
60
  }): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
62
61
  }
63
62
 
64
- removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
63
+ removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementError>
65
64
 
66
65
  hasChannel: ({
67
66
  target,
@@ -93,10 +92,10 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
93
92
  */
94
93
  channelName: string
95
94
  schema:
96
- | Schema.Schema<MsgListen | MsgSend, any>
95
+ | Schema.Codec<MsgListen | MsgSend, any>
97
96
  | {
98
- listen: Schema.Schema<MsgListen, any>
99
- send: Schema.Schema<MsgSend, any>
97
+ listen: Schema.Codec<MsgListen, any>
98
+ send: Schema.Codec<MsgSend, any>
100
99
  }
101
100
  /**
102
101
  * If possible, prefer using a DirectChannel with transferables (i.e. transferring memory instead of copying it).
@@ -107,13 +106,18 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
107
106
  *
108
107
  * @default 1 second
109
108
  */
110
- timeout?: Duration.DurationInput
109
+ timeout?: Duration.Input
111
110
  /**
112
111
  * If true, will close an existing channel if it exists.
113
112
  *
114
113
  * @default false
115
114
  */
116
115
  closeExisting?: boolean
116
+ /**
117
+ * Optional simulation parameters for testing timing-sensitive behavior.
118
+ * Only used when mode is 'proxy'.
119
+ */
120
+ simulation?: ProxyChannelSimulationParams
117
121
  }) => Effect.Effect<WebChannel.WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
118
122
 
119
123
  listenForChannel: Stream.Stream<ListenForChannelResult>
@@ -124,7 +128,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
124
128
  */
125
129
  makeBroadcastChannel: <Msg>(args: {
126
130
  channelName: string
127
- schema: Schema.Schema<Msg, any>
131
+ schema: Schema.Codec<Msg, any>
128
132
  }) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
129
133
  }
130
134
 
@@ -132,16 +136,17 @@ export const makeMeshNode = <TName extends MeshNodeName>(
132
136
  nodeName: TName,
133
137
  ): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
134
138
  Effect.gen(function* () {
135
- const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.RuntimeFiber<void> }>()
139
+ const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.Fiber<void> }>()
136
140
 
137
141
  // To avoid unbounded memory growth, we automatically forget about packet ids after a while
138
142
  const handledPacketIds = yield* TimeoutSet.make(Duration.minutes(1))
139
143
 
140
- const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>().pipe(Effect.acquireRelease(PubSub.shutdown))
144
+ const newEdgeAvailablePubSub = yield* Effect.acquireRelease(PubSub.unbounded<MeshNodeName>(), PubSub.shutdown)
141
145
 
142
- // const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
143
- // const messagePacketsToProcess = yield* Queue.unbounded<MessageQueueItem>().pipe(
144
- // Effect.acquireRelease(Queue.shutdown),
146
+ // const proxyPacketsToProcess = yield* Effect.acquireRelease(Queue.unbounded<ProxyQueueItem>(), Queue.shutdown)
147
+ // const messagePacketsToProcess = yield* Effect.acquireRelease(
148
+ // Queue.unbounded<MessageQueueItem>(),
149
+ // Queue.shutdown,
145
150
  // )
146
151
 
147
152
  const channelMap = new Map<
@@ -158,9 +163,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
158
163
  }
159
164
  >()
160
165
 
161
- const channelRequestsQueue = yield* Queue.unbounded<ListenForChannelResult>().pipe(
162
- Effect.acquireRelease(Queue.shutdown),
163
- )
166
+ const channelRequestsQueue = yield* Effect.acquireRelease(Queue.unbounded<ListenForChannelResult>(), Queue.shutdown)
164
167
 
165
168
  type RequestId = string
166
169
  const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
@@ -173,7 +176,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
173
176
  (packet._tag === 'DirectChannelRequest' &&
174
177
  (edgeChannels.size === 0 || // Either if direct edge does not support transferables ...
175
178
  edgeChannels.get(packet.target)?.channel.supportsTransferables === false)) || // ... or if no forward-edges support transferables
176
- ![...edgeChannels.values()].some((c) => c.channel.supportsTransferables === true)
179
+ [...edgeChannels.values()].some((c) => c.channel.supportsTransferables) === false
177
180
  ) {
178
181
  return WebmeshSchema.DirectChannelResponseNoTransferables.make({
179
182
  reqId: packet.id,
@@ -187,13 +190,14 @@ export const makeMeshNode = <TName extends MeshNodeName>(
187
190
  hops: [],
188
191
  })
189
192
  }
193
+ return undefined
190
194
  }
191
195
 
192
196
  const sendPacket = (packet: typeof WebmeshSchema.Packet.Type) =>
193
197
  Effect.gen(function* () {
194
198
  // yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
195
199
 
196
- if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet)) {
200
+ if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet) === true) {
197
201
  yield* Effect.spanEvent('NetworkEdgeAdded', { packet, nodeName })
198
202
  yield* PubSub.publish(newEdgeAvailablePubSub, packet.target)
199
203
 
@@ -205,7 +209,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
205
209
  return
206
210
  }
207
211
 
208
- if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
212
+ if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet) === true) {
209
213
  const edgesToForwardTo = Array.from(edgeChannels)
210
214
  .filter(([name]) => !packet.hops.includes(name))
211
215
  .map(([_, con]) => con.channel)
@@ -231,7 +235,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
231
235
  return
232
236
  }
233
237
 
234
- if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet)) {
238
+ if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet) === true) {
235
239
  if (packet.source !== nodeName) {
236
240
  const backEdgeName =
237
241
  packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
@@ -265,7 +269,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
265
269
  return
266
270
  }
267
271
 
268
- if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet)) {
272
+ if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet) === true) {
269
273
  if (packet.source === nodeName) {
270
274
  const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
271
275
  topologyRequestItem.set(packet.nodeName, new Set(packet.edges))
@@ -289,7 +293,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
289
293
  }
290
294
 
291
295
  // We have a direct edge to the target node
292
- if (edgeChannels.has(packet.target)) {
296
+ if (edgeChannels.has(packet.target) === true) {
293
297
  const edgeChannel = edgeChannels.get(packet.target)!.channel
294
298
  const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
295
299
  yield* Effect.annotateCurrentSpan({ hasDirectEdge: true })
@@ -325,7 +329,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
325
329
  .map(([name, con]) => ({ name, channel: con.channel }))
326
330
 
327
331
  // TODO if hops-depth=0, we should fail right away with no route found
328
- if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV) {
332
+ if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV === true) {
329
333
  yield* Effect.logWarning(nodeName, 'no route found to', packet.target, packet._tag, 'TODO handle better')
330
334
  // TODO return a expected failure
331
335
  }
@@ -348,8 +352,8 @@ export const makeMeshNode = <TName extends MeshNodeName>(
348
352
 
349
353
  const addEdge: MeshNode['addEdge'] = ({ target: targetNodeName, edgeChannel, replaceIfExists = false }) =>
350
354
  Effect.gen(function* () {
351
- if (edgeChannels.has(targetNodeName)) {
352
- if (replaceIfExists) {
355
+ if (edgeChannels.has(targetNodeName) === true) {
356
+ if (replaceIfExists === true) {
353
357
  yield* removeEdge(targetNodeName).pipe(Effect.orDie)
354
358
  } else {
355
359
  return yield* new EdgeAlreadyExistsError({ target: targetNodeName })
@@ -358,14 +362,14 @@ export const makeMeshNode = <TName extends MeshNodeName>(
358
362
 
359
363
  // TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
360
364
  const listenFiber = yield* edgeChannel.listen.pipe(
361
- Stream.flatten(),
365
+ Stream.mapEffect(Effect.fromResult),
362
366
  Stream.tap((message) =>
363
367
  Effect.gen(function* () {
364
- const packet = yield* Schema.decodeUnknown(WebmeshSchema.Packet)(message)
368
+ const packet = yield* Schema.decodeUnknownEffect(WebmeshSchema.Packet)(message)
365
369
 
366
370
  // console.debug(nodeName, 'recv', packet._tag, packet.source, packet.target)
367
371
 
368
- if (handledPacketIds.has(packet.id)) return
372
+ if (handledPacketIds.has(packet.id) === true) return
369
373
  handledPacketIds.add(packet.id)
370
374
 
371
375
  switch (packet._tag) {
@@ -380,9 +384,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
380
384
  if (packet.target === nodeName) {
381
385
  const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
382
386
 
383
- if (!channelMap.has(channelKey)) {
384
- const channelQueue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
385
- Effect.acquireRelease(Queue.shutdown),
387
+ if (channelMap.has(channelKey) === false) {
388
+ const channelQueue = yield* Effect.acquireRelease(
389
+ Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
390
+ Queue.shutdown,
386
391
  )
387
392
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
388
393
  }
@@ -400,9 +405,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
400
405
  Effect.orDie,
401
406
  )
402
407
 
403
- if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet)) {
408
+ if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet) === true) {
404
409
  yield* Queue.offer(channelQueue, { packet, respondToSender })
405
- } else if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet)) {
410
+ } else if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet) === true) {
406
411
  yield* Queue.offer(channelQueue, { packet, respondToSender })
407
412
  }
408
413
 
@@ -414,7 +419,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
414
419
  })
415
420
  }
416
421
  } else {
417
- if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet)) {
422
+ if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet) === true) {
418
423
  const noTransferableResponse = checkTransferableEdges(packet)
419
424
  if (noTransferableResponse !== undefined) {
420
425
  yield* Effect.spanEvent(`No transferable edges found for ${packet.source}→${packet.target}`)
@@ -455,8 +460,8 @@ export const makeMeshNode = <TName extends MeshNodeName>(
455
460
 
456
461
  const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
457
462
  Effect.gen(function* () {
458
- if (!edgeChannels.has(targetNodeName)) {
459
- yield* new Cause.NoSuchElementException(`No edge found for ${targetNodeName}`)
463
+ if (edgeChannels.has(targetNodeName) === false) {
464
+ return yield* new Cause.NoSuchElementError(`No edge found for ${targetNodeName}`)
460
465
  }
461
466
 
462
467
  yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
@@ -477,15 +482,16 @@ export const makeMeshNode = <TName extends MeshNodeName>(
477
482
  mode,
478
483
  timeout = Duration.seconds(1),
479
484
  closeExisting = false,
485
+ simulation,
480
486
  }) =>
481
487
  Effect.gen(function* () {
482
488
  const schema = WebChannel.mapSchema(inputSchema)
483
489
  const channelKey = `target:${target}, channelName:${channelName}` satisfies ChannelKey
484
490
 
485
- if (channelMap.has(channelKey)) {
491
+ if (channelMap.has(channelKey) === true) {
486
492
  const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
487
- if (existingChannel) {
488
- if (closeExisting) {
493
+ if (existingChannel !== undefined) {
494
+ if (closeExisting === true) {
489
495
  yield* existingChannel.shutdown
490
496
  channelMap.delete(channelKey)
491
497
  } else {
@@ -495,8 +501,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
495
501
  }
496
502
 
497
503
  if (channelMap.has(channelKey) === false) {
498
- const channelQueue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
499
- Effect.acquireRelease(Queue.shutdown),
504
+ const channelQueue = yield* Effect.acquireRelease(
505
+ Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
506
+ Queue.shutdown,
500
507
  )
501
508
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
502
509
  }
@@ -506,7 +513,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
506
513
  yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
507
514
 
508
515
  if (mode === 'direct') {
509
- const incomingPacketsQueue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
516
+ const incomingPacketsQueue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
510
517
 
511
518
  // We're we're draining the queue into another new queue.
512
519
  // It's a bit of a mystery why this is needed, since the unit tests also work without it.
@@ -534,7 +541,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
534
541
 
535
542
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel: webChannel, target } })
536
543
 
537
- yield* initialEdgeDeferred
544
+ yield* Deferred.await(initialEdgeDeferred)
538
545
 
539
546
  return webChannel
540
547
  } else {
@@ -546,6 +553,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
546
553
  schema,
547
554
  queue: channelQueue,
548
555
  sendPacket,
556
+ ...(simulation !== undefined ? { simulation } : {}),
549
557
  })
550
558
 
551
559
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel, target } })
@@ -564,7 +572,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
564
572
  // TODO also provide a way to allow for reconnects
565
573
  let listenAlreadyStarted = false
566
574
  const listenForChannel: MeshNode['listenForChannel'] = Stream.suspend(() => {
567
- if (listenAlreadyStarted) {
575
+ if (listenAlreadyStarted === true) {
568
576
  return shouldNeverHappen('listenForChannel already started')
569
577
  }
570
578
  listenAlreadyStarted = true
@@ -576,7 +584,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
576
584
  return Stream.fromQueue(channelRequestsQueue).pipe(
577
585
  Stream.filter((res) => {
578
586
  const hashed = hash(res)
579
- if (seen.has(hashed)) {
587
+ if (seen.has(hashed) === true) {
580
588
  return false
581
589
  }
582
590
  seen.add(hashed)
@@ -588,7 +596,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
588
596
  const makeBroadcastChannel: MeshNode['makeBroadcastChannel'] = ({ channelName, schema }) =>
589
597
  Effect.scopeWithCloseable((scope) =>
590
598
  Effect.gen(function* () {
591
- if (broadcastChannelListenQueueMap.has(channelName)) {
599
+ if (broadcastChannelListenQueueMap.has(channelName) === true) {
592
600
  return shouldNeverHappen(
593
601
  `Broadcast channel ${channelName} already exists`,
594
602
  broadcastChannelListenQueueMap.get(channelName),
@@ -597,12 +605,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
597
605
 
598
606
  const debugInfo = {}
599
607
 
600
- const queue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
608
+ const queue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
601
609
  broadcastChannelListenQueueMap.set(channelName, queue)
602
610
 
603
611
  const send = (message: any) =>
604
612
  Effect.gen(function* () {
605
- const payload = yield* Schema.encode(schema)(message)
613
+ const payload = yield* Schema.encodeEffect(schema)(message)
606
614
  const packet = WebmeshSchema.BroadcastChannelPacket.make({
607
615
  channelName,
608
616
  payload,
@@ -616,10 +624,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
616
624
 
617
625
  const listen = Stream.fromQueue(queue).pipe(
618
626
  Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
619
- Stream.map((_) => Schema.decodeEither(schema)(_.payload)),
627
+ Stream.map((_) => Schema.decodeResult(schema)(_.payload)),
620
628
  )
621
629
 
622
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
630
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
623
631
 
624
632
  return {
625
633
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
@@ -636,7 +644,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
636
644
 
637
645
  const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
638
646
 
639
- const runtime = yield* Effect.runtime()
647
+ const services = yield* Effect.context()
640
648
 
641
649
  const debug: MeshNode['debug'] = {
642
650
  print: () => {
@@ -657,12 +665,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
657
665
  supportsTransferables: value.debugInfo?.channel.supportsTransferables,
658
666
  ...value.debugInfo?.channel.debugInfo,
659
667
  })
660
- .map(([key, value]) => indent(`${key}=${value}`, 4))
668
+ .map(([key, value]) => indent(`${key}=${String(value)}`, 4))
661
669
  .join('\n'),
662
670
  ' ',
663
671
  value.debugInfo?.channel,
664
672
  '\n',
665
- indent(`Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`, 4),
673
+ indent(`Queue: ${Queue.sizeUnsafe(value.queue)}`, 4),
666
674
  value.queue,
667
675
  )
668
676
  }
@@ -691,7 +699,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
691
699
  yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
692
700
  yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
693
701
  }
694
- }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork)
702
+ }).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runFork)
695
703
  },
696
704
  requestTopology: (timeoutMs = 1000) =>
697
705
  Effect.gen(function* () {
@@ -714,7 +722,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
714
722
  for (const [key, value] of item) {
715
723
  yield* Effect.logDebug(` node '${key}' has edge to: ${Array.from(value.values()).join(', ')}`)
716
724
  }
717
- }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runPromise),
725
+ }).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runPromise),
718
726
  }
719
727
 
720
728
  return {
package/src/utils.ts CHANGED
@@ -6,14 +6,14 @@ import { Duration, Effect } from '@livestore/utils/effect'
6
6
  */
7
7
  export class TimeoutSet<V> {
8
8
  private values = new Map<V, number>()
9
- private timeoutHandle: NodeJS.Timeout | undefined
9
+ private timeoutHandle: ReturnType<typeof setTimeout> | undefined
10
10
  private readonly timeoutMs: number
11
11
 
12
- private constructor({ timeout }: { timeout: Duration.DurationInput }) {
12
+ private constructor({ timeout }: { timeout: Duration.Input }) {
13
13
  this.timeoutMs = Duration.toMillis(timeout)
14
14
  }
15
15
 
16
- static make = (timeout: Duration.DurationInput) =>
16
+ static make = (timeout: Duration.Input) =>
17
17
  Effect.gen(function* () {
18
18
  const timeoutSet = new TimeoutSet({ timeout })
19
19
 
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tests for websocket-edge.ts
3
+ *
4
+ * These tests verify basic WebSocket edge functionality.
5
+ */
6
+
7
+ import { expect } from 'vitest'
8
+
9
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
10
+ import { Effect, Result, Schema, WebChannel } from '@livestore/utils/effect'
11
+
12
+ import * as MeshSchema from './mesh-schema.ts'
13
+ import { MessageMsgpack, WSEdgeInit, WSEdgePayload } from './websocket-edge.ts'
14
+
15
+ Vitest.describe('websocket-edge', () => {
16
+ /**
17
+ * Test that WSEdgeInit messages can be encoded/decoded via MessageMsgpack.
18
+ */
19
+ Vitest.live('should encode/decode WSEdgeInit', (test) =>
20
+ Effect.gen(function* () {
21
+ const initMessage = WSEdgeInit.make({ from: 'test-node' })
22
+
23
+ // Encode to msgpack
24
+ const encoded = yield* Schema.encodeEffect(MessageMsgpack)(initMessage)
25
+
26
+ // Decode back
27
+ const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
28
+
29
+ expect(decoded._tag).toBe('WSEdgeInit')
30
+ if (decoded._tag === 'WSEdgeInit') {
31
+ expect(decoded.from).toBe('test-node')
32
+ }
33
+ }).pipe(Vitest.withTestCtx(test)),
34
+ )
35
+
36
+ /**
37
+ * Test that WSEdgePayload messages with valid Packet payloads work correctly.
38
+ */
39
+ Vitest.live('should encode/decode WSEdgePayload with Packet', (test) =>
40
+ Effect.gen(function* () {
41
+ const packet = {
42
+ _tag: 'NetworkEdgeAdded' as const,
43
+ id: 'test-id',
44
+ source: 'node-a',
45
+ target: 'node-b',
46
+ }
47
+ const wsMessage = WSEdgePayload.make({ from: 'test-node', payload: packet })
48
+
49
+ // Encode to msgpack
50
+ const encoded = yield* Schema.encodeEffect(MessageMsgpack)(wsMessage)
51
+
52
+ // Decode back
53
+ const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
54
+
55
+ expect(decoded._tag).toBe('WSEdgePayload')
56
+ if (decoded._tag === 'WSEdgePayload') {
57
+ expect(decoded.from).toBe('test-node')
58
+ expect(decoded.payload).toEqual(packet)
59
+ }
60
+ }).pipe(Vitest.withTestCtx(test)),
61
+ )
62
+
63
+ /**
64
+ * Test that mapSchema(Packet) includes WebChannel internal messages.
65
+ * This is important because channels can send/receive WebChannel.Ping/Pong/DebugPing.
66
+ */
67
+ Vitest.live('mapSchema should include WebChannel messages in schema', (test) =>
68
+ Effect.gen(function* () {
69
+ const schema = WebChannel.mapSchema(MeshSchema.Packet)
70
+
71
+ // WebChannel.Ping should be decodable via the wrapped listen schema
72
+ const pingPayload = { _tag: 'WebChannel.Ping' as const, requestId: 'test-123' }
73
+ const result = Schema.decodeUnknownResult(schema.listen)(pingPayload)
74
+
75
+ // mapSchema adds WebChannel messages to the schema
76
+ expect(Result.isSuccess(result)).toBe(true)
77
+ }).pipe(Vitest.withTestCtx(test)),
78
+ )
79
+
80
+ /**
81
+ * Test that valid webmesh packets can be decoded.
82
+ */
83
+ Vitest.live('should decode valid webmesh packets', (test) =>
84
+ Effect.gen(function* () {
85
+ const schema = WebChannel.mapSchema(MeshSchema.Packet)
86
+
87
+ const packet = {
88
+ _tag: 'NetworkEdgeAdded' as const,
89
+ id: 'test-id',
90
+ source: 'node-a',
91
+ target: 'node-b',
92
+ }
93
+
94
+ const result = yield* Schema.decodeUnknownEffect(schema.listen)(packet)
95
+ expect(result._tag).toBe('NetworkEdgeAdded')
96
+ }).pipe(Vitest.withTestCtx(test)),
97
+ )
98
+ })