@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230

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 (43) hide show
  1. package/README.md +19 -1
  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 +202 -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 +125 -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 +7 -5
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +8 -4
  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 +23 -1
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +21 -2
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/node.d.ts +12 -1
  24. package/dist/node.d.ts.map +1 -1
  25. package/dist/node.js +39 -9
  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 +256 -124
  30. package/dist/node.test.js.map +1 -1
  31. package/dist/websocket-connection.d.ts +1 -2
  32. package/dist/websocket-connection.d.ts.map +1 -1
  33. package/dist/websocket-connection.js +5 -4
  34. package/dist/websocket-connection.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/channel/message-channel-internal.ts +337 -0
  37. package/src/channel/message-channel.ts +177 -308
  38. package/src/channel/proxy-channel.ts +238 -230
  39. package/src/common.ts +3 -1
  40. package/src/mesh-schema.ts +20 -2
  41. package/src/node.test.ts +367 -150
  42. package/src/node.ts +68 -12
  43. package/src/websocket-connection.ts +83 -79
@@ -1,354 +1,223 @@
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,
9
7
  Queue,
10
- Schedule,
8
+ Schema,
9
+ Scope,
11
10
  Stream,
12
- SubscriptionRef,
11
+ TQueue,
13
12
  WebChannel,
14
13
  } 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
