@abloatai/humans 0.57.0 → 0.59.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 +10 -1
- package/dist/local/SyncClient.js +5 -0
- 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} +11 -10
- package/dist/local/client/{createModelProxy.js → createModelOperations.js} +240 -313
- package/dist/local/client/options.d.ts +2 -2
- package/dist/local/client/reactiveEngine.js +34 -45
- package/dist/local/client/resourceTypes.d.ts +3 -3
- package/dist/local/client/resourceTypes.js +1 -1
- package/dist/local/client/schemaConfig.js +1 -0
- package/dist/local/client/wsMutationExecutor.d.ts +4 -4
- package/dist/local/client/wsMutationExecutor.js +3 -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 +10 -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/BootstrapFetcher.js +4 -4
- 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/schemaDrift.d.ts +15 -1
- package/dist/local/sync/schemaDrift.js +30 -13
- 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/transactions/databaseCommitOutbox.d.ts +2 -2
- package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -19
- package/dist/local/transactions/mutations/MutationQueue.js +5 -70
- 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 +3 -8
- package/dist/local/transactions/mutations/commitLane.js +0 -11
- 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/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 +1 -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 -1
- package/src/local/Model.ts +12 -1
- package/src/local/SyncClient.ts +5 -0
- package/src/local/client/createInternalComponents.ts +2 -2
- package/src/local/client/{createModelProxy.ts → createModelOperations.ts} +292 -367
- package/src/local/client/options.ts +2 -2
- package/src/local/client/reactiveEngine.ts +63 -55
- package/src/local/client/resourceTypes.ts +4 -4
- package/src/local/client/schemaConfig.ts +3 -0
- package/src/local/client/wsMutationExecutor.ts +3 -9
- package/src/local/coordination/ClaimLog.ts +3 -3
- package/src/local/interfaces/index.ts +7 -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/BootstrapFetcher.ts +5 -4
- 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/schemaDrift.ts +32 -16
- package/src/local/sync/schemas.ts +1 -1
- package/src/local/sync/wsFrameHandlers.ts +1 -1
- package/src/local/transactions/databaseCommitOutbox.ts +2 -2
- package/src/local/transactions/mutations/MutationQueue.ts +8 -95
- 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 +3 -17
- 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/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 +2 -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,
|
|
@@ -593,20 +606,11 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
593
606
|
// only runs when someone actually writes a batch.
|
|
594
607
|
commitBatch: (commitOptions) => commits.create(commitOptions),
|
|
595
608
|
readPoint,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
// `readAt` (max of the pool-applied cursor and the acked
|
|
602
|
-
// watermark for our own writes — see logPosition.ts).
|
|
603
|
-
// Stamping a bare stream cursor made a claim taken right after
|
|
604
|
-
// an ack-confirmed write stale against that write's own delta.
|
|
605
|
-
// The socket/store cursors are persistence-gated and therefore
|
|
606
|
-
// never ahead of `applied` — no extra max() needed here.
|
|
607
|
-
getLastSyncId: () => syncClient.position.readFloor,
|
|
608
|
-
entities: { [modelKey]: id },
|
|
609
|
-
}),
|
|
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,
|
|
610
614
|
queue: (target) =>
|
|
611
615
|
publicClaims.queueFor(streamTarget(target)),
|
|
612
616
|
reorder: (target, order) =>
|
|
@@ -656,31 +660,39 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
656
660
|
);
|
|
657
661
|
}
|
|
658
662
|
|
|
659
|
-
const commits: CommitResource = {
|
|
660
|
-
async create(
|
|
663
|
+
const commits: CommitResource<ReadDependency | CapturedRow> = {
|
|
664
|
+
async create(
|
|
665
|
+
commitOptions: CommitCreateOptions<ReadDependency | CapturedRow>,
|
|
666
|
+
): Promise<CommitReceipt> {
|
|
661
667
|
await ready();
|
|
668
|
+
const prepared = prepareReadSet(
|
|
669
|
+
cluster.readSetContext,
|
|
670
|
+
syncClient as object,
|
|
671
|
+
commitOptions.readAt,
|
|
672
|
+
commitOptions.idempotencyKey,
|
|
673
|
+
commitOptions.reads,
|
|
674
|
+
);
|
|
662
675
|
// Same runtime contract as the per-model writes — one schema.
|
|
663
676
|
assertWriteOptions(
|
|
664
677
|
{
|
|
665
|
-
idempotencyKey:
|
|
666
|
-
readAt:
|
|
667
|
-
onStale: commitOptions.onStale,
|
|
678
|
+
idempotencyKey: prepared.idempotencyKey,
|
|
679
|
+
readAt: prepared.readAt,
|
|
668
680
|
wait: commitOptions.wait,
|
|
669
681
|
claim: commitOptions.claim,
|
|
682
|
+
reads: prepared.reads,
|
|
670
683
|
},
|
|
671
684
|
'commits.create',
|
|
672
685
|
);
|
|
673
|
-
const clientTxId = createClientTxId(
|
|
686
|
+
const clientTxId = createClientTxId(prepared.idempotencyKey);
|
|
674
687
|
// A claim handle supplies the batch stale-guard defaults — same
|
|
675
688
|
// semantics as `ablo.<model>.update({ id, data, claim })`, so the
|
|
676
689
|
// two write doors speak one claim vocabulary. Explicit options win.
|
|
677
690
|
const claim = commitOptions.claim ?? null;
|
|
678
691
|
const operations = normalizeCommitOperations(
|
|
679
692
|
{
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
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,
|
|
684
696
|
},
|
|
685
697
|
batchFence(claim?.target, claim?.fenceToken),
|
|
686
698
|
);
|
|
@@ -696,21 +708,19 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
696
708
|
// no need to leak an accessor through BaseSyncedStore.
|
|
697
709
|
const queue = syncClient.getMutationQueue();
|
|
698
710
|
await queue.enqueueCommit(clientTxId, operations, {
|
|
699
|
-
...(
|
|
700
|
-
...(commitOptions.track ? { track: [...commitOptions.track] } : {}),
|
|
711
|
+
...(prepared.reads ? { reads: [...prepared.reads] } : {}),
|
|
701
712
|
});
|
|
702
713
|
|
|
703
714
|
if (wait === 'queued') {
|
|
704
715
|
return { id: clientTxId, status: 'queued' };
|
|
705
716
|
}
|
|
706
717
|
|
|
707
|
-
const { lastSyncId,
|
|
718
|
+
const { lastSyncId, missingIds, operationResults } =
|
|
708
719
|
await queue.waitForCommitReceipt(clientTxId);
|
|
709
720
|
return {
|
|
710
721
|
id: clientTxId,
|
|
711
722
|
status: 'confirmed',
|
|
712
723
|
lastSyncId,
|
|
713
|
-
...(notifications && notifications.length > 0 ? { notifications } : {}),
|
|
714
724
|
...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
|
|
715
725
|
...(operationResults && operationResults.length > 0 ? { operationResults } : {}),
|
|
716
726
|
};
|
|
@@ -905,6 +915,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
905
915
|
const sessionParams = {
|
|
906
916
|
agent: { id },
|
|
907
917
|
can: params.can,
|
|
918
|
+
...(params.onBehalfOf ? { onBehalfOf: params.onBehalfOf } : {}),
|
|
908
919
|
...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
|
|
909
920
|
...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
|
|
910
921
|
...(userMeta ? { userMeta } : {}),
|
|
@@ -1018,21 +1029,18 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
1018
1029
|
|
|
1019
1030
|
commits,
|
|
1020
1031
|
|
|
1021
|
-
/** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
|
|
1022
|
-
snapshot<ModelName extends keyof S & string>(
|
|
1023
|
-
entities: Readonly<Record<ModelName, string | readonly string[]>>,
|
|
1024
|
-
): Snapshot<Schema<S>, ModelName> {
|
|
1025
|
-
return createSnapshot<Schema<S>, ModelName>({
|
|
1026
|
-
pool: objectPool,
|
|
1027
|
-
transport,
|
|
1028
|
-
getLastSyncId: () => transport.getLastSyncId(),
|
|
1029
|
-
entities,
|
|
1030
|
-
});
|
|
1031
|
-
},
|
|
1032
1032
|
} as Ablo<S>;
|
|
1033
1033
|
|
|
1034
1034
|
Object.defineProperty(engine, kReadEvidence, {
|
|
1035
|
-
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
|
+
},
|
|
1036
1044
|
enumerable: false,
|
|
1037
1045
|
});
|
|
1038
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';
|
|
@@ -185,6 +185,9 @@ export function deriveConfigFromSchema(schema: Schema): RuntimeConfig {
|
|
|
185
185
|
expectedModelHashes: Object.fromEntries(
|
|
186
186
|
Object.entries(toSchemaJSON(schema).models).map(([key, model]) => [key, modelHash(model)]),
|
|
187
187
|
),
|
|
188
|
+
expectedModelShapes: Object.fromEntries(
|
|
189
|
+
Object.entries(toSchemaJSON(schema).models).map(([key, model]) => [key, Object.fromEntries(Object.entries(model.fields).map(([field, meta]) => [field, { type: meta.type, isOptional: meta.isOptional }]))]),
|
|
190
|
+
),
|
|
188
191
|
// For a projection (`selectModels`/`omitModels`), also carry the full source
|
|
189
192
|
// schema's hash. The drift check accepts a server match on either hash, so a
|
|
190
193
|
// subset client stays quiet against a server running its full source schema.
|
|
@@ -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,20 +78,17 @@ 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 } : {}),
|
|
98
93
|
...(receipt.operationResults ? { operationResults: receipt.operationResults } : {}),
|
|
99
94
|
});
|
|
@@ -108,7 +103,6 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
|
108
103
|
clientTxId,
|
|
109
104
|
undefined, // use sendCommit's built-in 15s default; no per-call override
|
|
110
105
|
options?.reads,
|
|
111
|
-
options?.track,
|
|
112
106
|
);
|
|
113
107
|
} catch (err) {
|
|
114
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.
|
|
@@ -438,6 +436,8 @@ export interface RuntimeConfig {
|
|
|
438
436
|
* Advisory, like the hashes above.
|
|
439
437
|
*/
|
|
440
438
|
expectedModelHashes?: Readonly<Record<string, string>>;
|
|
439
|
+
/** Field shapes paired with expectedModelHashes so drift can name direction, not just a model. */
|
|
440
|
+
expectedModelShapes?: Readonly<Record<string, Readonly<Record<string, { readonly type: string; readonly isOptional: boolean }>>>>;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
// ─────────────────────────────────────────────
|
|
@@ -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. */
|
|
@@ -293,7 +293,7 @@ export class BootstrapFetcher {
|
|
|
293
293
|
// network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
|
|
294
294
|
const clientModels = this.runtime.config.expectedModelHashes;
|
|
295
295
|
if (clientModels && Object.keys(clientModels).length > 0) {
|
|
296
|
-
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
|
|
296
|
+
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where, this.runtime.config.expectedModelShapes);
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
this.warnWholeHashDrift(clientHash, serverHash, where);
|
|
@@ -306,6 +306,7 @@ export class BootstrapFetcher {
|
|
|
306
306
|
clientHash: string,
|
|
307
307
|
serverHash: string,
|
|
308
308
|
where: string,
|
|
309
|
+
clientShapes: NonNullable<typeof this.runtime.config.expectedModelShapes> | undefined,
|
|
309
310
|
): Promise<void> {
|
|
310
311
|
try {
|
|
311
312
|
const res = await fetch(`${this.options.baseUrl}/schema`, {
|
|
@@ -316,13 +317,13 @@ export class BootstrapFetcher {
|
|
|
316
317
|
const body = (await res.json()) as { models?: unknown };
|
|
317
318
|
const models = Array.isArray(body.models)
|
|
318
319
|
? body.models.flatMap((m): ServerSchemaModel[] => {
|
|
319
|
-
const entry = m as { key?: unknown; hash?: unknown };
|
|
320
|
+
const entry = m as { key?: unknown; hash?: unknown; fields?: unknown };
|
|
320
321
|
return typeof entry.key === 'string'
|
|
321
|
-
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}) }]
|
|
322
|
+
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}), ...(entry.fields && typeof entry.fields === 'object' ? { fields: entry.fields as ServerSchemaModel['fields'] } : {}) }]
|
|
322
323
|
: [];
|
|
323
324
|
})
|
|
324
325
|
: [];
|
|
325
|
-
const finding = classifySchemaDrift(clientModels, models);
|
|
326
|
+
const finding = classifySchemaDrift(clientModels, models, clientShapes);
|
|
326
327
|
if (finding.kind === 'aligned') return; // additive server lead — not this client's concern
|
|
327
328
|
if (finding.kind !== 'unknown') {
|
|
328
329
|
this.runtime.logger.warn(describeSchemaDrift(finding, where), {
|
|
@@ -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
|
+
}
|