@livestore/webmesh 0.3.0-dev.11 → 0.3.0-dev.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +20 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel copy.d.ts +9 -0
  4. package/dist/channel/message-channel copy.d.ts.map +1 -0
  5. package/dist/channel/message-channel copy.js +137 -0
  6. package/dist/channel/message-channel copy.js.map +1 -0
  7. package/dist/channel/message-channel-internal copy.d.ts +42 -0
  8. package/dist/channel/message-channel-internal copy.d.ts.map +1 -0
  9. package/dist/channel/message-channel-internal copy.js +239 -0
  10. package/dist/channel/message-channel-internal copy.js.map +1 -0
  11. package/dist/channel/message-channel-internal.d.ts +26 -0
  12. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  13. package/dist/channel/message-channel-internal.js +217 -0
  14. package/dist/channel/message-channel-internal.js.map +1 -0
  15. package/dist/channel/message-channel.d.ts +21 -19
  16. package/dist/channel/message-channel.d.ts.map +1 -1
  17. package/dist/channel/message-channel.js +132 -162
  18. package/dist/channel/message-channel.js.map +1 -1
  19. package/dist/channel/proxy-channel.d.ts +2 -2
  20. package/dist/channel/proxy-channel.d.ts.map +1 -1
  21. package/dist/channel/proxy-channel.js +7 -5
  22. package/dist/channel/proxy-channel.js.map +1 -1
  23. package/dist/common.d.ts +8 -4
  24. package/dist/common.d.ts.map +1 -1
  25. package/dist/common.js +2 -1
  26. package/dist/common.js.map +1 -1
  27. package/dist/mesh-schema.d.ts +23 -1
  28. package/dist/mesh-schema.d.ts.map +1 -1
  29. package/dist/mesh-schema.js +21 -2
  30. package/dist/mesh-schema.js.map +1 -1
  31. package/dist/node.d.ts +12 -1
  32. package/dist/node.d.ts.map +1 -1
  33. package/dist/node.js +40 -9
  34. package/dist/node.js.map +1 -1
  35. package/dist/node.test.d.ts +1 -1
  36. package/dist/node.test.d.ts.map +1 -1
  37. package/dist/node.test.js +315 -149
  38. package/dist/node.test.js.map +1 -1
  39. package/dist/websocket-connection.d.ts +1 -2
  40. package/dist/websocket-connection.d.ts.map +1 -1
  41. package/dist/websocket-connection.js +5 -4
  42. package/dist/websocket-connection.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/channel/message-channel-internal.ts +356 -0
  45. package/src/channel/message-channel.ts +190 -310
  46. package/src/channel/proxy-channel.ts +238 -230
  47. package/src/common.ts +3 -1
  48. package/src/mesh-schema.ts +20 -2
  49. package/src/node.test.ts +448 -179
  50. package/src/node.ts +70 -12
  51. package/src/websocket-connection.ts +83 -79
