@livestore/webmesh 0.3.0-dev.2 → 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.
Files changed (48) hide show
  1. package/README.md +26 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel-internal.d.ts +26 -0
  4. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  5. package/dist/channel/message-channel-internal.js +217 -0
  6. package/dist/channel/message-channel-internal.js.map +1 -0
  7. package/dist/channel/message-channel.d.ts +21 -19
  8. package/dist/channel/message-channel.d.ts.map +1 -1
  9. package/dist/channel/message-channel.js +132 -162
  10. package/dist/channel/message-channel.js.map +1 -1
  11. package/dist/channel/proxy-channel.d.ts +2 -2
  12. package/dist/channel/proxy-channel.d.ts.map +1 -1
  13. package/dist/channel/proxy-channel.js +30 -11
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +32 -5
  16. package/dist/common.d.ts.map +1 -1
  17. package/dist/common.js +2 -1
  18. package/dist/common.js.map +1 -1
  19. package/dist/mesh-schema.d.ts +68 -2
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +53 -4
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/node.d.ts +31 -9
  24. package/dist/node.d.ts.map +1 -1
  25. package/dist/node.js +225 -49
  26. package/dist/node.js.map +1 -1
  27. package/dist/node.test.d.ts +1 -1
  28. package/dist/node.test.d.ts.map +1 -1
  29. package/dist/node.test.js +384 -149
  30. package/dist/node.test.js.map +1 -1
  31. package/dist/websocket-connection.d.ts +5 -6
  32. package/dist/websocket-connection.d.ts.map +1 -1
  33. package/dist/websocket-connection.js +21 -26
  34. package/dist/websocket-connection.js.map +1 -1
  35. package/dist/websocket-server.d.ts.map +1 -1
  36. package/dist/websocket-server.js +17 -3
  37. package/dist/websocket-server.js.map +1 -1
  38. package/package.json +7 -6
  39. package/src/channel/message-channel-internal.ts +356 -0
  40. package/src/channel/message-channel.ts +190 -310
  41. package/src/channel/proxy-channel.ts +257 -229
  42. package/src/common.ts +4 -2
  43. package/src/mesh-schema.ts +60 -4
  44. package/src/node.test.ts +544 -179
  45. package/src/node.ts +363 -69
  46. package/src/websocket-connection.ts +96 -95
  47. package/src/websocket-server.ts +20 -3
  48. package/tmp/pack.tgz +0 -0
@@ -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,313 @@ 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
+ const debugInfo = {
74
+ pendingSends: 0,
75
+ totalSends: 0,
76
+ connectCounter: 0,
77
+ isConnected: false,
78
+ }
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
+ /**
81
+ * We need to unique identify a channel as multiple channels might exist between the same two nodes.
82
+ * We do this by letting each channel end generate a unique id and then combining them in a deterministic way.
83
+ */
84
+ const channelIdCandidate = nanoid(5)
85
+ yield* Effect.annotateCurrentSpan({ channelIdCandidate })
113
86
 
114
- const otherSideName = packet.source
115
- const channelKey = `${otherSideName}-${packet.channelName}` satisfies ChannelKey
116
- const channelState = channelStateRef.current
87
+ const channelSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
117
88
 
118
- switch (packet._tag) {
119
- case 'ProxyChannelRequest': {
120
- const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
89
+ const connectedStateRef = yield* SubscriptionRef.make<ProxiedChannelStateEstablished | false>(false)
121
90
 
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))
91
+ const waitForEstablished = Effect.gen(function* () {
92
+ const state = yield* SubscriptionRef.waitUntil(connectedStateRef, (state) => state !== false)
126
93
 
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
- }
132
-
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
- )
94
+ return state as ProxiedChannelStateEstablished
95
+ })
145
96
 
