@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
@@ -1,354 +1,234 @@
1
- import { casesHandled, shouldNeverHappen } from '@livestore/utils'
2
- import type { PubSub, Schema, Scope } from '@livestore/utils/effect'
3
1
  import {
2
+ Cause,
4
3
  Deferred,
5
4
  Effect,
6
5
  Either,
7
- Fiber,
8
- FiberHandle,
6
+ Exit,
7
+ Option,
9
8
  Queue,
10
- Schedule,
9
+ Schema,
10
+ Scope,
11
11
  Stream,
12
- SubscriptionRef,
12
+ TQueue,
13
13
  WebChannel,
14
14
  } from '@livestore/utils/effect'
15
-
16
- import { type ChannelName, type MeshNodeName, type MessageQueueItem, packetAsOtelAttributes } from '../common.js'
17
- import * as MeshSchema from '../mesh-schema.js'
18
-
19
- interface MakeMessageChannelArgs {
20
- nodeName: MeshNodeName
21
- queue: Queue.Queue<MessageQueueItem>
22
- newConnectionAvailablePubSub: PubSub.PubSub<MeshNodeName>
23
- channelName: ChannelName
24
- target: MeshNodeName
25
- sendPacket: (packet: typeof MeshSchema.MessageChannelPacket.Type) => Effect.Effect<void>
26
- checkTransferableConnections: (
27
- packet: typeof MeshSchema.MessageChannelPacket.Type,
28
- ) => typeof MeshSchema.MessageChannelResponseNoTransferables.Type | undefined
29
- schema: {
30
- send: Schema.Schema<any, any>
31
- listen: Schema.Schema<any, any>
32
- }
33
- }
34
-
15
+ import { nanoid } from '@livestore/utils/nanoid'
16
+
17
+ import { WebmeshSchema } from '../mod.js'
18
+ import type { MakeMessageChannelArgs } from './message-channel-internal.js'
19
+ import { makeMessageChannelInternal } from './message-channel-internal.js'
20
+
21
+ /**
22
+ * Behaviour:
23
+ * - Waits until there is an initial connection
24
+ * - Automatically reconnects on disconnect
25
+ *
26
+ * Implementation notes:
27
+ * - We've split up the functionality into a wrapper channel and an internal channel.
28
+ * - The wrapper channel is responsible for:
29
+ * - Forwarding send/listen messages to the internal channel (via a queue)
30
+ * - Establishing the initial channel and reconnecting on disconnect
31
+ * - Listening for new connections as a hint to reconnect if not already connected
32
+ * - The wrapper channel maintains a connection counter which is used as the channel version
33
+ *
34
+ * If needed we can also implement further functionality (like heartbeat) in this wrapper channel.
35
+ */
35
36
  export const makeMessageChannel = ({
36
- nodeName,
37
- queue,
37
+ schema,
38
38
  newConnectionAvailablePubSub,
39
- target,
40
- checkTransferableConnections,
41
39
  channelName,
42
- schema,
40
+ checkTransferableConnections,
41
+ nodeName,
42
+ incomingPacketsQueue,
43
+ target,
43
44
  sendPacket,
44
45
  }: MakeMessageChannelArgs) =>
