@livestore/webmesh 0.3.0-dev.37 → 0.3.0-dev.39
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 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/{message-channel-internal.d.ts → direct-channel-internal.d.ts} +7 -7
- package/dist/channel/direct-channel-internal.d.ts.map +1 -0
- package/dist/channel/{message-channel-internal.js → direct-channel-internal.js} +22 -22
- package/dist/channel/direct-channel-internal.js.map +1 -0
- package/dist/channel/{message-channel.d.ts → direct-channel.d.ts} +3 -3
- package/dist/channel/direct-channel.d.ts.map +1 -0
- package/dist/channel/{message-channel.js → direct-channel.js} +17 -17
- package/dist/channel/direct-channel.js.map +1 -0
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +84 -21
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +11 -5
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +6 -1
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +15 -15
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +9 -9
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +10 -5
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +68 -30
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +114 -17
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-edge.d.ts +2 -1
- package/dist/websocket-edge.d.ts.map +1 -1
- package/dist/websocket-edge.js +6 -2
- package/dist/websocket-edge.js.map +1 -1
- package/package.json +3 -4
- package/src/channel/{message-channel-internal.ts → direct-channel-internal.ts} +29 -29
- package/src/channel/{message-channel.ts → direct-channel.ts} +20 -20
- package/src/channel/proxy-channel.ts +107 -25
- package/src/common.ts +12 -4
- package/src/mesh-schema.ts +16 -19
- package/src/node.test.ts +185 -17
- package/src/node.ts +97 -35
- package/src/websocket-edge.ts +7 -1
- package/dist/channel/message-channel-internal.d.ts.map +0 -1
- package/dist/channel/message-channel-internal.js.map +0 -1
- package/dist/channel/message-channel.d.ts.map +0 -1
- package/dist/channel/message-channel.js.map +0 -1
|
@@ -16,30 +16,30 @@ import {
|
|
|
16
16
|
import { type ChannelName, type MeshNodeName, type MessageQueueItem, packetAsOtelAttributes } from '../common.js'
|
|
17
17
|
import * as MeshSchema from '../mesh-schema.js'
|
|
18
18
|
|
|
19
|
-
export interface
|
|
19
|
+
export interface MakeDirectChannelArgs {
|
|
20
20
|
nodeName: MeshNodeName
|
|
21
21
|
/** Queue of incoming messages for this channel */
|
|
22
22
|
incomingPacketsQueue: Queue.Queue<MessageQueueItem>
|
|
23
23
|
newEdgeAvailablePubSub: PubSub.PubSub<MeshNodeName>
|
|
24
24
|
channelName: ChannelName
|
|
25
25
|
target: MeshNodeName
|
|
26
|
-
sendPacket: (packet: typeof MeshSchema.
|
|
26
|
+
sendPacket: (packet: typeof MeshSchema.DirectChannelPacket.Type) => Effect.Effect<void>
|
|
27
27
|
checkTransferableEdges: (
|
|
28
|
-
packet: typeof MeshSchema.
|
|
29
|
-
) => typeof MeshSchema.
|
|
28
|
+
packet: typeof MeshSchema.DirectChannelPacket.Type,
|
|
29
|
+
) => typeof MeshSchema.DirectChannelResponseNoTransferables.Type | undefined
|
|
30
30
|
schema: WebChannel.OutputSchema<any, any, any, any>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const makeDeferredResult = Deferred.make<
|
|
34
34
|
WebChannel.WebChannel<any, any>,
|
|
35
|
-
typeof MeshSchema.
|
|
35
|
+
typeof MeshSchema.DirectChannelResponseNoTransferables.Type
|
|
36
36
|
>
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* The channel version is important here, as a channel will only be established once both sides have the same version.
|
|
40
40
|
* The version is used to avoid concurrency issues where both sides have different incompatible message ports.
|
|
41
41
|
*/
|
|
42
|
-
export const
|
|
42
|
+
export const makeDirectChannelInternal = ({
|
|
43
43
|
nodeName,
|
|
44
44
|
incomingPacketsQueue,
|
|
45
45
|
target,
|
|
@@ -50,14 +50,14 @@ export const makeMessageChannelInternal = ({
|
|
|
50
50
|
channelVersion,
|
|
51
51
|
scope,
|
|
52
52
|
sourceId,
|
|
53
|
-
}:
|
|
53
|
+
}: MakeDirectChannelArgs & {
|
|
54
54
|
channelVersion: number
|
|
55
|
-
/** We're passing in the closeable scope from the wrapping
|
|
55
|
+
/** We're passing in the closeable scope from the wrapping direct channel */
|
|
56
56
|
scope: Scope.CloseableScope
|
|
57
57
|
sourceId: string
|
|
58
58
|
}): Effect.Effect<
|
|
59
59
|
WebChannel.WebChannel<any, any>,
|
|
60
|
-
typeof MeshSchema.
|
|
60
|
+
typeof MeshSchema.DirectChannelResponseNoTransferables.Type,
|
|
61
61
|
Scope.Scope
|
|
62
62
|
> =>
|
|
63
63
|
Effect.gen(function* () {
|
|
@@ -95,8 +95,8 @@ export const makeMessageChannelInternal = ({
|
|
|
95
95
|
// }
|
|
96
96
|
|
|
97
97
|
const schema = {
|
|
98
|
-
send: Schema.Union(schema_.send, MeshSchema.
|
|
99
|
-
listen: Schema.Union(schema_.listen, MeshSchema.
|
|
98
|
+
send: Schema.Union(schema_.send, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong),
|
|
99
|
+
listen: Schema.Union(schema_.listen, MeshSchema.DirectChannelPing, MeshSchema.DirectChannelPong),
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const channelStateRef: { current: ChannelState } = {
|
|
@@ -122,7 +122,7 @@ export const makeMessageChannelInternal = ({
|
|
|
122
122
|
|
|
123
123
|
if (channelState._tag === 'Initial') return shouldNeverHappen()
|
|
124
124
|
|
|
125
|
-
if (packet._tag === '
|
|
125
|
+
if (packet._tag === 'DirectChannelResponseNoTransferables') {
|
|
126
126
|
yield* Deferred.fail(deferred, packet)
|
|
127
127
|
return 'close'
|
|
128
128
|
}
|
|
@@ -139,7 +139,7 @@ export const makeMessageChannelInternal = ({
|
|
|
139
139
|
// If this channel has a higher version, we need to signal the other side to close
|
|
140
140
|
// and recreate the channel with the new version
|
|
141
141
|
if (packet.channelVersion < channelVersion) {
|
|
142
|
-
const newPacket = MeshSchema.
|
|
142
|
+
const newPacket = MeshSchema.DirectChannelRequest.make({
|
|
143
143
|
source: nodeName,
|
|
144
144
|
sourceId,
|
|
145
145
|
target,
|
|
@@ -158,7 +158,7 @@ export const makeMessageChannelInternal = ({
|
|
|
158
158
|
return
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
if (channelState._tag === 'Established' && packet._tag === '
|
|
161
|
+
if (channelState._tag === 'Established' && packet._tag === 'DirectChannelRequest') {
|
|
162
162
|
if (packet.sourceId === channelState.otherSourceId) {
|
|
163
163
|
return
|
|
164
164
|
} else {
|
|
@@ -172,7 +172,7 @@ export const makeMessageChannelInternal = ({
|
|
|
172
172
|
|
|
173
173
|
switch (packet._tag) {
|
|
174
174
|
// Assumption: Each side has sent an initial request and another request as a response for an incoming request
|
|
175
|
-
case '
|
|
175
|
+
case 'DirectChannelRequest': {
|
|
176
176
|
if (channelState._tag !== 'RequestSent') {
|
|
177
177
|
// We can safely ignore further incoming requests as we're already creating a channel
|
|
178
178
|
return
|
|
@@ -181,7 +181,7 @@ export const makeMessageChannelInternal = ({
|
|
|
181
181
|
if (packet.reqId === channelState.reqPacketId) {
|
|
182
182
|
// Circuit-breaker: We've already sent a request so we don't need to send another one
|
|
183
183
|
} else {
|
|
184
|
-
const newRequestPacket = MeshSchema.
|
|
184
|
+
const newRequestPacket = MeshSchema.DirectChannelRequest.make({
|
|
185
185
|
source: nodeName,
|
|
186
186
|
sourceId,
|
|
187
187
|
target,
|
|
@@ -199,10 +199,10 @@ export const makeMessageChannelInternal = ({
|
|
|
199
199
|
const isWinner = nodeName > target
|
|
200
200
|
|
|
201
201
|
if (isWinner) {
|
|
202
|
-
span?.addEvent(`winner side: creating
|
|
202
|
+
span?.addEvent(`winner side: creating direct channel and sending response`)
|
|
203
203
|
const mc = new MessageChannel()
|
|
204
204
|
|
|
205
|
-
// We're using a
|
|
205
|
+
// We're using a direct channel with acks here to make sure messages are not lost
|
|
206
206
|
// which might happen during re-edge scenarios.
|
|
207
207
|
// Also we need to eagerly start listening since we're using the channel "ourselves"
|
|
208
208
|
// for the initial ping-pong sequence.
|
|
@@ -213,7 +213,7 @@ export const makeMessageChannelInternal = ({
|
|
|
213
213
|
}).pipe(Effect.andThen(WebChannel.toOpenChannel))
|
|
214
214
|
|
|
215
215
|
yield* respondToSender(
|
|
216
|
-
MeshSchema.
|
|
216
|
+
MeshSchema.DirectChannelResponseSuccess.make({
|
|
217
217
|
reqId: packet.id,
|
|
218
218
|
target,
|
|
219
219
|
source: nodeName,
|
|
@@ -232,14 +232,14 @@ export const makeMessageChannelInternal = ({
|
|
|
232
232
|
// Now we wait for the other side to respond via the channel
|
|
233
233
|
yield* channel.listen.pipe(
|
|
234
234
|
Stream.flatten(),
|
|
235
|
-
Stream.filter(Schema.is(MeshSchema.
|
|
235
|
+
Stream.filter(Schema.is(MeshSchema.DirectChannelPing)),
|
|
236
236
|
Stream.take(1),
|
|
237
237
|
Stream.runDrain,
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
// span?.addEvent(`winner side: sending pong`)
|
|
241
241
|
|
|
242
|
-
yield* channel.send(MeshSchema.
|
|
242
|
+
yield* channel.send(MeshSchema.DirectChannelPong.make({}))
|
|
243
243
|
|
|
244
244
|
span?.addEvent(`winner side: established`)
|
|
245
245
|
channelStateRef.current = { _tag: 'Established', otherSourceId: packet.sourceId }
|
|
@@ -247,20 +247,20 @@ export const makeMessageChannelInternal = ({
|
|
|
247
247
|
yield* Deferred.succeed(deferred, channel)
|
|
248
248
|
} else {
|
|
249
249
|
span?.addEvent(`loser side: waiting for response`)
|
|
250
|
-
// Wait for `
|
|
250
|
+
// Wait for `DirectChannelResponseSuccess` packet
|
|
251
251
|
channelStateRef.current = { _tag: 'loser:WaitingForResponse', otherSourceId: packet.sourceId }
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
break
|
|
255
255
|
}
|
|
256
|
-
case '
|
|
256
|
+
case 'DirectChannelResponseSuccess': {
|
|
257
257
|
if (channelState._tag !== 'loser:WaitingForResponse') {
|
|
258
258
|
return shouldNeverHappen(
|
|
259
|
-
`Expected to find
|
|
259
|
+
`Expected to find direct channel response from ${target}, but was in ${channelState._tag} state`,
|
|
260
260
|
)
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
// See
|
|
263
|
+
// See direct-channel notes above
|
|
264
264
|
const channel = yield* WebChannel.messagePortChannelWithAck({
|
|
265
265
|
port: packet.port,
|
|
266
266
|
schema,
|
|
@@ -269,7 +269,7 @@ export const makeMessageChannelInternal = ({
|
|
|
269
269
|
|
|
270
270
|
const waitForPongFiber = yield* channel.listen.pipe(
|
|
271
271
|
Stream.flatten(),
|
|
272
|
-
Stream.filter(Schema.is(MeshSchema.
|
|
272
|
+
Stream.filter(Schema.is(MeshSchema.DirectChannelPong)),
|
|
273
273
|
Stream.take(1),
|
|
274
274
|
Stream.runDrain,
|
|
275
275
|
Effect.fork,
|
|
@@ -282,7 +282,7 @@ export const makeMessageChannelInternal = ({
|
|
|
282
282
|
// TODO write a test that reproduces this issue and fix the root cause ()
|
|
283
283
|
// https://github.com/livestorejs/livestore/issues/262
|
|
284
284
|
yield* channel
|
|
285
|
-
.send(MeshSchema.
|
|
285
|
+
.send(MeshSchema.DirectChannelPing.make({}))
|
|
286
286
|
.pipe(Effect.timeout(10), Effect.retry({ times: 2 }))
|
|
287
287
|
|
|
288
288
|
// span?.addEvent(`loser side: waiting for pong`)
|
|
@@ -324,7 +324,7 @@ export const makeMessageChannelInternal = ({
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
const edgeRequest = Effect.gen(function* () {
|
|
327
|
-
const packet = MeshSchema.
|
|
327
|
+
const packet = MeshSchema.DirectChannelRequest.make({
|
|
328
328
|
source: nodeName,
|
|
329
329
|
sourceId,
|
|
330
330
|
target,
|
|
@@ -353,4 +353,4 @@ export const makeMessageChannelInternal = ({
|
|
|
353
353
|
const channel = yield* deferred
|
|
354
354
|
|
|
355
355
|
return channel
|
|
356
|
-
}).pipe(Effect.withSpanScoped(`
|
|
356
|
+
}).pipe(Effect.withSpanScoped(`makeDirectChannel:${channelVersion}`))
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
16
16
|
|
|
17
17
|
import * as WebmeshSchema from '../mesh-schema.js'
|
|
18
|
-
import type {
|
|
19
|
-
import {
|
|
18
|
+
import type { MakeDirectChannelArgs } from './direct-channel-internal.js'
|
|
19
|
+
import { makeDirectChannelInternal } from './direct-channel-internal.js'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Behaviour:
|
|
@@ -33,7 +33,7 @@ import { makeMessageChannelInternal } from './message-channel-internal.js'
|
|
|
33
33
|
*
|
|
34
34
|
* If needed we can also implement further functionality (like heartbeat) in this wrapper channel.
|
|
35
35
|
*/
|
|
36
|
-
export const
|
|
36
|
+
export const makeDirectChannel = ({
|
|
37
37
|
schema,
|
|
38
38
|
newEdgeAvailablePubSub,
|
|
39
39
|
channelName,
|
|
@@ -42,7 +42,7 @@ export const makeMessageChannel = ({
|
|
|
42
42
|
incomingPacketsQueue,
|
|
43
43
|
target,
|
|
44
44
|
sendPacket,
|
|
45
|
-
}:
|
|
45
|
+
}: MakeDirectChannelArgs) =>
|
|
46
46
|
Effect.scopeWithCloseable((scope) =>
|
|
47
47
|
Effect.gen(function* () {
|
|
48
48
|
/** Only used to identify whether a source is the same instance to know when to reconnect */
|
|
@@ -66,7 +66,7 @@ export const makeMessageChannel = ({
|
|
|
66
66
|
const resultDeferred = yield* Deferred.make<{
|
|
67
67
|
channel: WebChannel.WebChannel<any, any>
|
|
68
68
|
channelVersion: number
|
|
69
|
-
|
|
69
|
+
makeDirectChannelScope: Scope.CloseableScope
|
|
70
70
|
}>()
|
|
71
71
|
|
|
72
72
|
while (true) {
|
|
@@ -75,9 +75,9 @@ export const makeMessageChannel = ({
|
|
|
75
75
|
|
|
76
76
|
yield* Effect.spanEvent(`Connecting#${channelVersion}`)
|
|
77
77
|
|
|
78
|
-
const
|
|
78
|
+
const makeDirectChannelScope = yield* Scope.make()
|
|
79
79
|
// Attach the new scope to the parent scope
|
|
80
|
-
yield* Effect.addFinalizer((ex) => Scope.close(
|
|
80
|
+
yield* Effect.addFinalizer((ex) => Scope.close(makeDirectChannelScope, ex))
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Expected concurrency behaviour:
|
|
@@ -86,7 +86,7 @@ export const makeMessageChannel = ({
|
|
|
86
86
|
* - The edge setup succeeds and we can interrupt the waitForNewEdgeFiber
|
|
87
87
|
* - Tricky paths:
|
|
88
88
|
* - While a edge is still being setup, we want to re-try when there is a new edge
|
|
89
|
-
* - If the edge setup returns a `
|
|
89
|
+
* - If the edge setup returns a `DirectChannelResponseNoTransferables` error,
|
|
90
90
|
* we want to wait for a new edge and then re-try
|
|
91
91
|
* - Further notes:
|
|
92
92
|
* - If the parent scope closes, we want to also interrupt both the edge setup and the waitForNewEdgeFiber
|
|
@@ -102,7 +102,7 @@ export const makeMessageChannel = ({
|
|
|
102
102
|
Effect.fork,
|
|
103
103
|
)
|
|
104
104
|
|
|
105
|
-
const makeChannel =
|
|
105
|
+
const makeChannel = makeDirectChannelInternal({
|
|
106
106
|
nodeName,
|
|
107
107
|
sourceId,
|
|
108
108
|
incomingPacketsQueue,
|
|
@@ -113,10 +113,10 @@ export const makeMessageChannel = ({
|
|
|
113
113
|
channelVersion,
|
|
114
114
|
newEdgeAvailablePubSub,
|
|
115
115
|
sendPacket,
|
|
116
|
-
scope:
|
|
116
|
+
scope: makeDirectChannelScope,
|
|
117
117
|
}).pipe(
|
|
118
|
-
Scope.extend(
|
|
119
|
-
Effect.forkIn(
|
|
118
|
+
Scope.extend(makeDirectChannelScope),
|
|
119
|
+
Effect.forkIn(makeDirectChannelScope),
|
|
120
120
|
// Given we only call `Effect.exit` later when joining the fiber,
|
|
121
121
|
// we don't want Effect to produce a "unhandled error" log message
|
|
122
122
|
Effect.withUnhandledErrorLogLevel(Option.none()),
|
|
@@ -125,16 +125,16 @@ export const makeMessageChannel = ({
|
|
|
125
125
|
const raceResult = yield* Effect.raceFirst(makeChannel, waitForNewEdgeFiber.pipe(Effect.disconnect))
|
|
126
126
|
|
|
127
127
|
if (raceResult === 'new-edge') {
|
|
128
|
-
yield* Scope.close(
|
|
128
|
+
yield* Scope.close(makeDirectChannelScope, Exit.fail('new-edge'))
|
|
129
129
|
// We'll try again
|
|
130
130
|
} else {
|
|
131
131
|
const channelExit = yield* raceResult.pipe(Effect.exit)
|
|
132
132
|
if (channelExit._tag === 'Failure') {
|
|
133
|
-
yield* Scope.close(
|
|
133
|
+
yield* Scope.close(makeDirectChannelScope, channelExit)
|
|
134
134
|
|
|
135
135
|
if (
|
|
136
136
|
Cause.isFailType(channelExit.cause) &&
|
|
137
|
-
Schema.is(WebmeshSchema.
|
|
137
|
+
Schema.is(WebmeshSchema.DirectChannelResponseNoTransferables)(channelExit.cause.error)
|
|
138
138
|
) {
|
|
139
139
|
// Only retry when there is a new edge available
|
|
140
140
|
yield* waitForNewEdgeFiber.pipe(Effect.exit)
|
|
@@ -142,14 +142,14 @@ export const makeMessageChannel = ({
|
|
|
142
142
|
} else {
|
|
143
143
|
const channel = channelExit.value
|
|
144
144
|
|
|
145
|
-
yield* Deferred.succeed(resultDeferred, { channel,
|
|
145
|
+
yield* Deferred.succeed(resultDeferred, { channel, makeDirectChannelScope, channelVersion })
|
|
146
146
|
break
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// Now we wait until the first channel is established
|
|
152
|
-
const { channel,
|
|
152
|
+
const { channel, makeDirectChannelScope, channelVersion } = yield* resultDeferred
|
|
153
153
|
|
|
154
154
|
yield* Effect.spanEvent(`Connected#${channelVersion}`)
|
|
155
155
|
debugInfo.isConnected = true
|
|
@@ -164,7 +164,7 @@ export const makeMessageChannel = ({
|
|
|
164
164
|
Stream.tapChunk((chunk) => Queue.offerAll(listenQueue, chunk)),
|
|
165
165
|
Stream.runDrain,
|
|
166
166
|
Effect.tapCauseLogPretty,
|
|
167
|
-
Effect.forkIn(
|
|
167
|
+
Effect.forkIn(makeDirectChannelScope),
|
|
168
168
|
)
|
|
169
169
|
|
|
170
170
|
yield* Effect.gen(function* () {
|
|
@@ -177,12 +177,12 @@ export const makeMessageChannel = ({
|
|
|
177
177
|
yield* Deferred.succeed(deferred, void 0)
|
|
178
178
|
yield* TQueue.take(sendQueue) // Remove the message from the queue
|
|
179
179
|
}
|
|
180
|
-
}).pipe(Effect.forkIn(
|
|
180
|
+
}).pipe(Effect.forkIn(makeDirectChannelScope))
|
|
181
181
|
|
|
182
182
|
// Wait until the channel is closed and then try to reconnect
|
|
183
183
|
yield* channel.closedDeferred
|
|
184
184
|
|
|
185
|
-
yield* Scope.close(
|
|
185
|
+
yield* Scope.close(makeDirectChannelScope, Exit.succeed('channel-closed'))
|
|
186
186
|
|
|
187
187
|
yield* Effect.spanEvent(`Disconnected#${channelVersion}`)
|
|
188
188
|
debugInfo.isConnected = false
|
|
@@ -71,6 +71,7 @@ export const makeProxyChannel = ({
|
|
|
71
71
|
const channelStateRef = { current: { _tag: 'Initial' } as ProxiedChannelState }
|
|
72
72
|
|
|
73
73
|
const debugInfo = {
|
|
74
|
+
kind: 'proxy-channel',
|
|
74
75
|
pendingSends: 0,
|
|
75
76
|
totalSends: 0,
|
|
76
77
|
connectCounter: 0,
|
|
@@ -118,9 +119,15 @@ export const makeProxyChannel = ({
|
|
|
118
119
|
const getCombinedChannelId = (otherSideChannelIdCandidate: string) =>
|
|
119
120
|
[channelIdCandidate, otherSideChannelIdCandidate].sort().join('_')
|
|
120
121
|
|
|
122
|
+
const earlyPayloadBuffer = yield* Queue.unbounded<typeof MeshSchema.ProxyChannelPayload.Type>().pipe(
|
|
123
|
+
Effect.acquireRelease(Queue.shutdown),
|
|
124
|
+
)
|
|
125
|
+
|
|
121
126
|
const processProxyPacket = ({ packet, respondToSender }: ProxyQueueItem) =>
|
|
122
127
|
Effect.gen(function* () {
|
|
123
|
-
// yield* Effect.
|
|
128
|
+
// yield* Effect.logDebug(
|
|
129
|
+
// `[${nodeName}] processProxyPacket received: ${packet._tag} from ${packet.source} (reqId: ${packet.id})`,
|
|
130
|
+
// )
|
|
124
131
|
|
|
125
132
|
const otherSideName = packet.source
|
|
126
133
|
const channelKey = `target:${otherSideName}, channelName:${packet.channelName}` satisfies ChannelKey
|
|
@@ -130,19 +137,46 @@ export const makeProxyChannel = ({
|
|
|
130
137
|
case 'ProxyChannelRequest': {
|
|
131
138
|
const combinedChannelId = getCombinedChannelId(packet.channelIdCandidate)
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// Handle Established state explicitly
|
|
141
|
+
if (channelState._tag === 'Established') {
|
|
142
|
+
// Check if the incoming request is for the *same* channel instance
|
|
143
|
+
if (channelState.combinedChannelId === combinedChannelId) {
|
|
144
|
+
// Already established with the same ID, likely a redundant request.
|
|
145
|
+
// Just respond and stay established.
|
|
146
|
+
// yield* Effect.logDebug(
|
|
147
|
+
// `[${nodeName}] Received redundant ProxyChannelRequest for already established channel instance ${combinedChannelId}. Responding.`,
|
|
148
|
+
// )
|
|
149
|
+
} else {
|
|
150
|
+
// Established, but the incoming request has a different ID.
|
|
151
|
+
// This implies a reconnect scenario where IDs don't match. Reset to Pending and re-initiate.
|
|
152
|
+
yield* Effect.logWarning(
|
|
153
|
+
`[${nodeName}] Received ProxyChannelRequest with different channel ID (${combinedChannelId}) while established with ${channelState.combinedChannelId}. Re-establishing.`,
|
|
154
|
+
)
|
|
155
|
+
yield* SubscriptionRef.set(connectedStateRef, false)
|
|
156
|
+
channelStateRef.current = { _tag: 'Pending', initiatedVia: 'incoming-request' }
|
|
157
|
+
yield* Effect.spanEvent(`Reconnecting (received conflicting ProxyChannelRequest)`).pipe(
|
|
158
|
+
Effect.withParentSpan(channelSpan),
|
|
159
|
+
)
|
|
160
|
+
debugInfo.isConnected = false
|
|
161
|
+
debugInfo.connectCounter++
|
|
162
|
+
// We need to send our own request as well to complete the handshake for the new ID
|
|
142
163
|
yield* edgeRequest
|
|
143
164
|
}
|
|
165
|
+
} else if (channelState._tag === 'Initial') {
|
|
166
|
+
// Standard initial connection: set to Pending
|
|
167
|
+
yield* SubscriptionRef.set(connectedStateRef, false) // Ensure connectedStateRef is false if we were somehow Initial but it wasn't false
|
|
168
|
+
channelStateRef.current = { _tag: 'Pending', initiatedVia: 'incoming-request' }
|
|
169
|
+
yield* Effect.spanEvent(`Connecting (received ProxyChannelRequest)`).pipe(
|
|
170
|
+
Effect.withParentSpan(channelSpan),
|
|
171
|
+
)
|
|
172
|
+
debugInfo.isConnected = false // Should be false already, but ensure consistency
|
|
173
|
+
debugInfo.connectCounter++
|
|
174
|
+
// No need to send edgeRequest here, the response acts as our part of the handshake for the incoming request's ID
|
|
144
175
|
}
|
|
176
|
+
// If state is 'Pending', we are already trying to connect.
|
|
177
|
+
// Just let the response go out, don't change state.
|
|
145
178
|
|
|
179
|
+
// Send the response regardless of the initial state (unless an error occurred)
|
|
146
180
|
yield* respondToSender(
|
|
147
181
|
MeshSchema.ProxyChannelResponseSuccess.make({
|
|
148
182
|
reqId: packet.id,
|
|
@@ -160,7 +194,6 @@ export const makeProxyChannel = ({
|
|
|
160
194
|
}
|
|
161
195
|
case 'ProxyChannelResponseSuccess': {
|
|
162
196
|
if (channelState._tag !== 'Pending') {
|
|
163
|
-
// return shouldNeverHappen(`Expected proxy channel to be pending but got ${channelState._tag}`)
|
|
164
197
|
if (
|
|
165
198
|
channelState._tag === 'Established' &&
|
|
166
199
|
channelState.combinedChannelId !== packet.combinedChannelId
|
|
@@ -168,8 +201,14 @@ export const makeProxyChannel = ({
|
|
|
168
201
|
return shouldNeverHappen(
|
|
169
202
|
`ProxyChannel[${channelKey}]: Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
|
|
170
203
|
)
|
|
204
|
+
} else if (channelState._tag === 'Established') {
|
|
205
|
+
// yield* Effect.logDebug(`[${nodeName}] Ignoring redundant ResponseSuccess with same ID ${packet.id}`)
|
|
206
|
+
return
|
|
171
207
|
} else {
|
|
172
|
-
|
|
208
|
+
yield* Effect.logWarning(
|
|
209
|
+
`[${nodeName}] Ignoring ResponseSuccess ${packet.id} received in unexpected state ${channelState._tag}`,
|
|
210
|
+
)
|
|
211
|
+
return
|
|
173
212
|
}
|
|
174
213
|
}
|
|
175
214
|
|
|
@@ -182,21 +221,41 @@ export const makeProxyChannel = ({
|
|
|
182
221
|
|
|
183
222
|
yield* setStateToEstablished(packet.combinedChannelId)
|
|
184
223
|
|
|
224
|
+
const establishedState = channelStateRef.current
|
|
225
|
+
if (establishedState._tag === 'Established') {
|
|
226
|
+
//
|
|
227
|
+
const bufferedPackets = yield* Queue.takeAll(earlyPayloadBuffer)
|
|
228
|
+
// yield* Effect.logDebug(
|
|
229
|
+
// `[${nodeName}] Draining early payload buffer (${bufferedPackets.length}) after ResponseSuccess`,
|
|
230
|
+
// )
|
|
231
|
+
for (const bufferedPacket of bufferedPackets) {
|
|
232
|
+
if (establishedState.combinedChannelId !== bufferedPacket.combinedChannelId) {
|
|
233
|
+
yield* Effect.logWarning(
|
|
234
|
+
`[${nodeName}] Discarding buffered payload ${bufferedPacket.id}: Combined channel ID mismatch during drain. Expected ${establishedState.combinedChannelId}, got ${bufferedPacket.combinedChannelId}`,
|
|
235
|
+
)
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
const decodedMessage = yield* Schema.decodeUnknown(establishedState.listenSchema)(
|
|
239
|
+
bufferedPacket.payload,
|
|
240
|
+
)
|
|
241
|
+
yield* establishedState.listenQueue.pipe(Queue.offer(decodedMessage))
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
yield* Effect.logError(
|
|
245
|
+
`[${nodeName}] State is not Established immediately after setStateToEstablished was called. Cannot drain buffer. State: ${establishedState._tag}`,
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
185
249
|
return
|
|
186
250
|
}
|
|
187
251
|
case 'ProxyChannelPayload': {
|
|
188
|
-
if (channelState._tag
|
|
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
|
-
}
|
|
193
|
-
|
|
194
|
-
if (channelState.combinedChannelId !== packet.combinedChannelId) {
|
|
252
|
+
if (channelState._tag === 'Established' && channelState.combinedChannelId !== packet.combinedChannelId) {
|
|
195
253
|
return yield* Effect.die(
|
|
196
254
|
`ProxyChannel[${channelKey}]: Expected proxy channel to have the same combinedChannelId as the packet:\n${channelState.combinedChannelId} (channel) === ${packet.combinedChannelId} (packet)`,
|
|
197
255
|
)
|
|
198
256
|
}
|
|
199
257
|
|
|
258
|
+
// yield* Effect.logDebug(`[${nodeName}] Received payload reqId: ${packet.id}. Sending Ack.`)
|
|
200
259
|
yield* respondToSender(
|
|
201
260
|
MeshSchema.ProxyChannelPayloadAck.make({
|
|
202
261
|
reqId: packet.id,
|
|
@@ -205,25 +264,36 @@ export const makeProxyChannel = ({
|
|
|
205
264
|
target,
|
|
206
265
|
source: nodeName,
|
|
207
266
|
channelName,
|
|
208
|
-
combinedChannelId:
|
|
267
|
+
combinedChannelId:
|
|
268
|
+
channelState._tag === 'Established' ? channelState.combinedChannelId : packet.combinedChannelId,
|
|
209
269
|
}),
|
|
210
270
|
)
|
|
211
271
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
272
|
+
if (channelState._tag === 'Established') {
|
|
273
|
+
const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
|
|
274
|
+
yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
|
|
275
|
+
} else {
|
|
276
|
+
// yield* Effect.logDebug(
|
|
277
|
+
// `[${nodeName}] Buffering early payload reqId: ${packet.id} (state: ${channelState._tag})`,
|
|
278
|
+
// )
|
|
279
|
+
yield* Queue.offer(earlyPayloadBuffer, packet)
|
|
280
|
+
}
|
|
215
281
|
return
|
|
216
282
|
}
|
|
217
283
|
case 'ProxyChannelPayloadAck': {
|
|
284
|
+
// yield* Effect.logDebug(`[${nodeName}] Received Ack for reqId: ${packet.reqId}`)
|
|
285
|
+
|
|
218
286
|
if (channelState._tag !== 'Established') {
|
|
219
287
|
yield* Effect.spanEvent(`Not yet connected to ${target}. dropping message`)
|
|
288
|
+
yield* Effect.logWarning(
|
|
289
|
+
`[${nodeName}] Received Ack but not established (State: ${channelState._tag}). Dropping Ack for ${packet.reqId}`,
|
|
290
|
+
)
|
|
220
291
|
return
|
|
221
292
|
}
|
|
222
293
|
|
|
223
294
|
const ack =
|
|
224
295
|
channelState.ackMap.get(packet.reqId) ??
|
|
225
296
|
shouldNeverHappen(`[ProxyChannel[${channelKey}]] Expected ack for ${packet.reqId}`)
|
|
226
|
-
|
|
227
297
|
yield* Deferred.succeed(ack, void 0)
|
|
228
298
|
|
|
229
299
|
channelState.ackMap.delete(packet.reqId)
|
|
@@ -344,15 +414,27 @@ export const makeProxyChannel = ({
|
|
|
344
414
|
|
|
345
415
|
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
346
416
|
|
|
417
|
+
const runtime = yield* Effect.runtime()
|
|
418
|
+
|
|
347
419
|
const webChannel = {
|
|
348
420
|
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
349
421
|
send,
|
|
350
422
|
listen,
|
|
351
423
|
closedDeferred,
|
|
352
|
-
supportsTransferables:
|
|
424
|
+
supportsTransferables: false,
|
|
353
425
|
schema,
|
|
354
426
|
shutdown: Scope.close(scope, Exit.void),
|
|
355
427
|
debugInfo,
|
|
428
|
+
...({
|
|
429
|
+
debug: {
|
|
430
|
+
ping: (message: string = 'ping') =>
|
|
431
|
+
send(WebChannel.DebugPingMessage.make({ message })).pipe(
|
|
432
|
+
Effect.provide(runtime),
|
|
433
|
+
Effect.tapCauseLogPretty,
|
|
434
|
+
Effect.runFork,
|
|
435
|
+
),
|
|
436
|
+
},
|
|
437
|
+
} as {}),
|
|
356
438
|
} satisfies WebChannel.WebChannel<any, any>
|
|
357
439
|
|
|
358
440
|
return webChannel as WebChannel.WebChannel<any, any>
|
package/src/common.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Effect, Predicate, Schema } from '@livestore/utils/effect'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DirectChannelPacket, Packet, ProxyChannelPacket } from './mesh-schema.js'
|
|
4
4
|
|
|
5
5
|
export type ProxyQueueItem = {
|
|
6
6
|
packet: typeof ProxyChannelPacket.Type
|
|
@@ -8,8 +8,8 @@ export type ProxyQueueItem = {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export type MessageQueueItem = {
|
|
11
|
-
packet: typeof
|
|
12
|
-
respondToSender: (msg: typeof
|
|
11
|
+
packet: typeof DirectChannelPacket.Type
|
|
12
|
+
respondToSender: (msg: typeof DirectChannelPacket.Type) => Effect.Effect<void>
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type MeshNodeName = string
|
|
@@ -31,5 +31,13 @@ export const packetAsOtelAttributes = (packet: typeof Packet.Type) => ({
|
|
|
31
31
|
packetId: packet.id,
|
|
32
32
|
'span.label':
|
|
33
33
|
packet.id + (Predicate.hasProperty(packet, 'reqId') && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''),
|
|
34
|
-
...(packet._tag !== '
|
|
34
|
+
...(packet._tag !== 'DirectChannelResponseSuccess' && packet._tag !== 'ProxyChannelPayload' ? { packet } : {}),
|
|
35
35
|
})
|
|
36
|
+
|
|
37
|
+
export const ListenForChannelResult = Schema.Struct({
|
|
38
|
+
channelName: Schema.String,
|
|
39
|
+
source: Schema.String,
|
|
40
|
+
mode: Schema.Union(Schema.Literal('proxy'), Schema.Literal('direct')),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export type ListenForChannelResult = typeof ListenForChannelResult.Type
|