@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.
Files changed (130) hide show
  1. package/dist/Ablo.d.ts +8 -18
  2. package/dist/client.d.ts +6 -19
  3. package/dist/core.d.ts +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/local/Database.d.ts +1 -1
  6. package/dist/local/Model.js +10 -1
  7. package/dist/local/SyncClient.js +5 -0
  8. package/dist/local/client/createInternalComponents.d.ts +1 -1
  9. package/dist/local/client/createInternalComponents.js +1 -1
  10. package/dist/local/client/{createModelProxy.d.ts → createModelOperations.d.ts} +11 -10
  11. package/dist/local/client/{createModelProxy.js → createModelOperations.js} +240 -313
  12. package/dist/local/client/options.d.ts +2 -2
  13. package/dist/local/client/reactiveEngine.js +34 -45
  14. package/dist/local/client/resourceTypes.d.ts +3 -3
  15. package/dist/local/client/resourceTypes.js +1 -1
  16. package/dist/local/client/schemaConfig.js +1 -0
  17. package/dist/local/client/wsMutationExecutor.d.ts +4 -4
  18. package/dist/local/client/wsMutationExecutor.js +3 -5
  19. package/dist/local/coordination/ClaimLog.d.ts +3 -3
  20. package/dist/local/coordination/ClaimLog.js +2 -2
  21. package/dist/local/interfaces/index.d.ts +10 -7
  22. package/dist/local/mutators/mutateActions.d.ts +3 -2
  23. package/dist/local/query/types.d.ts +2 -2
  24. package/dist/local/stores/syncAction.d.ts +1 -1
  25. package/dist/local/stores/syncAction.js +1 -1
  26. package/dist/local/sync/BootstrapFetcher.js +4 -4
  27. package/dist/local/sync/ConnectionManager.d.ts +1 -1
  28. package/dist/local/sync/ConnectionManager.js +1 -1
  29. package/dist/local/sync/OnDemandLoader.d.ts +1 -1
  30. package/dist/local/sync/OnDemandLoader.js +1 -1
  31. package/dist/local/sync/SyncWebSocket.d.ts +3 -3
  32. package/dist/local/sync/SyncWebSocket.js +2 -2
  33. package/dist/local/sync/commitFrames.d.ts +1 -1
  34. package/dist/local/sync/commitFrames.js +1 -1
  35. package/dist/local/sync/contextOnChange.d.ts +8 -0
  36. package/dist/local/sync/contextOnChange.js +65 -0
  37. package/dist/local/sync/createClaimStream.d.ts +1 -1
  38. package/dist/local/sync/createClaimStream.js +47 -28
  39. package/dist/local/sync/credentialLifecycle.d.ts +1 -1
  40. package/dist/local/sync/credentialLifecycle.js +1 -1
  41. package/dist/local/sync/participants.d.ts +1 -1
  42. package/dist/local/sync/schemaDrift.d.ts +15 -1
  43. package/dist/local/sync/schemaDrift.js +30 -13
  44. package/dist/local/sync/schemas.d.ts +2 -2
  45. package/dist/local/sync/schemas.js +1 -1
  46. package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
  47. package/dist/local/sync/wsFrameHandlers.js +1 -1
  48. package/dist/local/transactions/databaseCommitOutbox.d.ts +2 -2
  49. package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -19
  50. package/dist/local/transactions/mutations/MutationQueue.js +5 -70
  51. package/dist/local/transactions/mutations/batchProcessing.d.ts +2 -13
  52. package/dist/local/transactions/mutations/batchProcessing.js +0 -20
  53. package/dist/local/transactions/mutations/commitApi.d.ts +2 -3
  54. package/dist/local/transactions/mutations/commitApi.js +0 -4
  55. package/dist/local/transactions/mutations/commitLane.d.ts +3 -8
  56. package/dist/local/transactions/mutations/commitLane.js +0 -11
  57. package/dist/local/transactions/mutations/commitPayload.d.ts +2 -4
  58. package/dist/local/transactions/mutations/commitPayload.js +2 -6
  59. package/dist/local/transactions/mutations/commitTransport.d.ts +3 -4
  60. package/dist/local/transactions/mutations/commitTransport.js +2 -5
  61. package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
  62. package/dist/local/transactions/mutations/durableCommitRestore.js +1 -4
  63. package/dist/local/transactions/mutations/durableWriteStore.d.ts +5 -5
  64. package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
  65. package/dist/local/transactions/mutations/failurePolicy.d.ts +2 -2
  66. package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
  67. package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
  68. package/dist/local/transactions/mutations/replayValidation.d.ts +0 -20
  69. package/dist/local/transactions/mutations/replayValidation.js +1 -3
  70. package/dist/local/transactions/persistedTransaction.d.ts +0 -2
  71. package/dist/plugin.d.ts +2 -2
  72. package/dist/presenceStream.d.ts +1 -1
  73. package/dist/react/createAbloReact.d.ts +1 -1
  74. package/dist/react/useAblo.d.ts +2 -2
  75. package/dist/react/useAblo.js +1 -1
  76. package/dist/surface.d.ts +1 -1
  77. package/dist/surface.js +1 -2
  78. package/package.json +2 -2
  79. package/src/Ablo.ts +9 -27
  80. package/src/client.ts +6 -22
  81. package/src/core.ts +1 -1
  82. package/src/local/Database.ts +1 -1
  83. package/src/local/Model.ts +12 -1
  84. package/src/local/SyncClient.ts +5 -0
  85. package/src/local/client/createInternalComponents.ts +2 -2
  86. package/src/local/client/{createModelProxy.ts → createModelOperations.ts} +292 -367
  87. package/src/local/client/options.ts +2 -2
  88. package/src/local/client/reactiveEngine.ts +63 -55
  89. package/src/local/client/resourceTypes.ts +4 -4
  90. package/src/local/client/schemaConfig.ts +3 -0
  91. package/src/local/client/wsMutationExecutor.ts +3 -9
  92. package/src/local/coordination/ClaimLog.ts +3 -3
  93. package/src/local/interfaces/index.ts +7 -7
  94. package/src/local/mutators/mutateActions.ts +3 -2
  95. package/src/local/query/types.ts +2 -2
  96. package/src/local/stores/syncAction.ts +1 -1
  97. package/src/local/sync/BootstrapFetcher.ts +5 -4
  98. package/src/local/sync/ConnectionManager.ts +1 -1
  99. package/src/local/sync/OnDemandLoader.ts +1 -1
  100. package/src/local/sync/SyncWebSocket.ts +3 -3
  101. package/src/local/sync/commitFrames.ts +1 -2
  102. package/src/local/sync/contextOnChange.ts +92 -0
  103. package/src/local/sync/createClaimStream.ts +77 -39
  104. package/src/local/sync/credentialLifecycle.ts +1 -1
  105. package/src/local/sync/participants.ts +1 -1
  106. package/src/local/sync/schemaDrift.ts +32 -16
  107. package/src/local/sync/schemas.ts +1 -1
  108. package/src/local/sync/wsFrameHandlers.ts +1 -1
  109. package/src/local/transactions/databaseCommitOutbox.ts +2 -2
  110. package/src/local/transactions/mutations/MutationQueue.ts +8 -95
  111. package/src/local/transactions/mutations/batchProcessing.ts +2 -38
  112. package/src/local/transactions/mutations/commitApi.ts +3 -7
  113. package/src/local/transactions/mutations/commitLane.ts +3 -17
  114. package/src/local/transactions/mutations/commitPayload.ts +3 -11
  115. package/src/local/transactions/mutations/commitTransport.ts +3 -7
  116. package/src/local/transactions/mutations/durableCommitRestore.ts +2 -5
  117. package/src/local/transactions/mutations/durableWriteStore.ts +5 -5
  118. package/src/local/transactions/mutations/failurePolicy.ts +2 -2
  119. package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
  120. package/src/local/transactions/mutations/pendingDrain.ts +2 -2
  121. package/src/local/transactions/mutations/replayValidation.ts +1 -3
  122. package/src/local/transactions/persistedTransaction.ts +0 -3
  123. package/src/plugin.ts +2 -2
  124. package/src/presenceStream.ts +2 -2
  125. package/src/react/createAbloReact.ts +1 -1
  126. package/src/react/useAblo.ts +2 -2
  127. package/src/surface.ts +2 -3
  128. package/dist/local/sync/createSnapshot.d.ts +0 -29
  129. package/dist/local/sync/createSnapshot.js +0 -116
  130. 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/durableWrites';
