@abloatai/humans 0.63.1 → 0.64.1
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 +6 -0
- package/dist/client.d.ts +12 -32
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/local/client/createModelOperations.d.ts +3 -3
- package/dist/local/client/createModelOperations.js +1 -1
- package/dist/local/client/reactiveEngine.js +7 -8
- 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/mutators/defineMutators.d.ts +3 -48
- package/dist/local/mutators/defineMutators.js +0 -15
- package/dist/local/storeAccess.d.ts +7 -0
- package/dist/local/storeAccess.js +6 -0
- package/dist/local/storeContract.d.ts +3 -4
- package/dist/presence/index.d.ts +2 -2
- package/dist/presence/index.js +8 -4
- package/dist/react/AbloProvider.d.ts +57 -121
- package/dist/react/AbloProvider.js +55 -160
- package/dist/react/context.d.ts +6 -45
- package/dist/react/context.js +8 -20
- package/dist/react/createAbloReact.d.ts +13 -48
- package/dist/react/createAbloReact.js +8 -54
- package/dist/react/internalContext.d.ts +1 -27
- package/dist/react/snapshot.d.ts +4 -0
- package/dist/react/snapshot.js +75 -0
- package/dist/react/useAblo.d.ts +28 -92
- package/dist/react/useAblo.js +36 -93
- package/dist/react/useAbloClient.d.ts +5 -0
- package/dist/react/useAbloClient.js +9 -0
- package/dist/react/useMutationFailure.d.ts +6 -0
- package/dist/react/useMutationFailure.js +11 -0
- package/dist/react/useMutators.d.ts +22 -19
- package/dist/react/useMutators.js +3 -3
- package/dist/react/usePresence.d.ts +9 -9
- package/dist/react/usePresence.js +8 -11
- package/dist/react/useReactive.d.ts +12 -0
- package/dist/react/useReactive.js +57 -0
- package/dist/react/useUndoScope.d.ts +16 -30
- package/dist/react/useUndoScope.js +21 -4
- package/dist/react.d.ts +9 -19
- package/dist/react.js +9 -15
- package/dist/reactRuntime.d.ts +1 -1
- package/dist/reactRuntime.js +1 -1
- package/package.json +4 -3
- package/src/Ablo.ts +7 -0
- package/src/client.ts +13 -32
- package/src/index.ts +2 -0
- package/src/local/client/createModelOperations.ts +4 -4
- package/src/local/client/reactiveEngine.ts +7 -8
- package/src/local/client/status.ts +20 -0
- package/src/local/client/storeLifecycle.ts +2 -2
- package/src/local/mutators/defineMutators.ts +3 -51
- package/src/local/storeAccess.ts +10 -0
- package/src/local/storeContract.ts +3 -4
- package/src/presence/index.ts +10 -5
- package/src/react/AbloProvider.tsx +88 -263
- package/src/react/context.ts +10 -61
- package/src/react/createAbloReact.ts +16 -125
- package/src/react/internalContext.ts +1 -27
- package/src/react/snapshot.ts +67 -0
- package/src/react/useAblo.ts +73 -209
- package/src/react/useAbloClient.ts +14 -0
- package/src/react/useMutationFailure.ts +17 -0
- package/src/react/useMutators.ts +36 -32
- package/src/react/usePresence.ts +22 -27
- package/src/react/useReactive.ts +56 -0
- package/src/react/useUndoScope.ts +24 -20
- package/src/react.ts +9 -69
- package/src/reactRuntime.ts +2 -2
- 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 -48
- package/dist/useReactive.d.ts +0 -6
- package/dist/useReactive.js +0 -41
- 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 -53
- package/src/useReactive.ts +0 -49
package/src/react/useMutators.ts
CHANGED
|
@@ -9,8 +9,8 @@ import type {
|
|
|
9
9
|
import { createTransaction } from '../local/mutators/Transaction.js';
|
|
10
10
|
import { createRecordingMutation } from '../local/mutators/RecordingMutation.js';
|
|
11
11
|
import type { UndoScope } from '../local/mutators/UndoManager.js';
|
|
12
|
-
import type { ResolveSchema } from '@abloatai/transaction/types/global';
|
|
13
|
-
import {
|
|
12
|
+
import type { ResolveSchema, RequireRegisteredSchema } from '@abloatai/transaction/types/global';
|
|
13
|
+
import { useAbloStoreContext } from './context.js';
|
|
14
14
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
15
15
|
import { getContext } from '../local/context.js';
|
|
16
16
|
|
|
@@ -26,7 +26,7 @@ import { getContext } from '../local/context.js';
|
|
|
26
26
|
* If a mutator throws, the error propagates to the caller and any writes it
|
|
27
27
|
* already dispatched stay in place — there is no automatic rollback. Wrap the
|
|
28
28
|
* call in your own try/catch and issue compensating writes when you need to
|
|
29
|
-
* undo a partial change, or pass an `undoScope` (see {@link
|
|
29
|
+
* undo a partial change, or pass an `undoScope` (see {@link useMutators.Options})
|
|
30
30
|
* to record inverses for undo and redo.
|
|
31
31
|
*/
|
|
32
32
|
|
|
@@ -50,45 +50,29 @@ export type InvokerFor<F> = F extends (options: infer O) => Promise<infer R>
|
|
|
50
50
|
* The hook's return shape: same tree as the input `MutatorDefs`, every leaf
|
|
51
51
|
* rewritten to its invoker form.
|
|
52
52
|
*/
|
|
53
|
-
export type MutatorInvokers<M> = {
|
|
54
|
-
[K in keyof M]: {
|
|
55
|
-
[N in keyof M[K]]: InvokerFor<M[K][N]>;
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Options passed to `useMutators`. When `undoScope` is set, every mutator
|
|
61
|
-
* invocation is wrapped in a `RecordingMutation` and its inverses are
|
|
62
|
-
* pushed to the scope as one undo entry.
|
|
63
|
-
*/
|
|
64
|
-
export interface UseMutatorsOptions<S extends Schema> {
|
|
65
|
-
/** Target undo scope for recording inverses. Omit to disable recording. */
|
|
66
|
-
undoScope?: UndoScope<S>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
53
|
/** Mutator invokers (explicit schema arg). */
|
|
70
54
|
export function useMutators<S extends Schema, M extends MutatorDefs<S>>(
|
|
71
55
|
schema: S,
|
|
72
56
|
mutators: M,
|
|
73
|
-
options?:
|
|
74
|
-
):
|
|
57
|
+
options?: useMutators.Options<S>,
|
|
58
|
+
): useMutators.Result<M>;
|
|
75
59
|
|
|
76
60
|
/** Mutator invokers via the `Register` module augmentation. Schema comes
|
|
77
|
-
* from the `
|
|
61
|
+
* from the `AbloProvider` store context; the mutator tree is typed against
|
|
78
62
|
* `ResolveSchema` at the call site. */
|
|
79
63
|
export function useMutators<
|
|
80
64
|
M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>,
|
|
81
65
|
>(
|
|
82
|
-
mutators: M
|
|
83
|
-
options?:
|
|
84
|
-
):
|
|
66
|
+
mutators: RequireRegisteredSchema<M>,
|
|
67
|
+
options?: useMutators.Options<ResolveSchema extends Schema ? ResolveSchema : Schema>,
|
|
68
|
+
): useMutators.Result<M>;
|
|
85
69
|
|
|
86
70
|
export function useMutators(
|
|
87
71
|
schemaOrMutators: Schema | MutatorDefs<Schema>,
|
|
88
|
-
mutatorsOrOptions?: MutatorDefs<Schema> |
|
|
89
|
-
maybeOptions?:
|
|
90
|
-
):
|
|
91
|
-
const { store, organizationId, schema: ctxSchema } =
|
|
72
|
+
mutatorsOrOptions?: MutatorDefs<Schema> | useMutators.Options<Schema>,
|
|
73
|
+
maybeOptions?: useMutators.Options<Schema>,
|
|
74
|
+
): useMutators.Result<MutatorDefs<Schema>> {
|
|
75
|
+
const { store, organizationId, schema: ctxSchema } = useAbloStoreContext();
|
|
92
76
|
|
|
93
77
|
// Disambiguate: explicit-schema path has the schema object in first slot;
|
|
94
78
|
// the global-resolved path has the mutator tree there. A schema object
|
|
@@ -101,21 +85,21 @@ export function useMutators(
|
|
|
101
85
|
const schema = isExplicit ? (schemaOrMutators as Schema) : ctxSchema;
|
|
102
86
|
const mutators = (isExplicit ? mutatorsOrOptions : schemaOrMutators) as MutatorDefs<Schema>;
|
|
103
87
|
const options = (isExplicit ? maybeOptions : mutatorsOrOptions) as
|
|
104
|
-
|
|
|
88
|
+
| useMutators.Options<Schema>
|
|
105
89
|
| undefined;
|
|
106
90
|
|
|
107
91
|
if (!schema) {
|
|
108
92
|
throw new AbloValidationError(
|
|
109
93
|
'useMutators: no schema available. Pass the schema as the first arg, ' +
|
|
110
94
|
'or build the <AbloProvider> above with `Ablo({ schema })` so the ' +
|
|
111
|
-
'
|
|
95
|
+
'schema-free overload can read it from context.',
|
|
112
96
|
{ code: 'mutators_schema_missing' },
|
|
113
97
|
);
|
|
114
98
|
}
|
|
115
99
|
|
|
116
100
|
const { undoScope } = options ?? {};
|
|
117
101
|
|
|
118
|
-
return useMemo<
|
|
102
|
+
return useMemo<useMutators.Result<MutatorDefs<Schema>>>(() => {
|
|
119
103
|
const out: Record<string, Record<string, (args: unknown) => Promise<unknown>>> = {};
|
|
120
104
|
|
|
121
105
|
for (const modelKey of Object.keys(mutators)) {
|
|
@@ -182,3 +166,23 @@ export function useMutators(
|
|
|
182
166
|
return out;
|
|
183
167
|
}, [schema, mutators, store, organizationId, undoScope]);
|
|
184
168
|
}
|
|
169
|
+
|
|
170
|
+
/** Optional annotations for custom mutation bindings. */
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
172
|
+
export namespace useMutators {
|
|
173
|
+
export type Result<M> = {
|
|
174
|
+
[K in keyof M]: {
|
|
175
|
+
[N in keyof M[K]]: InvokerFor<M[K][N]>;
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Options passed to `useMutators`. When `undoScope` is set, every mutator
|
|
181
|
+
* invocation is wrapped in a `RecordingMutation` and its inverses are
|
|
182
|
+
* pushed to the scope as one undo entry.
|
|
183
|
+
*/
|
|
184
|
+
export interface Options<S extends Schema> {
|
|
185
|
+
/** Target undo scope for recording inverses. Omit to disable recording. */
|
|
186
|
+
undoScope?: UndoScope<S>;
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/react/usePresence.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback, useEffect } from 'react';
|
|
4
4
|
import type { PresenceSession } from '@abloatai/transaction/presence';
|
|
5
5
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
6
6
|
import {
|
|
@@ -9,14 +9,9 @@ import {
|
|
|
9
9
|
} from '../local/client/createModelOperations.js';
|
|
10
10
|
import type { AbloClient as Ablo } from '../client.js';
|
|
11
11
|
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
type DefaultModels = ResolveSchema extends { models: infer M }
|
|
16
|
-
? M extends SchemaRecord
|
|
17
|
-
? M
|
|
18
|
-
: SchemaRecord
|
|
19
|
-
: SchemaRecord;
|
|
12
|
+
import type { ResolveModels as DefaultModels } from '@abloatai/transaction/types/global';
|
|
13
|
+
import { useAbloClient } from './useAbloClient.js';
|
|
14
|
+
import { useReactive } from './useReactive.js';
|
|
20
15
|
|
|
21
16
|
export type PresenceModelSelector<R extends SchemaRecord, T, C> =
|
|
22
17
|
(ablo: Ablo<R>) => ModelOperations<T, C>;
|
|
@@ -29,6 +24,7 @@ export type PresenceModelSelector<R extends SchemaRecord, T, C> =
|
|
|
29
24
|
export function usePresence<T, C>(
|
|
30
25
|
modelClient: ModelOperations<T, C>,
|
|
31
26
|
recordId: string,
|
|
27
|
+
options?: usePresence.Options,
|
|
32
28
|
): readonly PresenceSession[];
|
|
33
29
|
export function usePresence<
|
|
34
30
|
R extends SchemaRecord = DefaultModels,
|
|
@@ -37,6 +33,7 @@ export function usePresence<
|
|
|
37
33
|
>(
|
|
38
34
|
select: PresenceModelSelector<R, T, C>,
|
|
39
35
|
recordId: string,
|
|
36
|
+
options?: usePresence.Options,
|
|
40
37
|
): readonly PresenceSession[];
|
|
41
38
|
export function usePresence<
|
|
42
39
|
R extends SchemaRecord = DefaultModels,
|
|
@@ -45,17 +42,9 @@ export function usePresence<
|
|
|
45
42
|
>(
|
|
46
43
|
modelOrSelect: ModelOperations<T, C> | PresenceModelSelector<R, T, C>,
|
|
47
44
|
recordId: string,
|
|
45
|
+
options?: usePresence.Options,
|
|
48
46
|
): readonly PresenceSession[] {
|
|
49
|
-
const engine =
|
|
50
|
-
return usePresenceImpl(engine, modelOrSelect, recordId);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** @internal Shared by the global hook and schema-bound React factory. */
|
|
54
|
-
export function usePresenceImpl<R extends SchemaRecord, T, C>(
|
|
55
|
-
engine: Ablo<R> | null,
|
|
56
|
-
modelOrSelect: ModelOperations<T, C> | PresenceModelSelector<R, T, C>,
|
|
57
|
-
recordId: string,
|
|
58
|
-
): readonly PresenceSession[] {
|
|
47
|
+
const engine = useAbloClient<R>();
|
|
59
48
|
if (recordId.length === 0) {
|
|
60
49
|
throw new AbloValidationError(
|
|
61
50
|
'usePresence requires a non-empty record id.',
|
|
@@ -75,14 +64,20 @@ export function usePresenceImpl<R extends SchemaRecord, T, C>(
|
|
|
75
64
|
);
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
useEffect(
|
|
81
|
-
() => presence?.subscribe(() => { render((version) => version + 1); }),
|
|
82
|
-
[presence],
|
|
83
|
-
);
|
|
84
|
-
|
|
67
|
+
const subscribe = useCallback((notify: () => void) => presence?.subscribe(notify) ?? (() => undefined), [presence]);
|
|
68
|
+
const sessions = useReactive(() => presence?.get(recordId, options) ?? [], { subscribe });
|
|
85
69
|
useEffect(() => presence?.read(recordId), [presence, recordId]);
|
|
70
|
+
return sessions;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export namespace usePresence {
|
|
74
|
+
export interface Bound<S extends SchemaRecord> {
|
|
75
|
+
<T, C>(
|
|
76
|
+
model: ModelOperations<T, C> | PresenceModelSelector<S, T, C>,
|
|
77
|
+
recordId: string,
|
|
78
|
+
options?: Options,
|
|
79
|
+
): readonly PresenceSession[];
|
|
80
|
+
}
|
|
86
81
|
|
|
87
|
-
|
|
82
|
+
export type Options = import('@abloatai/transaction/presence').PresenceQueryOptions;
|
|
88
83
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
import { reaction } from 'mobx';
|
|
5
|
+
import { equalSnapshots, snapshotValue } from './snapshot.js';
|
|
6
|
+
|
|
7
|
+
interface ObservationOptions<T> {
|
|
8
|
+
equals?: (a: T, b: T) => boolean;
|
|
9
|
+
subscribe?: (listener: () => void) => () => void;
|
|
10
|
+
serverSnapshot?: () => T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Each render's computation has its own observation. A suspended render cannot
|
|
15
|
+
* replace the computation used by the committed tree's subscription. Reactions
|
|
16
|
+
* start only on subscription, so abandoned renders retain no store observers.
|
|
17
|
+
*/
|
|
18
|
+
export function useReactive<T>(compute: () => T, options: ObservationOptions<T> = {}): T {
|
|
19
|
+
const { equals = equalSnapshots, subscribe, serverSnapshot = compute } = options;
|
|
20
|
+
const committed = useRef<{ value: T } | null>(null);
|
|
21
|
+
const observation = useMemo(() => {
|
|
22
|
+
let cached = committed.current;
|
|
23
|
+
let server: { value: T } | null = null;
|
|
24
|
+
const failed = Symbol('selector error');
|
|
25
|
+
const read = (): T => {
|
|
26
|
+
const next = snapshotValue(compute());
|
|
27
|
+
if (cached === null || !equals(cached.value, next)) cached = { value: next };
|
|
28
|
+
return cached.value;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
read,
|
|
32
|
+
readServer: (): T => {
|
|
33
|
+
server ??= { value: snapshotValue(serverSnapshot()) };
|
|
34
|
+
return server.value;
|
|
35
|
+
},
|
|
36
|
+
subscribe: (notify: () => void): (() => void) => {
|
|
37
|
+
const stop = reaction(
|
|
38
|
+
() => {
|
|
39
|
+
// Notify React of selector failures; React re-reads and delivers the
|
|
40
|
+
// exception to its error boundary instead of MobX swallowing it.
|
|
41
|
+
try { return read(); } catch { return failed; }
|
|
42
|
+
},
|
|
43
|
+
() => { notify(); },
|
|
44
|
+
);
|
|
45
|
+
let stopExternal: (() => void) | undefined;
|
|
46
|
+
try { stopExternal = subscribe?.(notify); } catch (error) { stop(); throw error; }
|
|
47
|
+
return () => { stop(); stopExternal?.(); };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}, [compute, equals, subscribe, serverSnapshot]);
|
|
51
|
+
// read() recomputes from the actual store, including React's consistency
|
|
52
|
+
// checks between render and commit, while equal snapshots retain identity.
|
|
53
|
+
const value = useSyncExternalStore(observation.subscribe, observation.read, observation.readServer);
|
|
54
|
+
useEffect(() => { committed.current = { value }; }, [value]);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
type UndoScope,
|
|
8
8
|
type UndoScopeOptions,
|
|
9
9
|
} from '../local/mutators/UndoManager.js';
|
|
10
|
-
import type { ResolveSchema } from '@abloatai/transaction/types/global';
|
|
11
|
-
import {
|
|
10
|
+
import type { ResolveSchema, RequireRegisteredSchema } from '@abloatai/transaction/types/global';
|
|
11
|
+
import { useAbloStoreContext } from './context.js';
|
|
12
12
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -29,19 +29,8 @@ import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
|
29
29
|
* useHotkey('mod+z', () => { if (canUndo) void undo(); });
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
export interface UseUndoScopeResult<S extends Schema> {
|
|
33
|
-
/** Pass to `useMutators(..., { undoScope })` to enable recording. */
|
|
34
|
-
scope: UndoScope<S>;
|
|
35
|
-
undo: () => Promise<void>;
|
|
36
|
-
redo: () => Promise<void>;
|
|
37
|
-
canUndo: boolean;
|
|
38
|
-
canRedo: boolean;
|
|
39
|
-
/** Drop history. Use after sync errors / auth context changes. */
|
|
40
|
-
clear: () => void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
32
|
// Module-level weak registry: `SyncStoreContract` → `UndoManager`.
|
|
44
|
-
// A single
|
|
33
|
+
// A single AbloProvider shares one manager across
|
|
45
34
|
// every useUndoScope call, so scopes with the same name are identity-equal.
|
|
46
35
|
// The hook implementation already operates on the runtime-wide `Schema` type;
|
|
47
36
|
// its overloads restore the caller's precise schema type at the public boundary,
|
|
@@ -66,20 +55,20 @@ export function useUndoScope<S extends Schema>(
|
|
|
66
55
|
schema: S,
|
|
67
56
|
name: string,
|
|
68
57
|
options?: UndoScopeOptions,
|
|
69
|
-
):
|
|
58
|
+
): useUndoScope.Result<S>;
|
|
70
59
|
|
|
71
60
|
/** Per-surface undo/redo via the `Register` module augmentation. */
|
|
72
61
|
export function useUndoScope(
|
|
73
|
-
name: string
|
|
62
|
+
name: RequireRegisteredSchema<string>,
|
|
74
63
|
options?: UndoScopeOptions,
|
|
75
|
-
):
|
|
64
|
+
): useUndoScope.Result<ResolveSchema extends Schema ? ResolveSchema : Schema>;
|
|
76
65
|
|
|
77
66
|
export function useUndoScope(
|
|
78
67
|
schemaOrName: Schema | string,
|
|
79
68
|
nameOrOptions?: string | UndoScopeOptions,
|
|
80
69
|
maybeOptions?: UndoScopeOptions,
|
|
81
|
-
):
|
|
82
|
-
const { store, organizationId, schema: ctxSchema } =
|
|
70
|
+
): useUndoScope.Result<Schema> {
|
|
71
|
+
const { store, organizationId, schema: ctxSchema } = useAbloStoreContext();
|
|
83
72
|
|
|
84
73
|
const isExplicit = typeof schemaOrName !== 'string';
|
|
85
74
|
const schema = isExplicit ? (schemaOrName) : ctxSchema;
|
|
@@ -96,7 +85,7 @@ export function useUndoScope(
|
|
|
96
85
|
}
|
|
97
86
|
|
|
98
87
|
const scope = useMemo(() => {
|
|
99
|
-
// Store is the identity for the manager — one per
|
|
88
|
+
// Store is the identity for the manager — one per AbloProvider.
|
|
100
89
|
const manager = getManager(store, () => new UndoManager(schema, store, organizationId));
|
|
101
90
|
return manager.getScope(name, options);
|
|
102
91
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -141,3 +130,18 @@ export function useUndoScope(
|
|
|
141
130
|
},
|
|
142
131
|
};
|
|
143
132
|
}
|
|
133
|
+
|
|
134
|
+
/** The state returned by an undo scope; inferred for ordinary hook calls. */
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
136
|
+
export namespace useUndoScope {
|
|
137
|
+
export interface Result<S extends Schema> {
|
|
138
|
+
/** Pass to `useMutators(..., { undoScope })` to enable recording. */
|
|
139
|
+
scope: UndoScope<S>;
|
|
140
|
+
undo: () => Promise<void>;
|
|
141
|
+
redo: () => Promise<void>;
|
|
142
|
+
canUndo: boolean;
|
|
143
|
+
canRedo: boolean;
|
|
144
|
+
/** Drop history. Use after sync errors / auth context changes. */
|
|
145
|
+
clear: () => void;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/react.ts
CHANGED
|
@@ -1,69 +1,9 @@
|
|
|
1
|
-
/** React
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './react/
|
|
10
|
-
export {
|
|
11
|
-
useMutationFailureListener,
|
|
12
|
-
type MutationFailurePayload,
|
|
13
|
-
} from './react/useMutationFailureListener.js';
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
AbloProvider,
|
|
17
|
-
usePeers,
|
|
18
|
-
useSync,
|
|
19
|
-
useSyncStore,
|
|
20
|
-
type AbloProviderProps,
|
|
21
|
-
type GroupScope,
|
|
22
|
-
} from './react/AbloProvider.js';
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
ClientSideSuspense,
|
|
26
|
-
type ClientSideSuspenseProps,
|
|
27
|
-
} from './react/ClientSideSuspense.js';
|
|
28
|
-
|
|
29
|
-
export { DefaultFallback } from './react/DefaultFallback.js';
|
|
30
|
-
|
|
31
|
-
export {
|
|
32
|
-
createAbloReact,
|
|
33
|
-
type AbloReactBinding,
|
|
34
|
-
} from './react/createAbloReact.js';
|
|
35
|
-
|
|
36
|
-
export {
|
|
37
|
-
useAblo,
|
|
38
|
-
type UseAbloHydratedModelResult,
|
|
39
|
-
type UseAbloModelOptions,
|
|
40
|
-
type UseAbloModelResult,
|
|
41
|
-
} from './react/useAblo.js';
|
|
42
|
-
|
|
43
|
-
export {
|
|
44
|
-
useMutators,
|
|
45
|
-
type InvokerFor,
|
|
46
|
-
type MutatorInvokers,
|
|
47
|
-
type UseMutatorsOptions,
|
|
48
|
-
} from './react/useMutators.js';
|
|
49
|
-
|
|
50
|
-
export {
|
|
51
|
-
useUndoScope,
|
|
52
|
-
type UseUndoScopeResult,
|
|
53
|
-
} from './react/useUndoScope.js';
|
|
54
|
-
|
|
55
|
-
export type {
|
|
56
|
-
DefaultSyncShape,
|
|
57
|
-
ResolveSchema,
|
|
58
|
-
ResolveUserMeta,
|
|
59
|
-
ResolveClaimMeta,
|
|
60
|
-
ResolveModelKey,
|
|
61
|
-
} from '@abloatai/transaction/types/global';
|
|
62
|
-
|
|
63
|
-
export { ModelScope } from '@abloatai/transaction/types';
|
|
64
|
-
export type { SyncStoreContract } from './react/context.js';
|
|
65
|
-
export type { MutateActions } from './local/mutators/mutateActions.js';
|
|
66
|
-
export type {
|
|
67
|
-
ReaderActions,
|
|
68
|
-
ReaderFindOptions,
|
|
69
|
-
} 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 { useAbloClient } from './react/useAbloClient.js';
|
|
6
|
+
export { useMutationFailure } from './react/useMutationFailure.js';
|
|
7
|
+
export { usePresence } from './react/usePresence.js';
|
|
8
|
+
export { useMutators } from './react/useMutators.js';
|
|
9
|
+
export { useUndoScope } from './react/useUndoScope.js';
|
package/src/reactRuntime.ts
CHANGED
|
@@ -3,8 +3,8 @@ export {
|
|
|
3
3
|
type AbloInternalContextValue,
|
|
4
4
|
} from './react/internalContext.js';
|
|
5
5
|
export {
|
|
6
|
-
|
|
7
|
-
type
|
|
6
|
+
useAbloStoreContext,
|
|
7
|
+
type AbloStoreContextValue,
|
|
8
8
|
} from './react/context.js';
|
|
9
9
|
export type { SyncStoreContract } from './local/storeContract.js';
|
|
10
10
|
export type { QueuedMutation } from './local/transactions/mutations/MutationQueue.js';
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* Nested bootstrap gate for a subtree. `<AbloProvider>` already ships
|
|
4
|
-
* its own built-in gate (via its `fallback` prop) that handles the
|
|
5
|
-
* common "wait for first bootstrap" case. Use `ClientSideSuspense`
|
|
6
|
-
* only when you need a SEPARATE gate inside an already-ready provider —
|
|
7
|
-
* for example, rendering app chrome immediately while gating a single
|
|
8
|
-
* heavy product surface on its own query resolving.
|
|
9
|
-
*
|
|
10
|
-
* Like the provider-level gate, this component latches open on the
|
|
11
|
-
* first `connected` / `reconnecting` / `disconnected` transition and
|
|
12
|
-
* stays open. Subsequent transient `connecting` states (hard reconnect
|
|
13
|
-
* after offline) do NOT re-show the fallback — the app has already
|
|
14
|
-
* rendered once and its own reconnect UI should take over.
|
|
15
|
-
*
|
|
16
|
-
* v0.3.x implementation is non-Suspense: reads `useSyncStatus()` and
|
|
17
|
-
* conditionally renders. v0.3.x+ will ship a
|
|
18
|
-
* the React suspense entry point where `useQuery` / `useOne`
|
|
19
|
-
* actually throw Promises; this component becomes a thin wrapper around
|
|
20
|
-
* React's real `<Suspense>` at that point.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* <AbloProvider fallback={<AppSkeleton />}>
|
|
24
|
-
* <AppChrome />
|
|
25
|
-
* <ClientSideSuspense fallback={<CanvasSkeleton />}>
|
|
26
|
-
* <HeavyCanvas />
|
|
27
|
-
* </ClientSideSuspense>
|
|
28
|
-
* </AbloProvider>
|
|
29
|
-
*/
|
|
30
|
-
export interface ClientSideSuspenseProps {
|
|
31
|
-
/** What to render while the nested subtree is waiting for first bootstrap. */
|
|
32
|
-
fallback: ReactNode;
|
|
33
|
-
/** What to render once the subtree is cleared to render. */
|
|
34
|
-
children: ReactNode;
|
|
35
|
-
}
|
|
36
|
-
export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react").JSX.Element;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { useSyncStatus } from './useSyncStatus.js';
|
|
5
|
-
export function ClientSideSuspense({ fallback, children }) {
|
|
6
|
-
const status = useSyncStatus();
|
|
7
|
-
const [everConnected, setEverConnected] = useState(false);
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
if (status.name === 'connected' ||
|
|
10
|
-
status.name === 'reconnecting' ||
|
|
11
|
-
status.name === 'disconnected') {
|
|
12
|
-
setEverConnected(true);
|
|
13
|
-
}
|
|
14
|
-
}, [status.name]);
|
|
15
|
-
const showFallback = !everConnected && status.name === 'connecting';
|
|
16
|
-
return _jsx(_Fragment, { children: showFallback ? fallback : children });
|
|
17
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useContext } from 'react';
|
|
3
|
-
import { AbloInternalContext } from './internalContext.js';
|
|
4
|
-
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
5
|
-
/** Returns the application user id supplied to the nearest AbloProvider. */
|
|
6
|
-
export function useCurrentUserId() {
|
|
7
|
-
const context = useContext(AbloInternalContext);
|
|
8
|
-
if (!context) {
|
|
9
|
-
throw new AbloValidationError('useCurrentUserId: no <AbloProvider> mounted above this component.', { code: 'no_ablo_provider' });
|
|
10
|
-
}
|
|
11
|
-
return context.currentUserId;
|
|
12
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useContext, useEffect, useRef } from 'react';
|
|
3
|
-
import { AbloInternalContext } from './internalContext.js';
|
|
4
|
-
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
5
|
-
/** Subscribe to provider-level errors without causing component re-renders. */
|
|
6
|
-
export function useErrorListener(listener) {
|
|
7
|
-
const context = useContext(AbloInternalContext);
|
|
8
|
-
if (!context) {
|
|
9
|
-
throw new AbloValidationError('useErrorListener: no <AbloProvider> mounted above this component.', { code: 'no_ablo_provider' });
|
|
10
|
-
}
|
|
11
|
-
const listenerRef = useRef(listener);
|
|
12
|
-
listenerRef.current = listener;
|
|
13
|
-
useEffect(() => context.subscribeError((error) => { listenerRef.current(error); }), [context]);
|
|
14
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { type QueuedMutation } from '../reactRuntime.js';
|
|
2
|
-
export interface MutationFailurePayload {
|
|
3
|
-
transaction: QueuedMutation;
|
|
4
|
-
error: Error;
|
|
5
|
-
permanent?: boolean;
|
|
6
|
-
}
|
|
7
|
-
/** Subscribe to optimistic mutation rollbacks and permanent write failures. */
|
|
8
|
-
export declare function useMutationFailureListener(listener: (payload: MutationFailurePayload) => void): void;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useContext, useEffect, useRef } from 'react';
|
|
3
|
-
import { AbloInternalContext, } from '../reactRuntime.js';
|
|
4
|
-
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
5
|
-
/** Subscribe to optimistic mutation rollbacks and permanent write failures. */
|
|
6
|
-
export function useMutationFailureListener(listener) {
|
|
7
|
-
const context = useContext(AbloInternalContext);
|
|
8
|
-
if (!context) {
|
|
9
|
-
throw new AbloValidationError('useMutationFailureListener: no <AbloProvider> mounted above this component.', { code: 'no_ablo_provider' });
|
|
10
|
-
}
|
|
11
|
-
const listenerRef = useRef(listener);
|
|
12
|
-
listenerRef.current = listener;
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const engine = context.engine;
|
|
15
|
-
if (!engine)
|
|
16
|
-
return;
|
|
17
|
-
return engine.onMutationFailure((payload) => { listenerRef.current(payload); });
|
|
18
|
-
}, [context, context.engine]);
|
|
19
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useCallback, useContext } from 'react';
|
|
3
|
-
import { SyncContext, } from './context.js';
|
|
4
|
-
import { AbloInternalContext } from './internalContext.js';
|
|
5
|
-
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
6
|
-
import { useReactive } from '../useReactive.js';
|
|
7
|
-
/** Reactively exposes the local store's connection and confirmation status. */
|
|
8
|
-
export function useSyncStatus() {
|
|
9
|
-
const provider = useContext(AbloInternalContext);
|
|
10
|
-
const sync = useContext(SyncContext);
|
|
11
|
-
// Status does not require authenticated row scope. The client exists before
|
|
12
|
-
// ready() resolves, including inside passthrough children and custom fallbacks.
|
|
13
|
-
const store = provider?.engine?._store ?? sync?.store;
|
|
14
|
-
if (!store) {
|
|
15
|
-
throw new AbloValidationError('Sync hooks must be used within an <AbloProvider>.', {
|
|
16
|
-
code: 'sync_context_missing_provider',
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
const compute = useCallback(() => deriveStatus(store), [store]);
|
|
20
|
-
return useReactive(compute, sameSnapshot);
|
|
21
|
-
}
|
|
22
|
-
function deriveStatus(store) {
|
|
23
|
-
const { state, progress, pendingChanges, isSessionError, error } = store.syncStatus;
|
|
24
|
-
if (isSessionError)
|
|
25
|
-
return { name: 'needs-auth' };
|
|
26
|
-
if (state === 'reconnecting')
|
|
27
|
-
return { name: 'reconnecting', reason: error?.message };
|
|
28
|
-
if (state === 'offline')
|
|
29
|
-
return { name: 'disconnected', reason: 'offline' };
|
|
30
|
-
if (state === 'error')
|
|
31
|
-
return { name: 'disconnected', reason: error?.message };
|
|
32
|
-
if (store.isReady)
|
|
33
|
-
return { name: 'connected', hasUnsyncedChanges: pendingChanges > 0 };
|
|
34
|
-
if (state === 'idle' || state === 'syncing')
|
|
35
|
-
return { name: 'connecting', progress };
|
|
36
|
-
return { name: 'initial' };
|
|
37
|
-
}
|
|
38
|
-
function sameSnapshot(a, b) {
|
|
39
|
-
if (a.name !== b.name)
|
|
40
|
-
return false;
|
|
41
|
-
if (a.name === 'initial' || a.name === 'needs-auth')
|
|
42
|
-
return true;
|
|
43
|
-
if (a.name === 'connecting')
|
|
44
|
-
return b.name === 'connecting' && a.progress === b.progress;
|
|
45
|
-
if (a.name === 'connected')
|
|
46
|
-
return b.name === 'connected' && a.hasUnsyncedChanges === b.hasUnsyncedChanges;
|
|
47
|
-
return (b.name === 'reconnecting' || b.name === 'disconnected') && a.reason === b.reason;
|
|
48
|
-
}
|
package/dist/useReactive.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscribe a React component to a MobX computation with a cached snapshot.
|
|
3
|
-
* This is the framework-level reactive primitive owned by @abloatai/humans;
|
|
4
|
-
* store/model hooks are layered above it.
|
|
5
|
-
*/
|
|
6
|
-
export declare function useReactive<T>(compute: () => T, equals?: (a: T, b: T) => boolean): T;
|