@powerhousedao/reactor 6.2.2-dev.36 → 6.2.2-dev.38

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/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { A as AuthorizationDeniedError, C as DuplicateOperationError, D as DuplicateModuleError, E as DuplicateManifestError, F as throwIfAborted, M as DocumentNotFoundError, N as matchesScope, O as InvalidModuleError, P as parsePagingOptions, S as AppendConditionFailedError, T as RevisionMismatchError, _ as DocumentMetaCache, a as createForwardingPoolInstrumentation, b as CollectionMembershipCache, c as KyselyKeyframeStore, d as DriveCollectionId, f as buildDecisionModel, g as KyselyOperationIndex, h as KyselyWriteCache, i as runMigrations, j as DocumentDeletedError, k as ModuleNotFoundError, l as DocumentModelRegistry, m as EventBus, n as REACTOR_SCHEMA, o as instrumentPgPool, p as KyselyExecutionScope, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as createConsistencyToken, w as OptimisticLockError, x as APPEND_CONDITION_FAILED_PREFIX, y as createEmptyConsistencyToken } from "./drive-container-types-ZSLCC3lC.js";
1
+ import { A as OptimisticLockError, B as ExcessiveReshuffleError, C as DocumentMetaCache, D as APPEND_CONDITION_FAILED_PREFIX, E as CollectionMembershipCache, F as ModuleNotFoundError, H as matchesScope, I as AuthTimestampNotMonotonicError, L as AuthorizationDeniedError, M as DuplicateManifestError, N as DuplicateModuleError, O as AppendConditionFailedError, P as InvalidModuleError, R as DocumentDeletedError, S as KyselyOperationIndex, T as createEmptyConsistencyToken, U as parsePagingOptions, V as InvalidOperationTimestampError, W as throwIfAborted, _ as KyselyExecutionScope, a as createForwardingPoolInstrumentation, b as EventBus, c as KyselyKeyframeStore, d as DriveCollectionId, f as decideAtHead, g as authDecisionModel, h as buildDecisionModel, i as runMigrations, j as RevisionMismatchError, k as DuplicateOperationError, l as DocumentModelRegistry, m as documentDecisionModel, n as REACTOR_SCHEMA, o as instrumentPgPool, p as selectDecisionModel, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as FLAG_PREREQUISITES, w as createConsistencyToken, x as KyselyWriteCache, y as validateFeatureFlags, z as DocumentNotFoundError } from "./drive-container-types-BJCKXJwH.js";
2
2
  import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
3
3
  import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-D3S6Eysd.js";
4
- import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-BY-mPXJF.js";
4
+ import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-C0GB0b8Q.js";
5
5
  import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
6
- import { t as workerEntryPath } from "./worker-BMn-2vsH.js";
7
- import { actions, actions as documentActions, createPresignedHeader, decide, generateId, hashDocumentStateForScope, replayDocument } from "@powerhousedao/shared/document-model";
6
+ import { t as workerEntryPath } from "./worker-jEJW6_j7.js";
7
+ import { actions, actions as documentActions, createPresignedHeader, decide, garbageCollect, generateId, hashDocumentStateForScope, replayDocument, sortOperations } from "@powerhousedao/shared/document-model";
8
8
  import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
9
9
  import { v4 } from "uuid";
10
10
  import { ConsoleLogger, childLogger } from "document-model";
