@abloatai/ablo 0.28.0 → 0.29.1
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 +36 -0
- package/dist/Database.js +9 -6
- package/dist/SyncClient.d.ts +2 -2
- package/dist/SyncClient.js +22 -10
- package/dist/adapters/inMemoryStorage.js +18 -14
- package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
- package/dist/auth/schemas.d.ts +6 -0
- package/dist/auth/schemas.js +7 -0
- package/dist/cli.cjs +815 -400
- package/dist/client/Ablo.d.ts +29 -23
- package/dist/client/Ablo.js +8 -196
- package/dist/client/claimHeartbeatLoop.d.ts +1 -1
- package/dist/client/claimHeartbeatLoop.js +1 -1
- package/dist/client/createInternalComponents.d.ts +4 -2
- package/dist/client/createInternalComponents.js +3 -1
- package/dist/client/durableWrites.d.ts +21 -0
- package/dist/client/durableWrites.js +46 -0
- package/dist/client/httpClient.d.ts +19 -45
- package/dist/client/httpClient.js +104 -26
- package/dist/client/httpTransport.d.ts +8 -0
- package/dist/client/{ApiClient.js → httpTransport.js} +78 -291
- package/dist/client/options.d.ts +43 -19
- package/dist/client/options.js +3 -2
- package/dist/client/resourceTypes.d.ts +9 -85
- package/dist/client/resourceTypes.js +1 -1
- package/dist/client/sessionMint.d.ts +5 -6
- package/dist/client/sessionMint.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/coordination/schema.d.ts +30 -0
- package/dist/coordination/schema.js +14 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/errors.d.ts +4 -40
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +14 -8
- package/dist/index.js +7 -5
- package/dist/interfaces/index.d.ts +3 -0
- package/dist/mutators/defineMutators.d.ts +18 -0
- package/dist/mutators/defineMutators.js +4 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/select.d.ts +15 -0
- package/dist/schema/select.js +27 -3
- package/dist/source/connector.d.ts +3 -3
- package/dist/source/factory.d.ts +4 -6
- package/dist/source/factory.js +4 -7
- package/dist/source/index.d.ts +4 -4
- package/dist/source/index.js +2 -2
- package/dist/source/signing.d.ts +0 -3
- package/dist/source/types.d.ts +0 -26
- package/dist/stores/ObjectStore.js +8 -8
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/BootstrapFetcher.d.ts +10 -0
- package/dist/sync/BootstrapFetcher.js +27 -4
- package/dist/sync/createClaimStream.d.ts +11 -2
- package/dist/sync/createClaimStream.js +4 -3
- package/dist/testing/mocks/FakeDatabase.d.ts +1 -1
- package/dist/transactions/TransactionQueue.d.ts +2 -2
- package/dist/transactions/TransactionQueue.js +3 -3
- package/dist/transactions/commitEnvelope.d.ts +1 -1
- package/dist/transactions/commitEnvelope.js +2 -2
- package/dist/transactions/commitOutboxStore.d.ts +19 -19
- package/dist/transactions/durableWriteStore.d.ts +123 -0
- package/dist/transactions/durableWriteStore.js +30 -0
- package/dist/transactions/httpCommitEnvelope.d.ts +1 -0
- package/dist/transactions/httpCommitEnvelope.js +14 -12
- package/dist/transactions/idempotencyKey.d.ts +10 -0
- package/dist/transactions/idempotencyKey.js +9 -0
- package/dist/types/global.d.ts +10 -29
- package/dist/types/global.js +4 -7
- package/dist/types/streams.d.ts +2 -28
- package/dist/wire/frames.d.ts +6 -257
- package/dist/wire/frames.js +4 -25
- package/dist/wire/index.d.ts +4 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/protocolVersion.d.ts +30 -17
- package/dist/wire/protocolVersion.js +34 -18
- package/docs/agents.md +8 -3
- package/docs/api.md +6 -4
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +2 -3
- package/docs/data-sources.md +5 -8
- package/docs/guarantees.md +21 -0
- package/docs/integration-guide.md +1 -0
- package/docs/migration.md +21 -1
- package/docs/quickstart.md +11 -0
- package/docs/react.md +0 -46
- package/examples/README.md +6 -5
- package/llms.txt +15 -15
- package/package.json +2 -7
- package/dist/client/ApiClient.d.ts +0 -177
- package/dist/commit/contract.d.ts +0 -493
- package/dist/commit/contract.js +0 -187
- package/dist/commit/index.d.ts +0 -6
- package/dist/commit/index.js +0 -5
package/dist/schema/select.js
CHANGED
|
@@ -29,7 +29,7 @@ export function selectModels(schema, keys) {
|
|
|
29
29
|
for (const key of keys) {
|
|
30
30
|
const def = schema.models[key];
|
|
31
31
|
if (!def) {
|
|
32
|
-
throw new AbloValidationError(`selectModels: "${
|
|
32
|
+
throw new AbloValidationError(`selectModels: "${key}" is not a model in the source schema`, { code: 'invalid_schema', param: key });
|
|
33
33
|
}
|
|
34
34
|
// Prune relations whose target isn't in the selected set. A pruned
|
|
35
35
|
// `parent` edge is a routing error, not a silent drop.
|
|
@@ -40,8 +40,8 @@ export function selectModels(schema, keys) {
|
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
42
|
if (rel.options?.parent) {
|
|
43
|
-
throw new AbloValidationError(`selectModels: model "${
|
|
44
|
-
`which is not in the selected set. Include "${rel.target}" so scope inheritance still routes.`, { code: 'invalid_schema', param: `${
|
|
43
|
+
throw new AbloValidationError(`selectModels: model "${key}" has a parent relation "${relName}" → "${rel.target}", ` +
|
|
44
|
+
`which is not in the selected set. Include "${rel.target}" so scope inheritance still routes.`, { code: 'invalid_schema', param: `${key}.${relName}` });
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
models[key] = { ...def, relations };
|
|
@@ -53,3 +53,27 @@ export function selectModels(schema, keys) {
|
|
|
53
53
|
identityRoles: schema.identityRoles,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* `omitModels` is `selectModels` from the other side: keep every model EXCEPT
|
|
58
|
+
* the named ones. Use it when an app is the general case and a separate app
|
|
59
|
+
* owns the omitted models — the suite shell drops a specialized store that the
|
|
60
|
+
* standalone app selects for itself:
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* export const schema = omitModels(full, ['reports', 'reportSections']);
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* Same validation as `selectModels`: relations into the omitted set are
|
|
67
|
+
* dropped, and a dropped `parent` edge throws — so a model whose scope routes
|
|
68
|
+
* through an omitted parent cannot be silently kept.
|
|
69
|
+
*/
|
|
70
|
+
export function omitModels(schema, keys) {
|
|
71
|
+
const drop = new Set(keys);
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
if (!schema.models[key]) {
|
|
74
|
+
throw new AbloValidationError(`omitModels: "${key}" is not a model in the source schema`, { code: 'invalid_schema', param: key });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const kept = Object.keys(schema.models).filter((k) => !drop.has(k));
|
|
78
|
+
return selectModels(schema, kept);
|
|
79
|
+
}
|
|
@@ -62,9 +62,9 @@ export interface SourceConnectorOptions {
|
|
|
62
62
|
*/
|
|
63
63
|
readonly apiKey: string;
|
|
64
64
|
/**
|
|
65
|
-
* The Data Source handler to serve, as returned by `dataSource(options)
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* The Data Source handler to serve, as returned by `dataSource(options)`.
|
|
66
|
+
* The connector feeds it each request and relays the response back untouched;
|
|
67
|
+
* it never inspects or alters either one.
|
|
68
68
|
*/
|
|
69
69
|
readonly handler: (request: Request) => Promise<Response>;
|
|
70
70
|
/** The Ablo base URL to dial. Defaults to `https://api.abloatai.com`. */
|
package/dist/source/factory.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds the
|
|
3
|
-
*
|
|
2
|
+
* Builds the request handler returned by `dataSource()`. Given your options —
|
|
3
|
+
* the schema, the API key, and either
|
|
4
4
|
* per-model handlers or an ORM adapter — the handler verifies each signed request,
|
|
5
5
|
* enforces the per-key scopes, and routes the four wire operations (`load`,
|
|
6
6
|
* `list`, `commit`, and `events`) to whichever handlers you configured.
|
|
@@ -13,7 +13,7 @@ type SourceModels<S extends SchemaRecord, TAuth> = Partial<{
|
|
|
13
13
|
readonly id: string;
|
|
14
14
|
} & Record<string, unknown>, InferCreate<Schema<S>, K>, TAuth>;
|
|
15
15
|
}>;
|
|
16
|
-
export type
|
|
16
|
+
export type DataSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
|
|
17
17
|
readonly schema: Schema<S>;
|
|
18
18
|
/**
|
|
19
19
|
* Your Ablo project credential. Ablo signs each Data Source call with the same
|
|
@@ -66,7 +66,7 @@ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
|
|
|
66
66
|
readonly events?: SourceEventsHandler<TAuth>;
|
|
67
67
|
/**
|
|
68
68
|
* Groups per-model handlers under a `models` key. The spread form below is
|
|
69
|
-
* usually shorter — `
|
|
69
|
+
* usually shorter — `dataSource({ schema, files: { load, list, commit } })` —
|
|
70
70
|
* but this explicit form is available when you prefer it.
|
|
71
71
|
*/
|
|
72
72
|
readonly models?: SourceModels<S, TAuth>;
|
|
@@ -88,7 +88,5 @@ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
|
|
|
88
88
|
* canonical rows in your database: Ablo calls a narrow, signed endpoint you
|
|
89
89
|
* control rather than holding your database credentials.
|
|
90
90
|
*/
|
|
91
|
-
export declare function abloSource<const S extends SchemaRecord, TAuth = unknown>(options: AbloSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
|
|
92
|
-
export type DataSourceOptions<S extends SchemaRecord, TAuth = unknown> = AbloSourceOptions<S, TAuth>;
|
|
93
91
|
export declare function dataSource<const S extends SchemaRecord, TAuth = unknown>(options: DataSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
|
|
94
92
|
export {};
|
package/dist/source/factory.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds the
|
|
3
|
-
*
|
|
2
|
+
* Builds the request handler returned by `dataSource()`. Given your options —
|
|
3
|
+
* the schema, the API key, and either
|
|
4
4
|
* per-model handlers or an ORM adapter — the handler verifies each signed request,
|
|
5
5
|
* enforces the per-key scopes, and routes the four wire operations (`load`,
|
|
6
6
|
* `list`, `commit`, and `events`) to whichever handlers you configured.
|
|
@@ -131,8 +131,8 @@ function sameModel(operations) {
|
|
|
131
131
|
* canonical rows in your database: Ablo calls a narrow, signed endpoint you
|
|
132
132
|
* control rather than holding your database credentials.
|
|
133
133
|
*/
|
|
134
|
-
export function
|
|
135
|
-
return async function
|
|
134
|
+
export function dataSource(options) {
|
|
135
|
+
return async function handleDataSource(request) {
|
|
136
136
|
if (request.method !== 'POST') {
|
|
137
137
|
return json({ error: 'method_not_allowed' }, 405);
|
|
138
138
|
}
|
|
@@ -263,6 +263,3 @@ export function abloSource(options) {
|
|
|
263
263
|
return json({ error: 'unknown_source_request' }, 400);
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
|
-
export function dataSource(options) {
|
|
267
|
-
return abloSource(options);
|
|
268
|
-
}
|
package/dist/source/index.d.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - `types.ts` — the shared wire and handler types, plus `sourceEventForOperation`
|
|
6
6
|
* - `signing.ts` — request signing and verification
|
|
7
|
-
* - `factory.ts` — the `
|
|
7
|
+
* - `factory.ts` — the `dataSource()` endpoint factory
|
|
8
8
|
*
|
|
9
9
|
* Import from here; the sibling modules import one another directly.
|
|
10
10
|
*/
|
|
11
|
-
export { sourceEventForOperation, type SourcePrimitive, type SourceWhere, type SourceListQuery, type SourceListPage, type SourceListResult, type SourceRequestContext, type SourceOperation, type SourceDelta, type SourceEvent, type SourceEventForOperationOptions, type SourceCommitResult, type SourceCommitParams, type SourceScope, type SourceEventsResult, type SourceEventsHandler, type SourceAuthorizeContext, type SourceHandlerContext, type SourceModelHandlers, type SourceCommitHandler, type SourceApiKey, type SourceLoadRequest, type SourceListRequest, type SourceCommitRequest, type SourceEventsRequest, type SourceRequest, type SourceResponse,
|
|
12
|
-
export { ABLO_SOURCE_HEADERS, SourceSignatureError, signAbloSourceRequest, verifyAbloSourceRequest, type SourceSignatureOptions, type SourceSignatureVerificationOptions, type SourceSignatureVerificationResult,
|
|
13
|
-
export {
|
|
11
|
+
export { sourceEventForOperation, type SourcePrimitive, type SourceWhere, type SourceListQuery, type SourceListPage, type SourceListResult, type SourceRequestContext, type SourceOperation, type SourceDelta, type SourceEvent, type SourceEventForOperationOptions, type SourceCommitResult, type SourceCommitParams, type SourceScope, type SourceEventsResult, type SourceEventsHandler, type SourceAuthorizeContext, type SourceHandlerContext, type SourceModelHandlers, type SourceCommitHandler, type SourceApiKey, type SourceLoadRequest, type SourceListRequest, type SourceCommitRequest, type SourceEventsRequest, type SourceRequest, type SourceResponse, } from './types.js';
|
|
12
|
+
export { ABLO_SOURCE_HEADERS, SourceSignatureError, signAbloSourceRequest, verifyAbloSourceRequest, type SourceSignatureOptions, type SourceSignatureVerificationOptions, type SourceSignatureVerificationResult, } from './signing.js';
|
|
13
|
+
export { dataSource, type DataSourceOptions, } from './factory.js';
|
|
14
14
|
export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHEDULE, type PushQueue, type PushQueueItem, type PushQueueOptions, type PushQueueStorage, } from './pushQueue.js';
|
|
15
15
|
export { createSourceConnector, DEFAULT_RECONNECT_SCHEDULE, type SourceConnector, type SourceConnectorOptions, type ConnectorWebSocket, type ConnectorWebSocketFactory, type ConnectorStatus, } from './connector.js';
|
|
16
16
|
export { SOURCE_CONNECTOR_PROTOCOL_VERSION, SOURCE_CONNECTOR_WS_PATH, WS_SOURCE_SUBPROTOCOL, sourceConnectorSubprotocols, encodeFrame, decodeFrame, ConnectorProtocolError, connectorFrameSchema, type ConnectorFrame, type RegisterFrame, type ReadyFrame, type RequestFrame, type ResponseFrame, type ErrorFrame, } from './connectorProtocol.js';
|
package/dist/source/index.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - `types.ts` — the shared wire and handler types, plus `sourceEventForOperation`
|
|
6
6
|
* - `signing.ts` — request signing and verification
|
|
7
|
-
* - `factory.ts` — the `
|
|
7
|
+
* - `factory.ts` — the `dataSource()` endpoint factory
|
|
8
8
|
*
|
|
9
9
|
* Import from here; the sibling modules import one another directly.
|
|
10
10
|
*/
|
|
11
11
|
export { sourceEventForOperation, } from './types.js';
|
|
12
12
|
export { ABLO_SOURCE_HEADERS, SourceSignatureError, signAbloSourceRequest, verifyAbloSourceRequest, } from './signing.js';
|
|
13
|
-
export {
|
|
13
|
+
export { dataSource, } from './factory.js';
|
|
14
14
|
export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHEDULE, } from './pushQueue.js';
|
|
15
15
|
// The reverse-channel connector — an outbound transport for the load, list, and
|
|
16
16
|
// commit leg, and the dial-out counterpart to `createPushQueue`. It lets you serve
|
package/dist/source/signing.d.ts
CHANGED
|
@@ -83,6 +83,3 @@ export declare function signAbloSourceRequest(options: SourceSignatureOptions):
|
|
|
83
83
|
* id and signing time. This is the counterpart to {@link signAbloSourceRequest}.
|
|
84
84
|
*/
|
|
85
85
|
export declare function verifyAbloSourceRequest(options: SourceSignatureVerificationOptions): Promise<SourceSignatureVerificationResult>;
|
|
86
|
-
export type DataSourceSignatureOptions = SourceSignatureOptions;
|
|
87
|
-
export type DataSourceSignatureVerificationOptions = SourceSignatureVerificationOptions;
|
|
88
|
-
export type DataSourceSignatureVerificationResult = SourceSignatureVerificationResult;
|
package/dist/source/types.d.ts
CHANGED
|
@@ -323,29 +323,3 @@ export type SourceResponse<Row = Record<string, unknown>> = {
|
|
|
323
323
|
readonly rows?: readonly Row[];
|
|
324
324
|
readonly deltas?: readonly SourceDelta[];
|
|
325
325
|
};
|
|
326
|
-
export type DataSourcePrimitive = SourcePrimitive;
|
|
327
|
-
export type DataSourceWhere = SourceWhere;
|
|
328
|
-
export type DataSourceListQuery = SourceListQuery;
|
|
329
|
-
export type DataSourceListPage<Row> = SourceListPage<Row>;
|
|
330
|
-
export type DataSourceListResult<Row> = SourceListResult<Row>;
|
|
331
|
-
export type DataSourceRequestContext = SourceRequestContext;
|
|
332
|
-
export type DataSourceOperation = SourceOperation;
|
|
333
|
-
export type DataSourceDelta = SourceDelta;
|
|
334
|
-
export type DataSourceEvent = SourceEvent;
|
|
335
|
-
export type DataSourceEventForOperationOptions = SourceEventForOperationOptions;
|
|
336
|
-
export type DataSourceCommitResult<Row = Record<string, unknown>> = SourceCommitResult<Row>;
|
|
337
|
-
export type DataSourceCommitParams<TAuth = unknown> = SourceCommitParams<TAuth>;
|
|
338
|
-
export type DataSourceScope = SourceScope;
|
|
339
|
-
export type DataSourceEventsResult = SourceEventsResult;
|
|
340
|
-
export type DataSourceEventsHandler<TAuth = unknown> = SourceEventsHandler<TAuth>;
|
|
341
|
-
export type DataSourceAuthorizeContext = SourceAuthorizeContext;
|
|
342
|
-
export type DataSourceHandlerContext<TAuth = unknown> = SourceHandlerContext<TAuth>;
|
|
343
|
-
export type DataSourceModelHandlers<Row, CreateInput, TAuth = unknown> = SourceModelHandlers<Row, CreateInput, TAuth>;
|
|
344
|
-
export type DataSourceCommitHandler<TAuth = unknown> = SourceCommitHandler<TAuth>;
|
|
345
|
-
export type DataSourceApiKey = SourceApiKey;
|
|
346
|
-
export type DataSourceLoadRequest = SourceLoadRequest;
|
|
347
|
-
export type DataSourceListRequest = SourceListRequest;
|
|
348
|
-
export type DataSourceCommitRequest = SourceCommitRequest;
|
|
349
|
-
export type DataSourceEventsRequest = SourceEventsRequest;
|
|
350
|
-
export type DataSourceRequest = SourceRequest;
|
|
351
|
-
export type DataSourceResponse<Row = Record<string, unknown>> = SourceResponse<Row>;
|
|
@@ -41,8 +41,8 @@ export class ObjectStore {
|
|
|
41
41
|
const store = tx.objectStore(this.storeName);
|
|
42
42
|
const request = store.add(data);
|
|
43
43
|
tx.oncomplete = () => { resolve(); };
|
|
44
|
-
tx.onerror = () => { reject(tx.error
|
|
45
|
-
request.onerror = () => { reject(request.error
|
|
44
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
45
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
46
46
|
}
|
|
47
47
|
catch (error) {
|
|
48
48
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -93,8 +93,8 @@ export class ObjectStore {
|
|
|
93
93
|
const store = tx.objectStore(this.storeName);
|
|
94
94
|
const request = store.put(data);
|
|
95
95
|
tx.oncomplete = () => { resolve(); };
|
|
96
|
-
tx.onerror = () => { reject(tx.error
|
|
97
|
-
request.onerror = () => { reject(request.error
|
|
96
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
97
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
98
98
|
}
|
|
99
99
|
catch (error) {
|
|
100
100
|
// Propagate failure so callers do not continue with inconsistent state
|
|
@@ -197,8 +197,8 @@ export class ObjectStore {
|
|
|
197
197
|
const store = tx.objectStore(this.storeName);
|
|
198
198
|
const request = store.delete(id);
|
|
199
199
|
tx.oncomplete = () => { resolve(); };
|
|
200
|
-
tx.onerror = () => { reject(tx.error
|
|
201
|
-
request.onerror = () => { reject(request.error
|
|
200
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
201
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
202
202
|
}
|
|
203
203
|
catch (error) {
|
|
204
204
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -233,8 +233,8 @@ export class ObjectStore {
|
|
|
233
233
|
const store = tx.objectStore(this.storeName);
|
|
234
234
|
const request = store.clear();
|
|
235
235
|
tx.oncomplete = () => { resolve(); };
|
|
236
|
-
tx.onerror = () => { reject(tx.error
|
|
237
|
-
request.onerror = () => { reject(request.error
|
|
236
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
237
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
238
238
|
}
|
|
239
239
|
catch (error) {
|
|
240
240
|
reject(error instanceof Error ? error : new Error(String(error)));
|
package/dist/surface.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orde
|
|
|
30
30
|
* The keys of the client constructor options, {@link AbloOptions}. Only
|
|
31
31
|
* `schema` is required; every other key is optional.
|
|
32
32
|
*/
|
|
33
|
-
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "commitOutbox", "commitOutboxScope", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
33
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "durableWrites", "commitOutbox", "commitOutboxScope", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
34
34
|
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
35
35
|
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
36
36
|
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|
package/dist/surface.js
CHANGED
|
@@ -88,6 +88,16 @@ export declare class BootstrapFetcher {
|
|
|
88
88
|
* means the app's schema and the deployed schema have diverged — reads/writes
|
|
89
89
|
* relying on undeployed changes will later fail with an opaque DB constraint
|
|
90
90
|
* error. Warn once, actionably; never throws or blocks the bootstrap.
|
|
91
|
+
*
|
|
92
|
+
* The message names the SERVER it connected to, and spans all three real
|
|
93
|
+
* causes rather than assuming "you forgot to push". Drift most often means the
|
|
94
|
+
* schema was pushed to a different server, project, or environment than this
|
|
95
|
+
* client points at (a bare `ablo push` targets the hosted default; a local app
|
|
96
|
+
* usually reads a local server) — so the first, load-bearing pointer is `ablo
|
|
97
|
+
* status`, which names the exact org/project/environment the key resolves to
|
|
98
|
+
* and the deployed hash, turning "which of these is it?" into one glance. The
|
|
99
|
+
* older "Run `ablo push`" copy sent everyone down one path and confused the
|
|
100
|
+
* common wrong-target and version-skew cases.
|
|
91
101
|
*/
|
|
92
102
|
private warnOnSchemaDrift;
|
|
93
103
|
constructor(options: BootstrapOptions);
|
|
@@ -25,6 +25,16 @@ export class BootstrapFetcher {
|
|
|
25
25
|
* means the app's schema and the deployed schema have diverged — reads/writes
|
|
26
26
|
* relying on undeployed changes will later fail with an opaque DB constraint
|
|
27
27
|
* error. Warn once, actionably; never throws or blocks the bootstrap.
|
|
28
|
+
*
|
|
29
|
+
* The message names the SERVER it connected to, and spans all three real
|
|
30
|
+
* causes rather than assuming "you forgot to push". Drift most often means the
|
|
31
|
+
* schema was pushed to a different server, project, or environment than this
|
|
32
|
+
* client points at (a bare `ablo push` targets the hosted default; a local app
|
|
33
|
+
* usually reads a local server) — so the first, load-bearing pointer is `ablo
|
|
34
|
+
* status`, which names the exact org/project/environment the key resolves to
|
|
35
|
+
* and the deployed hash, turning "which of these is it?" into one glance. The
|
|
36
|
+
* older "Run `ablo push`" copy sent everyone down one path and confused the
|
|
37
|
+
* common wrong-target and version-skew cases.
|
|
28
38
|
*/
|
|
29
39
|
warnOnSchemaDrift(serverHash) {
|
|
30
40
|
if (this.schemaDriftWarned || !serverHash)
|
|
@@ -33,14 +43,27 @@ export class BootstrapFetcher {
|
|
|
33
43
|
if (!clientHash || clientHash === serverHash)
|
|
34
44
|
return;
|
|
35
45
|
this.schemaDriftWarned = true;
|
|
46
|
+
const org = this.options.organizationId;
|
|
47
|
+
const where = org ? `${this.baseUrl} (org ${org})` : this.baseUrl;
|
|
36
48
|
// Self-brand the message ("Ablo:") rather than rely on the default logger's
|
|
37
49
|
// `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
|
|
38
50
|
// that prefix, and a drift warning that reads like the app's own log is
|
|
39
51
|
// worse than none. The brand tells them at a glance who is talking.
|
|
40
|
-
getContext().logger.warn(`Ablo: Schema drift
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
|
|
52
|
+
getContext().logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
|
|
53
|
+
`one active on the server it connected to (${serverHash} at ${where}). Until they match, ` +
|
|
54
|
+
`operations that depend on the difference will fail later with an opaque database error. ` +
|
|
55
|
+
`This is usually one of three things. The schema may have been pushed to a different ` +
|
|
56
|
+
`server, project, or environment than this client points at — run \`ablo status\` to see ` +
|
|
57
|
+
`the exact org, project, and environment your key resolves to, alongside the deployed ` +
|
|
58
|
+
`hash, and confirm they match here. Your local schema may simply not be pushed to this ` +
|
|
59
|
+
`server yet — run \`ablo push\` against it. Or this client and the server may have been ` +
|
|
60
|
+
`built with different Ablo versions, which can hash an identical schema differently — ` +
|
|
61
|
+
`align the versions. This check is advisory and never blocks the connection.`, {
|
|
62
|
+
clientSchemaHash: clientHash,
|
|
63
|
+
serverSchemaHash: serverHash,
|
|
64
|
+
serverUrl: this.baseUrl,
|
|
65
|
+
...(org ? { organizationId: org } : {}),
|
|
66
|
+
});
|
|
44
67
|
}
|
|
45
68
|
constructor(options) {
|
|
46
69
|
// Defaults are spread first; the explicit `baseUrl` then takes precedence,
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
25
25
|
import type { ClaimOptions, Claim, ClaimStream, PresenceTarget } from '../types/streams.js';
|
|
26
|
+
/**
|
|
27
|
+
* The wire capability the claim stream actually uses: subscribe to typed
|
|
28
|
+
* inbound frames, check liveness, and send outbound frames. `SyncWebSocket`
|
|
29
|
+
* satisfies it, so production wiring is unchanged — but depending on the port
|
|
30
|
+
* rather than the whole socket class lets a test drive it with a plain object,
|
|
31
|
+
* no cast. `Pick` carries the exact (generic, typed-payload) `subscribe`
|
|
32
|
+
* signature, so every handler stays fully typed.
|
|
33
|
+
*/
|
|
34
|
+
export type ClaimTransport = Pick<SyncWebSocket, 'subscribe' | 'isConnected' | 'send'>;
|
|
26
35
|
export interface ClaimStreamConfig {
|
|
27
36
|
/** Identity used to filter our own active claims out of `others`. */
|
|
28
37
|
participantId: string;
|
|
@@ -36,7 +45,7 @@ export interface AttachableClaimStream extends ClaimStream {
|
|
|
36
45
|
* `ablo.<model>.claim({ id })`, which is built on this.
|
|
37
46
|
*/
|
|
38
47
|
claim(target: PresenceTarget, opts?: ClaimOptions): Claim;
|
|
39
|
-
attach(transport:
|
|
48
|
+
attach(transport: ClaimTransport): void;
|
|
40
49
|
dispose(): void;
|
|
41
50
|
}
|
|
42
|
-
export declare function createClaimStream(config: ClaimStreamConfig, transport?:
|
|
51
|
+
export declare function createClaimStream(config: ClaimStreamConfig, transport?: ClaimTransport | null): AttachableClaimStream;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { asyncIteratorFrom } from '../utils/asyncIterator.js';
|
|
25
25
|
import { toMs } from '../utils/duration.js';
|
|
26
|
-
import { claimHeartbeatAckPayloadSchema, descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
26
|
+
import { claimHeartbeatAckPayloadSchema, claimLostSchema, descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
27
27
|
import { AbloClaimedError, AbloConnectionError } from '../errors.js';
|
|
28
28
|
import { resolveHeartbeatOptions } from '../client/claimHeartbeatLoop.js';
|
|
29
29
|
import { getContext } from '../context.js';
|
|
@@ -173,9 +173,10 @@ export function createClaimStream(config, transport = null) {
|
|
|
173
173
|
// (preempted or expired). Distinct from a rejection, which is a claim
|
|
174
174
|
// the server refused.
|
|
175
175
|
unsubs.push(t.subscribe('claim_lost', (payload) => {
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
176
|
+
const parsed = claimLostSchema.safeParse(payload);
|
|
177
|
+
if (!parsed.success)
|
|
178
178
|
return;
|
|
179
|
+
const lost = parsed.data;
|
|
179
180
|
if (ownClaims.has(lost.claimId)) {
|
|
180
181
|
const c = ownClaims.get(lost.claimId);
|
|
181
182
|
getContext().logger.info(`claim: lost ${c ? claimLabel(c.entityType, c.entityId, c.field) : lost.claimId} (preempted or expired)`, { claimId: lost.claimId });
|
|
@@ -2,7 +2,7 @@ import type { Database } from '../../Database.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Overrides for {@link fakeDatabase}. Everything is typechecked against
|
|
4
4
|
* Database's public surface except `getPersistedTransactions`, which widens to
|
|
5
|
-
* `unknown[]`: persisted rows are untrusted input (the
|
|
5
|
+
* `unknown[]`: persisted rows are untrusted input (the DurableWriteStore port
|
|
6
6
|
* documents this), and restore-path tests feed deliberately corrupt rows.
|
|
7
7
|
*/
|
|
8
8
|
export type FakeDatabaseOverrides = Omit<Partial<Database>, 'getPersistedTransactions'> & {
|
|
@@ -19,7 +19,7 @@ import type { WriteOptions } from '../interfaces/index.js';
|
|
|
19
19
|
import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
|
|
20
20
|
import { type MutationInput, type Transaction, type UserContext } from './commitPayload.js';
|
|
21
21
|
import { type DurableCommitEnvelope, type DurableCommitOperation, type CommitOutboxScope } from './commitEnvelope.js';
|
|
22
|
-
import type {
|
|
22
|
+
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
23
23
|
export type { Transaction, UserContext } from './commitPayload.js';
|
|
24
24
|
/**
|
|
25
25
|
* A pre-built, multi-operation commit submitted through
|
|
@@ -161,7 +161,7 @@ export declare class TransactionQueue extends EventEmitter {
|
|
|
161
161
|
*/
|
|
162
162
|
private ensureCommitEnvelope;
|
|
163
163
|
/** Bind the strict local outbox used before any mutation reaches the wire. */
|
|
164
|
-
setCommitOutbox(outbox:
|
|
164
|
+
setCommitOutbox(outbox: DurableWriteStore): void;
|
|
165
165
|
setCommitOutboxScope(scope: CommitOutboxScope): void;
|
|
166
166
|
private sourceMutationIdsFor;
|
|
167
167
|
/**
|
|
@@ -350,7 +350,7 @@ export class TransactionQueue extends EventEmitter {
|
|
|
350
350
|
catch (cause) {
|
|
351
351
|
if (cause instanceof AbloError)
|
|
352
352
|
throw cause;
|
|
353
|
-
throw new AbloConnectionError('Could not
|
|
353
|
+
throw new AbloConnectionError('Could not persist the durable write before dispatch', {
|
|
354
354
|
code: 'db_not_opened',
|
|
355
355
|
cause,
|
|
356
356
|
});
|
|
@@ -370,7 +370,7 @@ export class TransactionQueue extends EventEmitter {
|
|
|
370
370
|
await this.commitOutbox.remove(commitEnvelopeRecordId(idempotencyKey));
|
|
371
371
|
}
|
|
372
372
|
catch (error) {
|
|
373
|
-
getContext().logger.debug('[TransactionQueue]
|
|
373
|
+
getContext().logger.debug('[TransactionQueue] Durable-write cleanup deferred', {
|
|
374
374
|
idempotencyKey,
|
|
375
375
|
error: error instanceof Error ? error.message : String(error),
|
|
376
376
|
});
|
|
@@ -2086,7 +2086,7 @@ export class TransactionQueue extends EventEmitter {
|
|
|
2086
2086
|
void this.processCommitLane();
|
|
2087
2087
|
}
|
|
2088
2088
|
catch (error) {
|
|
2089
|
-
getContext().logger.debug('[TransactionQueue] Failed to restore
|
|
2089
|
+
getContext().logger.debug('[TransactionQueue] Failed to restore durable writes', {
|
|
2090
2090
|
error: error instanceof Error ? error.message : String(error),
|
|
2091
2091
|
});
|
|
2092
2092
|
getContext().observability.captureTransactionFailure({
|
|
@@ -18,7 +18,7 @@ export declare const commitEnvelopeMemberSchema: z.ZodObject<{
|
|
|
18
18
|
sequence: z.ZodOptional<z.ZodNumber>;
|
|
19
19
|
}, z.core.$strict>;
|
|
20
20
|
export type CommitEnvelopeMember = z.infer<typeof commitEnvelopeMemberSchema>;
|
|
21
|
-
/**
|
|
21
|
+
/** One operation stored exactly as the current commit transport sends it. */
|
|
22
22
|
export declare const durableCommitOperationSchema: z.ZodObject<{
|
|
23
23
|
type: z.ZodEnum<{
|
|
24
24
|
CREATE: "CREATE";
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* operations, and the source mutations it supersedes.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { idempotencyKeySchema } from '../commit/contract.js';
|
|
11
10
|
import { readDependencySchema } from '../coordination/schema.js';
|
|
12
11
|
import { commitOperationSchema } from '../wire/frames.js';
|
|
12
|
+
import { idempotencyKeySchema } from './idempotencyKey.js';
|
|
13
13
|
export const COMMIT_ENVELOPE_VERSION = 1;
|
|
14
14
|
export const COMMIT_ENVELOPE_RECORD_PREFIX = 'commit-envelope:';
|
|
15
15
|
/** One transaction's position in a commit; this is not the envelope itself. */
|
|
@@ -22,7 +22,7 @@ export const commitEnvelopeMemberSchema = z
|
|
|
22
22
|
sequence: z.number().int().nonnegative().optional(),
|
|
23
23
|
})
|
|
24
24
|
.refine(({ operationIndex, operationCount }) => operationIndex < operationCount, { message: 'operationIndex must be smaller than operationCount' });
|
|
25
|
-
/**
|
|
25
|
+
/** One operation stored exactly as the current commit transport sends it. */
|
|
26
26
|
export const durableCommitOperationSchema = commitOperationSchema
|
|
27
27
|
.pick({
|
|
28
28
|
type: true,
|
|
@@ -5,28 +5,28 @@
|
|
|
5
5
|
* inject a workflow-, SQLite-, or filesystem-backed implementation without
|
|
6
6
|
* coupling TransactionQueue to the browser database/cache implementation.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
import type { DurableWriteStore, PendingWrite } from './durableWriteStore.js';
|
|
9
|
+
/** @deprecated Use {@link PendingWrite}. */
|
|
10
|
+
export type CommitOutboxRecord = PendingWrite;
|
|
11
|
+
/** @deprecated Use {@link DurableWriteStore}. */
|
|
12
|
+
export type CommitOutboxStore = DurableWriteStore;
|
|
13
|
+
/**
|
|
14
|
+
* The exact subset of the browser `Database` that {@link DatabaseCommitOutboxStore}
|
|
15
|
+
* drives. Typing the adapter to this narrow port instead of importing the
|
|
16
|
+
* concrete `Database` class keeps the outbox store off the
|
|
17
|
+
* `Database → DatabaseManager → ModelRegistry → BaseSyncedStore` import cycle;
|
|
18
|
+
* the real `Database` still satisfies it structurally, so callers are unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export interface CommitOutboxDatabase {
|
|
21
|
+
sealTransactionRecord(record: PendingWrite, consumedRecordIds: readonly string[]): Promise<unknown>;
|
|
22
|
+
getPersistedTransactions(): Promise<readonly unknown[]>;
|
|
23
|
+
removeTransaction(id: string): Promise<void>;
|
|
24
24
|
}
|
|
25
25
|
/** Strict IndexedDB adapter backed by Database's `__transactions` store. */
|
|
26
|
-
export declare class DatabaseCommitOutboxStore implements
|
|
26
|
+
export declare class DatabaseCommitOutboxStore implements DurableWriteStore {
|
|
27
27
|
private readonly database;
|
|
28
|
-
constructor(database:
|
|
29
|
-
seal(envelope:
|
|
28
|
+
constructor(database: CommitOutboxDatabase);
|
|
29
|
+
seal(envelope: PendingWrite, consumedRecordIds: readonly string[]): Promise<void>;
|
|
30
30
|
list(): Promise<readonly unknown[]>;
|
|
31
31
|
remove(envelopeRecordId: string): Promise<void>;
|
|
32
32
|
}
|