@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.
@@ -4499,7 +4499,7 @@ var init_package = __esm(() => {
4499
4499
  package_default = {
4500
4500
  name: "@powerhousedao/connect",
4501
4501
  productName: "Powerhouse-Connect",
4502
- version: "6.0.0-dev.89",
4502
+ version: "6.0.0-dev.90",
4503
4503
  description: "Powerhouse Connect",
4504
4504
  main: "dist/index.html",
4505
4505
  type: "module",
@@ -15957,6 +15957,7 @@ class GqlRequestChannel {
15957
15957
  cursorStorage;
15958
15958
  operationIndex;
15959
15959
  pollTimer;
15960
+ abortController = new AbortController;
15960
15961
  isShutdown;
15961
15962
  failureCount;
15962
15963
  lastSuccessUtcMs;
@@ -16037,6 +16038,7 @@ class GqlRequestChannel {
16037
16038
  });
16038
16039
  }
16039
16040
  shutdown() {
16041
+ this.abortController.abort();
16040
16042
  this.bufferedOutbox.flush();
16041
16043
  this.isShutdown = true;
16042
16044
  this.pollTimer.stop();
@@ -16064,7 +16066,7 @@ class GqlRequestChannel {
16064
16066
  };
16065
16067
  }
16066
16068
  async init() {
16067
- await this.touchRemoteChannel();
16069
+ const { ackOrdinal } = await this.touchRemoteChannel();
16068
16070
  const cursors = await this.cursorStorage.list(this.remoteName);
16069
16071
  const inboxOrdinal = cursors.find((c) => c.cursorType === "inbox")?.cursorOrdinal ?? 0;
16070
16072
  const outboxOrdinal = cursors.find((c) => c.cursorType === "outbox")?.cursorOrdinal ?? 0;
@@ -16072,6 +16074,9 @@ class GqlRequestChannel {
16072
16074
  this.outbox.init(outboxOrdinal);
16073
16075
  this.lastPersistedInboxOrdinal = inboxOrdinal;
16074
16076
  this.lastPersistedOutboxOrdinal = outboxOrdinal;
16077
+ if (ackOrdinal > 0) {
16078
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
16079
+ }
16075
16080
  this.pollTimer.setDelegate(() => this.poll());
16076
16081
  this.pollTimer.start();
16077
16082
  this.transitionConnectionState("connected");
@@ -16141,34 +16146,57 @@ class GqlRequestChannel {
16141
16146
  this.deadLetter.add(...syncOps);
16142
16147
  }
16143
16148
  handlePollError(error) {
16149
+ if (this.isShutdown)
16150
+ return true;
16144
16151
  const err = error instanceof Error ? error : new Error(String(error));
16145
16152
  if (err.message.includes("Channel not found")) {
16146
16153
  this.transitionConnectionState("reconnecting");
16147
16154
  this.recoverFromChannelNotFound();
16148
16155
  return true;
16149
16156
  }
16157
+ const classification = this.classifyError(err);
16150
16158
  this.failureCount++;
16151
16159
  this.lastFailureUtcMs = Date.now();
16152
- this.transitionConnectionState("error");
16153
16160
  const channelError = new ChannelError("inbox", err);
16154
- this.logger.error("GqlChannel poll error (@FailureCount): @Error", this.failureCount, channelError);
16161
+ this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
16162
+ if (classification === "unrecoverable") {
16163
+ this.pollTimer.stop();
16164
+ this.transitionConnectionState("error");
16165
+ return true;
16166
+ }
16167
+ this.transitionConnectionState("error");
16155
16168
  return false;
16156
16169
  }
16157
16170
  recoverFromChannelNotFound() {
16158
16171
  this.logger.info("GqlChannel @ChannelId not found on remote, re-registering...", this.channelId);
16159
16172
  this.pollTimer.stop();
16160
- this.touchRemoteChannel().then(() => {
16161
- this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
16162
- this.failureCount = 0;
16163
- this.pollTimer.start();
16164
- this.transitionConnectionState("connected");
16165
- }).catch((recoveryError) => {
16166
- this.logger.error("GqlChannel @ChannelId failed to re-register: @Error", this.channelId, recoveryError);
16167
- this.failureCount++;
16168
- this.lastFailureUtcMs = Date.now();
16169
- this.pollTimer.start();
16170
- this.transitionConnectionState("error");
16171
- });
16173
+ const attemptRecovery = (attempt) => {
16174
+ if (this.isShutdown)
16175
+ return;
16176
+ this.touchRemoteChannel().then(({ ackOrdinal }) => {
16177
+ this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
16178
+ this.failureCount = 0;
16179
+ if (ackOrdinal > 0) {
16180
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
16181
+ }
16182
+ this.pollTimer.start();
16183
+ this.transitionConnectionState("connected");
16184
+ }).catch((recoveryError) => {
16185
+ const err = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
16186
+ const classification = this.classifyError(err);
16187
+ this.logger.error("GqlChannel @ChannelId recovery attempt @Attempt failed (@Classification): @Error", this.channelId, attempt, classification, recoveryError);
16188
+ this.failureCount++;
16189
+ this.lastFailureUtcMs = Date.now();
16190
+ if (classification === "unrecoverable") {
16191
+ this.transitionConnectionState("error");
16192
+ return;
16193
+ }
16194
+ this.transitionConnectionState("reconnecting");
16195
+ const delay = calculateBackoffDelay(attempt, this.config.retryBaseDelayMs, this.config.retryMaxDelayMs, Math.random());
16196
+ setTimeout(() => attemptRecovery(attempt + 1), delay);
16197
+ });
16198
+ };
16199
+ attemptRecovery(1);
16172
16200
  }
16173
16201
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
16174
16202
  const query = `
@@ -16266,7 +16294,10 @@ class GqlRequestChannel {
16266
16294
  } catch {}
16267
16295
  const mutation = `
16268
16296
  mutation TouchChannel($input: TouchChannelInput!) {
16269
- touchChannel(input: $input)
16297
+ touchChannel(input: $input) {
16298
+ success
16299
+ ackOrdinal
16300
+ }
16270
16301
  }
16271
16302
  `;
16272
16303
  const variables = {
@@ -16282,7 +16313,11 @@ class GqlRequestChannel {
16282
16313
  sinceTimestampUtcMs
16283
16314
  }
16284
16315
  };
16285
- await this.executeGraphQL(mutation, variables);
16316
+ const data = await this.executeGraphQL(mutation, variables);
16317
+ if (!data.touchChannel.success) {
16318
+ throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
16319
+ }
16320
+ return { ackOrdinal: data.touchChannel.ackOrdinal };
16286
16321
  }
16287
16322
  attemptPush(syncOps) {
16288
16323
  this.pushSyncOperations(syncOps).then(() => {
@@ -16292,8 +16327,11 @@ class GqlRequestChannel {
16292
16327
  this.transitionConnectionState("connected");
16293
16328
  }
16294
16329
  }).catch((error) => {
16330
+ if (this.isShutdown)
16331
+ return;
16295
16332
  const err = error instanceof Error ? error : new Error(String(error));
16296
- if (this.isRecoverablePushError(err)) {
16333
+ const classification = this.classifyError(err);
16334
+ if (classification === "recoverable") {
16297
16335
  this.pushFailureCount++;
16298
16336
  this.pushBlocked = true;
16299
16337
  this.logger.error("GqlChannel push failed (attempt @FailureCount), will retry: @Error", this.pushFailureCount, err);
@@ -16327,12 +16365,26 @@ class GqlRequestChannel {
16327
16365
  this.attemptPush([...allItems]);
16328
16366
  }, delay);
16329
16367
  }
16330
- isRecoverablePushError(error) {
16331
- if (error.message.startsWith("GraphQL errors:"))
16332
- return false;
16333
- if (error.message === "GraphQL response missing data field")
16334
- return false;
16335
- return true;
16368
+ classifyError(error) {
16369
+ if (!(error instanceof GraphQLRequestError)) {
16370
+ return "recoverable";
16371
+ }
16372
+ switch (error.category) {
16373
+ case "network":
16374
+ return "recoverable";
16375
+ case "http": {
16376
+ if (error.statusCode !== undefined && error.statusCode >= 500) {
16377
+ return "recoverable";
16378
+ }
16379
+ return "unrecoverable";
16380
+ }
16381
+ case "parse":
16382
+ return "recoverable";
16383
+ case "graphql":
16384
+ return "unrecoverable";
16385
+ case "missing-data":
16386
+ return "unrecoverable";
16387
+ }
16336
16388
  }
16337
16389
  async pushSyncOperations(syncOps) {
16338
16390
  for (const syncOp of syncOps) {
@@ -16409,26 +16461,27 @@ class GqlRequestChannel {
16409
16461
  body: JSON.stringify({
16410
16462
  query,
16411
16463
  variables
16412
- })
16464
+ }),
16465
+ signal: this.abortController.signal
16413
16466
  });
16414
16467
  } catch (error) {
16415
- throw new Error(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`);
16468
+ throw new GraphQLRequestError(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`, "network");
16416
16469
  }
16417
16470
  if (!response.ok) {
16418
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
16471
+ throw new GraphQLRequestError(`GraphQL request failed: ${response.status} ${response.statusText}`, "http", response.status);
16419
16472
  }
16420
16473
  let result;
16421
16474
  try {
16422
16475
  result = await response.json();
16423
16476
  } catch (error) {
16424
- throw new Error(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`);
16477
+ throw new GraphQLRequestError(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`, "parse");
16425
16478
  }
16426
16479
  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");
16427
16480
  if (result.errors) {
16428
- throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
16481
+ throw new GraphQLRequestError(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`, "graphql");
16429
16482
  }
16430
16483
  if (!result.data) {
16431
- throw new Error("GraphQL response missing data field");
16484
+ throw new GraphQLRequestError("GraphQL response missing data field", "missing-data");
16432
16485
  }
16433
16486
  return result.data;
16434
16487
  }
@@ -17348,6 +17401,7 @@ class SyncManager {
17348
17401
  remotes;
17349
17402
  awaiter;
17350
17403
  syncAwaiter;
17404
+ abortController = new AbortController;
17351
17405
  isShutdown;
17352
17406
  eventUnsubscribe;
17353
17407
  failedEventUnsubscribe;
@@ -17408,6 +17462,7 @@ class SyncManager {
17408
17462
  }
17409
17463
  shutdown() {
17410
17464
  this.isShutdown = true;
17465
+ this.abortController.abort();
17411
17466
  this.batchAggregator.clear();
17412
17467
  if (this.eventUnsubscribe) {
17413
17468
  this.eventUnsubscribe();
@@ -17600,6 +17655,8 @@ class SyncManager {
17600
17655
  return Array.from(this.remotes.values()).filter((remote) => remote.collectionId === collectionId);
17601
17656
  }
17602
17657
  async processCompleteBatch(batch) {
17658
+ if (this.isShutdown)
17659
+ return;
17603
17660
  const collectionIds = [
17604
17661
  ...new Set(Object.values(batch.collectionMemberships).flatMap((collections) => collections))
17605
17662
  ];
@@ -17643,8 +17700,10 @@ class SyncManager {
17643
17700
  const operations = syncOp.operations.map((op) => op.operation);
17644
17701
  let jobInfo;
17645
17702
  try {
17646
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, undefined, { sourceRemote: remote.name });
17703
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
17647
17704
  } catch (error) {
17705
+ if (this.isShutdown)
17706
+ return;
17648
17707
  const err = error instanceof Error ? error : new Error(String(error));
17649
17708
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
17650
17709
  const channelError = new ChannelError("inbox", err);
@@ -17655,8 +17714,10 @@ class SyncManager {
17655
17714
  }
17656
17715
  let completedJobInfo;
17657
17716
  try {
17658
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
17717
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
17659
17718
  } catch (error) {
17719
+ if (this.isShutdown)
17720
+ return;
17660
17721
  const err = error instanceof Error ? error : new Error(String(error));
17661
17722
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
17662
17723
  const channelError = new ChannelError("inbox", err);
@@ -17691,10 +17752,10 @@ class SyncManager {
17691
17752
  const request = { jobs };
17692
17753
  let result;
17693
17754
  try {
17694
- result = await this.reactor.loadBatch(request, undefined, {
17695
- sourceRemote
17696
- });
17755
+ result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
17697
17756
  } catch (error) {
17757
+ if (this.isShutdown)
17758
+ return;
17698
17759
  for (const { remote, syncOp } of items) {
17699
17760
  const err = error instanceof Error ? error : new Error(String(error));
17700
17761
  syncOp.failed(new ChannelError("inbox", err));
@@ -17715,8 +17776,10 @@ class SyncManager {
17715
17776
  const jobInfo = result.jobs[syncOp.jobId];
17716
17777
  let completedJobInfo;
17717
17778
  try {
17718
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
17779
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
17719
17780
  } catch (error) {
17781
+ if (this.isShutdown)
17782
+ continue;
17720
17783
  const err = error instanceof Error ? error : new Error(String(error));
17721
17784
  syncOp.failed(new ChannelError("inbox", err));
17722
17785
  remote.channel.deadLetter.add(syncOp);
@@ -17763,7 +17826,7 @@ class SyncManager {
17763
17826
  remote.channel.outbox.add(...syncOps);
17764
17827
  }
17765
17828
  async getOperationsForRemote(remote, ackOrdinal) {
17766
- const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name });
17829
+ const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, undefined, this.abortController.signal);
17767
17830
  let operations = results.results.map((entry) => toOperationWithContext(entry));
17768
17831
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
17769
17832
  if (sinceTimestamp && sinceTimestamp !== "0") {
@@ -19086,7 +19149,7 @@ var __defProp2, __export2 = (target, all) => {
19086
19149
  }
19087
19150
  }, DocumentDeletedError, InvalidSignatureError, DowngradeNotSupportedError, DocumentNotFoundError, getNextIndexForScope = (document2, scope) => {
19088
19151
  return document2.header.revision[scope] || 0;
19089
- }, 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) => {
19152
+ }, 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) => {
19090
19153
  let maxOrdinal = 0;
19091
19154
  for (const syncOp of syncOps) {
19092
19155
  if (syncOp.status === 2) {
@@ -24294,6 +24357,16 @@ var init_src = __esm(() => {
24294
24357
  this.errors = errors2;
24295
24358
  }
24296
24359
  };
24360
+ GraphQLRequestError = class GraphQLRequestError extends Error {
24361
+ statusCode;
24362
+ category;
24363
+ constructor(message, category, statusCode) {
24364
+ super(message);
24365
+ this.name = "GraphQLRequestError";
24366
+ this.category = category;
24367
+ this.statusCode = statusCode;
24368
+ }
24369
+ };
24297
24370
  ChannelError = class ChannelError extends Error {
24298
24371
  source;
24299
24372
  error;
@@ -4499,7 +4499,7 @@ var init_package = __esm(() => {
4499
4499
  package_default = {
4500
4500
  name: "@powerhousedao/connect",
4501
4501
  productName: "Powerhouse-Connect",
4502
- version: "6.0.0-dev.89",
4502
+ version: "6.0.0-dev.90",
4503
4503
  description: "Powerhouse Connect",
4504
4504
  main: "dist/index.html",
4505
4505
  type: "module",
@@ -15957,6 +15957,7 @@ class GqlRequestChannel {
15957
15957
  cursorStorage;
15958
15958
  operationIndex;
15959
15959
  pollTimer;
15960
+ abortController = new AbortController;
15960
15961
  isShutdown;
15961
15962
  failureCount;
15962
15963
  lastSuccessUtcMs;
@@ -16037,6 +16038,7 @@ class GqlRequestChannel {
16037
16038
  });
16038
16039
  }
16039
16040
  shutdown() {
16041
+ this.abortController.abort();
16040
16042
  this.bufferedOutbox.flush();
16041
16043
  this.isShutdown = true;
16042
16044
  this.pollTimer.stop();
@@ -16064,7 +16066,7 @@ class GqlRequestChannel {
16064
16066
  };
16065
16067
  }
16066
16068
  async init() {
16067
- await this.touchRemoteChannel();
16069
+ const { ackOrdinal } = await this.touchRemoteChannel();
16068
16070
  const cursors = await this.cursorStorage.list(this.remoteName);
16069
16071
  const inboxOrdinal = cursors.find((c) => c.cursorType === "inbox")?.cursorOrdinal ?? 0;
16070
16072
  const outboxOrdinal = cursors.find((c) => c.cursorType === "outbox")?.cursorOrdinal ?? 0;
@@ -16072,6 +16074,9 @@ class GqlRequestChannel {
16072
16074
  this.outbox.init(outboxOrdinal);
16073
16075
  this.lastPersistedInboxOrdinal = inboxOrdinal;
16074
16076
  this.lastPersistedOutboxOrdinal = outboxOrdinal;
16077
+ if (ackOrdinal > 0) {
16078
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
16079
+ }
16075
16080
  this.pollTimer.setDelegate(() => this.poll());
16076
16081
  this.pollTimer.start();
16077
16082
  this.transitionConnectionState("connected");
@@ -16141,34 +16146,57 @@ class GqlRequestChannel {
16141
16146
  this.deadLetter.add(...syncOps);
16142
16147
  }
16143
16148
  handlePollError(error) {
16149
+ if (this.isShutdown)
16150
+ return true;
16144
16151
  const err = error instanceof Error ? error : new Error(String(error));
16145
16152
  if (err.message.includes("Channel not found")) {
16146
16153
  this.transitionConnectionState("reconnecting");
16147
16154
  this.recoverFromChannelNotFound();
16148
16155
  return true;
16149
16156
  }
16157
+ const classification = this.classifyError(err);
16150
16158
  this.failureCount++;
16151
16159
  this.lastFailureUtcMs = Date.now();
16152
- this.transitionConnectionState("error");
16153
16160
  const channelError = new ChannelError("inbox", err);
16154
- this.logger.error("GqlChannel poll error (@FailureCount): @Error", this.failureCount, channelError);
16161
+ this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
16162
+ if (classification === "unrecoverable") {
16163
+ this.pollTimer.stop();
16164
+ this.transitionConnectionState("error");
16165
+ return true;
16166
+ }
16167
+ this.transitionConnectionState("error");
16155
16168
  return false;
16156
16169
  }
16157
16170
  recoverFromChannelNotFound() {
16158
16171
  this.logger.info("GqlChannel @ChannelId not found on remote, re-registering...", this.channelId);
16159
16172
  this.pollTimer.stop();
16160
- this.touchRemoteChannel().then(() => {
16161
- this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
16162
- this.failureCount = 0;
16163
- this.pollTimer.start();
16164
- this.transitionConnectionState("connected");
16165
- }).catch((recoveryError) => {
16166
- this.logger.error("GqlChannel @ChannelId failed to re-register: @Error", this.channelId, recoveryError);
16167
- this.failureCount++;
16168
- this.lastFailureUtcMs = Date.now();
16169
- this.pollTimer.start();
16170
- this.transitionConnectionState("error");
16171
- });
16173
+ const attemptRecovery = (attempt) => {
16174
+ if (this.isShutdown)
16175
+ return;
16176
+ this.touchRemoteChannel().then(({ ackOrdinal }) => {
16177
+ this.logger.info("GqlChannel @ChannelId re-registered successfully", this.channelId);
16178
+ this.failureCount = 0;
16179
+ if (ackOrdinal > 0) {
16180
+ trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
16181
+ }
16182
+ this.pollTimer.start();
16183
+ this.transitionConnectionState("connected");
16184
+ }).catch((recoveryError) => {
16185
+ const err = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
16186
+ const classification = this.classifyError(err);
16187
+ this.logger.error("GqlChannel @ChannelId recovery attempt @Attempt failed (@Classification): @Error", this.channelId, attempt, classification, recoveryError);
16188
+ this.failureCount++;
16189
+ this.lastFailureUtcMs = Date.now();
16190
+ if (classification === "unrecoverable") {
16191
+ this.transitionConnectionState("error");
16192
+ return;
16193
+ }
16194
+ this.transitionConnectionState("reconnecting");
16195
+ const delay = calculateBackoffDelay(attempt, this.config.retryBaseDelayMs, this.config.retryMaxDelayMs, Math.random());
16196
+ setTimeout(() => attemptRecovery(attempt + 1), delay);
16197
+ });
16198
+ };
16199
+ attemptRecovery(1);
16172
16200
  }
16173
16201
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
16174
16202
  const query = `
@@ -16266,7 +16294,10 @@ class GqlRequestChannel {
16266
16294
  } catch {}
16267
16295
  const mutation = `
16268
16296
  mutation TouchChannel($input: TouchChannelInput!) {
16269
- touchChannel(input: $input)
16297
+ touchChannel(input: $input) {
16298
+ success
16299
+ ackOrdinal
16300
+ }
16270
16301
  }
16271
16302
  `;
16272
16303
  const variables = {
@@ -16282,7 +16313,11 @@ class GqlRequestChannel {
16282
16313
  sinceTimestampUtcMs
16283
16314
  }
16284
16315
  };
16285
- await this.executeGraphQL(mutation, variables);
16316
+ const data = await this.executeGraphQL(mutation, variables);
16317
+ if (!data.touchChannel.success) {
16318
+ throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
16319
+ }
16320
+ return { ackOrdinal: data.touchChannel.ackOrdinal };
16286
16321
  }
16287
16322
  attemptPush(syncOps) {
16288
16323
  this.pushSyncOperations(syncOps).then(() => {
@@ -16292,8 +16327,11 @@ class GqlRequestChannel {
16292
16327
  this.transitionConnectionState("connected");
16293
16328
  }
16294
16329
  }).catch((error) => {
16330
+ if (this.isShutdown)
16331
+ return;
16295
16332
  const err = error instanceof Error ? error : new Error(String(error));
16296
- if (this.isRecoverablePushError(err)) {
16333
+ const classification = this.classifyError(err);
16334
+ if (classification === "recoverable") {
16297
16335
  this.pushFailureCount++;
16298
16336
  this.pushBlocked = true;
16299
16337
  this.logger.error("GqlChannel push failed (attempt @FailureCount), will retry: @Error", this.pushFailureCount, err);
@@ -16327,12 +16365,26 @@ class GqlRequestChannel {
16327
16365
  this.attemptPush([...allItems]);
16328
16366
  }, delay);
16329
16367
  }
16330
- isRecoverablePushError(error) {
16331
- if (error.message.startsWith("GraphQL errors:"))
16332
- return false;
16333
- if (error.message === "GraphQL response missing data field")
16334
- return false;
16335
- return true;
16368
+ classifyError(error) {
16369
+ if (!(error instanceof GraphQLRequestError)) {
16370
+ return "recoverable";
16371
+ }
16372
+ switch (error.category) {
16373
+ case "network":
16374
+ return "recoverable";
16375
+ case "http": {
16376
+ if (error.statusCode !== undefined && error.statusCode >= 500) {
16377
+ return "recoverable";
16378
+ }
16379
+ return "unrecoverable";
16380
+ }
16381
+ case "parse":
16382
+ return "recoverable";
16383
+ case "graphql":
16384
+ return "unrecoverable";
16385
+ case "missing-data":
16386
+ return "unrecoverable";
16387
+ }
16336
16388
  }
16337
16389
  async pushSyncOperations(syncOps) {
16338
16390
  for (const syncOp of syncOps) {
@@ -16409,26 +16461,27 @@ class GqlRequestChannel {
16409
16461
  body: JSON.stringify({
16410
16462
  query,
16411
16463
  variables
16412
- })
16464
+ }),
16465
+ signal: this.abortController.signal
16413
16466
  });
16414
16467
  } catch (error) {
16415
- throw new Error(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`);
16468
+ throw new GraphQLRequestError(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`, "network");
16416
16469
  }
16417
16470
  if (!response.ok) {
16418
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
16471
+ throw new GraphQLRequestError(`GraphQL request failed: ${response.status} ${response.statusText}`, "http", response.status);
16419
16472
  }
16420
16473
  let result;
16421
16474
  try {
16422
16475
  result = await response.json();
16423
16476
  } catch (error) {
16424
- throw new Error(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`);
16477
+ throw new GraphQLRequestError(`Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`, "parse");
16425
16478
  }
16426
16479
  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");
16427
16480
  if (result.errors) {
16428
- throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
16481
+ throw new GraphQLRequestError(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`, "graphql");
16429
16482
  }
16430
16483
  if (!result.data) {
16431
- throw new Error("GraphQL response missing data field");
16484
+ throw new GraphQLRequestError("GraphQL response missing data field", "missing-data");
16432
16485
  }
16433
16486
  return result.data;
16434
16487
  }
@@ -17348,6 +17401,7 @@ class SyncManager {
17348
17401
  remotes;
17349
17402
  awaiter;
17350
17403
  syncAwaiter;
17404
+ abortController = new AbortController;
17351
17405
  isShutdown;
17352
17406
  eventUnsubscribe;
17353
17407
  failedEventUnsubscribe;
@@ -17408,6 +17462,7 @@ class SyncManager {
17408
17462
  }
17409
17463
  shutdown() {
17410
17464
  this.isShutdown = true;
17465
+ this.abortController.abort();
17411
17466
  this.batchAggregator.clear();
17412
17467
  if (this.eventUnsubscribe) {
17413
17468
  this.eventUnsubscribe();
@@ -17600,6 +17655,8 @@ class SyncManager {
17600
17655
  return Array.from(this.remotes.values()).filter((remote) => remote.collectionId === collectionId);
17601
17656
  }
17602
17657
  async processCompleteBatch(batch) {
17658
+ if (this.isShutdown)
17659
+ return;
17603
17660
  const collectionIds = [
17604
17661
  ...new Set(Object.values(batch.collectionMemberships).flatMap((collections) => collections))
17605
17662
  ];
@@ -17643,8 +17700,10 @@ class SyncManager {
17643
17700
  const operations = syncOp.operations.map((op) => op.operation);
17644
17701
  let jobInfo;
17645
17702
  try {
17646
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, undefined, { sourceRemote: remote.name });
17703
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
17647
17704
  } catch (error) {
17705
+ if (this.isShutdown)
17706
+ return;
17648
17707
  const err = error instanceof Error ? error : new Error(String(error));
17649
17708
  this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
17650
17709
  const channelError = new ChannelError("inbox", err);
@@ -17655,8 +17714,10 @@ class SyncManager {
17655
17714
  }
17656
17715
  let completedJobInfo;
17657
17716
  try {
17658
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
17717
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
17659
17718
  } catch (error) {
17719
+ if (this.isShutdown)
17720
+ return;
17660
17721
  const err = error instanceof Error ? error : new Error(String(error));
17661
17722
  this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
17662
17723
  const channelError = new ChannelError("inbox", err);
@@ -17691,10 +17752,10 @@ class SyncManager {
17691
17752
  const request = { jobs };
17692
17753
  let result;
17693
17754
  try {
17694
- result = await this.reactor.loadBatch(request, undefined, {
17695
- sourceRemote
17696
- });
17755
+ result = await this.reactor.loadBatch(request, this.abortController.signal, { sourceRemote });
17697
17756
  } catch (error) {
17757
+ if (this.isShutdown)
17758
+ return;
17698
17759
  for (const { remote, syncOp } of items) {
17699
17760
  const err = error instanceof Error ? error : new Error(String(error));
17700
17761
  syncOp.failed(new ChannelError("inbox", err));
@@ -17715,8 +17776,10 @@ class SyncManager {
17715
17776
  const jobInfo = result.jobs[syncOp.jobId];
17716
17777
  let completedJobInfo;
17717
17778
  try {
17718
- completedJobInfo = await this.awaiter.waitForJob(jobInfo.id);
17779
+ completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
17719
17780
  } catch (error) {
17781
+ if (this.isShutdown)
17782
+ continue;
17720
17783
  const err = error instanceof Error ? error : new Error(String(error));
17721
17784
  syncOp.failed(new ChannelError("inbox", err));
17722
17785
  remote.channel.deadLetter.add(syncOp);
@@ -17763,7 +17826,7 @@ class SyncManager {
17763
17826
  remote.channel.outbox.add(...syncOps);
17764
17827
  }
17765
17828
  async getOperationsForRemote(remote, ackOrdinal) {
17766
- const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name });
17829
+ const results = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, undefined, this.abortController.signal);
17767
17830
  let operations = results.results.map((entry) => toOperationWithContext(entry));
17768
17831
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
17769
17832
  if (sinceTimestamp && sinceTimestamp !== "0") {
@@ -19086,7 +19149,7 @@ var __defProp2, __export2 = (target, all) => {
19086
19149
  }
19087
19150
  }, DocumentDeletedError, InvalidSignatureError, DowngradeNotSupportedError, DocumentNotFoundError, getNextIndexForScope = (document2, scope) => {
19088
19151
  return document2.header.revision[scope] || 0;
19089
- }, 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) => {
19152
+ }, 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) => {
19090
19153
  let maxOrdinal = 0;
19091
19154
  for (const syncOp of syncOps) {
19092
19155
  if (syncOp.status === 2) {
@@ -24294,6 +24357,16 @@ var init_src = __esm(() => {
24294
24357
  this.errors = errors2;
24295
24358
  }
24296
24359
  };
24360
+ GraphQLRequestError = class GraphQLRequestError extends Error {
24361
+ statusCode;
24362
+ category;
24363
+ constructor(message, category, statusCode) {
24364
+ super(message);
24365
+ this.name = "GraphQLRequestError";
24366
+ this.category = category;
24367
+ this.statusCode = statusCode;
24368
+ }
24369
+ };
24297
24370
  ChannelError = class ChannelError extends Error {
24298
24371
  source;
24299
24372
  error;