146
- return
97
+ const setStateToEstablished = (channelId: string) =>
98
+ Effect.gen(function* () {
99
+ // TODO avoid "double" `Connected` events (we might call `setStateToEstablished` twice during initial connection)
100
+ yield* Effect.spanEvent(`Connected (${channelId})`).pipe(Effect.withParentSpan(channelSpan))
101
+ channelStateRef.current = {
102
+ _tag: 'Established',
103
+ listenSchema: schema.listen,
104
+ listenQueue,
105
+ ackMap,
106
+ combinedChannelId: channelId,
147
107
  }
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)
108
+ yield* SubscriptionRef.set(connectedStateRef, channelStateRef.current)
109
+ debugInfo.isConnected = true
110
+ })
111
+
112
+ const connectionRequest = Effect.suspend(() =>
113
+ sendPacket(
114
+ MeshSchema.ProxyChannelRequest.make({ channelName, hops: [], source: nodeName, target, channelIdCandidate }),
115
+ ),
116
+ )
117
+
118
+ const getCombinedChannelId = (otherSideChannelIdCandidate: string) =>
119
+ [channelIdCandidate, otherSideChannelIdCandidate].sort().join('_')
120
+
121
+ const processProxyPacket = ({ packet, respondToSender }: ProxyQueueItem) =>
122
+ Effect.gen(function* () {
123
+ // yield* Effect.log(`${nodeName}:processing packet ${packet._tag} from ${packet.source}`)
124
+
125
+ const otherSideName = packet.source
126
+ const channelKey = `target:${otherSideName}, channelName:${packet.channelName}` satisfies ChannelKey
127
+ const channelState = channelStateRef.current
128
+
129
+ switch (packet._tag) {
130
+ case 'ProxyChannelRequest': {
131
+ const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
132
+
133
+ if (channelState._tag === 'Initial' || channelState._tag === 'Established') {
134
+ yield* SubscriptionRef.set(connectedStateRef, false)
135
+ channelStateRef.current = { _tag: 'Pending', initiatedVia: 'incoming-request' }
136
+ yield* Effect.spanEvent(`Reconnecting`).pipe(Effect.withParentSpan(channelSpan))
137
+ debugInfo.isConnected = false
138
+ debugInfo.connectCounter++
139
+
140
+ // If we're already connected, we need to re-establish the connection
141
+ if (channelState._tag === 'Established' && channelState.combinedChannelId !== combinedChannelId) {
142
+ yield* connectionRequest
143
+ }
157
144
  }
158
- }
159
145
 
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)`,
146
+ yield* respondToSender(
147
+ MeshSchema.ProxyChannelResponseSuccess.make({
148
+ reqId: packet.id,
149
+ remainingHops: packet.hops,
150
+ hops: [],
151
+ target,
152
+ source: nodeName,
153
+ channelName,
154
+ combinedChannelId,
155
+ channelIdCandidate,
156
+ }),
164
157
  )
158
+
159
+ return
165
160
  }
161
+ case 'ProxyChannelResponseSuccess': {
162
+ if (channelState._tag !== 'Pending') {
163
+ // return shouldNeverHappen(`Expected proxy channel to be pending but got ${channelState._tag}`)
164
+ if (
165
+ channelState._tag === 'Established' &&
166
+ channelState.combinedChannelId !== packet.combinedChannelId
167
+ ) {
168
+ return shouldNeverHappen(
169
+ `ProxyChannel[${channelKey}]: Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
170
+ )
171
+ } else {
172
+ // for now just ignore it but should be looked into (there seems to be some kind of race condition/inefficiency)
173
+ }
174
+ }
166
175
 
167
- yield* setStateToEstablished(packet.combinedChannelId)
176
+ const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
177
+ if (combinedChannelId !== packet.combinedChannelId) {
178
+ return yield* Effect.die(
179
+ `ProxyChannel[${channelKey}]: Expected proxy channel to have the same combinedChannelId as the packet:\n${combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
180
+ )
181
+ }
182
+
183
+ yield* setStateToEstablished(packet.combinedChannelId)
168
184
 
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
185
  return
176
186
  }
187
+ case 'ProxyChannelPayload': {
188
+ if (channelState._tag !== 'Established') {
189
+ // return yield* Effect.die(`Not yet connected to ${target}. dropping message`)
190
+ yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`, { packet })
191
+ return
192
+ }
177
193
 
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
- }
194
+ if (channelState.combinedChannelId !== packet.combinedChannelId) {
195
+ return yield* Effect.die(
196
+ `ProxyChannel[${channelKey}]: Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
197
+ )
198
+ }
183
199
 
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
- )
200
+ yield* respondToSender(
201
+ MeshSchema.ProxyChannelPayloadAck.make({
202
+ reqId: packet.id,
203
+ remainingHops: packet.hops,
204
+ hops: [],
205
+ target,
206
+ source: nodeName,
207
+ channelName,
208
+ combinedChannelId: channelState.combinedChannelId,
209
+ }),
210
+ )
195
211
 
196
- const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
197
- yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
212
+ const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
213
+ yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
198
214
 
199
- return
200
- }
201
- case 'ProxyChannelPayloadAck': {
202
- if (channelState._tag !== 'Established') {
203
- yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`)
204
215
  return
205
216
  }
