@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.
Files changed (60) hide show
  1. package/dist/Ablo.d.ts +4 -0
  2. package/dist/client.d.ts +11 -31
  3. package/dist/local/client/reactiveEngine.js +4 -5
  4. package/dist/{react/useSyncStatus.d.ts → local/client/status.d.ts} +4 -3
  5. package/dist/local/client/status.js +14 -0
  6. package/dist/local/client/storeLifecycle.js +2 -2
  7. package/dist/local/storeContract.d.ts +3 -4
  8. package/dist/presence/index.js +6 -2
  9. package/dist/react/AbloProvider.d.ts +59 -139
  10. package/dist/react/AbloProvider.js +60 -181
  11. package/dist/react/createAbloReact.d.ts +10 -32
  12. package/dist/react/createAbloReact.js +10 -54
  13. package/dist/react/internalContext.d.ts +3 -20
  14. package/dist/react/snapshot.d.ts +4 -0
  15. package/dist/react/snapshot.js +75 -0
  16. package/dist/react/useAblo.d.ts +28 -45
  17. package/dist/react/useAblo.js +39 -77
  18. package/dist/react/useMutators.d.ts +20 -17
  19. package/dist/react/usePresence.js +7 -6
  20. package/dist/react/useReactive.d.ts +12 -0
  21. package/dist/react/useReactive.js +57 -0
  22. package/dist/react/useUndoScope.d.ts +15 -29
  23. package/dist/react/useUndoScope.js +17 -0
  24. package/dist/react.d.ts +7 -19
  25. package/dist/react.js +7 -15
  26. package/package.json +4 -3
  27. package/src/Ablo.ts +5 -0
  28. package/src/client.ts +12 -31
  29. package/src/local/client/reactiveEngine.ts +4 -5
  30. package/src/local/client/status.ts +20 -0
  31. package/src/local/client/storeLifecycle.ts +2 -2
  32. package/src/local/storeContract.ts +3 -4
  33. package/src/presence/index.ts +6 -2
  34. package/src/react/AbloProvider.tsx +94 -309
  35. package/src/react/createAbloReact.ts +18 -99
  36. package/src/react/internalContext.ts +3 -20
  37. package/src/react/snapshot.ts +67 -0
  38. package/src/react/useAblo.ts +71 -140
  39. package/src/react/useMutators.ts +30 -26
  40. package/src/react/usePresence.ts +7 -12
  41. package/src/react/useReactive.ts +56 -0
  42. package/src/react/useUndoScope.ts +18 -14
  43. package/src/react.ts +7 -69
  44. package/dist/react/ClientSideSuspense.d.ts +0 -36
  45. package/dist/react/ClientSideSuspense.js +0 -17
  46. package/dist/react/useCurrentUserId.d.ts +0 -2
  47. package/dist/react/useCurrentUserId.js +0 -12
  48. package/dist/react/useErrorListener.d.ts +0 -2
  49. package/dist/react/useErrorListener.js +0 -14
  50. package/dist/react/useMutationFailureListener.d.ts +0 -8
  51. package/dist/react/useMutationFailureListener.js +0 -19
  52. package/dist/react/useSyncStatus.js +0 -37
  53. package/dist/useReactive.d.ts +0 -6
  54. package/dist/useReactive.js +0 -43
  55. package/src/react/ClientSideSuspense.tsx +0 -57
  56. package/src/react/useCurrentUserId.ts +0 -17
  57. package/src/react/useErrorListener.ts +0 -22
  58. package/src/react/useMutationFailureListener.ts +0 -34
  59. package/src/react/useSyncStatus.ts +0 -42
  60. 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 './react/context.js';
22
+ import type { SyncStoreContract } from './local/storeContract.js';
23
23
  import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket.js';
24
- import type { SyncStatus } from './local/BaseSyncedStore.js';
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 `syncStatus.pendingChanges` reaches 0. If the engine is
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
- * Reactive sync status — a MobX observable.
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 syncStatus: SyncStatus;
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> = Omit<AbloClient<S>, keyof S & string> & {
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
- // Expose the store's MobX observable directly — single source of truth.
846
- // React components using observer() will re-render automatically on
847
- // any state change (syncing, error, offline, pendingChanges, progress).
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 `sync.syncStatus` observable reflects real-time state.
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
- // `sync.syncStatus.state === 'error'` to detect failures.
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 `useSyncStatus()` hook reads its fields to render connection and progress
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 the `useSyncStatus()` hook.
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 `useSyncStatus()` hook
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.
@@ -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() {