@@ -0,0 +1,217 @@
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 span = {
17
+ // addEvent: (...msg: any[]) => console.log(`${nodeName}→${channelName}→${target}[${channelVersion}]`, ...msg),
18
+ // }
19
+ const schema = {
20
+ send: Schema.Union(schema_.send, MeshSchema.MessageChannelPing, MeshSchema.MessageChannelPong),
21
+ listen: Schema.Union(schema_.listen, MeshSchema.MessageChannelPing, MeshSchema.MessageChannelPong),
22
+ };
23
+ const channelStateRef = {
24
+ current: { _tag: 'Initial' },
25
+ };
26
+ const processMessagePacket = ({ packet, respondToSender }) => Effect.gen(function* () {
27
+ const channelState = channelStateRef.current;
28
+ span?.addEvent(`process:${packet._tag}`, {
29
+ channelState: channelState._tag,
30
+ packetId: packet.id,
31
+ packetReqId: packet.reqId,
32
+ packetChannelVersion: Predicate.hasProperty('channelVersion')(packet) ? packet.channelVersion : undefined,
33
+ });
34
+ // const reqIdStr =
35
+ // Predicate.hasProperty('reqId')(packet) && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''
36
+ // yield* Effect.log(
37
+ // `${nodeName}→${channelName}→${target}[${channelVersion}]:process packet ${packet._tag} [${packet.id}${reqIdStr}], channel state: ${channelState._tag}`,
38
+ // )
39
+ if (channelState._tag === 'Initial')
40
+ return shouldNeverHappen();
41
+ if (packet._tag === 'MessageChannelResponseNoTransferables') {
42
+ yield* Deferred.fail(deferred, packet);
43
+ return 'close';
44
+ }
45
+ // If the other side has a higher version, we need to close this channel and
46
+ // recreate it with the new version
47
+ if (packet.channelVersion > channelVersion) {
48
+ span?.addEvent(`incoming packet has higher version (${packet.channelVersion}), closing channel`);
49
+ yield* Scope.close(scope, Exit.succeed('higher-version-expected'));
50
+ // TODO include expected version in the error so the channel gets recreated with the new version
51
+ return 'close';
52
+ }
53
+ // If this channel has a higher version, we need to signal the other side to close
54
+ // and recreate the channel with the new version
55
+ if (packet.channelVersion < channelVersion) {
56
+ const newPacket = MeshSchema.MessageChannelRequest.make({
57
+ source: nodeName,
58
+ sourceId,
59
+ target,
60
+ channelName,
61
+ channelVersion,
62
+ hops: [],
63
+ remainingHops: packet.hops,
64
+ reqId: undefined,
65
+ });
66
+ span?.addEvent(`incoming packet has lower version (${packet.channelVersion}), sending request to reconnect (${newPacket.id})`);
67
+ yield* sendPacket(newPacket);
68
+ return;
69
+ }
70
+ if (channelState._tag === 'Established' && packet._tag === 'MessageChannelRequest') {
71
+ if (packet.sourceId === channelState.otherSourceId) {
72
+ return;
73
+ }
74
+ else {
75
+ // In case the instance of the source has changed, we need to close the channel
76
+ // and reconnect with a new channel
77
+ span?.addEvent(`force-new-channel`);
78
+ yield* Scope.close(scope, Exit.succeed('force-new-channel'));
79
+ return 'close';
80
+ }
81
+ }
82
+ switch (packet._tag) {
83
+ // Assumption: Each side has sent an initial request and another request as a response for an incoming request
84
+ case 'MessageChannelRequest': {
85
+ if (channelState._tag !== 'RequestSent') {
86
+ // We can safely ignore further incoming requests as we're already creating a channel
87
+ return;
88
+ }
89
+ if (packet.reqId === channelState.reqPacketId) {
90
+ // Circuit-breaker: We've already sent a request so we don't need to send another one
91
+ }
92
+ else {
93
+ const newRequestPacket = MeshSchema.MessageChannelRequest.make({
94
+ source: nodeName,
95
+ sourceId,
96
+ target,
97
+ channelName,
98
+ channelVersion,
99
+ hops: [],
100
+ remainingHops: packet.hops,
101
+ reqId: packet.id,
102
+ });
103
+ span?.addEvent(`Re-sending new request (${newRequestPacket.id}) for incoming request (${packet.id})`);
104
+ yield* sendPacket(newRequestPacket);
105
+ }
106
+ const isWinner = nodeName > target;
107
+ if (isWinner) {
108
+ span?.addEvent(`winner side: creating message channel and sending response`);
109
+ const mc = new MessageChannel();
110
+ // We're using a message channel with acks here to make sure messages are not lost
111
+ // which might happen during re-connection scenarios.
112
+ // Also we need to eagerly start listening since we're using the channel "ourselves"
113
+ // for the initial ping-pong sequence.
114
+ const channel = yield* WebChannel.messagePortChannelWithAck({
115
+ port: mc.port1,
116
+ schema,
117
+ debugId: channelVersion,
118
+ }).pipe(Effect.andThen(WebChannel.toOpenChannel));
119
+ yield* respondToSender(MeshSchema.MessageChannelResponseSuccess.make({
120
+ reqId: packet.id,
121
+ target,
122
+ source: nodeName,
123
+ channelName: packet.channelName,
124
+ hops: [],
125
+ remainingHops: packet.hops,
126
+ port: mc.port2,
127
+ channelVersion,
128
+ }));
129
+ channelStateRef.current = { _tag: 'winner:ResponseSent', channel, otherSourceId: packet.sourceId };
130
+ // span?.addEvent(`winner side: waiting for ping`)
131
+ // Now we wait for the other side to respond via the channel
132
+ yield* channel.listen.pipe(Stream.flatten(), Stream.filter(Schema.is(MeshSchema.MessageChannelPing)), Stream.take(1), Stream.runDrain);
133
+ // span?.addEvent(`winner side: sending pong`)
134
+ yield* channel.send(MeshSchema.MessageChannelPong.make({}));
135
+ span?.addEvent(`winner side: established`);
136
+ channelStateRef.current = { _tag: 'Established', otherSourceId: packet.sourceId };
137
+ yield* Deferred.succeed(deferred, channel);
138
+ }
139
+ else {
140
+ span?.addEvent(`loser side: waiting for response`);
141
+ // Wait for `MessageChannelResponseSuccess` packet
142
+ channelStateRef.current = { _tag: 'loser:WaitingForResponse', otherSourceId: packet.sourceId };
143
+ }
144
+ break;
145
+ }
146
+ case 'MessageChannelResponseSuccess': {
147
+ if (channelState._tag !== 'loser:WaitingForResponse') {
148
+ return shouldNeverHappen(`Expected to find message channel response from ${target}, but was in ${channelState._tag} state`);
149
+ }
150
+ // See message-channel notes above
151
+ const channel = yield* WebChannel.messagePortChannelWithAck({
152
+ port: packet.port,
153
+ schema,
154
+ debugId: channelVersion,
155
+ }).pipe(Effect.andThen(WebChannel.toOpenChannel));
156
+ const waitForPongFiber = yield* channel.listen.pipe(Stream.flatten(), Stream.filter(Schema.is(MeshSchema.MessageChannelPong)), Stream.take(1), Stream.runDrain, Effect.fork);
157
+ // span?.addEvent(`loser side: sending ping`)
158
+ // There seems to be some scenario where the initial ping message is lost.
159
+ // As a workaround until we find the root cause, we're retrying the ping a few times.
160
+ // TODO write a test that reproduces this issue and fix the root cause ()
161
+ // https://github.com/livestorejs/livestore/issues/262
162
+ yield* channel
163
+ .send(MeshSchema.MessageChannelPing.make({}))
164
+ .pipe(Effect.timeout(10), Effect.retry({ times: 2 }));
165
+ // span?.addEvent(`loser side: waiting for pong`)
166
+ yield* waitForPongFiber;
167
+ span?.addEvent(`loser side: established`);
168
+ channelStateRef.current = { _tag: 'Established', otherSourceId: channelState.otherSourceId };
169
+ yield* Deferred.succeed(deferred, channel);
170
+ return;
171
+ }
172
+ default: {
173
+ return casesHandled(packet);
174
+ }
175
+ }
176
+ }).pipe(Effect.withSpan(`handleMessagePacket:${packet._tag}:${packet.source}→${packet.target}`, {
177
+ attributes: packetAsOtelAttributes(packet),
178
+ }));
179
+ yield* Effect.gen(function* () {
180
+ while (true) {
181
+ const packet = yield* Queue.take(incomingPacketsQueue);
182
+ const res = yield* processMessagePacket(packet);
183
+ // We want to give requests another chance to be processed
184
+ if (res === 'close') {
185
+ return;
186
+ }
187
+ }
188
+ }).pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped);
189
+ const channelState = channelStateRef.current;
190
+ if (channelState._tag !== 'Initial') {
191
+ return shouldNeverHappen(`Expected channel to be in Initial state, but was in ${channelState._tag} state`);
192
+ }
193
+ const connectionRequest = Effect.gen(function* () {
194
+ const packet = MeshSchema.MessageChannelRequest.make({
195
+ source: nodeName,
196
+ sourceId,
197
+ target,
198
+ channelName,
199
+ channelVersion,
200
+ hops: [],
201
+ reqId: undefined,
202
+ });
203
+ channelStateRef.current = { _tag: 'RequestSent', reqPacketId: packet.id };
204
+ // yield* Effect.log(`${nodeName}→${channelName}→${target}:connectionRequest [${channelVersion}]`)
205
+ const noTransferableResponse = checkTransferableConnections(packet);
206
+ if (noTransferableResponse !== undefined) {
207
+ yield* Effect.spanEvent(`No transferable connections found for ${packet.source}→${packet.target}`);
208
+ return yield* Effect.fail(noTransferableResponse);
209
+ }
210
+ yield* sendPacket(packet);
211
+ span?.addEvent(`initial connection request sent (${packet.id})`);
212
+ });
213
+ yield* connectionRequest;
214
+ const channel = yield* deferred;
215
+ return channel;
216
+ }).pipe(Effect.withSpanScoped(`makeMessageChannel:${channelVersion}`));
217
+ //# 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 type { PubSub, Schema, Scope } from '@livestore/utils/effect';
2
- import { Effect, Queue, 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
- interface MakeMessageChannelArgs {
6
- nodeName: MeshNodeName;
7
- queue: Queue.Queue<MessageQueueItem>;
8
- newConnectionAvailablePubSub: PubSub.PubSub<MeshNodeName>;
9
- channelName: ChannelName;
10
- target: MeshNodeName;
11
- sendPacket: (packet: typeof MeshSchema.MessageChannelPacket.Type) => Effect.Effect<void>;
12
- checkTransferableConnections: (packet: typeof MeshSchema.MessageChannelPacket.Type) => typeof MeshSchema.MessageChannelResponseNoTransferables.Type | undefined;
13
- schema: {
14
- send: Schema.Schema<any, any>;
15
- listen: Schema.Schema<any, any>;
16
- };
17
- }
18
- export declare const makeMessageChannel: ({ nodeName, queue, newConnectionAvailablePubSub, target, checkTransferableConnections, channelName, schema, sendPacket, }: MakeMessageChannelArgs) => Effect.Effect<WebChannel.WebChannel<any, any, never>, never, Scope.Scope>;
19
- export {};
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":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpE,OAAO,EAEL,MAAM,EAIN,KAAK,EAIL,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,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,YAAY,CAAA;IACtB,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;IACpC,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;QACN,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;KAChC,CAAA;CACF;AAED,eAAO,MAAM,kBAAkB,8HAS5B,sBAAsB,8EAsT6B,CAAA"}
1
+ {"version":3,"file":"message-channel.d.ts","sourceRoot":"","sources":["../../src/channel/message-channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EACR,MAAM,EAMN,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;gBAyLS,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;;sBAI9D,CAAA"}
@@ -1,175 +1,140 @@
1
- import { casesHandled, shouldNeverHappen } from '@livestore/utils';
2
- import { Deferred, Effect, Either, Fiber, FiberHandle, Queue, Schedule, Stream, SubscriptionRef, WebChannel, } from '@livestore/utils/effect';
3
- import { packetAsOtelAttributes } from '../common.js';
4
- import * as MeshSchema from '../mesh-schema.js';
5
- export const makeMessageChannel = ({ nodeName, queue, newConnectionAvailablePubSub, target, checkTransferableConnections, channelName, schema, sendPacket, }) => Effect.gen(function* () {
6
- const reconnectTriggerQueue = yield* Queue.unbounded();
7
- const reconnect = Queue.offer(reconnectTriggerQueue, void 0);
8
- const makeInitialState = Effect.gen(function* () {
9
- const deferred = yield* Deferred.make();
10
- return { _tag: 'Initial', deferred };
11
- });
12
- const channelStateRef = { current: yield* makeInitialState };
13
- const makeMessageChannelInternal = Effect.gen(function* () {
14
- const processMessagePacket = ({ packet, respondToSender }) => Effect.gen(function* () {
15
- const channelState = channelStateRef.current;
16
- // yield* Effect.log(`${nodeName}:processing packet ${packet._tag}, channel state: ${channelState._tag}`)
17
- switch (packet._tag) {
18
- // Since there can be concurrent MessageChannel responses from both sides,
19
- // we need to decide which side's port we want to use and which side's port we want to ignore.
20
- // This is only relevant in the case where both sides already sent their responses.
21
- // In this case we're using the target name as a "tie breaker" to decide which side's port to use.
22
- // We do this by sorting the target names lexicographically and use the first one as the winner.
23
- case 'MessageChannelResponseSuccess': {
24
- if (channelState._tag === 'Initial') {
25
- return shouldNeverHappen(`Expected to find message channel request from ${target}, but was in ${channelState._tag} state`);
26
- }
27
- if (channelState._tag === 'Established') {
28
- const deferred = yield* Deferred.make();
29
- channelStateRef.current = { _tag: 'RequestSent', deferred };
30
- yield* reconnect;
31
- return;
1
+ import { Cause, Deferred, Effect, Either, Exit, Option, 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: 0,
30
+ isConnected: false,
31
+ innerChannelRef: { current: undefined },
32
+ };
33
+ // #region reconnect-loop
34
+ yield* Effect.gen(function* () {
35
+ const resultDeferred = yield* Deferred.make();
36
+ while (true) {
37
+ debugInfo.connectCounter++;
38
+ const channelVersion = debugInfo.connectCounter;
39
+ yield* Effect.spanEvent(`Connecting#${channelVersion}`);
40
+ const makeMessageChannelScope = yield* Scope.make();
41
+ // Attach the new scope to the parent scope
42
+ yield* Effect.addFinalizer((ex) => Scope.close(makeMessageChannelScope, ex));
43
+ /**
44
+ * Expected concurrency behaviour:
45
+ * - We're concurrently running the connection setup and the waitForNewConnectionFiber
46
+ * - Happy path:
47
+ * - The connection setup succeeds and we can interrupt the waitForNewConnectionFiber
48
+ * - Tricky paths:
49
+ * - While a connection is still being setup, we want to re-try when there is a new connection
50
+ * - If the connection setup returns a `MessageChannelResponseNoTransferables` error,
51
+ * we want to wait for a new connection and then re-try
52
+ * - Further notes:
53
+ * - If the parent scope closes, we want to also interrupt both the connection setup and the waitForNewConnectionFiber
54
+ * - We're creating a separate scope for each connection attempt, which
55
+ * - we'll use to fork the message channel in which allows us to interrupt it later
56
+ * - We need to make sure that "interruption" isn't "bubbling out"
57
+ */
58
+ 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);
59
+ const makeChannel = makeMessageChannelInternal({
60
+ nodeName,
61
+ sourceId,
62
+ incomingPacketsQueue,
63
+ target,
64
+ checkTransferableConnections,
65
+ channelName,
66
+ schema,
67
+ channelVersion,
68
+ newConnectionAvailablePubSub,
69
+ sendPacket,
70
+ scope: makeMessageChannelScope,
71
+ }).pipe(Scope.extend(makeMessageChannelScope), Effect.forkIn(makeMessageChannelScope),
72
+ // Given we only call `Effect.exit` later when joining the fiber,
73
+ // we don't want Effect to produce a "unhandled error" log message
74
+ Effect.withUnhandledErrorLogLevel(Option.none()));
75
+ const raceResult = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect));
76
+ if (raceResult === 'new-connection') {
77
+ yield* Scope.close(makeMessageChannelScope, Exit.fail('new-connection'));
78
+ // We'll try again
79
+ }
80
+ else {
81
+ const channelExit = yield* raceResult.pipe(Effect.exit);
82
+ if (channelExit._tag === 'Failure') {
83
+ yield* Scope.close(makeMessageChannelScope, channelExit);
84
+ if (Cause.isFailType(channelExit.cause) &&
85
+ Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(channelExit.cause.error)) {
86
+ // Only retry when there is a new connection available
87
+ yield* waitForNewConnectionFiber.pipe(Effect.exit);
32
88
  }
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
89
  }
40
- case 'MessageChannelResponseNoTransferables': {
41
- if (channelState._tag === 'Established')
42
- return;
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
- }
90
+ else {
91
+ const channel = channelExit.value;
92
+ yield* Deferred.succeed(resultDeferred, { channel, makeMessageChannelScope, channelVersion });
68
93
  break;
69
94
  }
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
95
  }
115
96
  }
116
- else {
117
- // In this case we've already received a request from the other side (before we had a chance to send our request),
118
- // so we already created a MessageChannel,responded with one port
119
- // and are now using the other port to create the channel.
120
- if (channelState._tag === 'ResponseSent') {
121
- return yield* channelFromPort(channelState.locallyCreatedPort);
122
- }
123
- else {
124
- return shouldNeverHappen(`Expected pending message channel to be in ResponseSent state, but was in ${channelState._tag} state`);
97
+ // Now we wait until the first channel is established
98
+ const { channel, makeMessageChannelScope, channelVersion } = yield* resultDeferred;
99
+ yield* Effect.spanEvent(`Connected#${channelVersion}`);
100
+ debugInfo.isConnected = true;
101
+ debugInfo.innerChannelRef.current = channel;
102
+ yield* Deferred.succeed(initialConnectionDeferred, void 0);
103
+ // We'll now forward all incoming messages to the listen queue
104
+ yield* channel.listen.pipe(Stream.flatten(),
105
+ // Stream.tap((msg) => Effect.log(`${target}→${channelName}→${nodeName}:message:${msg.message}`)),
106
+ Stream.tapChunk((chunk) => Queue.offerAll(listenQueue, chunk)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkIn(makeMessageChannelScope));
107
+ yield* Effect.gen(function* () {
108
+ while (true) {
109
+ const [msg, deferred] = yield* TQueue.peek(sendQueue);
110
+ // NOTE we don't need an explicit retry flow here since in case of the channel being closed,
111
+ // the send will never succeed. Meanwhile the send-loop fiber will be interrupted and
112
+ // given we only peeked at the queue, the message to send is still there.
113
+ yield* channel.send(msg);
114
+ yield* Deferred.succeed(deferred, void 0);
115
+ yield* TQueue.take(sendQueue); // Remove the message from the queue
125
116
  }
126
- }
127
- });
128
- const internalChannelSref = yield* SubscriptionRef.make(false);
129
- const listenQueue = yield* Queue.unbounded();
130
- let connectCounter = 0;
131
- const connect = Effect.gen(function* () {
132
- const connectCount = ++connectCounter;
133
- yield* Effect.spanEvent(`Connecting#${connectCount}`);
134
- yield* SubscriptionRef.set(internalChannelSref, false);
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));
117
+ }).pipe(Effect.forkIn(makeMessageChannelScope));
118
+ // Wait until the channel is closed and then try to reconnect
119
+ yield* channel.closedDeferred;
120
+ yield* Scope.close(makeMessageChannelScope, Exit.succeed('channel-closed'));
121
+ yield* Effect.spanEvent(`Disconnected#${channelVersion}`);
122
+ debugInfo.isConnected = false;
123
+ debugInfo.innerChannelRef.current = undefined;
124
+ }).pipe(Effect.scoped, // Additionally scoping here to clean up finalizers after each loop run
125
+ Effect.forever, Effect.tapCauseLogPretty, Effect.forkScoped);
126
+ // #endregion reconnect-loop
154
127
  const parentSpan = yield* Effect.currentSpan.pipe(Effect.orDie);
155
128
  const send = (message) => Effect.gen(function* () {
156
- const sendFiberHandle = yield* FiberHandle.make();
157
129
  const sentDeferred = yield* Deferred.make();
158
- const trySend = Effect.gen(function* () {
159
- const channel = (yield* SubscriptionRef.waitUntil(internalChannelSref, (channel) => channel !== false));
160
- const innerSend = Effect.gen(function* () {
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);
130
+ debugInfo.pendingSends++;
131
+ debugInfo.totalSends++;
132
+ yield* TQueue.offer(sendQueue, [message, sentDeferred]);
168
133
  yield* sentDeferred;
169
- yield* Fiber.interrupt(rerunOnNewChannelFiber);
134
+ debugInfo.pendingSends--;
170
135
  }).pipe(Effect.scoped, Effect.withParentSpan(parentSpan));
171
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right));
172
- const closedDeferred = yield* Deferred.make();
136
+ const listen = Stream.fromQueue(listenQueue, { maxChunkSize: 1 }).pipe(Stream.map(Either.right));
137
+ const closedDeferred = yield* Deferred.make().pipe(Effect.acquireRelease(Deferred.done(Exit.void)));
173
138
  const webChannel = {
174
139
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
175
140
  send,
@@ -177,7 +142,12 @@ export const makeMessageChannel = ({ nodeName, queue, newConnectionAvailablePubS
177
142
  closedDeferred,
178
143
  supportsTransferables: true,
179
144
  schema,
145
+ debugInfo,
146
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
147
+ };
148
+ return {
149
+ webChannel: webChannel,
150
+ initialConnectionDeferred,
180
151
  };
181
- return webChannel;
182
- }).pipe(Effect.withSpanScoped('makeMessageChannel'));
152
+ }));
183
153
  //# sourceMappingURL=message-channel.js.map