@abloatai/humans 0.64.0 → 0.64.2
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 +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/local/BaseSyncedStore.d.ts +5 -4
- package/dist/local/BaseSyncedStore.js +13 -15
- package/dist/local/client/createModelOperations.d.ts +3 -3
- package/dist/local/client/createModelOperations.js +1 -1
- package/dist/local/client/reactiveEngine.js +3 -3
- 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/sync/groupChange.d.ts +7 -4
- package/dist/local/sync/groupChange.js +7 -4
- package/dist/local/sync/scopeGroups.d.ts +4 -1
- package/dist/presence/index.d.ts +2 -2
- package/dist/presence/index.js +2 -2
- package/dist/react/AbloProvider.js +6 -6
- package/dist/react/context.d.ts +6 -45
- package/dist/react/context.js +8 -20
- package/dist/react/createAbloReact.d.ts +9 -22
- package/dist/react/createAbloReact.js +5 -7
- package/dist/react/internalContext.d.ts +0 -9
- package/dist/react/useAblo.d.ts +7 -54
- package/dist/react/useAblo.js +6 -28
- 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 +3 -3
- package/dist/react/useMutators.js +3 -3
- package/dist/react/usePresence.d.ts +9 -9
- package/dist/react/usePresence.js +3 -7
- package/dist/react/useUndoScope.d.ts +2 -2
- package/dist/react/useUndoScope.js +4 -4
- package/dist/react.d.ts +2 -0
- package/dist/react.js +2 -0
- package/dist/reactRuntime.d.ts +1 -1
- package/dist/reactRuntime.js +1 -1
- package/package.json +2 -2
- package/src/Ablo.ts +2 -0
- package/src/client.ts +1 -1
- package/src/index.ts +2 -0
- package/src/local/BaseSyncedStore.ts +13 -15
- package/src/local/client/createModelOperations.ts +4 -4
- package/src/local/client/reactiveEngine.ts +3 -3
- package/src/local/mutators/defineMutators.ts +3 -51
- package/src/local/storeAccess.ts +10 -0
- package/src/local/sync/groupChange.ts +7 -4
- package/src/local/sync/scopeGroups.ts +4 -1
- package/src/presence/index.ts +4 -3
- package/src/react/AbloProvider.tsx +8 -8
- package/src/react/context.ts +10 -61
- package/src/react/createAbloReact.ts +12 -40
- package/src/react/internalContext.ts +0 -9
- package/src/react/useAblo.ts +19 -89
- package/src/react/useAbloClient.ts +14 -0
- package/src/react/useMutationFailure.ts +17 -0
- package/src/react/useMutators.ts +6 -6
- package/src/react/usePresence.ts +18 -18
- package/src/react/useUndoScope.ts +6 -6
- package/src/react.ts +2 -0
- package/src/reactRuntime.ts +2 -2
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Handles the delta types that change which sync groups a session can see. A
|
|
3
|
-
* sync group
|
|
4
|
-
*
|
|
3
|
+
* sync group connects shared state to authorized participant subscriptions.
|
|
4
|
+
* This module owns the client side of membership changes and local-state
|
|
5
|
+
* rebuilding; it neither grants server authority nor configures per-group loading.
|
|
6
|
+
* When a session's membership changes, these handlers update
|
|
5
7
|
* the client's subscription list; when access is revoked, they clear cached
|
|
6
|
-
* data and
|
|
7
|
-
*
|
|
8
|
+
* managed data and request re-bootstrap. Clients without automatic bootstrap
|
|
9
|
+
* rely on covering deltas or explicit reads instead. This cannot retract copies
|
|
10
|
+
* retained outside the managed cache.
|
|
8
11
|
*
|
|
9
12
|
* Every handler takes a {@link GroupChangeContext}, the narrow facade through
|
|
10
13
|
* which it reaches the client's local storage and connection lifecycle hooks.
|
|
@@ -2,7 +2,10 @@ import type { ClaimTarget } from '@abloatai/transaction/types/streams';
|
|
|
2
2
|
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
3
3
|
import { scopeKindOf, type ModelDef } from '@abloatai/transaction/schema/model';
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Selects group interest using model records or explicit group names. Resolving
|
|
7
|
+
* a selector names a requested scope; the server still checks authority.
|
|
8
|
+
*/
|
|
6
9
|
export type GroupScope =
|
|
7
10
|
| ClaimTarget
|
|
8
11
|
| readonly ClaimTarget[]
|
package/src/presence/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type PresenceProjection,
|
|
5
5
|
type PresenceProjectionEvents,
|
|
6
6
|
type PresenceView,
|
|
7
|
+
type PresenceQueryOptions,
|
|
7
8
|
} from '@abloatai/transaction/presence';
|
|
8
9
|
import type { PresenceTarget } from '@abloatai/transaction/presence';
|
|
9
10
|
import {
|
|
@@ -16,7 +17,7 @@ type PresenceTransport = PresenceProjectionEvents & ReadActivityTransport;
|
|
|
16
17
|
|
|
17
18
|
/** Reactive-client presence backed by the client's existing live connection. */
|
|
18
19
|
export interface ReactivePresence extends PresenceView {
|
|
19
|
-
forModel(model: string, recordId?: string): ReturnType<PresenceProjection['forModel']>;
|
|
20
|
+
forModel(model: string, recordId?: string, options?: PresenceQueryOptions): ReturnType<PresenceProjection['forModel']>;
|
|
20
21
|
onChange(listener: () => void): () => void;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -87,9 +88,9 @@ export function createPresence(
|
|
|
87
88
|
lifetime.stop();
|
|
88
89
|
};
|
|
89
90
|
},
|
|
90
|
-
forModel(model, recordId) {
|
|
91
|
+
forModel(model, recordId, options) {
|
|
91
92
|
version.get();
|
|
92
|
-
return projection?.forModel(model, recordId) ?? [];
|
|
93
|
+
return projection?.forModel(model, recordId, options) ?? [];
|
|
93
94
|
},
|
|
94
95
|
dispose() {
|
|
95
96
|
for (const read of reads) read.dispose();
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from 'react';
|
|
12
12
|
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
13
13
|
import type { AbloClient as Ablo } from '../client.js';
|
|
14
|
-
import {
|
|
14
|
+
import { AbloStoreContext, type SyncStoreContract } from './context.js';
|
|
15
15
|
import { AbloInternalContext, type AbloInternalContextValue } from './internalContext.js';
|
|
16
16
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
17
17
|
import { useAblo } from './useAblo.js';
|
|
@@ -100,7 +100,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
100
100
|
// onSessionExpired. Credential cleanup lives in the CLIENT, so direct
|
|
101
101
|
// consumers and React consumers have the same security boundary.
|
|
102
102
|
// 2. Drive `ready()` (idempotent) so bootstrap starts on mount, then read the
|
|
103
|
-
// resolved org scope for
|
|
103
|
+
// resolved org scope for the Ablo store context.
|
|
104
104
|
// It does NOT dispose the client (consumer-owned) and does NOT touch auth.
|
|
105
105
|
useEffect(() => {
|
|
106
106
|
let stale = false;
|
|
@@ -158,12 +158,12 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
158
158
|
return () => { window.removeEventListener('beforeunload', handler); };
|
|
159
159
|
}, [engine, preventUnsavedChanges]);
|
|
160
160
|
|
|
161
|
-
// ──
|
|
161
|
+
// ── Store context value (for Ablo data hooks) ────────────────────
|
|
162
162
|
//
|
|
163
163
|
// The engine is always present (it's the `client` prop), but its org scope is
|
|
164
|
-
// unknown until `ready()` resolves identity — so
|
|
164
|
+
// unknown until `ready()` resolves identity — so the store context is null until
|
|
165
165
|
// then, which drives the initial fallback below.
|
|
166
|
-
const
|
|
166
|
+
const storeContextValue = useMemo(() => {
|
|
167
167
|
const currentAccountScope =
|
|
168
168
|
(resolvedScope?.engine === engine ? resolvedScope.account : null) ??
|
|
169
169
|
(engine._store as SyncStoreContract & { orgId?: string }).orgId;
|
|
@@ -189,15 +189,15 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
189
189
|
|
|
190
190
|
return (
|
|
191
191
|
<AbloInternalContext.Provider value={internalValue}>
|
|
192
|
-
<
|
|
192
|
+
<AbloStoreContext.Provider value={storeContextValue}>
|
|
193
193
|
{passthrough ? (
|
|
194
194
|
children
|
|
195
|
-
) :
|
|
195
|
+
) : storeContextValue ? (
|
|
196
196
|
<BootstrapGate key={engineKey} fallback={fallback}>
|
|
197
197
|
{children}
|
|
198
198
|
</BootstrapGate>
|
|
199
199
|
) : fallback}
|
|
200
|
-
</
|
|
200
|
+
</AbloStoreContext.Provider>
|
|
201
201
|
</AbloInternalContext.Provider>
|
|
202
202
|
);
|
|
203
203
|
}
|
package/src/react/context.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext,
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
4
|
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
5
5
|
import type { SyncStoreContract } from '../local/storeContract.js';
|
|
6
6
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
@@ -13,77 +13,26 @@ export type {
|
|
|
13
13
|
LocalMutation,
|
|
14
14
|
} from '../local/storeContract.js';
|
|
15
15
|
|
|
16
|
-
export interface
|
|
16
|
+
export interface AbloStoreContextValue {
|
|
17
17
|
store: SyncStoreContract;
|
|
18
18
|
/** The organization id used as the default scope for reads and writes. */
|
|
19
19
|
organizationId: string;
|
|
20
|
-
/**
|
|
21
|
-
* An optional schema. When provided, hooks that take a model by name (such as
|
|
22
|
-
* `useQuery('items')`) read that model's metadata from this schema, so
|
|
23
|
-
* callers don't pass a schema at every call site. When omitted, those hooks
|
|
24
|
-
* require the schema as an argument instead.
|
|
25
|
-
*
|
|
26
|
-
* The field is loosely typed here because a single runtime context value is
|
|
27
|
-
* shared by every hook. Precise per-model types come from your `Register`
|
|
28
|
-
* module augmentation
|
|
29
|
-
* (`declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`),
|
|
30
|
-
* not from this reference.
|
|
31
|
-
*/
|
|
20
|
+
/** Runtime schema used by ambient mutator overloads. */
|
|
32
21
|
schema?: Schema;
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
export const
|
|
24
|
+
export const AbloStoreContext = createContext<AbloStoreContextValue | null>(null);
|
|
36
25
|
|
|
37
26
|
/**
|
|
38
|
-
* Reads the
|
|
39
|
-
*
|
|
40
|
-
* context by rendering the internal {@link SyncProvider}; you wire
|
|
41
|
-
* `<AbloProvider client={ablo}>` rather than touching this directly.
|
|
27
|
+
* Reads the store scope owned by `<AbloProvider>`, throwing a clear error when
|
|
28
|
+
* no provider is mounted above it.
|
|
42
29
|
*/
|
|
43
|
-
export function
|
|
44
|
-
const ctx = useContext(
|
|
30
|
+
export function useAbloStoreContext(): AbloStoreContextValue {
|
|
31
|
+
const ctx = useContext(AbloStoreContext);
|
|
45
32
|
if (!ctx) {
|
|
46
|
-
throw new AbloValidationError('
|
|
47
|
-
code: '
|
|
33
|
+
throw new AbloValidationError('Ablo hooks must be used within an <AbloProvider>.', {
|
|
34
|
+
code: 'ablo_context_missing_provider',
|
|
48
35
|
});
|
|
49
36
|
}
|
|
50
37
|
return ctx;
|
|
51
38
|
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Props for SyncProvider.
|
|
55
|
-
*/
|
|
56
|
-
export interface SyncProviderProps {
|
|
57
|
-
/** The sync store, which must implement {@link SyncStoreContract}. */
|
|
58
|
-
store: SyncStoreContract;
|
|
59
|
-
/** The organization id used as the default scope for reads and writes. */
|
|
60
|
-
organizationId: string;
|
|
61
|
-
/**
|
|
62
|
-
* An optional schema. Provide it to enable hooks that take a model by name
|
|
63
|
-
* (such as `useQuery('items')`); the model types also narrow through your
|
|
64
|
-
* `Register` augmentation. Omit it to pass the schema to those hooks directly
|
|
65
|
-
* instead.
|
|
66
|
-
*/
|
|
67
|
-
schema?: Schema;
|
|
68
|
-
children?: ReactNode;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A low-level provider that places a built sync store on React context so the
|
|
73
|
-
* data hooks can reach it. This is an internal building block: it is not part
|
|
74
|
-
* of the package's public entry point. Reach for `<AbloProvider>` instead,
|
|
75
|
-
* which builds the store from your `Ablo({ schema, apiKey })` client and
|
|
76
|
-
* renders this provider underneath.
|
|
77
|
-
*/
|
|
78
|
-
export function SyncProvider({
|
|
79
|
-
store,
|
|
80
|
-
organizationId,
|
|
81
|
-
schema,
|
|
82
|
-
children,
|
|
83
|
-
}: SyncProviderProps) {
|
|
84
|
-
return createElement(
|
|
85
|
-
SyncContext.Provider,
|
|
86
|
-
{ value: { store, organizationId, schema } },
|
|
87
|
-
children
|
|
88
|
-
);
|
|
89
|
-
}
|
|
@@ -1,47 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Capture schema inference once while reusing module-level React functions.
|
|
5
|
-
* This helper creates no components, hooks, contexts or client instances.
|
|
6
|
-
*
|
|
7
|
-
* Define the app binding at module scope:
|
|
8
|
-
* `export const { AbloProvider, useAblo, usePresence } = createAbloReact(schema)`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
3
|
import type { ReactElement } from 'react';
|
|
4
|
+
import { useAbloClient } from './useAbloClient.js';
|
|
5
|
+
import { useMutationFailure } from './useMutationFailure.js';
|
|
12
6
|
import { AbloProvider } from './AbloProvider.js';
|
|
13
|
-
import {
|
|
14
|
-
useAblo,
|
|
15
|
-
type AbloSelector,
|
|
16
|
-
type ModelClientSelector,
|
|
17
|
-
} from './useAblo.js';
|
|
7
|
+
import { useAblo } from './useAblo.js';
|
|
18
8
|
import type { AbloClient as Ablo } from '../client.js';
|
|
19
|
-
import type { ModelOperations } from '../local/client/createModelOperations.js';
|
|
20
9
|
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
21
|
-
import { usePresence
|
|
22
|
-
import type { PresenceSession } from '@abloatai/transaction/presence';
|
|
10
|
+
import { usePresence } from './usePresence.js';
|
|
23
11
|
|
|
24
|
-
/**
|
|
12
|
+
/** Shared provider and hooks specialized to one schema. */
|
|
25
13
|
export interface AbloReactBinding<S extends SchemaRecord> {
|
|
26
|
-
/** `AbloProvider` with its `client` prop typed `Ablo<S>` — same component,
|
|
27
|
-
* no per-app generics. */
|
|
28
14
|
AbloProvider: (props: AbloProvider.Props<S>) => ReactElement;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<T>(select: AbloSelector<S, T>): T | undefined;
|
|
34
|
-
<T, C>(
|
|
35
|
-
modelClientOrSelect: ModelOperations<T, C> | ModelClientSelector<S, T, C>,
|
|
36
|
-
id: string,
|
|
37
|
-
options?: useAblo.Options<T>,
|
|
38
|
-
): useAblo.Result<T>;
|
|
39
|
-
};
|
|
15
|
+
useAblo: useAblo.Bound<S>;
|
|
16
|
+
/** Writable client for actions; useAblo(selector) supplies render snapshots. */
|
|
17
|
+
useAbloClient: () => Ablo<S> | null;
|
|
18
|
+
useMutationFailure: typeof useMutationFailure;
|
|
40
19
|
/** Declare and reactively read record presence with the same model clients. */
|
|
41
|
-
usePresence: <
|
|
42
|
-
modelOrSelect: ModelOperations<T, C> | PresenceModelSelector<S, T, C>,
|
|
43
|
-
recordId: string,
|
|
44
|
-
) => readonly PresenceSession[];
|
|
20
|
+
usePresence: usePresence.Bound<S>;
|
|
45
21
|
}
|
|
46
22
|
|
|
47
23
|
/** Bind the existing React functions to one schema's types. */
|
|
@@ -50,10 +26,6 @@ export function createAbloReact<S extends SchemaRecord>(
|
|
|
50
26
|
): AbloReactBinding<S> {
|
|
51
27
|
void schema;
|
|
52
28
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
// type tests verify the specialization; no runtime value changes.
|
|
56
|
-
// Specialize types only. Every binding uses the same module-level functions,
|
|
57
|
-
// so calling this helper again cannot change component identity or reset state.
|
|
58
|
-
return { AbloProvider, useAblo, usePresence } as AbloReactBinding<S>;
|
|
29
|
+
// Specialize the shared functions without creating new contexts or identities.
|
|
30
|
+
return { AbloProvider, useAblo, useAbloClient, useMutationFailure, usePresence } as AbloReactBinding<S>;
|
|
59
31
|
}
|
|
@@ -6,15 +6,6 @@ import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
|
6
6
|
|
|
7
7
|
/** The provider owns only the reference to the application-owned client. */
|
|
8
8
|
export interface AbloInternalContextValue {
|
|
9
|
-
/**
|
|
10
|
-
* The typed `Ablo` client for this provider, available before bootstrap resolves. It is held here so `useAblo()` can return it without
|
|
11
|
-
* reaching into the store; the client and the store are sibling objects, and
|
|
12
|
-
* neither is derived from the other.
|
|
13
|
-
*
|
|
14
|
-
* It is typed loosely as `Ablo<SchemaRecord>` because generics do not flow
|
|
15
|
-
* through React context. `useAblo<R>()` restores the precise type through its
|
|
16
|
-
* own generic; the runtime value is the fully typed client.
|
|
17
|
-
*/
|
|
18
9
|
engine: Ablo<SchemaRecord> | null;
|
|
19
10
|
}
|
|
20
11
|
|
package/src/react/useAblo.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback,
|
|
4
|
-
import {
|
|
3
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
4
|
+
import { useAbloClient } from './useAbloClient.js';
|
|
5
5
|
import type { AbloClient as Ablo, AbloReads } from '../client.js';
|
|
6
6
|
import type { ModelClaim } from '@abloatai/transaction/coordination';
|
|
7
7
|
import {
|
|
@@ -9,48 +9,16 @@ import {
|
|
|
9
9
|
type ModelOperations,
|
|
10
10
|
} from '../local/client/createModelOperations.js';
|
|
11
11
|
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
12
|
-
import type {
|
|
12
|
+
import type { ResolveModels as DefaultModels } from '@abloatai/transaction/types/global';
|
|
13
13
|
import { useReactive } from './useReactive.js';
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* The app's resolved schema-record type. It reads your `Register` module
|
|
17
|
-
* augmentation when you declare one and falls back to the loose
|
|
18
|
-
* {@link SchemaRecord} otherwise, so `useAblo()` returns a fully typed client
|
|
19
|
-
* without you passing `<(typeof schema)['models']>` at every call site.
|
|
20
|
-
*/
|
|
21
|
-
type DefaultModels = ResolveSchema extends { models: infer M }
|
|
22
|
-
? M extends SchemaRecord
|
|
23
|
-
? M
|
|
24
|
-
: SchemaRecord
|
|
25
|
-
: SchemaRecord;
|
|
26
|
-
|
|
27
15
|
const EMPTY_CLAIMS: readonly ModelClaim[] = Object.freeze([]);
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
* Restore the caller's schema generics on the context-held engine. React
|
|
31
|
-
* context erases generics (see `AbloInternalContextValue.engine`), so this is
|
|
32
|
-
* the one deliberate rebind point: the runtime value is the fully typed
|
|
33
|
-
* client, and `R` is the compile-time view the calling hook declared.
|
|
34
|
-
*/
|
|
35
|
-
function rebindEngine<R extends SchemaRecord>(engine: Ablo<SchemaRecord>): Ablo<R> {
|
|
36
|
-
return engine as Ablo<R>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The reactive-read view of a client — the identical runtime object, with
|
|
41
|
-
* model reads typed as snapshot rows, because everything a selector returns
|
|
42
|
-
* is converted through `snapshotValue` before the hook hands it back. Same
|
|
43
|
-
* generic in and out, so this compiles with no schema rebinding.
|
|
44
|
-
*/
|
|
17
|
+
// Selector results are detached snapshots with no model methods or relations.
|
|
45
18
|
function reactiveReads<R extends SchemaRecord>(engine: Ablo<R>): AbloReads<R> {
|
|
46
19
|
return engine as AbloReads<R>;
|
|
47
20
|
}
|
|
48
21
|
|
|
49
|
-
// Selectors receive the reactive-read client: model reads are typed as
|
|
50
|
-
// snapshot rows (data fields + computeds, no relation accessors), which is the
|
|
51
|
-
// shape the hook actually returns after `toReactiveSnapshot()`. This makes the
|
|
52
|
-
// selector's inferred result type honest — `row.layers` fails to compile here
|
|
53
|
-
// instead of reading `undefined` at runtime.
|
|
54
22
|
export type ModelClientSelector<R extends SchemaRecord, T, C> =
|
|
55
23
|
(ablo: AbloReads<R>) => ModelOperations<T, C>;
|
|
56
24
|
export type AbloSelector<R extends SchemaRecord, T> = (ablo: AbloReads<R>) => T;
|
|
@@ -74,46 +42,7 @@ function readModelResult<R extends SchemaRecord, T, C>(
|
|
|
74
42
|
return { data, claims, claimed: claims.length > 0 };
|
|
75
43
|
}
|
|
76
44
|
|
|
77
|
-
/**
|
|
78
|
-
* Reads Ablo from inside an `<AbloProvider>` subtree. Called with no arguments
|
|
79
|
-
* it returns the typed client for use in callbacks and effects; called with a
|
|
80
|
-
* selector it subscribes the component to a reactive read — such as one
|
|
81
|
-
* `ablo.<model>` row — and re-renders when that read changes.
|
|
82
|
-
*
|
|
83
|
-
* You can call it with no type arguments once you declare the `Register` module
|
|
84
|
-
* augmentation (`declare module '@abloatai/ablo' { interface Register {
|
|
85
|
-
* Schema: typeof schema } }`); the default type then resolves through your
|
|
86
|
-
* schema's models, so call sites stay clean:
|
|
87
|
-
*
|
|
88
|
-
* **Prefer the binding.** `createAbloReact(schema)` captures the schema once
|
|
89
|
-
* in your app's binding file and returns a `useAblo` that needs none of the
|
|
90
|
-
* typing arrangements below — no type argument, no `Register` declaration
|
|
91
|
-
* (see `react.md`). Passing an explicit schema type argument to THIS hook is
|
|
92
|
-
* deprecated in favor of that binding; it keeps working for shared packages
|
|
93
|
-
* that cannot bind a concrete schema.
|
|
94
|
-
*
|
|
95
|
-
* ```ts
|
|
96
|
-
* // With the Register augmentation (recommended):
|
|
97
|
-
* const ablo = useAblo();
|
|
98
|
-
* if (!ablo) return <Loading />;
|
|
99
|
-
* const doc = await ablo.records.get({ id }); // observational async server read
|
|
100
|
-
*
|
|
101
|
-
* // Reactive selector (a synchronous local snapshot). The selector's reads
|
|
102
|
-
* // are typed as snapshot rows — data fields + computeds, no relation
|
|
103
|
-
* // accessors — matching what the hook actually returns:
|
|
104
|
-
* const doc = useAblo((ablo) => ablo.records.local.get(id)) ?? serverDoc;
|
|
105
|
-
* const { claimed } = useAblo((ablo) => ablo.records, id);
|
|
106
|
-
*
|
|
107
|
-
* // Without the augmentation, pass the schema as a type argument:
|
|
108
|
-
* const ablo = useAblo<(typeof schema)['models']>();
|
|
109
|
-
* ```
|
|
110
|
-
*
|
|
111
|
-
* The client and its status are available during provider startup. Select
|
|
112
|
-
* `ablo.status` to display connection state; await `ablo.ready()` before
|
|
113
|
-
* operations that require an initialized client. Without a provider, the
|
|
114
|
-
* no-argument form returns `null` and selectors return `undefined`.
|
|
115
|
-
*/
|
|
116
|
-
export function useAblo<R extends SchemaRecord = DefaultModels>(): Ablo<R> | null;
|
|
45
|
+
/** Select a reactive snapshot or read a row with its current claims. */
|
|
117
46
|
export function useAblo<
|
|
118
47
|
R extends SchemaRecord = DefaultModels,
|
|
119
48
|
T = unknown,
|
|
@@ -139,10 +68,10 @@ export function useAblo<
|
|
|
139
68
|
T = Record<string, unknown>,
|
|
140
69
|
C = unknown,
|
|
141
70
|
>(
|
|
142
|
-
modelOrSelect
|
|
71
|
+
modelOrSelect: ModelOperations<T, C> | ModelClientSelector<R, T, C> | AbloSelector<R, T>,
|
|
143
72
|
id?: string,
|
|
144
73
|
options?: useAblo.Options<T>,
|
|
145
|
-
):
|
|
74
|
+
): useAblo.Result<T> | T | undefined {
|
|
146
75
|
const engine = useAbloClient<R>();
|
|
147
76
|
const initial = options?.initial;
|
|
148
77
|
const isSelectorOnly = typeof modelOrSelect === 'function' && id === undefined;
|
|
@@ -161,11 +90,10 @@ export function useAblo<
|
|
|
161
90
|
// These dependencies define when the seed belongs to a different row.
|
|
162
91
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
92
|
const seed = useMemo(() => ({ received: false }), [engine, modelClient, id]);
|
|
164
|
-
const reading = modelOrSelect !== undefined;
|
|
165
93
|
const subscribe = useCallback((notify: () => void) => {
|
|
166
|
-
if (!engine
|
|
94
|
+
if (!engine) return () => undefined;
|
|
167
95
|
return engine.claims.onChange(notify);
|
|
168
|
-
}, [engine
|
|
96
|
+
}, [engine]);
|
|
169
97
|
const value = useReactive<T | useAblo.Result<T> | undefined>(() => {
|
|
170
98
|
if (isSelectorOnly && typeof modelOrSelect === 'function') {
|
|
171
99
|
return engine ? modelOrSelect(reactiveReads<R>(engine)) as T : undefined;
|
|
@@ -186,19 +114,21 @@ export function useAblo<
|
|
|
186
114
|
if (id !== undefined && modelClient?.local.get(id) !== undefined) seed.received = true;
|
|
187
115
|
}, [seed, modelClient, id, value]);
|
|
188
116
|
|
|
189
|
-
|
|
190
|
-
return engine;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** @internal Resolve the nearest provider's client through one schema rebind. */
|
|
194
|
-
export function useAbloClient<R extends SchemaRecord>(): Ablo<R> | null {
|
|
195
|
-
const ctx = useContext(AbloInternalContext);
|
|
196
|
-
return ctx?.engine ? rebindEngine<R>(ctx.engine) : null;
|
|
117
|
+
return value;
|
|
197
118
|
}
|
|
198
119
|
|
|
199
120
|
/** Type annotations belong to the operation; most callers rely on inference. */
|
|
200
121
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
201
122
|
export namespace useAblo {
|
|
123
|
+
export interface Bound<S extends SchemaRecord> {
|
|
124
|
+
<T>(select: AbloSelector<S, T>): T | undefined;
|
|
125
|
+
<T, C>(
|
|
126
|
+
model: ModelOperations<T, C> | ModelClientSelector<S, T, C>,
|
|
127
|
+
id: string,
|
|
128
|
+
options?: Options<T>,
|
|
129
|
+
): Result<T>;
|
|
130
|
+
}
|
|
131
|
+
|
|
202
132
|
export interface Options<T> {
|
|
203
133
|
/**
|
|
204
134
|
* An initial row, usually from a server component or a route loader. The hook
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useContext } from 'react';
|
|
4
|
+
import type { AbloClient } from '../client.js';
|
|
5
|
+
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
6
|
+
import type { ResolveModels } from '@abloatai/transaction/types/global';
|
|
7
|
+
import { AbloInternalContext } from './internalContext.js';
|
|
8
|
+
|
|
9
|
+
/** Writable client for event handlers. Available before ready(); null without a provider. */
|
|
10
|
+
export function useAbloClient<S extends SchemaRecord = ResolveModels>(): AbloClient<S> | null {
|
|
11
|
+
const client = useContext(AbloInternalContext)?.engine;
|
|
12
|
+
// React context erases the schema; the application binding restores it.
|
|
13
|
+
return client ? client as AbloClient<S> : null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useEffectEvent } from 'react';
|
|
4
|
+
import type { AbloClient } from '../client.js';
|
|
5
|
+
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
6
|
+
import { useAbloClient } from './useAbloClient.js';
|
|
7
|
+
|
|
8
|
+
/** Subscribe for this component's lifetime using the latest committed listener.
|
|
9
|
+
* Replacing the provider client moves the subscription; unmount removes it.
|
|
10
|
+
*/
|
|
11
|
+
export function useMutationFailure(
|
|
12
|
+
listener: Parameters<AbloClient<SchemaRecord>['onMutationFailure']>[0],
|
|
13
|
+
): void {
|
|
14
|
+
const client = useAbloClient();
|
|
15
|
+
const onFailure = useEffectEvent(listener);
|
|
16
|
+
useEffect(() => client?.onMutationFailure(onFailure), [client]);
|
|
17
|
+
}
|
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
|
|
|
@@ -58,12 +58,12 @@ export function useMutators<S extends Schema, M extends MutatorDefs<S>>(
|
|
|
58
58
|
): useMutators.Result<M>;
|
|
59
59
|
|
|
60
60
|
/** Mutator invokers via the `Register` module augmentation. Schema comes
|
|
61
|
-
* from the `
|
|
61
|
+
* from the `AbloProvider` store context; the mutator tree is typed against
|
|
62
62
|
* `ResolveSchema` at the call site. */
|
|
63
63
|
export function useMutators<
|
|
64
64
|
M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>,
|
|
65
65
|
>(
|
|
66
|
-
mutators: M
|
|
66
|
+
mutators: RequireRegisteredSchema<M>,
|
|
67
67
|
options?: useMutators.Options<ResolveSchema extends Schema ? ResolveSchema : Schema>,
|
|
68
68
|
): useMutators.Result<M>;
|
|
69
69
|
|
|
@@ -72,7 +72,7 @@ export function useMutators(
|
|
|
72
72
|
mutatorsOrOptions?: MutatorDefs<Schema> | useMutators.Options<Schema>,
|
|
73
73
|
maybeOptions?: useMutators.Options<Schema>,
|
|
74
74
|
): useMutators.Result<MutatorDefs<Schema>> {
|
|
75
|
-
const { store, organizationId, schema: ctxSchema } =
|
|
75
|
+
const { store, organizationId, schema: ctxSchema } = useAbloStoreContext();
|
|
76
76
|
|
|
77
77
|
// Disambiguate: explicit-schema path has the schema object in first slot;
|
|
78
78
|
// the global-resolved path has the mutator tree there. A schema object
|
|
@@ -92,7 +92,7 @@ export function useMutators(
|
|
|
92
92
|
throw new AbloValidationError(
|
|
93
93
|
'useMutators: no schema available. Pass the schema as the first arg, ' +
|
|
94
94
|
'or build the <AbloProvider> above with `Ablo({ schema })` so the ' +
|
|
95
|
-
'
|
|
95
|
+
'schema-free overload can read it from context.',
|
|
96
96
|
{ code: 'mutators_schema_missing' },
|
|
97
97
|
);
|
|
98
98
|
}
|
package/src/react/usePresence.ts
CHANGED
|
@@ -9,16 +9,10 @@ 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 { useAbloClient } from './
|
|
12
|
+
import type { ResolveModels as DefaultModels } from '@abloatai/transaction/types/global';
|
|
13
|
+
import { useAbloClient } from './useAbloClient.js';
|
|
14
14
|
import { useReactive } from './useReactive.js';
|
|
15
15
|
|
|
16
|
-
type DefaultModels = ResolveSchema extends { models: infer M }
|
|
17
|
-
? M extends SchemaRecord
|
|
18
|
-
? M
|
|
19
|
-
: SchemaRecord
|
|
20
|
-
: SchemaRecord;
|
|
21
|
-
|
|
22
16
|
export type PresenceModelSelector<R extends SchemaRecord, T, C> =
|
|
23
17
|
(ablo: Ablo<R>) => ModelOperations<T, C>;
|
|
24
18
|
|
|
@@ -30,6 +24,7 @@ export type PresenceModelSelector<R extends SchemaRecord, T, C> =
|
|
|
30
24
|
export function usePresence<T, C>(
|
|
31
25
|
modelClient: ModelOperations<T, C>,
|
|
32
26
|
recordId: string,
|
|
27
|
+
options?: usePresence.Options,
|
|
33
28
|
): readonly PresenceSession[];
|
|
34
29
|
export function usePresence<
|
|
35
30
|
R extends SchemaRecord = DefaultModels,
|
|
@@ -38,6 +33,7 @@ export function usePresence<
|
|
|
38
33
|
>(
|
|
39
34
|
select: PresenceModelSelector<R, T, C>,
|
|
40
35
|
recordId: string,
|
|
36
|
+
options?: usePresence.Options,
|
|
41
37
|
): readonly PresenceSession[];
|
|
42
38
|
export function usePresence<
|
|
43
39
|
R extends SchemaRecord = DefaultModels,
|
|
@@ -46,17 +42,9 @@ export function usePresence<
|
|
|
46
42
|
>(
|
|
47
43
|
modelOrSelect: ModelOperations<T, C> | PresenceModelSelector<R, T, C>,
|
|
48
44
|
recordId: string,
|
|
45
|
+
options?: usePresence.Options,
|
|
49
46
|
): readonly PresenceSession[] {
|
|
50
47
|
const engine = useAbloClient<R>();
|
|
51
|
-
return usePresenceImpl(engine, modelOrSelect, recordId);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** @internal Shared by the global hook and schema-bound React factory. */
|
|
55
|
-
export function usePresenceImpl<R extends SchemaRecord, T, C>(
|
|
56
|
-
engine: Ablo<R> | null,
|
|
57
|
-
modelOrSelect: ModelOperations<T, C> | PresenceModelSelector<R, T, C>,
|
|
58
|
-
recordId: string,
|
|
59
|
-
): readonly PresenceSession[] {
|
|
60
48
|
if (recordId.length === 0) {
|
|
61
49
|
throw new AbloValidationError(
|
|
62
50
|
'usePresence requires a non-empty record id.',
|
|
@@ -77,7 +65,19 @@ export function usePresenceImpl<R extends SchemaRecord, T, C>(
|
|
|
77
65
|
}
|
|
78
66
|
|
|
79
67
|
const subscribe = useCallback((notify: () => void) => presence?.subscribe(notify) ?? (() => undefined), [presence]);
|
|
80
|
-
const sessions = useReactive(() => presence?.get(recordId) ?? [], { subscribe });
|
|
68
|
+
const sessions = useReactive(() => presence?.get(recordId, options) ?? [], { subscribe });
|
|
81
69
|
useEffect(() => presence?.read(recordId), [presence, recordId]);
|
|
82
70
|
return sessions;
|
|
83
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
|
+
}
|
|
81
|
+
|
|
82
|
+
export type Options = import('@abloatai/transaction/presence').PresenceQueryOptions;
|
|
83
|
+
}
|
|
@@ -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
|
/**
|
|
@@ -30,7 +30,7 @@ import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
// Module-level weak registry: `SyncStoreContract` → `UndoManager`.
|
|
33
|
-
// A single
|
|
33
|
+
// A single AbloProvider shares one manager across
|
|
34
34
|
// every useUndoScope call, so scopes with the same name are identity-equal.
|
|
35
35
|
// The hook implementation already operates on the runtime-wide `Schema` type;
|
|
36
36
|
// its overloads restore the caller's precise schema type at the public boundary,
|
|
@@ -59,7 +59,7 @@ export function useUndoScope<S extends Schema>(
|
|
|
59
59
|
|
|
60
60
|
/** Per-surface undo/redo via the `Register` module augmentation. */
|
|
61
61
|
export function useUndoScope(
|
|
62
|
-
name: string
|
|
62
|
+
name: RequireRegisteredSchema<string>,
|
|
63
63
|
options?: UndoScopeOptions,
|
|
64
64
|
): useUndoScope.Result<ResolveSchema extends Schema ? ResolveSchema : Schema>;
|
|
65
65
|
|
|
@@ -68,7 +68,7 @@ export function useUndoScope(
|
|
|
68
68
|
nameOrOptions?: string | UndoScopeOptions,
|
|
69
69
|
maybeOptions?: UndoScopeOptions,
|
|
70
70
|
): useUndoScope.Result<Schema> {
|
|
71
|
-
const { store, organizationId, schema: ctxSchema } =
|
|
71
|
+
const { store, organizationId, schema: ctxSchema } = useAbloStoreContext();
|
|
72
72
|
|
|
73
73
|
const isExplicit = typeof schemaOrName !== 'string';
|
|
74
74
|
const schema = isExplicit ? (schemaOrName) : ctxSchema;
|
|
@@ -85,7 +85,7 @@ export function useUndoScope(
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const scope = useMemo(() => {
|
|
88
|
-
// Store is the identity for the manager — one per
|
|
88
|
+
// Store is the identity for the manager — one per AbloProvider.
|
|
89
89
|
const manager = getManager(store, () => new UndoManager(schema, store, organizationId));
|
|
90
90
|
return manager.getScope(name, options);
|
|
91
91
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|