@livestore/webmesh 0.3.0-dev.10 → 0.3.0-dev.12

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 (51) hide show
  1. package/README.md +20 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel copy.d.ts +9 -0
  4. package/dist/channel/message-channel copy.d.ts.map +1 -0
  5. package/dist/channel/message-channel copy.js +137 -0
  6. package/dist/channel/message-channel copy.js.map +1 -0
  7. package/dist/channel/message-channel-internal copy.d.ts +42 -0
  8. package/dist/channel/message-channel-internal copy.d.ts.map +1 -0
  9. package/dist/channel/message-channel-internal copy.js +239 -0
  10. package/dist/channel/message-channel-internal copy.js.map +1 -0
  11. package/dist/channel/message-channel-internal.d.ts +26 -0
  12. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  13. package/dist/channel/message-channel-internal.js +217 -0
  14. package/dist/channel/message-channel-internal.js.map +1 -0
  15. package/dist/channel/message-channel.d.ts +21 -19
  16. package/dist/channel/message-channel.d.ts.map +1 -1
  17. package/dist/channel/message-channel.js +128 -162
  18. package/dist/channel/message-channel.js.map +1 -1
  19. package/dist/channel/proxy-channel.d.ts +2 -2
  20. package/dist/channel/proxy-channel.d.ts.map +1 -1
  21. package/dist/channel/proxy-channel.js +7 -5
  22. package/dist/channel/proxy-channel.js.map +1 -1
  23. package/dist/common.d.ts +8 -4
  24. package/dist/common.d.ts.map +1 -1
  25. package/dist/common.js +2 -1
  26. package/dist/common.js.map +1 -1
  27. package/dist/mesh-schema.d.ts +23 -1
  28. package/dist/mesh-schema.d.ts.map +1 -1
  29. package/dist/mesh-schema.js +21 -2
  30. package/dist/mesh-schema.js.map +1 -1
  31. package/dist/node.d.ts +12 -1
  32. package/dist/node.d.ts.map +1 -1
  33. package/dist/node.js +40 -9
  34. package/dist/node.js.map +1 -1
  35. package/dist/node.test.d.ts +1 -1
  36. package/dist/node.test.d.ts.map +1 -1
  37. package/dist/node.test.js +300 -147
  38. package/dist/node.test.js.map +1 -1
  39. package/dist/websocket-connection.d.ts +1 -2
  40. package/dist/websocket-connection.d.ts.map +1 -1
  41. package/dist/websocket-connection.js +5 -4
  42. package/dist/websocket-connection.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/channel/message-channel-internal.ts +356 -0
  45. package/src/channel/message-channel.ts +183 -311
  46. package/src/channel/proxy-channel.ts +238 -230
  47. package/src/common.ts +3 -1
  48. package/src/mesh-schema.ts +20 -2
  49. package/src/node.test.ts +426 -177
  50. package/src/node.ts +70 -12
  51. package/src/websocket-connection.ts +83 -79
