@abloatai/humans 0.48.0 → 0.50.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 +69 -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 +81 -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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { SchemaRecord, Schema } from '@abloatai/transaction/schema/schema';
|
|
12
12
|
import type { ParticipantKind } from '@abloatai/transaction/types/participant';
|
|
13
|
+
import type { EffectiveAuthority } from '@abloatai/transaction/auth';
|
|
13
14
|
import type { Logger } from '@abloatai/transaction/logger';
|
|
14
15
|
import type { AuthCredentialSource } from '@abloatai/transaction/auth/credentialSource';
|
|
15
16
|
import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
|
|
@@ -23,6 +24,7 @@ export interface IdentitySeed {
|
|
|
23
24
|
/** The resolved account scope; null until known. */
|
|
24
25
|
readonly accountScope: string | null;
|
|
25
26
|
readonly syncGroups: readonly string[];
|
|
27
|
+
readonly authority: EffectiveAuthority;
|
|
26
28
|
}
|
|
27
29
|
export interface StoreLifecycle {
|
|
28
30
|
/**
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
|
|
8
8
|
import type { MutationExecutor, MutationOperation } from '../interfaces/index.js';
|
|
9
9
|
import type { CommitAck } from '../sync/commitFrames.js';
|
|
10
|
+
import type { CommitReceiptWire } from '@abloatai/transaction/wire/commit';
|
|
11
|
+
import { type ReadSetContext } from '@abloatai/transaction/internal/read-set';
|
|
10
12
|
/**
|
|
11
13
|
* Creates the default mutation executor, which sends each change as a `commit`
|
|
12
14
|
* frame over the sync engine's own WebSocket. The engine owns its socket, so
|
|
@@ -23,5 +25,6 @@ import type { CommitAck } from '../sync/commitFrames.js';
|
|
|
23
25
|
* generation below exists only for direct, one-shot executor consumers.
|
|
24
26
|
*/
|
|
25
27
|
export declare function createDefaultMutationExecutor(getWs: () => {
|
|
28
|
+
sendCommitReceipt?: (operations: readonly MutationOperation[], clientTxId: string, timeoutMs?: number, reads?: readonly ReadDependency[] | null, track?: readonly TrackDependency[] | null) => Promise<CommitReceiptWire>;
|
|
26
29
|
sendCommit?: (operations: readonly MutationOperation[], clientTxId: string, timeoutMs?: number, reads?: readonly ReadDependency[] | null, track?: readonly TrackDependency[] | null) => Promise<CommitAck>;
|
|
27
|
-
} | null): MutationExecutor;
|
|
30
|
+
} | null, readSetContext?: ReadSetContext): MutationExecutor;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* commit time because it does not exist yet when the executor is created. A
|
|
5
5
|
* client wires this up automatically unless you supply your own executor.
|
|
6
6
|
*/
|
|
7
|
+
import { recordWebSocketCommitReceipt, } from '@abloatai/transaction/internal/read-set';
|
|
8
|
+
import { commitAckSchema } from '@abloatai/transaction/wire/commit';
|
|
7
9
|
import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
8
10
|
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
9
11
|
/**
|
|
@@ -21,10 +23,10 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
21
23
|
* this key before its first attempt and owns reuse across retries. The fallback
|
|
22
24
|
* generation below exists only for direct, one-shot executor consumers.
|
|
23
25
|
*/
|
|
24
|
-
export function createDefaultMutationExecutor(getWs) {
|
|
26
|
+
export function createDefaultMutationExecutor(getWs, readSetContext) {
|
|
25
27
|
async function commit(operations, options) {
|
|
26
28
|
const ws = getWs();
|
|
27
|
-
if (!ws
|
|
29
|
+
if (!ws || (!ws.sendCommit && !ws.sendCommitReceipt)) {
|
|
28
30
|
throw new AbloConnectionError('SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
|
|
29
31
|
'before mutations can be sent.', { code: 'ws_not_ready' });
|
|
30
32
|
}
|
|
@@ -33,6 +35,28 @@ export function createDefaultMutationExecutor(getWs) {
|
|
|
33
35
|
? crypto.randomUUID()
|
|
34
36
|
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
|
|
35
37
|
try {
|
|
38
|
+
if (ws.sendCommitReceipt) {
|
|
39
|
+
const receipt = await ws.sendCommitReceipt(operations, clientTxId, undefined, options?.reads, options?.track);
|
|
40
|
+
recordWebSocketCommitReceipt(readSetContext, {
|
|
41
|
+
receipt,
|
|
42
|
+
operations,
|
|
43
|
+
reads: options?.reads,
|
|
44
|
+
track: options?.track,
|
|
45
|
+
});
|
|
46
|
+
return commitAckSchema.parse({
|
|
47
|
+
status: receipt.status,
|
|
48
|
+
statusAt: receipt.statusAt,
|
|
49
|
+
lastSyncId: receipt.lastSyncId,
|
|
50
|
+
...(receipt.correlationId ? { correlationId: receipt.correlationId } : {}),
|
|
51
|
+
...(receipt.notifications ? { notifications: receipt.notifications } : {}),
|
|
52
|
+
...(receipt.missingIds ? { missingIds: receipt.missingIds } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!ws.sendCommit) {
|
|
56
|
+
throw new AbloConnectionError('SyncWebSocket commit transport is unavailable.', {
|
|
57
|
+
code: 'ws_not_ready',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
36
60
|
return await ws.sendCommit(operations, clientTxId, undefined, // use sendCommit's built-in 15s default; no per-call override
|
|
37
61
|
options?.reads, options?.track);
|
|
38
62
|
}
|
package/dist/local/context.js
CHANGED
|
@@ -71,7 +71,11 @@ const _fallback = {
|
|
|
71
71
|
config: emptyConfig,
|
|
72
72
|
getModelMetadata: () => undefined,
|
|
73
73
|
mutationExecutor: {
|
|
74
|
-
commit: () => Promise.resolve({
|
|
74
|
+
commit: () => Promise.resolve({
|
|
75
|
+
status: 'confirmed',
|
|
76
|
+
statusAt: '1970-01-01T00:00:00.000Z',
|
|
77
|
+
lastSyncId: 0,
|
|
78
|
+
}),
|
|
75
79
|
executeCreate: () => Promise.resolve(),
|
|
76
80
|
executeUpdate: () => Promise.resolve(null),
|
|
77
81
|
executeDelete: () => Promise.resolve(),
|
|
@@ -142,7 +142,7 @@ import type { MutationOptions } from '@abloatai/transaction/resources/mutationOp
|
|
|
142
142
|
* `claim` are deliberately absent: both are resolved on the client before a write
|
|
143
143
|
* is staged, so neither reaches this layer.
|
|
144
144
|
*/
|
|
145
|
-
export type WriteOptions = Pick<MutationOptions, 'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken'>;
|
|
145
|
+
export type WriteOptions = Pick<MutationOptions, 'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'>;
|
|
146
146
|
/** A single mutation within a batch. Its `options` travel with it so the server
|
|
147
147
|
* can cache and replay the operation for idempotent retries. */
|
|
148
148
|
export interface MutationOperation {
|
|
@@ -162,6 +162,8 @@ export interface MutationOperation {
|
|
|
162
162
|
* the wire at once.
|
|
163
163
|
*/
|
|
164
164
|
transactionId?: string;
|
|
165
|
+
/** Server-issued claim identity the operation is attributed to. */
|
|
166
|
+
claimId?: string | null;
|
|
165
167
|
readAt?: number | null;
|
|
166
168
|
onStale?: OnStaleMode | null;
|
|
167
169
|
/**
|
|
@@ -168,7 +168,7 @@ export declare class UndoScope<S extends Schema> {
|
|
|
168
168
|
* Run `work` after every previously-enqueued scope operation has settled,
|
|
169
169
|
* in invocation order. The internal `tail` always resolves (failures are
|
|
170
170
|
* swallowed *for the chain only*) so one rejected mutator can't wedge the
|
|
171
|
-
* queue; the original
|
|
171
|
+
* queue; the original confirmation is still surfaced to this call's caller.
|
|
172
172
|
*/
|
|
173
173
|
private enqueue;
|
|
174
174
|
/**
|
|
@@ -315,7 +315,7 @@ export class UndoScope {
|
|
|
315
315
|
* Run `work` after every previously-enqueued scope operation has settled,
|
|
316
316
|
* in invocation order. The internal `tail` always resolves (failures are
|
|
317
317
|
* swallowed *for the chain only*) so one rejected mutator can't wedge the
|
|
318
|
-
* queue; the original
|
|
318
|
+
* queue; the original confirmation is still surfaced to this call's caller.
|
|
319
319
|
*/
|
|
320
320
|
enqueue(work) {
|
|
321
321
|
const result = this.tail.then(work, work);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* repeating the fetch and error-handling boilerplate.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod';
|
|
15
|
+
import { modelListEvidenceSchema } from '@abloatai/transaction/wire';
|
|
15
16
|
import { translateHttpError } from '@abloatai/transaction/errors';
|
|
16
17
|
import { classifyRecovery } from '@abloatai/transaction/errorCodes';
|
|
17
18
|
import { withAuthHeaders } from '@abloatai/transaction/auth/credentialSource';
|
|
@@ -33,6 +34,16 @@ const QueryResultSchema = z
|
|
|
33
34
|
const QueryBatchResultSchema = z
|
|
34
35
|
.object({
|
|
35
36
|
results: z.array(QueryResultSchema),
|
|
37
|
+
evidence: z.array(z.array(modelListEvidenceSchema)).optional(),
|
|
38
|
+
errors: z.array(z.object({
|
|
39
|
+
index: z.number().int().nonnegative(),
|
|
40
|
+
model: z.string(),
|
|
41
|
+
type: z.string(),
|
|
42
|
+
code: z.string(),
|
|
43
|
+
message: z.string(),
|
|
44
|
+
request_id: z.string().optional(),
|
|
45
|
+
event_id: z.string(),
|
|
46
|
+
})).optional(),
|
|
36
47
|
})
|
|
37
48
|
.loose();
|
|
38
49
|
/**
|
|
@@ -129,6 +140,10 @@ export async function postQuery(options, batch) {
|
|
|
129
140
|
});
|
|
130
141
|
return { results: batch.queries.map(() => []) };
|
|
131
142
|
}
|
|
143
|
+
for (const failure of parsed.data.errors ?? []) {
|
|
144
|
+
runtime.logger.warn(`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}.`);
|
|
145
|
+
runtime.logger.debug('[postQuery.error] query slot failure', failure);
|
|
146
|
+
}
|
|
132
147
|
return parsed.data;
|
|
133
148
|
}
|
|
134
149
|
finally {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export type { WherePrimitive, WhereOp, WhereClause, LoadWhere, } from '@abloatai/transaction/resources/where';
|
|
17
17
|
import type { WhereClause } from '@abloatai/transaction/resources/where';
|
|
18
|
+
import type { ModelListEvidence } from '@abloatai/transaction/wire';
|
|
18
19
|
/** A single structured fetch request. */
|
|
19
20
|
export interface Query {
|
|
20
21
|
/**
|
|
@@ -76,6 +77,18 @@ export interface QueryBatchResult {
|
|
|
76
77
|
* before use.
|
|
77
78
|
*/
|
|
78
79
|
results: unknown[];
|
|
80
|
+
/** Per-result-slot evidence for the primary rows in that slot. */
|
|
81
|
+
evidence?: readonly (readonly QueryReadEvidence[])[];
|
|
82
|
+
/** Per-slot failures that previously looked identical to an empty result. */
|
|
83
|
+
errors?: readonly {
|
|
84
|
+
index: number;
|
|
85
|
+
model: string;
|
|
86
|
+
type: string;
|
|
87
|
+
code: string;
|
|
88
|
+
message: string;
|
|
89
|
+
request_id?: string;
|
|
90
|
+
event_id: string;
|
|
91
|
+
}[];
|
|
79
92
|
/**
|
|
80
93
|
* The server watermark observed after the batch ran. Model reads expose this
|
|
81
94
|
* as `stamp`, and callers thread it into `commits.create({ readAt })` so the
|
|
@@ -83,3 +96,4 @@ export interface QueryBatchResult {
|
|
|
83
96
|
*/
|
|
84
97
|
lastSyncId?: number;
|
|
85
98
|
}
|
|
99
|
+
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
|
import { z } from 'zod';
|
|
@@ -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
|
import { z } from 'zod';
|
|
@@ -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
|
|
@@ -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
|
|
@@ -78,6 +78,7 @@ export interface FetchOptions<T> {
|
|
|
78
78
|
}
|
|
79
79
|
export declare class OnDemandLoader {
|
|
80
80
|
private readonly opts;
|
|
81
|
+
private readonly readEvidence;
|
|
81
82
|
private readonly inFlight;
|
|
82
83
|
/**
|
|
83
84
|
* Query keys with a background confirm currently in flight. Distinct from
|
|
@@ -125,6 +126,8 @@ export declare class OnDemandLoader {
|
|
|
125
126
|
* deduped on the (modelName, where, orderBy, limit) tuple.
|
|
126
127
|
*/
|
|
127
128
|
fetch<T>(modelName: string, options?: FetchOptions<T>): Promise<Model[]>;
|
|
129
|
+
/** Exact evidence retained for a row returned by a server-confirmed fetch. */
|
|
130
|
+
getReadEvidence(row: object): number | undefined;
|
|
128
131
|
private runFetch;
|
|
129
132
|
/**
|
|
130
133
|
* Read a query's rows from local storage only — pool first, then IndexedDB
|
|
@@ -49,6 +49,7 @@ function snapshotDoesNotAdvanceModel(data, model) {
|
|
|
49
49
|
}
|
|
50
50
|
export class OnDemandLoader {
|
|
51
51
|
opts;
|
|
52
|
+
readEvidence = new WeakMap();
|
|
52
53
|
inFlight = new Map();
|
|
53
54
|
/**
|
|
54
55
|
* Query keys with a background confirm currently in flight. Distinct from
|
|
@@ -135,6 +136,10 @@ export class OnDemandLoader {
|
|
|
135
136
|
.catch(() => undefined);
|
|
136
137
|
return work;
|
|
137
138
|
}
|
|
139
|
+
/** Exact evidence retained for a row returned by a server-confirmed fetch. */
|
|
140
|
+
getReadEvidence(row) {
|
|
141
|
+
return this.readEvidence.get(row);
|
|
142
|
+
}
|
|
138
143
|
async runFetch(modelName, typename, ModelClass, clauses, options, queryKey) {
|
|
139
144
|
// `{ type: 'complete' }` is the only way to force a server round-trip:
|
|
140
145
|
// read-after-write certainty. Every other read is local-first.
|
|
@@ -217,7 +222,8 @@ export class OnDemandLoader {
|
|
|
217
222
|
* revalidation kicked off after an `'unknown'` local hit.
|
|
218
223
|
*/
|
|
219
224
|
async fetchFromNetwork(modelName, typename, clauses, options) {
|
|
220
|
-
const
|
|
225
|
+
const network = await this.queryNetwork(modelName, clauses, options);
|
|
226
|
+
const networkRows = network.rows;
|
|
221
227
|
const networkModels = networkRows
|
|
222
228
|
// Strict: a row the server returned whose type name this client never
|
|
223
229
|
// registered is a genuine schema collision (the pushed schema differs
|
|
@@ -225,6 +231,12 @@ export class OnDemandLoader {
|
|
|
225
231
|
// dropping the row and failing downstream as `entity_not_found`.
|
|
226
232
|
.map((raw) => this.hydrateOne(raw, typename, { strict: true }))
|
|
227
233
|
.filter((m) => m !== null);
|
|
234
|
+
const evidenceById = new Map(network.evidence.map((entry) => [entry.id, entry.stamp]));
|
|
235
|
+
for (const model of networkModels) {
|
|
236
|
+
const stamp = evidenceById.get(model.id);
|
|
237
|
+
if (stamp !== undefined)
|
|
238
|
+
this.readEvidence.set(model, stamp);
|
|
239
|
+
}
|
|
228
240
|
if (networkModels.length > 0) {
|
|
229
241
|
this.opts.objectPool.addBatch(networkModels, ModelScope.live);
|
|
230
242
|
// Background IDB write — don't block the caller. Expanded children are
|
|
@@ -435,6 +447,7 @@ export class OnDemandLoader {
|
|
|
435
447
|
runtime: this.opts.runtime,
|
|
436
448
|
}, { queries: [query] });
|
|
437
449
|
const rows = Array.isArray(result.results[0]) ? result.results[0] : [];
|
|
450
|
+
const evidence = result.evidence?.[0] ?? [];
|
|
438
451
|
// Normalize: wire rows lack `__typename` when the server elides it.
|
|
439
452
|
const normalized = rows.map((row) => {
|
|
440
453
|
if (row && typeof row === 'object' && !('__typename' in row)) {
|
|
@@ -453,7 +466,7 @@ export class OnDemandLoader {
|
|
|
453
466
|
if (options?.expand && options.expand.length > 0) {
|
|
454
467
|
this.hydrateExpanded(modelName, normalized, options.expand);
|
|
455
468
|
}
|
|
456
|
-
return normalized;
|
|
469
|
+
return { rows: normalized, evidence };
|
|
457
470
|
}
|
|
458
471
|
/**
|
|
459
472
|
* Hydrate nested expanded rows. Resolves each relation's target
|
|
@@ -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
|
-
* 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
|
-
* 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
|
-
* 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): 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,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,5 +1,5 @@
|
|
|
1
1
|
import type { DurableWriteStore } from '@abloatai/transaction/durableWrites';
|
|
2
|
-
import type { PendingWrite } from '@abloatai/transaction/transactions/
|
|
2
|
+
import type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
|
|
3
3
|
export interface CommitOutboxDatabase {
|
|
4
4
|
sealTransactionRecord(record: PendingWrite, consumedRecordIds: readonly string[]): Promise<unknown>;
|
|
5
5
|
getPersistedTransactions(): Promise<readonly unknown[]>;
|
|
@@ -3,7 +3,7 @@ type EmitLocalMutation = (event: string, payload: unknown) => void;
|
|
|
3
3
|
/**
|
|
4
4
|
* Human-side optimistic-write adapter.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Confirmation retains ordering and receipts; this adapter owns the local
|
|
7
7
|
* pre-image ledger and announces apply/rollback work to the materialiser.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createLocalMutationPort(emit: EmitLocalMutation): LocalMutationPort;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Human-side optimistic-write adapter.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Confirmation retains ordering and receipts; this adapter owns the local
|
|
5
5
|
* pre-image ledger and announces apply/rollback work to the materialiser.
|
|
6
6
|
*/
|
|
7
7
|
export function createLocalMutationPort(emit) {
|
|
@@ -19,7 +19,7 @@ import { type LogPositionPort } from '../../logPosition.js';
|
|
|
19
19
|
import type { WriteOptions } from '../../interfaces/index.js';
|
|
20
20
|
import type { StaleNotification, ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
|
|
21
21
|
import { type MutationInput, type QueuedMutation, type UserContext } from './commitPayload.js';
|
|
22
|
-
import { type CommitOutboxScope } from '@abloatai/transaction/transactions/
|
|
22
|
+
import { type CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
23
23
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
24
24
|
import { type LocalMutationPort } from './localMutation.js';
|
|
25
25
|
import { type CommitTransaction } from './commitLane.js';
|
|
@@ -124,7 +124,7 @@ export declare class MutationQueue extends EventEmitter {
|
|
|
124
124
|
/**
|
|
125
125
|
* Small race buffer for authoritative echoes that arrive before the queued
|
|
126
126
|
* mutation receipt. The forward and WAL stream are independent channels, so
|
|
127
|
-
* either can win without changing the
|
|
127
|
+
* either can win without changing the confirmation result.
|
|
128
128
|
*/
|
|
129
129
|
private recentDeltaCorrelations;
|
|
130
130
|
/**
|
|
@@ -25,7 +25,7 @@ import { entityKey, mergeUpdateData, takeUnsentCreateForModel, findCreateBarrier
|
|
|
25
25
|
import { DeltaConfirmationTracker } from './deltaConfirmation.js';
|
|
26
26
|
import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId, legacyPendingMutationRecordSchema, pendingMutationRecordSchema, persistedMutationSchema, } from './replayValidation.js';
|
|
27
27
|
import { deserializeLegacyPendingMutation, loadPersistedTransactions, persistQueuedTransaction, removePersistedTransaction, settlePersistedFailure, } from './mutationPersistence.js';
|
|
28
|
-
import { createCommitEnvelopeMember, createDurableCommitEnvelope, commitEnvelopeRecordId, durableCommitEnvelopeSchema, } from '@abloatai/transaction/transactions/
|
|
28
|
+
import { createCommitEnvelopeMember, createDurableCommitEnvelope, commitEnvelopeRecordId, durableCommitEnvelopeSchema, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
29
29
|
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
30
30
|
import { createLocalMutationPort, } from './localMutation.js';
|
|
31
31
|
import { dispatchCommitBounded, parseMutationCommitResult, persistDurableCommitAcceptance, removeDurableCommit, sealDurableCommit, } from './commitTransport.js';
|
|
@@ -38,7 +38,7 @@ import { handleFailure } from './failureHandling.js';
|
|
|
38
38
|
import { handleConflict as resolveConflict, isPermanentError as classifyPermanentError, isDefinitiveRejection as classifyDefinitiveRejection } from './failurePolicy.js';
|
|
39
39
|
import { takeNextExecutionBatch as selectExecutionBatch, takePendingDrainBatch as selectPendingDrainBatch } from './executionSelection.js';
|
|
40
40
|
import { scheduleProcessing as scheduleProcessingExternal } from './processingScheduler.js';
|
|
41
|
-
import {
|
|
41
|
+
import { drainPendingConfirmations, } from './pendingDrain.js';
|
|
42
42
|
import { restoreDurableCommits as restoreDurableCommitsExternal } from './durableCommitRestore.js';
|
|
43
43
|
export class MutationQueue extends EventEmitter {
|
|
44
44
|
// Keep one hour of clock/network margin inside the server's 24-hour ledger.
|
|
@@ -92,7 +92,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
92
92
|
/**
|
|
93
93
|
* Small race buffer for authoritative echoes that arrive before the queued
|
|
94
94
|
* mutation receipt. The forward and WAL stream are independent channels, so
|
|
95
|
-
* either can win without changing the
|
|
95
|
+
* either can win without changing the confirmation result.
|
|
96
96
|
*/
|
|
97
97
|
recentDeltaCorrelations = new Map();
|
|
98
98
|
/**
|
|
@@ -1082,7 +1082,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1082
1082
|
return;
|
|
1083
1083
|
this.isProcessing = true;
|
|
1084
1084
|
try {
|
|
1085
|
-
await
|
|
1085
|
+
await drainPendingConfirmations(this.pendingDrainContext);
|
|
1086
1086
|
}
|
|
1087
1087
|
finally {
|
|
1088
1088
|
this.isProcessing = false;
|
|
@@ -5,7 +5,7 @@ import type { MutationStore } from './MutationStore.js';
|
|
|
5
5
|
import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
6
6
|
import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
|
|
7
7
|
import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
8
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/
|
|
8
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
9
9
|
export interface BatchProcessingContext {
|
|
10
10
|
readonly runtime: RuntimeContext;
|
|
11
11
|
readonly config: MutationQueueConfig;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
|
|
2
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/
|
|
2
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
3
3
|
import type { CommitTransaction } from './commitLane.js';
|
|
4
4
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
5
5
|
export interface CommitApiContext {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
2
|
import type { ReadDependency, TrackDependency, OnStaleMode, StaleNotification } from '@abloatai/transaction/coordination/schema';
|
|
3
3
|
import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
4
|
-
import type { DurableCommitEnvelope, DurableCommitOperation } from '@abloatai/transaction/transactions/
|
|
4
|
+
import type { DurableCommitEnvelope, DurableCommitOperation } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
5
5
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
6
6
|
export interface CommitTransaction {
|
|
7
7
|
id: string;
|
|
@@ -12,7 +12,7 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
|
12
12
|
import { MutationOperationType } from '@abloatai/transaction/types';
|
|
13
13
|
import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
|
|
14
14
|
import type { MutationOptions, WriteOptions } from '../../interfaces/index.js';
|
|
15
|
-
import type { CommitEnvelopeMember } from '@abloatai/transaction/transactions/
|
|
15
|
+
import type { CommitEnvelopeMember } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
16
16
|
export interface UserContext {
|
|
17
17
|
userId: string;
|
|
18
18
|
organizationId: string;
|
|
@@ -130,6 +130,7 @@ export interface WriteOperationFields {
|
|
|
130
130
|
readAt?: number | null;
|
|
131
131
|
onStale?: OnStaleMode | null;
|
|
132
132
|
fenceToken?: number | null;
|
|
133
|
+
claimId?: string;
|
|
133
134
|
options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
|
|
134
135
|
}
|
|
135
136
|
/**
|
|
@@ -110,6 +110,8 @@ export function hasStaleWriteOptions(options) {
|
|
|
110
110
|
/** Options whose identity/audit semantics forbid merging two caller writes. */
|
|
111
111
|
export function hasCommitCoalescingBarrier(options) {
|
|
112
112
|
return (hasStaleWriteOptions(options) ||
|
|
113
|
+
options?.claimRef != null ||
|
|
114
|
+
options?.fenceToken != null ||
|
|
113
115
|
typeof options?.idempotencyKey === 'string' ||
|
|
114
116
|
typeof options?.label === 'string');
|
|
115
117
|
}
|
|
@@ -134,6 +136,12 @@ export function applyWriteOptions(op, transaction) {
|
|
|
134
136
|
if (writeOptions.fenceToken !== undefined) {
|
|
135
137
|
operation.fenceToken = writeOptions.fenceToken;
|
|
136
138
|
}
|
|
139
|
+
if (writeOptions.claimRef != null) {
|
|
140
|
+
operation.claimId =
|
|
141
|
+
typeof writeOptions.claimRef === 'string'
|
|
142
|
+
? writeOptions.claimRef
|
|
143
|
+
: writeOptions.claimRef.id;
|
|
144
|
+
}
|
|
137
145
|
if (writeOptions.idempotencyKey != null || writeOptions.label !== undefined) {
|
|
138
146
|
operation.options = {
|
|
139
147
|
...(writeOptions.idempotencyKey != null
|
|
@@ -2,7 +2,7 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
|
2
2
|
import type { MutationExecutor } from '../../interfaces/index.js';
|
|
3
3
|
import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
|
|
4
4
|
import { type MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
5
|
-
import { type CommitOutboxScope, type DurableCommitEnvelope, type DurableCommitOperation } from '@abloatai/transaction/transactions/
|
|
5
|
+
import { type CommitOutboxScope, type DurableCommitEnvelope, type DurableCommitOperation } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
6
6
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
7
7
|
export interface CommitTransportContext {
|
|
8
8
|
readonly runtime: RuntimeContext;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AbloConnectionError, AbloError, AbloIdempotencyError, } from '@abloatai/transaction/errors';
|
|
2
2
|
import { mutationCommitResultSchema, } from '@abloatai/transaction/wire/commit';
|
|
3
|
-
import { createDurableCommitEnvelope, commitEnvelopeRecordId, durableCommitEnvelopeSchema, } from '@abloatai/transaction/transactions/
|
|
3
|
+
import { createDurableCommitEnvelope, commitEnvelopeRecordId, durableCommitEnvelopeSchema, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
4
4
|
export async function sealDurableCommit(ctx, input, pendingMutationRecordId) {
|
|
5
5
|
const sourceMutationIds = [...new Set(input.sourceMutationIds ?? [])];
|
|
6
6
|
const envelope = createDurableCommitEnvelope({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
2
|
import type { CommitTransaction } from './commitLane.js';
|
|
3
|
-
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/
|
|
3
|
+
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
4
4
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
5
5
|
export interface DurableCommitRestoreContext {
|
|
6
6
|
readonly config: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbloIdempotencyError, AbloValidationError } from '@abloatai/transaction/errors';
|
|
2
|
-
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/
|
|
2
|
+
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
3
3
|
export async function restoreDurableCommits(ctx) {
|
|
4
4
|
if (!ctx.config.enablePersistence)
|
|
5
5
|
return new Set();
|
|
@@ -1,14 +1,14 @@
|
|
|
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
|
export { durableWriteStoreSchema, durableWritesConfigSchema, } from '@abloatai/transaction/durableWrites';
|
|
12
12
|
export type { DurableWriteStore, DurableWritesConfig, } from '@abloatai/transaction/durableWrites';
|
|
13
|
-
export { pendingWriteSchema } from '@abloatai/transaction/transactions/
|
|
14
|
-
export type { PendingWrite } from '@abloatai/transaction/transactions/
|
|
13
|
+
export { pendingWriteSchema } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
|
|
14
|
+
export type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
|