23
- import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
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/transactions/confirmation/commitEnvelope';
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/coordination/awaitClaimGrant';
45
- import { createSnapshot } from '../sync/createSnapshot.js';
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, Snapshot } from '@abloatai/transaction/types/streams';
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 { createModelProxy, type ModelOperations } from './createModelProxy.js';
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 { translateHttpError } from '@abloatai/transaction/errors';
80
- import { kReadEvidence } from '@abloatai/transaction/internal/read-set';
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' | 'onStale'>,
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
- return {
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] = createModelProxy(
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
- createSnapshot: (modelKey, id) =>
597
- createSnapshot({
598
- pool: objectPool,
599
- transport,
600
- // `position.readFloor` is the value claims and snapshots stamp as
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(commitOptions: CommitCreateOptions): Promise<CommitReceipt> {
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: commitOptions.idempotencyKey,
666
- readAt: commitOptions.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(commitOptions.idempotencyKey);
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
- ...commitOptions,
681
- readAt: commitOptions.readAt ?? claim?.readAt ?? null,
682
- onStale:
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
- ...(commitOptions.reads ? { reads: [...commitOptions.reads] } : {}),
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, notifications, missingIds, operationResults } =
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: { context: cluster.readSetContext, client: syncClient as object },
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
- ModelRetrieveParams,
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 './createModelProxy.js';
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, TrackDependency } from '@abloatai/transaction/coordination/schema';
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/wire/commit';
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/wire/commit';
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/coordination/trace';
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/coordination/trace';
13
- export type { ClaimLogEntry } from '@abloatai/transaction/coordination/trace';
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/wire/commit';
11
- import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
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' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'
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. ID,
27
- * createdAt, updatedAt, organizationId default automatically per row.
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>[]>;
@@ -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. */
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { z } from 'zod';
9
- import { syncDeltaActionSchema } from '@abloatai/transaction/wire/delta';
9
+ import { syncDeltaActionSchema } from '@abloatai/transaction/observation';
10
10
 
11
11
  export const syncActionSchema = z.object({
12
12
  id: z.number(),
@@ -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), {
@@ -12,4 +12,4 @@ export {
12
12
  type ConnectionEvent,
13
13
  type ConnectionCallbacks,
14
14
  type ConnectionManagerOptions,
15
- } from '@abloatai/transaction/transport/connectionManager';
15
+ } from '@abloatai/transaction/transport/connection';
@@ -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/wire/delta';
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/wsTransport';
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/wsTransport';
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/commitFrames';
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
+ }