@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState, createContext, } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveScopeGroups } from '../local/sync/scopeGroups.js';
|
|
5
5
|
import { SyncContext } from './context.js';
|
|
6
6
|
import { AbloInternalContext } from './internalContext.js';
|
|
7
7
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
@@ -200,158 +200,10 @@ function BootstrapGate({ fallback, children, }) {
|
|
|
200
200
|
return _jsx(_Fragment, { children: showFallback ? fallback : children });
|
|
201
201
|
}
|
|
202
202
|
const EMPTY_PRESENCE = Object.freeze([]);
|
|
203
|
-
const EMPTY_INTENTS = Object.freeze([]);
|
|
204
|
-
/**
|
|
205
|
-
* Join multiplayer for a given scope. Returns the participant and its
|
|
206
|
-
* lifecycle status. Auto-cleans up on unmount or when `paused`
|
|
207
|
-
* flips to true.
|
|
208
|
-
*
|
|
209
|
-
* `useJoin` is the React form of `ablo.<model>.join` — scope-level
|
|
210
|
-
* read-interest + presence; returns the reactive participant facade
|
|
211
|
-
* (peers/claims/status).
|
|
212
|
-
*
|
|
213
|
-
* The returned `participant` is an `EngineParticipant` — `.presence`
|
|
214
|
-
* + `.claims` only — backed by the engine's existing socket. For
|
|
215
|
-
* headless-bot patterns (a separate identity in the same browser
|
|
216
|
-
* tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
|
|
217
|
-
*/
|
|
218
|
-
export function useJoin(opts) {
|
|
219
|
-
const ctx = useContext(AbloInternalContext);
|
|
220
|
-
const engine = ctx?.engine ?? null;
|
|
221
|
-
const { paused = false } = opts;
|
|
222
|
-
// Resolve the model-form scope ({ reports: id } / refs) THROUGH the schema, so a
|
|
223
|
-
// model's declared `scope` kind is honored (typename `Report` → `report:<id>`,
|
|
224
|
-
// not the `type:id` string fallback). Schema appears once the engine is ready;
|
|
225
|
-
// until then refs resolve by convention, then re-resolve when it arrives.
|
|
226
|
-
const scopeKey = JSON.stringify(resolveParticipantSyncGroups(opts.scope, engine?.schema).sort());
|
|
227
|
-
const scopedSyncGroups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
|
|
228
|
-
const [claimError, setClaimError] = useState(null);
|
|
229
|
-
const [claimConnected, setClaimConnected] = useState(false);
|
|
230
|
-
// Reference-stable participant facade — same socket as entity sync,
|
|
231
|
-
// so there is no `connect()` / `disconnect()` lifecycle here. The
|
|
232
|
-
// engine manages the connection; the hook is a thin window onto its
|
|
233
|
-
// already-attached presence + claim streams.
|
|
234
|
-
const participant = useMemo(() => {
|
|
235
|
-
if (!engine)
|
|
236
|
-
return null;
|
|
237
|
-
return { presence: engine.presence, claims: engine.claims };
|
|
238
|
-
}, [engine]);
|
|
239
|
-
// Status maps to the engine's sync state. `connecting` while the
|
|
240
|
-
// engine bootstraps; `connected` once `engine.ready()` resolves and
|
|
241
|
-
// any scoped participant claim has acked; `error` if the claim
|
|
242
|
-
// fails; `disconnected` while paused or before the engine exists.
|
|
243
|
-
const syncStatus = useSyncStatus();
|
|
244
|
-
// Only a write-claim participant waits on a claim ack. A pure reader
|
|
245
|
-
// (the default) is `connected` as soon as the engine is — its read
|
|
246
|
-
// interest is fire-and-forget `update_subscription`, not a claim.
|
|
247
|
-
const needsClaim = !!opts.claim && scopedSyncGroups.length > 0;
|
|
248
|
-
const status = paused || !engine
|
|
249
|
-
? 'disconnected'
|
|
250
|
-
: claimError
|
|
251
|
-
? 'error'
|
|
252
|
-
: syncStatus.name === 'connected'
|
|
253
|
-
? needsClaim && !claimConnected
|
|
254
|
-
? 'connecting'
|
|
255
|
-
: 'connected'
|
|
256
|
-
: syncStatus.name === 'disconnected' || syncStatus.name === 'needs-auth'
|
|
257
|
-
? 'disconnected'
|
|
258
|
-
: 'connecting';
|
|
259
|
-
const error = claimError;
|
|
260
|
-
// ── Read interest (always) ───────────────────────────────────────
|
|
261
|
-
// Subscribe the connection to the scope's sync groups while mounted +
|
|
262
|
-
// connected — the area-of-interest navigation primitive. No claim, no
|
|
263
|
-
// TTL: a viewer just receives the scope's deltas. Hysteresis (warm TTL)
|
|
264
|
-
// lives in the store's SubscriptionManager, so a quick unmount/remount
|
|
265
|
-
// (tab flip) doesn't re-bootstrap.
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
const scope = opts.scope;
|
|
268
|
-
if (paused || !engine || !scope || scopedSyncGroups.length === 0)
|
|
269
|
-
return;
|
|
270
|
-
if (syncStatus.name !== 'connected')
|
|
271
|
-
return;
|
|
272
|
-
const store = engine._store;
|
|
273
|
-
// `hydrate` backfills the scope's current state after subscribing
|
|
274
|
-
// (store handles subscribe-first ordering + single-flight). leaveScope
|
|
275
|
-
// only moves read interest; the hydrated rows stay in the pool.
|
|
276
|
-
void store.enterScope?.(scope, { hydrate: opts.hydrate });
|
|
277
|
-
return () => {
|
|
278
|
-
void store.leaveScope?.(scope);
|
|
279
|
-
};
|
|
280
|
-
// scopeKey is the stable proxy for the resolved groups; same idiom as
|
|
281
|
-
// the claim effect below.
|
|
282
|
-
}, [engine, paused, scopeKey, syncStatus.name, opts.hydrate]);
|
|
283
|
-
// ── Write claim (opt-in: `claim: true`) ─────────────────────────
|
|
284
|
-
// A claim is the write-claim primitive — distinct from read interest
|
|
285
|
-
// above. Only sent when the caller opts in; it makes peers observe the
|
|
286
|
-
// claim and pins the scope so it never warms while held.
|
|
287
|
-
useEffect(() => {
|
|
288
|
-
setClaimError(null);
|
|
289
|
-
setClaimConnected(false);
|
|
290
|
-
const scope = opts.scope;
|
|
291
|
-
if (paused || !engine || !opts.claim || !scope || scopedSyncGroups.length === 0)
|
|
292
|
-
return;
|
|
293
|
-
if (syncStatus.name !== 'connected')
|
|
294
|
-
return;
|
|
295
|
-
const ws = engine._ws;
|
|
296
|
-
const store = engine._store;
|
|
297
|
-
let cancelled = false;
|
|
298
|
-
const claimId = createParticipantClaimId();
|
|
299
|
-
ws.sendClaim(claimId, scopedSyncGroups, {
|
|
300
|
-
// Reading the retired spelling IS the compatibility path; it goes at 0.37.0.
|
|
301
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
302
|
-
ttlSeconds: parseParticipantTtlSeconds(opts.ttl ?? opts.ttlSeconds),
|
|
303
|
-
})
|
|
304
|
-
.then(() => {
|
|
305
|
-
if (!cancelled)
|
|
306
|
-
setClaimConnected(true);
|
|
307
|
-
})
|
|
308
|
-
.catch((err) => {
|
|
309
|
-
if (!cancelled) {
|
|
310
|
-
setClaimError(err instanceof Error ? err : new Error(String(err)));
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
// Prominence: hold the scope subscribed for as long as the claim lives.
|
|
314
|
-
void store.pinScope?.(scope);
|
|
315
|
-
return () => {
|
|
316
|
-
cancelled = true;
|
|
317
|
-
ws.sendRelease(claimId);
|
|
318
|
-
void store.unpinScope?.(scope);
|
|
319
|
-
};
|
|
320
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated -- same compatibility read as above.
|
|
321
|
-
}, [engine, paused, scopeKey, syncStatus.name, opts.ttl, opts.ttlSeconds, opts.claim]);
|
|
322
|
-
// Bridge the engine's presence + claims streams into React state.
|
|
323
|
-
// Plain useState + useEffect is sufficient — mid-frame tearing on a
|
|
324
|
-
// peer list is harmless (users won't notice one frame of stale
|
|
325
|
-
// presence). Queries and sync status use useSyncExternalStore
|
|
326
|
-
// because transactions CAN tear visibly; presence can't.
|
|
327
|
-
const [peers, setPeers] = useState(EMPTY_PRESENCE);
|
|
328
|
-
const [claims, setClaims] = useState(EMPTY_INTENTS);
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
if (!participant || paused) {
|
|
331
|
-
setPeers(EMPTY_PRESENCE);
|
|
332
|
-
setClaims(EMPTY_INTENTS);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
setPeers(participant.presence.others);
|
|
336
|
-
setClaims(participant.claims.others);
|
|
337
|
-
const unsubPresence = participant.presence.onChange(() => {
|
|
338
|
-
setPeers(participant.presence.others);
|
|
339
|
-
});
|
|
340
|
-
const unsubClaims = participant.claims.onChange(() => {
|
|
341
|
-
setClaims(participant.claims.others);
|
|
342
|
-
});
|
|
343
|
-
return () => {
|
|
344
|
-
unsubPresence();
|
|
345
|
-
unsubClaims();
|
|
346
|
-
};
|
|
347
|
-
}, [participant, paused]);
|
|
348
|
-
return { participant, peers, claims, status, error };
|
|
349
|
-
}
|
|
350
203
|
/**
|
|
351
204
|
* Read-only presence: the OTHER participants currently visible to this
|
|
352
|
-
* connection, bridged to React.
|
|
353
|
-
*
|
|
354
|
-
* it is a pure reader of the engine's already-flowing presence stream.
|
|
205
|
+
* connection, bridged to React. This is a pure reader of the engine's
|
|
206
|
+
* already-flowing presence stream; it does not mutate connection groups.
|
|
355
207
|
*
|
|
356
208
|
* Pass `scope` to narrow to the peers on that scope's sync group(s); omit
|
|
357
209
|
* it to get everyone on the engine's groups. Membership is driven entirely
|
|
@@ -359,11 +211,8 @@ export function useJoin(opts) {
|
|
|
359
211
|
* cursor/collaboration traffic), so reading it never affects what the
|
|
360
212
|
* connection is subscribed to and can't deadlock against a gated channel.
|
|
361
213
|
*
|
|
362
|
-
* Use this to answer "is anyone else here?"
|
|
363
|
-
* broadcasts while alone
|
|
364
|
-
* read interest (scope `leave` is not reference-counted, so a second
|
|
365
|
-
* `useJoin` on the same scope would warm-drop the owner's
|
|
366
|
-
* subscription on unmount).
|
|
214
|
+
* Use this to answer "is anyone else here?", for example to suppress
|
|
215
|
+
* live-cursor broadcasts while alone.
|
|
367
216
|
*
|
|
368
217
|
* ```ts
|
|
369
218
|
* const peers = usePeers({ reports: reportId });
|
|
@@ -373,9 +222,9 @@ export function useJoin(opts) {
|
|
|
373
222
|
export function usePeers(scope) {
|
|
374
223
|
const ctx = useContext(AbloInternalContext);
|
|
375
224
|
const engine = ctx?.engine ?? null;
|
|
376
|
-
// Resolve scope → groups through the schema
|
|
225
|
+
// Resolve scope → groups through the schema.
|
|
377
226
|
// The stringified, sorted key is the stable effect dependency.
|
|
378
|
-
const scopeKey = JSON.stringify(
|
|
227
|
+
const scopeKey = JSON.stringify(resolveScopeGroups(scope, engine?.schema).sort());
|
|
379
228
|
const groups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
|
|
380
229
|
const [peers, setPeers] = useState(EMPTY_PRESENCE);
|
|
381
230
|
useEffect(() => {
|
|
@@ -387,13 +236,12 @@ export function usePeers(scope) {
|
|
|
387
236
|
const compute = () => groups.length === 0
|
|
388
237
|
? presence.others
|
|
389
238
|
: presence.others.filter((p) => p.syncGroups.some((g) => groups.includes(g)));
|
|
390
|
-
// Plain useState + onChange — presence changes on
|
|
239
|
+
// Plain useState + onChange — presence changes on connect/disconnect/activity
|
|
391
240
|
// only (never on cursor traffic, a separate channel), so this fires
|
|
392
|
-
// rarely; a frame of stale presence is harmless
|
|
393
|
-
// useJoin's peers bridge).
|
|
241
|
+
// rarely; a frame of stale presence is harmless.
|
|
394
242
|
setPeers(compute());
|
|
395
243
|
return presence.onChange(() => { setPeers(compute()); });
|
|
396
|
-
}, [engine, scopeKey]);
|
|
244
|
+
}, [engine, groups, scopeKey]);
|
|
397
245
|
return peers;
|
|
398
246
|
}
|
|
399
247
|
// ── Escape-hatches: raw engine/store access ──────────────────────────
|
|
@@ -33,4 +33,4 @@ export interface ClientSideSuspenseProps {
|
|
|
33
33
|
/** What to render once the subtree is cleared to render. */
|
|
34
34
|
children: ReactNode;
|
|
35
35
|
}
|
|
36
|
-
export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react
|
|
36
|
+
export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react").JSX.Element;
|
|
@@ -21,4 +21,4 @@
|
|
|
21
21
|
* pass `fallback={null}`. Consumers who want to skip the gate entirely
|
|
22
22
|
* pass `fallback="passthrough"`.
|
|
23
23
|
*/
|
|
24
|
-
export declare function DefaultFallback(): import("react
|
|
24
|
+
export declare function DefaultFallback(): import("react").JSX.Element;
|
|
@@ -38,7 +38,7 @@ export function createAbloReact(schema) {
|
|
|
38
38
|
// implementation's internal-context fallback.
|
|
39
39
|
const BoundClientContext = createContext(null);
|
|
40
40
|
function BoundAbloProvider(props) {
|
|
41
|
-
return createElement(BoundClientContext.Provider, { value: props.client }, createElement(AbloProvider, props));
|
|
41
|
+
return createElement(BoundClientContext.Provider, { value: props.client }, createElement((AbloProvider), props));
|
|
42
42
|
}
|
|
43
43
|
function useBoundAblo(modelOrSelect, id, options) {
|
|
44
44
|
const bound = useContext(BoundClientContext);
|
|
@@ -10,5 +10,5 @@ export function useErrorListener(listener) {
|
|
|
10
10
|
}
|
|
11
11
|
const listenerRef = useRef(listener);
|
|
12
12
|
listenerRef.current = listener;
|
|
13
|
-
useEffect(() => context.subscribeError((error) => listenerRef.current(error)), [context]);
|
|
13
|
+
useEffect(() => context.subscribeError((error) => { listenerRef.current(error); }), [context]);
|
|
14
14
|
}
|
|
@@ -14,6 +14,6 @@ export function useMutationFailureListener(listener) {
|
|
|
14
14
|
const engine = context.engine;
|
|
15
15
|
if (!engine)
|
|
16
16
|
return;
|
|
17
|
-
return engine.onMutationFailure((payload) => listenerRef.current(payload));
|
|
17
|
+
return engine.onMutationFailure((payload) => { listenerRef.current(payload); });
|
|
18
18
|
}, [context, context.engine]);
|
|
19
19
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { useCurrentUserId } from './react/useCurrentUserId.js';
|
|
|
4
4
|
export { useErrorListener } from './react/useErrorListener.js';
|
|
5
5
|
export { useSyncStatus, type SyncStatusSnapshot } from './react/useSyncStatus.js';
|
|
6
6
|
export { useMutationFailureListener, type MutationFailurePayload, } from './react/useMutationFailureListener.js';
|
|
7
|
-
export { AbloProvider,
|
|
7
|
+
export { AbloProvider, usePeers, useSync, useSyncStore, type AbloProviderProps, type GroupScope, } from './react/AbloProvider.js';
|
|
8
8
|
export { ClientSideSuspense, type ClientSideSuspenseProps, } from './react/ClientSideSuspense.js';
|
|
9
9
|
export { DefaultFallback } from './react/DefaultFallback.js';
|
|
10
10
|
export { createAbloReact, type AbloReactBinding, } from './react/createAbloReact.js';
|
package/dist/react.js
CHANGED
|
@@ -4,7 +4,7 @@ export { useCurrentUserId } from './react/useCurrentUserId.js';
|
|
|
4
4
|
export { useErrorListener } from './react/useErrorListener.js';
|
|
5
5
|
export { useSyncStatus } from './react/useSyncStatus.js';
|
|
6
6
|
export { useMutationFailureListener, } from './react/useMutationFailureListener.js';
|
|
7
|
-
export { AbloProvider,
|
|
7
|
+
export { AbloProvider, usePeers, useSync, useSyncStore, } from './react/AbloProvider.js';
|
|
8
8
|
export { ClientSideSuspense, } from './react/ClientSideSuspense.js';
|
|
9
9
|
export { DefaultFallback } from './react/DefaultFallback.js';
|
|
10
10
|
export { createAbloReact, } from './react/createAbloReact.js';
|
package/dist/surface.d.ts
CHANGED
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
* tuple, so it is the one list of model-verb names a generated summary can
|
|
20
20
|
* describe.
|
|
21
21
|
*/
|
|
22
|
-
export declare const PUBLIC_MODEL_VERBS: readonly [
|
|
22
|
+
export declare const PUBLIC_MODEL_VERBS: readonly ["get", "read", "list", "listAll", "local", "create", "update", "delete", "claim", "onChange"];
|
|
23
23
|
/**
|
|
24
24
|
* The option keys accepted by `local.list` and `onChange`, matching the
|
|
25
25
|
* keys of {@link LocalReadOptions}. Note that the lifecycle filter is named
|
|
26
26
|
* `state`, not `scope`.
|
|
27
27
|
*/
|
|
28
|
-
export declare const PUBLIC_LIST_OPTION_KEYS: readonly [
|
|
28
|
+
export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
|
|
29
29
|
/**
|
|
30
30
|
* The keys of the client constructor options, {@link AbloOptions}. Only
|
|
31
31
|
* `schema` is required; every other key is optional.
|
|
32
32
|
*/
|
|
33
|
-
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly [
|
|
33
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "session", "projectId", "branchId", "persistence", "durableWrites", "commitOutbox", "commitOutboxScope", "debug", "logLevel", "logger", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser", "collaborationEvents", "plugins"];
|
|
34
34
|
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
35
35
|
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
36
36
|
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|
package/dist/surface.js
CHANGED
|
@@ -30,7 +30,6 @@ export const PUBLIC_MODEL_VERBS = [
|
|
|
30
30
|
'update',
|
|
31
31
|
'delete',
|
|
32
32
|
'claim',
|
|
33
|
-
'join',
|
|
34
33
|
'onChange',
|
|
35
34
|
];
|
|
36
35
|
// ── the read/list query option surface ─────────────────────────────────────
|
|
@@ -55,11 +54,9 @@ export const PUBLIC_LIST_OPTION_KEYS = [
|
|
|
55
54
|
export const PUBLIC_ABLO_OPTION_KEYS = [
|
|
56
55
|
'schema',
|
|
57
56
|
'apiKey',
|
|
57
|
+
'session',
|
|
58
58
|
'projectId',
|
|
59
59
|
'branchId',
|
|
60
|
-
'authEndpoint',
|
|
61
|
-
'authTimeoutMs',
|
|
62
|
-
'allowCrossOriginAuthEndpoint',
|
|
63
60
|
'persistence',
|
|
64
61
|
'durableWrites',
|
|
65
62
|
'commitOutbox',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"test:property": "jest --testPathPatterns=__tests__/property",
|
|
69
69
|
"test:migrated-integration": "jest --testPathPatterns=__tests__/integration",
|
|
70
70
|
"test:e2e": "E2E_TEST=true jest --config jest.e2e.config.ts",
|
|
71
|
-
"lint:eslint": "eslint . --cache",
|
|
71
|
+
"lint:eslint": "eslint . --cache --suppressions-location eslint-suppressions.json",
|
|
72
72
|
"check:boundary": "node scripts/check-boundary.mjs",
|
|
73
73
|
"test:integration": "npm run build && node scripts/check-package-integration.mjs",
|
|
74
74
|
"lint:pkg": "publint"
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"directory": "packages/humans"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.
|
|
87
|
+
"@abloatai/transaction": "^0.61.0",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
package/src/Ablo.ts
CHANGED
|
@@ -46,9 +46,9 @@ import { resolveClientPrelude } from './local/client/clientPrelude.js';
|
|
|
46
46
|
// The option types, the client's public shape, the pass over the options bag,
|
|
47
47
|
// the resource-type surface, and the default WebSocket mutation executor each
|
|
48
48
|
// live in their own module. The type-only ones (`options`, `abloClient`,
|
|
49
|
-
// `resourceTypes`) carry no runtime imports, which lets the HTTP client
|
|
50
|
-
//
|
|
51
|
-
//
|
|
49
|
+
// `resourceTypes`) carry no runtime imports, which lets the HTTP client
|
|
50
|
+
// reference the client types without importing this factory back and creating
|
|
51
|
+
// an import cycle.
|
|
52
52
|
import type { AbloOptions } from './local/client/options.js';
|
|
53
53
|
// `AbloReads` is named by `Ablo.Reads` in the namespace below as well as
|
|
54
54
|
// re-exported, so it needs the import too — same reason as `AbloOptions`.
|
|
@@ -92,11 +92,11 @@ export type Ablo<S extends SchemaRecord> = AbloClient<S>;
|
|
|
92
92
|
* ```
|
|
93
93
|
*
|
|
94
94
|
* In the browser (or any client that shouldn't hold a secret key), point
|
|
95
|
-
* `
|
|
95
|
+
* `session.endpoint` at your session-mint route instead — the SDK fetches it, keeps the
|
|
96
96
|
* short-lived token fresh, and re-mints on expiry:
|
|
97
97
|
*
|
|
98
98
|
* ```ts
|
|
99
|
-
* const ablo = Ablo({ schema,
|
|
99
|
+
* const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
|
|
100
100
|
* ```
|
|
101
101
|
*
|
|
102
102
|
* Server-side agents, workers, and services use `@abloatai/transaction`.
|
|
@@ -214,7 +214,6 @@ export function Ablo<const S extends SchemaRecord>(
|
|
|
214
214
|
transport,
|
|
215
215
|
presence: humansSurface.presence,
|
|
216
216
|
cluster,
|
|
217
|
-
createSibling: (siblingOptions) => Ablo(siblingOptions),
|
|
218
217
|
}), installedPlugins);
|
|
219
218
|
}
|
|
220
219
|
|
|
@@ -232,7 +231,6 @@ export function Ablo<const S extends SchemaRecord>(
|
|
|
232
231
|
// continue to work for callers who prefer them.
|
|
233
232
|
|
|
234
233
|
import type * as _Streams from '@abloatai/transaction/types/streams';
|
|
235
|
-
import type * as _Participants from './local/sync/participants.js';
|
|
236
234
|
import type * as _Mutators from './local/mutators/defineMutators.js';
|
|
237
235
|
import type * as _Tx from './local/mutators/Transaction.js';
|
|
238
236
|
import type * as _Undo from './local/mutators/UndoManager.js';
|
|
@@ -297,16 +295,6 @@ export namespace Ablo {
|
|
|
297
295
|
export type Actor = _Streams.ParticipantRef;
|
|
298
296
|
}
|
|
299
297
|
|
|
300
|
-
// ── Participant (sub-namespace — 5 names, shared concept) ─────────
|
|
301
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
302
|
-
export namespace Participant {
|
|
303
|
-
export type Manager = _Participants.ParticipantManager;
|
|
304
|
-
export type Joined = _Participants.JoinedParticipant;
|
|
305
|
-
export type Scope = _Participants.ParticipantScope;
|
|
306
|
-
export type Status = _Participants.ParticipantStatus;
|
|
307
|
-
export type JoinOptions = _Participants.ParticipantJoinOptions;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
298
|
// ── Schema (type + sub-namespace via declaration merge) ───────────
|
|
311
299
|
export type Schema<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord> = _SchemaTypes.Schema<S>;
|
|
312
300
|
/**
|
package/src/client.ts
CHANGED
|
@@ -27,8 +27,6 @@ import type { ModelOperations } from './local/client/createModelOperations.js';
|
|
|
27
27
|
import type {
|
|
28
28
|
ClaimResource,
|
|
29
29
|
CommitResource,
|
|
30
|
-
CreateAgentClientParams,
|
|
31
|
-
SessionResource,
|
|
32
30
|
} from '@abloatai/transaction/client/resources/httpResources';
|
|
33
31
|
import type { EffectiveAuthority } from '@abloatai/transaction/auth';
|
|
34
32
|
import type { ReadDependency } from '@abloatai/transaction/coordination';
|
|
@@ -125,55 +123,6 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
125
123
|
*/
|
|
126
124
|
nudgeReconnect(): void;
|
|
127
125
|
|
|
128
|
-
/**
|
|
129
|
-
* Mint a short-lived, scoped session token for one end user. Call this on your
|
|
130
|
-
* backend, where the `sk_` secret key lives, then hand the returned `token` to
|
|
131
|
-
* that user's browser — typically through a token route the browser's `apiKey`
|
|
132
|
-
* resolver fetches. The browser presents the token as its bearer, and the
|
|
133
|
-
* server verifies it. The browser must never see the `sk_` key, only the
|
|
134
|
-
* per-user session token.
|
|
135
|
-
*
|
|
136
|
-
* Pass `{ user: { id }, can: { items: ['read', 'update'] } }` for an end-user
|
|
137
|
-
* session. It mints an `ek_` and attributes writes to a user (recorded as
|
|
138
|
-
* `actor_kind` on the delta row). Pass `{ agent: { id }, can: {
|
|
139
|
-
* items: ['update'] } }` for a scoped agent session, which mints an `rk_`.
|
|
140
|
-
* Both kinds require `can`, typed against your schema's model names. This
|
|
141
|
-
* always authenticates with the original `sk_`, never the client's exchanged
|
|
142
|
-
* sync credential.
|
|
143
|
-
*/
|
|
144
|
-
sessions: SessionResource<S>;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Mint a scoped **agent identity** and return a ready-to-use client bound to
|
|
148
|
-
* it — the `ablo.<resource>.<verb>` shape for the agent use case. One call
|
|
149
|
-
* replaces `sessions.create({ agent, can })` + constructing a second
|
|
150
|
-
* `Ablo({ apiKey: token })`:
|
|
151
|
-
*
|
|
152
|
-
* ```ts
|
|
153
|
-
* const agent = await ablo.agents.create({
|
|
154
|
-
* name: 'researcher', // readable label (optional)
|
|
155
|
-
* can: { records: ['read', 'update'] },
|
|
156
|
-
* // id omitted → a fresh uuid: a distinct, independent participant
|
|
157
|
-
* });
|
|
158
|
-
* await agent.records.update({ id, data, claim });
|
|
159
|
-
* await agent.dispose(); // when the agent is done
|
|
160
|
-
* ```
|
|
161
|
-
*
|
|
162
|
-
* Server-side only: it requires the `sk_` secret key (like `sessions.create`)
|
|
163
|
-
* and throws `AbloAuthenticationError` in the browser. The returned client
|
|
164
|
-
* holds its own auto-refreshing `rk_`, so a long run never hits token expiry,
|
|
165
|
-
* and the `sk_` never leaves this process. Each call is a distinct participant
|
|
166
|
-
* by default (omit `id` for a fresh uuid), so even two agents sharing a `name`
|
|
167
|
-
* queue behind one another on a contended row — `name` is display only and
|
|
168
|
-
* never collapses identity. Humans don't get a server-built client; ship them a
|
|
169
|
-
* token via `sessions.create({ user, can })`. If you need the raw token for
|
|
170
|
-
* revocation, or a stable re-attachable id, use `sessions.create({ agent, can })`
|
|
171
|
-
* or pass `id`.
|
|
172
|
-
*/
|
|
173
|
-
agents: {
|
|
174
|
-
create(params: CreateAgentClientParams<S>): Promise<AbloClient<S>>;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
126
|
/**
|
|
178
127
|
* The organization this client resolved to — `null` until `ready()`
|
|
179
128
|
* completes. Use it instead of scraping CLI output or hardcoding env vars:
|
|
@@ -18,9 +18,9 @@ import { ConnectionManager } from './sync/ConnectionManager.js';
|
|
|
18
18
|
import { contextLogger, contextSocketObservability } from './sync/contextPorts.js';
|
|
19
19
|
import { SubscriptionManager } from './sync/SubscriptionManager.js';
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
type
|
|
23
|
-
} from './sync/
|
|
21
|
+
resolveScopeGroups,
|
|
22
|
+
type GroupScope,
|
|
23
|
+
} from './sync/scopeGroups.js';
|
|
24
24
|
import type { SyncClient } from './SyncClient.js';
|
|
25
25
|
import type { Database, BootstrapResult, BootstrapRequirements } from './Database.js';
|
|
26
26
|
import type { BootstrapData } from './sync/BootstrapFetcher.js';
|
|
@@ -394,8 +394,8 @@ export class BaseSyncedStore<
|
|
|
394
394
|
// {@link SubscriptionManager.reconcile}); the on-connect `resync` pushes
|
|
395
395
|
// whatever interest accumulated.
|
|
396
396
|
|
|
397
|
-
private scopeToGroups(scope:
|
|
398
|
-
return
|
|
397
|
+
private scopeToGroups(scope: GroupScope): string[] {
|
|
398
|
+
return resolveScopeGroups(scope, this.schema);
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
/**
|
|
@@ -406,7 +406,7 @@ export class BaseSyncedStore<
|
|
|
406
406
|
* Hydration is best-effort — a failed backfill never rejects `enterScope`,
|
|
407
407
|
* and the live delta stream keeps flowing regardless.
|
|
408
408
|
*/
|
|
409
|
-
enterScope(scope:
|
|
409
|
+
enterScope(scope: GroupScope, opts?: { hydrate?: boolean }): Promise<void> {
|
|
410
410
|
const groups = this.scopeToGroups(scope);
|
|
411
411
|
const subscribed = Promise.all(groups.map((g) => this.areaOfInterest.enter(g))).then(
|
|
412
412
|
() => undefined,
|
|
@@ -456,21 +456,21 @@ export class BaseSyncedStore<
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
/** Leave a scope → its groups go warm (hysteresis), then drop on sweep. */
|
|
459
|
-
leaveScope(scope:
|
|
459
|
+
leaveScope(scope: GroupScope): Promise<void> {
|
|
460
460
|
return Promise.all(
|
|
461
461
|
this.scopeToGroups(scope).map((g) => this.areaOfInterest.leave(g)),
|
|
462
462
|
).then(() => undefined);
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
/** Pin a scope (active claim / prominence) → never warms while pinned. */
|
|
466
|
-
pinScope(scope:
|
|
466
|
+
pinScope(scope: GroupScope): Promise<void> {
|
|
467
467
|
return Promise.all(
|
|
468
468
|
this.scopeToGroups(scope).map((g) => this.areaOfInterest.pin(g)),
|
|
469
469
|
).then(() => undefined);
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
/** Release a pin → the group transitions to warm rather than dropping. */
|
|
473
|
-
unpinScope(scope:
|
|
473
|
+
unpinScope(scope: GroupScope): Promise<void> {
|
|
474
474
|
return Promise.all(
|
|
475
475
|
this.scopeToGroups(scope).map((g) => this.areaOfInterest.unpin(g)),
|
|
476
476
|
).then(() => undefined);
|
|
@@ -1759,9 +1759,9 @@ export class BaseSyncedStore<
|
|
|
1759
1759
|
const isCreate = !this.objectPool.get(model.id);
|
|
1760
1760
|
if (isCreate) {
|
|
1761
1761
|
model.updatedAt = new Date();
|
|
1762
|
-
this.syncClient.add(model);
|
|
1762
|
+
await this.syncClient.add(model);
|
|
1763
1763
|
} else {
|
|
1764
|
-
this.syncClient.update(model);
|
|
1764
|
+
await this.syncClient.update(model);
|
|
1765
1765
|
}
|
|
1766
1766
|
}
|
|
1767
1767
|
|
|
@@ -1779,21 +1779,21 @@ export class BaseSyncedStore<
|
|
|
1779
1779
|
const model = rowAsModel(entity);
|
|
1780
1780
|
this.pendingDeletes.add(model.id);
|
|
1781
1781
|
// SyncClient.delete handles: pool remove, transaction queue
|
|
1782
|
-
this.syncClient.delete(model);
|
|
1782
|
+
await this.syncClient.delete(model);
|
|
1783
1783
|
}
|
|
1784
1784
|
|
|
1785
1785
|
/** Archive a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
1786
1786
|
async archive<T extends { id: string; archivedAt?: Date | null }>(entity: T): Promise<void> {
|
|
1787
1787
|
const model = rowAsModel(entity);
|
|
1788
1788
|
model.archivedAt = new Date();
|
|
1789
|
-
this.syncClient.archive(model);
|
|
1789
|
+
await this.syncClient.archive(model);
|
|
1790
1790
|
}
|
|
1791
1791
|
|
|
1792
1792
|
/** Unarchive a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
1793
1793
|
async unarchive<T extends { id: string; archivedAt?: Date | null }>(entity: T): Promise<void> {
|
|
1794
1794
|
const model = rowAsModel(entity);
|
|
1795
1795
|
model.archivedAt = null;
|
|
1796
|
-
this.syncClient.update(model);
|
|
1796
|
+
await this.syncClient.update(model);
|
|
1797
1797
|
}
|
|
1798
1798
|
|
|
1799
1799
|
|