@livestore/webmesh 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230 → 0.0.0-snapshot-7bcbc24bb8873481e482d982636957f0c1f791f6

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.
@@ -90,6 +90,9 @@ export const makeMessageChannelInternal = ({
90
90
  const deferred = yield* makeDeferredResult()
91
91
 
92
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
+ // }
93
96
 
94
97
  const schema = {
95
98
  send: Schema.Union(schema_.send, MeshSchema.MessageChannelPing, MeshSchema.MessageChannelPong),
@@ -111,8 +114,10 @@ export const makeMessageChannelInternal = ({
111
114
  packetChannelVersion: Predicate.hasProperty('channelVersion')(packet) ? packet.channelVersion : undefined,
112
115
  })
113
116
 
117
+ // const reqIdStr =
118
+ // Predicate.hasProperty('reqId')(packet) && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''
114
119
  // yield* Effect.log(
115
- // `${nodeName}→${channelName}→${target}:process packet ${packet._tag} [${channelVersion}], channel state: ${channelState._tag}`,
120
+ // `${nodeName}→${channelName}→${target}[${channelVersion}]:process packet ${packet._tag} [${packet.id}${reqIdStr}], channel state: ${channelState._tag}`,
116
121
  // )
117
122
 
118
123
  if (channelState._tag === 'Initial') return shouldNeverHappen()
@@ -222,6 +227,8 @@ export const makeMessageChannelInternal = ({
222
227
 
223
228
  channelStateRef.current = { _tag: 'winner:ResponseSent', channel, otherSourceId: packet.sourceId }
224
229
 
230
+ // span?.addEvent(`winner side: waiting for ping`)
231
+
225
232
  // Now we wait for the other side to respond via the channel
226
233
  yield* channel.listen.pipe(
227
234
  Stream.flatten(),
@@ -230,6 +237,8 @@ export const makeMessageChannelInternal = ({
230
237
  Stream.runDrain,
231
238
  )
232
239
 
240
+ // span?.addEvent(`winner side: sending pong`)
241
+
233
242
  yield* channel.send(MeshSchema.MessageChannelPong.make({}))
234
243
 
235
244
  span?.addEvent(`winner side: established`)
@@ -266,7 +275,17 @@ export const makeMessageChannelInternal = ({
266
275
  Effect.fork,
267
276
  )
268
277
 
269
- yield* channel.send(MeshSchema.MessageChannelPing.make({}))
278
+ // span?.addEvent(`loser side: sending ping`)
279
+
280
+ // There seems to be some scenario where the initial ping message is lost.
281
+ // As a workaround until we find the root cause, we're retrying the ping a few times.
282
+ // TODO write a test that reproduces this issue and fix the root cause ()
283
+ // https://github.com/livestorejs/livestore/issues/262
284
+ yield* channel
285
+ .send(MeshSchema.MessageChannelPing.make({}))
286
+ .pipe(Effect.timeout(10), Effect.retry({ times: 2 }))
287
+
288
+ // span?.addEvent(`loser side: waiting for pong`)
270
289
 
271
290
  yield* waitForPongFiber
272
291
 
@@ -4,6 +4,7 @@ import {
4
4
  Effect,
5
5
  Either,
6
6
  Exit,
7
+ Option,
7
8
  Queue,
8
9
  Schema,
9
10
  Scope,
@@ -55,8 +56,9 @@ export const makeMessageChannel = ({
55
56
  const debugInfo = {
56
57
  pendingSends: 0,
57
58
  totalSends: 0,
58
- connectCounter: 1,
59
+ connectCounter: 0,
59
60
  isConnected: false,
61
+ innerChannelRef: { current: undefined as WebChannel.WebChannel<any, any> | undefined },
60
62
  }
61
63
 
62
64
  // #region reconnect-loop
@@ -112,26 +114,33 @@ export const makeMessageChannel = ({
112
114
  newConnectionAvailablePubSub,
113
115
  sendPacket,
114
116
  scope: makeMessageChannelScope,
115
- }).pipe(Scope.extend(makeMessageChannelScope), Effect.forkIn(makeMessageChannelScope))
117
+ }).pipe(
118
+ Scope.extend(makeMessageChannelScope),
119
+ Effect.forkIn(makeMessageChannelScope),
120
+ // Given we only call `Effect.exit` later when joining the fiber,
121
+ // we don't want Effect to produce a "unhandled error" log message
122
+ Effect.withUnhandledErrorLogLevel(Option.none()),
123
+ )
116
124
 
117
- const res = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect))
125
+ const raceResult = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect))
118
126
 
119
- if (res === 'new-connection') {
127
+ if (raceResult === 'new-connection') {
120
128
  yield* Scope.close(makeMessageChannelScope, Exit.fail('new-connection'))
121
129
  // We'll try again
122
130
  } else {
123
- const result = yield* res.pipe(Effect.exit)
124
- if (result._tag === 'Failure') {
125
- yield* Scope.close(makeMessageChannelScope, result)
131
+ const channelExit = yield* raceResult.pipe(Effect.exit)
132
+ if (channelExit._tag === 'Failure') {
133
+ yield* Scope.close(makeMessageChannelScope, channelExit)
126
134
 
127
135
  if (
128
- Cause.isFailType(result.cause) &&
129
- Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(result.cause.error)
136
+ Cause.isFailType(channelExit.cause) &&
137
+ Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(channelExit.cause.error)
130
138
  ) {
139
+ // Only retry when there is a new connection available
131
140
  yield* waitForNewConnectionFiber.pipe(Effect.exit)
132
141
  }
133
142
  } else {
134
- const channel = result.value
143
+ const channel = channelExit.value
135
144
 
136
145
  yield* Deferred.succeed(resultDeferred, { channel, makeMessageChannelScope, channelVersion })
137
146
  break
@@ -144,6 +153,7 @@ export const makeMessageChannel = ({
144
153
 
145
154
  yield* Effect.spanEvent(`Connected#${channelVersion}`)
146
155
  debugInfo.isConnected = true
156
+ debugInfo.innerChannelRef.current = channel
147
157
 
148
158
  yield* Deferred.succeed(initialConnectionDeferred, void 0)
149
159
 
@@ -176,6 +186,7 @@ export const makeMessageChannel = ({
176
186
 
177
187
  yield* Effect.spanEvent(`Disconnected#${channelVersion}`)
178
188
  debugInfo.isConnected = false
189
+ debugInfo.innerChannelRef.current = undefined
179
190
  }).pipe(
180
191
  Effect.scoped, // Additionally scoping here to clean up finalizers after each loop run
181
192
  Effect.forever,
package/src/node.test.ts CHANGED
@@ -106,80 +106,79 @@ const propTestTimeout = IS_CI ? 60_000 : 20_000
106
106
  // TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
107
107
  // probably requires controlling the clocks
108
108
  Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
109
- Vitest.describe('A <> B', () => {
110
- Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
111
- const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
112
- // NOTE for message channels, we test both with and without transferables (i.e. proxying)
113
- const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
114
- const NodeNames = Schema.Union(
115
- Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
116
- Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
117
- )
109
+ const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
110
+ // NOTE for message channels, we test both with and without transferables (i.e. proxying)
111
+ const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
112
+ const NodeNames = Schema.Union(
113
+ Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
114
+ Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
115
+ )
118
116
 
119
- const fromChannelType = (
120
- channelType: typeof ChannelType.Type,
121
- ): {
122
- mode: 'messagechannel' | 'proxy'
123
- connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
124
- } => {
125
- switch (channelType) {
126
- case 'proxy': {
127
- return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
128
- }
129
- case 'messagechannel': {
130
- return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
131
- }
132
- case 'messagechannel.proxy': {
133
- return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
134
- }
135
- }
117
+ const fromChannelType = (
118
+ channelType: typeof ChannelType.Type,
119
+ ): {
120
+ mode: 'messagechannel' | 'proxy'
121
+ connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
122
+ } => {
123
+ switch (channelType) {
124
+ case 'proxy': {
125
+ return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
136
126
  }
127
+ case 'messagechannel': {
128
+ return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
129
+ }
130
+ case 'messagechannel.proxy': {
131
+ return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
132
+ }
133
+ }
134
+ }
135
+
136
+ const exchangeMessages = ({
137
+ nodeX,
138
+ nodeY,
139
+ channelType,
140
+ // numberOfMessages = 1,
141
+ delays,
142
+ }: {
143
+ nodeX: MeshNode
144
+ nodeY: MeshNode
145
+ channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
146
+ numberOfMessages?: number
147
+ delays?: { x?: number; y?: number; connect?: number }
148
+ }) =>
149
+ Effect.gen(function* () {
150
+ const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
151
+ const { mode, connectNodes } = fromChannelType(channelType)
152
+
153
+ const nodeXCode = Effect.gen(function* () {
154
+ const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
155
+
156
+ yield* channelXToY.send({ message: `${nodeLabel.x}1` })
157
+ // console.log('channelXToY', channelXToY.debugInfo)
158
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
159
+ // expect(channelXToY.debugInfo.connectCounter).toBe(1)
160
+ })
137
161
 
138
- const exchangeMessages = ({
139
- nodeX,
140
- nodeY,
141
- channelType,
142
- // numberOfMessages = 1,
143
- delays,
144
- }: {
145
- nodeX: MeshNode
146
- nodeY: MeshNode
147
- channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
148
- numberOfMessages?: number
149
- delays?: { x?: number; y?: number; connect?: number }
150
- }) =>
151
- Effect.gen(function* () {
152
- const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
153
- const { mode, connectNodes } = fromChannelType(channelType)
154
-
155
- const nodeXCode = Effect.gen(function* () {
156
- const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
157
-
158
- yield* channelXToY.send({ message: `${nodeLabel.x}1` })
159
- // console.log('channelXToY', channelXToY.debugInfo)
160
- expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
161
- // expect(channelXToY.debugInfo.connectCounter).toBe(1)
162
- })
163
-
164
- const nodeYCode = Effect.gen(function* () {
165
- const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
166
-
167
- yield* channelYToX.send({ message: `${nodeLabel.y}1` })
168
- // console.log('channelYToX', channelYToX.debugInfo)
169
- expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
170
- // expect(channelYToX.debugInfo.connectCounter).toBe(1)
171
- })
162
+ const nodeYCode = Effect.gen(function* () {
163
+ const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
172
164
 
173
- yield* Effect.all(
174
- [
175
- connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
176
- nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
177
- nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
178
- ],
179
- { concurrency: 'unbounded' },
180
- ).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
181
- })
165
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
166
+ // console.log('channelYToX', channelYToX.debugInfo)
167
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
168
+ // expect(channelYToX.debugInfo.connectCounter).toBe(1)
169
+ })
182
170
 
171
+ yield* Effect.all(
172
+ [
173
+ connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
174
+ nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
175
+ nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
176
+ ],
177
+ { concurrency: 'unbounded' },
178
+ ).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
179
+ })
180
+ Vitest.describe('A <> B', () => {
181
+ Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
183
182
  // const delayX = 40
184
183
  // const delayY = undefined
185
184
  // const connectDelay = undefined
@@ -455,6 +454,40 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
455
454
  })
456
455
  })
457
456
 
457
+ Vitest.describe('message channel specific tests', () => {
458
+ Vitest.scopedLive('differing initial connection counter', (test) =>
459
+ Effect.gen(function* () {
460
+ const nodeA = yield* makeMeshNode('A')
461
+ const nodeB = yield* makeMeshNode('B')
462
+
463
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
464
+
465
+ const messageCount = 3
466
+
467
+ const bFiber = yield* Effect.gen(function* () {
468
+ const channelBToA = yield* createChannel(nodeB, 'A')
469
+ yield* channelBToA.listen.pipe(
470
+ Stream.flatten(),
471
+ Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })),
472
+ Stream.take(messageCount),
473
+ Stream.runDrain,
474
+ )
475
+ }).pipe(Effect.scoped, Effect.fork)
476
+
477
+ // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
478
+ // // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
479
+ // // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
480
+ yield* Effect.gen(function* () {
481
+ const channelAToB = yield* createChannel(nodeA, 'B')
482
+ yield* channelAToB.send({ message: 'A' })
483
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' })
484
+ }).pipe(Effect.scoped, Effect.repeatN(messageCount))
485
+
486
+ yield* bFiber
487
+ }).pipe(withCtx(test)),
488
+ )
489
+ })
490
+
458
491
  Vitest.scopedLive('manual debug test', (test) =>
459
492
  Effect.gen(function* () {
460
493
  const nodeA = yield* makeMeshNode('A')
@@ -587,7 +620,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
587
620
  }).pipe(withCtx(test)),
588
621
  )
589
622
 
590
- Vitest.scopedLive('should fail', (test) =>
623
+ Vitest.scopedLive('should fail with timeout due to missing connection', (test) =>
591
624
  Effect.gen(function* () {
592
625
  const nodeA = yield* makeMeshNode('A')
593
626
  const nodeB = yield* makeMeshNode('B')
@@ -610,6 +643,27 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
610
643
  }).pipe(withCtx(test)),
611
644
  )
612
645
 
646
+ Vitest.scopedLive('should fail with timeout due no transferable', (test) =>
647
+ Effect.gen(function* () {
648
+ const nodeA = yield* makeMeshNode('A')
649
+ const nodeB = yield* makeMeshNode('B')
650
+
651
+ yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
652
+
653
+ const nodeACode = Effect.gen(function* () {
654
+ const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip)
655
+ expect(err._tag).toBe('TimeoutException')
656
+ })
657
+
658
+ const nodeBCode = Effect.gen(function* () {
659
+ const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip)
660
+ expect(err._tag).toBe('TimeoutException')
661
+ })
662
+
663
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
664
+ }).pipe(withCtx(test)),
665
+ )
666
+
613
667
  Vitest.scopedLive('reconnect with re-created node', (test) =>
614
668
  Effect.gen(function* () {
615
669
  const nodeCgen1Scope = yield* Scope.make()
@@ -705,7 +759,6 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
705
759
  }).pipe(withCtx(test)),
706
760
  )
707
761
 
708
- // TODO this currently fails but should work. probably needs some more guarding internally.
709
762
  Vitest.scopedLive('should work for messagechannels', (test) =>
710
763
  Effect.gen(function* () {
711
764
  const nodeA = yield* makeMeshNode('A')
package/src/node.ts CHANGED
@@ -156,6 +156,8 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
156
156
 
157
157
  const sendPacket = (packet: typeof MeshSchema.Packet.Type) =>
158
158
  Effect.gen(function* () {
159
+ // yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
160
+
159
161
  if (Schema.is(MeshSchema.NetworkConnectionAdded)(packet)) {
160
162
  yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
161
163
  yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)