@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
|
@@ -19,8 +19,8 @@ import type { AbloPersistence } from '../persistence.js';
|
|
|
19
19
|
import type {
|
|
20
20
|
DurableWriteStore,
|
|
21
21
|
DurableWritesConfig,
|
|
22
|
-
} from '@abloatai/transaction/
|
|
23
|
-
import type { CommitOutboxScope } from '@abloatai/transaction/
|
|
22
|
+
} from '@abloatai/transaction/commit';
|
|
23
|
+
import type { CommitOutboxScope } from '@abloatai/transaction/commit';
|
|
24
24
|
|
|
25
25
|
// ── Options ───────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
@@ -17,7 +17,7 @@ import { omittedModelError } from '@abloatai/transaction/schema/select';
|
|
|
17
17
|
import {
|
|
18
18
|
durableCommitOperationSchema,
|
|
19
19
|
type DurableCommitOperation,
|
|
20
|
-
} from '@abloatai/transaction/
|
|
20
|
+
} from '@abloatai/transaction/commit';
|
|
21
21
|
import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
|
|
22
22
|
import type { ModelTarget, ModelClaim } from '@abloatai/transaction/coordination/schema';
|
|
23
23
|
import type { BatchFence } from '@abloatai/transaction/coordination';
|
|
@@ -41,11 +41,14 @@ import type { StoreCluster } from './storeCluster.js';
|
|
|
41
41
|
import { startStoreLifecycle } from './storeLifecycle.js';
|
|
42
42
|
import type { SyncWebSocket, CoreSyncEventMap } from '../sync/SyncWebSocket.js';
|
|
43
43
|
import { createClaimStream } from '../sync/createClaimStream.js';
|
|
44
|
-
import { awaitClaimGrant } from '@abloatai/transaction/
|
|
45
|
-
import {
|
|
44
|
+
import { awaitClaimGrant } from '@abloatai/transaction/claims';
|
|
45
|
+
import {
|
|
46
|
+
bindClaimLifetime,
|
|
47
|
+
claimLifetimeOf,
|
|
48
|
+
} from '@abloatai/transaction/claims/lifetime';
|
|
46
49
|
import { createParticipantManager } from '../sync/participants.js';
|
|
47
50
|
import type { AttachablePresenceStream } from '../../presenceStream.js';
|
|
48
|
-
import type { ClaimWaitOptions
|
|
51
|
+
import type { ClaimWaitOptions } from '@abloatai/transaction/types/streams';
|
|
49
52
|
import type { Claim } from '@abloatai/transaction/types/streams';
|
|
50
53
|
import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
|
|
51
54
|
import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '@abloatai/transaction/auth/apiKey';
|
|
@@ -66,9 +69,9 @@ import type {
|
|
|
66
69
|
import {
|
|
67
70
|
claimAttemptFailure,
|
|
68
71
|
emitClaimStatus,
|
|
69
|
-
} from '@abloatai/transaction/resources/modelOperations';
|
|
70
|
-
import {
|
|
71
|
-
import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
|
|
72
|
+
} from '@abloatai/transaction/client/resources/modelOperations';
|
|
73
|
+
import { createModelOperations, type ModelOperations } from './createModelOperations.js';
|
|
74
|
+
import { assertWriteOptions } from '@abloatai/transaction/client/resources/writeOptionsSchema';
|
|
72
75
|
import type { AbloClient as Ablo } from '../../client.js';
|
|
73
76
|
import {
|
|
74
77
|
modelReadResponseSchema,
|
|
@@ -76,8 +79,17 @@ import {
|
|
|
76
79
|
commitRecordListSchema,
|
|
77
80
|
commitRecordWhereSchema,
|
|
78
81
|
} from '@abloatai/transaction/wire';
|
|
79
|
-
import {
|
|
80
|
-
|
|
82
|
+
import {
|
|
83
|
+
translateHttpError,
|
|
84
|
+
type AbloStaleContextError,
|
|
85
|
+
} from '@abloatai/transaction/errors';
|
|
86
|
+
import {
|
|
87
|
+
kReadEvidence,
|
|
88
|
+
prepareReadSet,
|
|
89
|
+
} from '@abloatai/transaction/internal/read-set';
|
|
90
|
+
import { contextOnChange } from '../sync/contextOnChange.js';
|
|
91
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination';
|
|
92
|
+
import type { CapturedRow } from '@abloatai/transaction/transport/http';
|
|
81
93
|
|
|
82
94
|
/**
|
|
83
95
|
* What the reactive build is fed: the factory's pass over the options bag
|
|
@@ -296,7 +308,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
296
308
|
|
|
297
309
|
function normalizeCommitOperation(
|
|
298
310
|
op: CommitOperationInput,
|
|
299
|
-
defaults: Pick<CommitCreateOptions, 'readAt'
|
|
311
|
+
defaults: Pick<CommitCreateOptions, 'readAt'>,
|
|
300
312
|
fence: BatchFence | null,
|
|
301
313
|
claim: Claim | null,
|
|
302
314
|
): DurableCommitOperation {
|
|
@@ -311,14 +323,13 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
311
323
|
claimId:
|
|
312
324
|
op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null) ?? undefined,
|
|
313
325
|
readAt: op.readAt ?? defaults.readAt ?? undefined,
|
|
314
|
-
onStale: op.onStale ?? defaults.onStale ?? undefined,
|
|
315
326
|
fenceToken:
|
|
316
327
|
op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null) ?? undefined,
|
|
317
328
|
});
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
function normalizeCommitOperations(
|
|
321
|
-
commitOptions: CommitCreateOptions,
|
|
332
|
+
commitOptions: Pick<CommitCreateOptions, 'operations' | 'readAt' | 'claim'>,
|
|
322
333
|
fence: BatchFence | null,
|
|
323
334
|
): DurableCommitOperation[] {
|
|
324
335
|
if (commitOptions.operations.length === 0) {
|
|
@@ -457,7 +468,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
457
468
|
// the one `awaitClaimGrant` read there; retain the handle fallback for
|
|
458
469
|
// wire-compatible transports that already stamped it locally.
|
|
459
470
|
const resolvedFenceToken = fenceToken ?? claim.fenceToken;
|
|
460
|
-
|
|
471
|
+
const wrapped = {
|
|
461
472
|
object: 'claim',
|
|
462
473
|
id: claim.id,
|
|
463
474
|
description: claim.description,
|
|
@@ -472,7 +483,9 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
472
483
|
// silently dropped from the public claim.
|
|
473
484
|
heartbeat: claim.heartbeat,
|
|
474
485
|
[Symbol.asyncDispose]: release,
|
|
475
|
-
};
|
|
486
|
+
} satisfies Claim;
|
|
487
|
+
const lifetime = claimLifetimeOf(claim);
|
|
488
|
+
return lifetime ? bindClaimLifetime(wrapped, lifetime) : wrapped;
|
|
476
489
|
}
|
|
477
490
|
|
|
478
491
|
const publicClaims: ClaimResource = Object.assign(claimStream, {
|
|
@@ -580,7 +593,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
580
593
|
const modelProxies: Record<string, ModelOperations<unknown, unknown>> = {};
|
|
581
594
|
for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
|
|
582
595
|
const registeredModelName = modelDef.typename ?? schemaKey;
|
|
583
|
-
modelProxies[schemaKey] =
|
|
596
|
+
modelProxies[schemaKey] = createModelOperations(
|
|
584
597
|
schemaKey,
|
|
585
598
|
registeredModelName,
|
|
586
599
|
objectPool,
|
|
@@ -589,21 +602,15 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
589
602
|
hydration,
|
|
590
603
|
{
|
|
591
604
|
createClaim: (claimOptions) => publicClaims.create(claimOptions),
|
|
605
|
+
// Lazily referenced: `commits` is declared below this loop, and this
|
|
606
|
+
// only runs when someone actually writes a batch.
|
|
607
|
+
commitBatch: (commitOptions) => commits.create(commitOptions),
|
|
592
608
|
readPoint,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// `readAt` (max of the pool-applied cursor and the acked
|
|
599
|
-
// watermark for our own writes — see logPosition.ts).
|
|
600
|
-
// Stamping a bare stream cursor made a claim taken right after
|
|
601
|
-
// an ack-confirmed write stale against that write's own delta.
|
|
602
|
-
// The socket/store cursors are persistence-gated and therefore
|
|
603
|
-
// never ahead of `applied` — no extra max() needed here.
|
|
604
|
-
getLastSyncId: () => syncClient.position.readFloor,
|
|
605
|
-
entities: { [modelKey]: id },
|
|
606
|
-
}),
|
|
609
|
+
// A claim needs the post-read watermark, not the legacy snapshot
|
|
610
|
+
// object. `readFloor` is max(applied, acked-own-write), so a claim
|
|
611
|
+
// taken immediately after its own confirmed mutation is not stale
|
|
612
|
+
// against an echo that has not materialised locally yet.
|
|
613
|
+
currentReadAt: () => syncClient.position.readFloor,
|
|
607
614
|
queue: (target) =>
|
|
608
615
|
publicClaims.queueFor(streamTarget(target)),
|
|
609
616
|
reorder: (target, order) =>
|
|
@@ -653,31 +660,39 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
653
660
|
);
|
|
654
661
|
}
|
|
655
662
|
|
|
656
|
-
const commits: CommitResource = {
|
|
657
|
-
async create(
|
|
663
|
+
const commits: CommitResource<ReadDependency | CapturedRow> = {
|
|
664
|
+
async create(
|
|
665
|
+
commitOptions: CommitCreateOptions<ReadDependency | CapturedRow>,
|
|
666
|
+
): Promise<CommitReceipt> {
|
|
658
667
|
await ready();
|
|
668
|
+
const prepared = prepareReadSet(
|
|
669
|
+
cluster.readSetContext,
|
|
670
|
+
syncClient as object,
|
|
671
|
+
commitOptions.readAt,
|
|
672
|
+
commitOptions.idempotencyKey,
|
|
673
|
+
commitOptions.reads,
|
|
674
|
+
);
|
|
659
675
|
// Same runtime contract as the per-model writes — one schema.
|
|
660
676
|
assertWriteOptions(
|
|
661
677
|
{
|
|
662
|
-
idempotencyKey:
|
|
663
|
-
readAt:
|
|
664
|
-
onStale: commitOptions.onStale,
|
|
678
|
+
idempotencyKey: prepared.idempotencyKey,
|
|
679
|
+
readAt: prepared.readAt,
|
|
665
680
|
wait: commitOptions.wait,
|
|
666
681
|
claim: commitOptions.claim,
|
|
682
|
+
reads: prepared.reads,
|
|
667
683
|
},
|
|
668
684
|
'commits.create',
|
|
669
685
|
);
|
|
670
|
-
const clientTxId = createClientTxId(
|
|
686
|
+
const clientTxId = createClientTxId(prepared.idempotencyKey);
|
|
671
687
|
// A claim handle supplies the batch stale-guard defaults — same
|
|
672
688
|
// semantics as `ablo.<model>.update({ id, data, claim })`, so the
|
|
673
689
|
// two write doors speak one claim vocabulary. Explicit options win.
|
|
674
690
|
const claim = commitOptions.claim ?? null;
|
|
675
691
|
const operations = normalizeCommitOperations(
|
|
676
692
|
{
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
693
|
+
operations: commitOptions.operations,
|
|
694
|
+
...(commitOptions.claim !== undefined ? { claim: commitOptions.claim } : {}),
|
|
695
|
+
readAt: prepared.readAt ?? claim?.readAt ?? null,
|
|
681
696
|
},
|
|
682
697
|
batchFence(claim?.target, claim?.fenceToken),
|
|
683
698
|
);
|
|
@@ -693,22 +708,21 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
693
708
|
// no need to leak an accessor through BaseSyncedStore.
|
|
694
709
|
const queue = syncClient.getMutationQueue();
|
|
695
710
|
await queue.enqueueCommit(clientTxId, operations, {
|
|
696
|
-
...(
|
|
697
|
-
...(commitOptions.track ? { track: [...commitOptions.track] } : {}),
|
|
711
|
+
...(prepared.reads ? { reads: [...prepared.reads] } : {}),
|
|
698
712
|
});
|
|
699
713
|
|
|
700
714
|
if (wait === 'queued') {
|
|
701
715
|
return { id: clientTxId, status: 'queued' };
|
|
702
716
|
}
|
|
703
717
|
|
|
704
|
-
const { lastSyncId,
|
|
718
|
+
const { lastSyncId, missingIds, operationResults } =
|
|
705
719
|
await queue.waitForCommitReceipt(clientTxId);
|
|
706
720
|
return {
|
|
707
721
|
id: clientTxId,
|
|
708
722
|
status: 'confirmed',
|
|
709
723
|
lastSyncId,
|
|
710
|
-
...(notifications && notifications.length > 0 ? { notifications } : {}),
|
|
711
724
|
...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
|
|
725
|
+
...(operationResults && operationResults.length > 0 ? { operationResults } : {}),
|
|
712
726
|
};
|
|
713
727
|
},
|
|
714
728
|
async get({ id }) {
|
|
@@ -901,6 +915,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
901
915
|
const sessionParams = {
|
|
902
916
|
agent: { id },
|
|
903
917
|
can: params.can,
|
|
918
|
+
...(params.onBehalfOf ? { onBehalfOf: params.onBehalfOf } : {}),
|
|
904
919
|
...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
|
|
905
920
|
...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
|
|
906
921
|
...(userMeta ? { userMeta } : {}),
|
|
@@ -1014,21 +1029,18 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
1014
1029
|
|
|
1015
1030
|
commits,
|
|
1016
1031
|
|
|
1017
|
-
/** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
|
|
1018
|
-
snapshot<ModelName extends keyof S & string>(
|
|
1019
|
-
entities: Readonly<Record<ModelName, string | readonly string[]>>,
|
|
1020
|
-
): Snapshot<Schema<S>, ModelName> {
|
|
1021
|
-
return createSnapshot<Schema<S>, ModelName>({
|
|
1022
|
-
pool: objectPool,
|
|
1023
|
-
transport,
|
|
1024
|
-
getLastSyncId: () => transport.getLastSyncId(),
|
|
1025
|
-
entities,
|
|
1026
|
-
});
|
|
1027
|
-
},
|
|
1028
1032
|
} as Ablo<S>;
|
|
1029
1033
|
|
|
1030
1034
|
Object.defineProperty(engine, kReadEvidence, {
|
|
1031
|
-
value: {
|
|
1035
|
+
value: {
|
|
1036
|
+
context: cluster.readSetContext,
|
|
1037
|
+
client: syncClient as object,
|
|
1038
|
+
onChange: (
|
|
1039
|
+
reads: readonly ReadDependency[],
|
|
1040
|
+
listener: (error: AbloStaleContextError) => void,
|
|
1041
|
+
) =>
|
|
1042
|
+
contextOnChange(syncClient, objectPool, reads, listener),
|
|
1043
|
+
},
|
|
1032
1044
|
enumerable: false,
|
|
1033
1045
|
});
|
|
1034
1046
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* live participant handle.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export * from '@abloatai/transaction/resources/httpResources';
|
|
11
|
+
export * from '@abloatai/transaction/client/resources/httpResources';
|
|
12
12
|
|
|
13
13
|
// The request contract for `ablo.<model>` — also core, re-joined here so this
|
|
14
14
|
// module stays the single import path for the whole resource surface.
|
|
@@ -17,7 +17,7 @@ export type {
|
|
|
17
17
|
LocalReadOptions,
|
|
18
18
|
ModelListScope,
|
|
19
19
|
ServerReadOptions,
|
|
20
|
-
|
|
20
|
+
ModelReadParams,
|
|
21
21
|
ModelWriteOptions,
|
|
22
22
|
ModelCreateParams,
|
|
23
23
|
ModelUpdateParams,
|
|
@@ -34,8 +34,8 @@ export type {
|
|
|
34
34
|
ClaimHeartbeatOptions,
|
|
35
35
|
HeldClaim,
|
|
36
36
|
HeldLease,
|
|
37
|
-
} from '@abloatai/transaction/resources/modelOperations';
|
|
37
|
+
} from '@abloatai/transaction/client/resources/modelOperations';
|
|
38
38
|
|
|
39
39
|
// `ModelOperations` binds the request contract to reactive model instances, so
|
|
40
40
|
// it lives with the factory that builds it rather than with the core.
|
|
41
|
-
export type { ModelOperations } from './
|
|
41
|
+
export type { ModelOperations } from './createModelOperations.js';
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
* client wires this up automatically unless you supply your own executor.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ReadDependency
|
|
8
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
|
|
9
9
|
import type {
|
|
10
10
|
MutationExecutor,
|
|
11
11
|
MutationOperation,
|
|
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/
|
|
15
|
+
import type { CommitReceiptWire } from '@abloatai/transaction/commit';
|
|
16
16
|
import {
|
|
17
17
|
recordWebSocketCommitReceipt,
|
|
18
18
|
type ReadSetContext,
|
|
19
19
|
} from '@abloatai/transaction/internal/read-set';
|
|
20
|
-
import { commitAckSchema } from '@abloatai/transaction/
|
|
20
|
+
import { commitAckSchema } from '@abloatai/transaction/commit';
|
|
21
21
|
import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
22
22
|
|
|
23
23
|
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
@@ -44,14 +44,12 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
44
44
|
clientTxId: string,
|
|
45
45
|
timeoutMs?: number,
|
|
46
46
|
reads?: readonly ReadDependency[] | null,
|
|
47
|
-
track?: readonly TrackDependency[] | null,
|
|
48
47
|
) => Promise<CommitReceiptWire>;
|
|
49
48
|
sendCommit?: (
|
|
50
49
|
operations: readonly MutationOperation[],
|
|
51
50
|
clientTxId: string,
|
|
52
51
|
timeoutMs?: number,
|
|
53
52
|
reads?: readonly ReadDependency[] | null,
|
|
54
|
-
track?: readonly TrackDependency[] | null,
|
|
55
53
|
) => Promise<CommitAck>;
|
|
56
54
|
} | null,
|
|
57
55
|
readSetContext?: ReadSetContext,
|
|
@@ -80,21 +78,19 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
80
78
|
clientTxId,
|
|
81
79
|
undefined,
|
|
82
80
|
options?.reads,
|
|
83
|
-
options?.track,
|
|
84
81
|
);
|
|
85
82
|
recordWebSocketCommitReceipt(readSetContext, {
|
|
86
83
|
receipt,
|
|
87
84
|
operations,
|
|
88
85
|
reads: options?.reads,
|
|
89
|
-
track: options?.track,
|
|
90
86
|
});
|
|
91
87
|
return commitAckSchema.parse({
|
|
92
88
|
status: receipt.status,
|
|
93
89
|
statusAt: receipt.statusAt,
|
|
94
90
|
lastSyncId: receipt.lastSyncId,
|
|
95
91
|
...(receipt.correlationId ? { correlationId: receipt.correlationId } : {}),
|
|
96
|
-
...(receipt.notifications ? { notifications: receipt.notifications } : {}),
|
|
97
92
|
...(receipt.missingIds ? { missingIds: receipt.missingIds } : {}),
|
|
93
|
+
...(receipt.operationResults ? { operationResults: receipt.operationResults } : {}),
|
|
98
94
|
});
|
|
99
95
|
}
|
|
100
96
|
if (!ws.sendCommit) {
|
|
@@ -107,7 +103,6 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
107
103
|
clientTxId,
|
|
108
104
|
undefined, // use sendCommit's built-in 15s default; no per-call override
|
|
109
105
|
options?.reads,
|
|
110
|
-
options?.track,
|
|
111
106
|
);
|
|
112
107
|
} catch (err) {
|
|
113
108
|
// Wrap transport-level failures as connection errors so the transaction
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* without leaking bootstrap, storage, or WebSocket vocabulary into the core.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { ClaimLog as CoordinationClaimLog } from '@abloatai/transaction/
|
|
9
|
+
import { ClaimLog as CoordinationClaimLog } from '@abloatai/transaction/claims';
|
|
10
10
|
import type { ObservabilityProvider } from '../interfaces/index.js';
|
|
11
11
|
|
|
12
|
-
export { formatClaim, formatConflict } from '@abloatai/transaction/
|
|
13
|
-
export type { ClaimLogEntry } from '@abloatai/transaction/
|
|
12
|
+
export { formatClaim, formatConflict } from '@abloatai/transaction/claims';
|
|
13
|
+
export type { ClaimLogEntry } from '@abloatai/transaction/claims';
|
|
14
14
|
|
|
15
15
|
export class ClaimLog extends CoordinationClaimLog implements ObservabilityProvider {
|
|
16
16
|
// Client-lifecycle hooks the claim log has no notion of. Every body below is
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* mutations to your backend. The SDK ships sensible no-op defaults where it can.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { MutationCommitResultInput } from '@abloatai/transaction/
|
|
11
|
-
|
|
12
|
-
export type { ClaimEvent, ConflictEvent } from '@abloatai/transaction/coordination/events';
|
|
10
|
+
import type { MutationCommitResultInput } from '@abloatai/transaction/commit';
|
|
11
|
+
export type { ClaimEvent, ConflictEvent } from '@abloatai/transaction/claims/events';
|
|
13
12
|
import type { CoordinationObservability } from '@abloatai/transaction/observability';
|
|
14
13
|
export type { CoordinationObservability } from '@abloatai/transaction/observability';
|
|
15
14
|
|
|
@@ -231,8 +230,8 @@ export type MutationCommitResult = MutationCommitResultInput;
|
|
|
231
230
|
// re-exports without binding X in this module's scope, and `Pick<X, K>` on an
|
|
232
231
|
// unbound X silently yields all-required properties rather than a missing-name
|
|
233
232
|
// error at the Pick site.
|
|
234
|
-
export type { MutationOptions } from '@abloatai/transaction/resources/mutationOptions';
|
|
235
|
-
import type { MutationOptions } from '@abloatai/transaction/resources/mutationOptions';
|
|
233
|
+
export type { MutationOptions } from '@abloatai/transaction/client/resources/mutationOptions';
|
|
234
|
+
import type { MutationOptions } from '@abloatai/transaction/client/resources/mutationOptions';
|
|
236
235
|
|
|
237
236
|
/**
|
|
238
237
|
* The subset of {@link MutationOptions} that travels with each write as it is
|
|
@@ -243,7 +242,7 @@ import type { MutationOptions } from '@abloatai/transaction/resources/mutationOp
|
|
|
243
242
|
*/
|
|
244
243
|
export type WriteOptions = Pick<
|
|
245
244
|
MutationOptions,
|
|
246
|
-
'readAt' | '
|
|
245
|
+
'readAt' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'
|
|
247
246
|
>;
|
|
248
247
|
|
|
249
248
|
/** A single mutation within a batch. Its `options` travel with it so the server
|
|
@@ -268,7 +267,6 @@ export interface MutationOperation {
|
|
|
268
267
|
/** Server-issued claim identity the operation is attributed to. */
|
|
269
268
|
claimId?: string | null;
|
|
270
269
|
readAt?: number | null;
|
|
271
|
-
onStale?: OnStaleMode | null;
|
|
272
270
|
/**
|
|
273
271
|
* The fencing token (Option B) carried on the wire for this op — the held
|
|
274
272
|
* claim's token, validated against the entity's high-water at commit.
|
|
@@ -23,8 +23,9 @@ type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> =
|
|
|
23
23
|
|
|
24
24
|
export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
25
25
|
/**
|
|
26
|
-
* Create one entity, or an array of entities in a single tick.
|
|
27
|
-
*
|
|
26
|
+
* Create one entity, or an array of entities in a single tick. The id and
|
|
27
|
+
* the tenancy value default per row; every other column, audit timestamps
|
|
28
|
+
* included, comes from the declared fields.
|
|
28
29
|
*/
|
|
29
30
|
create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
|
|
30
31
|
create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
|
package/src/local/query/types.ts
CHANGED
|
@@ -22,8 +22,8 @@ export type {
|
|
|
22
22
|
WhereOp,
|
|
23
23
|
WhereClause,
|
|
24
24
|
LoadWhere,
|
|
25
|
-
} from '@abloatai/transaction/resources/where';
|
|
26
|
-
import type { WhereClause } from '@abloatai/transaction/resources/where';
|
|
25
|
+
} from '@abloatai/transaction/client/resources/where';
|
|
26
|
+
import type { WhereClause } from '@abloatai/transaction/client/resources/where';
|
|
27
27
|
import type { ModelListEvidence } from '@abloatai/transaction/wire';
|
|
28
28
|
|
|
29
29
|
/** A single structured fetch request. */
|
|
@@ -38,7 +38,7 @@ import type { RuntimeContext } from '../RuntimeContext.js';
|
|
|
38
38
|
import { postQuery } from '../query/client.js';
|
|
39
39
|
import type { RecoveryClass } from '@abloatai/transaction/errorCodes';
|
|
40
40
|
import type { LoadWhere, Query, WhereClause, WhereOp } from '../query/types.js';
|
|
41
|
-
import { normalizeWhere } from '@abloatai/transaction/resources/where';
|
|
41
|
+
import { normalizeWhere } from '@abloatai/transaction/client/resources/where';
|
|
42
42
|
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
43
43
|
import type { LogPositionPort } from '../logPosition.js';
|
|
44
44
|
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { getContext } from '../context.js';
|
|
14
|
-
import { clientSyncDeltaSchema, type ClientSyncDelta } from '@abloatai/transaction/
|
|
14
|
+
import { clientSyncDeltaSchema, type ClientSyncDelta } from '@abloatai/transaction/observation';
|
|
15
15
|
import { drainProfilingEnabled, observeDrainStage } from './drainProfile.js';
|
|
16
16
|
import {
|
|
17
17
|
WsTransport,
|
|
18
18
|
type WsTransportOptions,
|
|
19
19
|
type EventMap,
|
|
20
20
|
type DefaultCollaborationEvents,
|
|
21
|
-
} from '@abloatai/transaction/transport/
|
|
21
|
+
} from '@abloatai/transaction/transport/websocket';
|
|
22
22
|
import { isRecord } from './wsFrameHandlers.js';
|
|
23
23
|
// Sync-position state (lastSyncId watermark, version vector, server cursor).
|
|
24
24
|
import { SyncCursor } from './syncCursor.js';
|
|
@@ -41,7 +41,7 @@ export type {
|
|
|
41
41
|
DefaultCollaborationEvents,
|
|
42
42
|
EventMap,
|
|
43
43
|
SyncWebSocketEventMap,
|
|
44
|
-
} from '@abloatai/transaction/transport/
|
|
44
|
+
} from '@abloatai/transaction/transport/websocket';
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* How often, while connected, the client polls for any deltas whose best-effort
|
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
export {
|
|
10
10
|
buildCommitFrame,
|
|
11
|
-
parseNotifications,
|
|
12
11
|
recordClaim,
|
|
13
12
|
type CommitAck,
|
|
14
13
|
type CommitFrameOperation,
|
|
15
14
|
type ClaimTracePorts,
|
|
16
|
-
} from '@abloatai/transaction/transport/
|
|
15
|
+
} from '@abloatai/transaction/transport/websocket';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination';
|
|
2
|
+
import { AbloStaleContextError } from '@abloatai/transaction/errors';
|
|
3
|
+
import type { InstanceCache } from '../InstanceCache.js';
|
|
4
|
+
import type { SyncDelta } from './SyncWebSocket.js';
|
|
5
|
+
|
|
6
|
+
export interface ContextOnChangeTransport {
|
|
7
|
+
subscribe(event: string, listener: (value?: unknown) => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Match context reads on the reactive client's existing delta connection. */
|
|
11
|
+
export function contextOnChange(
|
|
12
|
+
transport: ContextOnChangeTransport,
|
|
13
|
+
pool: Pick<InstanceCache, 'peek' | 'watermarks'>,
|
|
14
|
+
reads: readonly ReadDependency[],
|
|
15
|
+
listener: (error: AbloStaleContextError) => void,
|
|
16
|
+
): () => void {
|
|
17
|
+
let stopped = false;
|
|
18
|
+
const rowReads = reads.filter(
|
|
19
|
+
(read): read is Extract<ReadDependency, { model: string }> => 'model' in read,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const stopDelta = transport.subscribe('delta', (value) => {
|
|
23
|
+
if (isDelta(value)) changedBy([value]);
|
|
24
|
+
});
|
|
25
|
+
const stopBatch = transport.subscribe('delta_batch', (value) => {
|
|
26
|
+
if (Array.isArray(value)) changedBy(value.filter(isDelta));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function stop(): void {
|
|
30
|
+
if (stopped) return;
|
|
31
|
+
stopped = true;
|
|
32
|
+
stopDelta();
|
|
33
|
+
stopBatch();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function changedBy(deltas: readonly SyncDelta[]): void {
|
|
37
|
+
if (stopped) return;
|
|
38
|
+
for (const delta of deltas) {
|
|
39
|
+
const read = rowReads.find(
|
|
40
|
+
(candidate) =>
|
|
41
|
+
candidate.model.toLowerCase() === delta.modelName.toLowerCase() &&
|
|
42
|
+
candidate.id === delta.modelId &&
|
|
43
|
+
delta.id > candidate.readAt,
|
|
44
|
+
);
|
|
45
|
+
if (!read) continue;
|
|
46
|
+
stop();
|
|
47
|
+
listener(staleError(read.model, read.id, read.readAt, delta.id));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Subscribe first, then inspect the resident row. A delta that lands between
|
|
53
|
+
// the original read and this attachment is either observed above or has
|
|
54
|
+
// already advanced this exact row in the pool.
|
|
55
|
+
for (const read of rowReads) {
|
|
56
|
+
const resident = pool.peek(read.id);
|
|
57
|
+
if (!resident || resident.getModelName().toLowerCase() !== read.model.toLowerCase()) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const observed = pool.watermarks.of(resident);
|
|
61
|
+
if (observed !== undefined && observed > read.readAt) {
|
|
62
|
+
stop();
|
|
63
|
+
listener(staleError(read.model, read.id, read.readAt, observed));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return stop;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function staleError(
|
|
72
|
+
model: string,
|
|
73
|
+
id: string,
|
|
74
|
+
readAt: number,
|
|
75
|
+
observedSyncId: number,
|
|
76
|
+
): AbloStaleContextError {
|
|
77
|
+
return new AbloStaleContextError('Context changed after read.', {
|
|
78
|
+
code: 'stale_context',
|
|
79
|
+
readAt,
|
|
80
|
+
conflicts: [{ model, id, observedSyncId }],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDelta(value: unknown): value is SyncDelta {
|
|
85
|
+
return (
|
|
86
|
+
typeof value === 'object' &&
|
|
87
|
+
value !== null &&
|
|
88
|
+
typeof (value as { id?: unknown }).id === 'number' &&
|
|
89
|
+
typeof (value as { modelName?: unknown }).modelName === 'string' &&
|
|
90
|
+
typeof (value as { modelId?: unknown }).modelId === 'string'
|
|
91
|
+
);
|
|
92
|
+
}
|