@hops-ops/distributed 0.0.0
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/README.md +454 -0
- package/dist/auth-headers.d.ts +8 -0
- package/dist/auth-headers.js +35 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +1 -0
- package/dist/document.d.ts +8 -0
- package/dist/document.js +6 -0
- package/dist/identity.d.ts +23 -0
- package/dist/identity.js +73 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/internal/cache-engine/create.d.ts +5 -0
- package/dist/internal/cache-engine/create.js +7 -0
- package/dist/internal/cache-engine/engine.d.ts +32 -0
- package/dist/internal/cache-engine/engine.js +1287 -0
- package/dist/internal/cache-engine/errors.d.ts +5 -0
- package/dist/internal/cache-engine/errors.js +10 -0
- package/dist/internal/cache-engine/helpers.d.ts +55 -0
- package/dist/internal/cache-engine/helpers.js +545 -0
- package/dist/internal/cache-engine/index.d.ts +3 -0
- package/dist/internal/cache-engine/index.js +2 -0
- package/dist/internal/cache-engine/types.d.ts +300 -0
- package/dist/internal/cache-engine/types.js +1 -0
- package/dist/internal/cache-engine.d.ts +3 -0
- package/dist/internal/cache-engine.js +2 -0
- package/dist/lib/assert-name.d.ts +2 -0
- package/dist/lib/assert-name.js +6 -0
- package/dist/lib/compare-code-units.d.ts +2 -0
- package/dist/lib/compare-code-units.js +4 -0
- package/dist/lib/deep-equal.d.ts +2 -0
- package/dist/lib/deep-equal.js +24 -0
- package/dist/lib/freeze-record.d.ts +2 -0
- package/dist/lib/freeze-record.js +13 -0
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/is-plain-record.d.ts +2 -0
- package/dist/lib/is-plain-record.js +8 -0
- package/dist/lib/report.d.ts +7 -0
- package/dist/lib/report.js +26 -0
- package/dist/protocol.d.ts +179 -0
- package/dist/protocol.js +558 -0
- package/dist/react/context.d.ts +20 -0
- package/dist/react/context.js +24 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +3 -0
- package/dist/react/query.d.ts +20 -0
- package/dist/react/query.js +99 -0
- package/dist/replica/command-id.d.ts +2 -0
- package/dist/replica/command-id.js +19 -0
- package/dist/replica/command-runtime/constants.d.ts +5 -0
- package/dist/replica/command-runtime/constants.js +5 -0
- package/dist/replica/command-runtime/create.d.ts +2 -0
- package/dist/replica/command-runtime/create.js +644 -0
- package/dist/replica/command-runtime/errors.d.ts +16 -0
- package/dist/replica/command-runtime/errors.js +41 -0
- package/dist/replica/command-runtime/index.d.ts +5 -0
- package/dist/replica/command-runtime/index.js +4 -0
- package/dist/replica/command-runtime/lib/binding.d.ts +4 -0
- package/dist/replica/command-runtime/lib/binding.js +64 -0
- package/dist/replica/command-runtime/lib/effects.d.ts +11 -0
- package/dist/replica/command-runtime/lib/effects.js +89 -0
- package/dist/replica/command-runtime/lib/index.d.ts +9 -0
- package/dist/replica/command-runtime/lib/index.js +9 -0
- package/dist/replica/command-runtime/lib/inventory.d.ts +10 -0
- package/dist/replica/command-runtime/lib/inventory.js +141 -0
- package/dist/replica/command-runtime/lib/output.d.ts +13 -0
- package/dist/replica/command-runtime/lib/output.js +190 -0
- package/dist/replica/command-runtime/lib/projection.d.ts +20 -0
- package/dist/replica/command-runtime/lib/projection.js +255 -0
- package/dist/replica/command-runtime/lib/status.d.ts +7 -0
- package/dist/replica/command-runtime/lib/status.js +127 -0
- package/dist/replica/command-runtime/lib/transport.d.ts +16 -0
- package/dist/replica/command-runtime/lib/transport.js +112 -0
- package/dist/replica/command-runtime/lib/util.d.ts +16 -0
- package/dist/replica/command-runtime/lib/util.js +127 -0
- package/dist/replica/command-runtime/lifecycle.d.ts +11 -0
- package/dist/replica/command-runtime/lifecycle.js +13 -0
- package/dist/replica/command-runtime/symbols.d.ts +30 -0
- package/dist/replica/command-runtime/symbols.js +30 -0
- package/dist/replica/command-runtime/types.d.ts +222 -0
- package/dist/replica/command-runtime/types.js +1 -0
- package/dist/replica/command-runtime.d.ts +3 -0
- package/dist/replica/command-runtime.js +2 -0
- package/dist/replica/commands/clone.d.ts +12 -0
- package/dist/replica/commands/clone.js +187 -0
- package/dist/replica/commands/constants.d.ts +6 -0
- package/dist/replica/commands/constants.js +6 -0
- package/dist/replica/commands/errors.d.ts +7 -0
- package/dist/replica/commands/errors.js +22 -0
- package/dist/replica/commands/implementation.d.ts +8 -0
- package/dist/replica/commands/implementation.js +8 -0
- package/dist/replica/commands/index.d.ts +5 -0
- package/dist/replica/commands/index.js +4 -0
- package/dist/replica/commands/prepare.d.ts +27 -0
- package/dist/replica/commands/prepare.js +77 -0
- package/dist/replica/commands/presets.d.ts +12 -0
- package/dist/replica/commands/presets.js +84 -0
- package/dist/replica/commands/receipt.d.ts +3 -0
- package/dist/replica/commands/receipt.js +36 -0
- package/dist/replica/commands/resolve.d.ts +11 -0
- package/dist/replica/commands/resolve.js +165 -0
- package/dist/replica/commands/types.d.ts +278 -0
- package/dist/replica/commands/types.js +2 -0
- package/dist/replica/commands/util.d.ts +21 -0
- package/dist/replica/commands/util.js +96 -0
- package/dist/replica/commands/validate.d.ts +18 -0
- package/dist/replica/commands/validate.js +477 -0
- package/dist/replica/commands.d.ts +3 -0
- package/dist/replica/commands.js +2 -0
- package/dist/replica/diagnostics/event-log.d.ts +4 -0
- package/dist/replica/diagnostics/event-log.js +463 -0
- package/dist/replica/diagnostics/implementation.d.ts +4 -0
- package/dist/replica/diagnostics/implementation.js +4 -0
- package/dist/replica/diagnostics/index.d.ts +2 -0
- package/dist/replica/diagnostics/index.js +1 -0
- package/dist/replica/diagnostics/inspect.d.ts +6 -0
- package/dist/replica/diagnostics/inspect.js +170 -0
- package/dist/replica/diagnostics/types.d.ts +295 -0
- package/dist/replica/diagnostics/types.js +2 -0
- package/dist/replica/diagnostics.d.ts +3 -0
- package/dist/replica/diagnostics.js +2 -0
- package/dist/replica/distributed-replica/clocks.d.ts +23 -0
- package/dist/replica/distributed-replica/clocks.js +156 -0
- package/dist/replica/distributed-replica/constants.d.ts +12 -0
- package/dist/replica/distributed-replica/constants.js +10 -0
- package/dist/replica/distributed-replica/helpers.d.ts +48 -0
- package/dist/replica/distributed-replica/helpers.js +445 -0
- package/dist/replica/distributed-replica/hydration.d.ts +19 -0
- package/dist/replica/distributed-replica/hydration.js +380 -0
- package/dist/replica/distributed-replica/impl-diagnostics.d.ts +22 -0
- package/dist/replica/distributed-replica/impl-diagnostics.js +189 -0
- package/dist/replica/distributed-replica/impl-fetch-live.d.ts +39 -0
- package/dist/replica/distributed-replica/impl-fetch-live.js +395 -0
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +56 -0
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +299 -0
- package/dist/replica/distributed-replica/impl-optimistic.d.ts +31 -0
- package/dist/replica/distributed-replica/impl-optimistic.js +172 -0
- package/dist/replica/distributed-replica/impl-protocol.d.ts +53 -0
- package/dist/replica/distributed-replica/impl-protocol.js +108 -0
- package/dist/replica/distributed-replica/impl.d.ts +44 -0
- package/dist/replica/distributed-replica/impl.js +1776 -0
- package/dist/replica/distributed-replica/index.d.ts +2 -0
- package/dist/replica/distributed-replica/index.js +4 -0
- package/dist/replica/distributed-replica/optimistic.d.ts +30 -0
- package/dist/replica/distributed-replica/optimistic.js +221 -0
- package/dist/replica/distributed-replica/types.d.ts +147 -0
- package/dist/replica/distributed-replica/types.js +1 -0
- package/dist/replica/distributed-replica/watch.d.ts +19 -0
- package/dist/replica/distributed-replica/watch.js +95 -0
- package/dist/replica/distributed-replica.d.ts +2 -0
- package/dist/replica/distributed-replica.js +2 -0
- package/dist/replica/graphql-transport.d.ts +24 -0
- package/dist/replica/graphql-transport.js +122 -0
- package/dist/replica/identity/clone.d.ts +6 -0
- package/dist/replica/identity/clone.js +54 -0
- package/dist/replica/identity/codec.d.ts +48 -0
- package/dist/replica/identity/codec.js +789 -0
- package/dist/replica/identity/constants.d.ts +3 -0
- package/dist/replica/identity/constants.js +18 -0
- package/dist/replica/identity/implementation.d.ts +5 -0
- package/dist/replica/identity/implementation.js +5 -0
- package/dist/replica/identity/index.d.ts +3 -0
- package/dist/replica/identity/index.js +3 -0
- package/dist/replica/identity/keys.d.ts +29 -0
- package/dist/replica/identity/keys.js +173 -0
- package/dist/replica/identity.d.ts +2 -0
- package/dist/replica/identity.js +2 -0
- package/dist/replica/index-maintenance/engine.d.ts +84 -0
- package/dist/replica/index-maintenance/engine.js +677 -0
- package/dist/replica/index-maintenance/implementation.d.ts +4 -0
- package/dist/replica/index-maintenance/implementation.js +4 -0
- package/dist/replica/index-maintenance/index.d.ts +2 -0
- package/dist/replica/index-maintenance/index.js +1 -0
- package/dist/replica/index-maintenance/registry.d.ts +2 -0
- package/dist/replica/index-maintenance/registry.js +49 -0
- package/dist/replica/index-maintenance/types.d.ts +58 -0
- package/dist/replica/index-maintenance/types.js +1 -0
- package/dist/replica/index-maintenance.d.ts +3 -0
- package/dist/replica/index-maintenance.js +2 -0
- package/dist/replica/index.d.ts +17 -0
- package/dist/replica/index.js +9 -0
- package/dist/replica/materialize.d.ts +10 -0
- package/dist/replica/materialize.js +163 -0
- package/dist/replica/normalize.d.ts +27 -0
- package/dist/replica/normalize.js +298 -0
- package/dist/replica/operation-binding.d.ts +14 -0
- package/dist/replica/operation-binding.js +76 -0
- package/dist/replica/persistence/idb.d.ts +20 -0
- package/dist/replica/persistence/idb.js +193 -0
- package/dist/replica/persistence/implementation.d.ts +4 -0
- package/dist/replica/persistence/implementation.js +4 -0
- package/dist/replica/persistence/index.d.ts +2 -0
- package/dist/replica/persistence/index.js +1 -0
- package/dist/replica/persistence/state.d.ts +104 -0
- package/dist/replica/persistence/state.js +734 -0
- package/dist/replica/persistence/types.d.ts +54 -0
- package/dist/replica/persistence/types.js +1 -0
- package/dist/replica/persistence.d.ts +3 -0
- package/dist/replica/persistence.js +2 -0
- package/dist/replica/query-plan/constants.d.ts +13 -0
- package/dist/replica/query-plan/constants.js +7 -0
- package/dist/replica/query-plan/filter.d.ts +39 -0
- package/dist/replica/query-plan/filter.js +544 -0
- package/dist/replica/query-plan/index.d.ts +4 -0
- package/dist/replica/query-plan/index.js +3 -0
- package/dist/replica/query-plan/order.d.ts +11 -0
- package/dist/replica/query-plan/order.js +158 -0
- package/dist/replica/query-plan/pagination.d.ts +6 -0
- package/dist/replica/query-plan/pagination.js +90 -0
- package/dist/replica/query-plan/resolve.d.ts +14 -0
- package/dist/replica/query-plan/resolve.js +163 -0
- package/dist/replica/query-plan/types.d.ts +89 -0
- package/dist/replica/query-plan/types.js +1 -0
- package/dist/replica/query-plan/util.d.ts +13 -0
- package/dist/replica/query-plan/util.js +62 -0
- package/dist/replica/query-plan.d.ts +3 -0
- package/dist/replica/query-plan.js +2 -0
- package/dist/replica/revalidation.d.ts +9 -0
- package/dist/replica/revalidation.js +86 -0
- package/dist/replica/selection.d.ts +20 -0
- package/dist/replica/selection.js +109 -0
- package/dist/replica/types.d.ts +657 -0
- package/dist/replica/types.js +1 -0
- package/dist/request.d.ts +17 -0
- package/dist/request.js +123 -0
- package/dist/sveltekit/auth.d.ts +18 -0
- package/dist/sveltekit/auth.js +9 -0
- package/dist/sveltekit/context.d.ts +28 -0
- package/dist/sveltekit/context.js +58 -0
- package/dist/sveltekit/index.d.ts +4 -0
- package/dist/sveltekit/index.js +4 -0
- package/dist/sveltekit/replica.d.ts +145 -0
- package/dist/sveltekit/replica.js +491 -0
- package/dist/sveltekit/server-replica.d.ts +52 -0
- package/dist/sveltekit/server-replica.js +176 -0
- package/dist/sveltekit/vite.d.ts +103 -0
- package/dist/sveltekit/vite.js +878 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/dist/websocket.d.ts +41 -0
- package/dist/websocket.js +187 -0
- package/package.json +95 -0
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
import { CacheRevisionConflictError, createCacheEngine } from '../../internal/cache-engine.js';
|
|
2
|
+
import { compareDistributedDecimal, DistributedProtocolError, parseGraphqlResponseExtensions } from '../../protocol.js';
|
|
3
|
+
import { matchReplicaTrustedPresetInventory } from '../commands.js';
|
|
4
|
+
import { replicaCommandAuthority, replicaCommandDirectProjection, replicaResultObservation } from '../command-runtime.js';
|
|
5
|
+
import { canonicalizeOperationVariables, replicaIndexKey, replicaRecordKey, resolveArguments } from '../identity.js';
|
|
6
|
+
import { materializeReplicaOperation } from '../materialize.js';
|
|
7
|
+
import { normalizeReplicaResult } from '../normalize.js';
|
|
8
|
+
import { createReplicaRevalidationMatcher } from '../revalidation.js';
|
|
9
|
+
import { validateReplicaOperationBinding as validatedArtifactBinding } from '../operation-binding.js';
|
|
10
|
+
import { createReplicaIndexMaintenanceRegistry, formatReplicaIndexStaleReason } from '../index-maintenance.js';
|
|
11
|
+
import { EMPTY_ERRORS, EMPTY_TRUSTED_PRESETS, MAX_ANONYMOUS_RECORD_CLOCKS } from './constants.js';
|
|
12
|
+
import { compareCanonicalDecimalStrings, compareEvidenceToProjectedFence, compareIndexVector, compareProjectedRecordFields, compareRecordClock, compareSnapshotToOperationState, incrementCanonicalDecimal, indexClockMap, isComparableHandoffDisposition, latestCursors, protocolInvalid, recordKeyMatchesModel, responsePathKey, sameRecordClock, sameRecordRevision } from './clocks.js';
|
|
13
|
+
import { diagnosticReceiptCounts } from './optimistic.js';
|
|
14
|
+
import { assertWriteSource, indexKeyFromTarget, indexMaintenanceSnapshot, indexSemanticLayer, operationKey, prepareRecordEvidence, protocolOperationSource, replicaResultIndexKeys, reportSafely, reportUnhandledObserverError, snapshotFrom, stableErrors, trustedPresetDescriptorFingerprint, validatedCommandAuthorityContract } from './helpers.js';
|
|
15
|
+
import { ReplicaWatchState } from './watch.js';
|
|
16
|
+
import { closeActiveTransports as closeActiveTransportsOn, emitWatchState, fetchWatch, releaseLive as releaseLiveOn, restartLive as restartLiveOn, retainLive as retainLiveOn, resumeLiveWatches as resumeLiveWatchesOn } from './impl-fetch-live.js';
|
|
17
|
+
import { closeAuthorizationGeneration as closeAuthorizationGenerationOn, purgeProtocolGeneration as purgeProtocolGenerationOn, stageProtocolGeneration as stageProtocolGenerationOn, stageTrustedPresets as stageTrustedPresetsOn, validateProtocolBinding as validateProtocolBindingOn } from './impl-protocol.js';
|
|
18
|
+
import { applyReceiptOnly as applyReceiptOnlyOn, confirmOptimisticLayerOn, createOptimisticLayerOn, markOptimisticLayerAcceptedOn, planOptimisticReceipts as planOptimisticReceiptsOn, rejectOptimisticLayerOn } from './impl-optimistic.js';
|
|
19
|
+
import { dehydrateReplica, finishDehydration as finishDehydrationOn, hydrateReplica } from './impl-hydration-orchestrate.js';
|
|
20
|
+
import { diagnosticEvent as diagnosticEventOn, diagnosticOperation as diagnosticOperationOn, diagnosticScopeTransition as diagnosticScopeTransitionOn, retireDiagnosticLayer as retireDiagnosticLayerOn, syncDiagnostics as syncDiagnosticsOn } from './impl-diagnostics.js';
|
|
21
|
+
export class DistributedReplicaImpl {
|
|
22
|
+
#engine;
|
|
23
|
+
#transport;
|
|
24
|
+
#reportObserverError;
|
|
25
|
+
#diagnostics;
|
|
26
|
+
#diagnosticLayers;
|
|
27
|
+
#inFlight = new Map();
|
|
28
|
+
#inFlightAborts = new Map();
|
|
29
|
+
#queryStates = new Map();
|
|
30
|
+
#watches = new Map();
|
|
31
|
+
#lives = new Map();
|
|
32
|
+
#operationProtocols = new Map();
|
|
33
|
+
#operationGenerations = new Map();
|
|
34
|
+
#recordClocks = new Map();
|
|
35
|
+
#recordKeysByScope = new Map();
|
|
36
|
+
#projectedRecordFences = new Map();
|
|
37
|
+
#anonymousRecordClocks = new Map();
|
|
38
|
+
#optimisticReceipts = new Map();
|
|
39
|
+
#renderedOperations = new Map();
|
|
40
|
+
#readOperationKeys = new Set();
|
|
41
|
+
#watchRenderCounts = new Map();
|
|
42
|
+
#resultObservers = new Set();
|
|
43
|
+
#indexMaintenance = createReplicaIndexMaintenanceRegistry();
|
|
44
|
+
#indexPlanRegistrations = new Map();
|
|
45
|
+
#derivedIndexReconciler = (confirmed, layers) => this.#deriveMaintainedIndexes(confirmed, layers);
|
|
46
|
+
#commandAuthorityContract;
|
|
47
|
+
#trustedPresets = EMPTY_TRUSTED_PRESETS;
|
|
48
|
+
#authorizationAbort = new AbortController();
|
|
49
|
+
#artifactBinding;
|
|
50
|
+
#protocolGeneration;
|
|
51
|
+
#protocolGenerationSequence = 0;
|
|
52
|
+
#projectionGeneration = 0;
|
|
53
|
+
#nextIndexRevision = '0';
|
|
54
|
+
#diagnosticLayerSequence = 0;
|
|
55
|
+
#fetchLiveHostCache;
|
|
56
|
+
#protocolHostCache;
|
|
57
|
+
#optimisticHostCache;
|
|
58
|
+
#hydrationHostCache;
|
|
59
|
+
#diagnosticsHostCache;
|
|
60
|
+
constructor(options = {}) {
|
|
61
|
+
this.#transport = options.transport;
|
|
62
|
+
this.#reportObserverError = options.onObserverError ?? reportUnhandledObserverError;
|
|
63
|
+
this.#diagnostics = options.diagnostics;
|
|
64
|
+
this.#diagnosticLayers =
|
|
65
|
+
options.diagnostics === undefined ? undefined : new Map();
|
|
66
|
+
this.#engine = createCacheEngine({ onWatcherError: this.#reportObserverError });
|
|
67
|
+
this.#engine.setDerivedIndexReconciler(this.#derivedIndexReconciler);
|
|
68
|
+
this.#syncDiagnostics();
|
|
69
|
+
}
|
|
70
|
+
#diagnosticsEnabled() {
|
|
71
|
+
return Object.freeze({ enabled: this.#diagnostics !== undefined });
|
|
72
|
+
}
|
|
73
|
+
#diagnosticsHost() {
|
|
74
|
+
if (this.#diagnosticsHostCache !== undefined)
|
|
75
|
+
return this.#diagnosticsHostCache;
|
|
76
|
+
const self = this;
|
|
77
|
+
this.#diagnosticsHostCache = {
|
|
78
|
+
get engine() {
|
|
79
|
+
return self.#engine;
|
|
80
|
+
},
|
|
81
|
+
get diagnostics() {
|
|
82
|
+
return self.#diagnostics;
|
|
83
|
+
},
|
|
84
|
+
get diagnosticLayers() {
|
|
85
|
+
return self.#diagnosticLayers;
|
|
86
|
+
},
|
|
87
|
+
get optimisticReceipts() {
|
|
88
|
+
return self.#optimisticReceipts;
|
|
89
|
+
},
|
|
90
|
+
get reportObserverError() {
|
|
91
|
+
return self.#reportObserverError;
|
|
92
|
+
},
|
|
93
|
+
getProtocolGeneration: () => self.#protocolGeneration,
|
|
94
|
+
getProtocolGenerationSequence: () => self.#protocolGenerationSequence
|
|
95
|
+
};
|
|
96
|
+
return this.#diagnosticsHostCache;
|
|
97
|
+
}
|
|
98
|
+
#fetchLiveHost() {
|
|
99
|
+
if (this.#fetchLiveHostCache !== undefined)
|
|
100
|
+
return this.#fetchLiveHostCache;
|
|
101
|
+
const self = this;
|
|
102
|
+
this.#fetchLiveHostCache = {
|
|
103
|
+
get transport() {
|
|
104
|
+
return self.#transport;
|
|
105
|
+
},
|
|
106
|
+
get inFlight() {
|
|
107
|
+
return self.#inFlight;
|
|
108
|
+
},
|
|
109
|
+
get inFlightAborts() {
|
|
110
|
+
return self.#inFlightAborts;
|
|
111
|
+
},
|
|
112
|
+
get lives() {
|
|
113
|
+
return self.#lives;
|
|
114
|
+
},
|
|
115
|
+
get watches() {
|
|
116
|
+
return self.#watches;
|
|
117
|
+
},
|
|
118
|
+
get operationProtocols() {
|
|
119
|
+
return self.#operationProtocols;
|
|
120
|
+
},
|
|
121
|
+
get diagnostics() {
|
|
122
|
+
return self.#diagnosticsEnabled();
|
|
123
|
+
},
|
|
124
|
+
protocolGenerationSequence: () => self.#protocolGenerationSequence,
|
|
125
|
+
projectionGeneration: () => self.#projectionGeneration,
|
|
126
|
+
protocolGeneration: () => self.#protocolGeneration,
|
|
127
|
+
queryState: (key) => self.#queryState(key),
|
|
128
|
+
emitState: (key, allowFetch) => self.#emitState(key, allowFetch),
|
|
129
|
+
operationGeneration: (key) => self.#operationGeneration(key),
|
|
130
|
+
allocateIndexRevision: () => self.#allocateIndexRevision(),
|
|
131
|
+
writeCanonicalResult: (artifact, stableVariables, envelope, source, requestRevision, responseProjectionGeneration) => self.#writeCanonicalResult(artifact, stableVariables, envelope, source, requestRevision, responseProjectionGeneration),
|
|
132
|
+
diagnosticEvent: (event) => self.#diagnosticEvent(event),
|
|
133
|
+
resumeCursors: (key) => self.#resumeCursors(key)
|
|
134
|
+
};
|
|
135
|
+
return this.#fetchLiveHostCache;
|
|
136
|
+
}
|
|
137
|
+
#protocolHost() {
|
|
138
|
+
if (this.#protocolHostCache !== undefined)
|
|
139
|
+
return this.#protocolHostCache;
|
|
140
|
+
const self = this;
|
|
141
|
+
this.#protocolHostCache = {
|
|
142
|
+
get engine() {
|
|
143
|
+
return self.#engine;
|
|
144
|
+
},
|
|
145
|
+
get queryStates() {
|
|
146
|
+
return self.#queryStates;
|
|
147
|
+
},
|
|
148
|
+
get operationProtocols() {
|
|
149
|
+
return self.#operationProtocols;
|
|
150
|
+
},
|
|
151
|
+
get operationGenerations() {
|
|
152
|
+
return self.#operationGenerations;
|
|
153
|
+
},
|
|
154
|
+
get recordClocks() {
|
|
155
|
+
return self.#recordClocks;
|
|
156
|
+
},
|
|
157
|
+
get recordKeysByScope() {
|
|
158
|
+
return self.#recordKeysByScope;
|
|
159
|
+
},
|
|
160
|
+
get projectedRecordFences() {
|
|
161
|
+
return self.#projectedRecordFences;
|
|
162
|
+
},
|
|
163
|
+
get anonymousRecordClocks() {
|
|
164
|
+
return self.#anonymousRecordClocks;
|
|
165
|
+
},
|
|
166
|
+
get optimisticReceipts() {
|
|
167
|
+
return self.#optimisticReceipts;
|
|
168
|
+
},
|
|
169
|
+
get diagnosticLayers() {
|
|
170
|
+
return self.#diagnosticLayers;
|
|
171
|
+
},
|
|
172
|
+
get indexPlanRegistrations() {
|
|
173
|
+
return self.#indexPlanRegistrations;
|
|
174
|
+
},
|
|
175
|
+
get renderedOperations() {
|
|
176
|
+
return self.#renderedOperations;
|
|
177
|
+
},
|
|
178
|
+
get readOperationKeys() {
|
|
179
|
+
return self.#readOperationKeys;
|
|
180
|
+
},
|
|
181
|
+
get watchRenderCounts() {
|
|
182
|
+
return self.#watchRenderCounts;
|
|
183
|
+
},
|
|
184
|
+
get watches() {
|
|
185
|
+
return self.#watches;
|
|
186
|
+
},
|
|
187
|
+
get diagnostics() {
|
|
188
|
+
return self.#diagnosticsEnabled();
|
|
189
|
+
},
|
|
190
|
+
getProtocolGeneration: () => self.#protocolGeneration,
|
|
191
|
+
setProtocolGeneration: (value) => {
|
|
192
|
+
self.#protocolGeneration = value;
|
|
193
|
+
},
|
|
194
|
+
getProtocolGenerationSequence: () => self.#protocolGenerationSequence,
|
|
195
|
+
bumpProtocolGenerationSequence: () => {
|
|
196
|
+
self.#protocolGenerationSequence += 1;
|
|
197
|
+
},
|
|
198
|
+
getTrustedPresets: () => self.#trustedPresets,
|
|
199
|
+
setTrustedPresets: (value) => {
|
|
200
|
+
self.#trustedPresets = value;
|
|
201
|
+
},
|
|
202
|
+
getCommandAuthorityContract: () => self.#commandAuthorityContract,
|
|
203
|
+
setProjectionGeneration: (value) => {
|
|
204
|
+
self.#projectionGeneration = value;
|
|
205
|
+
},
|
|
206
|
+
setNextIndexRevision: (value) => {
|
|
207
|
+
self.#nextIndexRevision = value;
|
|
208
|
+
},
|
|
209
|
+
setDiagnosticLayerSequence: (value) => {
|
|
210
|
+
self.#diagnosticLayerSequence = value;
|
|
211
|
+
},
|
|
212
|
+
clearIndexMaintenance: () => {
|
|
213
|
+
self.#indexMaintenance.clear();
|
|
214
|
+
},
|
|
215
|
+
abortAuthorization: () => {
|
|
216
|
+
self.#authorizationAbort.abort();
|
|
217
|
+
self.#authorizationAbort = new AbortController();
|
|
218
|
+
},
|
|
219
|
+
closeActiveTransports: () => self.#closeActiveTransports(),
|
|
220
|
+
syncDiagnostics: () => self.#syncDiagnostics(),
|
|
221
|
+
emitState: (key, allowFetch) => self.#emitState(key, allowFetch),
|
|
222
|
+
diagnosticEvent: (event) => self.#diagnosticEvent(event),
|
|
223
|
+
rememberRenderedOperation: (key, artifact, variables, source) => self.#rememberRenderedOperation(key, artifact, variables, source)
|
|
224
|
+
};
|
|
225
|
+
return this.#protocolHostCache;
|
|
226
|
+
}
|
|
227
|
+
#optimisticHost() {
|
|
228
|
+
if (this.#optimisticHostCache !== undefined)
|
|
229
|
+
return this.#optimisticHostCache;
|
|
230
|
+
const self = this;
|
|
231
|
+
this.#optimisticHostCache = {
|
|
232
|
+
get engine() {
|
|
233
|
+
return self.#engine;
|
|
234
|
+
},
|
|
235
|
+
get optimisticReceipts() {
|
|
236
|
+
return self.#optimisticReceipts;
|
|
237
|
+
},
|
|
238
|
+
get diagnosticLayers() {
|
|
239
|
+
return self.#diagnosticLayers;
|
|
240
|
+
},
|
|
241
|
+
get diagnostics() {
|
|
242
|
+
return self.#diagnosticsEnabled();
|
|
243
|
+
},
|
|
244
|
+
getDiagnosticLayerSequence: () => self.#diagnosticLayerSequence,
|
|
245
|
+
setDiagnosticLayerSequence: (value) => {
|
|
246
|
+
self.#diagnosticLayerSequence = value;
|
|
247
|
+
},
|
|
248
|
+
diagnosticEvent: (event) => self.#diagnosticEvent(event),
|
|
249
|
+
syncDiagnostics: () => self.#syncDiagnostics(),
|
|
250
|
+
retireDiagnosticLayer: (id, action, receiptState, receipt) => self.#retireDiagnosticLayer(id, action, receiptState, receipt)
|
|
251
|
+
};
|
|
252
|
+
return this.#optimisticHostCache;
|
|
253
|
+
}
|
|
254
|
+
#hydrationHost() {
|
|
255
|
+
if (this.#hydrationHostCache !== undefined)
|
|
256
|
+
return this.#hydrationHostCache;
|
|
257
|
+
const self = this;
|
|
258
|
+
this.#hydrationHostCache = {
|
|
259
|
+
get engine() {
|
|
260
|
+
return self.#engine;
|
|
261
|
+
},
|
|
262
|
+
get operationProtocols() {
|
|
263
|
+
return self.#operationProtocols;
|
|
264
|
+
},
|
|
265
|
+
get operationGenerations() {
|
|
266
|
+
return self.#operationGenerations;
|
|
267
|
+
},
|
|
268
|
+
get recordClocks() {
|
|
269
|
+
return self.#recordClocks;
|
|
270
|
+
},
|
|
271
|
+
get recordKeysByScope() {
|
|
272
|
+
return self.#recordKeysByScope;
|
|
273
|
+
},
|
|
274
|
+
get anonymousRecordClocks() {
|
|
275
|
+
return self.#anonymousRecordClocks;
|
|
276
|
+
},
|
|
277
|
+
get optimisticReceipts() {
|
|
278
|
+
return self.#optimisticReceipts;
|
|
279
|
+
},
|
|
280
|
+
get diagnosticLayers() {
|
|
281
|
+
return self.#diagnosticLayers;
|
|
282
|
+
},
|
|
283
|
+
get renderedOperations() {
|
|
284
|
+
return self.#renderedOperations;
|
|
285
|
+
},
|
|
286
|
+
get readOperationKeys() {
|
|
287
|
+
return self.#readOperationKeys;
|
|
288
|
+
},
|
|
289
|
+
get watchRenderCounts() {
|
|
290
|
+
return self.#watchRenderCounts;
|
|
291
|
+
},
|
|
292
|
+
get indexPlanRegistrations() {
|
|
293
|
+
return self.#indexPlanRegistrations;
|
|
294
|
+
},
|
|
295
|
+
get queryStates() {
|
|
296
|
+
return self.#queryStates;
|
|
297
|
+
},
|
|
298
|
+
get diagnostics() {
|
|
299
|
+
return self.#diagnosticsEnabled();
|
|
300
|
+
},
|
|
301
|
+
getProtocolGeneration: () => self.#protocolGeneration,
|
|
302
|
+
setProtocolGeneration: (value) => {
|
|
303
|
+
self.#protocolGeneration = value;
|
|
304
|
+
},
|
|
305
|
+
getProtocolGenerationSequence: () => self.#protocolGenerationSequence,
|
|
306
|
+
getTrustedPresets: () => self.#trustedPresets,
|
|
307
|
+
setTrustedPresets: (value) => {
|
|
308
|
+
self.#trustedPresets = value;
|
|
309
|
+
},
|
|
310
|
+
getNextIndexRevision: () => self.#nextIndexRevision,
|
|
311
|
+
setNextIndexRevision: (value) => {
|
|
312
|
+
self.#nextIndexRevision = value;
|
|
313
|
+
},
|
|
314
|
+
getArtifactBinding: () => self.#artifactBinding,
|
|
315
|
+
setArtifactBinding: (value) => {
|
|
316
|
+
self.#artifactBinding = value;
|
|
317
|
+
},
|
|
318
|
+
getCommandAuthorityContract: () => self.#commandAuthorityContract,
|
|
319
|
+
setDiagnosticLayerSequence: (value) => {
|
|
320
|
+
self.#diagnosticLayerSequence = value;
|
|
321
|
+
},
|
|
322
|
+
operationGeneration: (key) => self.#operationGeneration(key),
|
|
323
|
+
closeActiveTransports: () => self.#closeActiveTransports(),
|
|
324
|
+
closeAuthorizationGeneration: () => self.#closeAuthorizationGeneration(),
|
|
325
|
+
resumeLiveWatches: () => self.#resumeLiveWatches(),
|
|
326
|
+
syncDiagnostics: () => self.#syncDiagnostics(),
|
|
327
|
+
diagnosticEvent: (event) => self.#diagnosticEvent(event),
|
|
328
|
+
refreshIndexMaintenance: () => self.#refreshIndexMaintenance(),
|
|
329
|
+
finishDehydration: () => self.#finishDehydration()
|
|
330
|
+
};
|
|
331
|
+
return this.#hydrationHostCache;
|
|
332
|
+
}
|
|
333
|
+
get scope() {
|
|
334
|
+
const scope = this.#protocolGeneration;
|
|
335
|
+
return scope === undefined
|
|
336
|
+
? undefined
|
|
337
|
+
: Object.freeze({
|
|
338
|
+
protocolVersion: scope.protocolVersion,
|
|
339
|
+
schemaHash: scope.schemaHash,
|
|
340
|
+
cacheScope: scope.cacheScope
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
get authorizationGeneration() {
|
|
344
|
+
return this.#protocolGenerationSequence;
|
|
345
|
+
}
|
|
346
|
+
[replicaCommandAuthority](contract) {
|
|
347
|
+
const next = validatedCommandAuthorityContract(contract);
|
|
348
|
+
const current = this.#commandAuthorityContract;
|
|
349
|
+
if (current !== undefined && current.fingerprint !== next.fingerprint) {
|
|
350
|
+
throw new TypeError('command inventory does not match the active replica client surface');
|
|
351
|
+
}
|
|
352
|
+
const binding = this.#artifactBinding;
|
|
353
|
+
if (binding === undefined) {
|
|
354
|
+
this.#artifactBinding = Object.freeze({
|
|
355
|
+
version: 1,
|
|
356
|
+
schemaHash: next.schemaHash,
|
|
357
|
+
surfaceIdentity: next.surfaceIdentity,
|
|
358
|
+
trustedPresets: next.trustedPresets
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else if (binding.version !== 1 ||
|
|
362
|
+
binding.schemaHash !== next.schemaHash ||
|
|
363
|
+
(binding.surfaceIdentity !== undefined &&
|
|
364
|
+
binding.surfaceIdentity !== next.surfaceIdentity) ||
|
|
365
|
+
(binding.trustedPresets !== undefined &&
|
|
366
|
+
trustedPresetDescriptorFingerprint(binding.trustedPresets) !==
|
|
367
|
+
trustedPresetDescriptorFingerprint(next.trustedPresets))) {
|
|
368
|
+
throw new TypeError('command inventory does not match the active replica client surface');
|
|
369
|
+
}
|
|
370
|
+
else if (binding.surfaceIdentity === undefined ||
|
|
371
|
+
binding.trustedPresets === undefined) {
|
|
372
|
+
this.#artifactBinding = Object.freeze({
|
|
373
|
+
...binding,
|
|
374
|
+
...(binding.surfaceIdentity === undefined
|
|
375
|
+
? { surfaceIdentity: next.surfaceIdentity }
|
|
376
|
+
: {}),
|
|
377
|
+
...(binding.trustedPresets === undefined
|
|
378
|
+
? { trustedPresets: next.trustedPresets }
|
|
379
|
+
: {})
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (this.#protocolGeneration !== undefined) {
|
|
383
|
+
try {
|
|
384
|
+
matchReplicaTrustedPresetInventory(next.trustedPresets, this.#trustedPresets);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
this.#purgeProtocolGeneration();
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
this.#commandAuthorityContract = next;
|
|
392
|
+
let active = true;
|
|
393
|
+
const read = () => {
|
|
394
|
+
if (!active) {
|
|
395
|
+
throw new TypeError('replica command authority registration is disposed');
|
|
396
|
+
}
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
generation: this.#protocolGenerationSequence,
|
|
399
|
+
scope: this.scope,
|
|
400
|
+
trustedPresets: this.#trustedPresets,
|
|
401
|
+
signal: this.#authorizationAbort.signal
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
read,
|
|
406
|
+
dispose() {
|
|
407
|
+
active = false;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
[replicaResultObservation](observer) {
|
|
412
|
+
if (typeof observer !== 'function') {
|
|
413
|
+
throw new TypeError('replica result observer must be a function');
|
|
414
|
+
}
|
|
415
|
+
this.#resultObservers.add(observer);
|
|
416
|
+
let active = true;
|
|
417
|
+
return Object.freeze({
|
|
418
|
+
dispose: () => {
|
|
419
|
+
if (!active)
|
|
420
|
+
return;
|
|
421
|
+
active = false;
|
|
422
|
+
this.#resultObservers.delete(observer);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
[replicaCommandDirectProjection](commandId, projection) {
|
|
427
|
+
const { model, identity, evidence, fields } = projection;
|
|
428
|
+
if (evidence.model !== model.id || evidence.tombstone) {
|
|
429
|
+
protocolInvalid('extensions.distributed.command.records');
|
|
430
|
+
}
|
|
431
|
+
const recordKey = replicaRecordKey(model, identity);
|
|
432
|
+
const pendingRecordClocks = new Map();
|
|
433
|
+
const pendingRecordScopes = new Map();
|
|
434
|
+
const pendingAnonymousRecordClocks = new Map();
|
|
435
|
+
const consumedAnonymousRecordClocks = new Set();
|
|
436
|
+
const apply = this.#resolveRecordEvidence(recordKey, evidence, pendingRecordClocks, pendingRecordScopes, pendingAnonymousRecordClocks, consumedAnonymousRecordClocks);
|
|
437
|
+
this.confirmOptimisticLayer(commandId, (writer) => {
|
|
438
|
+
if (!apply)
|
|
439
|
+
return false;
|
|
440
|
+
return writer.writeRecord(model, identity, evidence.revision, {
|
|
441
|
+
incarnation: evidence.incarnation,
|
|
442
|
+
fields
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
for (const [key, clock] of pendingRecordClocks) {
|
|
446
|
+
this.#recordClocks.set(key, clock);
|
|
447
|
+
this.#recordKeysByScope.set(clock.scopeToken, key);
|
|
448
|
+
}
|
|
449
|
+
for (const [scopeToken, key] of pendingRecordScopes) {
|
|
450
|
+
this.#recordKeysByScope.set(scopeToken, key);
|
|
451
|
+
}
|
|
452
|
+
for (const [scopeToken, clock] of pendingAnonymousRecordClocks) {
|
|
453
|
+
if (!consumedAnonymousRecordClocks.has(scopeToken)) {
|
|
454
|
+
this.#anonymousRecordClocks.set(scopeToken, clock);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const scopeToken of consumedAnonymousRecordClocks) {
|
|
458
|
+
this.#anonymousRecordClocks.delete(scopeToken);
|
|
459
|
+
}
|
|
460
|
+
if (apply) {
|
|
461
|
+
this.#projectionGeneration += 1;
|
|
462
|
+
/*
|
|
463
|
+
* The command result is authoritative before an asynchronous read
|
|
464
|
+
* model necessarily catches up. Retain its complete row and causal
|
|
465
|
+
* clock as a write fence until a query acknowledges it or a newer
|
|
466
|
+
* record/tombstone supersedes it.
|
|
467
|
+
*/
|
|
468
|
+
this.#projectedRecordFences.set(recordKey, Object.freeze({
|
|
469
|
+
fields: Object.freeze({ ...fields }),
|
|
470
|
+
clock: Object.freeze({
|
|
471
|
+
scopeToken: evidence.scopeToken,
|
|
472
|
+
incarnation: evidence.incarnation,
|
|
473
|
+
revision: evidence.revision,
|
|
474
|
+
tombstone: false
|
|
475
|
+
}),
|
|
476
|
+
projectionGeneration: this.#projectionGeneration
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
read(artifact, variables) {
|
|
481
|
+
this.#bindArtifact(artifact);
|
|
482
|
+
const stableVariables = canonicalizeOperationVariables(artifact, variables);
|
|
483
|
+
const key = operationKey(artifact, stableVariables);
|
|
484
|
+
this.#rememberRenderedOperation(key, artifact, stableVariables, 'read');
|
|
485
|
+
const materialized = this.#engine.read((reader) => materializeReplicaOperation(reader, artifact, stableVariables));
|
|
486
|
+
return snapshotFrom(materialized, this.#queryState(key));
|
|
487
|
+
}
|
|
488
|
+
watch(artifact, variables, options = {}) {
|
|
489
|
+
this.#bindArtifact(artifact);
|
|
490
|
+
return new ReplicaWatchState(this, artifact, variables, options);
|
|
491
|
+
}
|
|
492
|
+
writeResult(artifact, variables, envelope, source) {
|
|
493
|
+
assertWriteSource(source);
|
|
494
|
+
this.#bindArtifact(artifact);
|
|
495
|
+
const stableVariables = canonicalizeOperationVariables(artifact, variables);
|
|
496
|
+
this.#writeCanonicalResult(artifact, stableVariables, envelope, source);
|
|
497
|
+
}
|
|
498
|
+
revalidate(plan) {
|
|
499
|
+
const matches = createReplicaRevalidationMatcher(plan);
|
|
500
|
+
const generation = this.#protocolGenerationSequence;
|
|
501
|
+
const active = new Map();
|
|
502
|
+
for (const [key, watches] of this.#watches) {
|
|
503
|
+
const watch = watches.values().next().value;
|
|
504
|
+
if (watch !== undefined && matches(watch.artifact)) {
|
|
505
|
+
active.set(key, watch);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/*
|
|
509
|
+
* A command-status observation may arrive while an older query is
|
|
510
|
+
* already in flight. Drain that request first; the authoritative
|
|
511
|
+
* revalidation below must start after the command fence.
|
|
512
|
+
*/
|
|
513
|
+
const prior = [...active.keys()].flatMap((key) => {
|
|
514
|
+
const request = this.#inFlight.get(key);
|
|
515
|
+
return request === undefined ? [] : [request];
|
|
516
|
+
});
|
|
517
|
+
return Promise.all(prior)
|
|
518
|
+
.then(() => {
|
|
519
|
+
if (this.#protocolGenerationSequence !== generation)
|
|
520
|
+
return [];
|
|
521
|
+
/*
|
|
522
|
+
* Stale every rendered matching root before fetching. This both
|
|
523
|
+
* starts active watches through their normal coordinator and
|
|
524
|
+
* leaves inactive SSR/read consumers fail-closed until a future
|
|
525
|
+
* watch can refresh them.
|
|
526
|
+
*/
|
|
527
|
+
const rootKeys = new Set();
|
|
528
|
+
for (const rendered of this.#renderedOperations.values()) {
|
|
529
|
+
if (!matches(rendered.artifact))
|
|
530
|
+
continue;
|
|
531
|
+
for (const root of rendered.artifact.roots) {
|
|
532
|
+
rootKeys.add(replicaIndexKey({
|
|
533
|
+
field: root.field,
|
|
534
|
+
arguments: resolveArguments(root.arguments, rendered.variables, root.coverage)
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const key of rootKeys) {
|
|
539
|
+
this.#engine.batch((writer) => writer.markIndexStale(key, 'command-authoritative-revalidation'));
|
|
540
|
+
}
|
|
541
|
+
// One operation key may have several component subscribers. The
|
|
542
|
+
// existing fetch coordinator owns deduplication and response fences.
|
|
543
|
+
return [...active.values()].map((watch) => this._fetch(watch, true).then(() => watch));
|
|
544
|
+
})
|
|
545
|
+
.then((requests) => Promise.all(requests))
|
|
546
|
+
.then((watches) => {
|
|
547
|
+
if (this.#protocolGenerationSequence !== generation)
|
|
548
|
+
return;
|
|
549
|
+
const failed = watches.some((watch) => {
|
|
550
|
+
const state = this.#queryState(watch.key);
|
|
551
|
+
/*
|
|
552
|
+
* Revalidation proves that the authoritative HTTP response
|
|
553
|
+
* refreshed the confirmed graph. A surviving accepted layer
|
|
554
|
+
* may deliberately keep the visible index stale when its row
|
|
555
|
+
* policy cannot be evaluated locally; the command runtime
|
|
556
|
+
* retires that layer only after this proof succeeds.
|
|
557
|
+
*/
|
|
558
|
+
const confirmed = this.#engine.readConfirmed((reader) => materializeReplicaOperation(reader, watch.artifact, watch.variables));
|
|
559
|
+
return (state.errors.length > 0 ||
|
|
560
|
+
!confirmed.complete ||
|
|
561
|
+
confirmed.stale);
|
|
562
|
+
});
|
|
563
|
+
if (failed) {
|
|
564
|
+
throw new Error('authoritative command revalidation did not produce a complete result');
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
invalidateAuthorization() {
|
|
569
|
+
this.#purgeProtocolGeneration();
|
|
570
|
+
}
|
|
571
|
+
dehydrate() {
|
|
572
|
+
return dehydrateReplica(this.#hydrationHost());
|
|
573
|
+
}
|
|
574
|
+
hydrate(state, authoritativeScope) {
|
|
575
|
+
return hydrateReplica(this.#hydrationHost(), state, authoritativeScope);
|
|
576
|
+
}
|
|
577
|
+
#bindArtifact(artifact) {
|
|
578
|
+
const next = validatedArtifactBinding(artifact);
|
|
579
|
+
this.#validateActiveArtifactTrustedPresets(next);
|
|
580
|
+
const current = this.#artifactBinding;
|
|
581
|
+
if (current === undefined) {
|
|
582
|
+
this.#artifactBinding = Object.freeze({
|
|
583
|
+
version: next.version,
|
|
584
|
+
schemaHash: next.schemaHash,
|
|
585
|
+
surfaceIdentity: next.surfaceIdentity,
|
|
586
|
+
trustedPresets: next.trustedPresets
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if ((current.surfaceIdentity === undefined ||
|
|
591
|
+
current.trustedPresets === undefined) &&
|
|
592
|
+
current.version === next.version &&
|
|
593
|
+
current.schemaHash === next.schemaHash &&
|
|
594
|
+
(current.surfaceIdentity === undefined ||
|
|
595
|
+
current.surfaceIdentity === next.surfaceIdentity) &&
|
|
596
|
+
(current.trustedPresets === undefined ||
|
|
597
|
+
trustedPresetDescriptorFingerprint(current.trustedPresets) ===
|
|
598
|
+
trustedPresetDescriptorFingerprint(next.trustedPresets))) {
|
|
599
|
+
this.#artifactBinding = Object.freeze({
|
|
600
|
+
...current,
|
|
601
|
+
surfaceIdentity: next.surfaceIdentity,
|
|
602
|
+
trustedPresets: next.trustedPresets
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (current.version !== next.version ||
|
|
607
|
+
current.schemaHash !== next.schemaHash ||
|
|
608
|
+
(current.surfaceIdentity !== undefined &&
|
|
609
|
+
current.surfaceIdentity !== next.surfaceIdentity) ||
|
|
610
|
+
(current.trustedPresets !== undefined &&
|
|
611
|
+
trustedPresetDescriptorFingerprint(current.trustedPresets) !== trustedPresetDescriptorFingerprint(next.trustedPresets))) {
|
|
612
|
+
throw new TypeError('replica artifact schema does not match the active replica binding');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
#validateActiveArtifactTrustedPresets(binding) {
|
|
616
|
+
if (this.#protocolGeneration === undefined) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
matchReplicaTrustedPresetInventory(binding.trustedPresets, this.#trustedPresets);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
this.#purgeProtocolGeneration();
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
#writeCanonicalResult(artifact, stableVariables, envelope, source, requestRevision, responseProjectionGeneration) {
|
|
628
|
+
const extensions = parseGraphqlResponseExtensions(envelope.extensions);
|
|
629
|
+
const parsedEnvelope = Object.freeze({
|
|
630
|
+
...envelope,
|
|
631
|
+
...(extensions === undefined ? {} : { extensions })
|
|
632
|
+
});
|
|
633
|
+
const key = operationKey(artifact, stableVariables);
|
|
634
|
+
const distributed = extensions?.distributed;
|
|
635
|
+
if (distributed === undefined) {
|
|
636
|
+
protocolInvalid('extensions.distributed');
|
|
637
|
+
}
|
|
638
|
+
const accepted = this.#writeProtocolResult(key, artifact, stableVariables, parsedEnvelope, source, distributed, requestRevision, responseProjectionGeneration);
|
|
639
|
+
if (accepted)
|
|
640
|
+
this.#notifyResultObservers(parsedEnvelope);
|
|
641
|
+
return distributed;
|
|
642
|
+
}
|
|
643
|
+
#writeProtocolResult(key, artifact, stableVariables, envelope, source, distributed, requestRevision, responseProjectionGeneration) {
|
|
644
|
+
this.#validateProtocolBinding(artifact, distributed, source);
|
|
645
|
+
const previousProtocolGeneration = this.#protocolGeneration;
|
|
646
|
+
const nextProtocolGeneration = this.#stageProtocolGeneration(distributed);
|
|
647
|
+
const nextTrustedPresets = this.#stageTrustedPresets(distributed.trustedPresets, nextProtocolGeneration, artifact);
|
|
648
|
+
const snapshot = distributed.snapshot;
|
|
649
|
+
const live = distributed.live;
|
|
650
|
+
if (live !== undefined && snapshot === undefined) {
|
|
651
|
+
protocolInvalid('extensions.distributed.snapshot');
|
|
652
|
+
}
|
|
653
|
+
if (source === 'live' &&
|
|
654
|
+
(envelope.data !== undefined || envelope.errors !== undefined) &&
|
|
655
|
+
(snapshot === undefined || live === undefined)) {
|
|
656
|
+
protocolInvalid('extensions.distributed.live');
|
|
657
|
+
}
|
|
658
|
+
const operation = distributed.operation;
|
|
659
|
+
if ((snapshot !== undefined || live !== undefined) && operation === undefined) {
|
|
660
|
+
protocolInvalid('extensions.distributed.operation');
|
|
661
|
+
}
|
|
662
|
+
const operationSource = protocolOperationSource(source);
|
|
663
|
+
const operationState = operation === undefined
|
|
664
|
+
? undefined
|
|
665
|
+
: this.#operationProtocol(key, operation, operationSource);
|
|
666
|
+
if (snapshot === undefined || operationState === undefined) {
|
|
667
|
+
this.#applyReceiptOnly(distributed.command);
|
|
668
|
+
this.#trustedPresets = nextTrustedPresets;
|
|
669
|
+
this.#protocolGeneration = nextProtocolGeneration;
|
|
670
|
+
this.#resumeLiveWatches();
|
|
671
|
+
this.#diagnosticScopeTransition(previousProtocolGeneration, nextProtocolGeneration);
|
|
672
|
+
this.#syncDiagnostics();
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
this.#validateLiveSnapshot(snapshot, live);
|
|
676
|
+
const unsupportedLive = source === 'live' && live?.supported === false;
|
|
677
|
+
const reset = live?.reset === true;
|
|
678
|
+
const group = this.#operationProtocols.get(key);
|
|
679
|
+
/*
|
|
680
|
+
* An unsupported subscription response is an authorized fallback
|
|
681
|
+
* snapshot, not a live source. In particular, a row-filtered snapshot
|
|
682
|
+
* has no comparable index vector, so retaining live ownership here
|
|
683
|
+
* would reject every later query handoff. Relinquish any prior live
|
|
684
|
+
* ownership without advancing the generation; the forced HTTP fallback
|
|
685
|
+
* starts against the generation that remains after this frame.
|
|
686
|
+
*/
|
|
687
|
+
if (unsupportedLive && group.active === 'live') {
|
|
688
|
+
group.active = undefined;
|
|
689
|
+
}
|
|
690
|
+
const previousActiveSource = group.active;
|
|
691
|
+
const handoff = previousActiveSource !== undefined &&
|
|
692
|
+
previousActiveSource !== operationSource;
|
|
693
|
+
const activeState = previousActiveSource === undefined
|
|
694
|
+
? undefined
|
|
695
|
+
: group[previousActiveSource];
|
|
696
|
+
const ownDisposition = reset
|
|
697
|
+
? 'fresh'
|
|
698
|
+
: compareSnapshotToOperationState(operationState, snapshot);
|
|
699
|
+
const activeDisposition = handoff && activeState !== undefined
|
|
700
|
+
? compareSnapshotToOperationState(activeState, snapshot)
|
|
701
|
+
: 'fresh';
|
|
702
|
+
const sharedDisposition = this.#sharedIndexDisposition(key, replicaResultIndexKeys(artifact, stableVariables, envelope, snapshot), snapshot, requestRevision, source);
|
|
703
|
+
const handoffBlocked = handoff &&
|
|
704
|
+
(!snapshot.indexesComparable ||
|
|
705
|
+
!isComparableHandoffDisposition(ownDisposition) ||
|
|
706
|
+
!isComparableHandoffDisposition(activeDisposition));
|
|
707
|
+
let disposition = handoffBlocked
|
|
708
|
+
? !isComparableHandoffDisposition(activeDisposition)
|
|
709
|
+
? activeDisposition
|
|
710
|
+
: !isComparableHandoffDisposition(ownDisposition)
|
|
711
|
+
? ownDisposition
|
|
712
|
+
: 'incomparable'
|
|
713
|
+
: ownDisposition;
|
|
714
|
+
if (!handoffBlocked &&
|
|
715
|
+
isComparableHandoffDisposition(disposition) &&
|
|
716
|
+
sharedDisposition.compared) {
|
|
717
|
+
disposition =
|
|
718
|
+
sharedDisposition.disposition === 'lower'
|
|
719
|
+
? 'lower'
|
|
720
|
+
: sharedDisposition.disposition ?? disposition;
|
|
721
|
+
}
|
|
722
|
+
const sourceSwitched = !unsupportedLive &&
|
|
723
|
+
!handoffBlocked &&
|
|
724
|
+
isComparableHandoffDisposition(disposition) &&
|
|
725
|
+
this.#activateOperationSource(key, operationSource, artifact, stableVariables);
|
|
726
|
+
const rejectedHandoff = handoff && !sourceSwitched;
|
|
727
|
+
if (snapshot.indexesComparable &&
|
|
728
|
+
(reset || ownDisposition === 'incomparable')) {
|
|
729
|
+
if (rejectedHandoff) {
|
|
730
|
+
this.#resetOperationState(operationState);
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
this.#discardOperationSnapshot(operationState, artifact, stableVariables);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (sourceSwitched && disposition !== 'incomparable') {
|
|
737
|
+
// Query and live operation hashes describe independent protocol
|
|
738
|
+
// streams. A handoff is authoritative, but must receive a fresh local
|
|
739
|
+
// index revision rather than reusing the inactive stream's revision.
|
|
740
|
+
disposition = 'fresh';
|
|
741
|
+
}
|
|
742
|
+
if (!snapshot.indexesComparable) {
|
|
743
|
+
if (rejectedHandoff) {
|
|
744
|
+
this.#resetOperationState(operationState);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
/*
|
|
748
|
+
* The server-authorized GraphQL payload is still an exact
|
|
749
|
+
* replacement result. Only its partition-wide causal vector is
|
|
750
|
+
* unavailable (most commonly because exposing that position
|
|
751
|
+
* would leak denied-row activity). Preserve local membership,
|
|
752
|
+
* while dropping every capability that requires comparison.
|
|
753
|
+
*/
|
|
754
|
+
this.#resetOperationCausalState(operationState);
|
|
755
|
+
}
|
|
756
|
+
disposition = rejectedHandoff
|
|
757
|
+
? 'incomparable'
|
|
758
|
+
: sharedDisposition.disposition === 'lower'
|
|
759
|
+
? 'lower'
|
|
760
|
+
: 'fresh';
|
|
761
|
+
}
|
|
762
|
+
else if (disposition === 'incomparable') {
|
|
763
|
+
if (rejectedHandoff) {
|
|
764
|
+
this.#resetOperationState(operationState);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
this.#discardOperationSnapshot(operationState, artifact, stableVariables);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const writeIndexes = !rejectedHandoff &&
|
|
771
|
+
disposition !== 'lower' &&
|
|
772
|
+
disposition !== 'incomparable';
|
|
773
|
+
/*
|
|
774
|
+
* Revision zero is the cache engine's lowest legal checkpoint. It lets
|
|
775
|
+
* an unsupported live response fill an empty cache immediately while
|
|
776
|
+
* guaranteeing that any HTTP request revision can replace it.
|
|
777
|
+
*/
|
|
778
|
+
const indexRevision = unsupportedLive
|
|
779
|
+
? '0'
|
|
780
|
+
: writeIndexes &&
|
|
781
|
+
(sourceSwitched ||
|
|
782
|
+
sharedDisposition.disposition === 'higher')
|
|
783
|
+
? this.#allocateIndexRevision()
|
|
784
|
+
: writeIndexes &&
|
|
785
|
+
sharedDisposition.disposition === 'equal' &&
|
|
786
|
+
sharedDisposition.indexRevision !== undefined
|
|
787
|
+
? sharedDisposition.indexRevision
|
|
788
|
+
: snapshot.indexesComparable &&
|
|
789
|
+
disposition === 'equal' &&
|
|
790
|
+
operationState.indexRevision !== undefined
|
|
791
|
+
? operationState.indexRevision
|
|
792
|
+
: (requestRevision ?? this.#allocateIndexRevision());
|
|
793
|
+
const recordEvidence = prepareRecordEvidence(snapshot, distributed.command?.records ?? Object.freeze([]));
|
|
794
|
+
const pendingRecordClocks = new Map();
|
|
795
|
+
const pendingRecordScopes = new Map();
|
|
796
|
+
const pendingAnonymousRecordClocks = new Map();
|
|
797
|
+
const consumedAnonymousRecordClocks = new Set();
|
|
798
|
+
const pendingPathRecords = new Map();
|
|
799
|
+
const pendingProjectedRecordFenceClears = new Map();
|
|
800
|
+
const consumedRecordPaths = new Set();
|
|
801
|
+
const observationsAdmissible = snapshot.recordsComplete &&
|
|
802
|
+
snapshot.indexesComparable &&
|
|
803
|
+
disposition !== 'incomparable' &&
|
|
804
|
+
(disposition !== 'lower' ||
|
|
805
|
+
this.#operationProtocols.get(key)?.active ===
|
|
806
|
+
operationSource);
|
|
807
|
+
const receiptPlan = this.#planOptimisticReceipts(distributed.command, [
|
|
808
|
+
...(distributed.command?.observations ?? []),
|
|
809
|
+
...(observationsAdmissible ? snapshot.observations : [])
|
|
810
|
+
], observationsAdmissible);
|
|
811
|
+
const normalizationProtocol = {
|
|
812
|
+
indexRevision,
|
|
813
|
+
writeIndexes,
|
|
814
|
+
/*
|
|
815
|
+
* This is cache-membership completeness, not causal-vector
|
|
816
|
+
* comparability. normalizeReplicaResult independently downgrades
|
|
817
|
+
* GraphQL path errors and missing selected fields.
|
|
818
|
+
*/
|
|
819
|
+
indexesComplete: true,
|
|
820
|
+
allowSnapshotOnlyRecords: !snapshot.recordsComplete,
|
|
821
|
+
record: (path, model, recordKey, fields) => {
|
|
822
|
+
const encodedPath = responsePathKey(path);
|
|
823
|
+
const evidence = recordEvidence.byPath.get(encodedPath);
|
|
824
|
+
if (evidence === undefined) {
|
|
825
|
+
if (snapshot.recordsComplete) {
|
|
826
|
+
protocolInvalid('extensions.distributed.snapshot.records');
|
|
827
|
+
}
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
if (evidence.model !== model) {
|
|
831
|
+
protocolInvalid('extensions.distributed.snapshot.records.model');
|
|
832
|
+
}
|
|
833
|
+
if (evidence.tombstone) {
|
|
834
|
+
protocolInvalid('extensions.distributed.snapshot.records.tombstone');
|
|
835
|
+
}
|
|
836
|
+
consumedRecordPaths.add(encodedPath);
|
|
837
|
+
const projectedFence = this.#projectedRecordFences.get(recordKey);
|
|
838
|
+
const projectedDisposition = projectedFence === undefined
|
|
839
|
+
? undefined
|
|
840
|
+
: compareProjectedRecordFields(projectedFence.fields, fields);
|
|
841
|
+
const projectedClockComparison = projectedFence === undefined
|
|
842
|
+
? undefined
|
|
843
|
+
: compareEvidenceToProjectedFence(evidence, projectedFence);
|
|
844
|
+
const newerPostProjectionRow = projectedFence !== undefined &&
|
|
845
|
+
projectedDisposition === 'conflict' &&
|
|
846
|
+
projectedClockComparison === 1 &&
|
|
847
|
+
responseProjectionGeneration !== undefined &&
|
|
848
|
+
responseProjectionGeneration >=
|
|
849
|
+
projectedFence.projectionGeneration;
|
|
850
|
+
/*
|
|
851
|
+
* Snapshot bodies can race: SQL may be read before a command
|
|
852
|
+
* while response evidence is stamped after it. Conflicting
|
|
853
|
+
* pre-command responses therefore cannot override a projected
|
|
854
|
+
* fence, even when their evidence revision is numerically later.
|
|
855
|
+
* A causally newer row from an HTTP request or live frame that
|
|
856
|
+
* began after the command is an atomic supersession and may
|
|
857
|
+
* release the fence without first echoing the projected body.
|
|
858
|
+
*/
|
|
859
|
+
if (projectedFence !== undefined) {
|
|
860
|
+
const exactEcho = projectedDisposition === 'complete' &&
|
|
861
|
+
(projectedClockComparison === 0 ||
|
|
862
|
+
projectedClockComparison === 1);
|
|
863
|
+
const newerPartial = projectedDisposition === 'partial' &&
|
|
864
|
+
projectedClockComparison === 1;
|
|
865
|
+
if (exactEcho || newerPartial || newerPostProjectionRow) {
|
|
866
|
+
pendingProjectedRecordFenceClears.set(recordKey, projectedFence);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const resolution = this.#resolveRecordEvidence(recordKey, evidence, pendingRecordClocks, pendingRecordScopes, pendingAnonymousRecordClocks, consumedAnonymousRecordClocks, projectedDisposition === 'conflict' &&
|
|
870
|
+
!newerPostProjectionRow);
|
|
871
|
+
pendingPathRecords.set(encodedPath, recordKey);
|
|
872
|
+
return Object.freeze({ evidence, apply: resolution });
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
const state = this.#queryState(key);
|
|
876
|
+
const previousErrors = state.errors;
|
|
877
|
+
state.errors = stableErrors(previousErrors, envelope.errors ?? []);
|
|
878
|
+
let summary;
|
|
879
|
+
try {
|
|
880
|
+
const update = (writer) => {
|
|
881
|
+
this.#applyTombstoneEvidence(writer, recordEvidence.tombstones, operationState, pendingRecordClocks, pendingRecordScopes, pendingAnonymousRecordClocks, consumedAnonymousRecordClocks, consumedRecordPaths, pendingProjectedRecordFenceClears);
|
|
882
|
+
const normalized = normalizeReplicaResult(writer, artifact, stableVariables, envelope, normalizationProtocol);
|
|
883
|
+
for (const path of recordEvidence.livePaths) {
|
|
884
|
+
if (!consumedRecordPaths.has(path)) {
|
|
885
|
+
protocolInvalid('extensions.distributed.snapshot.records.path');
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
this.#applyPathlessEvidence(writer, recordEvidence.pathless, recordEvidence.byPath, consumedRecordPaths, pendingRecordClocks, pendingRecordScopes, pendingAnonymousRecordClocks, consumedAnonymousRecordClocks, pendingProjectedRecordFenceClears);
|
|
889
|
+
return normalized;
|
|
890
|
+
};
|
|
891
|
+
summary =
|
|
892
|
+
receiptPlan.satisfied.length === 0
|
|
893
|
+
? this.#engine.batch(update)
|
|
894
|
+
: this.#engine.confirmOptimisticLayers(receiptPlan.satisfied, update);
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
state.errors = previousErrors;
|
|
898
|
+
if (error instanceof CacheRevisionConflictError ||
|
|
899
|
+
error instanceof DistributedProtocolError) {
|
|
900
|
+
this.#discardOperationSnapshot(operationState, artifact, stableVariables);
|
|
901
|
+
}
|
|
902
|
+
this.#emitState(key, false);
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
for (const [recordKey, clock] of pendingRecordClocks) {
|
|
906
|
+
this.#recordClocks.set(recordKey, clock);
|
|
907
|
+
this.#recordKeysByScope.set(clock.scopeToken, recordKey);
|
|
908
|
+
}
|
|
909
|
+
for (const [scopeToken, recordKey] of pendingRecordScopes) {
|
|
910
|
+
this.#recordKeysByScope.set(scopeToken, recordKey);
|
|
911
|
+
}
|
|
912
|
+
for (const [scopeToken, clock] of pendingAnonymousRecordClocks) {
|
|
913
|
+
if (!consumedAnonymousRecordClocks.has(scopeToken)) {
|
|
914
|
+
this.#anonymousRecordClocks.set(scopeToken, clock);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
for (const scopeToken of consumedAnonymousRecordClocks) {
|
|
918
|
+
this.#anonymousRecordClocks.delete(scopeToken);
|
|
919
|
+
}
|
|
920
|
+
for (const [path, recordKey] of pendingPathRecords) {
|
|
921
|
+
operationState.pathRecords.set(path, recordKey);
|
|
922
|
+
}
|
|
923
|
+
for (const [recordKey, projectedFields] of pendingProjectedRecordFenceClears) {
|
|
924
|
+
if (this.#projectedRecordFences.get(recordKey) === projectedFields) {
|
|
925
|
+
this.#projectedRecordFences.delete(recordKey);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
for (const [id, receipt] of receiptPlan.updates) {
|
|
929
|
+
if (receiptPlan.satisfied.includes(id)) {
|
|
930
|
+
this.#retireDiagnosticLayer(id, 'retired', 'projected', receipt);
|
|
931
|
+
this.#optimisticReceipts.delete(id);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
this.#optimisticReceipts.set(id, receipt);
|
|
935
|
+
this.#engine.markOptimisticLayerAccepted(id);
|
|
936
|
+
const layer = this.#diagnosticLayers?.get(id);
|
|
937
|
+
if (layer !== undefined) {
|
|
938
|
+
this.#diagnosticLayers.set(id, Object.freeze({ ...layer, state: 'accepted' }));
|
|
939
|
+
}
|
|
940
|
+
if (this.#diagnostics !== undefined) {
|
|
941
|
+
const counts = diagnosticReceiptCounts(receipt);
|
|
942
|
+
this.#diagnosticEvent(Object.freeze({
|
|
943
|
+
kind: 'receipt',
|
|
944
|
+
command: id,
|
|
945
|
+
state: counts.obligations === 0
|
|
946
|
+
? 'accepted'
|
|
947
|
+
: 'accepted_pending_projection',
|
|
948
|
+
obligations: counts.obligations,
|
|
949
|
+
observed: counts.observed
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (writeIndexes) {
|
|
955
|
+
operationState.indexRevision = indexRevision;
|
|
956
|
+
for (const indexKey of summary.indexKeys) {
|
|
957
|
+
operationState.indexKeys.add(indexKey);
|
|
958
|
+
}
|
|
959
|
+
if (snapshot.indexesComparable) {
|
|
960
|
+
operationState.snapshotScope = snapshot.scopeToken;
|
|
961
|
+
operationState.indexClocks = indexClockMap(snapshot.indexes);
|
|
962
|
+
operationState.cursors = latestCursors(snapshot, live);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
this.#resetOperationCausalState(operationState);
|
|
966
|
+
operationState.indexRevision = indexRevision;
|
|
967
|
+
for (const indexKey of summary.indexKeys) {
|
|
968
|
+
operationState.indexKeys.add(indexKey);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
else if (live?.reset === true || !snapshot.indexesComparable) {
|
|
973
|
+
operationState.cursors = Object.freeze([]);
|
|
974
|
+
}
|
|
975
|
+
if (source === 'live' && !unsupportedLive) {
|
|
976
|
+
this.#advanceOperationGeneration(key);
|
|
977
|
+
}
|
|
978
|
+
if (source !== 'live' && sourceSwitched) {
|
|
979
|
+
this.#restartLive(key);
|
|
980
|
+
}
|
|
981
|
+
this.#trustedPresets = nextTrustedPresets;
|
|
982
|
+
this.#protocolGeneration = nextProtocolGeneration;
|
|
983
|
+
this.#resumeLiveWatches();
|
|
984
|
+
this.#emitState(key, false);
|
|
985
|
+
if (this.#diagnostics !== undefined) {
|
|
986
|
+
const cache = this.#engine.extract();
|
|
987
|
+
this.#diagnosticEvent(Object.freeze({
|
|
988
|
+
kind: 'normalization',
|
|
989
|
+
operation: artifact.id,
|
|
990
|
+
source,
|
|
991
|
+
records: cache.records.length,
|
|
992
|
+
indexes: summary.indexKeys.length,
|
|
993
|
+
partial: summary.partial ||
|
|
994
|
+
disposition === 'lower' ||
|
|
995
|
+
disposition === 'incomparable' ||
|
|
996
|
+
rejectedHandoff
|
|
997
|
+
}));
|
|
998
|
+
for (const indexKey of summary.indexKeys) {
|
|
999
|
+
const index = cache.indexes.find((candidate) => candidate.key === indexKey && !candidate.deleted);
|
|
1000
|
+
const staleReason = index?.metadata?.staleReason;
|
|
1001
|
+
this.#diagnosticEvent(Object.freeze({
|
|
1002
|
+
kind: 'index-decision',
|
|
1003
|
+
index: indexKey,
|
|
1004
|
+
decision: !writeIndexes ||
|
|
1005
|
+
!snapshot.indexesComparable ||
|
|
1006
|
+
disposition === 'incomparable'
|
|
1007
|
+
? 'revalidate'
|
|
1008
|
+
: staleReason === undefined
|
|
1009
|
+
? 'maintained'
|
|
1010
|
+
: 'stale',
|
|
1011
|
+
...(staleReason === undefined
|
|
1012
|
+
? {}
|
|
1013
|
+
: { reason: staleReason })
|
|
1014
|
+
}));
|
|
1015
|
+
}
|
|
1016
|
+
this.#diagnosticScopeTransition(previousProtocolGeneration, nextProtocolGeneration);
|
|
1017
|
+
this.#syncDiagnostics();
|
|
1018
|
+
}
|
|
1019
|
+
if (disposition === 'incomparable' || rejectedHandoff)
|
|
1020
|
+
return false;
|
|
1021
|
+
if (disposition !== 'lower')
|
|
1022
|
+
return true;
|
|
1023
|
+
// A lower index snapshot from the already-active source cannot replace
|
|
1024
|
+
// membership, but it can still carry independently fenced record clocks
|
|
1025
|
+
// and exact causal observations. Notify only when that transaction
|
|
1026
|
+
// actually committed data or receipt progress.
|
|
1027
|
+
return summary.wrote || receiptPlan.satisfied.length > 0;
|
|
1028
|
+
}
|
|
1029
|
+
#notifyResultObservers(envelope) {
|
|
1030
|
+
if (this.#resultObservers.size === 0)
|
|
1031
|
+
return;
|
|
1032
|
+
const errors = [];
|
|
1033
|
+
for (const observer of [...this.#resultObservers]) {
|
|
1034
|
+
try {
|
|
1035
|
+
observer(envelope);
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
errors.push(error);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
this._reportObserverErrors(errors);
|
|
1042
|
+
}
|
|
1043
|
+
createOptimisticLayer(id, update, semanticChanges = Object.freeze([])) {
|
|
1044
|
+
createOptimisticLayerOn(this.#optimisticHost(), id, update, semanticChanges);
|
|
1045
|
+
}
|
|
1046
|
+
markOptimisticLayerAccepted(id, receipt) {
|
|
1047
|
+
return markOptimisticLayerAcceptedOn(this.#optimisticHost(), id, receipt);
|
|
1048
|
+
}
|
|
1049
|
+
confirmOptimisticLayer(id, update) {
|
|
1050
|
+
return confirmOptimisticLayerOn(this.#optimisticHost(), id, update);
|
|
1051
|
+
}
|
|
1052
|
+
rejectOptimisticLayer(id) {
|
|
1053
|
+
return rejectOptimisticLayerOn(this.#optimisticHost(), id);
|
|
1054
|
+
}
|
|
1055
|
+
tombstoneRecord(model, identity, revision) {
|
|
1056
|
+
const wrote = this.#engine.batch((writer) => writer.tombstoneRecord(replicaRecordKey(model, identity), revision));
|
|
1057
|
+
if (wrote)
|
|
1058
|
+
this.#syncDiagnostics();
|
|
1059
|
+
return wrote;
|
|
1060
|
+
}
|
|
1061
|
+
markIndexStale(target, reason) {
|
|
1062
|
+
const key = indexKeyFromTarget(target);
|
|
1063
|
+
const marked = this.#engine.batch((writer) => writer.markIndexStale(key, reason));
|
|
1064
|
+
if (marked) {
|
|
1065
|
+
if (this.#diagnostics !== undefined) {
|
|
1066
|
+
this.#diagnosticEvent(Object.freeze({
|
|
1067
|
+
kind: 'index-decision',
|
|
1068
|
+
index: key,
|
|
1069
|
+
decision: 'stale',
|
|
1070
|
+
reason
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
1073
|
+
this.#syncDiagnostics();
|
|
1074
|
+
}
|
|
1075
|
+
return marked;
|
|
1076
|
+
}
|
|
1077
|
+
retainRecord(model, identity) {
|
|
1078
|
+
this.#engine.retain(replicaRecordKey(model, identity));
|
|
1079
|
+
}
|
|
1080
|
+
releaseRecord(model, identity) {
|
|
1081
|
+
this.#engine.release(replicaRecordKey(model, identity));
|
|
1082
|
+
}
|
|
1083
|
+
gc() {
|
|
1084
|
+
const collected = this.#engine.gc();
|
|
1085
|
+
if (this.#diagnostics !== undefined) {
|
|
1086
|
+
this.#diagnosticEvent(Object.freeze({
|
|
1087
|
+
kind: 'gc',
|
|
1088
|
+
records: collected.length
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
this.#syncDiagnostics();
|
|
1092
|
+
return collected;
|
|
1093
|
+
}
|
|
1094
|
+
inspectRecord(model, identity) {
|
|
1095
|
+
const key = replicaRecordKey(model, identity);
|
|
1096
|
+
return this.#engine.read((reader) => {
|
|
1097
|
+
const record = reader.record(key);
|
|
1098
|
+
if (!record)
|
|
1099
|
+
return undefined;
|
|
1100
|
+
return Object.freeze({
|
|
1101
|
+
key,
|
|
1102
|
+
revision: record.revision,
|
|
1103
|
+
incarnation: record.incarnation,
|
|
1104
|
+
presentFields: Object.freeze(Object.keys(record.fields).sort())
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
inspectIndex(target) {
|
|
1109
|
+
const key = indexKeyFromTarget(target);
|
|
1110
|
+
return this.#engine.read((reader) => {
|
|
1111
|
+
const index = reader.index(key);
|
|
1112
|
+
if (!index?.metadata)
|
|
1113
|
+
return undefined;
|
|
1114
|
+
return Object.freeze({
|
|
1115
|
+
key,
|
|
1116
|
+
revision: index.revision,
|
|
1117
|
+
...(index.staleRevision === undefined
|
|
1118
|
+
? {}
|
|
1119
|
+
: { staleRevision: index.staleRevision }),
|
|
1120
|
+
records: Object.freeze([...index.records]),
|
|
1121
|
+
complete: index.complete,
|
|
1122
|
+
field: index.metadata.field,
|
|
1123
|
+
...(index.metadata.parent === undefined
|
|
1124
|
+
? {}
|
|
1125
|
+
: { parent: index.metadata.parent }),
|
|
1126
|
+
arguments: index.metadata.arguments,
|
|
1127
|
+
coverage: index.metadata.coverage,
|
|
1128
|
+
dependencies: index.metadata.dependencies,
|
|
1129
|
+
...(index.metadata.staleReason === undefined
|
|
1130
|
+
? {}
|
|
1131
|
+
: { staleReason: index.metadata.staleReason }),
|
|
1132
|
+
nullValue: index.metadata.nullValue === true
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
#syncDiagnostics() {
|
|
1137
|
+
syncDiagnosticsOn(this.#diagnosticsHost());
|
|
1138
|
+
}
|
|
1139
|
+
#diagnosticEvent(event) {
|
|
1140
|
+
diagnosticEventOn(this.#diagnosticsHost(), event);
|
|
1141
|
+
}
|
|
1142
|
+
#diagnosticOperation(artifact) {
|
|
1143
|
+
diagnosticOperationOn(this.#diagnosticsHost(), artifact);
|
|
1144
|
+
}
|
|
1145
|
+
#diagnosticScopeTransition(previous, next) {
|
|
1146
|
+
diagnosticScopeTransitionOn(this.#diagnosticsHost(), previous, next);
|
|
1147
|
+
}
|
|
1148
|
+
#retireDiagnosticLayer(id, action, receiptState, receipt) {
|
|
1149
|
+
retireDiagnosticLayerOn(this.#diagnosticsHost(), id, action, receiptState, receipt);
|
|
1150
|
+
}
|
|
1151
|
+
#validateProtocolBinding(artifact, envelope, source) {
|
|
1152
|
+
validateProtocolBindingOn(this.#protocolHost(), artifact, envelope, source);
|
|
1153
|
+
}
|
|
1154
|
+
#stageProtocolGeneration(envelope) {
|
|
1155
|
+
return stageProtocolGenerationOn(this.#protocolHost(), envelope);
|
|
1156
|
+
}
|
|
1157
|
+
#stageTrustedPresets(incoming, nextGeneration, artifact) {
|
|
1158
|
+
return stageTrustedPresetsOn(this.#protocolHost(), incoming, nextGeneration, artifact);
|
|
1159
|
+
}
|
|
1160
|
+
#purgeProtocolGeneration() {
|
|
1161
|
+
purgeProtocolGenerationOn(this.#protocolHost());
|
|
1162
|
+
}
|
|
1163
|
+
#closeAuthorizationGeneration() {
|
|
1164
|
+
closeAuthorizationGenerationOn(this.#protocolHost());
|
|
1165
|
+
}
|
|
1166
|
+
#closeActiveTransports() {
|
|
1167
|
+
closeActiveTransportsOn(this.#fetchLiveHost());
|
|
1168
|
+
}
|
|
1169
|
+
#rememberRenderedOperation(key, artifact, variables, source) {
|
|
1170
|
+
this.#diagnosticOperation(artifact);
|
|
1171
|
+
if (!this.#indexPlanRegistrations.has(key)) {
|
|
1172
|
+
const registration = this.#indexMaintenance.registerOperation(artifact, variables);
|
|
1173
|
+
this.#indexPlanRegistrations.set(key, registration);
|
|
1174
|
+
try {
|
|
1175
|
+
this.#refreshIndexMaintenance();
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
this.#indexPlanRegistrations.delete(key);
|
|
1179
|
+
registration.dispose();
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
this.#renderedOperations.set(key, Object.freeze({
|
|
1184
|
+
artifact: artifact,
|
|
1185
|
+
variables
|
|
1186
|
+
}));
|
|
1187
|
+
if (source === 'read') {
|
|
1188
|
+
this.#readOperationKeys.add(key);
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
this.#watchRenderCounts.set(key, (this.#watchRenderCounts.get(key) ?? 0) + 1);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
#forgetRenderedWatch(key) {
|
|
1195
|
+
const count = this.#watchRenderCounts.get(key);
|
|
1196
|
+
if (count === undefined)
|
|
1197
|
+
return;
|
|
1198
|
+
if (count > 1) {
|
|
1199
|
+
this.#watchRenderCounts.set(key, count - 1);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
this.#watchRenderCounts.delete(key);
|
|
1203
|
+
if (!this.#readOperationKeys.has(key)) {
|
|
1204
|
+
this.#renderedOperations.delete(key);
|
|
1205
|
+
this.#disposeIndexPlan(key);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
#finishDehydration() {
|
|
1209
|
+
finishDehydrationOn(this.#hydrationHost());
|
|
1210
|
+
}
|
|
1211
|
+
#disposeIndexPlan(key) {
|
|
1212
|
+
const registration = this.#indexPlanRegistrations.get(key);
|
|
1213
|
+
if (registration === undefined)
|
|
1214
|
+
return;
|
|
1215
|
+
this.#indexPlanRegistrations.delete(key);
|
|
1216
|
+
registration.dispose();
|
|
1217
|
+
this.#refreshIndexMaintenance();
|
|
1218
|
+
}
|
|
1219
|
+
#refreshIndexMaintenance() {
|
|
1220
|
+
this.#engine.setDerivedIndexReconciler(this.#derivedIndexReconciler);
|
|
1221
|
+
}
|
|
1222
|
+
#deriveMaintainedIndexes(confirmed, layers) {
|
|
1223
|
+
const snapshot = indexMaintenanceSnapshot(confirmed);
|
|
1224
|
+
const indexes = new Map(snapshot.indexes.map((index) => [index.key, index]));
|
|
1225
|
+
const semanticLayers = layers.map(indexSemanticLayer);
|
|
1226
|
+
const mutations = [];
|
|
1227
|
+
for (const decision of this.#indexMaintenance.evaluate(snapshot, semanticLayers, this.#trustedPresets)) {
|
|
1228
|
+
if (decision.kind === 'unchanged')
|
|
1229
|
+
continue;
|
|
1230
|
+
if (decision.kind === 'stale') {
|
|
1231
|
+
mutations.push({
|
|
1232
|
+
kind: 'stale',
|
|
1233
|
+
key: decision.indexKey,
|
|
1234
|
+
reason: formatReplicaIndexStaleReason(decision.reason)
|
|
1235
|
+
});
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const index = indexes.get(decision.indexKey);
|
|
1239
|
+
if (index === undefined) {
|
|
1240
|
+
throw new Error(`index maintenance returned an unknown index: ${decision.indexKey}`);
|
|
1241
|
+
}
|
|
1242
|
+
mutations.push({
|
|
1243
|
+
kind: 'write',
|
|
1244
|
+
write: {
|
|
1245
|
+
key: decision.indexKey,
|
|
1246
|
+
records: decision.records,
|
|
1247
|
+
complete: true,
|
|
1248
|
+
metadata: index.metadata
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return Object.freeze(mutations);
|
|
1253
|
+
}
|
|
1254
|
+
#operationProtocol(key, operation, source) {
|
|
1255
|
+
let group = this.#operationProtocols.get(key);
|
|
1256
|
+
if (group === undefined) {
|
|
1257
|
+
group = {};
|
|
1258
|
+
this.#operationProtocols.set(key, group);
|
|
1259
|
+
}
|
|
1260
|
+
const current = group[source];
|
|
1261
|
+
if (current !== undefined) {
|
|
1262
|
+
if (current.operation !== operation) {
|
|
1263
|
+
protocolInvalid('extensions.distributed.operation');
|
|
1264
|
+
}
|
|
1265
|
+
return current;
|
|
1266
|
+
}
|
|
1267
|
+
const created = {
|
|
1268
|
+
operation,
|
|
1269
|
+
indexClocks: new Map(),
|
|
1270
|
+
indexKeys: new Set(),
|
|
1271
|
+
pathRecords: new Map(),
|
|
1272
|
+
cursors: Object.freeze([])
|
|
1273
|
+
};
|
|
1274
|
+
group[source] = created;
|
|
1275
|
+
return created;
|
|
1276
|
+
}
|
|
1277
|
+
#activateOperationSource(key, source, artifact, variables) {
|
|
1278
|
+
const group = this.#operationProtocols.get(key);
|
|
1279
|
+
if (group === undefined)
|
|
1280
|
+
return false;
|
|
1281
|
+
const previous = group.active;
|
|
1282
|
+
if (previous === source)
|
|
1283
|
+
return false;
|
|
1284
|
+
if (previous !== undefined) {
|
|
1285
|
+
const discardedStates = new Set([group.query, group.live].filter((state) => state !== undefined));
|
|
1286
|
+
const revisionsByKey = new Map();
|
|
1287
|
+
for (const state of [group.query, group.live]) {
|
|
1288
|
+
if (state?.indexRevision === undefined)
|
|
1289
|
+
continue;
|
|
1290
|
+
for (const indexKey of state.indexKeys) {
|
|
1291
|
+
let revisions = revisionsByKey.get(indexKey);
|
|
1292
|
+
if (revisions === undefined) {
|
|
1293
|
+
revisions = new Set();
|
|
1294
|
+
revisionsByKey.set(indexKey, revisions);
|
|
1295
|
+
}
|
|
1296
|
+
revisions.add(state.indexRevision);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
for (const root of artifact.roots) {
|
|
1300
|
+
const indexKey = replicaIndexKey({
|
|
1301
|
+
field: root.field,
|
|
1302
|
+
arguments: resolveArguments(root.arguments, variables, root.coverage)
|
|
1303
|
+
});
|
|
1304
|
+
let revisions = revisionsByKey.get(indexKey);
|
|
1305
|
+
if (revisions === undefined) {
|
|
1306
|
+
revisions = new Set();
|
|
1307
|
+
revisionsByKey.set(indexKey, revisions);
|
|
1308
|
+
}
|
|
1309
|
+
for (const state of [group.query, group.live]) {
|
|
1310
|
+
if (state?.indexRevision !== undefined) {
|
|
1311
|
+
revisions.add(state.indexRevision);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const confirmedFences = this.#confirmedIndexFences(revisionsByKey.keys());
|
|
1316
|
+
const ownedKeys = [...revisionsByKey].flatMap(([indexKey, revisions]) => {
|
|
1317
|
+
const revision = confirmedFences.get(indexKey);
|
|
1318
|
+
return revision !== undefined &&
|
|
1319
|
+
revisions.has(revision) &&
|
|
1320
|
+
!this.#indexClaimedByAnotherOperationState(indexKey, revision, discardedStates)
|
|
1321
|
+
? [indexKey]
|
|
1322
|
+
: [];
|
|
1323
|
+
});
|
|
1324
|
+
this.#engine.discardIndexes(ownedKeys);
|
|
1325
|
+
}
|
|
1326
|
+
group.active = source;
|
|
1327
|
+
this.#advanceOperationGeneration(key);
|
|
1328
|
+
return previous !== undefined;
|
|
1329
|
+
}
|
|
1330
|
+
#operationGeneration(key) {
|
|
1331
|
+
return this.#operationGenerations.get(key) ?? 0;
|
|
1332
|
+
}
|
|
1333
|
+
#indexClaimedByAnotherOperationState(indexKey, revision, excluded) {
|
|
1334
|
+
for (const group of this.#operationProtocols.values()) {
|
|
1335
|
+
for (const state of [group.query, group.live]) {
|
|
1336
|
+
if (state !== undefined &&
|
|
1337
|
+
!excluded.has(state) &&
|
|
1338
|
+
state.indexRevision === revision &&
|
|
1339
|
+
state.indexKeys.has(indexKey)) {
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
#confirmedIndexFences(indexKeys) {
|
|
1347
|
+
return this.#engine.confirmedIndexFences([...indexKeys]);
|
|
1348
|
+
}
|
|
1349
|
+
#sharedIndexDisposition(currentKey, incomingIndexKeys, snapshot, requestRevision, source) {
|
|
1350
|
+
if (incomingIndexKeys.size === 0)
|
|
1351
|
+
return { compared: false };
|
|
1352
|
+
const confirmedRevisions = this.#confirmedIndexFences(incomingIndexKeys);
|
|
1353
|
+
let compared = false;
|
|
1354
|
+
let lower = false;
|
|
1355
|
+
let higher = false;
|
|
1356
|
+
let incomparable = false;
|
|
1357
|
+
let equalRevision;
|
|
1358
|
+
let latestOwnerRevision;
|
|
1359
|
+
for (const [key, group] of this.#operationProtocols) {
|
|
1360
|
+
if (key === currentKey)
|
|
1361
|
+
continue;
|
|
1362
|
+
for (const state of [group.query, group.live]) {
|
|
1363
|
+
if (state?.indexRevision === undefined)
|
|
1364
|
+
continue;
|
|
1365
|
+
let ownsIncomingIndex = false;
|
|
1366
|
+
for (const indexKey of state.indexKeys) {
|
|
1367
|
+
if (incomingIndexKeys.has(indexKey) &&
|
|
1368
|
+
confirmedRevisions.get(indexKey) === state.indexRevision) {
|
|
1369
|
+
ownsIncomingIndex = true;
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (!ownsIncomingIndex)
|
|
1374
|
+
continue;
|
|
1375
|
+
latestOwnerRevision =
|
|
1376
|
+
latestOwnerRevision === undefined ||
|
|
1377
|
+
compareCanonicalDecimalStrings(state.indexRevision, latestOwnerRevision) > 0
|
|
1378
|
+
? state.indexRevision
|
|
1379
|
+
: latestOwnerRevision;
|
|
1380
|
+
/*
|
|
1381
|
+
* Snapshot scope is bound to one operation plan instance and is
|
|
1382
|
+
* not a cross-artifact identity. Shared semantic indexes are
|
|
1383
|
+
* comparable through their projection/scope/position vector.
|
|
1384
|
+
*/
|
|
1385
|
+
const disposition = !snapshot.indexesComparable
|
|
1386
|
+
? 'incomparable'
|
|
1387
|
+
: state.indexClocks.size === 0 ||
|
|
1388
|
+
snapshot.indexes.length === 0
|
|
1389
|
+
? state.indexClocks.size === snapshot.indexes.length
|
|
1390
|
+
? 'equal'
|
|
1391
|
+
: 'incomparable'
|
|
1392
|
+
: compareIndexVector(state.indexClocks, snapshot.indexes);
|
|
1393
|
+
if (disposition === 'fresh' ||
|
|
1394
|
+
disposition === 'incomparable') {
|
|
1395
|
+
incomparable = true;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
compared = true;
|
|
1399
|
+
if (disposition === 'lower')
|
|
1400
|
+
lower = true;
|
|
1401
|
+
else if (disposition === 'higher')
|
|
1402
|
+
higher = true;
|
|
1403
|
+
else {
|
|
1404
|
+
equalRevision =
|
|
1405
|
+
equalRevision === undefined ||
|
|
1406
|
+
compareCanonicalDecimalStrings(state.indexRevision, equalRevision) > 0
|
|
1407
|
+
? state.indexRevision
|
|
1408
|
+
: equalRevision;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/*
|
|
1413
|
+
* One response is a coherent replacement graph. If its vector straddles
|
|
1414
|
+
* the owners of two shared indexes, accepting only part would fabricate
|
|
1415
|
+
* a snapshot the server never produced, so preserve the current graph.
|
|
1416
|
+
*/
|
|
1417
|
+
if (lower)
|
|
1418
|
+
return { compared: true, disposition: 'lower' };
|
|
1419
|
+
if (incomparable) {
|
|
1420
|
+
/*
|
|
1421
|
+
* A comparable sibling must not promote an incomparable membership
|
|
1422
|
+
* to response-arrival order. Fall back to request-start order for
|
|
1423
|
+
* the whole graph, and reject it atomically when that request began
|
|
1424
|
+
* before any owner it would replace. Independent live streams have
|
|
1425
|
+
* no shared request-start fence, so they cannot safely replace it;
|
|
1426
|
+
* explicit synchronous ingress retains its caller-defined order.
|
|
1427
|
+
*/
|
|
1428
|
+
if (requestRevision === undefined && source === 'live') {
|
|
1429
|
+
return { compared: true, disposition: 'lower' };
|
|
1430
|
+
}
|
|
1431
|
+
if (requestRevision === undefined)
|
|
1432
|
+
return { compared: false };
|
|
1433
|
+
return latestOwnerRevision !== undefined &&
|
|
1434
|
+
compareCanonicalDecimalStrings(requestRevision, latestOwnerRevision) <
|
|
1435
|
+
0
|
|
1436
|
+
? { compared: true, disposition: 'lower' }
|
|
1437
|
+
: { compared: false };
|
|
1438
|
+
}
|
|
1439
|
+
if (!compared)
|
|
1440
|
+
return { compared: false };
|
|
1441
|
+
if (higher)
|
|
1442
|
+
return { compared: true, disposition: 'higher' };
|
|
1443
|
+
return {
|
|
1444
|
+
compared: true,
|
|
1445
|
+
disposition: 'equal',
|
|
1446
|
+
...(equalRevision === undefined
|
|
1447
|
+
? {}
|
|
1448
|
+
: { indexRevision: equalRevision })
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
#advanceOperationGeneration(key) {
|
|
1452
|
+
this.#operationGenerations.set(key, this.#operationGeneration(key) + 1);
|
|
1453
|
+
}
|
|
1454
|
+
#resumeCursors(key) {
|
|
1455
|
+
const group = this.#operationProtocols.get(key);
|
|
1456
|
+
if (group?.active === 'query' && group.query?.cursors.length) {
|
|
1457
|
+
return group.query.cursors;
|
|
1458
|
+
}
|
|
1459
|
+
if (group?.active === 'live' && group.live?.cursors.length) {
|
|
1460
|
+
return group.live.cursors;
|
|
1461
|
+
}
|
|
1462
|
+
return group?.live?.cursors.length
|
|
1463
|
+
? group.live.cursors
|
|
1464
|
+
: (group?.query?.cursors ?? Object.freeze([]));
|
|
1465
|
+
}
|
|
1466
|
+
#discardOperationSnapshot(state, artifact, variables) {
|
|
1467
|
+
const keys = new Set(state.indexKeys);
|
|
1468
|
+
for (const root of artifact.roots) {
|
|
1469
|
+
keys.add(replicaIndexKey({
|
|
1470
|
+
field: root.field,
|
|
1471
|
+
arguments: resolveArguments(root.arguments, variables, root.coverage)
|
|
1472
|
+
}));
|
|
1473
|
+
}
|
|
1474
|
+
const indexRevision = state.indexRevision;
|
|
1475
|
+
const discardedStates = new Set([state]);
|
|
1476
|
+
const confirmedFence = this.#confirmedIndexFences(keys);
|
|
1477
|
+
const ownedKeys = indexRevision === undefined
|
|
1478
|
+
? []
|
|
1479
|
+
: [...keys].filter((key) => confirmedFence.get(key) === indexRevision &&
|
|
1480
|
+
!this.#indexClaimedByAnotherOperationState(key, indexRevision, discardedStates));
|
|
1481
|
+
this.#engine.discardIndexes(ownedKeys);
|
|
1482
|
+
this.#resetOperationState(state);
|
|
1483
|
+
}
|
|
1484
|
+
#resetOperationState(state) {
|
|
1485
|
+
this.#resetOperationCausalState(state);
|
|
1486
|
+
state.indexRevision = undefined;
|
|
1487
|
+
state.indexKeys.clear();
|
|
1488
|
+
state.pathRecords.clear();
|
|
1489
|
+
}
|
|
1490
|
+
#resetOperationCausalState(state) {
|
|
1491
|
+
state.snapshotScope = undefined;
|
|
1492
|
+
state.indexClocks = new Map();
|
|
1493
|
+
state.cursors = Object.freeze([]);
|
|
1494
|
+
}
|
|
1495
|
+
#allocateIndexRevision() {
|
|
1496
|
+
this.#nextIndexRevision = incrementCanonicalDecimal(this.#nextIndexRevision);
|
|
1497
|
+
return this.#nextIndexRevision;
|
|
1498
|
+
}
|
|
1499
|
+
#resolveRecordEvidence(recordKey, evidence, pendingClocks, pendingScopes, pendingAnonymousClocks, consumedAnonymousClocks, projectedConflict = false) {
|
|
1500
|
+
if (!recordKeyMatchesModel(recordKey, evidence.model)) {
|
|
1501
|
+
protocolInvalid('extensions.distributed.snapshot.records.model');
|
|
1502
|
+
}
|
|
1503
|
+
const scopedKey = pendingScopes.get(evidence.scopeToken) ??
|
|
1504
|
+
this.#recordKeysByScope.get(evidence.scopeToken);
|
|
1505
|
+
if (scopedKey !== undefined && scopedKey !== recordKey) {
|
|
1506
|
+
protocolInvalid('extensions.distributed.snapshot.records.scopeToken');
|
|
1507
|
+
}
|
|
1508
|
+
pendingScopes.set(evidence.scopeToken, recordKey);
|
|
1509
|
+
const incoming = {
|
|
1510
|
+
scopeToken: evidence.scopeToken,
|
|
1511
|
+
incarnation: evidence.incarnation,
|
|
1512
|
+
revision: evidence.revision,
|
|
1513
|
+
tombstone: evidence.tombstone
|
|
1514
|
+
};
|
|
1515
|
+
const pending = pendingClocks.get(recordKey);
|
|
1516
|
+
if (pending !== undefined) {
|
|
1517
|
+
if (!sameRecordClock(pending, incoming)) {
|
|
1518
|
+
protocolInvalid('extensions.distributed.snapshot.records');
|
|
1519
|
+
}
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
let current = this.#recordClocks.get(recordKey);
|
|
1523
|
+
const anonymous = pendingAnonymousClocks.get(evidence.scopeToken) ??
|
|
1524
|
+
this.#anonymousRecordClocks.get(evidence.scopeToken);
|
|
1525
|
+
if (anonymous !== undefined) {
|
|
1526
|
+
if (anonymous.model !== evidence.model) {
|
|
1527
|
+
protocolInvalid('extensions.distributed.snapshot.records.model');
|
|
1528
|
+
}
|
|
1529
|
+
consumedAnonymousClocks.add(evidence.scopeToken);
|
|
1530
|
+
if (current === undefined) {
|
|
1531
|
+
current = anonymous.clock;
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
if (current.scopeToken !== anonymous.clock.scopeToken) {
|
|
1535
|
+
protocolInvalid('extensions.distributed.snapshot.records.scopeToken');
|
|
1536
|
+
}
|
|
1537
|
+
const anonymousComparison = compareRecordClock(anonymous.clock, current);
|
|
1538
|
+
if (anonymousComparison === 0 &&
|
|
1539
|
+
anonymous.clock.tombstone !== current.tombstone) {
|
|
1540
|
+
protocolInvalid('extensions.distributed.snapshot.records.tombstone');
|
|
1541
|
+
}
|
|
1542
|
+
if (anonymousComparison > 0)
|
|
1543
|
+
current = anonymous.clock;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (current === undefined) {
|
|
1547
|
+
pendingClocks.set(recordKey, incoming);
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1550
|
+
if (current.scopeToken !== incoming.scopeToken) {
|
|
1551
|
+
protocolInvalid('extensions.distributed.snapshot.records.scopeToken');
|
|
1552
|
+
}
|
|
1553
|
+
if (projectedConflict) {
|
|
1554
|
+
pendingClocks.set(recordKey, current);
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
const comparison = compareRecordClock(incoming, current);
|
|
1558
|
+
if (comparison < 0) {
|
|
1559
|
+
pendingClocks.set(recordKey, current);
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
if (comparison === 0) {
|
|
1563
|
+
if (current.tombstone !== incoming.tombstone) {
|
|
1564
|
+
protocolInvalid('extensions.distributed.snapshot.records.tombstone');
|
|
1565
|
+
}
|
|
1566
|
+
pendingClocks.set(recordKey, incoming);
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
if (current.tombstone &&
|
|
1570
|
+
!incoming.tombstone &&
|
|
1571
|
+
compareDistributedDecimal(incoming.incarnation, current.incarnation) <= 0) {
|
|
1572
|
+
protocolInvalid('extensions.distributed.snapshot.records.incarnation');
|
|
1573
|
+
}
|
|
1574
|
+
pendingClocks.set(recordKey, incoming);
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
#retainAnonymousRecordEvidence(evidence, pendingAnonymousClocks) {
|
|
1578
|
+
const incoming = {
|
|
1579
|
+
model: evidence.model,
|
|
1580
|
+
clock: {
|
|
1581
|
+
scopeToken: evidence.scopeToken,
|
|
1582
|
+
incarnation: evidence.incarnation,
|
|
1583
|
+
revision: evidence.revision,
|
|
1584
|
+
tombstone: evidence.tombstone
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
const current = pendingAnonymousClocks.get(evidence.scopeToken) ??
|
|
1588
|
+
this.#anonymousRecordClocks.get(evidence.scopeToken);
|
|
1589
|
+
if (current === undefined) {
|
|
1590
|
+
let retained = this.#anonymousRecordClocks.size;
|
|
1591
|
+
for (const scopeToken of pendingAnonymousClocks.keys()) {
|
|
1592
|
+
if (!this.#anonymousRecordClocks.has(scopeToken))
|
|
1593
|
+
retained += 1;
|
|
1594
|
+
}
|
|
1595
|
+
if (retained >= MAX_ANONYMOUS_RECORD_CLOCKS) {
|
|
1596
|
+
protocolInvalid('extensions.distributed.snapshot.records.capacity');
|
|
1597
|
+
}
|
|
1598
|
+
pendingAnonymousClocks.set(evidence.scopeToken, incoming);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (current.model !== incoming.model) {
|
|
1602
|
+
protocolInvalid('extensions.distributed.snapshot.records.model');
|
|
1603
|
+
}
|
|
1604
|
+
const comparison = compareRecordClock(incoming.clock, current.clock);
|
|
1605
|
+
if (comparison < 0)
|
|
1606
|
+
return;
|
|
1607
|
+
if (comparison === 0) {
|
|
1608
|
+
if (current.clock.tombstone !== incoming.clock.tombstone) {
|
|
1609
|
+
protocolInvalid('extensions.distributed.snapshot.records.tombstone');
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (current.clock.tombstone &&
|
|
1614
|
+
!incoming.clock.tombstone &&
|
|
1615
|
+
compareDistributedDecimal(incoming.clock.incarnation, current.clock.incarnation) <= 0) {
|
|
1616
|
+
protocolInvalid('extensions.distributed.snapshot.records.incarnation');
|
|
1617
|
+
}
|
|
1618
|
+
pendingAnonymousClocks.set(evidence.scopeToken, incoming);
|
|
1619
|
+
}
|
|
1620
|
+
#applyTombstoneEvidence(writer, evidenceItems, operation, pendingClocks, pendingScopes, pendingAnonymousClocks, consumedAnonymousClocks, consumedPaths, pendingProjectedRecordFenceClears) {
|
|
1621
|
+
for (const evidence of evidenceItems) {
|
|
1622
|
+
const encodedPath = evidence.path === undefined
|
|
1623
|
+
? undefined
|
|
1624
|
+
: responsePathKey(evidence.path);
|
|
1625
|
+
const recordKey = this.#recordKeysByScope.get(evidence.scopeToken) ??
|
|
1626
|
+
(encodedPath === undefined
|
|
1627
|
+
? undefined
|
|
1628
|
+
: operation.pathRecords.get(encodedPath));
|
|
1629
|
+
if (recordKey === undefined) {
|
|
1630
|
+
this.#retainAnonymousRecordEvidence(evidence, pendingAnonymousClocks);
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
if (encodedPath !== undefined)
|
|
1634
|
+
consumedPaths.add(encodedPath);
|
|
1635
|
+
const projectedFence = this.#projectedRecordFences.get(recordKey);
|
|
1636
|
+
const projectedClockComparison = projectedFence === undefined
|
|
1637
|
+
? undefined
|
|
1638
|
+
: compareEvidenceToProjectedFence(evidence, projectedFence);
|
|
1639
|
+
if (projectedFence !== undefined &&
|
|
1640
|
+
projectedClockComparison === 1) {
|
|
1641
|
+
pendingProjectedRecordFenceClears.set(recordKey, projectedFence);
|
|
1642
|
+
}
|
|
1643
|
+
if (this.#resolveRecordEvidence(recordKey, evidence, pendingClocks, pendingScopes, pendingAnonymousClocks, consumedAnonymousClocks, projectedFence !== undefined &&
|
|
1644
|
+
projectedClockComparison !== 1)) {
|
|
1645
|
+
writer.tombstoneRecord(recordKey, evidence.revision, evidence.incarnation);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
#applyPathlessEvidence(writer, evidenceItems, pathEvidence, consumedPaths, pendingClocks, pendingScopes, pendingAnonymousClocks, consumedAnonymousClocks, pendingProjectedRecordFenceClears) {
|
|
1650
|
+
for (const evidence of evidenceItems) {
|
|
1651
|
+
const recordKey = pendingScopes.get(evidence.scopeToken) ??
|
|
1652
|
+
this.#recordKeysByScope.get(evidence.scopeToken);
|
|
1653
|
+
if (recordKey === undefined) {
|
|
1654
|
+
this.#retainAnonymousRecordEvidence(evidence, pendingAnonymousClocks);
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
if (!recordKeyMatchesModel(recordKey, evidence.model)) {
|
|
1658
|
+
protocolInvalid('extensions.distributed.snapshot.records.model');
|
|
1659
|
+
}
|
|
1660
|
+
let certifiedByPath = false;
|
|
1661
|
+
for (const [path, candidate] of pathEvidence) {
|
|
1662
|
+
if (consumedPaths.has(path) &&
|
|
1663
|
+
candidate.scopeToken === evidence.scopeToken &&
|
|
1664
|
+
sameRecordRevision(candidate, evidence)) {
|
|
1665
|
+
certifiedByPath = true;
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (certifiedByPath)
|
|
1670
|
+
continue;
|
|
1671
|
+
const projectedFence = this.#projectedRecordFences.get(recordKey);
|
|
1672
|
+
const projectedClockComparison = projectedFence === undefined
|
|
1673
|
+
? undefined
|
|
1674
|
+
: compareEvidenceToProjectedFence(evidence, projectedFence);
|
|
1675
|
+
if (projectedFence !== undefined &&
|
|
1676
|
+
projectedClockComparison === 1) {
|
|
1677
|
+
pendingProjectedRecordFenceClears.set(recordKey, projectedFence);
|
|
1678
|
+
}
|
|
1679
|
+
if (this.#resolveRecordEvidence(recordKey, evidence, pendingClocks, pendingScopes, pendingAnonymousClocks, consumedAnonymousClocks, projectedFence !== undefined &&
|
|
1680
|
+
projectedClockComparison !== 1)) {
|
|
1681
|
+
// A change-log upsert proves only that a newer row exists. It
|
|
1682
|
+
// advances the tuple fence, but cannot certify any cached field.
|
|
1683
|
+
writer.discardRecord(recordKey);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
#validateLiveSnapshot(snapshot, live) {
|
|
1688
|
+
if (live === undefined || !live.supported)
|
|
1689
|
+
return;
|
|
1690
|
+
if (!snapshot.indexesComparable) {
|
|
1691
|
+
protocolInvalid('extensions.distributed.snapshot.indexesComparable');
|
|
1692
|
+
}
|
|
1693
|
+
const indexes = new Map(snapshot.indexes.map((index) => [index.projection, index]));
|
|
1694
|
+
if (indexes.size !== live.cursors.length) {
|
|
1695
|
+
protocolInvalid('extensions.distributed.live.cursors');
|
|
1696
|
+
}
|
|
1697
|
+
for (const cursor of live.cursors) {
|
|
1698
|
+
const index = indexes.get(cursor.projection);
|
|
1699
|
+
if (index === undefined ||
|
|
1700
|
+
index.position !== cursor.position ||
|
|
1701
|
+
(index.resume !== undefined &&
|
|
1702
|
+
index.resume.token !== cursor.token)) {
|
|
1703
|
+
protocolInvalid('extensions.distributed.live.cursors');
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
#planOptimisticReceipts(command, observations, satisfactionAdmissible) {
|
|
1708
|
+
return planOptimisticReceiptsOn(this.#optimisticHost(), command, observations, satisfactionAdmissible);
|
|
1709
|
+
}
|
|
1710
|
+
#applyReceiptOnly(command) {
|
|
1711
|
+
applyReceiptOnlyOn(this.#optimisticHost(), command);
|
|
1712
|
+
}
|
|
1713
|
+
/** Package-internal hook used by one watched operation. */
|
|
1714
|
+
_register(watch) {
|
|
1715
|
+
this.#rememberRenderedOperation(watch.key, watch.artifact, watch.variables, 'watch');
|
|
1716
|
+
let watches = this.#watches.get(watch.key);
|
|
1717
|
+
if (!watches) {
|
|
1718
|
+
watches = new Set();
|
|
1719
|
+
this.#watches.set(watch.key, watches);
|
|
1720
|
+
}
|
|
1721
|
+
watches.add(watch);
|
|
1722
|
+
const unwatch = this.#engine.watch((reader) => materializeReplicaOperation(reader, watch.artifact, watch.variables), (materialized) => watch._cacheChanged(materialized), { immediate: true });
|
|
1723
|
+
if (watch.liveRequested)
|
|
1724
|
+
this.#retainLive(watch);
|
|
1725
|
+
void this._fetch(watch, false);
|
|
1726
|
+
return () => {
|
|
1727
|
+
unwatch();
|
|
1728
|
+
watches?.delete(watch);
|
|
1729
|
+
if (watches?.size === 0)
|
|
1730
|
+
this.#watches.delete(watch.key);
|
|
1731
|
+
this.#forgetRenderedWatch(watch.key);
|
|
1732
|
+
if (watch.liveRequested)
|
|
1733
|
+
this.#releaseLive(watch.key);
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
/** Package-internal query-state lookup. */
|
|
1737
|
+
_state(key) {
|
|
1738
|
+
return this.#queryState(key);
|
|
1739
|
+
}
|
|
1740
|
+
/** Package-internal materialization for watch construction. */
|
|
1741
|
+
_materialize(artifact, variables) {
|
|
1742
|
+
return this.#engine.read((reader) => materializeReplicaOperation(reader, artifact, variables));
|
|
1743
|
+
}
|
|
1744
|
+
/** Package-internal cache-and-live coordinator. */
|
|
1745
|
+
_fetch(watch, force) {
|
|
1746
|
+
return fetchWatch(this.#fetchLiveHost(), watch, force);
|
|
1747
|
+
}
|
|
1748
|
+
_reportObserverErrors(errors) {
|
|
1749
|
+
if (errors.length === 0)
|
|
1750
|
+
return;
|
|
1751
|
+
reportSafely(this.#reportObserverError, new AggregateError(errors, 'replica observer delivery failed'));
|
|
1752
|
+
}
|
|
1753
|
+
#queryState(key) {
|
|
1754
|
+
let state = this.#queryStates.get(key);
|
|
1755
|
+
if (!state) {
|
|
1756
|
+
state = { fetching: false, errors: EMPTY_ERRORS, live: 'off' };
|
|
1757
|
+
this.#queryStates.set(key, state);
|
|
1758
|
+
}
|
|
1759
|
+
return state;
|
|
1760
|
+
}
|
|
1761
|
+
#emitState(key, allowFetch) {
|
|
1762
|
+
emitWatchState(this.#fetchLiveHost(), key, allowFetch);
|
|
1763
|
+
}
|
|
1764
|
+
#retainLive(watch) {
|
|
1765
|
+
retainLiveOn(this.#fetchLiveHost(), watch);
|
|
1766
|
+
}
|
|
1767
|
+
#restartLive(key) {
|
|
1768
|
+
restartLiveOn(this.#fetchLiveHost(), key);
|
|
1769
|
+
}
|
|
1770
|
+
#resumeLiveWatches() {
|
|
1771
|
+
resumeLiveWatchesOn(this.#fetchLiveHost());
|
|
1772
|
+
}
|
|
1773
|
+
#releaseLive(key) {
|
|
1774
|
+
releaseLiveOn(this.#fetchLiveHost(), key);
|
|
1775
|
+
}
|
|
1776
|
+
}
|