@abloatai/ablo 0.28.0 → 0.29.1

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 (96) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/Database.js +9 -6
  3. package/dist/SyncClient.d.ts +2 -2
  4. package/dist/SyncClient.js +22 -10
  5. package/dist/adapters/inMemoryStorage.js +18 -14
  6. package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
  7. package/dist/auth/schemas.d.ts +6 -0
  8. package/dist/auth/schemas.js +7 -0
  9. package/dist/cli.cjs +815 -400
  10. package/dist/client/Ablo.d.ts +29 -23
  11. package/dist/client/Ablo.js +8 -196
  12. package/dist/client/claimHeartbeatLoop.d.ts +1 -1
  13. package/dist/client/claimHeartbeatLoop.js +1 -1
  14. package/dist/client/createInternalComponents.d.ts +4 -2
  15. package/dist/client/createInternalComponents.js +3 -1
  16. package/dist/client/durableWrites.d.ts +21 -0
  17. package/dist/client/durableWrites.js +46 -0
  18. package/dist/client/httpClient.d.ts +19 -45
  19. package/dist/client/httpClient.js +104 -26
  20. package/dist/client/httpTransport.d.ts +8 -0
  21. package/dist/client/{ApiClient.js → httpTransport.js} +78 -291
  22. package/dist/client/options.d.ts +43 -19
  23. package/dist/client/options.js +3 -2
  24. package/dist/client/resourceTypes.d.ts +9 -85
  25. package/dist/client/resourceTypes.js +1 -1
  26. package/dist/client/sessionMint.d.ts +5 -6
  27. package/dist/client/sessionMint.js +4 -5
  28. package/dist/client/validateAbloOptions.js +3 -3
  29. package/dist/coordination/schema.d.ts +30 -0
  30. package/dist/coordination/schema.js +14 -0
  31. package/dist/core/index.d.ts +1 -1
  32. package/dist/errors.d.ts +4 -40
  33. package/dist/errors.js +5 -5
  34. package/dist/index.d.ts +14 -8
  35. package/dist/index.js +7 -5
  36. package/dist/interfaces/index.d.ts +3 -0
  37. package/dist/mutators/defineMutators.d.ts +18 -0
  38. package/dist/mutators/defineMutators.js +4 -8
  39. package/dist/react/index.d.ts +1 -1
  40. package/dist/schema/index.d.ts +1 -1
  41. package/dist/schema/index.js +1 -1
  42. package/dist/schema/select.d.ts +15 -0
  43. package/dist/schema/select.js +27 -3
  44. package/dist/source/connector.d.ts +3 -3
  45. package/dist/source/factory.d.ts +4 -6
  46. package/dist/source/factory.js +4 -7
  47. package/dist/source/index.d.ts +4 -4
  48. package/dist/source/index.js +2 -2
  49. package/dist/source/signing.d.ts +0 -3
  50. package/dist/source/types.d.ts +0 -26
  51. package/dist/stores/ObjectStore.js +8 -8
  52. package/dist/surface.d.ts +1 -1
  53. package/dist/surface.js +1 -0
  54. package/dist/sync/BootstrapFetcher.d.ts +10 -0
  55. package/dist/sync/BootstrapFetcher.js +27 -4
  56. package/dist/sync/createClaimStream.d.ts +11 -2
  57. package/dist/sync/createClaimStream.js +4 -3
  58. package/dist/testing/mocks/FakeDatabase.d.ts +1 -1
  59. package/dist/transactions/TransactionQueue.d.ts +2 -2
  60. package/dist/transactions/TransactionQueue.js +3 -3
  61. package/dist/transactions/commitEnvelope.d.ts +1 -1
  62. package/dist/transactions/commitEnvelope.js +2 -2
  63. package/dist/transactions/commitOutboxStore.d.ts +19 -19
  64. package/dist/transactions/durableWriteStore.d.ts +123 -0
  65. package/dist/transactions/durableWriteStore.js +30 -0
  66. package/dist/transactions/httpCommitEnvelope.d.ts +1 -0
  67. package/dist/transactions/httpCommitEnvelope.js +14 -12
  68. package/dist/transactions/idempotencyKey.d.ts +10 -0
  69. package/dist/transactions/idempotencyKey.js +9 -0
  70. package/dist/types/global.d.ts +10 -29
  71. package/dist/types/global.js +4 -7
  72. package/dist/types/streams.d.ts +2 -28
  73. package/dist/wire/frames.d.ts +6 -257
  74. package/dist/wire/frames.js +4 -25
  75. package/dist/wire/index.d.ts +4 -3
  76. package/dist/wire/index.js +2 -2
  77. package/dist/wire/protocolVersion.d.ts +30 -17
  78. package/dist/wire/protocolVersion.js +34 -18
  79. package/docs/agents.md +8 -3
  80. package/docs/api.md +6 -4
  81. package/docs/client-behavior.md +6 -3
  82. package/docs/coordination.md +2 -3
  83. package/docs/data-sources.md +5 -8
  84. package/docs/guarantees.md +21 -0
  85. package/docs/integration-guide.md +1 -0
  86. package/docs/migration.md +21 -1
  87. package/docs/quickstart.md +11 -0
  88. package/docs/react.md +0 -46
  89. package/examples/README.md +6 -5
  90. package/llms.txt +15 -15
  91. package/package.json +2 -7
  92. package/dist/client/ApiClient.d.ts +0 -177
  93. package/dist/commit/contract.d.ts +0 -493
  94. package/dist/commit/contract.js +0 -187
  95. package/dist/commit/index.d.ts +0 -6
  96. package/dist/commit/index.js +0 -5
