@powerhousedao/reactor 6.2.3-dev.24 → 6.2.3-dev.26
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-DxmDJ4QQ.js → build-worker-executor-Bvoba-AV.js} +4 -4
- package/dist/{build-worker-executor-DxmDJ4QQ.js.map → build-worker-executor-Bvoba-AV.js.map} +1 -1
- package/dist/{document-indexer-Dn9RB0sg.js → document-indexer-BVTGbEB0.js} +22 -21
- package/dist/document-indexer-BVTGbEB0.js.map +1 -0
- package/dist/entry.d.ts +2 -1
- package/dist/entry.js +2 -3
- package/dist/entry.js.map +1 -1
- package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
- package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
- package/dist/{drive-container-types-Be5-EY9W.js → forwarding-logger-CnuVDVze.js} +303 -53
- package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
- package/dist/index.d.ts +474 -307
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +700 -366
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +7 -13
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-mQhfVipO.js → projection-shard-manager-BpcqDtnQ.js} +2 -3
- package/dist/{projection-shard-manager-mQhfVipO.js.map → projection-shard-manager-BpcqDtnQ.js.map} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
- package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
- package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
- package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
- package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
- package/dist/{worker-handle-B3p-nuqG.js → worker-handle-C2rLUHwv.js} +9 -8
- package/dist/{worker-handle-B3p-nuqG.js.map → worker-handle-C2rLUHwv.js.map} +1 -1
- package/package.json +14 -7
- package/dist/document-indexer-Dn9RB0sg.js.map +0 -1
- package/dist/drive-container-types-Be5-EY9W.js.map +0 -1
- package/dist/error-info-Cpu4OY3o.js +0 -62
- package/dist/error-info-Cpu4OY3o.js.map +0 -1
- package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
- package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
- package/dist/types-DxDCmtId.js +0 -34
- package/dist/types-DxDCmtId.js.map +0 -1
- package/dist/worker-BDVzCtuf.js +0 -22
- package/dist/worker-BDVzCtuf.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import {
|
|
3
|
-
import { i as
|
|
4
|
-
import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-Dn9RB0sg.js";
|
|
5
|
-
import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
|
|
6
|
-
import { t as workerEntryPath } from "./worker-BDVzCtuf.js";
|
|
1
|
+
import { $ as DocumentNotFoundError, A as ModuleNotFoundError, B as authDecisionModel, C as DocumentMetaCache, D as DuplicateManifestError, E as JobExecutorEventTypes, F as summarizeSubmittedActions, G as DocumentExistence, H as APPEND_CONDITION_FAILED_PREFIX, I as targetDocumentId, J as RevisionMismatchError, K as DuplicateOperationError, L as decideAtHead, M as createConsistencyToken, N as createEmptyConsistencyToken, O as DuplicateModuleError, P as submittedActionIds, Q as DocumentDeletedError, R as selectDecisionModel, S as KyselyOperationIndex, T as DEFAULT_DEFERRED_JOB_TTL_MS, U as AppendConditionFailedError, V as buildDecisionModel, W as DocumentAlreadyExistsError, X as AuthTimestampNotMonotonicError, Y as AuthEnforcementDisabledError, Z as AuthorizationDeniedError, _ as DriveCollectionId, a as DEFAULT_DRIVE_CONTAINER_TYPES, at as EventBusAggregateError, b as EventBus, c as runMigrations, ct as parsePagingOptions, d as KyselyOperationStore, dt as __exportAll, et as ExcessiveReshuffleError, f as KyselyKeyframeStore, g as PassthroughSigner, h as verifyActionSignature, it as UpgradePreconditionFailedError, j as GATED_DOCUMENT_ACTIONS, k as InvalidModuleError, l as createForwardingPoolInstrumentation, lt as throwIfAborted, m as SimpleJobExecutor, n as errorToInfo, nt as InvalidSignatureError, o as REACTOR_SCHEMA, ot as ReactorEventTypes, p as DocumentModelRegistry, q as OptimisticLockError, r as sanitizeArg, rt as RelationshipNotFoundError, s as getMigrationStatus, st as matchesScope, t as createForwardingLogger, tt as InvalidOperationTimestampError, u as instrumentPgPool, v as KyselyExecutionScope, w as CollectionMembershipCache, x as KyselyWriteCache, y as resolveFeatureFlags, z as documentDecisionModel } from "./forwarding-logger-CnuVDVze.js";
|
|
2
|
+
import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-BM_ULVWd.js";
|
|
3
|
+
import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-BVTGbEB0.js";
|
|
7
4
|
import { DEFAULT_SIGNATURE_POLICY, DowngradeNotSupportedError, MAX_AUTH_GRANTS, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, requestedSignaturePolicy, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
8
5
|
import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
|
|
9
6
|
import { v4 } from "uuid";
|
|
@@ -315,6 +312,10 @@ let JobStatus = /* @__PURE__ */ function(JobStatus) {
|
|
|
315
312
|
* during construction, so the partial-`this` hazard does not apply.
|
|
316
313
|
*/
|
|
317
314
|
var DriveClient = class {
|
|
315
|
+
client;
|
|
316
|
+
logger;
|
|
317
|
+
reactor;
|
|
318
|
+
signer;
|
|
318
319
|
constructor(client, logger, reactor, signer) {
|
|
319
320
|
this.client = client;
|
|
320
321
|
this.logger = logger;
|
|
@@ -337,24 +338,25 @@ var DriveClient = class {
|
|
|
337
338
|
async addFile(driveIdentifier, document, parentFolder, signal) {
|
|
338
339
|
this.logger.verbose("drives.addFile(@driveIdentifier, @document, @parentFolder)", driveIdentifier, document.header.id, parentFolder);
|
|
339
340
|
const documentId = document.header.id;
|
|
341
|
+
const createInput = {
|
|
342
|
+
model: document.header.documentType,
|
|
343
|
+
version: 0,
|
|
344
|
+
documentId: document.header.id,
|
|
345
|
+
signing: {
|
|
346
|
+
signature: document.header.id,
|
|
347
|
+
publicKey: document.header.sig.publicKey,
|
|
348
|
+
nonce: document.header.sig.nonce,
|
|
349
|
+
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
350
|
+
documentType: document.header.documentType
|
|
351
|
+
},
|
|
352
|
+
slug: document.header.slug,
|
|
353
|
+
name: document.header.name,
|
|
354
|
+
branch: document.header.branch,
|
|
355
|
+
meta: document.header.meta,
|
|
356
|
+
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
357
|
+
};
|
|
340
358
|
const documentActions = await signActions([
|
|
341
|
-
createDocumentAction(
|
|
342
|
-
model: document.header.documentType,
|
|
343
|
-
version: 0,
|
|
344
|
-
documentId: document.header.id,
|
|
345
|
-
signing: {
|
|
346
|
-
signature: document.header.id,
|
|
347
|
-
publicKey: document.header.sig.publicKey,
|
|
348
|
-
nonce: document.header.sig.nonce,
|
|
349
|
-
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
350
|
-
documentType: document.header.documentType
|
|
351
|
-
},
|
|
352
|
-
slug: document.header.slug,
|
|
353
|
-
name: document.header.name,
|
|
354
|
-
branch: document.header.branch,
|
|
355
|
-
meta: document.header.meta,
|
|
356
|
-
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
357
|
-
}),
|
|
359
|
+
createDocumentAction(createInput),
|
|
358
360
|
upgradeDocumentAction({
|
|
359
361
|
documentId: document.header.id,
|
|
360
362
|
model: document.header.documentType,
|
|
@@ -502,8 +504,7 @@ var DriveClient = class {
|
|
|
502
504
|
return {
|
|
503
505
|
results: filtered.slice(startIndex, endIndex),
|
|
504
506
|
options: effective,
|
|
505
|
-
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
|
|
506
|
-
totalCount: filtered.length
|
|
507
|
+
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
|
|
507
508
|
};
|
|
508
509
|
}
|
|
509
510
|
/**
|
|
@@ -514,7 +515,7 @@ var DriveClient = class {
|
|
|
514
515
|
async runJobs(jobs, signal) {
|
|
515
516
|
const batchResult = await this.reactor.executeBatch({ jobs }, signal);
|
|
516
517
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
|
|
517
|
-
for (const job of completedJobs) if (job.status ===
|
|
518
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw this.toError(job);
|
|
518
519
|
return completedJobs;
|
|
519
520
|
}
|
|
520
521
|
toError(job) {
|
|
@@ -555,10 +556,10 @@ var DriveClient = class {
|
|
|
555
556
|
dependsOn: ["relationship"]
|
|
556
557
|
}] }, signal);
|
|
557
558
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
|
|
558
|
-
for (const job of completedJobs) if (job.status ===
|
|
559
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
|
|
559
560
|
const deleteJob = await this.reactor.deleteDocument(fileId, this.signer, signal);
|
|
560
561
|
const deleteCompleted = await this.client.waitForJob(deleteJob, signal);
|
|
561
|
-
if (deleteCompleted.status ===
|
|
562
|
+
if (deleteCompleted.status === "FAILED") throw new Error(deleteCompleted.error?.message);
|
|
562
563
|
}
|
|
563
564
|
};
|
|
564
565
|
//#endregion
|
|
@@ -569,13 +570,15 @@ var DriveClient = class {
|
|
|
569
570
|
* Only READ_READY and FAILED are truly terminal.
|
|
570
571
|
*/
|
|
571
572
|
function isTerminalStatus(status) {
|
|
572
|
-
return status ===
|
|
573
|
+
return status === "READ_READY" || status === "FAILED";
|
|
573
574
|
}
|
|
574
575
|
/**
|
|
575
576
|
* Event-driven implementation of IJobAwaiter.
|
|
576
577
|
* Subscribes to operation events to detect job completion without polling.
|
|
577
578
|
*/
|
|
578
579
|
var JobAwaiter = class {
|
|
580
|
+
eventBus;
|
|
581
|
+
getJobStatus;
|
|
579
582
|
pendingJobs = /* @__PURE__ */ new Map();
|
|
580
583
|
unsubscribers = [];
|
|
581
584
|
constructor(eventBus, getJobStatus) {
|
|
@@ -717,7 +720,54 @@ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
|
717
720
|
* would diverge permanently. The document scope carries the metadata the same
|
|
718
721
|
* argument covers. Grants gate domain-scope reads only.
|
|
719
722
|
*/
|
|
720
|
-
const ALWAYS_READABLE_SCOPES = new Set(["auth", "document"]);
|
|
723
|
+
const ALWAYS_READABLE_SCOPES = /* @__PURE__ */ new Set(["auth", "document"]);
|
|
724
|
+
const DECLARED_DOMAIN_SCOPES = ["global", "local"];
|
|
725
|
+
/**
|
|
726
|
+
* The scopes grants gate: the declared ones, and any other the document holds.
|
|
727
|
+
* A document fetched for a narrowed view holds only that view's scopes.
|
|
728
|
+
*/
|
|
729
|
+
function domainScopesOf(document) {
|
|
730
|
+
const state = document.state;
|
|
731
|
+
const held = Object.keys(state ?? {}).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
|
|
732
|
+
return [.../* @__PURE__ */ new Set([...DECLARED_DOMAIN_SCOPES, ...held])];
|
|
733
|
+
}
|
|
734
|
+
function allowsSomeDomainScope(document, readable) {
|
|
735
|
+
return domainScopesOf(document).some(readable);
|
|
736
|
+
}
|
|
737
|
+
/** A document with no state at all is not policied, so nothing withholds it. */
|
|
738
|
+
function refusesEveryDomainScope(document, readable) {
|
|
739
|
+
return document.state !== void 0 && !allowsSomeDomainScope(document, readable);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* The domain scopes the document does not hold whose read decision reads their
|
|
743
|
+
* own state. A condition's `doc.<scope>` path resolves only for that scope, so
|
|
744
|
+
* every other scope is decided on its name and the policy alone.
|
|
745
|
+
*/
|
|
746
|
+
function unheldScopesReadOnState(document) {
|
|
747
|
+
const grants = authOf(document)?.grants ?? [];
|
|
748
|
+
const onState = /* @__PURE__ */ new Set();
|
|
749
|
+
for (const grant of grants) {
|
|
750
|
+
collectStateScopes(grant.where, onState);
|
|
751
|
+
collectStateScopes(grant.principal, onState);
|
|
752
|
+
}
|
|
753
|
+
if (onState.size === 0) return [];
|
|
754
|
+
const held = document.state ?? {};
|
|
755
|
+
return domainScopesOf(document).filter((scope) => onState.has(scope) && !(scope in held));
|
|
756
|
+
}
|
|
757
|
+
function collectStateScopes(node, into) {
|
|
758
|
+
if (Array.isArray(node)) {
|
|
759
|
+
for (const child of node) collectStateScopes(child, into);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (typeof node !== "object" || node === null) return;
|
|
763
|
+
const { attr } = node;
|
|
764
|
+
if (typeof attr === "string") {
|
|
765
|
+
const [root, scope] = attr.split(".");
|
|
766
|
+
if (root === "doc" && scope) into.add(scope);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
for (const child of Object.values(node)) collectStateScopes(child, into);
|
|
770
|
+
}
|
|
721
771
|
/** What a gate does with an uninitialized policy when told nothing. */
|
|
722
772
|
const OPEN_BY_DEFAULT = { withholdUninitialized: false };
|
|
723
773
|
/**
|
|
@@ -780,8 +830,26 @@ var BareReadGate = class {
|
|
|
780
830
|
scope
|
|
781
831
|
}) === "allow");
|
|
782
832
|
}
|
|
833
|
+
prepare(document) {
|
|
834
|
+
return Promise.resolve((subject) => this.scopePredicate(document, subject));
|
|
835
|
+
}
|
|
783
836
|
};
|
|
784
837
|
/**
|
|
838
|
+
* Rethrows a failed read unless the document is confirmed not live. The read
|
|
839
|
+
* side reports absence as a plain Error, so absence is checked rather than
|
|
840
|
+
* inferred from the message, and a failed check surfaces the read's own error.
|
|
841
|
+
*/
|
|
842
|
+
async function assertAbsent(documentView, documentId, error, signal) {
|
|
843
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
844
|
+
let exists;
|
|
845
|
+
try {
|
|
846
|
+
exists = await documentView.exists([documentId], "LiveOnly", void 0, signal);
|
|
847
|
+
} catch {
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
if (exists[0]) throw error;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
785
853
|
* Answers a stream read from the document already fetched, and anything else
|
|
786
854
|
* through the read side.
|
|
787
855
|
*
|
|
@@ -791,6 +859,9 @@ var BareReadGate = class {
|
|
|
791
859
|
* list names is fetched.
|
|
792
860
|
*/
|
|
793
861
|
var SeededStateReader = class {
|
|
862
|
+
documentView;
|
|
863
|
+
seed;
|
|
864
|
+
branch;
|
|
794
865
|
constructor(documentView, seed, branch) {
|
|
795
866
|
this.documentView = documentView;
|
|
796
867
|
this.seed = seed;
|
|
@@ -812,20 +883,10 @@ var SeededStateReader = class {
|
|
|
812
883
|
scopes: [scope]
|
|
813
884
|
}, void 0, signal);
|
|
814
885
|
} catch (error) {
|
|
815
|
-
await this.
|
|
886
|
+
await assertAbsent(this.documentView, documentId, error, signal);
|
|
816
887
|
throw new DocumentNotFoundError(documentId);
|
|
817
888
|
}
|
|
818
889
|
}
|
|
819
|
-
async assertAbsent(documentId, error, signal) {
|
|
820
|
-
if (DocumentNotFoundError.isError(error)) return;
|
|
821
|
-
let exists;
|
|
822
|
-
try {
|
|
823
|
-
exists = await this.documentView.exists([documentId], DocumentExistence.LiveOnly, void 0, signal);
|
|
824
|
-
} catch {
|
|
825
|
-
throw error;
|
|
826
|
-
}
|
|
827
|
-
if (exists[0]) throw error;
|
|
828
|
-
}
|
|
829
890
|
};
|
|
830
891
|
/**
|
|
831
892
|
* Evaluates a read against the registered decision model, built at the stream
|
|
@@ -841,6 +902,12 @@ var SeededStateReader = class {
|
|
|
841
902
|
* from state no commit ever invalidates.
|
|
842
903
|
*/
|
|
843
904
|
var ModelReadGate = class {
|
|
905
|
+
model;
|
|
906
|
+
documentView;
|
|
907
|
+
servesGroups;
|
|
908
|
+
operationIndex;
|
|
909
|
+
logger;
|
|
910
|
+
options;
|
|
844
911
|
constructor(model, documentView, servesGroups, operationIndex, logger, options = OPEN_BY_DEFAULT) {
|
|
845
912
|
this.model = model;
|
|
846
913
|
this.documentView = documentView;
|
|
@@ -855,10 +922,18 @@ var ModelReadGate = class {
|
|
|
855
922
|
* group's other scopes are its own business and stay behind its own grants.
|
|
856
923
|
*/
|
|
857
924
|
async scopePredicate(document, subject, branch, signal) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
925
|
+
return (await this.prepare(document, branch, signal))(subject);
|
|
926
|
+
}
|
|
927
|
+
async prepare(document, branch, signal) {
|
|
928
|
+
const policy = await this.ownPolicy(document, branch, signal);
|
|
929
|
+
if (!this.servesGroup(document)) return (subject) => Promise.resolve(policy(subject));
|
|
930
|
+
const referencers = this.referencerReads(document.header.id, signal);
|
|
931
|
+
return async (subject) => {
|
|
932
|
+
const own = policy(subject);
|
|
933
|
+
if (own(GROUP_MEMBERSHIP_SCOPE)) return own;
|
|
934
|
+
const audience = await this.servesGroupTo(subject, referencers);
|
|
935
|
+
return (scope) => own(scope) || audience && scope === GROUP_MEMBERSHIP_SCOPE;
|
|
936
|
+
};
|
|
862
937
|
}
|
|
863
938
|
/**
|
|
864
939
|
* Whether the subject is served this group because a policy names it.
|
|
@@ -892,11 +967,8 @@ var ModelReadGate = class {
|
|
|
892
967
|
* running with nobody awaiting it, which is where unhandled rejections come
|
|
893
968
|
* from.
|
|
894
969
|
*/
|
|
895
|
-
async servesGroupTo(
|
|
896
|
-
|
|
897
|
-
const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
|
|
898
|
-
const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
|
|
899
|
-
if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
|
|
970
|
+
async servesGroupTo(subject, referencers) {
|
|
971
|
+
const examined = await referencers.examined();
|
|
900
972
|
const walk = {
|
|
901
973
|
next: 0,
|
|
902
974
|
served: false,
|
|
@@ -907,7 +979,7 @@ var ModelReadGate = class {
|
|
|
907
979
|
const referencerId = examined[walk.next++];
|
|
908
980
|
let serves = false;
|
|
909
981
|
try {
|
|
910
|
-
serves = await
|
|
982
|
+
serves = (await referencers.serves(referencerId))(subject);
|
|
911
983
|
} catch (error) {
|
|
912
984
|
walk.failure ??= Error.isError(error) ? error : /* @__PURE__ */ new Error(`Probing referencer ${referencerId} failed`);
|
|
913
985
|
}
|
|
@@ -918,21 +990,44 @@ var ModelReadGate = class {
|
|
|
918
990
|
if (!walk.served && walk.failure) throw walk.failure;
|
|
919
991
|
return walk.served;
|
|
920
992
|
}
|
|
993
|
+
/** Each read made once, however many subjects the walk runs for. */
|
|
994
|
+
referencerReads(groupId, signal) {
|
|
995
|
+
let examined;
|
|
996
|
+
const serves = /* @__PURE__ */ new Map();
|
|
997
|
+
return {
|
|
998
|
+
examined: () => examined ??= this.examinedReferencers(groupId, signal),
|
|
999
|
+
serves: (referencerId) => {
|
|
1000
|
+
let read = serves.get(referencerId);
|
|
1001
|
+
if (!read) {
|
|
1002
|
+
read = this.servesThrough(referencerId, signal);
|
|
1003
|
+
serves.set(referencerId, read);
|
|
1004
|
+
}
|
|
1005
|
+
return read;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
async examinedReferencers(groupId, signal) {
|
|
1010
|
+
if (!this.operationIndex) return [];
|
|
1011
|
+
const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
|
|
1012
|
+
const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
|
|
1013
|
+
if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
|
|
1014
|
+
return examined;
|
|
1015
|
+
}
|
|
921
1016
|
/**
|
|
922
|
-
* Whether one referencing document serves
|
|
1017
|
+
* Whether one referencing document serves a subject any domain scope. A
|
|
923
1018
|
* referencer this replica does not hold serves nothing, which fails closed
|
|
924
1019
|
* the same way a group it does not hold does.
|
|
925
1020
|
*/
|
|
926
|
-
async servesThrough(referencerId,
|
|
1021
|
+
async servesThrough(referencerId, signal) {
|
|
927
1022
|
let referencer;
|
|
928
1023
|
try {
|
|
929
1024
|
referencer = await this.documentView.get(referencerId, { branch: GROUP_BRANCH }, void 0, signal);
|
|
930
1025
|
} catch {
|
|
931
|
-
return false;
|
|
1026
|
+
return () => false;
|
|
932
1027
|
}
|
|
933
|
-
if (this.servesGroup(referencer)) return false;
|
|
934
|
-
const
|
|
935
|
-
return
|
|
1028
|
+
if (this.servesGroup(referencer)) return () => false;
|
|
1029
|
+
const policy = await this.ownPolicy(referencer, GROUP_BRANCH, signal);
|
|
1030
|
+
return (subject) => allowsSomeDomainScope(referencer, policy(subject));
|
|
936
1031
|
}
|
|
937
1032
|
servesGroup(document) {
|
|
938
1033
|
return this.servesGroups && this.operationIndex !== void 0 && document.header.documentType === groupDocumentType;
|
|
@@ -950,9 +1045,12 @@ var ModelReadGate = class {
|
|
|
950
1045
|
* publish an unpoliced group it names: the referencer walk asks this question
|
|
951
1046
|
* of the referencing document, whose real policy answers it.
|
|
952
1047
|
*/
|
|
953
|
-
async
|
|
1048
|
+
async ownPolicy(document, branch, signal) {
|
|
954
1049
|
const auth = authOf(document);
|
|
955
|
-
if (!auth || !auth.version)
|
|
1050
|
+
if (!auth || !auth.version) {
|
|
1051
|
+
const readable = this.options.withholdUninitialized ? (scope) => ALWAYS_READABLE_SCOPES.has(scope) : () => true;
|
|
1052
|
+
return () => readable;
|
|
1053
|
+
}
|
|
956
1054
|
const target = {
|
|
957
1055
|
documentId: document.header.id,
|
|
958
1056
|
branch
|
|
@@ -960,7 +1058,7 @@ var ModelReadGate = class {
|
|
|
960
1058
|
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), this.model, target, signal);
|
|
961
1059
|
const definition = this.model(target);
|
|
962
1060
|
const scopeStates = document.state ?? {};
|
|
963
|
-
return (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
|
|
1061
|
+
return (subject) => (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
|
|
964
1062
|
verb: "read",
|
|
965
1063
|
scope
|
|
966
1064
|
}, {
|
|
@@ -970,6 +1068,78 @@ var ModelReadGate = class {
|
|
|
970
1068
|
}
|
|
971
1069
|
};
|
|
972
1070
|
//#endregion
|
|
1071
|
+
//#region src/client/event-reads.ts
|
|
1072
|
+
/** One change event's reads, shared by every subscription it reaches. */
|
|
1073
|
+
var EventReads = class {
|
|
1074
|
+
reactor;
|
|
1075
|
+
documentView;
|
|
1076
|
+
readGate;
|
|
1077
|
+
documents = /* @__PURE__ */ new Map();
|
|
1078
|
+
existence = /* @__PURE__ */ new Map();
|
|
1079
|
+
prepared = /* @__PURE__ */ new WeakMap();
|
|
1080
|
+
constructor(reactor, documentView, readGate) {
|
|
1081
|
+
this.reactor = reactor;
|
|
1082
|
+
this.documentView = documentView;
|
|
1083
|
+
this.readGate = readGate;
|
|
1084
|
+
}
|
|
1085
|
+
get(id, view) {
|
|
1086
|
+
const key = JSON.stringify([
|
|
1087
|
+
id,
|
|
1088
|
+
view?.branch,
|
|
1089
|
+
view?.scopes
|
|
1090
|
+
]);
|
|
1091
|
+
let read = this.documents.get(key);
|
|
1092
|
+
if (!read) {
|
|
1093
|
+
read = this.reactor.get(id, view);
|
|
1094
|
+
this.documents.set(key, read);
|
|
1095
|
+
}
|
|
1096
|
+
return read;
|
|
1097
|
+
}
|
|
1098
|
+
exists(ids, existence) {
|
|
1099
|
+
const key = JSON.stringify([ids, existence]);
|
|
1100
|
+
let read = this.existence.get(key);
|
|
1101
|
+
if (!read) {
|
|
1102
|
+
read = this.documentView.exists(ids, existence);
|
|
1103
|
+
this.existence.set(key, read);
|
|
1104
|
+
}
|
|
1105
|
+
return read;
|
|
1106
|
+
}
|
|
1107
|
+
async scopePredicate(document, subject, branch) {
|
|
1108
|
+
if (!this.readGate.prepare) return this.readGate.scopePredicate(document, subject, branch);
|
|
1109
|
+
let byBranch = this.prepared.get(document);
|
|
1110
|
+
if (!byBranch) {
|
|
1111
|
+
byBranch = /* @__PURE__ */ new Map();
|
|
1112
|
+
this.prepared.set(document, byBranch);
|
|
1113
|
+
}
|
|
1114
|
+
let prepared = byBranch.get(branch);
|
|
1115
|
+
if (!prepared) {
|
|
1116
|
+
prepared = this.readGate.prepare(document, branch);
|
|
1117
|
+
byBranch.set(branch, prepared);
|
|
1118
|
+
}
|
|
1119
|
+
return (await prepared)(subject);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
var EventReadsSource = class {
|
|
1123
|
+
reactor;
|
|
1124
|
+
documentView;
|
|
1125
|
+
readGate;
|
|
1126
|
+
current;
|
|
1127
|
+
constructor(reactor, documentView, readGate) {
|
|
1128
|
+
this.reactor = reactor;
|
|
1129
|
+
this.documentView = documentView;
|
|
1130
|
+
this.readGate = readGate;
|
|
1131
|
+
}
|
|
1132
|
+
forEvent() {
|
|
1133
|
+
if (!this.current) {
|
|
1134
|
+
this.current = new EventReads(this.reactor, this.documentView, this.readGate);
|
|
1135
|
+
queueMicrotask(() => {
|
|
1136
|
+
this.current = void 0;
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return this.current;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
//#endregion
|
|
973
1143
|
//#region src/client/util.ts
|
|
974
1144
|
function authSubjectFromSigner(signer) {
|
|
975
1145
|
return {
|
|
@@ -980,10 +1150,23 @@ function authSubjectFromSigner(signer) {
|
|
|
980
1150
|
function withAuthScope(view) {
|
|
981
1151
|
if (view?.scopes && view.scopes.length > 0) return {
|
|
982
1152
|
...view,
|
|
983
|
-
scopes: [
|
|
1153
|
+
scopes: [.../* @__PURE__ */ new Set([...view.scopes, "auth"])]
|
|
984
1154
|
};
|
|
985
1155
|
return view;
|
|
986
1156
|
}
|
|
1157
|
+
function withAllScopes(view) {
|
|
1158
|
+
if (!view?.scopes) return view;
|
|
1159
|
+
const { scopes: _narrowed, ...whole } = view;
|
|
1160
|
+
return whole;
|
|
1161
|
+
}
|
|
1162
|
+
function narrowedScopes(view) {
|
|
1163
|
+
if (!view?.scopes || view.scopes.length === 0) return;
|
|
1164
|
+
return /* @__PURE__ */ new Set([
|
|
1165
|
+
"document",
|
|
1166
|
+
"auth",
|
|
1167
|
+
...view.scopes
|
|
1168
|
+
]);
|
|
1169
|
+
}
|
|
987
1170
|
/**
|
|
988
1171
|
* Drops the scopes the predicate refuses. The predicate is resolved by the read
|
|
989
1172
|
* gate, which decides against the whole policy once per document; this only
|
|
@@ -1056,6 +1239,7 @@ var ReactorClient = class {
|
|
|
1056
1239
|
documentIndexer;
|
|
1057
1240
|
documentView;
|
|
1058
1241
|
readGate;
|
|
1242
|
+
eventReads;
|
|
1059
1243
|
actionEvaluation;
|
|
1060
1244
|
createSignaturePolicy;
|
|
1061
1245
|
drives;
|
|
@@ -1068,33 +1252,12 @@ var ReactorClient = class {
|
|
|
1068
1252
|
this.documentIndexer = documentIndexer;
|
|
1069
1253
|
this.documentView = documentView;
|
|
1070
1254
|
this.readGate = readGate;
|
|
1255
|
+
this.eventReads = new EventReadsSource(reactor, documentView, readGate);
|
|
1071
1256
|
this.actionEvaluation = actionEvaluation;
|
|
1072
1257
|
this.createSignaturePolicy = createSignaturePolicy;
|
|
1073
1258
|
this.drives = new DriveClient(this, logger, reactor, signer);
|
|
1074
1259
|
this.logger.verbose("ReactorClient initialized");
|
|
1075
1260
|
}
|
|
1076
|
-
readSubject(subject) {
|
|
1077
|
-
return subject ?? authSubjectFromSigner(this.signer);
|
|
1078
|
-
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Which scopes of one document the subject may read. Resolved once per
|
|
1081
|
-
* document, so the gate builds its model once however many scopes are then
|
|
1082
|
-
* tested, and the filtering itself stays synchronous.
|
|
1083
|
-
*/
|
|
1084
|
-
readableScopes(document, view, signal) {
|
|
1085
|
-
return this.readGate.scopePredicate(document, this.readSubject(view?.subject), view?.branch ?? "main", signal);
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* One document, filtered to the scopes the subject may read. Every method
|
|
1089
|
-
* that hands a document back goes through here, including the ones that
|
|
1090
|
-
* follow a write: a document returned from a mutation is a read like any
|
|
1091
|
-
* other, and returning it whole served scopes the same subject would be
|
|
1092
|
-
* refused by `get`. Its author still sees what it wrote, because an allow on
|
|
1093
|
-
* execute confers read of that scope.
|
|
1094
|
-
*/
|
|
1095
|
-
async gateDocument(document, view, signal) {
|
|
1096
|
-
return filterReadableScopes(document, await this.readableScopes(document, view, signal));
|
|
1097
|
-
}
|
|
1098
1261
|
/**
|
|
1099
1262
|
* Retrieves a list of document model modules.
|
|
1100
1263
|
*/
|
|
@@ -1160,7 +1323,7 @@ var ReactorClient = class {
|
|
|
1160
1323
|
/** True when the id is taken; a soft-deleted document's id is still taken. */
|
|
1161
1324
|
async isDocumentIdTaken(documentId, signal) {
|
|
1162
1325
|
this.logger.verbose("isDocumentIdTaken(@documentId)", documentId);
|
|
1163
|
-
return (await this.documentView.exists([documentId],
|
|
1326
|
+
return (await this.documentView.exists([documentId], "IncludingDeleted", void 0, signal))[0];
|
|
1164
1327
|
}
|
|
1165
1328
|
/**
|
|
1166
1329
|
* Retrieves operations for a document
|
|
@@ -1200,31 +1363,6 @@ var ReactorClient = class {
|
|
|
1200
1363
|
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
1201
1364
|
};
|
|
1202
1365
|
}
|
|
1203
|
-
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
1204
|
-
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
1205
|
-
const allOperations = [];
|
|
1206
|
-
const activeCursors = {};
|
|
1207
|
-
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
1208
|
-
if (!canRead(scopeName)) continue;
|
|
1209
|
-
const scopeView = {
|
|
1210
|
-
...view,
|
|
1211
|
-
scopes: [scopeName]
|
|
1212
|
-
};
|
|
1213
|
-
const scopePaging = {
|
|
1214
|
-
cursor,
|
|
1215
|
-
limit: paging.limit
|
|
1216
|
-
};
|
|
1217
|
-
const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
|
|
1218
|
-
allOperations.push(...scopeResult.results);
|
|
1219
|
-
if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
|
|
1220
|
-
}
|
|
1221
|
-
allOperations.sort((a, b) => a.index - b.index);
|
|
1222
|
-
return {
|
|
1223
|
-
results: allOperations,
|
|
1224
|
-
options: paging,
|
|
1225
|
-
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
1366
|
/**
|
|
1229
1367
|
* Retrieves outgoing relationships of a given type from a source document.
|
|
1230
1368
|
*/
|
|
@@ -1278,68 +1416,24 @@ var ReactorClient = class {
|
|
|
1278
1416
|
return this.gateEdges(edges, "sourceId", view, signal);
|
|
1279
1417
|
}
|
|
1280
1418
|
/**
|
|
1281
|
-
* Drops the edges whose far-end document the subject may read no domain scope
|
|
1282
|
-
* of. An edge is withheld whole rather than stripped of its metadata: the
|
|
1283
|
-
* document-shaped relationship reads already answer with the far end stripped
|
|
1284
|
-
* to the scopes the gate allows, so the far end's existence is disclosed
|
|
1285
|
-
* either way, but an edge's metadata is content about the pair that the far
|
|
1286
|
-
* end's own reads would refuse. An edge to a far end stripped to nothing
|
|
1287
|
-
* therefore carries content past a refusal, and there is no useful shell to
|
|
1288
|
-
* hand back in its place.
|
|
1289
|
-
*
|
|
1290
|
-
* `nextCursor` and `options` are left as the underlying stream reported them,
|
|
1291
|
-
* because a caller must feed them back to resume from the right position. A
|
|
1292
|
-
* gated page can therefore be shorter than the limit it asked for.
|
|
1293
|
-
*/
|
|
1294
|
-
async gateEdges(page, farEnd, view, signal) {
|
|
1295
|
-
const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
|
|
1296
|
-
if (ids.length === 0) return page;
|
|
1297
|
-
const farEndView = {
|
|
1298
|
-
subject: view?.subject,
|
|
1299
|
-
branch: view?.branch
|
|
1300
|
-
};
|
|
1301
|
-
const documents = await this.reactor.find({ ids }, farEndView, {
|
|
1302
|
-
cursor: "0",
|
|
1303
|
-
limit: ids.length
|
|
1304
|
-
}, void 0, signal);
|
|
1305
|
-
const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
|
|
1306
|
-
const allows = await this.readableScopes(doc, view, signal);
|
|
1307
|
-
const domainScopes = Object.keys(doc.state).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
|
|
1308
|
-
return [doc.header.id, domainScopes.length === 0 || domainScopes.some(allows)];
|
|
1309
|
-
})));
|
|
1310
|
-
const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
|
|
1311
|
-
if (results.length === page.results.length) return page;
|
|
1312
|
-
const nextPage = page.next;
|
|
1313
|
-
return {
|
|
1314
|
-
...page,
|
|
1315
|
-
results,
|
|
1316
|
-
next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* One relationship edge, or undefined when it does not exist. A point lookup:
|
|
1321
|
-
* the pair is filtered in SQL rather than scanned out of the source's edge
|
|
1322
|
-
* list, which on a drive with thousands of children is the difference between
|
|
1323
|
-
* one query and dozens.
|
|
1324
|
-
*/
|
|
1325
|
-
async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
|
|
1326
|
-
const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
|
|
1327
|
-
const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
|
|
1328
|
-
return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
|
|
1329
|
-
cursor: "0",
|
|
1330
|
-
limit: 1
|
|
1331
|
-
}, void 0, signal)).results[0];
|
|
1332
|
-
}
|
|
1333
|
-
/**
|
|
1334
1419
|
* Filters documents by criteria and returns a list of them
|
|
1335
1420
|
*/
|
|
1336
1421
|
async find(search, view, paging, signal) {
|
|
1337
1422
|
this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
|
|
1338
1423
|
const results = await this.reactor.find(search, withAuthScope(view), paging, void 0, signal);
|
|
1339
|
-
return
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1424
|
+
return this.gateListing(results, view, signal);
|
|
1425
|
+
}
|
|
1426
|
+
/** Whether `find` would serve the document; false when it is absent. */
|
|
1427
|
+
async isServed(identifier, view, signal) {
|
|
1428
|
+
this.logger.verbose("isServed(@identifier, @view)", identifier, view);
|
|
1429
|
+
let document;
|
|
1430
|
+
try {
|
|
1431
|
+
document = await this.reactor.getByIdOrSlug(identifier, withAuthScope(view), void 0, signal);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
await assertAbsent(this.documentView, identifier, error, signal);
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
return await this.gateListed(document, view, signal) !== void 0;
|
|
1343
1437
|
}
|
|
1344
1438
|
/**
|
|
1345
1439
|
* Predicts the admission verdict for each candidate. See
|
|
@@ -1380,39 +1474,6 @@ var ReactorClient = class {
|
|
|
1380
1474
|
anyDenied: allowed < candidates.length
|
|
1381
1475
|
};
|
|
1382
1476
|
}
|
|
1383
|
-
/**
|
|
1384
|
-
* The decision model for one target document, built at its stream heads.
|
|
1385
|
-
*
|
|
1386
|
-
* The document is fetched unfiltered, because the policy is what decides:
|
|
1387
|
-
* reading it through the read gate would withhold the very scopes the
|
|
1388
|
-
* decision is about. A deleted document is served at its deletion boundary,
|
|
1389
|
-
* which is what lets the model refuse an execute against it -- authEnforcement
|
|
1390
|
-
* requires documentDecisions, so that read is available whenever this runs.
|
|
1391
|
-
*
|
|
1392
|
-
* Reading past the gate discloses nothing a submit does not. The `auth` and
|
|
1393
|
-
* `document` scopes are readable by every holder, so a verdict resting on the
|
|
1394
|
-
* policy alone is one the caller could compute unaided; and a verdict resting
|
|
1395
|
-
* on a conditional grant reads the executing scope's state exactly as
|
|
1396
|
-
* admission reads it, so the answer here is what submitting and being refused
|
|
1397
|
-
* would have revealed anyway.
|
|
1398
|
-
*
|
|
1399
|
-
* The append condition the build records is dropped. It guards a write, and
|
|
1400
|
-
* this makes none; reproducing it is also what the preflight cannot do, which
|
|
1401
|
-
* is why the answer is a prediction.
|
|
1402
|
-
*/
|
|
1403
|
-
async buildEvaluationTarget(config, documentId, branch, signal) {
|
|
1404
|
-
const document = await this.documentView.get(documentId, { branch }, void 0, signal);
|
|
1405
|
-
const target = {
|
|
1406
|
-
documentId,
|
|
1407
|
-
branch
|
|
1408
|
-
};
|
|
1409
|
-
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
|
|
1410
|
-
return {
|
|
1411
|
-
definition: config.model(target),
|
|
1412
|
-
model: built.model,
|
|
1413
|
-
scopeStates: document.state ?? {}
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
1477
|
getCreateSignaturePolicy() {
|
|
1417
1478
|
return Promise.resolve(this.createSignaturePolicy);
|
|
1418
1479
|
}
|
|
@@ -1423,7 +1484,7 @@ var ReactorClient = class {
|
|
|
1423
1484
|
this.logger.verbose("create(@id, @parentIdentifier)", document.header.id, parentIdentifier);
|
|
1424
1485
|
const documentId = document.header.id;
|
|
1425
1486
|
const branch = document.header.branch || "main";
|
|
1426
|
-
const
|
|
1487
|
+
const createInput = {
|
|
1427
1488
|
model: document.header.documentType,
|
|
1428
1489
|
version: 0,
|
|
1429
1490
|
documentId,
|
|
@@ -1439,7 +1500,8 @@ var ReactorClient = class {
|
|
|
1439
1500
|
branch: document.header.branch,
|
|
1440
1501
|
meta: document.header.meta,
|
|
1441
1502
|
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
1442
|
-
}
|
|
1503
|
+
};
|
|
1504
|
+
const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
|
|
1443
1505
|
documentId,
|
|
1444
1506
|
model: document.header.documentType,
|
|
1445
1507
|
fromVersion: 0,
|
|
@@ -1473,7 +1535,7 @@ var ReactorClient = class {
|
|
|
1473
1535
|
}
|
|
1474
1536
|
const batchResult = await this.reactor.executeBatch({ jobs }, signal);
|
|
1475
1537
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
|
|
1476
|
-
for (const job of completedJobs) if (job.status ===
|
|
1538
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
|
|
1477
1539
|
const created = await this.reactor.get(documentId);
|
|
1478
1540
|
return this.gateDocument(created, void 0, signal);
|
|
1479
1541
|
}
|
|
@@ -1521,22 +1583,26 @@ var ReactorClient = class {
|
|
|
1521
1583
|
const branch = document.header.branch || "main";
|
|
1522
1584
|
const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
|
|
1523
1585
|
let targetVersion = toVersion;
|
|
1524
|
-
if (targetVersion === void 0)
|
|
1586
|
+
if (targetVersion === void 0) {
|
|
1587
|
+
const module = await this.getDocumentModelModule(documentType);
|
|
1588
|
+
targetVersion = normalizeDocumentModelVersion(module.version);
|
|
1589
|
+
}
|
|
1525
1590
|
if (targetVersion === fromVersion) return this.gateDocument(document, { branch }, signal);
|
|
1526
1591
|
if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
|
|
1527
|
-
const
|
|
1592
|
+
const action = upgradeDocumentAction({
|
|
1528
1593
|
documentId,
|
|
1529
1594
|
model: documentType,
|
|
1530
1595
|
fromVersion,
|
|
1531
1596
|
toVersion: targetVersion,
|
|
1532
1597
|
revision: { ...document.header.revision }
|
|
1533
|
-
})
|
|
1598
|
+
});
|
|
1599
|
+
const signedActions = await signActions([action], this.signer, {
|
|
1534
1600
|
documentId,
|
|
1535
1601
|
branch
|
|
1536
1602
|
}, signal);
|
|
1537
1603
|
const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1538
1604
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1539
|
-
if (completedJob.status !==
|
|
1605
|
+
if (completedJob.status !== "FAILED") {
|
|
1540
1606
|
const upgraded = await this.reactor.getByIdOrSlug(documentId, { branch }, completedJob.consistencyToken, signal);
|
|
1541
1607
|
return this.gateDocument(upgraded, { branch }, signal);
|
|
1542
1608
|
}
|
|
@@ -1570,7 +1636,7 @@ var ReactorClient = class {
|
|
|
1570
1636
|
}, signal);
|
|
1571
1637
|
const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1572
1638
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1573
|
-
if (completedJob.status ===
|
|
1639
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1574
1640
|
const view = {
|
|
1575
1641
|
branch,
|
|
1576
1642
|
subject
|
|
@@ -1590,18 +1656,6 @@ var ReactorClient = class {
|
|
|
1590
1656
|
}, signal);
|
|
1591
1657
|
return this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1592
1658
|
}
|
|
1593
|
-
/**
|
|
1594
|
-
* The id a write on `identifier` is stored under, which its signatures bind.
|
|
1595
|
-
* A create names its own id, and an id no slug maps to is taken as given, so
|
|
1596
|
-
* a document still in flight resolves to itself.
|
|
1597
|
-
*/
|
|
1598
|
-
async resolveWriteTarget(identifier, branch, actions, signal) {
|
|
1599
|
-
if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
|
|
1600
|
-
const view = { branch };
|
|
1601
|
-
const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
|
|
1602
|
-
if (bySlug === void 0 || bySlug === identifier) return identifier;
|
|
1603
|
-
return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
|
|
1604
|
-
}
|
|
1605
1659
|
async executeBatch(request, signal) {
|
|
1606
1660
|
this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
|
|
1607
1661
|
const signedJobs = await Promise.all(request.jobs.map(async (job) => {
|
|
@@ -1617,7 +1671,7 @@ var ReactorClient = class {
|
|
|
1617
1671
|
}));
|
|
1618
1672
|
const batchResult = await this.reactor.executeBatch({ jobs: signedJobs }, signal);
|
|
1619
1673
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
|
|
1620
|
-
for (const job of completedJobs) if (job.status ===
|
|
1674
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
|
|
1621
1675
|
return batchResult;
|
|
1622
1676
|
}
|
|
1623
1677
|
/**
|
|
@@ -1642,7 +1696,7 @@ var ReactorClient = class {
|
|
|
1642
1696
|
this.logger.verbose("addRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @metadata, @branch)", sourceIdentifier, targetIdentifier, relationshipType, metadata, branch);
|
|
1643
1697
|
const jobInfo = await this.reactor.addRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
|
|
1644
1698
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1645
|
-
if (completedJob.status ===
|
|
1699
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1646
1700
|
const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
|
|
1647
1701
|
return this.gateDocument(result, { branch }, signal);
|
|
1648
1702
|
}
|
|
@@ -1654,7 +1708,7 @@ var ReactorClient = class {
|
|
|
1654
1708
|
if (!await this.readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, { branch }, signal)) throw new RelationshipNotFoundError(sourceIdentifier, targetIdentifier, relationshipType);
|
|
1655
1709
|
const jobInfo = await this.reactor.updateRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
|
|
1656
1710
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1657
|
-
if (completedJob.status ===
|
|
1711
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1658
1712
|
const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
|
|
1659
1713
|
return this.gateDocument(result, { branch }, signal);
|
|
1660
1714
|
}
|
|
@@ -1665,7 +1719,7 @@ var ReactorClient = class {
|
|
|
1665
1719
|
this.logger.verbose("removeRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @branch)", sourceIdentifier, targetIdentifier, relationshipType, branch);
|
|
1666
1720
|
const jobInfo = await this.reactor.removeRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
|
|
1667
1721
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1668
|
-
if (completedJob.status ===
|
|
1722
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1669
1723
|
const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
|
|
1670
1724
|
return this.gateDocument(result, { branch }, signal);
|
|
1671
1725
|
}
|
|
@@ -1677,10 +1731,10 @@ var ReactorClient = class {
|
|
|
1677
1731
|
const metadata = (await this.readRelationshipEdge(sourceParentIdentifier, targetIdentifier, relationshipType, { branch }, signal))?.metadata;
|
|
1678
1732
|
const removeJobInfo = await this.reactor.removeRelationship(sourceParentIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
|
|
1679
1733
|
const removeCompletedJob = await this.waitForJob(removeJobInfo, signal);
|
|
1680
|
-
if (removeCompletedJob.status ===
|
|
1734
|
+
if (removeCompletedJob.status === "FAILED") throw new Error(removeCompletedJob.error?.message);
|
|
1681
1735
|
const addJobInfo = await this.reactor.addRelationship(targetParentIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
|
|
1682
1736
|
const addCompletedJob = await this.waitForJob(addJobInfo, signal);
|
|
1683
|
-
if (addCompletedJob.status ===
|
|
1737
|
+
if (addCompletedJob.status === "FAILED") throw new Error(addCompletedJob.error?.message);
|
|
1684
1738
|
const sourceResult = await this.reactor.getByIdOrSlug(sourceParentIdentifier, { branch }, removeCompletedJob.consistencyToken, signal);
|
|
1685
1739
|
const targetResult = await this.reactor.getByIdOrSlug(targetParentIdentifier, { branch }, addCompletedJob.consistencyToken, signal);
|
|
1686
1740
|
return {
|
|
@@ -1694,7 +1748,7 @@ var ReactorClient = class {
|
|
|
1694
1748
|
const completedJobs = await Promise.all(Object.entries(result.jobs).map(async ([key, jobInfo]) => {
|
|
1695
1749
|
return [key, await this.waitForJob(jobInfo, signal)];
|
|
1696
1750
|
}));
|
|
1697
|
-
for (const [, completedJob] of completedJobs) if (completedJob.status ===
|
|
1751
|
+
for (const [, completedJob] of completedJobs) if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1698
1752
|
return { jobs: Object.fromEntries(completedJobs) };
|
|
1699
1753
|
}
|
|
1700
1754
|
/**
|
|
@@ -1703,8 +1757,8 @@ var ReactorClient = class {
|
|
|
1703
1757
|
async deleteDocument(identifier, propagate, signal) {
|
|
1704
1758
|
this.logger.verbose("deleteDocument(@identifier, @propagate)", identifier, propagate);
|
|
1705
1759
|
const jobs = [];
|
|
1706
|
-
if (propagate ===
|
|
1707
|
-
const toDelete = new Set([identifier]);
|
|
1760
|
+
if (propagate === "cascade") {
|
|
1761
|
+
const toDelete = /* @__PURE__ */ new Set([identifier]);
|
|
1708
1762
|
let changed = true;
|
|
1709
1763
|
while (changed) {
|
|
1710
1764
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
@@ -1728,7 +1782,7 @@ var ReactorClient = class {
|
|
|
1728
1782
|
const jobInfo = await this.reactor.deleteDocument(identifier, this.signer, signal);
|
|
1729
1783
|
jobs.push(jobInfo);
|
|
1730
1784
|
const completedJobs = await Promise.all(jobs.map((job) => this.waitForJob(job, signal)));
|
|
1731
|
-
for (const completedJob of completedJobs) if (completedJob.status ===
|
|
1785
|
+
for (const completedJob of completedJobs) if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1732
1786
|
}
|
|
1733
1787
|
/**
|
|
1734
1788
|
* Deletes documents and waits for completion
|
|
@@ -1758,7 +1812,13 @@ var ReactorClient = class {
|
|
|
1758
1812
|
*/
|
|
1759
1813
|
subscribe(search, callback, view) {
|
|
1760
1814
|
this.logger.verbose("subscribe(@search, @view)", search, view);
|
|
1761
|
-
const
|
|
1815
|
+
const served = async (type, documents, reads) => {
|
|
1816
|
+
const documentsServed = await this.gateServedAll(documents, view, void 0, reads);
|
|
1817
|
+
return documentsServed.length > 0 ? {
|
|
1818
|
+
type,
|
|
1819
|
+
documents: documentsServed
|
|
1820
|
+
} : void 0;
|
|
1821
|
+
};
|
|
1762
1822
|
let disposed = false;
|
|
1763
1823
|
let delivering = Promise.resolve();
|
|
1764
1824
|
/**
|
|
@@ -1772,49 +1832,46 @@ var ReactorClient = class {
|
|
|
1772
1832
|
* gated is withheld, because serving it unfiltered would leak the scopes
|
|
1773
1833
|
* the gate did not clear -- but it is logged rather than swallowed, since
|
|
1774
1834
|
* the gate rethrows a transient failure precisely so it is not read as a
|
|
1775
|
-
* denial.
|
|
1835
|
+
* denial. An event that resolves to undefined was withheld.
|
|
1776
1836
|
*/
|
|
1777
1837
|
const deliver = (event) => {
|
|
1778
1838
|
Promise.resolve(event).catch(() => void 0);
|
|
1779
1839
|
delivering = delivering.then(async () => {
|
|
1780
1840
|
const built = await event;
|
|
1781
|
-
if (disposed) return;
|
|
1841
|
+
if (disposed || !built) return;
|
|
1782
1842
|
callback(built);
|
|
1783
1843
|
}).catch((error) => {
|
|
1784
1844
|
this.logger.error("Subscription delivery failed for @search: @Error", { search }, error);
|
|
1785
1845
|
});
|
|
1786
1846
|
};
|
|
1787
1847
|
const unsubscribeCreated = this.subscriptionManager.onDocumentCreated((result) => {
|
|
1848
|
+
const reads = this.eventReads.forEvent();
|
|
1788
1849
|
deliver((async () => {
|
|
1789
|
-
const documents = await Promise.all(result.results.map((id) =>
|
|
1790
|
-
return
|
|
1791
|
-
type: DocumentChangeType.Created,
|
|
1792
|
-
documents: await Promise.all(documents.map(readable))
|
|
1793
|
-
};
|
|
1850
|
+
const documents = await Promise.all(result.results.map((id) => reads.get(id, withAllScopes(view))));
|
|
1851
|
+
return served("created", documents, reads);
|
|
1794
1852
|
})());
|
|
1795
1853
|
}, search);
|
|
1796
1854
|
const unsubscribeDeleted = this.subscriptionManager.onDocumentDeleted((documentIds) => {
|
|
1797
|
-
|
|
1798
|
-
|
|
1855
|
+
const reads = this.eventReads.forEvent();
|
|
1856
|
+
for (const childId of documentIds) deliver((async () => await this.servesEvery([childId], view, reads) ? {
|
|
1857
|
+
type: "deleted",
|
|
1799
1858
|
documents: [],
|
|
1800
|
-
context: { childId
|
|
1801
|
-
});
|
|
1859
|
+
context: { childId }
|
|
1860
|
+
} : void 0)());
|
|
1802
1861
|
}, search);
|
|
1803
1862
|
const unsubscribeUpdated = this.subscriptionManager.onDocumentStateUpdated((result) => {
|
|
1804
|
-
deliver((
|
|
1805
|
-
type: DocumentChangeType.Updated,
|
|
1806
|
-
documents: await Promise.all(result.results.map(readable))
|
|
1807
|
-
}))());
|
|
1863
|
+
deliver(served("updated", result.results, this.eventReads.forEvent()));
|
|
1808
1864
|
}, search, view);
|
|
1809
1865
|
const unsubscribeRelationship = this.subscriptionManager.onRelationshipChanged((parentId, childId, changeType) => {
|
|
1810
|
-
|
|
1811
|
-
|
|
1866
|
+
const reads = this.eventReads.forEvent();
|
|
1867
|
+
deliver((async () => await this.servesEvery([parentId, childId], view, reads) ? {
|
|
1868
|
+
type: changeType === "added" ? "child_added" : "child_removed",
|
|
1812
1869
|
documents: [],
|
|
1813
1870
|
context: {
|
|
1814
1871
|
parentId,
|
|
1815
1872
|
childId
|
|
1816
1873
|
}
|
|
1817
|
-
});
|
|
1874
|
+
} : void 0)());
|
|
1818
1875
|
}, search);
|
|
1819
1876
|
return () => {
|
|
1820
1877
|
disposed = true;
|
|
@@ -1824,6 +1881,225 @@ var ReactorClient = class {
|
|
|
1824
1881
|
unsubscribeRelationship();
|
|
1825
1882
|
};
|
|
1826
1883
|
}
|
|
1884
|
+
readSubject(subject) {
|
|
1885
|
+
return subject ?? authSubjectFromSigner(this.signer);
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Which scopes of one document the subject may read. Resolved once per
|
|
1889
|
+
* document, so the gate builds its model once however many scopes are then
|
|
1890
|
+
* tested, and the filtering itself stays synchronous.
|
|
1891
|
+
*/
|
|
1892
|
+
readableScopes(document, view, signal, reads) {
|
|
1893
|
+
const subject = this.readSubject(view?.subject);
|
|
1894
|
+
const branch = view?.branch ?? "main";
|
|
1895
|
+
return reads ? reads.scopePredicate(document, subject, branch) : this.readGate.scopePredicate(document, subject, branch, signal);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* One document, filtered to the scopes the subject may read. Every method
|
|
1899
|
+
* that hands a document back goes through here, including the ones that
|
|
1900
|
+
* follow a write: a document returned from a mutation is a read like any
|
|
1901
|
+
* other, and returning it whole served scopes the same subject would be
|
|
1902
|
+
* refused by `get`. Its author still sees what it wrote, because an allow on
|
|
1903
|
+
* execute confers read of that scope.
|
|
1904
|
+
*/
|
|
1905
|
+
async gateDocument(document, view, signal) {
|
|
1906
|
+
return filterReadableScopes(document, await this.readableScopes(document, view, signal));
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* One document as a listing or a feed serves it, or undefined when withheld.
|
|
1910
|
+
* The document must hold every scope, or withholding is decided on a subset;
|
|
1911
|
+
* a narrowing view is applied here instead.
|
|
1912
|
+
*/
|
|
1913
|
+
async gateServed(document, view, signal, reads) {
|
|
1914
|
+
const readable = await this.readableScopes(document, view, signal, reads);
|
|
1915
|
+
if (refusesEveryDomainScope(document, readable)) return;
|
|
1916
|
+
const narrowed = narrowedScopes(view);
|
|
1917
|
+
return filterReadableScopes(document, narrowed ? (scope) => readable(scope) && narrowed.has(scope) : readable);
|
|
1918
|
+
}
|
|
1919
|
+
async gateServedAll(documents, view, signal, reads) {
|
|
1920
|
+
return (await Promise.all(documents.map((document) => this.gateServed(document, view, signal, reads)))).filter((document) => document !== void 0);
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Gates a page and every page after it. `nextCursor` is left as the stream
|
|
1924
|
+
* reported it.
|
|
1925
|
+
*/
|
|
1926
|
+
async gateListing(page, view, signal) {
|
|
1927
|
+
const results = (await Promise.all(page.results.map((document) => this.gateListed(document, view, signal)))).filter((document) => document !== void 0);
|
|
1928
|
+
const next = page.next;
|
|
1929
|
+
return {
|
|
1930
|
+
...page,
|
|
1931
|
+
results,
|
|
1932
|
+
next: next ? async () => this.gateListing(await next(), view, signal) : void 0
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* One document as a listing serves it, or undefined when withheld. It holds
|
|
1937
|
+
* only the view's scopes; a domain scope it lacks is decided on its name, or
|
|
1938
|
+
* on its state read now when a condition needs that.
|
|
1939
|
+
*/
|
|
1940
|
+
async gateListed(document, view, signal) {
|
|
1941
|
+
const readable = await this.readableScopes(document, view, signal);
|
|
1942
|
+
const unheld = unheldScopesReadOnState(document);
|
|
1943
|
+
if (refusesEveryDomainScope(document, unheld.length === 0 ? readable : (scope) => !unheld.includes(scope) && readable(scope)) && !await this.servesUnheld(document, unheld, view, signal)) return;
|
|
1944
|
+
return filterReadableScopes(document, readable);
|
|
1945
|
+
}
|
|
1946
|
+
async servesUnheld(document, scopes, view, signal) {
|
|
1947
|
+
if (scopes.length === 0) return false;
|
|
1948
|
+
const id = document.header.id;
|
|
1949
|
+
let held;
|
|
1950
|
+
try {
|
|
1951
|
+
held = await this.reactor.get(id, withAuthScope({
|
|
1952
|
+
...view,
|
|
1953
|
+
scopes
|
|
1954
|
+
}), void 0, signal);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
await assertAbsent(this.documentView, id, error, signal);
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
const readable = await this.readableScopes(held, view, signal);
|
|
1960
|
+
return scopes.some(readable);
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Whether an event naming only these ids may reach the subject. An id with no
|
|
1964
|
+
* live document behind it has no content left to protect, so it does not
|
|
1965
|
+
* withhold; a deleted document is read at its deletion boundary when
|
|
1966
|
+
* deletion is positional, and gated like any other.
|
|
1967
|
+
*/
|
|
1968
|
+
async servesEvery(ids, view, reads) {
|
|
1969
|
+
return (await Promise.all(ids.map((id) => this.servesId(id, view, reads)))).every(Boolean);
|
|
1970
|
+
}
|
|
1971
|
+
async servesId(id, view, reads) {
|
|
1972
|
+
let document;
|
|
1973
|
+
try {
|
|
1974
|
+
document = await reads.get(id, withAllScopes(view));
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
await assertAbsent(reads, id, error);
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
return !refusesEveryDomainScope(document, await this.readableScopes(document, view, void 0, reads));
|
|
1980
|
+
}
|
|
1981
|
+
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
1982
|
+
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
1983
|
+
const allOperations = [];
|
|
1984
|
+
const activeCursors = {};
|
|
1985
|
+
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
1986
|
+
if (!canRead(scopeName)) continue;
|
|
1987
|
+
const scopeView = {
|
|
1988
|
+
...view,
|
|
1989
|
+
scopes: [scopeName]
|
|
1990
|
+
};
|
|
1991
|
+
const scopePaging = {
|
|
1992
|
+
cursor,
|
|
1993
|
+
limit: paging.limit
|
|
1994
|
+
};
|
|
1995
|
+
const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
|
|
1996
|
+
allOperations.push(...scopeResult.results);
|
|
1997
|
+
if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
|
|
1998
|
+
}
|
|
1999
|
+
allOperations.sort((a, b) => a.index - b.index);
|
|
2000
|
+
return {
|
|
2001
|
+
results: allOperations,
|
|
2002
|
+
options: paging,
|
|
2003
|
+
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Drops the edges whose far-end document the subject may read no domain scope
|
|
2008
|
+
* of. An edge is withheld whole rather than stripped of its metadata: the
|
|
2009
|
+
* document-shaped relationship reads already answer with the far end stripped
|
|
2010
|
+
* to the scopes the gate allows, so the far end's existence is disclosed
|
|
2011
|
+
* either way, but an edge's metadata is content about the pair that the far
|
|
2012
|
+
* end's own reads would refuse. An edge to a far end stripped to nothing
|
|
2013
|
+
* therefore carries content past a refusal, and there is no useful shell to
|
|
2014
|
+
* hand back in its place.
|
|
2015
|
+
*
|
|
2016
|
+
* `nextCursor` and `options` are left as the underlying stream reported them,
|
|
2017
|
+
* because a caller must feed them back to resume from the right position. A
|
|
2018
|
+
* gated page can therefore be shorter than the limit it asked for.
|
|
2019
|
+
*/
|
|
2020
|
+
async gateEdges(page, farEnd, view, signal) {
|
|
2021
|
+
const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
|
|
2022
|
+
if (ids.length === 0) return page;
|
|
2023
|
+
const farEndView = {
|
|
2024
|
+
subject: view?.subject,
|
|
2025
|
+
branch: view?.branch
|
|
2026
|
+
};
|
|
2027
|
+
const documents = await this.reactor.find({ ids }, farEndView, {
|
|
2028
|
+
cursor: "0",
|
|
2029
|
+
limit: ids.length
|
|
2030
|
+
}, void 0, signal);
|
|
2031
|
+
const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
|
|
2032
|
+
const allows = await this.readableScopes(doc, view, signal);
|
|
2033
|
+
return [doc.header.id, !refusesEveryDomainScope(doc, allows)];
|
|
2034
|
+
})));
|
|
2035
|
+
const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
|
|
2036
|
+
if (results.length === page.results.length) return page;
|
|
2037
|
+
const nextPage = page.next;
|
|
2038
|
+
return {
|
|
2039
|
+
...page,
|
|
2040
|
+
results,
|
|
2041
|
+
next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* One relationship edge, or undefined when it does not exist. A point lookup:
|
|
2046
|
+
* the pair is filtered in SQL rather than scanned out of the source's edge
|
|
2047
|
+
* list, which on a drive with thousands of children is the difference between
|
|
2048
|
+
* one query and dozens.
|
|
2049
|
+
*/
|
|
2050
|
+
async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
|
|
2051
|
+
const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
|
|
2052
|
+
const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
|
|
2053
|
+
return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
|
|
2054
|
+
cursor: "0",
|
|
2055
|
+
limit: 1
|
|
2056
|
+
}, void 0, signal)).results[0];
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* The id a write on `identifier` is stored under, which its signatures bind.
|
|
2060
|
+
* A create names its own id, and an id no slug maps to is taken as given, so
|
|
2061
|
+
* a document still in flight resolves to itself.
|
|
2062
|
+
*/
|
|
2063
|
+
async resolveWriteTarget(identifier, branch, actions, signal) {
|
|
2064
|
+
if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
|
|
2065
|
+
const view = { branch };
|
|
2066
|
+
const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
|
|
2067
|
+
if (bySlug === void 0 || bySlug === identifier) return identifier;
|
|
2068
|
+
return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* The decision model for one target document, built at its stream heads.
|
|
2072
|
+
*
|
|
2073
|
+
* The document is fetched unfiltered, because the policy is what decides:
|
|
2074
|
+
* reading it through the read gate would withhold the very scopes the
|
|
2075
|
+
* decision is about. A deleted document is served at its deletion boundary,
|
|
2076
|
+
* which is what lets the model refuse an execute against it -- authEnforcement
|
|
2077
|
+
* requires documentDecisions, so that read is available whenever this runs.
|
|
2078
|
+
*
|
|
2079
|
+
* Reading past the gate discloses nothing a submit does not. The `auth` and
|
|
2080
|
+
* `document` scopes are readable by every holder, so a verdict resting on the
|
|
2081
|
+
* policy alone is one the caller could compute unaided; and a verdict resting
|
|
2082
|
+
* on a conditional grant reads the executing scope's state exactly as
|
|
2083
|
+
* admission reads it, so the answer here is what submitting and being refused
|
|
2084
|
+
* would have revealed anyway.
|
|
2085
|
+
*
|
|
2086
|
+
* The append condition the build records is dropped. It guards a write, and
|
|
2087
|
+
* this makes none; reproducing it is also what the preflight cannot do, which
|
|
2088
|
+
* is why the answer is a prediction.
|
|
2089
|
+
*/
|
|
2090
|
+
async buildEvaluationTarget(config, documentId, branch, signal) {
|
|
2091
|
+
const document = await this.documentView.get(documentId, { branch }, void 0, signal);
|
|
2092
|
+
const target = {
|
|
2093
|
+
documentId,
|
|
2094
|
+
branch
|
|
2095
|
+
};
|
|
2096
|
+
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
|
|
2097
|
+
return {
|
|
2098
|
+
definition: config.model(target),
|
|
2099
|
+
model: built.model,
|
|
2100
|
+
scopeStates: document.state ?? {}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1827
2103
|
async removeAllIncomingRelationships(documentId, signal) {
|
|
1828
2104
|
const incoming = await this.documentIndexer.getIncoming(documentId, void 0, void 0, void 0, signal);
|
|
1829
2105
|
const jobs = [];
|
|
@@ -1837,18 +2113,6 @@ var ReactorClient = class {
|
|
|
1837
2113
|
//#endregion
|
|
1838
2114
|
//#region src/queue/types.ts
|
|
1839
2115
|
/**
|
|
1840
|
-
* State of a job in the queue
|
|
1841
|
-
*/
|
|
1842
|
-
let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
|
|
1843
|
-
JobQueueState[JobQueueState["UNKNOWN"] = -1] = "UNKNOWN";
|
|
1844
|
-
JobQueueState[JobQueueState["PREPROCESSING"] = 0] = "PREPROCESSING";
|
|
1845
|
-
JobQueueState[JobQueueState["PENDING"] = 1] = "PENDING";
|
|
1846
|
-
JobQueueState[JobQueueState["READY"] = 2] = "READY";
|
|
1847
|
-
JobQueueState[JobQueueState["RUNNING"] = 3] = "RUNNING";
|
|
1848
|
-
JobQueueState[JobQueueState["RESOLVED"] = 4] = "RESOLVED";
|
|
1849
|
-
return JobQueueState;
|
|
1850
|
-
}({});
|
|
1851
|
-
/**
|
|
1852
2116
|
* How a retry is accounted against the job's retry limit.
|
|
1853
2117
|
* - `CountAgainstLimit` (default): a fault; the job eventually exhausts its
|
|
1854
2118
|
* retries and fails terminally.
|
|
@@ -1881,6 +2145,11 @@ function toErrorInfo(error) {
|
|
|
1881
2145
|
};
|
|
1882
2146
|
}
|
|
1883
2147
|
var JobResultHandler = class {
|
|
2148
|
+
queue;
|
|
2149
|
+
jobTracker;
|
|
2150
|
+
eventBus;
|
|
2151
|
+
resolver;
|
|
2152
|
+
logger;
|
|
1884
2153
|
constructor(queue, jobTracker, eventBus, resolver, logger) {
|
|
1885
2154
|
this.queue = queue;
|
|
1886
2155
|
this.jobTracker = jobTracker;
|
|
@@ -1911,7 +2180,7 @@ var JobResultHandler = class {
|
|
|
1911
2180
|
if (result.error && AppendConditionFailedError.isError(result.error) && this.countConflicts(handle.job) < MAX_EXEMPT_CONFLICT_RETRIES) {
|
|
1912
2181
|
const errorInfo = toErrorInfo(result.error);
|
|
1913
2182
|
try {
|
|
1914
|
-
await this.queue.retryJob(handle.job.id, errorInfo,
|
|
2183
|
+
await this.queue.retryJob(handle.job.id, errorInfo, "exempt-from-limit");
|
|
1915
2184
|
return;
|
|
1916
2185
|
} catch {}
|
|
1917
2186
|
}
|
|
@@ -2009,6 +2278,12 @@ var JobResultHandler = class {
|
|
|
2009
2278
|
* is stranded at PENDING for as long as the deferral lasts.
|
|
2010
2279
|
*/
|
|
2011
2280
|
var DeferredJobs = class {
|
|
2281
|
+
queue;
|
|
2282
|
+
jobTracker;
|
|
2283
|
+
eventBus;
|
|
2284
|
+
logger;
|
|
2285
|
+
ttlMs;
|
|
2286
|
+
onReleased;
|
|
2012
2287
|
#byDocumentId = /* @__PURE__ */ new Map();
|
|
2013
2288
|
#timers = /* @__PURE__ */ new Map();
|
|
2014
2289
|
constructor(queue, jobTracker, eventBus, logger, ttlMs = DEFAULT_DEFERRED_JOB_TTL_MS, onReleased = () => Promise.resolve()) {
|
|
@@ -2170,6 +2445,13 @@ function bucketFor(documentId, numWorkers) {
|
|
|
2170
2445
|
* (Powerhouse board wiki id: d400d711-f07e-4389-a226-4e9fdd4fa8ba)
|
|
2171
2446
|
*/
|
|
2172
2447
|
var WorkerPoolJobExecutorManager = class {
|
|
2448
|
+
workerFactory;
|
|
2449
|
+
eventBus;
|
|
2450
|
+
queue;
|
|
2451
|
+
jobTracker;
|
|
2452
|
+
logger;
|
|
2453
|
+
resolver;
|
|
2454
|
+
operationIndex;
|
|
2173
2455
|
workers = [];
|
|
2174
2456
|
isRunning = false;
|
|
2175
2457
|
activeJobs = 0;
|
|
@@ -2434,6 +2716,12 @@ function fillMissingMemberships(documentIds, found) {
|
|
|
2434
2716
|
* Listens for job available events and dispatches jobs to executors.
|
|
2435
2717
|
*/
|
|
2436
2718
|
var SimpleJobExecutorManager = class {
|
|
2719
|
+
executorFactory;
|
|
2720
|
+
eventBus;
|
|
2721
|
+
queue;
|
|
2722
|
+
jobTracker;
|
|
2723
|
+
logger;
|
|
2724
|
+
resolver;
|
|
2437
2725
|
executors = [];
|
|
2438
2726
|
isRunning = false;
|
|
2439
2727
|
activeJobs = 0;
|
|
@@ -2608,6 +2896,7 @@ var SimpleJobExecutorManager = class {
|
|
|
2608
2896
|
* Subscribes to operation events to update job states.
|
|
2609
2897
|
*/
|
|
2610
2898
|
var InMemoryJobTracker = class {
|
|
2899
|
+
eventBus;
|
|
2611
2900
|
jobs = /* @__PURE__ */ new Map();
|
|
2612
2901
|
unsubscribers = [];
|
|
2613
2902
|
constructor(eventBus) {
|
|
@@ -2628,11 +2917,11 @@ var InMemoryJobTracker = class {
|
|
|
2628
2917
|
handleWriteReady(event) {
|
|
2629
2918
|
const jobId = event.jobId;
|
|
2630
2919
|
const job = this.jobs.get(jobId);
|
|
2631
|
-
if (job && job.status ===
|
|
2920
|
+
if (job && job.status === "RUNNING") {
|
|
2632
2921
|
const consistencyToken = createConsistencyToken(event.operations);
|
|
2633
2922
|
this.jobs.set(jobId, {
|
|
2634
2923
|
...job,
|
|
2635
|
-
status:
|
|
2924
|
+
status: "WRITE_READY",
|
|
2636
2925
|
consistencyToken,
|
|
2637
2926
|
result: summarizeSubmittedActions(event.operations, event.submittedActionIds)
|
|
2638
2927
|
});
|
|
@@ -2641,9 +2930,9 @@ var InMemoryJobTracker = class {
|
|
|
2641
2930
|
handleReadReady(event) {
|
|
2642
2931
|
const jobId = event.jobId;
|
|
2643
2932
|
const job = this.jobs.get(jobId);
|
|
2644
|
-
if (job && job.status ===
|
|
2933
|
+
if (job && job.status === "WRITE_READY") this.jobs.set(jobId, {
|
|
2645
2934
|
...job,
|
|
2646
|
-
status:
|
|
2935
|
+
status: "READ_READY"
|
|
2647
2936
|
});
|
|
2648
2937
|
}
|
|
2649
2938
|
handleJobFailed(event) {
|
|
@@ -2666,7 +2955,7 @@ var InMemoryJobTracker = class {
|
|
|
2666
2955
|
this.jobs.set(jobId, {
|
|
2667
2956
|
id: jobId,
|
|
2668
2957
|
documentId: "",
|
|
2669
|
-
status:
|
|
2958
|
+
status: "RUNNING",
|
|
2670
2959
|
createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2671
2960
|
consistencyToken: createEmptyConsistencyToken(),
|
|
2672
2961
|
meta: {
|
|
@@ -2678,7 +2967,7 @@ var InMemoryJobTracker = class {
|
|
|
2678
2967
|
}
|
|
2679
2968
|
this.jobs.set(jobId, {
|
|
2680
2969
|
...job,
|
|
2681
|
-
status:
|
|
2970
|
+
status: "RUNNING"
|
|
2682
2971
|
});
|
|
2683
2972
|
}
|
|
2684
2973
|
markFailed(jobId, error, job) {
|
|
@@ -2687,7 +2976,7 @@ var InMemoryJobTracker = class {
|
|
|
2687
2976
|
this.jobs.set(jobId, {
|
|
2688
2977
|
id: jobId,
|
|
2689
2978
|
documentId: job?.documentId ?? "",
|
|
2690
|
-
status:
|
|
2979
|
+
status: "FAILED",
|
|
2691
2980
|
createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2692
2981
|
completedAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2693
2982
|
error,
|
|
@@ -2702,7 +2991,7 @@ var InMemoryJobTracker = class {
|
|
|
2702
2991
|
}
|
|
2703
2992
|
this.jobs.set(jobId, {
|
|
2704
2993
|
...existing,
|
|
2705
|
-
status:
|
|
2994
|
+
status: "FAILED",
|
|
2706
2995
|
completedAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2707
2996
|
error,
|
|
2708
2997
|
job,
|
|
@@ -2796,6 +3085,7 @@ function highestOf(ops) {
|
|
|
2796
3085
|
return highest;
|
|
2797
3086
|
}
|
|
2798
3087
|
var ProcessorQueue = class {
|
|
3088
|
+
options;
|
|
2799
3089
|
tasks = [];
|
|
2800
3090
|
running = false;
|
|
2801
3091
|
closed = false;
|
|
@@ -3378,11 +3668,11 @@ var JobExecutionHandle = class {
|
|
|
3378
3668
|
onDefer;
|
|
3379
3669
|
getStateName(state) {
|
|
3380
3670
|
switch (state) {
|
|
3381
|
-
case
|
|
3382
|
-
case
|
|
3383
|
-
case
|
|
3384
|
-
case
|
|
3385
|
-
case
|
|
3671
|
+
case 0: return "PREPROCESSING";
|
|
3672
|
+
case 1: return "PENDING";
|
|
3673
|
+
case 2: return "READY";
|
|
3674
|
+
case 3: return "RUNNING";
|
|
3675
|
+
case 4: return "RESOLVED";
|
|
3386
3676
|
default: return `UNKNOWN`;
|
|
3387
3677
|
}
|
|
3388
3678
|
}
|
|
@@ -3401,23 +3691,23 @@ var JobExecutionHandle = class {
|
|
|
3401
3691
|
return this._state;
|
|
3402
3692
|
}
|
|
3403
3693
|
start() {
|
|
3404
|
-
if (this._state !==
|
|
3405
|
-
this._state =
|
|
3694
|
+
if (this._state !== 2) throw new Error(`Cannot start job in state ${this.getStateName(this._state)}`);
|
|
3695
|
+
this._state = 3;
|
|
3406
3696
|
this.onStart?.();
|
|
3407
3697
|
}
|
|
3408
3698
|
complete() {
|
|
3409
|
-
if (this._state !==
|
|
3410
|
-
this._state =
|
|
3699
|
+
if (this._state !== 3) throw new Error(`Cannot complete job in state ${this.getStateName(this._state)}`);
|
|
3700
|
+
this._state = 4;
|
|
3411
3701
|
this.onComplete?.();
|
|
3412
3702
|
}
|
|
3413
3703
|
fail(error) {
|
|
3414
|
-
if (this._state !==
|
|
3415
|
-
this._state =
|
|
3704
|
+
if (this._state !== 3) throw new Error(`Cannot fail job in state ${this.getStateName(this._state)}`);
|
|
3705
|
+
this._state = 4;
|
|
3416
3706
|
this.onFail?.(error);
|
|
3417
3707
|
}
|
|
3418
3708
|
defer() {
|
|
3419
|
-
if (this._state !==
|
|
3420
|
-
this._state =
|
|
3709
|
+
if (this._state !== 3) throw new Error(`Cannot defer job in state ${this.getStateName(this._state)}`);
|
|
3710
|
+
this._state = 4;
|
|
3421
3711
|
this.onDefer?.();
|
|
3422
3712
|
}
|
|
3423
3713
|
};
|
|
@@ -3430,6 +3720,8 @@ var JobExecutionHandle = class {
|
|
|
3430
3720
|
* Implements dependency management through queue hints.
|
|
3431
3721
|
*/
|
|
3432
3722
|
var InMemoryQueue = class {
|
|
3723
|
+
eventBus;
|
|
3724
|
+
resolver;
|
|
3433
3725
|
queues = /* @__PURE__ */ new Map();
|
|
3434
3726
|
jobIdToQueueKey = /* @__PURE__ */ new Map();
|
|
3435
3727
|
docIdToJobId = /* @__PURE__ */ new Map();
|
|
@@ -3564,7 +3856,7 @@ var InMemoryQueue = class {
|
|
|
3564
3856
|
this.jobIdToQueueKey.delete(job.id);
|
|
3565
3857
|
this.markJobExecuting(job);
|
|
3566
3858
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
3567
|
-
const handle = new JobExecutionHandle(job,
|
|
3859
|
+
const handle = new JobExecutionHandle(job, 2, {
|
|
3568
3860
|
onStart: () => {},
|
|
3569
3861
|
onComplete: () => {
|
|
3570
3862
|
this.completeJob(job.id);
|
|
@@ -3590,7 +3882,7 @@ var InMemoryQueue = class {
|
|
|
3590
3882
|
this.jobIdToQueueKey.delete(job.id);
|
|
3591
3883
|
this.markJobExecuting(job);
|
|
3592
3884
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
3593
|
-
const handle = new JobExecutionHandle(job,
|
|
3885
|
+
const handle = new JobExecutionHandle(job, 2, {
|
|
3594
3886
|
onStart: () => {},
|
|
3595
3887
|
onComplete: () => {
|
|
3596
3888
|
this.completeJob(job.id);
|
|
@@ -3624,7 +3916,7 @@ var InMemoryQueue = class {
|
|
|
3624
3916
|
this.jobIdToQueueKey.delete(job.id);
|
|
3625
3917
|
this.markJobExecuting(job);
|
|
3626
3918
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
3627
|
-
const handle = new JobExecutionHandle(job,
|
|
3919
|
+
const handle = new JobExecutionHandle(job, 2, {
|
|
3628
3920
|
onStart: () => {},
|
|
3629
3921
|
onComplete: () => {
|
|
3630
3922
|
this.completeJob(job.id);
|
|
@@ -3726,7 +4018,7 @@ var InMemoryQueue = class {
|
|
|
3726
4018
|
if (documentId) this.markJobComplete(jobId, documentId);
|
|
3727
4019
|
this.jobIndex.delete(jobId);
|
|
3728
4020
|
}
|
|
3729
|
-
async retryJob(jobId, error, accounting =
|
|
4021
|
+
async retryJob(jobId, error, accounting = "count-against-limit") {
|
|
3730
4022
|
const job = this.jobIndex.get(jobId);
|
|
3731
4023
|
if (!job) return;
|
|
3732
4024
|
job.lastError = error;
|
|
@@ -3738,7 +4030,7 @@ var InMemoryQueue = class {
|
|
|
3738
4030
|
const retryCount = job.retryCount || 0;
|
|
3739
4031
|
const updatedJob = {
|
|
3740
4032
|
...job,
|
|
3741
|
-
retryCount: accounting ===
|
|
4033
|
+
retryCount: accounting === "count-against-limit" ? retryCount + 1 : retryCount,
|
|
3742
4034
|
lastError: error
|
|
3743
4035
|
};
|
|
3744
4036
|
await this.enqueue(updatedJob);
|
|
@@ -3939,6 +4231,8 @@ function describeSource(source) {
|
|
|
3939
4231
|
* and failure-caching state.
|
|
3940
4232
|
*/
|
|
3941
4233
|
var DocumentModelResolver = class {
|
|
4234
|
+
registry;
|
|
4235
|
+
loader;
|
|
3942
4236
|
loadingModels = /* @__PURE__ */ new Map();
|
|
3943
4237
|
failedModelTypes = /* @__PURE__ */ new Set();
|
|
3944
4238
|
broadcastHook = null;
|
|
@@ -4009,6 +4303,7 @@ var DocumentModelResolver = class {
|
|
|
4009
4303
|
* Since there is no loader, missing models cannot be recovered.
|
|
4010
4304
|
*/
|
|
4011
4305
|
var NullDocumentModelResolver = class {
|
|
4306
|
+
registry;
|
|
4012
4307
|
constructor(registry) {
|
|
4013
4308
|
this.registry = registry;
|
|
4014
4309
|
}
|
|
@@ -4232,6 +4527,8 @@ var ReactorSubscriptionManager = class {
|
|
|
4232
4527
|
* is emitted, so that reactor.get() returns fresh data when callbacks fire.
|
|
4233
4528
|
*/
|
|
4234
4529
|
var SubscriptionNotificationReadModel = class {
|
|
4530
|
+
subscriptionManager;
|
|
4531
|
+
documentView;
|
|
4235
4532
|
name = "subscription-notification";
|
|
4236
4533
|
constructor(subscriptionManager, documentView) {
|
|
4237
4534
|
this.subscriptionManager = subscriptionManager;
|
|
@@ -4254,10 +4551,10 @@ var SubscriptionNotificationReadModel = class {
|
|
|
4254
4551
|
deleted.push(deletedId);
|
|
4255
4552
|
} else if (actionType === "ADD_RELATIONSHIP") {
|
|
4256
4553
|
const input = operation.action.input;
|
|
4257
|
-
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId,
|
|
4554
|
+
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, "added", input.childType);
|
|
4258
4555
|
} else if (actionType === "REMOVE_RELATIONSHIP") {
|
|
4259
4556
|
const input = operation.action.input;
|
|
4260
|
-
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId,
|
|
4557
|
+
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, "removed", input.childType);
|
|
4261
4558
|
} else if (!created.includes(context.documentId)) updatedIds.add(context.documentId);
|
|
4262
4559
|
}
|
|
4263
4560
|
if (created.length > 0) this.subscriptionManager.notifyDocumentsCreated(created, documentTypes, parentIds);
|
|
@@ -4282,6 +4579,10 @@ var SubscriptionNotificationReadModel = class {
|
|
|
4282
4579
|
* keeps the common case (a membership write later than all history) free.
|
|
4283
4580
|
*/
|
|
4284
4581
|
var GroupReevaluationTrigger = class {
|
|
4582
|
+
logger;
|
|
4583
|
+
eventBus;
|
|
4584
|
+
queue;
|
|
4585
|
+
operationIndex;
|
|
4285
4586
|
unsubscribe;
|
|
4286
4587
|
constructor(logger, eventBus, queue, operationIndex) {
|
|
4287
4588
|
this.logger = logger;
|
|
@@ -4433,7 +4734,7 @@ var Mailbox = class {
|
|
|
4433
4734
|
this.itemsMap.set(item.id, item);
|
|
4434
4735
|
for (const op of item.operations) this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
|
|
4435
4736
|
item.on((syncOp, _, next) => {
|
|
4436
|
-
if (next ===
|
|
4737
|
+
if (next === 2) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
|
|
4437
4738
|
});
|
|
4438
4739
|
}
|
|
4439
4740
|
if (this.paused) {
|
|
@@ -4547,7 +4848,7 @@ var BufferedMailbox = class {
|
|
|
4547
4848
|
this.itemsMap.set(item.id, item);
|
|
4548
4849
|
for (const op of item.operations) this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
|
|
4549
4850
|
item.on((syncOp, _, next) => {
|
|
4550
|
-
if (next ===
|
|
4851
|
+
if (next === 2) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
|
|
4551
4852
|
});
|
|
4552
4853
|
}
|
|
4553
4854
|
this.addedBuffer.push(...items);
|
|
@@ -4661,7 +4962,14 @@ var GraphQLRequestError = class extends Error {
|
|
|
4661
4962
|
* difference between a channel that recovers and one that is dead for the
|
|
4662
4963
|
* process lifetime.
|
|
4663
4964
|
*/
|
|
4664
|
-
const RECOVERABLE_GRAPHQL_ERROR_CODES = {
|
|
4965
|
+
const RECOVERABLE_GRAPHQL_ERROR_CODES = {
|
|
4966
|
+
/**
|
|
4967
|
+
* A stored operation cannot be represented in the schema - an action with no
|
|
4968
|
+
* id, say. The document holding it needs repairing, but the channel serves
|
|
4969
|
+
* every other document, and a peer that stopped polling would stop receiving
|
|
4970
|
+
* those too.
|
|
4971
|
+
*/
|
|
4972
|
+
malformedStoredOperation: "MALFORMED_STORED_OPERATION" };
|
|
4665
4973
|
const RECOVERABLE_CODES = new Set(Object.values(RECOVERABLE_GRAPHQL_ERROR_CODES));
|
|
4666
4974
|
/**
|
|
4667
4975
|
* True when every error the response carried named a recoverable code.
|
|
@@ -4770,23 +5078,23 @@ var SyncOperation = class {
|
|
|
4770
5078
|
this.scopes = scopes;
|
|
4771
5079
|
this.branch = branch;
|
|
4772
5080
|
this.operations = operations;
|
|
4773
|
-
this.status =
|
|
5081
|
+
this.status = -1;
|
|
4774
5082
|
}
|
|
4775
5083
|
on(callback) {
|
|
4776
5084
|
this.callbacks.push(callback);
|
|
4777
5085
|
}
|
|
4778
5086
|
started() {
|
|
4779
|
-
this.transition(
|
|
5087
|
+
this.transition(0);
|
|
4780
5088
|
}
|
|
4781
5089
|
transported() {
|
|
4782
|
-
this.transition(
|
|
5090
|
+
this.transition(1);
|
|
4783
5091
|
}
|
|
4784
5092
|
executed() {
|
|
4785
|
-
this.transition(
|
|
5093
|
+
this.transition(2);
|
|
4786
5094
|
}
|
|
4787
5095
|
failed(error) {
|
|
4788
5096
|
this.error = error;
|
|
4789
|
-
this.transition(
|
|
5097
|
+
this.transition(3);
|
|
4790
5098
|
}
|
|
4791
5099
|
transition(next) {
|
|
4792
5100
|
const prev = this.status;
|
|
@@ -4996,9 +5304,11 @@ function consolidateSyncOperations(syncOps) {
|
|
|
4996
5304
|
}
|
|
4997
5305
|
const first = group.ops[0];
|
|
4998
5306
|
const merged = new SyncOperation(first.id, first.jobId, remappedDeps, first.remoteName, first.documentId, first.scopes, first.branch, allOperations);
|
|
4999
|
-
if (first.status >
|
|
5000
|
-
|
|
5001
|
-
|
|
5307
|
+
if (first.status > 0) {
|
|
5308
|
+
if (first.status >= 3) merged.executed();
|
|
5309
|
+
else if (first.status >= 2) merged.transported();
|
|
5310
|
+
else merged.started();
|
|
5311
|
+
}
|
|
5002
5312
|
result.push(merged);
|
|
5003
5313
|
}
|
|
5004
5314
|
return result;
|
|
@@ -5129,7 +5439,7 @@ function syncOperationErrorType(error) {
|
|
|
5129
5439
|
return error?.errorType ?? classifyJobFailure(error?.error?.name ?? "Error");
|
|
5130
5440
|
}
|
|
5131
5441
|
/** Dead-letter types that must not stop the document syncing. */
|
|
5132
|
-
const NON_QUARANTINING_ERROR_TYPES = new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
|
|
5442
|
+
const NON_QUARANTINING_ERROR_TYPES = /* @__PURE__ */ new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
|
|
5133
5443
|
/**
|
|
5134
5444
|
* A held auth operation must not quarantine: reconciling the two policies needs
|
|
5135
5445
|
* the traffic a quarantine would stop.
|
|
@@ -5355,7 +5665,7 @@ function envelopesToSyncOperations(envelope, remoteName) {
|
|
|
5355
5665
|
}
|
|
5356
5666
|
const getLatestAppliedOrdinal = (syncOps) => {
|
|
5357
5667
|
let maxOrdinal = 0;
|
|
5358
|
-
for (const syncOp of syncOps) if (syncOp.status ===
|
|
5668
|
+
for (const syncOp of syncOps) if (syncOp.status === 2) for (const op of syncOp.operations) maxOrdinal = Math.max(maxOrdinal, op.context.ordinal);
|
|
5359
5669
|
return maxOrdinal;
|
|
5360
5670
|
};
|
|
5361
5671
|
//#endregion
|
|
@@ -5370,6 +5680,7 @@ const DECISION_FIELDS = ["deniedReason", "errorType"];
|
|
|
5370
5680
|
* GraphQL-based synchronization channel for network communication between reactors.
|
|
5371
5681
|
*/
|
|
5372
5682
|
var GqlRequestChannel = class {
|
|
5683
|
+
logger;
|
|
5373
5684
|
inbox;
|
|
5374
5685
|
outbox;
|
|
5375
5686
|
deadLetter;
|
|
@@ -5577,7 +5888,7 @@ var GqlRequestChannel = class {
|
|
|
5577
5888
|
const syncOps = [];
|
|
5578
5889
|
for (const dl of deadLetters) {
|
|
5579
5890
|
const syncOp = new SyncOperation(crypto.randomUUID(), dl.jobId, [], this.remoteName, dl.documentId, dl.scopes, dl.branch, []);
|
|
5580
|
-
syncOp.failed(new ChannelError(
|
|
5891
|
+
syncOp.failed(new ChannelError("outbox", new Error(dl.error), dl.errorType ?? void 0));
|
|
5581
5892
|
syncOps.push(syncOp);
|
|
5582
5893
|
}
|
|
5583
5894
|
this.deadLetter.add(...syncOps);
|
|
@@ -5597,7 +5908,7 @@ var GqlRequestChannel = class {
|
|
|
5597
5908
|
const classification = this.classifyError(err);
|
|
5598
5909
|
this.failureCount++;
|
|
5599
5910
|
this.lastFailureUtcMs = Date.now();
|
|
5600
|
-
const channelError = new ChannelError(
|
|
5911
|
+
const channelError = new ChannelError("inbox", err);
|
|
5601
5912
|
this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
|
|
5602
5913
|
if (classification === "unrecoverable") {
|
|
5603
5914
|
this.pollTimer.stop();
|
|
@@ -5841,7 +6152,7 @@ var GqlRequestChannel = class {
|
|
|
5841
6152
|
this.transitionConnectionState("reconnecting");
|
|
5842
6153
|
this.schedulePushRetry();
|
|
5843
6154
|
} else {
|
|
5844
|
-
const channelError = new ChannelError(
|
|
6155
|
+
const channelError = new ChannelError("outbox", err);
|
|
5845
6156
|
for (const syncOp of syncOps) syncOp.failed(channelError);
|
|
5846
6157
|
this.deadLetter.add(...syncOps);
|
|
5847
6158
|
this.outbox.remove(...syncOps);
|
|
@@ -6063,7 +6374,7 @@ var GqlRequestChannelFactory = class {
|
|
|
6063
6374
|
...retryMaxDelayMs !== void 0 && { retryMaxDelayMs },
|
|
6064
6375
|
...maxQueueDepth !== void 0 && { maxQueueDepth },
|
|
6065
6376
|
...backpressureCheckIntervalMs !== void 0 && { backpressureCheckIntervalMs },
|
|
6066
|
-
startPaused: options?.pollBehavior ===
|
|
6377
|
+
startPaused: options?.pollBehavior === "manual"
|
|
6067
6378
|
});
|
|
6068
6379
|
return new GqlRequestChannel(this.logger, remoteId, remoteName, cursorStorage, gqlConfig, operationIndex, pollTimer);
|
|
6069
6380
|
}
|
|
@@ -6080,6 +6391,7 @@ function firstOrdinalOf$1(syncOp) {
|
|
|
6080
6391
|
* In general, the resolvers are responsible for updating mailboxes.
|
|
6081
6392
|
*/
|
|
6082
6393
|
var GqlResponseChannel = class {
|
|
6394
|
+
logger;
|
|
6083
6395
|
inbox;
|
|
6084
6396
|
outbox;
|
|
6085
6397
|
deadLetter;
|
|
@@ -6190,7 +6502,7 @@ var GqlResponseChannel = class {
|
|
|
6190
6502
|
*/
|
|
6191
6503
|
rememberUnserved(syncOps) {
|
|
6192
6504
|
for (const syncOp of syncOps) {
|
|
6193
|
-
if (syncOp.status ===
|
|
6505
|
+
if (syncOp.status === 2) continue;
|
|
6194
6506
|
const first = firstOrdinalOf$1(syncOp);
|
|
6195
6507
|
if (first > 0 && first < this.evictedOutboxFloor) this.evictedOutboxFloor = first;
|
|
6196
6508
|
}
|
|
@@ -6235,7 +6547,7 @@ var GqlResponseChannel = class {
|
|
|
6235
6547
|
unservedFloor() {
|
|
6236
6548
|
let floor = this.evictedOutboxFloor;
|
|
6237
6549
|
for (const syncOp of this.outbox.items) {
|
|
6238
|
-
if (syncOp.status ===
|
|
6550
|
+
if (syncOp.status === 2) continue;
|
|
6239
6551
|
const first = firstOrdinalOf$1(syncOp);
|
|
6240
6552
|
if (first > 0 && first < floor) floor = first;
|
|
6241
6553
|
}
|
|
@@ -6275,6 +6587,7 @@ function remoteCursorToRow(cursor) {
|
|
|
6275
6587
|
};
|
|
6276
6588
|
}
|
|
6277
6589
|
var KyselySyncCursorStorage = class {
|
|
6590
|
+
db;
|
|
6278
6591
|
constructor(db) {
|
|
6279
6592
|
this.db = db;
|
|
6280
6593
|
}
|
|
@@ -6350,6 +6663,7 @@ function deadLetterRecordToRow(record) {
|
|
|
6350
6663
|
* PGlite/Kysely-backed implementation of {@link ISyncDeadLetterStorage}.
|
|
6351
6664
|
*/
|
|
6352
6665
|
var KyselySyncDeadLetterStorage = class {
|
|
6666
|
+
db;
|
|
6353
6667
|
constructor(db) {
|
|
6354
6668
|
this.db = db;
|
|
6355
6669
|
}
|
|
@@ -6471,6 +6785,7 @@ function remoteRecordToRow(remote) {
|
|
|
6471
6785
|
};
|
|
6472
6786
|
}
|
|
6473
6787
|
var KyselySyncRemoteStorage = class {
|
|
6788
|
+
db;
|
|
6474
6789
|
constructor(db) {
|
|
6475
6790
|
this.db = db;
|
|
6476
6791
|
}
|
|
@@ -6621,6 +6936,7 @@ var BatchAggregator = class {
|
|
|
6621
6936
|
* to provide a fast path for jobs that have already synced.
|
|
6622
6937
|
*/
|
|
6623
6938
|
var SyncAwaiter = class {
|
|
6939
|
+
eventBus;
|
|
6624
6940
|
completedResults = /* @__PURE__ */ new Map();
|
|
6625
6941
|
pendingWaiters = /* @__PURE__ */ new Map();
|
|
6626
6942
|
unsubscribers = [];
|
|
@@ -6820,24 +7136,19 @@ var SyncStatusTracker = class {
|
|
|
6820
7136
|
}
|
|
6821
7137
|
};
|
|
6822
7138
|
function deriveStatus(inboxCount, outboxCount, errorCount) {
|
|
6823
|
-
if (errorCount > 0) return
|
|
6824
|
-
if (inboxCount > 0 && outboxCount > 0) return
|
|
6825
|
-
if (inboxCount > 0) return
|
|
6826
|
-
if (outboxCount > 0) return
|
|
6827
|
-
return
|
|
7139
|
+
if (errorCount > 0) return "ERROR";
|
|
7140
|
+
if (inboxCount > 0 && outboxCount > 0) return "OUTGOING_AND_INCOMING";
|
|
7141
|
+
if (inboxCount > 0) return "INCOMING";
|
|
7142
|
+
if (outboxCount > 0) return "OUTGOING";
|
|
7143
|
+
return "SYNCED";
|
|
6828
7144
|
}
|
|
6829
7145
|
//#endregion
|
|
6830
7146
|
//#region src/sync/sync-manager.ts
|
|
6831
|
-
var OutboxMode = /* @__PURE__ */ function(OutboxMode) {
|
|
6832
|
-
OutboxMode["Backfill"] = "backfill";
|
|
6833
|
-
OutboxMode["BatchTriggered"] = "batch-triggered";
|
|
6834
|
-
return OutboxMode;
|
|
6835
|
-
}(OutboxMode || {});
|
|
6836
7147
|
const defaultSyncManagerConfig = {
|
|
6837
7148
|
maxDeadLettersPerRemote: 100,
|
|
6838
7149
|
maxInboxBatchSize: 32,
|
|
6839
7150
|
maxHeldOperationsPerRemote: 1e4,
|
|
6840
|
-
staleRemotePollWindowMs:
|
|
7151
|
+
staleRemotePollWindowMs: 3e5
|
|
6841
7152
|
};
|
|
6842
7153
|
const PLAN_KEY_TO_JOB_UUID_CAP = 1e4;
|
|
6843
7154
|
/**
|
|
@@ -6940,7 +7251,7 @@ var SyncManager = class {
|
|
|
6940
7251
|
if (outboxAckOrdinal > 0) {
|
|
6941
7252
|
const backfillController = new AbortController();
|
|
6942
7253
|
this.backfillAbortControllers.set(record.name, backfillController);
|
|
6943
|
-
this.updateOutbox(remote, outboxAckOrdinal,
|
|
7254
|
+
this.updateOutbox(remote, outboxAckOrdinal, "backfill", backfillController.signal).catch((error) => {
|
|
6944
7255
|
if (backfillController.signal.aborted) return;
|
|
6945
7256
|
this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
|
|
6946
7257
|
}).finally(() => {
|
|
@@ -7059,7 +7370,7 @@ var SyncManager = class {
|
|
|
7059
7370
|
}
|
|
7060
7371
|
const backfillController = new AbortController();
|
|
7061
7372
|
this.backfillAbortControllers.set(name, backfillController);
|
|
7062
|
-
this.updateOutbox(remote, 0,
|
|
7373
|
+
this.updateOutbox(remote, 0, "backfill", backfillController.signal).catch((error) => {
|
|
7063
7374
|
if (backfillController.signal.aborted) return;
|
|
7064
7375
|
this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
|
|
7065
7376
|
}).finally(() => {
|
|
@@ -7179,7 +7490,7 @@ var SyncManager = class {
|
|
|
7179
7490
|
scopes: syncOp.scopes,
|
|
7180
7491
|
branch: syncOp.branch,
|
|
7181
7492
|
operations: syncOp.operations,
|
|
7182
|
-
errorSource: syncOp.error?.source ??
|
|
7493
|
+
errorSource: syncOp.error?.source ?? "none",
|
|
7183
7494
|
errorMessage: syncOp.error?.error.message ?? "unknown",
|
|
7184
7495
|
errorType
|
|
7185
7496
|
};
|
|
@@ -7239,7 +7550,7 @@ var SyncManager = class {
|
|
|
7239
7550
|
for (const remote of affectedRemotes) trimMailboxFromBatch(remote.channel.inbox, batch);
|
|
7240
7551
|
for (const remote of affectedRemotes) {
|
|
7241
7552
|
if (!this.remotes.has(remote.meta.name) || this.removing.has(remote.meta.name)) continue;
|
|
7242
|
-
await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal,
|
|
7553
|
+
await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal, "batch-triggered");
|
|
7243
7554
|
}
|
|
7244
7555
|
await this.drainPrunes();
|
|
7245
7556
|
}
|
|
@@ -7281,7 +7592,7 @@ var SyncManager = class {
|
|
|
7281
7592
|
if (this.isShutdown) return;
|
|
7282
7593
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
7283
7594
|
this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.meta.name, syncOp.documentId, err.message);
|
|
7284
|
-
const channelError = new ChannelError(
|
|
7595
|
+
const channelError = new ChannelError("inbox", err);
|
|
7285
7596
|
syncOp.failed(channelError);
|
|
7286
7597
|
remote.channel.deadLetter.add(syncOp);
|
|
7287
7598
|
remote.channel.inbox.remove(syncOp);
|
|
@@ -7294,14 +7605,14 @@ var SyncManager = class {
|
|
|
7294
7605
|
if (this.isShutdown) return;
|
|
7295
7606
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
7296
7607
|
this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, jobInfo.id, err.message);
|
|
7297
|
-
const channelError = new ChannelError(
|
|
7608
|
+
const channelError = new ChannelError("inbox", err);
|
|
7298
7609
|
syncOp.failed(channelError);
|
|
7299
7610
|
remote.channel.deadLetter.add(syncOp);
|
|
7300
7611
|
remote.channel.inbox.remove(syncOp);
|
|
7301
7612
|
return;
|
|
7302
7613
|
}
|
|
7303
7614
|
if (this.isShutdown) return;
|
|
7304
|
-
if (completedJobInfo.status ===
|
|
7615
|
+
if (completedJobInfo.status === "FAILED") {
|
|
7305
7616
|
const errorMessage = completedJobInfo.error?.message || "Unknown error";
|
|
7306
7617
|
this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, completedJobInfo.id, errorMessage);
|
|
7307
7618
|
syncOp.failed(this.inboxFailure(completedJobInfo.error));
|
|
@@ -7344,7 +7655,7 @@ var SyncManager = class {
|
|
|
7344
7655
|
if (this.isShutdown) return;
|
|
7345
7656
|
for (const { remote, syncOp } of items) {
|
|
7346
7657
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
7347
|
-
syncOp.failed(new ChannelError(
|
|
7658
|
+
syncOp.failed(new ChannelError("inbox", err));
|
|
7348
7659
|
remote.channel.deadLetter.add(syncOp);
|
|
7349
7660
|
remote.channel.inbox.remove(syncOp);
|
|
7350
7661
|
}
|
|
@@ -7361,7 +7672,7 @@ var SyncManager = class {
|
|
|
7361
7672
|
for (const { remote, syncOp } of items) {
|
|
7362
7673
|
if (!(syncOp.jobId in result.jobs)) {
|
|
7363
7674
|
this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
|
|
7364
|
-
const error = new ChannelError(
|
|
7675
|
+
const error = new ChannelError("inbox", /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
|
|
7365
7676
|
syncOp.failed(error);
|
|
7366
7677
|
remote.channel.deadLetter.add(syncOp);
|
|
7367
7678
|
remote.channel.inbox.remove(syncOp);
|
|
@@ -7374,13 +7685,13 @@ var SyncManager = class {
|
|
|
7374
7685
|
} catch (error) {
|
|
7375
7686
|
if (this.isShutdown) continue;
|
|
7376
7687
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
7377
|
-
syncOp.failed(new ChannelError(
|
|
7688
|
+
syncOp.failed(new ChannelError("inbox", err));
|
|
7378
7689
|
remote.channel.deadLetter.add(syncOp);
|
|
7379
7690
|
remote.channel.inbox.remove(syncOp);
|
|
7380
7691
|
continue;
|
|
7381
7692
|
}
|
|
7382
7693
|
if (this.isShutdown) return;
|
|
7383
|
-
if (completedJobInfo.status ===
|
|
7694
|
+
if (completedJobInfo.status === "FAILED") {
|
|
7384
7695
|
syncOp.failed(this.inboxFailure(completedJobInfo.error));
|
|
7385
7696
|
remote.channel.deadLetter.add(syncOp);
|
|
7386
7697
|
} else syncOp.executed();
|
|
@@ -7399,7 +7710,7 @@ var SyncManager = class {
|
|
|
7399
7710
|
*/
|
|
7400
7711
|
inboxFailure(error) {
|
|
7401
7712
|
const message = error?.message || "Unknown error";
|
|
7402
|
-
return new ChannelError(
|
|
7713
|
+
return new ChannelError("inbox", /* @__PURE__ */ new Error(`Failed to apply operations: ${message}`), classifyJobFailure(error?.name ?? "Error"));
|
|
7403
7714
|
}
|
|
7404
7715
|
/**
|
|
7405
7716
|
* Where the next outbox derivation starts: at the evicted floor when there is
|
|
@@ -7426,7 +7737,7 @@ var SyncManager = class {
|
|
|
7426
7737
|
*/
|
|
7427
7738
|
evictPastOutboxBound(remote) {
|
|
7428
7739
|
const cap = this.config.maxHeldOperationsPerRemote;
|
|
7429
|
-
const queued = remote.channel.outbox.items.filter((syncOp) => syncOp.status ===
|
|
7740
|
+
const queued = remote.channel.outbox.items.filter((syncOp) => syncOp.status === -1).sort((a, b) => firstOrdinalOf(a) - firstOrdinalOf(b));
|
|
7430
7741
|
let kept = 0;
|
|
7431
7742
|
let cut = queued.length;
|
|
7432
7743
|
for (let i = 0; i < queued.length; i++) {
|
|
@@ -7518,7 +7829,7 @@ var SyncManager = class {
|
|
|
7518
7829
|
* it marked; the count is what keeps that drain from removing the remote this
|
|
7519
7830
|
* call is still adding to.
|
|
7520
7831
|
*/
|
|
7521
|
-
async updateOutbox(remote, ackOrdinal, mode =
|
|
7832
|
+
async updateOutbox(remote, ackOrdinal, mode = "backfill", signal) {
|
|
7522
7833
|
this.derivingOutboxes++;
|
|
7523
7834
|
try {
|
|
7524
7835
|
await this.deriveOutbox(remote, ackOrdinal, mode, signal);
|
|
@@ -7546,11 +7857,11 @@ var SyncManager = class {
|
|
|
7546
7857
|
const prevJobId = lastJobByDoc.get(batch.documentId);
|
|
7547
7858
|
const deps = [];
|
|
7548
7859
|
if (prevJobId) deps.push(prevJobId);
|
|
7549
|
-
if (mode ===
|
|
7860
|
+
if (mode === "batch-triggered" && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
|
|
7550
7861
|
const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.meta.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
|
|
7551
7862
|
syncOps.push(syncOp);
|
|
7552
7863
|
lastJobByDoc.set(batch.documentId, jobId);
|
|
7553
|
-
if (mode ===
|
|
7864
|
+
if (mode === "batch-triggered") prevChainJobId = jobId;
|
|
7554
7865
|
}
|
|
7555
7866
|
remote.channel.outbox.add(...syncOps);
|
|
7556
7867
|
this.evictPastOutboxBound(remote);
|
|
@@ -7907,7 +8218,7 @@ var Reactor = class {
|
|
|
7907
8218
|
const jobInfo = {
|
|
7908
8219
|
id: jobId,
|
|
7909
8220
|
documentId: job.documentId,
|
|
7910
|
-
status:
|
|
8221
|
+
status: "PENDING",
|
|
7911
8222
|
createdAtUtcIso,
|
|
7912
8223
|
consistencyToken: {
|
|
7913
8224
|
version: 1,
|
|
@@ -7949,7 +8260,7 @@ var Reactor = class {
|
|
|
7949
8260
|
const jobInfo = {
|
|
7950
8261
|
id: jobId,
|
|
7951
8262
|
documentId: job.documentId,
|
|
7952
|
-
status:
|
|
8263
|
+
status: "PENDING",
|
|
7953
8264
|
createdAtUtcIso,
|
|
7954
8265
|
consistencyToken: {
|
|
7955
8266
|
version: 1,
|
|
@@ -7987,7 +8298,7 @@ var Reactor = class {
|
|
|
7987
8298
|
const jobInfo = {
|
|
7988
8299
|
id: jobId,
|
|
7989
8300
|
documentId: job.documentId,
|
|
7990
|
-
status:
|
|
8301
|
+
status: "PENDING",
|
|
7991
8302
|
createdAtUtcIso,
|
|
7992
8303
|
consistencyToken: {
|
|
7993
8304
|
version: 1,
|
|
@@ -8027,7 +8338,7 @@ var Reactor = class {
|
|
|
8027
8338
|
const jobInfo = {
|
|
8028
8339
|
id: jobId,
|
|
8029
8340
|
documentId: job.documentId,
|
|
8030
|
-
status:
|
|
8341
|
+
status: "PENDING",
|
|
8031
8342
|
createdAtUtcIso,
|
|
8032
8343
|
consistencyToken: {
|
|
8033
8344
|
version: 1,
|
|
@@ -8062,7 +8373,7 @@ var Reactor = class {
|
|
|
8062
8373
|
const jobInfo = {
|
|
8063
8374
|
id: planKeyToJobId.get(jobPlan.key),
|
|
8064
8375
|
documentId: jobPlan.documentId,
|
|
8065
|
-
status:
|
|
8376
|
+
status: "PENDING",
|
|
8066
8377
|
createdAtUtcIso,
|
|
8067
8378
|
consistencyToken: {
|
|
8068
8379
|
version: 1,
|
|
@@ -8132,7 +8443,7 @@ var Reactor = class {
|
|
|
8132
8443
|
const jobInfo = {
|
|
8133
8444
|
id: planKeyToJobId.get(jobPlan.key),
|
|
8134
8445
|
documentId: jobPlan.documentId,
|
|
8135
|
-
status:
|
|
8446
|
+
status: "PENDING",
|
|
8136
8447
|
createdAtUtcIso,
|
|
8137
8448
|
consistencyToken: {
|
|
8138
8449
|
version: 1,
|
|
@@ -8221,7 +8532,7 @@ var Reactor = class {
|
|
|
8221
8532
|
return Promise.resolve({
|
|
8222
8533
|
id: jobId,
|
|
8223
8534
|
documentId: "",
|
|
8224
|
-
status:
|
|
8535
|
+
status: "FAILED",
|
|
8225
8536
|
createdAtUtcIso: now,
|
|
8226
8537
|
completedAtUtcIso: now,
|
|
8227
8538
|
error: toErrorInfo$1("Job not found"),
|
|
@@ -8572,11 +8883,12 @@ var ReactorBuilder = class {
|
|
|
8572
8883
|
if (resolver instanceof DocumentModelResolver) resolver.setModelLoadedHook((documentType) => eventBus.emit(ReactorEventTypes.MODEL_LOADED, { documentType }));
|
|
8573
8884
|
const queue = this.queueInstance ?? new InMemoryQueue(eventBus, resolver);
|
|
8574
8885
|
const jobTracker = new InMemoryJobTracker(eventBus);
|
|
8575
|
-
const
|
|
8886
|
+
const cacheConfig = {
|
|
8576
8887
|
maxDocuments: this.writeCacheConfig?.maxDocuments ?? 100,
|
|
8577
8888
|
ringBufferSize: this.writeCacheConfig?.ringBufferSize ?? 10,
|
|
8578
8889
|
keyframeInterval: this.writeCacheConfig?.keyframeInterval ?? 10
|
|
8579
|
-
}
|
|
8890
|
+
};
|
|
8891
|
+
const writeCache = new KyselyWriteCache(keyframeStore, operationStore, documentModelRegistry, cacheConfig);
|
|
8580
8892
|
await writeCache.startup();
|
|
8581
8893
|
const operationIndex = new KyselyOperationIndex(database);
|
|
8582
8894
|
const documentMetaCache = new DocumentMetaCache(operationStore, { maxDocuments: 1e3 });
|
|
@@ -8585,22 +8897,24 @@ var ReactorBuilder = class {
|
|
|
8585
8897
|
const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
|
|
8586
8898
|
let executorManager = this.executorManager;
|
|
8587
8899
|
let executorStartCount = this.executorConfig.maxConcurrency ?? 1;
|
|
8588
|
-
if (!executorManager)
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
if (
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8900
|
+
if (!executorManager) {
|
|
8901
|
+
if (this.workerPool) {
|
|
8902
|
+
const pool = this.workerPool;
|
|
8903
|
+
let factory = pool.factory;
|
|
8904
|
+
if (!factory) {
|
|
8905
|
+
if (pool.db === void 0) throw new Error("unreachable: worker pool configured without db or factory");
|
|
8906
|
+
if (this.signer && !this.workerSigner) this.logger.warn("Worker pool has no signer spec; pooled workers store synthesized operations unsigned");
|
|
8907
|
+
if (this.trustPolicy && !this.workerTrustPolicy) this.logger.warn("Worker pool has no trust policy spec; pooled workers apply the default trust policy");
|
|
8908
|
+
factory = await this.createDefaultWorkerFactory(pool.numWorkers, pool.db);
|
|
8909
|
+
}
|
|
8910
|
+
const poolManager = new WorkerPoolJobExecutorManager(factory, eventBus, queue, jobTracker, this.logger, resolver, operationIndex, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
|
|
8911
|
+
executorManager = poolManager;
|
|
8912
|
+
executorStartCount = pool.numWorkers;
|
|
8913
|
+
if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
|
|
8914
|
+
} else executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, executionScope, this.signer, this.trustPolicy), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
|
|
8915
|
+
}
|
|
8602
8916
|
await executorManager.start(executorStartCount);
|
|
8603
|
-
const callerReadModels = Array.from(new Set([...this.readModels]));
|
|
8917
|
+
const callerReadModels = Array.from(/* @__PURE__ */ new Set([...this.readModels]));
|
|
8604
8918
|
const degradedComponents = [];
|
|
8605
8919
|
const startDegraded = (component, error) => {
|
|
8606
8920
|
degradedComponents.push({
|
|
@@ -8614,7 +8928,7 @@ var ReactorBuilder = class {
|
|
|
8614
8928
|
yieldDeadlineMs: this.executorConfig.yieldDeadlineMs ?? 50
|
|
8615
8929
|
};
|
|
8616
8930
|
const documentViewConsistencyTracker = new ConsistencyTracker();
|
|
8617
|
-
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions ?
|
|
8931
|
+
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions ? "StateAtDeletion" : "NotFound", readModelIndexing);
|
|
8618
8932
|
try {
|
|
8619
8933
|
await documentView.init();
|
|
8620
8934
|
} catch (error) {
|
|
@@ -8670,7 +8984,7 @@ var ReactorBuilder = class {
|
|
|
8670
8984
|
const reactor = new Reactor(this.logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, this.features, documentView, documentIndexer, operationStore, eventBus, executorManager);
|
|
8671
8985
|
let syncModule = void 0;
|
|
8672
8986
|
if (this.channelScheme) {
|
|
8673
|
-
const factory = this.channelScheme ===
|
|
8987
|
+
const factory = this.channelScheme === "connect" ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
|
|
8674
8988
|
syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
|
|
8675
8989
|
await syncModule.syncManager.startup();
|
|
8676
8990
|
} else if (this.syncBuilder) {
|
|
@@ -8771,7 +9085,7 @@ var ReactorBuilder = class {
|
|
|
8771
9085
|
poolInstrumentations.push(forwarder);
|
|
8772
9086
|
this.instrumentedPools.push(forwarder);
|
|
8773
9087
|
}
|
|
8774
|
-
const { ProjectionShardManager } = await import("./projection-shard-manager-
|
|
9088
|
+
const { ProjectionShardManager } = await import("./projection-shard-manager-BpcqDtnQ.js");
|
|
8775
9089
|
const manager = new ProjectionShardManager({
|
|
8776
9090
|
shardCount: config.shardCount,
|
|
8777
9091
|
db,
|
|
@@ -8805,7 +9119,7 @@ var ReactorBuilder = class {
|
|
|
8805
9119
|
return manager;
|
|
8806
9120
|
}
|
|
8807
9121
|
async createDefaultProjectionWorkerFactory() {
|
|
8808
|
-
const [{ createProjectionThreadTransport }, { projectionWorkerEntryPath }] = await Promise.all([import("./transport-
|
|
9122
|
+
const [{ createProjectionThreadTransport }, { projectionWorkerEntryPath }] = await Promise.all([import("./transport-B3G50KVd.js"), import("./projection-worker-D8uNFdwX.js")]);
|
|
8809
9123
|
return () => createProjectionThreadTransport(projectionWorkerEntryPath);
|
|
8810
9124
|
}
|
|
8811
9125
|
/**
|
|
@@ -8815,9 +9129,9 @@ var ReactorBuilder = class {
|
|
|
8815
9129
|
*/
|
|
8816
9130
|
async createDefaultWorkerFactory(numWorkers, db) {
|
|
8817
9131
|
const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
|
|
8818
|
-
import("./worker-handle-
|
|
8819
|
-
import("./transport-
|
|
8820
|
-
|
|
9132
|
+
import("./worker-handle-C2rLUHwv.js"),
|
|
9133
|
+
import("./transport-CZDHzfvy.js"),
|
|
9134
|
+
Promise.resolve().then(() => worker_exports)
|
|
8821
9135
|
]);
|
|
8822
9136
|
const poolConfig = {
|
|
8823
9137
|
enabled: true,
|
|
@@ -9291,6 +9605,23 @@ const SIGNATURE_REFUSAL_CODES = [
|
|
|
9291
9605
|
"SIGNER_UNAUTHORIZED"
|
|
9292
9606
|
];
|
|
9293
9607
|
//#endregion
|
|
9608
|
+
//#region src/executor/worker/index.ts
|
|
9609
|
+
var worker_exports = /* @__PURE__ */ __exportAll({
|
|
9610
|
+
createForwardingLogger: () => createForwardingLogger,
|
|
9611
|
+
errorToInfo: () => errorToInfo,
|
|
9612
|
+
sanitizeArg: () => sanitizeArg,
|
|
9613
|
+
workerEntryPath: () => workerEntryPath
|
|
9614
|
+
});
|
|
9615
|
+
function resolveWorkerEntryPath() {
|
|
9616
|
+
const url = new URL("./entry.js", import.meta.url);
|
|
9617
|
+
if (url.protocol !== "file:") return url.href;
|
|
9618
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
9619
|
+
if (/^\/[A-Za-z]:[/\\]/.test(pathname)) pathname = pathname.slice(1);
|
|
9620
|
+
return pathname;
|
|
9621
|
+
}
|
|
9622
|
+
/** Absolute path to the worker entry script for use with `new Worker(workerEntryPath)`. */
|
|
9623
|
+
const workerEntryPath = resolveWorkerEntryPath();
|
|
9624
|
+
//#endregion
|
|
9294
9625
|
//#region src/decision/sync-scope-gate.ts
|
|
9295
9626
|
/** What a subject may read of a document that is not there to be read. */
|
|
9296
9627
|
const META_ONLY = (scope) => ALWAYS_READABLE_SCOPES.has(scope);
|
|
@@ -9302,6 +9633,9 @@ const META_ONLY = (scope) => ALWAYS_READABLE_SCOPES.has(scope);
|
|
|
9302
9633
|
* path's own job, and it is the only thing this adds to the gate it wraps.
|
|
9303
9634
|
*/
|
|
9304
9635
|
var SyncScopeGate = class {
|
|
9636
|
+
gate;
|
|
9637
|
+
documentView;
|
|
9638
|
+
logger;
|
|
9305
9639
|
constructor(gate, documentView, logger) {
|
|
9306
9640
|
this.gate = gate;
|
|
9307
9641
|
this.documentView = documentView;
|