@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.
- package/README.md +26 -0
- 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 +217 -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 +132 -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 +30 -11
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +32 -5
- 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 +68 -2
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +53 -4
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +31 -9
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +225 -49
- 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 +384 -149
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +5 -6
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +21 -26
- package/dist/websocket-connection.js.map +1 -1
- package/dist/websocket-server.d.ts.map +1 -1
- package/dist/websocket-server.js +17 -3
- package/dist/websocket-server.js.map +1 -1
- package/package.json +7 -6
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +190 -310
- package/src/channel/proxy-channel.ts +257 -229
- package/src/common.ts +4 -2
- package/src/mesh-schema.ts +60 -4
- package/src/node.test.ts +544 -179
- package/src/node.ts +363 -69
- package/src/websocket-connection.ts +96 -95
- package/src/websocket-server.ts +20 -3
- package/tmp/pack.tgz +0 -0
|
@@ -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
|
-
|
|
8
|
-
|
|
6
|
+
Exit,
|
|
7
|
+
Option,
|
|
9
8
|
Queue,
|
|
10
|
-
|
|
9
|
+
Schema,
|
|
10
|
+
Scope,
|
|
11
11
|
Stream,
|
|
12
|
-
|
|
12
|
+
TQueue,
|
|
13
13
|
WebChannel,
|
|
14
14
|
} from '@livestore/utils/effect'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import * as
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
import { nanoid } from '@livestore/utils/nanoid'
|
|
16
|
+
|
|
17
|
+
import * as WebmeshSchema from '../mesh-schema.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
|
-
|
|
37
|
-
queue,
|
|
37
|
+
schema,
|
|
38
38
|
newConnectionAvailablePubSub,
|
|
39
|
-
target,
|
|
40
|
-
checkTransferableConnections,
|
|
41
39
|
channelName,
|
|
42
|
-
|
|
40
|
+
checkTransferableConnections,
|
|
41
|
+
nodeName,
|
|
42
|
+
incomingPacketsQueue,
|
|
43
|
+
target,
|
|
43
44
|
sendPacket,
|
|
44
45
|
}: MakeMessageChannelArgs) =>
|
|
45
|
-
Effect.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
210
|
-
|
|
154
|
+
yield* Effect.spanEvent(`Connected#${channelVersion}`)
|
|
155
|
+
debugInfo.isConnected = true
|
|
156
|
+
debugInfo.innerChannelRef.current = channel
|
|
211
157
|
|
|
212
|
-
yield*
|
|
158
|
+
yield* Deferred.succeed(initialConnectionDeferred, void 0)
|
|
213
159
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Stream.
|
|
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.
|
|
166
|
+
Effect.tapCauseLogPretty,
|
|
167
|
+
Effect.forkIn(makeMessageChannelScope),
|
|
219
168
|
)
|
|
220
169
|
|
|
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
|
|
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
|
-
|
|
182
|
+
// Wait until the channel is closed and then try to reconnect
|
|
183
|
+
yield* channel.closedDeferred
|
|
265
184
|
|
|
266
|
-
|
|
185
|
+
yield* Scope.close(makeMessageChannelScope, Exit.succeed('channel-closed'))
|
|
267
186
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
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
|
-
|
|
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))
|
|
200
|
+
const send = (message: any) =>
|
|
201
|
+
Effect.gen(function* () {
|
|
202
|
+
const sentDeferred = yield* Deferred.make<void>()
|
|
325
203
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
|
|
329
|
-
Stream.runDrain,
|
|
330
|
-
Effect.fork,
|
|
331
|
-
)
|
|
204
|
+
debugInfo.pendingSends++
|
|
205
|
+
debugInfo.totalSends++
|
|
332
206
|
|
|
333
|
-
|
|
207
|
+
yield* TQueue.offer(sendQueue, [message, sentDeferred])
|
|
334
208
|
|
|
335
|
-
|
|
209
|
+
yield* sentDeferred
|
|
336
210
|
|
|
337
|
-
|
|
338
|
-
|
|
211
|
+
debugInfo.pendingSends--
|
|
212
|
+
}).pipe(Effect.scoped, Effect.withParentSpan(parentSpan))
|
|
339
213
|
|
|
340
|
-
|
|
214
|
+
const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right))
|
|
341
215
|
|
|
342
|
-
|
|
216
|
+
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
343
217
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
229
|
+
return {
|
|
230
|
+
webChannel: webChannel as WebChannel.WebChannel<any, any>,
|
|
231
|
+
initialConnectionDeferred,
|
|
232
|
+
}
|
|
233
|
+
}),
|
|
234
|
+
)
|