@abloatai/ablo 0.27.0 → 0.29.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 (126) hide show
  1. package/CHANGELOG.md +59 -3
  2. package/README.md +1 -1
  3. package/dist/BaseSyncedStore.js +8 -9
  4. package/dist/Database.d.ts +14 -1
  5. package/dist/Database.js +228 -28
  6. package/dist/Model.d.ts +17 -0
  7. package/dist/Model.js +32 -1
  8. package/dist/SyncClient.d.ts +10 -6
  9. package/dist/SyncClient.js +390 -75
  10. package/dist/adapters/inMemoryStorage.d.ts +1 -0
  11. package/dist/adapters/inMemoryStorage.js +29 -13
  12. package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
  13. package/dist/auth/schemas.d.ts +6 -0
  14. package/dist/auth/schemas.js +7 -0
  15. package/dist/cli.cjs +821 -402
  16. package/dist/client/Ablo.d.ts +53 -24
  17. package/dist/client/Ablo.js +12 -199
  18. package/dist/client/claimHeartbeatLoop.d.ts +1 -1
  19. package/dist/client/claimHeartbeatLoop.js +1 -1
  20. package/dist/client/createInternalComponents.d.ts +4 -0
  21. package/dist/client/createInternalComponents.js +3 -1
  22. package/dist/client/durableWrites.d.ts +21 -0
  23. package/dist/client/durableWrites.js +46 -0
  24. package/dist/client/httpClient.d.ts +21 -45
  25. package/dist/client/httpClient.js +104 -26
  26. package/dist/client/httpTransport.d.ts +8 -0
  27. package/dist/client/{ApiClient.js → httpTransport.js} +361 -308
  28. package/dist/client/modelRegistration.js +11 -0
  29. package/dist/client/options.d.ts +54 -7
  30. package/dist/client/options.js +3 -2
  31. package/dist/client/resourceTypes.d.ts +9 -85
  32. package/dist/client/resourceTypes.js +1 -1
  33. package/dist/client/sessionMint.d.ts +5 -6
  34. package/dist/client/sessionMint.js +4 -5
  35. package/dist/client/validateAbloOptions.js +3 -3
  36. package/dist/client/wsMutationExecutor.d.ts +3 -2
  37. package/dist/client/wsMutationExecutor.js +3 -2
  38. package/dist/coordination/schema.d.ts +30 -0
  39. package/dist/coordination/schema.js +14 -0
  40. package/dist/core/StoreManager.d.ts +2 -0
  41. package/dist/core/StoreManager.js +12 -0
  42. package/dist/core/index.d.ts +1 -1
  43. package/dist/errorCodes.js +6 -2
  44. package/dist/errors.d.ts +4 -40
  45. package/dist/errors.js +5 -5
  46. package/dist/index.d.ts +20 -8
  47. package/dist/index.js +9 -5
  48. package/dist/interfaces/index.d.ts +5 -2
  49. package/dist/mutators/UndoManager.d.ts +2 -0
  50. package/dist/mutators/UndoManager.js +32 -0
  51. package/dist/mutators/defineMutators.d.ts +18 -0
  52. package/dist/mutators/defineMutators.js +4 -8
  53. package/dist/react/index.d.ts +1 -1
  54. package/dist/react/useAblo.d.ts +6 -4
  55. package/dist/react/useAblo.js +25 -3
  56. package/dist/schema/index.d.ts +2 -2
  57. package/dist/schema/index.js +1 -1
  58. package/dist/schema/schema.d.ts +31 -1
  59. package/dist/schema/select.d.ts +15 -0
  60. package/dist/schema/select.js +27 -3
  61. package/dist/source/connector.d.ts +3 -3
  62. package/dist/source/factory.d.ts +4 -6
  63. package/dist/source/factory.js +4 -7
  64. package/dist/source/index.d.ts +4 -4
  65. package/dist/source/index.js +2 -2
  66. package/dist/source/signing.d.ts +0 -3
  67. package/dist/source/types.d.ts +0 -26
  68. package/dist/stores/ObjectStore.d.ts +14 -1
  69. package/dist/stores/ObjectStore.js +33 -10
  70. package/dist/stores/ObjectStoreContract.d.ts +2 -0
  71. package/dist/surface.d.ts +1 -1
  72. package/dist/surface.js +3 -0
  73. package/dist/sync/BootstrapFetcher.d.ts +10 -0
  74. package/dist/sync/BootstrapFetcher.js +27 -4
  75. package/dist/sync/SyncWebSocket.d.ts +2 -1
  76. package/dist/sync/createClaimStream.d.ts +11 -2
  77. package/dist/sync/createClaimStream.js +4 -3
  78. package/dist/sync/persistedPrefix.d.ts +12 -0
  79. package/dist/sync/persistedPrefix.js +22 -0
  80. package/dist/testing/index.d.ts +2 -0
  81. package/dist/testing/index.js +1 -0
  82. package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
  83. package/dist/testing/mocks/FakeDatabase.js +10 -0
  84. package/dist/testing/mocks/MockWebSocket.d.ts +2 -1
  85. package/dist/transactions/TransactionQueue.d.ts +66 -8
  86. package/dist/transactions/TransactionQueue.js +607 -89
  87. package/dist/transactions/commitEnvelope.d.ts +132 -0
  88. package/dist/transactions/commitEnvelope.js +139 -0
  89. package/dist/transactions/commitOutboxStore.d.ts +32 -0
  90. package/dist/transactions/commitOutboxStore.js +26 -0
  91. package/dist/transactions/commitPayload.d.ts +15 -0
  92. package/dist/transactions/commitPayload.js +6 -0
  93. package/dist/transactions/durableWriteStore.d.ts +123 -0
  94. package/dist/transactions/durableWriteStore.js +30 -0
  95. package/dist/transactions/httpCommitEnvelope.d.ts +44 -0
  96. package/dist/transactions/httpCommitEnvelope.js +181 -0
  97. package/dist/transactions/idempotencyKey.d.ts +10 -0
  98. package/dist/transactions/idempotencyKey.js +9 -0
  99. package/dist/transactions/replayValidation.d.ts +83 -0
  100. package/dist/transactions/replayValidation.js +46 -1
  101. package/dist/types/global.d.ts +10 -29
  102. package/dist/types/global.js +4 -7
  103. package/dist/types/streams.d.ts +2 -28
  104. package/dist/wire/bootstrapReason.d.ts +9 -0
  105. package/dist/wire/bootstrapReason.js +8 -0
  106. package/dist/wire/frames.d.ts +6 -21
  107. package/dist/wire/frames.js +4 -4
  108. package/dist/wire/index.d.ts +6 -3
  109. package/dist/wire/index.js +3 -2
  110. package/dist/wire/protocolVersion.d.ts +30 -17
  111. package/dist/wire/protocolVersion.js +34 -18
  112. package/docs/agents.md +8 -3
  113. package/docs/api.md +16 -14
  114. package/docs/client-behavior.md +6 -3
  115. package/docs/coordination.md +4 -5
  116. package/docs/data-sources.md +5 -8
  117. package/docs/guarantees.md +21 -0
  118. package/docs/integration-guide.md +1 -0
  119. package/docs/mcp.md +1 -1
  120. package/docs/migration.md +21 -1
  121. package/docs/quickstart.md +11 -0
  122. package/docs/react.md +0 -46
  123. package/examples/README.md +6 -5
  124. package/llms.txt +15 -15
  125. package/package.json +3 -3
  126. package/dist/client/ApiClient.d.ts +0 -177
