@abloatai/humans 0.63.0 → 0.64.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/dist/Ablo.d.ts +4 -0
- package/dist/client.d.ts +11 -31
- package/dist/local/client/reactiveEngine.js +4 -5
- package/dist/{react/useSyncStatus.d.ts → local/client/status.d.ts} +4 -3
- package/dist/local/client/status.js +14 -0
- package/dist/local/client/storeLifecycle.js +2 -2
- package/dist/local/storeContract.d.ts +3 -4
- package/dist/presence/index.js +6 -2
- package/dist/react/AbloProvider.d.ts +59 -139
- package/dist/react/AbloProvider.js +60 -181
- package/dist/react/createAbloReact.d.ts +10 -32
- package/dist/react/createAbloReact.js +10 -54
- package/dist/react/internalContext.d.ts +3 -20
- package/dist/react/snapshot.d.ts +4 -0
- package/dist/react/snapshot.js +75 -0
- package/dist/react/useAblo.d.ts +28 -45
- package/dist/react/useAblo.js +39 -77
- package/dist/react/useMutators.d.ts +20 -17
- package/dist/react/usePresence.js +7 -6
- package/dist/react/useReactive.d.ts +12 -0
- package/dist/react/useReactive.js +57 -0
- package/dist/react/useUndoScope.d.ts +15 -29
- package/dist/react/useUndoScope.js +17 -0
- package/dist/react.d.ts +7 -19
- package/dist/react.js +7 -15
- package/package.json +4 -3
- package/src/Ablo.ts +5 -0
- package/src/client.ts +12 -31
- package/src/local/client/reactiveEngine.ts +4 -5
- package/src/local/client/status.ts +20 -0
- package/src/local/client/storeLifecycle.ts +2 -2
- package/src/local/storeContract.ts +3 -4
- package/src/presence/index.ts +6 -2
- package/src/react/AbloProvider.tsx +94 -309
- package/src/react/createAbloReact.ts +18 -99
- package/src/react/internalContext.ts +3 -20
- package/src/react/snapshot.ts +67 -0
- package/src/react/useAblo.ts +71 -140
- package/src/react/useMutators.ts +30 -26
- package/src/react/usePresence.ts +7 -12
- package/src/react/useReactive.ts +56 -0
- package/src/react/useUndoScope.ts +18 -14
- package/src/react.ts +7 -69
- package/dist/react/ClientSideSuspense.d.ts +0 -36
- package/dist/react/ClientSideSuspense.js +0 -17
- package/dist/react/useCurrentUserId.d.ts +0 -2
- package/dist/react/useCurrentUserId.js +0 -12
- package/dist/react/useErrorListener.d.ts +0 -2
- package/dist/react/useErrorListener.js +0 -14
- package/dist/react/useMutationFailureListener.d.ts +0 -8
- package/dist/react/useMutationFailureListener.js +0 -19
- package/dist/react/useSyncStatus.js +0 -37
- package/dist/useReactive.d.ts +0 -6
- package/dist/useReactive.js +0 -43
- package/src/react/ClientSideSuspense.tsx +0 -57
- package/src/react/useCurrentUserId.ts +0 -17
- package/src/react/useErrorListener.ts +0 -22
- package/src/react/useMutationFailureListener.ts +0 -34
- package/src/react/useSyncStatus.ts +0 -42
- package/src/useReactive.ts +0 -51
package/src/client.ts
CHANGED
|
@@ -19,9 +19,9 @@ import type {
|
|
|
19
19
|
InferRow,
|
|
20
20
|
} from '@abloatai/transaction/schema/schema';
|
|
21
21
|
import type { InstanceCache } from './local/InstanceCache.js';
|
|
22
|
-
import type { SyncStoreContract } from './
|
|
22
|
+
import type { SyncStoreContract } from './local/storeContract.js';
|
|
23
23
|
import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket.js';
|
|
24
|
-
import type {
|
|
24
|
+
import type { ClientStatus } from './local/client/status.js';
|
|
25
25
|
import type { ModelOperations } from './local/client/createModelOperations.js';
|
|
26
26
|
import type {
|
|
27
27
|
ClaimResource,
|
|
@@ -39,7 +39,10 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
39
39
|
Model<Schema<S>, K>,
|
|
40
40
|
InferCreate<Schema<S>, K>
|
|
41
41
|
>;
|
|
42
|
-
} &
|
|
42
|
+
} & AbloCore<S>;
|
|
43
|
+
|
|
44
|
+
/** Core members stay intact even when the model schema is not registered. */
|
|
45
|
+
interface AbloCore<S extends SchemaRecord> {
|
|
43
46
|
/**
|
|
44
47
|
* Wait for the sync engine to finish its initial bootstrap.
|
|
45
48
|
* Resolves once entity data is loaded and the WebSocket is connected.
|
|
@@ -65,7 +68,7 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
65
68
|
* acknowledged everything before continuing — for example, before
|
|
66
69
|
* navigating away, before triggering a server-side workflow, or in tests.
|
|
67
70
|
*
|
|
68
|
-
* Resolves when
|
|
71
|
+
* Resolves when all pending local changes are confirmed. If the engine is
|
|
69
72
|
* offline, this waits until reconnect + flush completes.
|
|
70
73
|
*
|
|
71
74
|
* ```ts
|
|
@@ -205,32 +208,10 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
205
208
|
waitForConfirmation(modelName: string, modelId: string): Promise<void>;
|
|
206
209
|
|
|
207
210
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* Single source of truth for "what's the sync engine doing?" Contains:
|
|
211
|
-
* - `state`: `'idle' | 'syncing' | 'error' | 'offline' | 'reconnecting'`
|
|
212
|
-
* - `progress`: 0-100 for bootstrap progress
|
|
213
|
-
* - `error?`: Error object when `state === 'error'`
|
|
214
|
-
* - `pendingChanges`: Number of unconfirmed mutations in the queue
|
|
215
|
-
* - `lastSyncAt?`: Timestamp of the last successful delta processing
|
|
216
|
-
* - `offlineSince?`: When the connection dropped
|
|
217
|
-
* - `isSessionError`: True when the error requires re-authentication
|
|
218
|
-
*
|
|
219
|
-
* React components using `observer()` re-render automatically when
|
|
220
|
-
* any field changes — no manual subscription or polling needed.
|
|
221
|
-
*
|
|
222
|
-
* ```tsx
|
|
223
|
-
* import { observer } from 'mobx-react-lite';
|
|
224
|
-
*
|
|
225
|
-
* const SyncIndicator = observer(() => {
|
|
226
|
-
* if (sync.syncStatus.state === 'syncing') return <Spinner />;
|
|
227
|
-
* if (sync.syncStatus.state === 'error') return <Error msg={sync.syncStatus.error} />;
|
|
228
|
-
* if (sync.syncStatus.state === 'offline') return <OfflineBadge />;
|
|
229
|
-
* return null;
|
|
230
|
-
* });
|
|
231
|
-
* ```
|
|
211
|
+
* Current connection and confirmation state. Available before ready().
|
|
212
|
+
* React reads the same value with `useAblo(ablo => ablo.status)`.
|
|
232
213
|
*/
|
|
233
|
-
readonly
|
|
214
|
+
readonly status: ClientStatus;
|
|
234
215
|
|
|
235
216
|
/**
|
|
236
217
|
* Session-owned live activity projected from this client's existing
|
|
@@ -290,7 +271,7 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
290
271
|
* no window in which this is absent and nothing needs to guard for one.
|
|
291
272
|
*/
|
|
292
273
|
readonly _ws: SyncWebSocket;
|
|
293
|
-
}
|
|
274
|
+
}
|
|
294
275
|
|
|
295
276
|
/**
|
|
296
277
|
* The reactive-read client a `useAblo` selector receives. The same surface as
|
|
@@ -301,7 +282,7 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
301
282
|
* compile error here instead of a silent runtime `undefined`; compose
|
|
302
283
|
* relations through selectors or hooks that resolve the pool's instance.
|
|
303
284
|
*/
|
|
304
|
-
export type AbloReads<S extends SchemaRecord> =
|
|
285
|
+
export type AbloReads<S extends SchemaRecord> = AbloCore<S> & {
|
|
305
286
|
readonly [K in keyof S & string]: ModelOperations<
|
|
306
287
|
InferRow<Schema<S>, K>,
|
|
307
288
|
InferCreate<Schema<S>, K>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
streamTarget,
|
|
30
30
|
subTarget,
|
|
31
31
|
} from '@abloatai/transaction/coordination';
|
|
32
|
+
import { readStatus } from './status.js';
|
|
32
33
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
33
34
|
import type { StoreCluster } from './storeCluster.js';
|
|
34
35
|
import { startStoreLifecycle } from './storeLifecycle.js';
|
|
@@ -842,11 +843,9 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
842
843
|
return store.waitForConfirmation(modelName, modelId);
|
|
843
844
|
},
|
|
844
845
|
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
get syncStatus() {
|
|
849
|
-
return store.syncStatus;
|
|
846
|
+
// One core lifecycle projection; React selects the same observable reads.
|
|
847
|
+
get status() {
|
|
848
|
+
return readStatus(store);
|
|
850
849
|
},
|
|
851
850
|
|
|
852
851
|
// The humans capability owns the connection-backed presence projection.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SyncStoreContract } from '../storeContract.js';
|
|
2
|
+
|
|
3
|
+
/** The client lifecycle, shared by every framework binding. */
|
|
4
|
+
export type ClientStatus =
|
|
5
|
+
| { readonly name: 'initial' }
|
|
6
|
+
| { readonly name: 'connecting'; readonly progress: number }
|
|
7
|
+
| { readonly name: 'connected'; readonly hasUnsyncedChanges: boolean }
|
|
8
|
+
| { readonly name: 'reconnecting'; readonly reason?: string }
|
|
9
|
+
| { readonly name: 'disconnected'; readonly reason?: string }
|
|
10
|
+
| { readonly name: 'needs-auth' };
|
|
11
|
+
|
|
12
|
+
export function readStatus(store: SyncStoreContract): ClientStatus {
|
|
13
|
+
const { state, progress, pendingChanges, isSessionError, error } = store.syncStatus;
|
|
14
|
+
if (isSessionError) return { name: 'needs-auth' };
|
|
15
|
+
if (state === 'reconnecting') return { name: 'reconnecting', reason: error?.message };
|
|
16
|
+
if (state === 'offline') return { name: 'disconnected', reason: 'offline' };
|
|
17
|
+
if (state === 'error') return { name: 'disconnected', reason: error?.message };
|
|
18
|
+
if (store.isReady) return { name: 'connected', hasUnsyncedChanges: pendingChanges > 0 };
|
|
19
|
+
return { name: 'connecting', progress };
|
|
20
|
+
}
|
|
@@ -301,7 +301,7 @@ export function startStoreLifecycle<S extends SchemaRecord>(
|
|
|
301
301
|
//
|
|
302
302
|
// The store.initialize() generator updates store.syncStatus as it
|
|
303
303
|
// progresses (syncing → idle on success, error on failure), so the
|
|
304
|
-
// consumer's `
|
|
304
|
+
// consumer's `ablo.status` projection reflects real-time state.
|
|
305
305
|
// Resolve bootstrap mode: explicit option wins; otherwise
|
|
306
306
|
// agents default to 'none' (transactional participant — see
|
|
307
307
|
// option doc) and everyone else defaults to 'full'.
|
|
@@ -386,7 +386,7 @@ export function startStoreLifecycle<S extends SchemaRecord>(
|
|
|
386
386
|
if (!validationError && internalOptions.autoStart) {
|
|
387
387
|
void ready().catch(() => {
|
|
388
388
|
// Error is captured in store.syncStatus; consumers should check
|
|
389
|
-
// `
|
|
389
|
+
// `ablo.status.name === 'disconnected'` to detect failures.
|
|
390
390
|
});
|
|
391
391
|
}
|
|
392
392
|
|
|
@@ -19,8 +19,7 @@ import type { GroupScope } from './sync/scopeGroups.js';
|
|
|
19
19
|
/**
|
|
20
20
|
* A snapshot of the client's synchronization state, shaped for binding to UI.
|
|
21
21
|
* {@link SyncStoreContract.syncStatus} exposes a reactive instance of this, and
|
|
22
|
-
* the
|
|
23
|
-
* indicators.
|
|
22
|
+
* the client projects it into `ablo.status` for connection indicators.
|
|
24
23
|
*/
|
|
25
24
|
export interface SyncStatus {
|
|
26
25
|
state: 'idle' | 'syncing' | 'error' | 'offline' | 'reconnecting';
|
|
@@ -115,7 +114,7 @@ export interface SyncStoreContract {
|
|
|
115
114
|
* are backed by observable computeds, so reading them inside a reactive
|
|
116
115
|
* context — an observer component or a reaction — re-runs that context when
|
|
117
116
|
* the state changes. Code that prefers not to work with the reactivity system
|
|
118
|
-
* directly can read the same values through
|
|
117
|
+
* directly can read the same values through `useAblo(ablo => ablo.status)`.
|
|
119
118
|
*/
|
|
120
119
|
readonly isReady: boolean;
|
|
121
120
|
readonly isSyncing: boolean;
|
|
@@ -137,7 +136,7 @@ export interface SyncStoreContract {
|
|
|
137
136
|
pinScope?(scope: GroupScope): Promise<void>;
|
|
138
137
|
unpinScope?(scope: GroupScope): Promise<void>;
|
|
139
138
|
/**
|
|
140
|
-
* The full reactive {@link SyncStatus} record. The
|
|
139
|
+
* The full reactive {@link SyncStatus} record. The client status projection
|
|
141
140
|
* reads its fields — `state`, `progress`, `pendingChanges`, `isSessionError`,
|
|
142
141
|
* and `error` — to present the current sync state. It is part of the contract
|
|
143
142
|
* so hooks and test doubles can read or set it directly.
|
package/src/presence/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observable, runInAction } from 'mobx';
|
|
1
2
|
import {
|
|
2
3
|
createPresenceProjection,
|
|
3
4
|
type PresenceProjection,
|
|
@@ -44,11 +45,13 @@ export function createPresence(
|
|
|
44
45
|
): AttachablePresence {
|
|
45
46
|
let projection: PresenceProjection | null = null;
|
|
46
47
|
let attachedTransport: PresenceTransport | null = null;
|
|
48
|
+
const version = observable.box(0);
|
|
47
49
|
const listeners = new Set<() => void>();
|
|
48
50
|
const reads = new Set<ReadActivityLifetime>();
|
|
49
51
|
let unsubscribe: (() => void) | null = null;
|
|
50
52
|
|
|
51
53
|
const notify = (): void => {
|
|
54
|
+
runInAction(() => { version.set(version.get() + 1); });
|
|
52
55
|
for (const listener of listeners) listener();
|
|
53
56
|
};
|
|
54
57
|
|
|
@@ -63,8 +66,8 @@ export function createPresence(
|
|
|
63
66
|
if (transport !== null) attach(transport);
|
|
64
67
|
|
|
65
68
|
return {
|
|
66
|
-
get active() { return projection?.active ?? []; },
|
|
67
|
-
get others() { return projection?.others ?? []; },
|
|
69
|
+
get active() { version.get(); return projection?.active ?? []; },
|
|
70
|
+
get others() { version.get(); return projection?.others ?? []; },
|
|
68
71
|
onChange(listener) {
|
|
69
72
|
listeners.add(listener);
|
|
70
73
|
return () => { listeners.delete(listener); };
|
|
@@ -85,6 +88,7 @@ export function createPresence(
|
|
|
85
88
|
};
|
|
86
89
|
},
|
|
87
90
|
forModel(model, recordId) {
|
|
91
|
+
version.get();
|
|
88
92
|
return projection?.forModel(model, recordId) ?? [];
|
|
89
93
|
},
|
|
90
94
|
dispose() {
|