@abloatai/humans 0.48.0 → 0.49.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/dist/client.d.ts +6 -0
- package/dist/humans.d.ts +1 -1
- package/dist/local/RuntimeContext.d.ts +1 -1
- package/dist/local/SyncClient.d.ts +1 -1
- package/dist/local/SyncClient.js +3 -3
- package/dist/local/client/createModelProxy.d.ts +7 -1
- package/dist/local/client/createModelProxy.js +146 -39
- package/dist/local/client/options.d.ts +1 -1
- package/dist/local/client/reactiveEngine.js +64 -7
- package/dist/local/client/resourceTypes.d.ts +1 -1
- package/dist/local/client/resourceTypes.js +1 -1
- package/dist/local/client/storeCluster.d.ts +3 -0
- package/dist/local/client/storeCluster.js +4 -2
- package/dist/local/client/storeLifecycle.d.ts +2 -0
- package/dist/local/client/storeLifecycle.js +1 -0
- package/dist/local/client/wsMutationExecutor.d.ts +4 -1
- package/dist/local/client/wsMutationExecutor.js +26 -2
- package/dist/local/context.js +5 -1
- package/dist/local/interfaces/index.d.ts +3 -1
- package/dist/local/mutators/UndoManager.d.ts +1 -1
- package/dist/local/mutators/UndoManager.js +1 -1
- package/dist/local/query/client.js +15 -0
- package/dist/local/query/types.d.ts +14 -0
- package/dist/local/stores/syncAction.d.ts +1 -1
- package/dist/local/stores/syncAction.js +1 -1
- package/dist/local/sync/ConnectionManager.d.ts +1 -1
- package/dist/local/sync/ConnectionManager.js +1 -1
- package/dist/local/sync/OnDemandLoader.d.ts +3 -0
- package/dist/local/sync/OnDemandLoader.js +15 -2
- package/dist/local/sync/SyncWebSocket.d.ts +1 -1
- package/dist/local/sync/SyncWebSocket.js +1 -1
- package/dist/local/sync/commitFrames.d.ts +1 -1
- package/dist/local/sync/commitFrames.js +1 -1
- package/dist/local/sync/contextPorts.d.ts +1 -1
- package/dist/local/sync/contextPorts.js +1 -1
- package/dist/local/sync/credentialLifecycle.d.ts +1 -1
- package/dist/local/sync/credentialLifecycle.js +1 -1
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/sync/wsFrameHandlers.js +1 -1
- package/dist/local/transactions/databaseCommitOutbox.d.ts +1 -1
- package/dist/local/transactions/localMutation.d.ts +1 -1
- package/dist/local/transactions/localMutation.js +1 -1
- package/dist/local/transactions/mutations/MutationQueue.d.ts +2 -2
- package/dist/local/transactions/mutations/MutationQueue.js +4 -4
- package/dist/local/transactions/mutations/batchProcessing.d.ts +1 -1
- package/dist/local/transactions/mutations/commitApi.d.ts +1 -1
- package/dist/local/transactions/mutations/commitLane.d.ts +1 -1
- package/dist/local/transactions/mutations/commitPayload.d.ts +2 -1
- package/dist/local/transactions/mutations/commitPayload.js +8 -0
- package/dist/local/transactions/mutations/commitTransport.d.ts +1 -1
- package/dist/local/transactions/mutations/commitTransport.js +1 -1
- package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
- package/dist/local/transactions/mutations/durableCommitRestore.js +1 -1
- package/dist/local/transactions/mutations/durableWriteStore.d.ts +4 -4
- package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
- package/dist/local/transactions/mutations/localMutation.d.ts +1 -1
- package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
- package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
- package/dist/local/transactions/mutations/pendingDrain.js +1 -1
- package/dist/local/transactions/mutations/replayValidation.d.ts +13 -1
- package/dist/local/transactions/mutations/replayValidation.js +3 -2
- package/dist/local/transactions/reconnectDrain.d.ts +1 -1
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/react/useSyncStatus.js +1 -1
- package/package.json +2 -2
- package/src/Ablo.ts +1 -1
- package/src/client.ts +7 -0
- package/src/humans.ts +1 -1
- package/src/local/RuntimeContext.ts +1 -1
- package/src/local/SyncClient.ts +3 -3
- package/src/local/client/createModelProxy.ts +236 -55
- package/src/local/client/options.ts +1 -1
- package/src/local/client/reactiveEngine.ts +75 -4
- package/src/local/client/resourceTypes.ts +1 -1
- package/src/local/client/storeCluster.ts +12 -2
- package/src/local/client/storeLifecycle.ts +3 -0
- package/src/local/client/wsMutationExecutor.ts +43 -1
- package/src/local/context.ts +5 -1
- package/src/local/interfaces/index.ts +7 -5
- package/src/local/mutators/UndoManager.ts +1 -1
- package/src/local/query/client.ts +17 -0
- package/src/local/query/types.ts +16 -1
- package/src/local/stores/syncAction.ts +1 -1
- package/src/local/sync/ConnectionManager.ts +1 -1
- package/src/local/sync/OnDemandLoader.ts +20 -3
- package/src/local/sync/SyncWebSocket.ts +1 -1
- package/src/local/sync/commitFrames.ts +1 -1
- package/src/local/sync/contextPorts.ts +1 -1
- package/src/local/sync/credentialLifecycle.ts +1 -1
- package/src/local/sync/wsFrameHandlers.ts +1 -1
- package/src/local/transactions/databaseCommitOutbox.ts +1 -1
- package/src/local/transactions/localMutation.ts +1 -1
- package/src/local/transactions/mutations/MutationQueue.ts +4 -4
- package/src/local/transactions/mutations/batchProcessing.ts +1 -1
- package/src/local/transactions/mutations/commitApi.ts +1 -1
- package/src/local/transactions/mutations/commitLane.ts +1 -1
- package/src/local/transactions/mutations/commitPayload.ts +10 -1
- package/src/local/transactions/mutations/commitTransport.ts +1 -1
- package/src/local/transactions/mutations/durableCommitRestore.ts +2 -2
- package/src/local/transactions/mutations/durableWriteStore.ts +4 -4
- package/src/local/transactions/mutations/localMutation.ts +1 -1
- package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
- package/src/local/transactions/mutations/pendingDrain.ts +2 -2
- package/src/local/transactions/mutations/replayValidation.ts +3 -2
- package/src/local/transactions/reconnectDrain.ts +1 -1
- package/src/react/useSyncStatus.ts +1 -1
|
@@ -38,6 +38,10 @@ import { registerModelsFromSchema } from './modelRegistration.js';
|
|
|
38
38
|
import { deriveConfigFromSchema } from './schemaConfig.js';
|
|
39
39
|
import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
|
|
40
40
|
import type { InternalAbloOptions } from './options.js';
|
|
41
|
+
import {
|
|
42
|
+
createReadSetContext,
|
|
43
|
+
type ReadSetContext,
|
|
44
|
+
} from '@abloatai/transaction/internal/read-set';
|
|
41
45
|
export type { InternalAbloOptions } from './options.js';
|
|
42
46
|
|
|
43
47
|
/**
|
|
@@ -55,6 +59,8 @@ export interface StoreCluster {
|
|
|
55
59
|
readonly components: InternalComponents;
|
|
56
60
|
/** The store orchestrating the graph, holding the host-built connection. */
|
|
57
61
|
readonly store: BaseSyncedStore;
|
|
62
|
+
/** Client-local opaque evidence registry; never an ambient execution scope. */
|
|
63
|
+
readonly readSetContext: ReadSetContext;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/**
|
|
@@ -99,6 +105,7 @@ export function buildStoreCluster(
|
|
|
99
105
|
return null;
|
|
100
106
|
}
|
|
101
107
|
const schema = options.schema;
|
|
108
|
+
const readSetContext = createReadSetContext();
|
|
102
109
|
|
|
103
110
|
// Config derives from the schema; caller-supplied overrides layer on top,
|
|
104
111
|
// caller winning per key.
|
|
@@ -112,7 +119,10 @@ export function buildStoreCluster(
|
|
|
112
119
|
// error and the MutationQueue owns the retry. A caller-supplied executor
|
|
113
120
|
// still wins (test mocks, alternative transports).
|
|
114
121
|
const executor: MutationExecutor =
|
|
115
|
-
options.mutationExecutor ?? createDefaultMutationExecutor(
|
|
122
|
+
options.mutationExecutor ?? createDefaultMutationExecutor(
|
|
123
|
+
() => transport,
|
|
124
|
+
readSetContext,
|
|
125
|
+
);
|
|
116
126
|
|
|
117
127
|
// This client's runtime — the instance the whole graph is constructed
|
|
118
128
|
// with, so two clients in one process never read each other's logger,
|
|
@@ -168,5 +178,5 @@ export function buildStoreCluster(
|
|
|
168
178
|
{ collaborationEvents: options.collaborationEvents ?? [] },
|
|
169
179
|
);
|
|
170
180
|
|
|
171
|
-
return { runtime, components, store };
|
|
181
|
+
return { runtime, components, store, readSetContext };
|
|
172
182
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { SchemaRecord, Schema } from '@abloatai/transaction/schema/schema';
|
|
13
13
|
import type { ParticipantKind } from '@abloatai/transaction/types/participant';
|
|
14
|
+
import type { EffectiveAuthority } from '@abloatai/transaction/auth';
|
|
14
15
|
import type { Logger } from '@abloatai/transaction/logger';
|
|
15
16
|
import type { AuthCredentialSource } from '@abloatai/transaction/auth/credentialSource';
|
|
16
17
|
import type { RefreshScheduler } from '@abloatai/transaction/auth';
|
|
@@ -32,6 +33,7 @@ export interface IdentitySeed {
|
|
|
32
33
|
/** The resolved account scope; null until known. */
|
|
33
34
|
readonly accountScope: string | null;
|
|
34
35
|
readonly syncGroups: readonly string[];
|
|
36
|
+
readonly authority: EffectiveAuthority;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface StoreLifecycle {
|
|
@@ -282,6 +284,7 @@ export function startStoreLifecycle<S extends SchemaRecord>(
|
|
|
282
284
|
participantKind,
|
|
283
285
|
accountScope,
|
|
284
286
|
syncGroups: resolvedSyncGroups,
|
|
287
|
+
authority: resolved.authority,
|
|
285
288
|
});
|
|
286
289
|
|
|
287
290
|
if (resolved.refreshScheduler) {
|
|
@@ -12,6 +12,12 @@ import type {
|
|
|
12
12
|
MutationOptions,
|
|
13
13
|
} from '../interfaces/index.js';
|
|
14
14
|
import type { CommitAck } from '../sync/commitFrames.js';
|
|
15
|
+
import type { CommitReceiptWire } from '@abloatai/transaction/wire/commit';
|
|
16
|
+
import {
|
|
17
|
+
recordWebSocketCommitReceipt,
|
|
18
|
+
type ReadSetContext,
|
|
19
|
+
} from '@abloatai/transaction/internal/read-set';
|
|
20
|
+
import { commitAckSchema } from '@abloatai/transaction/wire/commit';
|
|
15
21
|
import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
16
22
|
|
|
17
23
|
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
@@ -33,6 +39,13 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
33
39
|
*/
|
|
34
40
|
export function createDefaultMutationExecutor(
|
|
35
41
|
getWs: () => {
|
|
42
|
+
sendCommitReceipt?: (
|
|
43
|
+
operations: readonly MutationOperation[],
|
|
44
|
+
clientTxId: string,
|
|
45
|
+
timeoutMs?: number,
|
|
46
|
+
reads?: readonly ReadDependency[] | null,
|
|
47
|
+
track?: readonly TrackDependency[] | null,
|
|
48
|
+
) => Promise<CommitReceiptWire>;
|
|
36
49
|
sendCommit?: (
|
|
37
50
|
operations: readonly MutationOperation[],
|
|
38
51
|
clientTxId: string,
|
|
@@ -41,13 +54,14 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
41
54
|
track?: readonly TrackDependency[] | null,
|
|
42
55
|
) => Promise<CommitAck>;
|
|
43
56
|
} | null,
|
|
57
|
+
readSetContext?: ReadSetContext,
|
|
44
58
|
): MutationExecutor {
|
|
45
59
|
async function commit(
|
|
46
60
|
operations: MutationOperation[],
|
|
47
61
|
options?: MutationOptions,
|
|
48
62
|
) {
|
|
49
63
|
const ws = getWs();
|
|
50
|
-
if (!ws
|
|
64
|
+
if (!ws || (!ws.sendCommit && !ws.sendCommitReceipt)) {
|
|
51
65
|
throw new AbloConnectionError(
|
|
52
66
|
'SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
|
|
53
67
|
'before mutations can be sent.',
|
|
@@ -60,6 +74,34 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
60
74
|
? crypto.randomUUID()
|
|
61
75
|
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
|
|
62
76
|
try {
|
|
77
|
+
if (ws.sendCommitReceipt) {
|
|
78
|
+
const receipt = await ws.sendCommitReceipt(
|
|
79
|
+
operations,
|
|
80
|
+
clientTxId,
|
|
81
|
+
undefined,
|
|
82
|
+
options?.reads,
|
|
83
|
+
options?.track,
|
|
84
|
+
);
|
|
85
|
+
recordWebSocketCommitReceipt(readSetContext, {
|
|
86
|
+
receipt,
|
|
87
|
+
operations,
|
|
88
|
+
reads: options?.reads,
|
|
89
|
+
track: options?.track,
|
|
90
|
+
});
|
|
91
|
+
return commitAckSchema.parse({
|
|
92
|
+
status: receipt.status,
|
|
93
|
+
statusAt: receipt.statusAt,
|
|
94
|
+
lastSyncId: receipt.lastSyncId,
|
|
95
|
+
...(receipt.correlationId ? { correlationId: receipt.correlationId } : {}),
|
|
96
|
+
...(receipt.notifications ? { notifications: receipt.notifications } : {}),
|
|
97
|
+
...(receipt.missingIds ? { missingIds: receipt.missingIds } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!ws.sendCommit) {
|
|
101
|
+
throw new AbloConnectionError('SyncWebSocket commit transport is unavailable.', {
|
|
102
|
+
code: 'ws_not_ready',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
63
105
|
return await ws.sendCommit(
|
|
64
106
|
operations,
|
|
65
107
|
clientTxId,
|
package/src/local/context.ts
CHANGED
|
@@ -86,7 +86,11 @@ const _fallback: RuntimeContext = {
|
|
|
86
86
|
config: emptyConfig,
|
|
87
87
|
getModelMetadata: () => undefined,
|
|
88
88
|
mutationExecutor: {
|
|
89
|
-
commit: () => Promise.resolve({
|
|
89
|
+
commit: () => Promise.resolve({
|
|
90
|
+
status: 'confirmed' as const,
|
|
91
|
+
statusAt: '1970-01-01T00:00:00.000Z',
|
|
92
|
+
lastSyncId: 0,
|
|
93
|
+
}),
|
|
90
94
|
executeCreate: () => Promise.resolve(),
|
|
91
95
|
executeUpdate: () => Promise.resolve(null),
|
|
92
96
|
executeDelete: () => Promise.resolve(),
|
|
@@ -18,7 +18,7 @@ export type { CoordinationObservability } from '@abloatai/transaction/observabil
|
|
|
18
18
|
// ─────────────────────────────────────────────
|
|
19
19
|
|
|
20
20
|
// The logging port carries no framework and no local state, so it lives in the
|
|
21
|
-
//
|
|
21
|
+
// confirmation core (ADR 0016). Re-exported here so `interfaces` stays the single
|
|
22
22
|
// place a consumer looks for the contracts it implements. The `import type` is
|
|
23
23
|
// load-bearing: `SyncLogger = Logger` below needs the name bound in this module.
|
|
24
24
|
export type { Logger } from '@abloatai/transaction/logger';
|
|
@@ -29,7 +29,7 @@ import type { Logger } from '@abloatai/transaction/logger';
|
|
|
29
29
|
// ─────────────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
// The transport-facing slice — breadcrumbs and socket errors — moved to the
|
|
32
|
-
//
|
|
32
|
+
// confirmation core with the duplex transport (ADR 0016): a socket held for
|
|
33
33
|
// claim push must report its lifecycle with no store present. Re-exported
|
|
34
34
|
// here so `interfaces` stays the single place a consumer looks; the `import
|
|
35
35
|
// type` is load-bearing for `ObservabilityProvider extends` below.
|
|
@@ -143,7 +143,7 @@ export interface ObservabilityProvider
|
|
|
143
143
|
captureSelfHealing(details: SelfHealingDetails): void;
|
|
144
144
|
|
|
145
145
|
// `captureClaim` and `captureConflict` are inherited from
|
|
146
|
-
// `CoordinationObservability` in the core — the
|
|
146
|
+
// `CoordinationObservability` in the core — the confirmation layer reports those
|
|
147
147
|
// two on its own behalf, with no store present.
|
|
148
148
|
|
|
149
149
|
/** Capture commit returning lastSyncId: 0 */
|
|
@@ -225,7 +225,7 @@ export type MutationCommitResult = MutationCommitResultInput;
|
|
|
225
225
|
// `MutationOptions` describes how a write is issued — request identity, commit
|
|
226
226
|
// disposition, and the premise it rests on (optimistic via `readAt`/`reads`, or
|
|
227
227
|
// claim-protected via `claimRef`/`fenceToken`) — and holds no local row state,
|
|
228
|
-
// so it lives in the
|
|
228
|
+
// so it lives in the confirmation core (ADR 0016). Re-exported here so the
|
|
229
229
|
// existing `interfaces` import path keeps resolving.
|
|
230
230
|
// The `import type` is load-bearing, not redundant: `export type { X } from`
|
|
231
231
|
// re-exports without binding X in this module's scope, and `Pick<X, K>` on an
|
|
@@ -243,7 +243,7 @@ import type { MutationOptions } from '@abloatai/transaction/resources/mutationOp
|
|
|
243
243
|
*/
|
|
244
244
|
export type WriteOptions = Pick<
|
|
245
245
|
MutationOptions,
|
|
246
|
-
'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken'
|
|
246
|
+
'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'
|
|
247
247
|
>;
|
|
248
248
|
|
|
249
249
|
/** A single mutation within a batch. Its `options` travel with it so the server
|
|
@@ -265,6 +265,8 @@ export interface MutationOperation {
|
|
|
265
265
|
* the wire at once.
|
|
266
266
|
*/
|
|
267
267
|
transactionId?: string;
|
|
268
|
+
/** Server-issued claim identity the operation is attributed to. */
|
|
269
|
+
claimId?: string | null;
|
|
268
270
|
readAt?: number | null;
|
|
269
271
|
onStale?: OnStaleMode | null;
|
|
270
272
|
/**
|
|
@@ -365,7 +365,7 @@ export class UndoScope<S extends Schema> {
|
|
|
365
365
|
* Run `work` after every previously-enqueued scope operation has settled,
|
|
366
366
|
* in invocation order. The internal `tail` always resolves (failures are
|
|
367
367
|
* swallowed *for the chain only*) so one rejected mutator can't wedge the
|
|
368
|
-
* queue; the original
|
|
368
|
+
* queue; the original confirmation is still surfaced to this call's caller.
|
|
369
369
|
*/
|
|
370
370
|
private enqueue<T>(work: () => Promise<T>): Promise<T> {
|
|
371
371
|
const result = this.tail.then(work, work);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { modelListEvidenceSchema } from '@abloatai/transaction/wire';
|
|
16
17
|
import type { QueryBatch, QueryBatchResult } from './types.js';
|
|
17
18
|
import { translateHttpError } from '@abloatai/transaction/errors';
|
|
18
19
|
import { classifyRecovery, type RecoveryClass } from '@abloatai/transaction/errorCodes';
|
|
@@ -37,6 +38,16 @@ const QueryResultSchema = z
|
|
|
37
38
|
const QueryBatchResultSchema = z
|
|
38
39
|
.object({
|
|
39
40
|
results: z.array(QueryResultSchema),
|
|
41
|
+
evidence: z.array(z.array(modelListEvidenceSchema)).optional(),
|
|
42
|
+
errors: z.array(z.object({
|
|
43
|
+
index: z.number().int().nonnegative(),
|
|
44
|
+
model: z.string(),
|
|
45
|
+
type: z.string(),
|
|
46
|
+
code: z.string(),
|
|
47
|
+
message: z.string(),
|
|
48
|
+
request_id: z.string().optional(),
|
|
49
|
+
event_id: z.string(),
|
|
50
|
+
})).optional(),
|
|
40
51
|
})
|
|
41
52
|
.loose();
|
|
42
53
|
|
|
@@ -189,6 +200,12 @@ export async function postQuery(
|
|
|
189
200
|
});
|
|
190
201
|
return { results: batch.queries.map(() => []) };
|
|
191
202
|
}
|
|
203
|
+
for (const failure of parsed.data.errors ?? []) {
|
|
204
|
+
runtime.logger.warn(
|
|
205
|
+
`Could not load ${failure.model} — ${failure.message} (code: ${failure.code}, ref: ${failure.request_id ?? failure.event_id}). No results were returned for slot ${failure.index}.`,
|
|
206
|
+
);
|
|
207
|
+
runtime.logger.debug('[postQuery.error] query slot failure', failure);
|
|
208
|
+
}
|
|
192
209
|
return parsed.data;
|
|
193
210
|
} finally {
|
|
194
211
|
clearTimeout(timer);
|
package/src/local/query/types.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
// The where grammar describes the *request*, not any local copy of the rows it
|
|
18
|
-
// returns, so it lives in the
|
|
18
|
+
// returns, so it lives in the confirmation core (ADR 0016). Re-exported here so
|
|
19
19
|
// the existing `query/types` import path keeps resolving.
|
|
20
20
|
export type {
|
|
21
21
|
WherePrimitive,
|
|
@@ -24,6 +24,7 @@ export type {
|
|
|
24
24
|
LoadWhere,
|
|
25
25
|
} from '@abloatai/transaction/resources/where';
|
|
26
26
|
import type { WhereClause } from '@abloatai/transaction/resources/where';
|
|
27
|
+
import type { ModelListEvidence } from '@abloatai/transaction/wire';
|
|
27
28
|
|
|
28
29
|
/** A single structured fetch request. */
|
|
29
30
|
export interface Query {
|
|
@@ -93,6 +94,18 @@ export interface QueryBatchResult {
|
|
|
93
94
|
* before use.
|
|
94
95
|
*/
|
|
95
96
|
results: unknown[];
|
|
97
|
+
/** Per-result-slot evidence for the primary rows in that slot. */
|
|
98
|
+
evidence?: readonly (readonly QueryReadEvidence[])[];
|
|
99
|
+
/** Per-slot failures that previously looked identical to an empty result. */
|
|
100
|
+
errors?: readonly {
|
|
101
|
+
index: number;
|
|
102
|
+
model: string;
|
|
103
|
+
type: string;
|
|
104
|
+
code: string;
|
|
105
|
+
message: string;
|
|
106
|
+
request_id?: string;
|
|
107
|
+
event_id: string;
|
|
108
|
+
}[];
|
|
96
109
|
/**
|
|
97
110
|
* The server watermark observed after the batch ran. Model reads expose this
|
|
98
111
|
* as `stamp`, and callers thread it into `commits.create({ readAt })` so the
|
|
@@ -100,3 +113,5 @@ export interface QueryBatchResult {
|
|
|
100
113
|
*/
|
|
101
114
|
lastSyncId?: number;
|
|
102
115
|
}
|
|
116
|
+
|
|
117
|
+
export type QueryReadEvidence = ModelListEvidence;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The persisted local action record consumed by `SyncActionStore`. It is a
|
|
3
3
|
* client materialization artifact, not a transaction-layer wire shape: the
|
|
4
|
-
* authoritative broadcast vocabulary is the
|
|
4
|
+
* authoritative broadcast vocabulary is the confirmation core's delta wire
|
|
5
5
|
* schema, while this one adds the local `__class` discriminator IndexedDB uses.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Moved to the
|
|
2
|
+
* Moved to the confirmation core with the duplex transport (ADR 0016): the
|
|
3
3
|
* connection state machine is plain, mobx-free plumbing an agent needs as much
|
|
4
4
|
* as a browser does. This path re-exports it so existing importers stay
|
|
5
5
|
* unchanged; the reactive store mirrors its transitions through
|
|
@@ -121,6 +121,7 @@ function snapshotDoesNotAdvanceModel(data: Record<string, unknown>, model: Model
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export class OnDemandLoader {
|
|
124
|
+
private readonly readEvidence = new WeakMap<object, number>();
|
|
124
125
|
private readonly inFlight = new Map<string, Promise<Model[]>>();
|
|
125
126
|
/**
|
|
126
127
|
* Query keys with a background confirm currently in flight. Distinct from
|
|
@@ -224,6 +225,11 @@ export class OnDemandLoader {
|
|
|
224
225
|
return work;
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
/** Exact evidence retained for a row returned by a server-confirmed fetch. */
|
|
229
|
+
getReadEvidence(row: object): number | undefined {
|
|
230
|
+
return this.readEvidence.get(row);
|
|
231
|
+
}
|
|
232
|
+
|
|
227
233
|
private async runFetch(
|
|
228
234
|
modelName: string,
|
|
229
235
|
typename: string,
|
|
@@ -337,7 +343,8 @@ export class OnDemandLoader {
|
|
|
337
343
|
clauses: readonly WhereClause[],
|
|
338
344
|
options: FetchOptions<unknown> | undefined,
|
|
339
345
|
): Promise<Model[]> {
|
|
340
|
-
const
|
|
346
|
+
const network = await this.queryNetwork(modelName, clauses, options);
|
|
347
|
+
const networkRows = network.rows;
|
|
341
348
|
const networkModels = networkRows
|
|
342
349
|
// Strict: a row the server returned whose type name this client never
|
|
343
350
|
// registered is a genuine schema collision (the pushed schema differs
|
|
@@ -346,6 +353,12 @@ export class OnDemandLoader {
|
|
|
346
353
|
.map((raw) => this.hydrateOne(raw, typename, { strict: true }))
|
|
347
354
|
.filter((m): m is Model => m !== null);
|
|
348
355
|
|
|
356
|
+
const evidenceById = new Map(network.evidence.map((entry) => [entry.id, entry.stamp]));
|
|
357
|
+
for (const model of networkModels) {
|
|
358
|
+
const stamp = evidenceById.get(model.id);
|
|
359
|
+
if (stamp !== undefined) this.readEvidence.set(model, stamp);
|
|
360
|
+
}
|
|
361
|
+
|
|
349
362
|
if (networkModels.length > 0) {
|
|
350
363
|
this.opts.objectPool.addBatch(networkModels, ModelScope.live);
|
|
351
364
|
// Background IDB write — don't block the caller. Expanded children are
|
|
@@ -558,7 +571,10 @@ export class OnDemandLoader {
|
|
|
558
571
|
modelName: string,
|
|
559
572
|
clauses: readonly WhereClause[],
|
|
560
573
|
options: FetchOptions<unknown> | undefined,
|
|
561
|
-
): Promise<
|
|
574
|
+
): Promise<{
|
|
575
|
+
rows: unknown[];
|
|
576
|
+
evidence: readonly { id: string; stamp: number }[];
|
|
577
|
+
}> {
|
|
562
578
|
const typename = this.resolveTypename(modelName);
|
|
563
579
|
const orderEntries = options?.orderBy ? Object.entries(options.orderBy) : [];
|
|
564
580
|
const firstOrder = orderEntries[0];
|
|
@@ -586,6 +602,7 @@ export class OnDemandLoader {
|
|
|
586
602
|
{ queries: [query] },
|
|
587
603
|
);
|
|
588
604
|
const rows: unknown[] = Array.isArray(result.results[0]) ? result.results[0] : [];
|
|
605
|
+
const evidence = result.evidence?.[0] ?? [];
|
|
589
606
|
// Normalize: wire rows lack `__typename` when the server elides it.
|
|
590
607
|
const normalized = rows.map((row) => {
|
|
591
608
|
if (row && typeof row === 'object' && !('__typename' in row)) {
|
|
@@ -605,7 +622,7 @@ export class OnDemandLoader {
|
|
|
605
622
|
if (options?.expand && options.expand.length > 0) {
|
|
606
623
|
this.hydrateExpanded(modelName, normalized, options.expand);
|
|
607
624
|
}
|
|
608
|
-
return normalized;
|
|
625
|
+
return { rows: normalized, evidence };
|
|
609
626
|
}
|
|
610
627
|
|
|
611
628
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The reactive engine's connection to the sync server: the
|
|
2
|
+
* The reactive engine's connection to the sync server: the confirmation core's
|
|
3
3
|
* duplex transport ({@link WsTransport}, ADR 0016) plus everything that turns
|
|
4
4
|
* the stream into a local, watchable copy — wire-delta validation, the resume
|
|
5
5
|
* cursor and its persistence-gated ack discipline, incremental sync and
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Moved to the
|
|
2
|
+
* Moved to the confirmation core with the duplex transport (ADR 0016): the
|
|
3
3
|
* commit-path frame builders are stateless wire helpers. This path re-exports
|
|
4
4
|
* them so existing importers stay unchanged. The core's `buildCommitFrame`
|
|
5
5
|
* takes the structural `CommitFrameOperation` slice, which this package's
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime context, sliced into the ports the
|
|
2
|
+
* The runtime context, sliced into the ports the confirmation core's transport
|
|
3
3
|
* takes. The core is injected, never service-located (ADR 0016), while this
|
|
4
4
|
* package resolves its shared dependencies through `getContext()` — so these
|
|
5
5
|
* adapters bridge the two: each method reads the context at call time, which
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Moved to the
|
|
2
|
+
* Moved to the confirmation core with the duplex transport (ADR 0016): keeping
|
|
3
3
|
* a long-lived socket's credential fresh is connection plumbing an agent needs
|
|
4
4
|
* as much as a browser does. This path re-exports it so existing importers
|
|
5
5
|
* stay unchanged.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Moved to the
|
|
2
|
+
* Moved to the confirmation core with the duplex transport (ADR 0016): the
|
|
3
3
|
* inbound frame dispatch is the receiving half of the wire protocol, and the
|
|
4
4
|
* handlers already worked against the minimal `WsSession` port rather than
|
|
5
5
|
* the transport object. This path re-exports it so existing importers stay
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DurableWriteStore,
|
|
3
3
|
} from '@abloatai/transaction/durableWrites';
|
|
4
|
-
import type { PendingWrite } from '@abloatai/transaction/transactions/
|
|
4
|
+
import type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
|
|
5
5
|
|
|
6
6
|
export interface CommitOutboxDatabase {
|
|
7
7
|
sealTransactionRecord(
|
|
@@ -10,7 +10,7 @@ type EmitLocalMutation = (event: string, payload: unknown) => void;
|
|
|
10
10
|
/**
|
|
11
11
|
* Human-side optimistic-write adapter.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* Confirmation retains ordering and receipts; this adapter owns the local
|
|
14
14
|
* pre-image ledger and announces apply/rollback work to the materialiser.
|
|
15
15
|
*/
|
|
16
16
|
export function createLocalMutationPort(
|
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
type DurableCommitOperation,
|
|
88
88
|
type DurableCommitOperationInput,
|
|
89
89
|
type CommitOutboxScope,
|
|
90
|
-
} from '@abloatai/transaction/transactions/
|
|
90
|
+
} from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
91
91
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
92
92
|
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
93
93
|
import {
|
|
@@ -126,7 +126,7 @@ import { handleConflict as resolveConflict, isPermanentError as classifyPermanen
|
|
|
126
126
|
import { takeNextExecutionBatch as selectExecutionBatch, takePendingDrainBatch as selectPendingDrainBatch } from './executionSelection.js';
|
|
127
127
|
import { scheduleProcessing as scheduleProcessingExternal, type ProcessingSchedulerContext } from './processingScheduler.js';
|
|
128
128
|
import {
|
|
129
|
-
|
|
129
|
+
drainPendingConfirmations,
|
|
130
130
|
type PendingDrainContext,
|
|
131
131
|
} from './pendingDrain.js';
|
|
132
132
|
import { restoreDurableCommits as restoreDurableCommitsExternal, type DurableCommitRestoreContext } from './durableCommitRestore.js';
|
|
@@ -286,7 +286,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
286
286
|
/**
|
|
287
287
|
* Small race buffer for authoritative echoes that arrive before the queued
|
|
288
288
|
* mutation receipt. The forward and WAL stream are independent channels, so
|
|
289
|
-
* either can win without changing the
|
|
289
|
+
* either can win without changing the confirmation result.
|
|
290
290
|
*/
|
|
291
291
|
private recentDeltaCorrelations = new Map<string, number>();
|
|
292
292
|
/**
|
|
@@ -1472,7 +1472,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1472
1472
|
if (this.isProcessing) return;
|
|
1473
1473
|
this.isProcessing = true;
|
|
1474
1474
|
try {
|
|
1475
|
-
await
|
|
1475
|
+
await drainPendingConfirmations(this.pendingDrainContext);
|
|
1476
1476
|
} finally {
|
|
1477
1477
|
this.isProcessing = false;
|
|
1478
1478
|
if (this.executionQueue.length > 0) this.scheduleProcessing(true);
|
|
@@ -6,7 +6,7 @@ import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
|
6
6
|
import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
|
|
7
7
|
import type { DeltaConfirmationTracker } from './deltaConfirmation.js';
|
|
8
8
|
import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
9
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/
|
|
9
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
10
10
|
import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
11
11
|
import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
|
|
12
12
|
import type { MutationOperationType } from '@abloatai/transaction/types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AbloIdempotencyError } from '@abloatai/transaction/errors';
|
|
2
2
|
import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
|
|
3
3
|
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
4
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/
|
|
4
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
5
5
|
import type { CommitTransaction } from './commitLane.js';
|
|
6
6
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
7
7
|
|
|
@@ -4,7 +4,7 @@ import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
|
4
4
|
import type {
|
|
5
5
|
DurableCommitEnvelope,
|
|
6
6
|
DurableCommitOperation,
|
|
7
|
-
} from '@abloatai/transaction/transactions/
|
|
7
|
+
} from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
8
8
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
9
9
|
import { transientRetryDelayMs } from './failureHandling.js';
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ import { MutationOperationType } from '@abloatai/transaction/types';
|
|
|
15
15
|
import { snapshotJsonValue } from '@abloatai/transaction/utils/json';
|
|
16
16
|
import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
|
|
17
17
|
import type { MutationOptions, WriteOptions } from '../../interfaces/index.js';
|
|
18
|
-
import type { CommitEnvelopeMember } from '@abloatai/transaction/transactions/
|
|
18
|
+
import type { CommitEnvelopeMember } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
19
19
|
|
|
20
20
|
export interface UserContext {
|
|
21
21
|
userId: string;
|
|
@@ -208,6 +208,8 @@ export function hasStaleWriteOptions(options?: WriteOptions): boolean {
|
|
|
208
208
|
export function hasCommitCoalescingBarrier(options?: WriteOptions): boolean {
|
|
209
209
|
return (
|
|
210
210
|
hasStaleWriteOptions(options) ||
|
|
211
|
+
options?.claimRef != null ||
|
|
212
|
+
options?.fenceToken != null ||
|
|
211
213
|
typeof options?.idempotencyKey === 'string' ||
|
|
212
214
|
typeof options?.label === 'string'
|
|
213
215
|
);
|
|
@@ -217,6 +219,7 @@ export interface WriteOperationFields {
|
|
|
217
219
|
readAt?: number | null;
|
|
218
220
|
onStale?: OnStaleMode | null;
|
|
219
221
|
fenceToken?: number | null;
|
|
222
|
+
claimId?: string;
|
|
220
223
|
options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
|
|
221
224
|
}
|
|
222
225
|
|
|
@@ -243,6 +246,12 @@ export function applyWriteOptions<T extends object>(
|
|
|
243
246
|
if (writeOptions.fenceToken !== undefined) {
|
|
244
247
|
operation.fenceToken = writeOptions.fenceToken;
|
|
245
248
|
}
|
|
249
|
+
if (writeOptions.claimRef != null) {
|
|
250
|
+
operation.claimId =
|
|
251
|
+
typeof writeOptions.claimRef === 'string'
|
|
252
|
+
? writeOptions.claimRef
|
|
253
|
+
: writeOptions.claimRef.id;
|
|
254
|
+
}
|
|
246
255
|
if (writeOptions.idempotencyKey != null || writeOptions.label !== undefined) {
|
|
247
256
|
operation.options = {
|
|
248
257
|
...(writeOptions.idempotencyKey != null
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type CommitOutboxScope,
|
|
18
18
|
type DurableCommitEnvelope,
|
|
19
19
|
type DurableCommitOperation,
|
|
20
|
-
} from '@abloatai/transaction/transactions/
|
|
20
|
+
} from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
21
21
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
22
22
|
|
|
23
23
|
export interface CommitTransportContext {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AbloIdempotencyError, AbloValidationError } from '@abloatai/transaction/errors';
|
|
2
2
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
3
3
|
import type { CommitTransaction } from './commitLane.js';
|
|
4
|
-
import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/transactions/
|
|
4
|
+
import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
5
5
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
6
|
-
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/
|
|
6
|
+
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
7
7
|
|
|
8
8
|
export interface DurableCommitRestoreContext {
|
|
9
9
|
readonly config: { enablePersistence: boolean };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The durable-write port moved to the
|
|
2
|
+
* The durable-write port moved to the confirmation core (ADR 0016): it is a
|
|
3
3
|
* contract over commit envelopes and holds no local rows. Re-exported here so
|
|
4
4
|
* the existing `transactions/mutations/durableWriteStore` import path keeps
|
|
5
5
|
* resolving for the queue, the outbox, and the client options.
|
|
6
6
|
*
|
|
7
7
|
* The port and its config live in the core's `durableWrites` module (a behavior
|
|
8
8
|
* contract, not a persisted shape); the records that cross it are owned by
|
|
9
|
-
* `transactions/
|
|
9
|
+
* `transactions/confirmation/pendingWrite`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export {
|
|
@@ -17,5 +17,5 @@ export type {
|
|
|
17
17
|
DurableWriteStore,
|
|
18
18
|
DurableWritesConfig,
|
|
19
19
|
} from '@abloatai/transaction/durableWrites';
|
|
20
|
-
export { pendingWriteSchema } from '@abloatai/transaction/transactions/
|
|
21
|
-
export type { PendingWrite } from '@abloatai/transaction/transactions/
|
|
20
|
+
export { pendingWriteSchema } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
|
|
21
|
+
export type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
|
|
@@ -30,7 +30,7 @@ export interface OptimisticEmitter {
|
|
|
30
30
|
emit(event: string, payload: unknown): void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Materializer capability consumed by
|
|
33
|
+
/** Materializer capability consumed by confirmation; implementation belongs to the local layer. */
|
|
34
34
|
export interface LocalMutationPort {
|
|
35
35
|
readonly updates: Map<string, OptimisticUpdateEntry>;
|
|
36
36
|
applyCreate(model: Model, transaction: QueuedMutation): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
2
|
import type { MutationPersistencePort } from '../../mutationPersistence.js';
|
|
3
3
|
import type { MutationQueueConfig } from './MutationQueue.js';
|
|
4
|
-
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/
|
|
4
|
+
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
5
5
|
import type { QueuedMutation } from './commitPayload.js';
|
|
6
6
|
import { MutationStore } from './MutationStore.js';
|
|
7
7
|
import { normalizeModelKey } from './commitPayload.js';
|
|
@@ -3,7 +3,7 @@ import type { QueuedMutation } from './commitPayload.js';
|
|
|
3
3
|
import type { MutationStore } from './MutationStore.js';
|
|
4
4
|
import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
5
5
|
import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
6
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/
|
|
6
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
7
7
|
import { applyWriteOptions, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
8
8
|
|
|
9
9
|
export interface PendingDrainContext {
|
|
@@ -31,7 +31,7 @@ export interface PendingDrainContext {
|
|
|
31
31
|
readonly emit: (event: string, payload: object) => boolean;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export async function
|
|
34
|
+
export async function drainPendingConfirmations(ctx: PendingDrainContext): Promise<void> {
|
|
35
35
|
ctx.assertDurableReplayOpen();
|
|
36
36
|
// Kick the commit lane too: atomic envelopes from `commits.create()` may
|
|
37
37
|
// have been left at the head of the lane while the connection was down.
|