@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth + URL resolution for `Ablo()`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the small, focused helpers Anthropic ships in `client.ts`
|
|
5
|
+
* (`apiKeyAuth`, `bearerAuth`, `validateHeaders`). Each function does
|
|
6
|
+
* one thing — resolve a value with the right precedence, or fail
|
|
7
|
+
* with an actionable message — so the constructor reads as a
|
|
8
|
+
* sequence of named decisions rather than a stream of `??`-chains.
|
|
9
|
+
*
|
|
10
|
+
* Precedence for every resolver: explicit option → environment
|
|
11
|
+
* variable → built-in default. The same shape Anthropic, OpenAI,
|
|
12
|
+
* and Stripe SDKs use.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Async callable that resolves to a fresh API key. Mirrors the shape
|
|
16
|
+
* Anthropic / OpenAI / Stripe ship — used for credential rotation
|
|
17
|
+
* (e.g. AWS STS, GCP IAM, Vault). Re-exported from `./Ablo` so
|
|
18
|
+
* existing import paths work; defined here so this module has no
|
|
19
|
+
* circular dependency back to `Ablo.ts`.
|
|
20
|
+
*/
|
|
21
|
+
export type ApiKeySetter = () => Promise<string>;
|
|
22
|
+
export interface AuthResolveInput {
|
|
23
|
+
/**
|
|
24
|
+
* The full options bag the caller passed to `Ablo()`. Resolvers
|
|
25
|
+
* read only the fields they care about; the wide shape avoids
|
|
26
|
+
* passing N parameters into each helper.
|
|
27
|
+
*/
|
|
28
|
+
readonly options: {
|
|
29
|
+
readonly apiKey?: string | ApiKeySetter | null;
|
|
30
|
+
readonly authToken?: string | null;
|
|
31
|
+
readonly baseURL?: string | null;
|
|
32
|
+
readonly dangerouslyAllowBrowser?: boolean;
|
|
33
|
+
};
|
|
34
|
+
readonly env: Record<string, string | undefined>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read `process.env` defensively. Works in browser (where `process`
|
|
38
|
+
* is undefined), Node, and edge runtimes that expose a partial
|
|
39
|
+
* process polyfill.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readProcessEnv(): Record<string, string | undefined>;
|
|
42
|
+
export declare function resolveApiKey(input: AuthResolveInput): string | ApiKeySetter | null;
|
|
43
|
+
export declare function resolveAuthToken(input: AuthResolveInput): string | null;
|
|
44
|
+
export declare const ABLO_DEFAULT_BASE_URL = "wss://mesh.ablo.finance";
|
|
45
|
+
export declare function resolveBaseURL(input: AuthResolveInput): string;
|
|
46
|
+
/**
|
|
47
|
+
* Browser guard — apiKey is server-side-only by default. Same check
|
|
48
|
+
* Anthropic, OpenAI, and Stripe ship: shipping `sk_live_...` to a
|
|
49
|
+
* browser exposes it in every visitor's network tab. Consumers opt
|
|
50
|
+
* in explicitly when they have a publishable key or a server proxy.
|
|
51
|
+
*/
|
|
52
|
+
export declare function assertBrowserSafety(input: {
|
|
53
|
+
apiKey: string | ApiKeySetter | null;
|
|
54
|
+
dangerouslyAllowBrowser: boolean | undefined;
|
|
55
|
+
}): void;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve an `ApiKeySetter` callable to its current string value.
|
|
58
|
+
* Used at request time so a rotating credential picks up rotations
|
|
59
|
+
* between requests. Returns `null` when no key was configured.
|
|
60
|
+
*
|
|
61
|
+
* Mirrors Anthropic's pattern of supporting both a static string and
|
|
62
|
+
* a callable for credential rotation.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveApiKeyValue(apiKey: string | ApiKeySetter | null): Promise<string | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Translate a sync-engine WebSocket URL to the matching HTTP API
|
|
67
|
+
* base URL, defaulting to `${url}/api` when the caller hasn't
|
|
68
|
+
* overridden `bootstrapBaseUrl`. Used by `BootstrapHelper`,
|
|
69
|
+
* `HydrationCoordinator`, the apiKey-exchange flow, and the
|
|
70
|
+
* self-derived identity flow — same derivation in all four spots,
|
|
71
|
+
* so it lives here as a single source of truth.
|
|
72
|
+
*
|
|
73
|
+
* Note: when both `wss://` and `https://` are valid, `replace(/^ws/, 'http')`
|
|
74
|
+
* preserves the protocol family (ws → http, wss → https).
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveBootstrapBaseUrl(input: {
|
|
77
|
+
readonly url: string;
|
|
78
|
+
readonly bootstrapBaseUrl?: string;
|
|
79
|
+
}): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth + URL resolution for `Ablo()`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the small, focused helpers Anthropic ships in `client.ts`
|
|
5
|
+
* (`apiKeyAuth`, `bearerAuth`, `validateHeaders`). Each function does
|
|
6
|
+
* one thing — resolve a value with the right precedence, or fail
|
|
7
|
+
* with an actionable message — so the constructor reads as a
|
|
8
|
+
* sequence of named decisions rather than a stream of `??`-chains.
|
|
9
|
+
*
|
|
10
|
+
* Precedence for every resolver: explicit option → environment
|
|
11
|
+
* variable → built-in default. The same shape Anthropic, OpenAI,
|
|
12
|
+
* and Stripe SDKs use.
|
|
13
|
+
*/
|
|
14
|
+
import { AbloAuthenticationError } from '../errors.js';
|
|
15
|
+
/**
|
|
16
|
+
* Read `process.env` defensively. Works in browser (where `process`
|
|
17
|
+
* is undefined), Node, and edge runtimes that expose a partial
|
|
18
|
+
* process polyfill.
|
|
19
|
+
*/
|
|
20
|
+
export function readProcessEnv() {
|
|
21
|
+
const maybeGlobal = globalThis;
|
|
22
|
+
return maybeGlobal.process?.env ?? {};
|
|
23
|
+
}
|
|
24
|
+
export function resolveApiKey(input) {
|
|
25
|
+
return input.options.apiKey ?? input.env.ABLO_API_KEY ?? null;
|
|
26
|
+
}
|
|
27
|
+
export function resolveAuthToken(input) {
|
|
28
|
+
return input.options.authToken ?? input.env.ABLO_AUTH_TOKEN ?? null;
|
|
29
|
+
}
|
|
30
|
+
export const ABLO_DEFAULT_BASE_URL = 'wss://mesh.ablo.finance';
|
|
31
|
+
export function resolveBaseURL(input) {
|
|
32
|
+
return (input.options.baseURL ?? input.env.ABLO_BASE_URL ?? ABLO_DEFAULT_BASE_URL);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Browser guard — apiKey is server-side-only by default. Same check
|
|
36
|
+
* Anthropic, OpenAI, and Stripe ship: shipping `sk_live_...` to a
|
|
37
|
+
* browser exposes it in every visitor's network tab. Consumers opt
|
|
38
|
+
* in explicitly when they have a publishable key or a server proxy.
|
|
39
|
+
*/
|
|
40
|
+
export function assertBrowserSafety(input) {
|
|
41
|
+
if (!input.dangerouslyAllowBrowser &&
|
|
42
|
+
typeof window !== 'undefined' &&
|
|
43
|
+
typeof input.apiKey === 'string' &&
|
|
44
|
+
input.apiKey.startsWith('sk_')) {
|
|
45
|
+
throw new AbloAuthenticationError("It looks like you're running in a browser-like environment.\n\n" +
|
|
46
|
+
'This is disabled by default — your secret API key would be ' +
|
|
47
|
+
"exposed to every visitor's network tab. If you understand the risks " +
|
|
48
|
+
'and have appropriate mitigations in place, you can set the ' +
|
|
49
|
+
'`dangerouslyAllowBrowser` option to `true`, e.g.,\n\n' +
|
|
50
|
+
' Ablo({ schema, apiKey, dangerouslyAllowBrowser: true });\n', { code: 'browser_apikey_blocked' });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve an `ApiKeySetter` callable to its current string value.
|
|
55
|
+
* Used at request time so a rotating credential picks up rotations
|
|
56
|
+
* between requests. Returns `null` when no key was configured.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors Anthropic's pattern of supporting both a static string and
|
|
59
|
+
* a callable for credential rotation.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveApiKeyValue(apiKey) {
|
|
62
|
+
if (apiKey == null)
|
|
63
|
+
return null;
|
|
64
|
+
if (typeof apiKey === 'function')
|
|
65
|
+
return apiKey();
|
|
66
|
+
return apiKey;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Translate a sync-engine WebSocket URL to the matching HTTP API
|
|
70
|
+
* base URL, defaulting to `${url}/api` when the caller hasn't
|
|
71
|
+
* overridden `bootstrapBaseUrl`. Used by `BootstrapHelper`,
|
|
72
|
+
* `HydrationCoordinator`, the apiKey-exchange flow, and the
|
|
73
|
+
* self-derived identity flow — same derivation in all four spots,
|
|
74
|
+
* so it lives here as a single source of truth.
|
|
75
|
+
*
|
|
76
|
+
* Note: when both `wss://` and `https://` are valid, `replace(/^ws/, 'http')`
|
|
77
|
+
* preserves the protocol family (ws → http, wss → https).
|
|
78
|
+
*/
|
|
79
|
+
export function resolveBootstrapBaseUrl(input) {
|
|
80
|
+
return input.bootstrapBaseUrl ?? `${input.url.replace(/^ws/, 'http')}/api`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal component construction for `Ablo()`.
|
|
3
|
+
*
|
|
4
|
+
* Builds the full sync-engine component graph from options + schema:
|
|
5
|
+
* `ModelRegistry`, `ObjectPool`, `BootstrapHelper`, `Database`,
|
|
6
|
+
* `SyncClient`, `HydrationCoordinator`. Each component depends on
|
|
7
|
+
* the previous one, so the construction order matters; isolating it
|
|
8
|
+
* here means `Ablo.ts` doesn't need to know the dependency order.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the pattern Anthropic uses: their client constructor
|
|
11
|
+
* imports each `Resource` class and wires it. Ours wires the
|
|
12
|
+
* sync-engine components instead.
|
|
13
|
+
*/
|
|
14
|
+
import { Database } from '../Database.js';
|
|
15
|
+
import { ModelRegistry } from '../ModelRegistry.js';
|
|
16
|
+
import { ObjectPool } from '../ObjectPool.js';
|
|
17
|
+
import { SyncClient } from '../SyncClient.js';
|
|
18
|
+
import { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
19
|
+
import { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
20
|
+
import type { Schema, SchemaRecord } from '../schema/schema.js';
|
|
21
|
+
import { type AbloPersistence } from './persistence.js';
|
|
22
|
+
export interface InternalComponentsInput<S extends SchemaRecord> {
|
|
23
|
+
readonly schema: Schema<S>;
|
|
24
|
+
/** WebSocket URL — used to derive bootstrap HTTP base when the
|
|
25
|
+
* caller didn't override `bootstrapBaseUrl`. */
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly options: {
|
|
28
|
+
readonly maxPoolSize?: number;
|
|
29
|
+
readonly bootstrapBaseUrl?: string;
|
|
30
|
+
readonly syncGroups?: string[];
|
|
31
|
+
readonly persistence?: AbloPersistence;
|
|
32
|
+
readonly offline?: boolean;
|
|
33
|
+
readonly inMemory?: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface InternalComponents {
|
|
37
|
+
readonly modelRegistry: ModelRegistry;
|
|
38
|
+
readonly objectPool: ObjectPool;
|
|
39
|
+
readonly bootstrapHelper: BootstrapHelper;
|
|
40
|
+
readonly database: Database;
|
|
41
|
+
readonly syncClient: SyncClient;
|
|
42
|
+
readonly hydration: HydrationCoordinator;
|
|
43
|
+
}
|
|
44
|
+
export declare function createInternalComponents<S extends SchemaRecord>(input: InternalComponentsInput<S>): InternalComponents;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal component construction for `Ablo()`.
|
|
3
|
+
*
|
|
4
|
+
* Builds the full sync-engine component graph from options + schema:
|
|
5
|
+
* `ModelRegistry`, `ObjectPool`, `BootstrapHelper`, `Database`,
|
|
6
|
+
* `SyncClient`, `HydrationCoordinator`. Each component depends on
|
|
7
|
+
* the previous one, so the construction order matters; isolating it
|
|
8
|
+
* here means `Ablo.ts` doesn't need to know the dependency order.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the pattern Anthropic uses: their client constructor
|
|
11
|
+
* imports each `Resource` class and wires it. Ours wires the
|
|
12
|
+
* sync-engine components instead.
|
|
13
|
+
*/
|
|
14
|
+
import { Database } from '../Database.js';
|
|
15
|
+
import { ModelRegistry, setActiveRegistry } from '../ModelRegistry.js';
|
|
16
|
+
import { ObjectPool } from '../ObjectPool.js';
|
|
17
|
+
import { SyncClient } from '../SyncClient.js';
|
|
18
|
+
import { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
19
|
+
import { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
20
|
+
import { resolveBootstrapBaseUrl } from './auth.js';
|
|
21
|
+
import { shouldUseInMemoryPersistence } from './persistence.js';
|
|
22
|
+
export function createInternalComponents(input) {
|
|
23
|
+
const { schema, url, options } = input;
|
|
24
|
+
// The registry is created here but model registration happens in
|
|
25
|
+
// the caller (Ablo.ts owns `registerModelsFromSchema` since the
|
|
26
|
+
// schema-to-class translation depends on private helpers there).
|
|
27
|
+
const modelRegistry = new ModelRegistry({
|
|
28
|
+
validateOnRegister: false,
|
|
29
|
+
allowLateReferences: true,
|
|
30
|
+
});
|
|
31
|
+
setActiveRegistry(modelRegistry);
|
|
32
|
+
const objectPool = new ObjectPool({ maxSize: options.maxPoolSize ?? 10000 }, modelRegistry);
|
|
33
|
+
const bootstrapBaseUrl = resolveBootstrapBaseUrl({
|
|
34
|
+
url,
|
|
35
|
+
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
36
|
+
});
|
|
37
|
+
const bootstrapHelper = new BootstrapHelper({
|
|
38
|
+
baseUrl: bootstrapBaseUrl,
|
|
39
|
+
syncGroups: options.syncGroups,
|
|
40
|
+
instantModels: deriveInstantModels(schema),
|
|
41
|
+
});
|
|
42
|
+
const database = new Database(modelRegistry, bootstrapHelper, {
|
|
43
|
+
// Point-solution default: no browser-local durable store unless the
|
|
44
|
+
// caller explicitly asks for it. Node/edge runtimes always use the
|
|
45
|
+
// volatile store because IndexedDB is unavailable there.
|
|
46
|
+
inMemory: shouldUseInMemoryPersistence(options),
|
|
47
|
+
});
|
|
48
|
+
const syncClient = new SyncClient(objectPool, database);
|
|
49
|
+
// Lazy-load lane: hydrates pool/IDB on `ablo.<model>.load(...)` for
|
|
50
|
+
// entities not in scope at bootstrap (`load: 'lazy'` models, or
|
|
51
|
+
// entities accessed via deep-link before the pool warmed up).
|
|
52
|
+
// Single-flight + IDB write-through.
|
|
53
|
+
const hydration = new HydrationCoordinator({
|
|
54
|
+
objectPool,
|
|
55
|
+
database,
|
|
56
|
+
registry: modelRegistry,
|
|
57
|
+
schema,
|
|
58
|
+
baseUrl: bootstrapBaseUrl,
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
modelRegistry,
|
|
62
|
+
objectPool,
|
|
63
|
+
bootstrapHelper,
|
|
64
|
+
database,
|
|
65
|
+
syncClient,
|
|
66
|
+
hydration,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Derive instant-bootstrap model names from schema load strategies.
|
|
71
|
+
* Models with `load: 'lazy'` or `'manual'` are excluded from the
|
|
72
|
+
* initial bootstrap request — they're fetched on demand by the
|
|
73
|
+
* `ensure*` loaders or (Phase 6) by the `ObjectPool` auto-fetch
|
|
74
|
+
* mechanism. Default load strategy is `'instant'`.
|
|
75
|
+
*/
|
|
76
|
+
function deriveInstantModels(schema) {
|
|
77
|
+
const schemaModels = schema.models ?? schema;
|
|
78
|
+
return Object.entries(schemaModels).flatMap(([key, def]) => {
|
|
79
|
+
if (!def || typeof def !== 'object' || !('load' in def)) {
|
|
80
|
+
return [key]; // no load → instant
|
|
81
|
+
}
|
|
82
|
+
const load = def.load;
|
|
83
|
+
if (!load || load === 'instant') {
|
|
84
|
+
return [def.typename ?? key];
|
|
85
|
+
}
|
|
86
|
+
return []; // lazy or manual → skip
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-model resource factory.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Anthropic SDK's `resources/messages.ts` / `resources/models.ts`
|
|
5
|
+
* pattern: each resource has its own file, the client just instantiates
|
|
6
|
+
* one per model. Extracted from `Ablo.ts` so the proxy logic is
|
|
7
|
+
* testable in isolation and the constructor doesn't carry it.
|
|
8
|
+
*
|
|
9
|
+
* Each schema model gets one `ModelOperations<T, CreateInput>` —
|
|
10
|
+
* exposes `retrieve`, `list`, `count`, `create`, `update`, `delete`,
|
|
11
|
+
* `edit`,
|
|
12
|
+
* `subscribe`, and `load`. The factory returns a plain object; the
|
|
13
|
+
* client assembles `ablo.<model>` lookup table from these.
|
|
14
|
+
*/
|
|
15
|
+
import type { MutationOptions } from '../interfaces/index.js';
|
|
16
|
+
import type { ModelRegistry } from '../ModelRegistry.js';
|
|
17
|
+
import type { ObjectPool } from '../ObjectPool.js';
|
|
18
|
+
import type { SyncClient } from '../SyncClient.js';
|
|
19
|
+
import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
20
|
+
import type { LoadWhere } from '../query/types.js';
|
|
21
|
+
import { ModelScope } from '../types/index.js';
|
|
22
|
+
import type { Duration, Snapshot } from '../types/streams.js';
|
|
23
|
+
export interface ModelResourceMeta {
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly typename: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function getModelResourceMeta(resource: unknown): ModelResourceMeta | undefined;
|
|
28
|
+
export type ModelListScope = ModelScope | 'live' | 'archived' | 'all';
|
|
29
|
+
export interface ModelListOptions<T> {
|
|
30
|
+
where?: Partial<T>;
|
|
31
|
+
/** Arbitrary local predicate. Applied after `where`. */
|
|
32
|
+
filter?: (entity: T) => boolean;
|
|
33
|
+
orderBy?: {
|
|
34
|
+
[K in keyof T]?: 'asc' | 'desc';
|
|
35
|
+
};
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
/** Lifecycle scope. Defaults to live rows. */
|
|
39
|
+
scope?: ModelListScope;
|
|
40
|
+
}
|
|
41
|
+
export type ModelCountOptions<T> = Pick<ModelListOptions<T>, 'where' | 'filter' | 'scope'>;
|
|
42
|
+
export interface ModelLoadOptions<T> {
|
|
43
|
+
/**
|
|
44
|
+
* Filter for the lookup. Accepts:
|
|
45
|
+
* - object form: `{ name: 'foo' }` (equality, array values → `IN`)
|
|
46
|
+
* - tuple form: `[['name', 'ILIKE', '%Goldman%']]` for operators
|
|
47
|
+
*
|
|
48
|
+
* See `LoadWhere<T>` in `query/types.ts`. For OR semantics, run two
|
|
49
|
+
* `load()` calls and union — the wire protocol is AND-only.
|
|
50
|
+
*/
|
|
51
|
+
where?: LoadWhere<T>;
|
|
52
|
+
orderBy?: {
|
|
53
|
+
[K in keyof T]?: 'asc' | 'desc';
|
|
54
|
+
};
|
|
55
|
+
limit?: number;
|
|
56
|
+
/**
|
|
57
|
+
* `complete` waits for the server. `unknown` returns whatever is local
|
|
58
|
+
* immediately and refreshes in the background.
|
|
59
|
+
*/
|
|
60
|
+
type?: 'complete' | 'unknown';
|
|
61
|
+
/**
|
|
62
|
+
* Schema-declared relation names to hydrate alongside the primary
|
|
63
|
+
* rows. The server's compiler resolves each name via the schema's
|
|
64
|
+
* relation metadata (`relation.belongsTo` / `relation.hasMany`)
|
|
65
|
+
* and emits the JOIN.
|
|
66
|
+
*/
|
|
67
|
+
expand?: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
export interface ModelEditOptions<T = Record<string, unknown>> {
|
|
70
|
+
/**
|
|
71
|
+
* Human-readable activity shown to other participants while this handle
|
|
72
|
+
* is open. Examples: `editing`, `summarizing`, `rewriting`, `reviewing`.
|
|
73
|
+
*/
|
|
74
|
+
activity?: string;
|
|
75
|
+
/** Optional field-level target for UI affordances such as busy badges. */
|
|
76
|
+
field?: keyof T & string;
|
|
77
|
+
/** Lease duration for the visible activity. Runtime death is cleaned up by TTL. */
|
|
78
|
+
ttl?: Duration;
|
|
79
|
+
/** Default wait mode for `handle.update(...)`. Defaults to `confirmed`. */
|
|
80
|
+
wait?: MutationOptions['wait'];
|
|
81
|
+
}
|
|
82
|
+
export interface ModelEditHandle<T> extends AsyncDisposable {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly intentId: string;
|
|
85
|
+
readonly activity: string;
|
|
86
|
+
readonly current: T;
|
|
87
|
+
readonly signal: AbortSignal;
|
|
88
|
+
update(data: Partial<T>, options?: MutationOptions): Promise<T>;
|
|
89
|
+
release(): Promise<void>;
|
|
90
|
+
revoke(): void;
|
|
91
|
+
}
|
|
92
|
+
export interface ModelIntentHandle {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
release(): Promise<void>;
|
|
95
|
+
revoke(): void;
|
|
96
|
+
}
|
|
97
|
+
export interface ModelCollaboration<T> {
|
|
98
|
+
createIntent(options: {
|
|
99
|
+
target: {
|
|
100
|
+
resource: string;
|
|
101
|
+
id: string;
|
|
102
|
+
field?: string;
|
|
103
|
+
};
|
|
104
|
+
action: string;
|
|
105
|
+
ttl?: Duration;
|
|
106
|
+
}): Promise<ModelIntentHandle>;
|
|
107
|
+
createSnapshot(modelKey: string, id: string): Snapshot;
|
|
108
|
+
}
|
|
109
|
+
export interface ModelOperations<T, CreateInput> {
|
|
110
|
+
/**
|
|
111
|
+
* Retrieve a single entity by id from the local pool. Synchronous.
|
|
112
|
+
* Returns `undefined` when the entity isn't loaded yet — use
|
|
113
|
+
* `load({where: {id}})` if you want to lazy-hydrate from storage/network.
|
|
114
|
+
*
|
|
115
|
+
* Mirrors `stripe.customers.retrieve(id)`.
|
|
116
|
+
*/
|
|
117
|
+
retrieve(id: string): T | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* List entities matching a filter from the local pool. Synchronous.
|
|
120
|
+
* No network round-trip — use `load()` for hydration.
|
|
121
|
+
*
|
|
122
|
+
* Mirrors `stripe.customers.list({...})`.
|
|
123
|
+
*/
|
|
124
|
+
list(options?: ModelListOptions<T>): T[];
|
|
125
|
+
/** Count entities matching a filter (synchronous, from local pool). */
|
|
126
|
+
count(options?: ModelCountOptions<T>): number;
|
|
127
|
+
/**
|
|
128
|
+
* Create a new entity — **optimistic, offline-first**. Resolves once
|
|
129
|
+
* the mutation is queued locally, not when the server confirms.
|
|
130
|
+
* Server rejection rolls back automatically; watch `sync.syncStatus`.
|
|
131
|
+
*/
|
|
132
|
+
create(data: CreateInput, options?: MutationOptions): Promise<T>;
|
|
133
|
+
/** Update an entity by id — optimistic, offline-first (see `create`). */
|
|
134
|
+
update(id: string, data: Partial<T>, options?: MutationOptions): Promise<T>;
|
|
135
|
+
/** Delete an entity by id — optimistic, offline-first (see `create`). */
|
|
136
|
+
delete(id: string, options?: MutationOptions): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Start a model-scoped activity lease for long-running AI or background work.
|
|
139
|
+
* Other participants can see the activity until `update`, `release`, or TTL.
|
|
140
|
+
*/
|
|
141
|
+
edit(id: string, options?: ModelEditOptions<T>): Promise<ModelEditHandle<T>>;
|
|
142
|
+
/** Subscribe to changes (callback called on every change). */
|
|
143
|
+
subscribe(callback: (entities: T[]) => void, options?: ModelListOptions<T>): () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Load matching rows into the local graph if they are not already
|
|
146
|
+
* present. Single-flight: concurrent calls with the same args share
|
|
147
|
+
* one in-flight request. Default `type: 'complete'` waits for the
|
|
148
|
+
* server; `type: 'unknown'` returns local + refreshes async.
|
|
149
|
+
*/
|
|
150
|
+
load(options?: ModelLoadOptions<T>): Promise<T[]>;
|
|
151
|
+
}
|
|
152
|
+
export declare function createModelProxy<T, C>(schemaKey: string, registeredModelName: string, objectPool: ObjectPool, syncClient: SyncClient, registry: ModelRegistry, hydration: HydrationCoordinator, collaboration?: ModelCollaboration<T>): ModelOperations<T, C>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-model resource factory.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Anthropic SDK's `resources/messages.ts` / `resources/models.ts`
|
|
5
|
+
* pattern: each resource has its own file, the client just instantiates
|
|
6
|
+
* one per model. Extracted from `Ablo.ts` so the proxy logic is
|
|
7
|
+
* testable in isolation and the constructor doesn't carry it.
|
|
8
|
+
*
|
|
9
|
+
* Each schema model gets one `ModelOperations<T, CreateInput>` —
|
|
10
|
+
* exposes `retrieve`, `list`, `count`, `create`, `update`, `delete`,
|
|
11
|
+
* `edit`,
|
|
12
|
+
* `subscribe`, and `load`. The factory returns a plain object; the
|
|
13
|
+
* client assembles `ablo.<model>` lookup table from these.
|
|
14
|
+
*/
|
|
15
|
+
import { autorun } from 'mobx';
|
|
16
|
+
import { AbloStaleContextError, AbloValidationError } from '../errors.js';
|
|
17
|
+
import { Model, modelAsRow } from '../Model.js';
|
|
18
|
+
import { ModelScope } from '../types/index.js';
|
|
19
|
+
const modelResourceMeta = new WeakMap();
|
|
20
|
+
export function getModelResourceMeta(resource) {
|
|
21
|
+
if (typeof resource !== 'object' || resource === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
return modelResourceMeta.get(resource);
|
|
24
|
+
}
|
|
25
|
+
export function createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, registry, hydration, collaboration) {
|
|
26
|
+
const ModelClass = registry.getModelByName(registeredModelName);
|
|
27
|
+
if (!ModelClass) {
|
|
28
|
+
throw new AbloValidationError(`Ablo: schema model "${schemaKey}" resolved to "${registeredModelName}", ` +
|
|
29
|
+
'but no matching constructor was registered.', { code: 'model_not_registered' });
|
|
30
|
+
}
|
|
31
|
+
const load = async (options) => {
|
|
32
|
+
const rows = await hydration.fetch(schemaKey, options);
|
|
33
|
+
// The coordinator returns Model instances. ModelOperations is
|
|
34
|
+
// typed against the schema-inferred row shape (`T`), which is
|
|
35
|
+
// structurally what the model exposes through its property
|
|
36
|
+
// accessors — cast at the boundary.
|
|
37
|
+
return rows;
|
|
38
|
+
};
|
|
39
|
+
const waitForMutation = async (model, options) => {
|
|
40
|
+
if (options?.wait !== 'confirmed')
|
|
41
|
+
return;
|
|
42
|
+
await syncClient.syncNow();
|
|
43
|
+
await syncClient.waitForConfirmation(model.getModelName(), model.id);
|
|
44
|
+
};
|
|
45
|
+
const operations = {
|
|
46
|
+
retrieve(id) {
|
|
47
|
+
return objectPool.get(id);
|
|
48
|
+
},
|
|
49
|
+
list(options) {
|
|
50
|
+
const all = objectPool.getByType(ModelClass, (options?.scope ?? ModelScope.live));
|
|
51
|
+
let result = all;
|
|
52
|
+
if (options?.where) {
|
|
53
|
+
const where = options.where;
|
|
54
|
+
result = result.filter((item) => {
|
|
55
|
+
for (const [key, value] of Object.entries(where)) {
|
|
56
|
+
if (item[key] !== value)
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (options?.filter) {
|
|
63
|
+
result = result.filter(options.filter);
|
|
64
|
+
}
|
|
65
|
+
if (options?.orderBy) {
|
|
66
|
+
const [field, dir] = Object.entries(options.orderBy)[0];
|
|
67
|
+
result = [...result].sort((a, b) => {
|
|
68
|
+
const av = a[field];
|
|
69
|
+
const bv = b[field];
|
|
70
|
+
if (av == null || bv == null)
|
|
71
|
+
return 0;
|
|
72
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
73
|
+
return dir === 'desc' ? -cmp : cmp;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (options?.offset)
|
|
77
|
+
result = result.slice(options.offset);
|
|
78
|
+
if (options?.limit)
|
|
79
|
+
result = result.slice(0, options.limit);
|
|
80
|
+
return result;
|
|
81
|
+
},
|
|
82
|
+
count(options) {
|
|
83
|
+
return this.list(options).length;
|
|
84
|
+
},
|
|
85
|
+
async create(data, options) {
|
|
86
|
+
// TODO(options-persistence): stash `options` alongside the
|
|
87
|
+
// queued transaction so idempotencyKey survives offline flush.
|
|
88
|
+
const model = new ModelClass({
|
|
89
|
+
id: Model.generateId(),
|
|
90
|
+
...data,
|
|
91
|
+
createdAt: new Date(),
|
|
92
|
+
updatedAt: new Date(),
|
|
93
|
+
});
|
|
94
|
+
syncClient.add(model, options);
|
|
95
|
+
await waitForMutation(model, options);
|
|
96
|
+
return modelAsRow(model);
|
|
97
|
+
},
|
|
98
|
+
async update(id, data, options) {
|
|
99
|
+
const model = objectPool.get(id);
|
|
100
|
+
if (!model)
|
|
101
|
+
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
102
|
+
model.updateFromData(data);
|
|
103
|
+
syncClient.update(model, options);
|
|
104
|
+
await waitForMutation(model, options);
|
|
105
|
+
return modelAsRow(model);
|
|
106
|
+
},
|
|
107
|
+
async delete(id, options) {
|
|
108
|
+
const model = objectPool.get(id);
|
|
109
|
+
if (!model)
|
|
110
|
+
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
111
|
+
syncClient.delete(model, options);
|
|
112
|
+
await waitForMutation(model, options);
|
|
113
|
+
},
|
|
114
|
+
async edit(id, options) {
|
|
115
|
+
if (!collaboration) {
|
|
116
|
+
throw new AbloValidationError(`Model "${schemaKey}" cannot start edit activity without collaboration wiring.`, { code: 'model_edit_not_configured' });
|
|
117
|
+
}
|
|
118
|
+
let model = objectPool.get(id);
|
|
119
|
+
if (!model) {
|
|
120
|
+
await load({ where: { id } });
|
|
121
|
+
model = objectPool.get(id);
|
|
122
|
+
}
|
|
123
|
+
if (!model) {
|
|
124
|
+
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
125
|
+
}
|
|
126
|
+
const activity = options?.activity ?? 'editing';
|
|
127
|
+
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
128
|
+
const intent = await collaboration.createIntent({
|
|
129
|
+
target: {
|
|
130
|
+
resource: schemaKey,
|
|
131
|
+
id,
|
|
132
|
+
...(options?.field ? { field: options.field } : {}),
|
|
133
|
+
},
|
|
134
|
+
action: activity,
|
|
135
|
+
ttl: options?.ttl,
|
|
136
|
+
});
|
|
137
|
+
let released = false;
|
|
138
|
+
const revoke = () => {
|
|
139
|
+
if (released)
|
|
140
|
+
return;
|
|
141
|
+
released = true;
|
|
142
|
+
snapshot.signal.removeEventListener('abort', revoke);
|
|
143
|
+
intent.revoke();
|
|
144
|
+
};
|
|
145
|
+
const release = async () => {
|
|
146
|
+
if (released)
|
|
147
|
+
return;
|
|
148
|
+
released = true;
|
|
149
|
+
snapshot.signal.removeEventListener('abort', revoke);
|
|
150
|
+
await intent.release();
|
|
151
|
+
};
|
|
152
|
+
snapshot.signal.addEventListener('abort', revoke, { once: true });
|
|
153
|
+
const handle = {
|
|
154
|
+
id,
|
|
155
|
+
intentId: intent.id,
|
|
156
|
+
activity,
|
|
157
|
+
current: modelAsRow(model),
|
|
158
|
+
signal: snapshot.signal,
|
|
159
|
+
async update(data, updateOptions) {
|
|
160
|
+
if (snapshot.signal.aborted) {
|
|
161
|
+
throw new AbloStaleContextError(`Edit context is stale for ${schemaKey}/${id}. Re-read the row and retry.`, {
|
|
162
|
+
code: 'edit_context_stale',
|
|
163
|
+
readAt: snapshot.stamp,
|
|
164
|
+
cause: snapshot.signal.reason,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return await operations.update(id, data, {
|
|
169
|
+
wait: options?.wait ?? 'confirmed',
|
|
170
|
+
readAt: snapshot.stamp,
|
|
171
|
+
onStale: 'reject',
|
|
172
|
+
...updateOptions,
|
|
173
|
+
intent,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await release();
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
release,
|
|
181
|
+
revoke,
|
|
182
|
+
[Symbol.asyncDispose]: release,
|
|
183
|
+
};
|
|
184
|
+
return handle;
|
|
185
|
+
},
|
|
186
|
+
subscribe(callback, options) {
|
|
187
|
+
return autorun(() => {
|
|
188
|
+
const entities = this.list(options);
|
|
189
|
+
callback(entities);
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
load,
|
|
193
|
+
};
|
|
194
|
+
modelResourceMeta.set(operations, {
|
|
195
|
+
key: schemaKey,
|
|
196
|
+
typename: registeredModelName,
|
|
197
|
+
});
|
|
198
|
+
return operations;
|
|
199
|
+
}
|