@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.
- package/CHANGELOG.md +36 -0
- package/dist/Database.js +9 -6
- package/dist/SyncClient.d.ts +2 -2
- package/dist/SyncClient.js +22 -10
- package/dist/adapters/inMemoryStorage.js +18 -14
- package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
- package/dist/auth/schemas.d.ts +6 -0
- package/dist/auth/schemas.js +7 -0
- package/dist/cli.cjs +815 -400
- package/dist/client/Ablo.d.ts +29 -23
- package/dist/client/Ablo.js +8 -196
- package/dist/client/claimHeartbeatLoop.d.ts +1 -1
- package/dist/client/claimHeartbeatLoop.js +1 -1
- package/dist/client/createInternalComponents.d.ts +4 -2
- package/dist/client/createInternalComponents.js +3 -1
- package/dist/client/durableWrites.d.ts +21 -0
- package/dist/client/durableWrites.js +46 -0
- package/dist/client/httpClient.d.ts +19 -45
- package/dist/client/httpClient.js +104 -26
- package/dist/client/httpTransport.d.ts +8 -0
- package/dist/client/{ApiClient.js → httpTransport.js} +78 -291
- package/dist/client/options.d.ts +43 -19
- package/dist/client/options.js +3 -2
- package/dist/client/resourceTypes.d.ts +9 -85
- package/dist/client/resourceTypes.js +1 -1
- package/dist/client/sessionMint.d.ts +5 -6
- package/dist/client/sessionMint.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/coordination/schema.d.ts +30 -0
- package/dist/coordination/schema.js +14 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/errors.d.ts +4 -40
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +14 -8
- package/dist/index.js +7 -5
- package/dist/interfaces/index.d.ts +3 -0
- package/dist/mutators/defineMutators.d.ts +18 -0
- package/dist/mutators/defineMutators.js +4 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/select.d.ts +15 -0
- package/dist/schema/select.js +27 -3
- package/dist/source/connector.d.ts +3 -3
- package/dist/source/factory.d.ts +4 -6
- package/dist/source/factory.js +4 -7
- package/dist/source/index.d.ts +4 -4
- package/dist/source/index.js +2 -2
- package/dist/source/signing.d.ts +0 -3
- package/dist/source/types.d.ts +0 -26
- package/dist/stores/ObjectStore.js +8 -8
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/BootstrapFetcher.d.ts +10 -0
- package/dist/sync/BootstrapFetcher.js +27 -4
- package/dist/sync/createClaimStream.d.ts +11 -2
- package/dist/sync/createClaimStream.js +4 -3
- package/dist/testing/mocks/FakeDatabase.d.ts +1 -1
- package/dist/transactions/TransactionQueue.d.ts +2 -2
- package/dist/transactions/TransactionQueue.js +3 -3
- package/dist/transactions/commitEnvelope.d.ts +1 -1
- package/dist/transactions/commitEnvelope.js +2 -2
- package/dist/transactions/commitOutboxStore.d.ts +19 -19
- package/dist/transactions/durableWriteStore.d.ts +123 -0
- package/dist/transactions/durableWriteStore.js +30 -0
- package/dist/transactions/httpCommitEnvelope.d.ts +1 -0
- package/dist/transactions/httpCommitEnvelope.js +14 -12
- package/dist/transactions/idempotencyKey.d.ts +10 -0
- package/dist/transactions/idempotencyKey.js +9 -0
- package/dist/types/global.d.ts +10 -29
- package/dist/types/global.js +4 -7
- package/dist/types/streams.d.ts +2 -28
- package/dist/wire/frames.d.ts +6 -257
- package/dist/wire/frames.js +4 -25
- package/dist/wire/index.d.ts +4 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/protocolVersion.d.ts +30 -17
- package/dist/wire/protocolVersion.js +34 -18
- package/docs/agents.md +8 -3
- package/docs/api.md +6 -4
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +2 -3
- package/docs/data-sources.md +5 -8
- package/docs/guarantees.md +21 -0
- package/docs/integration-guide.md +1 -0
- package/docs/migration.md +21 -1
- package/docs/quickstart.md +11 -0
- package/docs/react.md +0 -46
- package/examples/README.md +6 -5
- package/llms.txt +15 -15
- package/package.json +2 -7
- package/dist/client/ApiClient.d.ts +0 -177
- package/dist/commit/contract.d.ts +0 -493
- package/dist/commit/contract.js +0 -187
- package/dist/commit/index.d.ts +0 -6
- package/dist/commit/index.js +0 -5
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
444
|
-
type
|
|
445
|
-
type
|
|
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').
|
|
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 {
|
package/dist/client/Ablo.js
CHANGED
|
@@ -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,
|
|
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
|
|
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 ??
|
|
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.
|
|
539
|
-
throw new AbloValidationError('
|
|
507
|
+
if (commitOptions.operations.length === 0) {
|
|
508
|
+
throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
|
|
540
509
|
}
|
|
541
|
-
|
|
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 })`
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
28
|
-
/**
|
|
29
|
-
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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<
|
|
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<
|
|
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<
|
|
66
|
-
delete(params: ModelDeleteParams<T>): Promise<
|
|
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>;
|