@persistica/flux-agent 0.0.18 → 0.0.20

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/index.js CHANGED
@@ -132,6 +132,7 @@ var RPCServer = class {
132
132
  };
133
133
 
134
134
  // libs/flux/shared/ws/src/lib/ws-client.ts
135
+ var RECONNECTION_DELAY_ON_KICK_MS = 18e5;
135
136
  var WebSocketClient = class extends RPCServer {
136
137
  options;
137
138
  eventListeners = {
@@ -180,17 +181,25 @@ var WebSocketClient = class extends RPCServer {
180
181
  this.ws.onmessage = (event) => {
181
182
  this.emit("message", event.data);
182
183
  };
183
- this.ws.onclose = () => {
184
+ this.ws.onclose = (closeEvent) => {
185
+ const wasKicked = closeEvent.reason === "Kicked by process";
184
186
  clearTimeout(timeout);
185
187
  if (this.isOpen) {
186
- this.emit("close");
188
+ this.emit(
189
+ "close",
190
+ wasKicked ? "kicked" : void 0
191
+ );
187
192
  this.isOpen = false;
188
193
  }
189
194
  if (this.options.autoReconnect && (this.options.retries === void 0 || this.reconnectAttempts < this.options.retries)) {
195
+ const delay = wasKicked ? RECONNECTION_DELAY_ON_KICK_MS : Math.min(
196
+ this.options.reconnectDelay ?? Number.POSITIVE_INFINITY,
197
+ (this.options.reconnectDelay ?? 0) * (this.reconnectAttempts || 1)
198
+ );
190
199
  setTimeout(() => {
191
200
  this.reconnectAttempts++;
192
201
  this.connect();
193
- }, Math.min(this.options.reconnectDelay ?? Number.POSITIVE_INFINITY, this.options.reconnectDelay * (this.reconnectAttempts || 1)));
202
+ }, delay);
194
203
  } else if (this.options.autoReconnect) {
195
204
  this.emit("error", new Error("Connection failed: retries exhausted"));
196
205
  }
@@ -444,33 +453,6 @@ var FluxAgentNetworkConnection = class {
444
453
  }
445
454
  };
446
455
 
447
- // libs/flux/shared/connection/src/lib/flux-network-channel.class.ts
448
- var FluxNetworkChannel = class {
449
- constructor(channelName, _fluxWebSocketConnection) {
450
- this.channelName = channelName;
451
- this._fluxWebSocketConnection = _fluxWebSocketConnection;
452
- }
453
- /**
454
- * Broadcasts a message to the channel.
455
- */
456
- publish(message) {
457
- this._fluxWebSocketConnection.publish(
458
- this.channelName,
459
- message
460
- );
461
- }
462
- /**
463
- * Listen to messages on this channel.
464
- */
465
- onPublish(fn) {
466
- this._fluxWebSocketConnection.onPublish(
467
- this.channelName,
468
- fn
469
- // TODO
470
- );
471
- }
472
- };
473
-
474
456
  // libs/utils/logger/src/lib/utils-logger.ts
475
457
  var hasOverlap = (a, b) => a && b ? [...a].some((item) => (Array.isArray(b) ? b : [b]).includes(item)) : false;
476
458
  var PicoLogger = /* @__PURE__ */ (() => {
@@ -504,6 +486,91 @@ var PicoLogger = /* @__PURE__ */ (() => {
504
486
  };
505
487
  })();
506
488
 
489
+ // libs/flux/shared/connection/src/lib/flux-network-channel.class.ts
490
+ var FluxNetworkChannel = class {
491
+ constructor(channelName, _fluxWebSocketConnection) {
492
+ this.channelName = channelName;
493
+ this._fluxWebSocketConnection = _fluxWebSocketConnection;
494
+ }
495
+ /**
496
+ * Broadcasts a message to the channel.
497
+ */
498
+ publish(message) {
499
+ this._fluxWebSocketConnection.publish(
500
+ this.channelName,
501
+ message
502
+ );
503
+ }
504
+ /**
505
+ * Listen to messages on this channel.
506
+ */
507
+ onPublish(fn) {
508
+ this._fluxWebSocketConnection.onPublish(
509
+ this.channelName,
510
+ fn
511
+ // TODO
512
+ );
513
+ }
514
+ };
515
+
516
+ // libs/flux/shared/connection/src/lib/channel-state-manager.ts
517
+ var CONNECTION_TIMEOUT_MS = 2e3;
518
+ var ChannelStateManager = class {
519
+ connectionTimeouts = /* @__PURE__ */ new Map();
520
+ /**
521
+ * Join a channel.
522
+ */
523
+ async joinChannel(channelName, fluxWebSocketConnection, webSocketClient) {
524
+ if (webSocketClient) {
525
+ return new Promise((resolve, reject) => {
526
+ webSocketClient.send(`${SUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
527
+ const existingTimeout = this.connectionTimeouts.get(channelName);
528
+ if (existingTimeout) {
529
+ clearTimeout(existingTimeout);
530
+ this.connectionTimeouts.delete(channelName);
531
+ }
532
+ const cb = (message) => {
533
+ const timeout = this.connectionTimeouts.get(channelName);
534
+ if (timeout) {
535
+ clearTimeout(timeout);
536
+ this.connectionTimeouts.delete(channelName);
537
+ }
538
+ fluxWebSocketConnection.removePackageTypeInterceptor(SUBSCRIBED_NETWORK_CHANNEL_NAME, cb);
539
+ const receivedChannelName = message.substring(message.indexOf(":") + 1);
540
+ if (channelName !== receivedChannelName) {
541
+ reject(new Error(`Channel name mismatch: "${channelName}" !== "${receivedChannelName}"`));
542
+ return;
543
+ }
544
+ resolve(new FluxNetworkChannel(channelName, fluxWebSocketConnection));
545
+ };
546
+ this.connectionTimeouts.set(
547
+ channelName,
548
+ setTimeout(() => {
549
+ this.connectionTimeouts.delete(channelName);
550
+ fluxWebSocketConnection.removePackageTypeInterceptor(SUBSCRIBED_NETWORK_CHANNEL_NAME, cb);
551
+ reject(new Error(`Channel subscription timed out for channel "${channelName}"`));
552
+ }, CONNECTION_TIMEOUT_MS)
553
+ );
554
+ fluxWebSocketConnection.interceptPackageTypeMessages(
555
+ SUBSCRIBED_NETWORK_CHANNEL_NAME,
556
+ cb
557
+ );
558
+ });
559
+ }
560
+ return Promise.reject(new Error("Not connected"));
561
+ }
562
+ /**
563
+ * Leave a channel.
564
+ */
565
+ async leaveChannel(channelName, webSocketClient) {
566
+ if (webSocketClient) {
567
+ webSocketClient.send(`${UNSUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
568
+ return Promise.resolve(void 0);
569
+ }
570
+ return Promise.reject(new Error("Not connected"));
571
+ }
572
+ };
573
+
507
574
  // libs/flux/shared/connection/src/lib/flux-ws-connection.ts
508
575
  var createWSConnection = (id, ticket, stateManager, onReconnectCallback, options) => {
509
576
  return new FluxWebSocketConnection(
@@ -530,7 +597,7 @@ var FluxWebSocketConnection = class {
530
597
  secretKey: this.options?.secretKey,
531
598
  retries: this.options?.retries ?? 1e4
532
599
  };
533
- const url = new URL(this.options?.domain ?? "http://localhost:5100");
600
+ const url = new URL(this.options.domain);
534
601
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
535
602
  url.searchParams.set("token", this.token);
536
603
  this.socket = new FluxWebSocketClientConnection(
@@ -541,14 +608,36 @@ var FluxWebSocketConnection = class {
541
608
  retries: this.options.retries
542
609
  }
543
610
  );
611
+ this.interceptPackageTypeMessages("isReady", this.readyInterceptor);
544
612
  }
613
+ // ****************************************************************************
614
+ // *** State
615
+ // ****************************************************************************
545
616
  socket;
546
617
  callbacks = /* @__PURE__ */ new Set();
547
618
  channelCallbacks = /* @__PURE__ */ new Map();
548
- // Handles messages before the rest of the logic. Will not continue if there are interceptors
619
+ // Handles messages before the rest of the logic. Will not continue if there are interceptors.
549
620
  packageTypeInterceptorCallbacks = /* @__PURE__ */ new Map();
550
621
  webSocketClient;
622
+ connectPromise;
623
+ connectPromiseResolver;
551
624
  first = true;
625
+ channelStateManager = new ChannelStateManager();
626
+ // ****************************************************************************
627
+ // *** Reused socket callbacks
628
+ // ****************************************************************************
629
+ readyInterceptor = () => {
630
+ this.resolveConnectedSocket();
631
+ if (this.first) {
632
+ this.first = false;
633
+ return;
634
+ }
635
+ this.onReconnectCallback();
636
+ };
637
+ socketMessageHandler = this.handleMessage.bind(this);
638
+ socketCloseHandler = (reason) => this.handleSocketClose(reason);
639
+ socketConnectingHandler = (retryAttempt) => this.handleSocketConnecting(retryAttempt);
640
+ socketErrorHandler = (error) => this.handleSocketError(error);
552
641
  /**
553
642
  * Intercepts messages of a given package type.
554
643
  */
@@ -581,43 +670,24 @@ var FluxWebSocketConnection = class {
581
670
  if (this.webSocketClient) {
582
671
  return Promise.resolve(this.webSocketClient);
583
672
  }
584
- return new Promise((resolve) => {
585
- this.interceptPackageTypeMessages(
586
- "isReady",
587
- () => {
588
- this.stateManager.emitNetworkState("connected");
589
- this.webSocketClient = this.socket;
590
- resolve(this.socket);
591
- if (this.first) {
592
- this.first = false;
593
- return;
594
- }
595
- this.onReconnectCallback();
596
- }
597
- );
598
- this.socket.clearEventSubscribers();
599
- this.socket.on("message", this.handleMessage.bind(this)).on("close", () => {
600
- this.webSocketClient = void 0;
601
- this.stateManager.emitNetworkState("disconnected");
602
- PicoLogger.log("\u{1F50C}\u{1F534} Disconnected", this.fluxInstanceId);
603
- }).on("connecting", (retryAttempt) => {
604
- this.stateManager.emitNetworkState("disconnected");
605
- if (retryAttempt > 0) {
606
- PicoLogger.log(`\u{1F504} Connecting attempt: #${retryAttempt} of ${this.options?.retries ?? "none"}`, this.fluxInstanceId);
607
- } else {
608
- PicoLogger.log(`\u{1F504} Connecting: ${this.fluxInstanceId}`);
609
- }
610
- }).on("error", (error) => {
611
- PicoLogger.log(`\u274C Error: "${error.message}".`, this.fluxInstanceId);
612
- });
613
- this.stateManager.emitNetworkState("connecting");
614
- this.socket.connect();
673
+ if (this.connectPromise) {
674
+ return this.connectPromise;
675
+ }
676
+ this.connectPromise = new Promise((resolve) => {
677
+ this.connectPromiseResolver = resolve;
615
678
  });
679
+ this.socket.clearEventSubscribers();
680
+ this.socket.on("message", this.socketMessageHandler).on("close", this.socketCloseHandler).on("connecting", this.socketConnectingHandler).on("error", this.socketErrorHandler);
681
+ this.stateManager.emitNetworkState("connecting");
682
+ void this.socket.connect();
683
+ return this.connectPromise;
616
684
  }
617
685
  /**
618
686
  * Disconnects from the Flux platform.
619
687
  */
620
688
  disconnect() {
689
+ this.connectPromiseResolver = void 0;
690
+ this.connectPromise = void 0;
621
691
  this.socket.close();
622
692
  this.webSocketClient = void 0;
623
693
  this.stateManager.emitNetworkState("disconnected");
@@ -650,35 +720,20 @@ var FluxWebSocketConnection = class {
650
720
  * Join a channel.
651
721
  */
652
722
  async joinChannel(channelName) {
653
- if (this.webSocketClient) {
654
- this.webSocketClient.send(`${SUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
655
- return new Promise((resolve, reject) => {
656
- const cb = (message) => {
657
- this.removePackageTypeInterceptor(SUBSCRIBED_NETWORK_CHANNEL_NAME, cb);
658
- const receivedChannelName = message.substring(message.indexOf(":") + 1);
659
- if (channelName !== receivedChannelName) {
660
- reject(new Error(`Channel name mismatch: "${channelName}" !== "${receivedChannelName}"`));
661
- return;
662
- }
663
- resolve(new FluxNetworkChannel(channelName, this));
664
- };
665
- this.interceptPackageTypeMessages(
666
- SUBSCRIBED_NETWORK_CHANNEL_NAME,
667
- cb
668
- );
669
- });
670
- }
671
- return Promise.reject(new Error("Not connected"));
723
+ return this.channelStateManager.joinChannel(
724
+ channelName,
725
+ this,
726
+ this.webSocketClient
727
+ );
672
728
  }
673
729
  /**
674
- * Leaves a channel.
730
+ * Leave a channel.
675
731
  */
676
732
  async leaveChannel(channelName) {
677
- if (this.webSocketClient) {
678
- this.webSocketClient.send(`${UNSUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
679
- return Promise.resolve(void 0);
680
- }
681
- return Promise.reject(new Error("Not connected"));
733
+ return this.channelStateManager.leaveChannel(
734
+ channelName,
735
+ this.webSocketClient
736
+ );
682
737
  }
683
738
  /**
684
739
  * Publishes a message to a channel.
@@ -746,6 +801,33 @@ var FluxWebSocketConnection = class {
746
801
  // ****************************************************************************
747
802
  // *** Internal Helpers
748
803
  // ****************************************************************************
804
+ resolveConnectedSocket() {
805
+ this.stateManager.emitNetworkState("connected");
806
+ this.webSocketClient = this.socket;
807
+ const connectPromiseResolver = this.connectPromiseResolver;
808
+ this.connectPromiseResolver = void 0;
809
+ this.connectPromise = void 0;
810
+ connectPromiseResolver?.(this.socket);
811
+ }
812
+ handleSocketClose(reason) {
813
+ this.webSocketClient = void 0;
814
+ PicoLogger.log(
815
+ reason === "kicked" ? `\u{1F50C}\u{1F534} Kicked, delaying reconnect ${RECONNECTION_DELAY_ON_KICK_MS / 6e4} minutes` : "\u{1F50C}\u{1F534} Disconnected",
816
+ this.fluxInstanceId
817
+ );
818
+ this.stateManager.emitNetworkState(reason === "kicked" ? "kicked" : "disconnected");
819
+ }
820
+ handleSocketConnecting(retryAttempt) {
821
+ this.stateManager.emitNetworkState("disconnected");
822
+ if (retryAttempt > 0) {
823
+ PicoLogger.log(`\u{1F504} Connecting attempt: #${retryAttempt} of ${this.options?.retries ?? "none"}`, this.fluxInstanceId);
824
+ } else {
825
+ PicoLogger.log(`\u{1F504} Connecting: ${this.fluxInstanceId}`);
826
+ }
827
+ }
828
+ handleSocketError(error) {
829
+ PicoLogger.log(`\u274C Error: "${error.message}".`, this.fluxInstanceId);
830
+ }
749
831
  /**
750
832
  * Handles incoming WebSocket messages.
751
833
  */
@@ -1068,9 +1150,6 @@ var FluxClientData = class {
1068
1150
  };
1069
1151
 
1070
1152
  // packages/flux/agent/src/lib/flux-agent.class.ts
1071
- globalThis.agentLoadCount ??= 0;
1072
- globalThis.agentLoadCount++;
1073
- console.log(`[flux-agent] Reloaded ${globalThis.agentLoadCount} time(s)`);
1074
1153
  var DEFAULT_FLUX_DOMAIN = "https://mesh.persistica.io";
1075
1154
  var FluxAgent = class {
1076
1155
  constructor(networkId, options) {
@@ -1093,10 +1172,10 @@ var FluxAgent = class {
1093
1172
  async connect(identification, clientUId) {
1094
1173
  const domain = this.options?.domain ?? DEFAULT_FLUX_DOMAIN;
1095
1174
  this.stateManager.emitNetworkState("authorizing");
1096
- if (clientUId && !validateAgentUIDOrThrow(clientUId)) {
1097
- throw new Error("Will never be thrown");
1098
- }
1099
1175
  try {
1176
+ if (clientUId && !validateAgentUIDOrThrow(clientUId)) {
1177
+ throw new Error("Will never be thrown");
1178
+ }
1100
1179
  const ticket = await retryOrThrow(
1101
1180
  async () => {
1102
1181
  return authenticateAgentOrThrow(
@@ -1134,8 +1213,7 @@ var FluxAgent = class {
1134
1213
  }
1135
1214
  );
1136
1215
  this.fluxClientData.updateWsConnection(this.fluxWebSocketConnection);
1137
- const fluxNetworkConnection = await this.fluxWebSocketConnection.connectToNetwork();
1138
- return fluxNetworkConnection;
1216
+ return await this.fluxWebSocketConnection.connectToNetwork();
1139
1217
  } catch (error) {
1140
1218
  this.stateManager.emitNetworkState("auth-error");
1141
1219
  return Promise.reject(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistica/flux-agent",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Persistica Flux Agent",
5
5
  "type": "module",
6
6
  "keywords": [],
@@ -1,6 +1,3 @@
1
- declare global {
2
- var agentLoadCount: number | null | undefined;
3
- }
4
1
  import type { TMessageCallback } from '@flux/shared/ws';
5
2
  import type { FluxAgentNetworkConnection } from '@flux/shared/connection';
6
3
  export declare class FluxAgent {