@livestore/webmesh 0.3.0-dev.11 → 0.3.0-dev.13

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 +132 -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 +315 -149
  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 +190 -310
  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 +448 -179
  50. package/src/node.ts +70 -12
  51. package/src/websocket-connection.ts +83 -79
@@ -4,11 +4,13 @@ import {
4
4
  Deferred,
5
5
  Effect,
6
6
  Either,
7
+ Exit,
7
8
  Fiber,
8
9
  FiberHandle,
9
10
  Queue,
10
11
  Schedule,
11
12
  Schema,
13
+ Scope,
12
14
  Stream,
13
15
  SubscriptionRef,
14
16
  WebChannel,
@@ -46,287 +48,293 @@ export const makeProxyChannel = ({
46
48
  channelName,
47
49
  schema,
48
50
  }: MakeProxyChannelArgs) =>
49
- Effect.gen(function* () {
50
- type ProxiedChannelState =
51
- | {
52
- _tag: 'Initial'
53
- }
54
- | {
55
- _tag: 'Pending'
56
- initiatedVia: 'outgoing-request' | 'incoming-request'
57
- }
58
- | ProxiedChannelStateEstablished
59
-
60
- type ProxiedChannelStateEstablished = {
61
- _tag: 'Established'
62
- listenSchema: Schema.Schema<any, any>
63
- listenQueue: Queue.Queue<any>
64
- ackMap: Map<string, Deferred.Deferred<void, never>>
65
- combinedChannelId: string
66
- }
67
-
68
- const channelStateRef = { current: { _tag: 'Initial' } as ProxiedChannelState }
69
-
70
- /**
71
- * We need to unique identify a channel as multiple channels might exist between the same two nodes.
72
- * We do this by letting each channel end generate a unique id and then combining them in a deterministic way.
73
- */
74
- const channelIdCandidate = nanoid(5)
75
- yield* Effect.annotateCurrentSpan({ channelIdCandidate })
76
-
77
- const channelSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
78
-
79
- const connectedStateRef = yield* SubscriptionRef.make<ProxiedChannelStateEstablished | false>(false)
80
-
81
- const waitForEstablished = Effect.gen(function* () {
82
- const state = yield* SubscriptionRef.waitUntil(connectedStateRef, (state) => state !== false)
83
-
84
- return state as ProxiedChannelStateEstablished
85
- })
86
-
87
- const setStateToEstablished = (channelId: string) =>
88
- Effect.gen(function* () {
89
- // TODO avoid "double" `Connected` events (we might call `setStateToEstablished` twice during initial connection)
90
- yield* Effect.spanEvent(`Connected (${channelId})`).pipe(Effect.withParentSpan(channelSpan))
91
- channelStateRef.current = {
92
- _tag: 'Established',
93
- listenSchema: schema.listen,
94
- listenQueue,
95
- ackMap,
96
- combinedChannelId: channelId,
97
- }
98
- yield* SubscriptionRef.set(connectedStateRef, channelStateRef.current)
99
- })
51
+ Effect.scopeWithCloseable((scope) =>
52
+ Effect.gen(function* () {
53
+ type ProxiedChannelState =
54
+ | {
55
+ _tag: 'Initial'
56
+ }
57
+ | {
58
+ _tag: 'Pending'
59
+ initiatedVia: 'outgoing-request' | 'incoming-request'
60
+ }
61
+ | ProxiedChannelStateEstablished
62
+
63
+ type ProxiedChannelStateEstablished = {
64
+ _tag: 'Established'
65
+ listenSchema: Schema.Schema<any, any>
66
+ listenQueue: Queue.Queue<any>
67
+ ackMap: Map<string, Deferred.Deferred<void, never>>
68
+ combinedChannelId: string
69
+ }
100
70
 
101
- const connectionRequest = Effect.suspend(() =>
102
- sendPacket(
103
- MeshSchema.ProxyChannelRequest.make({ channelName, hops: [], source: nodeName, target, channelIdCandidate }),
104
- ),
105
- )
71
+ const channelStateRef = { current: { _tag: 'Initial' } as ProxiedChannelState }
106
72
 
107
- const getCombinedChannelId = (otherSideChannelIdCandidate: string) =>
108
- [channelIdCandidate, otherSideChannelIdCandidate].sort().join('_')
73
+ /**
74
+ * We need to unique identify a channel as multiple channels might exist between the same two nodes.
75
+ * We do this by letting each channel end generate a unique id and then combining them in a deterministic way.
76
+ */
77
+ const channelIdCandidate = nanoid(5)
78
+ yield* Effect.annotateCurrentSpan({ channelIdCandidate })
109
79
 
110
- const processProxyPacket = ({ packet, respondToSender }: ProxyQueueItem) =>
111
- Effect.gen(function* () {
112
- // yield* Effect.log(`${nodeName}:processing packet ${packet._tag} from ${packet.source}`)
80
+ const channelSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
113
81
 
114
- const otherSideName = packet.source
115
- const channelKey = `${otherSideName}-${packet.channelName}` satisfies ChannelKey
116
- const channelState = channelStateRef.current
82
+ const connectedStateRef = yield* SubscriptionRef.make<ProxiedChannelStateEstablished | false>(false)
117
83
 
118
- switch (packet._tag) {
119
- case 'ProxyChannelRequest': {
120
- const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
84
+ const waitForEstablished = Effect.gen(function* () {
85
+ const state = yield* SubscriptionRef.waitUntil(connectedStateRef, (state) => state !== false)
121
86
 
122
- if (channelState._tag === 'Initial' || channelState._tag === 'Established') {
123
- yield* SubscriptionRef.set(connectedStateRef, false)
124
- channelStateRef.current = { _tag: 'Pending', initiatedVia: 'incoming-request' }
125
- yield* Effect.spanEvent(`Reconnecting`).pipe(Effect.withParentSpan(channelSpan))
87
+ return state as ProxiedChannelStateEstablished
88
+ })
126
89
 
127
- // If we're already connected, we need to re-establish the connection
128
- if (channelState._tag === 'Established' && channelState.combinedChannelId !== combinedChannelId) {
129
- yield* connectionRequest
130
- }
131
- }
90
+ const setStateToEstablished = (channelId: string) =>
91
+ Effect.gen(function* () {
92
+ // TODO avoid "double" `Connected` events (we might call `setStateToEstablished` twice during initial connection)
93
+ yield* Effect.spanEvent(`Connected (${channelId})`).pipe(Effect.withParentSpan(channelSpan))
94
+ channelStateRef.current = {
95
+ _tag: 'Established',
96
+ listenSchema: schema.listen,
97
+ listenQueue,
98
+ ackMap,
99
+ combinedChannelId: channelId,
100
+ }
101
+ yield* SubscriptionRef.set(connectedStateRef, channelStateRef.current)
102
+ })
132
103
 
133
- yield* respondToSender(
134
- MeshSchema.ProxyChannelResponseSuccess.make({
135
- reqId: packet.id,
136
- remainingHops: packet.hops,
137
- hops: [],
138
- target,
139
- source: nodeName,
140
- channelName,
141
- combinedChannelId,
142
- channelIdCandidate,
143
- }),
144
- )
104
+ const connectionRequest = Effect.suspend(() =>
105
+ sendPacket(
106
+ MeshSchema.ProxyChannelRequest.make({ channelName, hops: [], source: nodeName, target, channelIdCandidate }),
107
+ ),
108
+ )
145
109
 
146
- return
147
- }
148
- case 'ProxyChannelResponseSuccess': {
149
- if (channelState._tag !== 'Pending') {
150
- // return shouldNeverHappen(`Expected proxy channel to be pending but got ${channelState._tag}`)
151
- if (channelState._tag === 'Established' && channelState.combinedChannelId !== packet.combinedChannelId) {
152
- return shouldNeverHappen(
153
- `Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
154
- )
155
- } else {
156
- // for now just ignore it but should be looked into (there seems to be some kind of race condition/inefficiency)
110
+ const getCombinedChannelId = (otherSideChannelIdCandidate: string) =>
111
+ [channelIdCandidate, otherSideChannelIdCandidate].sort().join('_')
112
+
113
+ const processProxyPacket = ({ packet, respondToSender }: ProxyQueueItem) =>
114
+ Effect.gen(function* () {
115
+ // yield* Effect.log(`${nodeName}:processing packet ${packet._tag} from ${packet.source}`)
116
+
117
+ const otherSideName = packet.source
118
+ const channelKey = `${otherSideName}-${packet.channelName}` satisfies ChannelKey
119
+ const channelState = channelStateRef.current
120
+
121
+ switch (packet._tag) {
122
+ case 'ProxyChannelRequest': {
123
+ const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
124
+
125
+ if (channelState._tag === 'Initial' || channelState._tag === 'Established') {
126
+ yield* SubscriptionRef.set(connectedStateRef, false)
127
+ channelStateRef.current = { _tag: 'Pending', initiatedVia: 'incoming-request' }
128
+ yield* Effect.spanEvent(`Reconnecting`).pipe(Effect.withParentSpan(channelSpan))
129
+
130
+ // If we're already connected, we need to re-establish the connection
131
+ if (channelState._tag === 'Established' && channelState.combinedChannelId !== combinedChannelId) {
132
+ yield* connectionRequest
133
+ }
157
134
  }
158
- }
159
135
 
160
- const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
161
- if (combinedChannelId !== packet.combinedChannelId) {
162
- return yield* Effect.die(
163
- `Expected proxy channel to have the same combinedChannelId as the packet:\n${combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
136
+ yield* respondToSender(
137
+ MeshSchema.ProxyChannelResponseSuccess.make({
138
+ reqId: packet.id,
139
+ remainingHops: packet.hops,
140
+ hops: [],
141
+ target,
142
+ source: nodeName,
143
+ channelName,
144
+ combinedChannelId,
145
+ channelIdCandidate,
146
+ }),
164
147
  )
148
+
149
+ return
165
150
  }
151
+ case 'ProxyChannelResponseSuccess': {
152
+ if (channelState._tag !== 'Pending') {
153
+ // return shouldNeverHappen(`Expected proxy channel to be pending but got ${channelState._tag}`)
154
+ if (
155
+ channelState._tag === 'Established' &&
156
+ channelState.combinedChannelId !== packet.combinedChannelId
157
+ ) {
158
+ return shouldNeverHappen(
159
+ `Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
160
+ )
161
+ } else {
162
+ // for now just ignore it but should be looked into (there seems to be some kind of race condition/inefficiency)
163
+ }
164
+ }
166
165
 
167
- yield* setStateToEstablished(packet.combinedChannelId)
166
+ const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
167
+ if (combinedChannelId !== packet.combinedChannelId) {
168
+ return yield* Effect.die(
169
+ `Expected proxy channel to have the same combinedChannelId as the packet:\n${combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
170
+ )
171
+ }
172
+
173
+ yield* setStateToEstablished(packet.combinedChannelId)
168
174
 
169
- return
170
- }
171
- case 'ProxyChannelPayload': {
172
- if (channelState._tag !== 'Established') {
173
- // return yield* Effect.die(`Not yet connected to ${target}. dropping message`)
174
- yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`, { packet })
175
175
  return
176
176
  }
177
+ case 'ProxyChannelPayload': {
178
+ if (channelState._tag !== 'Established') {
179
+ // return yield* Effect.die(`Not yet connected to ${target}. dropping message`)
180
+ yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`, { packet })
181
+ return
182
+ }
177
183
 
178
- if (channelState.combinedChannelId !== packet.combinedChannelId) {
179
- return yield* Effect.die(
180
- `Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
181
- )
182
- }
184
+ if (channelState.combinedChannelId !== packet.combinedChannelId) {
185
+ return yield* Effect.die(
186
+ `Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
187
+ )
188
+ }
183
189
 
184
- yield* respondToSender(
185
- MeshSchema.ProxyChannelPayloadAck.make({
186
- reqId: packet.id,
187
- remainingHops: packet.hops,
188
- hops: [],
189
- target,
190
- source: nodeName,
191
- channelName,
192
- combinedChannelId: channelState.combinedChannelId,
193
- }),
194
- )
190
+ yield* respondToSender(
191
+ MeshSchema.ProxyChannelPayloadAck.make({
192
+ reqId: packet.id,
193
+ remainingHops: packet.hops,
194
+ hops: [],
195
+ target,
196
+ source: nodeName,
197
+ channelName,
198
+ combinedChannelId: channelState.combinedChannelId,
199
+ }),
200
+ )
195
201
 
196
- const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
197
- yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
202
+ const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
203
+ yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
198
204
 
199
- return
200
- }
201
- case 'ProxyChannelPayloadAck': {
202
- if (channelState._tag !== 'Established') {
203
- yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`)
204
205
  return
205
206
  }
207
+ case 'ProxyChannelPayloadAck': {
208
+ if (channelState._tag !== 'Established') {
209
+ yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`)
210
+ return
211
+ }
206
212
 
207
- const ack =
208
- channelState.ackMap.get(packet.reqId) ??
209
- shouldNeverHappen(`Expected ack for ${packet.reqId} in proxy channel ${channelKey}`)
213
+ const ack =
214
+ channelState.ackMap.get(packet.reqId) ??
215
+ shouldNeverHappen(`Expected ack for ${packet.reqId} in proxy channel ${channelKey}`)
210
216
 
211
- yield* Deferred.succeed(ack, void 0)
217
+ yield* Deferred.succeed(ack, void 0)
212
218
 
213
- channelState.ackMap.delete(packet.reqId)
219
+ channelState.ackMap.delete(packet.reqId)
214
220
 
215
- return
216
- }
217
- default: {
218
- return casesHandled(packet)
221
+ return
222
+ }
223
+ default: {
224
+ return casesHandled(packet)
225
+ }
219
226
  }
220
- }
221
- }).pipe(
222
- Effect.withSpan(`handleProxyPacket:${packet._tag}:${packet.source}->${packet.target}`, {
223
- attributes: packetAsOtelAttributes(packet),
224
- }),
225
- )
227
+ }).pipe(
228
+ Effect.withSpan(`handleProxyPacket:${packet._tag}:${packet.source}->${packet.target}`, {
229
+ attributes: packetAsOtelAttributes(packet),
230
+ }),
231
+ )
226
232
 
227
- yield* Stream.fromQueue(queue).pipe(
228
- Stream.tap(processProxyPacket),
229
- Stream.runDrain,
230
- Effect.tapCauseLogPretty,
231
- Effect.forkScoped,
232
- )
233
+ yield* Stream.fromQueue(queue).pipe(
234
+ Stream.tap(processProxyPacket),
235
+ Stream.runDrain,
236
+ Effect.tapCauseLogPretty,
237
+ Effect.forkScoped,
238
+ )
233
239
 
234
- const listenQueue = yield* Queue.unbounded<any>()
240
+ const listenQueue = yield* Queue.unbounded<any>()
235
241
 
236
- yield* Effect.spanEvent(`Connecting`)
242
+ yield* Effect.spanEvent(`Connecting`)
237
243
 
238
- const ackMap = new Map<string, Deferred.Deferred<void, never>>()
244
+ const ackMap = new Map<string, Deferred.Deferred<void, never>>()
239
245
 
240
- // check if already established via incoming `ProxyChannelRequest` from other side
241
- // which indicates we already have a connection to the target node
242
- // const channelState = channelStateRef.current
243
- {
244
- if (channelStateRef.current._tag !== 'Initial') {
245
- return shouldNeverHappen('Expected proxy channel to be Initial')
246
- }
246
+ // check if already established via incoming `ProxyChannelRequest` from other side
247
+ // which indicates we already have a connection to the target node
248
+ // const channelState = channelStateRef.current
249
+ {
250
+ if (channelStateRef.current._tag !== 'Initial') {
251
+ return shouldNeverHappen('Expected proxy channel to be Initial')
252
+ }
247
253
 
248
- channelStateRef.current = { _tag: 'Pending', initiatedVia: 'outgoing-request' }
254
+ channelStateRef.current = { _tag: 'Pending', initiatedVia: 'outgoing-request' }
249
255
 
250
- yield* connectionRequest
256
+ yield* connectionRequest
251
257
 
252
- const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
253
- Stream.tap(() => connectionRequest),
254
- Stream.runDrain,
255
- Effect.forkScoped,
256
- )
258
+ const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
259
+ Stream.tap(() => connectionRequest),
260
+ Stream.runDrain,
261
+ Effect.forkScoped,
262
+ )
257
263
 
258
- const { combinedChannelId: channelId } = yield* waitForEstablished
264
+ const { combinedChannelId: channelId } = yield* waitForEstablished
259
265
 
260
- yield* Fiber.interrupt(retryOnNewConnectionFiber)
266
+ yield* Fiber.interrupt(retryOnNewConnectionFiber)
261
267
 
262
- yield* setStateToEstablished(channelId)
263
- }
268
+ yield* setStateToEstablished(channelId)
269
+ }
264
270
 
265
- const send = (message: any) =>
266
- Effect.gen(function* () {
267
- const payload = yield* Schema.encodeUnknown(schema.send)(message)
268
- const sendFiberHandle = yield* FiberHandle.make<void, never>()
271
+ const send = (message: any) =>
272
+ Effect.gen(function* () {
273
+ const payload = yield* Schema.encodeUnknown(schema.send)(message)
274
+ const sendFiberHandle = yield* FiberHandle.make<void, never>()
269
275
 
270
- const sentDeferred = yield* Deferred.make<void>()
276
+ const sentDeferred = yield* Deferred.make<void>()
271
277
 
272
- const trySend = Effect.gen(function* () {
273
- const { combinedChannelId } = (yield* SubscriptionRef.waitUntil(
274
- connectedStateRef,
275
- (channel) => channel !== false,
276
- )) as ProxiedChannelStateEstablished
278
+ const trySend = Effect.gen(function* () {
279
+ const { combinedChannelId } = (yield* SubscriptionRef.waitUntil(
280
+ connectedStateRef,
281
+ (channel) => channel !== false,
282
+ )) as ProxiedChannelStateEstablished
277
283
 
278
- const innerSend = Effect.gen(function* () {
279
- // Note we're re-creating new packets every time otherwise they will be skipped because of `handledIds`
280
- const ack = yield* Deferred.make<void, never>()
281
- const packet = MeshSchema.ProxyChannelPayload.make({
282
- channelName,
283
- payload,
284
- hops: [],
285
- source: nodeName,
286
- target,
287
- combinedChannelId,
288
- })
289
- ackMap.set(packet.id, ack)
284
+ const innerSend = Effect.gen(function* () {
285
+ // Note we're re-creating new packets every time otherwise they will be skipped because of `handledIds`
286
+ const ack = yield* Deferred.make<void, never>()
287
+ const packet = MeshSchema.ProxyChannelPayload.make({
288
+ channelName,
289
+ payload,
290
+ hops: [],
291
+ source: nodeName,
292
+ target,
293
+ combinedChannelId,
294
+ })
295
+ ackMap.set(packet.id, ack)
290
296
 
291
- yield* sendPacket(packet)
297
+ yield* sendPacket(packet)
292
298
 
293
- yield* ack
294
- yield* Deferred.succeed(sentDeferred, void 0)
295
- })
299
+ yield* ack
300
+ yield* Deferred.succeed(sentDeferred, void 0)
301
+ })
296
302
 
297
- yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(100)), Effect.orDie)
298
- }).pipe(Effect.tapErrorCause(Effect.logError))
303
+ yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(100)), Effect.orDie)
304
+ }).pipe(Effect.tapErrorCause(Effect.logError))
299
305
 
300
- const rerunOnNewChannelFiber = yield* connectedStateRef.changes.pipe(
301
- Stream.filter((_) => _ === false),
302
- Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
303
- Stream.runDrain,
304
- Effect.fork,
305
- )
306
+ const rerunOnNewChannelFiber = yield* connectedStateRef.changes.pipe(
307
+ Stream.filter((_) => _ === false),
308
+ Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
309
+ Stream.runDrain,
310
+ Effect.fork,
311
+ )
306
312
 
307
- yield* FiberHandle.run(sendFiberHandle, trySend)
313
+ yield* FiberHandle.run(sendFiberHandle, trySend)
308
314
 
309
- yield* sentDeferred
315
+ yield* sentDeferred
310
316
 
311
- yield* Fiber.interrupt(rerunOnNewChannelFiber)
312
- }).pipe(
313
- Effect.scoped,
314
- Effect.withSpan(`sendAckWithRetry:ProxyChannelPayload`),
315
- Effect.withParentSpan(channelSpan),
316
- )
317
+ yield* Fiber.interrupt(rerunOnNewChannelFiber)
318
+ }).pipe(
319
+ Effect.scoped,
320
+ Effect.withSpan(`sendAckWithRetry:ProxyChannelPayload`),
321
+ Effect.withParentSpan(channelSpan),
322
+ )
317
323
 
318
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
324
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
319
325
 
320
- const closedDeferred = yield* Deferred.make<void>()
326
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
321
327
 
322
- const webChannel = {
323
- [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
324
- send,
325
- listen,
326
- closedDeferred,
327
- supportsTransferables: true,
328
- schema,
329
- } satisfies WebChannel.WebChannel<any, any>
328
+ const webChannel = {
329
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
330
+ send,
331
+ listen,
332
+ closedDeferred,
333
+ supportsTransferables: true,
334
+ schema,
335
+ shutdown: Scope.close(scope, Exit.void),
336
+ } satisfies WebChannel.WebChannel<any, any>
330
337
 
331
- return webChannel as WebChannel.WebChannel<any, any>
332
- }).pipe(Effect.withSpanScoped('makeProxyChannel'))
338
+ return webChannel as WebChannel.WebChannel<any, any>
339
+ }).pipe(Effect.withSpanScoped('makeProxyChannel')),
340
+ )
package/src/common.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Effect, Schema } from '@livestore/utils/effect'
1
+ import { type Effect, Predicate, Schema } from '@livestore/utils/effect'
2
2
 
