@livestore/webmesh 0.3.0-dev.19 → 0.3.0-dev.21

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.
package/src/node.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { indent, LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
- import type { Scope } from '@livestore/utils/effect'
3
2
  import {
4
3
  Cause,
4
+ Deferred,
5
5
  Duration,
6
6
  Effect,
7
+ Exit,
7
8
  Fiber,
8
9
  Option,
9
10
  PubSub,
10
11
  Queue,
11
12
  Schema,
13
+ Scope,
12
14
  Stream,
13
15
  WebChannel,
14
16
  } from '@livestore/utils/effect'
@@ -17,13 +19,13 @@ import { makeMessageChannel } from './channel/message-channel.js'
17
19
  import { makeProxyChannel } from './channel/proxy-channel.js'
18
20
  import type { ChannelKey, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.js'
19
21
  import { ConnectionAlreadyExistsError, packetAsOtelAttributes } from './common.js'
20
- import * as MeshSchema from './mesh-schema.js'
22
+ import * as WebmeshSchema from './mesh-schema.js'
21
23
  import { TimeoutSet } from './utils.js'
22
24
 
23
- type ConnectionChannel = WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
25
+ type ConnectionChannel = WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
24
26
 
25
- export interface MeshNode {
26
- nodeName: MeshNodeName
27
+ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
28
+ nodeName: TName
27
29
 
28
30
  connectionKeys: Effect.Effect<Set<MeshNodeName>>
29
31
 
@@ -31,6 +33,10 @@ export interface MeshNode {
31
33
  print: () => void
32
34
  /** Sends a ping message to all connected nodes and channels */
33
35
  ping: (payload?: string) => void
36
+ /**
37
+ * Requests the topology of the network from all connected nodes
38
+ */
39
+ requestTopology: (timeoutMs?: number) => Promise<void>
34
40
  }
35
41
 
36
42
  /**
@@ -95,9 +101,20 @@ export interface MeshNode {
95
101
  */
96
102
  timeout?: Duration.DurationInput
97
103
  }) => Effect.Effect<WebChannel.WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
104
+
105
+ /**
106
+ * Creates a WebChannel that is broadcasted to all connected nodes.
107
+ * Messages won't be buffered for nodes that join the network after the broadcast channel has been created.
108
+ */
109
+ makeBroadcastChannel: <Msg>(args: {
110
+ channelName: string
111
+ schema: Schema.Schema<Msg, any>
112
+ }) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
98
113
  }
99
114
 