@@ -26,12 +26,11 @@ 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';
37
36
  /**
@@ -272,12 +271,6 @@ export type Ablo<S extends SchemaRecord> = {
272
271
  * down to `commits.create(...)`.
273
272
  */
274
273
  readonly commits: CommitResource;
275
- /**
276
- * Canonical untyped model API. This is the portable API shape that maps
277
- * cleanly to HTTP/Python/Ruby/Go clients. Typed `ablo.<model>` properties
278
- * are schema-powered sugar over the same model write/read path.
279
- */
280
- model<T = Record<string, unknown>>(name: string): ModelClient<T>;
281
274
  /**
282
275
  * Capture a context-staleness watermark over a set of entities.
283
276
  * Returns a flat snapshot with `stamp` (thread into writes as
@@ -337,7 +330,6 @@ export declare function Ablo<const S extends SchemaRecord>(options: AbloHttpClie
337
330
  transport: 'http';
338
331
  }): AbloHttpClient<S>;
339
332
  export declare function Ablo<const S extends SchemaRecord>(options: AbloOptions<S>): Ablo<S>;
340
- export declare function Ablo(options: AbloApiClientOptions): AbloApi;
341
333
  import type * as _Streams from '../types/streams.js';
342
334
  import type * as _Participants from '../sync/participants.js';
343
335
  import type * as _Policy from '../policy/types.js';
@@ -345,6 +337,7 @@ import type * as _Mutators from '../mutators/defineMutators.js';
345
337
  import type * as _Tx from '../mutators/Transaction.js';
346
338
  import type * as _Undo from '../mutators/UndoManager.js';
347
339
  import type * as _SchemaTypes from '../schema/schema.js';
340
+ import type * as _Global from '../types/global.js';
348
341
  /**
349
342
  * The canonical type namespace.
350
343
  *
@@ -368,15 +361,6 @@ export declare namespace Ablo {
368
361
  * typed as reactive rows (data fields + computeds, no relation accessors).
369
362
  */
370
363
  type Reads<S extends SchemaRecord = SchemaRecord> = AbloReads<S>;
371
- type Api = AbloApi;
372
- type ApiClaims = AbloApiClaims;
373
- type Capability = import('./ApiClient.js').Capability;
374
- type CapabilityCreateOptions = import('./ApiClient.js').CapabilityCreateOptions;
375
- type CapabilityRecord = import('./ApiClient.js').CapabilityRecord;
376
- type CapabilityResource = import('./ApiClient.js').CapabilityResource;
377
- type CapabilityRevocation = import('./ApiClient.js').CapabilityRevocation;
378
- type CapabilityRotateOptions = import('./ApiClient.js').CapabilityRotateOptions;
379
- type RotatedCapability = import('./ApiClient.js').RotatedCapability;
380
364
  type IfClaimedPolicy = import('./resourceTypes.js').IfClaimedPolicy;
381
365
  type ClaimedOptions = import('./resourceTypes.js').ClaimedOptions;
