@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.
@@ -48598,6 +48598,7 @@ class GqlRequestChannel {
48598
48598
  cursorStorage;
48599
48599
  operationIndex;
48600
48600
  pollTimer;
48601
+ abortController = new AbortController;
48601
48602
  isShutdown;
48602
48603
  failureCount;
48603
48604
  lastSuccessUtcMs;
@@ -48678,6 +48679,7 @@ class GqlRequestChannel {
48678
48679
  });
48679
48680
  }
48680
48681
  shutdown() {
48682
+ this.abortController.abort();
48681
48683
  this.bufferedOutbox.flush();
48682
48684
  this.isShutdown = true;
48683
48685
  this.pollTimer.stop();
@@ -48705,7 +48707,7 @@ class GqlRequestChannel {
48705
48707
  };
48706
48708
  }
48707
48709
  async init() {
48708
- await this.touchRemoteChannel();
48710
+ const { ackOrdinal } = await this.touchRemoteChannel();
48709
48711
  const cursors = await this.cursorStorage.list(this.remoteName);
48710
48712
  const inboxOrdinal = cursors.find((c3) => c3.cursorType === "inbox")?.cursorOrdinal ?? 0;
48711
48713
  const outboxOrdinal = cursors.find((c3) => c3.cursorType === "outbox")?.cursorOrdinal ?? 0;
@@ -48713,6 +48715,9 @@ class GqlRequestChannel {
48713
48715
  this.outbox.init(outboxOrdinal);
48714
48716
  this.lastPersistedInboxOrdinal = inboxOrdinal;
48715
48717
  this.lastPersistedOutboxOrdinal = outboxOrdinal;
48718
+ if (ackOrdinal > 0) {
48719
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
48720
+ }
48716
48721
  this.pollTimer.setDelegate(() => this.poll());
48717
48722
  this.pollTimer.start();
48718
48723
  this.transitionConnectionState("connected");
@@ -48782,34 +48787,57 @@ class GqlRequestChannel {
48782
48787
  this.deadLetter.add(...syncOps);
48783
48788
  }
48784
48789
  handlePollError(error3) {
48790
+ if (this.isShutdown)
48791
+ return true;
48785
48792
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
48786
48793
  if (err.message.includes("Channel not found")) {
48787
48794
  this.transitionConnectionState("reconnecting");
48788
48795
  this.recoverFromChannelNotFound();
48789
48796
  return true;
48790
48797
  }
48798
+ const classification = this.classifyError(err);
48791
48799
  this.failureCount++;
48792
48800
  this.lastFailureUtcMs = Date.now();
48793
- this.transitionConnectionState("error");
48794
48801
  const channelError = new ChannelError("inbox", err);
48795
- this.logger.error("GqlChannel poll error (@FailureCount): @Error", this.failureCount, channelError);
48802
+ this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
48803
+ if (classification === "unrecoverable") {
48804
+ this.pollTimer.stop();
48805
+ this.transitionConnectionState("error");
48806
+ return true;
48807
+ }
48808
+ this.transitionConnectionState("error");
48796
48809
  return false;
48797
48810
  }
48798
48811
  recoverFromChannelNotFound() {
48799
48812
  this.logger.info("GqlChannel @ChannelId not found on remote, re-registering...", this.channelId);
48800
48813
  this.pollTimer.stop();
48801
- this.touchRemoteChannel().then(() => {
48802
- this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
48803
- this.failureCount = 0;
48804
- this.pollTimer.start();
48805
- this.transitionConnectionState("connected");
48806
- }).catch((recoveryError) => {
48807
- this.logger.error("GqlChannel @ChannelId failed to re-register: @Error", this.channelId, recoveryError);
48808
- this.failureCount++;
48809
- this.lastFailureUtcMs = Date.now();
48810
- this.pollTimer.start();
48811
- this.transitionConnectionState("error");
48812
- });
48814
+ const attemptRecovery = (attempt) => {
48815
+ if (this.isShutdown)
48816
+ return;
48817
+ this.touchRemoteChannel().then(({ ackOrdinal }) => {
48818
+ this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
48819
+ this.failureCount = 0;
48820
+ if (ackOrdinal > 0) {
48821
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
48822
+ }
48823
+ this.pollTimer.start();
48824
+ this.transitionConnectionState("connected");
48825
+ }).catch((recoveryError) => {
48826
+ const err = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
48827
+ const classification = this.classifyError(err);
48828
+ this.logger.error("GqlChannel @ChannelId recovery attempt @Attempt failed (@Classification): @Error", this.channelId, attempt, classification, recoveryError);
48829
+ this.failureCount++;
48830
+ this.lastFailureUtcMs = Date.now();
48831
+ if (classification === "unrecoverable") {
48832
+ this.transitionConnectionState("error");
48833
+ return;
48834
+ }
48835
+ this.transitionConnectionState("reconnecting");
48836
+ const delay = calculateBackoffDelay(attempt, this.config.retryBaseDelayMs, this.config.retryMaxDelayMs, Math.random());
48837
+ setTimeout(() => attemptRecovery(attempt + 1), delay);
48838
+ });
48839
+ };
48840
+ attemptRecovery(1);
48813
48841
  }
48814
48842
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
48815
48843
  const query = `
@@ -48907,7 +48935,10 @@ class GqlRequestChannel {
48907
48935
  } catch {}
48908
48936
  const mutation = `
48909
48937
  mutation TouchChannel($input: TouchChannelInput!) {
48910
- touchChannel(input: $input)
48938
+ touchChannel(input: $input) {
48939
+ success
48940
+ ackOrdinal
48941
+ }
48911
48942
  }
48912
48943
  `;
48913
48944
  const variables = {
@@ -48923,7 +48954,11 @@ class GqlRequestChannel {
48923
48954
  sinceTimestampUtcMs
48924
48955
  }
48925
48956
  };
48926
- await this.executeGraphQL(mutation, variables);
48957
+ const data = await this.executeGraphQL(mutation, variables);
48958
+ if (!data.touchChannel.success) {
48959
+ throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
48960
+ }
48961
+ return { ackOrdinal: data.touchChannel.ackOrdinal };
48927
48962
  }
48928
48963
  attemptPush(syncOps) {
48929
48964
  this.pushSyncOperations(syncOps).then(() => {
@@ -48933,8 +48968,11 @@ class GqlRequestChannel {
48933
48968
  this.transitionConnectionState("connected");
48934
48969
  }
48935
48970
  }).catch((error3) => {
48971
+ if (this.isShutdown)
48972
+ return;
48936
48973
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
48937
- if (this.isRecoverablePushError(err)) {
48974
+ const classification = this.classifyError(err);
48975
+ if (classification === "recoverable") {
48938
48976
  this.pushFailureCount++;
48939
48977
  this.pushBlocked = true;
48940
48978
  this.logger.error("GqlChannel push failed (attempt @FailureCount), will retry: @Error", this.pushFailureCount, err);
@@ -48968,12 +49006,26 @@ class GqlRequestChannel {
48968
49006
  this.attemptPush([...allItems]);
48969
49007
  }, delay);
48970
49008
  }
48971
- isRecoverablePushError(error3) {
48972
- if (error3.message.startsWith("GraphQL errors:"))
48973
- return false;
48974
- if (error3.message === "GraphQL response missing data field")
48975
- return false;
48976
- return true;
49009
+ classifyError(error3) {
49010
+ if (!(error3 instanceof GraphQLRequestError)) {
49011
+ return "recoverable";
49012
+ }
49013
+ switch (error3.category) {
49014
+ case "network":
49015
+ return "recoverable";
49016
+ case "http": {
49017
+ if (error3.statusCode !== undefined && error3.statusCode >= 500) {
49018
+ return "recoverable";
49019
+ }
49020
+ return "unrecoverable";
49021
+ }
49022
+ case "parse":
49023
+ return "recoverable";
49024
+ case "graphql":
49025
+ return "unrecoverable";
49026
+ case "missing-data":
49027
+ return "unrecoverable";
49028
+ }
48977
49029
  }
48978
49030
  async pushSyncOperations(syncOps) {
48979
49031
  for (const syncOp of syncOps) {
@@ -49050,26 +49102,27 @@ class GqlRequestChannel {
49050
49102
  body: JSON.stringify({
49051
49103
  query,
49052
49104
  variables
49053
- })
49105
+ }),
49106
+ signal: this.abortController.signal
49054
49107
  });
49055
49108
  } catch (error3) {
49056
- throw new Error(`GraphQL request failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
49109
+ throw new GraphQLRequestError(`GraphQL request failed: ${error3 instanceof Error ? error3.message : String(error3)}`, "network");
49057
49110
  }
49058
49111
  if (!response.ok) {
49059
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
49112
+ throw new GraphQLRequestError(`GraphQL request failed: ${response.status} ${response.statusText}`, "http", response.status);
49060
49113
  }
49061
49114
  let result;
49062
49115
  try {
49063
49116
  result = await response.json();
49064
49117
  } catch (error3) {
49065
- throw new Error(`Failed to parse GraphQL response: ${error3 instanceof Error ? error3.message : String(error3)}`);
49118
+ throw new GraphQLRequestError(`Failed to parse GraphQL response: ${error3 instanceof Error ? error3.message : String(error3)}`, "parse");
49066
49119
  }
49067
49120
  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");
49068
49121
  if (result.errors) {
49069
- throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
49122
+ throw new GraphQLRequestError(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`, "graphql");
49070
49123
  }
49071
49124
  if (!result.data) {
49072
- throw new Error("GraphQL response missing data field");
49125
+ throw new GraphQLRequestError("GraphQL response missing data field", "missing-data");
49073
49126
  }
49074
49127
  return result.data;
49075
49128
  }
@@ -49989,6 +50042,7 @@ class SyncManager {
49989
50042
  remotes;
49990
50043
  awaiter;
49991
50044
  syncAwaiter;
50045
+ abortController = new AbortController;
49992
50046
  isShutdown;
49993
50047
  eventUnsubscribe;
49994
50048
  failedEventUnsubscribe;
@@ -50049,6 +50103,7 @@ class SyncManager {
50049
50103
  }
50050
50104
  shutdown() {
50051
50105
  this.isShutdown = true;
50106
+ this.abortController.abort();
50052
50107
  this.batchAggregator.clear();
50053
50108
  if (this.eventUnsubscribe) {
50054
50109
  this.eventUnsubscribe();
@@ -50241,6 +50296,8 @@ class SyncManager {
50241
50296
  return Array.from(this.remotes.values()).filter((remote) => remote.collectionId === collectionId);
50242
50297
  }
50243
50298
  async processCompleteBatch(batch) {
50299
+ if (this.isShutdown)
50300
+ return;
50244
50301
  const collectionIds = [
50245
50302
  ...new Set(Object.values(batch.collectionMemberships).flatMap((collections) => collections))
50246
50303
  ];
@@ -50284,8 +50341,10 @@ class SyncManager {
50284
50341
  const operations = syncOp.operations.map((op) => op.operation);
50285
50342
  let jobInfo;
50286
50343
  try {
50287
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, undefined, { sourceRemote: remote.name });
50344
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
50288
50345
  } catch (error3) {
50346
+ if (this.isShutdown)
50347
+ return;
50289
50348
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50290
50349
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
50291
50350
  const channelError = new ChannelError("inbox", err);
@@ -50296,8 +50355,10 @@ class SyncManager {
50296
50355
  }
50297
50356
  let completedJobInfo;
50298
50357
  try {
50299
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
50358
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
50300
50359
  } catch (error3) {
50360
+ if (this.isShutdown)
50361
+ return;
50301
50362
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50302
50363
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
50303
50364
  const channelError = new ChannelError("inbox", err);
@@ -50332,10 +50393,10 @@ class SyncManager {
50332
50393
  const request = { jobs };
50333
50394
  let result;
50334
50395
  try {
50335
- result = await this.reactor.loadBatch(request, undefined, {
50336
- sourceRemote
50337
- });
50396
+ result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
50338
50397
  } catch (error3) {
50398
+ if (this.isShutdown)
50399
+ return;
50339
50400
  for (const { remote, syncOp } of items) {
50340
50401
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50341
50402
  syncOp.failed(new ChannelError("inbox", err));
@@ -50356,8 +50417,10 @@ class SyncManager {
50356
50417
  const jobInfo = result.jobs[syncOp.jobId];
50357
50418
  let completedJobInfo;
50358
50419
  try {
50359
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
50420
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
50360
50421
  } catch (error3) {
50422
+ if (this.isShutdown)
50423
+ continue;
50361
50424
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50362
50425
  syncOp.failed(new ChannelError("inbox", err));
50363
50426
  remote.channel.deadLetter.add(syncOp);
@@ -50404,7 +50467,7 @@ class SyncManager {
50404
50467
  remote.channel.outbox.add(...syncOps);
50405
50468
  }
50406
50469
  async getOperationsForRemote(remote, ackOrdinal) {
50407
- const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name });
50470
+ const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, undefined, this.abortController.signal);
50408
50471
  let operations = results.results.map((entry) => toOperationWithContext(entry));
50409
50472
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
50410
50473
  if (sinceTimestamp && sinceTimestamp !== "0") {
@@ -51727,7 +51790,7 @@ var __defProp2, __export2 = (target, all) => {
51727
51790
  }
51728
51791
  }, DocumentDeletedError, InvalidSignatureError, DowngradeNotSupportedError, DocumentNotFoundError, getNextIndexForScope = (document2, scope) => {
51729
51792
  return document2.header.revision[scope] || 0;
51730
- }, 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) => {
51793
+ }, 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) => {
51731
51794
  let maxOrdinal = 0;
51732
51795
  for (const syncOp of syncOps) {
51733
51796
  if (syncOp.status === 2) {
@@ -56935,6 +56998,16 @@ var init_src = __esm(() => {
56935
56998
  this.errors = errors2;
56936
56999
  }
56937
57000
  };
57001
+ GraphQLRequestError = class GraphQLRequestError extends Error {
57002
+ statusCode;
57003
+ category;
57004
+ constructor(message, category, statusCode) {
57005
+ super(message);
57006
+ this.name = "GraphQLRequestError";
57007
+ this.category = category;
57008
+ this.statusCode = statusCode;
57009
+ }
57010
+ };
56938
57011
  ChannelError = class ChannelError extends Error {
56939
57012
  source;
56940
57013
  error;
@@ -93773,7 +93846,7 @@ var init_package = __esm(() => {
93773
93846
  package_default = {
93774
93847
  name: "@powerhousedao/connect",
93775
93848
  productName: "Powerhouse-Connect",
93776
- version: "6.0.0-dev.89",
93849
+ version: "6.0.0-dev.90",
93777
93850
  description: "Powerhouse Connect",
93778
93851
  main: "dist/index.html",
93779
93852
  type: "module",
@@ -48598,6 +48598,7 @@ class GqlRequestChannel {
48598
48598
  cursorStorage;
48599
48599
  operationIndex;
48600
48600
  pollTimer;
48601
+ abortController = new AbortController;
48601
48602
  isShutdown;
48602
48603
  failureCount;
48603
48604
  lastSuccessUtcMs;
@@ -48678,6 +48679,7 @@ class GqlRequestChannel {
48678
48679
  });
48679
48680
  }
48680
48681
  shutdown() {
48682
+ this.abortController.abort();
48681
48683
  this.bufferedOutbox.flush();
48682
48684
  this.isShutdown = true;
48683
48685
  this.pollTimer.stop();
@@ -48705,7 +48707,7 @@ class GqlRequestChannel {
48705
48707
  };
48706
48708
  }
48707
48709
  async init() {
48708
- await this.touchRemoteChannel();
48710
+ const { ackOrdinal } = await this.touchRemoteChannel();
48709
48711
  const cursors = await this.cursorStorage.list(this.remoteName);
48710
48712
  const inboxOrdinal = cursors.find((c3) => c3.cursorType === "inbox")?.cursorOrdinal ?? 0;
48711
48713
  const outboxOrdinal = cursors.find((c3) => c3.cursorType === "outbox")?.cursorOrdinal ?? 0;
@@ -48713,6 +48715,9 @@ class GqlRequestChannel {
48713
48715
  this.outbox.init(outboxOrdinal);
48714
48716
  this.lastPersistedInboxOrdinal = inboxOrdinal;
48715
48717
  this.lastPersistedOutboxOrdinal = outboxOrdinal;
48718
+ if (ackOrdinal > 0) {
48719
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
48720
+ }
48716
48721
  this.pollTimer.setDelegate(() => this.poll());
48717
48722
  this.pollTimer.start();
48718
48723
  this.transitionConnectionState("connected");
@@ -48782,34 +48787,57 @@ class GqlRequestChannel {
48782
48787
  this.deadLetter.add(...syncOps);
48783
48788
  }
48784
48789
  handlePollError(error3) {
48790
+ if (this.isShutdown)
48791
+ return true;
48785
48792
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
48786
48793
  if (err.message.includes("Channel not found")) {
48787
48794
  this.transitionConnectionState("reconnecting");
48788
48795
  this.recoverFromChannelNotFound();
48789
48796
  return true;
48790
48797
  }
48798
+ const classification = this.classifyError(err);
48791
48799
  this.failureCount++;
48792
48800
  this.lastFailureUtcMs = Date.now();
48793
- this.transitionConnectionState("error");
48794
48801
  const channelError = new ChannelError("inbox", err);
48795
- this.logger.error("GqlChannel poll error (@FailureCount): @Error", this.failureCount, channelError);
48802
+ this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
48803
+ if (classification === "unrecoverable") {
48804
+ this.pollTimer.stop();
48805
+ this.transitionConnectionState("error");
48806
+ return true;
48807
+ }
48808
+ this.transitionConnectionState("error");
48796
48809
  return false;
48797
48810
  }
48798
48811
  recoverFromChannelNotFound() {
48799
48812
  this.logger.info("GqlChannel @ChannelId not found on remote, re-registering...", this.channelId);
48800
48813
  this.pollTimer.stop();
48801
- this.touchRemoteChannel().then(() => {
48802
- this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
48803
- this.failureCount = 0;
48804
- this.pollTimer.start();
48805
- this.transitionConnectionState("connected");
48806
- }).catch((recoveryError) => {
48807
- this.logger.error("GqlChannel @ChannelId failed to re-register: @Error", this.channelId, recoveryError);
48808
- this.failureCount++;
48809
- this.lastFailureUtcMs = Date.now();
48810
- this.pollTimer.start();
48811
- this.transitionConnectionState("error");
48812
- });
48814
+ const attemptRecovery = (attempt) => {
48815
+ if (this.isShutdown)
48816
+ return;
48817
+ this.touchRemoteChannel().then(({ ackOrdinal }) => {
48818
+ this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
48819
+ this.failureCount = 0;
48820
+ if (ackOrdinal > 0) {
48821
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
48822
+ }
48823
+ this.pollTimer.start();
48824
+ this.transitionConnectionState("connected");
48825
+ }).catch((recoveryError) => {
48826
+ const err = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
48827
+ const classification = this.classifyError(err);
48828
+ this.logger.error("GqlChannel @ChannelId recovery attempt @Attempt failed (@Classification): @Error", this.channelId, attempt, classification, recoveryError);
48829
+ this.failureCount++;
48830
+ this.lastFailureUtcMs = Date.now();
48831
+ if (classification === "unrecoverable") {
48832
+ this.transitionConnectionState("error");
48833
+ return;
48834
+ }
48835
+ this.transitionConnectionState("reconnecting");
48836
+ const delay = calculateBackoffDelay(attempt, this.config.retryBaseDelayMs, this.config.retryMaxDelayMs, Math.random());
48837
+ setTimeout(() => attemptRecovery(attempt + 1), delay);
48838
+ });
48839
+ };
48840
+ attemptRecovery(1);
48813
48841
  }
48814
48842
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
48815
48843
  const query = `
@@ -48907,7 +48935,10 @@ class GqlRequestChannel {
48907
48935
  } catch {}
48908
48936
  const mutation = `
48909
48937
  mutation TouchChannel($input: TouchChannelInput!) {
48910
- touchChannel(input: $input)
48938
+ touchChannel(input: $input) {
48939
+ success
48940
+ ackOrdinal
48941
+ }
48911
48942
  }
48912
48943
  `;
48913
48944
  const variables = {
@@ -48923,7 +48954,11 @@ class GqlRequestChannel {
48923
48954
  sinceTimestampUtcMs
48924
48955
  }
48925
48956
  };
48926
- await this.executeGraphQL(mutation, variables);
48957
+ const data = await this.executeGraphQL(mutation, variables);
48958
+ if (!data.touchChannel.success) {
48959
+ throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
48960
+ }
48961
+ return { ackOrdinal: data.touchChannel.ackOrdinal };
48927
48962
  }
48928
48963
  attemptPush(syncOps) {
48929
48964
  this.pushSyncOperations(syncOps).then(() => {
@@ -48933,8 +48968,11 @@ class GqlRequestChannel {
48933
48968
  this.transitionConnectionState("connected");
48934
48969
  }
48935
48970
  }).catch((error3) => {
48971
+ if (this.isShutdown)
48972
+ return;
48936
48973
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
48937
- if (this.isRecoverablePushError(err)) {
48974
+ const classification = this.classifyError(err);
48975
+ if (classification === "recoverable") {
48938
48976
  this.pushFailureCount++;
48939
48977
  this.pushBlocked = true;
48940
48978
  this.logger.error("GqlChannel push failed (attempt @FailureCount), will retry: @Error", this.pushFailureCount, err);
@@ -48968,12 +49006,26 @@ class GqlRequestChannel {
48968
49006
  this.attemptPush([...allItems]);
48969
49007
  }, delay);
48970
49008
  }
48971
- isRecoverablePushError(error3) {
48972
- if (error3.message.startsWith("GraphQL errors:"))
48973
- return false;
48974
- if (error3.message === "GraphQL response missing data field")
48975
- return false;
48976
- return true;
49009
+ classifyError(error3) {
49010
+ if (!(error3 instanceof GraphQLRequestError)) {
49011
+ return "recoverable";
49012
+ }
49013
+ switch (error3.category) {
49014
+ case "network":
49015
+ return "recoverable";
49016
+ case "http": {
49017
+ if (error3.statusCode !== undefined && error3.statusCode >= 500) {
49018
+ return "recoverable";
49019
+ }
49020
+ return "unrecoverable";
49021
+ }
49022
+ case "parse":
49023
+ return "recoverable";
49024
+ case "graphql":
49025
+ return "unrecoverable";
49026
+ case "missing-data":
49027
+ return "unrecoverable";
49028
+ }
48977
49029
  }
48978
49030
  async pushSyncOperations(syncOps) {
48979
49031
  for (const syncOp of syncOps) {
@@ -49050,26 +49102,27 @@ class GqlRequestChannel {
49050
49102
  body: JSON.stringify({
49051
49103
  query,
49052
49104
  variables
49053
- })
49105
+ }),
49106
+ signal: this.abortController.signal
49054
49107
  });
49055
49108
  } catch (error3) {
49056
- throw new Error(`GraphQL request failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
49109
+ throw new GraphQLRequestError(`GraphQL request failed: ${error3 instanceof Error ? error3.message : String(error3)}`, "network");
49057
49110
  }
49058
49111
  if (!response.ok) {
49059
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
49112
+ throw new GraphQLRequestError(`GraphQL request failed: ${response.status} ${response.statusText}`, "http", response.status);
49060
49113
  }
49061
49114
  let result;
49062
49115
  try {
49063
49116
  result = await response.json();
49064
49117
  } catch (error3) {
49065
- throw new Error(`Failed to parse GraphQL response: ${error3 instanceof Error ? error3.message : String(error3)}`);
49118
+ throw new GraphQLRequestError(`Failed to parse GraphQL response: ${error3 instanceof Error ? error3.message : String(error3)}`, "parse");
49066
49119
  }
49067
49120
  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");
49068
49121
  if (result.errors) {
49069
- throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
49122
+ throw new GraphQLRequestError(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`, "graphql");
49070
49123
  }
49071
49124
  if (!result.data) {
49072
- throw new Error("GraphQL response missing data field");
49125
+ throw new GraphQLRequestError("GraphQL response missing data field", "missing-data");
49073
49126
  }
49074
49127
  return result.data;
49075
49128
  }
@@ -49989,6 +50042,7 @@ class SyncManager {
49989
50042
  remotes;
49990
50043
  awaiter;
49991
50044
  syncAwaiter;
50045
+ abortController = new AbortController;
49992
50046
  isShutdown;
49993
50047
  eventUnsubscribe;
49994
50048
  failedEventUnsubscribe;
@@ -50049,6 +50103,7 @@ class SyncManager {
50049
50103
  }
50050
50104
  shutdown() {
50051
50105
  this.isShutdown = true;
50106
+ this.abortController.abort();
50052
50107
  this.batchAggregator.clear();
50053
50108
  if (this.eventUnsubscribe) {
50054
50109
  this.eventUnsubscribe();
@@ -50241,6 +50296,8 @@ class SyncManager {
50241
50296
  return Array.from(this.remotes.values()).filter((remote) => remote.collectionId === collectionId);
50242
50297
  }
50243
50298
  async processCompleteBatch(batch) {
50299
+ if (this.isShutdown)
50300
+ return;
50244
50301
  const collectionIds = [
50245
50302
  ...new Set(Object.values(batch.collectionMemberships).flatMap((collections) => collections))
50246
50303
  ];
@@ -50284,8 +50341,10 @@ class SyncManager {
50284
50341
  const operations = syncOp.operations.map((op) => op.operation);
50285
50342
  let jobInfo;
50286
50343
  try {
50287
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, undefined, { sourceRemote: remote.name });
50344
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
50288
50345
  } catch (error3) {
50346
+ if (this.isShutdown)
50347
+ return;
50289
50348
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50290
50349
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
50291
50350
  const channelError = new ChannelError("inbox", err);
@@ -50296,8 +50355,10 @@ class SyncManager {
50296
50355
  }
50297
50356
  let completedJobInfo;
50298
50357
  try {
50299
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
50358
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
50300
50359
  } catch (error3) {
50360
+ if (this.isShutdown)
50361
+ return;
50301
50362
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50302
50363
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
50303
50364
  const channelError = new ChannelError("inbox", err);
@@ -50332,10 +50393,10 @@ class SyncManager {
50332
50393
  const request = { jobs };
50333
50394
  let result;
50334
50395
  try {
50335
- result = await this.reactor.loadBatch(request, undefined, {
50336
- sourceRemote
50337
- });
50396
+ result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
50338
50397
  } catch (error3) {
50398
+ if (this.isShutdown)
50399
+ return;
50339
50400
  for (const { remote, syncOp } of items) {
50340
50401
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50341
50402
  syncOp.failed(new ChannelError("inbox", err));
@@ -50356,8 +50417,10 @@ class SyncManager {
50356
50417
  const jobInfo = result.jobs[syncOp.jobId];
50357
50418
  let completedJobInfo;
50358
50419
  try {
50359
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
50420
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
50360
50421
  } catch (error3) {
50422
+ if (this.isShutdown)
50423
+ continue;
50361
50424
  const err = error3 instanceof Error ? error3 : new Error(String(error3));
50362
50425
  syncOp.failed(new ChannelError("inbox", err));
50363
50426
  remote.channel.deadLetter.add(syncOp);
@@ -50404,7 +50467,7 @@ class SyncManager {
50404
50467
  remote.channel.outbox.add(...syncOps);
50405
50468
  }
50406
50469
  async getOperationsForRemote(remote, ackOrdinal) {
50407
- const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name });
50470
+ const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, undefined, this.abortController.signal);
50408
50471
  let operations = results.results.map((entry) => toOperationWithContext(entry));
50409
50472
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
50410
50473
  if (sinceTimestamp && sinceTimestamp !== "0") {
@@ -51727,7 +51790,7 @@ var __defProp2, __export2 = (target, all) => {
51727
51790
  }
51728
51791
  }, DocumentDeletedError, InvalidSignatureError, DowngradeNotSupportedError, DocumentNotFoundError, getNextIndexForScope = (document2, scope) => {
51729
51792
  return document2.header.revision[scope] || 0;
51730
- }, 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) => {
51793
+ }, 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) => {
51731
51794
  let maxOrdinal = 0;
51732
51795
  for (const syncOp of syncOps) {
51733
51796
  if (syncOp.status === 2) {
@@ -56935,6 +56998,16 @@ var init_src = __esm(() => {
56935
56998
  this.errors = errors2;
56936
56999
  }
56937
57000
  };
57001
+ GraphQLRequestError = class GraphQLRequestError extends Error {
57002
+ statusCode;
57003
+ category;
57004
+ constructor(message, category, statusCode) {
57005
+ super(message);
57006
+ this.name = "GraphQLRequestError";
57007
+ this.category = category;
57008
+ this.statusCode = statusCode;
57009
+ }
57010
+ };
56938
57011
  ChannelError = class ChannelError extends Error {
56939
57012
  source;
56940
57013
  error;
@@ -93773,7 +93846,7 @@ var init_package = __esm(() => {
93773
93846
  package_default = {
93774
93847
  name: "@powerhousedao/connect",
93775
93848
  productName: "Powerhouse-Connect",
93776
- version: "6.0.0-dev.89",
93849
+ version: "6.0.0-dev.90",
93777
93850
  description: "Powerhouse Connect",
93778
93851
  main: "dist/index.html",
93779
93852
  type: "module",