package/src/node.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
1
+ import { indent, LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
2
  import type { Scope } from '@livestore/utils/effect'
3
3
  import {
4
4
  Cause,
@@ -29,6 +29,8 @@ export interface MeshNode {
29
29
 
30
30
  debug: {
31
31
  print: () => void
32
+ /** Sends a ping message to all connected nodes and channels */
33
+ ping: (payload?: string) => void
32
34
  }
33
35
 
34
36
  /**
@@ -58,7 +60,16 @@ export interface MeshNode {
58
60
  /**
59
61
  * Tries to broker a MessageChannel connection between the nodes, otherwise will proxy messages via hop-nodes
60
62
  *
61
- * For a channel to successfully open, both sides need to have a connection and call `makeChannel`
63
+ * For a channel to successfully open, both sides need to have a connection and call `makeChannel`.
64
+ *
65
+ * Example:
66
+ * ```ts
67
+ * // Code on node A
68
+ * const channel = nodeA.makeChannel({ target: 'B', channelName: 'my-channel', schema: ... })
69
+ *
70
+ * // Code on node B
71
+ * const channel = nodeB.makeChannel({ target: 'A', channelName: 'my-channel', schema: ... })
72
+ * ```
62
73
  */
63
74
  makeChannel: <MsgListen, MsgSend>(args: {
64
75
  target: MeshNodeName
@@ -145,6 +156,8 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
145
156
 
146
157
  const sendPacket = (packet: typeof MeshSchema.Packet.Type) =>
147
158
  Effect.gen(function* () {
159
+ // yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
160
+
148
161
  if (Schema.is(MeshSchema.NetworkConnectionAdded)(packet)) {
149
162
  yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
150
163
  yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
@@ -330,7 +343,12 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
330
343
  const schema = WebChannel.mapSchema(inputSchema)
331
344
  const channelKey = `${target}-${channelName}` satisfies ChannelKey
332
345
 
333
- if (!channelMap.has(channelKey)) {
346
+ if (channelMap.has(channelKey)) {
347
+ const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
348
+ if (existingChannel) {
349
+ shouldNeverHappen(`Channel ${channelKey} already exists`, existingChannel)
350
+ }
351
+ } else {
334
352
  const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
335
353
  Effect.acquireRelease(Queue.shutdown),
336
354
  )
@@ -342,12 +360,23 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
342
360
  yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
343
361
 
344
362
  if (mode === 'messagechannel') {
345
- // console.debug(nodeName, 'message mode', modeRef.current)
363
+ const incomingPacketsQueue = yield* Queue.unbounded<any>()
364
+
365
+ // We're we're draining the queue into another new queue.
366
+ // It's a bit of a mystery why this is needed, since the unit tests also work without it.
367
+ // But for the LiveStore devtools to actually work, we need to do this.
368
+ // We should figure out some day why this is needed and further simplify if possible.
369
+ yield* Queue.takeBetween(queue, 1, 10).pipe(
370
+ Effect.tap((_) => Queue.offerAll(incomingPacketsQueue, _)),
371
+ Effect.forever,
372
+ Effect.tapCauseLogPretty,
373
+ Effect.forkScoped,
374
+ )
346
375
 
347
376
  // NOTE already retries internally when transferables are required
348
- const channel = yield* makeMessageChannel({
377
+ const { webChannel, initialConnectionDeferred } = yield* makeMessageChannel({
349
378
  nodeName,
350
- queue,
379
+ incomingPacketsQueue,
351
380
  newConnectionAvailablePubSub,
352
381
  target,
353
382
  channelName,
@@ -356,9 +385,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
356
385
  checkTransferableConnections,
357
386
  })
358
387
 
359
- channelMap.set(channelKey, { queue, debugInfo: { channel, target } })
388
+ channelMap.set(channelKey, { queue, debugInfo: { channel: webChannel, target } })
360
389
 
361
- return channel
390
+ yield* initialConnectionDeferred
391
+
392
+ return webChannel
362
393
  } else {
363
394
  const channel = yield* makeProxyChannel({
364
395
  nodeName,
@@ -375,6 +406,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
375
406
  return channel
376
407
  }
377
408
  }).pipe(
409
+ // Effect.timeout(timeout),
378
410
  Effect.withSpanScoped(`makeChannel:${nodeName}→${target}(${channelName})`, {
379
411
  attributes: { target, channelName, mode, timeout },
380
412
  }),
@@ -393,15 +425,41 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
393
425
  console.log('Channels:', channelMap.size)
394
426
  for (const [key, value] of channelMap) {
395
427
  console.log(
396
- ` ${key}: \n`,
397
- ` Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`,
398
- value.queue,
428
+ indent(key, 2),
399
429
  '\n',
400
- ` Channel: target=${value.debugInfo?.target} supportsTransferables=${value.debugInfo?.channel.supportsTransferables}`,
430
+ Object.entries({
431
+ target: value.debugInfo?.target,
432
+ supportsTransferables: value.debugInfo?.channel.supportsTransferables,
433
+ ...value.debugInfo?.channel.debugInfo,
434
+ })
435
+ .map(([key, value]) => indent(`${key}=${value}`, 4))
436
+ .join('\n'),
437
+ ' ',
401
438
  value.debugInfo?.channel,
439
+ '\n',
440
+ indent(`Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`, 4),
441
+ value.queue,
402
442
  )
403
443
  }
404
444
  },
445
+ ping: (payload) => {
446
+ 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
+ }
451
+
452
+ yield* Effect.forEach(connectionChannels, ([channelName, con]) =>
453
+ con.channel.send(msg(`connection ${channelName}`) as any),
454
+ )
455
+
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)
462
+ },
405
463
  }
406
464
 
407
465
  return { nodeName, addConnection, removeConnection, makeChannel, connectionKeys, debug } satisfies MeshNode
@@ -1,12 +1,13 @@
1
- import type { Scope } from '@livestore/utils/effect'
2
1
  import {
3
2
  Deferred,
4
3
  Effect,
5
4
  Either,
5
+ Exit,
6
6
  FiberHandle,
7
7
  Queue,
8
8
  Schedule,
9
9
  Schema,
10
+ Scope,
10
11
  Stream,
11
12
  WebChannel,
12
13
  WebSocket,
@@ -77,82 +78,85 @@ export const makeWebSocketConnection = (
77
78
  never,
78
79
  Scope.Scope
79
80
  > =>
80
- Effect.gen(function* () {
81
- socket.binaryType = 'arraybuffer'
82
-
83
- const fromDeferred = yield* Deferred.make<string>()
84
-
85
- yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
86
- Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
87
- Stream.flatten(),
88
- Stream.tap((msg) =>
81
+ Effect.scopeWithCloseable((scope) =>
82
+ Effect.gen(function* () {
83
+ socket.binaryType = 'arraybuffer'
84
+
85
+ const fromDeferred = yield* Deferred.make<string>()
86
+
87
+ yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
88
+ Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
89
+ Stream.flatten(),
90
+ Stream.tap((msg) =>
91
+ Effect.gen(function* () {
92
+ if (msg._tag === 'WSConnectionInit') {
93
+ yield* Deferred.succeed(fromDeferred, msg.from)
94
+ } else {
95
+ const decodedPayload = yield* Schema.decode(MeshSchema.Packet)(msg.payload)
96
+ yield* Queue.offer(listenQueue, decodedPayload)
97
+ }
98
+ }),
99
+ ),
100
+ Stream.runDrain,
101
+ Effect.tapCauseLogPretty,
102
+ Effect.forkScoped,
103
+ )
104
+
105
+ const listenQueue = yield* Queue.unbounded<typeof MeshSchema.Packet.Type>().pipe(
106
+ Effect.acquireRelease(Queue.shutdown),
107
+ )
108
+
109
+ const initHandshake = (from: string) =>
110
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
111
+
112
+ if (socketType._tag === 'leaf') {
113
+ initHandshake(socketType.from)
114
+ }
115
+
116
+ const deferredResult = yield* fromDeferred
117
+ const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
118
+
119
+ if (socketType._tag === 'relay') {
120
+ initHandshake(from)
121
+ }
122
+
123
+ const isConnectedLatch = yield* Effect.makeLatch(true)
124
+
125
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
126
+
127
+ yield* Effect.eventListener<any>(
128
+ socket,
129
+ 'close',
130
+ () =>
131
+ Effect.gen(function* () {
132
+ yield* isConnectedLatch.close
133
+ yield* Deferred.succeed(closedDeferred, undefined)
134
+ }),
135
+ { once: true },
136
+ )
137
+
138
+ const send = (message: typeof MeshSchema.Packet.Type) =>
89
139
  Effect.gen(function* () {
90
- if (msg._tag === 'WSConnectionInit') {
91
- yield* Deferred.succeed(fromDeferred, msg.from)
92
- } else {
93
- const decodedPayload = yield* Schema.decode(MeshSchema.Packet)(msg.payload)
94
- yield* Queue.offer(listenQueue, decodedPayload)
95
- }
96
- }),
97
- ),
98
- Stream.runDrain,
99
- Effect.tapCauseLogPretty,
100
- Effect.forkScoped,
101
- )
102
-
103
- const listenQueue = yield* Queue.unbounded<typeof MeshSchema.Packet.Type>().pipe(
104
- Effect.acquireRelease(Queue.shutdown),
105
- )
106
-
107
- const initHandshake = (from: string) =>
108
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
109
-
110
- if (socketType._tag === 'leaf') {
111
- initHandshake(socketType.from)
112
- }
113
-
114
- const deferredResult = yield* fromDeferred
115
- const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
116
-
117
- if (socketType._tag === 'relay') {
118
- initHandshake(from)
119
- }
120
-
121
- const isConnectedLatch = yield* Effect.makeLatch(true)
122
-
123
- const closedDeferred = yield* Deferred.make<void>()
124
-
125
- yield* Effect.eventListener<any>(
126
- socket,
127
- 'close',
128
- () =>
129
- Effect.gen(function* () {
130
- yield* isConnectedLatch.close
131
- yield* Deferred.succeed(closedDeferred, undefined)
132
- }),
133
- { once: true },
134
- )
135
-
136
- const send = (message: typeof MeshSchema.Packet.Type) =>
137
- Effect.gen(function* () {
138
- yield* isConnectedLatch.await
139
- const payload = yield* Schema.encode(MeshSchema.Packet)(message)
140
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
141
- })
142
-
143
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
144
-
145
- const webChannel = {
146
- [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
147
- send,
148
- listen,
149
- closedDeferred,
150
- schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
151
- supportsTransferables: false,
152
- } satisfies WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
153
-
154
- return {
155
- webChannel: webChannel as WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>,
156
- from,
157
- }
158
- })
140
+ yield* isConnectedLatch.await
141
+ const payload = yield* Schema.encode(MeshSchema.Packet)(message)
142
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
143
+ })
144
+
145
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
146
+
147
+ const webChannel = {
148
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
149
+ send,
150
+ listen,
151
+ closedDeferred,
152
+ schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
153
+ supportsTransferables: false,
154
+ shutdown: Scope.close(scope, Exit.void),
155
+ } satisfies WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
156
+
157
+ return {
158
+ webChannel: webChannel as WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>,
159
+ from,
160
+ }
161
+ }).pipe(Effect.withSpanScoped('makeWebSocketConnection')),
162
+ )