@@ -18,7 +18,7 @@
18
18
  * });
19
19
  * await sync.reports.delete({ id: reportId });
20
20
  */
21
- import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
21
+ import type { Schema, SchemaRecord, InferModel, InferCreate, InferRow } from '../schema/schema.js';
22
22
  import type { ModelTarget, ModelClaim } from '../coordination/schema.js';
23
23
  export type { ModelTarget, ModelClaim };
24
24
  import { InstanceCache } from '../InstanceCache.js';
@@ -26,14 +26,25 @@ import type { SyncStoreContract } from '../react/context.js';
26
26
  import type { SyncWebSocket } from '../sync/SyncWebSocket.js';
27
27
  import { type SyncStatus } from '../BaseSyncedStore.js';
28
28
  import type { PresenceStream, Snapshot } from '../types/streams.js';
29
- import { type AbloApi, type AbloApiClientOptions, type AbloApiClaims } from './ApiClient.js';
30
29
  import { type AbloHttpClient, type AbloHttpClientOptions } from './httpClient.js';
31
30
  import type { AbloOptions } from './options.js';
32
- import type { AbloSession, CommitResource, CreateAgentClientParams, CreateSessionParams, ModelClient } from './resourceTypes.js';
31
+ import type { AbloSession, CommitResource, CreateAgentClientParams, CreateSessionParams } from './resourceTypes.js';
33
32
  export type { ApiKeySetter, AbloOptions, InternalAbloOptions } from './options.js';
34
- export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, ModelOperations, ModelOperationAction, CommitWait, ModelRead, IfClaimedPolicy, ClaimedOptions, ClaimWaitOptions, ModelReadOptions, ClaimCreateOptions, CommitOperationInput, CommitCreateOptions, CommitReceipt, CommitResource, ClaimResource, ModelMutationOptions, HttpClaimApi, ModelClient, SessionOperation, CreateUserSessionParams, CreateAgentSessionParams, CreateSessionParams, CreateAgentClientParams, AbloSession, } from './resourceTypes.js';
33
+ export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, ModelOperations, ModelOperationAction, CommitWait, IfClaimedPolicy, ClaimedOptions, ClaimWaitOptions, ModelReadOptions, ClaimCreateOptions, CommitOperationInput, CommitCreateOptions, CommitReceipt, CommitResource, ClaimResource, ModelMutationOptions, HttpClaimApi, SessionOperation, CreateUserSessionParams, CreateAgentSessionParams, CreateSessionParams, CreateAgentClientParams, AbloSession, } from './resourceTypes.js';
35
34
  export { computeFKDepthPriority } from './schemaConfig.js';
