@powerhousedao/reactor 6.2.2-dev.4 → 6.2.2-dev.41
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/{build-worker-executor--nhFRF47.js → build-worker-executor-CAEZD_yJ.js} +2 -2
- package/dist/{build-worker-executor--nhFRF47.js.map → build-worker-executor-CAEZD_yJ.js.map} +1 -1
- package/dist/{document-indexer-FGJmRAdX.js → document-indexer-C0GB0b8Q.js} +20 -7
- package/dist/document-indexer-C0GB0b8Q.js.map +1 -0
- package/dist/{drive-container-types-DpJp2AmE.js → drive-container-types-BJCKXJwH.js} +1498 -206
- package/dist/drive-container-types-BJCKXJwH.js.map +1 -0
- package/dist/entry.js +3 -2
- package/dist/entry.js.map +1 -1
- package/dist/index.d.ts +2045 -1637
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +319 -66
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +4 -4
- package/dist/projection-entry.js.map +1 -1
- package/dist/{worker-handle-B1w03nRA.js → worker-handle-CrERzl8s.js} +3 -2
- package/dist/worker-handle-CrERzl8s.js.map +1 -0
- package/dist/{worker-DBJOv8Gp.js → worker-jEJW6_j7.js} +2 -2
- package/dist/{worker-DBJOv8Gp.js.map → worker-jEJW6_j7.js.map} +1 -1
- package/package.json +5 -4
- package/dist/document-indexer-FGJmRAdX.js.map +0 -1
- package/dist/drive-container-types-DpJp2AmE.js.map +0 -1
- package/dist/worker-handle-B1w03nRA.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { A as
|
|
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-
|
|
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-
|
|
7
|
-
import { actions, actions as documentActions, createPresignedHeader, 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
|
};
|
|
@@ -680,6 +682,45 @@ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
|
680
682
|
return DocumentChangeType;
|
|
681
683
|
}({});
|
|
682
684
|
//#endregion
|
|
685
|
+
//#region src/client/util.ts
|
|
686
|
+
const ALWAYS_READABLE_SCOPES = new Set(["auth", "document"]);
|
|
687
|
+
function authSubjectFromSigner(signer) {
|
|
688
|
+
return {
|
|
689
|
+
address: signer.user?.address,
|
|
690
|
+
key: signer.app?.key
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function canReadScope(auth, subject, scope) {
|
|
694
|
+
return ALWAYS_READABLE_SCOPES.has(scope) || decide(auth, subject, {
|
|
695
|
+
verb: "read",
|
|
696
|
+
scope
|
|
697
|
+
}) === "allow";
|
|
698
|
+
}
|
|
699
|
+
function withAuthScope(view) {
|
|
700
|
+
if (view?.scopes && view.scopes.length > 0) return {
|
|
701
|
+
...view,
|
|
702
|
+
scopes: [...new Set([...view.scopes, "auth"])]
|
|
703
|
+
};
|
|
704
|
+
return view;
|
|
705
|
+
}
|
|
706
|
+
function filterReadableScopes(document, subject) {
|
|
707
|
+
const state = document.state;
|
|
708
|
+
if (!state) return document;
|
|
709
|
+
const auth = document.state.auth;
|
|
710
|
+
const readable = (scope) => canReadScope(auth, subject, scope);
|
|
711
|
+
return {
|
|
712
|
+
...document,
|
|
713
|
+
state: keepReadableScopes(state, readable),
|
|
714
|
+
initialState: keepReadableScopes(document.initialState, readable)
|
|
715
|
+
};
|
|
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
|
+
}
|
|
723
|
+
//#endregion
|
|
683
724
|
//#region src/client/reactor-client.ts
|
|
684
725
|
/**
|
|
685
726
|
* ReactorClient implementation that wraps lower-level APIs to provide
|
|
@@ -711,6 +752,9 @@ var ReactorClient = class {
|
|
|
711
752
|
this.drives = new DriveClient(this, logger, reactor, signer);
|
|
712
753
|
this.logger.verbose("ReactorClient initialized");
|
|
713
754
|
}
|
|
755
|
+
readSubject(subject) {
|
|
756
|
+
return subject ?? authSubjectFromSigner(this.signer);
|
|
757
|
+
}
|
|
714
758
|
/**
|
|
715
759
|
* Retrieves a list of document model modules.
|
|
716
760
|
*/
|
|
@@ -734,15 +778,15 @@ var ReactorClient = class {
|
|
|
734
778
|
*/
|
|
735
779
|
async get(identifier, view, signal) {
|
|
736
780
|
this.logger.verbose("get(@identifier, @view)", identifier, view);
|
|
737
|
-
return await this.reactor.getByIdOrSlug(identifier, view, void 0, signal);
|
|
781
|
+
return filterReadableScopes(await this.reactor.getByIdOrSlug(identifier, withAuthScope(view), void 0, signal), this.readSubject(view?.subject));
|
|
738
782
|
}
|
|
739
783
|
/**
|
|
740
784
|
* Resolves an identifier (id or slug) to the canonical document id, using the
|
|
741
785
|
* same lookup as the data path. Resolves against the "main" branch. Throws if
|
|
742
786
|
* the identifier cannot be resolved or is ambiguous.
|
|
743
787
|
*/
|
|
744
|
-
async resolveIdOrSlug(identifier, signal) {
|
|
745
|
-
return this.documentView.resolveIdOrSlug(identifier,
|
|
788
|
+
async resolveIdOrSlug(identifier, view, signal) {
|
|
789
|
+
return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
|
|
746
790
|
}
|
|
747
791
|
/**
|
|
748
792
|
* Retrieves operations for a document
|
|
@@ -750,8 +794,15 @@ var ReactorClient = class {
|
|
|
750
794
|
async getOperations(documentIdentifier, view, filter, paging, signal) {
|
|
751
795
|
this.logger.verbose("getOperations(@documentIdentifier, @view, @filter, @paging)", documentIdentifier, view, filter, paging);
|
|
752
796
|
const documentId = await this.documentView.resolveIdOrSlug(documentIdentifier, view, void 0, signal);
|
|
753
|
-
|
|
797
|
+
const authDoc = await this.reactor.getByIdOrSlug(documentId, {
|
|
798
|
+
scopes: ["auth"],
|
|
799
|
+
branch: view?.branch
|
|
800
|
+
}, void 0, signal);
|
|
801
|
+
const subject = this.readSubject(view?.subject);
|
|
802
|
+
const canRead = (scope) => canReadScope(authDoc?.state.auth, subject, scope);
|
|
803
|
+
if (paging?.cursor && isCompositeCursor(paging.cursor)) return this.getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead);
|
|
754
804
|
const operationsByScope = await this.reactor.getOperations(documentId, view, filter, paging, void 0, signal);
|
|
805
|
+
for (const scope of Object.keys(operationsByScope)) if (!canRead(scope)) delete operationsByScope[scope];
|
|
755
806
|
const scopeEntries = Object.entries(operationsByScope);
|
|
756
807
|
const effectivePaging = paging || {
|
|
757
808
|
cursor: "0",
|
|
@@ -779,11 +830,12 @@ var ReactorClient = class {
|
|
|
779
830
|
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
780
831
|
};
|
|
781
832
|
}
|
|
782
|
-
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal) {
|
|
833
|
+
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
783
834
|
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
784
835
|
const allOperations = [];
|
|
785
836
|
const activeCursors = {};
|
|
786
837
|
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
838
|
+
if (!canRead(scopeName)) continue;
|
|
787
839
|
const scopeView = {
|
|
788
840
|
...view,
|
|
789
841
|
scopes: [scopeName]
|
|
@@ -817,7 +869,7 @@ var ReactorClient = class {
|
|
|
817
869
|
limit: 0
|
|
818
870
|
}
|
|
819
871
|
};
|
|
820
|
-
return this.
|
|
872
|
+
return this.find({ ids: targetIds }, view, paging, signal);
|
|
821
873
|
}
|
|
822
874
|
/**
|
|
823
875
|
* Retrieves incoming relationships of a given type to a target document.
|
|
@@ -833,14 +885,19 @@ var ReactorClient = class {
|
|
|
833
885
|
limit: 0
|
|
834
886
|
}
|
|
835
887
|
};
|
|
836
|
-
return this.
|
|
888
|
+
return this.find({ ids: sourceIds }, view, paging, signal);
|
|
837
889
|
}
|
|
838
890
|
/**
|
|
839
891
|
* Filters documents by criteria and returns a list of them
|
|
840
892
|
*/
|
|
841
893
|
async find(search, view, paging, signal) {
|
|
842
894
|
this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
|
|
843
|
-
|
|
895
|
+
const results = await this.reactor.find(search, withAuthScope(view), paging, void 0, signal);
|
|
896
|
+
const readSubject = this.readSubject(view?.subject);
|
|
897
|
+
return {
|
|
898
|
+
...results,
|
|
899
|
+
results: results.results.map((doc) => filterReadableScopes(doc, readSubject))
|
|
900
|
+
};
|
|
844
901
|
}
|
|
845
902
|
/**
|
|
846
903
|
* Creates a document and waits for completion
|
|
@@ -1078,13 +1135,15 @@ var ReactorClient = class {
|
|
|
1078
1135
|
*/
|
|
1079
1136
|
subscribe(search, callback, view) {
|
|
1080
1137
|
this.logger.verbose("subscribe(@search, @view)", search, view);
|
|
1138
|
+
const subject = this.readSubject(view?.subject);
|
|
1139
|
+
const readable = (document) => filterReadableScopes(document, subject);
|
|
1081
1140
|
const unsubscribeCreated = this.subscriptionManager.onDocumentCreated((result) => {
|
|
1082
1141
|
(async () => {
|
|
1083
1142
|
try {
|
|
1084
|
-
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)));
|
|
1085
1144
|
callback({
|
|
1086
1145
|
type: DocumentChangeType.Created,
|
|
1087
|
-
documents
|
|
1146
|
+
documents: documents.map(readable)
|
|
1088
1147
|
});
|
|
1089
1148
|
} catch {}
|
|
1090
1149
|
})();
|
|
@@ -1099,7 +1158,7 @@ var ReactorClient = class {
|
|
|
1099
1158
|
const unsubscribeUpdated = this.subscriptionManager.onDocumentStateUpdated((result) => {
|
|
1100
1159
|
callback({
|
|
1101
1160
|
type: DocumentChangeType.Updated,
|
|
1102
|
-
documents: result.results
|
|
1161
|
+
documents: result.results.map(readable)
|
|
1103
1162
|
});
|
|
1104
1163
|
}, search, view);
|
|
1105
1164
|
const unsubscribeRelationship = this.subscriptionManager.onRelationshipChanged((parentId, childId, changeType) => {
|
|
@@ -1144,17 +1203,33 @@ let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
|
|
|
1144
1203
|
return JobQueueState;
|
|
1145
1204
|
}({});
|
|
1146
1205
|
/**
|
|
1206
|
+
* How a retry is accounted against the job's retry limit.
|
|
1207
|
+
* - `CountAgainstLimit` (default): a fault; the job eventually exhausts its
|
|
1208
|
+
* retries and fails terminally.
|
|
1209
|
+
* - `ExemptFromLimit`: not a fault, so the attempt is not charged to the job.
|
|
1210
|
+
* Used for concurrency conflicts, where the retry does new work.
|
|
1211
|
+
*/
|
|
1212
|
+
let RetryAccounting = /* @__PURE__ */ function(RetryAccounting) {
|
|
1213
|
+
RetryAccounting["CountAgainstLimit"] = "count-against-limit";
|
|
1214
|
+
RetryAccounting["ExemptFromLimit"] = "exempt-from-limit";
|
|
1215
|
+
return RetryAccounting;
|
|
1216
|
+
}({});
|
|
1217
|
+
/**
|
|
1147
1218
|
* Event types for the queue system
|
|
1148
1219
|
*/
|
|
1149
1220
|
const QueueEventTypes = { JOB_AVAILABLE: 1e4 };
|
|
1150
1221
|
//#endregion
|
|
1151
1222
|
//#region src/executor/job-result-handler.ts
|
|
1223
|
+
/** Conflict retries a job may take without charging its retry limit. */
|
|
1224
|
+
const MAX_EXEMPT_CONFLICT_RETRIES = 20;
|
|
1152
1225
|
function toErrorInfo(error) {
|
|
1153
1226
|
if (error instanceof Error) return {
|
|
1227
|
+
name: error.name,
|
|
1154
1228
|
message: error.message,
|
|
1155
1229
|
stack: error.stack || (/* @__PURE__ */ new Error()).stack || ""
|
|
1156
1230
|
};
|
|
1157
1231
|
return {
|
|
1232
|
+
name: "Error",
|
|
1158
1233
|
message: error,
|
|
1159
1234
|
stack: (/* @__PURE__ */ new Error()).stack || ""
|
|
1160
1235
|
};
|
|
@@ -1187,12 +1262,19 @@ var JobResultHandler = class {
|
|
|
1187
1262
|
} catch {}
|
|
1188
1263
|
}
|
|
1189
1264
|
}
|
|
1265
|
+
if (result.error && AppendConditionFailedError.isError(result.error) && this.countConflicts(handle.job) < MAX_EXEMPT_CONFLICT_RETRIES) {
|
|
1266
|
+
const errorInfo = toErrorInfo(result.error);
|
|
1267
|
+
try {
|
|
1268
|
+
await this.queue.retryJob(handle.job.id, errorInfo, RetryAccounting.ExemptFromLimit);
|
|
1269
|
+
return;
|
|
1270
|
+
} catch {}
|
|
1271
|
+
}
|
|
1190
1272
|
if (result.error && DocumentNotFoundError.isError(result.error)) {
|
|
1191
1273
|
handle.defer();
|
|
1192
1274
|
callbacks.deferJob(handle.job.documentId, handle.job);
|
|
1193
1275
|
return;
|
|
1194
1276
|
}
|
|
1195
|
-
if (result.error && DocumentDeletedError.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))) {
|
|
1196
1278
|
const errorInfo = toErrorInfo(result.error);
|
|
1197
1279
|
this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
|
|
1198
1280
|
this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
|
|
@@ -1230,6 +1312,12 @@ var JobResultHandler = class {
|
|
|
1230
1312
|
handle.fail(fullErrorInfo);
|
|
1231
1313
|
}
|
|
1232
1314
|
}
|
|
1315
|
+
/** How many times this job has already lost an append-condition race. */
|
|
1316
|
+
countConflicts(job) {
|
|
1317
|
+
let conflicts = 0;
|
|
1318
|
+
for (const error of job.errorHistory) if (AppendConditionFailedError.isFailureMessage(error.message)) conflicts++;
|
|
1319
|
+
return conflicts;
|
|
1320
|
+
}
|
|
1233
1321
|
hasCreateDocumentAction(job) {
|
|
1234
1322
|
for (const action of job.actions) if (action.type === "CREATE_DOCUMENT") return true;
|
|
1235
1323
|
for (const operation of job.operations) if (operation.action.type === "CREATE_DOCUMENT") return true;
|
|
@@ -1245,6 +1333,7 @@ var JobResultHandler = class {
|
|
|
1245
1333
|
stackLines.push(`[Attempt ${index + 1}] Stack trace:\n${error.stack}`);
|
|
1246
1334
|
});
|
|
1247
1335
|
return {
|
|
1336
|
+
name: currentError.name,
|
|
1248
1337
|
message: messageLines.join("\n"),
|
|
1249
1338
|
stack: stackLines.join("\n\n")
|
|
1250
1339
|
};
|
|
@@ -1850,6 +1939,7 @@ var InMemoryJobTracker = class {
|
|
|
1850
1939
|
}
|
|
1851
1940
|
handleJobFailed(event) {
|
|
1852
1941
|
this.markFailed(event.jobId, {
|
|
1942
|
+
name: event.error.name,
|
|
1853
1943
|
message: event.error.message,
|
|
1854
1944
|
stack: event.error.stack || ""
|
|
1855
1945
|
}, event.job);
|
|
@@ -2311,10 +2401,12 @@ var InMemoryQueue = class {
|
|
|
2311
2401
|
}
|
|
2312
2402
|
toErrorInfo(error) {
|
|
2313
2403
|
if (error instanceof Error) return {
|
|
2404
|
+
name: error.name,
|
|
2314
2405
|
message: error.message,
|
|
2315
2406
|
stack: error.stack || (/* @__PURE__ */ new Error()).stack || ""
|
|
2316
2407
|
};
|
|
2317
2408
|
return {
|
|
2409
|
+
name: "Error",
|
|
2318
2410
|
message: error,
|
|
2319
2411
|
stack: (/* @__PURE__ */ new Error()).stack || ""
|
|
2320
2412
|
};
|
|
@@ -2402,6 +2494,7 @@ var InMemoryQueue = class {
|
|
|
2402
2494
|
await this.resolver.ensureModelLoaded(documentType);
|
|
2403
2495
|
} catch {
|
|
2404
2496
|
await this.failJob(job.id, {
|
|
2497
|
+
name: "Error",
|
|
2405
2498
|
message: `Failed to load document model for type: ${documentType}`,
|
|
2406
2499
|
stack: (/* @__PURE__ */ new Error()).stack || ""
|
|
2407
2500
|
});
|
|
@@ -2587,7 +2680,7 @@ var InMemoryQueue = class {
|
|
|
2587
2680
|
if (documentId) this.markJobComplete(jobId, documentId);
|
|
2588
2681
|
this.jobIndex.delete(jobId);
|
|
2589
2682
|
}
|
|
2590
|
-
async retryJob(jobId, error) {
|
|
2683
|
+
async retryJob(jobId, error, accounting = RetryAccounting.CountAgainstLimit) {
|
|
2591
2684
|
const job = this.jobIndex.get(jobId);
|
|
2592
2685
|
if (!job) return;
|
|
2593
2686
|
job.lastError = error;
|
|
@@ -2596,9 +2689,10 @@ var InMemoryQueue = class {
|
|
|
2596
2689
|
this.jobIndex.delete(jobId);
|
|
2597
2690
|
this.jobIdToQueueKey.delete(jobId);
|
|
2598
2691
|
if (error) job.errorHistory.push(error);
|
|
2692
|
+
const retryCount = job.retryCount || 0;
|
|
2599
2693
|
const updatedJob = {
|
|
2600
2694
|
...job,
|
|
2601
|
-
retryCount:
|
|
2695
|
+
retryCount: accounting === RetryAccounting.CountAgainstLimit ? retryCount + 1 : retryCount,
|
|
2602
2696
|
lastError: error
|
|
2603
2697
|
};
|
|
2604
2698
|
await this.enqueue(updatedJob);
|
|
@@ -3437,11 +3531,18 @@ var PollingChannelError = class extends Error {
|
|
|
3437
3531
|
var ChannelError = class extends Error {
|
|
3438
3532
|
source;
|
|
3439
3533
|
error;
|
|
3440
|
-
|
|
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) {
|
|
3441
3541
|
super(`ChannelError[${source}]: ${error.message}`);
|
|
3442
3542
|
this.name = "ChannelError";
|
|
3443
3543
|
this.source = source;
|
|
3444
3544
|
this.error = error;
|
|
3545
|
+
this.errorType = errorType;
|
|
3445
3546
|
}
|
|
3446
3547
|
};
|
|
3447
3548
|
//#endregion
|
|
@@ -3646,6 +3747,7 @@ function toOperationWithContext(entry) {
|
|
|
3646
3747
|
skip: entry.skip,
|
|
3647
3748
|
hash: entry.hash,
|
|
3648
3749
|
timestampUtcMs: entry.timestampUtcMs,
|
|
3750
|
+
deniedReason: entry.deniedReason,
|
|
3649
3751
|
action: entry.action
|
|
3650
3752
|
},
|
|
3651
3753
|
context: {
|
|
@@ -3811,6 +3913,33 @@ function splitComponent(items, maxSize) {
|
|
|
3811
3913
|
for (let i = 0; i < sorted.length; i += maxSize) chunks.push(sorted.slice(i, i + maxSize));
|
|
3812
3914
|
return chunks;
|
|
3813
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
|
+
}
|
|
3814
3943
|
//#endregion
|
|
3815
3944
|
//#region src/sync/channels/interval-poll-timer.ts
|
|
3816
3945
|
const DEFAULT_CONFIG = {
|
|
@@ -3973,6 +4102,7 @@ function serializeEnvelope(envelope) {
|
|
|
3973
4102
|
hash: opWithContext.operation.hash,
|
|
3974
4103
|
skip: opWithContext.operation.skip,
|
|
3975
4104
|
error: opWithContext.operation.error,
|
|
4105
|
+
...opWithContext.operation.deniedReason !== void 0 ? { deniedReason: opWithContext.operation.deniedReason } : {},
|
|
3976
4106
|
id: opWithContext.operation.id,
|
|
3977
4107
|
action: serializeAction(opWithContext.operation.action)
|
|
3978
4108
|
},
|
|
@@ -4000,21 +4130,33 @@ function deserializeSignature(sig) {
|
|
|
4000
4130
|
return sig.split(", ");
|
|
4001
4131
|
}
|
|
4002
4132
|
/**
|
|
4003
|
-
*
|
|
4004
|
-
*
|
|
4005
|
-
*
|
|
4006
|
-
* 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.
|
|
4007
4136
|
*/
|
|
4008
|
-
function
|
|
4009
|
-
const
|
|
4010
|
-
if (
|
|
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
|
+
};
|
|
4011
4153
|
const deserializedSignatures = signer.signatures.map(deserializeSignature);
|
|
4012
4154
|
const deserializedOperation = {
|
|
4013
|
-
...
|
|
4155
|
+
...operation,
|
|
4014
4156
|
action: {
|
|
4015
|
-
...
|
|
4157
|
+
...operation.action,
|
|
4016
4158
|
context: {
|
|
4017
|
-
...
|
|
4159
|
+
...operation.action.context,
|
|
4018
4160
|
signer: {
|
|
4019
4161
|
...signer,
|
|
4020
4162
|
signatures: deserializedSignatures
|
|
@@ -4041,7 +4183,7 @@ function deserializeOperationSignatures(opWithContext) {
|
|
|
4041
4183
|
*/
|
|
4042
4184
|
function envelopesToSyncOperations(envelope, remoteName) {
|
|
4043
4185
|
if (!envelope.operations || envelope.operations.length === 0) return [];
|
|
4044
|
-
return batchOperationsByDocument(envelope.operations.map(
|
|
4186
|
+
return batchOperationsByDocument(envelope.operations.map(deserializeOperation)).map((batch) => {
|
|
4045
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);
|
|
4046
4188
|
});
|
|
4047
4189
|
}
|
|
@@ -4053,6 +4195,12 @@ const getLatestAppliedOrdinal = (syncOps) => {
|
|
|
4053
4195
|
//#endregion
|
|
4054
4196
|
//#region src/sync/channels/gql-req-channel.ts
|
|
4055
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
|
+
/**
|
|
4056
4204
|
* GraphQL-based synchronization channel for network communication between reactors.
|
|
4057
4205
|
*/
|
|
4058
4206
|
var GqlRequestChannel = class {
|
|
@@ -4079,6 +4227,8 @@ var GqlRequestChannel = class {
|
|
|
4079
4227
|
isPushing = false;
|
|
4080
4228
|
pendingDrain = false;
|
|
4081
4229
|
receivingPages = false;
|
|
4230
|
+
/** Cleared for good the first time the remote rejects {@link DECISION_FIELDS}. */
|
|
4231
|
+
peerServesDecisionFields = true;
|
|
4082
4232
|
isRecovering = false;
|
|
4083
4233
|
connectionState = "connecting";
|
|
4084
4234
|
/** Latest unrecoverable error was an auth rejection; cleared on connect. */
|
|
@@ -4257,7 +4407,7 @@ var GqlRequestChannel = class {
|
|
|
4257
4407
|
const syncOps = [];
|
|
4258
4408
|
for (const dl of deadLetters) {
|
|
4259
4409
|
const syncOp = new SyncOperation(crypto.randomUUID(), dl.jobId, [], this.remoteName, dl.documentId, dl.scopes, dl.branch, []);
|
|
4260
|
-
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));
|
|
4261
4411
|
syncOps.push(syncOp);
|
|
4262
4412
|
}
|
|
4263
4413
|
this.deadLetter.add(...syncOps);
|
|
@@ -4349,7 +4499,44 @@ var GqlRequestChannel = class {
|
|
|
4349
4499
|
* Queries the remote GraphQL endpoint for sync envelopes.
|
|
4350
4500
|
*/
|
|
4351
4501
|
async pollSyncEnvelopes(ackOrdinal, latestOrdinal) {
|
|
4352
|
-
const
|
|
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 `
|
|
4353
4540
|
query PollSyncEnvelopes($channelId: String!, $outboxAck: Int!, $outboxLatest: Int!) {
|
|
4354
4541
|
pollSyncEnvelopes(channelId: $channelId, outboxAck: $outboxAck, outboxLatest: $outboxLatest) {
|
|
4355
4542
|
envelopes {
|
|
@@ -4364,6 +4551,7 @@ var GqlRequestChannel = class {
|
|
|
4364
4551
|
hash
|
|
4365
4552
|
skip
|
|
4366
4553
|
error
|
|
4554
|
+
${withDecisionFields ? "deniedReason" : ""}
|
|
4367
4555
|
id
|
|
4368
4556
|
action {
|
|
4369
4557
|
id
|
|
@@ -4407,6 +4595,7 @@ var GqlRequestChannel = class {
|
|
|
4407
4595
|
deadLetters {
|
|
4408
4596
|
documentId
|
|
4409
4597
|
error
|
|
4598
|
+
${withDecisionFields ? "errorType" : ""}
|
|
4410
4599
|
jobId
|
|
4411
4600
|
branch
|
|
4412
4601
|
scopes
|
|
@@ -4416,18 +4605,6 @@ var GqlRequestChannel = class {
|
|
|
4416
4605
|
}
|
|
4417
4606
|
}
|
|
4418
4607
|
`;
|
|
4419
|
-
const variables = {
|
|
4420
|
-
channelId: this.channelId,
|
|
4421
|
-
outboxAck: ackOrdinal,
|
|
4422
|
-
outboxLatest: latestOrdinal
|
|
4423
|
-
};
|
|
4424
|
-
const response = await this.executeGraphQL(query, variables);
|
|
4425
|
-
return {
|
|
4426
|
-
envelopes: response.pollSyncEnvelopes.envelopes,
|
|
4427
|
-
ackOrdinal: response.pollSyncEnvelopes.ackOrdinal,
|
|
4428
|
-
deadLetters: response.pollSyncEnvelopes.deadLetters ?? [],
|
|
4429
|
-
hasMore: response.pollSyncEnvelopes.hasMore
|
|
4430
|
-
};
|
|
4431
4608
|
}
|
|
4432
4609
|
/**
|
|
4433
4610
|
* Registers or updates this channel on the remote server via GraphQL mutation.
|
|
@@ -4909,7 +5086,8 @@ function rowToDeadLetterRecord(row) {
|
|
|
4909
5086
|
branch: row.branch,
|
|
4910
5087
|
operations: row.operations,
|
|
4911
5088
|
errorSource: row.error_source,
|
|
4912
|
-
errorMessage: row.error_message
|
|
5089
|
+
errorMessage: row.error_message,
|
|
5090
|
+
errorType: row.error_type ?? "UNCLASSIFIED"
|
|
4913
5091
|
};
|
|
4914
5092
|
}
|
|
4915
5093
|
function deadLetterRecordToRow(record) {
|
|
@@ -4923,7 +5101,8 @@ function deadLetterRecordToRow(record) {
|
|
|
4923
5101
|
branch: record.branch,
|
|
4924
5102
|
operations: JSON.stringify(record.operations),
|
|
4925
5103
|
error_source: record.errorSource,
|
|
4926
|
-
error_message: record.errorMessage
|
|
5104
|
+
error_message: record.errorMessage,
|
|
5105
|
+
error_type: record.errorType
|
|
4927
5106
|
};
|
|
4928
5107
|
}
|
|
4929
5108
|
/**
|
|
@@ -4980,9 +5159,11 @@ var KyselySyncDeadLetterStorage = class {
|
|
|
4980
5159
|
}
|
|
4981
5160
|
async listQuarantinedDocumentIds(signal) {
|
|
4982
5161
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
4983
|
-
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();
|
|
4984
5163
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
4985
|
-
|
|
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];
|
|
4986
5167
|
}
|
|
4987
5168
|
};
|
|
4988
5169
|
//#endregion
|
|
@@ -5655,7 +5836,8 @@ var SyncManager = class {
|
|
|
5655
5836
|
remote.channel.deadLetter.onAdded((syncOps) => {
|
|
5656
5837
|
for (const syncOp of syncOps) {
|
|
5657
5838
|
this.logger.error("Dead letter (@remote, @documentId, @jobId, @error, @dependencies)", remote.meta.name, syncOp.documentId, syncOp.jobId, syncOp.error?.message ?? "unknown", syncOp.jobDependencies);
|
|
5658
|
-
|
|
5839
|
+
const errorType = syncOperationErrorType(syncOp.error);
|
|
5840
|
+
if (quarantinesDocument(errorType)) this.quarantinedDocumentIds.add(syncOp.documentId);
|
|
5659
5841
|
const record = {
|
|
5660
5842
|
id: syncOp.id,
|
|
5661
5843
|
jobId: syncOp.jobId,
|
|
@@ -5666,7 +5848,8 @@ var SyncManager = class {
|
|
|
5666
5848
|
branch: syncOp.branch,
|
|
5667
5849
|
operations: syncOp.operations,
|
|
5668
5850
|
errorSource: syncOp.error?.source ?? ChannelErrorSource.None,
|
|
5669
|
-
errorMessage: syncOp.error?.error.message ?? "unknown"
|
|
5851
|
+
errorMessage: syncOp.error?.error.message ?? "unknown",
|
|
5852
|
+
errorType
|
|
5670
5853
|
};
|
|
5671
5854
|
this.deadLetterStorage.add(record).catch((err) => {
|
|
5672
5855
|
this.logger.error("Failed to persist dead letter (@id, @error)", record.id, err instanceof Error ? err.message : String(err));
|
|
@@ -5676,7 +5859,8 @@ var SyncManager = class {
|
|
|
5676
5859
|
jobId: record.jobId,
|
|
5677
5860
|
remoteName: record.remoteName,
|
|
5678
5861
|
documentId: record.documentId,
|
|
5679
|
-
errorSource: record.errorSource
|
|
5862
|
+
errorSource: record.errorSource,
|
|
5863
|
+
errorType: record.errorType
|
|
5680
5864
|
}).catch(() => {});
|
|
5681
5865
|
}
|
|
5682
5866
|
const items = remote.channel.deadLetter.items;
|
|
@@ -5703,7 +5887,7 @@ var SyncManager = class {
|
|
|
5703
5887
|
const syncOps = [];
|
|
5704
5888
|
for (const record of records) {
|
|
5705
5889
|
const syncOp = new SyncOperation(record.id, record.jobId, record.jobDependencies, record.remoteName, record.documentId, record.scopes, record.branch, record.operations);
|
|
5706
|
-
syncOp.failed(new ChannelError(record.errorSource, new Error(record.errorMessage)));
|
|
5890
|
+
syncOp.failed(new ChannelError(record.errorSource, new Error(record.errorMessage), record.errorType));
|
|
5707
5891
|
syncOps.push(syncOp);
|
|
5708
5892
|
}
|
|
5709
5893
|
remote.channel.deadLetter.add(...syncOps);
|
|
@@ -5784,8 +5968,7 @@ var SyncManager = class {
|
|
|
5784
5968
|
if (completedJobInfo.status === JobStatus.FAILED) {
|
|
5785
5969
|
const errorMessage = completedJobInfo.error?.message || "Unknown error";
|
|
5786
5970
|
this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, completedJobInfo.id, errorMessage);
|
|
5787
|
-
|
|
5788
|
-
syncOp.failed(error);
|
|
5971
|
+
syncOp.failed(this.inboxFailure(completedJobInfo.error));
|
|
5789
5972
|
remote.channel.deadLetter.add(syncOp);
|
|
5790
5973
|
} else syncOp.executed();
|
|
5791
5974
|
remote.channel.inbox.remove(syncOp);
|
|
@@ -5862,14 +6045,26 @@ var SyncManager = class {
|
|
|
5862
6045
|
}
|
|
5863
6046
|
if (this.isShutdown) return;
|
|
5864
6047
|
if (completedJobInfo.status === JobStatus.FAILED) {
|
|
5865
|
-
|
|
5866
|
-
const channelError = new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Failed to apply operations: ${errorMessage}`));
|
|
5867
|
-
syncOp.failed(channelError);
|
|
6048
|
+
syncOp.failed(this.inboxFailure(completedJobInfo.error));
|
|
5868
6049
|
remote.channel.deadLetter.add(syncOp);
|
|
5869
6050
|
} else syncOp.executed();
|
|
5870
6051
|
remote.channel.inbox.remove(syncOp);
|
|
5871
6052
|
}
|
|
5872
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
|
+
}
|
|
5873
6068
|
async updateOutbox(remote, ackOrdinal, mode = OutboxMode.Backfill, signal) {
|
|
5874
6069
|
const composedSignal = signal ? AbortSignal.any([signal, this.abortController.signal]) : this.abortController.signal;
|
|
5875
6070
|
let maxOrdinal = ackOrdinal;
|
|
@@ -6769,6 +6964,8 @@ var ReactorBuilder = class {
|
|
|
6769
6964
|
}
|
|
6770
6965
|
async buildModule() {
|
|
6771
6966
|
if (!this.logger) this.logger = new ConsoleLogger(["reactor"]);
|
|
6967
|
+
validateFeatureFlags(this.executorConfig.featureFlags ?? {}, FLAG_PREREQUISITES);
|
|
6968
|
+
if (this.projectionShardConfig !== void 0 && this.readModelFactories.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModelFactory; projection workers cannot receive host-only factory dependencies");
|
|
6772
6969
|
const resolvedSources = await resolveModelSources(this.documentModelSources);
|
|
6773
6970
|
if (this.workerPool) {
|
|
6774
6971
|
if (resolvedSources.manifest.length === 0) throw new Error("withWorkerPool requires at least one worker-importable document-model source ({ filePath } or { packageName }).");
|
|
@@ -6826,7 +7023,7 @@ var ReactorBuilder = class {
|
|
|
6826
7023
|
await executorManager.start(executorStartCount);
|
|
6827
7024
|
const readModelInstances = Array.from(new Set([...this.readModels]));
|
|
6828
7025
|
const documentViewConsistencyTracker = new ConsistencyTracker();
|
|
6829
|
-
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker);
|
|
7026
|
+
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, this.executorConfig.featureFlags?.documentDecisions ?? false);
|
|
6830
7027
|
try {
|
|
6831
7028
|
await documentView.init();
|
|
6832
7029
|
} catch (error) {
|
|
@@ -6852,6 +7049,7 @@ var ReactorBuilder = class {
|
|
|
6852
7049
|
}
|
|
6853
7050
|
for (const factory of this.readModelFactories) {
|
|
6854
7051
|
const readModel = await factory({
|
|
7052
|
+
documentModelRegistry,
|
|
6855
7053
|
operationIndex,
|
|
6856
7054
|
writeCache,
|
|
6857
7055
|
processorManagerConsistencyTracker
|
|
@@ -6962,9 +7160,9 @@ var ReactorBuilder = class {
|
|
|
6962
7160
|
*/
|
|
6963
7161
|
async createDefaultWorkerFactory(numWorkers, db, signatureVerifier) {
|
|
6964
7162
|
const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
|
|
6965
|
-
import("./worker-handle-
|
|
7163
|
+
import("./worker-handle-CrERzl8s.js"),
|
|
6966
7164
|
import("./transport-ByGviWdZ.js"),
|
|
6967
|
-
import("./worker-
|
|
7165
|
+
import("./worker-jEJW6_j7.js").then((n) => n.n)
|
|
6968
7166
|
]);
|
|
6969
7167
|
const poolConfig = {
|
|
6970
7168
|
enabled: true,
|
|
@@ -6985,7 +7183,8 @@ var ReactorBuilder = class {
|
|
|
6985
7183
|
poolConfig,
|
|
6986
7184
|
db,
|
|
6987
7185
|
signatureVerifier,
|
|
6988
|
-
models
|
|
7186
|
+
models,
|
|
7187
|
+
executorConfig: this.executorConfig
|
|
6989
7188
|
},
|
|
6990
7189
|
logger,
|
|
6991
7190
|
poolInstrumentation
|
|
@@ -7225,6 +7424,43 @@ function driveIdFromUrl(url) {
|
|
|
7225
7424
|
return url.split("/").pop() ?? "";
|
|
7226
7425
|
}
|
|
7227
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
|
|
7459
|
+
//#region src/read-models/interfaces.ts
|
|
7460
|
+
function supportsLiveReadModelRegistration(coordinator) {
|
|
7461
|
+
return "addReadModel" in coordinator && typeof coordinator.addReadModel === "function";
|
|
7462
|
+
}
|
|
7463
|
+
//#endregion
|
|
7228
7464
|
//#region src/admin/passthrough-keyframe-store.ts
|
|
7229
7465
|
const passthroughKeyframeStore = {
|
|
7230
7466
|
putKeyframe: () => Promise.resolve(),
|
|
@@ -7250,6 +7486,7 @@ var DocumentIntegrityService = class {
|
|
|
7250
7486
|
async validateDocument(documentId, branch = "main", signal) {
|
|
7251
7487
|
const keyframeIssues = [];
|
|
7252
7488
|
const snapshotIssues = [];
|
|
7489
|
+
const streamOrderIssues = await this.findStreamOrderIssues(documentId, branch, signal);
|
|
7253
7490
|
const replayCache = new KyselyWriteCache(passthroughKeyframeStore, this.operationStore, this.documentModelRegistry, {
|
|
7254
7491
|
maxDocuments: 1,
|
|
7255
7492
|
ringBufferSize: 1,
|
|
@@ -7276,9 +7513,10 @@ var DocumentIntegrityService = class {
|
|
|
7276
7513
|
} catch {
|
|
7277
7514
|
return {
|
|
7278
7515
|
documentId,
|
|
7279
|
-
isConsistent: keyframeIssues.length === 0,
|
|
7516
|
+
isConsistent: keyframeIssues.length === 0 && streamOrderIssues.length === 0,
|
|
7280
7517
|
keyframeIssues,
|
|
7281
|
-
snapshotIssues
|
|
7518
|
+
snapshotIssues,
|
|
7519
|
+
streamOrderIssues
|
|
7282
7520
|
};
|
|
7283
7521
|
}
|
|
7284
7522
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
@@ -7304,9 +7542,10 @@ var DocumentIntegrityService = class {
|
|
|
7304
7542
|
}
|
|
7305
7543
|
return {
|
|
7306
7544
|
documentId,
|
|
7307
|
-
isConsistent: keyframeIssues.length === 0 && snapshotIssues.length === 0,
|
|
7545
|
+
isConsistent: keyframeIssues.length === 0 && snapshotIssues.length === 0 && streamOrderIssues.length === 0,
|
|
7308
7546
|
keyframeIssues,
|
|
7309
|
-
snapshotIssues
|
|
7547
|
+
snapshotIssues,
|
|
7548
|
+
streamOrderIssues
|
|
7310
7549
|
};
|
|
7311
7550
|
}
|
|
7312
7551
|
async rebuildKeyframes(documentId, branch = "main", signal) {
|
|
@@ -7328,12 +7567,26 @@ var DocumentIntegrityService = class {
|
|
|
7328
7567
|
scopesInvalidated: scopes.length
|
|
7329
7568
|
};
|
|
7330
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
|
+
}
|
|
7331
7584
|
async discoverScopes(documentId, branch, signal) {
|
|
7332
7585
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
7333
7586
|
return Object.keys(revisions.revision);
|
|
7334
7587
|
}
|
|
7335
7588
|
};
|
|
7336
7589
|
//#endregion
|
|
7337
|
-
export { 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, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, batchOperationsByDocument, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createMutableShutdownStatus, createRelationalDb, deleteDocumentAction, documentActions, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, getMigrationStatus, instrumentPgPool, isDriveAuthError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, removeRelationshipAction, runMigrations, sanitizeArg, 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 };
|
|
7338
7591
|
|
|
7339
7592
|
//# sourceMappingURL=index.js.map
|