@abloatai/humans 0.59.2 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Ablo.d.ts +2 -10
- package/dist/Ablo.js +0 -1
- package/dist/client.d.ts +1 -48
- package/dist/humans.d.ts +1 -1
- package/dist/local/BaseSyncedStore.d.ts +5 -5
- package/dist/local/BaseSyncedStore.js +7 -7
- package/dist/local/Database.d.ts +2 -2
- package/dist/local/LazyReferenceCollection.d.ts +1 -1
- package/dist/local/Model.js +46 -56
- package/dist/local/NetworkMonitor.js +2 -0
- package/dist/local/RuntimeContext.js +2 -0
- package/dist/local/SyncClient.d.ts +12 -36
- package/dist/local/SyncClient.js +50 -108
- package/dist/local/client/createModelOperations.d.ts +3 -27
- package/dist/local/client/createModelOperations.js +20 -21
- package/dist/local/client/options.d.ts +14 -39
- package/dist/local/client/reactiveEngine.d.ts +3 -9
- package/dist/local/client/reactiveEngine.js +6 -151
- package/dist/local/client/storeLifecycle.js +5 -1
- package/dist/local/fileUploads.d.ts +27 -0
- package/dist/local/fileUploads.js +55 -0
- package/dist/local/storeContract.d.ts +5 -5
- package/dist/local/stores/syncAction.d.ts +4 -4
- package/dist/local/sync/contextOnChange.js +1 -1
- package/dist/local/sync/createClaimStream.js +1 -1
- package/dist/local/sync/credentialLifecycle.d.ts +4 -5
- package/dist/local/sync/credentialLifecycle.js +4 -5
- package/dist/local/sync/deltaPipeline.js +12 -6
- package/dist/local/sync/schemas.d.ts +10 -10
- package/dist/local/sync/scopeGroups.d.ts +11 -0
- package/dist/local/sync/scopeGroups.js +75 -0
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/transactions/localMutation.js +3 -3
- package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -5
- package/dist/local/transactions/mutations/MutationQueue.js +25 -51
- package/dist/local/transactions/mutations/batchProcessing.js +23 -10
- package/dist/local/transactions/mutations/commitPayload.d.ts +9 -2
- package/dist/local/transactions/mutations/commitTransport.js +3 -1
- package/dist/local/transactions/mutations/executionSelection.d.ts +0 -1
- package/dist/local/transactions/mutations/executionSelection.js +9 -17
- package/dist/local/transactions/mutations/failureHandling.js +9 -0
- package/dist/local/transactions/mutations/localMutation.js +3 -3
- package/dist/local/transactions/mutations/queueCoalescing.js +8 -0
- package/dist/local/transactions/mutations/replayValidation.d.ts +15 -15
- package/dist/react/AbloProvider.d.ts +11 -86
- package/dist/react/AbloProvider.js +11 -163
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/createAbloReact.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/surface.d.ts +3 -3
- package/dist/surface.js +1 -4
- package/package.json +3 -3
- package/src/Ablo.ts +5 -17
- package/src/client.ts +0 -51
- package/src/local/BaseSyncedStore.ts +14 -14
- package/src/local/Model.ts +45 -55
- package/src/local/NetworkMonitor.ts +2 -0
- package/src/local/RuntimeContext.ts +2 -0
- package/src/local/SyncClient.ts +73 -140
- package/src/local/client/createModelOperations.ts +30 -64
- package/src/local/client/options.ts +20 -43
- package/src/local/client/reactiveEngine.ts +7 -179
- package/src/local/client/storeLifecycle.ts +6 -1
- package/src/local/fileUploads.ts +97 -0
- package/src/local/storeContract.ts +5 -5
- package/src/local/sync/contextOnChange.ts +1 -1
- package/src/local/sync/createClaimStream.ts +1 -1
- package/src/local/sync/credentialLifecycle.ts +4 -5
- package/src/local/sync/deltaPipeline.ts +10 -6
- package/src/local/sync/scopeGroups.ts +91 -0
- package/src/local/sync/wsFrameHandlers.ts +0 -1
- package/src/local/transactions/localMutation.ts +3 -3
- package/src/local/transactions/mutations/MutationQueue.ts +24 -53
- package/src/local/transactions/mutations/batchProcessing.ts +25 -10
- package/src/local/transactions/mutations/commitPayload.ts +11 -1
- package/src/local/transactions/mutations/commitTransport.ts +2 -2
- package/src/local/transactions/mutations/executionSelection.ts +9 -15
- package/src/local/transactions/mutations/failureHandling.ts +10 -0
- package/src/local/transactions/mutations/localMutation.ts +3 -3
- package/src/local/transactions/mutations/queueCoalescing.ts +6 -0
- package/src/react/AbloProvider.tsx +17 -249
- package/src/react/useErrorListener.ts +1 -1
- package/src/react/useMutationFailureListener.ts +1 -1
- package/src/react.ts +1 -5
- package/src/surface.ts +1 -4
- package/dist/local/sync/participants.d.ts +0 -132
- package/dist/local/sync/participants.js +0 -342
- package/dist/local/transactions/mutations/pendingDrain.d.ts +0 -33
- package/dist/local/transactions/mutations/pendingDrain.js +0 -117
- package/src/local/sync/participants.ts +0 -564
- package/src/local/transactions/mutations/pendingDrain.ts +0 -169
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `read` and `list`, with the same point lookup restricted to the local graph under
|
|
7
7
|
* `local`, the writes `create`, `update`, and `delete`, the coordination
|
|
8
8
|
* namespace `claim` (callable as `claim({ id })`, plus `claim.state`,
|
|
9
|
-
* `claim.queue`, `claim.release`, and `claim.reorder`),
|
|
9
|
+
* `claim.queue`, `claim.release`, and `claim.reorder`), and `onChange`.
|
|
10
10
|
* The factory returns a plain object; the client assembles the `ablo.<model>`
|
|
11
11
|
* lookup table from one of these per model.
|
|
12
12
|
*/
|
|
@@ -66,7 +66,6 @@ import type { ModelRegistry } from '../ModelRegistry.js';
|
|
|
66
66
|
import type { InstanceCache } from '../InstanceCache.js';
|
|
67
67
|
import type { SyncClient } from '../SyncClient.js';
|
|
68
68
|
import type { OnDemandLoader } from '../sync/OnDemandLoader.js';
|
|
69
|
-
import type { JoinedParticipant } from '../sync/participants.js';
|
|
70
69
|
import { ModelScope } from '@abloatai/transaction/types';
|
|
71
70
|
import type {
|
|
72
71
|
Duration,
|
|
@@ -112,7 +111,6 @@ export type {
|
|
|
112
111
|
ModelCreateParams,
|
|
113
112
|
ModelUpdateParams,
|
|
114
113
|
ModelDeleteParams,
|
|
115
|
-
JoinOptions,
|
|
116
114
|
} from '@abloatai/transaction/client/resources/modelOperations';
|
|
117
115
|
export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease };
|
|
118
116
|
|
|
@@ -126,7 +124,6 @@ import type {
|
|
|
126
124
|
ClaimAttemptEvent,
|
|
127
125
|
ClaimQueueView,
|
|
128
126
|
ClaimReorderParams,
|
|
129
|
-
JoinOptions,
|
|
130
127
|
LocalCountOptions,
|
|
131
128
|
LocalReadOptions,
|
|
132
129
|
ModelCreateManyParams,
|
|
@@ -152,6 +149,9 @@ import {
|
|
|
152
149
|
type ReadSetContext,
|
|
153
150
|
} from '@abloatai/transaction/internal/read-set';
|
|
154
151
|
|
|
152
|
+
const ignoreSeparatelyObservedMutationFailure = (): undefined => undefined;
|
|
153
|
+
const ignoreBestEffortClaimReleaseFailure = (): undefined => undefined;
|
|
154
|
+
|
|
155
155
|
export interface ModelClientMeta {
|
|
156
156
|
readonly key: string;
|
|
157
157
|
readonly typename: string;
|
|
@@ -284,17 +284,6 @@ export interface ModelCollaboration {
|
|
|
284
284
|
* fire-and-forget, best-effort semantics as `enterScope`.
|
|
285
285
|
*/
|
|
286
286
|
pinScope?(scope: Record<string, string>): void | Promise<void>;
|
|
287
|
-
/**
|
|
288
|
-
* Opens a presence and claim subscription on this model's sync group(s) and
|
|
289
|
-
* returns the live participant handle. Backs `ablo.<model>.join(ids)`.
|
|
290
|
-
* WebSocket only, since presence needs a live socket; it is absent on other
|
|
291
|
-
* client constructions, where the surface throws a clear error.
|
|
292
|
-
*/
|
|
293
|
-
createJoin?(
|
|
294
|
-
modelKey: string,
|
|
295
|
-
ids: string | readonly string[],
|
|
296
|
-
options?: JoinOptions,
|
|
297
|
-
): Promise<JoinedParticipant>;
|
|
298
287
|
}
|
|
299
288
|
|
|
300
289
|
|
|
@@ -375,26 +364,6 @@ interface ReactiveModelSurface<T, Fields = T> {
|
|
|
375
364
|
*/
|
|
376
365
|
claim: ClaimApi<T, Fields>;
|
|
377
366
|
|
|
378
|
-
/**
|
|
379
|
-
* Joins the sync group(s) for one or more rows of this model and returns a
|
|
380
|
-
* live participant handle — presence (`.peers`), the scoped claim stream
|
|
381
|
-
* (`.claims`), and `.leave()` / `await using` disposal. This is a presence
|
|
382
|
-
* subscription: it reports who else is here and what they hold, not row
|
|
383
|
-
* values changing — for the latter, use `onChange`.
|
|
384
|
-
*
|
|
385
|
-
* WebSocket only: presence needs a live socket, so this is absent on HTTP
|
|
386
|
-
* clients and throws on any non-WebSocket construction.
|
|
387
|
-
*
|
|
388
|
-
* ```ts
|
|
389
|
-
* await using participant = await ablo.sections.join(sectionIds, { ttl: '5m' });
|
|
390
|
-
* participant.peers; // who else is here
|
|
391
|
-
* ```
|
|
392
|
-
*/
|
|
393
|
-
join(
|
|
394
|
-
ids: string | readonly string[],
|
|
395
|
-
options?: JoinOptions,
|
|
396
|
-
): Promise<JoinedParticipant>;
|
|
397
|
-
|
|
398
367
|
/** Subscribe to changes; the callback runs on every change. */
|
|
399
368
|
onChange(
|
|
400
369
|
callback: (entities: T[]) => void,
|
|
@@ -524,7 +493,7 @@ export function createModelOperations<T, C>(
|
|
|
524
493
|
// await does not create an unhandled-rejection process error. Returning
|
|
525
494
|
// the original promise preserves normal rejection for callers that do
|
|
526
495
|
// await or attach their own catch handler.
|
|
527
|
-
void confirmation.catch(
|
|
496
|
+
void confirmation.catch(ignoreSeparatelyObservedMutationFailure);
|
|
528
497
|
return confirmation;
|
|
529
498
|
};
|
|
530
499
|
};
|
|
@@ -546,7 +515,10 @@ export function createModelOperations<T, C>(
|
|
|
546
515
|
return rows.map((row) => modelAsRow<T>(row));
|
|
547
516
|
};
|
|
548
517
|
|
|
549
|
-
const waitForMutation = async (
|
|
518
|
+
const waitForMutation = async (
|
|
519
|
+
model: Model,
|
|
520
|
+
exactConfirmation?: Promise<void>,
|
|
521
|
+
): Promise<void> => {
|
|
550
522
|
// Model writes are optimistic locally, but their promise has one stable
|
|
551
523
|
// meaning: authoritative confirmation. Callers that do not need the
|
|
552
524
|
// barrier can keep using the row immediately and leave the promise to the
|
|
@@ -558,7 +530,8 @@ export function createModelOperations<T, C>(
|
|
|
558
530
|
// coalescer and producing one SQL transaction per delta.
|
|
559
531
|
await Promise.resolve();
|
|
560
532
|
await syncClient.syncNow();
|
|
561
|
-
|
|
533
|
+
if (exactConfirmation) await exactConfirmation;
|
|
534
|
+
else await syncClient.waitForConfirmation(model.getModelName(), model.id);
|
|
562
535
|
};
|
|
563
536
|
|
|
564
537
|
// Claims this model surface currently holds, keyed by the exact grant id.
|
|
@@ -690,9 +663,9 @@ export function createModelOperations<T, C>(
|
|
|
690
663
|
// This runs after authoritative confirmation. A best-effort abandon frame
|
|
691
664
|
// cannot turn a committed write into an apparent failure; the server has
|
|
692
665
|
// already fulfilled the participant's claims as part of that commit.
|
|
693
|
-
await releaseClaimsForEntity(entityId).catch(
|
|
666
|
+
await releaseClaimsForEntity(entityId).catch(ignoreBestEffortClaimReleaseFailure);
|
|
694
667
|
if (explicit && !explicitWasLocal) {
|
|
695
|
-
await explicit.release?.().catch(
|
|
668
|
+
await explicit.release?.().catch(ignoreBestEffortClaimReleaseFailure);
|
|
696
669
|
}
|
|
697
670
|
};
|
|
698
671
|
|
|
@@ -1402,11 +1375,11 @@ export function createModelOperations<T, C>(
|
|
|
1402
1375
|
}
|
|
1403
1376
|
: {}),
|
|
1404
1377
|
};
|
|
1405
|
-
syncClient.add(model, effective);
|
|
1406
|
-
await waitForMutation(model);
|
|
1378
|
+
const confirmation = syncClient.add(model, effective);
|
|
1379
|
+
await waitForMutation(model, confirmation);
|
|
1407
1380
|
return modelAsRow<T>(model);
|
|
1408
1381
|
} finally {
|
|
1409
|
-
await autoLease?.release?.().catch(
|
|
1382
|
+
await autoLease?.release?.().catch(ignoreBestEffortClaimReleaseFailure);
|
|
1410
1383
|
}
|
|
1411
1384
|
});
|
|
1412
1385
|
|
|
@@ -1528,8 +1501,12 @@ export function createModelOperations<T, C>(
|
|
|
1528
1501
|
: {}),
|
|
1529
1502
|
};
|
|
1530
1503
|
model.applyChanges(patch);
|
|
1531
|
-
syncClient.update(
|
|
1532
|
-
|
|
1504
|
+
const confirmation = syncClient.update(
|
|
1505
|
+
model,
|
|
1506
|
+
effective,
|
|
1507
|
+
patch,
|
|
1508
|
+
);
|
|
1509
|
+
await waitForMutation(model, confirmation);
|
|
1533
1510
|
return modelAsRow<T>(model);
|
|
1534
1511
|
},
|
|
1535
1512
|
});
|
|
@@ -1585,8 +1562,12 @@ export function createModelOperations<T, C>(
|
|
|
1585
1562
|
// the server. (`updateFromData` is the hydration path and would discard
|
|
1586
1563
|
// the tracking, producing an empty `input: {}` no-op mutation.)
|
|
1587
1564
|
model.applyChanges(params.data);
|
|
1588
|
-
syncClient.update(
|
|
1589
|
-
|
|
1565
|
+
const confirmation = syncClient.update(
|
|
1566
|
+
model,
|
|
1567
|
+
effective,
|
|
1568
|
+
params.data,
|
|
1569
|
+
);
|
|
1570
|
+
await waitForMutation(model, confirmation);
|
|
1590
1571
|
const updated = modelAsRow<T>(model);
|
|
1591
1572
|
await settleClaimsAfterWrite(id, handle);
|
|
1592
1573
|
return updated;
|
|
@@ -1658,8 +1639,8 @@ export function createModelOperations<T, C>(
|
|
|
1658
1639
|
...opts,
|
|
1659
1640
|
...(selected ? { claimRef: { id: selected.id } } : {}),
|
|
1660
1641
|
};
|
|
1661
|
-
syncClient.delete(model, effective);
|
|
1662
|
-
await waitForMutation(model);
|
|
1642
|
+
const confirmation = syncClient.delete(model, effective);
|
|
1643
|
+
await waitForMutation(model, confirmation);
|
|
1663
1644
|
await settleClaimsAfterWrite(id, handle);
|
|
1664
1645
|
}),
|
|
1665
1646
|
|
|
@@ -1667,21 +1648,6 @@ export function createModelOperations<T, C>(
|
|
|
1667
1648
|
// readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
|
|
1668
1649
|
claim: claimApi,
|
|
1669
1650
|
|
|
1670
|
-
join: guard(
|
|
1671
|
-
(
|
|
1672
|
-
ids: string | readonly string[],
|
|
1673
|
-
options?: JoinOptions,
|
|
1674
|
-
): Promise<JoinedParticipant> => {
|
|
1675
|
-
if (!collaboration?.createJoin) {
|
|
1676
|
-
throw new AbloValidationError(
|
|
1677
|
-
`Model "${schemaKey}" was built without a WebSocket runtime, so join() is unavailable here. Presence needs a live socket — use the standard Ablo({ schema, apiKey }) client (not the HTTP transport).`,
|
|
1678
|
-
{ code: 'model_join_not_configured' },
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
|
-
return collaboration.createJoin(schemaKey, ids, options);
|
|
1682
|
-
},
|
|
1683
|
-
),
|
|
1684
|
-
|
|
1685
1651
|
onChange(callback, options): () => void {
|
|
1686
1652
|
return autorun(() => {
|
|
1687
1653
|
callback(local.list(options));
|
|
@@ -32,6 +32,11 @@ import type { CommitOutboxScope } from '@abloatai/transaction/commit';
|
|
|
32
32
|
*/
|
|
33
33
|
export type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
|
|
34
34
|
import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
|
|
35
|
+
import type {
|
|
36
|
+
SessionCredential,
|
|
37
|
+
SessionEndpoint,
|
|
38
|
+
SessionProvider,
|
|
39
|
+
} from '@abloatai/transaction/sessions';
|
|
35
40
|
import type { AbloPlugin } from '../../plugin.js';
|
|
36
41
|
import type { ParticipantKind } from '@abloatai/transaction/types/participant';
|
|
37
42
|
|
|
@@ -74,19 +79,21 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
74
79
|
* `ABLO_API_KEY` environment variable, so you usually pass nothing. A
|
|
75
80
|
* long-lived key needs no refresh; the client uses it as-is.
|
|
76
81
|
*
|
|
77
|
-
* - **An async resolver**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* the same renewal machinery as the endpoint form.
|
|
81
|
-
*
|
|
82
|
-
* The endpoint and resolver forms share one contract: return a token; return
|
|
83
|
-
* `null` when the login itself is gone (terminal — the client signs out and
|
|
84
|
-
* fails `ready()` with `session_expired`); or throw on a transient failure, which
|
|
85
|
-
* backs off and retries without signing out. The endpoint form maps HTTP onto
|
|
86
|
-
* this for you: only a structured `401 session_expired` means signed out.
|
|
82
|
+
* - **An async resolver** for advanced process-owned key rotation, such as a
|
|
83
|
+
* vault or workload-identity exchange. Scoped actor renewal belongs in
|
|
84
|
+
* `session` instead.
|
|
87
85
|
*/
|
|
88
86
|
apiKey?: string | CredentialProvider | null | undefined;
|
|
89
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Scoped actor identity. Pass a session returned by `sessions.create()` for
|
|
90
|
+
* bounded work, a provider that re-mints it for a long-lived client, or
|
|
91
|
+
* `{ endpoint: '/api/ablo-session' }` in a browser. Endpoint responses use
|
|
92
|
+
* the canonical credential protocol; only a structured `401
|
|
93
|
+
* session_expired` ends the underlying login.
|
|
94
|
+
*/
|
|
95
|
+
session?: SessionCredential | SessionProvider | SessionEndpoint | null | undefined;
|
|
96
|
+
|
|
90
97
|
/**
|
|
91
98
|
* Pins this client to one Ablo project. During `ready()` the server resolves
|
|
92
99
|
* the API key's actual project and the client refuses to start when it differs.
|
|
@@ -105,33 +112,6 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
105
112
|
*/
|
|
106
113
|
branchId?: string | null | undefined;
|
|
107
114
|
|
|
108
|
-
/**
|
|
109
|
-
* The session-mint endpoint — the browser-side auth field, and the named
|
|
110
|
-
* endpoint for the route that mints the signed-in user's short-lived token:
|
|
111
|
-
*
|
|
112
|
-
* ```ts
|
|
113
|
-
* const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
114
|
-
* ```
|
|
115
|
-
*
|
|
116
|
-
* The client owns the whole exchange: it POSTs the route (same-origin, cookies
|
|
117
|
-
* included), validates the canonical auth response contract, keeps it fresh
|
|
118
|
-
* ahead of expiry, and re-mints when the server reports the token stale. Only
|
|
119
|
-
* a structured `401 session_expired` response means signed out. It also
|
|
120
|
-
* accepts an async resolver `() => Promise<string | null>` when the exchange
|
|
121
|
-
* needs custom headers or a body — the same contract as the resolver form of
|
|
122
|
-
* `apiKey`.
|
|
123
|
-
*
|
|
124
|
-
* Mutually exclusive with `apiKey`: a server holds a key, a browser holds a mint
|
|
125
|
-
* route, and passing both is a validation error.
|
|
126
|
-
*/
|
|
127
|
-
authEndpoint?: string | CredentialProvider | null | undefined;
|
|
128
|
-
|
|
129
|
-
/** Timeout for a session-mint request. @default 10000 */
|
|
130
|
-
authTimeoutMs?: number | undefined;
|
|
131
|
-
|
|
132
|
-
/** Explicit opt-in for a cross-origin session-mint endpoint. */
|
|
133
|
-
allowCrossOriginAuthEndpoint?: boolean | undefined;
|
|
134
|
-
|
|
135
115
|
/**
|
|
136
116
|
* Local persistence mode. Pass `indexeddb` only when you want offline
|
|
137
117
|
* queueing and a reload-surviving browser cache.
|
|
@@ -264,15 +244,12 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
264
244
|
*/
|
|
265
245
|
apiKey?: string | CredentialProvider | null | undefined;
|
|
266
246
|
|
|
247
|
+
/** A scoped session, or a provider that re-mints it for a long-lived client. */
|
|
248
|
+
session?: SessionCredential | SessionProvider | SessionEndpoint | null | undefined;
|
|
249
|
+
|
|
267
250
|
/** Expected project assertion; see {@link AbloOptions.projectId}. */
|
|
268
251
|
projectId?: string | null | undefined;
|
|
269
252
|
|
|
270
|
-
/**
|
|
271
|
-
* Session-mint endpoint (string or async resolver) — see
|
|
272
|
-
* {@link AbloOptions.authEndpoint}. Mutually exclusive with `apiKey`.
|
|
273
|
-
*/
|
|
274
|
-
authEndpoint?: string | CredentialProvider | null | undefined;
|
|
275
|
-
|
|
276
253
|
/**
|
|
277
254
|
* A bearer auth token, sent as `Authorization: Bearer <token>` on every request.
|
|
278
255
|
*
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* The reactive engine assembly (ADR 0016). `Ablo({ ... })` resolves auth and
|
|
3
3
|
* capabilities; `humans().init` constructs the store cluster; the lifecycle
|
|
4
4
|
* — first mint, identity, ready() — lives in `./storeLifecycle.ts`. What
|
|
5
|
-
* remains here is assembly around those parts: the claim
|
|
6
|
-
*
|
|
7
|
-
* commit
|
|
5
|
+
* remains here is assembly around those parts: the claim and presence streams,
|
|
6
|
+
* options validation, the typed model proxies, and the
|
|
7
|
+
* commit and claim resources — composed into the reactive client.
|
|
8
8
|
*
|
|
9
9
|
* Extracted from the factory so the composition root stays a root: resolve,
|
|
10
10
|
* dispatch, return. The remaining assembly converts to decoration of a
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
* design step (docs/plans/package-split.md).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type {
|
|
15
|
+
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
16
16
|
import { omittedModelError } from '@abloatai/transaction/schema/select';
|
|
17
17
|
import {
|
|
18
18
|
durableCommitOperationSchema,
|
|
19
19
|
type DurableCommitOperation,
|
|
20
20
|
} from '@abloatai/transaction/commit';
|
|
21
|
-
import {
|
|
21
|
+
import { AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
|
|
22
22
|
import type { ModelTarget, ModelClaim } from '@abloatai/transaction/coordination/schema';
|
|
23
23
|
import type { BatchFence } from '@abloatai/transaction/coordination';
|
|
24
24
|
import {
|
|
@@ -30,13 +30,6 @@ import {
|
|
|
30
30
|
subTarget,
|
|
31
31
|
} from '@abloatai/transaction/coordination';
|
|
32
32
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
33
|
-
import { mintSession } from '@abloatai/transaction/auth/sessionMint';
|
|
34
|
-
import type { MintSessionContext } from '@abloatai/transaction/auth/sessionMint';
|
|
35
|
-
import {
|
|
36
|
-
revokeCapability,
|
|
37
|
-
rotateCapability,
|
|
38
|
-
} from '@abloatai/transaction/auth/capabilityLifecycle';
|
|
39
|
-
import { modelWireNames } from '@abloatai/transaction/auth/capability';
|
|
40
33
|
import type { StoreCluster } from './storeCluster.js';
|
|
41
34
|
import { startStoreLifecycle } from './storeLifecycle.js';
|
|
42
35
|
import type { SyncWebSocket, CoreSyncEventMap } from '../sync/SyncWebSocket.js';
|
|
@@ -46,25 +39,19 @@ import {
|
|
|
46
39
|
bindClaimLifetime,
|
|
47
40
|
claimLifetimeOf,
|
|
48
41
|
} from '@abloatai/transaction/claims/lifetime';
|
|
49
|
-
import { createParticipantManager } from '../sync/participants.js';
|
|
50
42
|
import type { AttachablePresenceStream } from '../../presenceStream.js';
|
|
51
43
|
import type { ClaimWaitOptions } from '@abloatai/transaction/types/streams';
|
|
52
44
|
import type { Claim } from '@abloatai/transaction/types/streams';
|
|
53
|
-
import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
|
|
54
45
|
import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '@abloatai/transaction/auth/apiKey';
|
|
55
46
|
import type { AbloOptions } from './options.js';
|
|
56
47
|
import type { ClientPrelude } from './clientPrelude.js';
|
|
57
48
|
import type {
|
|
58
|
-
AbloSession,
|
|
59
49
|
ClaimCreateOptions,
|
|
60
50
|
ClaimResource,
|
|
61
51
|
CommitCreateOptions,
|
|
62
52
|
CommitOperationInput,
|
|
63
53
|
CommitReceipt,
|
|
64
54
|
CommitResource,
|
|
65
|
-
CreateAgentClientParams,
|
|
66
|
-
CreateAgentSessionParams,
|
|
67
|
-
CreateSessionParams,
|
|
68
55
|
} from './resourceTypes.js';
|
|
69
56
|
import {
|
|
70
57
|
claimAttemptFailure,
|
|
@@ -116,12 +103,6 @@ export interface ReactiveEngineInputs<S extends SchemaRecord> extends ClientPrel
|
|
|
116
103
|
* assembles around it and constructs none of it.
|
|
117
104
|
*/
|
|
118
105
|
cluster: StoreCluster;
|
|
119
|
-
/**
|
|
120
|
-
* Constructs a sibling client (`ablo.agents.create(...)` mints a scoped key
|
|
121
|
-
* and builds a second engine with it). Injected by the factory — a direct
|
|
122
|
-
* import back into it would close a runtime cycle.
|
|
123
|
-
*/
|
|
124
|
-
createSibling: (options: AbloOptions<S>) => Ablo<S>;
|
|
125
106
|
}
|
|
126
107
|
|
|
127
108
|
export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
@@ -141,7 +122,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
141
122
|
kind,
|
|
142
123
|
presence,
|
|
143
124
|
cluster,
|
|
144
|
-
createSibling,
|
|
145
125
|
} = inputs;
|
|
146
126
|
const schema = options.schema;
|
|
147
127
|
const pointReadBaseUrl = resolveBootstrapBaseUrl({
|
|
@@ -231,7 +211,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
231
211
|
logger.warn(
|
|
232
212
|
'Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
|
|
233
213
|
'the server derives participant identity from the key’s scope. Remove ' +
|
|
234
|
-
'them (or mint a scoped session
|
|
214
|
+
'them (or mint a scoped session with `Sessions({ schema, apiKey }).create({ agent })` ' +
|
|
235
215
|
'for a distinct agent identity). They apply only to the self-hosted ' +
|
|
236
216
|
'`capabilityToken` path.',
|
|
237
217
|
);
|
|
@@ -274,14 +254,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
274
254
|
});
|
|
275
255
|
const ready = lifecycle.ready;
|
|
276
256
|
|
|
277
|
-
const participantManager = createParticipantManager({
|
|
278
|
-
ready,
|
|
279
|
-
transport,
|
|
280
|
-
presence: presenceStream,
|
|
281
|
-
claims: claimStream,
|
|
282
|
-
schema,
|
|
283
|
-
});
|
|
284
|
-
|
|
285
257
|
// 9b. waitForFlush — drains pending mutations using the store's
|
|
286
258
|
// pendingChanges counter (already maintained by BaseSyncedStore based
|
|
287
259
|
// on MutationQueue events). Polls every 50ms; uses the existing
|
|
@@ -644,17 +616,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
644
616
|
// reconcile errors so read interest never makes a read reject or stall.
|
|
645
617
|
enterScope: (scope) => store.enterScope(scope),
|
|
646
618
|
pinScope: (scope) => store.pinScope(scope),
|
|
647
|
-
// `ablo.<model>.join(ids, { ttl })` performs a scoped participant join
|
|
648
|
-
// on this model's sync group(s). WebSocket only — `join` throws
|
|
649
|
-
// `AbloConnectionError` if the socket isn't ready.
|
|
650
|
-
// `ttl` passes straight through — both surfaces spell the lease the
|
|
651
|
-
// same way now, so there is no rename here to make a field's name
|
|
652
|
-
// disagree with the value it carries.
|
|
653
|
-
createJoin: (modelKey, ids, options) =>
|
|
654
|
-
participantManager.join({
|
|
655
|
-
scope: { [modelKey]: ids },
|
|
656
|
-
...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
657
|
-
}),
|
|
658
619
|
},
|
|
659
620
|
readSetContext,
|
|
660
621
|
);
|
|
@@ -667,7 +628,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
667
628
|
await ready();
|
|
668
629
|
const prepared = prepareReadSet(
|
|
669
630
|
cluster.readSetContext,
|
|
670
|
-
syncClient
|
|
631
|
+
syncClient,
|
|
671
632
|
commitOptions.readAt,
|
|
672
633
|
commitOptions.idempotencyKey,
|
|
673
634
|
commitOptions.reads,
|
|
@@ -752,50 +713,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
752
713
|
},
|
|
753
714
|
};
|
|
754
715
|
|
|
755
|
-
/**
|
|
756
|
-
* The control-plane credential: always the original configured secret key.
|
|
757
|
-
* Never reads `authCredentials` — that holds the exchanged sync credential
|
|
758
|
-
* (a wide-scope `rk_` on the hosted path), which control-plane routes
|
|
759
|
-
* rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
|
|
760
|
-
* `getAuthToken()`, which resolves the sync-plane token.
|
|
761
|
-
*
|
|
762
|
-
* The secret-key-only rule is enforced on the server; the credential-kind taxonomy
|
|
763
|
-
* (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
|
|
764
|
-
*/
|
|
765
|
-
async function controlPlaneApiKey(): Promise<string | null> {
|
|
766
|
-
return resolveApiKeyValue(configuredApiKey);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* Resolve the control-plane context a session/agent mint needs (sk_ +
|
|
771
|
-
* bootstrap base URL + the schema-key→typename map the server gates on).
|
|
772
|
-
* Shared by `sessions.create` and `agents.create` so the two mint doors
|
|
773
|
-
* can never drift on how a token is minted. Throws if no `sk_` is present —
|
|
774
|
-
* minting is a backend-only operation.
|
|
775
|
-
*/
|
|
776
|
-
async function buildMintContext(resource: string): Promise<MintSessionContext> {
|
|
777
|
-
const apiKey = await controlPlaneApiKey();
|
|
778
|
-
if (!apiKey) {
|
|
779
|
-
throw new AbloAuthenticationError(
|
|
780
|
-
`${resource} requires a secret (sk_) API key — call it from your backend, not the browser.`,
|
|
781
|
-
{ code: 'apikey_missing' },
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
return {
|
|
785
|
-
apiKey,
|
|
786
|
-
baseUrl: resolveBootstrapBaseUrl({
|
|
787
|
-
url,
|
|
788
|
-
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
789
|
-
}),
|
|
790
|
-
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
791
|
-
// Map every `can` schema-key to the wire typename the server gates on, so a
|
|
792
|
-
// typename override (`documents` → `Document`) doesn't mint a capability
|
|
793
|
-
// the server then denies. Derived from this client's schema by the one rule
|
|
794
|
-
// the HTTP client and the mint route also read. See `MintSessionContext`.
|
|
795
|
-
modelTypenames: modelWireNames(schema.models),
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
|
|
799
716
|
const engine = {
|
|
800
717
|
...modelProxies,
|
|
801
718
|
|
|
@@ -826,11 +743,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
826
743
|
// is the canonical credential; fall back to a configured API key.
|
|
827
744
|
//
|
|
828
745
|
// This is the sync-plane token (bootstrap, WebSocket, query HTTP). Control-plane
|
|
829
|
-
// calls (sessions.create, datasource registration) never use it — they
|
|
830
|
-
// present the original secret key via `controlPlaneApiKey()` below. The
|
|
831
|
-
// split matters: after the startup exchange this resolver returns the
|
|
832
|
-
// derived wide-scope `rk_`, a credential the control-plane routes
|
|
833
|
-
// correctly refuse (an agent token must never mint humans).
|
|
834
746
|
return (
|
|
835
747
|
authCredentials.getAuthToken() ??
|
|
836
748
|
(await resolveApiKeyValue(configuredApiKey)) ??
|
|
@@ -857,90 +769,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
857
769
|
store.nudgeReconnect();
|
|
858
770
|
},
|
|
859
771
|
|
|
860
|
-
sessions: {
|
|
861
|
-
// A backend (holding `sk_`) mints a short-lived scoped token for one end
|
|
862
|
-
// user or one agent.
|
|
863
|
-
//
|
|
864
|
-
// Both arms authenticate with the original secret key
|
|
865
|
-
// (`controlPlaneApiKey()`), never the wide-scope `rk_` the startup exchange
|
|
866
|
-
// installed as the sync credential. A derived agent credential silently
|
|
867
|
-
// replacing the secret key on control-plane calls is how humans would get
|
|
868
|
-
// minted as agents — and correct attribution is the point.
|
|
869
|
-
async create(params: CreateSessionParams<S>): Promise<AbloSession> {
|
|
870
|
-
// Both mint paths (`{ user }` → /v1/ephemeral_keys → `ek_`,
|
|
871
|
-
// `{ agent, can }` → /v1/capabilities → scoped `rk_`) resolve their
|
|
872
|
-
// control-plane context through the shared `buildMintContext`, so this
|
|
873
|
-
// client, `agents.create`, and the stateless HTTP client can't drift on
|
|
874
|
-
// how a token is minted.
|
|
875
|
-
return mintSession(params, await buildMintContext('sessions.create'));
|
|
876
|
-
},
|
|
877
|
-
async revoke({ id }) {
|
|
878
|
-
const context = await buildMintContext('sessions.revoke');
|
|
879
|
-
return revokeCapability({
|
|
880
|
-
apiKey: context.apiKey,
|
|
881
|
-
baseUrl: context.baseUrl,
|
|
882
|
-
id,
|
|
883
|
-
...(context.fetch ? { fetch: context.fetch } : {}),
|
|
884
|
-
});
|
|
885
|
-
},
|
|
886
|
-
async rotate({ id, graceSeconds, ttlSeconds }) {
|
|
887
|
-
const context = await buildMintContext('sessions.rotate');
|
|
888
|
-
return rotateCapability({
|
|
889
|
-
apiKey: context.apiKey,
|
|
890
|
-
baseUrl: context.baseUrl,
|
|
891
|
-
id,
|
|
892
|
-
...(graceSeconds !== undefined ? { graceSeconds } : {}),
|
|
893
|
-
...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
|
|
894
|
-
...(context.fetch ? { fetch: context.fetch } : {}),
|
|
895
|
-
});
|
|
896
|
-
},
|
|
897
|
-
},
|
|
898
|
-
|
|
899
|
-
// Mint a scoped agent identity and hand back a connected client bound to it —
|
|
900
|
-
// `sessions.create({ agent })` plus a typed `Ablo({ schema, apiKey })` client,
|
|
901
|
-
// for agents that run in this (secret-key-holding) process. Omitting `id`
|
|
902
|
-
// yields a fresh uuid per call, so concurrent agents are distinct participants
|
|
903
|
-
// that queue behind each other (even when they share a `name`). Humans don't
|
|
904
|
-
// get a server-built client — ship them a token via `sessions.create({ user })`.
|
|
905
|
-
agents: {
|
|
906
|
-
async create(params: CreateAgentClientParams<S>): Promise<Ablo<S>> {
|
|
907
|
-
// Distinct participant by default: omit `id` → a fresh uuid, so even two
|
|
908
|
-
// agents that share a `name` are independent participants and queue
|
|
909
|
-
// behind one another. `name` is display only (→ userMeta.name); it never
|
|
910
|
-
// derives the id. Pass an explicit `id` only to re-attach an agent to
|
|
911
|
-
// its own held claims.
|
|
912
|
-
const id = params.id ?? globalThis.crypto.randomUUID();
|
|
913
|
-
const userMeta =
|
|
914
|
-
params.name !== undefined ? { ...params.userMeta, name: params.name } : params.userMeta;
|
|
915
|
-
const sessionParams = {
|
|
916
|
-
agent: { id },
|
|
917
|
-
can: params.can,
|
|
918
|
-
...(params.onBehalfOf ? { onBehalfOf: params.onBehalfOf } : {}),
|
|
919
|
-
...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
|
|
920
|
-
...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
|
|
921
|
-
...(userMeta ? { userMeta } : {}),
|
|
922
|
-
} satisfies CreateAgentSessionParams<S>;
|
|
923
|
-
// Re-mint the `rk_` on every resolver call so a long-lived agent client
|
|
924
|
-
// never hits token expiry; the `sk_` stays in this process — the child
|
|
925
|
-
// only ever sees its own short-lived `rk_`.
|
|
926
|
-
const mintToken = async (): Promise<string> =>
|
|
927
|
-
(await mintSession(sessionParams, await buildMintContext('agents.create')))
|
|
928
|
-
.token;
|
|
929
|
-
// Mint once up front so a bad key / denied scope throws HERE, not later
|
|
930
|
-
// inside the child's bootstrap; reuse that first token, re-mint on refresh.
|
|
931
|
-
let pending: string | null = await mintToken();
|
|
932
|
-
const apiKey: CredentialProvider = async () => {
|
|
933
|
-
if (pending !== null) {
|
|
934
|
-
const token = pending;
|
|
935
|
-
pending = null;
|
|
936
|
-
return token;
|
|
937
|
-
}
|
|
938
|
-
return mintToken();
|
|
939
|
-
};
|
|
940
|
-
return createSibling({ ...(internalOptions as AbloOptions<S>), apiKey });
|
|
941
|
-
},
|
|
942
|
-
},
|
|
943
|
-
|
|
944
772
|
async dispose() {
|
|
945
773
|
lifecycle.dispose();
|
|
946
774
|
try {
|
|
@@ -143,7 +143,12 @@ export function startStoreLifecycle<S extends SchemaRecord>(
|
|
|
143
143
|
// unambiguously a deliberate server client). User-kind clients in Node (an
|
|
144
144
|
// SSR/RSC module evaluating scaffolded browser code) stay reactive-only.
|
|
145
145
|
if (credentialResolver) {
|
|
146
|
-
const rawEndpoint =
|
|
146
|
+
const rawEndpoint =
|
|
147
|
+
internalOptions.session &&
|
|
148
|
+
typeof internalOptions.session === 'object' &&
|
|
149
|
+
'endpoint' in internalOptions.session
|
|
150
|
+
? internalOptions.session.endpoint
|
|
151
|
+
: internalOptions.apiKey;
|
|
147
152
|
const absoluteEndpoint =
|
|
148
153
|
typeof rawEndpoint === 'string' && /^https?:\/\//i.test(rawEndpoint);
|
|
149
154
|
store.startCredentialLifecycle(credentialResolver, {
|