45
- Effect.gen(function* () {
46
- const reconnectTriggerQueue = yield* Queue.unbounded<void>()
47
- const reconnect = Queue.offer(reconnectTriggerQueue, void 0)
48
-
49
- type ChannelState =
50
- | { _tag: 'Established' }
51
- | {
52
- _tag: 'Initial'
53
- deferred: Deferred.Deferred<MessagePort, typeof MeshSchema.MessageChannelResponseNoTransferables.Type>
54
- }
55
- | {
56
- _tag: 'RequestSent'
57
- deferred: Deferred.Deferred<MessagePort, typeof MeshSchema.MessageChannelResponseNoTransferables.Type>
58
- }
59
- | {
60
- _tag: 'ResponseSent'
61
- // Set in the case where this side already received a request, and created a port.
62
- // Might be used or discarded based on tie-breaking logic below.
63
- locallyCreatedPort: MessagePort
64
- deferred: Deferred.Deferred<MessagePort, typeof MeshSchema.MessageChannelResponseNoTransferables.Type>
65
- }
66
-
67
- const makeInitialState = Effect.gen(function* () {
68
- const deferred = yield* Deferred.make<MessagePort, typeof MeshSchema.MessageChannelResponseNoTransferables.Type>()
69
- return { _tag: 'Initial', deferred } as ChannelState
70
- })
71
-
72
- const channelStateRef = { current: yield* makeInitialState }
73
-
74
- const makeMessageChannelInternal: Effect.Effect<
75
- WebChannel.WebChannel<any, any, never>,
76
- never,
77
- Scope.Scope
78
- > = Effect.gen(function* () {
79
- const processMessagePacket = ({ packet, respondToSender }: MessageQueueItem) =>
80
- Effect.gen(function* () {
81
- const channelState = channelStateRef.current
82
-
83
- // yield* Effect.log(`${nodeName}:processing packet ${packet._tag}, channel state: ${channelState._tag}`)
84
-
85
- switch (packet._tag) {
86
- // Since there can be concurrent MessageChannel responses from both sides,
87
- // we need to decide which side's port we want to use and which side's port we want to ignore.
88
- // This is only relevant in the case where both sides already sent their responses.
89
- // In this case we're using the target name as a "tie breaker" to decide which side's port to use.
90
- // We do this by sorting the target names lexicographically and use the first one as the winner.
91
- case 'MessageChannelResponseSuccess': {
92
- if (channelState._tag === 'Initial') {
93
- return shouldNeverHappen(
94
- `Expected to find message channel request from ${target}, but was in ${channelState._tag} state`,
95
- )
96
- }
97
-
98
- if (channelState._tag === 'Established') {
99
- const deferred = yield* Deferred.make<
100
- MessagePort,
101
- typeof MeshSchema.MessageChannelResponseNoTransferables.Type
102
- >()
46
+ Effect.scopeWithCloseable((scope) =>
47
+ Effect.gen(function* () {
48
+ /** Only used to identify whether a source is the same instance to know when to reconnect */
49
+ const sourceId = nanoid()
50
+
51
+ const listenQueue = yield* Queue.unbounded<any>()
52
+ const sendQueue = yield* TQueue.unbounded<[msg: any, deferred: Deferred.Deferred<void>]>()
53
+
54
+ const initialConnectionDeferred = yield* Deferred.make<void>()
55
+
56
+ const debugInfo = {
57
+ pendingSends: 0,
58
+ totalSends: 0,
59
+ connectCounter: 0,
60
+ isConnected: false,
61
+ innerChannelRef: { current: undefined as WebChannel.WebChannel<any, any> | undefined },
62
+ }
103
63
 
104
- channelStateRef.current = { _tag: 'RequestSent', deferred }
64
+ // #region reconnect-loop
65
+ yield* Effect.gen(function* () {
66
+ const resultDeferred = yield* Deferred.make<{
67
+ channel: WebChannel.WebChannel<any, any>
68
+ channelVersion: number
69
+ makeMessageChannelScope: Scope.CloseableScope
70
+ }>()
71
+
72
+ while (true) {
73
+ debugInfo.connectCounter++
74
+ const channelVersion = debugInfo.connectCounter
75
+
76
+ yield* Effect.spanEvent(`Connecting#${channelVersion}`)
77
+
78
+ const makeMessageChannelScope = yield* Scope.make()
79
+ // Attach the new scope to the parent scope
80
+ yield* Effect.addFinalizer((ex) => Scope.close(makeMessageChannelScope, ex))
81
+
82
+ /**
83
+ * Expected concurrency behaviour:
84
+ * - We're concurrently running the connection setup and the waitForNewConnectionFiber
85
+ * - Happy path:
86
+ * - The connection setup succeeds and we can interrupt the waitForNewConnectionFiber
87
+ * - Tricky paths:
88
+ * - While a connection is still being setup, we want to re-try when there is a new connection
89
+ * - If the connection setup returns a `MessageChannelResponseNoTransferables` error,
90
+ * we want to wait for a new connection and then re-try
91
+ * - Further notes:
92
+ * - If the parent scope closes, we want to also interrupt both the connection setup and the waitForNewConnectionFiber
93
+ * - We're creating a separate scope for each connection attempt, which
94
+ * - we'll use to fork the message channel in which allows us to interrupt it later
95
+ * - We need to make sure that "interruption" isn't "bubbling out"
96
+ */
97
+ const waitForNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
98
+ Stream.tap((connectionName) => Effect.spanEvent(`new-conn:${connectionName}`)),
99
+ Stream.take(1),
100
+ Stream.runDrain,
101
+ Effect.as('new-connection' as const),
102
+ Effect.fork,
103
+ )
105
104
 
106
- yield* reconnect
105
+ const makeChannel = makeMessageChannelInternal({
106
+ nodeName,
107
+ sourceId,
108
+ incomingPacketsQueue,
109
+ target,
110
+ checkTransferableConnections,
111
+ channelName,
112
+ schema,
113
+ channelVersion,
114
+ newConnectionAvailablePubSub,
115
+ sendPacket,
116
+ scope: 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
+ )
107
124
 
108
- return
109
- }
110
-
111
- const thisSideAlsoResponded = channelState._tag === 'ResponseSent'
112
-
113
- const usePortFromThisSide = thisSideAlsoResponded && nodeName > target
114
- yield* Effect.annotateCurrentSpan({ usePortFromThisSide })
115
-
116
- const winnerPort = usePortFromThisSide ? channelState.locallyCreatedPort : packet.port
117
- yield* Deferred.succeed(channelState.deferred, winnerPort)
118
-
119
- return
120
- }
121
- case 'MessageChannelResponseNoTransferables': {
122
- if (channelState._tag === 'Established') return
123
-
124
- yield* Deferred.fail(channelState!.deferred, packet)
125
- channelStateRef.current = yield* makeInitialState
126
- return
127
- }
128
- case 'MessageChannelRequest': {
129
- const mc = new MessageChannel()
130
-
131
- const shouldReconnect = channelState._tag === 'Established'
132
-
133
- const deferred =
134
- channelState._tag === 'Established'
135
- ? yield* Deferred.make<MessagePort, typeof MeshSchema.MessageChannelResponseNoTransferables.Type>()
136
- : channelState.deferred
137
-
138
- channelStateRef.current = { _tag: 'ResponseSent', locallyCreatedPort: mc.port1, deferred }
139
-
140
- yield* respondToSender(
141
- MeshSchema.MessageChannelResponseSuccess.make({
142
- reqId: packet.id,
143
- target,
144
- source: nodeName,
145
- channelName: packet.channelName,
146
- hops: [],
147
- remainingHops: packet.hops,
148
- port: mc.port2,
149
- }),
150
- )
151
-
152
- // If there's an established channel, we use the new request as a signal
153
- // to drop the old channel and use the new one
154
- if (shouldReconnect) {
155
- yield* reconnect
125
+ const raceResult = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect))
126
+
127
+ if (raceResult === 'new-connection') {
128
+ yield* Scope.close(makeMessageChannelScope, Exit.fail('new-connection'))
129
+ // We'll try again
130
+ } else {
131
+ const channelExit = yield* raceResult.pipe(Effect.exit)
132
+ if (channelExit._tag === 'Failure') {
133
+ yield* Scope.close(makeMessageChannelScope, channelExit)
134
+
135
+ if (
136
+ Cause.isFailType(channelExit.cause) &&
137
+ Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(channelExit.cause.error)
138
+ ) {
139
+ // Only retry when there is a new connection available
140
+ yield* waitForNewConnectionFiber.pipe(Effect.exit)
156
141
  }
142
+ } else {
143
+ const channel = channelExit.value
157
144
 
145
+ yield* Deferred.succeed(resultDeferred, { channel, makeMessageChannelScope, channelVersion })
158
146
  break
159
147
  }
160
- default: {
161
- return casesHandled(packet)
162
- }
163
148
  }
164
- }).pipe(
165
- Effect.withSpan(`handleMessagePacket:${packet._tag}:${packet.source}→${packet.target}`, {
166
- attributes: packetAsOtelAttributes(packet),
167
- }),
168
- )
169
-
170
- yield* Stream.fromQueue(queue).pipe(
171
- Stream.tap(processMessagePacket),
172
- Stream.runDrain,
173
- Effect.tapCauseLogPretty,
174
- Effect.forkScoped,
175
- )
176
-
177
- const channelFromPort = (port: MessagePort) =>
178
- Effect.gen(function* () {
179
- channelStateRef.current = { _tag: 'Established' }
180
-
181
- // NOTE to support re-connects we need to ack each message
182
- const channel = yield* WebChannel.messagePortChannelWithAck({ port, schema })
183
-
184
- return channel
185
- })
186
-
187
- const channelState = channelStateRef.current
188
-
189
- if (channelState._tag === 'Initial' || channelState._tag === 'RequestSent') {
190
- // Important to make a new deferred here as the old one might have been used already
191
- // TODO model this better
192
- const deferred =
193
- channelState._tag === 'RequestSent'
194
- ? yield* Deferred.make<MessagePort, typeof MeshSchema.MessageChannelResponseNoTransferables.Type>()
195
- : channelState.deferred
196
-
197
- channelStateRef.current = { _tag: 'RequestSent', deferred }
198
-
199
- const connectionRequest = Effect.gen(function* () {
200
- const packet = MeshSchema.MessageChannelRequest.make({ source: nodeName, target, channelName, hops: [] })
149
+ }
201
150
 
202
- const noTransferableResponse = checkTransferableConnections(packet)
203
- if (noTransferableResponse !== undefined) {
204
- yield* Effect.spanEvent(`No transferable connections found for ${packet.source}→${packet.target}`)
205
- yield* Deferred.fail(deferred, noTransferableResponse)
206
- return
207
- }
151
+ // Now we wait until the first channel is established
152
+ const { channel, makeMessageChannelScope, channelVersion } = yield* resultDeferred
208
153
 
209
- yield* sendPacket(packet)
210
- })
154
+ yield* Effect.spanEvent(`Connected#${channelVersion}`)
155
+ debugInfo.isConnected = true
156
+ debugInfo.innerChannelRef.current = channel
211
157
 
212
- yield* connectionRequest
158
+ yield* Deferred.succeed(initialConnectionDeferred, void 0)
213
159
 
214
- const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
215
- Stream.tap(() => Effect.spanEvent(`RetryOnNewConnection`)),
216
- Stream.tap(() => connectionRequest),
160
+ // We'll now forward all incoming messages to the listen queue
161
+ yield* channel.listen.pipe(
162
+ Stream.flatten(),
163
+ // Stream.tap((msg) => Effect.log(`${target}→${channelName}→${nodeName}:message:${msg.message}`)),
164
+ Stream.tapChunk((chunk) => Queue.offerAll(listenQueue, chunk)),
217
165
  Stream.runDrain,
218
- Effect.forkScoped,
166
+ Effect.tapCauseLogPretty,
167
+ Effect.forkIn(makeMessageChannelScope),
219
168
  )
220
169
 
221
- const portResult = yield* deferred.pipe(Effect.either)
222
- yield* Fiber.interrupt(retryOnNewConnectionFiber)
223
-
224
- if (portResult._tag === 'Right') {
225
- return yield* channelFromPort(portResult.right)
226
- } else {
227
- // We'll keep retrying with a new connection
228
- yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(Stream.take(1), Stream.runDrain)
229
-
230
- yield* reconnect
231
-
232
- return yield* Effect.interrupt
233
- }
234
- } else {
235
- // In this case we've already received a request from the other side (before we had a chance to send our request),
236
- // so we already created a MessageChannel,responded with one port
237
- // and are now using the other port to create the channel.
238
- if (channelState._tag === 'ResponseSent') {
239
- return yield* channelFromPort(channelState.locallyCreatedPort)
240
- } else {
241
- return shouldNeverHappen(
242
- `Expected pending message channel to be in ResponseSent state, but was in ${channelState._tag} state`,
243
- )
244
- }
245
- }
246
- })
247
-
248
- const internalChannelSref = yield* SubscriptionRef.make<WebChannel.WebChannel<any, any> | false>(false)
249
-
250
- const listenQueue = yield* Queue.unbounded<any>()
251
-
252
- let connectCounter = 0
253
-
254
- const connect = Effect.gen(function* () {
255
- const connectCount = ++connectCounter
256
- yield* Effect.spanEvent(`Connecting#${connectCount}`)
257
-
258
- yield* SubscriptionRef.set(internalChannelSref, false)
259
-
260
- yield* Effect.addFinalizer(() => Effect.spanEvent(`Disconnected#${connectCount}`))
261
-
262
- const internalChannel = yield* makeMessageChannelInternal
170
+ yield* Effect.gen(function* () {
171
+ while (true) {
172
+ const [msg, deferred] = yield* TQueue.peek(sendQueue)
173
+ // NOTE we don't need an explicit retry flow here since in case of the channel being closed,
174
+ // the send will never succeed. Meanwhile the send-loop fiber will be interrupted and
175
+ // given we only peeked at the queue, the message to send is still there.
176
+ yield* channel.send(msg)
177
+ yield* Deferred.succeed(deferred, void 0)
178
+ yield* TQueue.take(sendQueue) // Remove the message from the queue
179
+ }
180
+ }).pipe(Effect.forkIn(makeMessageChannelScope))
263
181
 
264
- yield* SubscriptionRef.set(internalChannelSref, internalChannel)
182
+ // Wait until the channel is closed and then try to reconnect
183
+ yield* channel.closedDeferred
265
184
 
266
- yield* Effect.spanEvent(`Connected#${connectCount}`)
185
+ yield* Scope.close(makeMessageChannelScope, Exit.succeed('channel-closed'))
267
186
 
268
- yield* internalChannel.listen.pipe(
269
- Stream.flatten(),
270
- Stream.tap((msg) => Queue.offer(listenQueue, msg)),
271
- Stream.runDrain,
187
+ yield* Effect.spanEvent(`Disconnected#${channelVersion}`)
188
+ debugInfo.isConnected = false
189
+ debugInfo.innerChannelRef.current = undefined
190
+ }).pipe(
191
+ Effect.scoped, // Additionally scoping here to clean up finalizers after each loop run
192
+ Effect.forever,
272
193
  Effect.tapCauseLogPretty,
273
194
  Effect.forkScoped,
274
195
  )
196
+ // #endregion reconnect-loop
275
197
 
276
- yield* Effect.never
277
- }).pipe(Effect.scoped)
278
-
279
- const fiberHandle = yield* FiberHandle.make<void, never>()
280
-
281
- const runConnect = Effect.gen(function* () {
282
- // Cleanly shutdown the previous connection first
283
- // Otherwise the old and new connection will "overlap"
284
- yield* FiberHandle.clear(fiberHandle)
285
- yield* FiberHandle.run(fiberHandle, connect)
286
- })
287
-
288
- yield* runConnect
289
-
290
- // Then listen for reconnects
291
- yield* Stream.fromQueue(reconnectTriggerQueue).pipe(
292
- Stream.tap(() => runConnect),
293
- Stream.runDrain,
294
- Effect.tapCauseLogPretty,
295
- Effect.forkScoped,
296
- )
297
-
298
- // Wait for the initial connection to be established or for an error to occur
299
- yield* Effect.raceFirst(
300
- SubscriptionRef.waitUntil(internalChannelSref, (channel) => channel !== false),
301
- FiberHandle.join(fiberHandle),
302
- )
198
+ const parentSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
303
199
 
304
- const parentSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
305
-
306
- const send = (message: any) =>
307
- Effect.gen(function* () {
308
- const sendFiberHandle = yield* FiberHandle.make<void, never>()
309
-
310
- const sentDeferred = yield* Deferred.make<void>()
311
-
312
- const trySend = Effect.gen(function* () {
313
- const channel = (yield* SubscriptionRef.waitUntil(
314
- internalChannelSref,
315
- (channel) => channel !== false,
316
- )) as WebChannel.WebChannel<any, any>
317
-
318
- const innerSend = Effect.gen(function* () {
319
- yield* channel.send(message)
320
- yield* Deferred.succeed(sentDeferred, void 0)
321
- })
322
-
323
- yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(100)), Effect.orDie)
324
- }).pipe(Effect.tapErrorCause(Effect.logError))
200
+ const send = (message: any) =>
201
+ Effect.gen(function* () {
202
+ const sentDeferred = yield* Deferred.make<void>()
325
203
 
326
- const rerunOnNewChannelFiber = yield* internalChannelSref.changes.pipe(
327
- Stream.filter((_) => _ === false),
328
- Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
329
- Stream.runDrain,
330
- Effect.fork,
331
- )
204
+ debugInfo.pendingSends++
205
+ debugInfo.totalSends++
332
206
 
333
- yield* FiberHandle.run(sendFiberHandle, trySend)
207
+ yield* TQueue.offer(sendQueue, [message, sentDeferred])
334
208
 
335
- yield* sentDeferred
209
+ yield* sentDeferred
336
210
 
337
- yield* Fiber.interrupt(rerunOnNewChannelFiber)
338
- }).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
211
+ debugInfo.pendingSends--
212
+ }).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
339
213
 
340
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
214
+ const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right))
341
215
 
342
- const closedDeferred = yield* Deferred.make<void>()
216
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
343
217
 
344
- const webChannel = {
345
- [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
346
- send,
347
- listen,
348
- closedDeferred,
349
- supportsTransferables: true,
350
- schema,
351
- } satisfies WebChannel.WebChannel<any, any>
218
+ const webChannel = {
219
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
220
+ send,
221
+ listen,
222
+ closedDeferred,
223
+ supportsTransferables: true,
224
+ schema,
225
+ debugInfo,
226
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
227
+ } satisfies WebChannel.WebChannel<any, any>
352
228
 
353
- return webChannel as WebChannel.WebChannel<any, any>
354
- }).pipe(Effect.withSpanScoped('makeMessageChannel'))
229
+ return {
230
+ webChannel: webChannel as WebChannel.WebChannel<any, any>,
231
+ initialConnectionDeferred,
232
+ }
233
+ }),
234
+ )