@powerhousedao/connect 6.0.0-dev.90 → 6.0.0-dev.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/main.js CHANGED
@@ -7982,7 +7982,7 @@ var init_package = __esm(() => {
7982
7982
  package_default = {
7983
7983
  name: "@powerhousedao/connect",
7984
7984
  productName: "Powerhouse-Connect",
7985
- version: "6.0.0-dev.89",
7985
+ version: "6.0.0-dev.90",
7986
7986
  description: "Powerhouse Connect",
7987
7987
  main: "dist/index.html",
7988
7988
  type: "module",
@@ -19440,6 +19440,7 @@ class GqlRequestChannel {
19440
19440
  cursorStorage;
19441
19441
  operationIndex;
19442
19442
  pollTimer;
19443
+ abortController = new AbortController;
19443
19444
  isShutdown;
19444
19445
  failureCount;
19445
19446
  lastSuccessUtcMs;
@@ -19520,6 +19521,7 @@ class GqlRequestChannel {
19520
19521
  });
19521
19522
  }
19522
19523
  shutdown() {
19524
+ this.abortController.abort();
19523
19525
  this.bufferedOutbox.flush();
19524
19526
  this.isShutdown = true;
19525
19527
  this.pollTimer.stop();
@@ -19547,7 +19549,7 @@ class GqlRequestChannel {
19547
19549
  };
19548
19550
  }
19549
19551
  async init() {
19550
- await this.touchRemoteChannel();
19552
+ const { ackOrdinal } = await this.touchRemoteChannel();
19551
19553
  const cursors = await this.cursorStorage.list(this.remoteName);
19552
19554
  const inboxOrdinal = cursors.find((c2) => c2.cursorType === "inbox")?.cursorOrdinal ?? 0;
19553
19555
  const outboxOrdinal = cursors.find((c2) => c2.cursorType === "outbox")?.cursorOrdinal ?? 0;
@@ -19555,6 +19557,9 @@ class GqlRequestChannel {
19555
19557
  this.outbox.init(outboxOrdinal);
19556
19558
  this.lastPersistedInboxOrdinal = inboxOrdinal;
19557
19559
  this.lastPersistedOutboxOrdinal = outboxOrdinal;
19560
+ if (ackOrdinal > 0) {
19561
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
19562
+ }
19558
19563
  this.pollTimer.setDelegate(() => this.poll());
19559
19564
  this.pollTimer.start();
19560
19565
  this.transitionConnectionState("connected");
@@ -19624,34 +19629,57 @@ class GqlRequestChannel {
19624
19629
  this.deadLetter.add(...syncOps);
19625
19630
  }
19626
19631
  handlePollError(error) {
19632
+ if (this.isShutdown)
19633
+ return true;
19627
19634
  const err = error instanceof Error ? error : new Error(String(error));
19628
19635
  if (err.message.includes("Channel not found")) {
19629
19636
  this.transitionConnectionState("reconnecting");
19630
19637
  this.recoverFromChannelNotFound();
19631
19638
  return true;
19632
19639
  }
19640
+ const classification = this.classifyError(err);
19633
19641
  this.failureCount++;
19634
19642
  this.lastFailureUtcMs = Date.now();
19635
- this.transitionConnectionState("error");
19636
19643
  const channelError = new ChannelError("inbox", err);
19637
- this.logger.error("GqlChannel poll error (@FailureCount): @Error", this.failureCount, channelError);
19644
+ this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
19645
+ if (classification === "unrecoverable") {
19646
+ this.pollTimer.stop();
19647
+ this.transitionConnectionState("error");
19648
+ return true;
19649
+ }
19650
+ this.transitionConnectionState("error");
19638
19651
  return false;
19639
19652
  }
19640
19653
  recoverFromChannelNotFound() {
19641
19654
  this.logger.info("GqlChannel @ChannelId not found on remote, re-registering...", this.channelId);
19642
19655
  this.pollTimer.stop();
19643
- this.touchRemoteChannel().then(() => {
19644
- this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
19645
- this.failureCount = 0;
19646
- this.pollTimer.start();
19647
- this.transitionConnectionState("connected");
19648
- }).catch((recoveryError) => {
19649
- this.logger.error("GqlChannel @ChannelId failed to re-register: @Error", this.channelId, recoveryError);
19650
- this.failureCount++;
19651
- this.lastFailureUtcMs = Date.now();
19652
- this.pollTimer.start();
19653
- this.transitionConnectionState("error");
19654
- });
19656
+ const attemptRecovery = (attempt) => {
19657
+ if (this.isShutdown)
19658
+ return;
19659
+ this.touchRemoteChannel().then(({ ackOrdinal }) => {
19660
+ this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
19661
+ this.failureCount = 0;
19662
+ if (ackOrdinal > 0) {
19663
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
19664
+ }
19665
+ this.pollTimer.start();
19666
+ this.transitionConnectionState("connected");
19667
+ }).catch((recoveryError) => {
19668
+ const err = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
19669
+ const classification = this.classifyError(err);
19670
+ this.logger.error("GqlChannel @ChannelId recovery attempt @Attempt failed (@Classification): @Error", this.channelId, attempt, classification, recoveryError);
19671
+ this.failureCount++;
19672
+ this.lastFailureUtcMs = Date.now();
19673
+ if (classification === "unrecoverable") {
19674
+ this.transitionConnectionState("error");
19675
+ return;
19676
+ }
19677
+ this.transitionConnectionState("reconnecting");
19678
+ const delay = calculateBackoffDelay(attempt, this.config.retryBaseDelayMs, this.config.retryMaxDelayMs, Math.random());
19679
+ setTimeout(() => attemptRecovery(attempt + 1), delay);
19680
+ });
19681
+ };
19682
+ attemptRecovery(1);
19655
19683
  }
19656
19684
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
19657
19685
  const query = `
@@ -19749,7 +19777,10 @@ class GqlRequestChannel {
19749
19777
  } catch {}
19750
19778
  const mutation = `
19751
19779
  mutation TouchChannel($input: TouchChannelInput!) {
19752
- touchChannel(input: $input)
19780
+ touchChannel(input: $input) {
19781
+ success
19782
+ ackOrdinal
19783
+ }
19753
19784
  }
19754
19785
  `;
19755
19786
  const variables = {
@@ -19765,7 +19796,11 @@ class GqlRequestChannel {
19765
19796
  sinceTimestampUtcMs
19766
19797
  }
19767
19798
  };
19768
- await this.executeGraphQL(mutation, variables);
19799
+ const data = await this.executeGraphQL(mutation, variables);
19800
+ if (!data.touchChannel.success) {
19801
+ throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
19802
+ }
19803
+ return { ackOrdinal: data.touchChannel.ackOrdinal };
19769
19804
  }
19770
19805
  attemptPush(syncOps) {
19771
19806
  this.pushSyncOperations(syncOps).then(() => {
@@ -19775,8 +19810,11 @@ class GqlRequestChannel {
19775
19810
  this.transitionConnectionState("connected");
19776
19811
  }
19777
19812
  }).catch((error) => {
19813
+ if (this.isShutdown)
19814
+ return;
19778
19815
  const err = error instanceof Error ? error : new Error(String(error));
19779
- if (this.isRecoverablePushError(err)) {
19816
+ const classification = this.classifyError(err);
19817
+ if (classification === "recoverable") {
19780
19818
  this.pushFailureCount++;
19781
19819
  this.pushBlocked = true;
19782
19820
  this.logger.error("GqlChannel push failed (attempt @FailureCount), will retry: @Error", this.pushFailureCount, err);
@@ -19810,12 +19848,26 @@ class GqlRequestChannel {
19810
19848
  this.attemptPush([...allItems]);
19811
19849
  }, delay);
19812
19850
  }
19813
- isRecoverablePushError(error) {
19814
- if (error.message.startsWith("GraphQL errors:"))
19815
- return false;
19816
- if (error.message === "GraphQL response missing data field")
19817
- return false;
19818
- return true;
19851
+ classifyError(error) {
19852
+ if (!(error instanceof GraphQLRequestError)) {
19853
+ return "recoverable";
19854
+ }
19855
+ switch (error.category) {
19856
+ case "network":
19857
+ return "recoverable";
19858
+ case "http": {
19859
+ if (error.statusCode !== undefined && error.statusCode >= 500) {
19860
+ return "recoverable";
19861
+ }
19862
+ return "unrecoverable";
19863
+ }
19864
+ case "parse":
19865
+ return "recoverable";
19866
+ case "graphql":
19867
+ return "unrecoverable";
19868
+ case "missing-data":
19869
+ return "unrecoverable";
19870
+ }
19819
19871
  }
19820
19872
  async pushSyncOperations(syncOps) {
19821
19873
  for (const syncOp of syncOps) {
@@ -19892,26 +19944,27 @@ class GqlRequestChannel {
19892
19944
  body: JSON.stringify({
19893
19945
  query,
19894
19946
  variables
19895
- })
19947
+ }),
19948
+ signal: this.abortController.signal
19896
19949
  });
19897
19950
  } catch (error) {
19898
- throw new Error(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`);
19951
+ throw new GraphQLRequestError(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`, "network");
19899
19952
  }
19900
19953
  if (!response.ok) {
19901
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
19954
+ throw new GraphQLRequestError(`GraphQL request failed: ${response.status} ${response.statusText}`, "http", response.status);
19902
19955
  }
19903
19956
  let result;
19904
19957
  try {
19905
19958
  result = await response.json();
19906
19959
  } catch (error) {
19907
- throw new Error(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`);
19960
+ throw new GraphQLRequestError(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`, "parse");
19908
19961
  }
19909
19962
  this.logger.verbose("GQL response @channelId @operation status=@status data=@data errors=@errors", this.channelId, operationName, response.status, JSON.stringify(result.data), result.errors ? JSON.stringify(result.errors) : "none");
19910
19963
  if (result.errors) {
19911
- throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
19964
+ throw new GraphQLRequestError(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`, "graphql");
19912
19965
  }
19913
19966
  if (!result.data) {
19914
- throw new Error("GraphQL response missing data field");
19967
+ throw new GraphQLRequestError("GraphQL response missing data field", "missing-data");
19915
19968
  }
19916
19969
  return result.data;
19917
19970
  }
@@ -20831,6 +20884,7 @@ class SyncManager {
20831
20884
  remotes;
20832
20885
  awaiter;
20833
20886
  syncAwaiter;
20887
+ abortController = new AbortController;
20834
20888
  isShutdown;
20835
20889
  eventUnsubscribe;
20836
20890
  failedEventUnsubscribe;
@@ -20891,6 +20945,7 @@ class SyncManager {
20891
20945
  }
20892
20946
  shutdown() {
20893
20947
  this.isShutdown = true;
20948
+ this.abortController.abort();
20894
20949
  this.batchAggregator.clear();
20895
20950
  if (this.eventUnsubscribe) {
20896
20951
  this.eventUnsubscribe();
@@ -21083,6 +21138,8 @@ class SyncManager {
21083
21138
  return Array.from(this.remotes.values()).filter((remote) => remote.collectionId === collectionId);
21084
21139
  }
21085
21140
  async processCompleteBatch(batch) {
21141
+ if (this.isShutdown)
21142
+ return;
21086
21143
  const collectionIds = [
21087
21144
  ...new Set(Object.values(batch.collectionMemberships).flatMap((collections) => collections))
21088
21145
  ];
@@ -21126,8 +21183,10 @@ class SyncManager {
21126
21183
  const operations = syncOp.operations.map((op) => op.operation);
21127
21184
  let jobInfo;
21128
21185
  try {
21129
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, undefined, { sourceRemote: remote.name });
21186
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
21130
21187
  } catch (error) {
21188
+ if (this.isShutdown)
21189
+ return;
21131
21190
  const err = error instanceof Error ? error : new Error(String(error));
21132
21191
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
21133
21192
  const channelError = new ChannelError("inbox", err);
@@ -21138,8 +21197,10 @@ class SyncManager {
21138
21197
  }
21139
21198
  let completedJobInfo;
21140
21199
  try {
21141
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
21200
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
21142
21201
  } catch (error) {
21202
+ if (this.isShutdown)
21203
+ return;
21143
21204
  const err = error instanceof Error ? error : new Error(String(error));
21144
21205
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
21145
21206
  const channelError = new ChannelError("inbox", err);
@@ -21174,10 +21235,10 @@ class SyncManager {
21174
21235
  const request = { jobs };
21175
21236
  let result;
21176
21237
  try {
21177
- result = await this.reactor.loadBatch(request, undefined, {
21178
- sourceRemote
21179
- });
21238
+ result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
21180
21239
  } catch (error) {
21240
+ if (this.isShutdown)
21241
+ return;
21181
21242
  for (const { remote, syncOp } of items) {
21182
21243
  const err = error instanceof Error ? error : new Error(String(error));
21183
21244
  syncOp.failed(new ChannelError("inbox", err));
@@ -21198,8 +21259,10 @@ class SyncManager {
21198
21259
  const jobInfo = result.jobs[syncOp.jobId];
21199
21260
  let completedJobInfo;
21200
21261
  try {
21201
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
21262
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
21202
21263
  } catch (error) {
21264
+ if (this.isShutdown)
21265
+ continue;
21203
21266
  const err = error instanceof Error ? error : new Error(String(error));
21204
21267
  syncOp.failed(new ChannelError("inbox", err));
21205
21268
  remote.channel.deadLetter.add(syncOp);
@@ -21246,7 +21309,7 @@ class SyncManager {
21246
21309
  remote.channel.outbox.add(...syncOps);
21247
21310
  }
21248
21311
  async getOperationsForRemote(remote, ackOrdinal) {
21249
- const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name });
21312
+ const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, undefined, this.abortController.signal);
21250
21313
  let operations = results.results.map((entry) => toOperationWithContext(entry));
21251
21314
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
21252
21315
  if (sinceTimestamp && sinceTimestamp !== "0") {
@@ -22569,7 +22632,7 @@ var __defProp2, __export2 = (target, all) => {
22569
22632
  }
22570
22633
  }, DocumentDeletedError, InvalidSignatureError, DowngradeNotSupportedError, DocumentNotFoundError, getNextIndexForScope = (document2, scope) => {
22571
22634
  return document2.header.revision[scope] || 0;
22572
- }, EventBusAggregateError, ReactorEventTypes, QueueEventTypes, ModuleNotFoundError, DuplicateModuleError, DuplicateManifestError, ManifestNotFoundError, DowngradeNotSupportedError2, MissingUpgradeTransitionError, InvalidUpgradeStepError, STRICT_ORDER_ACTION_TYPES, MAX_SKIP_THRESHOLD = 1000, documentScopeActions, DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive", ProcessorManager, KyselyDocumentView, KyselyDocumentIndexer, DuplicateOperationError, RevisionMismatchError, exports_001_create_operation_table, exports_002_create_keyframe_table, exports_003_create_document_table, exports_004_create_document_relationship_table, exports_005_create_indexer_state_table, exports_006_create_document_snapshot_table, exports_007_create_slug_mapping_table, exports_008_create_view_state_table, exports_009_create_operation_index_tables, exports_010_create_sync_tables, exports_011_add_cursor_type_column, exports_012_add_source_remote_column, exports_013_create_sync_dead_letters_table, exports_014_create_processor_cursor_table, REACTOR_SCHEMA = "reactor", migrations, ChannelScheme, SyncOperationStatus, ChannelErrorSource, SyncEventTypes, MailboxAggregateError, ChannelError, SyncOperationAggregateError, DEFAULT_CONFIG, syncOpCounter = 0, getLatestAppliedOrdinal = (syncOps) => {
22635
+ }, EventBusAggregateError, ReactorEventTypes, QueueEventTypes, ModuleNotFoundError, DuplicateModuleError, DuplicateManifestError, ManifestNotFoundError, DowngradeNotSupportedError2, MissingUpgradeTransitionError, InvalidUpgradeStepError, STRICT_ORDER_ACTION_TYPES, MAX_SKIP_THRESHOLD = 1000, documentScopeActions, DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive", ProcessorManager, KyselyDocumentView, KyselyDocumentIndexer, DuplicateOperationError, RevisionMismatchError, exports_001_create_operation_table, exports_002_create_keyframe_table, exports_003_create_document_table, exports_004_create_document_relationship_table, exports_005_create_indexer_state_table, exports_006_create_document_snapshot_table, exports_007_create_slug_mapping_table, exports_008_create_view_state_table, exports_009_create_operation_index_tables, exports_010_create_sync_tables, exports_011_add_cursor_type_column, exports_012_add_source_remote_column, exports_013_create_sync_dead_letters_table, exports_014_create_processor_cursor_table, REACTOR_SCHEMA = "reactor", migrations, ChannelScheme, SyncOperationStatus, ChannelErrorSource, SyncEventTypes, MailboxAggregateError, GraphQLRequestError, ChannelError, SyncOperationAggregateError, DEFAULT_CONFIG, syncOpCounter = 0, getLatestAppliedOrdinal = (syncOps) => {
22573
22636
  let maxOrdinal = 0;
22574
22637
  for (const syncOp of syncOps) {
22575
22638
  if (syncOp.status === 2) {
@@ -27777,6 +27840,16 @@ var init_src = __esm(() => {
27777
27840
  this.errors = errors2;
27778
27841
  }
27779
27842
  };
27843
+ GraphQLRequestError = class GraphQLRequestError extends Error {
27844
+ statusCode;
27845
+ category;
27846
+ constructor(message, category, statusCode) {
27847
+ super(message);
27848
+ this.name = "GraphQLRequestError";
27849
+ this.category = category;
27850
+ this.statusCode = statusCode;
27851
+ }
27852
+ };
27780
27853
  ChannelError = class ChannelError extends Error {
27781
27854
  source;
27782
27855
  error;
@@ -7982,7 +7982,7 @@ var init_package = __esm(() => {
7982
7982
  package_default = {
7983
7983
  name: "@powerhousedao/connect",
7984
7984
  productName: "Powerhouse-Connect",
7985
- version: "6.0.0-dev.89",
7985
+ version: "6.0.0-dev.90",
7986
7986
  description: "Powerhouse Connect",
7987
7987
  main: "dist/index.html",
7988
7988
  type: "module",
@@ -19440,6 +19440,7 @@ class GqlRequestChannel {
19440
19440
  cursorStorage;
19441
19441
  operationIndex;
19442
19442
  pollTimer;
19443
+ abortController = new AbortController;
19443
19444
  isShutdown;
19444
19445
  failureCount;
19445
19446
  lastSuccessUtcMs;
@@ -19520,6 +19521,7 @@ class GqlRequestChannel {
19520
19521
  });
19521
19522
  }
19522
19523
  shutdown() {
19524
+ this.abortController.abort();
19523
19525
  this.bufferedOutbox.flush();
19524
19526
  this.isShutdown = true;
19525
19527
  this.pollTimer.stop();
@@ -19547,7 +19549,7 @@ class GqlRequestChannel {
19547
19549
  };
19548
19550
  }
19549
19551
  async init() {
19550
- await this.touchRemoteChannel();
19552
+ const { ackOrdinal } = await this.touchRemoteChannel();
19551
19553
  const cursors = await this.cursorStorage.list(this.remoteName);
19552
19554
  const inboxOrdinal = cursors.find((c2) => c2.cursorType === "inbox")?.cursorOrdinal ?? 0;
19553
19555
  const outboxOrdinal = cursors.find((c2) => c2.cursorType === "outbox")?.cursorOrdinal ?? 0;
@@ -19555,6 +19557,9 @@ class GqlRequestChannel {
19555
19557
  this.outbox.init(outboxOrdinal);
19556
19558
  this.lastPersistedInboxOrdinal = inboxOrdinal;
19557
19559
  this.lastPersistedOutboxOrdinal = outboxOrdinal;
19560
+ if (ackOrdinal > 0) {
19561
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
19562
+ }
19558
19563
  this.pollTimer.setDelegate(() => this.poll());
19559
19564
  this.pollTimer.start();
19560
19565
  this.transitionConnectionState("connected");
@@ -19624,34 +19629,57 @@ class GqlRequestChannel {
19624
19629
  this.deadLetter.add(...syncOps);
19625
19630
  }
19626
19631
  handlePollError(error) {
19632
+ if (this.isShutdown)
19633
+ return true;
19627
19634
  const err = error instanceof Error ? error : new Error(String(error));
19628
19635
  if (err.message.includes("Channel not found")) {
19629
19636
  this.transitionConnectionState("reconnecting");
19630
19637
  this.recoverFromChannelNotFound();
19631
19638
  return true;
19632
19639
  }
19640
+ const classification = this.classifyError(err);
19633
19641
  this.failureCount++;
19634
19642
  this.lastFailureUtcMs = Date.now();
19635
- this.transitionConnectionState("error");
19636
19643
  const channelError = new ChannelError("inbox", err);
19637
- this.logger.error("GqlChannel poll error (@FailureCount): @Error", this.failureCount, channelError);
19644
+ this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
19645
+ if (classification === "unrecoverable") {
19646
+ this.pollTimer.stop();
19647
+ this.transitionConnectionState("error");
19648
+ return true;
19649
+ }
19650
+ this.transitionConnectionState("error");
19638
19651
  return false;
19639
19652
  }
19640
19653
  recoverFromChannelNotFound() {
19641
19654
  this.logger.info("GqlChannel @ChannelId not found on remote, re-registering...", this.channelId);
19642
19655
  this.pollTimer.stop();
19643
- this.touchRemoteChannel().then(() => {
19644
- this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
19645
- this.failureCount = 0;
19646
- this.pollTimer.start();
19647
- this.transitionConnectionState("connected");
19648
- }).catch((recoveryError) => {
19649
- this.logger.error("GqlChannel @ChannelId failed to re-register: @Error", this.channelId, recoveryError);
19650
- this.failureCount++;
19651
- this.lastFailureUtcMs = Date.now();
19652
- this.pollTimer.start();
19653
- this.transitionConnectionState("error");
19654
- });
19656
+ const attemptRecovery = (attempt) => {
19657
+ if (this.isShutdown)
19658
+ return;
19659
+ this.touchRemoteChannel().then(({ ackOrdinal }) => {
19660
+ this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
19661
+ this.failureCount = 0;
19662
+ if (ackOrdinal > 0) {
19663
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
19664
+ }
19665
+ this.pollTimer.start();
19666
+ this.transitionConnectionState("connected");
19667
+ }).catch((recoveryError) => {
19668
+ const err = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
19669
+ const classification = this.classifyError(err);
19670
+ this.logger.error("GqlChannel @ChannelId recovery attempt @Attempt failed (@Classification): @Error", this.channelId, attempt, classification, recoveryError);
19671
+ this.failureCount++;
19672
+ this.lastFailureUtcMs = Date.now();
19673
+ if (classification === "unrecoverable") {
19674
+ this.transitionConnectionState("error");
19675
+ return;
19676
+ }
19677
+ this.transitionConnectionState("reconnecting");
19678
+ const delay = calculateBackoffDelay(attempt, this.config.retryBaseDelayMs, this.config.retryMaxDelayMs, Math.random());
19679
+ setTimeout(() => attemptRecovery(attempt + 1), delay);
19680
+ });
19681
+ };
19682
+ attemptRecovery(1);
19655
19683
  }
19656
19684
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
19657
19685
  const query = `
@@ -19749,7 +19777,10 @@ class GqlRequestChannel {
19749
19777
  } catch {}
19750
19778
  const mutation = `
19751
19779
  mutation TouchChannel($input: TouchChannelInput!) {
19752
- touchChannel(input: $input)
19780
+ touchChannel(input: $input) {
19781
+ success
19782
+ ackOrdinal
19783
+ }
19753
19784
  }
19754
19785
  `;
19755
19786
  const variables = {
@@ -19765,7 +19796,11 @@ class GqlRequestChannel {
19765
19796
  sinceTimestampUtcMs
19766
19797
  }
19767
19798
  };
19768
- await this.executeGraphQL(mutation, variables);
19799
+ const data = await this.executeGraphQL(mutation, variables);
19800
+ if (!data.touchChannel.success) {
19801
+ throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
19802
+ }
19803
+ return { ackOrdinal: data.touchChannel.ackOrdinal };
19769
19804
  }
19770
19805
  attemptPush(syncOps) {
19771
19806
  this.pushSyncOperations(syncOps).then(() => {
@@ -19775,8 +19810,11 @@ class GqlRequestChannel {
19775
19810
  this.transitionConnectionState("connected");
19776
19811
  }
19777
19812
  }).catch((error) => {
19813
+ if (this.isShutdown)
19814
+ return;
19778
19815
  const err = error instanceof Error ? error : new Error(String(error));
19779
- if (this.isRecoverablePushError(err)) {
19816
+ const classification = this.classifyError(err);
19817
+ if (classification === "recoverable") {
19780
19818
  this.pushFailureCount++;
19781
19819
  this.pushBlocked = true;
19782
19820
  this.logger.error("GqlChannel push failed (attempt @FailureCount), will retry: @Error", this.pushFailureCount, err);
@@ -19810,12 +19848,26 @@ class GqlRequestChannel {
19810
19848
  this.attemptPush([...allItems]);
19811
19849
  }, delay);
19812
19850
  }
19813
- isRecoverablePushError(error) {
19814
- if (error.message.startsWith("GraphQL errors:"))
19815
- return false;
19816
- if (error.message === "GraphQL response missing data field")
19817
- return false;
19818
- return true;
19851
+ classifyError(error) {
19852
+ if (!(error instanceof GraphQLRequestError)) {
19853
+ return "recoverable";
19854
+ }
19855
+ switch (error.category) {
19856
+ case "network":
19857
+ return "recoverable";
19858
+ case "http": {
19859
+ if (error.statusCode !== undefined && error.statusCode >= 500) {
19860
+ return "recoverable";
19861
+ }
19862
+ return "unrecoverable";
19863
+ }
19864
+ case "parse":
19865
+ return "recoverable";
19866
+ case "graphql":
19867
+ return "unrecoverable";
19868
+ case "missing-data":
19869
+ return "unrecoverable";
19870
+ }
19819
19871
  }
19820
19872
  async pushSyncOperations(syncOps) {
19821
19873
  for (const syncOp of syncOps) {
@@ -19892,26 +19944,27 @@ class GqlRequestChannel {
19892
19944
  body: JSON.stringify({
19893
19945
  query,
19894
19946
  variables
19895
- })
19947
+ }),
19948
+ signal: this.abortController.signal
19896
19949
  });
19897
19950
  } catch (error) {
19898
- throw new Error(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`);
19951
+ throw new GraphQLRequestError(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`, "network");
19899
19952
  }
19900
19953
  if (!response.ok) {
19901
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
19954
+ throw new GraphQLRequestError(`GraphQL request failed: ${response.status} ${response.statusText}`, "http", response.status);
19902
19955
  }
19903
19956
  let result;
19904
19957
  try {
19905
19958
  result = await response.json();
19906
19959
  } catch (error) {
19907
- throw new Error(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`);
19960
+ throw new GraphQLRequestError(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`, "parse");
19908
19961
  }
19909
19962
  this.logger.verbose("GQL response @channelId @operation status=@status data=@data errors=@errors", this.channelId, operationName, response.status, JSON.stringify(result.data), result.errors ? JSON.stringify(result.errors) : "none");
19910
19963
  if (result.errors) {
19911
- throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
19964
+ throw new GraphQLRequestError(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`, "graphql");
19912
19965
  }
19913
19966
  if (!result.data) {
19914
- throw new Error("GraphQL response missing data field");
19967
+ throw new GraphQLRequestError("GraphQL response missing data field", "missing-data");
19915
19968
  }
19916
19969
  return result.data;
19917
19970
  }
@@ -20831,6 +20884,7 @@ class SyncManager {
20831
20884
  remotes;
20832
20885
  awaiter;
20833
20886
  syncAwaiter;
20887
+ abortController = new AbortController;
20834
20888
  isShutdown;
20835
20889
  eventUnsubscribe;
20836
20890
  failedEventUnsubscribe;
@@ -20891,6 +20945,7 @@ class SyncManager {
20891
20945
  }
20892
20946
  shutdown() {
20893
20947
  this.isShutdown = true;
20948
+ this.abortController.abort();
20894
20949
  this.batchAggregator.clear();
20895
20950
  if (this.eventUnsubscribe) {
20896
20951
  this.eventUnsubscribe();
@@ -21083,6 +21138,8 @@ class SyncManager {
21083
21138
  return Array.from(this.remotes.values()).filter((remote) => remote.collectionId === collectionId);
21084
21139
  }
21085
21140
  async processCompleteBatch(batch) {
21141
+ if (this.isShutdown)
21142
+ return;
21086
21143
  const collectionIds = [
21087
21144
  ...new Set(Object.values(batch.collectionMemberships).flatMap((collections) => collections))
21088
21145
  ];
@@ -21126,8 +21183,10 @@ class SyncManager {
21126
21183
  const operations = syncOp.operations.map((op) => op.operation);
21127
21184
  let jobInfo;
21128
21185
  try {
21129
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, undefined, { sourceRemote: remote.name });
21186
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
21130
21187
  } catch (error) {
21188
+ if (this.isShutdown)
21189
+ return;
21131
21190
  const err = error instanceof Error ? error : new Error(String(error));
21132
21191
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
21133
21192
  const channelError = new ChannelError("inbox", err);
@@ -21138,8 +21197,10 @@ class SyncManager {
21138
21197
  }
21139
21198
  let completedJobInfo;
21140
21199
  try {
21141
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
21200
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
21142
21201
  } catch (error) {
21202
+ if (this.isShutdown)
21203
+ return;
21143
21204
  const err = error instanceof Error ? error : new Error(String(error));
21144
21205
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
21145
21206
  const channelError = new ChannelError("inbox", err);
@@ -21174,10 +21235,10 @@ class SyncManager {
21174
21235
  const request = { jobs };
21175
21236
  let result;
21176
21237
  try {
21177
- result = await this.reactor.loadBatch(request, undefined, {
21178
- sourceRemote
21179
- });
21238
+ result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
21180
21239
  } catch (error) {
21240
+ if (this.isShutdown)
21241
+ return;
21181
21242
  for (const { remote, syncOp } of items) {
21182
21243
  const err = error instanceof Error ? error : new Error(String(error));
21183
21244
  syncOp.failed(new ChannelError("inbox", err));
@@ -21198,8 +21259,10 @@ class SyncManager {
21198
21259
  const jobInfo = result.jobs[syncOp.jobId];
21199
21260
  let completedJobInfo;
21200
21261
  try {
21201
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
21262
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
21202
21263
  } catch (error) {
21264
+ if (this.isShutdown)
21265
+ continue;
21203
21266
  const err = error instanceof Error ? error : new Error(String(error));
21204
21267
  syncOp.failed(new ChannelError("inbox", err));
21205
21268
  remote.channel.deadLetter.add(syncOp);
@@ -21246,7 +21309,7 @@ class SyncManager {
21246
21309
  remote.channel.outbox.add(...syncOps);
21247
21310
  }
21248
21311
  async getOperationsForRemote(remote, ackOrdinal) {
21249
- const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name });
21312
+ const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, undefined, this.abortController.signal);
21250
21313
  let operations = results.results.map((entry) => toOperationWithContext(entry));
21251
21314
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
21252
21315
  if (sinceTimestamp && sinceTimestamp !== "0") {
@@ -22569,7 +22632,7 @@ var __defProp2, __export2 = (target, all) => {
22569
22632
  }
22570
22633
  }, DocumentDeletedError, InvalidSignatureError, DowngradeNotSupportedError, DocumentNotFoundError, getNextIndexForScope = (document2, scope) => {
22571
22634
  return document2.header.revision[scope] || 0;
22572
- }, EventBusAggregateError, ReactorEventTypes, QueueEventTypes, ModuleNotFoundError, DuplicateModuleError, DuplicateManifestError, ManifestNotFoundError, DowngradeNotSupportedError2, MissingUpgradeTransitionError, InvalidUpgradeStepError, STRICT_ORDER_ACTION_TYPES, MAX_SKIP_THRESHOLD = 1000, documentScopeActions, DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive", ProcessorManager, KyselyDocumentView, KyselyDocumentIndexer, DuplicateOperationError, RevisionMismatchError, exports_001_create_operation_table, exports_002_create_keyframe_table, exports_003_create_document_table, exports_004_create_document_relationship_table, exports_005_create_indexer_state_table, exports_006_create_document_snapshot_table, exports_007_create_slug_mapping_table, exports_008_create_view_state_table, exports_009_create_operation_index_tables, exports_010_create_sync_tables, exports_011_add_cursor_type_column, exports_012_add_source_remote_column, exports_013_create_sync_dead_letters_table, exports_014_create_processor_cursor_table, REACTOR_SCHEMA = "reactor", migrations, ChannelScheme, SyncOperationStatus, ChannelErrorSource, SyncEventTypes, MailboxAggregateError, ChannelError, SyncOperationAggregateError, DEFAULT_CONFIG, syncOpCounter = 0, getLatestAppliedOrdinal = (syncOps) => {
22635
+ }, EventBusAggregateError, ReactorEventTypes, QueueEventTypes, ModuleNotFoundError, DuplicateModuleError, DuplicateManifestError, ManifestNotFoundError, DowngradeNotSupportedError2, MissingUpgradeTransitionError, InvalidUpgradeStepError, STRICT_ORDER_ACTION_TYPES, MAX_SKIP_THRESHOLD = 1000, documentScopeActions, DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive", ProcessorManager, KyselyDocumentView, KyselyDocumentIndexer, DuplicateOperationError, RevisionMismatchError, exports_001_create_operation_table, exports_002_create_keyframe_table, exports_003_create_document_table, exports_004_create_document_relationship_table, exports_005_create_indexer_state_table, exports_006_create_document_snapshot_table, exports_007_create_slug_mapping_table, exports_008_create_view_state_table, exports_009_create_operation_index_tables, exports_010_create_sync_tables, exports_011_add_cursor_type_column, exports_012_add_source_remote_column, exports_013_create_sync_dead_letters_table, exports_014_create_processor_cursor_table, REACTOR_SCHEMA = "reactor", migrations, ChannelScheme, SyncOperationStatus, ChannelErrorSource, SyncEventTypes, MailboxAggregateError, GraphQLRequestError, ChannelError, SyncOperationAggregateError, DEFAULT_CONFIG, syncOpCounter = 0, getLatestAppliedOrdinal = (syncOps) => {
22573
22636
  let maxOrdinal = 0;
22574
22637
  for (const syncOp of syncOps) {
22575
22638
  if (syncOp.status === 2) {
@@ -27777,6 +27840,16 @@ var init_src = __esm(() => {
27777
27840
  this.errors = errors2;
27778
27841
  }
27779
27842
  };
27843
+ GraphQLRequestError = class GraphQLRequestError extends Error {
27844
+ statusCode;
27845
+ category;
27846
+ constructor(message, category, statusCode) {
27847
+ super(message);
27848
+ this.name = "GraphQLRequestError";
27849
+ this.category = category;
27850
+ this.statusCode = statusCode;
27851
+ }
27852
+ };
27780
27853
  ChannelError = class ChannelError extends Error {
27781
27854
  source;
27782
27855
  error;