382
366
  type ClaimTarget = _Streams.ClaimTarget;
@@ -404,6 +388,25 @@ export declare namespace Ablo {
404
388
  type JoinOptions = _Participants.ParticipantJoinOptions;
405
389
  }
406
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;
407
410
  namespace Schema {
408
411
  type InferModel<S extends _SchemaTypes.Schema, K extends keyof S['models']> = _SchemaTypes.InferModel<S, K>;
409
412
  type InferCreate<S extends _SchemaTypes.Schema, K extends keyof S['models']> = _SchemaTypes.InferCreate<S, K>;
@@ -433,6 +436,7 @@ export declare namespace Ablo {
433
436
  }
434
437
  namespace Claim {
435
438
  type Handle = import('./resourceTypes.js').Claim;
439
+ type Held<T = Record<string, unknown>> = import('../types/streams.js').HeldClaim<T>;
436
440
  type CreateOptions = import('./resourceTypes.js').ClaimCreateOptions;
437
441
  type WaitOptions = import('./resourceTypes.js').ClaimWaitOptions;
438
442
  type Client = import('./resourceTypes.js').ClaimResource;
@@ -440,9 +444,11 @@ export declare namespace Ablo {
440
444
  namespace Model {
441
445
  type Target = import('./resourceTypes.js').ModelTarget;
442
446
  type Claim = import('./resourceTypes.js').ModelClaim;
443
- type Read<T = Record<string, unknown>> = import('./resourceTypes.js').ModelRead<T>;
444
- type Client<T = Record<string, unknown>> = import('./resourceTypes.js').ModelClient<T>;
445
- 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>;
446
452
  type MutationOptions = import('./resourceTypes.js').ModelMutationOptions;
447
453
  }
448
454
  namespace Source {
@@ -452,7 +458,7 @@ export declare namespace Ablo {
452
458
  type EventsResult = import('../source/index.js').SourceEventsResult;
453
459
  type Scope = import('../source/index.js').SourceScope;
454
460
  type ApiKey = import('../source/index.js').SourceApiKey;
455
- 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>;
456
462
  type ModelHandlers<Row, CreateInput, TAuth = unknown> = import('../source/index.js').SourceModelHandlers<Row, CreateInput, TAuth>;
457
463
  type SignatureVerificationResult = import('../source/index.js').SourceSignatureVerificationResult;
458
464
  namespace Commit {
@@ -19,7 +19,7 @@
19
19
  * await sync.reports.delete({ id: reportId });
20
20
  */
21
21
  import { durableCommitOperationSchema, } from '../transactions/commitEnvelope.js';
22
- import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
22
+ import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, toAbloError, claimedError } from '../errors.js';
23
23
  import { descriptionFromMeta } from '../coordination/schema.js';
24
24
  import { initSyncEngine } from '../context.js';
25
25
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
@@ -36,10 +36,8 @@ import { createPresenceStream } from '../sync/createPresenceStream.js';
36
36
  import { createClaimStream } from '../sync/createClaimStream.js';
37
37
  import { awaitClaimGrant } from '../sync/awaitClaimGrant.js';
38
38
  import { createSnapshot } from '../sync/createSnapshot.js';
39
- import { reconcileFunctionalUpdate } from './functionalUpdate.js';
40
39
  import { createParticipantManager } from '../sync/participants.js';
41
- import { createProtocolClient, } from './ApiClient.js';
42
- // 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
43
41
  // types from the `options`/`resourceTypes` leaves, never from this module.
44
42
  import { createAbloHttpClient, } from './httpClient.js';
45
43
  import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
@@ -75,11 +73,6 @@ function resolveCredentialResolver(apiKey) {
75
73
  return null;
76
74
  }
77
75
  export function Ablo(options) {
78
- if (options.schema == null) {
79
- // The protocol client IS the stateless HTTP plane (string-keyed models),
80
- // so `transport: 'http'` needs no special-casing here.
81
- return createProtocolClient(options);
82
- }
83
76
  if (options.transport === 'http') {
84
77
  return createAbloHttpClient(options);
85
78
  }
@@ -490,10 +483,6 @@ export function Ablo(options) {
490
483
  await new Promise((resolve) => setTimeout(resolve, 50));
491
484
  }
492
485
  }
493
- const fetchImpl = options.fetch ?? globalThis.fetch;
494
- function authHeaders() {
495
- return authCredentials.withAuthHeaders({ 'Content-Type': 'application/json' });
496
- }
497
486
  function createClientTxId(idempotencyKey) {
498
487
  if (idempotencyKey && idempotencyKey.length > 0)
499
488
  return idempotencyKey;
@@ -501,32 +490,12 @@ export function Ablo(options) {
501
490
  ? crypto.randomUUID()
502
491
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
503
492
  }
504
- function createModelId() {
505
- return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
506
- ? crypto.randomUUID()
507
- : `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
508
- }
509
- function normalizeClaimId(claim) {
510
- if (typeof claim === 'string')
511
- return claim;
512
- return claim?.id;
513
- }
514
- function isClaimHandleValue(value) {
515
- return (typeof value === 'object' &&
516
- value !== null &&
517
- value.object === 'claim' &&
518
- typeof value.id === 'string');
519
- }
520
493
  function normalizeCommitOperation(op, defaults) {
521
- const model = op.model ?? op.target?.model;
522
- if (!model) {
523
- throw new AbloValidationError('Commit operation requires `model` or `target.model`.', { code: 'commit_operation_model_required' });
524
- }
525
494
  const type = op.action.toUpperCase();
526
- const id = op.id ?? op.target?.id ?? '';
495
+ const id = op.id ?? '';
527
496
  return durableCommitOperationSchema.parse({
528
497
  type,
529
- model: model.toLowerCase(),
498
+ model: op.model.toLowerCase(),
530
499
  id,
531
500
  input: op.data ?? undefined,
532
501
  transactionId: op.transactionId ?? undefined,
@@ -535,16 +504,10 @@ export function Ablo(options) {
535
504
  });
536
505
  }
537
506
  function normalizeCommitOperations(commitOptions) {
538
- if (commitOptions.operation && commitOptions.operations) {
539
- 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' });
540
509
  }
541
- const inputOperations = commitOptions.operation
542
- ? [commitOptions.operation]
543
- : commitOptions.operations ?? [];
544
- if (inputOperations.length === 0) {
545
- throw new AbloValidationError('Commit requires at least one operation.', { code: 'commit_operation_required' });
546
- }
547
- return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
510
+ return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions));
548
511
  }
549
512
  function modelClaimFromActive(claim) {
550
513
  const description = descriptionFromMeta(claim.target.meta);
@@ -632,16 +595,6 @@ export function Ablo(options) {
632
595
  }
633
596
  });
634
597
  }
635
- async function applyClaimedPolicy(target, options) {
636
- const policy = options?.ifClaimed ?? 'return';
637
- if (policy === 'return')
638
- return;
639
- const current = listModelClaims(target);
640
- if (current.length === 0)
641
- return;
642
- // policy === 'fail' — gate the read only when the caller opts in.
643
- throw claimedError(target, current, 'model_claimed');
644
- }
645
598
  function wrapClaimHandle(claim, waited = false) {
646
599
  const release = async () => {
647
600
  claim.revoke?.();
@@ -806,8 +759,6 @@ export function Ablo(options) {
806
759
  onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
807
760
  });
808
761
  const wait = commitOptions.wait ?? 'confirmed';
809
- const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.id;
810
- void claimId; // The current wire clears claims by entity after commit.
811
762
  // Route through the TransactionQueue's commit lane so the call
812
763
  // tolerates WS disconnects: the envelope stays in memory until
813
764
  // reconnect, mutationExecutor.commit() owns transport-level
@@ -833,144 +784,6 @@ export function Ablo(options) {
833
784
  };
834
785
  },
835
786
  };
836
- async function retrieveModel(modelName, id, options) {
837
- await applyClaimedPolicy({ model: modelName, id }, options);
838
- await ready();
839
- const res = await fetchImpl(`${bootstrapHelper.baseUrl}/sync/query`, {
840
- method: 'POST',
841
- headers: authHeaders(),
842
- body: JSON.stringify({
843
- queries: [
844
- {
845
- model: modelName,
846
- where: [['id', '=', id]],
847
- limit: 1,
848
- },
849
- ],
850
- }),
851
- });
852
- const bodyText = await res.text();
853
- let body = bodyText;
854
- if (bodyText.length > 0) {
855
- try {
856
- body = JSON.parse(bodyText);
857
- }
858
- catch {
859
- // Keep raw body text.
860
- }
861
- }
862
- if (!res.ok) {
863
- throw translateHttpError(res.status, body || `Model retrieve failed: ${res.status} ${res.statusText}`, res.headers.get('x-request-id') ?? undefined);
864
- }
865
- const parsed = body;
866
- const slot = parsed.results?.[0];
867
- const rows = Array.isArray(slot) ? slot : [];
868
- const data = rows[0];
869
- if (!data) {
870
- throw new AbloValidationError(`Model row not found: ${modelName}/${id}`, { code: 'model_not_found' });
871
- }
872
- const stamp = typeof parsed.lastSyncId === 'number'
873
- ? parsed.lastSyncId
874
- : store.getSyncWebSocket()?.getLastSyncId() ?? store.lastSyncId ?? 0;
875
- return {
876
- data,
877
- stamp,
878
- claims: listModelClaims({ model: modelName, id }),
879
- };
880
- }
881
- function model(name) {
882
- function updateModel(arg, updater, contention) {
883
- if (typeof arg === 'string') {
884
- const id = arg;
885
- if (typeof updater !== 'function') {
886
- throw new AbloValidationError(`${name}.update('${id}', updater): the second argument must be an updater ` +
887
- `function (current) => next. To write a fixed value, use update({ id, data }).`, { code: 'write_options_invalid' });
888
- }
889
- return reconcileFunctionalUpdate(updater, contention, {
890
- model: name,
891
- id,
892
- readFresh: async () => {
893
- const read = await retrieveModel(name, id, {});
894
- return { data: read.data, stamp: read.stamp };
895
- },
896
- writeNext: (patch, readAt) => commits.create({
897
- readAt,
898
- onStale: 'reject',
899
- wait: 'confirmed',
900
- operations: [
901
- { action: 'update', model: name, id, data: patch },
902
- ],
903
- }),
904
- });
905
- }
906
- const params = arg;
907
- return (async () => {
908
- await applyClaimedPolicy({ model: name, id: params.id }, params);
909
- return commits.create({
910
- claimRef: params.claimRef,
911
- idempotencyKey: params.idempotencyKey,
912
- readAt: params.readAt,
913
- onStale: params.onStale,
914
- ...(isClaimHandleValue(params.claim) ? { claim: params.claim } : {}),
915
- wait: params.wait,
916
- operations: [
917
- { action: 'update', model: name, id: params.id, data: params.data },
918
- ],
919
- });
920
- })();
921
- }
922
- return {
923
- retrieve(params) {
924
- return retrieveModel(name, params.id, params);
925
- },
926
- async create(params) {
927
- const id = params.id ?? createModelId();
928
- await applyClaimedPolicy({ model: name, id }, params);
929
- // Confirm, then return the authoritative row (with framework defaults;
930
- // the existing row on an idempotent re-create) — mirrors the WebSocket client.
931
- await commits.create({
932
- claimRef: params.claimRef,
933
- idempotencyKey: params.idempotencyKey,
934
- readAt: params.readAt,
935
- onStale: params.onStale,
936
- ...(isClaimHandleValue(params.claim) ? { claim: params.claim } : {}),
937
- wait: params.wait ?? 'confirmed',
938
- operations: [
939
- {
940
- action: 'create',
941
- model: name,
942
- id,
943
- data: params.data,
944
- },
945
- ],
946
- });
947
- const { data } = await retrieveModel(name, id, {});
948
- if (data === undefined) {
949
- throw new AbloNotFoundError(`create ${name}/${id} did not yield a readable row (the write did not confirm).`, [id]);
950
- }
951
- return data;
952
- },
953
- update: updateModel,
954
- async delete(params) {
955
- await applyClaimedPolicy({ model: name, id: params.id }, params);
956
- return commits.create({
957
- claimRef: params.claimRef,
958
- idempotencyKey: params.idempotencyKey,
959
- readAt: params.readAt,
960
- onStale: params.onStale,
961
- ...(isClaimHandleValue(params.claim) ? { claim: params.claim } : {}),
962
- wait: params.wait,
963
- operations: [
964
- {
965
- action: 'delete',
966
- model: name,
967
- id: params.id,
968
- },
969
- ],
970
- });
971
- },
972
- };
973
- }
974
787
  /**
975
788
  * The control-plane credential: always the original configured secret key.
976
789
  * Never reads `authCredentials` — that holds the exchanged sync credential
@@ -1072,7 +885,7 @@ export function Ablo(options) {
1072
885
  },
1073
886
  },
1074
887
  // Mint a scoped agent identity and hand back a connected client bound to it —
1075
- // `sessions.create({ agent })` plus `Ablo({ apiKey })` fused into one call,
888
+ // `sessions.create({ agent })` plus a typed `Ablo({ schema, apiKey })` client,
1076
889
  // for agents that run in this (secret-key-holding) process. Omitting `id`
1077
890
  // yields a fresh uuid per call, so concurrent agents are distinct participants
1078
891
  // that queue behind each other (even when they share a `name`). Humans don't
@@ -1178,7 +991,6 @@ export function Ablo(options) {
1178
991
  /** Claim livestream — same socket. Stable reference. */
1179
992
  claims: publicClaims,
1180
993
  commits,
1181
- model,
1182
994
  /** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
1183
995
  snapshot(entities) {
1184
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,7 +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 { CommitOutboxStore } from '../transactions/commitOutboxStore.js';
19
+ import type { DurableWriteStore, DurableWritesConfig } from '../transactions/durableWriteStore.js';
20
20
  export interface InternalComponentsInput<S extends SchemaRecord> {
21
21
  readonly schema: Schema<S>;
22
22
  /** The WebSocket URL. Used to derive the bootstrap HTTP base URL when the
@@ -29,7 +29,9 @@ export interface InternalComponentsInput<S extends SchemaRecord> {
29
29
  readonly persistence?: AbloPersistence;
30
30
  readonly offline?: boolean;
31
31
  readonly inMemory?: boolean;
32
- readonly commitOutbox?: CommitOutboxStore;
32
+ readonly durableWrites?: DurableWritesConfig;
33
+ /** @deprecated Use `durableWrites`. */
34
+ readonly commitOutbox?: DurableWriteStore;
33
35
  };
34
36
  readonly auth?: AuthCredentialSource;
35
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, options.commitOutbox, url);
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
+ }
@@ -1,32 +1,15 @@
1
- /**
2
- * Creates a stateless, typed HTTP client for server-side actors — agents,
3
- * workers, and serverless handlers. It talks to Ablo over plain request/response
4
- * HTTP, uses the bearer credential as its identity, and holds no WebSocket and no
5
- * connection state.
6
- *
7
- * This is the counterpart to the stateful {@link Ablo} client. The stateful
8
- * client is for interactive participants: it opens a WebSocket, learns its
9
- * identity (user id and organization id) during the connect-and-bootstrap step,
10
- * and routes writes through a queue that waits for that identity. A server-side
11
- * actor has no socket, so instead of reusing that machinery it uses this client,
12
- * where the credential itself carries identity and the server resolves it on
13
- * every request.
14
- *
15
- * Under the hood this wraps the schema-agnostic protocol client that
16
- * {@link createProtocolClient} returns in a typed proxy. The protocol client
17
- * commits over `POST /v1/commits` and reads over HTTP, authenticating with the
18
- * bearer token each time; its model access is string-keyed (`api.model('slides')`).
19
- * The proxy gives server code the same typed `client.<model>` surface the
20
- * stateful client offers, over stateless transport.
21
- */
22
- import { type AbloApiClientOptions } from './ApiClient.js';
23
- import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions, CreateSessionParams, AbloSession } from './resourceTypes.js';
1
+ import type { AbloOptions } from './options.js';
2
+ import type { CommitResource, HttpClaimApi, ModelReadOptions, CreateSessionParams, AbloSession } from './resourceTypes.js';
24
3
  import type { ModelCreateParams, ModelDeleteParams, ServerReadOptions, ModelRetrieveParams, ModelUpdateParams } from './createModelProxy.js';
25
4
  import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
26
5
  import type { ModelUpdater, ContentionOptions } from './functionalUpdate.js';
27
- export interface AbloHttpClientOptions<S extends SchemaRecord> extends Omit<AbloApiClientOptions, 'schema'> {
28
- /** The schema. Used only to type the model proxies; it is never sent over the wire or read at runtime. */
29
- readonly schema: Schema<S>;
6
+ export interface AbloHttpClientOptions<S extends SchemaRecord> extends AbloOptions<S> {
7
+ /**
8
+ * Per-request deadline. A black-holed HTTP request otherwise has no platform
9
+ * timeout and can stall a headless worker forever. Pass `0` to disable it.
10
+ * @default 30_000
11
+ */
12
+ readonly timeoutMs?: number;
30
13
  }
31
14
  /**
32
15
  * The per-model surface of the stateless HTTP client — everything reachable over
@@ -37,14 +20,14 @@ export interface AbloHttpClientOptions<S extends SchemaRecord> extends Omit<Ablo
37
20
  * need a persistent socket; those are absent from the type, so reaching for one
38
21
  * is a compile error rather than a runtime gap.
39
22
  *
40
- * The read shapes differ on purpose. `retrieve` returns a {@link ModelRead}
41
- * envelope of `{ data, stamp, claims }`, because a stateless client keeps no local
42
- * copy of the data: the watermark (`stamp`) and any active claims must travel
43
- * inline on the read so a caller can follow it with a stale-guarded write. `list`
44
- * returns a plain array.
23
+ * The typed model contract is transport-independent: `retrieve` returns one row,
24
+ * `list` returns rows, `create`/`update` return the resulting row, and `delete`
25
+ * returns nothing. The low-level protocol client keeps its read watermark and
26
+ * commit receipt envelopes internally; callers should not have to change data
27
+ * access syntax when they switch transport.
45
28
  */
46
29
  export interface HttpModelClient<T, C = T> {
47
- retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<ModelRead<T>>;
30
+ retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined>;
48
31
  list(options?: ServerReadOptions<T>): Promise<T[]>;
49
32
  /**
50
33
  * Creates a row and returns the confirmed server row, including any
@@ -52,7 +35,7 @@ export interface HttpModelClient<T, C = T> {
52
35
  * id that already exists is idempotent: the existing row is returned unchanged.
53
36
  */
54
37
  create(params: ModelCreateParams<T, C>): Promise<T>;
55
- update(params: ModelUpdateParams<C>): Promise<CommitReceipt>;
38
+ update(params: ModelUpdateParams<T>): Promise<T>;
56
39
  /**
57
40
  * Updates a row with a function of its latest value — `update(id, current =>
58
41
  * next)`, the data-layer equivalent of a `setState(prev => next)` reducer. The
@@ -62,8 +45,8 @@ export interface HttpModelClient<T, C = T> {
62
45
  * lands or throws `AbloContentionError` once its retry budget is spent. Return
63
46
  * `null` or `undefined` from the updater to skip the write.
64
47
  */
65
- update(id: string, updater: ModelUpdater<T>, options?: ContentionOptions): Promise<CommitReceipt | undefined>;
66
- delete(params: ModelDeleteParams<T>): Promise<CommitReceipt>;
48
+ update(id: string, updater: ModelUpdater<T>, options?: ContentionOptions): Promise<T | undefined>;
49
+ delete(params: ModelDeleteParams<T>): Promise<void>;
67
50
  claim: HttpClaimApi<T>;
68
51
  }
69
52
  /**
@@ -90,17 +73,8 @@ export type AbloHttpClient<S extends SchemaRecord> = {
90
73
  * request, so it is available here even though the local-cache reads are not.
91
74
  * Pass `{ user }` to mint an end-user key (`ek_`) or `{ agent, can }` to mint a
92
75
  * scoped agent key (`rk_`). See {@link CreateSessionParams}.
93
- */
76
+ */
94
77
  readonly sessions: {
95
78
  create(params: CreateSessionParams<S>): Promise<AbloSession>;
96
79
  };
97
- /** Looks up a model client by name, for when the model name is only known at runtime. */
98
- model<T = Record<string, unknown>>(name: string): HttpModelClient<T>;
99
80
  };
100
- /**
101
- * Builds the stateless, typed HTTP client. Each `client.<model>` resolves to the
102
- * protocol client's model accessor, while `commits`, `dispose`, and the other
103
- * protocol members pass through unchanged. No socket is ever opened; the bearer
104
- * credential is the identity.
105
- */
106
- export declare function createAbloHttpClient<S extends SchemaRecord>(options: AbloHttpClientOptions<S>): AbloHttpClient<S>;