@abloatai/humans 0.59.2 → 0.60.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 +2 -2
- package/dist/local/Database.d.ts +2 -2
- package/dist/local/LazyReferenceCollection.d.ts +1 -1
- package/dist/local/SyncClient.d.ts +7 -7
- package/dist/local/SyncClient.js +25 -10
- package/dist/local/client/createModelOperations.d.ts +3 -27
- package/dist/local/client/createModelOperations.js +14 -17
- 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/storeContract.d.ts +5 -5
- package/dist/local/stores/syncAction.d.ts +4 -4
- package/dist/local/sync/credentialLifecycle.d.ts +4 -5
- package/dist/local/sync/credentialLifecycle.js +4 -5
- 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/mutations/MutationQueue.d.ts +3 -3
- package/dist/local/transactions/mutations/commitPayload.d.ts +1 -1
- 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.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 -2
- package/src/Ablo.ts +5 -17
- package/src/client.ts +0 -51
- package/src/local/BaseSyncedStore.ts +9 -9
- package/src/local/SyncClient.ts +41 -14
- package/src/local/client/createModelOperations.ts +23 -60
- 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/storeContract.ts +5 -5
- package/src/local/sync/credentialLifecycle.ts +4 -5
- package/src/local/sync/scopeGroups.ts +91 -0
- package/src/local/sync/wsFrameHandlers.ts +0 -1
- package/src/react/AbloProvider.tsx +17 -249
- 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/src/local/sync/participants.ts +0 -564
|
@@ -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, {
|
|
@@ -14,7 +14,7 @@ import type { Model } from './Model.js';
|
|
|
14
14
|
import type { ModelScope } from '@abloatai/transaction/types';
|
|
15
15
|
import type { QueryView, QueryViewOptions } from './views/QueryView.js';
|
|
16
16
|
import type { ViewRegistry } from './views/ViewRegistry.js';
|
|
17
|
-
import type {
|
|
17
|
+
import type { GroupScope } from './sync/scopeGroups.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* A snapshot of the client's synchronization state, shaped for binding to UI.
|
|
@@ -132,10 +132,10 @@ export interface SyncStoreContract {
|
|
|
132
132
|
* read subscriptions and write claims always agree on which group they refer
|
|
133
133
|
* to. These are optional and do nothing until the connection is open.
|
|
134
134
|
*/
|
|
135
|
-
enterScope?(scope:
|
|
136
|
-
leaveScope?(scope:
|
|
137
|
-
pinScope?(scope:
|
|
138
|
-
unpinScope?(scope:
|
|
135
|
+
enterScope?(scope: GroupScope, opts?: { hydrate?: boolean }): Promise<void>;
|
|
136
|
+
leaveScope?(scope: GroupScope): Promise<void>;
|
|
137
|
+
pinScope?(scope: GroupScope): Promise<void>;
|
|
138
|
+
unpinScope?(scope: GroupScope): Promise<void>;
|
|
139
139
|
/**
|
|
140
140
|
* The full reactive {@link SyncStatus} record. The `useSyncStatus()` hook
|
|
141
141
|
* reads its fields — `state`, `progress`, `pendingChanges`, `isSessionError`,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* stay unchanged.
|
|
2
|
+
* The shared session subsystem owns renewal for both browser and agent
|
|
3
|
+
* sessions. This local boundary keeps the reactive store pointed downward at
|
|
4
|
+
* that one lifecycle implementation.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
export {
|
|
@@ -15,4 +14,4 @@ export {
|
|
|
15
14
|
type CredentialRefreshResult,
|
|
16
15
|
type CredentialRefresher,
|
|
17
16
|
type CredentialLifecycleContext,
|
|
18
|
-
} from '@abloatai/transaction/
|
|
17
|
+
} from '@abloatai/transaction/sessions';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ClaimTarget } from '@abloatai/transaction/types/streams';
|
|
2
|
+
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
3
|
+
import { scopeKindOf, type ModelDef } from '@abloatai/transaction/schema/model';
|
|
4
|
+
|
|
5
|
+
/** A schema-shaped selector used to narrow connection groups and presence reads. */
|
|
6
|
+
export type GroupScope =
|
|
7
|
+
| ClaimTarget
|
|
8
|
+
| readonly ClaimTarget[]
|
|
9
|
+
| string
|
|
10
|
+
| readonly string[]
|
|
11
|
+
| { readonly syncGroup: string }
|
|
12
|
+
| { readonly syncGroups: readonly string[] }
|
|
13
|
+
| Record<string, string | readonly string[] | undefined>;
|
|
14
|
+
|
|
15
|
+
/** Resolve an application-shaped scope into the wire groups owned by the schema. */
|
|
16
|
+
export function resolveScopeGroups(
|
|
17
|
+
scope: GroupScope | undefined,
|
|
18
|
+
schema?: Schema,
|
|
19
|
+
): string[] {
|
|
20
|
+
if (!scope) return [];
|
|
21
|
+
if (typeof scope === 'string') return [scope];
|
|
22
|
+
if (Array.isArray(scope)) {
|
|
23
|
+
const groups: string[] = [];
|
|
24
|
+
for (const entry of scope as readonly unknown[]) {
|
|
25
|
+
if (typeof entry === 'string') groups.push(entry);
|
|
26
|
+
else if (isEntityScope(entry)) groups.push(groupFromEntityRef(entry, schema));
|
|
27
|
+
}
|
|
28
|
+
return groups;
|
|
29
|
+
}
|
|
30
|
+
const direct = scope as { syncGroup?: unknown; syncGroups?: unknown };
|
|
31
|
+
if (isEntityScope(scope)) return [groupFromEntityRef(scope, schema)];
|
|
32
|
+
if (typeof direct.syncGroup === 'string') return [direct.syncGroup];
|
|
33
|
+
if (Array.isArray(direct.syncGroups)) {
|
|
34
|
+
return direct.syncGroups.filter((group): group is string => typeof group === 'string');
|
|
35
|
+
}
|
|
36
|
+
const groups: string[] = [];
|
|
37
|
+
for (const [key, value] of Object.entries(scope) as [string, unknown][]) {
|
|
38
|
+
if (value === undefined) continue;
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
for (const id of value as unknown[]) {
|
|
41
|
+
if (typeof id === 'string') groups.push(groupFromSchemaKey(key, id, schema));
|
|
42
|
+
}
|
|
43
|
+
} else if (typeof value === 'string') {
|
|
44
|
+
groups.push(groupFromSchemaKey(key, value, schema));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return groups;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function groupFromEntityRef(ref: ClaimTarget, schema?: Schema): string {
|
|
51
|
+
const match = findModelForEntityRef(ref, schema);
|
|
52
|
+
const kind = match
|
|
53
|
+
? groupKindForModel(match.def, match.key)
|
|
54
|
+
: ref.type.toLowerCase();
|
|
55
|
+
return `${kind}:${ref.id}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function groupFromSchemaKey(schemaKey: string, id: string, schema?: Schema): string {
|
|
59
|
+
const def = schema?.models[schemaKey];
|
|
60
|
+
const kind = def ? groupKindForModel(def, schemaKey) : schemaKey.toLowerCase();
|
|
61
|
+
return `${kind}:${id}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function groupKindForModel(def: ModelDef, key: string): string {
|
|
65
|
+
return scopeKindOf(def, key) ?? (def.typename ?? key).toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findModelForEntityRef(
|
|
69
|
+
ref: ClaimTarget,
|
|
70
|
+
schema?: Schema,
|
|
71
|
+
): { key: string; def: ModelDef } | null {
|
|
72
|
+
if (!schema?.models) return null;
|
|
73
|
+
const wanted = ref.type.toLowerCase();
|
|
74
|
+
for (const [key, def] of Object.entries(schema.models) as [string, ModelDef][]) {
|
|
75
|
+
const typename = def.typename ?? key;
|
|
76
|
+
if (typename.toLowerCase() === wanted || key.toLowerCase() === wanted) {
|
|
77
|
+
return { key, def };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isEntityScope(scope: unknown): scope is ClaimTarget {
|
|
84
|
+
return (
|
|
85
|
+
typeof scope === 'object' &&
|
|
86
|
+
scope !== null &&
|
|
87
|
+
!Array.isArray(scope) &&
|
|
88
|
+
typeof (scope as { type?: unknown }).type === 'string' &&
|
|
89
|
+
typeof (scope as { id?: unknown }).id === 'string'
|
|
90
|
+
);
|
|
91
|
+
}
|