100
- export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, never, Scope.Scope> =>
115
+ export const makeMeshNode = <TName extends MeshNodeName>(
116
+ nodeName: TName,
117
+ ): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
101
118
  Effect.gen(function* () {
102
119
  const connectionChannels = new Map<
103
120
  MeshNodeName,
@@ -130,7 +147,13 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
130
147
  }
131
148
  >()
132
149
 
133
- const checkTransferableConnections = (packet: typeof MeshSchema.MessageChannelPacket.Type) => {
150
+ type RequestId = string
151
+ const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
152
+
153
+ type BroadcastChannelName = string
154
+ const broadcastChannelListenQueueMap = new Map<BroadcastChannelName, Queue.Queue<any>>()
155
+
156
+ const checkTransferableConnections = (packet: typeof WebmeshSchema.MessageChannelPacket.Type) => {
134
157
  if (
135
158
  (packet._tag === 'MessageChannelRequest' &&
136
159
  (connectionChannels.size === 0 ||
@@ -139,7 +162,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
139
162
  // ... or if no forward-connections support transferables
140
163
  ![...connectionChannels.values()].some((c) => c.channel.supportsTransferables === true)
141
164
  ) {
142
- return MeshSchema.MessageChannelResponseNoTransferables.make({
165
+ return WebmeshSchema.MessageChannelResponseNoTransferables.make({
143
166
  reqId: packet.id,
144
167
  channelName: packet.channelName,
145
168
  // NOTE for now we're "pretending" that the message is coming from the target node
@@ -154,11 +177,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
154
177
  }
155
178
  }
156
179
 
157
- const sendPacket = (packet: typeof MeshSchema.Packet.Type) =>
180
+ const sendPacket = (packet: typeof WebmeshSchema.Packet.Type) =>
158
181
  Effect.gen(function* () {
159
182
  // yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
160
183
 
161
- if (Schema.is(MeshSchema.NetworkConnectionAdded)(packet)) {
184
+ if (Schema.is(WebmeshSchema.NetworkConnectionAdded)(packet)) {
162
185
  yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
163
186
  yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
164
187
 
@@ -170,6 +193,89 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
170
193
  return
171
194
  }
172
195
 
196
+ if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
197
+ const connectionsToForwardTo = Array.from(connectionChannels)
198
+ .filter(([name]) => !packet.hops.includes(name))
199
+ .map(([_, con]) => con.channel)
200
+
201
+ const adjustedPacket = {
202
+ ...packet,
203
+ hops: [...packet.hops, nodeName],
204
+ }
205
+
206
+ yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
207
+
208
+ // Don't emit the packet to the own node listen queue
209
+ if (packet.source === nodeName) {
210
+ return
211
+ }
212
+
213
+ const queue = broadcastChannelListenQueueMap.get(packet.channelName)
214
+ // In case this node is listening to this channel, add the packet to the listen queue
215
+ if (queue !== undefined) {
216
+ yield* Queue.offer(queue, packet)
217
+ }
218
+
219
+ return
220
+ }
221
+
222
+ if (Schema.is(WebmeshSchema.NetworkConnectionTopologyRequest)(packet)) {
223
+ if (packet.source !== nodeName) {
224
+ const backConnectionName =
225
+ packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
226
+ const backConnectionChannel = connectionChannels.get(backConnectionName)!.channel
227
+
228
+ // Respond with own connection info
229
+ const response = WebmeshSchema.NetworkConnectionTopologyResponse.make({
230
+ reqId: packet.id,
231
+ source: packet.source,
232
+ target: packet.target,
233
+ remainingHops: packet.hops.slice(0, -1),
234
+ nodeName,
235
+ connections: Array.from(connectionChannels.keys()),
236
+ })
237
+
238
+ yield* backConnectionChannel.send(response)
239
+ }
240
+
241
+ // Forward the packet to all connections except the already visited ones
242
+ const connectionsToForwardTo = Array.from(connectionChannels)
243
+ .filter(([name]) => !packet.hops.includes(name))
244
+ .map(([_, con]) => con.channel)
245
+
246
+ const adjustedPacket = {
247
+ ...packet,
248
+ hops: [...packet.hops, nodeName],
249
+ }
250
+
251
+ yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
252
+
253
+ return
254
+ }
255
+
256
+ if (Schema.is(WebmeshSchema.NetworkConnectionTopologyResponse)(packet)) {
257
+ if (packet.source === nodeName) {
258
+ const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
259
+ topologyRequestItem.set(packet.nodeName, new Set(packet.connections))
260
+ } else {
261
+ const remainingHops = packet.remainingHops
262
+ // Forwarding the response to the original sender via the route back
263
+ const routeBack =
264
+ remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
265
+ const connectionChannel =
266
+ connectionChannels.get(routeBack)?.channel ??
267
+ shouldNeverHappen(
268
+ `${nodeName}: Expected connection channel (${routeBack}) for packet`,
269
+ packet,
270
+ 'Available connections:',
271
+ Array.from(connectionChannels.keys()),
272
+ )
273
+
274
+ yield* connectionChannel.send({ ...packet, remainingHops: packet.remainingHops.slice(0, -1) })
275
+ }
276
+ return
277
+ }
278
+
173
279
  // We have a direct connection to the target node
174
280
  if (connectionChannels.has(packet.target)) {
175
281
  const connectionChannel = connectionChannels.get(packet.target)!.channel
@@ -180,7 +286,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
180
286
  // eslint-disable-next-line unicorn/no-negated-condition
181
287
  else if (packet.remainingHops !== undefined) {
182
288
  const hopTarget =
183
- packet.remainingHops[0] ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
289
+ packet.remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
184
290
  const connectionChannel = connectionChannels.get(hopTarget)?.channel
185
291
 
186
292
  if (connectionChannel === undefined) {
@@ -193,7 +299,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
193
299
 
194
300
  yield* connectionChannel.send({
195
301
  ...packet,
196
- remainingHops: packet.remainingHops.slice(1),
302
+ remainingHops: packet.remainingHops.slice(0, -1),
197
303
  hops: [...packet.hops, nodeName],
198
304
  })
199
305
  }
@@ -245,57 +351,68 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
245
351
  Stream.flatten(),
246
352
  Stream.tap((message) =>
247
353
  Effect.gen(function* () {
248
- const packet = yield* Schema.decodeUnknown(MeshSchema.Packet)(message)
354
+ const packet = yield* Schema.decodeUnknown(WebmeshSchema.Packet)(message)
249
355
 
250
356
  // console.debug(nodeName, 'received', packet._tag, packet.source, packet.target)
251
357
 
252
358
  if (handledPacketIds.has(packet.id)) return
253
359
  handledPacketIds.add(packet.id)
254
360
 
255
- if (packet._tag === 'NetworkConnectionAdded') {
256
- yield* sendPacket(packet)
257
- } else if (packet.target === nodeName) {
258
- const channelKey = `${packet.source}-${packet.channelName}` satisfies ChannelKey
361
+ switch (packet._tag) {
362
+ case 'NetworkConnectionAdded':
363
+ case 'NetworkConnectionTopologyRequest':
364
+ case 'NetworkConnectionTopologyResponse': {
365
+ yield* sendPacket(packet)
259
366
 
260
- if (!channelMap.has(channelKey)) {
261
- const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
262
- Effect.acquireRelease(Queue.shutdown),
263
- )
264
- channelMap.set(channelKey, { queue, debugInfo: undefined })
367
+ break
265
368
  }
266
-
267
- const queue = channelMap.get(channelKey)!.queue
268
-
269
- const respondToSender = (outgoingPacket: typeof MeshSchema.Packet.Type) =>
270
- connectionChannel
271
- .send(outgoingPacket)
272
- .pipe(
273
- Effect.withSpan(
274
- `respondToSender:${outgoingPacket._tag}:${outgoingPacket.source}→${outgoingPacket.target}`,
275
- { attributes: packetAsOtelAttributes(outgoingPacket) },
276
- ),
277
- Effect.orDie,
278
- )
279
-
280
- if (Schema.is(MeshSchema.ProxyChannelPacket)(packet)) {
281
- yield* Queue.offer(queue, { packet, respondToSender })
282
- } else if (Schema.is(MeshSchema.MessageChannelPacket)(packet)) {
283
- yield* Queue.offer(queue, { packet, respondToSender })
284
- }
285
- } else {
286
- if (Schema.is(MeshSchema.MessageChannelPacket)(packet)) {
287
- const noTransferableResponse = checkTransferableConnections(packet)
288
- if (noTransferableResponse !== undefined) {
289
- yield* Effect.spanEvent(`No transferable connections found for ${packet.source}→${packet.target}`)
290
- return yield* connectionChannel.send(noTransferableResponse).pipe(
291
- Effect.withSpan(`sendNoTransferableResponse:${packet.source}→${packet.target}`, {
292
- attributes: packetAsOtelAttributes(noTransferableResponse),
293
- }),
294
- )
369
+ default: {
370
+ if (packet.target === nodeName) {
371
+ const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
372
+
373
+ if (!channelMap.has(channelKey)) {
374
+ const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
375
+ Effect.acquireRelease(Queue.shutdown),
376
+ )
377
+ channelMap.set(channelKey, { queue, debugInfo: undefined })
378
+ }
379
+
380
+ const queue = channelMap.get(channelKey)!.queue
381
+
382
+ const respondToSender = (outgoingPacket: typeof WebmeshSchema.Packet.Type) =>
383
+ connectionChannel
384
+ .send(outgoingPacket)
385
+ .pipe(
386
+ Effect.withSpan(
387
+ `respondToSender:${outgoingPacket._tag}:${outgoingPacket.source}→${outgoingPacket.target}`,
388
+ { attributes: packetAsOtelAttributes(outgoingPacket) },
389
+ ),
390
+ Effect.orDie,
391
+ )
392
+
393
+ if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet)) {
394
+ yield* Queue.offer(queue, { packet, respondToSender })
395
+ } else if (Schema.is(WebmeshSchema.MessageChannelPacket)(packet)) {
396
+ yield* Queue.offer(queue, { packet, respondToSender })
397
+ }
398
+ } else {
399
+ if (Schema.is(WebmeshSchema.MessageChannelPacket)(packet)) {
400
+ const noTransferableResponse = checkTransferableConnections(packet)
401
+ if (noTransferableResponse !== undefined) {
402
+ yield* Effect.spanEvent(
403
+ `No transferable connections found for ${packet.source}→${packet.target}`,
404
+ )
405
+ return yield* connectionChannel.send(noTransferableResponse).pipe(
406
+ Effect.withSpan(`sendNoTransferableResponse:${packet.source}→${packet.target}`, {
407
+ attributes: packetAsOtelAttributes(noTransferableResponse),
408
+ }),
409
+ )
410
+ }
411
+ }
412
+
413
+ yield* sendPacket(packet)
295
414
  }
296
415
  }
297
-
298
- yield* sendPacket(packet)
299
416
  }
300
417
  }),
301
418
  ),
@@ -307,11 +424,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
307
424
 
308
425
  connectionChannels.set(targetNodeName, { channel: connectionChannel, listenFiber })
309
426
 
310
- const connectionAddedPacket = MeshSchema.NetworkConnectionAdded.make({
427
+ const connectionAddedPacket = WebmeshSchema.NetworkConnectionAdded.make({
311
428
  source: nodeName,
312
429
  target: targetNodeName,
313
430
  })
314
- yield* sendPacket(connectionAddedPacket).pipe(Effect.ignoreLogged)
431
+ yield* sendPacket(connectionAddedPacket).pipe(Effect.orDie)
315
432
  }).pipe(
316
433
  Effect.withSpan(`addConnection:${nodeName}→${targetNodeName}`, {
317
434
  attributes: { supportsTransferables: connectionChannel.supportsTransferables },
@@ -341,7 +458,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
341
458
  }) =>
342
459
  Effect.gen(function* () {
343
460
  const schema = WebChannel.mapSchema(inputSchema)
344
- const channelKey = `${target}-${channelName}` satisfies ChannelKey
461
+ const channelKey = `target:${target}, channelName:${channelName}` satisfies ChannelKey
345
462
 
346
463
  if (channelMap.has(channelKey)) {
347
464
  const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
@@ -413,10 +530,63 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
413
530
  Effect.annotateLogs({ nodeName }),
414
531
  )
415
532
 
533
+ const makeBroadcastChannel: MeshNode['makeBroadcastChannel'] = ({ channelName, schema }) =>
534
+ Effect.scopeWithCloseable((scope) =>
535
+ Effect.gen(function* () {
536
+ if (broadcastChannelListenQueueMap.has(channelName)) {
537
+ return shouldNeverHappen(
538
+ `Broadcast channel ${channelName} already exists`,
539
+ broadcastChannelListenQueueMap.get(channelName),
540
+ )
541
+ }
542
+
543
+ const debugInfo = {}
544
+
545
+ const queue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
546
+ broadcastChannelListenQueueMap.set(channelName, queue)
547
+
548
+ const send = (message: any) =>
549
+ Effect.gen(function* () {
550
+ const payload = yield* Schema.encode(schema)(message)
551
+ const packet = WebmeshSchema.BroadcastChannelPacket.make({
552
+ channelName,
553
+ payload,
554
+ source: nodeName,
555
+ target: '-',
556
+ hops: [],
557
+ })
558
+
559
+ yield* sendPacket(packet)
560
+ })
561
+
562
+ const listen = Stream.fromQueue(queue).pipe(
563
+ Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
564
+ Stream.map((_) => Schema.decodeEither(schema)(_.payload)),
565
+ )
566
+
567
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
568
+
569
+ return {
570
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
571
+ send,
572
+ listen,
573
+ closedDeferred,
574
+ supportsTransferables: true,
575
+ schema: { listen: schema, send: schema },
576
+ shutdown: Scope.close(scope, Exit.void),
577
+ debugInfo,
578
+ } satisfies WebChannel.WebChannel<any, any>
579
+ }),
580
+ )
581
+
416
582
  const connectionKeys: MeshNode['connectionKeys'] = Effect.sync(() => new Set(connectionChannels.keys()))
417
583
 
584
+ const runtime = yield* Effect.runtime()
585
+
418
586
  const debug: MeshNode['debug'] = {
419
587
  print: () => {
588
+ console.log('Webmesh debug info for node:', nodeName)
589
+
420
590
  console.log('Connections:', connectionChannels.size)
421
591
  for (const [key, value] of connectionChannels) {
422
592
  console.log(` ${key}: supportsTransferables=${value.channel.supportsTransferables}`)
@@ -441,26 +611,59 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
441
611
  value.queue,
442
612
  )
443
613
  }
614
+
615
+ console.log('Broadcast channels:', broadcastChannelListenQueueMap.size)
616
+ for (const [key, _value] of broadcastChannelListenQueueMap) {
617
+ console.log(indent(key, 2))
618
+ }
444
619
  },
445
620
  ping: (payload) => {
446
621
  Effect.gen(function* () {
447
- const msg = (via: string) => {
448
- console.log(`sending message to ${via}`)
449
- return WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via ${via}`, payload })
450
- }
622
+ const msg = (via: string) =>
623
+ WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via connection ${via}`, payload })
451
624
 
452
- yield* Effect.forEach(connectionChannels, ([channelName, con]) =>
453
- con.channel.send(msg(`connection ${channelName}`) as any),
454
- )
625
+ for (const [channelName, con] of connectionChannels) {
626
+ yield* Effect.logDebug(`sending ping via connection ${channelName}`)
627
+ yield* con.channel.send(msg(`connection ${channelName}`) as any)
628
+ }
455
629
 
456
- yield* Effect.forEach(
457
- channelMap,
458
- ([channelKey, channel]) =>
459
- channel.debugInfo?.channel.send(msg(`channel ${channelKey}`) as any) ?? Effect.void,
460
- )
461
- }).pipe(Effect.runFork)
630
+ for (const [channelKey, channel] of channelMap) {
631
+ if (channel.debugInfo === undefined) continue
632
+ yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
633
+ yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
634
+ }
635
+ }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork)
462
636
  },
637
+ requestTopology: (timeoutMs = 1000) =>
638
+ Effect.gen(function* () {
639
+ const packet = WebmeshSchema.NetworkConnectionTopologyRequest.make({
640
+ source: nodeName,
641
+ target: '-',
642
+ hops: [],
643
+ })
644
+
645
+ const item = new Map<MeshNodeName, Set<MeshNodeName>>()
646
+ item.set(nodeName, new Set(connectionChannels.keys()))
647
+ topologyRequestsMap.set(packet.id, item)
648
+
649
+ yield* sendPacket(packet)
650
+
651
+ yield* Effect.logDebug(`Waiting ${timeoutMs}ms for topology response`)
652
+ yield* Effect.sleep(timeoutMs)
653
+
654
+ for (const [key, value] of item) {
655
+ yield* Effect.logDebug(`node '${key}' is connected to: ${Array.from(value.values()).join(', ')}`)
656
+ }
657
+ }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runPromise),
463
658
  }
464
659
 
465
- return { nodeName, addConnection, removeConnection, makeChannel, connectionKeys, debug } satisfies MeshNode
660
+ return {
661
+ nodeName,
662
+ addConnection,
663
+ removeConnection,
664
+ makeChannel,
665
+ makeBroadcastChannel,
666
+ connectionKeys,
667
+ debug,
668
+ } satisfies MeshNode
466
669
  }).pipe(Effect.withSpan(`makeMeshNode:${nodeName}`))
@@ -82,6 +82,8 @@ export const makeWebSocketConnection = (
82
82
  Effect.acquireRelease(Queue.shutdown),
83
83
  )
84
84
 
85
+ const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
86
+
85
87
  yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
86
88
  Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
87
89
  Stream.flatten(),
@@ -90,7 +92,7 @@ export const makeWebSocketConnection = (
90
92
  if (msg._tag === 'WSConnectionInit') {
91
93
  yield* Deferred.succeed(fromDeferred, msg.from)
92
94
  } else {
93
- const decodedPayload = yield* Schema.decode(WebmeshSchema.Packet)(msg.payload)
95
+ const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
94
96
  yield* Queue.offer(listenQueue, decodedPayload)
95
97
  }
96
98
  }),
@@ -133,18 +135,21 @@ export const makeWebSocketConnection = (
133
135
  const send = (message: typeof WebmeshSchema.Packet.Type) =>
134
136
  Effect.gen(function* () {
135
137
  yield* isConnectedLatch.await
136
- const payload = yield* Schema.encode(WebmeshSchema.Packet)(message)
138
+ const payload = yield* Schema.encode(schema.send)(message)
137
139
  socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
138
140
  })
139
141
 
140
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
142
+ const listen = Stream.fromQueue(listenQueue).pipe(
143
+ Stream.map(Either.right),
144
+ WebChannel.listenToDebugPing('websocket-connection'),
145
+ )
141
146
 
142
147
  const webChannel = {
143
148
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
144
149
  send,
145
150
  listen,
146
151
  closedDeferred,
147
- schema: { listen: WebmeshSchema.Packet, send: WebmeshSchema.Packet },
152
+ schema,
148
153
  supportsTransferables: false,
149
154
  shutdown: Scope.close(scope, Exit.void),
150
155
  } satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>