@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.
- package/README.md +19 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts +26 -0
- package/dist/channel/message-channel-internal.d.ts.map +1 -0
- package/dist/channel/message-channel-internal.js +202 -0
- package/dist/channel/message-channel-internal.js.map +1 -0
- package/dist/channel/message-channel.d.ts +21 -19
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +125 -162
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +7 -5
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +8 -4
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +2 -1
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +23 -1
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +21 -2
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +12 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +39 -9
- package/dist/node.js.map +1 -1
- package/dist/node.test.d.ts +1 -1
- package/dist/node.test.d.ts.map +1 -1
- package/dist/node.test.js +256 -124
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +1 -2
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +337 -0
- package/src/channel/message-channel.ts +177 -308
- package/src/channel/proxy-channel.ts +238 -230
- package/src/common.ts +3 -1
- package/src/mesh-schema.ts +20 -2
- package/src/node.test.ts +367 -150
- package/src/node.ts +68 -12
- 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
|
-
|
|
8
|
-
FiberHandle,
|
|
6
|
+
Exit,
|
|
9
7
|
Queue,
|
|
10
|
-
|
|
8
|
+
Schema,
|
|
9
|
+
Scope,
|
|
11
10
|
Stream,
|
|
12
|
-
|
|
11
|
+
TQueue,
|
|
13
12
|
WebChannel,
|
|
14
13
|
} from '@livestore/utils/effect'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
queue,
|
|
36
|
+
schema,
|
|
38
37
|
newConnectionAvailablePubSub,
|
|
39
|
-
target,
|
|
40
|
-
checkTransferableConnections,
|
|
41
38
|
channelName,
|
|
42
|
-
|
|
39
|
+
checkTransferableConnections,
|
|
40
|
+
nodeName,
|
|
41
|
+
incomingPacketsQueue,
|
|
42
|
+
target,
|
|
43
43
|
sendPacket,
|
|
44
44
|
}: MakeMessageChannelArgs) =>
|
|
45
|
-
Effect.
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
53
|
+
const initialConnectionDeferred = yield* Deferred.make<void>()
|
|
105
54
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
210
|
-
|
|
145
|
+
yield* Effect.spanEvent(`Connected#${channelVersion}`)
|
|
146
|
+
debugInfo.isConnected = true
|
|
211
147
|
|
|
212
|
-
yield*
|
|
148
|
+
yield* Deferred.succeed(initialConnectionDeferred, void 0)
|
|
213
149
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Stream.
|
|
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.
|
|
156
|
+
Effect.tapCauseLogPretty,
|
|
157
|
+
Effect.forkIn(makeMessageChannelScope),
|
|
219
158
|
)
|
|
220
159
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
172
|
+
// Wait until the channel is closed and then try to reconnect
|
|
173
|
+
yield* channel.closedDeferred
|
|
265
174
|
|
|
266
|
-
|
|
175
|
+
yield* Scope.close(makeMessageChannelScope, Exit.succeed('channel-closed'))
|
|
267
176
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
|
|
329
|
-
Stream.runDrain,
|
|
330
|
-
Effect.fork,
|
|
331
|
-
)
|
|
193
|
+
debugInfo.pendingSends++
|
|
194
|
+
debugInfo.totalSends++
|
|
332
195
|
|
|
333
|
-
|
|
196
|
+
yield* TQueue.offer(sendQueue, [message, sentDeferred])
|
|
334
197
|
|
|
335
|
-
|
|
198
|
+
yield* sentDeferred
|
|
336
199
|
|
|
337
|
-
|
|
338
|
-
|
|
200
|
+
debugInfo.pendingSends--
|
|
201
|
+
}).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
|
|
339
202
|
|
|
340
|
-
|
|
203
|
+
const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right))
|
|
341
204
|
|
|
342
|
-
|
|
205
|
+
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
343
206
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
218
|
+
return {
|
|
219
|
+
webChannel: webChannel as WebChannel.WebChannel<any, any>,
|
|
220
|
+
initialConnectionDeferred,
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
)
|