cross-tab-worker-databus 0.21.57 → 0.21.59

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.
@@ -1,4 +1,4 @@
1
- // node_modules/.pnpm/centrifuge@5.7.4/node_modules/centrifuge/build/index.mjs
1
+ // node_modules/.pnpm/centrifuge@5.8.0/node_modules/centrifuge/build/index.mjs
2
2
  function __awaiter(thisArg, _arguments, P, generator) {
3
3
  function adopt(value) {
4
4
  return value instanceof P ? value : new P(function(resolve) {
@@ -504,6 +504,23 @@ function localStorageItem(key) {
504
504
  return null;
505
505
  }
506
506
  }
507
+ function hasOffset(offset) {
508
+ return offset !== void 0 && offset !== null && Number(offset) > 0;
509
+ }
510
+ function toOffset(offset) {
511
+ if (offset === void 0 || offset === null) {
512
+ return 0;
513
+ }
514
+ if (typeof offset === "object") {
515
+ const low = offset.low;
516
+ const high = offset.high;
517
+ if (typeof low === "number" && typeof high === "number") {
518
+ return (high >>> 0) * 4294967296 + (low >>> 0);
519
+ }
520
+ }
521
+ const value = Number(offset);
522
+ return isNaN(value) ? 0 : value;
523
+ }
507
524
  var MapPhase;
508
525
  (function(MapPhase2) {
509
526
  MapPhase2[MapPhase2["Live"] = 0] = "Live";
@@ -516,6 +533,8 @@ var BaseSubscription = class extends EventEmitter {
516
533
  super();
517
534
  this._resubscribeTimeout = null;
518
535
  this._refreshTimeout = null;
536
+ this._pendingOffset = null;
537
+ this._removed = false;
519
538
  this._getState = null;
520
539
  this._map = false;
521
540
  this._mapPresenceType = 1;
@@ -525,6 +544,18 @@ var BaseSubscription = class extends EventEmitter {
525
544
  this._mapCursor = "";
526
545
  this._mapPageSize = 0;
527
546
  this._mapUnrecoverableStrategy = "from_scratch";
547
+ this._mapPositionResets = 0;
548
+ this._subscribeFlow = 0;
549
+ this._refreshGeneration = 0;
550
+ this._sharedPollReplayPending = false;
551
+ this._stateTransitions = 0;
552
+ this._sharedPollQueue = Promise.resolve();
553
+ this._subscribeCallFlow = null;
554
+ this._deferredUnsubscribe = null;
555
+ this._mapFlowRecovering = false;
556
+ this._mapFlowOffset = 0;
557
+ this._mapFlowEpoch = null;
558
+ this._mapFlowPositionResets = 0;
528
559
  this._debounceMs = 0;
529
560
  this._debouncePending = /* @__PURE__ */ new Map();
530
561
  this._sharedPoll = false;
@@ -540,6 +571,8 @@ var BaseSubscription = class extends EventEmitter {
540
571
  this._sharedPollSignatures = [];
541
572
  this._sharedPollSignatureRefreshTargetMs = null;
542
573
  this._sharedPollSignatureRefreshInFlight = false;
574
+ this._sharedPollSignatureRefreshAgain = false;
575
+ this._sharedPollSignatureRefreshKeys = /* @__PURE__ */ new Set();
543
576
  this.channel = channel;
544
577
  this.state = SubscriptionState.Unsubscribed;
545
578
  this._centrifuge = centrifuge;
@@ -598,29 +631,36 @@ var BaseSubscription = class extends EventEmitter {
598
631
  return Promise.resolve();
599
632
  }
600
633
  return new Promise((res, rej) => {
634
+ const id = this._nextPromiseId();
601
635
  const ctx = {
602
636
  resolve: res,
603
637
  reject: rej
604
638
  };
605
639
  if (timeout) {
606
- ctx.timeout = setTimeout(function() {
640
+ ctx.timeout = setTimeout(() => {
641
+ delete this._promises[id];
607
642
  rej({ code: errorCodes.timeout, message: "timeout" });
608
643
  }, timeout);
609
644
  }
610
- this._promises[this._nextPromiseId()] = ctx;
645
+ this._promises[id] = ctx;
611
646
  });
612
647
  }
613
648
  /** subscribe to a channel.*/
614
649
  subscribe() {
650
+ if (this._removed) {
651
+ throw new Error("Subscription to the channel " + this.channel + " was removed from the client");
652
+ }
615
653
  if (this._isSubscribed()) {
616
654
  return;
617
655
  }
618
- this._resubscribeAttempts = 0;
656
+ if (this._isUnsubscribed()) {
657
+ this._resubscribeAttempts = 0;
658
+ }
619
659
  this._setSubscribing(subscribingCodes.subscribeCalled, "subscribe called");
620
660
  }
621
661
  /** unsubscribe from a channel, keeping position state.*/
622
662
  unsubscribe() {
623
- this._unsubPromise = this._setUnsubscribed(unsubscribedCodes.unsubscribeCalled, "unsubscribe called", true);
663
+ this._setUnsubscribed(unsubscribedCodes.unsubscribeCalled, "unsubscribe called", true);
624
664
  }
625
665
  _debouncedPublish(key, data, isMap) {
626
666
  const existing = this._debouncePending.get(key);
@@ -648,7 +688,8 @@ var BaseSubscription = class extends EventEmitter {
648
688
  return;
649
689
  }
650
690
  this._debouncePending.delete(key);
651
- this._debouncedPublish(key, p.data, isMap);
691
+ this._debouncedPublish(key, p.data, isMap).catch(() => {
692
+ });
652
693
  }, this._debounceMs);
653
694
  }, this._debounceMs);
654
695
  this._debouncePending.set(key, entry);
@@ -731,6 +772,7 @@ var BaseSubscription = class extends EventEmitter {
731
772
  this._recover = false;
732
773
  this._offset = null;
733
774
  this._epoch = null;
775
+ this._mapPositionResets++;
734
776
  }
735
777
  }
736
778
  /** setData allows setting subscription data. This only applied on the next subscription attempt,
@@ -763,10 +805,12 @@ var BaseSubscription = class extends EventEmitter {
763
805
  }
764
806
  return new Promise((resolve, reject) => {
765
807
  const timeoutDuration = this._centrifuge._config.timeout;
808
+ const id = this._nextPromiseId();
766
809
  const timeout = setTimeout(() => {
810
+ delete this._promises[id];
767
811
  reject({ code: errorCodes.timeout, message: "timeout" });
768
812
  }, timeoutDuration);
769
- this._promises[this._nextPromiseId()] = {
813
+ this._promises[id] = {
770
814
  timeout,
771
815
  resolve,
772
816
  reject
@@ -792,6 +836,7 @@ var BaseSubscription = class extends EventEmitter {
792
836
  if (this.state !== newState) {
793
837
  const oldState = this.state;
794
838
  this.state = newState;
839
+ this._stateTransitions++;
795
840
  this.emit("state", { newState, oldState, channel: this.channel });
796
841
  return true;
797
842
  }
@@ -806,18 +851,24 @@ var BaseSubscription = class extends EventEmitter {
806
851
  }
807
852
  _clearSubscribedState() {
808
853
  this._clearRefreshTimeout();
854
+ this._refreshGeneration++;
809
855
  this._cancelAllDebounce();
810
856
  this._clearSharedPollSignatureRefresh();
811
857
  this._clearSharedPollTrackRetry();
812
858
  this._clearSharedPollReplayRetry();
813
859
  this._sharedPollSignatureRefreshTargetMs = null;
814
860
  this._sharedPollSignatureRefreshInFlight = false;
861
+ this._sharedPollSignatureRefreshAgain = false;
815
862
  }
816
863
  /** Called on "state invalidated" — unsubscribe code 2502 for this channel,
817
- * or connection disconnect code 3014. Clears the token (next subscribe
818
- * fetches a fresh one) and the fossil delta base (every subscription type
819
- * uses _prevValueMap; a stale base would corrupt decoding of the first
820
- * publication after re-subscribe).
864
+ * or connection disconnect code 3014. Clears the token when getToken is set
865
+ * (next subscribe fetches a fresh one) and the fossil delta base (every
866
+ * subscription type uses _prevValueMap; a stale base would corrupt decoding
867
+ * of the first publication after re-subscribe).
868
+ *
869
+ * Without getToken there is no new token to get: the subscription keeps the
870
+ * token it has (or none) and resubscribes with it. The server rejects it if
871
+ * it's no longer valid.
821
872
  *
822
873
  * Map subscriptions restart from scratch: their recovery position and
823
874
  * materialized-state buffers are dropped so the next subscribe does a full
@@ -831,17 +882,20 @@ var BaseSubscription = class extends EventEmitter {
831
882
  * subscribe), while a non-recoverable one simply resubscribes. The real
832
883
  * epoch/offset are adopted from the subscribe reply. */
833
884
  _invalidateState() {
834
- this._token = "";
885
+ if (this._getToken !== null) {
886
+ this._token = "";
887
+ }
835
888
  this._prevValueMap = /* @__PURE__ */ new Map();
836
889
  if (this._map) {
837
890
  this._offset = null;
838
891
  this._epoch = null;
839
892
  this._recover = false;
893
+ this._mapPositionResets++;
840
894
  this._mapStateBuffer = [];
841
895
  this._mapStreamBuffer = [];
842
896
  this._mapCursor = "";
843
897
  this._mapPhase = null;
844
- } else {
898
+ } else if (this._offset !== null) {
845
899
  this._offset = 0;
846
900
  this._epoch = "_";
847
901
  }
@@ -856,8 +910,10 @@ var BaseSubscription = class extends EventEmitter {
856
910
  }
857
911
  if (result.recoverable) {
858
912
  this._recover = true;
859
- this._offset = result.offset || 0;
913
+ this._offset = toOffset(result.offset);
860
914
  this._epoch = result.epoch || "";
915
+ } else {
916
+ this._recover = false;
861
917
  }
862
918
  if (result.delta) {
863
919
  this._delta_negotiated = true;
@@ -877,19 +933,20 @@ var BaseSubscription = class extends EventEmitter {
877
933
  this._sharedPollEpoch = newEpoch;
878
934
  }
879
935
  this._setState(SubscriptionState.Subscribed);
936
+ if (!this._isSubscribed()) {
937
+ return;
938
+ }
880
939
  const ctx = this._centrifuge._getSubscribeContext(this.channel, result);
881
940
  this.emit("subscribed", ctx);
941
+ if (!this._isSubscribed()) {
942
+ return;
943
+ }
882
944
  this._resolvePromises();
883
- const pubs = result.publications;
884
- if (pubs && pubs.length > 0) {
885
- for (const i in pubs) {
886
- if (!pubs.hasOwnProperty(i)) {
887
- continue;
888
- }
889
- this._handlePublication(pubs[i]);
890
- }
945
+ const pubs = result.publications || [];
946
+ for (let i = 0; i < pubs.length && this._isSubscribed(); i++) {
947
+ this._handlePublication(pubs[i]);
891
948
  }
892
- if (result.expires === true) {
949
+ if (result.expires === true && this._isSubscribed()) {
893
950
  this._refreshTimeout = setTimeout(() => this._refresh(), ttlMilliseconds(result.ttl));
894
951
  }
895
952
  }
@@ -902,11 +959,14 @@ var BaseSubscription = class extends EventEmitter {
902
959
  this._clearSubscribedState();
903
960
  }
904
961
  this._id = 0;
905
- if (this._setState(SubscriptionState.Subscribing)) {
962
+ const transition = this._stateTransitions + 1;
963
+ if (this._setState(SubscriptionState.Subscribing) && this._stateTransitions === transition) {
906
964
  this.emit("subscribing", { channel: this.channel, code, reason });
907
965
  }
908
966
  if (this._centrifuge._transport && this._centrifuge._transport.emulation()) {
909
967
  yield this._unsubPromise;
968
+ yield this._centrifuge._removedSubscriptionUnsubscribed(this.channel);
969
+ yield this._sharedPollQueue;
910
970
  }
911
971
  if (!this._isSubscribing()) {
912
972
  return;
@@ -928,25 +988,60 @@ var BaseSubscription = class extends EventEmitter {
928
988
  this._mapSubscribe();
929
989
  return null;
930
990
  }
991
+ const flow = this._subscribeFlow;
931
992
  if (this._getState && this._offset === null) {
932
- this._loadStreamState();
993
+ this._loadStreamState(flow);
933
994
  return null;
934
995
  }
935
996
  if (this._canSubscribeWithoutGettingToken()) {
936
- return this._subscribeWithoutToken();
997
+ return this._subscribeWithoutToken(flow);
937
998
  }
938
- this._getSubscriptionToken().then((token) => this._handleTokenResponse(token)).catch((e) => this._handleTokenError(e));
999
+ this._getSubscriptionToken().then((token) => this._handleTokenResponse(token, flow)).catch((e) => this._handleTokenError(e, flow));
939
1000
  return null;
940
1001
  }
1002
+ _flowOutdated(flow) {
1003
+ return flow !== this._subscribeFlow;
1004
+ }
1005
+ // Over emulation commands go in separate requests the server may handle in any
1006
+ // order: an unsubscribe of a subscribe in flight could overtake it, and the
1007
+ // server would keep the subscription. The unsubscribe waits for the
1008
+ // subscribe's reply then.
1009
+ _mustDeferUnsubscribe() {
1010
+ const transport = this._centrifuge._transport;
1011
+ return this._subscribeCallFlow === this._subscribeFlow && !!transport && transport.emulation();
1012
+ }
1013
+ // Called first by the reply handlers of a subscribe command or map page. True
1014
+ // when the reply belongs to a subscribe whose unsubscribe was deferred, which is
1015
+ // handled here: the unsubscribe goes out if the subscribe succeeded. A timeout
1016
+ // leaves unknown whether the server subscribed: the client reconnects.
1017
+ _settleSubscribeCall(flow, ok, error) {
1018
+ if (this._subscribeCallFlow === flow) {
1019
+ this._subscribeCallFlow = null;
1020
+ }
1021
+ const deferred = this._deferredUnsubscribe;
1022
+ if (deferred === null || deferred.flow !== flow) {
1023
+ return false;
1024
+ }
1025
+ this._deferredUnsubscribe = null;
1026
+ if (ok) {
1027
+ this._centrifuge._unsubscribe(this).then(deferred.resolve);
1028
+ return true;
1029
+ }
1030
+ deferred.resolve();
1031
+ if (error && error.code === errorCodes.timeout) {
1032
+ this._centrifuge._disconnect(connectingCodes.subscribeTimeout, "subscribe timeout", true);
1033
+ }
1034
+ return true;
1035
+ }
941
1036
  _isTransportOpen() {
942
1037
  return this._centrifuge._transportIsOpen;
943
1038
  }
944
1039
  _canSubscribeWithoutGettingToken() {
945
1040
  return !this._usesToken() || !!this._token;
946
1041
  }
947
- _subscribeWithoutToken() {
1042
+ _subscribeWithoutToken(flow) {
948
1043
  if (this._getData) {
949
- this._getDataAndSubscribe(this._token);
1044
+ this._getDataAndSubscribe(this._token, flow);
950
1045
  return null;
951
1046
  } else {
952
1047
  return this._sendSubscribe(this._token);
@@ -969,25 +1064,31 @@ var BaseSubscription = class extends EventEmitter {
969
1064
  * This order is critical: reading position first ensures it's a lower bound.
970
1065
  * Recovered publications may overlap with data the app already loaded — this
971
1066
  * requires idempotent updates or offset-based dedup. */
972
- _loadStreamState() {
1067
+ _loadStreamState(flow) {
973
1068
  if (!this._isSubscribing()) {
974
1069
  this._inflight = false;
975
1070
  return;
976
1071
  }
977
- this._getState().then((result) => {
1072
+ this._callAsync(() => this._getState()).then((result) => {
1073
+ if (this._flowOutdated(flow)) {
1074
+ return;
1075
+ }
978
1076
  if (!this._isSubscribing()) {
979
1077
  this._inflight = false;
980
1078
  return;
981
1079
  }
982
- this._offset = result.offset;
1080
+ this._offset = toOffset(result.offset);
983
1081
  this._epoch = result.epoch;
984
1082
  this._recover = true;
985
1083
  if (this._canSubscribeWithoutGettingToken()) {
986
- this._subscribeWithoutToken();
1084
+ this._subscribeWithoutToken(flow);
987
1085
  } else {
988
- this._getSubscriptionToken().then((token) => this._handleTokenResponse(token)).catch((e) => this._handleTokenError(e));
1086
+ this._getSubscriptionToken().then((token) => this._handleTokenResponse(token, flow)).catch((e) => this._handleTokenError(e, flow));
989
1087
  }
990
1088
  }).catch((e) => {
1089
+ if (this._flowOutdated(flow)) {
1090
+ return;
1091
+ }
991
1092
  if (!this._isSubscribing()) {
992
1093
  this._inflight = false;
993
1094
  return;
@@ -1000,21 +1101,27 @@ var BaseSubscription = class extends EventEmitter {
1000
1101
  });
1001
1102
  });
1002
1103
  }
1003
- _getDataAndSubscribe(token) {
1104
+ _getDataAndSubscribe(token, flow) {
1004
1105
  if (!this._getData) {
1005
1106
  this._inflight = false;
1006
1107
  return;
1007
1108
  }
1008
- this._getData({ channel: this.channel }).then((data) => {
1109
+ this._callAsync(() => this._getData({ channel: this.channel })).then((data) => {
1110
+ if (this._flowOutdated(flow)) {
1111
+ return;
1112
+ }
1009
1113
  if (!this._isSubscribing()) {
1010
1114
  this._inflight = false;
1011
1115
  return;
1012
1116
  }
1013
1117
  this._data = data;
1014
1118
  this._sendSubscribe(token);
1015
- }).catch((e) => this._handleGetDataError(e));
1119
+ }).catch((e) => this._handleGetDataError(e, flow));
1016
1120
  }
1017
- _handleGetDataError(error) {
1121
+ _handleGetDataError(error, flow) {
1122
+ if (this._flowOutdated(flow)) {
1123
+ return;
1124
+ }
1018
1125
  if (!this._isSubscribing()) {
1019
1126
  this._inflight = false;
1020
1127
  return;
@@ -1024,18 +1131,22 @@ var BaseSubscription = class extends EventEmitter {
1024
1131
  this._failUnauthorized();
1025
1132
  return;
1026
1133
  }
1027
- this.emit("error", {
1134
+ this._emitError({
1028
1135
  type: "subscribeData",
1029
1136
  channel: this.channel,
1030
1137
  error: {
1031
1138
  code: errorCodes.badConfiguration,
1032
1139
  message: (error === null || error === void 0 ? void 0 : error.toString()) || ""
1033
1140
  }
1141
+ }, () => {
1142
+ this._inflight = false;
1143
+ this._scheduleResubscribe();
1034
1144
  });
1035
- this._inflight = false;
1036
- this._scheduleResubscribe();
1037
1145
  }
1038
- _handleTokenResponse(token) {
1146
+ _handleTokenResponse(token, flow) {
1147
+ if (this._flowOutdated(flow)) {
1148
+ return;
1149
+ }
1039
1150
  if (!this._isSubscribing()) {
1040
1151
  this._inflight = false;
1041
1152
  return;
@@ -1047,12 +1158,15 @@ var BaseSubscription = class extends EventEmitter {
1047
1158
  }
1048
1159
  this._token = token;
1049
1160
  if (this._getData) {
1050
- this._getDataAndSubscribe(token);
1161
+ this._getDataAndSubscribe(token, flow);
1051
1162
  } else {
1052
1163
  this._sendSubscribe(token);
1053
1164
  }
1054
1165
  }
1055
- _handleTokenError(error) {
1166
+ _handleTokenError(error, flow) {
1167
+ if (this._flowOutdated(flow)) {
1168
+ return;
1169
+ }
1056
1170
  if (!this._isSubscribing()) {
1057
1171
  this._inflight = false;
1058
1172
  return;
@@ -1062,16 +1176,17 @@ var BaseSubscription = class extends EventEmitter {
1062
1176
  this._failUnauthorized();
1063
1177
  return;
1064
1178
  }
1065
- this.emit("error", {
1179
+ this._emitError({
1066
1180
  type: "subscribeToken",
1067
1181
  channel: this.channel,
1068
1182
  error: {
1069
1183
  code: errorCodes.subscriptionSubscribeToken,
1070
1184
  message: (error === null || error === void 0 ? void 0 : error.toString()) || ""
1071
1185
  }
1186
+ }, () => {
1187
+ this._inflight = false;
1188
+ this._scheduleResubscribe();
1072
1189
  });
1073
- this._inflight = false;
1074
- this._scheduleResubscribe();
1075
1190
  }
1076
1191
  _sendSubscribe(token) {
1077
1192
  if (!this._isTransportOpen()) {
@@ -1079,18 +1194,41 @@ var BaseSubscription = class extends EventEmitter {
1079
1194
  return null;
1080
1195
  }
1081
1196
  const cmd = this._buildSubscribeCommand(token);
1197
+ const flow = this._subscribeFlow;
1198
+ this._subscribeCallFlow = flow;
1082
1199
  this._centrifuge._call(cmd).then((resolveCtx) => {
1083
- this._inflight = false;
1084
- const result = resolveCtx.reply.subscribe;
1085
- this._handleSubscribeResponse(result);
1086
- if (resolveCtx.next) {
1087
- resolveCtx.next();
1200
+ try {
1201
+ if (this._settleSubscribeCall(flow, true)) {
1202
+ return;
1203
+ }
1204
+ if (flow !== this._subscribeFlow) {
1205
+ return;
1206
+ }
1207
+ this._inflight = false;
1208
+ this._handleSubscribeResponse(resolveCtx.reply.subscribe);
1209
+ } catch (err) {
1210
+ this._centrifuge._dispatchFailed(err);
1211
+ } finally {
1212
+ if (resolveCtx.next) {
1213
+ resolveCtx.next();
1214
+ }
1088
1215
  }
1089
1216
  }, (rejectCtx) => {
1090
- this._inflight = false;
1091
- this._handleSubscribeError(rejectCtx.error);
1092
- if (rejectCtx.next) {
1093
- rejectCtx.next();
1217
+ try {
1218
+ if (this._settleSubscribeCall(flow, false, rejectCtx.error)) {
1219
+ return;
1220
+ }
1221
+ if (flow !== this._subscribeFlow) {
1222
+ return;
1223
+ }
1224
+ this._inflight = false;
1225
+ this._handleSubscribeError(rejectCtx.error);
1226
+ } catch (err) {
1227
+ this._centrifuge._rejectionFailed(err, rejectCtx);
1228
+ } finally {
1229
+ if (rejectCtx.next) {
1230
+ rejectCtx.next();
1231
+ }
1094
1232
  }
1095
1233
  });
1096
1234
  return cmd;
@@ -1135,10 +1273,22 @@ var BaseSubscription = class extends EventEmitter {
1135
1273
  _debug(...args) {
1136
1274
  this._centrifuge._debug(...args);
1137
1275
  }
1276
+ /** @internal An exception escaping a handler stops the client: see Centrifuge._handlerFailed. */
1277
+ emit(event, ...args) {
1278
+ try {
1279
+ return EventEmitter.prototype.emit.call(this, event, ...args);
1280
+ } catch (e) {
1281
+ this._centrifuge._handlerFailed(e);
1282
+ return true;
1283
+ }
1284
+ }
1138
1285
  _handleSubscribeError(error) {
1139
1286
  if (!this._isSubscribing()) {
1140
1287
  return;
1141
1288
  }
1289
+ if (error.code === errorCodes.connectionClosed) {
1290
+ return;
1291
+ }
1142
1292
  if (error.code === errorCodes.timeout) {
1143
1293
  this._centrifuge._disconnect(connectingCodes.subscribeTimeout, "subscribe timeout", true);
1144
1294
  return;
@@ -1157,7 +1307,12 @@ var BaseSubscription = class extends EventEmitter {
1157
1307
  if (!this._isSubscribing()) {
1158
1308
  return;
1159
1309
  }
1160
- this._setSubscribed(result);
1310
+ this._sharedPollReplayPending = this._sharedPoll;
1311
+ try {
1312
+ this._setSubscribed(result);
1313
+ } finally {
1314
+ this._sharedPollReplayPending = false;
1315
+ }
1161
1316
  if (this._sharedPoll) {
1162
1317
  this._sharedPollReplayTrack();
1163
1318
  }
@@ -1169,45 +1324,61 @@ var BaseSubscription = class extends EventEmitter {
1169
1324
  let promise = Promise.resolve();
1170
1325
  if (this._isSubscribed()) {
1171
1326
  if (sendUnsubscribe) {
1172
- promise = this._centrifuge._unsubscribe(this);
1327
+ promise = this._unsubPromise = this._centrifuge._unsubscribe(this);
1173
1328
  }
1174
1329
  this._clearSubscribedState();
1175
1330
  } else if (this._isSubscribing()) {
1176
1331
  if (this._inflight && sendUnsubscribe) {
1177
- promise = this._centrifuge._unsubscribe(this);
1332
+ if (this._mustDeferUnsubscribe()) {
1333
+ const flow = this._subscribeFlow;
1334
+ promise = this._unsubPromise = new Promise((resolve) => {
1335
+ this._deferredUnsubscribe = { flow, resolve };
1336
+ });
1337
+ } else {
1338
+ promise = this._unsubPromise = this._centrifuge._unsubscribe(this);
1339
+ }
1178
1340
  }
1179
1341
  this._clearSubscribingState();
1180
1342
  }
1181
1343
  this._inflight = false;
1344
+ this._subscribeFlow++;
1182
1345
  this._id = 0;
1183
1346
  this._sharedPollEpoch = "";
1184
1347
  this._sharedPollSignatures = [];
1185
1348
  this._sharedPollTrackedItems.clear();
1186
1349
  this._sharedPollSignatureRefreshInFlight = false;
1350
+ this._sharedPollSignatureRefreshAgain = false;
1187
1351
  this._sharedPollSignatureRefreshTargetMs = null;
1188
1352
  this._cancelAllDebounce();
1189
- if (this._setState(SubscriptionState.Unsubscribed)) {
1353
+ this._rejectPromises({ code: errorCodes.subscriptionUnsubscribed, message: SubscriptionState.Unsubscribed });
1354
+ const transition = this._stateTransitions + 1;
1355
+ if (this._setState(SubscriptionState.Unsubscribed) && this._stateTransitions === transition) {
1190
1356
  this.emit("unsubscribed", { channel: this.channel, code, reason });
1191
1357
  }
1192
- this._rejectPromises({ code: errorCodes.subscriptionUnsubscribed, message: this.state });
1193
1358
  return promise;
1194
1359
  }
1195
1360
  _handlePublication(pub) {
1361
+ if (this._sharedPoll && pub.key && !pub.removed && hasOffset(pub.version)) {
1362
+ const known = this._sharedPollTrackedItems.get(pub.key);
1363
+ if (known !== void 0 && known > 0 && toOffset(pub.version) <= known) {
1364
+ return;
1365
+ }
1366
+ }
1196
1367
  if (this._delta && this._delta_negotiated) {
1197
1368
  const deltaKey = this._map || this._sharedPoll ? pub.key || "" : "";
1198
- const { newData, newPrevValue, isDelta, wireBytes, fullBytes } = this._centrifuge._codec.applyDeltaIfNeeded(pub, this._prevValueMap.get(deltaKey));
1199
- pub.data = newData;
1200
- this._deltaNumPubs++;
1201
- this._deltaBytesReceived += wireBytes;
1202
- this._deltaBytesDecoded += fullBytes;
1203
- if (isDelta) {
1204
- this._deltaNumDelta++;
1205
- } else {
1206
- this._deltaNumFull++;
1207
- }
1208
1369
  if (pub.removed) {
1209
1370
  this._prevValueMap.delete(deltaKey);
1210
1371
  } else {
1372
+ const { newData, newPrevValue, isDelta, wireBytes, fullBytes } = this._centrifuge._codec.applyDeltaIfNeeded(pub, this._prevValueMap.get(deltaKey));
1373
+ pub.data = newData;
1374
+ this._deltaNumPubs++;
1375
+ this._deltaBytesReceived += wireBytes;
1376
+ this._deltaBytesDecoded += fullBytes;
1377
+ if (isDelta) {
1378
+ this._deltaNumDelta++;
1379
+ } else {
1380
+ this._deltaNumFull++;
1381
+ }
1211
1382
  this._prevValueMap.set(deltaKey, newPrevValue);
1212
1383
  }
1213
1384
  }
@@ -1216,25 +1387,43 @@ var BaseSubscription = class extends EventEmitter {
1216
1387
  if (pub.key && !this._sharedPollTrackedItems.has(pub.key)) {
1217
1388
  return;
1218
1389
  }
1219
- if (pub.key) {
1220
- if (pub.removed) {
1221
- this._sharedPollTrackedItems.delete(pub.key);
1222
- } else if (pub.version) {
1223
- this._sharedPollTrackedItems.set(pub.key, pub.version);
1224
- }
1225
- }
1226
1390
  ctx = this._getSharedPollUpdateContext(pub);
1227
1391
  } else if (this._map) {
1228
1392
  ctx = this._getMapUpdateContext(pub);
1229
1393
  } else {
1230
1394
  ctx = this._centrifuge._getPublicationContext(this.channel, pub);
1231
1395
  }
1232
- this.emit("publication", ctx);
1233
1396
  if (this._map || this._sharedPoll) {
1397
+ this.emit("publication", ctx);
1398
+ if (!this._isSubscribed()) {
1399
+ return;
1400
+ }
1401
+ this._setPublicationPosition(pub);
1234
1402
  this.emit("update", ctx);
1403
+ } else {
1404
+ this._pendingOffset = hasOffset(pub.offset) ? toOffset(pub.offset) : null;
1405
+ const failures = this._centrifuge._handlerFailures;
1406
+ try {
1407
+ this.emit("publication", ctx);
1408
+ } finally {
1409
+ this._pendingOffset = null;
1410
+ }
1411
+ if (this._centrifuge._handlerFailures !== failures) {
1412
+ return;
1413
+ }
1414
+ this._setPublicationPosition(pub);
1235
1415
  }
1236
- if (pub.offset) {
1237
- this._offset = pub.offset;
1416
+ }
1417
+ _setPublicationPosition(pub) {
1418
+ if (this._sharedPoll && pub.key && this._sharedPollTrackedItems.has(pub.key)) {
1419
+ if (pub.removed) {
1420
+ this._sharedPollTrackedItems.delete(pub.key);
1421
+ } else if (pub.version) {
1422
+ this._sharedPollTrackedItems.set(pub.key, toOffset(pub.version));
1423
+ }
1424
+ }
1425
+ if (hasOffset(pub.offset)) {
1426
+ this._offset = toOffset(pub.offset);
1238
1427
  }
1239
1428
  if (pub.epoch) {
1240
1429
  this._epoch = pub.epoch;
@@ -1242,18 +1431,24 @@ var BaseSubscription = class extends EventEmitter {
1242
1431
  }
1243
1432
  /** Seed per-key delta tracking from state/stream entries.
1244
1433
  * Handles JSON-escaped data (server-side delta escaping) and protobuf data.
1245
- * Decodes escaped data back to original format for user consumption. */
1246
- _seedDeltaTracking(pub) {
1434
+ * Decodes escaped data back to original format for user consumption.
1435
+ * Values are escaped only when delta is negotiated: otherwise they come as
1436
+ * they are, and a string value must not be parsed. */
1437
+ _seedDeltaTracking(pub, deltaNegotiated) {
1247
1438
  if (!this._delta || !pub.key)
1248
1439
  return;
1440
+ if (pub.removed) {
1441
+ this._prevValueMap.delete(pub.key);
1442
+ return;
1443
+ }
1444
+ if (!deltaNegotiated) {
1445
+ this._prevValueMap.set(pub.key, pub.data);
1446
+ return;
1447
+ }
1249
1448
  if (typeof pub.data === "string") {
1250
1449
  const rawBytes = pub.data;
1251
1450
  const encoded = new TextEncoder().encode(rawBytes);
1252
- if (!pub.removed) {
1253
- this._prevValueMap.set(pub.key, encoded);
1254
- } else {
1255
- this._prevValueMap.delete(pub.key);
1256
- }
1451
+ this._prevValueMap.set(pub.key, encoded);
1257
1452
  const byteLen = encoded.length;
1258
1453
  this._deltaNumPubs++;
1259
1454
  this._deltaNumFull++;
@@ -1261,11 +1456,7 @@ var BaseSubscription = class extends EventEmitter {
1261
1456
  this._deltaBytesDecoded += byteLen;
1262
1457
  pub.data = JSON.parse(rawBytes);
1263
1458
  } else if (pub.data instanceof Uint8Array) {
1264
- if (!pub.removed) {
1265
- this._prevValueMap.set(pub.key, pub.data);
1266
- } else {
1267
- this._prevValueMap.delete(pub.key);
1268
- }
1459
+ this._prevValueMap.set(pub.key, pub.data);
1269
1460
  const byteLen = pub.data.length;
1270
1461
  this._deltaNumPubs++;
1271
1462
  this._deltaNumFull++;
@@ -1312,17 +1503,47 @@ var BaseSubscription = class extends EventEmitter {
1312
1503
  }
1313
1504
  const self2 = this;
1314
1505
  const delay = this._getResubscribeDelay();
1506
+ this._clearResubscribeTimeout();
1315
1507
  this._resubscribeTimeout = setTimeout(function() {
1508
+ self2._resubscribeTimeout = null;
1316
1509
  if (self2._isSubscribing()) {
1317
1510
  self2._subscribe();
1318
1511
  }
1319
1512
  }, delay);
1320
1513
  this._debug("resubscribe scheduled after " + delay, this.channel);
1321
1514
  }
1515
+ // Emits an error event, then runs the continuation: not when a handler unsubscribed,
1516
+ // maybe to subscribe again, whose new subscribe the continuation must not touch.
1517
+ _emitError(ctx, continuation) {
1518
+ const flow = this._subscribeFlow;
1519
+ try {
1520
+ this.emit("error", ctx);
1521
+ } finally {
1522
+ if (flow === this._subscribeFlow) {
1523
+ continuation();
1524
+ }
1525
+ }
1526
+ }
1322
1527
  _subscribeError(err) {
1323
1528
  if (!this._isSubscribing()) {
1324
1529
  return;
1325
1530
  }
1531
+ if (err.code === 109 && this._getToken === null) {
1532
+ try {
1533
+ this.emit("error", {
1534
+ type: "configuration",
1535
+ channel: this.channel,
1536
+ error: {
1537
+ code: errorCodes.badConfiguration,
1538
+ message: "subscription token expired but no getToken function set"
1539
+ }
1540
+ });
1541
+ } catch (e) {
1542
+ this._centrifuge._reportDispatchError(e);
1543
+ }
1544
+ this._setUnsubscribed(unsubscribedCodes.unauthorized, "unauthorized", false);
1545
+ return;
1546
+ }
1326
1547
  if (err.code < 100 || err.code === 109 || err.temporary === true) {
1327
1548
  if (err.code === 109) {
1328
1549
  this._token = "";
@@ -1333,9 +1554,10 @@ var BaseSubscription = class extends EventEmitter {
1333
1554
  error: err
1334
1555
  };
1335
1556
  if (this._centrifuge.state === State.Connected) {
1336
- this.emit("error", errContext);
1557
+ this._emitError(errContext, () => this._scheduleResubscribe());
1558
+ } else {
1559
+ this._scheduleResubscribe();
1337
1560
  }
1338
- this._scheduleResubscribe();
1339
1561
  } else {
1340
1562
  this._setUnsubscribed(err.code, err.message, false);
1341
1563
  }
@@ -1350,7 +1572,7 @@ var BaseSubscription = class extends EventEmitter {
1350
1572
  return;
1351
1573
  }
1352
1574
  if (options.since) {
1353
- this._offset = options.since.offset || 0;
1575
+ this._offset = toOffset(options.since.offset);
1354
1576
  this._epoch = options.since.epoch || "";
1355
1577
  this._recover = true;
1356
1578
  }
@@ -1418,6 +1640,9 @@ var BaseSubscription = class extends EventEmitter {
1418
1640
  }
1419
1641
  }
1420
1642
  _getOffset() {
1643
+ if (this._pendingOffset !== null) {
1644
+ return this._pendingOffset;
1645
+ }
1421
1646
  const offset = this._offset;
1422
1647
  if (offset !== null) {
1423
1648
  return offset;
@@ -1450,23 +1675,28 @@ var BaseSubscription = class extends EventEmitter {
1450
1675
  };
1451
1676
  const getToken = this._getToken;
1452
1677
  if (getToken === null) {
1453
- this.emit("error", {
1454
- type: "configuration",
1455
- channel: this.channel,
1456
- error: {
1457
- code: errorCodes.badConfiguration,
1458
- message: "provide a function to get channel subscription token"
1459
- }
1460
- });
1678
+ try {
1679
+ this.emit("error", {
1680
+ type: "configuration",
1681
+ channel: this.channel,
1682
+ error: {
1683
+ code: errorCodes.badConfiguration,
1684
+ message: "provide a function to get channel subscription token"
1685
+ }
1686
+ });
1687
+ } catch (e) {
1688
+ this._centrifuge._reportDispatchError(e);
1689
+ }
1461
1690
  return Promise.reject(new UnauthorizedError(""));
1462
1691
  }
1463
- return getToken(ctx);
1692
+ return this._callAsync(() => getToken(ctx));
1464
1693
  }
1465
1694
  _refresh() {
1466
1695
  this._clearRefreshTimeout();
1467
1696
  const self2 = this;
1697
+ const generation = this._refreshGeneration;
1468
1698
  this._getSubscriptionToken().then(function(token) {
1469
- if (!self2._isSubscribed()) {
1699
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed()) {
1470
1700
  return;
1471
1701
  }
1472
1702
  if (!token) {
@@ -1482,31 +1712,47 @@ var BaseSubscription = class extends EventEmitter {
1482
1712
  "sub_refresh": req
1483
1713
  };
1484
1714
  self2._centrifuge._call(msg).then((resolveCtx) => {
1485
- const result = resolveCtx.reply.sub_refresh;
1486
- self2._refreshResponse(result);
1487
- if (resolveCtx.next) {
1488
- resolveCtx.next();
1715
+ try {
1716
+ self2._refreshResponse(resolveCtx.reply.sub_refresh);
1717
+ } catch (err) {
1718
+ self2._centrifuge._dispatchFailed(err);
1719
+ } finally {
1720
+ if (resolveCtx.next) {
1721
+ resolveCtx.next();
1722
+ }
1489
1723
  }
1490
1724
  }, (rejectCtx) => {
1491
- self2._refreshError(rejectCtx.error);
1492
- if (rejectCtx.next) {
1493
- rejectCtx.next();
1725
+ try {
1726
+ self2._refreshError(rejectCtx.error);
1727
+ } catch (err) {
1728
+ self2._centrifuge._rejectionFailed(err, rejectCtx);
1729
+ } finally {
1730
+ if (rejectCtx.next) {
1731
+ rejectCtx.next();
1732
+ }
1494
1733
  }
1495
1734
  });
1496
1735
  }).catch(function(e) {
1736
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed()) {
1737
+ return;
1738
+ }
1497
1739
  if (e instanceof UnauthorizedError) {
1498
1740
  self2._failUnauthorized();
1499
1741
  return;
1500
1742
  }
1501
- self2.emit("error", {
1743
+ self2._emitError({
1502
1744
  type: "refreshToken",
1503
1745
  channel: self2.channel,
1504
1746
  error: {
1505
1747
  code: errorCodes.subscriptionRefreshToken,
1506
1748
  message: e !== void 0 ? e.toString() : ""
1507
1749
  }
1750
+ }, () => {
1751
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed()) {
1752
+ return;
1753
+ }
1754
+ self2._refreshTimeout = setTimeout(() => self2._refresh(), self2._getRefreshRetryDelay());
1508
1755
  });
1509
- self2._refreshTimeout = setTimeout(() => self2._refresh(), self2._getRefreshRetryDelay());
1510
1756
  });
1511
1757
  }
1512
1758
  _refreshResponse(result) {
@@ -1524,12 +1770,16 @@ var BaseSubscription = class extends EventEmitter {
1524
1770
  return;
1525
1771
  }
1526
1772
  if (err.code < 100 || err.temporary === true) {
1527
- this.emit("error", {
1773
+ this._emitError({
1528
1774
  type: "refresh",
1529
1775
  channel: this.channel,
1530
1776
  error: err
1777
+ }, () => {
1778
+ if (!this._isSubscribed()) {
1779
+ return;
1780
+ }
1781
+ this._refreshTimeout = setTimeout(() => this._refresh(), this._getRefreshRetryDelay());
1531
1782
  });
1532
- this._refreshTimeout = setTimeout(() => this._refresh(), this._getRefreshRetryDelay());
1533
1783
  } else {
1534
1784
  this._setUnsubscribed(err.code, err.message, true);
1535
1785
  }
@@ -1537,6 +1787,16 @@ var BaseSubscription = class extends EventEmitter {
1537
1787
  _getRefreshRetryDelay() {
1538
1788
  return backoff(0, 1e4, 2e4);
1539
1789
  }
1790
+ // Calls an app callback expected to return a promise: one throwing instead of
1791
+ // returning a rejected promise fails the same way, not by throwing out of the
1792
+ // subscribe or refresh in progress.
1793
+ _callAsync(callback) {
1794
+ try {
1795
+ return Promise.resolve(callback());
1796
+ } catch (e) {
1797
+ return Promise.reject(e);
1798
+ }
1799
+ }
1540
1800
  _failUnauthorized() {
1541
1801
  this._setUnsubscribed(unsubscribedCodes.unauthorized, "unauthorized", true);
1542
1802
  }
@@ -1569,7 +1829,8 @@ var BaseSubscription = class extends EventEmitter {
1569
1829
  }
1570
1830
  if (current.length > 0)
1571
1831
  frames.push(current);
1572
- const send = (frame, frameUntrackKeys) => new Promise((resolve, reject) => {
1832
+ const keysFrom = (idx) => frames.slice(idx).flatMap((f) => f.flatMap((b) => b.items.map((i) => i.key)));
1833
+ const send = (frame, idx, frameUntrackKeys) => new Promise((resolve, reject) => {
1573
1834
  const req = {
1574
1835
  channel: this.channel,
1575
1836
  type: 1,
@@ -1583,20 +1844,55 @@ var BaseSubscription = class extends EventEmitter {
1583
1844
  }
1584
1845
  const msg = { "sub_refresh": req };
1585
1846
  this._centrifuge._call(msg).then((resolveCtx) => {
1586
- this._handleTrackResponse(resolveCtx.reply.sub_refresh);
1587
- if (resolveCtx.next)
1588
- resolveCtx.next();
1847
+ try {
1848
+ this._handleTrackResponse(resolveCtx.reply.sub_refresh, frame.flatMap((b) => b.items.map((i) => i.key)), frameUntrackKeys);
1849
+ } catch (err) {
1850
+ this._centrifuge._dispatchFailed(err);
1851
+ } finally {
1852
+ if (resolveCtx.next)
1853
+ resolveCtx.next();
1854
+ }
1589
1855
  resolve();
1590
1856
  }, (rejectCtx) => {
1591
1857
  if (rejectCtx.next)
1592
1858
  rejectCtx.next();
1593
- reject(rejectCtx.error);
1859
+ this._sharedPollRequestFailed(rejectCtx.error);
1860
+ reject({ error: rejectCtx.error, keys: keysFrom(idx) });
1594
1861
  });
1595
1862
  });
1596
- return frames.reduce((chain, frame, idx) => chain.then(() => send(frame, idx === 0 ? untrackKeys : void 0)), Promise.resolve());
1863
+ return this._sharedPollSerial(() => frames.reduce((chain, frame, idx) => chain.then(() => send(frame, idx, idx === 0 ? untrackKeys : void 0)), Promise.resolve()));
1864
+ }
1865
+ _isEmulation() {
1866
+ const transport = this._centrifuge._transport;
1867
+ return !!transport && transport.emulation();
1868
+ }
1869
+ // Over emulation each request is a separate HTTP request the server may handle
1870
+ // in any order: a track and an untrack of a key could overtake each other. The
1871
+ // channel's requests go one at a time then, in the order made; one made for a
1872
+ // subscription that has ended by its turn is dropped.
1873
+ _sharedPollSerial(send) {
1874
+ if (!this._isEmulation()) {
1875
+ return send();
1876
+ }
1877
+ const generation = this._refreshGeneration;
1878
+ const run = this._sharedPollQueue.then(() => {
1879
+ if (generation !== this._refreshGeneration || !this._isSubscribed()) {
1880
+ return;
1881
+ }
1882
+ return send();
1883
+ });
1884
+ this._sharedPollQueue = run.then(() => void 0, () => void 0);
1885
+ return run;
1886
+ }
1887
+ // A timed-out request over emulation may still reach the server after later
1888
+ // ones: whether the keys are tracked is unknown, the client reconnects.
1889
+ _sharedPollRequestFailed(error) {
1890
+ if (error && error.code === errorCodes.timeout && this._isEmulation()) {
1891
+ this._centrifuge._disconnect(connectingCodes.subscribeTimeout, "shared poll request timeout", true);
1892
+ }
1597
1893
  }
1598
1894
  _sendUntrackRequest(keys) {
1599
- return new Promise((resolve, reject) => {
1895
+ return this._sharedPollSerial(() => new Promise((resolve, reject) => {
1600
1896
  const req = {
1601
1897
  channel: this.channel,
1602
1898
  type: 2,
@@ -1612,19 +1908,36 @@ var BaseSubscription = class extends EventEmitter {
1612
1908
  if (rejectCtx.next) {
1613
1909
  rejectCtx.next();
1614
1910
  }
1911
+ this._sharedPollRequestFailed(rejectCtx.error);
1615
1912
  reject(rejectCtx.error);
1616
1913
  });
1617
- });
1914
+ }));
1618
1915
  }
1619
- _handleTrackResponse(result) {
1916
+ _handleTrackResponse(result, keys, untrackKeys) {
1917
+ if (!this._isSubscribed()) {
1918
+ return;
1919
+ }
1620
1920
  this._clearSharedPollTrackRetry();
1621
1921
  if (result && result.items && result.items.length > 0) {
1622
1922
  for (const pub of result.items) {
1923
+ if (!this._isSubscribed()) {
1924
+ return;
1925
+ }
1623
1926
  this._handlePublication(pub);
1624
1927
  }
1625
1928
  }
1626
- if (result && result.expires === true && result.ttl > 0) {
1627
- const targetMs = Date.now() + result.ttl * 1e3;
1929
+ if (!this._isSubscribed()) {
1930
+ return;
1931
+ }
1932
+ const untracked = new Set(untrackKeys || []);
1933
+ const stale = keys.filter((key) => !untracked.has(key) && !this._sharedPollTrackedItems.has(key));
1934
+ if (stale.length > 0) {
1935
+ this._sendUntrackRequest(stale).catch((err) => {
1936
+ this.emit("error", { type: "untrack", channel: this.channel, error: err });
1937
+ });
1938
+ }
1939
+ if (result && result.expires === true) {
1940
+ const targetMs = Date.now() + (result.ttl > 0 ? result.ttl * 1e3 : 1e3);
1628
1941
  this._maybeScheduleSharedPollSignatureRefresh(targetMs);
1629
1942
  }
1630
1943
  }
@@ -1660,6 +1973,12 @@ var BaseSubscription = class extends EventEmitter {
1660
1973
  }
1661
1974
  this._sharedPollTrackRetryAttempts = 0;
1662
1975
  }
1976
+ _scheduleSharedPollReplayRetry() {
1977
+ if (this._sharedPollReplayRetryTimeout !== null) {
1978
+ clearTimeout(this._sharedPollReplayRetryTimeout);
1979
+ }
1980
+ this._sharedPollReplayRetryTimeout = setTimeout(() => this._sharedPollReplayTrack(), backoff(this._sharedPollReplayRetryAttempts++, 5e3, 3e4));
1981
+ }
1663
1982
  _clearSharedPollReplayRetry() {
1664
1983
  if (this._sharedPollReplayRetryTimeout !== null) {
1665
1984
  clearTimeout(this._sharedPollReplayRetryTimeout);
@@ -1667,21 +1986,59 @@ var BaseSubscription = class extends EventEmitter {
1667
1986
  }
1668
1987
  this._sharedPollReplayRetryAttempts = 0;
1669
1988
  }
1670
- _handleTrackError(err) {
1989
+ // Adds a signature to the library, dropping the entries whose keys it covers
1990
+ // entirely: they may have expired, and a replay sends every entry in one request,
1991
+ // which a single expired signature fails as a whole.
1992
+ _addSharedPollSignature(keys, signature) {
1993
+ const covered = new Set(keys);
1994
+ this._sharedPollSignatures = this._sharedPollSignatures.filter((entry) => !entry.keys.every((key) => covered.has(key)));
1995
+ this._sharedPollSignatures.push({ keys, signature });
1996
+ }
1997
+ _handleTrackError(err, keys) {
1671
1998
  if (!this._isSubscribed()) {
1672
1999
  return;
1673
2000
  }
1674
- this.emit("error", {
2001
+ this._emitError({
1675
2002
  type: "track",
1676
2003
  channel: this.channel,
1677
2004
  error: err
2005
+ }, () => {
2006
+ if (!this._isSubscribed()) {
2007
+ return;
2008
+ }
2009
+ if (err.code === 109) {
2010
+ this._sharedPollRefreshSignature();
2011
+ return;
2012
+ }
2013
+ if (err.code < 100 || err.temporary === true) {
2014
+ this._sharedPollTrackRetryTimeout = setTimeout(() => this._sharedPollReplayTrack(), backoff(this._sharedPollTrackRetryAttempts++, 1e3, 15e3));
2015
+ return;
2016
+ }
2017
+ this._untrackFailedKeys(keys);
1678
2018
  });
1679
- if (err.code === 109) {
1680
- this._sharedPollRefreshSignature();
2019
+ }
2020
+ _untrackFailedKeys(keys) {
2021
+ const failed = keys.filter((key) => this._sharedPollTrackedItems.has(key));
2022
+ if (failed.length === 0) {
1681
2023
  return;
1682
2024
  }
1683
- if (err.code < 100 || err.temporary === true) {
1684
- this._sharedPollTrackRetryTimeout = setTimeout(() => this._sharedPollReplayTrack(), backoff(this._sharedPollTrackRetryAttempts++, 1e3, 15e3));
2025
+ for (const key of failed) {
2026
+ this._sharedPollTrackedItems.delete(key);
2027
+ }
2028
+ this._sharedPollSignatures = this._sharedPollSignatures.filter((entry) => entry.keys.some((k) => this._sharedPollTrackedItems.has(k)));
2029
+ this._sendUntrackRequest(failed).catch((err) => {
2030
+ this.emit("error", { type: "untrack", channel: this.channel, error: err });
2031
+ });
2032
+ for (const key of failed) {
2033
+ if (!this._isSubscribed()) {
2034
+ return;
2035
+ }
2036
+ this.emit("update", {
2037
+ channel: this.channel,
2038
+ key,
2039
+ data: null,
2040
+ removed: true
2041
+ });
1685
2042
  }
1686
2043
  }
1687
2044
  // _sharedPollRefreshSignature obtains a fresh consolidated signature
@@ -1697,23 +2054,41 @@ var BaseSubscription = class extends EventEmitter {
1697
2054
  return;
1698
2055
  if (this._sharedPollTrackedItems.size === 0)
1699
2056
  return;
1700
- if (this._sharedPollSignatureRefreshInFlight)
2057
+ if (this._sharedPollSignatureRefreshInFlight) {
2058
+ for (const key of this._sharedPollTrackedItems.keys()) {
2059
+ if (!this._sharedPollSignatureRefreshKeys.has(key)) {
2060
+ this._sharedPollSignatureRefreshAgain = true;
2061
+ break;
2062
+ }
2063
+ }
1701
2064
  return;
2065
+ }
1702
2066
  this._sharedPollSignatureRefreshTargetMs = null;
1703
2067
  this._sharedPollSignatureRefreshInFlight = true;
1704
2068
  const keys = Array.from(this._sharedPollTrackedItems.keys());
2069
+ this._sharedPollSignatureRefreshKeys = new Set(keys);
1705
2070
  const self2 = this;
1706
- this._sharedPollGetSignature({ keys }).then((result) => {
2071
+ const generation = this._refreshGeneration;
2072
+ this._callAsync(() => this._sharedPollGetSignature({ keys })).then((result) => {
2073
+ if (generation !== self2._refreshGeneration)
2074
+ return;
1707
2075
  self2._sharedPollSignatureRefreshInFlight = false;
1708
2076
  if (!self2._isSubscribed())
1709
2077
  return;
1710
2078
  self2._sharedPollSignatureRefreshAttempts = 0;
2079
+ if (self2._sharedPollSignatureRefreshAgain) {
2080
+ self2._sharedPollSignatureRefreshAgain = false;
2081
+ Promise.resolve().then(() => self2._sharedPollRefreshSignature());
2082
+ }
1711
2083
  const returnedKeys = new Set(result.keys);
1712
2084
  const revokedKeys = [];
1713
2085
  for (const key of keys) {
1714
- if (!returnedKeys.has(key)) {
2086
+ if (!returnedKeys.has(key) && this._sharedPollTrackedItems.has(key)) {
1715
2087
  self2._sharedPollTrackedItems.delete(key);
1716
2088
  revokedKeys.push(key);
2089
+ if (!self2._isSubscribed()) {
2090
+ continue;
2091
+ }
1717
2092
  self2.emit("update", {
1718
2093
  channel: self2.channel,
1719
2094
  key,
@@ -1739,20 +2114,27 @@ var BaseSubscription = class extends EventEmitter {
1739
2114
  self2._sharedPollSignatures = items.length > 0 ? [{ keys: result.keys, signature: result.signature }, ...uncovered] : uncovered;
1740
2115
  if (items.length === 0)
1741
2116
  return;
1742
- self2._sendTrackRequest([{ items, signature: result.signature }]).catch((err) => {
1743
- self2._handleTrackError(err);
2117
+ self2._sendTrackRequest([{ items, signature: result.signature }]).catch((e) => {
2118
+ self2._handleTrackError(e.error, e.keys);
1744
2119
  });
1745
2120
  }).catch((e) => {
2121
+ if (generation !== self2._refreshGeneration)
2122
+ return;
1746
2123
  self2._sharedPollSignatureRefreshInFlight = false;
1747
- self2.emit("error", {
2124
+ self2._sharedPollSignatureRefreshAgain = false;
2125
+ self2._emitError({
1748
2126
  type: "signatureRefresh",
1749
2127
  channel: self2.channel,
1750
2128
  error: {
1751
2129
  code: errorCodes.sharedPollGetSignature,
1752
2130
  message: e !== void 0 ? e.toString() : ""
1753
2131
  }
2132
+ }, () => {
2133
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed()) {
2134
+ return;
2135
+ }
2136
+ self2._sharedPollSignatureRefreshTimeout = setTimeout(() => self2._sharedPollRefreshSignature(), backoff(self2._sharedPollSignatureRefreshAttempts++, 5e3, 3e4));
1754
2137
  });
1755
- self2._sharedPollSignatureRefreshTimeout = setTimeout(() => self2._sharedPollRefreshSignature(), backoff(self2._sharedPollSignatureRefreshAttempts++, 5e3, 3e4));
1756
2138
  });
1757
2139
  }
1758
2140
  // _sharedPollReplayTrack runs after subscribe completes — on initial
@@ -1801,8 +2183,8 @@ var BaseSubscription = class extends EventEmitter {
1801
2183
  }
1802
2184
  }
1803
2185
  if (replayBatches.length > 0) {
1804
- this._sendTrackRequest(replayBatches, allUntrackedInReplay).catch((err) => {
1805
- this._handleTrackError(err);
2186
+ this._sendTrackRequest(replayBatches, allUntrackedInReplay).catch((e) => {
2187
+ this._handleTrackError(e.error, e.keys);
1806
2188
  });
1807
2189
  }
1808
2190
  const uncoveredKeys = [];
@@ -1821,16 +2203,20 @@ var BaseSubscription = class extends EventEmitter {
1821
2203
  return;
1822
2204
  }
1823
2205
  const self2 = this;
1824
- this._sharedPollGetSignature({ keys: uncoveredKeys }).then((result) => {
1825
- if (!self2._isSubscribed())
2206
+ const generation = this._refreshGeneration;
2207
+ this._callAsync(() => this._sharedPollGetSignature({ keys: uncoveredKeys })).then((result) => {
2208
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed())
1826
2209
  return;
1827
2210
  self2._clearSharedPollReplayRetry();
1828
2211
  const returnedKeys = new Set(result.keys);
1829
2212
  const revokedKeys = [];
1830
2213
  for (const key of uncoveredKeys) {
1831
- if (!returnedKeys.has(key)) {
2214
+ if (!returnedKeys.has(key) && this._sharedPollTrackedItems.has(key)) {
1832
2215
  self2._sharedPollTrackedItems.delete(key);
1833
2216
  revokedKeys.push(key);
2217
+ if (!self2._isSubscribed()) {
2218
+ continue;
2219
+ }
1834
2220
  self2.emit("update", {
1835
2221
  channel: self2.channel,
1836
2222
  key,
@@ -1853,46 +2239,60 @@ var BaseSubscription = class extends EventEmitter {
1853
2239
  }
1854
2240
  if (items.length === 0)
1855
2241
  return;
1856
- self2._sharedPollSignatures.push({
1857
- keys: result.keys,
1858
- signature: result.signature
1859
- });
1860
- self2._sendTrackRequest([{ items, signature: result.signature }]).catch((err) => {
1861
- self2._handleTrackError(err);
2242
+ self2._addSharedPollSignature(result.keys, result.signature);
2243
+ self2._sendTrackRequest([{ items, signature: result.signature }]).catch((e) => {
2244
+ self2._handleTrackError(e.error, e.keys);
1862
2245
  });
1863
2246
  }).catch((e) => {
1864
- self2.emit("error", {
2247
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed())
2248
+ return;
2249
+ self2._emitError({
1865
2250
  type: "signatureRefresh",
1866
2251
  channel: self2.channel,
1867
2252
  error: {
1868
2253
  code: errorCodes.sharedPollGetSignature,
1869
2254
  message: e !== void 0 ? e.toString() : ""
1870
2255
  }
2256
+ }, () => {
2257
+ if (generation !== self2._refreshGeneration || !self2._isSubscribed())
2258
+ return;
2259
+ self2._sharedPollReplayRetryTimeout = setTimeout(() => self2._sharedPollReplayTrack(), backoff(self2._sharedPollReplayRetryAttempts++, 5e3, 3e4));
1871
2260
  });
1872
- self2._sharedPollReplayRetryTimeout = setTimeout(() => self2._sharedPollReplayTrack(), backoff(self2._sharedPollReplayRetryAttempts++, 5e3, 3e4));
1873
2261
  });
1874
2262
  }
1875
2263
  // ============ Keyed Subscription Methods ============
1876
2264
  /** Entry point for map subscriptions */
1877
2265
  _mapSubscribe() {
1878
2266
  this._debug("starting map subscribe on", this.channel);
1879
- this._mapStateBuffer = [];
1880
- this._mapStreamBuffer = [];
1881
- this._mapCursor = "";
1882
- if (!(this._recover && this._offset !== null && this._epoch !== null)) {
1883
- this._prevValueMap = /* @__PURE__ */ new Map();
1884
- }
1885
- this._mapPhase = MapPhase.State;
1886
- if (this._recover && this._offset !== null && this._epoch !== null) {
1887
- this._debug("map subscribe: recovering from position, skipping to stream phase");
1888
- this._mapPhase = MapPhase.Stream;
1889
- this._fetchStream();
1890
- return;
1891
- }
2267
+ const flow = ++this._subscribeFlow;
2268
+ const start = () => {
2269
+ this._mapStateBuffer = [];
2270
+ this._mapStreamBuffer = [];
2271
+ this._mapCursor = "";
2272
+ const recovering = this._recover && this._offset !== null && this._epoch !== null;
2273
+ if (!recovering) {
2274
+ this._prevValueMap = /* @__PURE__ */ new Map();
2275
+ }
2276
+ this._mapFlowRecovering = recovering;
2277
+ this._mapFlowOffset = recovering ? this._offset : 0;
2278
+ this._mapFlowEpoch = recovering ? this._epoch : null;
2279
+ this._mapFlowPositionResets = this._mapPositionResets;
2280
+ this._mapPhase = MapPhase.State;
2281
+ if (recovering) {
2282
+ this._debug("map subscribe: recovering from position, skipping to stream phase");
2283
+ this._mapPhase = MapPhase.Stream;
2284
+ this._fetchStream(flow);
2285
+ } else {
2286
+ this._fetchSnapshot(flow);
2287
+ }
2288
+ };
1892
2289
  if (this._canSubscribeWithoutGettingToken()) {
1893
- this._fetchSnapshot();
2290
+ start();
1894
2291
  } else {
1895
2292
  this._getSubscriptionToken().then((token) => {
2293
+ if (flow !== this._subscribeFlow) {
2294
+ return;
2295
+ }
1896
2296
  if (!this._isSubscribing()) {
1897
2297
  this._inflight = false;
1898
2298
  return;
@@ -1903,171 +2303,212 @@ var BaseSubscription = class extends EventEmitter {
1903
2303
  return;
1904
2304
  }
1905
2305
  this._token = token;
1906
- this._fetchSnapshot();
1907
- }).catch((e) => this._handleTokenError(e));
2306
+ start();
2307
+ }).catch((e) => {
2308
+ this._handleTokenError(e, flow);
2309
+ });
1908
2310
  }
1909
2311
  }
1910
2312
  /** Fetch a page of snapshot data */
1911
- _fetchSnapshot(cursor) {
2313
+ _fetchSnapshot(flow, cursor) {
1912
2314
  if (!this._isSubscribing() || !this._isTransportOpen()) {
1913
2315
  this._inflight = false;
1914
2316
  return;
1915
2317
  }
1916
2318
  const cmd = this._buildMapSubscribeCommand(MapPhase.State, cursor);
1917
2319
  this._debug("map subscribe: fetching snapshot page", cursor ? `cursor=${cursor}` : "initial");
2320
+ this._subscribeCallFlow = flow;
1918
2321
  this._centrifuge._call(cmd).then((resolveCtx) => {
1919
- const result = resolveCtx.reply.subscribe;
1920
- this._handleMapStateResponse(result);
1921
- if (resolveCtx.next) {
1922
- resolveCtx.next();
2322
+ try {
2323
+ if (this._settleSubscribeCall(flow, true)) {
2324
+ return;
2325
+ }
2326
+ if (flow === this._subscribeFlow) {
2327
+ this._handleMapStateResponse(flow, resolveCtx.reply.subscribe);
2328
+ }
2329
+ } catch (err) {
2330
+ this._centrifuge._dispatchFailed(err);
2331
+ } finally {
2332
+ if (resolveCtx.next) {
2333
+ resolveCtx.next();
2334
+ }
1923
2335
  }
1924
2336
  }, (rejectCtx) => {
1925
- this._handleMapSubscribeError(rejectCtx.error);
1926
- if (rejectCtx.next) {
1927
- rejectCtx.next();
1928
- }
1929
- });
1930
- }
1931
- /** Process snapshot response */
1932
- _handleMapStateResponse(result) {
1933
- if (!this._isSubscribing()) {
1934
- this._inflight = false;
2337
+ try {
2338
+ if (this._settleSubscribeCall(flow, false, rejectCtx.error)) {
2339
+ return;
2340
+ }
2341
+ if (flow === this._subscribeFlow) {
2342
+ this._handleMapSubscribeError(rejectCtx.error);
2343
+ }
2344
+ } catch (err) {
2345
+ this._centrifuge._rejectionFailed(err, rejectCtx);
2346
+ } finally {
2347
+ if (rejectCtx.next) {
2348
+ rejectCtx.next();
2349
+ }
2350
+ }
2351
+ });
2352
+ }
2353
+ /** Process snapshot response */
2354
+ _handleMapStateResponse(flow, result) {
2355
+ if (!this._isSubscribing()) {
2356
+ this._inflight = false;
1935
2357
  return;
1936
2358
  }
1937
2359
  if (!result.phase) {
1938
2360
  this._debug("map subscribe: server forced LIVE transition during state pagination");
1939
- this._handleMapLiveResponse(result);
2361
+ this._handleMapLiveResponse(flow, result);
1940
2362
  return;
1941
2363
  }
1942
- if (!this._epoch && result.epoch) {
1943
- this._epoch = result.epoch;
1944
- this._offset = result.offset || 0;
2364
+ if (!this._mapFlowEpoch && result.epoch) {
2365
+ this._mapFlowEpoch = result.epoch;
2366
+ this._mapFlowOffset = toOffset(result.offset);
1945
2367
  }
1946
- if (this._epoch && result.epoch && this._epoch !== result.epoch) {
2368
+ if (this._mapFlowEpoch && result.epoch && this._mapFlowEpoch !== result.epoch) {
1947
2369
  this._debug("map subscribe: epoch changed during snapshot pagination, restarting");
1948
- this._mapStateBuffer = [];
1949
- this._mapCursor = "";
1950
- this._epoch = null;
1951
- this._offset = null;
1952
- this._prevValueMap = /* @__PURE__ */ new Map();
1953
- this._fetchSnapshot();
2370
+ this._restartMapFlow();
2371
+ this._fetchSnapshot(flow);
1954
2372
  return;
1955
2373
  }
1956
2374
  if (result.state && result.state.length > 0) {
1957
- for (const pub of result.state) {
1958
- this._seedDeltaTracking(pub);
1959
- this._mapStateBuffer.push(this._getMapUpdateContext(pub));
1960
- }
2375
+ this._mapStateBuffer.push(...result.state);
1961
2376
  }
1962
2377
  if (result.cursor) {
1963
2378
  this._mapCursor = result.cursor;
1964
- this._fetchSnapshot(this._mapCursor);
2379
+ this._fetchSnapshot(flow, this._mapCursor);
1965
2380
  return;
1966
2381
  }
1967
- this._transitionFromSnapshot();
2382
+ this._transitionFromSnapshot(flow);
2383
+ }
2384
+ // The stream epoch changed during the flow: what it collected and the stored
2385
+ // position are outdated. The flow continues from a snapshot.
2386
+ _restartMapFlow() {
2387
+ this._mapStateBuffer = [];
2388
+ this._mapStreamBuffer = [];
2389
+ this._mapCursor = "";
2390
+ this._mapFlowRecovering = false;
2391
+ this._mapFlowOffset = 0;
2392
+ this._mapFlowEpoch = null;
2393
+ this._epoch = null;
2394
+ this._offset = null;
2395
+ this._recover = false;
2396
+ this._prevValueMap = /* @__PURE__ */ new Map();
2397
+ this._mapPhase = MapPhase.State;
1968
2398
  }
1969
2399
  /** Transition from STATE to STREAM phase after snapshot pagination completes */
1970
- _transitionFromSnapshot() {
2400
+ _transitionFromSnapshot(flow) {
1971
2401
  this._debug("map subscribe: snapshot complete, transitioning to stream phase");
1972
2402
  this._mapPhase = MapPhase.Stream;
1973
- this._fetchStream();
2403
+ this._fetchStream(flow);
1974
2404
  }
1975
2405
  /** Fetch stream data (offset-based catch-up) */
1976
- _fetchStream() {
2406
+ _fetchStream(flow) {
1977
2407
  if (!this._isSubscribing() || !this._isTransportOpen()) {
1978
2408
  this._inflight = false;
1979
2409
  return;
1980
2410
  }
1981
2411
  const cmd = this._buildMapSubscribeCommand(MapPhase.Stream);
1982
- this._debug("map subscribe: fetching stream from offset", this._offset);
2412
+ this._debug("map subscribe: fetching stream from offset", this._mapFlowOffset);
2413
+ this._subscribeCallFlow = flow;
1983
2414
  this._centrifuge._call(cmd).then((resolveCtx) => {
1984
- const result = resolveCtx.reply.subscribe;
1985
- this._handleMapStreamResponse(result);
1986
- if (resolveCtx.next) {
1987
- resolveCtx.next();
2415
+ try {
2416
+ if (this._settleSubscribeCall(flow, true)) {
2417
+ return;
2418
+ }
2419
+ if (flow === this._subscribeFlow) {
2420
+ this._handleMapStreamResponse(flow, resolveCtx.reply.subscribe);
2421
+ }
2422
+ } catch (err) {
2423
+ this._centrifuge._dispatchFailed(err);
2424
+ } finally {
2425
+ if (resolveCtx.next) {
2426
+ resolveCtx.next();
2427
+ }
1988
2428
  }
1989
2429
  }, (rejectCtx) => {
1990
- this._handleMapSubscribeError(rejectCtx.error);
1991
- if (rejectCtx.next) {
1992
- rejectCtx.next();
2430
+ try {
2431
+ if (this._settleSubscribeCall(flow, false, rejectCtx.error)) {
2432
+ return;
2433
+ }
2434
+ if (flow === this._subscribeFlow) {
2435
+ this._handleMapSubscribeError(rejectCtx.error);
2436
+ }
2437
+ } catch (err) {
2438
+ this._centrifuge._rejectionFailed(err, rejectCtx);
2439
+ } finally {
2440
+ if (rejectCtx.next) {
2441
+ rejectCtx.next();
2442
+ }
1993
2443
  }
1994
2444
  });
1995
2445
  }
1996
2446
  /** Process stream response */
1997
- _handleMapStreamResponse(result) {
2447
+ _handleMapStreamResponse(flow, result) {
1998
2448
  if (!this._isSubscribing()) {
1999
2449
  this._inflight = false;
2000
2450
  return;
2001
2451
  }
2002
2452
  if (!result.phase) {
2003
2453
  this._debug("map subscribe: server forced LIVE transition during stream");
2004
- this._handleMapLiveResponse(result);
2454
+ this._handleMapLiveResponse(flow, result);
2005
2455
  return;
2006
2456
  }
2007
- if (this._epoch && result.epoch && this._epoch !== result.epoch) {
2457
+ if (this._mapFlowEpoch && result.epoch && this._mapFlowEpoch !== result.epoch) {
2008
2458
  this._debug("map subscribe: epoch changed during stream, restarting");
2009
- this._mapStateBuffer = [];
2010
- this._mapStreamBuffer = [];
2011
- this._epoch = null;
2012
- this._offset = null;
2013
- this._prevValueMap = /* @__PURE__ */ new Map();
2014
- this._mapPhase = MapPhase.State;
2015
- this._fetchSnapshot();
2459
+ this._restartMapFlow();
2460
+ this._fetchSnapshot(flow);
2016
2461
  return;
2017
2462
  }
2018
2463
  if (result.publications && result.publications.length > 0) {
2019
- for (const pub of result.publications) {
2020
- this._seedDeltaTracking(pub);
2021
- this._mapStreamBuffer.push(this._getMapUpdateContext(pub));
2022
- }
2464
+ this._mapStreamBuffer.push(...result.publications);
2023
2465
  }
2024
2466
  if (result.offset !== void 0) {
2025
- this._offset = result.offset;
2467
+ this._mapFlowOffset = toOffset(result.offset);
2026
2468
  }
2027
- this._fetchStream();
2469
+ this._fetchStream(flow);
2028
2470
  }
2029
2471
  /** Process live response - complete the map subscription */
2030
- _handleMapLiveResponse(result) {
2472
+ _handleMapLiveResponse(flow, result) {
2031
2473
  if (!this._isSubscribing()) {
2032
2474
  this._inflight = false;
2033
2475
  return;
2034
2476
  }
2035
2477
  this._inflight = false;
2036
- if (this._epoch && result.epoch && this._epoch !== result.epoch) {
2478
+ if (this._mapFlowEpoch && result.epoch && this._mapFlowEpoch !== result.epoch) {
2037
2479
  this._debug("map subscribe: epoch changed during live transition, restarting");
2038
- this._mapStateBuffer = [];
2039
- this._mapStreamBuffer = [];
2040
- this._epoch = null;
2041
- this._offset = null;
2042
- this._prevValueMap = /* @__PURE__ */ new Map();
2480
+ this._restartMapFlow();
2043
2481
  this._inflight = true;
2044
- this._mapPhase = MapPhase.State;
2045
- this._fetchSnapshot();
2482
+ this._fetchSnapshot(flow);
2046
2483
  return;
2047
2484
  }
2048
- if (result.state && result.state.length > 0) {
2049
- for (const pub of result.state) {
2050
- this._seedDeltaTracking(pub);
2051
- this._mapStateBuffer.push(this._getMapUpdateContext(pub));
2052
- }
2053
- }
2485
+ const deltaNegotiated = !!(this._delta && result.delta);
2486
+ const statePubs = result.state && result.state.length > 0 ? this._mapStateBuffer.concat(result.state) : this._mapStateBuffer;
2487
+ this._mapStateBuffer = statePubs.map((pub) => {
2488
+ this._seedDeltaTracking(pub, deltaNegotiated);
2489
+ return this._getMapUpdateContext(pub);
2490
+ });
2491
+ this._mapStreamBuffer = this._mapStreamBuffer.map((pub) => {
2492
+ this._seedDeltaTracking(pub, deltaNegotiated);
2493
+ return this._getMapUpdateContext(pub);
2494
+ });
2054
2495
  if (result.publications && result.publications.length > 0) {
2055
2496
  for (const pub of result.publications) {
2056
- if (this._delta && result.delta) {
2497
+ if (deltaNegotiated) {
2057
2498
  const deltaKey = pub.key || "";
2058
- const { newData, newPrevValue, isDelta, wireBytes, fullBytes } = this._centrifuge._codec.applyDeltaIfNeeded(pub, this._prevValueMap.get(deltaKey));
2059
- pub.data = newData;
2060
- this._deltaNumPubs++;
2061
- this._deltaBytesReceived += wireBytes;
2062
- this._deltaBytesDecoded += fullBytes;
2063
- if (isDelta) {
2064
- this._deltaNumDelta++;
2065
- } else {
2066
- this._deltaNumFull++;
2067
- }
2068
2499
  if (pub.removed) {
2069
2500
  this._prevValueMap.delete(deltaKey);
2070
2501
  } else {
2502
+ const { newData, newPrevValue, isDelta, wireBytes, fullBytes } = this._centrifuge._codec.applyDeltaIfNeeded(pub, this._prevValueMap.get(deltaKey));
2503
+ pub.data = newData;
2504
+ this._deltaNumPubs++;
2505
+ this._deltaBytesReceived += wireBytes;
2506
+ this._deltaBytesDecoded += fullBytes;
2507
+ if (isDelta) {
2508
+ this._deltaNumDelta++;
2509
+ } else {
2510
+ this._deltaNumFull++;
2511
+ }
2071
2512
  this._prevValueMap.set(deltaKey, newPrevValue);
2072
2513
  }
2073
2514
  } else if (this._delta && pub.key) {
@@ -2080,13 +2521,13 @@ var BaseSubscription = class extends EventEmitter {
2080
2521
  this._mapStreamBuffer.push(this._getMapUpdateContext(pub));
2081
2522
  }
2082
2523
  }
2083
- this._offset = result.offset || 0;
2084
- this._epoch = result.epoch || "";
2524
+ const offset = toOffset(result.offset);
2525
+ const epoch = result.epoch || "";
2085
2526
  this._clearSubscribingState();
2086
2527
  if (result.id) {
2087
2528
  this._id = result.id;
2088
2529
  }
2089
- this._recover = result.recoverable === true;
2530
+ const recover = result.recoverable === true;
2090
2531
  if (result.delta) {
2091
2532
  this._delta_negotiated = true;
2092
2533
  } else {
@@ -2095,36 +2536,78 @@ var BaseSubscription = class extends EventEmitter {
2095
2536
  if (result.publish_debounce) {
2096
2537
  this._debounceMs = result.publish_debounce;
2097
2538
  }
2098
- this._setState(SubscriptionState.Subscribed);
2539
+ let stateEntries = this._mapStateBuffer;
2540
+ let streamEntries = this._mapStreamBuffer;
2541
+ this._mapStateBuffer = [];
2542
+ this._mapStreamBuffer = [];
2543
+ this._mapPhase = null;
2099
2544
  const ctx = this._centrifuge._getSubscribeContext(this.channel, result);
2100
- ctx.state = this._mapStateBuffer;
2545
+ ctx.state = stateEntries;
2546
+ const positionResets = this._mapFlowPositionResets;
2547
+ if (this._mapPositionResets === positionResets) {
2548
+ this._recover = recover;
2549
+ this._epoch = epoch;
2550
+ if (!ctx.recovered) {
2551
+ this._recover = false;
2552
+ this._offset = null;
2553
+ this._epoch = null;
2554
+ } else if (streamEntries.length > 0 && streamEntries[0].offset !== void 0) {
2555
+ this._offset = streamEntries[0].offset - 1;
2556
+ } else {
2557
+ this._offset = offset;
2558
+ }
2559
+ }
2560
+ this._setState(SubscriptionState.Subscribed);
2561
+ if (!this._isSubscribed()) {
2562
+ return;
2563
+ }
2101
2564
  this.emit("subscribed", ctx);
2565
+ if (!this._isSubscribed()) {
2566
+ return;
2567
+ }
2102
2568
  this._resolvePromises();
2103
2569
  if (!ctx.recovered) {
2104
- if (this._mapStreamBuffer.length > 0) {
2570
+ if (streamEntries.length > 0) {
2105
2571
  const stateMap = /* @__PURE__ */ new Map();
2106
- for (const entry of this._mapStateBuffer) {
2572
+ for (const entry of stateEntries) {
2107
2573
  stateMap.set(entry.key, entry);
2108
2574
  }
2109
- for (const entry of this._mapStreamBuffer) {
2575
+ for (const entry of streamEntries) {
2110
2576
  if (entry.removed) {
2111
2577
  stateMap.delete(entry.key);
2112
2578
  } else {
2113
2579
  stateMap.set(entry.key, entry);
2114
2580
  }
2115
2581
  }
2116
- this._mapStateBuffer = Array.from(stateMap.values());
2117
- this._mapStreamBuffer = [];
2582
+ stateEntries = Array.from(stateMap.values());
2583
+ streamEntries = [];
2118
2584
  }
2119
- this.emit("sync", { entries: this._mapStateBuffer });
2585
+ if (this._mapPositionResets === positionResets) {
2586
+ this._recover = recover;
2587
+ this._offset = offset;
2588
+ this._epoch = epoch;
2589
+ }
2590
+ this.emit("sync", { entries: stateEntries });
2120
2591
  }
2121
- for (const pub of this._mapStreamBuffer) {
2122
- this.emit("publication", pub);
2123
- this.emit("update", pub);
2592
+ for (const entry of streamEntries) {
2593
+ if (!this._isSubscribed()) {
2594
+ return;
2595
+ }
2596
+ this.emit("publication", entry);
2597
+ if (!this._isSubscribed()) {
2598
+ return;
2599
+ }
2600
+ if (entry.offset !== void 0 && this._mapPositionResets === positionResets) {
2601
+ this._offset = entry.offset;
2602
+ }
2603
+ this.emit("update", entry);
2604
+ }
2605
+ if (!this._isSubscribed()) {
2606
+ return;
2607
+ }
2608
+ if (this._mapPositionResets === positionResets) {
2609
+ this._offset = offset;
2124
2610
  }
2125
- this._mapStateBuffer = [];
2126
- this._mapStreamBuffer = [];
2127
- this._mapPhase = null;
2128
2611
  if (result.expires === true) {
2129
2612
  this._refreshTimeout = setTimeout(() => this._refresh(), ttlMilliseconds(result.ttl));
2130
2613
  }
@@ -2135,6 +2618,9 @@ var BaseSubscription = class extends EventEmitter {
2135
2618
  if (!this._isSubscribing()) {
2136
2619
  return;
2137
2620
  }
2621
+ if (error.code === errorCodes.connectionClosed) {
2622
+ return;
2623
+ }
2138
2624
  if (error.code === errorCodes.timeout) {
2139
2625
  this._centrifuge._disconnect(connectingCodes.subscribeTimeout, "subscribe timeout", true);
2140
2626
  return;
@@ -2142,10 +2628,10 @@ var BaseSubscription = class extends EventEmitter {
2142
2628
  this._mapStateBuffer = [];
2143
2629
  this._mapStreamBuffer = [];
2144
2630
  this._mapPhase = null;
2145
- this._prevValueMap = /* @__PURE__ */ new Map();
2146
2631
  if (error.code === 112) {
2147
2632
  if (this._mapUnrecoverableStrategy === "from_scratch") {
2148
2633
  this._debug("map subscribe: unrecoverable position, restarting from scratch");
2634
+ this._prevValueMap = /* @__PURE__ */ new Map();
2149
2635
  this._offset = null;
2150
2636
  this._epoch = null;
2151
2637
  this._recover = false;
@@ -2175,9 +2661,9 @@ var BaseSubscription = class extends EventEmitter {
2175
2661
  req.limit = this._mapPageSize;
2176
2662
  if (cursor)
2177
2663
  req.cursor = cursor;
2178
- if (this._epoch) {
2179
- req.offset = this._offset;
2180
- req.epoch = this._epoch;
2664
+ if (this._mapFlowEpoch) {
2665
+ req.offset = this._mapFlowOffset;
2666
+ req.epoch = this._mapFlowEpoch;
2181
2667
  } else {
2182
2668
  if (this._data)
2183
2669
  req.data = this._data;
@@ -2186,9 +2672,9 @@ var BaseSubscription = class extends EventEmitter {
2186
2672
  if (phase === MapPhase.Stream) {
2187
2673
  if (this._mapPageSize > 0)
2188
2674
  req.limit = this._mapPageSize;
2189
- req.offset = this._offset;
2190
- req.epoch = this._epoch;
2191
- if (this._recover) {
2675
+ req.offset = this._mapFlowOffset;
2676
+ req.epoch = this._mapFlowEpoch;
2677
+ if (this._mapFlowRecovering) {
2192
2678
  req.recover = true;
2193
2679
  if (this._mapStreamBuffer.length === 0) {
2194
2680
  if (this._data)
@@ -2209,7 +2695,7 @@ var BaseSubscription = class extends EventEmitter {
2209
2695
  ctx.removed = true;
2210
2696
  }
2211
2697
  if (pub.offset !== void 0) {
2212
- ctx.offset = pub.offset;
2698
+ ctx.offset = toOffset(pub.offset);
2213
2699
  }
2214
2700
  if (pub.info) {
2215
2701
  ctx.info = this._centrifuge._getJoinLeaveContext(pub.info);
@@ -2230,7 +2716,7 @@ var BaseSubscription = class extends EventEmitter {
2230
2716
  ctx.removed = true;
2231
2717
  }
2232
2718
  if (pub.version !== void 0) {
2233
- ctx.version = pub.version;
2719
+ ctx.version = toOffset(pub.version);
2234
2720
  }
2235
2721
  return ctx;
2236
2722
  }
@@ -2307,19 +2793,17 @@ var SharedPollSubscription = class extends BaseSubscription {
2307
2793
  items = keysOrItems;
2308
2794
  }
2309
2795
  for (const item of items) {
2796
+ const version = toOffset(item.version);
2310
2797
  const existing = this._sharedPollTrackedItems.get(item.key);
2311
- if (existing === void 0 || item.version > existing) {
2312
- this._sharedPollTrackedItems.set(item.key, item.version);
2798
+ if (existing === void 0 || version > existing) {
2799
+ this._sharedPollTrackedItems.set(item.key, version);
2313
2800
  }
2314
2801
  }
2315
2802
  if (sig !== void 0) {
2316
- this._sharedPollSignatures.push({
2317
- keys: items.map((i) => i.key),
2318
- signature: sig
2319
- });
2320
- if (this._isSubscribed()) {
2321
- this._sendTrackRequest([{ items, signature: sig }]).catch((err) => {
2322
- this._handleTrackError(err);
2803
+ this._addSharedPollSignature(items.map((i) => i.key), sig);
2804
+ if (this._isSubscribed() && !this._sharedPollReplayPending) {
2805
+ this._sendTrackRequest([{ items, signature: sig }]).catch((e) => {
2806
+ this._handleTrackError(e.error, e.keys);
2323
2807
  });
2324
2808
  }
2325
2809
  return;
@@ -2332,19 +2816,23 @@ var SharedPollSubscription = class extends BaseSubscription {
2332
2816
  });
2333
2817
  return;
2334
2818
  }
2335
- if (!this._isSubscribed()) {
2819
+ if (!this._isSubscribed() || this._sharedPollReplayPending) {
2336
2820
  return;
2337
2821
  }
2338
2822
  const keys = items.map((i) => i.key);
2339
- this._sharedPollGetSignature({ keys }).then((result) => {
2340
- if (!this._isSubscribed())
2823
+ const generation = this._refreshGeneration;
2824
+ this._callAsync(() => this._sharedPollGetSignature({ keys })).then((result) => {
2825
+ if (generation !== this._refreshGeneration || !this._isSubscribed())
2341
2826
  return;
2342
2827
  const returnedKeys = new Set(result.keys);
2343
2828
  const revokedKeys = [];
2344
2829
  for (const key of keys) {
2345
- if (!returnedKeys.has(key)) {
2830
+ if (!returnedKeys.has(key) && this._sharedPollTrackedItems.has(key)) {
2346
2831
  this._sharedPollTrackedItems.delete(key);
2347
2832
  revokedKeys.push(key);
2833
+ if (!this._isSubscribed()) {
2834
+ continue;
2835
+ }
2348
2836
  this.emit("update", {
2349
2837
  channel: this.channel,
2350
2838
  key,
@@ -2358,6 +2846,7 @@ var SharedPollSubscription = class extends BaseSubscription {
2358
2846
  this.emit("error", { type: "untrack", channel: this.channel, error: err });
2359
2847
  });
2360
2848
  }
2849
+ this._sharedPollReplayRetryAttempts = 0;
2361
2850
  const authorizedItems = [];
2362
2851
  for (const key of result.keys) {
2363
2852
  const version = this._sharedPollTrackedItems.get(key);
@@ -2367,18 +2856,21 @@ var SharedPollSubscription = class extends BaseSubscription {
2367
2856
  }
2368
2857
  if (authorizedItems.length === 0)
2369
2858
  return;
2370
- this._sharedPollSignatures.push({
2371
- keys: result.keys,
2372
- signature: result.signature
2373
- });
2374
- this._sendTrackRequest([{ items: authorizedItems, signature: result.signature }]).catch((err) => {
2375
- this._handleTrackError(err);
2859
+ this._addSharedPollSignature(result.keys, result.signature);
2860
+ this._sendTrackRequest([{ items: authorizedItems, signature: result.signature }]).catch((e) => {
2861
+ this._handleTrackError(e.error, e.keys);
2376
2862
  });
2377
2863
  }).catch((e) => {
2378
- this.emit("error", {
2864
+ if (generation !== this._refreshGeneration || !this._isSubscribed())
2865
+ return;
2866
+ this._emitError({
2379
2867
  type: "track",
2380
2868
  channel: this.channel,
2381
2869
  error: { code: errorCodes.sharedPollGetSignature, message: e !== void 0 ? e.toString() : "getSignature failed" }
2870
+ }, () => {
2871
+ if (generation !== this._refreshGeneration || !this._isSubscribed())
2872
+ return;
2873
+ this._scheduleSharedPollReplayRetry();
2382
2874
  });
2383
2875
  });
2384
2876
  }
@@ -2444,7 +2936,9 @@ var SockjsTransport = class {
2444
2936
  };
2445
2937
  }
2446
2938
  close() {
2447
- this._transport.close();
2939
+ if (this._transport !== null) {
2940
+ this._transport.close();
2941
+ }
2448
2942
  }
2449
2943
  send(data) {
2450
2944
  this._transport.send(data);
@@ -2495,18 +2989,89 @@ var WebsocketTransport = class {
2495
2989
  };
2496
2990
  }
2497
2991
  close() {
2498
- this._transport.close();
2992
+ if (this._transport !== null) {
2993
+ this._transport.close();
2994
+ }
2499
2995
  }
2500
2996
  send(data) {
2501
2997
  this._transport.send(data);
2502
2998
  }
2503
2999
  };
3000
+ var maxRetainedSize = 64 * 1024;
3001
+ var ReplyStreamBuffer = class {
3002
+ constructor() {
3003
+ this._buf = new Uint8Array(0);
3004
+ this._start = 0;
3005
+ this._end = 0;
3006
+ }
3007
+ push(chunk) {
3008
+ const size = this._end - this._start;
3009
+ if (this._end + chunk.length > this._buf.length) {
3010
+ if (size + chunk.length <= this._buf.length / 2) {
3011
+ this._buf.copyWithin(0, this._start, this._end);
3012
+ } else {
3013
+ const buf = new Uint8Array(Math.max(this._buf.length * 2, size + chunk.length));
3014
+ buf.set(this._buf.subarray(this._start, this._end));
3015
+ this._buf = buf;
3016
+ }
3017
+ this._start = 0;
3018
+ this._end = size;
3019
+ }
3020
+ this._buf.set(chunk, this._end);
3021
+ this._end += chunk.length;
3022
+ }
3023
+ // Calls onReply with a copy of each complete reply buffered, in order.
3024
+ // decodeReply tells whether the data starts with a complete reply, and where it ends.
3025
+ drain(decodeReply, onReply) {
3026
+ while (this._start < this._end) {
3027
+ const result = decodeReply(this._buf.subarray(this._start, this._end));
3028
+ if (!result.ok) {
3029
+ break;
3030
+ }
3031
+ const end = this._start + result.pos;
3032
+ const reply = this._buf.slice(this._start, end);
3033
+ this._start = end;
3034
+ onReply(reply);
3035
+ }
3036
+ const size = this._end - this._start;
3037
+ if (this._buf.length > maxRetainedSize && size <= this._buf.length / 4) {
3038
+ this._buf = this._buf.slice(this._start, this._end);
3039
+ this._start = 0;
3040
+ this._end = size;
3041
+ } else if (size === 0) {
3042
+ this._start = 0;
3043
+ this._end = 0;
3044
+ }
3045
+ }
3046
+ };
3047
+ var LineStreamBuffer = class {
3048
+ constructor() {
3049
+ this._decoder = new TextDecoder();
3050
+ this._parts = [];
3051
+ }
3052
+ // Calls onLine with each complete line, in order, without its '\n'.
3053
+ push(chunk, onLine) {
3054
+ const text = this._decoder.decode(chunk, { stream: true });
3055
+ let start = 0;
3056
+ let end = text.indexOf("\n");
3057
+ while (end !== -1) {
3058
+ this._parts.push(text.substring(start, end));
3059
+ const line = this._parts.join("");
3060
+ this._parts = [];
3061
+ start = end + 1;
3062
+ onLine(line);
3063
+ end = text.indexOf("\n", start);
3064
+ }
3065
+ if (start < text.length) {
3066
+ this._parts.push(text.substring(start));
3067
+ }
3068
+ }
3069
+ };
2504
3070
  var HttpStreamTransport = class {
2505
3071
  constructor(endpoint, options) {
2506
3072
  this.endpoint = endpoint;
2507
3073
  this.options = options;
2508
3074
  this._abortController = null;
2509
- this._utf8decoder = new TextDecoder();
2510
3075
  this._protocol = "json";
2511
3076
  }
2512
3077
  name() {
@@ -2528,9 +3093,8 @@ var HttpStreamTransport = class {
2528
3093
  const fetchFunc = self2.options.fetch;
2529
3094
  fetchFunc(endpoint, options).then(self2._handleErrors).then((response) => {
2530
3095
  eventTarget.dispatchEvent(new Event("open"));
2531
- let jsonStreamBuf = "";
2532
- let jsonStreamPos = 0;
2533
- let protoStreamBuf = new Uint8Array();
3096
+ const jsonStreamBuf = new LineStreamBuffer();
3097
+ const protoStreamBuf = new ReplyStreamBuffer();
2534
3098
  const reader = response.body.getReader();
2535
3099
  return new self2.options.readableStream({
2536
3100
  start(controller) {
@@ -2543,32 +3107,10 @@ var HttpStreamTransport = class {
2543
3107
  }
2544
3108
  try {
2545
3109
  if (self2._protocol === "json") {
2546
- jsonStreamBuf += self2._utf8decoder.decode(value, { stream: true });
2547
- while (jsonStreamPos < jsonStreamBuf.length) {
2548
- if (jsonStreamBuf[jsonStreamPos] === "\n") {
2549
- const line = jsonStreamBuf.substring(0, jsonStreamPos);
2550
- eventTarget.dispatchEvent(new MessageEvent("message", { data: line }));
2551
- jsonStreamBuf = jsonStreamBuf.substring(jsonStreamPos + 1);
2552
- jsonStreamPos = 0;
2553
- } else {
2554
- ++jsonStreamPos;
2555
- }
2556
- }
3110
+ jsonStreamBuf.push(value, (line) => eventTarget.dispatchEvent(new MessageEvent("message", { data: line })));
2557
3111
  } else {
2558
- const mergedArray = new Uint8Array(protoStreamBuf.length + value.length);
2559
- mergedArray.set(protoStreamBuf);
2560
- mergedArray.set(value, protoStreamBuf.length);
2561
- protoStreamBuf = mergedArray;
2562
- while (true) {
2563
- const result = self2.options.decoder.decodeReply(protoStreamBuf);
2564
- if (result.ok) {
2565
- const data = protoStreamBuf.slice(0, result.pos);
2566
- eventTarget.dispatchEvent(new MessageEvent("message", { data }));
2567
- protoStreamBuf = protoStreamBuf.slice(result.pos);
2568
- continue;
2569
- }
2570
- break;
2571
- }
3112
+ protoStreamBuf.push(value);
3113
+ protoStreamBuf.drain((data) => self2.options.decoder.decodeReply(data), (reply) => eventTarget.dispatchEvent(new MessageEvent("message", { data: reply })));
2572
3114
  }
2573
3115
  } catch (error) {
2574
3116
  eventTarget.dispatchEvent(new Event("error", { detail: error }));
@@ -2642,7 +3184,9 @@ var HttpStreamTransport = class {
2642
3184
  });
2643
3185
  }
2644
3186
  close() {
2645
- this._abortController.abort();
3187
+ if (this._abortController !== null) {
3188
+ this._abortController.abort();
3189
+ }
2646
3190
  }
2647
3191
  send(data, session2, node) {
2648
3192
  let headers;
@@ -2669,9 +3213,18 @@ var HttpStreamTransport = class {
2669
3213
  headers,
2670
3214
  body,
2671
3215
  mode: "cors",
2672
- credentials: "same-origin"
3216
+ credentials: "same-origin",
3217
+ // Closing the transport aborts requests still pending, e.g. hung in an
3218
+ // intermediary, which would otherwise keep connections of the per-host pool.
3219
+ signal: this._abortController.signal
2673
3220
  };
2674
- fetchFunc(this.options.emulationEndpoint, fetchOptions);
3221
+ fetchFunc(this.options.emulationEndpoint, fetchOptions).then((response) => {
3222
+ if (response && (response.status === 404 || response.status >= 500)) {
3223
+ this.close();
3224
+ }
3225
+ }, () => {
3226
+ this.close();
3227
+ });
2675
3228
  }
2676
3229
  };
2677
3230
  var SseTransport = class {
@@ -2681,6 +3234,7 @@ var SseTransport = class {
2681
3234
  this._protocol = "json";
2682
3235
  this._transport = null;
2683
3236
  this._onClose = null;
3237
+ this._abortController = null;
2684
3238
  }
2685
3239
  name() {
2686
3240
  return "sse";
@@ -2695,6 +3249,9 @@ var SseTransport = class {
2695
3249
  return this.options.eventsource !== null && this.options.fetch !== null;
2696
3250
  }
2697
3251
  initialize(_protocol, callbacks, initialData) {
3252
+ if (typeof AbortController !== "undefined") {
3253
+ this._abortController = new AbortController();
3254
+ }
2698
3255
  let url;
2699
3256
  if (globalThis && globalThis.document && globalThis.document.baseURI) {
2700
3257
  url = new URL(this.endpoint, globalThis.document.baseURI);
@@ -2728,7 +3285,12 @@ var SseTransport = class {
2728
3285
  };
2729
3286
  }
2730
3287
  close() {
2731
- this._transport.close();
3288
+ if (this._abortController !== null) {
3289
+ this._abortController.abort();
3290
+ }
3291
+ if (this._transport !== null) {
3292
+ this._transport.close();
3293
+ }
2732
3294
  if (this._onClose !== null) {
2733
3295
  this._onClose();
2734
3296
  }
@@ -2744,14 +3306,25 @@ var SseTransport = class {
2744
3306
  };
2745
3307
  const body = JSON.stringify(req);
2746
3308
  const fetchFunc = this.options.fetch;
3309
+ const signal = this._abortController !== null ? this._abortController.signal : void 0;
2747
3310
  const fetchOptions = {
2748
3311
  method: "POST",
2749
3312
  headers,
2750
3313
  body,
2751
3314
  mode: "cors",
2752
- credentials: "same-origin"
3315
+ credentials: "same-origin",
3316
+ signal
2753
3317
  };
2754
- fetchFunc(this.options.emulationEndpoint, fetchOptions);
3318
+ fetchFunc(this.options.emulationEndpoint, fetchOptions).then((response) => {
3319
+ if (response && (response.status === 404 || response.status >= 500)) {
3320
+ this.close();
3321
+ }
3322
+ }, () => {
3323
+ if (signal && signal.aborted) {
3324
+ return;
3325
+ }
3326
+ this.close();
3327
+ });
2755
3328
  }
2756
3329
  };
2757
3330
  var WebtransportTransport = class {
@@ -2761,7 +3334,6 @@ var WebtransportTransport = class {
2761
3334
  this._transport = null;
2762
3335
  this._stream = null;
2763
3336
  this._writer = null;
2764
- this._utf8decoder = new TextDecoder();
2765
3337
  this._protocol = "json";
2766
3338
  }
2767
3339
  name() {
@@ -2832,40 +3404,17 @@ var WebtransportTransport = class {
2832
3404
  _startReading(eventTarget) {
2833
3405
  return __awaiter(this, void 0, void 0, function* () {
2834
3406
  const reader = this._stream.readable.getReader();
2835
- let jsonStreamBuf = "";
2836
- let jsonStreamPos = 0;
2837
- let protoStreamBuf = new Uint8Array();
3407
+ const jsonStreamBuf = new LineStreamBuffer();
3408
+ const protoStreamBuf = new ReplyStreamBuffer();
2838
3409
  try {
2839
3410
  while (true) {
2840
3411
  const { done, value } = yield reader.read();
2841
3412
  if (value && value.length > 0) {
2842
3413
  if (this._protocol === "json") {
2843
- jsonStreamBuf += this._utf8decoder.decode(value, { stream: true });
2844
- while (jsonStreamPos < jsonStreamBuf.length) {
2845
- if (jsonStreamBuf[jsonStreamPos] === "\n") {
2846
- const line = jsonStreamBuf.substring(0, jsonStreamPos);
2847
- eventTarget.dispatchEvent(new MessageEvent("message", { data: line }));
2848
- jsonStreamBuf = jsonStreamBuf.substring(jsonStreamPos + 1);
2849
- jsonStreamPos = 0;
2850
- } else {
2851
- ++jsonStreamPos;
2852
- }
2853
- }
3414
+ jsonStreamBuf.push(value, (line) => eventTarget.dispatchEvent(new MessageEvent("message", { data: line })));
2854
3415
  } else {
2855
- const mergedArray = new Uint8Array(protoStreamBuf.length + value.length);
2856
- mergedArray.set(protoStreamBuf);
2857
- mergedArray.set(value, protoStreamBuf.length);
2858
- protoStreamBuf = mergedArray;
2859
- while (true) {
2860
- const result = this.options.decoder.decodeReply(protoStreamBuf);
2861
- if (result.ok) {
2862
- const data = protoStreamBuf.slice(0, result.pos);
2863
- eventTarget.dispatchEvent(new MessageEvent("message", { data }));
2864
- protoStreamBuf = protoStreamBuf.slice(result.pos);
2865
- continue;
2866
- }
2867
- break;
2868
- }
3416
+ protoStreamBuf.push(value);
3417
+ protoStreamBuf.drain((data) => this.options.decoder.decodeReply(data), (reply) => eventTarget.dispatchEvent(new MessageEvent("message", { data: reply })));
2869
3418
  }
2870
3419
  }
2871
3420
  if (done) {
@@ -3179,7 +3728,7 @@ var JsonCodec = class {
3179
3728
  return commands.map((c) => JSON.stringify(c)).join("\n");
3180
3729
  }
3181
3730
  decodeReplies(data) {
3182
- return data.trim().split("\n").map((r) => JSON.parse(r));
3731
+ return data.split("\n").filter((r) => r.trim() !== "").map((r) => JSON.parse(r));
3183
3732
  }
3184
3733
  applyDeltaIfNeeded(pub, prevValue) {
3185
3734
  let newData, newPrevValue, wireBytes;
@@ -3220,8 +3769,19 @@ var defaults = {
3220
3769
  maxReconnectDelay: 2e4,
3221
3770
  timeout: 5e3,
3222
3771
  maxServerPingDelay: 1e4,
3223
- networkEventTarget: null
3772
+ networkEventTarget: null,
3773
+ pageEventTarget: null
3224
3774
  };
3775
+ var websocketNotFound = "WebSocket constructor not found, make sure it is available globally or passed as a dependency in Centrifuge options";
3776
+ var lateCallTimer = 1e3;
3777
+ var lateCallGrace = 1e3;
3778
+ function errorMessage(err) {
3779
+ try {
3780
+ return err instanceof Error ? err.message : String(err);
3781
+ } catch (e) {
3782
+ return "value without a string form";
3783
+ }
3784
+ }
3225
3785
  var UnauthorizedError = class extends Error {
3226
3786
  constructor(message) {
3227
3787
  super(message);
@@ -3233,8 +3793,12 @@ var Centrifuge = class extends EventEmitter {
3233
3793
  constructor(endpoint, options) {
3234
3794
  super();
3235
3795
  this._reconnectTimeout = null;
3796
+ this._connectTimeout = null;
3797
+ this._removedUnsubscribes = {};
3798
+ this._pendingServerSubOffsets = {};
3236
3799
  this._refreshTimeout = null;
3237
3800
  this._serverPingTimeout = null;
3801
+ this._handlerFailures = 0;
3238
3802
  this.state = State.Disconnected;
3239
3803
  this._transportIsOpen = false;
3240
3804
  this._endpoint = endpoint;
@@ -3250,6 +3814,7 @@ var Centrifuge = class extends EventEmitter {
3250
3814
  this._codec = new JsonCodec();
3251
3815
  this._reconnecting = false;
3252
3816
  this._reconnectTimeout = null;
3817
+ this._connectTimeout = null;
3253
3818
  this._reconnectAttempts = 0;
3254
3819
  this._client = null;
3255
3820
  this._session = "";
@@ -3271,7 +3836,13 @@ var Centrifuge = class extends EventEmitter {
3271
3836
  this._promises = {};
3272
3837
  this._promiseId = 0;
3273
3838
  this._debugEnabled = false;
3274
- this._networkEventsSet = false;
3839
+ this._networkEvents = null;
3840
+ this._pageEvents = null;
3841
+ this._pageCached = false;
3842
+ this._stateTransitions = 0;
3843
+ this._disconnects = 0;
3844
+ this._abortedTransportId = 0;
3845
+ this._reconnectAttemptsResetPending = false;
3275
3846
  this._config = Object.assign(Object.assign({}, defaults), options);
3276
3847
  this._configure();
3277
3848
  if (this._debugEnabled) {
@@ -3422,10 +3993,15 @@ var Centrifuge = class extends EventEmitter {
3422
3993
  if (!sub) {
3423
3994
  return;
3424
3995
  }
3996
+ if (this._subs[sub.channel] !== sub) {
3997
+ return;
3998
+ }
3999
+ delete this._subs[sub.channel];
4000
+ sub._removed = true;
3425
4001
  if (sub.state !== SubscriptionState.Unsubscribed) {
3426
4002
  sub.unsubscribe();
3427
4003
  }
3428
- this._removeSubscription(sub);
4004
+ this._recordRemovedUnsubscribe(sub);
3429
4005
  }
3430
4006
  /** Remove a map subscription. */
3431
4007
  removeMapSubscription(sub) {
@@ -3469,13 +4045,15 @@ var Centrifuge = class extends EventEmitter {
3469
4045
  return;
3470
4046
  default:
3471
4047
  return new Promise((resolve, reject) => {
4048
+ const id = this._nextPromiseId();
3472
4049
  const ctx = { resolve, reject };
3473
4050
  if (timeout) {
3474
4051
  ctx.timeout = setTimeout(() => {
4052
+ delete this._promises[id];
3475
4053
  reject({ code: errorCodes.timeout, message: "timeout" });
3476
4054
  }, timeout);
3477
4055
  }
3478
- this._promises[this._nextPromiseId()] = ctx;
4056
+ this._promises[id] = ctx;
3479
4057
  });
3480
4058
  }
3481
4059
  });
@@ -3491,6 +4069,7 @@ var Centrifuge = class extends EventEmitter {
3491
4069
  return;
3492
4070
  }
3493
4071
  this._debug("connect called");
4072
+ this._validateTransportConfig();
3494
4073
  this._reconnectAttempts = 0;
3495
4074
  this._startConnecting();
3496
4075
  }
@@ -3501,6 +4080,9 @@ var Centrifuge = class extends EventEmitter {
3501
4080
  /** setToken allows setting connection token. Or resetting used token to be empty. */
3502
4081
  setToken(token) {
3503
4082
  this._token = token;
4083
+ if (this._config.getToken === null) {
4084
+ this._refreshRequired = false;
4085
+ }
3504
4086
  }
3505
4087
  /** setData allows setting connection data. This only affects the next connection attempt,
3506
4088
  * not the current one. Note that if getData callback is configured, it will override
@@ -3605,7 +4187,7 @@ var Centrifuge = class extends EventEmitter {
3605
4187
  return {
3606
4188
  publications,
3607
4189
  epoch: result.epoch || "",
3608
- offset: result.offset || 0
4190
+ offset: toOffset(result.offset)
3609
4191
  };
3610
4192
  });
3611
4193
  }
@@ -3721,15 +4303,29 @@ var Centrifuge = class extends EventEmitter {
3721
4303
  throw new Error("unsupported url configuration type: only string or array of objects are supported");
3722
4304
  }
3723
4305
  }
4306
+ // Returns the number of the transition made, or 0 if the client already was in newState.
3724
4307
  _setState(newState) {
3725
4308
  if (this.state !== newState) {
3726
4309
  this._reconnecting = false;
3727
4310
  const oldState = this.state;
3728
4311
  this.state = newState;
4312
+ const transition = ++this._stateTransitions;
4313
+ if (newState === State.Connecting) {
4314
+ this._setNetworkEvents();
4315
+ this._setPageEvents();
4316
+ } else if (newState === State.Disconnected) {
4317
+ this._clearNetworkEvents();
4318
+ this._clearPageEvents();
4319
+ }
3729
4320
  this.emit("state", { newState, oldState });
3730
- return true;
4321
+ return transition;
3731
4322
  }
3732
- return false;
4323
+ return 0;
4324
+ }
4325
+ // Whether an event handler made another state transition after the given one
4326
+ // (as returned by _setState), so that the given one is outdated.
4327
+ _transitionSuperseded(transition) {
4328
+ return transition !== 0 && this._stateTransitions !== transition;
3733
4329
  }
3734
4330
  _isDisconnected() {
3735
4331
  return this.state === State.Disconnected;
@@ -3744,7 +4340,7 @@ var Centrifuge = class extends EventEmitter {
3744
4340
  return ++this._commandId;
3745
4341
  }
3746
4342
  _setNetworkEvents() {
3747
- if (this._networkEventsSet) {
4343
+ if (this._networkEvents !== null) {
3748
4344
  return;
3749
4345
  }
3750
4346
  let eventTarget = null;
@@ -3754,33 +4350,120 @@ var Centrifuge = class extends EventEmitter {
3754
4350
  eventTarget = globalThis;
3755
4351
  }
3756
4352
  if (eventTarget) {
3757
- eventTarget.addEventListener("offline", () => {
4353
+ const onOffline = () => {
3758
4354
  this._debug("offline event triggered");
3759
4355
  if (this.state === State.Connected || this.state === State.Connecting) {
3760
- this._disconnect(connectingCodes.transportClosed, "transport closed", true);
3761
4356
  this._deviceWentOffline = true;
4357
+ this._disconnect(connectingCodes.transportClosed, "transport closed", true);
3762
4358
  }
3763
- });
3764
- eventTarget.addEventListener("online", () => {
4359
+ };
4360
+ const onOnline = () => {
3765
4361
  this._debug("online event triggered");
4362
+ const wentOffline = this._deviceWentOffline;
4363
+ this._deviceWentOffline = false;
3766
4364
  if (this.state !== State.Connecting) {
3767
4365
  return;
3768
4366
  }
3769
- if (this._deviceWentOffline && !this._transportClosed) {
3770
- this._deviceWentOffline = false;
3771
- this._transportClosed = true;
4367
+ if (wentOffline && !this._transportClosed) {
4368
+ this._abortedTransportId = this._transportId;
4369
+ this._disconnect(connectingCodes.transportClosed, "transport closed", true);
3772
4370
  }
3773
4371
  this._clearReconnectTimeout();
3774
4372
  this._startReconnecting();
3775
- });
3776
- this._networkEventsSet = true;
4373
+ };
4374
+ eventTarget.addEventListener("offline", onOffline);
4375
+ eventTarget.addEventListener("online", onOnline);
4376
+ this._networkEvents = { target: eventTarget, onOffline, onOnline };
3777
4377
  }
3778
4378
  }
4379
+ // The online listener drives reconnect, so listeners stay until the client is
4380
+ // disconnected; removing them then lets a disconnected client be garbage-collected.
4381
+ _clearNetworkEvents() {
4382
+ if (this._networkEvents === null) {
4383
+ return;
4384
+ }
4385
+ const { target, onOffline, onOnline } = this._networkEvents;
4386
+ if (!isFunction(target.removeEventListener)) {
4387
+ this._debug("network event target has no removeEventListener, listeners kept");
4388
+ return;
4389
+ }
4390
+ target.removeEventListener("offline", onOffline);
4391
+ target.removeEventListener("online", onOnline);
4392
+ this._networkEvents = null;
4393
+ }
4394
+ // A page entering the back/forward cache is frozen, but the browser may keep its
4395
+ // socket open: the server then sees a client that stopped answering pings. So the
4396
+ // transport is closed when the page is hidden into the cache, and the client
4397
+ // reconnects as soon as the page is restored from it. The reconnect is scheduled
4398
+ // as usual too: its timer does not run in a frozen page, and it still reconnects
4399
+ // a page which was not frozen, or was restored without a pageshow event.
4400
+ _setPageEvents() {
4401
+ if (this._pageEvents !== null) {
4402
+ return;
4403
+ }
4404
+ let eventTarget = null;
4405
+ if (this._config.pageEventTarget !== null) {
4406
+ eventTarget = this._config.pageEventTarget;
4407
+ } else if (typeof globalThis.addEventListener !== "undefined") {
4408
+ eventTarget = globalThis;
4409
+ }
4410
+ if (!eventTarget) {
4411
+ return;
4412
+ }
4413
+ const onPageHide = (e) => {
4414
+ if (!e.persisted) {
4415
+ return;
4416
+ }
4417
+ this._debug("page entered back/forward cache");
4418
+ if (this.state === State.Connected || this.state === State.Connecting) {
4419
+ this._pageCached = true;
4420
+ this._disconnect(connectingCodes.transportClosed, "page entered back/forward cache", true);
4421
+ }
4422
+ };
4423
+ const onPageShow = (e) => {
4424
+ if (!e.persisted) {
4425
+ return;
4426
+ }
4427
+ this._debug("page restored from back/forward cache");
4428
+ const cached = this._pageCached;
4429
+ this._pageCached = false;
4430
+ if (!cached || this.state !== State.Connecting) {
4431
+ return;
4432
+ }
4433
+ this._clearReconnectTimeout();
4434
+ this._startReconnecting();
4435
+ };
4436
+ eventTarget.addEventListener("pagehide", onPageHide);
4437
+ eventTarget.addEventListener("pageshow", onPageShow);
4438
+ this._pageEvents = { target: eventTarget, onPageHide, onPageShow };
4439
+ }
4440
+ // Removed once the client is disconnected, like the network listeners.
4441
+ _clearPageEvents() {
4442
+ if (this._pageEvents === null) {
4443
+ return;
4444
+ }
4445
+ const { target, onPageHide, onPageShow } = this._pageEvents;
4446
+ if (!isFunction(target.removeEventListener)) {
4447
+ this._debug("page event target has no removeEventListener, listeners kept");
4448
+ return;
4449
+ }
4450
+ target.removeEventListener("pagehide", onPageHide);
4451
+ target.removeEventListener("pageshow", onPageShow);
4452
+ this._pageEvents = null;
4453
+ }
3779
4454
  _getReconnectDelay() {
3780
4455
  const delay = backoff(this._reconnectAttempts, this._config.minReconnectDelay, this._config.maxReconnectDelay);
3781
4456
  this._reconnectAttempts += 1;
3782
4457
  return delay;
3783
4458
  }
4459
+ // See _connectResponse: an emulation connection resets the backoff once it
4460
+ // proved usable.
4461
+ _resetReconnectAttemptsIfPending() {
4462
+ if (this._reconnectAttemptsResetPending) {
4463
+ this._reconnectAttemptsResetPending = false;
4464
+ this._reconnectAttempts = 0;
4465
+ }
4466
+ }
3784
4467
  _clearOutgoingRequests() {
3785
4468
  for (const id in this._callbacks) {
3786
4469
  if (this._callbacks.hasOwnProperty(id)) {
@@ -3809,7 +4492,8 @@ var Centrifuge = class extends EventEmitter {
3809
4492
  }
3810
4493
  }
3811
4494
  for (const channel in this._serverSubs) {
3812
- if (this._serverSubs.hasOwnProperty(channel)) {
4495
+ if (this._serverSubs.hasOwnProperty(channel) && this._serverSubs[channel].subscribed) {
4496
+ this._serverSubs[channel].subscribed = false;
3813
4497
  this.emit("subscribing", { channel });
3814
4498
  }
3815
4499
  }
@@ -3850,7 +4534,10 @@ var Centrifuge = class extends EventEmitter {
3850
4534
  this._currentTransportIndex = 0;
3851
4535
  }
3852
4536
  }
3853
- _initializeTransport() {
4537
+ // Transport dependencies from the config, or else from globals. An absent one
4538
+ // stays null, not undefined: supported() of sockjs, sse and http_stream only
4539
+ // checks `!== null`.
4540
+ _resolveTransportDeps() {
3854
4541
  let websocket;
3855
4542
  if (this._config.websocket !== null) {
3856
4543
  websocket = this._config.websocket;
@@ -3891,101 +4578,105 @@ var Centrifuge = class extends EventEmitter {
3891
4578
  readableStream = globalThis.ReadableStream;
3892
4579
  }
3893
4580
  }
4581
+ return { websocket, sockjs, eventsource, fetchFunc, readableStream };
4582
+ }
4583
+ // Constructs a transport, without checking that it is supported.
4584
+ _createTransport(transportName, endpoint, deps) {
4585
+ switch (transportName) {
4586
+ case "websocket":
4587
+ return new WebsocketTransport(endpoint, {
4588
+ websocket: deps.websocket
4589
+ });
4590
+ case "webtransport":
4591
+ return new WebtransportTransport(endpoint, {
4592
+ webtransport: globalThis.WebTransport,
4593
+ decoder: this._codec,
4594
+ encoder: this._codec
4595
+ });
4596
+ case "http_stream":
4597
+ return new HttpStreamTransport(endpoint, {
4598
+ fetch: deps.fetchFunc,
4599
+ readableStream: deps.readableStream,
4600
+ emulationEndpoint: this._config.emulationEndpoint,
4601
+ decoder: this._codec,
4602
+ encoder: this._codec
4603
+ });
4604
+ case "sse":
4605
+ return new SseTransport(endpoint, {
4606
+ eventsource: deps.eventsource,
4607
+ fetch: deps.fetchFunc,
4608
+ emulationEndpoint: this._config.emulationEndpoint
4609
+ });
4610
+ case "sockjs":
4611
+ return new SockjsTransport(endpoint, {
4612
+ sockjs: deps.sockjs,
4613
+ sockjsOptions: this._config.sockjsOptions
4614
+ });
4615
+ default:
4616
+ throw new Error("unknown transport " + transportName);
4617
+ }
4618
+ }
4619
+ // Throws for a configuration no retry can fix. Called from connect() before
4620
+ // the state changes, so the client stays disconnected, and before a token or
4621
+ // data is loaded, so it fails the same way with getToken or getData.
4622
+ _validateTransportConfig() {
4623
+ const deps = this._resolveTransportDeps();
3894
4624
  if (!this._emulation) {
3895
4625
  if (startsWith(this._endpoint, "http")) {
3896
4626
  throw new Error("Provide explicit transport endpoints configuration in case of using HTTP (i.e. using array of TransportEndpoint instead of a single string), or use ws(s):// scheme in an endpoint if you aimed using WebSocket transport");
3897
- } else {
3898
- this._debug("client will use websocket");
3899
- this._transport = new WebsocketTransport(this._endpoint, {
3900
- websocket
3901
- });
3902
- if (!this._transport.supported()) {
3903
- throw new Error("WebSocket constructor not found, make sure it is available globally or passed as a dependency in Centrifuge options");
3904
- }
4627
+ }
4628
+ if (!this._createTransport("websocket", this._endpoint, deps).supported()) {
4629
+ throw new Error(websocketNotFound);
4630
+ }
4631
+ return;
4632
+ }
4633
+ for (const transportConfig of this._transports) {
4634
+ if (this._createTransport(transportConfig.transport, transportConfig.endpoint, deps).supported()) {
4635
+ return;
4636
+ }
4637
+ }
4638
+ throw new Error("no supported transport found");
4639
+ }
4640
+ _initializeTransport() {
4641
+ const deps = this._resolveTransportDeps();
4642
+ let selected = null;
4643
+ if (!this._emulation) {
4644
+ this._debug("client will use websocket");
4645
+ const candidate = this._createTransport("websocket", this._endpoint, deps);
4646
+ if (candidate.supported()) {
4647
+ selected = candidate;
3905
4648
  }
3906
4649
  } else {
3907
4650
  if (this._currentTransportIndex >= this._transports.length) {
3908
4651
  this._triedAllTransports = true;
3909
4652
  this._currentTransportIndex = 0;
3910
4653
  }
3911
- let count = 0;
3912
- while (true) {
3913
- if (count >= this._transports.length) {
3914
- throw new Error("no supported transport found");
3915
- }
4654
+ for (let count = 0; count < this._transports.length; count++) {
3916
4655
  const transportConfig = this._transports[this._currentTransportIndex];
3917
- const transportName = transportConfig.transport;
3918
- const transportEndpoint = transportConfig.endpoint;
3919
- if (transportName === "websocket") {
3920
- this._debug("trying websocket transport");
3921
- this._transport = new WebsocketTransport(transportEndpoint, {
3922
- websocket
3923
- });
3924
- if (!this._transport.supported()) {
3925
- this._debug("websocket transport not available");
3926
- this._advanceTransportIndex();
3927
- count++;
3928
- continue;
3929
- }
3930
- } else if (transportName === "webtransport") {
3931
- this._debug("trying webtransport transport");
3932
- this._transport = new WebtransportTransport(transportEndpoint, {
3933
- webtransport: globalThis.WebTransport,
3934
- decoder: this._codec,
3935
- encoder: this._codec
3936
- });
3937
- if (!this._transport.supported()) {
3938
- this._debug("webtransport transport not available");
3939
- this._advanceTransportIndex();
3940
- count++;
3941
- continue;
3942
- }
3943
- } else if (transportName === "http_stream") {
3944
- this._debug("trying http_stream transport");
3945
- this._transport = new HttpStreamTransport(transportEndpoint, {
3946
- fetch: fetchFunc,
3947
- readableStream,
3948
- emulationEndpoint: this._config.emulationEndpoint,
3949
- decoder: this._codec,
3950
- encoder: this._codec
3951
- });
3952
- if (!this._transport.supported()) {
3953
- this._debug("http_stream transport not available");
3954
- this._advanceTransportIndex();
3955
- count++;
3956
- continue;
3957
- }
3958
- } else if (transportName === "sse") {
3959
- this._debug("trying sse transport");
3960
- this._transport = new SseTransport(transportEndpoint, {
3961
- eventsource,
3962
- fetch: fetchFunc,
3963
- emulationEndpoint: this._config.emulationEndpoint
3964
- });
3965
- if (!this._transport.supported()) {
3966
- this._debug("sse transport not available");
3967
- this._advanceTransportIndex();
3968
- count++;
3969
- continue;
3970
- }
3971
- } else if (transportName === "sockjs") {
3972
- this._debug("trying sockjs");
3973
- this._transport = new SockjsTransport(transportEndpoint, {
3974
- sockjs,
3975
- sockjsOptions: this._config.sockjsOptions
3976
- });
3977
- if (!this._transport.supported()) {
3978
- this._debug("sockjs transport not available");
3979
- this._advanceTransportIndex();
3980
- count++;
3981
- continue;
3982
- }
3983
- } else {
3984
- throw new Error("unknown transport " + transportName);
4656
+ this._debug("trying " + transportConfig.transport + " transport");
4657
+ const candidate = this._createTransport(transportConfig.transport, transportConfig.endpoint, deps);
4658
+ if (candidate.supported()) {
4659
+ selected = candidate;
4660
+ break;
3985
4661
  }
3986
- break;
4662
+ this._debug(transportConfig.transport + " transport not available");
4663
+ this._advanceTransportIndex();
3987
4664
  }
3988
4665
  }
4666
+ if (selected === null) {
4667
+ const message = this._emulation ? "no supported transport found" : websocketNotFound;
4668
+ this._debug(message);
4669
+ this._disconnect(connectingCodes.transportClosed, "transport closed", true);
4670
+ this.emit("error", {
4671
+ type: "transport",
4672
+ error: {
4673
+ code: errorCodes.transportClosed,
4674
+ message
4675
+ }
4676
+ });
4677
+ return;
4678
+ }
4679
+ this._transport = selected;
3989
4680
  const self2 = this;
3990
4681
  const transport = this._transport;
3991
4682
  const transportId = this._nextTransportId();
@@ -3996,24 +4687,48 @@ var Centrifuge = class extends EventEmitter {
3996
4687
  const connectCommand = self2._sendConnect(true);
3997
4688
  initialCommands.push(connectCommand);
3998
4689
  }
3999
- this._setNetworkEvents();
4000
- const initialData = this._codec.encodeCommands(initialCommands);
4001
4690
  this._transportClosed = false;
4002
- let connectTimeout;
4003
- connectTimeout = setTimeout(function() {
4004
- transport.close();
4691
+ const failTransport = function(reason) {
4692
+ if (self2._transportId != transportId) {
4693
+ self2._debug("failure of non-actual transport", reason);
4694
+ return;
4695
+ }
4696
+ self2._debug(transport.name(), "transport failed:", reason);
4697
+ if (self2._emulation && !self2._transportWasOpen) {
4698
+ self2._advanceTransportIndex();
4699
+ }
4700
+ self2._abortedTransportId = transportId;
4701
+ self2._disconnect(connectingCodes.transportClosed, "transport closed", true);
4702
+ self2.emit("error", {
4703
+ type: "transport",
4704
+ error: {
4705
+ code: errorCodes.transportClosed,
4706
+ message: reason
4707
+ },
4708
+ transport: transport.name()
4709
+ });
4710
+ };
4711
+ this._clearConnectTimeout();
4712
+ const connectDue = Date.now() + this._config.timeout;
4713
+ this._connectTimeout = setTimeout(function() {
4714
+ const late = Date.now() - connectDue >= lateCallTimer;
4715
+ self2._connectTimeout = setTimeout(function() {
4716
+ self2._connectTimeout = null;
4717
+ failTransport("connect timeout");
4718
+ }, late ? lateCallGrace : 0);
4005
4719
  }, this._config.timeout);
4006
- this._transport.initialize(this._codecName(), {
4720
+ const callbacks = {
4007
4721
  onOpen: function() {
4008
- if (connectTimeout) {
4009
- clearTimeout(connectTimeout);
4010
- connectTimeout = null;
4011
- }
4012
4722
  if (self2._transportId != transportId) {
4013
4723
  self2._debug("open callback from non-actual transport");
4014
- transport.close();
4724
+ try {
4725
+ transport.close();
4726
+ } catch (e) {
4727
+ self2._debug("error closing transport", e);
4728
+ }
4015
4729
  return;
4016
4730
  }
4731
+ self2._clearConnectTimeout();
4017
4732
  wasOpen = true;
4018
4733
  self2._debug(transport.subName(), "transport open");
4019
4734
  if (transport.emulation()) {
@@ -4022,9 +4737,14 @@ var Centrifuge = class extends EventEmitter {
4022
4737
  self2._transportIsOpen = true;
4023
4738
  self2._transportWasOpen = true;
4024
4739
  self2.startBatching();
4025
- self2._sendConnect(false);
4026
- self2._sendSubscribeCommands();
4027
- self2.stopBatching();
4740
+ try {
4741
+ self2._sendConnect(false);
4742
+ self2._sendSubscribeCommands();
4743
+ } catch (e) {
4744
+ self2._reportDispatchError(e);
4745
+ } finally {
4746
+ self2.stopBatching();
4747
+ }
4028
4748
  self2.emit("__centrifuge_debug:connect_frame_sent", {});
4029
4749
  },
4030
4750
  onError: function(e) {
@@ -4034,15 +4754,16 @@ var Centrifuge = class extends EventEmitter {
4034
4754
  }
4035
4755
  self2._debug("transport level error", e);
4036
4756
  },
4037
- onClose: function(closeEvent) {
4038
- if (connectTimeout) {
4039
- clearTimeout(connectTimeout);
4040
- connectTimeout = null;
4041
- }
4757
+ onClose: function handleClose(closeEvent, dispatched) {
4042
4758
  if (self2._transportId != transportId) {
4043
4759
  self2._debug("close callback from non-actual transport");
4044
4760
  return;
4045
4761
  }
4762
+ if (!dispatched && (transport.emulation() || transport.name() === "webtransport")) {
4763
+ self2._dispatchPromise.then(() => handleClose(closeEvent, true));
4764
+ return;
4765
+ }
4766
+ self2._clearConnectTimeout();
4046
4767
  self2._debug(transport.subName(), "transport closed");
4047
4768
  self2._transportClosed = true;
4048
4769
  self2._transportIsOpen = false;
@@ -4079,38 +4800,76 @@ var Centrifuge = class extends EventEmitter {
4079
4800
  } else {
4080
4801
  self2._transportWasOpen = true;
4081
4802
  }
4803
+ const teardown = function() {
4804
+ self2._reconnecting = false;
4805
+ self2._disconnect(code, reason, needReconnect);
4806
+ };
4082
4807
  if (self2._isConnecting() && !wasOpen) {
4083
- self2.emit("error", {
4808
+ self2._abortedTransportId = transportId;
4809
+ self2._emitError({
4084
4810
  type: "transport",
4085
4811
  error: {
4086
4812
  code: errorCodes.transportClosed,
4087
4813
  message: "transport closed"
4088
4814
  },
4089
4815
  transport: transport.name()
4090
- });
4816
+ }, teardown);
4817
+ } else {
4818
+ teardown();
4091
4819
  }
4092
- self2._reconnecting = false;
4093
- self2._disconnect(code, reason, needReconnect);
4094
4820
  },
4095
4821
  onMessage: function(data) {
4096
- self2._dataReceived(data);
4822
+ if (self2._transportId != transportId) {
4823
+ self2._debug("message from non-actual transport");
4824
+ return;
4825
+ }
4826
+ self2._dataReceived(data, transportId);
4097
4827
  }
4098
- }, initialData);
4828
+ };
4829
+ const onInitializeError = function(e) {
4830
+ self2._debug("error initializing transport", e);
4831
+ failTransport(e instanceof Error ? e.message : String(e));
4832
+ };
4833
+ let initialized;
4834
+ try {
4835
+ initialized = transport.initialize(this._codecName(), callbacks, this._codec.encodeCommands(initialCommands));
4836
+ } catch (e) {
4837
+ onInitializeError(e);
4838
+ return;
4839
+ }
4840
+ if (initialized && isFunction(initialized.then)) {
4841
+ initialized.then(null, onInitializeError);
4842
+ }
4099
4843
  self2.emit("__centrifuge_debug:transport_initialized", {});
4100
4844
  }
4101
4845
  _sendConnect(skipSending) {
4102
4846
  const connectCommand = this._constructConnectCommand();
4103
4847
  const self2 = this;
4848
+ const transportId = this._transportId;
4849
+ const token = this._token;
4104
4850
  this._call(connectCommand, skipSending).then((resolveCtx) => {
4105
- const result = resolveCtx.reply.connect;
4106
- self2._connectResponse(result);
4107
- if (resolveCtx.next) {
4108
- resolveCtx.next();
4851
+ try {
4852
+ self2._connectResponse(resolveCtx.reply.connect);
4853
+ } catch (err) {
4854
+ self2._dispatchFailed(err);
4855
+ } finally {
4856
+ if (resolveCtx.next) {
4857
+ resolveCtx.next();
4858
+ }
4109
4859
  }
4110
4860
  }, (rejectCtx) => {
4111
- self2._connectError(rejectCtx.error);
4112
- if (rejectCtx.next) {
4113
- rejectCtx.next();
4861
+ try {
4862
+ if (transportId === self2._abortedTransportId) {
4863
+ self2._debug("connect command of a transport aborted by the client rejected");
4864
+ } else {
4865
+ self2._connectError(rejectCtx.error, self2._transportId !== transportId, rejectCtx.next !== void 0, token);
4866
+ }
4867
+ } catch (err) {
4868
+ self2._rejectionFailed(err, rejectCtx);
4869
+ } finally {
4870
+ if (rejectCtx.next) {
4871
+ rejectCtx.next();
4872
+ }
4114
4873
  }
4115
4874
  });
4116
4875
  return connectCommand;
@@ -4130,17 +4889,21 @@ var Centrifuge = class extends EventEmitter {
4130
4889
  return;
4131
4890
  }
4132
4891
  this._reconnecting = true;
4892
+ const disconnects = this._disconnects;
4133
4893
  const emptyToken = this._token === "";
4134
4894
  const needTokenRefresh = this._refreshRequired || emptyToken && this._config.getToken !== null;
4895
+ let tokenResolved = false;
4896
+ let dataResolved = false;
4135
4897
  if (!needTokenRefresh) {
4136
4898
  if (this._config.getData) {
4137
- this._config.getData().then((data) => {
4138
- if (!this._isConnecting()) {
4899
+ this._getData().then((data) => {
4900
+ dataResolved = true;
4901
+ if (this._attemptAborted(disconnects)) {
4139
4902
  return;
4140
4903
  }
4141
4904
  this._data = data;
4142
4905
  this._initializeTransport();
4143
- }).catch((e) => this._handleGetDataError(e));
4906
+ }).catch((e) => this._handleGetDataError(e, disconnects, dataResolved));
4144
4907
  } else {
4145
4908
  this._initializeTransport();
4146
4909
  }
@@ -4148,7 +4911,8 @@ var Centrifuge = class extends EventEmitter {
4148
4911
  }
4149
4912
  const self2 = this;
4150
4913
  this._getToken().then(function(token) {
4151
- if (!self2._isConnecting()) {
4914
+ tokenResolved = true;
4915
+ if (self2._attemptAborted(disconnects)) {
4152
4916
  return;
4153
4917
  }
4154
4918
  if (token == null || token == void 0) {
@@ -4158,73 +4922,116 @@ var Centrifuge = class extends EventEmitter {
4158
4922
  self2._token = token;
4159
4923
  self2._debug("connection token refreshed");
4160
4924
  if (self2._config.getData) {
4161
- self2._config.getData().then(function(data) {
4162
- if (!self2._isConnecting()) {
4925
+ self2._getData().then(function(data) {
4926
+ dataResolved = true;
4927
+ if (self2._attemptAborted(disconnects)) {
4163
4928
  return;
4164
4929
  }
4165
4930
  self2._data = data;
4166
4931
  self2._initializeTransport();
4167
- }).catch((e) => self2._handleGetDataError(e));
4932
+ }).catch((e) => self2._handleGetDataError(e, disconnects, dataResolved));
4168
4933
  } else {
4169
4934
  self2._initializeTransport();
4170
4935
  }
4171
4936
  }).catch(function(e) {
4172
- if (!self2._isConnecting()) {
4937
+ if (self2._attemptAborted(disconnects)) {
4938
+ if (tokenResolved) {
4939
+ self2._reportDispatchError(e);
4940
+ }
4173
4941
  return;
4174
4942
  }
4175
4943
  if (e instanceof UnauthorizedError) {
4176
4944
  self2._failUnauthorized();
4177
4945
  return;
4178
4946
  }
4179
- self2.emit("error", {
4947
+ self2._emitError({
4180
4948
  "type": "connectToken",
4181
4949
  "error": {
4182
4950
  code: errorCodes.clientConnectToken,
4183
4951
  message: e !== void 0 ? e.toString() : ""
4184
4952
  }
4953
+ }, () => {
4954
+ const delay = self2._getReconnectDelay();
4955
+ self2._debug("error on getting connection token, reconnect after " + delay + " milliseconds", e);
4956
+ self2._reconnecting = false;
4957
+ self2._reconnectTimeout = setTimeout(() => {
4958
+ self2._startReconnecting();
4959
+ }, delay);
4185
4960
  });
4186
- const delay = self2._getReconnectDelay();
4187
- self2._debug("error on getting connection token, reconnect after " + delay + " milliseconds", e);
4188
- self2._reconnecting = false;
4189
- self2._reconnectTimeout = setTimeout(() => {
4190
- self2._startReconnecting();
4191
- }, delay);
4192
4961
  });
4193
4962
  }
4194
- _handleGetDataError(e) {
4963
+ // Emits an error event, then runs the continuation: not when a handler disconnected
4964
+ // the client (or threw, which disconnects it), maybe to connect again, whose new
4965
+ // attempt the continuation must not touch.
4966
+ _emitError(ctx, continuation) {
4967
+ const disconnects = this._disconnects;
4968
+ try {
4969
+ this.emit("error", ctx);
4970
+ } finally {
4971
+ if (this._disconnects === disconnects) {
4972
+ continuation();
4973
+ }
4974
+ }
4975
+ }
4976
+ _attemptAborted(disconnects) {
4977
+ return !this._isConnecting() || this._disconnects !== disconnects;
4978
+ }
4979
+ // resolved: the data promise resolved, so the error was thrown by its continuation.
4980
+ _handleGetDataError(e, disconnects, resolved) {
4981
+ if (this._attemptAborted(disconnects)) {
4982
+ if (resolved) {
4983
+ this._reportDispatchError(e);
4984
+ }
4985
+ return;
4986
+ }
4195
4987
  if (e instanceof UnauthorizedError) {
4196
4988
  this._failUnauthorized();
4197
4989
  return;
4198
4990
  }
4199
- this.emit("error", {
4991
+ this._emitError({
4200
4992
  type: "connectData",
4201
4993
  error: {
4202
4994
  code: errorCodes.badConfiguration,
4203
4995
  message: (e === null || e === void 0 ? void 0 : e.toString()) || ""
4204
4996
  }
4997
+ }, () => {
4998
+ const delay = this._getReconnectDelay();
4999
+ this._debug("error on getting connect data, reconnect after " + delay + " milliseconds", e);
5000
+ this._reconnecting = false;
5001
+ this._reconnectTimeout = setTimeout(() => {
5002
+ this._startReconnecting();
5003
+ }, delay);
4205
5004
  });
4206
- const delay = this._getReconnectDelay();
4207
- this._debug("error on getting connect data, reconnect after " + delay + " milliseconds", e);
4208
- this._reconnecting = false;
4209
- this._reconnectTimeout = setTimeout(() => {
4210
- this._startReconnecting();
4211
- }, delay);
4212
5005
  }
4213
- _connectError(err) {
5006
+ // stale: the error belongs to a transport that was already closed. It is still
5007
+ // reported, but must not tear down the current connection attempt.
5008
+ // replied: the server returned the error in a reply, so the transport works.
5009
+ // token: the token the connect command carried.
5010
+ _connectError(err, stale, replied, token) {
4214
5011
  if (this.state !== State.Connecting) {
4215
5012
  return;
4216
5013
  }
4217
- if (err.code === 109) {
5014
+ if (err.code === 109 && token === this._token) {
4218
5015
  this._refreshRequired = true;
4219
5016
  }
4220
5017
  if (err.code < 100 || err.temporary === true || err.code === 109) {
4221
- this.emit("error", {
5018
+ this._emitError({
4222
5019
  "type": "connect",
4223
5020
  "error": err
5021
+ }, () => {
5022
+ if (stale) {
5023
+ return;
5024
+ }
5025
+ if (replied) {
5026
+ this._transportWasOpen = true;
5027
+ }
5028
+ if (!replied && this._emulation && !this._transportWasOpen) {
5029
+ this._advanceTransportIndex();
5030
+ }
5031
+ this._debug("closing transport due to connect error");
5032
+ this._disconnect(err.code, err.message, true);
4224
5033
  });
4225
- this._debug("closing transport due to connect error");
4226
- this._disconnect(err.code, err.message, true);
4227
- } else {
5034
+ } else if (!stale) {
4228
5035
  this._disconnect(err.code, err.message, false);
4229
5036
  }
4230
5037
  }
@@ -4236,10 +5043,7 @@ var Centrifuge = class extends EventEmitter {
4236
5043
  if (this._emulation && !this._transportWasOpen && !this._triedAllTransports) {
4237
5044
  isInitialHandshake = true;
4238
5045
  }
4239
- let delay = this._getReconnectDelay();
4240
- if (isInitialHandshake) {
4241
- delay = 0;
4242
- }
5046
+ const delay = isInitialHandshake ? 0 : this._getReconnectDelay();
4243
5047
  this._debug("reconnect after " + delay + " milliseconds");
4244
5048
  this._clearReconnectTimeout();
4245
5049
  this._reconnectTimeout = setTimeout(() => {
@@ -4271,8 +5075,10 @@ var Centrifuge = class extends EventEmitter {
4271
5075
  const sub = {
4272
5076
  "recover": true
4273
5077
  };
4274
- if (this._serverSubs[channel].offset) {
4275
- sub["offset"] = this._serverSubs[channel].offset;
5078
+ const pending = this._pendingServerSubOffsets[channel];
5079
+ const offset = pending !== void 0 && pending !== null ? pending : this._serverSubs[channel].offset;
5080
+ if (offset) {
5081
+ sub["offset"] = offset;
4276
5082
  }
4277
5083
  if (this._serverSubs[channel].epoch) {
4278
5084
  sub["epoch"] = this._serverSubs[channel].epoch;
@@ -4314,10 +5120,12 @@ var Centrifuge = class extends EventEmitter {
4314
5120
  return Promise.resolve();
4315
5121
  }
4316
5122
  return new Promise((res, rej) => {
4317
- const timeout = setTimeout(function() {
5123
+ const id = this._nextPromiseId();
5124
+ const timeout = setTimeout(() => {
5125
+ delete this._promises[id];
4318
5126
  rej({ code: errorCodes.timeout, message: "timeout" });
4319
5127
  }, this._config.timeout);
4320
- this._promises[this._nextPromiseId()] = {
5128
+ this._promises[id] = {
4321
5129
  timeout,
4322
5130
  resolve: res,
4323
5131
  reject: rej
@@ -4328,9 +5136,13 @@ var Centrifuge = class extends EventEmitter {
4328
5136
  return new Promise((resolve, reject) => {
4329
5137
  this._call(cmd, false).then((resolveCtx) => {
4330
5138
  var _a;
4331
- const result = resultCB(resolveCtx.reply);
4332
- resolve(result);
4333
- (_a = resolveCtx.next) === null || _a === void 0 ? void 0 : _a.call(resolveCtx);
5139
+ try {
5140
+ resolve(resultCB(resolveCtx.reply));
5141
+ } catch (e) {
5142
+ reject(e);
5143
+ } finally {
5144
+ (_a = resolveCtx.next) === null || _a === void 0 ? void 0 : _a.call(resolveCtx);
5145
+ }
4334
5146
  }, (rejectCtx) => {
4335
5147
  var _a;
4336
5148
  reject(rejectCtx.error);
@@ -4338,25 +5150,36 @@ var Centrifuge = class extends EventEmitter {
4338
5150
  });
4339
5151
  });
4340
5152
  }
4341
- _dataReceived(data) {
4342
- if (this._serverPing > 0) {
5153
+ _dataReceived(data, transportId = this._transportId) {
5154
+ let replies;
5155
+ try {
5156
+ replies = this._codec.decodeReplies(data);
5157
+ } catch (e) {
5158
+ this._debug("error decoding received data", e);
5159
+ if (this._emulation && !this._transportWasOpen) {
5160
+ this._advanceTransportIndex();
5161
+ }
5162
+ this._disconnect(connectingCodes.transportClosed, "transport closed", true);
5163
+ return;
5164
+ }
5165
+ if (this._serverPing > 0 && replies.length > 0) {
4343
5166
  this._waitServerPing();
4344
5167
  }
4345
- const replies = this._codec.decodeReplies(data);
4346
- this._dispatchPromise = this._dispatchPromise.then(() => {
4347
- let finishDispatch;
4348
- this._dispatchPromise = new Promise((resolve) => {
4349
- finishDispatch = resolve;
4350
- });
4351
- this._dispatchSynchronized(replies, finishDispatch);
4352
- });
5168
+ this._dispatchPromise = this._dispatchPromise.then(() => new Promise((resolve) => {
5169
+ this._dispatchSynchronized(replies, resolve, transportId);
5170
+ }));
4353
5171
  }
4354
- _dispatchSynchronized(replies, finishDispatch) {
5172
+ _dispatchSynchronized(replies, finishDispatch, transportId) {
4355
5173
  let p = Promise.resolve();
4356
5174
  for (const i in replies) {
4357
5175
  if (replies.hasOwnProperty(i)) {
4358
5176
  p = p.then(() => {
5177
+ if (this._transportId !== transportId) {
5178
+ return;
5179
+ }
4359
5180
  return this._dispatchReply(replies[i]);
5181
+ }).catch((err) => {
5182
+ this._dispatchFailed(err);
4360
5183
  });
4361
5184
  }
4362
5185
  }
@@ -4364,6 +5187,52 @@ var Centrifuge = class extends EventEmitter {
4364
5187
  finishDispatch();
4365
5188
  });
4366
5189
  }
5190
+ /** @internal Every event reaches the application's handlers through here. */
5191
+ emit(event, ...args) {
5192
+ try {
5193
+ return EventEmitter.prototype.emit.call(this, event, ...args);
5194
+ } catch (e) {
5195
+ this._handlerFailed(e);
5196
+ return true;
5197
+ }
5198
+ }
5199
+ // An exception escaping an application event handler. The SDK can't know what the
5200
+ // handler left undone, so it fails loudly instead of going on as if nothing
5201
+ // happened: the client disconnects without reconnecting, and the exception is
5202
+ // re-raised as an unhandled rejection. The SDK's own work continues as if the
5203
+ // handler had returned, as for a handler calling disconnect(), so no state change
5204
+ // is left half done.
5205
+ /** @internal */
5206
+ _handlerFailed(err) {
5207
+ this._handlerFailures++;
5208
+ this._reportDispatchError(err);
5209
+ this._disconnect(disconnectedCodes.badProtocol, `exception in event handler: ${errorMessage(err)}`, false);
5210
+ }
5211
+ // An exception while handling a reply, other than one of an event handler (see
5212
+ // _handlerFailed), may have left the client's state inconsistent. The client stops
5213
+ // in a state the application can see: it disconnects without reconnecting, so no
5214
+ // later reply is processed.
5215
+ _dispatchFailed(err) {
5216
+ this._reportDispatchError(err);
5217
+ this._disconnect(disconnectedCodes.badProtocol, `exception during message handling: ${errorMessage(err)}`, false);
5218
+ }
5219
+ // The exception is surfaced as an unhandled rejection, as before.
5220
+ _reportDispatchError(err) {
5221
+ this._debug("error dispatching reply", err);
5222
+ Promise.reject(err);
5223
+ }
5224
+ // An exception, other than one of an event handler, while handling a rejected
5225
+ // command. A rejection with next() carries a reply, and is handled as any reply
5226
+ // (see _dispatchFailed). A timeout or a teardown doesn't: stopping the client would
5227
+ // cancel the reconnect or retry the handling scheduled, so the exception is only
5228
+ // reported.
5229
+ _rejectionFailed(err, rejectCtx) {
5230
+ if (rejectCtx.next !== void 0) {
5231
+ this._dispatchFailed(err);
5232
+ } else {
5233
+ this._reportDispatchError(err);
5234
+ }
5235
+ }
4367
5236
  _dispatchReply(reply) {
4368
5237
  let next;
4369
5238
  const p = new Promise((resolve) => {
@@ -4397,16 +5266,25 @@ var Centrifuge = class extends EventEmitter {
4397
5266
  }
4398
5267
  _startConnecting() {
4399
5268
  this._debug("start connecting");
4400
- if (this._setState(State.Connecting)) {
4401
- this.emit("connecting", { code: connectingCodes.connectCalled, reason: "connect called" });
5269
+ const transition = this.state !== State.Connecting ? this._stateTransitions + 1 : 0;
5270
+ try {
5271
+ this._setState(State.Connecting);
5272
+ if (transition && !this._transitionSuperseded(transition)) {
5273
+ this.emit("connecting", { code: connectingCodes.connectCalled, reason: "connect called" });
5274
+ }
5275
+ } finally {
5276
+ if (!this._transitionSuperseded(transition)) {
5277
+ this._client = null;
5278
+ this._startReconnecting();
5279
+ }
4402
5280
  }
4403
- this._client = null;
4404
- this._startReconnecting();
4405
5281
  }
4406
5282
  _disconnect(code, reason, reconnect) {
4407
5283
  if (code === disconnectedCodes.stateInvalidated) {
4408
- this._token = "";
4409
- this._refreshRequired = true;
5284
+ if (this._config.getToken !== null) {
5285
+ this._token = "";
5286
+ this._refreshRequired = true;
5287
+ }
4410
5288
  for (const channel in this._subs) {
4411
5289
  if (this._subs.hasOwnProperty(channel)) {
4412
5290
  this._subs[channel]._invalidateState();
@@ -4420,47 +5298,70 @@ var Centrifuge = class extends EventEmitter {
4420
5298
  }
4421
5299
  }
4422
5300
  if (this._isDisconnected()) {
5301
+ if (!reconnect) {
5302
+ this._rejectPromises({ code: errorCodes.clientDisconnected, message: "disconnected" });
5303
+ }
4423
5304
  return;
4424
5305
  }
4425
- this._transportIsOpen = false;
4426
5306
  const previousState = this.state;
5307
+ const newState = reconnect ? State.Connecting : State.Disconnected;
5308
+ this._transportIsOpen = false;
4427
5309
  this._reconnecting = false;
4428
- const ctx = {
4429
- code,
4430
- reason
4431
- };
4432
- let needEvent;
4433
- if (reconnect) {
4434
- needEvent = this._setState(State.Connecting);
4435
- } else {
4436
- needEvent = this._setState(State.Disconnected);
4437
- this._rejectPromises({ code: errorCodes.clientDisconnected, message: "disconnected" });
4438
- }
4439
- this._clearOutgoingRequests();
4440
5310
  if (previousState === State.Connecting) {
4441
5311
  this._clearReconnectTimeout();
4442
5312
  }
4443
- if (previousState === State.Connected) {
4444
- this._clearConnectedState();
4445
- }
4446
- if (needEvent) {
4447
- if (this._isConnecting()) {
4448
- this.emit("connecting", ctx);
4449
- } else {
4450
- this.emit("disconnected", ctx);
4451
- }
4452
- }
5313
+ this._clearConnectTimeout();
5314
+ this._nextTransportId();
5315
+ this._disconnects++;
4453
5316
  if (this._transport) {
4454
5317
  this._debug("closing existing transport");
4455
5318
  const transport = this._transport;
4456
5319
  this._transport = null;
4457
- transport.close();
5320
+ try {
5321
+ transport.close();
5322
+ } catch (e) {
5323
+ this._debug("error closing transport", e);
5324
+ }
4458
5325
  this._transportClosed = true;
4459
- this._nextTransportId();
4460
5326
  } else {
4461
5327
  this._debug("no transport to close");
4462
5328
  }
4463
- this._scheduleReconnect();
5329
+ this._reconnectAttemptsResetPending = false;
5330
+ this._commands = [];
5331
+ this._clearOutgoingRequests();
5332
+ if (!reconnect) {
5333
+ this._rejectPromises({ code: errorCodes.clientDisconnected, message: "disconnected" });
5334
+ this._deviceWentOffline = false;
5335
+ this._pageCached = false;
5336
+ }
5337
+ const ctx = {
5338
+ code,
5339
+ reason
5340
+ };
5341
+ const transition = this.state !== newState ? this._stateTransitions + 1 : 0;
5342
+ try {
5343
+ try {
5344
+ this._setState(newState);
5345
+ } finally {
5346
+ if (!reconnect && !this._transitionSuperseded(transition)) {
5347
+ this._rejectPromises({ code: errorCodes.clientDisconnected, message: "disconnected" });
5348
+ }
5349
+ if (previousState === State.Connected) {
5350
+ this._clearConnectedState();
5351
+ }
5352
+ }
5353
+ if (transition && !this._transitionSuperseded(transition)) {
5354
+ if (reconnect) {
5355
+ this.emit("connecting", ctx);
5356
+ } else {
5357
+ this.emit("disconnected", ctx);
5358
+ }
5359
+ }
5360
+ } finally {
5361
+ if (reconnect && !this._transitionSuperseded(transition)) {
5362
+ this._scheduleReconnect();
5363
+ }
5364
+ }
4464
5365
  }
4465
5366
  _failUnauthorized() {
4466
5367
  this._disconnect(disconnectedCodes.unauthorized, "unauthorized", false);
@@ -4468,21 +5369,39 @@ var Centrifuge = class extends EventEmitter {
4468
5369
  _getToken() {
4469
5370
  this._debug("get connection token");
4470
5371
  if (!this._config.getToken) {
4471
- this.emit("error", {
4472
- type: "configuration",
4473
- error: {
4474
- code: errorCodes.badConfiguration,
4475
- message: "token expired but no getToken function set in the configuration"
4476
- }
4477
- });
5372
+ try {
5373
+ this.emit("error", {
5374
+ type: "configuration",
5375
+ error: {
5376
+ code: errorCodes.badConfiguration,
5377
+ message: "token expired but no getToken function set in the configuration"
5378
+ }
5379
+ });
5380
+ } catch (e) {
5381
+ this._reportDispatchError(e);
5382
+ }
4478
5383
  return Promise.reject(new UnauthorizedError(""));
4479
5384
  }
4480
- return this._config.getToken({});
5385
+ try {
5386
+ return Promise.resolve(this._config.getToken({}));
5387
+ } catch (e) {
5388
+ return Promise.reject(e);
5389
+ }
5390
+ }
5391
+ // See _getToken.
5392
+ _getData() {
5393
+ try {
5394
+ return Promise.resolve(this._config.getData());
5395
+ } catch (e) {
5396
+ return Promise.reject(e);
5397
+ }
4481
5398
  }
4482
5399
  _refresh() {
4483
5400
  const clientId = this._client;
4484
5401
  const self2 = this;
5402
+ let tokenResolved = false;
4485
5403
  this._getToken().then(function(token) {
5404
+ tokenResolved = true;
4486
5405
  if (clientId !== self2._client) {
4487
5406
  return;
4488
5407
  }
@@ -4499,42 +5418,58 @@ var Centrifuge = class extends EventEmitter {
4499
5418
  refresh: { token: self2._token }
4500
5419
  };
4501
5420
  self2._call(cmd, false).then((resolveCtx) => {
4502
- const result = resolveCtx.reply.refresh;
4503
- self2._refreshResponse(result);
4504
- if (resolveCtx.next) {
4505
- resolveCtx.next();
5421
+ try {
5422
+ self2._refreshResponse(resolveCtx.reply.refresh);
5423
+ } catch (err) {
5424
+ self2._dispatchFailed(err);
5425
+ } finally {
5426
+ if (resolveCtx.next) {
5427
+ resolveCtx.next();
5428
+ }
4506
5429
  }
4507
5430
  }, (rejectCtx) => {
4508
- self2._refreshError(rejectCtx.error);
4509
- if (rejectCtx.next) {
4510
- rejectCtx.next();
5431
+ try {
5432
+ if (clientId === self2._client) {
5433
+ self2._refreshError(rejectCtx.error);
5434
+ }
5435
+ } catch (err) {
5436
+ self2._rejectionFailed(err, rejectCtx);
5437
+ } finally {
5438
+ if (rejectCtx.next) {
5439
+ rejectCtx.next();
5440
+ }
4511
5441
  }
4512
5442
  });
4513
5443
  }).catch(function(e) {
4514
- if (!self2._isConnected()) {
5444
+ if (clientId !== self2._client || !self2._isConnected()) {
5445
+ if (tokenResolved) {
5446
+ self2._reportDispatchError(e);
5447
+ }
4515
5448
  return;
4516
5449
  }
4517
5450
  if (e instanceof UnauthorizedError) {
4518
5451
  self2._failUnauthorized();
4519
5452
  return;
4520
5453
  }
4521
- self2.emit("error", {
5454
+ self2._emitError({
4522
5455
  type: "refreshToken",
4523
5456
  error: {
4524
5457
  code: errorCodes.clientRefreshToken,
4525
5458
  message: e !== void 0 ? e.toString() : ""
4526
5459
  }
5460
+ }, () => {
5461
+ self2._refreshTimeout = setTimeout(() => self2._refresh(), self2._getRefreshRetryDelay());
4527
5462
  });
4528
- self2._refreshTimeout = setTimeout(() => self2._refresh(), self2._getRefreshRetryDelay());
4529
5463
  });
4530
5464
  }
4531
5465
  _refreshError(err) {
4532
5466
  if (err.code < 100 || err.temporary === true) {
4533
- this.emit("error", {
5467
+ this._emitError({
4534
5468
  type: "refresh",
4535
5469
  error: err
5470
+ }, () => {
5471
+ this._refreshTimeout = setTimeout(() => this._refresh(), this._getRefreshRetryDelay());
4536
5472
  });
4537
- this._refreshTimeout = setTimeout(() => this._refresh(), this._getRefreshRetryDelay());
4538
5473
  } else {
4539
5474
  this._disconnect(err.code, err.message, false);
4540
5475
  }
@@ -4548,15 +5483,33 @@ var Centrifuge = class extends EventEmitter {
4548
5483
  this._refreshTimeout = null;
4549
5484
  }
4550
5485
  if (result.expires) {
4551
- this._client = result.client;
5486
+ if (result.client) {
5487
+ this._client = result.client;
5488
+ }
4552
5489
  this._refreshTimeout = setTimeout(() => this._refresh(), ttlMilliseconds(result.ttl));
4553
5490
  }
4554
5491
  }
4555
- _removeSubscription(sub) {
4556
- if (sub === null) {
4557
- return;
4558
- }
4559
- delete this._subs[sub.channel];
5492
+ // A new subscription of the channel waits for the unsubscribe of a removed one, and
5493
+ // of those removed before it still unsubscribing (see _setSubscribing of a
5494
+ // subscription): a subscription removed without sending an unsubscribe doesn't
5495
+ // drop one still pending.
5496
+ _recordRemovedUnsubscribe(sub) {
5497
+ const channel = sub.channel;
5498
+ const previous = this._removedUnsubscribes[channel];
5499
+ const own = sub._unsubPromise;
5500
+ const unsubscribed = previous ? Promise.all([previous, own]).then(() => void 0) : own;
5501
+ this._removedUnsubscribes[channel] = unsubscribed;
5502
+ unsubscribed.then(() => {
5503
+ if (this._removedUnsubscribes[channel] === unsubscribed) {
5504
+ delete this._removedUnsubscribes[channel];
5505
+ }
5506
+ });
5507
+ }
5508
+ // Resolves once a subscription of the channel removed from the client has
5509
+ // unsubscribed (see BaseSubscription._setSubscribing).
5510
+ // @ts-ignore – this is used by a subscription in subscription.ts.
5511
+ _removedSubscriptionUnsubscribed(channel) {
5512
+ return this._removedUnsubscribes[channel] || Promise.resolve();
4560
5513
  }
4561
5514
  _unsubscribe(sub) {
4562
5515
  if (!this._transportIsOpen) {
@@ -4567,6 +5520,7 @@ var Centrifuge = class extends EventEmitter {
4567
5520
  };
4568
5521
  const cmd = { unsubscribe: req };
4569
5522
  const self2 = this;
5523
+ const transportId = this._transportId;
4570
5524
  const unsubscribePromise = new Promise((resolve, _) => {
4571
5525
  this._call(cmd, false).then((resolveCtx) => {
4572
5526
  resolve();
@@ -4578,7 +5532,15 @@ var Centrifuge = class extends EventEmitter {
4578
5532
  if (rejectCtx.next) {
4579
5533
  rejectCtx.next();
4580
5534
  }
4581
- self2._disconnect(connectingCodes.unsubscribeError, "unsubscribe error", true);
5535
+ if (self2._transportId !== transportId) {
5536
+ self2._debug("unsubscribe command of a closed transport rejected");
5537
+ return;
5538
+ }
5539
+ try {
5540
+ self2._disconnect(connectingCodes.unsubscribeError, "unsubscribe error", true);
5541
+ } catch (err) {
5542
+ self2._rejectionFailed(err, rejectCtx);
5543
+ }
4582
5544
  });
4583
5545
  });
4584
5546
  return unsubscribePromise;
@@ -4604,6 +5566,17 @@ var Centrifuge = class extends EventEmitter {
4604
5566
  _isServerSub(channel) {
4605
5567
  return this._serverSubs[channel] !== void 0;
4606
5568
  }
5569
+ // The client-side subscription a push is for. The server allows one subscription
5570
+ // per channel: while a server-side subscription has the channel, its pushes
5571
+ // belong to it unless the client-side subscription is subscribed, e.g. when the
5572
+ // app kept an unsubscribed subscription object of that channel.
5573
+ _getSubForPush(channel, id) {
5574
+ const sub = this._getSub(channel, id);
5575
+ if (sub && channel && this._isServerSub(channel) && !sub._isSubscribed()) {
5576
+ return null;
5577
+ }
5578
+ return sub;
5579
+ }
4607
5580
  _sendSubscribeCommands() {
4608
5581
  const commands = [];
4609
5582
  for (const channel in this._subs) {
@@ -4614,6 +5587,9 @@ var Centrifuge = class extends EventEmitter {
4614
5587
  if (sub._inflight === true) {
4615
5588
  continue;
4616
5589
  }
5590
+ if (sub._resubscribeTimeout !== null) {
5591
+ continue;
5592
+ }
4617
5593
  if (sub.state === SubscriptionState.Subscribing) {
4618
5594
  const cmd = sub._subscribe();
4619
5595
  if (cmd) {
@@ -4626,13 +5602,22 @@ var Centrifuge = class extends EventEmitter {
4626
5602
  _connectResponse(result) {
4627
5603
  this._transportIsOpen = true;
4628
5604
  this._transportWasOpen = true;
4629
- this._reconnectAttempts = 0;
5605
+ if (this._transport !== null && this._transport.emulation()) {
5606
+ this._reconnectAttemptsResetPending = true;
5607
+ } else {
5608
+ this._reconnectAttempts = 0;
5609
+ }
4630
5610
  this._refreshRequired = false;
5611
+ this._deviceWentOffline = false;
5612
+ this._pageCached = false;
4631
5613
  if (this._isConnected()) {
4632
5614
  return;
4633
5615
  }
4634
5616
  this._client = result.client;
4635
- this._setState(State.Connected);
5617
+ const transition = this._setState(State.Connected);
5618
+ if (this._transitionSuperseded(transition)) {
5619
+ return;
5620
+ }
4636
5621
  if (this._refreshTimeout) {
4637
5622
  clearTimeout(this._refreshTimeout);
4638
5623
  }
@@ -4652,8 +5637,11 @@ var Centrifuge = class extends EventEmitter {
4652
5637
  ctx.data = result.data;
4653
5638
  }
4654
5639
  this.emit("connected", ctx);
5640
+ if (this._transitionSuperseded(transition)) {
5641
+ return;
5642
+ }
4655
5643
  this._resolvePromises();
4656
- this._processServerSubs(result.subs || {});
5644
+ this._processServerSubs(result.subs || {}, transition);
4657
5645
  if (result.ping && result.ping > 0) {
4658
5646
  this._serverPing = result.ping * 1e3;
4659
5647
  this._sendPong = result.pong === true;
@@ -4662,16 +5650,20 @@ var Centrifuge = class extends EventEmitter {
4662
5650
  this._serverPing = 0;
4663
5651
  }
4664
5652
  }
4665
- _processServerSubs(subs) {
5653
+ _processServerSubs(subs, transition) {
4666
5654
  for (const channel in subs) {
4667
5655
  if (!subs.hasOwnProperty(channel)) {
4668
5656
  continue;
4669
5657
  }
5658
+ if (this._transitionSuperseded(transition)) {
5659
+ continue;
5660
+ }
4670
5661
  const sub = subs[channel];
4671
5662
  this._serverSubs[channel] = {
4672
- "offset": sub.offset,
5663
+ "offset": toOffset(sub.offset),
4673
5664
  "epoch": sub.epoch,
4674
- "recoverable": sub.recoverable || false
5665
+ "recoverable": sub.recoverable || false,
5666
+ "subscribed": true
4675
5667
  };
4676
5668
  const subCtx = this._getSubscribeContext(channel, sub);
4677
5669
  this.emit("subscribed", subCtx);
@@ -4682,13 +5674,9 @@ var Centrifuge = class extends EventEmitter {
4682
5674
  }
4683
5675
  const sub = subs[channel];
4684
5676
  if (sub.recovered) {
4685
- const pubs = sub.publications;
4686
- if (pubs && pubs.length > 0) {
4687
- for (const i in pubs) {
4688
- if (pubs.hasOwnProperty(i)) {
4689
- this._handlePublication(channel, pubs[i]);
4690
- }
4691
- }
5677
+ const pubs = sub.publications || [];
5678
+ for (let i = 0; i < pubs.length && !this._transitionSuperseded(transition); i++) {
5679
+ this._handlePublication(channel, pubs[i]);
4692
5680
  }
4693
5681
  }
4694
5682
  }
@@ -4714,6 +5702,12 @@ var Centrifuge = class extends EventEmitter {
4714
5702
  this._reconnectTimeout = null;
4715
5703
  }
4716
5704
  }
5705
+ _clearConnectTimeout() {
5706
+ if (this._connectTimeout !== null) {
5707
+ clearTimeout(this._connectTimeout);
5708
+ this._connectTimeout = null;
5709
+ }
5710
+ }
4717
5711
  _clearServerPingTimeout() {
4718
5712
  if (this._serverPingTimeout !== null) {
4719
5713
  clearTimeout(this._serverPingTimeout);
@@ -4728,12 +5722,20 @@ var Centrifuge = class extends EventEmitter {
4728
5722
  return;
4729
5723
  }
4730
5724
  this._clearServerPingTimeout();
5725
+ const delay = this._serverPing + this._config.maxServerPingDelay;
5726
+ const due = Date.now() + delay;
4731
5727
  this._serverPingTimeout = setTimeout(() => {
4732
5728
  if (!this._isConnected()) {
4733
5729
  return;
4734
5730
  }
4735
- this._disconnect(connectingCodes.noPing, "no ping", true);
4736
- }, this._serverPing + this._config.maxServerPingDelay);
5731
+ const late = Date.now() - due >= lateCallTimer;
5732
+ this._serverPingTimeout = setTimeout(() => {
5733
+ if (!this._isConnected()) {
5734
+ return;
5735
+ }
5736
+ this._disconnect(connectingCodes.noPing, "no ping", true);
5737
+ }, late ? lateCallGrace : 0);
5738
+ }, delay);
4737
5739
  }
4738
5740
  _getSubscribeContext(channel, result) {
4739
5741
  const ctx = {
@@ -4762,7 +5764,7 @@ var Centrifuge = class extends EventEmitter {
4762
5764
  }
4763
5765
  let offset = 0;
4764
5766
  if ("offset" in result) {
4765
- offset = result.offset;
5767
+ offset = toOffset(result.offset);
4766
5768
  }
4767
5769
  if (ctx.positioned || ctx.recoverable) {
4768
5770
  ctx.streamPosition = {
@@ -4779,6 +5781,7 @@ var Centrifuge = class extends EventEmitter {
4779
5781
  return ctx;
4780
5782
  }
4781
5783
  _handleReply(reply, next) {
5784
+ this._resetReconnectAttemptsIfPending();
4782
5785
  const id = reply.id;
4783
5786
  if (!(id in this._callbacks)) {
4784
5787
  next();
@@ -4804,7 +5807,7 @@ var Centrifuge = class extends EventEmitter {
4804
5807
  }
4805
5808
  }
4806
5809
  _handleJoin(channel, join, id) {
4807
- const sub = this._getSub(channel, id);
5810
+ const sub = this._getSubForPush(channel, id);
4808
5811
  if (!sub) {
4809
5812
  if (channel && this._isServerSub(channel)) {
4810
5813
  const ctx = { channel, info: this._getJoinLeaveContext(join.info) };
@@ -4812,10 +5815,14 @@ var Centrifuge = class extends EventEmitter {
4812
5815
  }
4813
5816
  return;
4814
5817
  }
5818
+ if (!sub._isSubscribed()) {
5819
+ this._debug("join for a subscription that is not subscribed", channel);
5820
+ return;
5821
+ }
4815
5822
  sub._handleJoin(join);
4816
5823
  }
4817
5824
  _handleLeave(channel, leave, id) {
4818
- const sub = this._getSub(channel, id);
5825
+ const sub = this._getSubForPush(channel, id);
4819
5826
  if (!sub) {
4820
5827
  if (channel && this._isServerSub(channel)) {
4821
5828
  const ctx = { channel, info: this._getJoinLeaveContext(leave.info) };
@@ -4823,17 +5830,25 @@ var Centrifuge = class extends EventEmitter {
4823
5830
  }
4824
5831
  return;
4825
5832
  }
5833
+ if (!sub._isSubscribed()) {
5834
+ this._debug("leave for a subscription that is not subscribed", channel);
5835
+ return;
5836
+ }
4826
5837
  sub._handleLeave(leave);
4827
5838
  }
4828
5839
  _handleUnsubscribe(channel, unsubscribe) {
4829
- const sub = this._getSub(channel, 0);
4830
- if (!sub && channel) {
4831
- if (this._isServerSub(channel)) {
5840
+ const sub = this._getSubForPush(channel, 0);
5841
+ if (!sub) {
5842
+ if (channel && this._isServerSub(channel)) {
4832
5843
  delete this._serverSubs[channel];
4833
5844
  this.emit("unsubscribed", { channel });
4834
5845
  }
4835
5846
  return;
4836
5847
  }
5848
+ if (!sub._isSubscribed()) {
5849
+ this._debug("unsubscribe push for a previous subscription", channel);
5850
+ return;
5851
+ }
4837
5852
  if (unsubscribe.code < 2500) {
4838
5853
  sub._setUnsubscribed(unsubscribe.code, unsubscribe.reason, false);
4839
5854
  } else {
@@ -4845,13 +5860,15 @@ var Centrifuge = class extends EventEmitter {
4845
5860
  }
4846
5861
  _handleSubscribe(channel, sub) {
4847
5862
  this._serverSubs[channel] = {
4848
- "offset": sub.offset,
5863
+ "offset": toOffset(sub.offset),
4849
5864
  "epoch": sub.epoch,
4850
- "recoverable": sub.recoverable || false
5865
+ "recoverable": sub.recoverable || false,
5866
+ "subscribed": true
4851
5867
  };
4852
5868
  this.emit("subscribed", this._getSubscribeContext(channel, sub));
4853
5869
  }
4854
5870
  _handleDisconnect(disconnect) {
5871
+ this._transportWasOpen = true;
4855
5872
  const code = disconnect.code;
4856
5873
  let reconnect = true;
4857
5874
  if (code >= 3500 && code < 4e3 || code >= 4500 && code < 5e3) {
@@ -4864,8 +5881,8 @@ var Centrifuge = class extends EventEmitter {
4864
5881
  channel,
4865
5882
  data: pub.data
4866
5883
  };
4867
- if (pub.offset) {
4868
- ctx.offset = pub.offset;
5884
+ if (hasOffset(pub.offset)) {
5885
+ ctx.offset = toOffset(pub.offset);
4869
5886
  }
4870
5887
  if (pub.info) {
4871
5888
  ctx.info = this._getJoinLeaveContext(pub.info);
@@ -4891,23 +5908,40 @@ var Centrifuge = class extends EventEmitter {
4891
5908
  return info;
4892
5909
  }
4893
5910
  _handlePublication(channel, pub, id) {
4894
- const sub = this._getSub(channel, id);
5911
+ const sub = this._getSubForPush(channel, id);
4895
5912
  if (!sub) {
4896
5913
  if (channel && this._isServerSub(channel)) {
4897
5914
  const ctx = this._getPublicationContext(channel, pub);
4898
- this.emit("publication", ctx);
4899
- if (pub.offset !== void 0) {
4900
- this._serverSubs[channel].offset = pub.offset;
5915
+ this._pendingServerSubOffsets[channel] = hasOffset(pub.offset) ? toOffset(pub.offset) : null;
5916
+ const failures = this._handlerFailures;
5917
+ try {
5918
+ this.emit("publication", ctx);
5919
+ } finally {
5920
+ delete this._pendingServerSubOffsets[channel];
5921
+ }
5922
+ if (this._handlerFailures !== failures) {
5923
+ return;
5924
+ }
5925
+ if (hasOffset(pub.offset)) {
5926
+ this._serverSubs[channel].offset = toOffset(pub.offset);
5927
+ }
5928
+ if (pub.epoch) {
5929
+ this._serverSubs[channel].epoch = pub.epoch;
4901
5930
  }
4902
5931
  }
4903
5932
  return;
4904
5933
  }
5934
+ if (!sub._isSubscribed()) {
5935
+ this._debug("publication for a subscription that is not subscribed", channel);
5936
+ return;
5937
+ }
4905
5938
  sub._handlePublication(pub);
4906
5939
  }
4907
5940
  _handleMessage(message) {
4908
5941
  this.emit("message", { data: message.data });
4909
5942
  }
4910
5943
  _handleServerPing(next) {
5944
+ this._resetReconnectAttemptsIfPending();
4911
5945
  if (this._sendPong) {
4912
5946
  const cmd = {};
4913
5947
  this._transportSendCommands([cmd]);
@@ -4955,11 +5989,15 @@ var Centrifuge = class extends EventEmitter {
4955
5989
  errback,
4956
5990
  timeout: null
4957
5991
  };
5992
+ const due = Date.now() + this._config.timeout;
4958
5993
  this._callbacks[id].timeout = setTimeout(() => {
4959
- delete this._callbacks[id];
4960
- if (isFunction(errback)) {
4961
- errback({ error: this._createErrorObject(errorCodes.timeout, "timeout") });
4962
- }
5994
+ const late = Date.now() - due >= lateCallTimer;
5995
+ this._callbacks[id].timeout = setTimeout(() => {
5996
+ delete this._callbacks[id];
5997
+ if (isFunction(errback)) {
5998
+ errback({ error: this._createErrorObject(errorCodes.timeout, "timeout") });
5999
+ }
6000
+ }, late ? lateCallGrace : 0);
4963
6001
  }, this._config.timeout);
4964
6002
  }
4965
6003
  _addCommand(command) {