@hops-ops/distributed 0.0.0 → 4.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 +26 -13
- package/dist/auth-headers.js +3 -3
- package/dist/internal/cache-engine/engine.d.ts +3 -1
- package/dist/internal/cache-engine/engine.js +137 -22
- package/dist/internal/cache-engine/errors.d.ts +4 -0
- package/dist/internal/cache-engine/errors.js +8 -0
- package/dist/internal/cache-engine/index.d.ts +2 -2
- package/dist/internal/cache-engine/index.js +1 -1
- package/dist/internal/cache-engine/types.d.ts +16 -0
- package/dist/internal/cache-engine.d.ts +2 -2
- package/dist/internal/cache-engine.js +1 -1
- package/dist/protocol.d.ts +9 -2
- package/dist/protocol.js +35 -7
- package/dist/replica/command-runtime/create.js +499 -57
- package/dist/replica/command-runtime/index.d.ts +1 -1
- package/dist/replica/command-runtime/index.js +1 -1
- package/dist/replica/command-runtime/lib/effects.d.ts +22 -9
- package/dist/replica/command-runtime/lib/effects.js +118 -20
- package/dist/replica/command-runtime/lib/projection.d.ts +2 -2
- package/dist/replica/command-runtime/lib/projection.js +14 -6
- package/dist/replica/command-runtime/lib/status.js +18 -10
- package/dist/replica/command-runtime/lib/transport.js +4 -0
- package/dist/replica/command-runtime/lib/util.js +8 -3
- package/dist/replica/command-runtime/symbols.d.ts +9 -0
- package/dist/replica/command-runtime/symbols.js +9 -0
- package/dist/replica/command-runtime/types.d.ts +15 -6
- package/dist/replica/command-runtime/types.js +1 -1
- package/dist/replica/command-runtime.d.ts +1 -1
- package/dist/replica/command-runtime.js +1 -1
- package/dist/replica/commands/clone.js +2 -1
- package/dist/replica/commands/index.d.ts +1 -0
- package/dist/replica/commands/prepare.js +7 -4
- package/dist/replica/commands/receipt.js +30 -16
- package/dist/replica/commands/types.d.ts +10 -5
- package/dist/replica/commands/validate.d.ts +5 -1
- package/dist/replica/commands/validate.js +129 -62
- package/dist/replica/diagnostics/inspect.js +51 -19
- package/dist/replica/diagnostics/types.d.ts +7 -7
- package/dist/replica/distributed-replica/helpers.js +22 -8
- package/dist/replica/distributed-replica/hydration.js +14 -2
- package/dist/replica/distributed-replica/impl-diagnostics.d.ts +1 -1
- package/dist/replica/distributed-replica/impl-diagnostics.js +4 -3
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +1 -1
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +76 -42
- package/dist/replica/distributed-replica/impl-optimistic.d.ts +11 -2
- package/dist/replica/distributed-replica/impl-optimistic.js +22 -6
- package/dist/replica/distributed-replica/impl-protocol.js +4 -2
- package/dist/replica/distributed-replica/impl.d.ts +8 -2
- package/dist/replica/distributed-replica/impl.js +22 -5
- package/dist/replica/distributed-replica/optimistic.js +31 -3
- package/dist/replica/distributed-replica/types.d.ts +1 -0
- package/dist/replica/index-maintenance/engine.js +14 -6
- package/dist/replica/index.d.ts +4 -0
- package/dist/replica/index.js +2 -0
- package/dist/replica/materialize.js +25 -0
- package/dist/replica/mutation-cache.d.ts +62 -0
- package/dist/replica/mutation-cache.js +91 -0
- package/dist/replica/operation-binding.js +17 -6
- package/dist/replica/persistence/state.js +9 -1
- package/dist/replica/projection-delta/canonical.d.ts +6 -0
- package/dist/replica/projection-delta/canonical.js +29 -0
- package/dist/replica/projection-delta/index.d.ts +5 -0
- package/dist/replica/projection-delta/index.js +3 -0
- package/dist/replica/projection-delta/resolve.d.ts +8 -0
- package/dist/replica/projection-delta/resolve.js +266 -0
- package/dist/replica/projection-delta/types.d.ts +415 -0
- package/dist/replica/projection-delta/types.js +6 -0
- package/dist/replica/projection-delta/validate.d.ts +18 -0
- package/dist/replica/projection-delta/validate.js +1718 -0
- package/dist/replica/projection-delta/wasm-pure.d.ts +36 -0
- package/dist/replica/projection-delta/wasm-pure.js +71 -0
- package/dist/replica/query-plan/pagination.js +18 -8
- package/dist/replica/types.d.ts +13 -2
- package/dist/sveltekit/replica.d.ts +1 -1
- package/dist/sveltekit/replica.js +3 -0
- package/dist/sveltekit/vite.d.ts +6 -6
- package/dist/sveltekit/vite.js +6 -6
- package/package.json +1 -1
|
@@ -19,4 +19,4 @@ export declare function syncDiagnostics(host: DiagnosticsHost): void;
|
|
|
19
19
|
export declare function diagnosticEvent(host: DiagnosticsHost, event: ReplicaDiagnosticEventInput): void;
|
|
20
20
|
export declare function diagnosticOperation<TData, TVariables extends GraphqlVariables>(host: DiagnosticsHost, artifact: ReplicaOperationArtifact<TData, TVariables>): void;
|
|
21
21
|
export declare function diagnosticScopeTransition(host: DiagnosticsHost, previous: ProtocolGeneration | undefined, next: ProtocolGeneration): void;
|
|
22
|
-
export declare function retireDiagnosticLayer(host: DiagnosticsHost, id: string, action: 'retired' | 'rejected', receiptState: '
|
|
22
|
+
export declare function retireDiagnosticLayer(host: DiagnosticsHost, id: string, action: 'retired' | 'rejected', receiptState: 'atomic' | 'rejected', receipt?: OptimisticReceiptState): void;
|
|
@@ -85,8 +85,8 @@ export function syncDiagnostics(host) {
|
|
|
85
85
|
state: receipt === undefined
|
|
86
86
|
? 'optimistic'
|
|
87
87
|
: receipt.expectations.size === 0
|
|
88
|
-
? '
|
|
89
|
-
: '
|
|
88
|
+
? 'succeeded'
|
|
89
|
+
: 'succeeded_pending_projection',
|
|
90
90
|
expectations: receipt === undefined
|
|
91
91
|
? Object.freeze([])
|
|
92
92
|
: diagnosticReceiptExpectations(receipt)
|
|
@@ -142,7 +142,8 @@ export function diagnosticScopeTransition(host, previous, next) {
|
|
|
142
142
|
return;
|
|
143
143
|
if (previous !== undefined &&
|
|
144
144
|
previous.cacheScope === next.cacheScope &&
|
|
145
|
-
previous.schemaHash === next.schemaHash
|
|
145
|
+
previous.schemaHash === next.schemaHash &&
|
|
146
|
+
previous.authorizationGeneration === next.authorizationGeneration) {
|
|
146
147
|
return;
|
|
147
148
|
}
|
|
148
149
|
diagnosticEvent(host, Object.freeze({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CacheEngine, type CacheEngineSnapshot } from '../../internal/cache-engine.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type DistributedOpaqueString, type DistributedTrustedPreset } from '../../protocol.js';
|
|
3
3
|
import type { ReplicaDiagnosticEventInput } from '../diagnostics.js';
|
|
4
4
|
import type { ReplicaIndexPlanRegistration } from '../index-maintenance.js';
|
|
5
5
|
import type { ReplicaAuthoritativeScope, ReplicaDehydratedState } from '../types.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCacheEngine } from '../../internal/cache-engine.js';
|
|
2
|
+
import { compareDistributedDecimal } from '../../protocol.js';
|
|
2
3
|
import { matchReplicaTrustedPresetInventory } from '../commands.js';
|
|
3
4
|
import { replicaIndexKey, resolveArguments } from '../identity.js';
|
|
4
5
|
import { runtimeRoot } from '../selection.js';
|
|
@@ -170,6 +171,7 @@ export function dehydrateReplica(host) {
|
|
|
170
171
|
scope: Object.freeze({
|
|
171
172
|
protocolVersion: 1,
|
|
172
173
|
schemaHash: scope.schemaHash,
|
|
174
|
+
authorizationGeneration: scope.authorizationGeneration,
|
|
173
175
|
cacheScope: scope.cacheScope
|
|
174
176
|
}),
|
|
175
177
|
payload
|
|
@@ -191,6 +193,8 @@ export function hydrateReplica(host, state, authoritativeScope) {
|
|
|
191
193
|
}
|
|
192
194
|
if (parsed.scope.protocolVersion !== expectedScope.protocolVersion ||
|
|
193
195
|
parsed.scope.schemaHash !== expectedScope.schemaHash ||
|
|
196
|
+
parsed.scope.authorizationGeneration !==
|
|
197
|
+
expectedScope.authorizationGeneration ||
|
|
194
198
|
parsed.scope.cacheScope !== expectedScope.cacheScope) {
|
|
195
199
|
return rejected('scope-mismatch');
|
|
196
200
|
}
|
|
@@ -203,6 +207,8 @@ export function hydrateReplica(host, state, authoritativeScope) {
|
|
|
203
207
|
if (current !== undefined &&
|
|
204
208
|
(current.protocolVersion !== parsed.scope.protocolVersion ||
|
|
205
209
|
current.schemaHash !== parsed.scope.schemaHash ||
|
|
210
|
+
current.authorizationGeneration !==
|
|
211
|
+
parsed.scope.authorizationGeneration ||
|
|
206
212
|
current.cacheScope !== parsed.scope.cacheScope)) {
|
|
207
213
|
return rejected('active-scope-mismatch');
|
|
208
214
|
}
|
|
@@ -230,54 +236,82 @@ export function hydrateReplica(host, state, authoritativeScope) {
|
|
|
230
236
|
return rejected('metadata-mismatch');
|
|
231
237
|
}
|
|
232
238
|
if (preserveLocalCommandState) {
|
|
239
|
+
// Warm same-scope re-hydrate (soft nav / second SSR seed): keep confirmed
|
|
240
|
+
// records and indexes the route seed omitted. Seed keys upsert; purge only
|
|
241
|
+
// happens on auth/scope change (closeAuthorizationGeneration path).
|
|
233
242
|
host.closeActiveTransports();
|
|
243
|
+
host.queryStates.clear();
|
|
244
|
+
for (const [key, group] of parsed.operationProtocols) {
|
|
245
|
+
host.operationProtocols.set(key, group);
|
|
246
|
+
}
|
|
247
|
+
for (const [key, generation] of parsed.operationGenerations) {
|
|
248
|
+
host.operationGenerations.set(key, generation);
|
|
249
|
+
}
|
|
250
|
+
for (const [key, clock] of parsed.recordClocks) {
|
|
251
|
+
host.recordClocks.set(key, clock);
|
|
252
|
+
}
|
|
253
|
+
for (const [scopeToken, key] of parsed.recordKeysByScope) {
|
|
254
|
+
host.recordKeysByScope.set(scopeToken, key);
|
|
255
|
+
}
|
|
256
|
+
for (const [scopeToken, clock] of parsed.anonymousRecordClocks) {
|
|
257
|
+
host.anonymousRecordClocks.set(scopeToken, clock);
|
|
258
|
+
}
|
|
259
|
+
host.setTrustedPresets(parsed.trustedPresets);
|
|
260
|
+
if (compareDistributedDecimal(parsed.nextIndexRevision, host.getNextIndexRevision()) > 0) {
|
|
261
|
+
host.setNextIndexRevision(parsed.nextIndexRevision);
|
|
262
|
+
}
|
|
263
|
+
host.setProtocolGeneration(parsed.scope);
|
|
264
|
+
host.setArtifactBinding(Object.freeze({
|
|
265
|
+
version: 1,
|
|
266
|
+
schemaHash: parsed.scope.schemaHash,
|
|
267
|
+
...(binding?.surfaceIdentity !== undefined
|
|
268
|
+
? { surfaceIdentity: binding.surfaceIdentity }
|
|
269
|
+
: {}),
|
|
270
|
+
...(binding?.trustedPresets !== undefined
|
|
271
|
+
? { trustedPresets: binding.trustedPresets }
|
|
272
|
+
: {})
|
|
273
|
+
}));
|
|
274
|
+
host.engine.mergeConfirmed(parsed.cache);
|
|
234
275
|
}
|
|
235
276
|
else {
|
|
236
277
|
host.closeAuthorizationGeneration();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
if (!preserveLocalCommandState) {
|
|
278
|
+
host.queryStates.clear();
|
|
279
|
+
host.operationProtocols.clear();
|
|
280
|
+
for (const [key, group] of parsed.operationProtocols) {
|
|
281
|
+
host.operationProtocols.set(key, group);
|
|
282
|
+
}
|
|
283
|
+
host.operationGenerations.clear();
|
|
284
|
+
for (const [key, generation] of parsed.operationGenerations) {
|
|
285
|
+
host.operationGenerations.set(key, generation);
|
|
286
|
+
}
|
|
287
|
+
host.recordClocks.clear();
|
|
288
|
+
for (const [key, clock] of parsed.recordClocks) {
|
|
289
|
+
host.recordClocks.set(key, clock);
|
|
290
|
+
}
|
|
291
|
+
host.recordKeysByScope.clear();
|
|
292
|
+
for (const [scopeToken, key] of parsed.recordKeysByScope) {
|
|
293
|
+
host.recordKeysByScope.set(scopeToken, key);
|
|
294
|
+
}
|
|
295
|
+
host.anonymousRecordClocks.clear();
|
|
296
|
+
for (const [scopeToken, clock] of parsed.anonymousRecordClocks) {
|
|
297
|
+
host.anonymousRecordClocks.set(scopeToken, clock);
|
|
298
|
+
}
|
|
260
299
|
host.optimisticReceipts.clear();
|
|
261
300
|
host.diagnosticLayers?.clear();
|
|
262
301
|
host.setDiagnosticLayerSequence(0);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}));
|
|
277
|
-
if (preserveLocalCommandState) {
|
|
278
|
-
host.engine.restoreConfirmed(parsed.cache);
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
302
|
+
host.setTrustedPresets(parsed.trustedPresets);
|
|
303
|
+
host.setNextIndexRevision(parsed.nextIndexRevision);
|
|
304
|
+
host.setProtocolGeneration(parsed.scope);
|
|
305
|
+
host.setArtifactBinding(Object.freeze({
|
|
306
|
+
version: 1,
|
|
307
|
+
schemaHash: parsed.scope.schemaHash,
|
|
308
|
+
...(binding?.surfaceIdentity !== undefined
|
|
309
|
+
? { surfaceIdentity: binding.surfaceIdentity }
|
|
310
|
+
: {}),
|
|
311
|
+
...(binding?.trustedPresets !== undefined
|
|
312
|
+
? { trustedPresets: binding.trustedPresets }
|
|
313
|
+
: {})
|
|
314
|
+
}));
|
|
281
315
|
host.engine.restore(parsed.cache);
|
|
282
316
|
}
|
|
283
317
|
host.resumeLiveWatches();
|
|
@@ -286,7 +320,7 @@ export function hydrateReplica(host, state, authoritativeScope) {
|
|
|
286
320
|
host.diagnosticEvent(Object.freeze({
|
|
287
321
|
kind: 'hydration',
|
|
288
322
|
action: 'accepted',
|
|
289
|
-
reason: 'accepted'
|
|
323
|
+
reason: preserveLocalCommandState ? 'same-scope-merge' : 'accepted'
|
|
290
324
|
}));
|
|
291
325
|
host.diagnosticEvent(Object.freeze({
|
|
292
326
|
kind: 'scope',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CacheEngine } from '../../internal/cache-engine.js';
|
|
1
|
+
import type { CacheEngine, OptimisticLayerReplacement } from '../../internal/cache-engine.js';
|
|
2
2
|
import { type DistributedCommandMetadata, type DistributedProjectionObservation } from '../../protocol.js';
|
|
3
3
|
import type { ReplicaDiagnosticEventInput, ReplicaDiagnosticLayerInput } from '../diagnostics.js';
|
|
4
4
|
import type { ReplicaIndexSemanticChange } from '../index-maintenance.js';
|
|
@@ -18,9 +18,18 @@ export type OptimisticHost = {
|
|
|
18
18
|
setDiagnosticLayerSequence(value: number): void;
|
|
19
19
|
diagnosticEvent(event: ReplicaDiagnosticEventInput): void;
|
|
20
20
|
syncDiagnostics(): void;
|
|
21
|
-
retireDiagnosticLayer(id: string, action: 'retired' | 'rejected', receiptState: '
|
|
21
|
+
retireDiagnosticLayer(id: string, action: 'retired' | 'rejected', receiptState: 'atomic' | 'rejected', receipt?: OptimisticReceiptState): void;
|
|
22
22
|
};
|
|
23
23
|
export declare function createOptimisticLayerOn(host: OptimisticHost, id: string, update: (writer: ReplicaOptimisticWriter) => void, semanticChanges?: readonly ReplicaIndexSemanticChange[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Package-private correction seam for projection-aware command runtimes.
|
|
26
|
+
*
|
|
27
|
+
* Receipts and diagnostics remain attached to the stable layer identity. The
|
|
28
|
+
* cache engine owns atomic prefix evaluation and preserves the layer context.
|
|
29
|
+
*/
|
|
30
|
+
export declare function replaceOptimisticLayerOn(host: OptimisticHost, id: string, replacement: OptimisticLayerReplacement, context?: import('../../internal/cache-engine.js').OptimisticLayerContext): boolean;
|
|
31
|
+
/** Atomically replace both record operations and semantic index context. */
|
|
32
|
+
export declare function replaceReplicaOptimisticLayerOn(host: OptimisticHost, id: string, update: (writer: ReplicaOptimisticWriter) => void, semanticChanges: readonly ReplicaIndexSemanticChange[]): boolean;
|
|
24
33
|
export declare function markOptimisticLayerAcceptedOn(host: OptimisticHost, id: string, receipt?: DistributedCommandMetadata): boolean;
|
|
25
34
|
export declare function confirmOptimisticLayerOn<T>(host: OptimisticHost, id: string, update: (writer: ReplicaBaseWriter) => T): T;
|
|
26
35
|
export declare function rejectOptimisticLayerOn(host: OptimisticHost, id: string): boolean;
|
|
@@ -44,6 +44,22 @@ export function createOptimisticLayerOn(host, id, update, semanticChanges = Obje
|
|
|
44
44
|
host.syncDiagnostics();
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Package-private correction seam for projection-aware command runtimes.
|
|
49
|
+
*
|
|
50
|
+
* Receipts and diagnostics remain attached to the stable layer identity. The
|
|
51
|
+
* cache engine owns atomic prefix evaluation and preserves the layer context.
|
|
52
|
+
*/
|
|
53
|
+
export function replaceOptimisticLayerOn(host, id, replacement, context) {
|
|
54
|
+
assertReplicaOptimisticLayerId(id);
|
|
55
|
+
return host.engine.replaceOptimisticLayer(id, replacement, context);
|
|
56
|
+
}
|
|
57
|
+
/** Atomically replace both record operations and semantic index context. */
|
|
58
|
+
export function replaceReplicaOptimisticLayerOn(host, id, update, semanticChanges) {
|
|
59
|
+
assertReplicaOptimisticLayerId(id);
|
|
60
|
+
const captured = captureReplicaOptimisticUpdate(id, update, semanticChanges);
|
|
61
|
+
return host.engine.replaceOptimisticLayer(id, (_reader, writer) => replayReplicaOptimisticUpdate(writer, captured.operations), captured.context);
|
|
62
|
+
}
|
|
47
63
|
export function markOptimisticLayerAcceptedOn(host, id, receipt) {
|
|
48
64
|
if (receipt !== undefined && receipt.commandId !== id) {
|
|
49
65
|
throw new TypeError('optimistic layer id must equal the causal command id');
|
|
@@ -67,7 +83,7 @@ export function markOptimisticLayerAcceptedOn(host, id, receipt) {
|
|
|
67
83
|
host.diagnosticEvent(Object.freeze({
|
|
68
84
|
kind: 'receipt',
|
|
69
85
|
command: id,
|
|
70
|
-
state: '
|
|
86
|
+
state: 'succeeded',
|
|
71
87
|
obligations: 0,
|
|
72
88
|
observed: 0
|
|
73
89
|
}));
|
|
@@ -87,8 +103,8 @@ export function markOptimisticLayerAcceptedOn(host, id, receipt) {
|
|
|
87
103
|
kind: 'receipt',
|
|
88
104
|
command: id,
|
|
89
105
|
state: counts.obligations === 0
|
|
90
|
-
? '
|
|
91
|
-
: '
|
|
106
|
+
? 'succeeded'
|
|
107
|
+
: 'succeeded_pending_projection',
|
|
92
108
|
obligations: counts.obligations,
|
|
93
109
|
observed: counts.observed
|
|
94
110
|
}));
|
|
@@ -98,7 +114,7 @@ export function markOptimisticLayerAcceptedOn(host, id, receipt) {
|
|
|
98
114
|
}
|
|
99
115
|
export function confirmOptimisticLayerOn(host, id, update) {
|
|
100
116
|
const result = host.engine.confirmOptimisticLayer(id, (writer) => update(baseWriter(writer)));
|
|
101
|
-
host.retireDiagnosticLayer(id, 'retired', '
|
|
117
|
+
host.retireDiagnosticLayer(id, 'retired', 'atomic');
|
|
102
118
|
host.optimisticReceipts.delete(id);
|
|
103
119
|
host.syncDiagnostics();
|
|
104
120
|
return result;
|
|
@@ -162,8 +178,8 @@ export function applyReceiptOnly(host, command) {
|
|
|
162
178
|
kind: 'receipt',
|
|
163
179
|
command: id,
|
|
164
180
|
state: counts.obligations === 0
|
|
165
|
-
? '
|
|
166
|
-
: '
|
|
181
|
+
? 'succeeded'
|
|
182
|
+
: 'succeeded_pending_projection',
|
|
167
183
|
obligations: counts.obligations,
|
|
168
184
|
observed: counts.observed
|
|
169
185
|
}));
|
|
@@ -25,13 +25,15 @@ export function stageProtocolGeneration(host, envelope) {
|
|
|
25
25
|
const next = {
|
|
26
26
|
protocolVersion: 1,
|
|
27
27
|
cacheScope: envelope.cacheScope,
|
|
28
|
-
schemaHash: envelope.schemaHash
|
|
28
|
+
schemaHash: envelope.schemaHash,
|
|
29
|
+
authorizationGeneration: envelope.authorizationGeneration
|
|
29
30
|
};
|
|
30
31
|
const current = host.getProtocolGeneration();
|
|
31
32
|
if (current === undefined)
|
|
32
33
|
return next;
|
|
33
34
|
if (current.cacheScope === next.cacheScope &&
|
|
34
|
-
current.schemaHash === next.schemaHash
|
|
35
|
+
current.schemaHash === next.schemaHash &&
|
|
36
|
+
current.authorizationGeneration === next.authorizationGeneration) {
|
|
35
37
|
return current;
|
|
36
38
|
}
|
|
37
39
|
purgeProtocolGeneration(host);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { GraphqlVariables } from '../../types.js';
|
|
2
2
|
import { type DistributedCommandMetadata } from '../../protocol.js';
|
|
3
|
-
import { replicaCommandAuthority, replicaCommandDirectProjection, replicaResultObservation, type ReplicaCommandAuthorityRegistration, type ReplicaCommandDirectProjection, type ReplicaCommandSurfaceContract, type ReplicaResultObservationRegistration } from '../command-runtime.js';
|
|
3
|
+
import { replicaCommandAuthority, replicaCommandDirectProjection, replicaCommandProjectionDelta, replicaCommandReadRecord, replicaResultObservation, type ReplicaCommandAuthorityRegistration, type ReplicaCommandDirectProjection, type ReplicaCommandSurfaceContract, type ReplicaResultObservationRegistration } from '../command-runtime.js';
|
|
4
4
|
import { type MaterializedReplicaResult } from '../materialize.js';
|
|
5
5
|
import { type ReplicaIndexSemanticChange } from '../index-maintenance.js';
|
|
6
|
-
import type { DistributedReplicaOptions, DistributedReplica as DistributedReplicaApi, ReplicaAuthoritativeScope, ReplicaBaseWriter, ReplicaDehydratedState, ReplicaIdentity, ReplicaIndexInspection, ReplicaIndexTarget, ReplicaModelArtifact, ReplicaOperationArtifact, ReplicaOptimisticWriter, ReplicaRecordInspection, ReplicaRevalidationPlan, ReplicaRevision, ReplicaResultEnvelope, ReplicaSnapshot, ReplicaWatch, ReplicaWriteSource, WatchReplicaOptions } from '../types.js';
|
|
6
|
+
import type { DistributedReplicaOptions, DistributedReplica as DistributedReplicaApi, ReplicaAuthoritativeScope, ReplicaBaseWriter, ReplicaDehydratedState, ReplicaIdentity, ReplicaIndexInspection, ReplicaIndexTarget, ReplicaModelArtifact, ReplicaOperationArtifact, ReplicaOptimisticWriter, ReplicaRecordInspection, ReplicaRevalidationPlan, ReplicaRevision, ReplicaResultEnvelope, ReplicaSnapshot, ReplicaValue, ReplicaWatch, ReplicaWriteSource, WatchReplicaOptions } from '../types.js';
|
|
7
7
|
import type { QueryState } from './types.js';
|
|
8
8
|
import { ReplicaWatchState } from './watch.js';
|
|
9
9
|
export declare class DistributedReplicaImpl implements DistributedReplicaApi {
|
|
@@ -14,6 +14,7 @@ export declare class DistributedReplicaImpl implements DistributedReplicaApi {
|
|
|
14
14
|
[replicaCommandAuthority](contract: ReplicaCommandSurfaceContract): ReplicaCommandAuthorityRegistration;
|
|
15
15
|
[replicaResultObservation](observer: (envelope: ReplicaResultEnvelope<unknown>) => void): ReplicaResultObservationRegistration;
|
|
16
16
|
[replicaCommandDirectProjection](commandId: string, projection: ReplicaCommandDirectProjection): void;
|
|
17
|
+
[replicaCommandProjectionDelta](commandId: string, update: (writer: ReplicaOptimisticWriter) => void, semanticChanges: readonly ReplicaIndexSemanticChange[]): boolean;
|
|
17
18
|
read<TData, TVariables extends GraphqlVariables>(artifact: ReplicaOperationArtifact<TData, TVariables>, variables: TVariables): ReplicaSnapshot<TData>;
|
|
18
19
|
watch<TData, TVariables extends GraphqlVariables>(artifact: ReplicaOperationArtifact<TData, TVariables>, variables: TVariables, options?: WatchReplicaOptions): ReplicaWatch<TData>;
|
|
19
20
|
writeResult<TData, TVariables extends GraphqlVariables>(artifact: ReplicaOperationArtifact<TData, TVariables>, variables: TVariables, envelope: ReplicaResultEnvelope<TData>, source: ReplicaWriteSource): void;
|
|
@@ -31,6 +32,11 @@ export declare class DistributedReplicaImpl implements DistributedReplicaApi {
|
|
|
31
32
|
releaseRecord(model: ReplicaModelArtifact, identity: ReplicaIdentity): void;
|
|
32
33
|
gc(): readonly string[];
|
|
33
34
|
inspectRecord(model: ReplicaModelArtifact, identity: ReplicaIdentity): ReplicaRecordInspection | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Package-private live fields for pure-reduce optimism.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
[replicaCommandReadRecord](model: ReplicaModelArtifact, identity: ReplicaIdentity): Readonly<Record<string, ReplicaValue>> | undefined;
|
|
34
40
|
inspectIndex(target: ReplicaIndexTarget): ReplicaIndexInspection | undefined;
|
|
35
41
|
/** Package-internal hook used by one watched operation. */
|
|
36
42
|
_register<TData, TVariables extends GraphqlVariables>(watch: ReplicaWatchState<TData, TVariables>): () => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CacheRevisionConflictError, createCacheEngine } from '../../internal/cache-engine.js';
|
|
2
2
|
import { compareDistributedDecimal, DistributedProtocolError, parseGraphqlResponseExtensions } from '../../protocol.js';
|
|
3
3
|
import { matchReplicaTrustedPresetInventory } from '../commands.js';
|
|
4
|
-
import { replicaCommandAuthority, replicaCommandDirectProjection, replicaResultObservation } from '../command-runtime.js';
|
|
4
|
+
import { replicaCommandAuthority, replicaCommandDirectProjection, replicaCommandProjectionDelta, replicaCommandReadRecord, replicaResultObservation } from '../command-runtime.js';
|
|
5
5
|
import { canonicalizeOperationVariables, replicaIndexKey, replicaRecordKey, resolveArguments } from '../identity.js';
|
|
6
6
|
import { materializeReplicaOperation } from '../materialize.js';
|
|
7
7
|
import { normalizeReplicaResult } from '../normalize.js';
|
|
@@ -15,7 +15,7 @@ import { assertWriteSource, indexKeyFromTarget, indexMaintenanceSnapshot, indexS
|
|
|
15
15
|
import { ReplicaWatchState } from './watch.js';
|
|
16
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
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';
|
|
18
|
+
import { applyReceiptOnly as applyReceiptOnlyOn, confirmOptimisticLayerOn, createOptimisticLayerOn, markOptimisticLayerAcceptedOn, planOptimisticReceipts as planOptimisticReceiptsOn, rejectOptimisticLayerOn, replaceReplicaOptimisticLayerOn } from './impl-optimistic.js';
|
|
19
19
|
import { dehydrateReplica, finishDehydration as finishDehydrationOn, hydrateReplica } from './impl-hydration-orchestrate.js';
|
|
20
20
|
import { diagnosticEvent as diagnosticEventOn, diagnosticOperation as diagnosticOperationOn, diagnosticScopeTransition as diagnosticScopeTransitionOn, retireDiagnosticLayer as retireDiagnosticLayerOn, syncDiagnostics as syncDiagnosticsOn } from './impl-diagnostics.js';
|
|
21
21
|
export class DistributedReplicaImpl {
|
|
@@ -337,6 +337,7 @@ export class DistributedReplicaImpl {
|
|
|
337
337
|
: Object.freeze({
|
|
338
338
|
protocolVersion: scope.protocolVersion,
|
|
339
339
|
schemaHash: scope.schemaHash,
|
|
340
|
+
authorizationGeneration: scope.authorizationGeneration,
|
|
340
341
|
cacheScope: scope.cacheScope
|
|
341
342
|
});
|
|
342
343
|
}
|
|
@@ -477,6 +478,9 @@ export class DistributedReplicaImpl {
|
|
|
477
478
|
}));
|
|
478
479
|
}
|
|
479
480
|
}
|
|
481
|
+
[replicaCommandProjectionDelta](commandId, update, semanticChanges) {
|
|
482
|
+
return replaceReplicaOptimisticLayerOn(this.#optimisticHost(), commandId, update, semanticChanges);
|
|
483
|
+
}
|
|
480
484
|
read(artifact, variables) {
|
|
481
485
|
this.#bindArtifact(artifact);
|
|
482
486
|
const stableVariables = canonicalizeOperationVariables(artifact, variables);
|
|
@@ -927,7 +931,7 @@ export class DistributedReplicaImpl {
|
|
|
927
931
|
}
|
|
928
932
|
for (const [id, receipt] of receiptPlan.updates) {
|
|
929
933
|
if (receiptPlan.satisfied.includes(id)) {
|
|
930
|
-
this.#retireDiagnosticLayer(id, 'retired', '
|
|
934
|
+
this.#retireDiagnosticLayer(id, 'retired', 'atomic', receipt);
|
|
931
935
|
this.#optimisticReceipts.delete(id);
|
|
932
936
|
}
|
|
933
937
|
else {
|
|
@@ -943,8 +947,8 @@ export class DistributedReplicaImpl {
|
|
|
943
947
|
kind: 'receipt',
|
|
944
948
|
command: id,
|
|
945
949
|
state: counts.obligations === 0
|
|
946
|
-
? '
|
|
947
|
-
: '
|
|
950
|
+
? 'succeeded'
|
|
951
|
+
: 'succeeded_pending_projection',
|
|
948
952
|
obligations: counts.obligations,
|
|
949
953
|
observed: counts.observed
|
|
950
954
|
}));
|
|
@@ -1105,6 +1109,19 @@ export class DistributedReplicaImpl {
|
|
|
1105
1109
|
});
|
|
1106
1110
|
});
|
|
1107
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Package-private live fields for pure-reduce optimism.
|
|
1114
|
+
* @internal
|
|
1115
|
+
*/
|
|
1116
|
+
[replicaCommandReadRecord](model, identity) {
|
|
1117
|
+
const key = replicaRecordKey(model, identity);
|
|
1118
|
+
return this.#engine.read((reader) => {
|
|
1119
|
+
const record = reader.record(key);
|
|
1120
|
+
if (!record)
|
|
1121
|
+
return undefined;
|
|
1122
|
+
return Object.freeze({ ...record.fields });
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1108
1125
|
inspectIndex(target) {
|
|
1109
1126
|
const key = indexKeyFromTarget(target);
|
|
1110
1127
|
return this.#engine.read((reader) => {
|
|
@@ -85,18 +85,29 @@ export function captureReplicaOptimisticUpdate(id, update, semanticChanges) {
|
|
|
85
85
|
const key = replicaRecordKey(model, identity);
|
|
86
86
|
const fields = cloneOptimisticFields(patch.fields);
|
|
87
87
|
const links = cloneOptimisticLinks(patch.links);
|
|
88
|
-
|
|
88
|
+
const unset = cloneOptimisticUnset(patch.unset, fields);
|
|
89
|
+
if (Object.keys(fields).length === 0 &&
|
|
90
|
+
Object.keys(links).length === 0 &&
|
|
91
|
+
unset.length === 0) {
|
|
89
92
|
return;
|
|
90
93
|
}
|
|
91
94
|
operations.push(Object.freeze({
|
|
92
95
|
kind: 'write-record',
|
|
93
|
-
write: Object.freeze({
|
|
96
|
+
write: Object.freeze({
|
|
97
|
+
key,
|
|
98
|
+
fields,
|
|
99
|
+
links,
|
|
100
|
+
...(unset.length === 0 ? {} : { unset }),
|
|
101
|
+
...(patch.ifPresent === true ? { ifPresent: true } : {})
|
|
102
|
+
})
|
|
94
103
|
}));
|
|
95
104
|
changes.push(Object.freeze({
|
|
96
105
|
kind: 'upsert',
|
|
97
106
|
model: model.id,
|
|
98
107
|
key,
|
|
99
|
-
fields
|
|
108
|
+
fields,
|
|
109
|
+
...(unset.length === 0 ? {} : { unset }),
|
|
110
|
+
...(patch.ifPresent === true ? { ifPresent: true } : {})
|
|
100
111
|
}));
|
|
101
112
|
},
|
|
102
113
|
tombstoneRecord(model, identity) {
|
|
@@ -174,6 +185,23 @@ export function cloneOptimisticFields(fields) {
|
|
|
174
185
|
return [name, cloneJsonValue(value)];
|
|
175
186
|
})));
|
|
176
187
|
}
|
|
188
|
+
function cloneOptimisticUnset(unset, fields) {
|
|
189
|
+
if (unset === undefined)
|
|
190
|
+
return Object.freeze([]);
|
|
191
|
+
if (!Array.isArray(unset))
|
|
192
|
+
throw new TypeError('record unset must be an array');
|
|
193
|
+
const result = unset.map((name) => {
|
|
194
|
+
assertReplicaName(name, 'record unset field');
|
|
195
|
+
if (Object.hasOwn(fields, name)) {
|
|
196
|
+
throw new TypeError('record field cannot be set and unset');
|
|
197
|
+
}
|
|
198
|
+
return name;
|
|
199
|
+
});
|
|
200
|
+
if (new Set(result).size !== result.length) {
|
|
201
|
+
throw new TypeError('record unset fields must be unique');
|
|
202
|
+
}
|
|
203
|
+
return Object.freeze(result);
|
|
204
|
+
}
|
|
177
205
|
export function cloneOptimisticLinks(links) {
|
|
178
206
|
if (links === undefined)
|
|
179
207
|
return Object.freeze({});
|
|
@@ -20,6 +20,7 @@ export type ProtocolGeneration = {
|
|
|
20
20
|
protocolVersion: 1;
|
|
21
21
|
cacheScope: DistributedOpaqueString;
|
|
22
22
|
schemaHash: string;
|
|
23
|
+
authorizationGeneration: string;
|
|
23
24
|
};
|
|
24
25
|
export type RenderedOperation = {
|
|
25
26
|
readonly artifact: ReplicaOperationArtifact<unknown, GraphqlVariables>;
|
|
@@ -199,13 +199,19 @@ export function applyLayers(base, layers) {
|
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
201
201
|
const current = records.get(change.key);
|
|
202
|
+
if (change.ifPresent === true && current === undefined) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
202
205
|
if (current !== undefined && current.model !== change.model) {
|
|
203
206
|
throw new TypeError(`record model changed for ${change.key}`);
|
|
204
207
|
}
|
|
208
|
+
const fields = { ...(current?.fields ?? {}), ...change.fields };
|
|
209
|
+
for (const field of change.unset ?? [])
|
|
210
|
+
delete fields[field];
|
|
205
211
|
records.set(change.key, {
|
|
206
212
|
key: change.key,
|
|
207
213
|
model: change.model,
|
|
208
|
-
fields
|
|
214
|
+
fields
|
|
209
215
|
});
|
|
210
216
|
}
|
|
211
217
|
}
|
|
@@ -421,10 +427,8 @@ export function maintainEntityIndex(planValue, index, changes, trustedPresets) {
|
|
|
421
427
|
index.metadata.coverage.offset === 0 &&
|
|
422
428
|
index.metadata.coverage.limit !== undefined &&
|
|
423
429
|
records.length > index.metadata.coverage.limit) {
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
// operation's exact window when stacked optimistic inserts cross its
|
|
427
|
-
// limit.
|
|
430
|
+
// First-page inserts re-sort known members then clamp to the operation
|
|
431
|
+
// window so a full live page still shows optimistic rows that sort in.
|
|
428
432
|
records = records.slice(0, index.metadata.coverage.limit);
|
|
429
433
|
}
|
|
430
434
|
return sameStringList(records, current)
|
|
@@ -451,6 +455,10 @@ export function certifyOffsetCoverage(planValue, index, confirmedRecords, change
|
|
|
451
455
|
typeof artifact.maxLimit === 'number'
|
|
452
456
|
? Math.min(configuredLimit, artifact.maxLimit)
|
|
453
457
|
: configuredLimit;
|
|
458
|
+
// First-page inserts remain local even when hasNext is true: membership is
|
|
459
|
+
// re-sorted and truncated to the limit among known rows. Delete/reorder on a
|
|
460
|
+
// page with a next boundary still fail closed (unseen fill-in).
|
|
461
|
+
const hasNextBlocksLocality = coverage.hasNext === true && changeKind !== 'insert';
|
|
454
462
|
if (typeof expectedOffset !== 'number' ||
|
|
455
463
|
!Number.isSafeInteger(expectedOffset) ||
|
|
456
464
|
expectedOffset < 0 ||
|
|
@@ -461,7 +469,7 @@ export function certifyOffsetCoverage(planValue, index, confirmedRecords, change
|
|
|
461
469
|
coverage.offset !== expectedOffset ||
|
|
462
470
|
coverage.limit !== expectedLimit ||
|
|
463
471
|
coverage.returned !== confirmedRecords ||
|
|
464
|
-
|
|
472
|
+
hasNextBlocksLocality) {
|
|
465
473
|
return reason('invalid_index_metadata', ['indexes', index.key, 'coverage'], 'offset coverage does not match the request, confirmed index size, or observed page boundary');
|
|
466
474
|
}
|
|
467
475
|
return undefined;
|
package/dist/replica/index.d.ts
CHANGED
|
@@ -10,8 +10,12 @@ export { createReplicaIndexedDbPersistence, REPLICA_OFFLINE_COMMAND_OUTBOX_SUPPO
|
|
|
10
10
|
export type { ReplicaBoundCommand, ReplicaBoundCommands, ReplicaCommandCallOptions, ReplicaCommandProjectedOutcome, ReplicaCommandReceipt, ReplicaCommandRecoveryReceipt, ReplicaCommandRuntime, ReplicaCommandRuntimeErrorCode, ReplicaCommandRuntimeOptions, ReplicaCommandStatus, ReplicaCommandStatusArtifact, ReplicaCommandStatusRequest, ReplicaCommandTransport, ReplicaCommandTransportRequest, ReplicaCommandTransportResult } from './command-runtime.js';
|
|
11
11
|
export type { ReplicaIndexedDbFactory, ReplicaIndexedDbPersistence, ReplicaIndexedDbPersistenceOptions, ReplicaPersistenceModelPolicy, ReplicaPersistencePolicy } from './persistence.js';
|
|
12
12
|
export type { PrepareReplicaCommandOptions, ReplicaCommandArtifact, ReplicaCommandConfirmation, ReplicaCommandConfirmations, ReplicaCommandContractErrorCode, ReplicaCommandDirectProjection, ReplicaCommandEffect, ReplicaCommandEffectExpression, ReplicaCommandEffectField, ReplicaCommandEffectKey, ReplicaCommandEffectRelationship, ReplicaCommandEffects, ReplicaCommandGenerators, ReplicaCommandInputDefault, ReplicaCommandInputDefaults, ReplicaCommandRevalidationPlan, ReplicaCommandScalarCodec, ReplicaCommandShape, ReplicaCommandTypeDefinition, ReplicaCommandTypeField, ReplicaCommandVariables, ReplicaTrustedPresetDescriptor, ReplicaPreparedCommand, ReplicaPreparedCommandEffect, ReplicaPreparedConfirmation, ReplicaPreparedConfirmations, ReplicaPreparedEffectField, ReplicaPreparedEffectKey, ReplicaReceiptVerification } from './commands.js';
|
|
13
|
+
export { createWasmJsonPure } from './projection-delta/index.js';
|
|
14
|
+
export type { CreateWasmJsonPureOptions, PreparedCommandProjection, PreparedProjectionOperation, ReplicaCommandProjection, WasmJsonModule, WasmJsonPureHost } from './projection-delta/index.js';
|
|
13
15
|
export { compareReplicaOrder, decideReplicaPaginationMaintenance, evaluateReplicaFilter } from './query-plan.js';
|
|
14
16
|
export { createReplicaIndexMaintenanceRegistry, formatReplicaIndexStaleReason } from './index-maintenance.js';
|
|
15
17
|
export type { ReplicaIndexDependencyChange, ReplicaIndexMaintenanceDecision, ReplicaIndexMaintenanceIndex, ReplicaIndexMaintenanceReason, ReplicaIndexMaintenanceReasonCode, ReplicaIndexMaintenanceRecord, ReplicaIndexMaintenanceRegistry, ReplicaIndexMaintenanceSnapshot, ReplicaIndexPlanRegistration, ReplicaIndexRecordChange, ReplicaIndexRelationshipChange, ReplicaIndexSemanticChange, ReplicaIndexSemanticLayer } from './index-maintenance.js';
|
|
16
18
|
export type { ReplicaFilterEvaluation, ReplicaFilterEvaluationOptions, ReplicaOrderComparison, ReplicaPaginationChange, ReplicaPaginationMaintenanceDecision, ReplicaQueryPlanPath, ReplicaQueryPlanReason, ReplicaQueryPlanReasonCode, ReplicaRelationshipFilterRequest } from './query-plan.js';
|
|
17
19
|
export type { DistributedReplicaOptions, DistributedReplica, ReplicaArgumentsArtifact, ReplicaArgumentValue, ReplicaAuthoritativeScope, ReplicaBaseWriter, ReplicaBranchSemantic, ReplicaClientSurface, ReplicaCoverageArtifact, ReplicaDehydratedState, ReplicaFilterArtifact, ReplicaFilterExpression, ReplicaFilterFieldArtifact, ReplicaFilterLiteral, ReplicaFilterOperand, ReplicaFilterOperator, ReplicaIdentity, ReplicaIndexCoverage, ReplicaIndexInspection, ReplicaIndexTarget, ReplicaListValue, ReplicaLiveObserver, ReplicaLiveState, ReplicaLiteralValue, ReplicaModelArtifact, ReplicaObjectBranch, ReplicaObjectMember, ReplicaObjectSelection, ReplicaObjectValue, ReplicaOperationArtifact, ReplicaOperationSourceLocation, ReplicaOperationProtocol, ReplicaOrderArtifact, ReplicaOrderFieldArtifact, ReplicaOrderTieBreakerArtifact, ReplicaOptimisticWriter, ReplicaPaginationArtifact, ReplicaPaginationDisposition, ReplicaProtocolOperationArtifact, ReplicaRecordInspection, ReplicaRecordPatch, ReplicaRevalidationPlan, ReplicaRevalidationRelationship, ReplicaRevision, ReplicaRelationshipArtifact, ReplicaRelationshipKeyMapping, ReplicaRelationshipKind, ReplicaResultEnvelope, ReplicaRowPolicyArtifact, ReplicaRootSelection, ReplicaScalarSelection, ReplicaSelectionStorage, ReplicaSparse, ReplicaSnapshot, ReplicaStatus, ReplicaTransport, ReplicaTransportRequest, ReplicaVariableValue, ReplicaVariableCodecArtifact, ReplicaVariableEnumInputRef, ReplicaVariableFilterInputDefinition, ReplicaVariableFilterInputField, ReplicaVariableFilterInputRelationship, ReplicaVariableFilterInputTarget, ReplicaVariableInputDefinition, ReplicaVariableInputRef, ReplicaVariableListInputRef, ReplicaVariableNamedInputRef, ReplicaVariableOrderInputDefinition, ReplicaVariableOrderInputField, ReplicaVariableScalarInputRef, ReplicaValue, ReplicaWatch, ReplicaWriteSource, WatchReplicaOptions } from './types.js';
|
|
20
|
+
export { lowerMutationCache, MUTATION_CACHE_VISIBILITY_FULL, MUTATION_CACHE_VISIBILITY_UNAUTHORIZED, } from './mutation-cache.js';
|
|
21
|
+
export type { MutationCacheEffect, MutationCacheProgram, MutationCacheVisibility, MutationField, MutationOperation, MutationProgram, MutationTarget, } from './mutation-cache.js';
|
package/dist/replica/index.js
CHANGED
|
@@ -5,5 +5,7 @@ export { canonicalizeOperationVariables, replicaIndexKey, replicaRecordKey } fro
|
|
|
5
5
|
export { prepareReplicaCommand, ReplicaCommandContractError, verifyReplicaCommandReceipt } from './commands.js';
|
|
6
6
|
export { createReplicaCommandRuntime, ReplicaCommandRuntimeError } from './command-runtime.js';
|
|
7
7
|
export { createReplicaIndexedDbPersistence, REPLICA_OFFLINE_COMMAND_OUTBOX_SUPPORTED } from './persistence.js';
|
|
8
|
+
export { createWasmJsonPure } from './projection-delta/index.js';
|
|
8
9
|
export { compareReplicaOrder, decideReplicaPaginationMaintenance, evaluateReplicaFilter } from './query-plan.js';
|
|
9
10
|
export { createReplicaIndexMaintenanceRegistry, formatReplicaIndexStaleReason } from './index-maintenance.js';
|
|
11
|
+
export { lowerMutationCache, MUTATION_CACHE_VISIBILITY_FULL, MUTATION_CACHE_VISIBILITY_UNAUTHORIZED, } from './mutation-cache.js';
|
|
@@ -54,6 +54,16 @@ function materializeBranch(reader, selection, index, variables) {
|
|
|
54
54
|
if (selection.cardinality === 'one') {
|
|
55
55
|
const key = index.records[0];
|
|
56
56
|
if (key === undefined) {
|
|
57
|
+
// Empty relationship edge: null when the selection allows it.
|
|
58
|
+
if (selection.nullable) {
|
|
59
|
+
return {
|
|
60
|
+
value: null,
|
|
61
|
+
present: true,
|
|
62
|
+
complete: indexComplete,
|
|
63
|
+
stale,
|
|
64
|
+
identitySignature: `null:${index.metadata.staleReason ?? ''}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
57
67
|
return {
|
|
58
68
|
present: false,
|
|
59
69
|
complete: false,
|
|
@@ -144,6 +154,21 @@ function materializeNestedBranch(reader, parentKey, parentIncarnation, selection
|
|
|
144
154
|
});
|
|
145
155
|
const index = reader.index(indexKey);
|
|
146
156
|
if (index?.metadata?.parentIncarnation !== parentIncarnation) {
|
|
157
|
+
/*
|
|
158
|
+
* Optimistic parent upserts often write the FK row before a relationship
|
|
159
|
+
* index exists. For nullable joins (e.g. ChatMessages.author), treat the
|
|
160
|
+
* missing edge as an authorized null so the parent list stays complete
|
|
161
|
+
* and visible. Required relationships still fail closed as incomplete.
|
|
162
|
+
*/
|
|
163
|
+
if (selection.nullable) {
|
|
164
|
+
return {
|
|
165
|
+
value: null,
|
|
166
|
+
present: true,
|
|
167
|
+
complete: true,
|
|
168
|
+
stale: index !== undefined,
|
|
169
|
+
identitySignature: `null-missing-edge:${selection.field}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
147
172
|
return {
|
|
148
173
|
present: false,
|
|
149
174
|
complete: false,
|