@@ -190,10 +190,12 @@ function topologicalSort(jobs) {
190
190
  */
191
191
  function toErrorInfo$1(error) {
192
192
  if (error instanceof Error) return {
193
+ name: error.name,
193
194
  message: error.message,
194
195
  stack: error.stack || (/* @__PURE__ */ new Error()).stack || ""
195
196
  };
196
197
  return {
198
+ name: "Error",
197
199
  message: error,
198
200
  stack: (/* @__PURE__ */ new Error()).stack || ""
199
201
  };
@@ -705,13 +707,19 @@ function filterReadableScopes(document, subject) {
705
707
  const state = document.state;
706
708
  if (!state) return document;
707
709
  const auth = document.state.auth;
708
- const filtered = {};
709
- for (const scope of Object.keys(state)) if (canReadScope(auth, subject, scope)) filtered[scope] = state[scope];
710
+ const readable = (scope) => canReadScope(auth, subject, scope);
710
711
  return {
711
712
  ...document,
712
- state: filtered
713
+ state: keepReadableScopes(state, readable),
714
+ initialState: keepReadableScopes(document.initialState, readable)
713
715
  };
714
716
  }
717
+ function keepReadableScopes(scopes, readable) {
718
+ if (!scopes) return scopes;
719
+ const kept = {};
720
+ for (const scope of Object.keys(scopes)) if (readable(scope)) kept[scope] = scopes[scope];
721
+ return kept;
722
+ }
715
723
  //#endregion
716
724
  //#region src/client/reactor-client.ts
717
725
  /**
@@ -777,8 +785,8 @@ var ReactorClient = class {
777
785
  * same lookup as the data path. Resolves against the "main" branch. Throws if
778
786
  * the identifier cannot be resolved or is ambiguous.
779
787
  */
780
- async resolveIdOrSlug(identifier, signal) {
781
- return this.documentView.resolveIdOrSlug(identifier, void 0, void 0, signal);
788
+ async resolveIdOrSlug(identifier, view, signal) {
789
+ return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
782
790
  }
783
791
  /**
784
792
  * Retrieves operations for a document
@@ -1127,13 +1135,15 @@ var ReactorClient = class {
1127
1135
  */
1128
1136
  subscribe(search, callback, view) {
1129
1137
  this.logger.verbose("subscribe(@search, @view)", search, view);
1138
+ const subject = this.readSubject(view?.subject);
1139
+ const readable = (document) => filterReadableScopes(document, subject);
1130
1140
  const unsubscribeCreated = this.subscriptionManager.onDocumentCreated((result) => {
1131
1141
  (async () => {
1132
1142
  try {
1133
- const documents = await Promise.all(result.results.map((id) => this.reactor.get(id, view, void 0, void 0)));
1143
+ const documents = await Promise.all(result.results.map((id) => this.reactor.get(id, withAuthScope(view), void 0, void 0)));
1134
1144
  callback({
1135
1145
  type: DocumentChangeType.Created,
1136
- documents
1146
+ documents: documents.map(readable)
1137
1147
  });
1138
1148
  } catch {}
1139
1149
  })();
@@ -1148,7 +1158,7 @@ var ReactorClient = class {
1148
1158
  const unsubscribeUpdated = this.subscriptionManager.onDocumentStateUpdated((result) => {
1149
1159
  callback({
1150
1160
  type: DocumentChangeType.Updated,
1151
- documents: result.results
1161
+ documents: result.results.map(readable)
1152
1162
  });
1153
1163
  }, search, view);
1154
1164
  const unsubscribeRelationship = this.subscriptionManager.onRelationshipChanged((parentId, childId, changeType) => {
@@ -1214,10 +1224,12 @@ const QueueEventTypes = { JOB_AVAILABLE: 1e4 };
1214
1224
  const MAX_EXEMPT_CONFLICT_RETRIES = 20;
1215
1225
  function toErrorInfo(error) {
1216
1226
  if (error instanceof Error) return {
1227
+ name: error.name,
1217
1228
  message: error.message,
1218
1229
  stack: error.stack || (/* @__PURE__ */ new Error()).stack || ""
1219
1230
  };
1220
1231
  return {
1232
+ name: "Error",
1221
1233
  message: error,
1222
1234
  stack: (/* @__PURE__ */ new Error()).stack || ""
1223
1235
  };
@@ -1262,7 +1274,7 @@ var JobResultHandler = class {
1262
1274
  callbacks.deferJob(handle.job.documentId, handle.job);
1263
1275
  return;
1264
1276
  }
1265
- if (result.error && (DocumentDeletedError.isError(result.error) || AuthorizationDeniedError.isError(result.error))) {
1277
+ if (result.error && (DocumentDeletedError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error))) {
1266
1278
  const errorInfo = toErrorInfo(result.error);
1267
1279
  this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
1268
1280
  this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
@@ -1321,6 +1333,7 @@ var JobResultHandler = class {
1321
1333
  stackLines.push(`[Attempt ${index + 1}] Stack trace:\n${error.stack}`);
1322
1334
  });
1323
1335
  return {
1336
+ name: currentError.name,
1324
1337
  message: messageLines.join("\n"),
1325
1338
  stack: stackLines.join("\n\n")
1326
1339
  };
@@ -1685,29 +1698,6 @@ function extractMembershipTarget(op) {
1685
1698
  if (actionType === "DELETE_DOCUMENT") return input?.documentId ?? op.context.documentId;
1686
1699
  }
1687
1700
  //#endregion
1688
- //#region src/core/feature-flags.ts
1689
- /**
1690
- * Every flag this reactor knows, with the flags it requires. A stage adds its
1691
- * flag here when it ships, so asking an older reactor for a later stage's flag
1692
- * is an unrecognized name rather than a flag that quietly does nothing.
1693
- */
1694
- const FLAG_PREREQUISITES = { documentDecisions: [] };
1695
- /**
1696
- * Throws when the flags ask for enforcement the reactor cannot deliver. Either
1697
- * failure would otherwise read as enforcement being on while the reactor
1698
- * applies less than the caller asked for.
1699
- */
1700
- function validateFeatureFlags(flags, prerequisites) {
1701
- const known = Object.keys(prerequisites);
1702
- const unrecognized = Object.keys(flags).filter((name) => !known.includes(name));
1703
- if (unrecognized.length > 0) throw new Error(`Unrecognized reactor feature flag: ${unrecognized.join(", ")}. This reactor knows: ${known.join(", ")}.`);
1704
- for (const name of known) {
1705
- if (flags[name] !== true) continue;
1706
- const missing = prerequisites[name].filter((required) => flags[required] !== true);
1707
- if (missing.length > 0) throw new Error(`Reactor feature flag ${name} requires ${missing.join(", ")}.`);
1708
- }
1709
- }
1710
- //#endregion
1711
1701
  //#region src/executor/simple-job-executor-manager.ts
1712
1702
  /**
1713
1703
  * Manages multiple job executors and coordinates job distribution.
@@ -1949,6 +1939,7 @@ var InMemoryJobTracker = class {
1949
1939
  }
1950
1940
  handleJobFailed(event) {
1951
1941
  this.markFailed(event.jobId, {
1942
+ name: event.error.name,
1952
1943
  message: event.error.message,
1953
1944
  stack: event.error.stack || ""
1954
1945
  }, event.job);
@@ -2410,10 +2401,12 @@ var InMemoryQueue = class {
2410
2401
  }
2411
2402
  toErrorInfo(error) {
2412
2403
  if (error instanceof Error) return {
2404
+ name: error.name,
2413
2405
  message: error.message,
2414
2406
  stack: error.stack || (/* @__PURE__ */ new Error()).stack || ""
2415
2407
  };
2416
2408
  return {
2409
+ name: "Error",
2417
2410
  message: error,
2418
2411
  stack: (/* @__PURE__ */ new Error()).stack || ""
2419
2412
  };
@@ -2501,6 +2494,7 @@ var InMemoryQueue = class {
2501
2494
  await this.resolver.ensureModelLoaded(documentType);
2502
2495
  } catch {
2503
2496
  await this.failJob(job.id, {
2497
+ name: "Error",
2504
2498
  message: `Failed to load document model for type: ${documentType}`,
2505
2499
  stack: (/* @__PURE__ */ new Error()).stack || ""
2506
2500
  });
@@ -3537,11 +3531,18 @@ var PollingChannelError = class extends Error {
3537
3531
  var ChannelError = class extends Error {
3538
3532
  source;
3539
3533
  error;
3540
- constructor(source, error) {
3534
+ /**
3535
+ * The classification when something other than the error carries it. Absent
3536
+ * means derive it from `error.name`; a dead letter mirrored from a peer sets it,
3537
+ * because only the message crosses the wire.
3538
+ */
3539
+ errorType;
3540
+ constructor(source, error, errorType) {
3541
3541
  super(`ChannelError[${source}]: ${error.message}`);
3542
3542
  this.name = "ChannelError";
3543
3543
  this.source = source;
3544
3544
  this.error = error;
3545
+ this.errorType = errorType;
3545
3546
  }
3546
3547
  };
3547
3548
  //#endregion
@@ -3912,6 +3913,33 @@ function splitComponent(items, maxSize) {
3912
3913
  for (let i = 0; i < sorted.length; i += maxSize) chunks.push(sorted.slice(i, i + maxSize));
3913
3914
  return chunks;
3914
3915
  }
3916
+ /**
3917
+ * Classifies a failure by error name rather than `instanceof`, because a failure
3918
+ * that crossed the pooled-worker boundary arrives as plain data.
3919
+ */
3920
+ function classifyJobFailure(errorName) {
3921
+ switch (errorName) {
3922
+ case "AuthTimestampNotMonotonicError": return "AUTH_TIMESTAMP_NOT_MONOTONIC";
3923
+ case "InvalidOperationTimestampError": return "INVALID_TIMESTAMP";
3924
+ case "ExcessiveReshuffleError": return "EXCESSIVE_SHUFFLE";
3925
+ case "InvalidSignatureError": return "SIGNATURE_INVALID";
3926
+ case "HashMismatchError": return "HASH_MISMATCH";
3927
+ default: return "UNCLASSIFIED";
3928
+ }
3929
+ }
3930
+ /** The explicit type when something else carried it, else derived by name. */
3931
+ function syncOperationErrorType(error) {
3932
+ return error?.errorType ?? classifyJobFailure(error?.error?.name ?? "Error");
3933
+ }
3934
+ /** Dead-letter types that must not stop the document syncing. */
3935
+ const NON_QUARANTINING_ERROR_TYPES = new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
3936
+ /**
3937
+ * A held auth operation must not quarantine: reconciling the two policies needs
3938
+ * the traffic a quarantine would stop.
3939
+ */
3940
+ function quarantinesDocument(errorType) {
3941
+ return !NON_QUARANTINING_ERROR_TYPES.has(errorType);
3942
+ }
3915
3943
  //#endregion
3916
3944
  //#region src/sync/channels/interval-poll-timer.ts
3917
3945
  const DEFAULT_CONFIG = {
@@ -4074,7 +4102,7 @@ function serializeEnvelope(envelope) {
4074
4102
  hash: opWithContext.operation.hash,
4075
4103
  skip: opWithContext.operation.skip,
4076
4104
  error: opWithContext.operation.error,
4077
- deniedReason: opWithContext.operation.deniedReason,
4105
+ ...opWithContext.operation.deniedReason !== void 0 ? { deniedReason: opWithContext.operation.deniedReason } : {},
4078
4106
  id: opWithContext.operation.id,
4079
4107
  action: serializeAction(opWithContext.operation.action)
4080
4108
  },
@@ -4102,21 +4130,33 @@ function deserializeSignature(sig) {
4102
4130
  return sig.split(", ");
4103
4131
  }
4104
4132
  /**
4105
- * Deserializes signatures in an operation's signer context from strings back to tuples.
4106
- *
4107
- * When operations are transported via GraphQL, signatures are serialized as comma-separated
4108
- * strings. This function restores them to the Signature tuple format required for verification.
4133
+ * `isDenied` tests strictly against undefined, so a null left in place would mark
4134
+ * every synced operation denied. The key is removed rather than set to undefined,
4135
+ * so an operation that arrived without it stays identical to the one sent.
4109
4136
  */
4110
- function deserializeOperationSignatures(opWithContext) {
4111
- const signer = opWithContext.operation.action.context?.signer;
4112
- if (!signer?.signatures || signer.signatures.length === 0) return opWithContext;
4137
+ function normalizeAbsentFields(operation) {
4138
+ const wire = operation;
4139
+ if (wire.error !== null && wire.deniedReason !== null) return operation;
4140
+ const normalized = { ...operation };
4141
+ if (wire.error === null) delete normalized.error;
4142
+ if (wire.deniedReason === null) delete normalized.deniedReason;
4143
+ return normalized;
4144
+ }
4145
+ /** Restores signature tuples and null-valued optional fields to undefined. */
4146
+ function deserializeOperation(opWithContext) {
4147
+ const operation = normalizeAbsentFields(opWithContext.operation);
4148
+ const signer = operation.action.context?.signer;
4149
+ if (!signer?.signatures || signer.signatures.length === 0) return {
4150
+ ...opWithContext,
4151
+ operation
4152
+ };
4113
4153
  const deserializedSignatures = signer.signatures.map(deserializeSignature);
4114
4154
  const deserializedOperation = {
4115
- ...opWithContext.operation,
4155
+ ...operation,
4116
4156
  action: {
4117
- ...opWithContext.operation.action,
4157
+ ...operation.action,
4118
4158
  context: {
4119
- ...opWithContext.operation.action.context,
4159
+ ...operation.action.context,
4120
4160
  signer: {
4121
4161
  ...signer,
4122
4162
  signatures: deserializedSignatures
@@ -4143,7 +4183,7 @@ function deserializeOperationSignatures(opWithContext) {
4143
4183
  */
4144
4184
  function envelopesToSyncOperations(envelope, remoteName) {
4145
4185
  if (!envelope.operations || envelope.operations.length === 0) return [];
4146
- return batchOperationsByDocument(envelope.operations.map(deserializeOperationSignatures)).map((batch) => {
4186
+ return batchOperationsByDocument(envelope.operations.map(deserializeOperation)).map((batch) => {
4147
4187
  return new SyncOperation(`syncop-${envelope.channelMeta.id}-${Date.now()}-${syncOpCounter++}`, envelope.key ?? "", (envelope.dependsOn ?? []).filter(Boolean), remoteName, batch.documentId, [batch.scope], batch.branch, batch.operations);
4148
4188
  });
4149
4189
  }
@@ -4155,6 +4195,12 @@ const getLatestAppliedOrdinal = (syncOps) => {
4155
4195
  //#endregion
4156
4196
  //#region src/sync/channels/gql-req-channel.ts
4157
4197
  /**
4198
+ * Fields the auth projection added to the sync schema. A remote that predates
4199
+ * them rejects the whole query for naming one, so they are selected only while
4200
+ * the remote is known to serve them.
4201
+ */
4202
+ const DECISION_FIELDS = ["deniedReason", "errorType"];
4203
+ /**
4158
4204
  * GraphQL-based synchronization channel for network communication between reactors.
4159
4205
  */
4160
4206
  var GqlRequestChannel = class {
@@ -4181,6 +4227,8 @@ var GqlRequestChannel = class {
4181
4227
  isPushing = false;
4182
4228
  pendingDrain = false;
4183
4229
  receivingPages = false;
4230
+ /** Cleared for good the first time the remote rejects {@link DECISION_FIELDS}. */
4231
+ peerServesDecisionFields = true;
4184
4232
  isRecovering = false;
4185
4233
  connectionState = "connecting";
4186
4234
  /** Latest unrecoverable error was an auth rejection; cleared on connect. */
@@ -4359,7 +4407,7 @@ var GqlRequestChannel = class {
4359
4407
  const syncOps = [];
4360
4408
  for (const dl of deadLetters) {
4361
4409
  const syncOp = new SyncOperation(crypto.randomUUID(), dl.jobId, [], this.remoteName, dl.documentId, dl.scopes, dl.branch, []);
4362
- syncOp.failed(new ChannelError(ChannelErrorSource.Outbox, new Error(dl.error)));
4410
+ syncOp.failed(new ChannelError(ChannelErrorSource.Outbox, new Error(dl.error), dl.errorType ?? void 0));
4363
4411
  syncOps.push(syncOp);
4364
4412
  }
4365
4413
  this.deadLetter.add(...syncOps);
@@ -4451,7 +4499,44 @@ var GqlRequestChannel = class {
4451
4499
  * Queries the remote GraphQL endpoint for sync envelopes.
4452
4500
  */
4453
4501
  async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
4454
- const query = `
4502
+ const variables = {
4503
+ channelId: this.channelId,
4504
+ outboxAck: ackOrdinal,
4505
+ outboxLatest: latestOrdinal
4506
+ };
4507
+ let response;
4508
+ try {
4509
+ response = await this.executeGraphQL(this.pollQuery(this.peerServesDecisionFields), variables);
4510
+ } catch (error) {
4511
+ if (!this.rejectsDecisionFields(error)) throw error;
4512
+ this.logger.warn("Remote @channelId does not serve deniedReason/errorType; polling without them. The remote is on an older schema, so it has neither to report.", this.channelId);
4513
+ this.peerServesDecisionFields = false;
4514
+ response = await this.executeGraphQL(this.pollQuery(false), variables);
4515
+ }
4516
+ return {
4517
+ envelopes: response.pollSyncEnvelopes.envelopes,
4518
+ ackOrdinal: response.pollSyncEnvelopes.ackOrdinal,
4519
+ deadLetters: response.pollSyncEnvelopes.deadLetters ?? [],
4520
+ hasMore: response.pollSyncEnvelopes.hasMore
4521
+ };
4522
+ }
4523
+ /**
4524
+ * True when the remote rejected the query for naming a field it does not
4525
+ * have. Selecting an unknown field fails validation for the whole query, so
4526
+ * an unhandled one takes the channel's polling down until the process
4527
+ * restarts rather than degrading.
4528
+ */
4529
+ rejectsDecisionFields(error) {
4530
+ if (!this.peerServesDecisionFields) return false;
4531
+ if (!(error instanceof GraphQLRequestError) || error.category !== "graphql") return false;
4532
+ return DECISION_FIELDS.some((field) => error.message.includes(field));
4533
+ }
4534
+ /**
4535
+ * The poll query. `withDecisionFields` selects the two fields added with the
4536
+ * auth projection; a remote on the previous schema is polled without them.
4537
+ */
4538
+ pollQuery(withDecisionFields) {
4539
+ return `
4455
4540
  query PollSyncEnvelopes($channelId: String!, $outboxAck: Int!, $outboxLatest: Int!) {
4456
4541
  pollSyncEnvelopes(channelId: $channelId, outboxAck: $outboxAck, outboxLatest: $outboxLatest) {
4457
4542
  envelopes {
@@ -4466,6 +4551,7 @@ var GqlRequestChannel = class {
4466
4551
  hash
4467
4552
  skip
4468
4553
  error
4554
+ ${withDecisionFields ? "deniedReason" : ""}
4469
4555
  id
4470
4556
  action {
4471
4557
  id
@@ -4509,6 +4595,7 @@ var GqlRequestChannel = class {
4509
4595
  deadLetters {
4510
4596
  documentId
4511
4597
  error
4598
+ ${withDecisionFields ? "errorType" : ""}
4512
4599
  jobId
4513
4600
  branch
4514
4601
  scopes
@@ -4518,18 +4605,6 @@ var GqlRequestChannel = class {
4518
4605
  }
4519
4606
  }
4520
4607
  `;
4521
- const variables = {
4522
- channelId: this.channelId,
4523
- outboxAck: ackOrdinal,
4524
- outboxLatest: latestOrdinal
4525
- };
4526
- const response = await this.executeGraphQL(query, variables);
4527
- return {
4528
- envelopes: response.pollSyncEnvelopes.envelopes,
4529
- ackOrdinal: response.pollSyncEnvelopes.ackOrdinal,
4530
- deadLetters: response.pollSyncEnvelopes.deadLetters ?? [],
4531
- hasMore: response.pollSyncEnvelopes.hasMore
4532
- };
4533
4608
  }
4534
4609
  /**
4535
4610
  * Registers or updates this channel on the remote server via GraphQL mutation.
@@ -5011,7 +5086,8 @@ function rowToDeadLetterRecord(row) {
5011
5086
  branch: row.branch,
5012
5087
  operations: row.operations,
5013
5088
  errorSource: row.error_source,
5014
- errorMessage: row.error_message
5089
+ errorMessage: row.error_message,
5090
+ errorType: row.error_type ?? "UNCLASSIFIED"
5015
5091
  };
5016
5092
  }
5017
5093
  function deadLetterRecordToRow(record) {
@@ -5025,7 +5101,8 @@ function deadLetterRecordToRow(record) {
5025
5101
  branch: record.branch,
5026
5102
  operations: JSON.stringify(record.operations),
5027
5103
  error_source: record.errorSource,
5028
- error_message: record.errorMessage
5104
+ error_message: record.errorMessage,
5105
+ error_type: record.errorType
5029
5106
  };
5030
5107
  }
5031
5108
  /**
@@ -5082,9 +5159,11 @@ var KyselySyncDeadLetterStorage = class {
5082
5159
  }
5083
5160
  async listQuarantinedDocumentIds(signal) {
5084
5161
  if (signal?.aborted) throw new Error("Operation aborted");
5085
- const rows = await this.db.selectFrom("sync_dead_letters").select("document_id").distinct().execute();
5162
+ const rows = await this.db.selectFrom("sync_dead_letters").select(["document_id", "error_type"]).distinct().execute();
5086
5163
  if (signal?.aborted) throw new Error("Operation aborted");
5087
- return rows.map((row) => row.document_id);
5164
+ const quarantined = /* @__PURE__ */ new Set();
5165
+ for (const row of rows) if (quarantinesDocument(row.error_type ?? "UNCLASSIFIED")) quarantined.add(row.document_id);
5166
+ return [...quarantined];
5088
5167
  }
5089
5168
  };
5090
5169
  //#endregion
@@ -5757,7 +5836,8 @@ var SyncManager = class {
5757
5836
  remote.channel.deadLetter.onAdded((syncOps) => {
5758
5837
  for (const syncOp of syncOps) {
5759
5838
  this.logger.error("Dead letter (@remote, @documentId, @jobId, @error, @dependencies)", remote.meta.name, syncOp.documentId, syncOp.jobId, syncOp.error?.message ?? "unknown", syncOp.jobDependencies);
5760
- this.quarantinedDocumentIds.add(syncOp.documentId);
5839
+ const errorType = syncOperationErrorType(syncOp.error);
5840
+ if (quarantinesDocument(errorType)) this.quarantinedDocumentIds.add(syncOp.documentId);
5761
5841
  const record = {
5762
5842
  id: syncOp.id,
5763
5843
  jobId: syncOp.jobId,
@@ -5768,7 +5848,8 @@ var SyncManager = class {
5768
5848
  branch: syncOp.branch,
5769
5849
  operations: syncOp.operations,
5770
5850
  errorSource: syncOp.error?.source ?? ChannelErrorSource.None,
5771
- errorMessage: syncOp.error?.error.message ?? "unknown"
5851
+ errorMessage: syncOp.error?.error.message ?? "unknown",
5852
+ errorType
5772
5853
  };
5773
5854
  this.deadLetterStorage.add(record).catch((err) => {
5774
5855
  this.logger.error("Failed to persist dead letter (@id, @error)", record.id, err instanceof Error ? err.message : String(err));
@@ -5778,7 +5859,8 @@ var SyncManager = class {
5778
5859
  jobId: record.jobId,
5779
5860
  remoteName: record.remoteName,
5780
5861
  documentId: record.documentId,
5781
- errorSource: record.errorSource
5862
+ errorSource: record.errorSource,
5863
+ errorType: record.errorType
5782
5864
  }).catch(() => {});
5783
5865
  }
5784
5866
  const items = remote.channel.deadLetter.items;
@@ -5805,7 +5887,7 @@ var SyncManager = class {
5805
5887
  const syncOps = [];
5806
5888
  for (const record of records) {
5807
5889
  const syncOp = new SyncOperation(record.id, record.jobId, record.jobDependencies, record.remoteName, record.documentId, record.scopes, record.branch, record.operations);
5808
- syncOp.failed(new ChannelError(record.errorSource, new Error(record.errorMessage)));
5890
+ syncOp.failed(new ChannelError(record.errorSource, new Error(record.errorMessage), record.errorType));
5809
5891
  syncOps.push(syncOp);
5810
5892
  }
5811
5893
  remote.channel.deadLetter.add(...syncOps);
@@ -5886,8 +5968,7 @@ var SyncManager = class {
5886
5968
  if (completedJobInfo.status === JobStatus.FAILED) {
5887
5969
  const errorMessage = completedJobInfo.error?.message || "Unknown error";
5888
5970
  this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, completedJobInfo.id, errorMessage);
5889
- const error = new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Failed to apply operations: ${errorMessage}`));
5890
- syncOp.failed(error);
5971
+ syncOp.failed(this.inboxFailure(completedJobInfo.error));
5891
5972
  remote.channel.deadLetter.add(syncOp);
5892
5973
  } else syncOp.executed();
5893
5974
  remote.channel.inbox.remove(syncOp);
@@ -5964,14 +6045,26 @@ var SyncManager = class {
5964
6045
  }
5965
6046
  if (this.isShutdown) return;
5966
6047
  if (completedJobInfo.status === JobStatus.FAILED) {
5967
- const errorMessage = completedJobInfo.error?.message || "Unknown error";
5968
- const channelError = new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Failed to apply operations: ${errorMessage}`));
5969
- syncOp.failed(channelError);
6048
+ syncOp.failed(this.inboxFailure(completedJobInfo.error));
5970
6049
  remote.channel.deadLetter.add(syncOp);
5971
6050
  } else syncOp.executed();
5972
6051
  remote.channel.inbox.remove(syncOp);
5973
6052
  }
5974
6053
  }
6054
+ /**
6055
+ * The dead letter for a load job the executor failed.
6056
+ *
6057
+ * The classification is passed explicitly because it cannot be recovered
6058
+ * downstream: the wrapper carries the failure's message, not the failure, so
6059
+ * deriving it from the wrapper's own name would classify every one of these
6060
+ * as unclassified and quarantine the document. A held auth operation must
6061
+ * keep syncing, because reconciling the two policies needs the traffic a
6062
+ * quarantine would stop.
6063
+ */
6064
+ inboxFailure(error) {
6065
+ const message = error?.message || "Unknown error";
6066
+ return new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Failed to apply operations: ${message}`), classifyJobFailure(error?.name ?? "Error"));
6067
+ }
5975
6068
  async updateOutbox(remote, ackOrdinal, mode = OutboxMode.Backfill, signal) {
5976
6069
  const composedSignal = signal ? AbortSignal.any([signal, this.abortController.signal]) : this.abortController.signal;
5977
6070
  let maxOrdinal = ackOrdinal;
@@ -6930,7 +7023,7 @@ var ReactorBuilder = class {
6930
7023
  await executorManager.start(executorStartCount);
6931
7024
  const readModelInstances = Array.from(new Set([...this.readModels]));
6932
7025
  const documentViewConsistencyTracker = new ConsistencyTracker();
6933
- const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker);
7026
+ const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, this.executorConfig.featureFlags?.documentDecisions ?? false);
6934
7027
  try {
6935
7028
  await documentView.init();
6936
7029
  } catch (error) {
@@ -7069,7 +7162,7 @@ var ReactorBuilder = class {
7069
7162
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
7070
7163
  import("./worker-handle-CrERzl8s.js"),
7071
7164
  import("./transport-ByGviWdZ.js"),
7072
- import("./worker-BMn-2vsH.js").then((n) => n.n)
7165
+ import("./worker-jEJW6_j7.js").then((n) => n.n)
7073
7166
  ]);
7074
7167
  const poolConfig = {
7075
7168
  enabled: true,
@@ -7331,6 +7424,38 @@ function driveIdFromUrl(url) {
7331
7424
  return url.split("/").pop() ?? "";
7332
7425
  }
7333
7426
  //#endregion
7427
+ //#region src/decision/stream-order.ts
7428
+ /**
7429
+ * The first pair of effective operations whose stored order contradicts their
7430
+ * timestamps, or undefined when the stream is in position order.
7431
+ *
7432
+ * Such a stream cannot be walked, and the auth stream is never reshuffled once
7433
+ * the monotonic rule is on, so run this before enabling enforcement on a fleet.
7434
+ *
7435
+ * `requireStrict` additionally rejects a tie, which is what the auth stream's
7436
+ * monotonic rule requires and what the walk alone does not care about.
7437
+ */
7438
+ function firstOutOfOrderPair(operations, options) {
7439
+ const requireStrict = options?.requireStrict ?? false;
7440
+ const effective = garbageCollect(sortOperations([...operations]));
7441
+ for (let i = 1; i < effective.length; i++) {
7442
+ const previous = effective[i - 1];
7443
+ const current = effective[i];
7444
+ const previousAt = Date.parse(previous.timestampUtcMs);
7445
+ const currentAt = Date.parse(current.timestampUtcMs);
7446
+ if (currentAt < previousAt) return {
7447
+ previous,
7448
+ current,
7449
+ kind: "descending"
7450
+ };
7451
+ if (requireStrict && currentAt === previousAt) return {
7452
+ previous,
7453
+ current,
7454
+ kind: "tied"
7455
+ };
7456
+ }
7457
+ }
7458
+ //#endregion
7334
7459
  //#region src/read-models/interfaces.ts
7335
7460
  function supportsLiveReadModelRegistration(coordinator) {
7336
7461
  return "addReadModel" in coordinator && typeof coordinator.addReadModel === "function";
@@ -7361,6 +7486,7 @@ var DocumentIntegrityService = class {
7361
7486
  async validateDocument(documentId, branch = "main", signal) {
7362
7487
  const keyframeIssues = [];
7363
7488
  const snapshotIssues = [];
7489
+ const streamOrderIssues = await this.findStreamOrderIssues(documentId, branch, signal);
7364
7490
  const replayCache = new KyselyWriteCache(passthroughKeyframeStore, this.operationStore, this.documentModelRegistry, {
7365
7491
  maxDocuments: 1,
7366
7492
  ringBufferSize: 1,
@@ -7387,9 +7513,10 @@ var DocumentIntegrityService = class {
7387
7513
  } catch {
7388
7514
  return {
7389
7515
  documentId,
7390
- isConsistent: keyframeIssues.length === 0,
7516
+ isConsistent: keyframeIssues.length === 0 && streamOrderIssues.length === 0,
7391
7517
  keyframeIssues,
7392
- snapshotIssues
7518
+ snapshotIssues,
7519
+ streamOrderIssues
7393
7520
  };
7394
7521
  }
7395
7522
  const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
@@ -7415,9 +7542,10 @@ var DocumentIntegrityService = class {
7415
7542
  }
7416
7543
  return {
7417
7544
  documentId,
7418
- isConsistent: keyframeIssues.length === 0 && snapshotIssues.length === 0,
7545
+ isConsistent: keyframeIssues.length === 0 && snapshotIssues.length === 0 && streamOrderIssues.length === 0,
7419
7546
  keyframeIssues,
7420
- snapshotIssues
7547
+ snapshotIssues,
7548
+ streamOrderIssues
7421
7549
  };
7422
7550
  }
7423
7551
  async rebuildKeyframes(documentId, branch = "main", signal) {
@@ -7439,12 +7567,26 @@ var DocumentIntegrityService = class {
7439
7567
  scopesInvalidated: scopes.length
7440
7568
  };
7441
7569
  }
7570
+ async findStreamOrderIssues(documentId, branch, signal) {
7571
+ const scopes = await this.discoverScopes(documentId, branch, signal);
7572
+ const issues = [];
7573
+ for (const scope of scopes) {
7574
+ throwIfAborted(signal);
7575
+ const pair = firstOutOfOrderPair((await this.operationStore.getSince(documentId, scope, branch, -1, void 0, void 0, signal)).results, { requireStrict: scope === "auth" });
7576
+ if (pair !== void 0) issues.push({
7577
+ scope,
7578
+ branch,
7579
+ ...pair
7580
+ });
7581
+ }
7582
+ return issues;
7583
+ }
7442
7584
  async discoverScopes(documentId, branch, signal) {
7443
7585
  const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
7444
7586
  return Object.keys(revisions.revision);
7445
7587
  }
7446
7588
  };
7447
7589
  //#endregion
7448
- export { APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_DRIVE_CONTAINER_TYPES, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DocumentChangeType, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RetryAccounting, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, batchOperationsByDocument, buildDecisionModel, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createMutableShutdownStatus, createRelationalDb, deleteDocumentAction, documentActions, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, getMigrationStatus, instrumentPgPool, isDriveAuthError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, removeRelationshipAction, runMigrations, sanitizeArg, supportsLiveReadModelRegistration, trimMailboxFromAckOrdinal, updateRelationshipAction, upgradeDocumentAction, workerEntryPath };
7590
+ export { APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_DRIVE_CONTAINER_TYPES, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DocumentChangeType, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RetryAccounting, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, authDecisionModel, batchOperationsByDocument, buildDecisionModel, classifyJobFailure, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createMutableShutdownStatus, createRelationalDb, decideAtHead, deleteDocumentAction, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, firstOutOfOrderPair, getMigrationStatus, instrumentPgPool, isDriveAuthError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, quarantinesDocument, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, supportsLiveReadModelRegistration, syncOperationErrorType, trimMailboxFromAckOrdinal, updateRelationshipAction, upgradeDocumentAction, workerEntryPath };
7449
7591
 
7450
7592
  //# sourceMappingURL=index.js.map