@abloatai/ablo 0.7.0 → 0.9.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 +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +7 -3
- package/dist/schema/serialize.js +6 -2
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
package/dist/client/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type ClaimedOptions, type IfClaimedPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ModelReadOptions, } from './Ablo.js';
|
|
33
|
+
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './auth.js';
|
|
33
34
|
export type { AbloPersistence } from './persistence.js';
|
|
34
35
|
export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
|
|
35
36
|
export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
|
package/dist/client/index.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface RegisterDataSourceInput {
|
|
2
|
+
/** HTTP API base, e.g. `https://api.abloatai.com/api` (from resolveBootstrapBaseUrl). */
|
|
3
|
+
readonly baseUrl: string;
|
|
4
|
+
/** Secret key (`sk_…`) used to authenticate + derive the org. */
|
|
5
|
+
readonly apiKey: string | null;
|
|
6
|
+
/** Postgres connection string for the direct connector. */
|
|
7
|
+
readonly databaseUrl: string;
|
|
8
|
+
/** Optional Postgres schema (defaults server-side to `public`). */
|
|
9
|
+
readonly schema?: string;
|
|
10
|
+
/** Custom fetch (tests/proxies/odd runtimes). */
|
|
11
|
+
readonly fetchImpl?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* POST the connection string to the self-serve direct connector route. Resolves
|
|
15
|
+
* on success (the org is now a dedicated tenant pointed at this DB); throws an
|
|
16
|
+
* `AbloError` with `datasource_registration_failed` otherwise so `ready()`
|
|
17
|
+
* surfaces it instead of silently bootstrapping against the wrong store.
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerDataSource(input: RegisterDataSourceInput): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-serve direct-Postgres connector registration.
|
|
3
|
+
*
|
|
4
|
+
* Historical note: this module name says "DataSource", but this path registers
|
|
5
|
+
* a direct database URL. It is not the signed `dataSource(...)` endpoint path.
|
|
6
|
+
*
|
|
7
|
+
* When a client is constructed with `databaseUrl`, the SDK registers that
|
|
8
|
+
* connection string BEFORE bootstrap so the server resolves the org's data plane
|
|
9
|
+
* to that direct connector.
|
|
10
|
+
*
|
|
11
|
+
* The org is derived server-side from the API key — the caller never sends an
|
|
12
|
+
* organization id. The connection string is sent once over TLS and is never
|
|
13
|
+
* echoed back (the server stores it as a secret and returns only a safe
|
|
14
|
+
* `datasource` projection: host, database, schema).
|
|
15
|
+
*/
|
|
16
|
+
import { AbloError } from '../errors.js';
|
|
17
|
+
/**
|
|
18
|
+
* POST the connection string to the self-serve direct connector route. Resolves
|
|
19
|
+
* on success (the org is now a dedicated tenant pointed at this DB); throws an
|
|
20
|
+
* `AbloError` with `datasource_registration_failed` otherwise so `ready()`
|
|
21
|
+
* surfaces it instead of silently bootstrapping against the wrong store.
|
|
22
|
+
*/
|
|
23
|
+
export async function registerDataSource(input) {
|
|
24
|
+
if (!input.apiKey) {
|
|
25
|
+
throw new AbloError('databaseUrl requires an apiKey to register the direct Postgres connector (the org is derived from the key).', { code: 'datasource_registration_failed' });
|
|
26
|
+
}
|
|
27
|
+
const doFetch = input.fetchImpl ?? fetch;
|
|
28
|
+
const endpoint = `${input.baseUrl.replace(/\/+$/, '')}/v1/datasource`;
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await doFetch(endpoint, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'content-type': 'application/json',
|
|
35
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
connectionString: input.databaseUrl,
|
|
39
|
+
...(input.schema ? { schema: input.schema } : {}),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (cause) {
|
|
44
|
+
throw new AbloError('Could not reach the Ablo API to register the direct Postgres connector.', {
|
|
45
|
+
code: 'datasource_registration_failed',
|
|
46
|
+
cause,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
let detail = '';
|
|
51
|
+
try {
|
|
52
|
+
detail = (await response.text()).slice(0, 500);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// ignore body read failures — the status alone is enough to fail loud
|
|
56
|
+
}
|
|
57
|
+
throw new AbloError(`Direct Postgres connector registration failed (HTTP ${response.status}). ${detail}`, { code: 'datasource_registration_failed', httpStatus: response.status });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* because the error messages reference URLs and would mislead if a
|
|
11
11
|
* URL was actually present.
|
|
12
12
|
*/
|
|
13
|
+
import { AbloError } from '../errors.js';
|
|
13
14
|
/**
|
|
14
15
|
* Minimal subset of `AbloOptions` the validator actually inspects.
|
|
15
16
|
* Defined here as its own interface so the validator doesn't pull
|
|
@@ -39,4 +40,4 @@ export interface ValidateAbloOptionsInput {
|
|
|
39
40
|
readonly configuredApiKey: unknown;
|
|
40
41
|
readonly configuredAuthToken: unknown;
|
|
41
42
|
}
|
|
42
|
-
export declare function validateAbloOptions(input: ValidateAbloOptionsInput):
|
|
43
|
+
export declare function validateAbloOptions(input: ValidateAbloOptionsInput): AbloError | null;
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* because the error messages reference URLs and would mislead if a
|
|
11
11
|
* URL was actually present.
|
|
12
12
|
*/
|
|
13
|
+
import { AbloValidationError } from '../errors.js';
|
|
13
14
|
export function validateAbloOptions(input) {
|
|
14
15
|
const { options, url, configuredApiKey, configuredAuthToken } = input;
|
|
15
16
|
const kind = options.kind ?? 'user';
|
|
16
17
|
if (!url) {
|
|
17
|
-
return new
|
|
18
|
-
`Ablo({ baseURL: 'wss://
|
|
18
|
+
return new AbloValidationError('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
|
|
19
|
+
`Ablo({ baseURL: 'wss://api.abloatai.com', schema, user })`, { code: 'base_url_missing' });
|
|
19
20
|
}
|
|
20
21
|
// Schema is optional for the model-first API:
|
|
21
22
|
// Ablo({ apiKey }).model('clauses').retrieve(...)
|
|
@@ -26,18 +27,18 @@ export function validateAbloOptions(input) {
|
|
|
26
27
|
kind === 'user' &&
|
|
27
28
|
options.user &&
|
|
28
29
|
!options.user.id) {
|
|
29
|
-
return new
|
|
30
|
+
return new AbloValidationError('Ablo: `user.id` must be a non-empty string when `user` is provided.', { code: 'invalid_options', param: 'user.id' });
|
|
30
31
|
}
|
|
31
32
|
if (!configuredApiKey && !configuredAuthToken && kind === 'agent' && !options.agentId) {
|
|
32
|
-
return new
|
|
33
|
+
return new AbloValidationError('Ablo: provide either `apiKey` or `agentId` for `kind: "agent"`. ' +
|
|
33
34
|
'Hosted-cloud consumers pass `apiKey` and the server derives the ' +
|
|
34
35
|
'agent identity from its scope; self-hosted passes `agentId` + ' +
|
|
35
|
-
'`capabilityToken` directly.');
|
|
36
|
+
'`capabilityToken` directly.', { code: 'invalid_options', param: 'agentId' });
|
|
36
37
|
}
|
|
37
38
|
if (!configuredApiKey && !configuredAuthToken && kind === 'agent' && !options.capabilityToken) {
|
|
38
|
-
return new
|
|
39
|
+
return new AbloValidationError('Ablo: provide either `apiKey` (hosted cloud — SDK exchanges internally) ' +
|
|
39
40
|
'or `capabilityToken` (self-hosted — your auth layer mints + hands in). ' +
|
|
40
|
-
'See https://abloatai.com/docs/api-keys for the full pattern.');
|
|
41
|
+
'See https://abloatai.com/docs/api-keys for the full pattern.', { code: 'invalid_options', param: 'capabilityToken' });
|
|
41
42
|
}
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Follows Ablo's architecture for database management.
|
|
9
9
|
*/
|
|
10
10
|
import { getContext } from '../context.js';
|
|
11
|
-
import { openIDBWithTimeout } from './openIDBWithTimeout.js';
|
|
11
|
+
import { openIDBWithTimeout, deleteIDBWithTimeout, IDBOpenTimeoutError, } from './openIDBWithTimeout.js';
|
|
12
12
|
import { AbloConnectionError } from '../errors.js';
|
|
13
13
|
import { getActiveRegistry, hasActiveRegistry } from '../ModelRegistry.js';
|
|
14
14
|
/**
|
|
@@ -30,7 +30,7 @@ export class DatabaseManager {
|
|
|
30
30
|
* Initialize the meta database (ablo_databases)
|
|
31
31
|
*/
|
|
32
32
|
async initializeMetaDatabase() {
|
|
33
|
-
|
|
33
|
+
const open = () => openIDBWithTimeout(this.metaDbName, 1, {
|
|
34
34
|
onUpgrade: (request) => {
|
|
35
35
|
const db = request.result;
|
|
36
36
|
if (!db.objectStoreNames.contains('databases')) {
|
|
@@ -42,6 +42,34 @@ export class DatabaseManager {
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
+
try {
|
|
46
|
+
this.metaDb = await open();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Self-heal a wedged meta DB. When `ablo_databases`'s backing store gets
|
|
50
|
+
// stuck (a corrupted store, or a leaked connection from a prior
|
|
51
|
+
// timed-out open), every open of that name hangs with no event and the
|
|
52
|
+
// app is permanently bricked until the user manually clears site data —
|
|
53
|
+
// the "open did not resolve within 10000ms" dead end. The registry this
|
|
54
|
+
// DB holds is rebuildable from the server on the next bootstrap, so it is
|
|
55
|
+
// safe to delete and re-create. Try exactly once: delete, then re-open.
|
|
56
|
+
if (!(error instanceof IDBOpenTimeoutError))
|
|
57
|
+
throw error;
|
|
58
|
+
getContext().logger.warn('[sync-engine] meta DB open timed out — attempting self-heal (delete + retry)', { db: this.metaDbName, reason: error.reason });
|
|
59
|
+
getContext().observability.captureBootstrapFailure(error, {
|
|
60
|
+
type: 'meta-db-open-timeout',
|
|
61
|
+
});
|
|
62
|
+
const deleted = await deleteIDBWithTimeout(this.metaDbName);
|
|
63
|
+
if (!deleted) {
|
|
64
|
+
// The delete itself was blocked/stuck — a live connection in another
|
|
65
|
+
// window or a deadlocked backing store. We cannot recover in-page;
|
|
66
|
+
// rethrow so the provider surfaces the real (now actionable) error.
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
// Fresh store — this open creates `ablo_databases` from scratch.
|
|
70
|
+
this.metaDb = await open();
|
|
71
|
+
getContext().logger.info('[sync-engine] meta DB self-heal succeeded');
|
|
72
|
+
}
|
|
45
73
|
}
|
|
46
74
|
/**
|
|
47
75
|
* Calculate database info for a user/workspace combination
|
|
@@ -16,12 +16,48 @@
|
|
|
16
16
|
export declare class IDBOpenTimeoutError extends Error {
|
|
17
17
|
readonly dbName: string;
|
|
18
18
|
readonly reason: 'blocked' | 'timeout';
|
|
19
|
+
/**
|
|
20
|
+
* Stable, transport-independent code. `toAbloError` preserves a string
|
|
21
|
+
* `.code`, so this survives the wrap into `AbloError` and reaches the
|
|
22
|
+
* provider's `onError` intact — letting the app distinguish a wedged-storage
|
|
23
|
+
* failure (show a recovery screen) from any other bootstrap error without a
|
|
24
|
+
* brittle message match.
|
|
25
|
+
*/
|
|
26
|
+
readonly code = "storage_open_timeout";
|
|
19
27
|
constructor(dbName: string, reason: 'blocked' | 'timeout', message: string);
|
|
20
28
|
}
|
|
29
|
+
/** True for the wedged-IndexedDB failure, after it has been wrapped anywhere. */
|
|
30
|
+
export declare function isStorageOpenTimeout(err: unknown): boolean;
|
|
21
31
|
export interface OpenIDBOptions {
|
|
22
32
|
/** Called inside `onupgradeneeded` — mirrors `IDBOpenDBRequest.onupgradeneeded`. */
|
|
23
33
|
onUpgrade?: (request: IDBOpenDBRequest, event: IDBVersionChangeEvent) => void;
|
|
24
34
|
/** Max milliseconds to wait for the open request to resolve. Default 10_000. */
|
|
25
35
|
timeoutMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Called when another context (a new tab, a fresh deploy, or our own
|
|
38
|
+
* `deleteIDBWithTimeout` self-heal) fires `versionchange` on this connection.
|
|
39
|
+
* By default the connection is `close()`d immediately — the W3C/MDN-mandated
|
|
40
|
+
* behavior that lets the other context's upgrade/delete proceed instead of
|
|
41
|
+
* blocking forever. Provide this to ALSO react (e.g. prompt a reload) AFTER
|
|
42
|
+
* the close. Throwing here is swallowed.
|
|
43
|
+
*/
|
|
44
|
+
onVersionChange?: () => void;
|
|
26
45
|
}
|
|
27
46
|
export declare function openIDBWithTimeout(name: string, version: number | undefined, options?: OpenIDBOptions): Promise<IDBDatabase>;
|
|
47
|
+
/**
|
|
48
|
+
* Bounded `indexedDB.deleteDatabase()` — the delete counterpart of
|
|
49
|
+
* `openIDBWithTimeout`. Used by the meta-DB self-heal: when opening
|
|
50
|
+
* `ablo_databases` times out (a wedged backing store), we attempt to delete it
|
|
51
|
+
* and re-create from scratch. The registry it holds is rebuildable from the
|
|
52
|
+
* server on the next bootstrap, so dropping it is safe.
|
|
53
|
+
*
|
|
54
|
+
* Like `open`, `deleteDatabase` can hang indefinitely: if another live
|
|
55
|
+
* connection holds the DB it fires `onblocked` and waits, and on a truly stuck
|
|
56
|
+
* store it fires *no* event at all. Both become a bounded rejection here so the
|
|
57
|
+
* caller can fall through to surfacing a real error instead of spinning.
|
|
58
|
+
*
|
|
59
|
+
* Resolves `true` on a clean delete, `false` if it was blocked or timed out
|
|
60
|
+
* (caller decides whether to retry the open regardless — a no-op delete still
|
|
61
|
+
* leaves us no worse off).
|
|
62
|
+
*/
|
|
63
|
+
export declare function deleteIDBWithTimeout(name: string, timeoutMs?: number): Promise<boolean>;
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
export class IDBOpenTimeoutError extends Error {
|
|
17
17
|
dbName;
|
|
18
18
|
reason;
|
|
19
|
+
/**
|
|
20
|
+
* Stable, transport-independent code. `toAbloError` preserves a string
|
|
21
|
+
* `.code`, so this survives the wrap into `AbloError` and reaches the
|
|
22
|
+
* provider's `onError` intact — letting the app distinguish a wedged-storage
|
|
23
|
+
* failure (show a recovery screen) from any other bootstrap error without a
|
|
24
|
+
* brittle message match.
|
|
25
|
+
*/
|
|
26
|
+
code = 'storage_open_timeout';
|
|
19
27
|
constructor(dbName, reason, message) {
|
|
20
28
|
super(message);
|
|
21
29
|
this.dbName = dbName;
|
|
@@ -23,6 +31,12 @@ export class IDBOpenTimeoutError extends Error {
|
|
|
23
31
|
this.name = 'IDBOpenTimeoutError';
|
|
24
32
|
}
|
|
25
33
|
}
|
|
34
|
+
/** True for the wedged-IndexedDB failure, after it has been wrapped anywhere. */
|
|
35
|
+
export function isStorageOpenTimeout(err) {
|
|
36
|
+
return (typeof err === 'object' &&
|
|
37
|
+
err !== null &&
|
|
38
|
+
err.code === 'storage_open_timeout');
|
|
39
|
+
}
|
|
26
40
|
export function openIDBWithTimeout(name, version, options = {}) {
|
|
27
41
|
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
28
42
|
return new Promise((resolve, reject) => {
|
|
@@ -42,7 +56,47 @@ export function openIDBWithTimeout(name, version, options = {}) {
|
|
|
42
56
|
options.onUpgrade(request, event);
|
|
43
57
|
};
|
|
44
58
|
}
|
|
45
|
-
request.onsuccess = () =>
|
|
59
|
+
request.onsuccess = () => {
|
|
60
|
+
// If we ALREADY timed out (or blocked) and rejected, this is a late
|
|
61
|
+
// success: the native open eventually completed after we gave up. The
|
|
62
|
+
// resulting connection is orphaned — nobody up the stack holds it, so
|
|
63
|
+
// nobody will `.close()` it. A leaked open connection holds an IndexedDB
|
|
64
|
+
// lock that wedges every subsequent open/delete of this DB name (the
|
|
65
|
+
// exact "ablo_databases open/delete hangs forever with no event" failure
|
|
66
|
+
// mode). Close it here so a timed-out attempt can't poison the store.
|
|
67
|
+
if (settled) {
|
|
68
|
+
try {
|
|
69
|
+
request.result.close();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Best-effort — a half-open connection may already be unusable.
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const db = request.result;
|
|
77
|
+
// MANDATORY resilience handler (W3C IndexedDB / MDN): close this
|
|
78
|
+
// connection the instant any other context wants to upgrade or delete the
|
|
79
|
+
// DB. Without it, an open connection that ignores `versionchange` blocks
|
|
80
|
+
// the other context's request indefinitely — the root cause of a wedged
|
|
81
|
+
// `ablo_databases` that survives reloads (an interrupted transaction's
|
|
82
|
+
// connection never closes, so every later open/delete hangs with no
|
|
83
|
+
// event). Auto-closing here makes the store self-releasing.
|
|
84
|
+
db.onversionchange = () => {
|
|
85
|
+
try {
|
|
86
|
+
db.close();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Already closing/closed — nothing to do.
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
options.onVersionChange?.();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// A consumer reaction must never break the close.
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
settle(() => resolve(db));
|
|
99
|
+
};
|
|
46
100
|
request.onerror = () => settle(() => reject(request.error));
|
|
47
101
|
// The critical handler: another tab is blocking us. Native API leaves
|
|
48
102
|
// the request pending indefinitely; we fail fast with a clear error so
|
|
@@ -61,3 +115,36 @@ export function openIDBWithTimeout(name, version, options = {}) {
|
|
|
61
115
|
}, timeoutMs);
|
|
62
116
|
});
|
|
63
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Bounded `indexedDB.deleteDatabase()` — the delete counterpart of
|
|
120
|
+
* `openIDBWithTimeout`. Used by the meta-DB self-heal: when opening
|
|
121
|
+
* `ablo_databases` times out (a wedged backing store), we attempt to delete it
|
|
122
|
+
* and re-create from scratch. The registry it holds is rebuildable from the
|
|
123
|
+
* server on the next bootstrap, so dropping it is safe.
|
|
124
|
+
*
|
|
125
|
+
* Like `open`, `deleteDatabase` can hang indefinitely: if another live
|
|
126
|
+
* connection holds the DB it fires `onblocked` and waits, and on a truly stuck
|
|
127
|
+
* store it fires *no* event at all. Both become a bounded rejection here so the
|
|
128
|
+
* caller can fall through to surfacing a real error instead of spinning.
|
|
129
|
+
*
|
|
130
|
+
* Resolves `true` on a clean delete, `false` if it was blocked or timed out
|
|
131
|
+
* (caller decides whether to retry the open regardless — a no-op delete still
|
|
132
|
+
* leaves us no worse off).
|
|
133
|
+
*/
|
|
134
|
+
export function deleteIDBWithTimeout(name, timeoutMs = 5_000) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
let settled = false;
|
|
137
|
+
const settle = (value) => {
|
|
138
|
+
if (settled)
|
|
139
|
+
return;
|
|
140
|
+
settled = true;
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
resolve(value);
|
|
143
|
+
};
|
|
144
|
+
const request = indexedDB.deleteDatabase(name);
|
|
145
|
+
request.onsuccess = () => settle(true);
|
|
146
|
+
request.onerror = () => settle(false);
|
|
147
|
+
request.onblocked = () => settle(false);
|
|
148
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
149
|
+
});
|
|
150
|
+
}
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* carry no `httpStatus`, exactly as Stripe omits client-side
|
|
30
30
|
* programmer errors from its published code list.
|
|
31
31
|
*/
|
|
32
|
+
import { z } from 'zod';
|
|
32
33
|
/**
|
|
33
34
|
* Version of the error contract — the envelope shape + the set of codes and
|
|
34
35
|
* their semantics. Date-based, like Stripe's API versions. Bump it (and only
|
|
@@ -36,9 +37,48 @@
|
|
|
36
37
|
* code, a changed HTTP status, an envelope field. Emitted in `errors.json`
|
|
37
38
|
* and on the `Ablo-Version` response header so a consumer can detect drift.
|
|
38
39
|
*/
|
|
39
|
-
export declare const ERROR_CONTRACT_VERSION = "2026-
|
|
40
|
+
export declare const ERROR_CONTRACT_VERSION = "2026-06-02";
|
|
40
41
|
/** Coarse grouping for metrics dashboards and docs sectioning. */
|
|
41
42
|
export type ErrorCategory = 'auth' | 'permission' | 'capability' | 'claim' | 'conflict' | 'validation' | 'not_found' | 'tenant' | 'schema' | 'intent' | 'bootstrap' | 'transport' | 'rate_limit' | 'server' | 'client';
|
|
43
|
+
/**
|
|
44
|
+
* The closed taxonomy of *how a failure recovers* — one rung above the raw
|
|
45
|
+
* `code`. Where `code` says **what** went wrong, `RecoveryClass` says **what
|
|
46
|
+
* the client should do about it**, which is exactly the discriminant the sync
|
|
47
|
+
* FSM and the network probe need. It collapses what used to be three scattered
|
|
48
|
+
* booleans (`retryable`, `authBlocked`, `sessionValid`) into one exhaustive,
|
|
49
|
+
* Zod-validated enum so the connection layer branches on a single value with
|
|
50
|
+
* compile-time completeness instead of ad-hoc `if (!isRetryableCode(...))`
|
|
51
|
+
* chains.
|
|
52
|
+
*
|
|
53
|
+
* - `access_credential_expiry` — the Stripe-style ephemeral key (`ek_`/`rk_`)
|
|
54
|
+
* the sync-engine presents as its Bearer has expired. The long-lived login
|
|
55
|
+
* is fine; the remedy is to silently RE-MINT a fresh key from the session
|
|
56
|
+
* and retry the same request. This MUST NOT sign the user out (the whole
|
|
57
|
+
* point of the wake-from-sleep fix: a 15-min `ek_` dying after a laptop nap
|
|
58
|
+
* is routine, not a logout).
|
|
59
|
+
* - `session_expiry` — the LONG-LIVED login itself is gone. Terminal:
|
|
60
|
+
* sign out and route to re-authentication.
|
|
61
|
+
* - `auth_blocked` — reachable, but the credential TYPE/config was rejected
|
|
62
|
+
* (wrong key kind, untrusted issuer, no org). Re-auth re-mints the same
|
|
63
|
+
* rejected credential and loops, so STOP — don't reconnect, don't sign out.
|
|
64
|
+
* - `permission` — a 403 authorization denial (scope/role/membership).
|
|
65
|
+
* - `transient` — retry the same request unchanged (5xx, lease contention…).
|
|
66
|
+
* - `none` — not a recoverable-auth condition (validation, not-found, local
|
|
67
|
+
* invariants, and any forward-compat code an older SDK doesn't know).
|
|
68
|
+
*/
|
|
69
|
+
export declare const RECOVERY_CLASSES: readonly ["access_credential_expiry", "session_expiry", "auth_blocked", "permission", "transient", "none"];
|
|
70
|
+
/** Zod enum derived from {@link RECOVERY_CLASSES} — the runtime-validatable
|
|
71
|
+
* form of the recovery taxonomy. */
|
|
72
|
+
export declare const recoveryClassSchema: z.ZodEnum<{
|
|
73
|
+
permission: "permission";
|
|
74
|
+
access_credential_expiry: "access_credential_expiry";
|
|
75
|
+
session_expiry: "session_expiry";
|
|
76
|
+
auth_blocked: "auth_blocked";
|
|
77
|
+
transient: "transient";
|
|
78
|
+
none: "none";
|
|
79
|
+
}>;
|
|
80
|
+
/** How a failure recovers. See {@link RECOVERY_CLASSES}. */
|
|
81
|
+
export type RecoveryClass = z.infer<typeof recoveryClassSchema>;
|
|
42
82
|
/** One registry entry. `httpStatus` is present only for `surface: 'wire'`
|
|
43
83
|
* codes — status is a property of the wire boundary, never of a
|
|
44
84
|
* purely-local client invariant. */
|
|
@@ -55,6 +95,14 @@ export interface ErrorCodeSpec {
|
|
|
55
95
|
readonly retryable: boolean;
|
|
56
96
|
/** One-line human description — the source text for the `doc_url` page. */
|
|
57
97
|
readonly message: string;
|
|
98
|
+
/**
|
|
99
|
+
* Explicit recovery class. Set ONLY where it diverges from what `category` /
|
|
100
|
+
* `httpStatus` / `retryable` already imply — i.e. the handful of auth codes
|
|
101
|
+
* whose remedy (`session_expiry` vs `access_credential_expiry`) the bare
|
|
102
|
+
* status can't distinguish. Everything else is derived by
|
|
103
|
+
* {@link classifyRecovery}, so adding a normal code needs no `recovery`.
|
|
104
|
+
*/
|
|
105
|
+
readonly recovery?: RecoveryClass;
|
|
58
106
|
}
|
|
59
107
|
/**
|
|
60
108
|
* The closed set of stable error codes. Add a code here BEFORE throwing it
|
|
@@ -69,14 +117,30 @@ export declare const ERROR_CODES: {
|
|
|
69
117
|
readonly capability_id_missing: ErrorCodeSpec;
|
|
70
118
|
readonly exchange_failed: ErrorCodeSpec;
|
|
71
119
|
readonly identity_resolve_failed: ErrorCodeSpec;
|
|
120
|
+
readonly auth_no_credentials: ErrorCodeSpec;
|
|
121
|
+
readonly identity_missing_organization: ErrorCodeSpec;
|
|
72
122
|
readonly session_expired: ErrorCodeSpec;
|
|
123
|
+
readonly jwt_invalid: ErrorCodeSpec;
|
|
124
|
+
readonly jwt_malformed: ErrorCodeSpec;
|
|
125
|
+
readonly jwt_missing_issuer: ErrorCodeSpec;
|
|
126
|
+
readonly jwt_issuer_untrusted: ErrorCodeSpec;
|
|
127
|
+
readonly jwt_signature_invalid: ErrorCodeSpec;
|
|
128
|
+
readonly jwt_audience_mismatch: ErrorCodeSpec;
|
|
129
|
+
readonly jwt_missing_subject: ErrorCodeSpec;
|
|
130
|
+
readonly jwt_missing_organization: ErrorCodeSpec;
|
|
131
|
+
readonly jwt_expired: ErrorCodeSpec;
|
|
132
|
+
readonly jwt_org_membership_denied: ErrorCodeSpec;
|
|
73
133
|
readonly file_upload_auth_required: ErrorCodeSpec;
|
|
74
134
|
readonly browser_apikey_blocked: ErrorCodeSpec;
|
|
135
|
+
readonly browser_database_url_blocked: ErrorCodeSpec;
|
|
136
|
+
readonly datasource_registration_failed: ErrorCodeSpec;
|
|
75
137
|
readonly capability_scope_denied: ErrorCodeSpec;
|
|
138
|
+
readonly issuer_register_forbidden: ErrorCodeSpec;
|
|
76
139
|
readonly capability_invalid: ErrorCodeSpec;
|
|
77
140
|
readonly byo_role_cannot_enforce_rls: ErrorCodeSpec;
|
|
78
141
|
readonly byo_role_unreadable: ErrorCodeSpec;
|
|
79
142
|
readonly byo_tenant_tables_unforced_rls: ErrorCodeSpec;
|
|
143
|
+
readonly byo_host_not_allowed: ErrorCodeSpec;
|
|
80
144
|
readonly claim_conflict: ErrorCodeSpec;
|
|
81
145
|
readonly claim_lost: ErrorCodeSpec;
|
|
82
146
|
readonly entity_claimed: ErrorCodeSpec;
|
|
@@ -92,6 +156,7 @@ export declare const ERROR_CODES: {
|
|
|
92
156
|
readonly commit_operation_required: ErrorCodeSpec;
|
|
93
157
|
readonly commit_operation_model_required: ErrorCodeSpec;
|
|
94
158
|
readonly commit_operations_ambiguous: ErrorCodeSpec;
|
|
159
|
+
readonly commit_too_many_operations: ErrorCodeSpec;
|
|
95
160
|
readonly model_required_field_missing: ErrorCodeSpec;
|
|
96
161
|
readonly model_identifier_missing: ErrorCodeSpec;
|
|
97
162
|
readonly snapshot_reserved_key: ErrorCodeSpec;
|
|
@@ -103,6 +168,11 @@ export declare const ERROR_CODES: {
|
|
|
103
168
|
readonly model_not_found: ErrorCodeSpec;
|
|
104
169
|
readonly mutate_update_entity_not_found: ErrorCodeSpec;
|
|
105
170
|
readonly task_id_missing: ErrorCodeSpec;
|
|
171
|
+
readonly not_null_violation: ErrorCodeSpec;
|
|
172
|
+
readonly foreign_key_violation: ErrorCodeSpec;
|
|
173
|
+
readonly unique_violation: ErrorCodeSpec;
|
|
174
|
+
readonly check_violation: ErrorCodeSpec;
|
|
175
|
+
readonly constraint_violation: ErrorCodeSpec;
|
|
106
176
|
readonly server_execute_unknown_model: ErrorCodeSpec;
|
|
107
177
|
readonly mutate_create_unknown_model: ErrorCodeSpec;
|
|
108
178
|
readonly tenant_model_columns_unknown: ErrorCodeSpec;
|
|
@@ -146,15 +216,18 @@ export declare const ERROR_CODES: {
|
|
|
146
216
|
readonly queue_too_deep: ErrorCodeSpec;
|
|
147
217
|
readonly flush_timeout: ErrorCodeSpec;
|
|
148
218
|
readonly wait_for_timeout: ErrorCodeSpec;
|
|
219
|
+
readonly instance_at_capacity: ErrorCodeSpec;
|
|
149
220
|
readonly fetch_unavailable: ErrorCodeSpec;
|
|
150
221
|
readonly base_url_missing: ErrorCodeSpec;
|
|
151
222
|
readonly sync_not_ready: ErrorCodeSpec;
|
|
152
223
|
readonly ws_not_ready: ErrorCodeSpec;
|
|
153
224
|
readonly quota_exceeded: ErrorCodeSpec;
|
|
225
|
+
readonly connection_limit_exceeded: ErrorCodeSpec;
|
|
154
226
|
readonly internal_error: ErrorCodeSpec;
|
|
155
227
|
readonly quota_lookup_failed: ErrorCodeSpec;
|
|
156
228
|
readonly turn_open_failed: ErrorCodeSpec;
|
|
157
229
|
readonly turn_close_failed: ErrorCodeSpec;
|
|
230
|
+
readonly invalid_options: ErrorCodeSpec;
|
|
158
231
|
readonly no_ablo_provider: ErrorCodeSpec;
|
|
159
232
|
readonly no_sync_group_provider: ErrorCodeSpec;
|
|
160
233
|
readonly sync_context_missing_provider: ErrorCodeSpec;
|
|
@@ -189,6 +262,7 @@ export declare const ERROR_CODES: {
|
|
|
189
262
|
readonly mutator_registry_unnamed_def: ErrorCodeSpec;
|
|
190
263
|
readonly mutators_schema_missing: ErrorCodeSpec;
|
|
191
264
|
readonly undo_scope_schema_missing: ErrorCodeSpec;
|
|
265
|
+
readonly undo_entry_invalid: ErrorCodeSpec;
|
|
192
266
|
readonly mock_mutation_failed: ErrorCodeSpec;
|
|
193
267
|
readonly mock_unsupported_operation: ErrorCodeSpec;
|
|
194
268
|
readonly invalid_body: ErrorCodeSpec;
|
|
@@ -262,3 +336,20 @@ export declare function errorCodeSpec(code: string): ErrorCodeSpec | undefined;
|
|
|
262
336
|
/** Whether a code's spec marks it retryable. Unknown / dynamic codes
|
|
263
337
|
* default to non-retryable (safe default — don't auto-retry the unknown). */
|
|
264
338
|
export declare function isRetryableCode(code: string): boolean;
|
|
339
|
+
/**
|
|
340
|
+
* Classify a `code` into its {@link RecoveryClass} — the single discriminant
|
|
341
|
+
* the connection FSM and the network probe branch on.
|
|
342
|
+
*
|
|
343
|
+
* The registry stays the source of truth: an explicit `spec.recovery` wins
|
|
344
|
+
* (set only on the few auth codes whose remedy the status can't reveal), and
|
|
345
|
+
* everything else is DERIVED from the spec so the registry stays terse:
|
|
346
|
+
* - retryable → `transient`
|
|
347
|
+
* - 403 → `permission`
|
|
348
|
+
* - residual `auth`-category → `auth_blocked` (the 401 credential-type codes)
|
|
349
|
+
* - otherwise / unknown → `none`
|
|
350
|
+
*
|
|
351
|
+
* Unknown / dynamic `policy:*` / forward-compat codes (`spec === undefined`)
|
|
352
|
+
* default to `none`, mirroring {@link isRetryableCode}'s safe default — never
|
|
353
|
+
* silently treat an unrecognised code as a credential expiry or a logout.
|
|
354
|
+
*/
|
|
355
|
+
export declare function classifyRecovery(code: string): RecoveryClass;
|