@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PubSub } from '@livestore/utils/effect';
|
|
2
|
+
import { Effect, Queue, Scope, WebChannel } from '@livestore/utils/effect';
|
|
3
|
+
import { type ChannelName, type MeshNodeName, type MessageQueueItem } from '../common.js';
|
|
4
|
+
import * as MeshSchema from '../mesh-schema.js';
|
|
5
|
+
export interface MakeMessageChannelArgs {
|
|
6
|
+
nodeName: MeshNodeName;
|
|
7
|
+
/** Queue of incoming messages for this channel */
|
|
8
|
+
incomingPacketsQueue: Queue.Queue<MessageQueueItem>;
|
|
9
|
+
newConnectionAvailablePubSub: PubSub.PubSub<MeshNodeName>;
|
|
10
|
+
channelName: ChannelName;
|
|
11
|
+
target: MeshNodeName;
|
|
12
|
+
sendPacket: (packet: typeof MeshSchema.MessageChannelPacket.Type) => Effect.Effect<void>;
|
|
13
|
+
checkTransferableConnections: (packet: typeof MeshSchema.MessageChannelPacket.Type) => typeof MeshSchema.MessageChannelResponseNoTransferables.Type | undefined;
|
|
14
|
+
schema: WebChannel.OutputSchema<any, any, any, any>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The channel version is important here, as a channel will only be established once both sides have the same version.
|
|
18
|
+
* The version is used to avoid concurrency issues where both sides have different incompatible message ports.
|
|
19
|
+
*/
|
|
20
|
+
export declare const makeMessageChannelInternal: ({ nodeName, incomingPacketsQueue, target, checkTransferableConnections, channelName, schema: schema_, sendPacket, channelVersion, scope, sourceId, }: MakeMessageChannelArgs & {
|
|
21
|
+
channelVersion: number;
|
|
22
|
+
/** We're passing in the closeable scope from the wrapping message channel */
|
|
23
|
+
scope: Scope.CloseableScope;
|
|
24
|
+
sourceId: string;
|
|
25
|
+
}) => Effect.Effect<WebChannel.WebChannel<any, any>, typeof MeshSchema.MessageChannelResponseNoTransferables.Type, Scope.Scope>;
|
|
26
|
+
//# sourceMappingURL=message-channel-internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-channel-internal.d.ts","sourceRoot":"","sources":["../../src/channel/message-channel-internal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAEL,MAAM,EAIN,KAAK,EAEL,KAAK,EAEL,UAAU,EACX,MAAM,yBAAyB,CAAA;AAEhC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAA0B,MAAM,cAAc,CAAA;AACjH,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAE/C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,YAAY,CAAA;IACtB,kDAAkD;IAClD,oBAAoB,EAAE,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;IACnD,4BAA4B,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACzD,WAAW,EAAE,WAAW,CAAA;IACxB,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,UAAU,CAAC,oBAAoB,CAAC,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxF,4BAA4B,EAAE,CAC5B,MAAM,EAAE,OAAO,UAAU,CAAC,oBAAoB,CAAC,IAAI,KAChD,OAAO,UAAU,CAAC,qCAAqC,CAAC,IAAI,GAAG,SAAS,CAAA;IAC7E,MAAM,EAAE,UAAU,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;CACpD;AAOD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,yJAWpC,sBAAsB,GAAG;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,6EAA6E;IAC7E,KAAK,EAAE,KAAK,CAAC,cAAc,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;CACjB,KAAG,MAAM,CAAC,MAAM,CACf,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,EAC/B,OAAO,UAAU,CAAC,qCAAqC,CAAC,IAAI,EAC5D,KAAK,CAAC,KAAK,CAoR2D,CAAA"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { casesHandled, shouldNeverHappen } from '@livestore/utils';
|
|
2
|
+
import { Deferred, Effect, Exit, OtelTracer, Predicate, Queue, Schema, Scope, Stream, WebChannel, } from '@livestore/utils/effect';
|
|
3
|
+
import { packetAsOtelAttributes } from '../common.js';
|
|
4
|
+
import * as MeshSchema from '../mesh-schema.js';
|
|
5
|
+
const makeDeferredResult = (Deferred.make);
|
|
6
|
+
/**
|
|
7
|
+
* The channel version is important here, as a channel will only be established once both sides have the same version.
|
|
8
|
+
* The version is used to avoid concurrency issues where both sides have different incompatible message ports.
|
|
9
|
+
*/
|
|
10
|
+
export const makeMessageChannelInternal = ({ nodeName, incomingPacketsQueue, target, checkTransferableConnections, channelName, schema: schema_, sendPacket, channelVersion, scope, sourceId, }) => Effect.gen(function* () {
|
|
11
|
+
// yield* Effect.addFinalizer((exit) =>
|
|
12
|
+
// Effect.spanEvent(`shutdown:${exit._tag === 'Success' ? 'Success' : Cause.pretty(exit.cause)}`),
|
|
13
|
+
// )
|
|
14
|
+
const deferred = yield* makeDeferredResult();
|
|
15
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)));
|
|
16
|
+
const schema = {
|
|
17
|
+
send: Schema.Union(schema_.send, MeshSchema.MessageChannelPing, MeshSchema.MessageChannelPong),
|
|
18
|
+
listen: Schema.Union(schema_.listen, MeshSchema.MessageChannelPing, MeshSchema.MessageChannelPong),
|
|
19
|
+
};
|
|
20
|
+
const channelStateRef = {
|
|
21
|
+
current: { _tag: 'Initial' },
|
|
22
|
+
};
|
|
23
|
+
const processMessagePacket = ({ packet, respondToSender }) => Effect.gen(function* () {
|
|
24
|
+
const channelState = channelStateRef.current;
|
|
25
|
+
span?.addEvent(`process:${packet._tag}`, {
|
|
26
|
+
channelState: channelState._tag,
|
|
27
|
+
packetId: packet.id,
|
|
28
|
+
packetReqId: packet.reqId,
|
|
29
|
+
packetChannelVersion: Predicate.hasProperty('channelVersion')(packet) ? packet.channelVersion : undefined,
|
|
30
|
+
});
|
|
31
|
+
// yield* Effect.log(
|
|
32
|
+
// `${nodeName}→${channelName}→${target}:process packet ${packet._tag} [${channelVersion}], channel state: ${channelState._tag}`,
|
|
33
|
+
// )
|
|
34
|
+
if (channelState._tag === 'Initial')
|
|
35
|
+
return shouldNeverHappen();
|
|
36
|
+
if (packet._tag === 'MessageChannelResponseNoTransferables') {
|
|
37
|
+
yield* Deferred.fail(deferred, packet);
|
|
38
|
+
return 'close';
|
|
39
|
+
}
|
|
40
|
+
// If the other side has a higher version, we need to close this channel and
|
|
41
|
+
// recreate it with the new version
|
|
42
|
+
if (packet.channelVersion > channelVersion) {
|
|
43
|
+
span?.addEvent(`incoming packet has higher version (${packet.channelVersion}), closing channel`);
|
|
44
|
+
yield* Scope.close(scope, Exit.succeed('higher-version-expected'));
|
|
45
|
+
// TODO include expected version in the error so the channel gets recreated with the new version
|
|
46
|
+
return 'close';
|
|
47
|
+
}
|
|
48
|
+
// If this channel has a higher version, we need to signal the other side to close
|
|
49
|
+
// and recreate the channel with the new version
|
|
50
|
+
if (packet.channelVersion < channelVersion) {
|
|
51
|
+
const newPacket = MeshSchema.MessageChannelRequest.make({
|
|
52
|
+
source: nodeName,
|
|
53
|
+
sourceId,
|
|
54
|
+
target,
|
|
55
|
+
channelName,
|
|
56
|
+
channelVersion,
|
|
57
|
+
hops: [],
|
|
58
|
+
remainingHops: packet.hops,
|
|
59
|
+
reqId: undefined,
|
|
60
|
+
});
|
|
61
|
+
span?.addEvent(`incoming packet has lower version (${packet.channelVersion}), sending request to reconnect (${newPacket.id})`);
|
|
62
|
+
yield* sendPacket(newPacket);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (channelState._tag === 'Established' && packet._tag === 'MessageChannelRequest') {
|
|
66
|
+
if (packet.sourceId === channelState.otherSourceId) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// In case the instance of the source has changed, we need to close the channel
|
|
71
|
+
// and reconnect with a new channel
|
|
72
|
+
span?.addEvent(`force-new-channel`);
|
|
73
|
+
yield* Scope.close(scope, Exit.succeed('force-new-channel'));
|
|
74
|
+
return 'close';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
switch (packet._tag) {
|
|
78
|
+
// Assumption: Each side has sent an initial request and another request as a response for an incoming request
|
|
79
|
+
case 'MessageChannelRequest': {
|
|
80
|
+
if (channelState._tag !== 'RequestSent') {
|
|
81
|
+
// We can safely ignore further incoming requests as we're already creating a channel
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (packet.reqId === channelState.reqPacketId) {
|
|
85
|
+
// Circuit-breaker: We've already sent a request so we don't need to send another one
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const newRequestPacket = MeshSchema.MessageChannelRequest.make({
|
|
89
|
+
source: nodeName,
|
|
90
|
+
sourceId,
|
|
91
|
+
target,
|
|
92
|
+
channelName,
|
|
93
|
+
channelVersion,
|
|
94
|
+
hops: [],
|
|
95
|
+
remainingHops: packet.hops,
|
|
96
|
+
reqId: packet.id,
|
|
97
|
+
});
|
|
98
|
+
span?.addEvent(`Re-sending new request (${newRequestPacket.id}) for incoming request (${packet.id})`);
|
|
99
|
+
yield* sendPacket(newRequestPacket);
|
|
100
|
+
}
|
|
101
|
+
const isWinner = nodeName > target;
|
|
102
|
+
if (isWinner) {
|
|
103
|
+
span?.addEvent(`winner side: creating message channel and sending response`);
|
|
104
|
+
const mc = new MessageChannel();
|
|
105
|
+
// We're using a message channel with acks here to make sure messages are not lost
|
|
106
|
+
// which might happen during re-connection scenarios.
|
|
107
|
+
// Also we need to eagerly start listening since we're using the channel "ourselves"
|
|
108
|
+
// for the initial ping-pong sequence.
|
|
109
|
+
const channel = yield* WebChannel.messagePortChannelWithAck({
|
|
110
|
+
port: mc.port1,
|
|
111
|
+
schema,
|
|
112
|
+
debugId: channelVersion,
|
|
113
|
+
}).pipe(Effect.andThen(WebChannel.toOpenChannel));
|
|
114
|
+
yield* respondToSender(MeshSchema.MessageChannelResponseSuccess.make({
|
|
115
|
+
reqId: packet.id,
|
|
116
|
+
target,
|
|
117
|
+
source: nodeName,
|
|
118
|
+
channelName: packet.channelName,
|
|
119
|
+
hops: [],
|
|
120
|
+
remainingHops: packet.hops,
|
|
121
|
+
port: mc.port2,
|
|
122
|
+
channelVersion,
|
|
123
|
+
}));
|
|
124
|
+
channelStateRef.current = { _tag: 'winner:ResponseSent', channel, otherSourceId: packet.sourceId };
|
|
125
|
+
// Now we wait for the other side to respond via the channel
|
|
126
|
+
yield* channel.listen.pipe(Stream.flatten(), Stream.filter(Schema.is(MeshSchema.MessageChannelPing)), Stream.take(1), Stream.runDrain);
|
|
127
|
+
yield* channel.send(MeshSchema.MessageChannelPong.make({}));
|
|
128
|
+
span?.addEvent(`winner side: established`);
|
|
129
|
+
channelStateRef.current = { _tag: 'Established', otherSourceId: packet.sourceId };
|
|
130
|
+
yield* Deferred.succeed(deferred, channel);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
span?.addEvent(`loser side: waiting for response`);
|
|
134
|
+
// Wait for `MessageChannelResponseSuccess` packet
|
|
135
|
+
channelStateRef.current = { _tag: 'loser:WaitingForResponse', otherSourceId: packet.sourceId };
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'MessageChannelResponseSuccess': {
|
|
140
|
+
if (channelState._tag !== 'loser:WaitingForResponse') {
|
|
141
|
+
return shouldNeverHappen(`Expected to find message channel response from ${target}, but was in ${channelState._tag} state`);
|
|
142
|
+
}
|
|
143
|
+
// See message-channel notes above
|
|
144
|
+
const channel = yield* WebChannel.messagePortChannelWithAck({
|
|
145
|
+
port: packet.port,
|
|
146
|
+
schema,
|
|
147
|
+
debugId: channelVersion,
|
|
148
|
+
}).pipe(Effect.andThen(WebChannel.toOpenChannel));
|
|
149
|
+
const waitForPongFiber = yield* channel.listen.pipe(Stream.flatten(), Stream.filter(Schema.is(MeshSchema.MessageChannelPong)), Stream.take(1), Stream.runDrain, Effect.fork);
|
|
150
|
+
yield* channel.send(MeshSchema.MessageChannelPing.make({}));
|
|
151
|
+
yield* waitForPongFiber;
|
|
152
|
+
span?.addEvent(`loser side: established`);
|
|
153
|
+
channelStateRef.current = { _tag: 'Established', otherSourceId: channelState.otherSourceId };
|
|
154
|
+
yield* Deferred.succeed(deferred, channel);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
return casesHandled(packet);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}).pipe(Effect.withSpan(`handleMessagePacket:${packet._tag}:${packet.source}→${packet.target}`, {
|
|
162
|
+
attributes: packetAsOtelAttributes(packet),
|
|
163
|
+
}));
|
|
164
|
+
yield* Effect.gen(function* () {
|
|
165
|
+
while (true) {
|
|
166
|
+
const packet = yield* Queue.take(incomingPacketsQueue);
|
|
167
|
+
const res = yield* processMessagePacket(packet);
|
|
168
|
+
// We want to give requests another chance to be processed
|
|
169
|
+
if (res === 'close') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}).pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped);
|
|
174
|
+
const channelState = channelStateRef.current;
|
|
175
|
+
if (channelState._tag !== 'Initial') {
|
|
176
|
+
return shouldNeverHappen(`Expected channel to be in Initial state, but was in ${channelState._tag} state`);
|
|
177
|
+
}
|
|
178
|
+
const connectionRequest = Effect.gen(function* () {
|
|
179
|
+
const packet = MeshSchema.MessageChannelRequest.make({
|
|
180
|
+
source: nodeName,
|
|
181
|
+
sourceId,
|
|
182
|
+
target,
|
|
183
|
+
channelName,
|
|
184
|
+
channelVersion,
|
|
185
|
+
hops: [],
|
|
186
|
+
reqId: undefined,
|
|
187
|
+
});
|
|
188
|
+
channelStateRef.current = { _tag: 'RequestSent', reqPacketId: packet.id };
|
|
189
|
+
// yield* Effect.log(`${nodeName}→${channelName}→${target}:connectionRequest [${channelVersion}]`)
|
|
190
|
+
const noTransferableResponse = checkTransferableConnections(packet);
|
|
191
|
+
if (noTransferableResponse !== undefined) {
|
|
192
|
+
yield* Effect.spanEvent(`No transferable connections found for ${packet.source}→${packet.target}`);
|
|
193
|
+
return yield* Effect.fail(noTransferableResponse);
|
|
194
|
+
}
|
|
195
|
+
yield* sendPacket(packet);
|
|
196
|
+
span?.addEvent(`initial connection request sent (${packet.id})`);
|
|
197
|
+
});
|
|
198
|
+
yield* connectionRequest;
|
|
199
|
+
const channel = yield* deferred;
|
|
200
|
+
return channel;
|
|
201
|
+
}).pipe(Effect.withSpanScoped(`makeMessageChannel:${channelVersion}`));
|
|
202
|
+
//# sourceMappingURL=message-channel-internal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-channel-internal.js","sourceRoot":"","sources":["../../src/channel/message-channel-internal.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export declare const makeMessageChannel: ({
|
|
19
|
-
|
|
1
|
+
import { Deferred, Effect, Scope, WebChannel } from '@livestore/utils/effect';
|
|
2
|
+
import type { MakeMessageChannelArgs } from './message-channel-internal.js';
|
|
3
|
+
/**
|
|
4
|
+
* Behaviour:
|
|
5
|
+
* - Waits until there is an initial connection
|
|
6
|
+
* - Automatically reconnects on disconnect
|
|
7
|
+
*
|
|
8
|
+
* Implementation notes:
|
|
9
|
+
* - We've split up the functionality into a wrapper channel and an internal channel.
|
|
10
|
+
* - The wrapper channel is responsible for:
|
|
11
|
+
* - Forwarding send/listen messages to the internal channel (via a queue)
|
|
12
|
+
* - Establishing the initial channel and reconnecting on disconnect
|
|
13
|
+
* - Listening for new connections as a hint to reconnect if not already connected
|
|
14
|
+
* - The wrapper channel maintains a connection counter which is used as the channel version
|
|
15
|
+
*
|
|
16
|
+
* If needed we can also implement further functionality (like heartbeat) in this wrapper channel.
|
|
17
|
+
*/
|
|
18
|
+
export declare const makeMessageChannel: ({ schema, newConnectionAvailablePubSub, channelName, checkTransferableConnections, nodeName, incomingPacketsQueue, target, sendPacket, }: MakeMessageChannelArgs) => Effect.Effect<{
|
|
19
|
+
webChannel: WebChannel.WebChannel<any, any>;
|
|
20
|
+
initialConnectionDeferred: Deferred.Deferred<void, never>;
|
|
21
|
+
}, never, Scope.Scope>;
|
|
20
22
|
//# sourceMappingURL=message-channel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-channel.d.ts","sourceRoot":"","sources":["../../src/channel/message-channel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"message-channel.d.ts","sourceRoot":"","sources":["../../src/channel/message-channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EACR,MAAM,EAKN,KAAK,EAGL,UAAU,EACX,MAAM,yBAAyB,CAAA;AAIhC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAG3E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,6IAS5B,sBAAsB;gBA+KS,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;;sBAI9D,CAAA"}
|
|
@@ -1,175 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import { Cause, Deferred, Effect, Either, Exit, Queue, Schema, Scope, Stream, TQueue, WebChannel, } from '@livestore/utils/effect';
|
|
2
|
+
import { nanoid } from '@livestore/utils/nanoid';
|
|
3
|
+
import { WebmeshSchema } from '../mod.js';
|
|
4
|
+
import { makeMessageChannelInternal } from './message-channel-internal.js';
|
|
5
|
+
/**
|
|
6
|
+
* Behaviour:
|
|
7
|
+
* - Waits until there is an initial connection
|
|
8
|
+
* - Automatically reconnects on disconnect
|
|
9
|
+
*
|
|
10
|
+
* Implementation notes:
|
|
11
|
+
* - We've split up the functionality into a wrapper channel and an internal channel.
|
|
12
|
+
* - The wrapper channel is responsible for:
|
|
13
|
+
* - Forwarding send/listen messages to the internal channel (via a queue)
|
|
14
|
+
* - Establishing the initial channel and reconnecting on disconnect
|
|
15
|
+
* - Listening for new connections as a hint to reconnect if not already connected
|
|
16
|
+
* - The wrapper channel maintains a connection counter which is used as the channel version
|
|
17
|
+
*
|
|
18
|
+
* If needed we can also implement further functionality (like heartbeat) in this wrapper channel.
|
|
19
|
+
*/
|
|
20
|
+
export const makeMessageChannel = ({ schema, newConnectionAvailablePubSub, channelName, checkTransferableConnections, nodeName, incomingPacketsQueue, target, sendPacket, }) => Effect.scopeWithCloseable((scope) => Effect.gen(function* () {
|
|
21
|
+
/** Only used to identify whether a source is the same instance to know when to reconnect */
|
|
22
|
+
const sourceId = nanoid();
|
|
23
|
+
const listenQueue = yield* Queue.unbounded();
|
|
24
|
+
const sendQueue = yield* TQueue.unbounded();
|
|
25
|
+
const initialConnectionDeferred = yield* Deferred.make();
|
|
26
|
+
const debugInfo = {
|
|
27
|
+
pendingSends: 0,
|
|
28
|
+
totalSends: 0,
|
|
29
|
+
connectCounter: 1,
|
|
30
|
+
isConnected: false,
|
|
31
|
+
};
|
|
32
|
+
// #region reconnect-loop
|
|
33
|
+
yield* Effect.gen(function* () {
|
|
34
|
+
const resultDeferred = yield* Deferred.make();
|
|
35
|
+
while (true) {
|
|
36
|
+
debugInfo.connectCounter++;
|
|
37
|
+
const channelVersion = debugInfo.connectCounter;
|
|
38
|
+
yield* Effect.spanEvent(`Connecting#${channelVersion}`);
|
|
39
|
+
const makeMessageChannelScope = yield* Scope.make();
|
|
40
|
+
// Attach the new scope to the parent scope
|
|
41
|
+
yield* Effect.addFinalizer((ex) => Scope.close(makeMessageChannelScope, ex));
|
|
42
|
+
/**
|
|
43
|
+
* Expected concurrency behaviour:
|
|
44
|
+
* - We're concurrently running the connection setup and the waitForNewConnectionFiber
|
|
45
|
+
* - Happy path:
|
|
46
|
+
* - The connection setup succeeds and we can interrupt the waitForNewConnectionFiber
|
|
47
|
+
* - Tricky paths:
|
|
48
|
+
* - While a connection is still being setup, we want to re-try when there is a new connection
|
|
49
|
+
* - If the connection setup returns a `MessageChannelResponseNoTransferables` error,
|
|
50
|
+
* we want to wait for a new connection and then re-try
|
|
51
|
+
* - Further notes:
|
|
52
|
+
* - If the parent scope closes, we want to also interrupt both the connection setup and the waitForNewConnectionFiber
|
|
53
|
+
* - We're creating a separate scope for each connection attempt, which
|
|
54
|
+
* - we'll use to fork the message channel in which allows us to interrupt it later
|
|
55
|
+
* - We need to make sure that "interruption" isn't "bubbling out"
|
|
56
|
+
*/
|
|
57
|
+
const waitForNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(Stream.tap((connectionName) => Effect.spanEvent(`new-conn:${connectionName}`)), Stream.take(1), Stream.runDrain, Effect.as('new-connection'), Effect.fork);
|
|
58
|
+
const makeChannel = makeMessageChannelInternal({
|
|
59
|
+
nodeName,
|
|
60
|
+
sourceId,
|
|
61
|
+
incomingPacketsQueue,
|
|
62
|
+
target,
|
|
63
|
+
checkTransferableConnections,
|
|
64
|
+
channelName,
|
|
65
|
+
schema,
|
|
66
|
+
channelVersion,
|
|
67
|
+
newConnectionAvailablePubSub,
|
|
68
|
+
sendPacket,
|
|
69
|
+
scope: makeMessageChannelScope,
|
|
70
|
+
}).pipe(Scope.extend(makeMessageChannelScope), Effect.forkIn(makeMessageChannelScope));
|
|
71
|
+
const res = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect));
|
|
72
|
+
if (res === 'new-connection') {
|
|
73
|
+
yield* Scope.close(makeMessageChannelScope, Exit.fail('new-connection'));
|
|
74
|
+
// We'll try again
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const result = yield* res.pipe(Effect.exit);
|
|
78
|
+
if (result._tag === 'Failure') {
|
|
79
|
+
yield* Scope.close(makeMessageChannelScope, result);
|
|
80
|
+
if (Cause.isFailType(result.cause) &&
|
|
81
|
+
Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(result.cause.error)) {
|
|
82
|
+
yield* waitForNewConnectionFiber.pipe(Effect.exit);
|
|
32
83
|
}
|
|
33
|
-
const thisSideAlsoResponded = channelState._tag === 'ResponseSent';
|
|
34
|
-
const usePortFromThisSide = thisSideAlsoResponded && nodeName > target;
|
|
35
|
-
yield* Effect.annotateCurrentSpan({ usePortFromThisSide });
|
|
36
|
-
const winnerPort = usePortFromThisSide ? channelState.locallyCreatedPort : packet.port;
|
|
37
|
-
yield* Deferred.succeed(channelState.deferred, winnerPort);
|
|
38
|
-
return;
|
|
39
84
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
yield* Deferred.fail(channelState.deferred, packet);
|
|
44
|
-
channelStateRef.current = yield* makeInitialState;
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
case 'MessageChannelRequest': {
|
|
48
|
-
const mc = new MessageChannel();
|
|
49
|
-
const shouldReconnect = channelState._tag === 'Established';
|
|
50
|
-
const deferred = channelState._tag === 'Established'
|
|
51
|
-
? yield* Deferred.make()
|
|
52
|
-
: channelState.deferred;
|
|
53
|
-
channelStateRef.current = { _tag: 'ResponseSent', locallyCreatedPort: mc.port1, deferred };
|
|
54
|
-
yield* respondToSender(MeshSchema.MessageChannelResponseSuccess.make({
|
|
55
|
-
reqId: packet.id,
|
|
56
|
-
target,
|
|
57
|
-
source: nodeName,
|
|
58
|
-
channelName: packet.channelName,
|
|
59
|
-
hops: [],
|
|
60
|
-
remainingHops: packet.hops,
|
|
61
|
-
port: mc.port2,
|
|
62
|
-
}));
|
|
63
|
-
// If there's an established channel, we use the new request as a signal
|
|
64
|
-
// to drop the old channel and use the new one
|
|
65
|
-
if (shouldReconnect) {
|
|
66
|
-
yield* reconnect;
|
|
67
|
-
}
|
|
85
|
+
else {
|
|
86
|
+
const channel = result.value;
|
|
87
|
+
yield* Deferred.succeed(resultDeferred, { channel, makeMessageChannelScope, channelVersion });
|
|
68
88
|
break;
|
|
69
89
|
}
|
|
70
|
-
default: {
|
|
71
|
-
return casesHandled(packet);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}).pipe(Effect.withSpan(`handleMessagePacket:${packet._tag}:${packet.source}→${packet.target}`, {
|
|
75
|
-
attributes: packetAsOtelAttributes(packet),
|
|
76
|
-
}));
|
|
77
|
-
yield* Stream.fromQueue(queue).pipe(Stream.tap(processMessagePacket), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkScoped);
|
|
78
|
-
const channelFromPort = (port) => Effect.gen(function* () {
|
|
79
|
-
channelStateRef.current = { _tag: 'Established' };
|
|
80
|
-
// NOTE to support re-connects we need to ack each message
|
|
81
|
-
const channel = yield* WebChannel.messagePortChannelWithAck({ port, schema });
|
|
82
|
-
return channel;
|
|
83
|
-
});
|
|
84
|
-
const channelState = channelStateRef.current;
|
|
85
|
-
if (channelState._tag === 'Initial' || channelState._tag === 'RequestSent') {
|
|
86
|
-
// Important to make a new deferred here as the old one might have been used already
|
|
87
|
-
// TODO model this better
|
|
88
|
-
const deferred = channelState._tag === 'RequestSent'
|
|
89
|
-
? yield* Deferred.make()
|
|
90
|
-
: channelState.deferred;
|
|
91
|
-
channelStateRef.current = { _tag: 'RequestSent', deferred };
|
|
92
|
-
const connectionRequest = Effect.gen(function* () {
|
|
93
|
-
const packet = MeshSchema.MessageChannelRequest.make({ source: nodeName, target, channelName, hops: [] });
|
|
94
|
-
const noTransferableResponse = checkTransferableConnections(packet);
|
|
95
|
-
if (noTransferableResponse !== undefined) {
|
|
96
|
-
yield* Effect.spanEvent(`No transferable connections found for ${packet.source}→${packet.target}`);
|
|
97
|
-
yield* Deferred.fail(deferred, noTransferableResponse);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
yield* sendPacket(packet);
|
|
101
|
-
});
|
|
102
|
-
yield* connectionRequest;
|
|
103
|
-
const retryOnNewConnectionFiber = yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(Stream.tap(() => Effect.spanEvent(`RetryOnNewConnection`)), Stream.tap(() => connectionRequest), Stream.runDrain, Effect.forkScoped);
|
|
104
|
-
const portResult = yield* deferred.pipe(Effect.either);
|
|
105
|
-
yield* Fiber.interrupt(retryOnNewConnectionFiber);
|
|
106
|
-
if (portResult._tag === 'Right') {
|
|
107
|
-
return yield* channelFromPort(portResult.right);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
// We'll keep retrying with a new connection
|
|
111
|
-
yield* Stream.fromPubSub(newConnectionAvailablePubSub).pipe(Stream.take(1), Stream.runDrain);
|
|
112
|
-
yield* reconnect;
|
|
113
|
-
return yield* Effect.interrupt;
|
|
114
90
|
}
|
|
115
91
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
92
|
+
// Now we wait until the first channel is established
|
|
93
|
+
const { channel, makeMessageChannelScope, channelVersion } = yield* resultDeferred;
|
|
94
|
+
yield* Effect.spanEvent(`Connected#${channelVersion}`);
|
|
95
|
+
debugInfo.isConnected = true;
|
|
96
|
+
yield* Deferred.succeed(initialConnectionDeferred, void 0);
|
|
97
|
+
// We'll now forward all incoming messages to the listen queue
|
|
98
|
+
yield* channel.listen.pipe(Stream.flatten(),
|
|
99
|
+
// Stream.tap((msg) => Effect.log(`${target}→${channelName}→${nodeName}:message:${msg.message}`)),
|
|
100
|
+
Stream.tapChunk((chunk) => Queue.offerAll(listenQueue, chunk)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkIn(makeMessageChannelScope));
|
|
101
|
+
yield* Effect.gen(function* () {
|
|
102
|
+
while (true) {
|
|
103
|
+
const [msg, deferred] = yield* TQueue.peek(sendQueue);
|
|
104
|
+
// NOTE we don't need an explicit retry flow here since in case of the channel being closed,
|
|
105
|
+
// the send will never succeed. Meanwhile the send-loop fiber will be interrupted and
|
|
106
|
+
// given we only peeked at the queue, the message to send is still there.
|
|
107
|
+
yield* channel.send(msg);
|
|
108
|
+
yield* Deferred.succeed(deferred, void 0);
|
|
109
|
+
yield* TQueue.take(sendQueue); // Remove the message from the queue
|
|
125
110
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
yield* Effect.addFinalizer(() => Effect.spanEvent(`Disconnected#${connectCount}`));
|
|
136
|
-
const internalChannel = yield* makeMessageChannelInternal;
|
|
137
|
-
yield* SubscriptionRef.set(internalChannelSref, internalChannel);
|
|
138
|
-
yield* Effect.spanEvent(`Connected#${connectCount}`);
|
|
139
|
-
yield* internalChannel.listen.pipe(Stream.flatten(), Stream.tap((msg) => Queue.offer(listenQueue, msg)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkScoped);
|
|
140
|
-
yield* Effect.never;
|
|
141
|
-
}).pipe(Effect.scoped);
|
|
142
|
-
const fiberHandle = yield* FiberHandle.make();
|
|
143
|
-
const runConnect = Effect.gen(function* () {
|
|
144
|
-
// Cleanly shutdown the previous connection first
|
|
145
|
-
// Otherwise the old and new connection will "overlap"
|
|
146
|
-
yield* FiberHandle.clear(fiberHandle);
|
|
147
|
-
yield* FiberHandle.run(fiberHandle, connect);
|
|
148
|
-
});
|
|
149
|
-
yield* runConnect;
|
|
150
|
-
// Then listen for reconnects
|
|
151
|
-
yield* Stream.fromQueue(reconnectTriggerQueue).pipe(Stream.tap(() => runConnect), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkScoped);
|
|
152
|
-
// Wait for the initial connection to be established or for an error to occur
|
|
153
|
-
yield* Effect.raceFirst(SubscriptionRef.waitUntil(internalChannelSref, (channel) => channel !== false), FiberHandle.join(fiberHandle));
|
|
111
|
+
}).pipe(Effect.forkIn(makeMessageChannelScope));
|
|
112
|
+
// Wait until the channel is closed and then try to reconnect
|
|
113
|
+
yield* channel.closedDeferred;
|
|
114
|
+
yield* Scope.close(makeMessageChannelScope, Exit.succeed('channel-closed'));
|
|
115
|
+
yield* Effect.spanEvent(`Disconnected#${channelVersion}`);
|
|
116
|
+
debugInfo.isConnected = false;
|
|
117
|
+
}).pipe(Effect.scoped, // Additionally scoping here to clean up finalizers after each loop run
|
|
118
|
+
Effect.forever, Effect.tapCauseLogPretty, Effect.forkScoped);
|
|
119
|
+
// #endregion reconnect-loop
|
|
154
120
|
const parentSpan = yield* Effect.currentSpan.pipe(Effect.orDie);
|
|
155
121
|
const send = (message) => Effect.gen(function* () {
|
|
156
|
-
const sendFiberHandle = yield* FiberHandle.make();
|
|
157
122
|
const sentDeferred = yield* Deferred.make();
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
yield* channel.send(message);
|
|
162
|
-
yield* Deferred.succeed(sentDeferred, void 0);
|
|
163
|
-
});
|
|
164
|
-
yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(100)), Effect.orDie);
|
|
165
|
-
}).pipe(Effect.tapErrorCause(Effect.logError));
|
|
166
|
-
const rerunOnNewChannelFiber = yield* internalChannelSref.changes.pipe(Stream.filter((_) => _ === false), Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)), Stream.runDrain, Effect.fork);
|
|
167
|
-
yield* FiberHandle.run(sendFiberHandle, trySend);
|
|
123
|
+
debugInfo.pendingSends++;
|
|
124
|
+
debugInfo.totalSends++;
|
|
125
|
+
yield* TQueue.offer(sendQueue, [message, sentDeferred]);
|
|
168
126
|
yield* sentDeferred;
|
|
169
|
-
|
|
127
|
+
debugInfo.pendingSends--;
|
|
170
128
|
}).pipe(Effect.scoped, Effect.withParentSpan(parentSpan));
|
|
171
|
-
const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right));
|
|
172
|
-
const closedDeferred = yield* Deferred.make();
|
|
129
|
+
const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right));
|
|
130
|
+
const closedDeferred = yield* Deferred.make().pipe(Effect.acquireRelease(Deferred.done(Exit.void)));
|
|
173
131
|
const webChannel = {
|
|
174
132
|
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
175
133
|
send,
|
|
@@ -177,7 +135,12 @@ export const makeMessageChannel = ({ nodeName, queue, newConnectionAvailablePubS
|
|
|
177
135
|
closedDeferred,
|
|
178
136
|
supportsTransferables: true,
|
|
179
137
|
schema,
|
|
138
|
+
debugInfo,
|
|
139
|
+
shutdown: Scope.close(scope, Exit.succeed('shutdown')),
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
webChannel: webChannel,
|
|
143
|
+
initialConnectionDeferred,
|
|
180
144
|
};
|
|
181
|
-
|
|
182
|
-
}).pipe(Effect.withSpanScoped('makeMessageChannel'));
|
|
145
|
+
}));
|
|
183
146
|
//# sourceMappingURL=message-channel.js.map
|