@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/Database.js +9 -6
  3. package/dist/SyncClient.d.ts +2 -2
  4. package/dist/SyncClient.js +22 -10
  5. package/dist/adapters/inMemoryStorage.js +18 -14
  6. package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
  7. package/dist/auth/schemas.d.ts +6 -0
  8. package/dist/auth/schemas.js +7 -0
  9. package/dist/cli.cjs +815 -400
  10. package/dist/client/Ablo.d.ts +29 -23
  11. package/dist/client/Ablo.js +8 -196
  12. package/dist/client/claimHeartbeatLoop.d.ts +1 -1
  13. package/dist/client/claimHeartbeatLoop.js +1 -1
  14. package/dist/client/createInternalComponents.d.ts +4 -2
  15. package/dist/client/createInternalComponents.js +3 -1
  16. package/dist/client/durableWrites.d.ts +21 -0
  17. package/dist/client/durableWrites.js +46 -0
  18. package/dist/client/httpClient.d.ts +19 -45
  19. package/dist/client/httpClient.js +104 -26
  20. package/dist/client/httpTransport.d.ts +8 -0
  21. package/dist/client/{ApiClient.js → httpTransport.js} +78 -291
  22. package/dist/client/options.d.ts +43 -19
  23. package/dist/client/options.js +3 -2
  24. package/dist/client/resourceTypes.d.ts +9 -85
  25. package/dist/client/resourceTypes.js +1 -1
  26. package/dist/client/sessionMint.d.ts +5 -6
  27. package/dist/client/sessionMint.js +4 -5
  28. package/dist/client/validateAbloOptions.js +3 -3
  29. package/dist/coordination/schema.d.ts +30 -0
  30. package/dist/coordination/schema.js +14 -0
  31. package/dist/core/index.d.ts +1 -1
  32. package/dist/errors.d.ts +4 -40
  33. package/dist/errors.js +5 -5
  34. package/dist/index.d.ts +14 -8
  35. package/dist/index.js +7 -5
  36. package/dist/interfaces/index.d.ts +3 -0
  37. package/dist/mutators/defineMutators.d.ts +18 -0
  38. package/dist/mutators/defineMutators.js +4 -8
  39. package/dist/react/index.d.ts +1 -1
  40. package/dist/schema/index.d.ts +1 -1
  41. package/dist/schema/index.js +1 -1
  42. package/dist/schema/select.d.ts +15 -0
  43. package/dist/schema/select.js +27 -3
  44. package/dist/source/connector.d.ts +3 -3
  45. package/dist/source/factory.d.ts +4 -6
  46. package/dist/source/factory.js +4 -7
  47. package/dist/source/index.d.ts +4 -4
  48. package/dist/source/index.js +2 -2
  49. package/dist/source/signing.d.ts +0 -3
  50. package/dist/source/types.d.ts +0 -26
  51. package/dist/stores/ObjectStore.js +8 -8
  52. package/dist/surface.d.ts +1 -1
  53. package/dist/surface.js +1 -0
  54. package/dist/sync/BootstrapFetcher.d.ts +10 -0
  55. package/dist/sync/BootstrapFetcher.js +27 -4
  56. package/dist/sync/createClaimStream.d.ts +11 -2
  57. package/dist/sync/createClaimStream.js +4 -3
  58. package/dist/testing/mocks/FakeDatabase.d.ts +1 -1
  59. package/dist/transactions/TransactionQueue.d.ts +2 -2
  60. package/dist/transactions/TransactionQueue.js +3 -3
  61. package/dist/transactions/commitEnvelope.d.ts +1 -1
  62. package/dist/transactions/commitEnvelope.js +2 -2
  63. package/dist/transactions/commitOutboxStore.d.ts +19 -19
  64. package/dist/transactions/durableWriteStore.d.ts +123 -0
  65. package/dist/transactions/durableWriteStore.js +30 -0
  66. package/dist/transactions/httpCommitEnvelope.d.ts +1 -0
  67. package/dist/transactions/httpCommitEnvelope.js +14 -12
  68. package/dist/transactions/idempotencyKey.d.ts +10 -0
  69. package/dist/transactions/idempotencyKey.js +9 -0
  70. package/dist/types/global.d.ts +10 -29
  71. package/dist/types/global.js +4 -7
  72. package/dist/types/streams.d.ts +2 -28
  73. package/dist/wire/frames.d.ts +6 -257
  74. package/dist/wire/frames.js +4 -25
  75. package/dist/wire/index.d.ts +4 -3
  76. package/dist/wire/index.js +2 -2
  77. package/dist/wire/protocolVersion.d.ts +30 -17
  78. package/dist/wire/protocolVersion.js +34 -18
  79. package/docs/agents.md +8 -3
  80. package/docs/api.md +6 -4
  81. package/docs/client-behavior.md +6 -3
  82. package/docs/coordination.md +2 -3
  83. package/docs/data-sources.md +5 -8
  84. package/docs/guarantees.md +21 -0
  85. package/docs/integration-guide.md +1 -0
  86. package/docs/migration.md +21 -1
  87. package/docs/quickstart.md +11 -0
  88. package/docs/react.md +0 -46
  89. package/examples/README.md +6 -5
  90. package/llms.txt +15 -15
  91. package/package.json +2 -7
  92. package/dist/client/ApiClient.d.ts +0 -177
  93. package/dist/commit/contract.d.ts +0 -493
  94. package/dist/commit/contract.js +0 -187
  95. package/dist/commit/index.d.ts +0 -6
  96. package/dist/commit/index.js +0 -5
