@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,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { AbloInternalContext } from './internalContext.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Returns the app user ID passed to the nearest `<AbloProvider>`, when
|
|
7
|
+
* the app chose to provide one.
|
|
8
|
+
*
|
|
9
|
+
* Hosted Ablo identity is resolved server-side from the API key, session,
|
|
10
|
+
* or capability token. This hook is only for app-owned fields like
|
|
11
|
+
* `assigneeId`; it is not required for Ablo sync to connect.
|
|
12
|
+
*
|
|
13
|
+
* Use this in leaf components that need the current user ID for
|
|
14
|
+
* mutation payloads, presence labels, permission checks, etc.
|
|
15
|
+
* @example
|
|
16
|
+
* function TaskRow({ id }) {
|
|
17
|
+
* const userId = useCurrentUserId();
|
|
18
|
+
* const ablo = useAblo();
|
|
19
|
+
* if (!userId) return null;
|
|
20
|
+
* return <button onClick={() => ablo?.tasks.update(id, { assigneeId: userId })}>
|
|
21
|
+
* Assign to me
|
|
22
|
+
* </button>;
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export function useCurrentUserId() {
|
|
26
|
+
const ctx = useContext(AbloInternalContext);
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new AbloValidationError('useCurrentUserId: no <AbloProvider> mounted above this component. ' +
|
|
29
|
+
'Wrap your tree with <AbloProvider ...> from ' +
|
|
30
|
+
'@ablo/sync-engine/react.', { code: 'no_ablo_provider' });
|
|
31
|
+
}
|
|
32
|
+
return ctx.currentUserId;
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register an imperative callback that fires whenever the provider
|
|
3
|
+
* surfaces an error. Covers engine errors (bootstrap failures,
|
|
4
|
+
* mutation rejections), WebSocket errors, and uncaught exceptions
|
|
5
|
+
* inside `postBootstrap` hooks.
|
|
6
|
+
*
|
|
7
|
+
* Use this for telemetry (Sentry, Datadog), user-facing toasts, or
|
|
8
|
+
* any side effect that should NOT trigger a re-render. The listener
|
|
9
|
+
* is stored in a ref, so re-renders don't thrash the subscription.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* function ErrorToaster() {
|
|
13
|
+
* useErrorListener((err) => {
|
|
14
|
+
* toast.error(err.message);
|
|
15
|
+
* Sentry.captureException(err);
|
|
16
|
+
* });
|
|
17
|
+
* return null;
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export declare function useErrorListener(listener: (error: Error) => void): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
3
|
+
import { AbloInternalContext } from './internalContext.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Register an imperative callback that fires whenever the provider
|
|
7
|
+
* surfaces an error. Covers engine errors (bootstrap failures,
|
|
8
|
+
* mutation rejections), WebSocket errors, and uncaught exceptions
|
|
9
|
+
* inside `postBootstrap` hooks.
|
|
10
|
+
*
|
|
11
|
+
* Use this for telemetry (Sentry, Datadog), user-facing toasts, or
|
|
12
|
+
* any side effect that should NOT trigger a re-render. The listener
|
|
13
|
+
* is stored in a ref, so re-renders don't thrash the subscription.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* function ErrorToaster() {
|
|
17
|
+
* useErrorListener((err) => {
|
|
18
|
+
* toast.error(err.message);
|
|
19
|
+
* Sentry.captureException(err);
|
|
20
|
+
* });
|
|
21
|
+
* return null;
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export function useErrorListener(listener) {
|
|
25
|
+
const ctx = useContext(AbloInternalContext);
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
throw new AbloValidationError('useErrorListener: no <AbloProvider> mounted above this component. ' +
|
|
28
|
+
'Wrap your tree with <AbloProvider ...> from @ablo/sync-engine/react.', { code: 'no_ablo_provider' });
|
|
29
|
+
}
|
|
30
|
+
// Stash the latest callback in a ref so the effect subscription
|
|
31
|
+
// stays stable across renders. Matches the `useEventCallback`
|
|
32
|
+
// pattern: late-bind the listener so callers can pass inline
|
|
33
|
+
// arrows without thrashing the subscription.
|
|
34
|
+
const ref = useRef(listener);
|
|
35
|
+
ref.current = listener;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
return ctx.subscribeError((err) => ref.current(err));
|
|
38
|
+
}, [ctx]);
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ResolveIntents } from '../types/global.js';
|
|
2
|
+
/**
|
|
3
|
+
* Named-intent invoker, typed via `ResolveIntents[IntentName]`.
|
|
4
|
+
*
|
|
5
|
+
* The consumer declares their intent vocabulary in the global:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* declare global {
|
|
9
|
+
* interface AbloSync {
|
|
10
|
+
* Intents: {
|
|
11
|
+
* editLayer: { slideId: string; layerId: string };
|
|
12
|
+
* generateWithAI: { entityId: string; tool: string };
|
|
13
|
+
* };
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Then `useIntent('editLayer')` returns a function whose sole argument
|
|
19
|
+
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
20
|
+
* time narrowing.
|
|
21
|
+
*
|
|
22
|
+
* The SDK doesn't own what happens next: the `beginIntent` function on
|
|
23
|
+
* the React context (supplied via `SyncProvider`) is where the intent
|
|
24
|
+
* claim turns into a network effect. A Node-backed consumer wires it
|
|
25
|
+
* through `SyncAgent.beginIntent`; a browser-backed consumer may
|
|
26
|
+
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
27
|
+
* that adds the typed name + claim narrowing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useIntent<Name extends keyof ResolveIntents & string>(intentName: Name): (claim: ResolveIntents[Name]) => unknown;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { useSyncContext } from './context.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Named-intent invoker, typed via `ResolveIntents[IntentName]`.
|
|
7
|
+
*
|
|
8
|
+
* The consumer declares their intent vocabulary in the global:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* declare global {
|
|
12
|
+
* interface AbloSync {
|
|
13
|
+
* Intents: {
|
|
14
|
+
* editLayer: { slideId: string; layerId: string };
|
|
15
|
+
* generateWithAI: { entityId: string; tool: string };
|
|
16
|
+
* };
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Then `useIntent('editLayer')` returns a function whose sole argument
|
|
22
|
+
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
23
|
+
* time narrowing.
|
|
24
|
+
*
|
|
25
|
+
* The SDK doesn't own what happens next: the `beginIntent` function on
|
|
26
|
+
* the React context (supplied via `SyncProvider`) is where the intent
|
|
27
|
+
* claim turns into a network effect. A Node-backed consumer wires it
|
|
28
|
+
* through `SyncAgent.beginIntent`; a browser-backed consumer may
|
|
29
|
+
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
30
|
+
* that adds the typed name + claim narrowing.
|
|
31
|
+
*/
|
|
32
|
+
export function useIntent(intentName) {
|
|
33
|
+
const { beginIntent } = useSyncContext();
|
|
34
|
+
return useCallback((claim) => {
|
|
35
|
+
if (!beginIntent) {
|
|
36
|
+
throw new AbloValidationError(`useIntent: no \`beginIntent\` wired into SyncProvider. Pass ` +
|
|
37
|
+
`a \`beginIntent\` prop (typically bound to your transport) ` +
|
|
38
|
+
`to enable intent invocations.`, { code: 'intent_not_wired' });
|
|
39
|
+
}
|
|
40
|
+
return beginIntent(intentName, claim);
|
|
41
|
+
}, [beginIntent, intentName]);
|
|
42
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
|
|
2
|
+
import type { ResolveSchema } from '../types/global.js';
|
|
3
|
+
import type { SyncStoreContract } from './context.js';
|
|
4
|
+
type GlobalMutateKey = ResolveSchema extends {
|
|
5
|
+
models: infer M;
|
|
6
|
+
} ? keyof M & string : string;
|
|
7
|
+
type GlobalMutateActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? MutateActions<ResolveSchema, K> : MutateActions<Schema, string> : MutateActions<Schema, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Compatibility mutation hook. Returns CRUD methods for a single model type.
|
|
10
|
+
*
|
|
11
|
+
* Prefer `useAblo()` and call `ablo.<model>.create/update/delete` inside
|
|
12
|
+
* callbacks for new integrations. This hook remains for older string-keyed code.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { schema } from '@ablo/schema';
|
|
16
|
+
* import { useMutate } from '@ablo/sync-engine/react';
|
|
17
|
+
*
|
|
18
|
+
* const tasks = useMutate(schema, 'tasks');
|
|
19
|
+
*
|
|
20
|
+
* // Create — fields are type-checked against the schema's Zod shape
|
|
21
|
+
* await tasks.create({ title: 'Fix bug', status: 'todo', projectId });
|
|
22
|
+
*
|
|
23
|
+
* // Update — id + partial changes, no need to hold a model instance
|
|
24
|
+
* await tasks.update({ id: task.id, status: 'done', completedAt: new Date() });
|
|
25
|
+
*
|
|
26
|
+
* // Delete / archive / unarchive — by id
|
|
27
|
+
* await tasks.delete(task.id);
|
|
28
|
+
* await tasks.archive(task.id);
|
|
29
|
+
*
|
|
30
|
+
* Mirrors the Zero pattern: `zero.mutate.task.update({ id, status: 'done' })`.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* `create` / `update` / `delete` are overloaded: pass one row or an
|
|
34
|
+
* array. Drizzle and Prisma use the same shape (`db.insert(table).values(rowOrRows)`).
|
|
35
|
+
* Avoids the `*Many` suffix while keeping the semantics: every entry in
|
|
36
|
+
* an array call lands in the same synchronous tick (Promise.all under
|
|
37
|
+
* the hood), so the microtask coalescer in `TransactionQueue` collapses
|
|
38
|
+
* N pushes into one wire commit with one `batchIndex` — structurally
|
|
39
|
+
* identical to Zero's mutator-boundary commit.
|
|
40
|
+
*/
|
|
41
|
+
type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
|
|
42
|
+
id: string;
|
|
43
|
+
} & Partial<InferModel<S, K>>;
|
|
44
|
+
export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
45
|
+
/**
|
|
46
|
+
* Create one entity, or an array of entities in a single tick. ID,
|
|
47
|
+
* createdAt, updatedAt, organizationId default automatically per row.
|
|
48
|
+
*/
|
|
49
|
+
create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
|
|
50
|
+
create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Update one row, or an array of rows in a single tick. Each patch is
|
|
53
|
+
* `{ id, ...changes }` — missing ids throw. Schema-generated models
|
|
54
|
+
* are MobX-observable, so direct assignment fires reactivity.
|
|
55
|
+
*/
|
|
56
|
+
update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
|
|
57
|
+
update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Delete one row by id, or an array of ids in a single tick. Missing
|
|
60
|
+
* ids are silently ignored.
|
|
61
|
+
*/
|
|
62
|
+
delete(id: string): Promise<void>;
|
|
63
|
+
delete(ids: string[]): Promise<void>;
|
|
64
|
+
/** Soft-archive by ID. */
|
|
65
|
+
archive: (id: string) => Promise<void>;
|
|
66
|
+
/** Restore an archived entity by ID. */
|
|
67
|
+
unarchive: (id: string) => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pure factory — testable without React. The hook just wraps this in
|
|
71
|
+
* useMemo with the React context.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
|
|
74
|
+
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`. */
|
|
75
|
+
export declare function useMutate<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): MutateActions<S, K>;
|
|
76
|
+
/** Typed CRUD via the `AbloSync` global augmentation. The schema is
|
|
77
|
+
* resolved from the `SyncProvider`'s context — consumer doesn't pass it
|
|
78
|
+
* at the call site.
|
|
79
|
+
*
|
|
80
|
+
* @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function useMutate<K extends GlobalMutateKey>(modelKey: K): GlobalMutateActions<K>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Model, modelAsRow } from '../Model.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
import { useSyncContext } from './context.js';
|
|
6
|
+
/**
|
|
7
|
+
* Pure factory — testable without React. The hook just wraps this in
|
|
8
|
+
* useMemo with the React context.
|
|
9
|
+
*/
|
|
10
|
+
export function createMutateActions(schema, modelKey, store, organizationId) {
|
|
11
|
+
const modelDef = schema.models[modelKey];
|
|
12
|
+
const typename = modelDef?.typename ?? modelKey;
|
|
13
|
+
// Materialise one input row into a Model and stage a save. The default
|
|
14
|
+
// fields land here once so create-of-one and create-of-array share the
|
|
15
|
+
// same defaulting logic.
|
|
16
|
+
const buildModelForCreate = (data, now) => {
|
|
17
|
+
const record = data;
|
|
18
|
+
const fullData = {
|
|
19
|
+
...record,
|
|
20
|
+
__typename: typename,
|
|
21
|
+
id: record.id ?? Model.generateId(),
|
|
22
|
+
organizationId: record.organizationId ?? organizationId,
|
|
23
|
+
createdAt: record.createdAt ?? now,
|
|
24
|
+
updatedAt: record.updatedAt ?? now,
|
|
25
|
+
};
|
|
26
|
+
const model = store.pool.createFromData(fullData);
|
|
27
|
+
if (!model) {
|
|
28
|
+
throw new AbloValidationError(`useMutate: failed to create ${typename} — no constructor in registry`, { code: 'mutate_create_unknown_model' });
|
|
29
|
+
}
|
|
30
|
+
return model;
|
|
31
|
+
};
|
|
32
|
+
// Apply a patch onto an existing pool model. Returns the model.
|
|
33
|
+
const applyPatch = (patch, now) => {
|
|
34
|
+
const { id, ...changes } = patch;
|
|
35
|
+
const model = store.pool.get(id);
|
|
36
|
+
if (!model) {
|
|
37
|
+
throw new AbloValidationError(`useMutate: ${typename} with id "${id}" not found in pool`, { code: 'mutate_update_entity_not_found' });
|
|
38
|
+
}
|
|
39
|
+
// Schema-derived patch keys are validated at the call-site type
|
|
40
|
+
// signature (`UpdatePatch<S, K>`); writes here are dynamic-class
|
|
41
|
+
// field assignments. `Reflect.set` is the typed bridge — Model
|
|
42
|
+
// doesn't carry an index signature for arbitrary string keys, but
|
|
43
|
+
// the dynamic field installation in `createDynamicModelClass`
|
|
44
|
+
// guarantees these keys resolve at runtime.
|
|
45
|
+
for (const [fieldName, value] of Object.entries(changes)) {
|
|
46
|
+
Reflect.set(model, fieldName, value);
|
|
47
|
+
}
|
|
48
|
+
Reflect.set(model, 'updatedAt', now);
|
|
49
|
+
return model;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
// Overloaded — runtime check on `Array.isArray` decides shape. Both
|
|
53
|
+
// branches stage via `Promise.all` so the microtask coalescer in
|
|
54
|
+
// `TransactionQueue` collapses N pushes into one wire commit.
|
|
55
|
+
create: (async (data) => {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
if (Array.isArray(data)) {
|
|
58
|
+
if (data.length === 0)
|
|
59
|
+
return [];
|
|
60
|
+
const models = data.map((d) => buildModelForCreate(d, now));
|
|
61
|
+
await Promise.all(models.map((m) => store.save(m)));
|
|
62
|
+
return models.map((m) => modelAsRow(m));
|
|
63
|
+
}
|
|
64
|
+
const model = buildModelForCreate(data, now);
|
|
65
|
+
await store.save(model);
|
|
66
|
+
return modelAsRow(model);
|
|
67
|
+
}),
|
|
68
|
+
update: (async (patch) => {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
if (Array.isArray(patch)) {
|
|
71
|
+
if (patch.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
const models = patch.map((p) => applyPatch(p, now));
|
|
74
|
+
await Promise.all(models.map((m) => store.save(m)));
|
|
75
|
+
return models.map((m) => modelAsRow(m));
|
|
76
|
+
}
|
|
77
|
+
const model = applyPatch(patch, now);
|
|
78
|
+
await store.save(model);
|
|
79
|
+
return modelAsRow(model);
|
|
80
|
+
}),
|
|
81
|
+
delete: (async (idOrIds) => {
|
|
82
|
+
if (Array.isArray(idOrIds)) {
|
|
83
|
+
if (idOrIds.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const models = [];
|
|
86
|
+
for (const id of idOrIds) {
|
|
87
|
+
const m = store.pool.get(id);
|
|
88
|
+
if (m)
|
|
89
|
+
models.push(m);
|
|
90
|
+
}
|
|
91
|
+
await Promise.all(models.map((m) => store.delete(m)));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const model = store.pool.get(idOrIds);
|
|
95
|
+
if (!model)
|
|
96
|
+
return;
|
|
97
|
+
await store.delete(model);
|
|
98
|
+
}),
|
|
99
|
+
archive: async (id) => {
|
|
100
|
+
const model = store.pool.get(id);
|
|
101
|
+
if (!model)
|
|
102
|
+
return;
|
|
103
|
+
await store.archive(model);
|
|
104
|
+
},
|
|
105
|
+
unarchive: async (id) => {
|
|
106
|
+
const model = store.pool.get(id);
|
|
107
|
+
if (!model)
|
|
108
|
+
return;
|
|
109
|
+
await store.unarchive(model);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function useMutate(schemaOrKey, maybeKey) {
|
|
114
|
+
const { store, organizationId, schema: ctxSchema } = useSyncContext();
|
|
115
|
+
const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
|
|
116
|
+
const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
|
|
117
|
+
if (!resolvedSchema) {
|
|
118
|
+
throw new AbloValidationError('useMutate: no schema available. Pass the schema as the first arg ' +
|
|
119
|
+
'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutate_schema_missing' });
|
|
120
|
+
}
|
|
121
|
+
return useMemo(() => createMutateActions(resolvedSchema, resolvedKey, store, organizationId), [store, organizationId, resolvedSchema, resolvedKey]);
|
|
122
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Transaction } from '../transactions/TransactionQueue.js';
|
|
2
|
+
export interface MutationFailurePayload {
|
|
3
|
+
transaction: Transaction;
|
|
4
|
+
error: Error;
|
|
5
|
+
permanent?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Register a side-effect listener for mutation failures. Fires whenever
|
|
9
|
+
* the underlying transaction queue rolls back an optimistic write —
|
|
10
|
+
* permanent rejections (validation, FK, auth) and exhausted-retry
|
|
11
|
+
* rollbacks (connection lost mid-burst).
|
|
12
|
+
*
|
|
13
|
+
* Use this to mount a single `<MutationFailureBoundary>` near the app
|
|
14
|
+
* shell that turns silent pool rollbacks into toasts / banners. The
|
|
15
|
+
* listener is stored in a ref so re-renders don't thrash the
|
|
16
|
+
* subscription — matches `useErrorListener`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* function MutationFailureBoundary() {
|
|
20
|
+
* useMutationFailureListener(({ transaction, error }) => {
|
|
21
|
+
* toast.error(`Couldn't save ${transaction.modelName}: ${error.message}`);
|
|
22
|
+
* });
|
|
23
|
+
* return null;
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export declare function useMutationFailureListener(listener: (payload: MutationFailurePayload) => void): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
3
|
+
import { AbloInternalContext } from './internalContext.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Register a side-effect listener for mutation failures. Fires whenever
|
|
7
|
+
* the underlying transaction queue rolls back an optimistic write —
|
|
8
|
+
* permanent rejections (validation, FK, auth) and exhausted-retry
|
|
9
|
+
* rollbacks (connection lost mid-burst).
|
|
10
|
+
*
|
|
11
|
+
* Use this to mount a single `<MutationFailureBoundary>` near the app
|
|
12
|
+
* shell that turns silent pool rollbacks into toasts / banners. The
|
|
13
|
+
* listener is stored in a ref so re-renders don't thrash the
|
|
14
|
+
* subscription — matches `useErrorListener`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* function MutationFailureBoundary() {
|
|
18
|
+
* useMutationFailureListener(({ transaction, error }) => {
|
|
19
|
+
* toast.error(`Couldn't save ${transaction.modelName}: ${error.message}`);
|
|
20
|
+
* });
|
|
21
|
+
* return null;
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export function useMutationFailureListener(listener) {
|
|
25
|
+
const ctx = useContext(AbloInternalContext);
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
throw new AbloValidationError('useMutationFailureListener: no <AbloProvider> mounted above this component. ' +
|
|
28
|
+
'Wrap your tree with <AbloProvider ...> from @ablo/sync-engine/react.', { code: 'no_ablo_provider' });
|
|
29
|
+
}
|
|
30
|
+
const ref = useRef(listener);
|
|
31
|
+
ref.current = listener;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const engine = ctx.engine;
|
|
34
|
+
if (!engine)
|
|
35
|
+
return;
|
|
36
|
+
return engine.onMutationFailure((payload) => ref.current(payload));
|
|
37
|
+
}, [ctx, ctx.engine]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Schema } from '../schema/schema.js';
|
|
2
|
+
import type { MutatorDefs } from '../mutators/defineMutators.js';
|
|
3
|
+
import type { UndoScope } from '../mutators/UndoManager.js';
|
|
4
|
+
import type { ResolveSchema } from '../types/global.js';
|
|
5
|
+
/**
|
|
6
|
+
* useMutators — turn a `defineMutators` tree into callable invokers.
|
|
7
|
+
*
|
|
8
|
+
* The returned object mirrors the mutator tree one-to-one, but each leaf is
|
|
9
|
+
* now a `(args) => Promise<TResult>` function. Internally each invocation:
|
|
10
|
+
* 1. Builds a fresh `Transaction` bound to the current store/org context.
|
|
11
|
+
* 2. Calls the user's mutator with `{ tx, args }`.
|
|
12
|
+
* 3. Returns the mutator's resolved value.
|
|
13
|
+
*
|
|
14
|
+
* V1 error handling: if the mutator throws, we `console.error` + rethrow.
|
|
15
|
+
* Any writes that already dispatched stay in place (no rollback). That
|
|
16
|
+
* matches the existing behaviour of batch helpers like `saveManyOptimized`
|
|
17
|
+
* and keeps the contract honest — consumers can layer their own try/catch
|
|
18
|
+
* + compensating writes until V2 adds atomicity.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Map a `MutatorFn` onto its invoker form — strip `tx`, keep `args`/return.
|
|
22
|
+
*
|
|
23
|
+
* Uses nested `infer O` so the `args`/`result` types are extracted from the
|
|
24
|
+
* function signature without binding the `tx` parameter to a specific
|
|
25
|
+
* `Transaction<S>` variance. Function parameters are contravariant, so a
|
|
26
|
+
* match against `MutatorFn<Schema>` would reject mutators declared against
|
|
27
|
+
* a narrower schema (e.g. `Transaction<typeof appSchema>`). The two-step
|
|
28
|
+
* inference sidesteps that without resorting to `any`/`unknown` placeholders.
|
|
29
|
+
*/
|
|
30
|
+
export type InvokerFor<F> = F extends (options: infer O) => Promise<infer R> ? O extends {
|
|
31
|
+
args: infer A;
|
|
32
|
+
} ? (args: A) => Promise<R> : never : never;
|
|
33
|
+
/**
|
|
34
|
+
* The hook's return shape: same tree as the input `MutatorDefs`, every leaf
|
|
35
|
+
* rewritten to its invoker form.
|
|
36
|
+
*/
|
|
37
|
+
export type MutatorInvokers<M> = {
|
|
38
|
+
[K in keyof M]: {
|
|
39
|
+
[N in keyof M[K]]: InvokerFor<M[K][N]>;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Options passed to `useMutators`. When `undoScope` is set, every mutator
|
|
44
|
+
* invocation is wrapped in a `RecordingTransaction` and its inverses are
|
|
45
|
+
* pushed to the scope as one undo entry.
|
|
46
|
+
*/
|
|
47
|
+
export interface UseMutatorsOptions<S extends Schema> {
|
|
48
|
+
/** Target undo scope for recording inverses. Omit to disable recording. */
|
|
49
|
+
undoScope?: UndoScope<S>;
|
|
50
|
+
}
|
|
51
|
+
/** Mutator invokers (explicit schema arg). */
|
|
52
|
+
export declare function useMutators<S extends Schema, M extends MutatorDefs<S>>(schema: S, mutators: M, options?: UseMutatorsOptions<S>): MutatorInvokers<M>;
|
|
53
|
+
/** Mutator invokers via the `AbloSync` global augmentation. Schema comes
|
|
54
|
+
* from the `SyncProvider`'s context; the mutator tree is typed against
|
|
55
|
+
* `ResolveSchema` at the call site. */
|
|
56
|
+
export declare function useMutators<M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>>(mutators: M, options?: UseMutatorsOptions<ResolveSchema extends Schema ? ResolveSchema : Schema>): MutatorInvokers<M>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { createTransaction } from '../mutators/Transaction.js';
|
|
4
|
+
import { createRecordingTransaction } from '../mutators/RecordingTransaction.js';
|
|
5
|
+
import { useSyncContext } from './context.js';
|
|
6
|
+
import { AbloValidationError } from '../errors.js';
|
|
7
|
+
import { getContext } from '../context.js';
|
|
8
|
+
export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
|
|
9
|
+
const { store, organizationId, schema: ctxSchema } = useSyncContext();
|
|
10
|
+
// Disambiguate: explicit-schema path has the schema object in first slot;
|
|
11
|
+
// the global-resolved path has the mutator tree there. A schema object
|
|
12
|
+
// has a `.models` property; a mutator tree doesn't.
|
|
13
|
+
const isExplicit = typeof schemaOrMutators === 'object' &&
|
|
14
|
+
schemaOrMutators !== null &&
|
|
15
|
+
'models' in schemaOrMutators;
|
|
16
|
+
const schema = isExplicit ? schemaOrMutators : ctxSchema;
|
|
17
|
+
const mutators = (isExplicit ? mutatorsOrOptions : schemaOrMutators);
|
|
18
|
+
const options = (isExplicit ? maybeOptions : mutatorsOrOptions);
|
|
19
|
+
if (!schema) {
|
|
20
|
+
throw new AbloValidationError('useMutators: no schema available. Pass the schema as the first arg ' +
|
|
21
|
+
'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutators_schema_missing' });
|
|
22
|
+
}
|
|
23
|
+
const { undoScope } = options ?? {};
|
|
24
|
+
return useMemo(() => {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const modelKey of Object.keys(mutators)) {
|
|
27
|
+
const group = mutators[modelKey];
|
|
28
|
+
if (!group)
|
|
29
|
+
continue;
|
|
30
|
+
const invokers = {};
|
|
31
|
+
for (const mutatorName of Object.keys(group)) {
|
|
32
|
+
const fn = group[mutatorName];
|
|
33
|
+
const label = `${String(modelKey)}.${mutatorName}`;
|
|
34
|
+
invokers[mutatorName] = async (args) => {
|
|
35
|
+
// Recording path: wrap the transaction so each write snapshots its
|
|
36
|
+
// inverse. On success, push the captured entry to the scope.
|
|
37
|
+
if (undoScope) {
|
|
38
|
+
const recording = createRecordingTransaction(schema, store, organizationId);
|
|
39
|
+
try {
|
|
40
|
+
const result = await fn({ tx: recording.tx, args });
|
|
41
|
+
const entry = recording.getEntry(label);
|
|
42
|
+
if (entry)
|
|
43
|
+
undoScope.record(entry);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
getContext().logger.error(`[useMutators] mutator "${label}" threw`, { error: err });
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Non-recording path — plain transaction, identical to pre-undo V1.
|
|
52
|
+
const tx = createTransaction(schema, store, organizationId);
|
|
53
|
+
try {
|
|
54
|
+
return await fn({ tx, args });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
getContext().logger.error(`[useMutators] mutator "${label}" threw`, { error: err });
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
out[modelKey] = invokers;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}, [schema, mutators, store, organizationId, undoScope]);
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ResolvePresence } from '../types/global.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read the consumer-supplied presence state with `ResolvePresence`d
|
|
4
|
+
* typing — the shape the consumer declared in
|
|
5
|
+
* `declare global { interface AbloSync { Presence: ... } }`.
|
|
6
|
+
*
|
|
7
|
+
* The SDK doesn't own a presence wire format. Consumers plug whatever
|
|
8
|
+
* backs their cursors, status, or activity (a MobX store, a custom
|
|
9
|
+
* WebSocket channel, `SyncAgent` in Node, a Zustand slice) via the
|
|
10
|
+
* `presence` prop on `SyncProvider`. This hook returns it typed.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // apps/your-app/src/ablo-sync.d.ts
|
|
14
|
+
* declare global {
|
|
15
|
+
* interface AbloSync {
|
|
16
|
+
* Presence: { cursor: { x: number; y: number } | null; status: 'away' | 'online' };
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* // consumer's <SyncProvider> wiring
|
|
21
|
+
* <SyncProvider store={store} organizationId={orgId} presence={presenceStore}>
|
|
22
|
+
*
|
|
23
|
+
* // any component
|
|
24
|
+
* const presence = usePresence();
|
|
25
|
+
* presence?.cursor?.x; // fully typed
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Returns `undefined` when no provider-level presence source is wired —
|
|
29
|
+
* consumers can narrow with a guard or configure a default in their
|
|
30
|
+
* provider.
|
|
31
|
+
*/
|
|
32
|
+
export declare function usePresence(): ResolvePresence | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useSyncContext } from './context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Read the consumer-supplied presence state with `ResolvePresence`d
|
|
5
|
+
* typing — the shape the consumer declared in
|
|
6
|
+
* `declare global { interface AbloSync { Presence: ... } }`.
|
|
7
|
+
*
|
|
8
|
+
* The SDK doesn't own a presence wire format. Consumers plug whatever
|
|
9
|
+
* backs their cursors, status, or activity (a MobX store, a custom
|
|
10
|
+
* WebSocket channel, `SyncAgent` in Node, a Zustand slice) via the
|
|
11
|
+
* `presence` prop on `SyncProvider`. This hook returns it typed.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* // apps/your-app/src/ablo-sync.d.ts
|
|
15
|
+
* declare global {
|
|
16
|
+
* interface AbloSync {
|
|
17
|
+
* Presence: { cursor: { x: number; y: number } | null; status: 'away' | 'online' };
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* // consumer's <SyncProvider> wiring
|
|
22
|
+
* <SyncProvider store={store} organizationId={orgId} presence={presenceStore}>
|
|
23
|
+
*
|
|
24
|
+
* // any component
|
|
25
|
+
* const presence = usePresence();
|
|
26
|
+
* presence?.cursor?.x; // fully typed
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Returns `undefined` when no provider-level presence source is wired —
|
|
30
|
+
* consumers can narrow with a guard or configure a default in their
|
|
31
|
+
* provider.
|
|
32
|
+
*/
|
|
33
|
+
export function usePresence() {
|
|
34
|
+
const ctx = useSyncContext();
|
|
35
|
+
// The runtime value is whatever the consumer passed to `SyncProvider`.
|
|
36
|
+
// The type assertion reflects the consumer's declared global, which
|
|
37
|
+
// the hook can't verify at runtime — but the consumer controls both
|
|
38
|
+
// ends (the global declaration and the provider prop) so this is a
|
|
39
|
+
// single-source-of-truth contract, not blind trust.
|
|
40
|
+
return ctx.presence;
|
|
41
|
+
}
|