@abloatai/ablo 0.3.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/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ModelScope } from '../types/index.js';
|
|
2
|
+
import type { Schema } from '../schema/schema.js';
|
|
3
|
+
import type { InferModel } from '../schema/schema.js';
|
|
4
|
+
import type { ResolveSchema } from '../types/global.js';
|
|
5
|
+
/** Narrow model-key union for the zero-arg overload. */
|
|
6
|
+
type GlobalModelKey = ResolveSchema extends {
|
|
7
|
+
models: infer M;
|
|
8
|
+
} ? keyof M & string : string;
|
|
9
|
+
/** Typed entity shape for a given model key. Falls back to a loose shape
|
|
10
|
+
* when the resolved schema doesn't extend the full `Schema` contract
|
|
11
|
+
* (i.e., no global augmentation present). */
|
|
12
|
+
type GlobalEntity<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] ? InferModel<ResolveSchema, K> : Record<string, unknown> : Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Compatibility query hook for entity collections.
|
|
15
|
+
*
|
|
16
|
+
* Prefer selector reads for new integrations:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const tasks = useAblo((ablo) =>
|
|
20
|
+
* ablo.tasks.list({ where: { status: 'todo' } }),
|
|
21
|
+
* );
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* This hook remains for older string-keyed integrations.
|
|
25
|
+
*
|
|
26
|
+
* **Typed overload:**
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { schema } from '@/sync/schema';
|
|
29
|
+
* const chats = useQuery(schema, 'chats', { where: { userId } });
|
|
30
|
+
* // chats is fully typed: Chat[] with displayTitle, icon, color, etc.
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* **Untyped overload (legacy):**
|
|
34
|
+
* ```ts
|
|
35
|
+
* const chats = useQuery('Chat');
|
|
36
|
+
* // chats is Record<string, unknown>[]
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export interface QueryOptions<T = Record<string, unknown>> {
|
|
40
|
+
/** Declarative field-level filter. Shallow match: all specified fields must match. */
|
|
41
|
+
where?: Partial<T>;
|
|
42
|
+
/** Arbitrary predicate function for complex logic. Applied AFTER where. */
|
|
43
|
+
filter?: (entity: T) => boolean;
|
|
44
|
+
/** Sort field name. */
|
|
45
|
+
orderBy?: keyof T & string;
|
|
46
|
+
/** Sort direction. Default: 'asc'. */
|
|
47
|
+
order?: 'asc' | 'desc';
|
|
48
|
+
/** Max results. */
|
|
49
|
+
limit?: number;
|
|
50
|
+
/** Skip N results (pagination). */
|
|
51
|
+
offset?: number;
|
|
52
|
+
/** Filter by model scope (live, archived, all). Default: live. */
|
|
53
|
+
scope?: ModelScope;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Typed query (explicit schema arg).
|
|
57
|
+
*
|
|
58
|
+
* @deprecated Prefer `useAblo((ablo) => ablo.<model>.list(options))` for new
|
|
59
|
+
* integrations. This overload remains for compatibility with older
|
|
60
|
+
* string-keyed React code.
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* const tasks = useQuery(schema, 'tasks', { where: { status: 'todo' } });
|
|
64
|
+
* // tasks: Task[] — fully typed from Zod shape + computed getters
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function useQuery<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, options?: QueryOptions<InferModel<S, K>>): InferModel<S, K>[];
|
|
68
|
+
/**
|
|
69
|
+
* Typed query (global-augmented): pass just the model key. Resolves
|
|
70
|
+
* the schema from the `AbloSync` global augmentation the consumer
|
|
71
|
+
* declared in a `.d.ts`. No `schema` arg at the call site — this is
|
|
72
|
+
* the Liveblocks-style ergonomic path.
|
|
73
|
+
*
|
|
74
|
+
* @deprecated Prefer `useAblo((ablo) => ablo.<model>.list(options))` for new
|
|
75
|
+
* integrations. This overload remains for compatibility with older
|
|
76
|
+
* string-keyed React code.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* // apps/your-app/src/ablo-sync.d.ts
|
|
80
|
+
* declare global { interface AbloSync { Schema: typeof schema } }
|
|
81
|
+
*
|
|
82
|
+
* // any component
|
|
83
|
+
* const tasks = useQuery('tasks', { where: { status: 'todo' } });
|
|
84
|
+
* // tasks: Task[] — typed via the declared global
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* When no global augmentation exists, `GlobalEntity` falls back to
|
|
88
|
+
* `Record<string, unknown>` — same ergonomics as the legacy untyped
|
|
89
|
+
* overload, with the key still validated against the resolved schema's
|
|
90
|
+
* model keys when that schema is declared.
|
|
91
|
+
*/
|
|
92
|
+
export declare function useQuery<K extends GlobalModelKey>(modelKey: K, options?: QueryOptions<GlobalEntity<K>>): GlobalEntity<K>[];
|
|
93
|
+
/** @deprecated Prefer selector reads through `useAblo`. */
|
|
94
|
+
export declare function useQuery<T = Record<string, unknown>>(typename: string, options?: QueryOptions<T>): T[];
|
|
95
|
+
/**
|
|
96
|
+
* Compatibility single-entity lookup. Prefer selector reads:
|
|
97
|
+
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* const task = useAblo((ablo) => ablo.tasks.retrieve(taskId));
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* // Typed
|
|
104
|
+
* const task = useOne(schema, 'tasks', taskId);
|
|
105
|
+
*
|
|
106
|
+
* // Untyped (legacy)
|
|
107
|
+
* const task = useOne(taskId);
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
/** @deprecated Prefer `useAblo((ablo) => ablo.<model>.retrieve(id))`. */
|
|
111
|
+
export declare function useOne<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, id?: string): InferModel<S, K> | undefined;
|
|
112
|
+
/** Typed single-entity lookup via the `AbloSync` global augmentation.
|
|
113
|
+
*
|
|
114
|
+
* @deprecated Prefer `useAblo((ablo) => ablo.<model>.retrieve(id))`.
|
|
115
|
+
*
|
|
116
|
+
* The pool `.get(id)` call doesn't actually need the typename at runtime
|
|
117
|
+
* — the return is already keyed by id globally — so the model key serves
|
|
118
|
+
* as a compile-time narrowing hint for consumers who want the specific
|
|
119
|
+
* entity type at the call site. */
|
|
120
|
+
export declare function useOne<K extends GlobalModelKey>(modelKey: K, id?: string): GlobalEntity<K> | undefined;
|
|
121
|
+
/** @deprecated Prefer `useAblo((ablo) => ablo.<model>.retrieve(id))`. */
|
|
122
|
+
export declare function useOne<T = Record<string, unknown>>(id?: string): T | undefined;
|
|
123
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo, useEffect, useRef, useCallback } from 'react';
|
|
3
|
+
import { useSyncContext } from './context.js';
|
|
4
|
+
import { useReactive } from './useReactive.js';
|
|
5
|
+
// ── Stable key helper ───────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Produce a stable string key for QueryOptions so useMemo only recreates
|
|
8
|
+
* the view when the logical query changes.
|
|
9
|
+
*
|
|
10
|
+
* - `where` is serialized via JSON.stringify (deterministic for simple values).
|
|
11
|
+
* - `filter` is a function — we track its reference identity.
|
|
12
|
+
* - Primitives (orderBy, order, limit, offset, scope) are included directly.
|
|
13
|
+
*/
|
|
14
|
+
function useStableKey(options) {
|
|
15
|
+
// We use a ref to track the previous filter reference. When it changes
|
|
16
|
+
// the key changes and the view is recreated.
|
|
17
|
+
const filterRef = useRef(undefined);
|
|
18
|
+
// Bump a generation counter when the filter reference changes
|
|
19
|
+
const genRef = useRef(0);
|
|
20
|
+
if (options?.filter !== filterRef.current) {
|
|
21
|
+
filterRef.current = options?.filter;
|
|
22
|
+
genRef.current++;
|
|
23
|
+
}
|
|
24
|
+
return useMemo(() => {
|
|
25
|
+
if (!options)
|
|
26
|
+
return '';
|
|
27
|
+
const parts = [];
|
|
28
|
+
if (options.where)
|
|
29
|
+
parts.push('w:' + JSON.stringify(options.where));
|
|
30
|
+
if (options.filter)
|
|
31
|
+
parts.push('f:' + genRef.current);
|
|
32
|
+
if (options.orderBy)
|
|
33
|
+
parts.push('ob:' + String(options.orderBy));
|
|
34
|
+
if (options.order)
|
|
35
|
+
parts.push('o:' + options.order);
|
|
36
|
+
if (options.limit !== undefined)
|
|
37
|
+
parts.push('l:' + options.limit);
|
|
38
|
+
if (options.offset !== undefined)
|
|
39
|
+
parts.push('off:' + options.offset);
|
|
40
|
+
if (options.scope)
|
|
41
|
+
parts.push('s:' + options.scope);
|
|
42
|
+
return parts.join('|');
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, [
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
options?.where ? JSON.stringify(options.where) : '',
|
|
47
|
+
genRef.current,
|
|
48
|
+
options?.orderBy,
|
|
49
|
+
options?.order,
|
|
50
|
+
options?.limit,
|
|
51
|
+
options?.offset,
|
|
52
|
+
options?.scope,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
// ── Implementation ──────────────────────────────────────────────────
|
|
56
|
+
export function useQuery(schemaOrTypename, modelKeyOrOptions, maybeOptions) {
|
|
57
|
+
const ctx = useSyncContext();
|
|
58
|
+
const { store } = ctx;
|
|
59
|
+
let typename;
|
|
60
|
+
let options;
|
|
61
|
+
if (typeof schemaOrTypename === 'string') {
|
|
62
|
+
// First arg is a string. Could be either the new zero-arg typed
|
|
63
|
+
// overload (a schema model key, resolved via `ctx.schema`) or the
|
|
64
|
+
// legacy untyped overload (a raw typename like 'Chat'). When a
|
|
65
|
+
// schema is present on the context and the string maps to a known
|
|
66
|
+
// model key, we look up the real typename from the schema's
|
|
67
|
+
// `ModelDef.typename`. Otherwise we treat the string as a typename
|
|
68
|
+
// directly — preserving the legacy behavior for any non-opting
|
|
69
|
+
// consumer. Both paths converge on the same runtime lookup.
|
|
70
|
+
const key = schemaOrTypename;
|
|
71
|
+
const ctxSchema = ctx.schema;
|
|
72
|
+
const modelDef = ctxSchema
|
|
73
|
+
? ctxSchema.models[key]
|
|
74
|
+
: undefined;
|
|
75
|
+
typename = modelDef?.typename ?? key;
|
|
76
|
+
options = modelKeyOrOptions;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Explicit schema path: useQuery(schema, 'chats', options?)
|
|
80
|
+
const schema = schemaOrTypename;
|
|
81
|
+
const modelKey = modelKeyOrOptions;
|
|
82
|
+
const modelDef = schema.models[modelKey];
|
|
83
|
+
typename = modelDef?.typename ?? modelKey;
|
|
84
|
+
options = maybeOptions;
|
|
85
|
+
}
|
|
86
|
+
const optionsKey = useStableKey(options);
|
|
87
|
+
// The QueryView is generic-erased to `Record<string, unknown>`, but
|
|
88
|
+
// the caller's filter is typed in `T`. Wrap rather than cast: the
|
|
89
|
+
// view passes a Record at runtime and the wrapper narrows to T —
|
|
90
|
+
// single typed boundary, no `as unknown as` chain.
|
|
91
|
+
const userFilter = options?.filter;
|
|
92
|
+
const viewOptions = options
|
|
93
|
+
? {
|
|
94
|
+
where: options.where,
|
|
95
|
+
filter: userFilter
|
|
96
|
+
? (entity) => userFilter(entity)
|
|
97
|
+
: undefined,
|
|
98
|
+
orderBy: options.orderBy,
|
|
99
|
+
order: options.order,
|
|
100
|
+
limit: options.limit,
|
|
101
|
+
offset: options.offset,
|
|
102
|
+
scope: options.scope,
|
|
103
|
+
}
|
|
104
|
+
: undefined;
|
|
105
|
+
const view = useMemo(() => store.pool.createView(typename, viewOptions),
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
[store.pool, typename, optionsKey]);
|
|
108
|
+
useEffect(() => () => view.dispose(), [view]);
|
|
109
|
+
// Self-subscribing — consumers never wrap their component in
|
|
110
|
+
// `observer`. `useReactive` tracks the observables read inside the
|
|
111
|
+
// compute function (`view.results`), recomputes on change, and
|
|
112
|
+
// returns a stable slice so downstream `.sort()` / `.reverse()`
|
|
113
|
+
// calls don't trip MobX error 37. The default structural equality
|
|
114
|
+
// check prevents re-renders when nothing actually moved.
|
|
115
|
+
//
|
|
116
|
+
// The compute closure MUST be stable when `view` is stable. Without
|
|
117
|
+
// useCallback([view]), each render passes a fresh arrow to
|
|
118
|
+
// useReactive, which then can't distinguish "swapped to a new
|
|
119
|
+
// QueryView" from "same view, new render" — the wrong call would
|
|
120
|
+
// either re-subscribe every render (waste) or never re-subscribe
|
|
121
|
+
// when view actually swaps (stale snapshot bug returning a previous
|
|
122
|
+
// view's results forever).
|
|
123
|
+
const compute = useCallback(() => view.results.slice(), [view]);
|
|
124
|
+
return useReactive(compute);
|
|
125
|
+
}
|
|
126
|
+
export function useOne(schemaOrIdOrKey, modelKeyOrId, maybeId) {
|
|
127
|
+
const { store } = useSyncContext();
|
|
128
|
+
if (schemaOrIdOrKey === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
if (typeof schemaOrIdOrKey === 'string') {
|
|
132
|
+
// Either `useOne(id)` (legacy, one arg) or `useOne(modelKey, id)` (global).
|
|
133
|
+
// Disambiguate by whether a second arg was passed — both paths
|
|
134
|
+
// converge on the same runtime pool lookup because entity IDs are
|
|
135
|
+
// globally unique across model types.
|
|
136
|
+
if (modelKeyOrId !== undefined) {
|
|
137
|
+
return store.pool.get(modelKeyOrId);
|
|
138
|
+
}
|
|
139
|
+
return store.pool.get(schemaOrIdOrKey);
|
|
140
|
+
}
|
|
141
|
+
// Explicit schema path: useOne(schema, 'tasks', id)
|
|
142
|
+
if (!maybeId)
|
|
143
|
+
return undefined;
|
|
144
|
+
return store.pool.get(maybeId);
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscribe a component to a reactive computation and re-render when
|
|
3
|
+
* the result changes. Concurrent-render safe, referentially stable
|
|
4
|
+
* across renders when the value is unchanged.
|
|
5
|
+
*
|
|
6
|
+
* Thin wrapper over React's `useSyncExternalStore` + MobX `reaction`
|
|
7
|
+
* that hides the three-arg ceremony and the "cached snapshot" rule
|
|
8
|
+
* those primitives impose. Consumers write domain code:
|
|
9
|
+
*
|
|
10
|
+
* const count = useReactive(() => store.tasks.length);
|
|
11
|
+
* const todos = useReactive(() => store.tasks.findMany({ status: 'todo' }));
|
|
12
|
+
* const title = useReactive(() => store.user.name ?? 'Guest');
|
|
13
|
+
*
|
|
14
|
+
* and get values that track MobX observables transparently. No
|
|
15
|
+
* subscribe callbacks, no getSnapshot identity contract, no React
|
|
16
|
+
* concurrent-mode mechanics to reason about.
|
|
17
|
+
*
|
|
18
|
+
* ## Equality
|
|
19
|
+
*
|
|
20
|
+
* The default `equals` is structural for arrays (length + element
|
|
21
|
+
* identity) and `Object.is` for everything else — matches what 99%
|
|
22
|
+
* of UI code expects from a reactive read. Pass a custom `equals`
|
|
23
|
+
* for bespoke shapes (deep objects, tuples, etc.).
|
|
24
|
+
*
|
|
25
|
+
* ## Why not raw useSyncExternalStore?
|
|
26
|
+
*
|
|
27
|
+
* `useSyncExternalStore` is React's low-level primitive for library
|
|
28
|
+
* authors. It leaks concurrent-mode internals (tearing protection,
|
|
29
|
+
* commit-phase subscription) into consumer code and enforces a
|
|
30
|
+
* "getSnapshot must return a cached reference" contract that's easy
|
|
31
|
+
* to violate — violating it causes React error #185 (infinite render
|
|
32
|
+
* loop). This helper lives once and enforces the contract so every
|
|
33
|
+
* caller writes the domain-level code, not the primitive.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useReactive<T>(compute: () => T, equals?: (a: T, b: T) => boolean): T;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useRef } from 'react';
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import { reaction } from 'mobx';
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe a component to a reactive computation and re-render when
|
|
7
|
+
* the result changes. Concurrent-render safe, referentially stable
|
|
8
|
+
* across renders when the value is unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Thin wrapper over React's `useSyncExternalStore` + MobX `reaction`
|
|
11
|
+
* that hides the three-arg ceremony and the "cached snapshot" rule
|
|
12
|
+
* those primitives impose. Consumers write domain code:
|
|
13
|
+
*
|
|
14
|
+
* const count = useReactive(() => store.tasks.length);
|
|
15
|
+
* const todos = useReactive(() => store.tasks.findMany({ status: 'todo' }));
|
|
16
|
+
* const title = useReactive(() => store.user.name ?? 'Guest');
|
|
17
|
+
*
|
|
18
|
+
* and get values that track MobX observables transparently. No
|
|
19
|
+
* subscribe callbacks, no getSnapshot identity contract, no React
|
|
20
|
+
* concurrent-mode mechanics to reason about.
|
|
21
|
+
*
|
|
22
|
+
* ## Equality
|
|
23
|
+
*
|
|
24
|
+
* The default `equals` is structural for arrays (length + element
|
|
25
|
+
* identity) and `Object.is` for everything else — matches what 99%
|
|
26
|
+
* of UI code expects from a reactive read. Pass a custom `equals`
|
|
27
|
+
* for bespoke shapes (deep objects, tuples, etc.).
|
|
28
|
+
*
|
|
29
|
+
* ## Why not raw useSyncExternalStore?
|
|
30
|
+
*
|
|
31
|
+
* `useSyncExternalStore` is React's low-level primitive for library
|
|
32
|
+
* authors. It leaks concurrent-mode internals (tearing protection,
|
|
33
|
+
* commit-phase subscription) into consumer code and enforces a
|
|
34
|
+
* "getSnapshot must return a cached reference" contract that's easy
|
|
35
|
+
* to violate — violating it causes React error #185 (infinite render
|
|
36
|
+
* loop). This helper lives once and enforces the contract so every
|
|
37
|
+
* caller writes the domain-level code, not the primitive.
|
|
38
|
+
*/
|
|
39
|
+
export function useReactive(compute, equals = defaultEquals) {
|
|
40
|
+
// Late-binding refs so the subscribe callback stays stable across
|
|
41
|
+
// re-renders — otherwise React would re-subscribe every render,
|
|
42
|
+
// which both churns the MobX reaction and leaks listeners.
|
|
43
|
+
const computeRef = useRef(compute);
|
|
44
|
+
const equalsRef = useRef(equals);
|
|
45
|
+
equalsRef.current = equals;
|
|
46
|
+
// Cached snapshot — referentially stable between reaction fires.
|
|
47
|
+
const snapshotRef = useRef(null);
|
|
48
|
+
// When `compute` identity changes, its closed-over observable source
|
|
49
|
+
// may have swapped (e.g. useQuery memoized a new QueryView because
|
|
50
|
+
// the where clause changed). The MobX reaction subscribed in
|
|
51
|
+
// `subscribe` only tracks the observables read on its FIRST run; if
|
|
52
|
+
// the source swaps without a re-subscription, the reaction never
|
|
53
|
+
// re-tracks the new observables and `getSnapshot` keeps returning
|
|
54
|
+
// the stale value forever.
|
|
55
|
+
//
|
|
56
|
+
// Detect the swap by reference and:
|
|
57
|
+
// 1. Recompute the snapshot synchronously so the next render returns
|
|
58
|
+
// the fresh value rather than the previous source's last value.
|
|
59
|
+
// Reconcile through `equals` so unchanged values keep their
|
|
60
|
+
// snapshot identity (avoids unnecessary re-renders downstream).
|
|
61
|
+
// 2. Bump `subscribeVersion`, which is the dependency for `subscribe`
|
|
62
|
+
// below — useSyncExternalStore re-subscribes when subscribe's
|
|
63
|
+
// identity changes, the new reaction tracker runs against the
|
|
64
|
+
// latest `computeRef.current`, and dependencies for the new
|
|
65
|
+
// source are correctly captured.
|
|
66
|
+
const subscribeVersionRef = useRef(0);
|
|
67
|
+
if (snapshotRef.current === null) {
|
|
68
|
+
snapshotRef.current = { value: compute() };
|
|
69
|
+
computeRef.current = compute;
|
|
70
|
+
}
|
|
71
|
+
else if (computeRef.current !== compute) {
|
|
72
|
+
const next = compute();
|
|
73
|
+
if (!equals(snapshotRef.current.value, next)) {
|
|
74
|
+
snapshotRef.current = { value: next };
|
|
75
|
+
}
|
|
76
|
+
computeRef.current = compute;
|
|
77
|
+
subscribeVersionRef.current++;
|
|
78
|
+
}
|
|
79
|
+
const subscribeVersion = subscribeVersionRef.current;
|
|
80
|
+
const subscribe = useCallback((onChange) => {
|
|
81
|
+
return reaction(() => computeRef.current(), (next) => {
|
|
82
|
+
const current = snapshotRef.current.value;
|
|
83
|
+
if (!equalsRef.current(current, next)) {
|
|
84
|
+
snapshotRef.current = { value: next };
|
|
85
|
+
onChange();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
+
}, [subscribeVersion]);
|
|
90
|
+
const getSnapshot = useCallback(() => snapshotRef.current.value, []);
|
|
91
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Default equality: structural for arrays, `Object.is` otherwise.
|
|
95
|
+
* Covers the common cases (collection reads, scalar reads) without
|
|
96
|
+
* requiring callers to think about reference identity.
|
|
97
|
+
*/
|
|
98
|
+
function defaultEquals(a, b) {
|
|
99
|
+
if (Object.is(a, b))
|
|
100
|
+
return true;
|
|
101
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
102
|
+
if (a.length !== b.length)
|
|
103
|
+
return false;
|
|
104
|
+
for (let i = 0; i < a.length; i++) {
|
|
105
|
+
if (!Object.is(a[i], b[i]))
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Schema, InferModel } from '../schema/schema.js';
|
|
2
|
+
import type { ResolveSchema } from '../types/global.js';
|
|
3
|
+
import type { SyncStoreContract } from './context.js';
|
|
4
|
+
type GlobalReaderKey = ResolveSchema extends {
|
|
5
|
+
models: infer M;
|
|
6
|
+
} ? keyof M & string : string;
|
|
7
|
+
type GlobalReaderActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? ReaderActions<ResolveSchema, K> : ReaderActions<Schema, string> : ReaderActions<Schema, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Compatibility schema-typed imperative reader. Returns functions for one-off lookups
|
|
10
|
+
* without subscribing the component to collection changes.
|
|
11
|
+
*
|
|
12
|
+
* Prefer `useAblo()` and call `ablo.<model>.retrieve/list` inside callbacks and
|
|
13
|
+
* effects in new integrations. This hook remains for older string-keyed code.
|
|
14
|
+
*
|
|
15
|
+
* Use this inside event handlers, mutation callbacks, or effects where you
|
|
16
|
+
* need a current snapshot of the pool but don't want to trigger re-renders
|
|
17
|
+
* on every entity change.
|
|
18
|
+
*
|
|
19
|
+
* For reactive reads, use selector reads through
|
|
20
|
+
* `useAblo((ablo) => ablo.<model>.retrieve(id))`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* import { schema } from '@ablo/schema';
|
|
24
|
+
* import { useReader } from '@ablo/sync-engine/react';
|
|
25
|
+
*
|
|
26
|
+
* function useTaskMutations() {
|
|
27
|
+
* const read = useReader(schema, 'tasks');
|
|
28
|
+
*
|
|
29
|
+
* return {
|
|
30
|
+
* create: async (data) => {
|
|
31
|
+
* // Imperative read — uses FK index when available (O(1))
|
|
32
|
+
* const existing = read.findMany({ where: { projectId: data.projectId } });
|
|
33
|
+
* const order = existing.reduce((m, t) => Math.max(m, t.order ?? 0), 0) + 1;
|
|
34
|
+
* // ...
|
|
35
|
+
* },
|
|
36
|
+
* };
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
export interface ReaderFindOptions<T> {
|
|
40
|
+
/** Equality filter — uses FK index when the field is registered. */
|
|
41
|
+
where?: Partial<T>;
|
|
42
|
+
/** Predicate applied AFTER `where` filtering. */
|
|
43
|
+
filter?: (entity: T) => boolean;
|
|
44
|
+
/** Sort field. */
|
|
45
|
+
orderBy?: keyof T & string;
|
|
46
|
+
/** Sort direction. Default: 'asc'. */
|
|
47
|
+
order?: 'asc' | 'desc';
|
|
48
|
+
/** Max results. */
|
|
49
|
+
limit?: number;
|
|
50
|
+
/** Skip N results. */
|
|
51
|
+
offset?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ReaderActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
54
|
+
/** Get a single entity by id. Returns undefined if not in pool. */
|
|
55
|
+
retrieve: (id: string) => InferModel<S, K> | undefined;
|
|
56
|
+
/** Read a collection with optional filters. Snapshot — not reactive. */
|
|
57
|
+
list: (options?: ReaderFindOptions<InferModel<S, K>>) => InferModel<S, K>[];
|
|
58
|
+
/** Count entities matching the options. */
|
|
59
|
+
count: (options?: ReaderFindOptions<InferModel<S, K>>) => number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pure factory — testable without React. `useReader` wraps this in useMemo.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createReaderActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract): ReaderActions<S, K>;
|
|
65
|
+
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.retrieve/list` in callbacks/effects. */
|
|
66
|
+
export declare function useReader<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): ReaderActions<S, K>;
|
|
67
|
+
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.retrieve/list` in callbacks/effects. */
|
|
68
|
+
export declare function useReader<K extends GlobalReaderKey>(modelKey: K): GlobalReaderActions<K>;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useSyncContext } from './context.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Pure factory — testable without React. `useReader` wraps this in useMemo.
|
|
7
|
+
*/
|
|
8
|
+
export function createReaderActions(schema, modelKey, store) {
|
|
9
|
+
const modelDef = schema.models[modelKey];
|
|
10
|
+
const typename = modelDef?.typename ?? modelKey;
|
|
11
|
+
function read(options) {
|
|
12
|
+
// FK index fast path: single-field `where` on a registered FK index → O(1) lookup.
|
|
13
|
+
let candidates;
|
|
14
|
+
const whereEntries = options?.where ? Object.entries(options.where) : [];
|
|
15
|
+
const singleWhere = whereEntries.length === 1 ? whereEntries[0] : undefined;
|
|
16
|
+
if (singleWhere &&
|
|
17
|
+
typeof singleWhere[1] === 'string' &&
|
|
18
|
+
store.pool.hasForeignKeyIndex(typename, singleWhere[0])) {
|
|
19
|
+
candidates = store.pool.getByForeignKey(typename, singleWhere[0], singleWhere[1]);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
candidates = store.pool.getByTypeName(typename);
|
|
23
|
+
if (options?.where) {
|
|
24
|
+
candidates = candidates.filter((entity) => {
|
|
25
|
+
for (const [field, value] of whereEntries) {
|
|
26
|
+
if (entity[field] !== value)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let results = candidates;
|
|
34
|
+
if (options?.filter) {
|
|
35
|
+
results = results.filter(options.filter);
|
|
36
|
+
}
|
|
37
|
+
if (options?.orderBy) {
|
|
38
|
+
const field = options.orderBy;
|
|
39
|
+
const dir = options.order === 'desc' ? -1 : 1;
|
|
40
|
+
results = [...results].sort((a, b) => {
|
|
41
|
+
const av = a[field];
|
|
42
|
+
const bv = b[field];
|
|
43
|
+
if (av === bv)
|
|
44
|
+
return 0;
|
|
45
|
+
if (av == null)
|
|
46
|
+
return 1;
|
|
47
|
+
if (bv == null)
|
|
48
|
+
return -1;
|
|
49
|
+
return av < bv ? -dir : dir;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (options?.offset)
|
|
53
|
+
results = results.slice(options.offset);
|
|
54
|
+
if (options?.limit !== undefined)
|
|
55
|
+
results = results.slice(0, options.limit);
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
retrieve: (id) => store.pool.get(id),
|
|
60
|
+
list: (options) => read(options),
|
|
61
|
+
count: (options) => read(options).length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function useReader(schemaOrKey, maybeKey) {
|
|
65
|
+
const { store, schema: ctxSchema } = useSyncContext();
|
|
66
|
+
const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
|
|
67
|
+
const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
|
|
68
|
+
if (!resolvedSchema) {
|
|
69
|
+
throw new AbloValidationError('useReader: no schema available. Pass the schema as the first arg ' +
|
|
70
|
+
'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'reader_schema_missing' });
|
|
71
|
+
}
|
|
72
|
+
return useMemo(() => createReaderActions(resolvedSchema, resolvedKey, store), [store, resolvedSchema, resolvedKey]);
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive sync-status snapshot as a discriminated union. Impossible
|
|
3
|
+
* states (e.g., "connected AND offline") are unrepresentable — each
|
|
4
|
+
* variant carries only the fields that make sense in that state.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by Liveblocks' `useStatus()` and Zero's `useConnectionState()`:
|
|
7
|
+
* one hook, one switch, no six-boolean guessing games.
|
|
8
|
+
*
|
|
9
|
+
* Variants:
|
|
10
|
+
* - `initial` — the provider just mounted; no connection attempt yet.
|
|
11
|
+
* - `connecting` — bootstrap in progress. `progress` is 0–100 and
|
|
12
|
+
* can drive a determinate progress bar.
|
|
13
|
+
* - `connected` — hydrated and listening. `hasUnsyncedChanges` is
|
|
14
|
+
* true while local writes are waiting for server ack; flip it into
|
|
15
|
+
* "Saving…" UI.
|
|
16
|
+
* - `reconnecting` — WebSocket dropped and the client is retrying.
|
|
17
|
+
* `reason` carries the human-readable close reason when available.
|
|
18
|
+
* - `disconnected` — network failure, server error, or the retry loop
|
|
19
|
+
* gave up. Show the offline / error UI.
|
|
20
|
+
* - `needs-auth` — server rejected the session (1008/4001/4003). The
|
|
21
|
+
* consumer's `onSessionExpired` callback has already been invoked
|
|
22
|
+
* by `<AbloProvider>`; this variant exists for UI that wants to
|
|
23
|
+
* reflect the auth state itself.
|
|
24
|
+
*/
|
|
25
|
+
export type SyncStatusSnapshot = {
|
|
26
|
+
readonly name: 'initial';
|
|
27
|
+
} | {
|
|
28
|
+
readonly name: 'connecting';
|
|
29
|
+
readonly progress: number;
|
|
30
|
+
} | {
|
|
31
|
+
readonly name: 'connected';
|
|
32
|
+
readonly hasUnsyncedChanges: boolean;
|
|
33
|
+
} | {
|
|
34
|
+
readonly name: 'reconnecting';
|
|
35
|
+
readonly reason?: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly name: 'disconnected';
|
|
38
|
+
readonly reason?: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly name: 'needs-auth';
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Reactive sync-status hook. Bridges MobX `store.syncStatus` +
|
|
44
|
+
* `store.isReady` into React via `useReactive` — concurrent-render
|
|
45
|
+
* safe and immune to the React #185 "getSnapshot should be cached"
|
|
46
|
+
* infinite-loop class of bugs.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* function StatusPill() {
|
|
50
|
+
* const status = useSyncStatus();
|
|
51
|
+
* switch (status.name) {
|
|
52
|
+
* case 'initial':
|
|
53
|
+
* case 'connecting': return <Pill progress={status.name === 'connecting' ? status.progress : 0}>Loading…</Pill>;
|
|
54
|
+
* case 'connected': return status.hasUnsyncedChanges ? <Pill>Saving…</Pill> : null;
|
|
55
|
+
* case 'reconnecting': return <Pill title={status.reason}>Reconnecting…</Pill>;
|
|
56
|
+
* case 'disconnected': return <Pill title={status.reason}>Offline</Pill>;
|
|
57
|
+
* case 'needs-auth': return null;
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
export declare function useSyncStatus(): SyncStatusSnapshot;
|