36
35
  import type { ModelOperations } from './createModelProxy.js';
36
+ /**
37
+ * The reactive-read client a `useAblo` selector receives. The same surface as
38
+ * {@link Ablo}, except model reads are typed as reactive rows
39
+ * ({@link InferRow}: data fields + computeds, no relation accessors, no model
40
+ * methods) — the shape a reactive read actually delivers after
41
+ * `toReactiveSnapshot()`. Reading `row.layers` off a reactive read is a
42
+ * compile error here instead of a silent runtime `undefined`; compose
43
+ * relations through selectors or hooks that resolve the pool's instance.
44
+ */
45
+ export type AbloReads<S extends SchemaRecord> = Omit<Ablo<S>, keyof S & string> & {
46
+ readonly [K in keyof S & string]: ModelOperations<InferRow<Schema<S>, K>, InferCreate<Schema<S>, K>>;
47
+ };
37
48
  /** The typed sync engine client — one property per model in the schema */
38
49
  export type Ablo<S extends SchemaRecord> = {
39
50
  readonly [K in keyof S & string]: ModelOperations<InferModel<Schema<S>, K>, InferCreate<Schema<S>, K>>;
@@ -260,12 +271,6 @@ export type Ablo<S extends SchemaRecord> = {
260
271
  * down to `commits.create(...)`.
261
272
  */
262
273
  readonly commits: CommitResource;
263
- /**
264
- * Canonical untyped model API. This is the portable API shape that maps
265
- * cleanly to HTTP/Python/Ruby/Go clients. Typed `ablo.<model>` properties
266
- * are schema-powered sugar over the same model write/read path.
267
- */
268
- model<T = Record<string, unknown>>(name: string): ModelClient<T>;
269
274
  /**
270
275
  * Capture a context-staleness watermark over a set of entities.
271
276
  * Returns a flat snapshot with `stamp` (thread into writes as
@@ -325,7 +330,6 @@ export declare function Ablo<const S extends SchemaRecord>(options: AbloHttpClie
325
330
  transport: 'http';
326
331
  }): AbloHttpClient<S>;
327
332
  export declare function Ablo<const S extends SchemaRecord>(options: AbloOptions<S>): Ablo<S>;
328
- export declare function Ablo(options: AbloApiClientOptions): AbloApi;
329
333
  import type * as _Streams from '../types/streams.js';
330
334
  import type * as _Participants from '../sync/participants.js';
331
335
  import type * as _Policy from '../policy/types.js';
@@ -333,6 +337,7 @@ import type * as _Mutators from '../mutators/defineMutators.js';
333
337
  import type * as _Tx from '../mutators/Transaction.js';
334
338
  import type * as _Undo from '../mutators/UndoManager.js';
335
339
  import type * as _SchemaTypes from '../schema/schema.js';
340
+ import type * as _Global from '../types/global.js';
336
341
  /**
337
342
  * The canonical type namespace.
338
343
  *
@@ -351,15 +356,11 @@ import type * as _SchemaTypes from '../schema/schema.js';
351
356
  */
352
357
  export declare namespace Ablo {
353
358
  type Options<S extends SchemaRecord = SchemaRecord> = AbloOptions<S>;
354
- type Api = AbloApi;
355
- type ApiClaims = AbloApiClaims;
356
- type Capability = import('./ApiClient.js').Capability;
357
- type CapabilityCreateOptions = import('./ApiClient.js').CapabilityCreateOptions;
358
- type CapabilityRecord = import('./ApiClient.js').CapabilityRecord;
359
- type CapabilityResource = import('./ApiClient.js').CapabilityResource;
360
- type CapabilityRevocation = import('./ApiClient.js').CapabilityRevocation;
361
- type CapabilityRotateOptions = import('./ApiClient.js').CapabilityRotateOptions;
362
- type RotatedCapability = import('./ApiClient.js').RotatedCapability;
359
+ /**
360
+ * The read view of the client that `useAblo` selectors receive: model reads
361
+ * typed as reactive rows (data fields + computeds, no relation accessors).
362
+ */
363
+ type Reads<S extends SchemaRecord = SchemaRecord> = AbloReads<S>;
363
364
  type IfClaimedPolicy = import('./resourceTypes.js').IfClaimedPolicy;
364
365
  type ClaimedOptions = import('./resourceTypes.js').ClaimedOptions;
365
366
  type ClaimTarget = _Streams.ClaimTarget;
@@ -387,9 +388,34 @@ export declare namespace Ablo {
387
388
  type JoinOptions = _Participants.ParticipantJoinOptions;
388
389
  }
389
390
  type Schema<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord> = _SchemaTypes.Schema<S>;
391
+ /**
392
+ * The schema this program has registered via `interface Register { Schema }`
393
+ * (falls back to a loose shape when unregistered). Use it where shared code
394
+ * needs "this app's schema" without importing a specific one —
395
+ * `Ablo<Ablo.ResolveSchema['models']>` resolves to whatever the consuming
396
+ * app registered, so one component types correctly across apps that bind
397
+ * different schemas.
398
+ */
399
+ type ResolveSchema = _Global.ResolveSchema;
400
+ /**
401
+ * `ResolveSchema` guaranteed to satisfy the `Schema` bound. `ResolveSchema`
402
+ * falls back to a loose `{ models }` shape when nothing is registered, which
403
+ * doesn't extend the branded `Schema` type — so generics bounded by `Schema`
404
+ * (mutator anchors, `Transaction<S>`) can't take `ResolveSchema` directly.
405
+ * `RegisteredSchema` collapses that fallback to `Schema`, so shared mutator
406
+ * code can anchor "this app's schema" and stay assignable at the consumer,
407
+ * which reads the same `Register`. Both resolve in lockstep per app.
408
+ */
409
+ type RegisteredSchema = _Global.ResolveSchema extends _SchemaTypes.Schema ? _Global.ResolveSchema : _SchemaTypes.Schema;
390
410
  namespace Schema {
391
411
  type InferModel<S extends _SchemaTypes.Schema, K extends keyof S['models']> = _SchemaTypes.InferModel<S, K>;
392
412
  type InferCreate<S extends _SchemaTypes.Schema, K extends keyof S['models']> = _SchemaTypes.InferCreate<S, K>;
413
+ /**
414
+ * The reactive-row companion to {@link InferModel}: data fields + computed
415
+ * getters, no relation accessors, no model methods — the shape `useAblo`
416
+ * reads return.
417
+ */
418
+ type InferRow<S extends _SchemaTypes.Schema, K extends keyof S['models']> = _SchemaTypes.InferRow<S, K>;
393
419
  type InferModelNames<S extends _SchemaTypes.Schema> = _SchemaTypes.InferModelNames<S>;
394
420
  }
395
421
  type Conflict = _Policy.Conflict;
@@ -410,6 +436,7 @@ export declare namespace Ablo {
410
436
  }
411
437
  namespace Claim {
412
438
  type Handle = import('./resourceTypes.js').Claim;
439
+ type Held<T = Record<string, unknown>> = import('../types/streams.js').HeldClaim<T>;
413
440
  type CreateOptions = import('./resourceTypes.js').ClaimCreateOptions;
414
441
  type WaitOptions = import('./resourceTypes.js').ClaimWaitOptions;
415
442
  type Client = import('./resourceTypes.js').ClaimResource;
@@ -417,9 +444,11 @@ export declare namespace Ablo {
417
444
  namespace Model {
418
445
  type Target = import('./resourceTypes.js').ModelTarget;
419
446
  type Claim = import('./resourceTypes.js').ModelClaim;
420
- type Read<T = Record<string, unknown>> = import('./resourceTypes.js').ModelRead<T>;
421
- type Client<T = Record<string, unknown>> = import('./resourceTypes.js').ModelClient<T>;
422
- type ReadOptions = import('./resourceTypes.js').ModelReadOptions;
447
+ type Operations<T, CreateInput = T> = import('./createModelProxy.js').ModelOperations<T, CreateInput>;
448
+ type ClaimOptions<T = Record<string, unknown>> = import('./createModelProxy.js').ClaimOptions<T>;
449
+ type ClaimParams<T = Record<string, unknown>> = import('./createModelProxy.js').ClaimParams<T>;
450
+ type ClaimLookupParams<T = Record<string, unknown>> = import('./createModelProxy.js').ClaimLookupParams<T>;
451
+ type ClaimReorderParams<T = Record<string, unknown>> = import('./createModelProxy.js').ClaimReorderParams<T>;
423
452
  type MutationOptions = import('./resourceTypes.js').ModelMutationOptions;
424
453
  }
425
454
  namespace Source {
@@ -429,7 +458,7 @@ export declare namespace Ablo {
429
458
  type EventsResult = import('../source/index.js').SourceEventsResult;
430
459
  type Scope = import('../source/index.js').SourceScope;
431
460
  type ApiKey = import('../source/index.js').SourceApiKey;
432
- type Options<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord, TAuth = unknown> = import('../source/index.js').AbloSourceOptions<S, TAuth>;
461
+ type Options<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord, TAuth = unknown> = import('../source/index.js').DataSourceOptions<S, TAuth>;
433
462
  type ModelHandlers<Row, CreateInput, TAuth = unknown> = import('../source/index.js').SourceModelHandlers<Row, CreateInput, TAuth>;
434
463
  type SignatureVerificationResult = import('../source/index.js').SourceSignatureVerificationResult;
435
464
  namespace Commit {
@@ -18,7 +18,8 @@
18
18
  * });
19
19
  * await sync.reports.delete({ id: reportId });
20
20
  */
21
- import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
21
+ import { durableCommitOperationSchema, } from '../transactions/commitEnvelope.js';
22
+ import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, toAbloError, claimedError } from '../errors.js';
22
23
  import { descriptionFromMeta } from '../coordination/schema.js';
23
24
  import { initSyncEngine } from '../context.js';
24
25
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
@@ -35,10 +36,8 @@ import { createPresenceStream } from '../sync/createPresenceStream.js';
35
36
  import { createClaimStream } from '../sync/createClaimStream.js';
36
37
  import { awaitClaimGrant } from '../sync/awaitClaimGrant.js';
37
38
  import { createSnapshot } from '../sync/createSnapshot.js';
38
- import { reconcileFunctionalUpdate } from './functionalUpdate.js';
39
39
  import { createParticipantManager } from '../sync/participants.js';
40
- import { createProtocolClient, } from './ApiClient.js';
41
- // Value import is cycle-safe: httpClient.js and ApiClient.js take the client
40
+ // Value import is cycle-safe: httpClient.js and httpTransport.js take the client
42
41
  // types from the `options`/`resourceTypes` leaves, never from this module.
43
42
  import { createAbloHttpClient, } from './httpClient.js';
44
43
  import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
@@ -74,11 +73,6 @@ function resolveCredentialResolver(apiKey) {
74
73
  return null;
75
74
  }
76
75
  export function Ablo(options) {
77
- if (options.schema == null) {
78
- // The protocol client IS the stateless HTTP plane (string-keyed models),
79
- // so `transport: 'http'` needs no special-casing here.
80
- return createProtocolClient(options);
81
- }
82
76
  if (options.transport === 'http') {
83
77
  return createAbloHttpClient(options);
84
78
  }
@@ -489,10 +483,6 @@ export function Ablo(options) {
489
483
  await new Promise((resolve) => setTimeout(resolve, 50));
490
484
  }
491
485
  }
492
- const fetchImpl = options.fetch ?? globalThis.fetch;
493
- function authHeaders() {
494
- return authCredentials.withAuthHeaders({ 'Content-Type': 'application/json' });
495
- }
496
486
  function createClientTxId(idempotencyKey) {
497
487
  if (idempotencyKey && idempotencyKey.length > 0)
498
488
  return idempotencyKey;
@@ -500,50 +490,24 @@ export function Ablo(options) {
500
490
  ? crypto.randomUUID()
501
491
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
502
492
  }
503
- function createModelId() {
504
- return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
505
- ? crypto.randomUUID()
506
- : `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
507
- }
508
- function normalizeClaimId(claim) {
509
- if (typeof claim === 'string')
510
- return claim;
511
- return claim?.id;
512
- }
513
- function isClaimHandleValue(value) {
514
- return (typeof value === 'object' &&
515
- value !== null &&
516
- value.object === 'claim' &&
517
- typeof value.id === 'string');
518
- }
519
493
  function normalizeCommitOperation(op, defaults) {
520
- const model = op.model ?? op.target?.model;
521
- if (!model) {
522
- throw new AbloValidationError('Commit operation requires `model` or `target.model`.', { code: 'commit_operation_model_required' });
523
- }
524
494
  const type = op.action.toUpperCase();
525
- const id = op.id ?? op.target?.id ?? '';
526
- return {
495
+ const id = op.id ?? '';
496
+ return durableCommitOperationSchema.parse({
527
497
  type,
528
- model: model.toLowerCase(),
498
+ model: op.model.toLowerCase(),
529
499
  id,
530
500
  input: op.data ?? undefined,
531
501
  transactionId: op.transactionId ?? undefined,
532
502
  readAt: op.readAt ?? defaults.readAt ?? undefined,
533
503
  onStale: op.onStale ?? defaults.onStale ?? undefined,
534
- };
504
+ });
535
505
  }
536
506
  function normalizeCommitOperations(commitOptions) {
537
- if (commitOptions.operation && commitOptions.operations) {
538
- throw new AbloValidationError('Pass either `operation` or `operations`, not both.', { code: 'commit_operations_ambiguous' });
507
+ if (commitOptions.operations.length === 0) {
508
+ throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
539
509
  }
540
- const inputOperations = commitOptions.operation
541
- ? [commitOptions.operation]
542
- : commitOptions.operations ?? [];
543
- if (inputOperations.length === 0) {
544
- throw new AbloValidationError('Commit requires at least one operation.', { code: 'commit_operation_required' });
545
- }
546
- return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
510
+ return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions));
547
511
  }
548
512
  function modelClaimFromActive(claim) {
549
513
  const description = descriptionFromMeta(claim.target.meta);
@@ -631,16 +595,6 @@ export function Ablo(options) {
631
595
  }
632
596
  });
633
597
  }
634
- async function applyClaimedPolicy(target, options) {
635
- const policy = options?.ifClaimed ?? 'return';
636
- if (policy === 'return')
637
- return;
638
- const current = listModelClaims(target);
639
- if (current.length === 0)
640
- return;
641
- // policy === 'fail' — gate the read only when the caller opts in.
642
- throw claimedError(target, current, 'model_claimed');
643
- }
644
598
  function wrapClaimHandle(claim, waited = false) {
645
599
  const release = async () => {
646
600
  claim.revoke?.();
@@ -805,8 +759,6 @@ export function Ablo(options) {
805
759
  onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
806
760
  });
807
761
  const wait = commitOptions.wait ?? 'confirmed';
808
- const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.id;
809
- void claimId; // The current wire clears claims by entity after commit.
810
762
  // Route through the TransactionQueue's commit lane so the call
811
763
  // tolerates WS disconnects: the envelope stays in memory until
812
764
  // reconnect, mutationExecutor.commit() owns transport-level
@@ -817,7 +769,7 @@ export function Ablo(options) {
817
769
  // SyncClient we already hold from createInternalComponents —
818
770
  // no need to leak an accessor through BaseSyncedStore.
819
771
  const queue = syncClient.getTransactionQueue();
820
- queue.enqueueCommit(clientTxId, operations, {
772
+ await queue.enqueueCommit(clientTxId, operations, {
821
773
  ...(commitOptions.reads ? { reads: [...commitOptions.reads] } : {}),
822
774
  });
823
775
  if (wait === 'queued') {
@@ -832,144 +784,6 @@ export function Ablo(options) {
832
784
  };
833
785
  },
834
786
  };
835
- async function retrieveModel(modelName, id, options) {
836
- await applyClaimedPolicy({ model: modelName, id }, options);
837
- await ready();
838
- const res = await fetchImpl(`${bootstrapHelper.baseUrl}/sync/query`, {
839
- method: 'POST',
840
- headers: authHeaders(),
841
- body: JSON.stringify({
842
- queries: [
843
- {
844
- model: modelName,
845
- where: [['id', '=', id]],
846
- limit: 1,
847
- },
848
- ],
849
- }),
850
- });
851
- const bodyText = await res.text();
852
- let body = bodyText;
853
- if (bodyText.length > 0) {
854
- try {
855
- body = JSON.parse(bodyText);
856
- }
857
- catch {
858
- // Keep raw body text.
859
- }
860
- }
861
- if (!res.ok) {
862
- throw translateHttpError(res.status, body || `Model retrieve failed: ${res.status} ${res.statusText}`, res.headers.get('x-request-id') ?? undefined);
863
- }
864
- const parsed = body;
865
- const slot = parsed.results?.[0];
866
- const rows = Array.isArray(slot) ? slot : [];
867
- const data = rows[0];
868
- if (!data) {
869
- throw new AbloValidationError(`Model row not found: ${modelName}/${id}`, { code: 'model_not_found' });
870
- }
871
- const stamp = typeof parsed.lastSyncId === 'number'
872
- ? parsed.lastSyncId
873
- : store.getSyncWebSocket()?.getLastSyncId() ?? store.lastSyncId ?? 0;
874
- return {
875
- data,
876
- stamp,
877
- claims: listModelClaims({ model: modelName, id }),
878
- };
879
- }
880
- function model(name) {
881
- function updateModel(arg, updater, contention) {
882
- if (typeof arg === 'string') {
883
- const id = arg;
884
- if (typeof updater !== 'function') {
885
- throw new AbloValidationError(`${name}.update('${id}', updater): the second argument must be an updater ` +
886
- `function (current) => next. To write a fixed value, use update({ id, data }).`, { code: 'write_options_invalid' });
887
- }
888
- return reconcileFunctionalUpdate(updater, contention, {
889
- model: name,
890
- id,
891
- readFresh: async () => {
892
- const read = await retrieveModel(name, id, {});
893
- return { data: read.data, stamp: read.stamp };
894
- },
895
- writeNext: (patch, readAt) => commits.create({
896
- readAt,
897
- onStale: 'reject',
898
- wait: 'confirmed',
899
- operations: [
900
- { action: 'update', model: name, id, data: patch },
901
- ],
902
- }),
903
- });
904
- }
905
- const params = arg;
906
- return (async () => {
907
- await applyClaimedPolicy({ model: name, id: params.id }, params);
908
- return commits.create({
909
- claimRef: params.claimRef,
910
- idempotencyKey: params.idempotencyKey,
911
- readAt: params.readAt,
912
- onStale: params.onStale,
913
- ...(isClaimHandleValue(params.claim) ? { claim: params.claim } : {}),
914
- wait: params.wait,
915
- operations: [
916
- { action: 'update', model: name, id: params.id, data: params.data },
917
- ],
918
- });
919
- })();
920
- }
921
- return {
922
- retrieve(params) {
923
- return retrieveModel(name, params.id, params);
924
- },
925
- async create(params) {
926
- const id = params.id ?? createModelId();
927
- await applyClaimedPolicy({ model: name, id }, params);
928
- // Confirm, then return the authoritative row (with framework defaults;
929
- // the existing row on an idempotent re-create) — mirrors the WebSocket client.
930
- await commits.create({
931
- claimRef: params.claimRef,
932
- idempotencyKey: params.idempotencyKey,
933
- readAt: params.readAt,
934
- onStale: params.onStale,
935
- ...(isClaimHandleValue(params.claim) ? { claim: params.claim } : {}),
936
- wait: params.wait ?? 'confirmed',
937
- operations: [
938
- {
939
- action: 'create',
940
- model: name,
941
- id,
942
- data: params.data,
943
- },
944
- ],
945
- });
946
- const { data } = await retrieveModel(name, id, {});
947
- if (data === undefined) {
948
- throw new AbloNotFoundError(`create ${name}/${id} did not yield a readable row (the write did not confirm).`, [id]);
949
- }
950
- return data;
951
- },
952
- update: updateModel,
953
- async delete(params) {
954
- await applyClaimedPolicy({ model: name, id: params.id }, params);
955
- return commits.create({
956
- claimRef: params.claimRef,
957
- idempotencyKey: params.idempotencyKey,
958
- readAt: params.readAt,
959
- onStale: params.onStale,
960
- ...(isClaimHandleValue(params.claim) ? { claim: params.claim } : {}),
961
- wait: params.wait,
962
- operations: [
963
- {
964
- action: 'delete',
965
- model: name,
966
- id: params.id,
967
- },
968
- ],
969
- });
970
- },
971
- };
972
- }
973
787
  /**
974
788
  * The control-plane credential: always the original configured secret key.
975
789
  * Never reads `authCredentials` — that holds the exchanged sync credential
@@ -1071,7 +885,7 @@ export function Ablo(options) {
1071
885
  },
1072
886
  },
1073
887
  // Mint a scoped agent identity and hand back a connected client bound to it —
1074
- // `sessions.create({ agent })` plus `Ablo({ apiKey })` fused into one call,
888
+ // `sessions.create({ agent })` plus a typed `Ablo({ schema, apiKey })` client,
1075
889
  // for agents that run in this (secret-key-holding) process. Omitting `id`
1076
890
  // yields a fresh uuid per call, so concurrent agents are distinct participants
1077
891
  // that queue behind each other (even when they share a `name`). Humans don't
@@ -1177,7 +991,6 @@ export function Ablo(options) {
1177
991
  /** Claim livestream — same socket. Stable reference. */
1178
992
  claims: publicClaims,
1179
993
  commits,
1180
- model,
1181
994
  /** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
1182
995
  snapshot(entities) {
1183
996
  return createSnapshot({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The auto-heartbeat loop behind `claim({ id, heartbeat: true })` — one
3
3
  * implementation shared by both transports (the WebSocket claim stream and the
4
- * HTTP `ApiClient`), so the cadence and failure semantics cannot drift.
4
+ * private HTTP transport), so the cadence and failure semantics cannot drift.
5
5
  *
6
6
  * A beat is the "still working" signal that keeps a lease alive for the
7
7
  * duration of real work. The loop's failure handling follows the lease-system
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The auto-heartbeat loop behind `claim({ id, heartbeat: true })` — one
3
3
  * implementation shared by both transports (the WebSocket claim stream and the
4
- * HTTP `ApiClient`), so the cadence and failure semantics cannot drift.
4
+ * private HTTP transport), so the cadence and failure semantics cannot drift.
5
5
  *
6
6
  * A beat is the "still working" signal that keeps a lease alive for the
7
7
  * duration of real work. The loop's failure handling follows the lease-system
@@ -16,6 +16,7 @@ import { BootstrapFetcher } from '../sync/BootstrapFetcher.js';
16
16
  import type { AuthCredentialSource } from '../auth/credentialSource.js';
17
17
  import type { Schema, SchemaRecord } from '../schema/schema.js';
18
18
  import { type AbloPersistence } from './persistence.js';
19
+ import type { DurableWriteStore, DurableWritesConfig } from '../transactions/durableWriteStore.js';
19
20
  export interface InternalComponentsInput<S extends SchemaRecord> {
20
21
  readonly schema: Schema<S>;
21
22
  /** The WebSocket URL. Used to derive the bootstrap HTTP base URL when the
@@ -28,6 +29,9 @@ export interface InternalComponentsInput<S extends SchemaRecord> {
28
29
  readonly persistence?: AbloPersistence;
29
30
  readonly offline?: boolean;
30
31
  readonly inMemory?: boolean;
32
+ readonly durableWrites?: DurableWritesConfig;
33
+ /** @deprecated Use `durableWrites`. */
34
+ readonly commitOutbox?: DurableWriteStore;
31
35
  };
32
36
  readonly auth?: AuthCredentialSource;
33
37
  }
@@ -15,6 +15,7 @@ import { OnDemandLoader } from '../sync/OnDemandLoader.js';
15
15
  import { BootstrapFetcher } from '../sync/BootstrapFetcher.js';
16
16
  import { resolveBootstrapBaseUrl } from './auth.js';
17
17
  import { shouldUseInMemoryPersistence } from './persistence.js';
18
+ import { resolveDurableWrites } from './durableWrites.js';
18
19
  export function createInternalComponents(input) {
19
20
  const { schema, url, options, auth } = input;
20
21
  // The registry is created here, but model registration happens in the caller,
@@ -41,7 +42,8 @@ export function createInternalComponents(input) {
41
42
  // IndexedDB is unavailable there.
42
43
  inMemory: shouldUseInMemoryPersistence(options),
43
44
  });
44
- const syncClient = new SyncClient(objectPool, database);
45
+ const durableWrites = resolveDurableWrites(options);
46
+ const syncClient = new SyncClient(objectPool, database, durableWrites.store, durableWrites.namespace ?? url);
45
47
  // Lazy-load lane: hydrates the object pool and IndexedDB on demand for
46
48
  // entities not in scope at bootstrap (`load: 'lazy'` models, or an entity
47
49
  // reached by deep link before the pool warmed up). Single-flight, with
@@ -0,0 +1,21 @@
1
+ /** Normalize the product-facing durable-write option and its legacy aliases. */
2
+ import type { CommitOutboxScope } from '../transactions/commitEnvelope.js';
3
+ import { type DurableWriteStore, type DurableWritesConfig } from '../transactions/durableWriteStore.js';
4
+ interface DurableWriteOptionInput {
5
+ readonly durableWrites?: DurableWritesConfig;
6
+ /** Compatibility input for pre-`durableWrites` clients. */
7
+ readonly commitOutbox?: DurableWriteStore;
8
+ /** Compatibility input and internal child-client identity seam. */
9
+ readonly commitOutboxScope?: CommitOutboxScope;
10
+ }
11
+ export interface ResolvedDurableWrites {
12
+ readonly store?: DurableWriteStore;
13
+ readonly namespace?: string;
14
+ }
15
+ /**
16
+ * Resolves one canonical store configuration for both HTTP and WebSocket
17
+ * clients. Mixed old/new configuration fails loudly so Ablo never persists to
18
+ * one adapter while the caller believes another adapter is authoritative.
19
+ */
20
+ export declare function resolveDurableWrites(options: DurableWriteOptionInput): ResolvedDurableWrites;
21
+ export {};
@@ -0,0 +1,46 @@
1
+ /** Normalize the product-facing durable-write option and its legacy aliases. */
2
+ import { AbloValidationError } from '../errors.js';
3
+ import { durableWritesConfigSchema, durableWriteStoreSchema, } from '../transactions/durableWriteStore.js';
4
+ function invalidDurableWrites(message) {
5
+ return new AbloValidationError(`Ablo: invalid \`durableWrites\` option — ${message}.`, {
6
+ code: 'invalid_options',
7
+ param: 'durableWrites',
8
+ });
9
+ }
10
+ /**
11
+ * Resolves one canonical store configuration for both HTTP and WebSocket
12
+ * clients. Mixed old/new configuration fails loudly so Ablo never persists to
13
+ * one adapter while the caller believes another adapter is authoritative.
14
+ */
15
+ export function resolveDurableWrites(options) {
16
+ if (options.durableWrites !== undefined && options.commitOutbox !== undefined) {
17
+ throw invalidDurableWrites('pass `durableWrites` or the deprecated `commitOutbox`, not both');
18
+ }
19
+ if (options.durableWrites !== undefined) {
20
+ const parsed = durableWritesConfigSchema.safeParse(options.durableWrites);
21
+ if (!parsed.success) {
22
+ throw invalidDurableWrites(parsed.error.issues.map((issue) => issue.message).join('; '));
23
+ }
24
+ return {
25
+ store: parsed.data.store,
26
+ ...(parsed.data.namespace !== undefined
27
+ ? { namespace: parsed.data.namespace }
28
+ : options.commitOutboxScope?.namespace !== undefined
29
+ ? { namespace: options.commitOutboxScope.namespace }
30
+ : {}),
31
+ };
32
+ }
33
+ if (options.commitOutbox !== undefined) {
34
+ const parsed = durableWriteStoreSchema.safeParse(options.commitOutbox);
35
+ if (!parsed.success) {
36
+ throw invalidDurableWrites(parsed.error.issues.map((issue) => issue.message).join('; '));
37
+ }
38
+ return {
39
+ store: parsed.data,
40
+ ...(options.commitOutboxScope?.namespace !== undefined
41
+ ? { namespace: options.commitOutboxScope.namespace }
42
+ : {}),
43
+ };
44
+ }
45
+ return {};
46
+ }