@@ -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: "${String(key)}" is not a model in the source schema`, { code: 'invalid_schema', param: String(key) });
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 "${String(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: `${String(key)}.${relName}` });
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)` or
66
- * `abloSource(options)`. The connector feeds it each request and relays the
67
- * response back untouched; it never inspects or alters either one.
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`. */
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Builds the Data Source request handler returned by `abloSource()` and its alias
3
- * `dataSource()`. Given your options — the schema, the API key, and either
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 AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
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 — `abloSource({ schema, files: { load, list, commit } })` —
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 {};
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Builds the Data Source request handler returned by `abloSource()` and its alias
3
- * `dataSource()`. Given your options — the schema, the API key, and either
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 abloSource(options) {
135
- return async function handleAbloSource(request) {
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
- }
@@ -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 `abloSource()` / `dataSource()` endpoint factory
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, type DataSourcePrimitive, type DataSourceWhere, type DataSourceListQuery, type DataSourceListPage, type DataSourceListResult, type DataSourceRequestContext, type DataSourceOperation, type DataSourceDelta, type DataSourceEvent, type DataSourceEventForOperationOptions, type DataSourceCommitResult, type DataSourceCommitParams, type DataSourceScope, type DataSourceEventsResult, type DataSourceEventsHandler, type DataSourceAuthorizeContext, type DataSourceHandlerContext, type DataSourceModelHandlers, type DataSourceCommitHandler, type DataSourceApiKey, type DataSourceLoadRequest, type DataSourceListRequest, type DataSourceCommitRequest, type DataSourceEventsRequest, type DataSourceRequest, type DataSourceResponse, } from './types.js';
12
- export { ABLO_SOURCE_HEADERS, SourceSignatureError, signAbloSourceRequest, verifyAbloSourceRequest, type SourceSignatureOptions, type SourceSignatureVerificationOptions, type SourceSignatureVerificationResult, type DataSourceSignatureOptions, type DataSourceSignatureVerificationOptions, type DataSourceSignatureVerificationResult, } from './signing.js';
13
- export { abloSource, dataSource, type AbloSourceOptions, type DataSourceOptions, } from './factory.js';
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';
@@ -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 `abloSource()` / `dataSource()` endpoint factory
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 { abloSource, dataSource, } from './factory.js';
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
@@ -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;
@@ -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 || new Error('IndexedDB transaction error')); };
45
- request.onerror = () => { reject(request.error || new Error('IndexedDB 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 || new Error('IndexedDB transaction error')); };
97
- request.onerror = () => { reject(request.error || new Error('IndexedDB 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 || new Error('IndexedDB transaction error')); };
201
- request.onerror = () => { reject(request.error || new Error('IndexedDB 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 || new Error('IndexedDB transaction error')); };
237
- request.onerror = () => { reject(request.error || new Error('IndexedDB 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
@@ -58,6 +58,7 @@ export const PUBLIC_ABLO_OPTION_KEYS = [
58
58
  'authEndpoint',
59
59
  'databaseUrl',
60
60
  'persistence',
61
+ 'durableWrites',
61
62
  'commitOutbox',
62
63
  'commitOutboxScope',
63
64
  'transport',
@@ -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 detected — this app was built against schema ${clientHash}, ` +
41
- `but the deployed schema is ${serverHash}. Operations that depend on schema ` +
42
- `changes not yet deployed will fail later with an opaque database error. Run ` +
43
- `\`ablo push\` to deploy your schema (or update this app to match the deployed one).`, { clientSchemaHash: clientHash, serverSchemaHash: serverHash });
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: SyncWebSocket): void;
48
+ attach(transport: ClaimTransport): void;
40
49
  dispose(): void;
41
50
  }
42
- export declare function createClaimStream(config: ClaimStreamConfig, transport?: SyncWebSocket | null): AttachableClaimStream;
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 lost = payload;
177
- if (!lost.claimId)
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 CommitOutboxStore port
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 { CommitOutboxStore } from './commitOutboxStore.js';
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: CommitOutboxStore): void;
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 seal the commit outbox before dispatch', {
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] Commit outbox cleanup deferred', {
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 commit outbox', {
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
- /** The legacy mutation operation shape sent by the current commit transport. */
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
- /** The legacy mutation operation shape sent by the current commit transport. */
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 { Database } from '../Database.js';
9
- import type { DurableCommitEnvelope } from './commitEnvelope.js';
10
- import type { DurableHttpCommitEnvelope } from './httpCommitEnvelope.js';
11
- export type CommitOutboxRecord = DurableCommitEnvelope | DurableHttpCommitEnvelope;
12
- export interface CommitOutboxStore {
13
- /**
14
- * Atomically reserve an envelope and consume the staged records it owns.
15
- * Implementations must be scoped to one logical participant + server plane,
16
- * reject same-id/different-request seals, and let only one envelope claim a
17
- * staged record.
18
- */
19
- seal(envelope: CommitOutboxRecord, consumedRecordIds: readonly string[]): Promise<void>;
20
- /** Load unacknowledged records. Implementations may return untrusted data. */
21
- list(): Promise<readonly unknown[]>;
22
- /** Remove one definitively settled envelope. */
23
- remove(envelopeRecordId: string): Promise<void>;
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 CommitOutboxStore {
26
+ export declare class DatabaseCommitOutboxStore implements DurableWriteStore {
27
27
  private readonly database;
28
- constructor(database: Database);
29
- seal(envelope: CommitOutboxRecord, consumedRecordIds: readonly string[]): Promise<void>;
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
  }