@powerhousedao/reactor 6.2.3-dev.32 → 6.2.3-dev.33
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-Bvoba-AV.js → build-worker-executor-BonPsKdx.js} +2 -2
- package/dist/{build-worker-executor-Bvoba-AV.js.map → build-worker-executor-BonPsKdx.js.map} +1 -1
- package/dist/{document-indexer-BVTGbEB0.js → document-indexer-I6i_YzsR.js} +759 -130
- package/dist/document-indexer-I6i_YzsR.js.map +1 -0
- package/dist/entry.js +2 -2
- package/dist/{forwarding-logger-CnuVDVze.js → forwarding-logger-BkwlvD9W.js} +484 -110
- package/dist/forwarding-logger-BkwlvD9W.js.map +1 -0
- package/dist/index.d.ts +2318 -1790
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1122 -140
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +54 -6
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-BpcqDtnQ.js → projection-shard-manager-CJIdjsd8.js} +23 -3
- package/dist/projection-shard-manager-CJIdjsd8.js.map +1 -0
- package/dist/{worker-handle-C2rLUHwv.js → worker-handle-DjbEQkq-.js} +2 -2
- package/dist/{worker-handle-C2rLUHwv.js.map → worker-handle-DjbEQkq-.js.map} +1 -1
- package/package.json +5 -4
- package/dist/document-indexer-BVTGbEB0.js.map +0 -1
- package/dist/forwarding-logger-CnuVDVze.js.map +0 -1
- package/dist/projection-shard-manager-BpcqDtnQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as OptimisticLockError, A as DEFAULT_DEFERRED_JOB_TTL_MS, B as summarizeSubmittedActions, C as SettledWatermark, D as snapshotFunctionsFor, E as parseSnapshot, F as ModuleNotFoundError, G as authDecisionModel, H as decideAtHead, I as GATED_DOCUMENT_ACTIONS, J as APPEND_CONDITION_FAILED_PREFIX, K as buildDecisionModel, L as createConsistencyToken, M as DuplicateManifestError, N as DuplicateModuleError, O as DocumentMetaCache, P as InvalidModuleError, Q as DuplicateOperationError, R as createEmptyConsistencyToken, S as ProbeSettler, T as describeWaitingSessions, U as selectDecisionModel, V as targetDocumentId, W as documentDecisionModel, X as DocumentAlreadyExistsError, Y as AppendConditionFailedError, Z as DocumentExistence, _ as KyselyExecutionScope, _t as throwIfAborted, a as DEFAULT_DRIVE_CONTAINER_TYPES, at as DocumentNotFoundError, b as KyselyWriteCache, c as runMigrations, ct as InvalidSignatureError, d as KyselyOperationStore, dt as UnsupportedStoredProtocolError, et as RevisionMismatchError, f as KyselyKeyframeStore, ft as UpgradePreconditionFailedError, g as PassthroughSigner, gt as parsePagingOptions, h as verifyActionSignature, ht as matchesScope, it as DocumentDeletedError, j as JobExecutorEventTypes, k as CollectionMembershipCache, l as createForwardingPoolInstrumentation, lt as RelationshipNotFoundError, m as SimpleJobExecutor, mt as ReactorEventTypes, n as errorToInfo, nt as AuthTimestampNotMonotonicError, o as REACTOR_SCHEMA, ot as ExcessiveReshuffleError, p as DocumentModelRegistry, pt as EventBusAggregateError, q as DriveCollectionId, r as sanitizeArg, rt as AuthorizationDeniedError, s as getMigrationStatus, st as InvalidOperationTimestampError, t as createForwardingLogger, tt as AuthEnforcementDisabledError, u as instrumentPgPool, ut as UnsupportedProtocolVersionError, v as resolveFeatureFlags, w as createKyselyWatermarkProbe, x as KyselyOperationIndex, y as EventBus, yt as __exportAll, z as submittedActionIds } from "./forwarding-logger-BkwlvD9W.js";
|
|
2
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
|
|
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";
|
|
3
|
+
import { _ as ContiguousCursor, a as KyselyDocumentView, b as isCatchUpConsumer, c as ReadModelCoordinator, d as reserveBatch, f as BaseReadModel, g as unchunkedReadModelIndexingConfig, h as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as indexReserved, m as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as DEFAULT_COMMIT_CHUNK_SIZE, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as releaseBatch, v as defaultCatchUpConfig, y as CatchUpScheduler } from "./document-indexer-I6i_YzsR.js";
|
|
4
|
+
import { DEFAULT_SIGNATURE_POLICY, DowngradeNotSupportedError, MAX_AUTH_GRANTS, PEER_CAPABILITIES, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, canonicalJson, coversLocal, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, holdReason, isOlderManifest, legacySupports, localPeerManifest, localSupports, mergePeerCapabilities, normalizeDocumentModelVersion, peerSupports, readPeerManifest, requestedSignaturePolicy, selectProtocolVersions, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
5
5
|
import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
|
|
6
6
|
import { v4 } from "uuid";
|
|
7
7
|
import { ConsoleLogger, childLogger } from "document-model";
|
|
@@ -311,6 +311,16 @@ let JobStatus = /* @__PURE__ */ function(JobStatus) {
|
|
|
311
311
|
* for batch execution. The back-reference is captured but never invoked
|
|
312
312
|
* during construction, so the partial-`this` hazard does not apply.
|
|
313
313
|
*/
|
|
314
|
+
/** A copy keeps its source's versions; selection fills only the keys it lacks. */
|
|
315
|
+
function withSelectedVersions(header, selected) {
|
|
316
|
+
return {
|
|
317
|
+
documentType: header.documentType,
|
|
318
|
+
protocolVersions: header.protocolVersions ? {
|
|
319
|
+
...selected,
|
|
320
|
+
...header.protocolVersions
|
|
321
|
+
} : void 0
|
|
322
|
+
};
|
|
323
|
+
}
|
|
314
324
|
var DriveClient = class {
|
|
315
325
|
client;
|
|
316
326
|
logger;
|
|
@@ -328,7 +338,10 @@ var DriveClient = class {
|
|
|
328
338
|
name: input.global.name || "",
|
|
329
339
|
icon: input.global.icon ?? null,
|
|
330
340
|
nodes: []
|
|
331
|
-
} }), requestedSignaturePolicy(input, await this.client.getCreateSignaturePolicy()), { protocolVersions:
|
|
341
|
+
} }), requestedSignaturePolicy(input, await this.client.getCreateSignaturePolicy()), { protocolVersions: {
|
|
342
|
+
...await this.client.getCreateProtocolVersions(void 0, signal),
|
|
343
|
+
...input.protocolVersions
|
|
344
|
+
} });
|
|
332
345
|
if (input.preferredEditor) driveDoc.header.meta = {
|
|
333
346
|
...driveDoc.header.meta,
|
|
334
347
|
preferredEditor: input.preferredEditor
|
|
@@ -468,13 +481,14 @@ var DriveClient = class {
|
|
|
468
481
|
resolvedNamesByTargetId.set(entry.targetId, resolved);
|
|
469
482
|
}
|
|
470
483
|
const policy = await this.client.getCreateSignaturePolicy();
|
|
484
|
+
const base = await this.client.getCreateProtocolVersions(drive.header.id, signal);
|
|
471
485
|
for (const entry of copyPlan) {
|
|
472
486
|
const node = drive.state.global.nodes.find((n) => n.id === entry.srcId);
|
|
473
487
|
if (!node || !isFileNode(node)) continue;
|
|
474
488
|
const srcDoc = await this.client.get(entry.srcId, void 0, signal);
|
|
475
489
|
const duplicated = {
|
|
476
490
|
...srcDoc,
|
|
477
|
-
header: createCopyHeader(srcDoc.header, entry.targetId, policy),
|
|
491
|
+
header: createCopyHeader(withSelectedVersions(srcDoc.header, base), entry.targetId, policy),
|
|
478
492
|
initialState: srcDoc.state,
|
|
479
493
|
operations: {}
|
|
480
494
|
};
|
|
@@ -1242,8 +1256,14 @@ var ReactorClient = class {
|
|
|
1242
1256
|
eventReads;
|
|
1243
1257
|
actionEvaluation;
|
|
1244
1258
|
createSignaturePolicy;
|
|
1259
|
+
protocolSelection;
|
|
1245
1260
|
drives;
|
|
1246
|
-
constructor(logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, readGate = new BareReadGate(), actionEvaluation, createSignaturePolicy = DEFAULT_SIGNATURE_POLICY
|
|
1261
|
+
constructor(logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, readGate = new BareReadGate(), actionEvaluation, createSignaturePolicy = DEFAULT_SIGNATURE_POLICY, protocolSelection = {
|
|
1262
|
+
capabilities: PEER_CAPABILITIES,
|
|
1263
|
+
flags: {},
|
|
1264
|
+
collectionsOf: () => Promise.resolve({})
|
|
1265
|
+
}) {
|
|
1266
|
+
this.protocolSelection = protocolSelection;
|
|
1247
1267
|
this.logger = logger;
|
|
1248
1268
|
this.reactor = reactor;
|
|
1249
1269
|
this.signer = signer;
|
|
@@ -1477,6 +1497,27 @@ var ReactorClient = class {
|
|
|
1477
1497
|
getCreateSignaturePolicy() {
|
|
1478
1498
|
return Promise.resolve(this.createSignaturePolicy);
|
|
1479
1499
|
}
|
|
1500
|
+
async getCreateProtocolVersions(parentIdentifier, signal) {
|
|
1501
|
+
const { capabilities, flags, agreement, collectionsOf } = this.protocolSelection;
|
|
1502
|
+
let members = [];
|
|
1503
|
+
if (parentIdentifier !== void 0 && agreement) {
|
|
1504
|
+
let parentId;
|
|
1505
|
+
try {
|
|
1506
|
+
parentId = await this.resolveIdOrSlug(parentIdentifier, void 0, signal);
|
|
1507
|
+
} catch {
|
|
1508
|
+
parentId = void 0;
|
|
1509
|
+
}
|
|
1510
|
+
if (parentId !== void 0) {
|
|
1511
|
+
const collectionIds = [...(await collectionsOf([parentId]))[parentId] ?? [], DriveCollectionId.forDrive(parentId).key];
|
|
1512
|
+
members = [...agreement().members(collectionIds).values()];
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return selectProtocolVersions({
|
|
1516
|
+
capabilities,
|
|
1517
|
+
flags,
|
|
1518
|
+
members
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1480
1521
|
/**
|
|
1481
1522
|
* Creates a document and waits for completion
|
|
1482
1523
|
*/
|
|
@@ -1557,7 +1598,11 @@ var ReactorClient = class {
|
|
|
1557
1598
|
}, void 0);
|
|
1558
1599
|
if (!module) throw new Error(`Document model not found for type: ${documentModelType}`);
|
|
1559
1600
|
}
|
|
1560
|
-
const
|
|
1601
|
+
const base = await this.getCreateProtocolVersions(options?.parentIdentifier, signal);
|
|
1602
|
+
const document = withSignaturePolicy(module.utils.createDocument(), requestedSignaturePolicy(options, this.createSignaturePolicy), { protocolVersions: {
|
|
1603
|
+
...base,
|
|
1604
|
+
...options?.protocolVersions
|
|
1605
|
+
} });
|
|
1561
1606
|
document.state.document.version = normalizeDocumentModelVersion(module.version);
|
|
1562
1607
|
return this.create(document, options?.parentIdentifier, signal);
|
|
1563
1608
|
}
|
|
@@ -2189,7 +2234,7 @@ var JobResultHandler = class {
|
|
|
2189
2234
|
callbacks.deferJob(result.error.documentId, handle.job);
|
|
2190
2235
|
return;
|
|
2191
2236
|
}
|
|
2192
|
-
if (result.error && (DocumentDeletedError.isError(result.error) || DocumentAlreadyExistsError.isError(result.error) || DocumentNotFoundError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error) || InvalidSignatureError.isError(result.error) || UpgradePreconditionFailedError.isError(result.error))) {
|
|
2237
|
+
if (result.error && (DocumentDeletedError.isError(result.error) || DocumentAlreadyExistsError.isError(result.error) || DocumentNotFoundError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error) || InvalidSignatureError.isError(result.error) || UpgradePreconditionFailedError.isError(result.error) || UnsupportedProtocolVersionError.isError(result.error))) {
|
|
2193
2238
|
const errorInfo = toErrorInfo(result.error);
|
|
2194
2239
|
this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
|
|
2195
2240
|
this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
|
|
@@ -2710,6 +2755,65 @@ function fillMissingMemberships(documentIds, found) {
|
|
|
2710
2755
|
return result;
|
|
2711
2756
|
}
|
|
2712
2757
|
//#endregion
|
|
2758
|
+
//#region src/storage/kysely/stored-protocol-versions.ts
|
|
2759
|
+
const created = sql.raw(`scope = 'document' and "index" = 0 and (action->'input'->'protocolVersions') is not null`);
|
|
2760
|
+
const key = sql.raw(`md5((action->'input'->'protocolVersions')::text)`);
|
|
2761
|
+
/** Distinct creation protocolVersions, one index probe per value. */
|
|
2762
|
+
async function storedProtocolVersions(db, schema) {
|
|
2763
|
+
const operation = sql.id(schema, "Operation");
|
|
2764
|
+
return (await sql`
|
|
2765
|
+
with recursive hashes(hash) as (
|
|
2766
|
+
select (select ${key} from ${operation} where ${created} order by 1 limit 1)
|
|
2767
|
+
union all
|
|
2768
|
+
select (
|
|
2769
|
+
select ${key} from ${operation}
|
|
2770
|
+
where ${created} and ${key} > hashes.hash
|
|
2771
|
+
order by 1 limit 1
|
|
2772
|
+
)
|
|
2773
|
+
from hashes where hashes.hash is not null
|
|
2774
|
+
)
|
|
2775
|
+
select hash, (
|
|
2776
|
+
select action->'input'->'protocolVersions' from ${operation}
|
|
2777
|
+
where ${created} and ${key} = hashes.hash limit 1
|
|
2778
|
+
) as versions
|
|
2779
|
+
from hashes where hash is not null
|
|
2780
|
+
`.execute(db)).rows;
|
|
2781
|
+
}
|
|
2782
|
+
/** Documents created with any of the protocolVersions `hashes` name. */
|
|
2783
|
+
async function countDocumentsCreatedWith(db, schema, hashes) {
|
|
2784
|
+
if (hashes.length === 0) return 0;
|
|
2785
|
+
const operation = sql.id(schema, "Operation");
|
|
2786
|
+
const result = await sql`
|
|
2787
|
+
select count(distinct "documentId") as documents from ${operation}
|
|
2788
|
+
where ${created} and ${key} in (${sql.join(hashes)})
|
|
2789
|
+
`.execute(db);
|
|
2790
|
+
return Number(result.rows[0]?.documents ?? 0);
|
|
2791
|
+
}
|
|
2792
|
+
//#endregion
|
|
2793
|
+
//#region src/core/stored-protocol-check.ts
|
|
2794
|
+
/** Refuses to run below a version the store holds, unless told read-only. */
|
|
2795
|
+
async function checkStoredProtocols(db, schema, support, mode, logger) {
|
|
2796
|
+
const unsupported = /* @__PURE__ */ new Map();
|
|
2797
|
+
const hashes = [];
|
|
2798
|
+
for (const { hash, versions } of await storedProtocolVersions(db, schema)) {
|
|
2799
|
+
let refused = false;
|
|
2800
|
+
for (const [protocol, version] of Object.entries(versions)) {
|
|
2801
|
+
const supported = support[protocol];
|
|
2802
|
+
if (supported === void 0 || supported.includes(version)) continue;
|
|
2803
|
+
unsupported.set(`${protocol}:${version}`, {
|
|
2804
|
+
protocol,
|
|
2805
|
+
version
|
|
2806
|
+
});
|
|
2807
|
+
refused = true;
|
|
2808
|
+
}
|
|
2809
|
+
if (refused) hashes.push(hash);
|
|
2810
|
+
}
|
|
2811
|
+
if (hashes.length === 0) return;
|
|
2812
|
+
const error = new UnsupportedStoredProtocolError([...unsupported.values()], await countDocumentsCreatedWith(db, schema, hashes));
|
|
2813
|
+
if (mode === "refuse") throw error;
|
|
2814
|
+
logger.warn("Starting with documents read-only: @message", error.message);
|
|
2815
|
+
}
|
|
2816
|
+
//#endregion
|
|
2713
2817
|
//#region src/executor/simple-job-executor-manager.ts
|
|
2714
2818
|
/**
|
|
2715
2819
|
* Manages multiple job executors and coordinates job distribution.
|
|
@@ -3293,9 +3397,9 @@ var ProcessorQueue = class {
|
|
|
3293
3397
|
}
|
|
3294
3398
|
async raiseCursor(through) {
|
|
3295
3399
|
const { cursor } = this.options;
|
|
3296
|
-
|
|
3297
|
-
if (
|
|
3298
|
-
cursor.lastOrdinal =
|
|
3400
|
+
const capped = Math.min(through, this.options.confirmedThrough());
|
|
3401
|
+
if (cursor.status !== "active" || capped <= cursor.lastOrdinal) return;
|
|
3402
|
+
cursor.lastOrdinal = capped;
|
|
3299
3403
|
await this.persist();
|
|
3300
3404
|
}
|
|
3301
3405
|
async parkBelow(missed) {
|
|
@@ -3327,6 +3431,8 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3327
3431
|
knownDrives = /* @__PURE__ */ new Map();
|
|
3328
3432
|
highestRoutedOrdinal = 0;
|
|
3329
3433
|
cursorCache = /* @__PURE__ */ new Map();
|
|
3434
|
+
persistedOrdinals = /* @__PURE__ */ new Map();
|
|
3435
|
+
advancing = Promise.resolve();
|
|
3330
3436
|
cursorWrites = /* @__PURE__ */ new Map();
|
|
3331
3437
|
draining = /* @__PURE__ */ new Map();
|
|
3332
3438
|
logger;
|
|
@@ -3566,6 +3672,7 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3566
3672
|
floor,
|
|
3567
3673
|
readSince: (ordinal) => this.operationIndex.getSinceOrdinal(ordinal),
|
|
3568
3674
|
routedThrough: () => this.highWater(),
|
|
3675
|
+
confirmedThrough: () => this.lastOrdinal,
|
|
3569
3676
|
persist: (state) => this.writeCursor(tracked, state),
|
|
3570
3677
|
logger: this.logger
|
|
3571
3678
|
});
|
|
@@ -3574,6 +3681,16 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3574
3681
|
queue
|
|
3575
3682
|
};
|
|
3576
3683
|
}
|
|
3684
|
+
/** Resolves once every queue has taken the latest cursor advance. */
|
|
3685
|
+
whenCursorsAdvanced() {
|
|
3686
|
+
return this.advancing;
|
|
3687
|
+
}
|
|
3688
|
+
/** Every bound processor may now move up to the manager's cursor. */
|
|
3689
|
+
onCursorAdvanced(appliedThrough) {
|
|
3690
|
+
const advances = [];
|
|
3691
|
+
for (const { queue } of this.allBound()) advances.push(queue.advance(appliedThrough));
|
|
3692
|
+
this.advancing = Promise.all(advances).then(() => void 0);
|
|
3693
|
+
}
|
|
3577
3694
|
async discard(slot, disconnect) {
|
|
3578
3695
|
try {
|
|
3579
3696
|
await disconnect();
|
|
@@ -3599,7 +3716,10 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3599
3716
|
}
|
|
3600
3717
|
async loadAllCursors() {
|
|
3601
3718
|
const rows = await this.db.selectFrom("ProcessorCursor").selectAll().execute();
|
|
3602
|
-
for (const row of rows)
|
|
3719
|
+
for (const row of rows) {
|
|
3720
|
+
this.cursorCache.set(row.processorId, row);
|
|
3721
|
+
this.persistedOrdinals.set(row.processorId, row.lastOrdinal);
|
|
3722
|
+
}
|
|
3603
3723
|
}
|
|
3604
3724
|
lane(processorId, write) {
|
|
3605
3725
|
const next = (this.cursorWrites.get(processorId) ?? Promise.resolve()).then(write).then(() => void 0, (error) => {
|
|
@@ -3626,7 +3746,28 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3626
3746
|
updatedAt: now
|
|
3627
3747
|
};
|
|
3628
3748
|
this.cursorCache.set(row.processorId, row);
|
|
3629
|
-
return this.lane(row.processorId, () =>
|
|
3749
|
+
return this.lane(row.processorId, async () => {
|
|
3750
|
+
const expected = this.persistedOrdinals.get(row.processorId);
|
|
3751
|
+
if (expected === void 0) {
|
|
3752
|
+
await this.insertCursor(row, now);
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
const result = await this.db.updateTable("ProcessorCursor").set({
|
|
3756
|
+
lastOrdinal: row.lastOrdinal,
|
|
3757
|
+
status: row.status,
|
|
3758
|
+
lastError: row.lastError,
|
|
3759
|
+
lastErrorTimestamp: row.lastErrorTimestamp,
|
|
3760
|
+
updatedAt: now
|
|
3761
|
+
}).where("processorId", "=", row.processorId).where("lastOrdinal", "=", expected).executeTakeFirst();
|
|
3762
|
+
if (Number(result.numUpdatedRows) > 0) {
|
|
3763
|
+
this.persistedOrdinals.set(row.processorId, row.lastOrdinal);
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
await this.reconcileCursor(tracked, row, now);
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
async insertCursor(row, now) {
|
|
3770
|
+
await this.db.insertInto("ProcessorCursor").values({
|
|
3630
3771
|
processorId: row.processorId,
|
|
3631
3772
|
factoryId: row.factoryId,
|
|
3632
3773
|
driveId: row.driveId,
|
|
@@ -3642,13 +3783,29 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3642
3783
|
lastError: row.lastError,
|
|
3643
3784
|
lastErrorTimestamp: row.lastErrorTimestamp,
|
|
3644
3785
|
updatedAt: now
|
|
3645
|
-
})).execute()
|
|
3786
|
+
})).execute();
|
|
3787
|
+
this.persistedOrdinals.set(row.processorId, row.lastOrdinal);
|
|
3788
|
+
}
|
|
3789
|
+
/** A failed compare-and-set: a lowered row resets the cursor and backfills. */
|
|
3790
|
+
async reconcileCursor(tracked, row, now) {
|
|
3791
|
+
const stored = await this.db.selectFrom("ProcessorCursor").select("lastOrdinal").where("processorId", "=", row.processorId).executeTakeFirst();
|
|
3792
|
+
if (stored === void 0) {
|
|
3793
|
+
await this.insertCursor(row, now);
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
this.persistedOrdinals.set(row.processorId, stored.lastOrdinal);
|
|
3797
|
+
if (stored.lastOrdinal >= row.lastOrdinal) return;
|
|
3798
|
+
this.logger.info("Processor '@ProcessorId' cursor lowered externally from @Old to @New; replaying", row.processorId, row.lastOrdinal, stored.lastOrdinal);
|
|
3799
|
+
tracked.lastOrdinal = stored.lastOrdinal;
|
|
3800
|
+
row.lastOrdinal = stored.lastOrdinal;
|
|
3801
|
+
for (const bound of this.allBound()) if (bound.tracked.processorId === row.processorId) bound.queue.backfill();
|
|
3646
3802
|
}
|
|
3647
3803
|
deleteCursors(matches) {
|
|
3648
3804
|
const deletes = [];
|
|
3649
3805
|
for (const [processorId, row] of this.cursorCache) {
|
|
3650
3806
|
if (!matches(row)) continue;
|
|
3651
3807
|
this.cursorCache.delete(processorId);
|
|
3808
|
+
this.persistedOrdinals.delete(processorId);
|
|
3652
3809
|
deletes.push(this.lane(processorId, () => this.db.deleteFrom("ProcessorCursor").where("processorId", "=", processorId).execute()));
|
|
3653
3810
|
}
|
|
3654
3811
|
return deletes;
|
|
@@ -4567,6 +4724,10 @@ var SubscriptionNotificationReadModel = class {
|
|
|
4567
4724
|
};
|
|
4568
4725
|
//#endregion
|
|
4569
4726
|
//#region src/core/group-reevaluation-trigger.ts
|
|
4727
|
+
const GROUP_REEVALUATION_TRIGGER = "group-reevaluation-trigger";
|
|
4728
|
+
function isMembershipChange({ operation, context }) {
|
|
4729
|
+
return context.documentType === groupDocumentType && context.scope === "global" && groupMembershipActionTypes.includes(operation.action.type);
|
|
4730
|
+
}
|
|
4570
4731
|
/**
|
|
4571
4732
|
* Watches committed writes for group membership changes and enqueues a
|
|
4572
4733
|
* re-evaluation job for every document whose auth history references the
|
|
@@ -4578,33 +4739,49 @@ var SubscriptionNotificationReadModel = class {
|
|
|
4578
4739
|
* skips the pass when everything the document holds sorts before it, which
|
|
4579
4740
|
* keeps the common case (a membership write later than all history) free.
|
|
4580
4741
|
*/
|
|
4581
|
-
var GroupReevaluationTrigger = class {
|
|
4742
|
+
var GroupReevaluationTrigger = class extends BaseReadModel {
|
|
4582
4743
|
logger;
|
|
4583
4744
|
eventBus;
|
|
4584
4745
|
queue;
|
|
4585
|
-
operationIndex;
|
|
4586
4746
|
unsubscribe;
|
|
4587
|
-
constructor(logger, eventBus, queue, operationIndex) {
|
|
4747
|
+
constructor(logger, eventBus, queue, operationIndex, db) {
|
|
4748
|
+
super(db, operationIndex, {}, new ConsistencyTracker(), {
|
|
4749
|
+
readModelId: GROUP_REEVALUATION_TRIGGER,
|
|
4750
|
+
rebuildStateOnInit: false,
|
|
4751
|
+
indexing: unchunkedReadModelIndexingConfig,
|
|
4752
|
+
startFrom: "head",
|
|
4753
|
+
replayStreamSuffix: false
|
|
4754
|
+
});
|
|
4588
4755
|
this.logger = logger;
|
|
4589
4756
|
this.eventBus = eventBus;
|
|
4590
4757
|
this.queue = queue;
|
|
4591
|
-
this.operationIndex = operationIndex;
|
|
4592
4758
|
}
|
|
4593
|
-
|
|
4759
|
+
/** A first start begins at the watermark: it has no history to trust. */
|
|
4760
|
+
async startup() {
|
|
4761
|
+
await this.init();
|
|
4594
4762
|
this.unsubscribe = this.eventBus.subscribe(ReactorEventTypes.JOB_WRITE_READY, async (_type, event) => this.onWriteReady(event));
|
|
4595
4763
|
}
|
|
4596
4764
|
shutdown() {
|
|
4597
4765
|
this.unsubscribe?.();
|
|
4598
4766
|
this.unsubscribe = void 0;
|
|
4599
4767
|
}
|
|
4600
|
-
|
|
4768
|
+
/** Sweeps membership changes only; other operations never hold the cursor. */
|
|
4769
|
+
async sweep(settledThrough, _present, signal) {
|
|
4770
|
+
signal?.throwIfAborted();
|
|
4771
|
+
const rows = await this.db.selectFrom("operation_index_operations").select("ordinal").where("ordinal", ">", this.appliedThrough).where("ordinal", "<=", settledThrough).where("documentType", "=", groupDocumentType).where("scope", "=", "global").where(sql`action->>'type'`, "in", [...groupMembershipActionTypes]).orderBy("ordinal", "asc").execute();
|
|
4772
|
+
return super.sweep(settledThrough, rows.map((row) => Number(row.ordinal)), signal);
|
|
4773
|
+
}
|
|
4774
|
+
/** Throws when any lookup or enqueue failed, so the batch is retried. */
|
|
4775
|
+
async commitOperations(items) {
|
|
4601
4776
|
const changed = /* @__PURE__ */ new Map();
|
|
4602
|
-
for (const
|
|
4603
|
-
if (
|
|
4777
|
+
for (const item of items) {
|
|
4778
|
+
if (!isMembershipChange(item)) continue;
|
|
4779
|
+
const { operation, context } = item;
|
|
4604
4780
|
const existing = changed.get(context.documentId);
|
|
4605
4781
|
if (existing === void 0 || Date.parse(operation.timestampUtcMs) < Date.parse(existing)) changed.set(context.documentId, operation.timestampUtcMs);
|
|
4606
4782
|
}
|
|
4607
4783
|
if (changed.size === 0) return;
|
|
4784
|
+
const failures = [];
|
|
4608
4785
|
const affected = /* @__PURE__ */ new Map();
|
|
4609
4786
|
for (const [groupId, timestamp] of changed) {
|
|
4610
4787
|
let referencers;
|
|
@@ -4612,6 +4789,7 @@ var GroupReevaluationTrigger = class {
|
|
|
4612
4789
|
referencers = await this.operationIndex.getGroupReferencers(groupId);
|
|
4613
4790
|
} catch (error) {
|
|
4614
4791
|
this.logger.error("Failed to resolve referencers of group @groupId: @error", groupId, error);
|
|
4792
|
+
failures.push(error);
|
|
4615
4793
|
continue;
|
|
4616
4794
|
}
|
|
4617
4795
|
for (const documentId of referencers) {
|
|
@@ -4642,8 +4820,17 @@ var GroupReevaluationTrigger = class {
|
|
|
4642
4820
|
await this.queue.enqueue(job);
|
|
4643
4821
|
} catch (error) {
|
|
4644
4822
|
this.logger.error("Failed to enqueue re-evaluation of @documentId: @error", documentId, error);
|
|
4823
|
+
failures.push(error);
|
|
4645
4824
|
}
|
|
4646
4825
|
}
|
|
4826
|
+
if (failures.length > 0) throw new AggregateError(failures, "group re-evaluation was not enqueued for every affected document");
|
|
4827
|
+
}
|
|
4828
|
+
async onWriteReady(event) {
|
|
4829
|
+
const changes = event.operations.filter(isMembershipChange);
|
|
4830
|
+
if (changes.length === 0) return;
|
|
4831
|
+
try {
|
|
4832
|
+
await this.indexOperations(changes);
|
|
4833
|
+
} catch {}
|
|
4647
4834
|
}
|
|
4648
4835
|
};
|
|
4649
4836
|
//#endregion
|
|
@@ -4689,7 +4876,9 @@ const SyncEventTypes = {
|
|
|
4689
4876
|
SYNC_SUCCEEDED: 20002,
|
|
4690
4877
|
SYNC_FAILED: 20003,
|
|
4691
4878
|
DEAD_LETTER_ADDED: 20004,
|
|
4692
|
-
CONNECTION_STATE_CHANGED: 20005
|
|
4879
|
+
CONNECTION_STATE_CHANGED: 20005,
|
|
4880
|
+
SYNC_HELD: 20006,
|
|
4881
|
+
SYNC_RELEASED: 20007
|
|
4693
4882
|
};
|
|
4694
4883
|
//#endregion
|
|
4695
4884
|
//#region src/sync/mailbox.ts
|
|
@@ -5431,6 +5620,7 @@ function classifyJobFailure(errorName) {
|
|
|
5431
5620
|
case "ExcessiveReshuffleError": return "EXCESSIVE_SHUFFLE";
|
|
5432
5621
|
case "InvalidSignatureError": return "SIGNATURE_INVALID";
|
|
5433
5622
|
case "HashMismatchError": return "HASH_MISMATCH";
|
|
5623
|
+
case "UnsupportedProtocolVersionError": return "UNSUPPORTED_PROTOCOL";
|
|
5434
5624
|
default: return "UNCLASSIFIED";
|
|
5435
5625
|
}
|
|
5436
5626
|
}
|
|
@@ -5439,10 +5629,15 @@ function syncOperationErrorType(error) {
|
|
|
5439
5629
|
return error?.errorType ?? classifyJobFailure(error?.error?.name ?? "Error");
|
|
5440
5630
|
}
|
|
5441
5631
|
/** Dead-letter types that must not stop the document syncing. */
|
|
5442
|
-
const NON_QUARANTINING_ERROR_TYPES = /* @__PURE__ */ new Set([
|
|
5632
|
+
const NON_QUARANTINING_ERROR_TYPES = /* @__PURE__ */ new Set([
|
|
5633
|
+
"AUTH_TIMESTAMP_NOT_MONOTONIC",
|
|
5634
|
+
"UNSUPPORTED_PROTOCOL",
|
|
5635
|
+
"PEER_PROTOCOL_UNSUPPORTED"
|
|
5636
|
+
]);
|
|
5443
5637
|
/**
|
|
5444
5638
|
* A held auth operation must not quarantine: reconciling the two policies needs
|
|
5445
|
-
* the traffic a quarantine would stop.
|
|
5639
|
+
* the traffic a quarantine would stop. A protocol refusal concerns one peer,
|
|
5640
|
+
* and quarantine is global to the document.
|
|
5446
5641
|
*/
|
|
5447
5642
|
function quarantinesDocument(errorType) {
|
|
5448
5643
|
return !NON_QUARANTINING_ERROR_TYPES.has(errorType);
|
|
@@ -5676,6 +5871,27 @@ const getLatestAppliedOrdinal = (syncOps) => {
|
|
|
5676
5871
|
* the remote is known to serve them.
|
|
5677
5872
|
*/
|
|
5678
5873
|
const DECISION_FIELDS = ["deniedReason", "errorType"];
|
|
5874
|
+
/** Fields peer agreement added; a remote without them is a silent peer. */
|
|
5875
|
+
const AGREEMENT_FIELDS = [
|
|
5876
|
+
"manifest",
|
|
5877
|
+
"manifestRevision",
|
|
5878
|
+
"peerManifestRevision",
|
|
5879
|
+
"refusals",
|
|
5880
|
+
"SyncRefusalInput"
|
|
5881
|
+
];
|
|
5882
|
+
/** How often a channel whose remote went silent asks again whether it serves agreement. */
|
|
5883
|
+
const AGREEMENT_PROBE_INTERVAL_MS = 3e5;
|
|
5884
|
+
function pushMutation(withGatedUnder) {
|
|
5885
|
+
return withGatedUnder ? `
|
|
5886
|
+
mutation PushSyncEnvelopes($envelopes: [SyncEnvelopeInput!]!, $peerManifestRevision: String) {
|
|
5887
|
+
pushSyncEnvelopes(envelopes: $envelopes, peerManifestRevision: $peerManifestRevision)
|
|
5888
|
+
}
|
|
5889
|
+
` : `
|
|
5890
|
+
mutation PushSyncEnvelopes($envelopes: [SyncEnvelopeInput!]!) {
|
|
5891
|
+
pushSyncEnvelopes(envelopes: $envelopes)
|
|
5892
|
+
}
|
|
5893
|
+
`;
|
|
5894
|
+
}
|
|
5679
5895
|
/**
|
|
5680
5896
|
* GraphQL-based synchronization channel for network communication between reactors.
|
|
5681
5897
|
*/
|
|
@@ -5706,6 +5922,16 @@ var GqlRequestChannel = class {
|
|
|
5706
5922
|
receivingPages = false;
|
|
5707
5923
|
/** Cleared for good the first time the remote rejects {@link DECISION_FIELDS}. */
|
|
5708
5924
|
peerServesDecisionFields = true;
|
|
5925
|
+
/** Cleared when the remote rejects {@link AGREEMENT_FIELDS}; set again by a touch that carries them. */
|
|
5926
|
+
peerServesAgreement = true;
|
|
5927
|
+
agreementStoppedUtcMs = 0;
|
|
5928
|
+
localManifestProvider;
|
|
5929
|
+
/** Undefined until the first handshake; null for a silent peer. */
|
|
5930
|
+
peerManifest = void 0;
|
|
5931
|
+
peerManifestCallbacks = /* @__PURE__ */ new Set();
|
|
5932
|
+
manifestRefresh;
|
|
5933
|
+
/** Polled rows this reactor could not run, reported on the next poll. */
|
|
5934
|
+
pendingRefusals = /* @__PURE__ */ new Map();
|
|
5709
5935
|
isRecovering = false;
|
|
5710
5936
|
connectionState = "connecting";
|
|
5711
5937
|
/** Latest unrecoverable error was an auth rejection; cleared on connect. */
|
|
@@ -5734,7 +5960,13 @@ var GqlRequestChannel = class {
|
|
|
5734
5960
|
this.outbox = this.bufferedOutbox;
|
|
5735
5961
|
this.deadLetter = new Mailbox();
|
|
5736
5962
|
this.deadLetter.onAdded((syncOps) => {
|
|
5737
|
-
for (const syncOp of syncOps)
|
|
5963
|
+
for (const syncOp of syncOps) {
|
|
5964
|
+
this.logger.warn("Dead letter added for document @DocumentId on channel @ChannelId", syncOp.documentId, this.channelId);
|
|
5965
|
+
if (syncOp.error?.source === "inbox" && syncOperationErrorType(syncOp.error) === "UNSUPPORTED_PROTOCOL") this.pendingRefusals.set(`${syncOp.documentId}:${syncOp.branch}`, {
|
|
5966
|
+
documentId: syncOp.documentId,
|
|
5967
|
+
branch: syncOp.branch
|
|
5968
|
+
});
|
|
5969
|
+
}
|
|
5738
5970
|
});
|
|
5739
5971
|
this.outbox.onAdded((syncOps) => {
|
|
5740
5972
|
if (this.isShutdown) return;
|
|
@@ -5750,13 +5982,13 @@ var GqlRequestChannel = class {
|
|
|
5750
5982
|
this.attemptPush(syncOps);
|
|
5751
5983
|
});
|
|
5752
5984
|
this.outbox.onRemoved((syncOps) => {
|
|
5753
|
-
const
|
|
5754
|
-
if (
|
|
5755
|
-
this.lastPersistedOutboxOrdinal =
|
|
5985
|
+
const ordinal = Math.min(getLatestAppliedOrdinal(syncOps), this.unappliedFloor() - 1);
|
|
5986
|
+
if (ordinal > this.lastPersistedOutboxOrdinal) {
|
|
5987
|
+
this.lastPersistedOutboxOrdinal = ordinal;
|
|
5756
5988
|
this.cursorStorage.upsert({
|
|
5757
5989
|
remoteName: this.remoteName,
|
|
5758
5990
|
cursorType: "outbox",
|
|
5759
|
-
cursorOrdinal:
|
|
5991
|
+
cursorOrdinal: ordinal,
|
|
5760
5992
|
lastSyncedAtUtcMs: Date.now()
|
|
5761
5993
|
}).catch((error) => {
|
|
5762
5994
|
this.logger.error("Failed to update outbox cursor for @ChannelId! This means that future application runs may resend duplicate operations. This is recoverable (with deduplication protection), but not-optimal: @Error", this.channelId, error);
|
|
@@ -5819,6 +6051,58 @@ var GqlRequestChannel = class {
|
|
|
5819
6051
|
notePoll() {}
|
|
5820
6052
|
/** No holder, so nothing this channel reports may strand one. */
|
|
5821
6053
|
lastHolderPollUtcMs() {}
|
|
6054
|
+
setLocalManifest(provider) {
|
|
6055
|
+
this.localManifestProvider = provider;
|
|
6056
|
+
}
|
|
6057
|
+
onPeerManifest(callback) {
|
|
6058
|
+
this.peerManifestCallbacks.add(callback);
|
|
6059
|
+
return () => {
|
|
6060
|
+
this.peerManifestCallbacks.delete(callback);
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
async hearPeer(manifest, undelivered) {
|
|
6064
|
+
if (this.peerManifest !== void 0 && (this.peerManifest?.revision ?? null) === (manifest?.revision ?? null) || isOlderManifest(manifest, this.peerManifest)) return;
|
|
6065
|
+
this.peerManifest = manifest;
|
|
6066
|
+
await Promise.all([...this.peerManifestCallbacks].map(async (callback) => {
|
|
6067
|
+
try {
|
|
6068
|
+
await callback(manifest, undelivered);
|
|
6069
|
+
} catch (error) {
|
|
6070
|
+
this.logger.error("Peer manifest callback error: @Error", error);
|
|
6071
|
+
}
|
|
6072
|
+
}));
|
|
6073
|
+
}
|
|
6074
|
+
/**
|
|
6075
|
+
* Re-touches once when either side's manifest moved; touching is idempotent.
|
|
6076
|
+
* False when the refresh failed, so polled rows must not be judged yet.
|
|
6077
|
+
*/
|
|
6078
|
+
async refreshManifestsIfStale(manifestRevision, peerManifestRevision) {
|
|
6079
|
+
if (!this.peerServesAgreement || typeof manifestRevision !== "string") return true;
|
|
6080
|
+
const local = this.localManifestProvider?.().revision ?? null;
|
|
6081
|
+
if (manifestRevision === (this.peerManifest?.revision ?? null) && (peerManifestRevision ?? null) === local) return true;
|
|
6082
|
+
if (this.isShutdown) return false;
|
|
6083
|
+
this.manifestRefresh ??= this.touchRemoteChannel().then(({ ackOrdinal }) => {
|
|
6084
|
+
if (ackOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
|
|
6085
|
+
}).finally(() => {
|
|
6086
|
+
this.manifestRefresh = void 0;
|
|
6087
|
+
});
|
|
6088
|
+
try {
|
|
6089
|
+
await this.manifestRefresh;
|
|
6090
|
+
return true;
|
|
6091
|
+
} catch (error) {
|
|
6092
|
+
this.logger.error("GqlChannel @ChannelId manifest refresh failed: @Error", this.channelId, error);
|
|
6093
|
+
return false;
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
/** Re-touches a silent remote in case it was upgraded. */
|
|
6097
|
+
async probeAgreement() {
|
|
6098
|
+
this.agreementStoppedUtcMs = Date.now();
|
|
6099
|
+
try {
|
|
6100
|
+
const { ackOrdinal } = await this.touchRemoteChannel();
|
|
6101
|
+
if (ackOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
|
|
6102
|
+
} catch (error) {
|
|
6103
|
+
this.logger.error("GqlChannel @ChannelId agreement probe failed: @Error", this.channelId, error);
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
5822
6106
|
/**
|
|
5823
6107
|
* Initializes the channel by registering it on the remote server and starting polling.
|
|
5824
6108
|
*/
|
|
@@ -5852,6 +6136,7 @@ var GqlRequestChannel = class {
|
|
|
5852
6136
|
*/
|
|
5853
6137
|
async poll() {
|
|
5854
6138
|
if (this.isShutdown) return;
|
|
6139
|
+
if (!this.peerServesAgreement && Date.now() - this.agreementStoppedUtcMs >= AGREEMENT_PROBE_INTERVAL_MS) await this.probeAgreement();
|
|
5855
6140
|
let response;
|
|
5856
6141
|
try {
|
|
5857
6142
|
response = await this.pollSyncEnvelopes(this.inbox.ackOrdinal, this.inbox.latestOrdinal);
|
|
@@ -5859,8 +6144,9 @@ var GqlRequestChannel = class {
|
|
|
5859
6144
|
if (!this.handlePollError(error)) throw error;
|
|
5860
6145
|
return;
|
|
5861
6146
|
}
|
|
5862
|
-
const { envelopes, ackOrdinal, deadLetters, hasMore } = response;
|
|
6147
|
+
const { envelopes, ackOrdinal, deadLetters, hasMore, manifestRevision, peerManifestRevision } = response;
|
|
5863
6148
|
if (ackOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, ackOrdinal);
|
|
6149
|
+
if (!await this.refreshManifestsIfStale(manifestRevision, peerManifestRevision)) return;
|
|
5864
6150
|
const allSyncOps = [];
|
|
5865
6151
|
for (const envelope of envelopes) if (envelope.type.toLowerCase() === "operations" && envelope.operations) {
|
|
5866
6152
|
const syncOps = envelopesToSyncOperations(envelope, this.remoteName);
|
|
@@ -5986,21 +6272,51 @@ var GqlRequestChannel = class {
|
|
|
5986
6272
|
outboxLatest: latestOrdinal
|
|
5987
6273
|
};
|
|
5988
6274
|
let response;
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
6275
|
+
let refusals = [];
|
|
6276
|
+
for (;;) {
|
|
6277
|
+
const revision = this.peerServesAgreement ? this.localManifestProvider?.().revision : void 0;
|
|
6278
|
+
refusals = this.peerServesAgreement ? [...this.pendingRefusals.values()] : [];
|
|
6279
|
+
try {
|
|
6280
|
+
response = await this.executeGraphQL(this.pollQuery(this.peerServesDecisionFields, this.peerServesAgreement), this.peerServesAgreement ? {
|
|
6281
|
+
...variables,
|
|
6282
|
+
manifestRevision: revision,
|
|
6283
|
+
refusals
|
|
6284
|
+
} : variables);
|
|
6285
|
+
break;
|
|
6286
|
+
} catch (error) {
|
|
6287
|
+
if (this.rejectsAgreementFields(error)) {
|
|
6288
|
+
await this.stopAgreement();
|
|
6289
|
+
continue;
|
|
6290
|
+
}
|
|
6291
|
+
if (!this.rejectsDecisionFields(error)) throw error;
|
|
6292
|
+
this.logger.warn("Remote @channelId does not serve deniedReason/errorType; polling without them. The remote is on an older schema, so it has neither to report.", this.channelId);
|
|
6293
|
+
this.peerServesDecisionFields = false;
|
|
6294
|
+
}
|
|
5996
6295
|
}
|
|
6296
|
+
for (const refusal of refusals) this.pendingRefusals.delete(`${refusal.documentId}:${refusal.branch}`);
|
|
5997
6297
|
return {
|
|
5998
6298
|
envelopes: response.pollSyncEnvelopes.envelopes,
|
|
5999
6299
|
ackOrdinal: response.pollSyncEnvelopes.ackOrdinal,
|
|
6000
6300
|
deadLetters: response.pollSyncEnvelopes.deadLetters ?? [],
|
|
6001
|
-
hasMore: response.pollSyncEnvelopes.hasMore
|
|
6301
|
+
hasMore: response.pollSyncEnvelopes.hasMore,
|
|
6302
|
+
manifestRevision: response.pollSyncEnvelopes.manifestRevision,
|
|
6303
|
+
peerManifestRevision: response.pollSyncEnvelopes.peerManifestRevision
|
|
6002
6304
|
};
|
|
6003
6305
|
}
|
|
6306
|
+
rejectsAgreementFields(error) {
|
|
6307
|
+
return this.peerServesAgreement && this.isAgreementRejection(error);
|
|
6308
|
+
}
|
|
6309
|
+
isAgreementRejection(error) {
|
|
6310
|
+
if (!(error instanceof GraphQLRequestError) || error.category !== "graphql") return false;
|
|
6311
|
+
return AGREEMENT_FIELDS.some((field) => error.message.includes(field));
|
|
6312
|
+
}
|
|
6313
|
+
/** Resolves once what the silent peer cannot run is held. */
|
|
6314
|
+
async stopAgreement(undelivered) {
|
|
6315
|
+
this.logger.warn("Remote @channelId does not serve peer manifests; treating it as a silent peer.", this.channelId);
|
|
6316
|
+
this.peerServesAgreement = false;
|
|
6317
|
+
this.agreementStoppedUtcMs = Date.now();
|
|
6318
|
+
await this.hearPeer(null, undelivered);
|
|
6319
|
+
}
|
|
6004
6320
|
/**
|
|
6005
6321
|
* True when the remote rejected the query for naming a field it does not
|
|
6006
6322
|
* have. Selecting an unknown field fails validation for the whole query, so
|
|
@@ -6016,10 +6332,10 @@ var GqlRequestChannel = class {
|
|
|
6016
6332
|
* The poll query. `withDecisionFields` selects the two fields added with the
|
|
6017
6333
|
* auth projection; a remote on the previous schema is polled without them.
|
|
6018
6334
|
*/
|
|
6019
|
-
pollQuery(withDecisionFields) {
|
|
6335
|
+
pollQuery(withDecisionFields, withAgreementFields) {
|
|
6020
6336
|
return `
|
|
6021
|
-
query PollSyncEnvelopes($channelId: String!, $outboxAck: Int!, $outboxLatest: Int!) {
|
|
6022
|
-
pollSyncEnvelopes(channelId: $channelId, outboxAck: $outboxAck, outboxLatest: $outboxLatest) {
|
|
6337
|
+
query PollSyncEnvelopes($channelId: String!, $outboxAck: Int!, $outboxLatest: Int!${withAgreementFields ? ", $manifestRevision: String, $refusals: [SyncRefusalInput!]" : ""}) {
|
|
6338
|
+
pollSyncEnvelopes(channelId: $channelId, outboxAck: $outboxAck, outboxLatest: $outboxLatest${withAgreementFields ? ", manifestRevision: $manifestRevision, refusals: $refusals" : ""}) {
|
|
6023
6339
|
envelopes {
|
|
6024
6340
|
type
|
|
6025
6341
|
channelMeta {
|
|
@@ -6083,6 +6399,7 @@ var GqlRequestChannel = class {
|
|
|
6083
6399
|
operationCount
|
|
6084
6400
|
}
|
|
6085
6401
|
hasMore
|
|
6402
|
+
${withAgreementFields ? "manifestRevision\n peerManifestRevision" : ""}
|
|
6086
6403
|
}
|
|
6087
6404
|
}
|
|
6088
6405
|
`;
|
|
@@ -6097,27 +6414,40 @@ var GqlRequestChannel = class {
|
|
|
6097
6414
|
const result = await this.operationIndex.getLatestTimestampForCollection(this.config.collectionId.key);
|
|
6098
6415
|
if (result) sinceTimestampUtcMs = result;
|
|
6099
6416
|
} catch {}
|
|
6100
|
-
const
|
|
6417
|
+
const touch = (withAgreement) => {
|
|
6418
|
+
const manifest = withAgreement ? this.localManifestProvider?.() : void 0;
|
|
6419
|
+
return this.executeGraphQL(`
|
|
6101
6420
|
mutation TouchChannel($input: TouchChannelInput!) {
|
|
6102
6421
|
touchChannel(input: $input) {
|
|
6103
6422
|
success
|
|
6104
6423
|
ackOrdinal
|
|
6424
|
+
${withAgreement ? "manifest" : ""}
|
|
6105
6425
|
}
|
|
6106
6426
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6427
|
+
`, { input: {
|
|
6428
|
+
id: this.channelId,
|
|
6429
|
+
name: this.channelId,
|
|
6430
|
+
collectionId: this.config.collectionId.key,
|
|
6431
|
+
filter: {
|
|
6432
|
+
documentId: this.config.filter.documentId,
|
|
6433
|
+
scope: this.config.filter.scope,
|
|
6434
|
+
branch: this.config.filter.branch
|
|
6435
|
+
},
|
|
6436
|
+
sinceTimestampUtcMs,
|
|
6437
|
+
...manifest ? { manifest } : {}
|
|
6438
|
+
} });
|
|
6439
|
+
};
|
|
6440
|
+
let data;
|
|
6441
|
+
try {
|
|
6442
|
+
data = await touch(true);
|
|
6443
|
+
this.peerServesAgreement = true;
|
|
6444
|
+
} catch (error) {
|
|
6445
|
+
if (!this.isAgreementRejection(error)) throw error;
|
|
6446
|
+
await this.stopAgreement();
|
|
6447
|
+
data = await touch(false);
|
|
6448
|
+
}
|
|
6120
6449
|
if (!data.touchChannel.success) throw new GraphQLRequestError("touchChannel returned success=false", "graphql");
|
|
6450
|
+
await this.hearPeer(this.peerServesAgreement ? readPeerManifest(data.touchChannel.manifest) : null);
|
|
6121
6451
|
return { ackOrdinal: data.touchChannel.ackOrdinal };
|
|
6122
6452
|
}
|
|
6123
6453
|
/**
|
|
@@ -6214,6 +6544,24 @@ var GqlRequestChannel = class {
|
|
|
6214
6544
|
*/
|
|
6215
6545
|
async pushSyncOperations(syncOps) {
|
|
6216
6546
|
for (const syncOp of syncOps) syncOp.started();
|
|
6547
|
+
const gatedUnder = this.peerServesAgreement ? this.peerManifest?.revision : void 0;
|
|
6548
|
+
try {
|
|
6549
|
+
await this.executeGraphQL(pushMutation(gatedUnder !== void 0), {
|
|
6550
|
+
envelopes: this.envelopesFor(syncOps),
|
|
6551
|
+
...gatedUnder !== void 0 ? { peerManifestRevision: gatedUnder } : {}
|
|
6552
|
+
});
|
|
6553
|
+
return;
|
|
6554
|
+
} catch (error) {
|
|
6555
|
+
if (gatedUnder === void 0 || !this.rejectsAgreementFields(error)) throw error;
|
|
6556
|
+
}
|
|
6557
|
+
await this.stopAgreement(syncOps);
|
|
6558
|
+
const unsent = new Set(this.outbox.items);
|
|
6559
|
+
const remaining = syncOps.filter((syncOp) => unsent.has(syncOp));
|
|
6560
|
+
if (remaining.length === 0) return;
|
|
6561
|
+
await this.executeGraphQL(pushMutation(false), { envelopes: this.envelopesFor(remaining) });
|
|
6562
|
+
}
|
|
6563
|
+
/** One envelope per SyncOperation, with key/dependsOn for batch ordering. */
|
|
6564
|
+
envelopesFor(syncOps) {
|
|
6217
6565
|
const jobIdToKeys = /* @__PURE__ */ new Map();
|
|
6218
6566
|
const envelopes = [];
|
|
6219
6567
|
for (let i = 0; i < syncOps.length; i++) {
|
|
@@ -6237,13 +6585,7 @@ var GqlRequestChannel = class {
|
|
|
6237
6585
|
dependsOn
|
|
6238
6586
|
});
|
|
6239
6587
|
}
|
|
6240
|
-
|
|
6241
|
-
mutation PushSyncEnvelopes($envelopes: [SyncEnvelopeInput!]!) {
|
|
6242
|
-
pushSyncEnvelopes(envelopes: $envelopes)
|
|
6243
|
-
}
|
|
6244
|
-
`;
|
|
6245
|
-
const variables = { envelopes: envelopes.map((e) => serializeEnvelope(e)) };
|
|
6246
|
-
await this.executeGraphQL(mutation, variables);
|
|
6588
|
+
return envelopes.map((e) => serializeEnvelope(e));
|
|
6247
6589
|
}
|
|
6248
6590
|
/**
|
|
6249
6591
|
* Gets the authorization header value using jwtHandler.
|
|
@@ -6296,6 +6638,18 @@ var GqlRequestChannel = class {
|
|
|
6296
6638
|
get poller() {
|
|
6297
6639
|
return this.pollTimer;
|
|
6298
6640
|
}
|
|
6641
|
+
/** The lowest ordinal of an outbox item the remote has not applied. */
|
|
6642
|
+
unappliedFloor() {
|
|
6643
|
+
let floor = Number.POSITIVE_INFINITY;
|
|
6644
|
+
for (const syncOp of this.outbox.items) {
|
|
6645
|
+
if (syncOp.status === 2) continue;
|
|
6646
|
+
for (const op of syncOp.operations) {
|
|
6647
|
+
const ordinal = op.context.ordinal;
|
|
6648
|
+
if (ordinal > 0 && ordinal < floor) floor = ordinal;
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
return floor;
|
|
6652
|
+
}
|
|
6299
6653
|
};
|
|
6300
6654
|
//#endregion
|
|
6301
6655
|
//#region src/sync/channels/gql-request-channel-factory.ts
|
|
@@ -6469,6 +6823,11 @@ var GqlResponseChannel = class {
|
|
|
6469
6823
|
lastHolderPollUtcMs() {
|
|
6470
6824
|
return this.lastPollUtcMs;
|
|
6471
6825
|
}
|
|
6826
|
+
/** The client announces through touchChannel, which the sync manager serves. */
|
|
6827
|
+
setLocalManifest(_provider) {}
|
|
6828
|
+
onPeerManifest(_callback) {
|
|
6829
|
+
return () => {};
|
|
6830
|
+
}
|
|
6472
6831
|
async init() {
|
|
6473
6832
|
const cursors = await this.cursorStorage.list(this.remoteName);
|
|
6474
6833
|
const inboxOrdinal = cursors.find((c) => c.cursorType === "inbox")?.cursorOrdinal ?? 0;
|
|
@@ -6722,9 +7081,67 @@ var KyselySyncDeadLetterStorage = class {
|
|
|
6722
7081
|
}
|
|
6723
7082
|
};
|
|
6724
7083
|
//#endregion
|
|
7084
|
+
//#region src/storage/kysely/sync-hold-storage.ts
|
|
7085
|
+
var KyselySyncHoldStorage = class {
|
|
7086
|
+
db;
|
|
7087
|
+
constructor(db) {
|
|
7088
|
+
this.db = db;
|
|
7089
|
+
}
|
|
7090
|
+
async list(filter = {}) {
|
|
7091
|
+
let query = this.db.selectFrom("sync_holds").selectAll();
|
|
7092
|
+
if (filter.remoteName !== void 0) query = query.where("remote_name", "=", filter.remoteName);
|
|
7093
|
+
if (filter.documentId !== void 0) query = query.where("document_id", "=", filter.documentId);
|
|
7094
|
+
return (await query.orderBy("held_at_utc_ms").execute()).map((row) => ({
|
|
7095
|
+
remoteName: row.remote_name,
|
|
7096
|
+
documentId: row.document_id,
|
|
7097
|
+
branch: row.branch,
|
|
7098
|
+
protocol: row.protocol,
|
|
7099
|
+
version: row.version,
|
|
7100
|
+
heldAtUtcMs: Number(row.held_at_utc_ms)
|
|
7101
|
+
}));
|
|
7102
|
+
}
|
|
7103
|
+
async upsert(hold) {
|
|
7104
|
+
const row = {
|
|
7105
|
+
remote_name: hold.remoteName,
|
|
7106
|
+
document_id: hold.documentId,
|
|
7107
|
+
branch: hold.branch,
|
|
7108
|
+
protocol: hold.protocol,
|
|
7109
|
+
version: hold.version,
|
|
7110
|
+
held_at_utc_ms: hold.heldAtUtcMs
|
|
7111
|
+
};
|
|
7112
|
+
await this.db.insertInto("sync_holds").values(row).onConflict((oc) => oc.columns([
|
|
7113
|
+
"remote_name",
|
|
7114
|
+
"document_id",
|
|
7115
|
+
"branch"
|
|
7116
|
+
]).doUpdateSet({
|
|
7117
|
+
protocol: row.protocol,
|
|
7118
|
+
version: row.version
|
|
7119
|
+
})).execute();
|
|
7120
|
+
}
|
|
7121
|
+
async remove(remoteName, documentId, branch) {
|
|
7122
|
+
await this.db.deleteFrom("sync_holds").where("remote_name", "=", remoteName).where("document_id", "=", documentId).where("branch", "=", branch).execute();
|
|
7123
|
+
}
|
|
7124
|
+
async removeRemote(remoteName) {
|
|
7125
|
+
await this.db.deleteFrom("sync_holds").where("remote_name", "=", remoteName).execute();
|
|
7126
|
+
}
|
|
7127
|
+
};
|
|
7128
|
+
//#endregion
|
|
6725
7129
|
//#region src/storage/kysely/sync-remote-storage.ts
|
|
7130
|
+
function rowToPeer(row) {
|
|
7131
|
+
if (row.peer_manifest_at_utc_ms === null) return;
|
|
7132
|
+
let manifest = null;
|
|
7133
|
+
if (row.peer_manifest !== null) try {
|
|
7134
|
+
manifest = readPeerManifest(JSON.parse(row.peer_manifest));
|
|
7135
|
+
} catch {}
|
|
7136
|
+
return {
|
|
7137
|
+
manifest,
|
|
7138
|
+
receivedAtUtcMs: Number(row.peer_manifest_at_utc_ms)
|
|
7139
|
+
};
|
|
7140
|
+
}
|
|
6726
7141
|
function rowToRemoteRecord(row) {
|
|
7142
|
+
const peer = rowToPeer(row);
|
|
6727
7143
|
return {
|
|
7144
|
+
...peer ? { peer } : {},
|
|
6728
7145
|
id: row.channel_id,
|
|
6729
7146
|
name: row.name,
|
|
6730
7147
|
collectionId: DriveCollectionId.fromKey(row.collection_id),
|
|
@@ -6781,7 +7198,9 @@ function remoteRecordToRow(remote) {
|
|
|
6781
7198
|
pull_last_success_utc_ms: remote.status.pull.lastSuccessUtcMs ? new Date(remote.status.pull.lastSuccessUtcMs).toISOString() : null,
|
|
6782
7199
|
pull_last_failure_utc_ms: remote.status.pull.lastFailureUtcMs ? new Date(remote.status.pull.lastFailureUtcMs).toISOString() : null,
|
|
6783
7200
|
pull_failure_count: remote.status.pull.failureCount,
|
|
6784
|
-
bound_address: remote.options.boundAddress ?? null
|
|
7201
|
+
bound_address: remote.options.boundAddress ?? null,
|
|
7202
|
+
peer_manifest: remote.peer?.manifest ? canonicalJson(remote.peer.manifest, "peer manifest") : null,
|
|
7203
|
+
peer_manifest_at_utc_ms: remote.peer?.receivedAtUtcMs ?? null
|
|
6785
7204
|
};
|
|
6786
7205
|
}
|
|
6787
7206
|
var KyselySyncRemoteStorage = class {
|
|
@@ -6823,22 +7242,31 @@ var KyselySyncRemoteStorage = class {
|
|
|
6823
7242
|
};
|
|
6824
7243
|
//#endregion
|
|
6825
7244
|
//#region src/sync/batch-aggregator.ts
|
|
7245
|
+
/** Queued between write-ready events when the settled watermark advances. */
|
|
7246
|
+
const SETTLED = Symbol("settled");
|
|
6826
7247
|
var BatchAggregator = class {
|
|
6827
7248
|
logger;
|
|
6828
7249
|
driveContainerTypes;
|
|
6829
7250
|
onBatchReady;
|
|
7251
|
+
onSettled;
|
|
6830
7252
|
queue = [];
|
|
6831
7253
|
processing = false;
|
|
6832
7254
|
pendingBatches = /* @__PURE__ */ new Map();
|
|
6833
|
-
constructor(logger, driveContainerTypes, onBatchReady) {
|
|
7255
|
+
constructor(logger, driveContainerTypes, onBatchReady, onSettled) {
|
|
6834
7256
|
this.logger = logger;
|
|
6835
7257
|
this.driveContainerTypes = driveContainerTypes;
|
|
6836
7258
|
this.onBatchReady = onBatchReady;
|
|
7259
|
+
this.onSettled = onSettled;
|
|
6837
7260
|
}
|
|
6838
7261
|
async enqueueWriteReady(event) {
|
|
6839
7262
|
this.queue.push(event);
|
|
6840
7263
|
await this.processQueue();
|
|
6841
7264
|
}
|
|
7265
|
+
/** Runs onSettled on the serial queue; one queued run covers many. */
|
|
7266
|
+
async enqueueSettled() {
|
|
7267
|
+
if (!this.queue.includes(SETTLED)) this.queue.push(SETTLED);
|
|
7268
|
+
await this.processQueue();
|
|
7269
|
+
}
|
|
6842
7270
|
async handleJobFailed(event) {
|
|
6843
7271
|
const batchId = event.job?.meta.batchId;
|
|
6844
7272
|
if (!batchId) return;
|
|
@@ -6856,12 +7284,20 @@ var BatchAggregator = class {
|
|
|
6856
7284
|
this.processing = true;
|
|
6857
7285
|
try {
|
|
6858
7286
|
while (this.queue.length > 0) {
|
|
6859
|
-
const
|
|
7287
|
+
const item = this.queue.shift();
|
|
7288
|
+
if (item === SETTLED) {
|
|
7289
|
+
try {
|
|
7290
|
+
await this.onSettled();
|
|
7291
|
+
} catch (error) {
|
|
7292
|
+
this.logger.error("Failed to derive settled outboxes (@error)", error instanceof Error ? error.message : String(error));
|
|
7293
|
+
}
|
|
7294
|
+
continue;
|
|
7295
|
+
}
|
|
6860
7296
|
try {
|
|
6861
|
-
await this.handleWriteReady(
|
|
7297
|
+
await this.handleWriteReady(item);
|
|
6862
7298
|
} catch (error) {
|
|
6863
7299
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6864
|
-
this.logger.error("Failed to process write-ready event (@jobId, @error)",
|
|
7300
|
+
this.logger.error("Failed to process write-ready event (@jobId, @error)", item.jobId, err.message);
|
|
6865
7301
|
}
|
|
6866
7302
|
}
|
|
6867
7303
|
} finally {
|
|
@@ -6929,6 +7365,59 @@ var BatchAggregator = class {
|
|
|
6929
7365
|
}
|
|
6930
7366
|
};
|
|
6931
7367
|
//#endregion
|
|
7368
|
+
//#region src/sync/memory-hold-storage.ts
|
|
7369
|
+
const keyOf = (remoteName, documentId, branch) => `${remoteName}\u0000${documentId}\u0000${branch}`;
|
|
7370
|
+
/** For a sync manager built without a database. */
|
|
7371
|
+
var InMemorySyncHoldStorage = class {
|
|
7372
|
+
holds = /* @__PURE__ */ new Map();
|
|
7373
|
+
list(filter = {}) {
|
|
7374
|
+
return Promise.resolve([...this.holds.values()].filter((hold) => (filter.remoteName === void 0 || hold.remoteName === filter.remoteName) && (filter.documentId === void 0 || hold.documentId === filter.documentId)));
|
|
7375
|
+
}
|
|
7376
|
+
upsert(hold) {
|
|
7377
|
+
const key = keyOf(hold.remoteName, hold.documentId, hold.branch);
|
|
7378
|
+
const known = this.holds.get(key);
|
|
7379
|
+
this.holds.set(key, {
|
|
7380
|
+
...hold,
|
|
7381
|
+
heldAtUtcMs: known?.heldAtUtcMs ?? hold.heldAtUtcMs
|
|
7382
|
+
});
|
|
7383
|
+
return Promise.resolve();
|
|
7384
|
+
}
|
|
7385
|
+
remove(remoteName, documentId, branch) {
|
|
7386
|
+
this.holds.delete(keyOf(remoteName, documentId, branch));
|
|
7387
|
+
return Promise.resolve();
|
|
7388
|
+
}
|
|
7389
|
+
removeRemote(remoteName) {
|
|
7390
|
+
for (const [key, hold] of this.holds) if (hold.remoteName === remoteName) this.holds.delete(key);
|
|
7391
|
+
return Promise.resolve();
|
|
7392
|
+
}
|
|
7393
|
+
};
|
|
7394
|
+
//#endregion
|
|
7395
|
+
//#region src/sync/peer-agreement.ts
|
|
7396
|
+
/** Pure over the remote records it is given, live or not. */
|
|
7397
|
+
function createPeerAgreement(basis, remotes) {
|
|
7398
|
+
const supportsOf = (remote) => remote.peer?.manifest ?? basis.legacy;
|
|
7399
|
+
const members = (collectionIds) => {
|
|
7400
|
+
const wanted = new Set(collectionIds);
|
|
7401
|
+
const found = /* @__PURE__ */ new Map();
|
|
7402
|
+
for (const remote of remotes) if (wanted.has(remote.collectionId.key)) found.set(remote.name, supportsOf(remote));
|
|
7403
|
+
return found;
|
|
7404
|
+
};
|
|
7405
|
+
return {
|
|
7406
|
+
local: () => basis.local,
|
|
7407
|
+
basis: () => basis,
|
|
7408
|
+
peer: (remoteName) => {
|
|
7409
|
+
const remote = remotes.find((candidate) => candidate.name === remoteName);
|
|
7410
|
+
return remote ? supportsOf(remote) : basis.legacy;
|
|
7411
|
+
},
|
|
7412
|
+
members,
|
|
7413
|
+
limitedBy: (collectionId, protocol) => {
|
|
7414
|
+
const version = basis.wanted[protocol];
|
|
7415
|
+
if (version === void 0) return [];
|
|
7416
|
+
return [...members([collectionId])].filter(([, supports]) => !supports.protocols[protocol]?.includes(version)).map(([name]) => name);
|
|
7417
|
+
}
|
|
7418
|
+
};
|
|
7419
|
+
}
|
|
7420
|
+
//#endregion
|
|
6932
7421
|
//#region src/sync/sync-awaiter.ts
|
|
6933
7422
|
/**
|
|
6934
7423
|
* Provides a promise-based interface for waiting on sync completion.
|
|
@@ -7160,6 +7649,12 @@ function isCredentialOrNetworkError(error) {
|
|
|
7160
7649
|
if (isDriveAuthError(error)) return true;
|
|
7161
7650
|
return error instanceof GraphQLRequestError && error.category === "network";
|
|
7162
7651
|
}
|
|
7652
|
+
const holdKey = (documentId, branch) => `${documentId}\u0000${branch}`;
|
|
7653
|
+
const VERSION_CACHE_CAP = 1e4;
|
|
7654
|
+
/** The versions a CREATE_DOCUMENT among `operations` fixes for the document. */
|
|
7655
|
+
function createdVersions(operations, documentId) {
|
|
7656
|
+
for (const { operation, context } of operations) if (context.documentId === documentId && operation.action.type === "CREATE_DOCUMENT") return operation.action.input.protocolVersions ?? {};
|
|
7657
|
+
}
|
|
7163
7658
|
/** Where a sync operation's run of ordinals begins. */
|
|
7164
7659
|
function firstOrdinalOf(syncOp) {
|
|
7165
7660
|
return syncOp.operations.length > 0 ? syncOp.operations[0].context.ordinal : 0;
|
|
@@ -7194,8 +7689,39 @@ var SyncManager = class {
|
|
|
7194
7689
|
pruneDrainDeferred = false;
|
|
7195
7690
|
removing = /* @__PURE__ */ new Set();
|
|
7196
7691
|
lastEnqueuedJobIdByKey = /* @__PURE__ */ new Map();
|
|
7692
|
+
watermark;
|
|
7693
|
+
owed = /* @__PURE__ */ new Map();
|
|
7694
|
+
settledUnsubscribe;
|
|
7197
7695
|
inboxChunkChain = Promise.resolve();
|
|
7198
|
-
|
|
7696
|
+
capabilities;
|
|
7697
|
+
manifest;
|
|
7698
|
+
localSupport;
|
|
7699
|
+
legacy;
|
|
7700
|
+
wanted = {};
|
|
7701
|
+
holds;
|
|
7702
|
+
heldKeys = /* @__PURE__ */ new Map();
|
|
7703
|
+
records = /* @__PURE__ */ new Map();
|
|
7704
|
+
versionCache = /* @__PURE__ */ new Map();
|
|
7705
|
+
protocolVersionsOf;
|
|
7706
|
+
peerUpdates = /* @__PURE__ */ new Map();
|
|
7707
|
+
peerUnsubscribes = /* @__PURE__ */ new Map();
|
|
7708
|
+
constructor(logger, remoteStorage, cursorStorage, deadLetterStorage, channelFactory, operationIndex, reactor, eventBus, driveContainerTypes, watermark, config = {}, localPeer = {
|
|
7709
|
+
capabilities: PEER_CAPABILITIES,
|
|
7710
|
+
flags: {}
|
|
7711
|
+
}, holds = new InMemorySyncHoldStorage()) {
|
|
7712
|
+
this.watermark = watermark;
|
|
7713
|
+
this.capabilities = localPeer.capabilities;
|
|
7714
|
+
this.manifest = localPeerManifest(localPeer.capabilities, localPeer.flags, localPeer.appKey, localPeer.sequence ?? Date.now());
|
|
7715
|
+
this.localSupport = localSupports(localPeer.capabilities, localPeer.flags);
|
|
7716
|
+
this.legacy = legacySupports(localPeer.capabilities);
|
|
7717
|
+
for (const capability of localPeer.capabilities) {
|
|
7718
|
+
if (capability.kind !== "protocol") continue;
|
|
7719
|
+
const supported = capability.supported(localPeer.flags);
|
|
7720
|
+
const wanted = capability.preferred?.(localPeer.flags) ?? (supported.length > 0 ? Math.max(...supported) : void 0);
|
|
7721
|
+
if (wanted !== void 0) this.wanted[capability.name] = wanted;
|
|
7722
|
+
}
|
|
7723
|
+
this.holds = holds;
|
|
7724
|
+
this.protocolVersionsOf = localPeer.protocolVersionsOf ?? ((documentId, branch) => this.indexedVersions(documentId, branch));
|
|
7199
7725
|
this.logger = logger;
|
|
7200
7726
|
this.remoteStorage = remoteStorage;
|
|
7201
7727
|
this.cursorStorage = cursorStorage;
|
|
@@ -7212,7 +7738,7 @@ var SyncManager = class {
|
|
|
7212
7738
|
this.awaiter = new JobAwaiter(eventBus, (jobId, signal) => reactor.getJobStatus(jobId, signal));
|
|
7213
7739
|
this.syncAwaiter = new SyncAwaiter(eventBus);
|
|
7214
7740
|
this.isShutdown = false;
|
|
7215
|
-
this.batchAggregator = new BatchAggregator(logger, driveContainerTypes, (batch) => this.processCompleteBatch(batch));
|
|
7741
|
+
this.batchAggregator = new BatchAggregator(logger, driveContainerTypes, (batch) => this.processCompleteBatch(batch), () => this.deriveSettled());
|
|
7216
7742
|
this.syncStatusTracker = new SyncStatusTracker();
|
|
7217
7743
|
}
|
|
7218
7744
|
async startup() {
|
|
@@ -7223,7 +7749,13 @@ var SyncManager = class {
|
|
|
7223
7749
|
} catch (error) {
|
|
7224
7750
|
this.logger.error("Failed to load quarantined document IDs (@error)", error instanceof Error ? error.message : String(error));
|
|
7225
7751
|
}
|
|
7752
|
+
try {
|
|
7753
|
+
for (const hold of await this.holds.list()) this.heldFor(hold.remoteName).set(holdKey(hold.documentId, hold.branch), hold);
|
|
7754
|
+
} catch (error) {
|
|
7755
|
+
this.logger.error("Failed to load sync holds (@error)", error instanceof Error ? error.message : String(error));
|
|
7756
|
+
}
|
|
7226
7757
|
const remoteRecords = await this.remoteStorage.list();
|
|
7758
|
+
const head = await this.watermarkHead();
|
|
7227
7759
|
for (const record of remoteRecords) {
|
|
7228
7760
|
const channel = this.channelFactory.instance(record.id, record.name, record.channelConfig, this.cursorStorage, record.collectionId, record.filter, this.operationIndex, record.options);
|
|
7229
7761
|
const remote = {
|
|
@@ -7233,11 +7765,14 @@ var SyncManager = class {
|
|
|
7233
7765
|
collectionId: record.collectionId,
|
|
7234
7766
|
channelConfig: record.channelConfig,
|
|
7235
7767
|
filter: record.filter,
|
|
7236
|
-
options: record.options
|
|
7768
|
+
options: record.options,
|
|
7769
|
+
peer: record.peer
|
|
7237
7770
|
},
|
|
7238
7771
|
channel
|
|
7239
7772
|
};
|
|
7240
7773
|
this.remotes.set(record.name, remote);
|
|
7774
|
+
this.owe(record.name, head);
|
|
7775
|
+
this.records.set(record.name, remote.meta);
|
|
7241
7776
|
await this.loadDeadLetters(remote);
|
|
7242
7777
|
this.wireChannelCallbacks(remote);
|
|
7243
7778
|
try {
|
|
@@ -7247,6 +7782,7 @@ var SyncManager = class {
|
|
|
7247
7782
|
await this.dropRemoteAfterFailedInit(remote, false);
|
|
7248
7783
|
continue;
|
|
7249
7784
|
}
|
|
7785
|
+
await this.peerUpdates.get(record.name);
|
|
7250
7786
|
const outboxAckOrdinal = remote.channel.outbox.ackOrdinal;
|
|
7251
7787
|
if (outboxAckOrdinal > 0) {
|
|
7252
7788
|
const backfillController = new AbortController();
|
|
@@ -7260,6 +7796,7 @@ var SyncManager = class {
|
|
|
7260
7796
|
});
|
|
7261
7797
|
}
|
|
7262
7798
|
}
|
|
7799
|
+
this.settledUnsubscribe = this.watermark.onAdvance(() => void this.batchAggregator.enqueueSettled());
|
|
7263
7800
|
this.eventUnsubscribe = this.eventBus.subscribe(ReactorEventTypes.JOB_WRITE_READY, async (_type, event) => this.batchAggregator.enqueueWriteReady(event));
|
|
7264
7801
|
this.failedEventUnsubscribe = this.eventBus.subscribe(ReactorEventTypes.JOB_FAILED, async (_type, event) => this.batchAggregator.handleJobFailed(event));
|
|
7265
7802
|
}
|
|
@@ -7273,6 +7810,9 @@ var SyncManager = class {
|
|
|
7273
7810
|
this.prunePending.clear();
|
|
7274
7811
|
this.pruneDrainDeferred = false;
|
|
7275
7812
|
this.batchAggregator.clear();
|
|
7813
|
+
this.owed.clear();
|
|
7814
|
+
this.settledUnsubscribe?.();
|
|
7815
|
+
this.settledUnsubscribe = void 0;
|
|
7276
7816
|
if (this.eventUnsubscribe) {
|
|
7277
7817
|
this.eventUnsubscribe();
|
|
7278
7818
|
this.eventUnsubscribe = void 0;
|
|
@@ -7286,6 +7826,9 @@ var SyncManager = class {
|
|
|
7286
7826
|
this.syncStatusTracker.clear();
|
|
7287
7827
|
for (const unsub of this.connectionStateUnsubscribes.values()) unsub();
|
|
7288
7828
|
this.connectionStateUnsubscribes.clear();
|
|
7829
|
+
for (const unsub of this.peerUnsubscribes.values()) unsub();
|
|
7830
|
+
this.peerUnsubscribes.clear();
|
|
7831
|
+
this.peerUpdates.clear();
|
|
7289
7832
|
const promises = [];
|
|
7290
7833
|
for (const remote of this.remotes.values()) promises.push(remote.channel.shutdown());
|
|
7291
7834
|
this.remotes.clear();
|
|
@@ -7312,54 +7855,316 @@ var SyncManager = class {
|
|
|
7312
7855
|
...remote.meta.options,
|
|
7313
7856
|
boundAddress
|
|
7314
7857
|
};
|
|
7315
|
-
await this.remoteStorage.upsert(
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7858
|
+
await this.remoteStorage.upsert(this.recordOf(remote.meta));
|
|
7859
|
+
}
|
|
7860
|
+
localManifest() {
|
|
7861
|
+
return this.manifest;
|
|
7862
|
+
}
|
|
7863
|
+
async setPeerManifest(id, manifest) {
|
|
7864
|
+
await this.queuePeerManifest(this.getById(id), manifest);
|
|
7865
|
+
}
|
|
7866
|
+
/** Applied in arrival order per remote; the handshake awaits the tail. */
|
|
7867
|
+
queuePeerManifest(remote, manifest, undelivered) {
|
|
7868
|
+
const name = remote.meta.name;
|
|
7869
|
+
const next = (this.peerUpdates.get(name) ?? Promise.resolve()).then(() => this.applyPeerManifest(remote, manifest, undelivered));
|
|
7870
|
+
const settled = next.catch((error) => {
|
|
7871
|
+
this.logger.error("Failed to record the peer manifest of @name: @error", name, error instanceof Error ? error.message : String(error));
|
|
7872
|
+
});
|
|
7873
|
+
this.peerUpdates.set(name, settled);
|
|
7874
|
+
return next;
|
|
7875
|
+
}
|
|
7876
|
+
async applyPeerManifest(remote, manifest, undelivered) {
|
|
7877
|
+
const name = remote.meta.name;
|
|
7878
|
+
if (this.remotes.get(name) !== remote || this.removing.has(name)) return;
|
|
7879
|
+
const known = remote.meta.peer;
|
|
7880
|
+
if (isOlderManifest(manifest, known?.manifest)) return;
|
|
7881
|
+
if (known !== void 0 && (known.manifest?.revision ?? null) === (manifest?.revision ?? null)) {
|
|
7882
|
+
if (this.heldKeys.get(name)?.size) await this.releaseSupported(remote);
|
|
7883
|
+
return;
|
|
7884
|
+
}
|
|
7885
|
+
remote.meta.peer = {
|
|
7886
|
+
manifest,
|
|
7887
|
+
receivedAtUtcMs: Date.now()
|
|
7888
|
+
};
|
|
7889
|
+
await this.remoteStorage.upsert(this.recordOf(remote.meta));
|
|
7890
|
+
await this.holdUnsupported(remote, undelivered);
|
|
7891
|
+
await this.releaseSupported(remote);
|
|
7892
|
+
}
|
|
7893
|
+
async listHolds(filter = {}) {
|
|
7894
|
+
return (await this.holds.list(filter)).map((record) => ({
|
|
7895
|
+
remoteName: record.remoteName,
|
|
7896
|
+
documentId: record.documentId,
|
|
7897
|
+
branch: record.branch,
|
|
7898
|
+
reason: {
|
|
7899
|
+
protocol: record.protocol,
|
|
7900
|
+
version: record.version,
|
|
7901
|
+
peerSupports: this.agreement().peer(record.remoteName).protocols[record.protocol] ?? []
|
|
7902
|
+
},
|
|
7903
|
+
heldAtUtcMs: record.heldAtUtcMs
|
|
7904
|
+
}));
|
|
7905
|
+
}
|
|
7906
|
+
agreement() {
|
|
7907
|
+
return createPeerAgreement({
|
|
7908
|
+
local: this.manifest,
|
|
7909
|
+
legacy: this.legacy,
|
|
7910
|
+
wanted: this.wanted
|
|
7911
|
+
}, [...this.records.values()]);
|
|
7912
|
+
}
|
|
7913
|
+
heldFor(remoteName) {
|
|
7914
|
+
let held = this.heldKeys.get(remoteName);
|
|
7915
|
+
if (!held) {
|
|
7916
|
+
held = /* @__PURE__ */ new Map();
|
|
7917
|
+
this.heldKeys.set(remoteName, held);
|
|
7918
|
+
}
|
|
7919
|
+
return held;
|
|
7920
|
+
}
|
|
7921
|
+
async forgetRemote(name) {
|
|
7922
|
+
this.records.delete(name);
|
|
7923
|
+
this.heldKeys.delete(name);
|
|
7924
|
+
await this.holds.removeRemote(name);
|
|
7925
|
+
}
|
|
7926
|
+
peerSupportsOf(remote) {
|
|
7927
|
+
return peerSupports(remote.meta.peer?.manifest ?? null, this.capabilities);
|
|
7928
|
+
}
|
|
7929
|
+
/** Cached once found: a document's versions never change. */
|
|
7930
|
+
async versionsOf(documentId, branch) {
|
|
7931
|
+
const key = holdKey(documentId, branch);
|
|
7932
|
+
const cached = this.versionCache.get(key);
|
|
7933
|
+
if (cached) return cached;
|
|
7934
|
+
let versions;
|
|
7935
|
+
try {
|
|
7936
|
+
versions = await this.protocolVersionsOf(documentId, branch);
|
|
7937
|
+
} catch {
|
|
7938
|
+
versions = void 0;
|
|
7939
|
+
}
|
|
7940
|
+
if (versions) {
|
|
7941
|
+
if (this.versionCache.size >= VERSION_CACHE_CAP) {
|
|
7942
|
+
const oldest = this.versionCache.keys().next().value;
|
|
7943
|
+
if (oldest !== void 0) this.versionCache.delete(oldest);
|
|
7944
|
+
}
|
|
7945
|
+
this.versionCache.set(key, versions);
|
|
7946
|
+
}
|
|
7947
|
+
return versions;
|
|
7948
|
+
}
|
|
7949
|
+
/** The fallback without a meta cache: the document's indexed creation. */
|
|
7950
|
+
async indexedVersions(documentId, branch) {
|
|
7951
|
+
return createdVersions((await this.operationIndex.get(documentId, {
|
|
7952
|
+
branch,
|
|
7953
|
+
scopes: ["document"]
|
|
7954
|
+
}, {
|
|
7955
|
+
cursor: "0",
|
|
7956
|
+
limit: 10
|
|
7957
|
+
})).results.map((entry) => toOperationWithContext(entry)), documentId);
|
|
7958
|
+
}
|
|
7959
|
+
async hold(remote, documentId, branch, reason) {
|
|
7960
|
+
const held = this.heldFor(remote.meta.name);
|
|
7961
|
+
const key = holdKey(documentId, branch);
|
|
7962
|
+
if (held.has(key)) return;
|
|
7963
|
+
const record = {
|
|
7964
|
+
remoteName: remote.meta.name,
|
|
7965
|
+
documentId,
|
|
7966
|
+
branch,
|
|
7967
|
+
protocol: reason.protocol,
|
|
7968
|
+
version: reason.version,
|
|
7969
|
+
heldAtUtcMs: Date.now()
|
|
7970
|
+
};
|
|
7971
|
+
held.set(key, record);
|
|
7972
|
+
try {
|
|
7973
|
+
await this.holds.upsert(record);
|
|
7974
|
+
} catch (error) {
|
|
7975
|
+
this.logger.error("Failed to persist a sync hold (@remote, @documentId): @error", remote.meta.name, documentId, error instanceof Error ? error.message : String(error));
|
|
7976
|
+
}
|
|
7977
|
+
this.eventBus.emit(SyncEventTypes.SYNC_HELD, {
|
|
7978
|
+
remoteName: remote.meta.name,
|
|
7979
|
+
documentId,
|
|
7980
|
+
branch,
|
|
7981
|
+
reason
|
|
7982
|
+
}).catch(() => {});
|
|
7983
|
+
}
|
|
7984
|
+
/** Narrowed: unsent outbox items for documents the peer cannot run. */
|
|
7985
|
+
async holdUnsupported(remote, undelivered = []) {
|
|
7986
|
+
const support = this.peerSupportsOf(remote);
|
|
7987
|
+
if (coversLocal(support, this.localSupport, this.capabilities)) return;
|
|
7988
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
7989
|
+
const held = [];
|
|
7990
|
+
const unsent = new Set(undelivered);
|
|
7991
|
+
for (const item of remote.channel.outbox.items) {
|
|
7992
|
+
if ((item.status !== -1 || item.emittedCount > 0) && !unsent.has(item)) continue;
|
|
7993
|
+
const key = holdKey(item.documentId, item.branch);
|
|
7994
|
+
let reason = verdicts.get(key);
|
|
7995
|
+
if (reason === void 0) {
|
|
7996
|
+
const versions = createdVersions(item.operations, item.documentId) ?? await this.versionsOf(item.documentId, item.branch);
|
|
7997
|
+
reason = (versions && holdReason(support, versions, this.capabilities)) ?? null;
|
|
7998
|
+
verdicts.set(key, reason);
|
|
7999
|
+
}
|
|
8000
|
+
if (reason) {
|
|
8001
|
+
held.push(item);
|
|
8002
|
+
await this.hold(remote, item.documentId, item.branch, reason);
|
|
8003
|
+
}
|
|
8004
|
+
}
|
|
8005
|
+
if (held.length > 0) remote.channel.outbox.remove(...held);
|
|
8006
|
+
}
|
|
8007
|
+
/** Widened: held documents the peer can now run go out whole. */
|
|
8008
|
+
async releaseSupported(remote) {
|
|
8009
|
+
const held = this.heldKeys.get(remote.meta.name);
|
|
8010
|
+
if (!held?.size) return;
|
|
8011
|
+
const support = this.peerSupportsOf(remote);
|
|
8012
|
+
for (const [key, record] of [...held]) {
|
|
8013
|
+
const versions = await this.versionsOf(record.documentId, record.branch);
|
|
8014
|
+
if (versions && holdReason(support, versions, this.capabilities)) continue;
|
|
8015
|
+
held.delete(key);
|
|
8016
|
+
await this.backfillDocument(remote, record.documentId, record.branch);
|
|
8017
|
+
await this.holds.remove(record.remoteName, record.documentId, record.branch);
|
|
8018
|
+
this.eventBus.emit(SyncEventTypes.SYNC_RELEASED, {
|
|
8019
|
+
remoteName: record.remoteName,
|
|
8020
|
+
documentId: record.documentId,
|
|
8021
|
+
branch: record.branch
|
|
8022
|
+
}).catch(() => {});
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
/**
|
|
8026
|
+
* Queues a released document's whole history. The peer never received it
|
|
8027
|
+
* from this remote, so sinceTimestampUtcMs does not apply; the receiver
|
|
8028
|
+
* dedups by action id.
|
|
8029
|
+
*/
|
|
8030
|
+
async backfillDocument(remote, documentId, branch) {
|
|
8031
|
+
const entries = [];
|
|
8032
|
+
let page = await this.operationIndex.get(documentId, { branch }, void 0, this.abortController.signal);
|
|
8033
|
+
for (;;) {
|
|
8034
|
+
entries.push(...page.results);
|
|
8035
|
+
if (!page.next) break;
|
|
8036
|
+
page = await page.next();
|
|
8037
|
+
}
|
|
8038
|
+
let operations = entries.filter((entry) => entry.sourceRemote !== remote.meta.name).map((entry) => toOperationWithContext(entry));
|
|
8039
|
+
operations = filterOperations(operations, remote.meta.filter);
|
|
8040
|
+
if (operations.length === 0) return;
|
|
8041
|
+
operations.sort((a, b) => {
|
|
8042
|
+
if (a.context.scope !== b.context.scope) {
|
|
8043
|
+
if (a.context.scope === "document") return -1;
|
|
8044
|
+
if (b.context.scope === "document") return 1;
|
|
8045
|
+
return a.context.scope < b.context.scope ? -1 : 1;
|
|
8046
|
+
}
|
|
8047
|
+
return a.context.ordinal - b.context.ordinal;
|
|
8048
|
+
});
|
|
8049
|
+
this.emitBatches(remote, operations, "backfill", { lastJobByDoc: /* @__PURE__ */ new Map() });
|
|
8050
|
+
}
|
|
8051
|
+
/** A peer that announced support refused the document: hold it from that peer. */
|
|
8052
|
+
async holdRefused(remote, syncOp) {
|
|
8053
|
+
const versions = await this.versionsOf(syncOp.documentId, syncOp.branch) ?? {};
|
|
8054
|
+
const support = this.peerSupportsOf(remote);
|
|
8055
|
+
const protocols = this.capabilities.filter((capability) => capability.kind === "protocol" && versions[capability.name] !== void 0);
|
|
8056
|
+
const protocol = (protocols.find((candidate) => !candidate.baseline.includes(versions[candidate.name])) ?? protocols.at(0))?.name ?? "unknown";
|
|
8057
|
+
await this.hold(remote, syncOp.documentId, syncOp.branch, {
|
|
8058
|
+
protocol,
|
|
8059
|
+
version: versions[protocol] ?? 0,
|
|
8060
|
+
peerSupports: support.protocols[protocol] ?? []
|
|
8061
|
+
});
|
|
8062
|
+
}
|
|
8063
|
+
/**
|
|
8064
|
+
* Refuses, before load, what this reactor cannot run and what the sending
|
|
8065
|
+
* peer does not announce. Neither quarantines the document.
|
|
8066
|
+
*/
|
|
8067
|
+
async refuseOnReceipt(remote, syncOps) {
|
|
8068
|
+
await this.peerUpdates.get(remote.meta.name);
|
|
8069
|
+
const peer = this.peerSupportsOf(remote);
|
|
8070
|
+
const refused = /* @__PURE__ */ new Set();
|
|
8071
|
+
for (const syncOp of syncOps) {
|
|
8072
|
+
const versions = createdVersions(syncOp.operations, syncOp.documentId) ?? await this.versionsOf(syncOp.documentId, syncOp.branch);
|
|
8073
|
+
if (versions && this.refuse(remote, syncOp, versions, peer)) refused.add(syncOp);
|
|
8074
|
+
}
|
|
8075
|
+
return refused;
|
|
8076
|
+
}
|
|
8077
|
+
/** Dead-letters a refused sync operation. */
|
|
8078
|
+
refuse(remote, syncOp, versions, peer) {
|
|
8079
|
+
let errorType;
|
|
8080
|
+
let reason = holdReason(this.localSupport, versions, this.capabilities);
|
|
8081
|
+
if (reason) errorType = "UNSUPPORTED_PROTOCOL";
|
|
8082
|
+
else {
|
|
8083
|
+
reason = holdReason(peer, versions, this.capabilities);
|
|
8084
|
+
if (!reason) return false;
|
|
8085
|
+
errorType = "PEER_PROTOCOL_UNSUPPORTED";
|
|
8086
|
+
}
|
|
8087
|
+
const message = errorType === "UNSUPPORTED_PROTOCOL" ? `Document ${syncOp.documentId} requires ${reason.protocol} ${reason.version}, which this reactor does not support` : `Remote ${remote.meta.name} wrote into document ${syncOp.documentId} at ${reason.protocol} ${reason.version}, which its peer does not announce`;
|
|
8088
|
+
syncOp.failed(new ChannelError("inbox", new Error(message), errorType));
|
|
8089
|
+
remote.channel.deadLetter.add(syncOp);
|
|
8090
|
+
remote.channel.inbox.remove(syncOp);
|
|
8091
|
+
return true;
|
|
8092
|
+
}
|
|
8093
|
+
/** Whether anything could be held from this remote; synchronous when not. */
|
|
8094
|
+
gates(remote) {
|
|
8095
|
+
return (this.heldKeys.get(remote.meta.name)?.size ?? 0) > 0 || !coversLocal(this.peerSupportsOf(remote), this.localSupport, this.capabilities);
|
|
8096
|
+
}
|
|
8097
|
+
/** Drops rows of documents the remote's peer cannot run, recording holds. */
|
|
8098
|
+
async gateOutbound(remote, operations) {
|
|
8099
|
+
if (operations.length === 0) return operations;
|
|
8100
|
+
const held = this.heldKeys.get(remote.meta.name);
|
|
8101
|
+
const support = this.peerSupportsOf(remote);
|
|
8102
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
8103
|
+
const kept = [];
|
|
8104
|
+
for (const entry of operations) {
|
|
8105
|
+
const { documentId, branch } = entry.context;
|
|
8106
|
+
const key = holdKey(documentId, branch);
|
|
8107
|
+
let isHeld = verdicts.get(key);
|
|
8108
|
+
if (isHeld === void 0) {
|
|
8109
|
+
isHeld = held?.has(key) ?? false;
|
|
8110
|
+
if (!isHeld) {
|
|
8111
|
+
const versions = createdVersions(operations, documentId) ?? await this.versionsOf(documentId, branch);
|
|
8112
|
+
const reason = versions && holdReason(support, versions, this.capabilities);
|
|
8113
|
+
if (reason) {
|
|
8114
|
+
await this.hold(remote, documentId, branch, reason);
|
|
8115
|
+
isHeld = true;
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
verdicts.set(key, isHeld);
|
|
8119
|
+
}
|
|
8120
|
+
if (!isHeld) kept.push(entry);
|
|
8121
|
+
}
|
|
8122
|
+
return kept;
|
|
8123
|
+
}
|
|
8124
|
+
recordOf(meta) {
|
|
8125
|
+
return {
|
|
8126
|
+
id: meta.id,
|
|
8127
|
+
name: meta.name,
|
|
8128
|
+
collectionId: meta.collectionId,
|
|
8129
|
+
channelConfig: meta.channelConfig,
|
|
8130
|
+
filter: meta.filter,
|
|
8131
|
+
options: meta.options,
|
|
7322
8132
|
status: {
|
|
7323
8133
|
push: createIdleHealth(),
|
|
7324
8134
|
pull: createIdleHealth()
|
|
7325
|
-
}
|
|
7326
|
-
|
|
8135
|
+
},
|
|
8136
|
+
peer: meta.peer
|
|
8137
|
+
};
|
|
7327
8138
|
}
|
|
7328
8139
|
async add(name, collectionId, channelConfig, filter = {
|
|
7329
8140
|
documentId: [],
|
|
7330
8141
|
scope: [],
|
|
7331
8142
|
branch: ""
|
|
7332
|
-
}, options = { sinceTimestampUtcMs: "0" }, id) {
|
|
8143
|
+
}, options = { sinceTimestampUtcMs: "0" }, id, peer) {
|
|
7333
8144
|
if (this.isShutdown) throw new Error("SyncManager is shutdown and cannot add remotes");
|
|
7334
8145
|
if (this.remotes.has(name)) throw new Error(`Remote with name '${name}' already exists`);
|
|
7335
8146
|
this.logger.debug("Adding remote (@name, @collectionId, @channelConfig, @filter, @options, @id)", name, collectionId.key, channelConfig, filter, options, id);
|
|
7336
8147
|
const remoteId = id ?? crypto.randomUUID();
|
|
7337
|
-
const
|
|
8148
|
+
const meta = {
|
|
7338
8149
|
id: remoteId,
|
|
7339
8150
|
name,
|
|
7340
8151
|
collectionId,
|
|
7341
8152
|
channelConfig,
|
|
7342
8153
|
filter,
|
|
7343
8154
|
options,
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
8155
|
+
peer: peer === void 0 ? void 0 : {
|
|
8156
|
+
manifest: peer,
|
|
8157
|
+
receivedAtUtcMs: Date.now()
|
|
7347
8158
|
}
|
|
7348
8159
|
};
|
|
7349
|
-
await this.remoteStorage.upsert(
|
|
8160
|
+
await this.remoteStorage.upsert(this.recordOf(meta));
|
|
7350
8161
|
const channel = this.channelFactory.instance(remoteId, name, channelConfig, this.cursorStorage, collectionId, filter, this.operationIndex, options);
|
|
7351
8162
|
const remote = {
|
|
7352
|
-
meta
|
|
7353
|
-
id: remoteId,
|
|
7354
|
-
name,
|
|
7355
|
-
collectionId,
|
|
7356
|
-
channelConfig,
|
|
7357
|
-
filter,
|
|
7358
|
-
options
|
|
7359
|
-
},
|
|
8163
|
+
meta,
|
|
7360
8164
|
channel
|
|
7361
8165
|
};
|
|
7362
8166
|
this.remotes.set(name, remote);
|
|
8167
|
+
this.records.set(name, meta);
|
|
7363
8168
|
await this.loadDeadLetters(remote);
|
|
7364
8169
|
this.wireChannelCallbacks(remote);
|
|
7365
8170
|
try {
|
|
@@ -7368,6 +8173,8 @@ var SyncManager = class {
|
|
|
7368
8173
|
await this.dropRemoteAfterFailedInit(remote, !isCredentialOrNetworkError(error));
|
|
7369
8174
|
throw error;
|
|
7370
8175
|
}
|
|
8176
|
+
await this.peerUpdates.get(name);
|
|
8177
|
+
this.owe(name, await this.watermarkHead());
|
|
7371
8178
|
const backfillController = new AbortController();
|
|
7372
8179
|
this.backfillAbortControllers.set(name, backfillController);
|
|
7373
8180
|
this.updateOutbox(remote, 0, "backfill", backfillController.signal).catch((error) => {
|
|
@@ -7388,10 +8195,12 @@ var SyncManager = class {
|
|
|
7388
8195
|
const remote = this.remotes.get(name);
|
|
7389
8196
|
if (!remote) throw new Error(`Remote with name '${name}' does not exist`);
|
|
7390
8197
|
this.removing.add(name);
|
|
8198
|
+
this.owed.delete(name);
|
|
7391
8199
|
try {
|
|
7392
8200
|
await this.teardownRemoteResources(remote);
|
|
7393
8201
|
await this.remoteStorage.remove(name);
|
|
7394
8202
|
await this.cursorStorage.remove(name);
|
|
8203
|
+
await this.forgetRemote(name);
|
|
7395
8204
|
} finally {
|
|
7396
8205
|
this.remotes.delete(name);
|
|
7397
8206
|
this.removing.delete(name);
|
|
@@ -7409,7 +8218,10 @@ var SyncManager = class {
|
|
|
7409
8218
|
this.removing.add(name);
|
|
7410
8219
|
try {
|
|
7411
8220
|
await this.teardownRemoteResources(remote);
|
|
7412
|
-
if (removeStorageRecord)
|
|
8221
|
+
if (removeStorageRecord) {
|
|
8222
|
+
await this.remoteStorage.remove(name);
|
|
8223
|
+
await this.forgetRemote(name);
|
|
8224
|
+
}
|
|
7413
8225
|
} catch (error) {
|
|
7414
8226
|
this.logger.error("Error tearing down remote after failed init (@name, @error)", name, error instanceof Error ? error.message : String(error));
|
|
7415
8227
|
} finally {
|
|
@@ -7442,6 +8254,9 @@ var SyncManager = class {
|
|
|
7442
8254
|
}
|
|
7443
8255
|
this.evictedOutboxFloors.delete(name);
|
|
7444
8256
|
this.prunePending.delete(name);
|
|
8257
|
+
this.peerUnsubscribes.get(name)?.();
|
|
8258
|
+
this.peerUnsubscribes.delete(name);
|
|
8259
|
+
this.peerUpdates.delete(name);
|
|
7445
8260
|
}
|
|
7446
8261
|
}
|
|
7447
8262
|
list() {
|
|
@@ -7465,6 +8280,9 @@ var SyncManager = class {
|
|
|
7465
8280
|
}
|
|
7466
8281
|
wireChannelCallbacks(remote) {
|
|
7467
8282
|
remote.channel.inbox.onAdded((syncOps) => this.handleInboxAdded(remote, syncOps));
|
|
8283
|
+
remote.channel.setLocalManifest?.(() => this.manifest);
|
|
8284
|
+
const unsubscribePeer = remote.channel.onPeerManifest?.((manifest, undelivered) => this.queuePeerManifest(remote, manifest, undelivered).catch(() => {}));
|
|
8285
|
+
if (unsubscribePeer) this.peerUnsubscribes.set(remote.meta.name, unsubscribePeer);
|
|
7468
8286
|
this.syncStatusTracker.trackRemote(remote.meta.name, remote.channel);
|
|
7469
8287
|
const unsubscribe = remote.channel.onConnectionStateChange((snapshot) => {
|
|
7470
8288
|
this.eventBus.emit(SyncEventTypes.CONNECTION_STATE_CHANGED, {
|
|
@@ -7476,7 +8294,13 @@ var SyncManager = class {
|
|
|
7476
8294
|
}).catch(() => {});
|
|
7477
8295
|
});
|
|
7478
8296
|
this.connectionStateUnsubscribes.set(remote.meta.name, unsubscribe);
|
|
7479
|
-
remote.channel.deadLetter.onAdded((
|
|
8297
|
+
remote.channel.deadLetter.onAdded((added) => {
|
|
8298
|
+
const refusals = added.filter((syncOp) => syncOp.error?.source === "outbox" && syncOperationErrorType(syncOp.error) === "UNSUPPORTED_PROTOCOL");
|
|
8299
|
+
if (refusals.length > 0) {
|
|
8300
|
+
remote.channel.deadLetter.remove(...refusals);
|
|
8301
|
+
for (const syncOp of refusals) this.holdRefused(remote, syncOp).catch(() => {});
|
|
8302
|
+
}
|
|
8303
|
+
const syncOps = added.filter((syncOp) => !refusals.includes(syncOp));
|
|
7480
8304
|
for (const syncOp of syncOps) {
|
|
7481
8305
|
this.logger.error("Dead letter (@remote, @documentId, @jobId, @error, @dependencies)", remote.meta.name, syncOp.documentId, syncOp.jobId, syncOp.error?.message ?? "unknown", syncOp.jobDependencies);
|
|
7482
8306
|
const errorType = syncOperationErrorType(syncOp.error);
|
|
@@ -7548,12 +8372,43 @@ var SyncManager = class {
|
|
|
7548
8372
|
for (const remote of remotes) if (!affectedRemotes.includes(remote)) affectedRemotes.push(remote);
|
|
7549
8373
|
}
|
|
7550
8374
|
for (const remote of affectedRemotes) trimMailboxFromBatch(remote.channel.inbox, batch);
|
|
8375
|
+
const through = await this.watermark.refresh();
|
|
8376
|
+
let batchMax = 0;
|
|
8377
|
+
for (const { event } of batch.entries) for (const op of event.operations) batchMax = Math.max(batchMax, op.context.ordinal);
|
|
7551
8378
|
for (const remote of affectedRemotes) {
|
|
7552
8379
|
if (!this.remotes.has(remote.meta.name) || this.removing.has(remote.meta.name)) continue;
|
|
7553
8380
|
await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal, "batch-triggered");
|
|
8381
|
+
if (batchMax > through) this.owe(remote.meta.name, batchMax);
|
|
7554
8382
|
}
|
|
7555
8383
|
await this.drainPrunes();
|
|
7556
8384
|
}
|
|
8385
|
+
/** Derives every owed remote up to the watermark. */
|
|
8386
|
+
async deriveSettled() {
|
|
8387
|
+
if (this.isShutdown) return;
|
|
8388
|
+
const through = this.watermark.settledThrough;
|
|
8389
|
+
for (const [name, upTo] of [...this.owed]) {
|
|
8390
|
+
const remote = this.remotes.get(name);
|
|
8391
|
+
if (!remote || this.removing.has(name)) {
|
|
8392
|
+
this.owed.delete(name);
|
|
8393
|
+
continue;
|
|
8394
|
+
}
|
|
8395
|
+
await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal, "batch-triggered");
|
|
8396
|
+
if (through >= upTo) this.owed.delete(name);
|
|
8397
|
+
}
|
|
8398
|
+
await this.drainPrunes();
|
|
8399
|
+
}
|
|
8400
|
+
owe(name, upTo) {
|
|
8401
|
+
this.owed.set(name, Math.max(this.owed.get(name) ?? 0, upTo));
|
|
8402
|
+
}
|
|
8403
|
+
/** The sequence head after a fresh probe; a remote is owed through it. */
|
|
8404
|
+
async watermarkHead() {
|
|
8405
|
+
try {
|
|
8406
|
+
await this.watermark.refresh();
|
|
8407
|
+
} catch (error) {
|
|
8408
|
+
this.logger.warn("Settled watermark probe failed; owed remotes wait for the next one: @error", error);
|
|
8409
|
+
}
|
|
8410
|
+
return this.watermark.status().head;
|
|
8411
|
+
}
|
|
7557
8412
|
handleInboxAdded(remote, syncOps) {
|
|
7558
8413
|
if (this.isShutdown) return;
|
|
7559
8414
|
const eligible = syncOps.filter((op) => !this.quarantinedDocumentIds.has(op.documentId));
|
|
@@ -7584,6 +8439,7 @@ var SyncManager = class {
|
|
|
7584
8439
|
return next;
|
|
7585
8440
|
}
|
|
7586
8441
|
async applyInboxJob(remote, syncOp) {
|
|
8442
|
+
if ((await this.refuseOnReceipt(remote, [syncOp])).size > 0) return;
|
|
7587
8443
|
const operations = syncOp.operations.map((op) => op.operation);
|
|
7588
8444
|
let jobInfo;
|
|
7589
8445
|
try {
|
|
@@ -7620,7 +8476,14 @@ var SyncManager = class {
|
|
|
7620
8476
|
} else syncOp.executed();
|
|
7621
8477
|
remote.channel.inbox.remove(syncOp);
|
|
7622
8478
|
}
|
|
7623
|
-
async applyInboxBatch(
|
|
8479
|
+
async applyInboxBatch(received) {
|
|
8480
|
+
const refused = /* @__PURE__ */ new Set();
|
|
8481
|
+
for (const { remote, syncOp } of received) {
|
|
8482
|
+
const refusals = await this.refuseOnReceipt(remote, [syncOp]);
|
|
8483
|
+
for (const refusal of refusals) refused.add(refusal);
|
|
8484
|
+
}
|
|
8485
|
+
const items = received.filter(({ syncOp }) => !refused.has(syncOp));
|
|
8486
|
+
if (items.length === 0) return;
|
|
7624
8487
|
const sourceRemote = items[0].remote.meta.name;
|
|
7625
8488
|
const chunkKeys = new Set(items.map(({ syncOp }) => syncOp.jobId));
|
|
7626
8489
|
const jobs = items.map(({ syncOp }) => {
|
|
@@ -7841,32 +8704,35 @@ var SyncManager = class {
|
|
|
7841
8704
|
}
|
|
7842
8705
|
}
|
|
7843
8706
|
}
|
|
8707
|
+
emitBatches(remote, operations, mode, chain) {
|
|
8708
|
+
if (operations.length === 0) return;
|
|
8709
|
+
const batches = batchOperationsByDocument(operations);
|
|
8710
|
+
const syncOps = [];
|
|
8711
|
+
for (const batch of batches) {
|
|
8712
|
+
const jobId = crypto.randomUUID();
|
|
8713
|
+
const prevJobId = chain.lastJobByDoc.get(batch.documentId);
|
|
8714
|
+
const deps = [];
|
|
8715
|
+
if (prevJobId) deps.push(prevJobId);
|
|
8716
|
+
if (mode === "batch-triggered" && chain.prevChainJobId && chain.prevChainJobId !== prevJobId) deps.push(chain.prevChainJobId);
|
|
8717
|
+
const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.meta.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
|
|
8718
|
+
syncOps.push(syncOp);
|
|
8719
|
+
chain.lastJobByDoc.set(batch.documentId, jobId);
|
|
8720
|
+
if (mode === "batch-triggered") chain.prevChainJobId = jobId;
|
|
8721
|
+
}
|
|
8722
|
+
remote.channel.outbox.add(...syncOps);
|
|
8723
|
+
this.evictPastOutboxBound(remote);
|
|
8724
|
+
}
|
|
7844
8725
|
async deriveOutbox(remote, ackOrdinal, mode, signal) {
|
|
7845
8726
|
const composedSignal = signal ? AbortSignal.any([signal, this.abortController.signal]) : this.abortController.signal;
|
|
7846
8727
|
const startOrdinal = this.refillOrdinal(remote, ackOrdinal);
|
|
7847
8728
|
let maxOrdinal = startOrdinal;
|
|
7848
|
-
const
|
|
7849
|
-
let prevChainJobId;
|
|
8729
|
+
const chain = { lastJobByDoc: /* @__PURE__ */ new Map() };
|
|
7850
8730
|
const sinceTimestamp = remote.meta.options.sinceTimestampUtcMs;
|
|
7851
|
-
const emitBatches = (operations) =>
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
const jobId = crypto.randomUUID();
|
|
7857
|
-
const prevJobId = lastJobByDoc.get(batch.documentId);
|
|
7858
|
-
const deps = [];
|
|
7859
|
-
if (prevJobId) deps.push(prevJobId);
|
|
7860
|
-
if (mode === "batch-triggered" && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
|
|
7861
|
-
const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.meta.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
|
|
7862
|
-
syncOps.push(syncOp);
|
|
7863
|
-
lastJobByDoc.set(batch.documentId, jobId);
|
|
7864
|
-
if (mode === "batch-triggered") prevChainJobId = jobId;
|
|
7865
|
-
}
|
|
7866
|
-
remote.channel.outbox.add(...syncOps);
|
|
7867
|
-
this.evictPastOutboxBound(remote);
|
|
7868
|
-
};
|
|
7869
|
-
let page = await this.operationIndex.find(remote.meta.collectionId.key, startOrdinal, { excludeSourceRemote: remote.meta.name }, void 0, composedSignal);
|
|
8731
|
+
const emitBatches = (operations) => this.emitBatches(remote, operations, mode, chain);
|
|
8732
|
+
let page = await this.operationIndex.find(remote.meta.collectionId.key, startOrdinal, {
|
|
8733
|
+
excludeSourceRemote: remote.meta.name,
|
|
8734
|
+
throughOrdinal: this.watermark.settledThrough
|
|
8735
|
+
}, void 0, composedSignal);
|
|
7870
8736
|
let carry = [];
|
|
7871
8737
|
let hasMore;
|
|
7872
8738
|
do {
|
|
@@ -7880,6 +8746,7 @@ var SyncManager = class {
|
|
|
7880
8746
|
if (sinceTimestamp && sinceTimestamp !== "0") operations = operations.filter((op) => op.operation.timestampUtcMs >= sinceTimestamp);
|
|
7881
8747
|
operations = filterOperations(operations, remote.meta.filter);
|
|
7882
8748
|
operations = operations.filter((op) => !this.quarantinedDocumentIds.has(op.context.documentId));
|
|
8749
|
+
if (this.gates(remote)) operations = await this.gateOutbound(remote, operations);
|
|
7883
8750
|
hasMore = !!page.next;
|
|
7884
8751
|
if (operations.length > 0) {
|
|
7885
8752
|
const created = /* @__PURE__ */ new Set();
|
|
@@ -7914,6 +8781,7 @@ var SyncBuilder = class {
|
|
|
7914
8781
|
remoteStorage;
|
|
7915
8782
|
cursorStorage;
|
|
7916
8783
|
deadLetterStorage;
|
|
8784
|
+
holdStorage;
|
|
7917
8785
|
config = {};
|
|
7918
8786
|
withChannelFactory(factory) {
|
|
7919
8787
|
this.channelFactory = factory;
|
|
@@ -7931,6 +8799,10 @@ var SyncBuilder = class {
|
|
|
7931
8799
|
this.deadLetterStorage = storage;
|
|
7932
8800
|
return this;
|
|
7933
8801
|
}
|
|
8802
|
+
withHoldStorage(storage) {
|
|
8803
|
+
this.holdStorage = storage;
|
|
8804
|
+
return this;
|
|
8805
|
+
}
|
|
7934
8806
|
withMaxDeadLettersPerRemote(limit) {
|
|
7935
8807
|
this.config.maxDeadLettersPerRemote = limit;
|
|
7936
8808
|
return this;
|
|
@@ -7947,19 +8819,21 @@ var SyncBuilder = class {
|
|
|
7947
8819
|
this.config.staleRemotePollWindowMs = windowMs;
|
|
7948
8820
|
return this;
|
|
7949
8821
|
}
|
|
7950
|
-
build(reactor, logger, operationIndex, eventBus, db, driveContainerTypes) {
|
|
7951
|
-
return this.buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes).syncManager;
|
|
8822
|
+
build(reactor, logger, operationIndex, eventBus, db, driveContainerTypes, watermark, localPeer) {
|
|
8823
|
+
return this.buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes, watermark, localPeer).syncManager;
|
|
7952
8824
|
}
|
|
7953
|
-
buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes) {
|
|
8825
|
+
buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes, watermark, localPeer) {
|
|
7954
8826
|
if (!this.channelFactory) throw new Error("Channel factory is required");
|
|
7955
8827
|
const remoteStorage = this.remoteStorage ?? new KyselySyncRemoteStorage(db);
|
|
7956
8828
|
const cursorStorage = this.cursorStorage ?? new KyselySyncCursorStorage(db);
|
|
7957
8829
|
const deadLetterStorage = this.deadLetterStorage ?? new KyselySyncDeadLetterStorage(db);
|
|
7958
|
-
const
|
|
8830
|
+
const holdStorage = this.holdStorage ?? new KyselySyncHoldStorage(db);
|
|
8831
|
+
const syncManager = new SyncManager(logger, remoteStorage, cursorStorage, deadLetterStorage, this.channelFactory, operationIndex, reactor, eventBus, driveContainerTypes, watermark, this.config, localPeer, holdStorage);
|
|
7959
8832
|
return {
|
|
7960
8833
|
remoteStorage,
|
|
7961
8834
|
cursorStorage,
|
|
7962
8835
|
deadLetterStorage,
|
|
8836
|
+
holdStorage,
|
|
7963
8837
|
channelFactory: this.channelFactory,
|
|
7964
8838
|
syncManager
|
|
7965
8839
|
};
|
|
@@ -8033,7 +8907,9 @@ var Reactor = class {
|
|
|
8033
8907
|
operationStore;
|
|
8034
8908
|
eventBus;
|
|
8035
8909
|
executorManager;
|
|
8036
|
-
|
|
8910
|
+
catchUp;
|
|
8911
|
+
constructor(logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, features, documentView, documentIndexer, operationStore, eventBus, executorManager, catchUp) {
|
|
8912
|
+
this.catchUp = catchUp;
|
|
8037
8913
|
this.logger = logger;
|
|
8038
8914
|
this.documentModelRegistry = documentModelRegistry;
|
|
8039
8915
|
this.queue = queue;
|
|
@@ -8060,6 +8936,7 @@ var Reactor = class {
|
|
|
8060
8936
|
this.setShutdown(true);
|
|
8061
8937
|
const shutdownAsync = async () => {
|
|
8062
8938
|
await this.executorManager.stop(true);
|
|
8939
|
+
await this.catchUp?.stop();
|
|
8063
8940
|
this.readModelCoordinator.stop();
|
|
8064
8941
|
this.jobTracker.shutdown();
|
|
8065
8942
|
};
|
|
@@ -8589,6 +9466,14 @@ var Reactor = class {
|
|
|
8589
9466
|
};
|
|
8590
9467
|
//#endregion
|
|
8591
9468
|
//#region src/core/reactor-builder.ts
|
|
9469
|
+
/** One contiguous cursor per read model cannot serve shards that each see part of the stream. */
|
|
9470
|
+
function validateShardCount(shardCount) {
|
|
9471
|
+
if (shardCount !== 1) throw new Error(`shardCount ${shardCount} is not supported: read-side catch-up keeps one cursor per read model, so projection runs in exactly one worker (shardCount: 1)`);
|
|
9472
|
+
}
|
|
9473
|
+
/** A coordinator whose models sweep in a worker reports that status. */
|
|
9474
|
+
function hasCatchUpStatuses(coordinator) {
|
|
9475
|
+
return "catchUpStatuses" in coordinator && typeof coordinator.catchUpStatuses === "function";
|
|
9476
|
+
}
|
|
8592
9477
|
function sameDatabaseTarget(a, b) {
|
|
8593
9478
|
return a.host === b.host && a.port === b.port && a.database === b.database;
|
|
8594
9479
|
}
|
|
@@ -8620,6 +9505,7 @@ var ReactorBuilder = class {
|
|
|
8620
9505
|
executorConfig = {};
|
|
8621
9506
|
writeCacheConfig;
|
|
8622
9507
|
migrationStrategy = "auto";
|
|
9508
|
+
unsupportedStoredDocuments = "refuse";
|
|
8623
9509
|
syncBuilder;
|
|
8624
9510
|
eventBus;
|
|
8625
9511
|
readModelCoordinator;
|
|
@@ -8642,6 +9528,8 @@ var ReactorBuilder = class {
|
|
|
8642
9528
|
projectionShardConfig;
|
|
8643
9529
|
projectionWorkerFactory;
|
|
8644
9530
|
instrumentedPools = [];
|
|
9531
|
+
catchUpConfig = defaultCatchUpConfig;
|
|
9532
|
+
extraPeerCapabilities = [];
|
|
8645
9533
|
withLogger(logger) {
|
|
8646
9534
|
this.logger = logger;
|
|
8647
9535
|
return this;
|
|
@@ -8657,6 +9545,15 @@ var ReactorBuilder = class {
|
|
|
8657
9545
|
this.documentModelSources.push(...sources);
|
|
8658
9546
|
return this;
|
|
8659
9547
|
}
|
|
9548
|
+
/** Capabilities beyond the registry, for tests and hosts that ship their own. */
|
|
9549
|
+
withPeerCapabilities(extra) {
|
|
9550
|
+
this.extraPeerCapabilities = mergePeerCapabilities(this.extraPeerCapabilities, extra);
|
|
9551
|
+
return this;
|
|
9552
|
+
}
|
|
9553
|
+
/** The registry plus any capabilities added with withPeerCapabilities. */
|
|
9554
|
+
getPeerCapabilities() {
|
|
9555
|
+
return mergePeerCapabilities(PEER_CAPABILITIES, this.extraPeerCapabilities);
|
|
9556
|
+
}
|
|
8660
9557
|
withUpgradeManifests(manifests) {
|
|
8661
9558
|
this.upgradeManifests = manifests;
|
|
8662
9559
|
return this;
|
|
@@ -8719,10 +9616,27 @@ var ReactorBuilder = class {
|
|
|
8719
9616
|
this.migrationStrategy = strategy;
|
|
8720
9617
|
return this;
|
|
8721
9618
|
}
|
|
9619
|
+
/**
|
|
9620
|
+
* Stored documents at protocol versions this build does not run: "refuse"
|
|
9621
|
+
* (the default) fails buildModule; "read-only" starts with a warning, and
|
|
9622
|
+
* every job and received row into them is refused.
|
|
9623
|
+
*/
|
|
9624
|
+
withUnsupportedStoredDocuments(mode) {
|
|
9625
|
+
this.unsupportedStoredDocuments = mode;
|
|
9626
|
+
return this;
|
|
9627
|
+
}
|
|
8722
9628
|
withSync(syncBuilder) {
|
|
8723
9629
|
this.syncBuilder = syncBuilder;
|
|
8724
9630
|
return this;
|
|
8725
9631
|
}
|
|
9632
|
+
/** Tunes the read-side catch-up sweep. */
|
|
9633
|
+
withCatchUp(config) {
|
|
9634
|
+
this.catchUpConfig = {
|
|
9635
|
+
...this.catchUpConfig,
|
|
9636
|
+
...config
|
|
9637
|
+
};
|
|
9638
|
+
return this;
|
|
9639
|
+
}
|
|
8726
9640
|
withEventBus(eventBus) {
|
|
8727
9641
|
this.eventBus = eventBus;
|
|
8728
9642
|
return this;
|
|
@@ -8848,11 +9762,18 @@ var ReactorBuilder = class {
|
|
|
8848
9762
|
async buildModule() {
|
|
8849
9763
|
if (!this.logger) this.logger = new ConsoleLogger(["reactor"]);
|
|
8850
9764
|
const featureFlags = resolveFeatureFlags(this.executorConfig.featureFlags);
|
|
9765
|
+
if (this.executorConfig.protocolSupport === void 0) this.executorConfig = {
|
|
9766
|
+
...this.executorConfig,
|
|
9767
|
+
protocolSupport: localSupports(this.getPeerCapabilities(), featureFlags).protocols
|
|
9768
|
+
};
|
|
8851
9769
|
if (this.readModelCoordinator !== void 0 && this.readModelCoordinatorFactory !== void 0) throw new Error("withReadModelCoordinator and withReadModelCoordinatorFactory are mutually exclusive; register one coordinator source");
|
|
8852
9770
|
if (this.projectionShardConfig !== void 0 && this.readModelCoordinatorFactory !== void 0) throw new Error("withProjectionShards and withReadModelCoordinatorFactory are mutually exclusive; the factory owns the projection worker through its createProjectionShardManager dependency");
|
|
8853
9771
|
if (this.projectionShardConfig !== void 0 && this.readModelFactories.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModelFactory; projection workers cannot receive host-only factory dependencies");
|
|
8854
9772
|
if (this.projectionShardConfig !== void 0 && this.readModels.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModel; projection workers build their own read models from the shard config and would silently omit these");
|
|
8855
|
-
if (this.projectionShardConfig !== void 0)
|
|
9773
|
+
if (this.projectionShardConfig !== void 0) {
|
|
9774
|
+
validateShardCount(this.projectionShardConfig.shardCount);
|
|
9775
|
+
validateBuiltInKindCoverage(this.projectionShardConfig.preReadyKinds, this.projectionShardConfig.postReadyKinds);
|
|
9776
|
+
}
|
|
8856
9777
|
const resolvedSources = await resolveModelSources(this.documentModelSources);
|
|
8857
9778
|
if (this.workerPool) {
|
|
8858
9779
|
if (resolvedSources.manifest.length === 0) throw new Error("withWorkerPool requires at least one worker-importable document-model source ({ filePath } or { packageName }).");
|
|
@@ -8875,6 +9796,7 @@ var ReactorBuilder = class {
|
|
|
8875
9796
|
const result = await runMigrations(baseDatabase, REACTOR_SCHEMA);
|
|
8876
9797
|
if (!result.success && result.error) throw new Error(`Database migration failed: ${result.error.message}`);
|
|
8877
9798
|
}
|
|
9799
|
+
await checkStoredProtocols(baseDatabase, REACTOR_SCHEMA, this.executorConfig.protocolSupport ?? {}, this.unsupportedStoredDocuments, this.logger);
|
|
8878
9800
|
const database = baseDatabase.withSchema(REACTOR_SCHEMA);
|
|
8879
9801
|
const operationStore = new KyselyOperationStore(database);
|
|
8880
9802
|
const keyframeStore = new KyselyKeyframeStore(database);
|
|
@@ -8891,6 +9813,14 @@ var ReactorBuilder = class {
|
|
|
8891
9813
|
const writeCache = new KyselyWriteCache(keyframeStore, operationStore, documentModelRegistry, cacheConfig);
|
|
8892
9814
|
await writeCache.startup();
|
|
8893
9815
|
const operationIndex = new KyselyOperationIndex(database);
|
|
9816
|
+
const settledWatermark = new SettledWatermark(createKyselyWatermarkProbe(database), this.logger);
|
|
9817
|
+
const catchUp = new CatchUpScheduler(settledWatermark, operationIndex, this.catchUpConfig, this.logger, {
|
|
9818
|
+
onSwept: (result, thread) => void eventBus.emit(ReactorEventTypes.CATCHUP_SWEPT, {
|
|
9819
|
+
...result,
|
|
9820
|
+
thread
|
|
9821
|
+
}).catch(() => {}),
|
|
9822
|
+
describeSessions: (xids) => describeWaitingSessions(database, xids)
|
|
9823
|
+
});
|
|
8894
9824
|
const documentMetaCache = new DocumentMetaCache(operationStore, { maxDocuments: 1e3 });
|
|
8895
9825
|
await documentMetaCache.startup();
|
|
8896
9826
|
const collectionMembershipCache = new CollectionMembershipCache(operationIndex);
|
|
@@ -8929,6 +9859,7 @@ var ReactorBuilder = class {
|
|
|
8929
9859
|
};
|
|
8930
9860
|
const documentViewConsistencyTracker = new ConsistencyTracker();
|
|
8931
9861
|
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions ? "StateAtDeletion" : "NotFound", readModelIndexing);
|
|
9862
|
+
documentView.attachCatchUp(settledWatermark, this.catchUpConfig.maxTrackedAboveCursor);
|
|
8932
9863
|
try {
|
|
8933
9864
|
await documentView.init();
|
|
8934
9865
|
} catch (error) {
|
|
@@ -8936,6 +9867,7 @@ var ReactorBuilder = class {
|
|
|
8936
9867
|
}
|
|
8937
9868
|
const documentIndexerConsistencyTracker = new ConsistencyTracker();
|
|
8938
9869
|
const documentIndexer = new KyselyDocumentIndexer(database, operationIndex, writeCache, documentIndexerConsistencyTracker, readModelIndexing);
|
|
9870
|
+
documentIndexer.attachCatchUp(settledWatermark, this.catchUpConfig.maxTrackedAboveCursor);
|
|
8939
9871
|
try {
|
|
8940
9872
|
await documentIndexer.init();
|
|
8941
9873
|
} catch (error) {
|
|
@@ -8945,6 +9877,7 @@ var ReactorBuilder = class {
|
|
|
8945
9877
|
const subscriptionNotificationReadModel = new SubscriptionNotificationReadModel(subscriptionManager, documentView);
|
|
8946
9878
|
const processorManagerConsistencyTracker = new ConsistencyTracker();
|
|
8947
9879
|
const processorManager = new ProcessorManager(database, operationIndex, writeCache, processorManagerConsistencyTracker, this.logger, this.driveContainerTypes, { legacyProcessorIds: this.features.legacyProcessorIds !== false });
|
|
9880
|
+
processorManager.attachCatchUp(settledWatermark, this.catchUpConfig.maxTrackedAboveCursor);
|
|
8948
9881
|
try {
|
|
8949
9882
|
await processorManager.init();
|
|
8950
9883
|
} catch (error) {
|
|
@@ -8981,20 +9914,38 @@ var ReactorBuilder = class {
|
|
|
8981
9914
|
createProjectionShardManager: (config) => this.createProjectionShardManager(config, eventBus, hostTrackers, readModelIndexing, false),
|
|
8982
9915
|
registerShutdownHook: (hook) => this.shutdownHooks.push(hook)
|
|
8983
9916
|
}) : this.projectionShardConfig ? await this.createProjectionShardManager(this.projectionShardConfig, eventBus, hostTrackers, readModelIndexing, true) : new ReadModelCoordinator(eventBus, readModelInstances, [subscriptionNotificationReadModel, processorManager]);
|
|
8984
|
-
|
|
9917
|
+
if (hasCatchUpStatuses(readModelCoordinator)) catchUp.addStatusSource(() => readModelCoordinator.catchUpStatuses().flatMap((status) => status.consumers));
|
|
9918
|
+
const indexedReadModels = readModelCoordinator.indexedReadModels?.bind(readModelCoordinator);
|
|
9919
|
+
if (indexedReadModels) catchUp.addSource(() => indexedReadModels().filter((model) => isCatchUpConsumer(model)), "host");
|
|
9920
|
+
else this.logger.warn("The read model coordinator does not report indexedReadModels; the host runs no read-side catch-up sweep");
|
|
9921
|
+
const reactor = new Reactor(this.logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, this.features, documentView, documentIndexer, operationStore, eventBus, executorManager, catchUp);
|
|
9922
|
+
const localPeer = {
|
|
9923
|
+
capabilities: this.getPeerCapabilities(),
|
|
9924
|
+
flags: featureFlags,
|
|
9925
|
+
appKey: this.signer?.app?.key,
|
|
9926
|
+
protocolVersionsOf: async (documentId, branch) => {
|
|
9927
|
+
try {
|
|
9928
|
+
return (await documentMetaCache.getDocumentMeta(documentId, branch)).protocolVersions;
|
|
9929
|
+
} catch {
|
|
9930
|
+
return;
|
|
9931
|
+
}
|
|
9932
|
+
}
|
|
9933
|
+
};
|
|
8985
9934
|
let syncModule = void 0;
|
|
8986
9935
|
if (this.channelScheme) {
|
|
8987
9936
|
const factory = this.channelScheme === "connect" ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
|
|
8988
|
-
syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
|
|
9937
|
+
syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
|
|
8989
9938
|
await syncModule.syncManager.startup();
|
|
8990
9939
|
} else if (this.syncBuilder) {
|
|
8991
|
-
syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
|
|
9940
|
+
syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
|
|
8992
9941
|
await syncModule.syncManager.startup();
|
|
8993
9942
|
}
|
|
8994
9943
|
let groupReevaluationTrigger;
|
|
8995
9944
|
if (featureFlags.authGroups) {
|
|
8996
|
-
groupReevaluationTrigger = new GroupReevaluationTrigger(this.logger, eventBus, queue, operationIndex);
|
|
8997
|
-
groupReevaluationTrigger.
|
|
9945
|
+
groupReevaluationTrigger = new GroupReevaluationTrigger(this.logger, eventBus, queue, operationIndex, database);
|
|
9946
|
+
groupReevaluationTrigger.attachCatchUp(settledWatermark, this.catchUpConfig.maxTrackedAboveCursor);
|
|
9947
|
+
await groupReevaluationTrigger.startup();
|
|
9948
|
+
catchUp.addConsumer(groupReevaluationTrigger, "host");
|
|
8998
9949
|
}
|
|
8999
9950
|
const module = {
|
|
9000
9951
|
eventBus,
|
|
@@ -9020,8 +9971,11 @@ var ReactorBuilder = class {
|
|
|
9020
9971
|
reactor,
|
|
9021
9972
|
groupReevaluationTrigger,
|
|
9022
9973
|
pools: this.instrumentedPools,
|
|
9023
|
-
degradedComponents
|
|
9974
|
+
degradedComponents,
|
|
9975
|
+
catchUp,
|
|
9976
|
+
settledWatermark
|
|
9024
9977
|
};
|
|
9978
|
+
catchUp.start();
|
|
9025
9979
|
if (degradedComponents.length > 0) this.logger.warn("Reactor started with @count degraded component(s): @components", degradedComponents.length, degradedComponents.map(({ component }) => component).join(", "));
|
|
9026
9980
|
if (this.signalHandlersEnabled) this.attachSignalHandlers(module);
|
|
9027
9981
|
return module;
|
|
@@ -9070,6 +10024,7 @@ var ReactorBuilder = class {
|
|
|
9070
10024
|
if (!parentDb) throw new Error(`The projection worker db passed to ${caller} must also be the parent reactor's database, but nothing configures the parent: it would fall back to the default embedded database while the worker projects into Postgres. Pass the same { db } to withWorkerPool, or give the parent its connection with withKysely.`);
|
|
9071
10025
|
if (!sameDatabaseTarget(config.db, parentDb)) throw new Error(`The projection worker db passed to ${caller} and the parent reactor database must address the same Postgres database (same host, port, and database); the parent writes operations there and the projection shards read them.`);
|
|
9072
10026
|
}
|
|
10027
|
+
validateShardCount(config.shardCount);
|
|
9073
10028
|
validateBuiltInKindCoverage(config.preReadyKinds, config.postReadyKinds);
|
|
9074
10029
|
if (this.moduleOnlyModelKeys.length > 0) throw new Error(`projection workers require worker-importable sources, but these models were registered only as live modules: ${this.moduleOnlyModelKeys.join(", ")}. Provide a { filePath } or { packageName } source for each.`);
|
|
9075
10030
|
const models = this.resolvedModelManifest ?? [];
|
|
@@ -9085,7 +10040,7 @@ var ReactorBuilder = class {
|
|
|
9085
10040
|
poolInstrumentations.push(forwarder);
|
|
9086
10041
|
this.instrumentedPools.push(forwarder);
|
|
9087
10042
|
}
|
|
9088
|
-
const { ProjectionShardManager } = await import("./projection-shard-manager-
|
|
10043
|
+
const { ProjectionShardManager } = await import("./projection-shard-manager-CJIdjsd8.js");
|
|
9089
10044
|
const manager = new ProjectionShardManager({
|
|
9090
10045
|
shardCount: config.shardCount,
|
|
9091
10046
|
db,
|
|
@@ -9093,6 +10048,7 @@ var ReactorBuilder = class {
|
|
|
9093
10048
|
preReadyKinds: config.preReadyKinds,
|
|
9094
10049
|
postReadyKinds: config.postReadyKinds,
|
|
9095
10050
|
indexing,
|
|
10051
|
+
catchUp: this.catchUpConfig,
|
|
9096
10052
|
factory,
|
|
9097
10053
|
logger: this.logger,
|
|
9098
10054
|
hostBus: eventBus,
|
|
@@ -9129,7 +10085,7 @@ var ReactorBuilder = class {
|
|
|
9129
10085
|
*/
|
|
9130
10086
|
async createDefaultWorkerFactory(numWorkers, db) {
|
|
9131
10087
|
const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
|
|
9132
|
-
import("./worker-handle-
|
|
10088
|
+
import("./worker-handle-DjbEQkq-.js"),
|
|
9133
10089
|
import("./transport-CZDHzfvy.js"),
|
|
9134
10090
|
Promise.resolve().then(() => worker_exports)
|
|
9135
10091
|
]);
|
|
@@ -9272,7 +10228,9 @@ var HybridProjectionCoordinator = class {
|
|
|
9272
10228
|
}
|
|
9273
10229
|
const enqueuedAt = performance.now();
|
|
9274
10230
|
const key = this.queueKeyFor(event);
|
|
9275
|
-
const
|
|
10231
|
+
const reservations = reserveBatch(this.indexedReadModels(), event.operations, this.logger);
|
|
10232
|
+
const run = () => this.runHostChain(event, enqueuedAt, reservations);
|
|
10233
|
+
const current = (this.chains.get(key) ?? Promise.resolve()).then(run, run);
|
|
9276
10234
|
this.chains.set(key, current);
|
|
9277
10235
|
current.finally(() => {
|
|
9278
10236
|
if (this.chains.get(key) === current) this.chains.delete(key);
|
|
@@ -9287,6 +10245,12 @@ var HybridProjectionCoordinator = class {
|
|
|
9287
10245
|
getChainDepth() {
|
|
9288
10246
|
return this.manager.getChainDepth() + this.chains.size;
|
|
9289
10247
|
}
|
|
10248
|
+
indexedReadModels() {
|
|
10249
|
+
return [...this.preReady, ...this.postReady];
|
|
10250
|
+
}
|
|
10251
|
+
catchUpStatuses() {
|
|
10252
|
+
return this.manager.catchUpStatuses();
|
|
10253
|
+
}
|
|
9290
10254
|
/** Worker chains flush first, so every relayed read-ready is in `chains`. */
|
|
9291
10255
|
async drain() {
|
|
9292
10256
|
await this.manager.drain();
|
|
@@ -9304,11 +10268,18 @@ var HybridProjectionCoordinator = class {
|
|
|
9304
10268
|
}
|
|
9305
10269
|
await this.manager.shutdown();
|
|
9306
10270
|
}
|
|
9307
|
-
async runHostChain(event, enqueuedAt) {
|
|
10271
|
+
async runHostChain(event, enqueuedAt, reservations) {
|
|
10272
|
+
try {
|
|
10273
|
+
await this.runHostBatch(event, enqueuedAt, reservations);
|
|
10274
|
+
} finally {
|
|
10275
|
+
releaseBatch(reservations);
|
|
10276
|
+
}
|
|
10277
|
+
}
|
|
10278
|
+
async runHostBatch(event, enqueuedAt, reservations) {
|
|
9308
10279
|
const chainWaitDurationMs = performance.now() - enqueuedAt;
|
|
9309
10280
|
const preReadyStart = performance.now();
|
|
9310
10281
|
try {
|
|
9311
|
-
await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event)));
|
|
10282
|
+
await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event, reservations)));
|
|
9312
10283
|
} catch (error) {
|
|
9313
10284
|
this.logger.error("Host pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
9314
10285
|
}
|
|
@@ -9322,7 +10293,7 @@ var HybridProjectionCoordinator = class {
|
|
|
9322
10293
|
const emitDurationMs = performance.now() - emitStart;
|
|
9323
10294
|
const postReadyStart = performance.now();
|
|
9324
10295
|
try {
|
|
9325
|
-
await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event)));
|
|
10296
|
+
await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event, reservations)));
|
|
9326
10297
|
} catch (error) {
|
|
9327
10298
|
this.logger.error("Host post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
9328
10299
|
}
|
|
@@ -9336,11 +10307,11 @@ var HybridProjectionCoordinator = class {
|
|
|
9336
10307
|
postReadyDurationMs
|
|
9337
10308
|
});
|
|
9338
10309
|
}
|
|
9339
|
-
async indexWithTiming(readModel, stage, event) {
|
|
10310
|
+
async indexWithTiming(readModel, stage, event, reservations) {
|
|
9340
10311
|
const start = performance.now();
|
|
9341
10312
|
let success = false;
|
|
9342
10313
|
try {
|
|
9343
|
-
await readModel
|
|
10314
|
+
await indexReserved(readModel, event.operations, reservations);
|
|
9344
10315
|
success = true;
|
|
9345
10316
|
} finally {
|
|
9346
10317
|
this.emitReadModelIndexed({
|
|
@@ -9521,6 +10492,17 @@ var ReactorClientBuilder = class {
|
|
|
9521
10492
|
flags: reactorModule.featureFlags
|
|
9522
10493
|
} : void 0;
|
|
9523
10494
|
}
|
|
10495
|
+
/** Selection reads the sync manager's agreement; without a module, none. */
|
|
10496
|
+
resolveProtocolSelection(reactorModule) {
|
|
10497
|
+
if (!reactorModule || !this.reactorBuilder) return;
|
|
10498
|
+
const syncManager = reactorModule.syncModule?.syncManager;
|
|
10499
|
+
return {
|
|
10500
|
+
capabilities: this.reactorBuilder.getPeerCapabilities(),
|
|
10501
|
+
flags: reactorModule.featureFlags,
|
|
10502
|
+
agreement: syncManager ? () => syncManager.agreement() : void 0,
|
|
10503
|
+
collectionsOf: (documentIds) => reactorModule.operationIndex.getCollectionsForDocuments(documentIds)
|
|
10504
|
+
};
|
|
10505
|
+
}
|
|
9524
10506
|
async build() {
|
|
9525
10507
|
return (await this.buildModule()).client;
|
|
9526
10508
|
}
|
|
@@ -9552,7 +10534,7 @@ var ReactorClientBuilder = class {
|
|
|
9552
10534
|
const jobAwaiter = this.jobAwaiter ?? new JobAwaiter(eventBus, (jobId, signal) => reactor.getJobStatus(jobId, signal));
|
|
9553
10535
|
const decisionModel = reactorModule ? readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry) : void 0;
|
|
9554
10536
|
return {
|
|
9555
|
-
client: new ReactorClient(this.logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, this.readGate ?? this.resolveReadGate(reactorModule, documentView, decisionModel), this.resolveActionEvaluation(reactorModule, decisionModel), this.createSignaturePolicy),
|
|
10537
|
+
client: new ReactorClient(this.logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, this.readGate ?? this.resolveReadGate(reactorModule, documentView, decisionModel), this.resolveActionEvaluation(reactorModule, decisionModel), this.createSignaturePolicy, this.resolveProtocolSelection(reactorModule)),
|
|
9556
10538
|
reactor,
|
|
9557
10539
|
eventBus,
|
|
9558
10540
|
documentIndexer,
|
|
@@ -9852,6 +10834,6 @@ var DocumentIntegrityService = class {
|
|
|
9852
10834
|
}
|
|
9853
10835
|
};
|
|
9854
10836
|
//#endregion
|
|
9855
|
-
export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_COMMIT_CHUNK_SIZE, DEFAULT_DRIVE_CONTAINER_TYPES, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DeletedDocumentRead, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DriveRequestError, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, HybridProjectionCoordinator, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, InvalidSignatureError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RelationshipNotFoundError, RetryAccounting, RevisionMismatchError, SIGNATURE_REFUSAL_CODES, SeededStateReader, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncScopeGate, SyncStatus, SyncStatusTracker, addRelationshipAction, authDecisionModel, batchOperationsByDocument, buildDecisionModel, classifyJobFailure, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createHybridProjectionCoordinatorFactory, createMutableShutdownStatus, createReactorHostModuleBase, createRelationalDb, decideAtHead, defaultReadModelIndexingConfig, deleteDocumentAction, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, firstOutOfOrderPair, getMigrationStatus, instrumentPgPool, isDriveAuthError, isRecoverableGraphQLError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, quarantinesDocument, readDecisionModel, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, supportsLiveReadModelRegistration, syncOperationErrorType, trimMailboxFromAckOrdinal, unchunkedReadModelIndexingConfig, updateRelationshipAction, upgradeDocumentAction, verifyActionSignature, workerEntryPath };
|
|
10837
|
+
export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, CatchUpScheduler, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, ContiguousCursor, DEFAULT_COMMIT_CHUNK_SIZE, DEFAULT_DRIVE_CONTAINER_TYPES, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DeletedDocumentRead, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DriveRequestError, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, HybridProjectionCoordinator, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, InMemorySyncHoldStorage, IntervalPollTimer, InvalidModuleError, InvalidSignatureError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncHoldStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProbeSettler, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RelationshipNotFoundError, RetryAccounting, RevisionMismatchError, SIGNATURE_REFUSAL_CODES, SeededStateReader, SettledWatermark, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncScopeGate, SyncStatus, SyncStatusTracker, UnsupportedStoredProtocolError, addRelationshipAction, authDecisionModel, batchOperationsByDocument, buildDecisionModel, classifyJobFailure, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createHybridProjectionCoordinatorFactory, createKyselyWatermarkProbe, createMutableShutdownStatus, createPeerAgreement, createReactorHostModuleBase, createRelationalDb, decideAtHead, defaultCatchUpConfig, defaultReadModelIndexingConfig, deleteDocumentAction, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, firstOutOfOrderPair, getMigrationStatus, instrumentPgPool, isCatchUpConsumer, isDriveAuthError, isRecoverableGraphQLError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, parseSnapshot, quarantinesDocument, readDecisionModel, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, snapshotFunctionsFor, supportsLiveReadModelRegistration, syncOperationErrorType, trimMailboxFromAckOrdinal, unchunkedReadModelIndexingConfig, updateRelationshipAction, upgradeDocumentAction, verifyActionSignature, workerEntryPath };
|
|
9856
10838
|
|
|
9857
10839
|
//# sourceMappingURL=index.js.map
|