-
14
+ import { nanoid } from '@livestore/utils/nanoid'
15
+
16
+ import { WebmeshSchema } from '../mod.js'
17
+ import type { MakeMessageChannelArgs } from './message-channel-internal.js'
18
+ import { makeMessageChannelInternal } from './message-channel-internal.js'
19
+
20
+ /**
21
+ * Behaviour:
22
+ * - Waits until there is an initial connection
23
+ * - Automatically reconnects on disconnect
24
+ *
25
+ * Implementation notes:
26
+ * - We've split up the functionality into a wrapper channel and an internal channel.
27
+ * - The wrapper channel is responsible for:
28
+ * - Forwarding send/listen messages to the internal channel (via a queue)
29
+ * - Establishing the initial channel and reconnecting on disconnect
30
+ * - Listening for new connections as a hint to reconnect if not already connected
31
+ * - The wrapper channel maintains a connection counter which is used as the channel version
32
+ *
33
+ * If needed we can also implement further functionality (like heartbeat) in this wrapper channel.
34
+ */
35
35
  export const makeMessageChannel = ({
36
- nodeName,
37
- queue,
36
+ schema,
38
37
  newConnectionAvailablePubSub,
39
- target,
40
- checkTransferableConnections,
41
38
  channelName,
42
- schema,
39
+ checkTransferableConnections,
40
+ nodeName,
41
+ incomingPacketsQueue,
42
+ target,
43
43
  sendPacket,
44
44
  }: 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
- }
45
+ Effect.scopeWithCloseable((scope) =>
46
+ Effect.gen(function* () {
47
+ /** Only used to identify whether a source is the same instance to know when to reconnect */
48
+ const sourceId = nanoid()
97
49
 
98
- if (channelState._tag === 'Established') {
99
- const deferred = yield* Deferred.make<
100
- MessagePort,
101
- typeof MeshSchema.MessageChannelResponseNoTransferables.Type
102
- >()
50
+ const listenQueue = yield* Queue.unbounded<any>()
51
+ const sendQueue = yield* TQueue.unbounded<[msg: any, deferred: Deferred.Deferred<void>]>()
103
52
 
104
- channelStateRef.current = { _tag: 'RequestSent', deferred }
53
+ const initialConnectionDeferred = yield* Deferred.make<void>()
105
54
 
106
- yield* reconnect
107
-
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)
55
+ const debugInfo = {
56
+ pendingSends: 0,
57
+ totalSends: 0,
58
+ connectCounter: 1,
59
+ isConnected: false,
60
+ }
118
61
 
119
- return
120
- }
121
- case 'MessageChannelResponseNoTransferables': {
122
- if (channelState._tag === 'Established') return
62
+ // #region reconnect-loop
63
+ yield* Effect.gen(function* () {
64
+ const resultDeferred = yield* Deferred.make<{
65
+ channel: WebChannel.WebChannel<any, any>
66
+ channelVersion: number
67
+ makeMessageChannelScope: Scope.CloseableScope
68
+ }>()
69
+
70
+ while (true) {
71
+ debugInfo.connectCounter++
72
+ const channelVersion = debugInfo.connectCounter
73
+
74
+ yield* Effect.spanEvent(`Connecting#${channelVersion}`)
75
+
76
+ const makeMessageChannelScope = yield* Scope.make()
77
+ // Attach the new scope to the parent scope
78
+ yield* Effect.addFinalizer((ex) => Scope.close(makeMessageChannelScope, ex))
79
+
80
+ /**
81
+ * Expected concurrency behaviour:
82
+ * - We're concurrently running the connection setup and the waitForNewConnectionFiber
83
+ * - Happy path:
84
+ * - The connection setup succeeds and we can interrupt the waitForNewConnectionFiber
85
+ * - Tricky paths:
86
+ * - While a connection is still being setup, we want to re-try when there is a new connection
87
+ * - If the connection setup returns a `MessageChannelResponseNoTransferables` error,
88
+ * we want to wait for a new connection and then re-try
89
+ * - Further notes:
90
+ * - If the parent scope closes, we want to also interrupt both the connection setup and the waitForNewConnectionFiber
91
+ * - We're creating a separate scope for each connection attempt, which
92
+ * - we'll use to fork the message channel in which allows us to interrupt it later
93
+ * - We need to make sure that "interruption" isn't "bubbling out"
94
+ */
95
+ const waitForNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
96
+ Stream.tap((connectionName) => Effect.spanEvent(`new-conn:${connectionName}`)),
97
+ Stream.take(1),
98
+ Stream.runDrain,
99
+ Effect.as('new-connection' as const),
100
+ Effect.fork,
101
+ )
123
102
 
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
103
+ const makeChannel = makeMessageChannelInternal({
104
+ nodeName,
105
+ sourceId,
106
+ incomingPacketsQueue,
107
+ target,
108
+ checkTransferableConnections,
109
+ channelName,
110
+ schema,
111
+ channelVersion,
112
+ newConnectionAvailablePubSub,
113
+ sendPacket,
114
+ scope: makeMessageChannelScope,
115
+ }).pipe(Scope.extend(makeMessageChannelScope), Effect.forkIn(makeMessageChannelScope))
116
+
117
+ const res = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect))
118
+
119
+ if (res === 'new-connection') {
120
+ yield* Scope.close(makeMessageChannelScope, Exit.fail('new-connection'))
121
+ // We'll try again
122
+ } else {
123
+ const result = yield* res.pipe(Effect.exit)
124
+ if (result._tag === 'Failure') {
125
+ yield* Scope.close(makeMessageChannelScope, result)
126
+
127
+ if (
128
+ Cause.isFailType(result.cause) &&
129
+ Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(result.cause.error)
130
+ ) {
131
+ yield* waitForNewConnectionFiber.pipe(Effect.exit)
156
132
  }
133
+ } else {
134
+ const channel = result.value
157
135
 
136
+ yield* Deferred.succeed(resultDeferred, { channel, makeMessageChannelScope, channelVersion })
158
137
  break
159
138
  }
160
- default: {
161
- return casesHandled(packet)
162
- }
163
139
  }
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: [] })
140
+ }
201
141
 
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
- }
142
+ // Now we wait until the first channel is established
143
+ const { channel, makeMessageChannelScope, channelVersion } = yield* resultDeferred
208
144
 
209
- yield* sendPacket(packet)
210
- })
145
+ yield* Effect.spanEvent(`Connected#${channelVersion}`)
146
+ debugInfo.isConnected = true
211
147
 
212
- yield* connectionRequest
148
+ yield* Deferred.succeed(initialConnectionDeferred, void 0)
213
149
 