3
3
  import type { MessageChannelPacket, Packet, ProxyChannelPacket } from './mesh-schema.js'
4
4
 
@@ -32,5 +32,7 @@ export class ConnectionAlreadyExistsError extends Schema.TaggedError<ConnectionA
32
32
 
33
33
  export const packetAsOtelAttributes = (packet: typeof Packet.Type) => ({
34
34
  packetId: packet.id,
35
+ 'span.label':
36
+ packet.id + (Predicate.hasProperty(packet, 'reqId') && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''),
35
37
  ...(packet._tag !== 'MessageChannelResponseSuccess' && packet._tag !== 'ProxyChannelPayload' ? { packet } : {}),
36
38
  })
@@ -16,10 +16,24 @@ const defaultPacketFields = {
16
16
 
17
17
  const remainingHopsUndefined = Schema.Undefined.pipe(Schema.optional)
18
18
 
19
- // Needs to go through already existing MessageChannel connections, times out otherwise
19
+ /**
20
+ * Needs to go through already existing MessageChannel connections, times out otherwise
21
+ *
22
+ * Can't yet contain the `port` because the request might be duplicated while forwarding to multiple nodes.
23
+ * We need a clear path back to the sender to avoid this, thus we respond with a separate
24
+ * `MessageChannelResponseSuccess` which contains the `port`.
25
+ */
20
26
  export class MessageChannelRequest extends Schema.TaggedStruct('MessageChannelRequest', {
21
27
  ...defaultPacketFields,
22
- remainingHops: remainingHopsUndefined,
28
+ remainingHops: Schema.Array(Schema.String).pipe(Schema.optional),
29
+ channelVersion: Schema.Number,
30
+ /** Only set if the request is in response to an incoming request */
31
+ reqId: Schema.UndefinedOr(Schema.String),
32
+ /**
33
+ * Additionally to the `source` field, we use this field to track whether the instance of a
34
+ * source has changed.
35
+ */
36
+ sourceId: Schema.String,
23
37
  }) {}
24
38
 
25
39
  export class MessageChannelResponseSuccess extends Schema.TaggedStruct('MessageChannelResponseSuccess', {
@@ -28,6 +42,7 @@ export class MessageChannelResponseSuccess extends Schema.TaggedStruct('MessageC
28
42
  port: Transferable.MessagePort,
29
43
  // Since we can't copy this message, we need to follow the exact route back to the sender
30
44
  remainingHops: Schema.Array(Schema.String),
45
+ channelVersion: Schema.Number,
31
46
  }) {}
32
47
 
33
48
  export class MessageChannelResponseNoTransferables extends Schema.TaggedStruct(
@@ -92,3 +107,6 @@ export class ProxyChannelPacket extends Schema.Union(
92
107
  ) {}
93
108
 
94
109
  export class Packet extends Schema.Union(MessageChannelPacket, ProxyChannelPacket, NetworkConnectionAdded) {}
110
+
111
+ export class MessageChannelPing extends Schema.TaggedStruct('MessageChannelPing', {}) {}
112
+ export class MessageChannelPong extends Schema.TaggedStruct('MessageChannelPong', {}) {}