@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/dist/react/useAblo.d.ts
CHANGED
|
@@ -14,24 +14,6 @@ type DefaultModels = ResolveSchema extends {
|
|
|
14
14
|
} ? M extends SchemaRecord ? M : SchemaRecord : SchemaRecord;
|
|
15
15
|
export type ModelClientSelector<R extends SchemaRecord, T, C> = (ablo: AbloReads<R>) => ModelOperations<T, C>;
|
|
16
16
|
export type AbloSelector<R extends SchemaRecord, T> = (ablo: AbloReads<R>) => T;
|
|
17
|
-
export interface UseAbloModelOptions<T> {
|
|
18
|
-
/**
|
|
19
|
-
* An initial row, usually from a server component or a route loader. The hook
|
|
20
|
-
* returns it until sync delivers a newer row for the same id.
|
|
21
|
-
*/
|
|
22
|
-
readonly initial?: T;
|
|
23
|
-
}
|
|
24
|
-
export interface UseAbloModelResult<T> {
|
|
25
|
-
/** The current row for the id, or `initial` until the row has synced. */
|
|
26
|
-
readonly data: T | undefined;
|
|
27
|
-
/** The work claims currently held on this row by any participant. */
|
|
28
|
-
readonly claims: readonly ModelClaim[];
|
|
29
|
-
/** True while another participant holds a claim — handy for disabling UI. */
|
|
30
|
-
readonly claimed: boolean;
|
|
31
|
-
}
|
|
32
|
-
export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'> & {
|
|
33
|
-
readonly data: T;
|
|
34
|
-
};
|
|
35
17
|
/**
|
|
36
18
|
* Reads Ablo from inside an `<AbloProvider>` subtree. Called with no arguments
|
|
37
19
|
* it returns the typed client for use in callbacks and effects; called with a
|
|
@@ -60,39 +42,40 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
|
|
|
60
42
|
* // are typed as snapshot rows — data fields + computeds, no relation
|
|
61
43
|
* // accessors — matching what the hook actually returns:
|
|
62
44
|
* const doc = useAblo((ablo) => ablo.records.local.get(id)) ?? serverDoc;
|
|
63
|
-
* const
|
|
45
|
+
* const { claimed } = useAblo((ablo) => ablo.records, id);
|
|
64
46
|
*
|
|
65
47
|
* // Without the augmentation, pass the schema as a type argument:
|
|
66
48
|
* const ablo = useAblo<(typeof schema)['models']>();
|
|
67
49
|
* ```
|
|
68
50
|
*
|
|
69
|
-
* The
|
|
70
|
-
*
|
|
71
|
-
*
|
|
51
|
+
* The client and its status are available during provider startup. Select
|
|
52
|
+
* `ablo.status` to display connection state; await `ablo.ready()` before
|
|
53
|
+
* operations that require an initialized client. Without a provider, the
|
|
54
|
+
* no-argument form returns `null` and selectors return `undefined`.
|
|
72
55
|
*/
|
|
73
56
|
export declare function useAblo<R extends SchemaRecord = DefaultModels>(): Ablo<R> | null;
|
|
74
57
|
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = unknown>(select: AbloSelector<R, T>): T | undefined;
|
|
75
|
-
export declare function useAblo<T, C>(modelClient: ModelOperations<T, C>, id: string, options
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export declare function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
58
|
+
export declare function useAblo<T, C>(modelClient: ModelOperations<T, C>, id: string, options?: useAblo.Options<T>): useAblo.Result<T>;
|
|
59
|
+
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = Record<string, unknown>, C = unknown>(select: ModelClientSelector<R, T, C>, id: string, options?: useAblo.Options<T>): useAblo.Result<T>;
|
|
60
|
+
/** @internal Resolve the nearest provider's client through one schema rebind. */
|
|
61
|
+
export declare function useAbloClient<R extends SchemaRecord>(): Ablo<R> | null;
|
|
62
|
+
/** Type annotations belong to the operation; most callers rely on inference. */
|
|
63
|
+
export declare namespace useAblo {
|
|
64
|
+
interface Options<T> {
|
|
65
|
+
/**
|
|
66
|
+
* An initial row, usually from a server component or a route loader. The hook
|
|
67
|
+
* uses it for hydration and until a local row has been observed. A later
|
|
68
|
+
* local removal returns undefined instead of restoring this seed.
|
|
69
|
+
*/
|
|
70
|
+
readonly initial?: T;
|
|
71
|
+
}
|
|
72
|
+
interface Result<T> {
|
|
73
|
+
/** The local row or its initial seed. Undefined is a local cache miss, not proof of server absence. */
|
|
74
|
+
readonly data: T | undefined;
|
|
75
|
+
/** The work claims currently held on this row by any participant. */
|
|
76
|
+
readonly claims: readonly ModelClaim[];
|
|
77
|
+
/** True while another participant holds a claim — handy for disabling UI. */
|
|
78
|
+
readonly claimed: boolean;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
98
81
|
export {};
|
package/dist/react/useAblo.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useContext, useEffect,
|
|
2
|
+
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
|
3
3
|
import { AbloInternalContext } from './internalContext.js';
|
|
4
4
|
import { getModelClientMeta, } from '../local/client/createModelOperations.js';
|
|
5
|
-
import {
|
|
6
|
-
import { useReactive } from '../useReactive.js';
|
|
5
|
+
import { useReactive } from './useReactive.js';
|
|
7
6
|
const EMPTY_CLAIMS = Object.freeze([]);
|
|
8
7
|
/**
|
|
9
8
|
* Restore the caller's schema generics on the context-held engine. React
|
|
@@ -27,50 +26,15 @@ function readModelResult(engine, modelClient, id, initial) {
|
|
|
27
26
|
if (!modelClient || id === undefined) {
|
|
28
27
|
return { data: initial, claims: EMPTY_CLAIMS, claimed: false };
|
|
29
28
|
}
|
|
30
|
-
const data =
|
|
29
|
+
const data = modelClient.local.get(id) ?? initial;
|
|
31
30
|
const meta = getModelClientMeta(modelClient);
|
|
32
31
|
const claims = meta && engine
|
|
33
32
|
? engine.claims.list({ model: meta.key, id })
|
|
34
33
|
: EMPTY_CLAIMS;
|
|
35
34
|
return { data, claims, claimed: claims.length > 0 };
|
|
36
35
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Projects a reactive read into the value that `useReactive` caches and
|
|
39
|
-
* returns.
|
|
40
|
-
*
|
|
41
|
-
* For a `Model`, this reads the row's fields through `toReactiveSnapshot`
|
|
42
|
-
* rather than returning the instance itself. Property access is what subscribes
|
|
43
|
-
* the reaction to those fields, so the read has to happen inside this tracked
|
|
44
|
-
* function; returning the live instance without reading its fields would leave
|
|
45
|
-
* the component blind to later edits. The fresh object it produces also lets
|
|
46
|
-
* `useReactive`'s equality check detect an in-place update.
|
|
47
|
-
*/
|
|
48
|
-
function snapshotValue(value) {
|
|
49
|
-
if (value instanceof Model) {
|
|
50
|
-
return value.toReactiveSnapshot();
|
|
51
|
-
}
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
return value.map((item) => snapshotValue(item));
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
36
|
export function useAblo(modelOrSelect, id, options) {
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* @internal The one implementation behind `useAblo` and the bound hooks a
|
|
62
|
-
* `createAbloReact` binding returns — written once so the reactive read path
|
|
63
|
-
* cannot fork between the global hook and a factory's.
|
|
64
|
-
*
|
|
65
|
-
* `boundClient` is a binding's own context value — typed `Ablo<S>` at the
|
|
66
|
-
* factory, so that path never rebinds and never casts. `null` means "no
|
|
67
|
-
* binding provider in this tree": the global hook always passes it, and a
|
|
68
|
-
* binding hook mounted under a legacy provider falls through to the erased
|
|
69
|
-
* internal context, which is what keeps both mounts working while the last
|
|
70
|
-
* legacy mount migrates.
|
|
71
|
-
*/
|
|
72
|
-
export function useAbloImpl(boundClient, modelOrSelect, id, options) {
|
|
73
|
-
const engine = useAbloClientImpl(boundClient);
|
|
37
|
+
const engine = useAbloClient();
|
|
74
38
|
const initial = options?.initial;
|
|
75
39
|
const isSelectorOnly = typeof modelOrSelect === 'function' && id === undefined;
|
|
76
40
|
const modelClient = typeof modelOrSelect === 'function' && id !== undefined
|
|
@@ -80,46 +44,44 @@ export function useAbloImpl(boundClient, modelOrSelect, id, options) {
|
|
|
80
44
|
: typeof modelOrSelect === 'function'
|
|
81
45
|
? undefined
|
|
82
46
|
: modelOrSelect;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
|
|
99
|
-
return undefined;
|
|
47
|
+
// The initial row is a seed for this client/model/id, not a permanent
|
|
48
|
+
// fallback: once local data has been committed to the UI, its removal must
|
|
49
|
+
// not resurrect the seed. Only committed effects change this marker.
|
|
50
|
+
// These dependencies define when the seed belongs to a different row.
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
const seed = useMemo(() => ({ received: false }), [engine, modelClient, id]);
|
|
53
|
+
const reading = modelOrSelect !== undefined;
|
|
54
|
+
const subscribe = useCallback((notify) => {
|
|
55
|
+
if (!engine || !reading)
|
|
56
|
+
return () => undefined;
|
|
57
|
+
return engine.claims.onChange(notify);
|
|
58
|
+
}, [engine, reading]);
|
|
59
|
+
const value = useReactive(() => {
|
|
60
|
+
if (isSelectorOnly && typeof modelOrSelect === 'function') {
|
|
61
|
+
return engine ? modelOrSelect(reactiveReads(engine)) : undefined;
|
|
100
62
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
63
|
+
if (modelOrSelect) {
|
|
64
|
+
return readModelResult(engine, modelClient, id, seed.received ? undefined : initial);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}, {
|
|
68
|
+
subscribe,
|
|
69
|
+
// The same seed produces the server HTML and the first hydration render,
|
|
70
|
+
// even if the browser already has a newer row or claim in its local cache.
|
|
71
|
+
...(id !== undefined && initial !== undefined ? {
|
|
72
|
+
serverSnapshot: () => ({ data: initial, claims: EMPTY_CLAIMS, claimed: false }),
|
|
73
|
+
} : {}),
|
|
110
74
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (id !== undefined && modelClient?.local.get(id) !== undefined)
|
|
77
|
+
seed.received = true;
|
|
78
|
+
}, [seed, modelClient, id, value]);
|
|
79
|
+
if (isSelectorOnly || modelOrSelect)
|
|
80
|
+
return value;
|
|
115
81
|
return engine;
|
|
116
82
|
}
|
|
117
|
-
/** @internal Resolve the
|
|
118
|
-
export function
|
|
83
|
+
/** @internal Resolve the nearest provider's client through one schema rebind. */
|
|
84
|
+
export function useAbloClient() {
|
|
119
85
|
const ctx = useContext(AbloInternalContext);
|
|
120
|
-
|
|
121
|
-
// fallback is the ONE remaining schema rebind in the SDK; it retires with
|
|
122
|
-
// the last legacy provider mount (docs/plans/typed-react-binding.md).
|
|
123
|
-
const engine = boundClient ?? (ctx?.engine ? rebindEngine(ctx.engine) : null);
|
|
124
|
-
return engine;
|
|
86
|
+
return ctx?.engine ? rebindEngine(ctx.engine) : null;
|
|
125
87
|
}
|
|
@@ -14,7 +14,7 @@ import type { ResolveSchema } from '@abloatai/transaction/types/global';
|
|
|
14
14
|
* If a mutator throws, the error propagates to the caller and any writes it
|
|
15
15
|
* already dispatched stay in place — there is no automatic rollback. Wrap the
|
|
16
16
|
* call in your own try/catch and issue compensating writes when you need to
|
|
17
|
-
* undo a partial change, or pass an `undoScope` (see {@link
|
|
17
|
+
* undo a partial change, or pass an `undoScope` (see {@link useMutators.Options})
|
|
18
18
|
* to record inverses for undo and redo.
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
@@ -34,23 +34,26 @@ export type InvokerFor<F> = F extends (options: infer O) => Promise<infer R> ? O
|
|
|
34
34
|
* The hook's return shape: same tree as the input `MutatorDefs`, every leaf
|
|
35
35
|
* rewritten to its invoker form.
|
|
36
36
|
*/
|
|
37
|
-
export type MutatorInvokers<M> = {
|
|
38
|
-
[K in keyof M]: {
|
|
39
|
-
[N in keyof M[K]]: InvokerFor<M[K][N]>;
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Options passed to `useMutators`. When `undoScope` is set, every mutator
|
|
44
|
-
* invocation is wrapped in a `RecordingMutation` and its inverses are
|
|
45
|
-
* pushed to the scope as one undo entry.
|
|
46
|
-
*/
|
|
47
|
-
export interface UseMutatorsOptions<S extends Schema> {
|
|
48
|
-
/** Target undo scope for recording inverses. Omit to disable recording. */
|
|
49
|
-
undoScope?: UndoScope<S>;
|
|
50
|
-
}
|
|
51
37
|
/** Mutator invokers (explicit schema arg). */
|
|
52
|
-
export declare function useMutators<S extends Schema, M extends MutatorDefs<S>>(schema: S, mutators: M, options?:
|
|
38
|
+
export declare function useMutators<S extends Schema, M extends MutatorDefs<S>>(schema: S, mutators: M, options?: useMutators.Options<S>): useMutators.Result<M>;
|
|
53
39
|
/** Mutator invokers via the `Register` module augmentation. Schema comes
|
|
54
40
|
* from the `SyncProvider`'s context; the mutator tree is typed against
|
|
55
41
|
* `ResolveSchema` at the call site. */
|
|
56
|
-
export declare function useMutators<M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>>(mutators: M, options?:
|
|
42
|
+
export declare function useMutators<M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>>(mutators: M, options?: useMutators.Options<ResolveSchema extends Schema ? ResolveSchema : Schema>): useMutators.Result<M>;
|
|
43
|
+
/** Optional annotations for custom mutation bindings. */
|
|
44
|
+
export declare namespace useMutators {
|
|
45
|
+
type Result<M> = {
|
|
46
|
+
[K in keyof M]: {
|
|
47
|
+
[N in keyof M[K]]: InvokerFor<M[K][N]>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Options passed to `useMutators`. When `undoScope` is set, every mutator
|
|
52
|
+
* invocation is wrapped in a `RecordingMutation` and its inverses are
|
|
53
|
+
* pushed to the scope as one undo entry.
|
|
54
|
+
*/
|
|
55
|
+
interface Options<S extends Schema> {
|
|
56
|
+
/** Target undo scope for recording inverses. Omit to disable recording. */
|
|
57
|
+
undoScope?: UndoScope<S>;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useEffect } from 'react';
|
|
3
3
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
4
4
|
import { getModelClientMeta, } from '../local/client/createModelOperations.js';
|
|
5
|
-
import {
|
|
5
|
+
import { useAbloClient } from './useAblo.js';
|
|
6
|
+
import { useReactive } from './useReactive.js';
|
|
6
7
|
export function usePresence(modelOrSelect, recordId) {
|
|
7
|
-
const engine =
|
|
8
|
+
const engine = useAbloClient();
|
|
8
9
|
return usePresenceImpl(engine, modelOrSelect, recordId);
|
|
9
10
|
}
|
|
10
11
|
/** @internal Shared by the global hook and schema-bound React factory. */
|
|
@@ -21,8 +22,8 @@ export function usePresenceImpl(engine, modelOrSelect, recordId) {
|
|
|
21
22
|
if (modelClient !== null && presence === undefined) {
|
|
22
23
|
throw new AbloValidationError('usePresence requires a model from the reactive Ablo client.', { code: 'invalid_request', param: 'modelClient' });
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const subscribe = useCallback((notify) => presence?.subscribe(notify) ?? (() => undefined), [presence]);
|
|
26
|
+
const sessions = useReactive(() => presence?.get(recordId) ?? [], { subscribe });
|
|
26
27
|
useEffect(() => presence?.read(recordId), [presence, recordId]);
|
|
27
|
-
return
|
|
28
|
+
return sessions;
|
|
28
29
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface ObservationOptions<T> {
|
|
2
|
+
equals?: (a: T, b: T) => boolean;
|
|
3
|
+
subscribe?: (listener: () => void) => () => void;
|
|
4
|
+
serverSnapshot?: () => T;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Each render's computation has its own observation. A suspended render cannot
|
|
8
|
+
* replace the computation used by the committed tree's subscription. Reactions
|
|
9
|
+
* start only on subscription, so abandoned renders retain no store observers.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useReactive<T>(compute: () => T, options?: ObservationOptions<T>): T;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
3
|
+
import { reaction } from 'mobx';
|
|
4
|
+
import { equalSnapshots, snapshotValue } from './snapshot.js';
|
|
5
|
+
/**
|
|
6
|
+
* Each render's computation has its own observation. A suspended render cannot
|
|
7
|
+
* replace the computation used by the committed tree's subscription. Reactions
|
|
8
|
+
* start only on subscription, so abandoned renders retain no store observers.
|
|
9
|
+
*/
|
|
10
|
+
export function useReactive(compute, options = {}) {
|
|
11
|
+
const { equals = equalSnapshots, subscribe, serverSnapshot = compute } = options;
|
|
12
|
+
const committed = useRef(null);
|
|
13
|
+
const observation = useMemo(() => {
|
|
14
|
+
let cached = committed.current;
|
|
15
|
+
let server = null;
|
|
16
|
+
const failed = Symbol('selector error');
|
|
17
|
+
const read = () => {
|
|
18
|
+
const next = snapshotValue(compute());
|
|
19
|
+
if (cached === null || !equals(cached.value, next))
|
|
20
|
+
cached = { value: next };
|
|
21
|
+
return cached.value;
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
read,
|
|
25
|
+
readServer: () => {
|
|
26
|
+
server ??= { value: snapshotValue(serverSnapshot()) };
|
|
27
|
+
return server.value;
|
|
28
|
+
},
|
|
29
|
+
subscribe: (notify) => {
|
|
30
|
+
const stop = reaction(() => {
|
|
31
|
+
// Notify React of selector failures; React re-reads and delivers the
|
|
32
|
+
// exception to its error boundary instead of MobX swallowing it.
|
|
33
|
+
try {
|
|
34
|
+
return read();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return failed;
|
|
38
|
+
}
|
|
39
|
+
}, () => { notify(); });
|
|
40
|
+
let stopExternal;
|
|
41
|
+
try {
|
|
42
|
+
stopExternal = subscribe?.(notify);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
stop();
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return () => { stop(); stopExternal?.(); };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}, [compute, equals, subscribe, serverSnapshot]);
|
|
52
|
+
// read() recomputes from the actual store, including React's consistency
|
|
53
|
+
// checks between render and commit, while equal snapshots retain identity.
|
|
54
|
+
const value = useSyncExternalStore(observation.subscribe, observation.read, observation.readServer);
|
|
55
|
+
useEffect(() => { committed.current = { value }; }, [value]);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
@@ -1,34 +1,20 @@
|
|
|
1
1
|
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
2
2
|
import { type UndoScope, type UndoScopeOptions } from '../local/mutators/UndoManager.js';
|
|
3
3
|
import type { ResolveSchema } from '@abloatai/transaction/types/global';
|
|
4
|
-
/**
|
|
5
|
-
* Provides per-surface undo and redo for mutator invocations. Each named scope
|
|
6
|
-
* owns an independent undo/redo stack, so different parts of your app — a main
|
|
7
|
-
* editor, a sidebar form — can undo separately without stepping on each other.
|
|
8
|
-
*
|
|
9
|
-
* Wire the returned `scope` into `useMutators(schema, mutators, { undoScope:
|
|
10
|
-
* scope })` and those invocations become recorded. `undo()` and `redo()` replay
|
|
11
|
-
* the captured inverses and forwards as new transactions that do not record
|
|
12
|
-
* themselves; the manager moves the entry between the two stacks explicitly.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* const { undo, redo, canUndo, canRedo, scope } = useUndoScope('report-editor');
|
|
16
|
-
* const mutate = useMutators(schema, reportMutators, { undoScope: scope });
|
|
17
|
-
*
|
|
18
|
-
* // Cmd+Z handler
|
|
19
|
-
* useHotkey('mod+z', () => { if (canUndo) void undo(); });
|
|
20
|
-
*/
|
|
21
|
-
export interface UseUndoScopeResult<S extends Schema> {
|
|
22
|
-
/** Pass to `useMutators(..., { undoScope })` to enable recording. */
|
|
23
|
-
scope: UndoScope<S>;
|
|
24
|
-
undo: () => Promise<void>;
|
|
25
|
-
redo: () => Promise<void>;
|
|
26
|
-
canUndo: boolean;
|
|
27
|
-
canRedo: boolean;
|
|
28
|
-
/** Drop history. Use after sync errors / auth context changes. */
|
|
29
|
-
clear: () => void;
|
|
30
|
-
}
|
|
31
4
|
/** Per-surface undo/redo (explicit schema arg). */
|
|
32
|
-
export declare function useUndoScope<S extends Schema>(schema: S, name: string, options?: UndoScopeOptions):
|
|
5
|
+
export declare function useUndoScope<S extends Schema>(schema: S, name: string, options?: UndoScopeOptions): useUndoScope.Result<S>;
|
|
33
6
|
/** Per-surface undo/redo via the `Register` module augmentation. */
|
|
34
|
-
export declare function useUndoScope(name: string, options?: UndoScopeOptions):
|
|
7
|
+
export declare function useUndoScope(name: string, options?: UndoScopeOptions): useUndoScope.Result<ResolveSchema extends Schema ? ResolveSchema : Schema>;
|
|
8
|
+
/** The state returned by an undo scope; inferred for ordinary hook calls. */
|
|
9
|
+
export declare namespace useUndoScope {
|
|
10
|
+
interface Result<S extends Schema> {
|
|
11
|
+
/** Pass to `useMutators(..., { undoScope })` to enable recording. */
|
|
12
|
+
scope: UndoScope<S>;
|
|
13
|
+
undo: () => Promise<void>;
|
|
14
|
+
redo: () => Promise<void>;
|
|
15
|
+
canUndo: boolean;
|
|
16
|
+
canRedo: boolean;
|
|
17
|
+
/** Drop history. Use after sync errors / auth context changes. */
|
|
18
|
+
clear: () => void;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -3,6 +3,23 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { UndoManager, } from '../local/mutators/UndoManager.js';
|
|
4
4
|
import { useSyncContext } from './context.js';
|
|
5
5
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
6
|
+
/**
|
|
7
|
+
* Provides per-surface undo and redo for mutator invocations. Each named scope
|
|
8
|
+
* owns an independent undo/redo stack, so different parts of your app — a main
|
|
9
|
+
* editor, a sidebar form — can undo separately without stepping on each other.
|
|
10
|
+
*
|
|
11
|
+
* Wire the returned `scope` into `useMutators(schema, mutators, { undoScope:
|
|
12
|
+
* scope })` and those invocations become recorded. `undo()` and `redo()` replay
|
|
13
|
+
* the captured inverses and forwards as new transactions that do not record
|
|
14
|
+
* themselves; the manager moves the entry between the two stacks explicitly.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { undo, redo, canUndo, canRedo, scope } = useUndoScope('report-editor');
|
|
18
|
+
* const mutate = useMutators(schema, reportMutators, { undoScope: scope });
|
|
19
|
+
*
|
|
20
|
+
* // Cmd+Z handler
|
|
21
|
+
* useHotkey('mod+z', () => { if (canUndo) void undo(); });
|
|
22
|
+
*/
|
|
6
23
|
// Module-level weak registry: `SyncStoreContract` → `UndoManager`.
|
|
7
24
|
// A single app wiring through one SyncProvider shares one manager across
|
|
8
25
|
// every useUndoScope call, so scopes with the same name are identity-equal.
|
package/dist/react.d.ts
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
/** React
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export { AbloProvider, usePeers, useSync, useSyncStore, type AbloProviderProps, type GroupScope, } from './react/AbloProvider.js';
|
|
9
|
-
export { ClientSideSuspense, type ClientSideSuspenseProps, } from './react/ClientSideSuspense.js';
|
|
10
|
-
export { DefaultFallback } from './react/DefaultFallback.js';
|
|
11
|
-
export { createAbloReact, type AbloReactBinding, } from './react/createAbloReact.js';
|
|
12
|
-
export { useAblo, type UseAbloHydratedModelResult, type UseAbloModelOptions, type UseAbloModelResult, } from './react/useAblo.js';
|
|
13
|
-
export { useMutators, type InvokerFor, type MutatorInvokers, type UseMutatorsOptions, } from './react/useMutators.js';
|
|
14
|
-
export { useUndoScope, type UseUndoScopeResult, } from './react/useUndoScope.js';
|
|
15
|
-
export type { DefaultSyncShape, ResolveSchema, ResolveUserMeta, ResolveClaimMeta, ResolveModelKey, } from '@abloatai/transaction/types/global';
|
|
16
|
-
export { ModelScope } from '@abloatai/transaction/types';
|
|
17
|
-
export type { SyncStoreContract } from './react/context.js';
|
|
18
|
-
export type { MutateActions } from './local/mutators/mutateActions.js';
|
|
19
|
-
export type { ReaderActions, ReaderFindOptions, } from './local/mutators/readerActions.js';
|
|
1
|
+
/** React owns context, subscriptions and component lifetimes over core Ablo. */
|
|
2
|
+
export { AbloProvider } from './react/AbloProvider.js';
|
|
3
|
+
export { createAbloReact } from './react/createAbloReact.js';
|
|
4
|
+
export { useAblo } from './react/useAblo.js';
|
|
5
|
+
export { usePresence } from './react/usePresence.js';
|
|
6
|
+
export { useMutators } from './react/useMutators.js';
|
|
7
|
+
export { useUndoScope } from './react/useUndoScope.js';
|
package/dist/react.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
/** React
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export { AbloProvider, usePeers, useSync, useSyncStore, } from './react/AbloProvider.js';
|
|
9
|
-
export { ClientSideSuspense, } from './react/ClientSideSuspense.js';
|
|
10
|
-
export { DefaultFallback } from './react/DefaultFallback.js';
|
|
11
|
-
export { createAbloReact, } from './react/createAbloReact.js';
|
|
12
|
-
export { useAblo, } from './react/useAblo.js';
|
|
13
|
-
export { useMutators, } from './react/useMutators.js';
|
|
14
|
-
export { useUndoScope, } from './react/useUndoScope.js';
|
|
15
|
-
export { ModelScope } from '@abloatai/transaction/types';
|
|
1
|
+
/** React owns context, subscriptions and component lifetimes over core Ablo. */
|
|
2
|
+
export { AbloProvider } from './react/AbloProvider.js';
|
|
3
|
+
export { createAbloReact } from './react/createAbloReact.js';
|
|
4
|
+
export { useAblo } from './react/useAblo.js';
|
|
5
|
+
export { usePresence } from './react/usePresence.js';
|
|
6
|
+
export { useMutators } from './react/useMutators.js';
|
|
7
|
+
export { useUndoScope } from './react/useUndoScope.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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",
|
|
@@ -71,7 +71,8 @@
|
|
|
71
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
|
-
"lint:pkg": "publint"
|
|
74
|
+
"lint:pkg": "publint",
|
|
75
|
+
"typecheck:react": "tsc -p typetests/binding/tsconfig.json"
|
|
75
76
|
},
|
|
76
77
|
"publishConfig": {
|
|
77
78
|
"access": "public",
|
|
@@ -84,7 +85,7 @@
|
|
|
84
85
|
"directory": "packages/humans"
|
|
85
86
|
},
|
|
86
87
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.
|
|
88
|
+
"@abloatai/transaction": "^0.64.0",
|
|
88
89
|
"mobx": "^6.13.7",
|
|
89
90
|
"uuid": "^11.1.0",
|
|
90
91
|
"zod": "^4.4.3"
|
package/src/Ablo.ts
CHANGED
|
@@ -264,6 +264,11 @@ import type * as _Global from '@abloatai/transaction/types/global';
|
|
|
264
264
|
*/
|
|
265
265
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
266
266
|
export namespace Ablo {
|
|
267
|
+
/** Payload delivered by the core client's onMutationFailure subscription. */
|
|
268
|
+
export type MutationFailure = Parameters<Parameters<AbloClient<SchemaRecord>['onMutationFailure']>[0]>[0];
|
|
269
|
+
/** Current client lifecycle, also selected through React's useAblo. */
|
|
270
|
+
export type Status = import('./local/client/status.js').ClientStatus;
|
|
271
|
+
|
|
267
272
|
// ── Factory options ────────────────────────────────────────────────
|
|
268
273
|
export type Options<S extends SchemaRecord = SchemaRecord> = AbloOptions<S>;
|
|
269
274
|
/**
|