214
- const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(
215
- Stream.tap(() => Effect.spanEvent(`RetryOnNewConnection`)),
216
- Stream.tap(() => connectionRequest),
150
+ // We'll now forward all incoming messages to the listen queue
151
+ yield* channel.listen.pipe(
152
+ Stream.flatten(),
153
+ // Stream.tap((msg) => Effect.log(`${target}→${channelName}→${nodeName}:message:${msg.message}`)),
154
+ Stream.tapChunk((chunk) => Queue.offerAll(listenQueue, chunk)),
217
155
  Stream.runDrain,
218
- Effect.forkScoped,
156
+ Effect.tapCauseLogPretty,
157
+ Effect.forkIn(makeMessageChannelScope),
219
158
  )
220
159
 
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
160
+ yield* Effect.gen(function* () {
161
+ while (true) {
162
+ const [msg, deferred] = yield* TQueue.peek(sendQueue)
163
+ // NOTE we don't need an explicit retry flow here since in case of the channel being closed,
164
+ // the send will never succeed. Meanwhile the send-loop fiber will be interrupted and
165
+ // given we only peeked at the queue, the message to send is still there.
166
+ yield* channel.send(msg)
167
+ yield* Deferred.succeed(deferred, void 0)
168
+ yield* TQueue.take(sendQueue) // Remove the message from the queue
169
+ }
170
+ }).pipe(Effect.forkIn(makeMessageChannelScope))
263
171
 
264
- yield* SubscriptionRef.set(internalChannelSref, internalChannel)
172
+ // Wait until the channel is closed and then try to reconnect
173
+ yield* channel.closedDeferred
265
174
 
266
- yield* Effect.spanEvent(`Connected#${connectCount}`)
175
+ yield* Scope.close(makeMessageChannelScope, Exit.succeed('channel-closed'))
267
176
 
268
- yield* internalChannel.listen.pipe(
269
- Stream.flatten(),
270
- Stream.tap((msg) => Queue.offer(listenQueue, msg)),
271
- Stream.runDrain,
177
+ yield* Effect.spanEvent(`Disconnected#${channelVersion}`)
178
+ debugInfo.isConnected = false
179
+ }).pipe(
180
+ Effect.scoped, // Additionally scoping here to clean up finalizers after each loop run
181
+ Effect.forever,
272
182
  Effect.tapCauseLogPretty,
273
183
  Effect.forkScoped,
274
184
  )
185
+ // #endregion reconnect-loop
275
186
 
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
- )
187
+ const parentSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
303
188
 
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))
189
+ const send = (message: any) =>
190
+ Effect.gen(function* () {
191
+ const sentDeferred = yield* Deferred.make<void>()
325
192
 
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
- )
193
+ debugInfo.pendingSends++
194
+ debugInfo.totalSends++
332
195
 
333
- yield* FiberHandle.run(sendFiberHandle, trySend)
196
+ yield* TQueue.offer(sendQueue, [message, sentDeferred])
334
197
 
335
- yield* sentDeferred
198
+ yield* sentDeferred
336
199
 
337
- yield* Fiber.interrupt(rerunOnNewChannelFiber)
338
- }).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
200
+ debugInfo.pendingSends--
201
+ }).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
339
202
 
340
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
203
+ const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right))
341
204
 
342
- const closedDeferred = yield* Deferred.make<void>()
205
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
343
206
 
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>
207
+ const webChannel = {
208
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
209
+ send,
210
+ listen,
211
+ closedDeferred,
212
+ supportsTransferables: true,
213
+ schema,
214
+ debugInfo,
215
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
216
+ } satisfies WebChannel.WebChannel<any, any>
352
217
 
353
- return webChannel as WebChannel.WebChannel<any, any>
354
- }).pipe(Effect.withSpanScoped('makeMessageChannel'))
218
+ return {
219
+ webChannel: webChannel as WebChannel.WebChannel<any, any>,
220
+ initialConnectionDeferred,
221
+ }
222
+ }),
223
+ )