217
+ case 'ProxyChannelPayloadAck': {
218
+ if (channelState._tag !== 'Established') {
219
+ yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`)
220
+ return
221
+ }
206
222
 
207
- const ack =
208
- channelState.ackMap.get(packet.reqId) ??
209
- shouldNeverHappen(`Expected ack for ${packet.reqId} in proxy channel ${channelKey}`)
223
+ const ack =
224
+ channelState.ackMap.get(packet.reqId) ??
225
+ shouldNeverHappen(`[ProxyChannel[${channelKey}]] Expected ack for ${packet.reqId}`)
210
226
 
211
- yield* Deferred.succeed(ack, void 0)
227
+ yield* Deferred.succeed(ack, void 0)
212
228
 
213
- channelState.ackMap.delete(packet.reqId)
229
+ channelState.ackMap.delete(packet.reqId)
214
230
 
215
- return
216
- }
217
- default: {
218
- return casesHandled(packet)
231
+ return
232
+ }
233
+ default: {
234
+ return casesHandled(packet)
235
+ }
219
236
  }
220
- }
221
- }).pipe(
222
- Effect.withSpan(`handleProxyPacket:${packet._tag}:${packet.source}->${packet.target}`, {
223
- attributes: packetAsOtelAttributes(packet),
224
- }),
237
+ }).pipe(
238
+ Effect.withSpan(`handleProxyPacket:${packet._tag}:${packet.source}->${packet.target}`, {
239
+ attributes: packetAsOtelAttributes(packet),
240
+ }),
241
+ )
242
+
243
+ yield* Stream.fromQueue(queue).pipe(
244
+ Stream.tap(processProxyPacket),
245
+ Stream.runDrain,
246
+ Effect.tapCauseLogPretty,
247
+ Effect.forkScoped,
225
248
  )
226
249
 
227
- yield* Stream.fromQueue(queue).pipe(
228
- Stream.tap(processProxyPacket),
229
- Stream.runDrain,
230
- Effect.tapCauseLogPretty,
231
- Effect.forkScoped,
232
- )
250
+ const listenQueue = yield* Queue.unbounded<any>()
233
251
 
234
- const listenQueue = yield* Queue.unbounded<any>()
252
+ yield* Effect.spanEvent(`Connecting`)
235
253
 
236
- yield* Effect.spanEvent(`Connecting`)
254
+ const ackMap = new Map<string, Deferred.Deferred<void, never>>()
237
255
 
238
- const ackMap = new Map<string, Deferred.Deferred<void, never>>()
256
+ // check if already established via incoming `ProxyChannelRequest` from other side
257
+ // which indicates we already have a connection to the target node
258
+ // const channelState = channelStateRef.current
259
+ {
260
+ if (channelStateRef.current._tag !== 'Initial') {
261
+ return shouldNeverHappen('Expected proxy channel to be Initial')
262
+ }
239
263
 
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
- }
264
+ channelStateRef.current = { _tag: 'Pending', initiatedVia: 'outgoing-request' }
247
265
 
248
- channelStateRef.current = { _tag: 'Pending', initiatedVia: 'outgoing-request' }
266
+ yield* connectionRequest
249
267
 
250
- yield* connectionRequest
268
+ const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
269
+ Stream.tap(() => connectionRequest),
270
+ Stream.runDrain,
271
+ Effect.forkScoped,
272
+ )
251
273
 
252
- const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
253
- Stream.tap(() => connectionRequest),
254
- Stream.runDrain,
255
- Effect.forkScoped,
256
- )
274
+ const { combinedChannelId: channelId } = yield* waitForEstablished
257
275
 
258
- const { combinedChannelId: channelId } = yield* waitForEstablished
276
+ yield* Fiber.interrupt(retryOnNewConnectionFiber)
259
277
 
260
- yield* Fiber.interrupt(retryOnNewConnectionFiber)
278
+ yield* setStateToEstablished(channelId)
279
+ }
261
280
 
262
- yield* setStateToEstablished(channelId)
263
- }
281
+ const send = (message: any) =>
282
+ Effect.gen(function* () {
283
+ const payload = yield* Schema.encodeUnknown(schema.send)(message)
284
+ const sendFiberHandle = yield* FiberHandle.make<void, never>()
264
285
 
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>()
286
+ const sentDeferred = yield* Deferred.make<void>()
269
287
 
270
- const sentDeferred = yield* Deferred.make<void>()
288
+ debugInfo.pendingSends++
289
+ debugInfo.totalSends++
271
290
 
272
- const trySend = Effect.gen(function* () {
273
- const { combinedChannelId } = (yield* SubscriptionRef.waitUntil(
274
- connectedStateRef,
275
- (channel) => channel !== false,
276
- )) as ProxiedChannelStateEstablished
291
+ const trySend = Effect.gen(function* () {
292
+ const { combinedChannelId } = (yield* SubscriptionRef.waitUntil(
293
+ connectedStateRef,
294
+ (channel) => channel !== false,
295
+ )) as ProxiedChannelStateEstablished
277
296
 
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)
297
+ const innerSend = Effect.gen(function* () {
298
+ // Note we're re-creating new packets every time otherwise they will be skipped because of `handledIds`
299
+ const ack = yield* Deferred.make<void, never>()
300
+ const packet = MeshSchema.ProxyChannelPayload.make({
301
+ channelName,
302
+ payload,
303
+ hops: [],
304
+ source: nodeName,
305
+ target,
306
+ combinedChannelId,
307
+ })
308
+ // TODO consider handling previous ackMap entries which might leak/fill-up memory
309
+ // as only successful acks are removed from the map
310
+ ackMap.set(packet.id, ack)
290
311
 
291
- yield* sendPacket(packet)
312
+ yield* sendPacket(packet)
292
313
 
293
- yield* ack
294
- yield* Deferred.succeed(sentDeferred, void 0)
295
- })
314
+ yield* ack
315
+ yield* Deferred.succeed(sentDeferred, void 0)
296
316
 
297
- yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(100)), Effect.orDie)
298
- }).pipe(Effect.tapErrorCause(Effect.logError))
317
+ debugInfo.pendingSends--
318
+ })
299
319
 
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
- )
320
+ // TODO make this configurable
321
+ // Schedule.exponential(10): 10, 20, 40, 80, 160, 320, ...
322
+ yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(10)), Effect.orDie)
323
+ }).pipe(Effect.tapErrorCause(Effect.logError))
306
324
 
307
- yield* FiberHandle.run(sendFiberHandle, trySend)
325
+ const rerunOnNewChannelFiber = yield* connectedStateRef.changes.pipe(
326
+ Stream.filter((_) => _ === false),
327
+ Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
328
+ Stream.runDrain,
329
+ Effect.fork,
330
+ )
308
331
 
309
- yield* sentDeferred
332
+ yield* FiberHandle.run(sendFiberHandle, trySend)
310
333
 
311
- yield* Fiber.interrupt(rerunOnNewChannelFiber)
312
- }).pipe(
313
- Effect.scoped,
314
- Effect.withSpan(`sendAckWithRetry:ProxyChannelPayload`),
315
- Effect.withParentSpan(channelSpan),
316
- )
334
+ yield* sentDeferred
335
+
336
+ yield* Fiber.interrupt(rerunOnNewChannelFiber)
337
+ }).pipe(
338
+ Effect.scoped,
339
+ Effect.withSpan(`sendAckWithRetry:ProxyChannelPayload`),
340
+ Effect.withParentSpan(channelSpan),
341
+ )
317
342
 
318
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
343
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
319
344
 
320
- const closedDeferred = yield* Deferred.make<void>()
345
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
321
346
 
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>
347
+ const webChannel = {
348
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
349
+ send,
350
+ listen,
351
+ closedDeferred,
352
+ supportsTransferables: true,
353
+ schema,
354
+ shutdown: Scope.close(scope, Exit.void),
355
+ debugInfo,
356
+ } satisfies WebChannel.WebChannel<any, any>
330
357
 
331
- return webChannel as WebChannel.WebChannel<any, any>
332
- }).pipe(Effect.withSpanScoped('makeProxyChannel'))
358
+ return webChannel as WebChannel.WebChannel<any, any>
359
+ }).pipe(Effect.withSpanScoped('makeProxyChannel')),
360
+ )
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
 
@@ -15,7 +15,7 @@ export type MessageQueueItem = {
15
15
  export type MeshNodeName = string
16
16
 
17
17
  export type ChannelName = string
18
- export type ChannelKey = `${MeshNodeName}-${ChannelName}`
18
+ export type ChannelKey = `target:${MeshNodeName}, channelName:${ChannelName}`
19
19
 
20
20
  // TODO actually use this to avoid timeouts in certain cases
21
21
  export class NoConnectionRouteSignal extends Schema.TaggedError<NoConnectionRouteSignal>()(
@@ -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
  })