@abloatai/humans 0.56.0 → 0.58.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/Ablo.d.ts +8 -18
- package/dist/client.d.ts +6 -19
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/local/Database.d.ts +1 -1
- package/dist/local/Model.js +1 -1
- package/dist/local/SyncClient.d.ts +3 -34
- package/dist/local/SyncClient.js +6 -16
- package/dist/local/client/createInternalComponents.d.ts +1 -1
- package/dist/local/client/createInternalComponents.js +1 -1
- package/dist/local/client/{createModelProxy.d.ts → createModelOperations.d.ts} +22 -9
- package/dist/local/client/{createModelProxy.js → createModelOperations.js} +341 -335
- package/dist/local/client/options.d.ts +2 -2
- package/dist/local/client/reactiveEngine.js +38 -45
- package/dist/local/client/resourceTypes.d.ts +3 -3
- package/dist/local/client/resourceTypes.js +1 -1
- package/dist/local/client/wsMutationExecutor.d.ts +4 -4
- package/dist/local/client/wsMutationExecutor.js +4 -5
- package/dist/local/coordination/ClaimLog.d.ts +3 -3
- package/dist/local/coordination/ClaimLog.js +2 -2
- package/dist/local/interfaces/index.d.ts +5 -7
- package/dist/local/mutators/mutateActions.d.ts +3 -2
- package/dist/local/query/types.d.ts +2 -2
- 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 +1 -1
- package/dist/local/sync/OnDemandLoader.js +1 -1
- package/dist/local/sync/SyncWebSocket.d.ts +3 -3
- package/dist/local/sync/SyncWebSocket.js +2 -2
- package/dist/local/sync/commitFrames.d.ts +1 -1
- package/dist/local/sync/commitFrames.js +1 -1
- package/dist/local/sync/contextOnChange.d.ts +8 -0
- package/dist/local/sync/contextOnChange.js +65 -0
- package/dist/local/sync/createClaimStream.d.ts +1 -1
- package/dist/local/sync/createClaimStream.js +47 -28
- package/dist/local/sync/credentialLifecycle.d.ts +1 -1
- package/dist/local/sync/credentialLifecycle.js +1 -1
- package/dist/local/sync/participants.d.ts +1 -1
- package/dist/local/sync/schemas.d.ts +2 -2
- package/dist/local/sync/schemas.js +1 -1
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/sync/wsFrameHandlers.js +1 -1
- package/dist/local/syncClientTypes.d.ts +41 -0
- package/dist/local/syncClientTypes.js +11 -0
- package/dist/local/transactions/databaseCommitOutbox.d.ts +2 -2
- package/dist/local/transactions/mutations/MutationQueue.d.ts +5 -18
- package/dist/local/transactions/mutations/MutationQueue.js +57 -137
- package/dist/local/transactions/mutations/batchProcessing.d.ts +2 -13
- package/dist/local/transactions/mutations/batchProcessing.js +0 -20
- package/dist/local/transactions/mutations/commitApi.d.ts +2 -3
- package/dist/local/transactions/mutations/commitApi.js +0 -4
- package/dist/local/transactions/mutations/commitLane.d.ts +6 -8
- package/dist/local/transactions/mutations/commitLane.js +12 -14
- package/dist/local/transactions/mutations/commitPayload.d.ts +2 -4
- package/dist/local/transactions/mutations/commitPayload.js +2 -6
- package/dist/local/transactions/mutations/commitTransport.d.ts +3 -4
- package/dist/local/transactions/mutations/commitTransport.js +2 -5
- package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
- package/dist/local/transactions/mutations/durableCommitRestore.js +1 -4
- package/dist/local/transactions/mutations/durableWriteStore.d.ts +5 -5
- package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
- package/dist/local/transactions/mutations/failurePolicy.d.ts +2 -2
- package/dist/local/transactions/mutations/mutationInput.d.ts +40 -0
- package/dist/local/transactions/mutations/mutationInput.js +53 -0
- 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/replayValidation.d.ts +0 -20
- package/dist/local/transactions/mutations/replayValidation.js +1 -3
- package/dist/local/transactions/persistedTransaction.d.ts +0 -2
- package/dist/plugin.d.ts +2 -2
- package/dist/presenceStream.d.ts +1 -1
- package/dist/react/createAbloReact.d.ts +1 -1
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useAblo.js +1 -1
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -2
- package/package.json +2 -2
- package/src/Ablo.ts +9 -27
- package/src/client.ts +6 -22
- package/src/core.ts +1 -1
- package/src/local/Database.ts +1 -2
- package/src/local/Model.ts +1 -1
- package/src/local/SyncClient.ts +17 -72
- package/src/local/client/createInternalComponents.ts +2 -2
- package/src/local/client/{createModelProxy.ts → createModelOperations.ts} +396 -320
- package/src/local/client/options.ts +2 -2
- package/src/local/client/reactiveEngine.ts +67 -55
- package/src/local/client/resourceTypes.ts +4 -4
- package/src/local/client/wsMutationExecutor.ts +4 -9
- package/src/local/coordination/ClaimLog.ts +3 -3
- package/src/local/interfaces/index.ts +5 -7
- package/src/local/mutators/mutateActions.ts +3 -2
- package/src/local/query/types.ts +2 -2
- package/src/local/stores/syncAction.ts +1 -1
- package/src/local/sync/ConnectionManager.ts +1 -1
- package/src/local/sync/OnDemandLoader.ts +1 -1
- package/src/local/sync/SyncWebSocket.ts +3 -3
- package/src/local/sync/commitFrames.ts +1 -2
- package/src/local/sync/contextOnChange.ts +92 -0
- package/src/local/sync/createClaimStream.ts +77 -39
- package/src/local/sync/credentialLifecycle.ts +1 -1
- package/src/local/sync/participants.ts +1 -1
- package/src/local/sync/schemas.ts +1 -1
- package/src/local/sync/wsFrameHandlers.ts +1 -1
- package/src/local/syncClientTypes.ts +59 -0
- package/src/local/transactions/databaseCommitOutbox.ts +2 -2
- package/src/local/transactions/mutations/MutationQueue.ts +69 -183
- package/src/local/transactions/mutations/batchProcessing.ts +2 -38
- package/src/local/transactions/mutations/commitApi.ts +3 -7
- package/src/local/transactions/mutations/commitLane.ts +24 -20
- package/src/local/transactions/mutations/commitPayload.ts +3 -11
- package/src/local/transactions/mutations/commitTransport.ts +3 -7
- package/src/local/transactions/mutations/durableCommitRestore.ts +2 -5
- package/src/local/transactions/mutations/durableWriteStore.ts +5 -5
- package/src/local/transactions/mutations/failurePolicy.ts +2 -2
- package/src/local/transactions/mutations/mutationInput.ts +69 -0
- 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 +1 -3
- package/src/local/transactions/persistedTransaction.ts +0 -3
- package/src/plugin.ts +2 -2
- package/src/presenceStream.ts +2 -2
- package/src/react/createAbloReact.ts +1 -1
- package/src/react/useAblo.ts +2 -2
- package/src/surface.ts +3 -3
- package/dist/local/sync/createSnapshot.d.ts +0 -29
- package/dist/local/sync/createSnapshot.js +0 -116
- package/src/local/sync/createSnapshot.ts +0 -160
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AbloIdempotencyError } from '@abloatai/transaction/errors';
|
|
2
|
-
import type { ReadDependency
|
|
2
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
|
|
3
3
|
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
4
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/
|
|
4
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
5
5
|
import type { CommitTransaction } from './commitLane.js';
|
|
6
6
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@ export async function enqueueCommit(
|
|
|
21
21
|
ctx: CommitApiContext,
|
|
22
22
|
clientTxId: string,
|
|
23
23
|
operations: CommitTransaction['operations'],
|
|
24
|
-
options: { reads?: ReadDependency[] | null
|
|
24
|
+
options: { reads?: ReadDependency[] | null } = {},
|
|
25
25
|
): Promise<void> {
|
|
26
26
|
ctx.assertDurableReplayOpen();
|
|
27
27
|
const existing = ctx.commitStore.get(clientTxId);
|
|
@@ -30,12 +30,10 @@ export async function enqueueCommit(
|
|
|
30
30
|
const existingIntent = stableStringify({
|
|
31
31
|
operations: existing.operations,
|
|
32
32
|
reads: existing.reads ?? null,
|
|
33
|
-
track: existing.track ?? null,
|
|
34
33
|
});
|
|
35
34
|
const incomingIntent = stableStringify({
|
|
36
35
|
operations,
|
|
37
36
|
reads: options.reads ?? null,
|
|
38
|
-
track: options.track ?? null,
|
|
39
37
|
});
|
|
40
38
|
if (existingIntent !== incomingIntent) {
|
|
41
39
|
throw new AbloIdempotencyError(
|
|
@@ -59,7 +57,6 @@ export async function enqueueCommit(
|
|
|
59
57
|
kind: 'commit',
|
|
60
58
|
operations: [...operations],
|
|
61
59
|
...(options.reads ? { reads: options.reads } : {}),
|
|
62
|
-
...(options.track ? { track: options.track } : {}),
|
|
63
60
|
status: 'pending',
|
|
64
61
|
createdAt: now,
|
|
65
62
|
attempts: 0,
|
|
@@ -73,7 +70,6 @@ export async function enqueueCommit(
|
|
|
73
70
|
operations: tx.operations,
|
|
74
71
|
commitOptions: {
|
|
75
72
|
...(tx.reads ? { reads: tx.reads } : {}),
|
|
76
|
-
...(tx.track ? { track: tx.track } : {}),
|
|
77
73
|
},
|
|
78
74
|
createdAt: tx.createdAt,
|
|
79
75
|
sealedAt: tx.sealedAt,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
|
-
import type { ReadDependency
|
|
3
|
-
import type {
|
|
2
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
|
|
3
|
+
import type {
|
|
4
|
+
CommitOperationResult,
|
|
5
|
+
MutationCommitResult,
|
|
6
|
+
} from '@abloatai/transaction/commit';
|
|
4
7
|
import type {
|
|
5
8
|
DurableCommitEnvelope,
|
|
6
9
|
DurableCommitOperation,
|
|
7
|
-
} from '@abloatai/transaction/
|
|
10
|
+
} from '@abloatai/transaction/commit';
|
|
8
11
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
9
12
|
import { transientRetryDelayMs } from './failureHandling.js';
|
|
10
13
|
|
|
@@ -18,16 +21,16 @@ export interface CommitTransaction {
|
|
|
18
21
|
input?: Record<string, unknown>;
|
|
19
22
|
transactionId?: string;
|
|
20
23
|
readAt?: number | null;
|
|
21
|
-
onStale?: OnStaleMode | null;
|
|
22
24
|
}[];
|
|
23
25
|
reads?: ReadDependency[] | null;
|
|
24
|
-
track?: TrackDependency[] | null;
|
|
25
26
|
status: 'pending' | 'executing' | 'awaiting_delta' | 'completed' | 'failed';
|
|
26
27
|
createdAt: number;
|
|
27
28
|
attempts: number;
|
|
28
29
|
transientAttempts?: number;
|
|
29
30
|
firstTransientFailureAt?: number;
|
|
30
31
|
lastSyncId?: number;
|
|
32
|
+
/** Fresh transport rows. Deliberately transient: durable server replays redact row data. */
|
|
33
|
+
operationResults?: CommitOperationResult[];
|
|
31
34
|
correlationId?: string;
|
|
32
35
|
error?: Error;
|
|
33
36
|
sealedAt: number;
|
|
@@ -45,7 +48,6 @@ export interface CommitLaneContext {
|
|
|
45
48
|
retryBackoff: { baseMs: number; capMs: number };
|
|
46
49
|
};
|
|
47
50
|
readonly commitLane: CommitTransaction[];
|
|
48
|
-
readonly commitNotifications: Map<string, StaleNotification[]>;
|
|
49
51
|
readonly commitMissingIds: Map<string, string[]>;
|
|
50
52
|
readonly commitProcessing: boolean;
|
|
51
53
|
readonly setCommitProcessing: (value: boolean) => void;
|
|
@@ -67,7 +69,6 @@ export interface CommitLaneContext {
|
|
|
67
69
|
|
|
68
70
|
export interface CommitReceiptContext {
|
|
69
71
|
readonly commitStore: Map<string, CommitTransaction>;
|
|
70
|
-
readonly commitNotifications: Map<string, StaleNotification[]>;
|
|
71
72
|
readonly commitMissingIds: Map<string, string[]>;
|
|
72
73
|
readonly replicationLagErrors: Map<string, Error>;
|
|
73
74
|
readonly on: (event: string, listener: (payload: object) => void) => void;
|
|
@@ -77,34 +78,38 @@ export interface CommitReceiptContext {
|
|
|
77
78
|
export function waitForCommitReceipt(
|
|
78
79
|
ctx: CommitReceiptContext,
|
|
79
80
|
clientTxId: string,
|
|
80
|
-
): Promise<{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return notifications.length > 0 ? notifications : undefined;
|
|
86
|
-
};
|
|
81
|
+
): Promise<{
|
|
82
|
+
lastSyncId: number;
|
|
83
|
+
missingIds?: string[];
|
|
84
|
+
operationResults?: CommitOperationResult[];
|
|
85
|
+
}> {
|
|
87
86
|
const drainMissingIds = (): string[] | undefined => {
|
|
88
87
|
const ids = ctx.commitMissingIds.get(clientTxId);
|
|
89
88
|
if (!ids) return undefined;
|
|
90
89
|
ctx.commitMissingIds.delete(clientTxId);
|
|
91
90
|
return ids.length > 0 ? ids : undefined;
|
|
92
91
|
};
|
|
93
|
-
const receipt = (lastSyncId: number) => {
|
|
92
|
+
const receipt = (transaction: CommitTransaction, lastSyncId: number) => {
|
|
94
93
|
const missingIds = drainMissingIds();
|
|
95
94
|
return {
|
|
96
95
|
lastSyncId,
|
|
97
|
-
|
|
96
|
+
...(transaction.operationResults
|
|
97
|
+
? { operationResults: transaction.operationResults }
|
|
98
|
+
: {}),
|
|
98
99
|
...(missingIds ? { missingIds } : {}),
|
|
99
100
|
};
|
|
100
101
|
};
|
|
101
102
|
return new Promise((resolve, reject) => {
|
|
102
103
|
const existing = ctx.commitStore.get(clientTxId);
|
|
103
|
-
if (existing?.status === 'completed') { resolve(receipt(existing.lastSyncId ?? 0)); return; }
|
|
104
|
+
if (existing?.status === 'completed') { resolve(receipt(existing, existing.lastSyncId ?? 0)); return; }
|
|
104
105
|
if (existing?.status === 'failed' && existing.error) { reject(existing.error); return; }
|
|
105
106
|
const lagError = ctx.replicationLagErrors.get(clientTxId);
|
|
106
107
|
if (lagError) { reject(lagError); return; }
|
|
107
|
-
const onCompleted = (tx: object) => {
|
|
108
|
+
const onCompleted = (tx: object) => {
|
|
109
|
+
cleanup();
|
|
110
|
+
const completed = tx as CommitTransaction;
|
|
111
|
+
resolve(receipt(completed, completed.lastSyncId ?? 0));
|
|
112
|
+
};
|
|
108
113
|
const onFailed = (payload: object) => { cleanup(); reject((payload as { error: Error }).error); };
|
|
109
114
|
const onLagged = (payload: object) => { cleanup(); reject((payload as { error: Error }).error); };
|
|
110
115
|
const cleanup = () => {
|
|
@@ -140,7 +145,6 @@ export async function processCommitLane(ctx: CommitLaneContext): Promise<void> {
|
|
|
140
145
|
sourceMutationIds: tx.sourceMutationIds,
|
|
141
146
|
commitOptions: {
|
|
142
147
|
...(tx.reads ? { reads: tx.reads } : {}),
|
|
143
|
-
...(tx.track ? { track: tx.track } : {}),
|
|
144
148
|
},
|
|
145
149
|
createdAt: tx.createdAt,
|
|
146
150
|
sealedAt: tx.sealedAt,
|
|
@@ -152,7 +156,7 @@ export async function processCommitLane(ctx: CommitLaneContext): Promise<void> {
|
|
|
152
156
|
const result = await ctx.dispatchCommit(durableEnvelope);
|
|
153
157
|
tx.durableEnvelope = await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
|
|
154
158
|
tx.lastSyncId = result.lastSyncId;
|
|
155
|
-
if (result.
|
|
159
|
+
if (result.operationResults?.length) tx.operationResults = [...result.operationResults];
|
|
156
160
|
if (result.missingIds?.length) ctx.commitMissingIds.set(tx.id, result.missingIds);
|
|
157
161
|
ctx.commitLane.shift();
|
|
158
162
|
if (result.status === 'queued') {
|
|
@@ -13,9 +13,8 @@ import { globalRuntime } from '../../context.js';
|
|
|
13
13
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
14
14
|
import { MutationOperationType } from '@abloatai/transaction/types';
|
|
15
15
|
import { snapshotJsonValue } from '@abloatai/transaction/utils/json';
|
|
16
|
-
import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
|
|
17
16
|
import type { MutationOptions, WriteOptions } from '../../interfaces/index.js';
|
|
18
|
-
import type { CommitEnvelopeMember } from '@abloatai/transaction/
|
|
17
|
+
import type { CommitEnvelopeMember } from '@abloatai/transaction/commit';
|
|
19
18
|
|
|
20
19
|
export interface UserContext {
|
|
21
20
|
userId: string;
|
|
@@ -198,10 +197,7 @@ export const TX_TYPE_TO_MUTATION_OP: Record<QueuedMutation['type'], MutationOper
|
|
|
198
197
|
};
|
|
199
198
|
|
|
200
199
|
export function hasStaleWriteOptions(options?: WriteOptions): boolean {
|
|
201
|
-
return
|
|
202
|
-
options?.readAt !== undefined ||
|
|
203
|
-
options?.onStale !== undefined
|
|
204
|
-
);
|
|
200
|
+
return options?.readAt !== undefined;
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
/** Options whose identity/audit semantics forbid merging two caller writes. */
|
|
@@ -217,7 +213,6 @@ export function hasCommitCoalescingBarrier(options?: WriteOptions): boolean {
|
|
|
217
213
|
|
|
218
214
|
export interface WriteOperationFields {
|
|
219
215
|
readAt?: number | null;
|
|
220
|
-
onStale?: OnStaleMode | null;
|
|
221
216
|
fenceToken?: number | null;
|
|
222
217
|
claimId?: string;
|
|
223
218
|
options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
|
|
@@ -225,7 +220,7 @@ export interface WriteOperationFields {
|
|
|
225
220
|
|
|
226
221
|
/**
|
|
227
222
|
* Copies a transaction's `writeOptions` onto the wire operation. The
|
|
228
|
-
* stale-context
|
|
223
|
+
* stale-context guard (`readAt`) sits at the operation's root,
|
|
229
224
|
* while `idempotencyKey` and `label` go in its `options` slot — the
|
|
230
225
|
* `mutation_log` cache key and audit tag. This is the one place caller-supplied
|
|
231
226
|
* write options cross onto the wire.
|
|
@@ -240,9 +235,6 @@ export function applyWriteOptions<T extends object>(
|
|
|
240
235
|
if (writeOptions.readAt !== undefined) {
|
|
241
236
|
operation.readAt = writeOptions.readAt;
|
|
242
237
|
}
|
|
243
|
-
if (writeOptions.onStale !== undefined) {
|
|
244
|
-
operation.onStale = writeOptions.onStale;
|
|
245
|
-
}
|
|
246
238
|
if (writeOptions.fenceToken !== undefined) {
|
|
247
239
|
operation.fenceToken = writeOptions.fenceToken;
|
|
248
240
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
2
|
import type { MutationExecutor } from '../../interfaces/index.js';
|
|
3
|
-
import type { ReadDependency
|
|
3
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
|
|
4
4
|
import {
|
|
5
5
|
AbloConnectionError,
|
|
6
6
|
AbloError,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
mutationCommitResultSchema,
|
|
11
11
|
type MutationCommitResult,
|
|
12
|
-
} from '@abloatai/transaction/
|
|
12
|
+
} from '@abloatai/transaction/commit';
|
|
13
13
|
import {
|
|
14
14
|
createDurableCommitEnvelope,
|
|
15
15
|
commitEnvelopeRecordId,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type CommitOutboxScope,
|
|
18
18
|
type DurableCommitEnvelope,
|
|
19
19
|
type DurableCommitOperation,
|
|
20
|
-
} from '@abloatai/transaction/
|
|
20
|
+
} from '@abloatai/transaction/commit';
|
|
21
21
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
22
22
|
|
|
23
23
|
export interface CommitTransportContext {
|
|
@@ -39,7 +39,6 @@ export interface SealDurableCommitInput {
|
|
|
39
39
|
sourceMutationIds?: string[];
|
|
40
40
|
commitOptions?: {
|
|
41
41
|
reads?: readonly ReadDependency[] | null;
|
|
42
|
-
track?: readonly TrackDependency[] | null;
|
|
43
42
|
};
|
|
44
43
|
createdAt: number;
|
|
45
44
|
sealedAt: number;
|
|
@@ -63,9 +62,6 @@ export async function sealDurableCommit(
|
|
|
63
62
|
...(input.commitOptions?.reads !== undefined
|
|
64
63
|
? { reads: input.commitOptions.reads === null ? null : [...input.commitOptions.reads] }
|
|
65
64
|
: {}),
|
|
66
|
-
...(input.commitOptions?.track !== undefined
|
|
67
|
-
? { track: input.commitOptions.track === null ? null : [...input.commitOptions.track] }
|
|
68
|
-
: {}),
|
|
69
65
|
},
|
|
70
66
|
...(ctx.commitOutboxScope ? { scope: ctx.commitOutboxScope } : {}),
|
|
71
67
|
createdAt: input.createdAt,
|
|
@@ -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/
|
|
4
|
+
import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/commit';
|
|
5
5
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
6
|
-
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/
|
|
6
|
+
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/commit';
|
|
7
7
|
|
|
8
8
|
export interface DurableCommitRestoreContext {
|
|
9
9
|
readonly config: { enablePersistence: boolean };
|
|
@@ -95,9 +95,6 @@ export async function restoreDurableCommits(ctx: DurableCommitRestoreContext): P
|
|
|
95
95
|
...(envelope.commitOptions.reads
|
|
96
96
|
? { reads: [...envelope.commitOptions.reads] }
|
|
97
97
|
: {}),
|
|
98
|
-
...(envelope.commitOptions.track
|
|
99
|
-
? { track: [...envelope.commitOptions.track] }
|
|
100
|
-
: {}),
|
|
101
98
|
status: 'pending',
|
|
102
99
|
createdAt: envelope.createdAt,
|
|
103
100
|
sealedAt: envelope.sealedAt,
|
|
@@ -6,16 +6,16 @@
|
|
|
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
|
-
* `
|
|
9
|
+
* `commit/confirmation/pendingWrite`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export {
|
|
13
13
|
durableWriteStoreSchema,
|
|
14
14
|
durableWritesConfigSchema,
|
|
15
|
-
} from '@abloatai/transaction/
|
|
15
|
+
} from '@abloatai/transaction/commit';
|
|
16
16
|
export type {
|
|
17
17
|
DurableWriteStore,
|
|
18
18
|
DurableWritesConfig,
|
|
19
|
-
} from '@abloatai/transaction/
|
|
20
|
-
export { pendingWriteSchema } from '@abloatai/transaction/
|
|
21
|
-
export type { PendingWrite } from '@abloatai/transaction/
|
|
19
|
+
} from '@abloatai/transaction/commit';
|
|
20
|
+
export { pendingWriteSchema } from '@abloatai/transaction/commit';
|
|
21
|
+
export type { PendingWrite } from '@abloatai/transaction/commit';
|
|
@@ -25,7 +25,7 @@ export function isDefinitiveRejection(error: Error): boolean {
|
|
|
25
25
|
return status !== undefined && status >= 400 && status < 500 && status !== 429;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface
|
|
28
|
+
export interface ConflictResolutionContext {
|
|
29
29
|
readonly config: Pick<MutationQueueConfig, 'conflictResolution'>;
|
|
30
30
|
readonly store: MutationStore;
|
|
31
31
|
readonly rollbackOptimistic: (transaction: QueuedMutation, reason: string) => Promise<void>;
|
|
@@ -34,7 +34,7 @@ export interface ConflictPolicyContext {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function handleConflict(
|
|
37
|
-
ctx:
|
|
37
|
+
ctx: ConflictResolutionContext,
|
|
38
38
|
transaction: QueuedMutation,
|
|
39
39
|
serverData: MutationInput,
|
|
40
40
|
): Promise<void> {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The input a queued mutation carries, derived from a local model.
|
|
3
|
+
*
|
|
4
|
+
* Six small rules that answer one question: given a model and what changed on
|
|
5
|
+
* it, what does the commit actually send? They sat as private methods on
|
|
6
|
+
* `MutationQueue` because that is where they were called, which is how a queue
|
|
7
|
+
* ends up also owning the payload vocabulary.
|
|
8
|
+
*
|
|
9
|
+
* The before-image rule is the one worth reading twice, and it is stated in
|
|
10
|
+
* `previousDataFor` rather than here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { LocalModel } from '../../localModelContract.js';
|
|
14
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
15
|
+
import { projectCommitPayload, type MutationInput } from './commitPayload.js';
|
|
16
|
+
|
|
17
|
+
/** A queued transaction's local identity, unique per process and monotonic enough to sort. */
|
|
18
|
+
export function generateTransactionId(): string {
|
|
19
|
+
return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Local values win: they are the edit the caller made, remote is the base. */
|
|
23
|
+
export function mergeMutationData(
|
|
24
|
+
local: MutationInput | undefined,
|
|
25
|
+
remote: MutationInput | undefined,
|
|
26
|
+
): MutationInput {
|
|
27
|
+
return { ...(remote ?? {}), ...(local ?? {}) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A create sends the whole row, `undefined` included, so absence is explicit. */
|
|
31
|
+
export function createDataFor(model: LocalModel, runtime?: RuntimeContext): MutationInput {
|
|
32
|
+
return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, runtime);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** An arbitrary change set, projected the way an update is. */
|
|
36
|
+
export function changesToInput(
|
|
37
|
+
modelName: string,
|
|
38
|
+
changes: Record<string, unknown>,
|
|
39
|
+
runtime?: RuntimeContext,
|
|
40
|
+
): MutationInput {
|
|
41
|
+
return projectCommitPayload(modelName, changes, { dropUndefined: true }, runtime);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An update sends only what changed. */
|
|
45
|
+
export function updateDataFor(model: LocalModel, runtime?: RuntimeContext): MutationInput {
|
|
46
|
+
return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, runtime);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The before-image an undo reverts to.
|
|
51
|
+
*
|
|
52
|
+
* When the update's written keys are known, capture a before-image for exactly
|
|
53
|
+
* those keys, so the recorded undo inverse reverts them and nothing else — a
|
|
54
|
+
* full-row inverse would clobber concurrent edits to unrelated fields.
|
|
55
|
+
* `fallbackToLive: false` makes `Model.capturePreviousValues` omit any key it
|
|
56
|
+
* cannot resolve, and `buildUndoOps` then drops an un-revertible inverse rather
|
|
57
|
+
* than inventing one. With no `updateInput` (a full extract) it falls back to
|
|
58
|
+
* every tracked field.
|
|
59
|
+
*
|
|
60
|
+
* Model-specific special cases do not belong here: a model that needs to
|
|
61
|
+
* surface previous state beyond `modifiedProperties` should expose a typed
|
|
62
|
+
* `getPreviousData()` accessor for this to call.
|
|
63
|
+
*/
|
|
64
|
+
export function previousDataFor(model: LocalModel, updateInput?: MutationInput): MutationInput {
|
|
65
|
+
const keys = updateInput
|
|
66
|
+
? Object.keys(updateInput)
|
|
67
|
+
: [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
|
|
68
|
+
return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
|
|
69
|
+
}
|
|
@@ -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/
|
|
4
|
+
import type { CommitOutboxScope } from '@abloatai/transaction/commit';
|
|
5
5
|
import type { QueuedMutation } from './commitPayload.js';
|
|
6
6
|
import { MutationStore } from './MutationStore.js';
|
|
7
7
|
import { normalizeModelKey } from './commitPayload.js';
|
|
@@ -2,8 +2,8 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
|
2
2
|
import type { QueuedMutation } from './commitPayload.js';
|
|
3
3
|
import type { MutationStore } from './MutationStore.js';
|
|
4
4
|
import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
5
|
-
import type { MutationCommitResult } from '@abloatai/transaction/
|
|
6
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/
|
|
5
|
+
import type { MutationCommitResult } from '@abloatai/transaction/commit';
|
|
6
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
7
7
|
import { applyWriteOptions, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
8
8
|
|
|
9
9
|
export interface PendingDrainContext {
|
|
@@ -23,14 +23,12 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
|
23
23
|
import {
|
|
24
24
|
commitEnvelopeMemberSchema,
|
|
25
25
|
commitOutboxScopeSchema,
|
|
26
|
-
} from '@abloatai/transaction/
|
|
27
|
-
import { onStaleModeSchema } from '@abloatai/transaction/coordination/schema';
|
|
26
|
+
} from '@abloatai/transaction/commit';
|
|
28
27
|
|
|
29
28
|
/** The subset of a write's options that is stored with each transaction or queued mutation. */
|
|
30
29
|
const persistedWriteOptionsSchema = z
|
|
31
30
|
.object({
|
|
32
31
|
readAt: z.number().nullable().optional(),
|
|
33
|
-
onStale: onStaleModeSchema.nullable().optional(),
|
|
34
32
|
idempotencyKey: z.string().optional(),
|
|
35
33
|
label: z.string().optional(),
|
|
36
34
|
// Aligned with the `WriteOptions` type: a claimed write persisted locally
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
|
|
2
|
-
|
|
3
1
|
type ModelData = Record<string, unknown>;
|
|
4
2
|
|
|
5
3
|
/** One mutation retained in the durable local transaction journal. */
|
|
@@ -10,7 +8,6 @@ interface PersistedMutation {
|
|
|
10
8
|
timestamp: string;
|
|
11
9
|
writeOptions?: {
|
|
12
10
|
readAt?: number | null;
|
|
13
|
-
onStale?: OnStaleMode | null;
|
|
14
11
|
};
|
|
15
12
|
}
|
|
16
13
|
|
package/src/plugin.ts
CHANGED
|
@@ -19,9 +19,9 @@ import type { ParticipantKind } from '@abloatai/transaction/types/participant';
|
|
|
19
19
|
import type { ErrorCodeSpec } from '@abloatai/transaction/errorCodes';
|
|
20
20
|
import type { Logger } from '@abloatai/transaction/logger';
|
|
21
21
|
import type { CoordinationObservability } from '@abloatai/transaction/observability';
|
|
22
|
-
import type { WsTransport } from '@abloatai/transaction/transport/
|
|
22
|
+
import type { WsTransport } from '@abloatai/transaction/transport/websocket';
|
|
23
23
|
import type { AuthCredentialSource } from '@abloatai/transaction/auth/credentialSource';
|
|
24
|
-
import type { SyncDeltaWireCore } from '@abloatai/transaction/
|
|
24
|
+
import type { SyncDeltaWireCore } from '@abloatai/transaction/observation';
|
|
25
25
|
import type { ModelData } from '@abloatai/transaction/types/modelData';
|
|
26
26
|
|
|
27
27
|
/**
|
package/src/presenceStream.ts
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* `leave`.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type { WsTransport } from '@abloatai/transaction/transport/
|
|
27
|
-
import type { PresenceUpdate } from '@abloatai/transaction/transport/
|
|
26
|
+
import type { WsTransport } from '@abloatai/transaction/transport/websocket';
|
|
27
|
+
import type { PresenceUpdate } from '@abloatai/transaction/transport/websocket';
|
|
28
28
|
import type {
|
|
29
29
|
Activity,
|
|
30
30
|
Peer,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
type UseAbloModelResult,
|
|
37
37
|
} from './useAblo.js';
|
|
38
38
|
import type { AbloClient as Ablo } from '../client.js';
|
|
39
|
-
import type { ModelOperations } from '../local/client/
|
|
39
|
+
import type { ModelOperations } from '../local/client/createModelOperations.js';
|
|
40
40
|
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
41
41
|
|
|
42
42
|
/** What a binding returns: the provider and the hook, with `S` fixed. */
|
package/src/react/useAblo.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ModelClaim } from '@abloatai/transaction/coordination';
|
|
|
7
7
|
import {
|
|
8
8
|
getModelClientMeta,
|
|
9
9
|
type ModelOperations,
|
|
10
|
-
} from '../local/client/
|
|
10
|
+
} from '../local/client/createModelOperations.js';
|
|
11
11
|
import { Model } from '../local/Model.js';
|
|
12
12
|
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
13
13
|
import type { ResolveSchema } from '@abloatai/transaction/types/global';
|
|
@@ -138,7 +138,7 @@ function snapshotValue<T>(value: T): T {
|
|
|
138
138
|
* // With the Register augmentation (recommended):
|
|
139
139
|
* const ablo = useAblo();
|
|
140
140
|
* if (!ablo) return <Loading />;
|
|
141
|
-
* const doc = await ablo.records.get({ id }); // async server read
|
|
141
|
+
* const doc = await ablo.records.get({ id }); // observational async server read
|
|
142
142
|
*
|
|
143
143
|
* // Reactive selector (a synchronous local snapshot). The selector's reads
|
|
144
144
|
* // are typed as snapshot rows — data fields + computeds, no relation
|
package/src/surface.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* written by hand, since prose cannot be type-checked.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { ModelOperations, LocalReadOptions } from './local/client/
|
|
17
|
+
import type { ModelOperations, LocalReadOptions } from './local/client/createModelOperations.js';
|
|
18
18
|
import type { AbloOptions } from './Ablo.js';
|
|
19
19
|
|
|
20
20
|
// ── compile-time exact-equality (no runtime, no casts) ─────────────────────
|
|
@@ -33,14 +33,14 @@ type Expect<T extends true> = T;
|
|
|
33
33
|
*/
|
|
34
34
|
export const PUBLIC_MODEL_VERBS = [
|
|
35
35
|
'get',
|
|
36
|
-
'
|
|
36
|
+
'read',
|
|
37
37
|
'list',
|
|
38
|
+
'listAll',
|
|
38
39
|
'local',
|
|
39
40
|
'create',
|
|
40
41
|
'update',
|
|
41
42
|
'delete',
|
|
42
43
|
'claim',
|
|
43
|
-
'track',
|
|
44
44
|
'join',
|
|
45
45
|
'onChange',
|
|
46
46
|
] as const;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Captures a local {@link Snapshot} of a chosen set of entities, along with a
|
|
3
|
-
* watermark, so a caller can detect when that state has gone stale. This is
|
|
4
|
-
* what an LLM caller threads into a prompt: `stamp` flows into later writes as
|
|
5
|
-
* `readAt`, so the server rejects a mutation premised on data that has since
|
|
6
|
-
* changed; `signal` is an `AbortSignal` that fires as soon as any captured
|
|
7
|
-
* entity receives a delta, so a mid-generation invalidation can abort the token
|
|
8
|
-
* stream instead of producing output against stale context.
|
|
9
|
-
*
|
|
10
|
-
* It reads the current entity state from the in-memory pool, reads the engine's
|
|
11
|
-
* current `lastSyncId` as the watermark, and subscribes to delta frames on the
|
|
12
|
-
* existing sync connection — no second connection.
|
|
13
|
-
*/
|
|
14
|
-
import type { InstanceCache } from '../InstanceCache.js';
|
|
15
|
-
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
16
|
-
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
17
|
-
import type { Snapshot } from '@abloatai/transaction/types/streams';
|
|
18
|
-
export interface CreateSnapshotArgs<TSchema extends Schema = Schema, K extends keyof TSchema['models'] & string = keyof TSchema['models'] & string> {
|
|
19
|
-
pool: InstanceCache;
|
|
20
|
-
/** Live transport for delta subscriptions. May be null if the engine
|
|
21
|
-
* hasn't connected yet — the snapshot still resolves with current
|
|
22
|
-
* pool state, but `signal` won't fire until reconnect. */
|
|
23
|
-
transport: SyncWebSocket | null;
|
|
24
|
-
/** Returns the engine's current `lastSyncId`. Read at snapshot time
|
|
25
|
-
* to stamp the watermark; not re-read after. */
|
|
26
|
-
getLastSyncId: () => number;
|
|
27
|
-
entities: Readonly<Record<K, string | readonly string[]>>;
|
|
28
|
-
}
|
|
29
|
-
export declare function createSnapshot<TSchema extends Schema, K extends keyof TSchema['models'] & string>(args: CreateSnapshotArgs<TSchema, K>): Snapshot<TSchema, K>;
|