@abloatai/ablo 0.9.1 → 0.9.2
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/AGENTS.md +84 -0
- package/CHANGELOG.md +34 -0
- package/README.md +14 -6
- package/dist/BaseSyncedStore.js +6 -2
- package/dist/SyncClient.d.ts +12 -0
- package/dist/SyncClient.js +15 -0
- package/dist/agent/index.js +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +7 -49
- package/dist/client/Ablo.js +2 -104
- package/dist/client/ApiClient.d.ts +1 -113
- package/dist/client/ApiClient.js +0 -232
- package/dist/client/auth.js +32 -2
- package/dist/client/httpClient.d.ts +5 -6
- package/dist/client/httpClient.js +2 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/errorCodes.js +3 -3
- package/dist/index.js +1 -1
- package/dist/interfaces/index.d.ts +4 -4
- package/dist/mutators/UndoManager.d.ts +17 -0
- package/dist/mutators/UndoManager.js +53 -0
- package/dist/react/AbloProvider.d.ts +18 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useUndoScope.js +7 -0
- package/dist/server/commit.d.ts +4 -5
- package/dist/types/streams.d.ts +2 -1
- package/dist/wire/frames.d.ts +6 -8
- package/docs/api.md +1 -1
- package/docs/cli.md +17 -4
- package/docs/data-sources.md +68 -83
- package/docs/examples/ai-sdk-tool.md +11 -5
- package/docs/examples/existing-python-backend.md +26 -4
- package/docs/examples/nextjs.md +3 -2
- package/docs/examples/scoped-agent.md +38 -11
- package/docs/identity.md +86 -59
- package/docs/integration-guide.md +85 -54
- package/docs/react.md +39 -28
- package/llms.txt +18 -11
- package/package.json +2 -2
package/dist/client/ApiClient.js
CHANGED
|
@@ -9,7 +9,6 @@ import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloVal
|
|
|
9
9
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, } from './auth.js';
|
|
10
10
|
import { toSeconds } from '../utils/duration.js';
|
|
11
11
|
const DEFAULT_AGENT_LEASE = '10m';
|
|
12
|
-
const DEFAULT_INTENT_LEASE = '2m';
|
|
13
12
|
export function createProtocolClient(options) {
|
|
14
13
|
const env = readProcessEnv();
|
|
15
14
|
const authInput = { options, env };
|
|
@@ -100,144 +99,6 @@ export function createProtocolClient(options) {
|
|
|
100
99
|
schema: null,
|
|
101
100
|
});
|
|
102
101
|
}
|
|
103
|
-
function createAgent(id, agentOptions) {
|
|
104
|
-
return {
|
|
105
|
-
id,
|
|
106
|
-
async run(runOptions, handler) {
|
|
107
|
-
if (runOptions.signal?.aborted) {
|
|
108
|
-
return { status: 'cancelled' };
|
|
109
|
-
}
|
|
110
|
-
let capability = null;
|
|
111
|
-
let task = null;
|
|
112
|
-
try {
|
|
113
|
-
const leaseOptions = agentOptions.leaseSeconds !== undefined
|
|
114
|
-
? { leaseSeconds: agentOptions.leaseSeconds }
|
|
115
|
-
: { lease: agentOptions.lease ?? DEFAULT_AGENT_LEASE };
|
|
116
|
-
capability = await capabilities.create({
|
|
117
|
-
participantKind: 'agent',
|
|
118
|
-
participantId: id,
|
|
119
|
-
syncGroups: agentOptions.syncGroups ?? ['default'],
|
|
120
|
-
operations: agentOptions.can,
|
|
121
|
-
label: agentOptions.label ?? id,
|
|
122
|
-
userMeta: agentOptions.userMeta,
|
|
123
|
-
...leaseOptions,
|
|
124
|
-
});
|
|
125
|
-
const agentClient = capability.client();
|
|
126
|
-
task = await agentClient.tasks.create({
|
|
127
|
-
prompt: runOptions.prompt,
|
|
128
|
-
parentTaskId: runOptions.parentTaskId,
|
|
129
|
-
surface: runOptions.surface ?? 'agent',
|
|
130
|
-
metadata: runOptions.metadata,
|
|
131
|
-
});
|
|
132
|
-
const context = createAgentRunContext(agentClient, task);
|
|
133
|
-
const value = await handler(context);
|
|
134
|
-
await task.close({
|
|
135
|
-
costInputTokens: runOptions.costInputTokens,
|
|
136
|
-
costOutputTokens: runOptions.costOutputTokens,
|
|
137
|
-
costComputeMs: runOptions.costComputeMs,
|
|
138
|
-
});
|
|
139
|
-
return { status: 'done', task, value };
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
if (task) {
|
|
143
|
-
await task.close({
|
|
144
|
-
costInputTokens: runOptions.costInputTokens,
|
|
145
|
-
costOutputTokens: runOptions.costOutputTokens,
|
|
146
|
-
costComputeMs: runOptions.costComputeMs,
|
|
147
|
-
}).catch(() => { });
|
|
148
|
-
}
|
|
149
|
-
if (isAbortError(error) || runOptions.signal?.aborted) {
|
|
150
|
-
return { status: 'cancelled', task: task ?? undefined, error };
|
|
151
|
-
}
|
|
152
|
-
return { status: 'failed', task: task ?? undefined, error };
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
if (capability) {
|
|
156
|
-
await capabilities.revoke(capability.id).catch(() => { });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function createAgentRunContext(agentClient, task) {
|
|
163
|
-
return {
|
|
164
|
-
task,
|
|
165
|
-
ablo: agentClient,
|
|
166
|
-
model(name) {
|
|
167
|
-
return createAgentModelClient(agentClient, name);
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function createAgentModelClient(agentClient, name) {
|
|
172
|
-
const base = agentClient.model(name);
|
|
173
|
-
return {
|
|
174
|
-
retrieve(params) {
|
|
175
|
-
// Reads are never blocked by a claim (coordination.md): a claim
|
|
176
|
-
// serializes WRITERS, not readers. So — unlike the create/update/
|
|
177
|
-
// delete paths below — retrieve does NOT apply the agent claimed
|
|
178
|
-
// default; options pass through and the read path's `'return'`
|
|
179
|
-
// default keeps a claimed row readable. A caller can still opt into
|
|
180
|
-
// gating with an explicit `ifClaimed` (developer's choice).
|
|
181
|
-
return base.retrieve(params);
|
|
182
|
-
},
|
|
183
|
-
create(params) {
|
|
184
|
-
const id = params.id ?? createModelId();
|
|
185
|
-
return withAgentIntent(agentClient, name, id, params, (commitIntent) => base.create({
|
|
186
|
-
...stripAgentRuntimeOptions(params),
|
|
187
|
-
id,
|
|
188
|
-
data: params.data,
|
|
189
|
-
intent: commitIntent,
|
|
190
|
-
}));
|
|
191
|
-
},
|
|
192
|
-
update(params) {
|
|
193
|
-
return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.update({
|
|
194
|
-
...stripAgentRuntimeOptions(params),
|
|
195
|
-
id: params.id,
|
|
196
|
-
data: params.data,
|
|
197
|
-
intent: commitIntent,
|
|
198
|
-
}));
|
|
199
|
-
},
|
|
200
|
-
delete(params) {
|
|
201
|
-
return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.delete({
|
|
202
|
-
...stripAgentRuntimeOptions(params),
|
|
203
|
-
id: params.id,
|
|
204
|
-
intent: commitIntent,
|
|
205
|
-
}));
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
async function withAgentIntent(agentClient, modelName, id, mutationOptions, commit) {
|
|
210
|
-
const intentInput = mutationOptions?.intent;
|
|
211
|
-
const targetOverride = intentInput != null && typeof intentInput === 'object' && !isIntentHandleRef(intentInput)
|
|
212
|
-
? intentInput.target ?? {}
|
|
213
|
-
: {};
|
|
214
|
-
const target = {
|
|
215
|
-
...targetOverride,
|
|
216
|
-
model: targetOverride.model ?? modelName,
|
|
217
|
-
id: targetOverride.id ?? id,
|
|
218
|
-
...(intentInput != null && typeof intentInput === 'object' && !isIntentHandleRef(intentInput) && intentInput.field
|
|
219
|
-
? { field: intentInput.field }
|
|
220
|
-
: {}),
|
|
221
|
-
};
|
|
222
|
-
await applyClaimedPolicy(target, withAgentClaimedDefault(mutationOptions), 'wait');
|
|
223
|
-
if (intentInput == null || isIntentHandleRef(intentInput)) {
|
|
224
|
-
return commit(intentInput);
|
|
225
|
-
}
|
|
226
|
-
const action = typeof intentInput === 'string' ? intentInput : intentInput.action;
|
|
227
|
-
const intent = await agentClient.intents.create({
|
|
228
|
-
target,
|
|
229
|
-
action,
|
|
230
|
-
ttl: typeof intentInput === 'object'
|
|
231
|
-
? intentInput.ttl ?? DEFAULT_INTENT_LEASE
|
|
232
|
-
: DEFAULT_INTENT_LEASE,
|
|
233
|
-
});
|
|
234
|
-
try {
|
|
235
|
-
return await commit(intent);
|
|
236
|
-
}
|
|
237
|
-
finally {
|
|
238
|
-
await intent.release().catch(() => { });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
102
|
function normalizeCommitOperation(op, defaults) {
|
|
242
103
|
const model = op.model ?? op.target?.model;
|
|
243
104
|
if (!model) {
|
|
@@ -476,51 +337,6 @@ export function createProtocolClient(options) {
|
|
|
476
337
|
return capabilities.create(options);
|
|
477
338
|
},
|
|
478
339
|
};
|
|
479
|
-
const tasks = {
|
|
480
|
-
async create(taskOptions) {
|
|
481
|
-
const body = await requestJson('/v1/tasks', {
|
|
482
|
-
method: 'POST',
|
|
483
|
-
body: JSON.stringify({
|
|
484
|
-
prompt: taskOptions.prompt,
|
|
485
|
-
parentTaskId: taskOptions.parentTaskId,
|
|
486
|
-
surface: taskOptions.surface,
|
|
487
|
-
metadata: taskOptions.metadata,
|
|
488
|
-
}),
|
|
489
|
-
});
|
|
490
|
-
const id = body.id ?? body.taskId ?? body.turnId;
|
|
491
|
-
if (!id) {
|
|
492
|
-
throw new AbloValidationError('Task create response did not include an id.', { code: 'task_id_missing' });
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
id,
|
|
496
|
-
turnId: id,
|
|
497
|
-
promptHash: body.promptHash,
|
|
498
|
-
openedAt: body.openedAt,
|
|
499
|
-
close: (stats) => tasks.close(id, stats),
|
|
500
|
-
};
|
|
501
|
-
},
|
|
502
|
-
async close(id, stats) {
|
|
503
|
-
const body = await requestJson(`/v1/tasks/${encodeURIComponent(id)}/close`, {
|
|
504
|
-
method: 'POST',
|
|
505
|
-
body: JSON.stringify({
|
|
506
|
-
costInputTokens: stats?.costInputTokens ?? 0,
|
|
507
|
-
costOutputTokens: stats?.costOutputTokens ?? 0,
|
|
508
|
-
costComputeMs: stats?.costComputeMs ?? 0,
|
|
509
|
-
}),
|
|
510
|
-
});
|
|
511
|
-
const closedId = body.id ?? body.taskId ?? body.turnId ?? id;
|
|
512
|
-
return {
|
|
513
|
-
id: closedId,
|
|
514
|
-
turnId: closedId,
|
|
515
|
-
closed: body.closed ?? body.alreadyClosed ?? true,
|
|
516
|
-
alreadyClosed: body.alreadyClosed,
|
|
517
|
-
endedAt: body.endedAt,
|
|
518
|
-
};
|
|
519
|
-
},
|
|
520
|
-
open(options) {
|
|
521
|
-
return tasks.create(options);
|
|
522
|
-
},
|
|
523
|
-
};
|
|
524
340
|
const intents = {
|
|
525
341
|
async create(intentOptions) {
|
|
526
342
|
const intentId = createIntentId();
|
|
@@ -780,35 +596,14 @@ export function createProtocolClient(options) {
|
|
|
780
596
|
async dispose() { },
|
|
781
597
|
async purge() { },
|
|
782
598
|
capabilities,
|
|
783
|
-
tasks,
|
|
784
599
|
intents,
|
|
785
600
|
commits,
|
|
786
601
|
model,
|
|
787
|
-
agent: createAgent,
|
|
788
602
|
async getAuthToken() {
|
|
789
603
|
// Mirror `authHeaders()`: a configured API key wins, else the
|
|
790
604
|
// construction-time auth token. Resolve the (possibly async) key setter.
|
|
791
605
|
return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
|
|
792
606
|
},
|
|
793
|
-
async beginTurn(turnOptions) {
|
|
794
|
-
const task = await tasks.create(turnOptions);
|
|
795
|
-
let closed = false;
|
|
796
|
-
const close = async (stats) => {
|
|
797
|
-
if (closed)
|
|
798
|
-
return;
|
|
799
|
-
closed = true;
|
|
800
|
-
await task.close(stats);
|
|
801
|
-
};
|
|
802
|
-
const dispose = () => {
|
|
803
|
-
closed = true;
|
|
804
|
-
};
|
|
805
|
-
return {
|
|
806
|
-
turnId: task.id,
|
|
807
|
-
close,
|
|
808
|
-
dispose,
|
|
809
|
-
[Symbol.asyncDispose]: close,
|
|
810
|
-
};
|
|
811
|
-
},
|
|
812
607
|
};
|
|
813
608
|
}
|
|
814
609
|
function normalizeIntentId(intent) {
|
|
@@ -816,33 +611,6 @@ function normalizeIntentId(intent) {
|
|
|
816
611
|
return intent;
|
|
817
612
|
return intent?.id;
|
|
818
613
|
}
|
|
819
|
-
function withAgentClaimedDefault(options) {
|
|
820
|
-
return {
|
|
821
|
-
ifClaimed: 'fail',
|
|
822
|
-
...(options ?? {}),
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
function stripAgentRuntimeOptions(options) {
|
|
826
|
-
if (!options)
|
|
827
|
-
return undefined;
|
|
828
|
-
const { intent: _intent, ifClaimed: _ifClaimed, claimedTimeout: _claimedTimeout, claimedPollInterval: _claimedPollInterval, maxQueueDepth: _maxQueueDepth, ...rest } = options;
|
|
829
|
-
return rest;
|
|
830
|
-
}
|
|
831
|
-
function isIntentHandleRef(input) {
|
|
832
|
-
return (typeof input === 'object' &&
|
|
833
|
-
input !== null &&
|
|
834
|
-
'id' in input &&
|
|
835
|
-
typeof input.id === 'string' &&
|
|
836
|
-
!('action' in input));
|
|
837
|
-
}
|
|
838
|
-
function isAbortError(error) {
|
|
839
|
-
return (typeof DOMException !== 'undefined' &&
|
|
840
|
-
error instanceof DOMException &&
|
|
841
|
-
error.name === 'AbortError') || (typeof error === 'object' &&
|
|
842
|
-
error !== null &&
|
|
843
|
-
'name' in error &&
|
|
844
|
-
error.name === 'AbortError');
|
|
845
|
-
}
|
|
846
614
|
function parseBody(bodyText) {
|
|
847
615
|
if (bodyText.length === 0)
|
|
848
616
|
return null;
|
package/dist/client/auth.js
CHANGED
|
@@ -146,8 +146,38 @@ export function resolveBootstrapBaseUrl(input) {
|
|
|
146
146
|
// legitimately arrive as `wss://…` — normalize it here rather than
|
|
147
147
|
// faceplanting at fetch time. The derive branch below already does this;
|
|
148
148
|
// the override branch silently skipped it.
|
|
149
|
-
return normalizeAbloHostedBaseUrl(input.bootstrapBaseUrl).replace(/^ws/, 'http');
|
|
149
|
+
return ensureApiSuffix(normalizeAbloHostedBaseUrl(input.bootstrapBaseUrl).replace(/^ws/, 'http'));
|
|
150
150
|
}
|
|
151
151
|
const url = normalizeAbloHostedBaseUrl(input.url);
|
|
152
|
-
return
|
|
152
|
+
return ensureApiSuffix(url.replace(/^ws/, 'http'));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Guarantee the HTTP base ends in the `/api` route segment the sync-server
|
|
156
|
+
* mounts every endpoint under (`apps/sync-server/src/index.ts` — `app.route('/api', …)`).
|
|
157
|
+
*
|
|
158
|
+
* The derive branch always appended `/api`; the override branch did NOT,
|
|
159
|
+
* trusting the caller (apps/web passes `${baseUrl}/api`). But a hosted
|
|
160
|
+
* customer setting a custom `baseURL`/`bootstrapBaseUrl` (their own subdomain,
|
|
161
|
+
* staging, etc.) without the suffix sent every credential exchange to
|
|
162
|
+
* `…/auth/capability` instead of `…/api/auth/capability` → a 404 surfaced as
|
|
163
|
+
* `exchange_failed`. Since the SDK hardcodes routes relative to this base and
|
|
164
|
+
* there is no valid Ablo deployment that serves them off the root, normalizing
|
|
165
|
+
* to a single trailing `/api` here is always correct — and idempotent for
|
|
166
|
+
* callers who already include it.
|
|
167
|
+
*/
|
|
168
|
+
function ensureApiSuffix(httpBase) {
|
|
169
|
+
const trimmed = httpBase.replace(/\/+$/, '');
|
|
170
|
+
try {
|
|
171
|
+
const u = new URL(trimmed);
|
|
172
|
+
const segments = u.pathname.split('/').filter(Boolean);
|
|
173
|
+
if (segments[segments.length - 1] === 'api')
|
|
174
|
+
return trimmed;
|
|
175
|
+
u.pathname = `${u.pathname.replace(/\/+$/, '')}/api`;
|
|
176
|
+
return u.toString().replace(/\/+$/, '');
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Should be unreachable post-`normalizeAbloHostedBaseUrl` (which yields an
|
|
180
|
+
// absolute URL), but fall back to a string check rather than throwing.
|
|
181
|
+
return /\/api$/.test(trimmed) ? trimmed : `${trimmed}/api`;
|
|
182
|
+
}
|
|
153
183
|
}
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* wraps it in a typed proxy facade so server code gets the SAME `client.<model>`
|
|
23
23
|
* surface as the browser client — typed proxies, stateless transport.
|
|
24
24
|
*/
|
|
25
|
-
import { type AbloApiClientOptions
|
|
26
|
-
import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions
|
|
25
|
+
import { type AbloApiClientOptions } from './ApiClient.js';
|
|
26
|
+
import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions } from './Ablo.js';
|
|
27
27
|
import type { ModelCreateParams, ModelDeleteParams, ModelLoadOptions, ModelRetrieveParams, ModelUpdateParams } from './createModelProxy.js';
|
|
28
28
|
import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
|
|
29
29
|
export interface AbloHttpClientOptions<S extends SchemaRecord> extends Omit<AbloApiClientOptions, 'schema'> {
|
|
@@ -47,7 +47,7 @@ export interface HttpModelClient<T, C = T> {
|
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
49
|
* The honest type of the stateless HTTP client: typed model proxies (the
|
|
50
|
-
* request/response subset) + `commits` + `
|
|
50
|
+
* request/response subset) + `commits` + `dispose`. Reaching for a
|
|
51
51
|
* stateful-only capability (`get`/`getAll`/`getCount`, `onChange`,
|
|
52
52
|
* `claim.state`/`queue`/`reorder`) is a COMPILE error here, not a latent runtime
|
|
53
53
|
* `undefined` — the type matches what the transport can actually do.
|
|
@@ -56,7 +56,6 @@ export type AbloHttpClient<S extends SchemaRecord> = {
|
|
|
56
56
|
readonly [K in keyof S & string]: HttpModelClient<InferModel<Schema<S>, K>, InferCreate<Schema<S>, K>>;
|
|
57
57
|
} & {
|
|
58
58
|
readonly commits: CommitResource;
|
|
59
|
-
beginTurn(options: TaskCreateOptions): Promise<Turn>;
|
|
60
59
|
dispose(): Promise<void>;
|
|
61
60
|
/** Resolve the bearer credential this client authenticates with (see `AbloApi.getAuthToken`). */
|
|
62
61
|
getAuthToken(): Promise<string | null>;
|
|
@@ -65,7 +64,7 @@ export type AbloHttpClient<S extends SchemaRecord> = {
|
|
|
65
64
|
};
|
|
66
65
|
/**
|
|
67
66
|
* Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
|
|
68
|
-
* client's `model(name)`; `commits`, `
|
|
69
|
-
*
|
|
67
|
+
* client's `model(name)`; `commits`, `dispose`, etc. pass through. No socket is
|
|
68
|
+
* ever opened; identity is the Bearer credential.
|
|
70
69
|
*/
|
|
71
70
|
export declare function createAbloHttpClient<S extends SchemaRecord>(options: AbloHttpClientOptions<S>): AbloHttpClient<S>;
|
|
@@ -36,14 +36,13 @@ const PROTOCOL_MEMBERS = new Set([
|
|
|
36
36
|
'dispose',
|
|
37
37
|
'purge',
|
|
38
38
|
'commits',
|
|
39
|
-
'beginTurn',
|
|
40
39
|
'model',
|
|
41
40
|
'getAuthToken',
|
|
42
41
|
]);
|
|
43
42
|
/**
|
|
44
43
|
* Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
|
|
45
|
-
* client's `model(name)`; `commits`, `
|
|
46
|
-
*
|
|
44
|
+
* client's `model(name)`; `commits`, `dispose`, etc. pass through. No socket is
|
|
45
|
+
* ever opened; identity is the Bearer credential.
|
|
47
46
|
*/
|
|
48
47
|
export function createAbloHttpClient(options) {
|
|
49
48
|
// The schema is type-level only; the protocol client is schema-agnostic.
|
package/dist/client/index.d.ts
CHANGED
|
@@ -32,5 +32,5 @@
|
|
|
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
33
|
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './auth.js';
|
|
34
34
|
export type { AbloPersistence } from './persistence.js';
|
|
35
|
-
export type { AbloApi, AbloApiClientOptions, AbloApiIntents,
|
|
35
|
+
export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, } from './ApiClient.js';
|
|
36
36
|
export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
|
package/dist/errorCodes.js
CHANGED
|
@@ -176,8 +176,8 @@ export const ERROR_CODES = {
|
|
|
176
176
|
check_violation: wire('validation', 400, false, 'A value violates a database check constraint.'),
|
|
177
177
|
constraint_violation: wire('validation', 400, false, 'A database integrity constraint was violated.'),
|
|
178
178
|
// ── tenant / unknown model (400) ───────────────────────────────────
|
|
179
|
-
server_execute_unknown_model: wire('tenant', 400, false, 'The server
|
|
180
|
-
mutate_create_unknown_model: wire('tenant', 400, false, '
|
|
179
|
+
server_execute_unknown_model: wire('tenant', 400, false, 'Wrote to a model the server does not know. The server keeps its own copy of the schema — run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` before writing to new or changed models.'),
|
|
180
|
+
mutate_create_unknown_model: wire('tenant', 400, false, 'Created a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` first — the server keeps its own copy of the schema.'),
|
|
181
181
|
tenant_model_columns_unknown: wire('tenant', 400, false, "The tenant model's columns could not be resolved."),
|
|
182
182
|
tenant_model_missing_organization_id: wire('tenant', 400, false, 'The tenant model is missing the organization_id column required for isolation.'),
|
|
183
183
|
// ── schema migration / declaration (validation) ────────────────────
|
|
@@ -286,7 +286,7 @@ export const ERROR_CODES = {
|
|
|
286
286
|
provisioner_unavailable: wire('server', 503, false, 'No database provisioner is configured.'),
|
|
287
287
|
invalid_model: wire('validation', 400, false, 'The request named an invalid model.'),
|
|
288
288
|
invalid_id: wire('validation', 400, false, 'The request carried an invalid id.'),
|
|
289
|
-
unknown_model: wire('tenant', 400, false, '
|
|
289
|
+
unknown_model: wire('tenant', 400, false, 'Named a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` — the server keeps its own copy of the schema.'),
|
|
290
290
|
model_not_tenant_scoped: wire('tenant', 400, false, 'The model is not tenant-scoped and cannot be queried this way.'),
|
|
291
291
|
schema_table_invalid: wire('schema', 500, false, "The model's table identifier is invalid."),
|
|
292
292
|
schema_scope_invalid: wire('schema', 500, false, "The model's scope predicate could not be built."),
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_UR
|
|
|
61
61
|
// Participant types live under `Ablo.Participant.*` —
|
|
62
62
|
// `Ablo.Participant.Joined`, `Ablo.Participant.Manager`,
|
|
63
63
|
// `Ablo.Participant.JoinOptions`, etc. Same dot-access shape as
|
|
64
|
-
// `Ablo.Peer`, `Ablo.Claim
|
|
64
|
+
// `Ablo.Peer`, `Ablo.Claim`. No flat re-exports.
|
|
65
65
|
// Advanced — most apps never import this. Principal constructors for
|
|
66
66
|
// delegated agent paths (`Ablo({ kind: 'agent', as: session({...}) })`).
|
|
67
67
|
// The default `Ablo({ schema, apiKey })` resolves identity from the key;
|
|
@@ -164,10 +164,10 @@ export interface MutationOptions {
|
|
|
164
164
|
readonly id: string;
|
|
165
165
|
} | null;
|
|
166
166
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
167
|
+
* Dormant agent-task lineage field, forwarded as the wire-level
|
|
168
|
+
* `causedByTaskId`. Turns/tasks were removed from the SDK; nothing
|
|
169
|
+
* populates this anymore (write attribution rides on the claim/intent
|
|
170
|
+
* id). Kept optional for wire-compat; always `null` from the client.
|
|
171
171
|
*/
|
|
172
172
|
causedByTaskId?: string | null;
|
|
173
173
|
}
|
|
@@ -75,6 +75,15 @@ export declare class UndoScope<S extends Schema> {
|
|
|
75
75
|
* observer can never wedge the editor's recording path.
|
|
76
76
|
*/
|
|
77
77
|
private readonly recordListeners;
|
|
78
|
+
/**
|
|
79
|
+
* Observers notified after ANY stack change — record, undo, redo, or clear.
|
|
80
|
+
* Distinct from {@link recordListeners} (forward actions only): this fires on
|
|
81
|
+
* reversals too, so React consumers can keep `canUndo`/`canRedo` live. The
|
|
82
|
+
* stream-recording path pushes entries WITHOUT a React render, so without this
|
|
83
|
+
* a freshly-recorded entry leaves `canUndo` stale (snapshot from last render)
|
|
84
|
+
* and a Cmd+Z handler gated on `canUndo !== false` silently no-ops.
|
|
85
|
+
*/
|
|
86
|
+
private readonly changeListeners;
|
|
78
87
|
/**
|
|
79
88
|
* Serialization tail. Recording, undo, and redo all chain off this single
|
|
80
89
|
* promise so they run strictly in the order they were *invoked* — never
|
|
@@ -175,6 +184,14 @@ export declare class UndoScope<S extends Schema> {
|
|
|
175
184
|
*/
|
|
176
185
|
onRecord(listener: (entry: UndoEntry) => void): () => void;
|
|
177
186
|
private emitRecord;
|
|
187
|
+
/**
|
|
188
|
+
* Subscribe to ANY stack change (record/undo/redo/clear). Used by
|
|
189
|
+
* `useUndoScope` to re-render so `canUndo`/`canRedo` stay live across every
|
|
190
|
+
* consumer — not just the component that invoked undo/redo. Returns an
|
|
191
|
+
* unsubscribe function.
|
|
192
|
+
*/
|
|
193
|
+
onChange(listener: () => void): () => void;
|
|
194
|
+
private emitChange;
|
|
178
195
|
canUndo(): boolean;
|
|
179
196
|
canRedo(): boolean;
|
|
180
197
|
/**
|
|
@@ -46,6 +46,15 @@ export class UndoScope {
|
|
|
46
46
|
* observer can never wedge the editor's recording path.
|
|
47
47
|
*/
|
|
48
48
|
recordListeners = new Set();
|
|
49
|
+
/**
|
|
50
|
+
* Observers notified after ANY stack change — record, undo, redo, or clear.
|
|
51
|
+
* Distinct from {@link recordListeners} (forward actions only): this fires on
|
|
52
|
+
* reversals too, so React consumers can keep `canUndo`/`canRedo` live. The
|
|
53
|
+
* stream-recording path pushes entries WITHOUT a React render, so without this
|
|
54
|
+
* a freshly-recorded entry leaves `canUndo` stale (snapshot from last render)
|
|
55
|
+
* and a Cmd+Z handler gated on `canUndo !== false` silently no-ops.
|
|
56
|
+
*/
|
|
57
|
+
changeListeners = new Set();
|
|
49
58
|
/**
|
|
50
59
|
* Serialization tail. Recording, undo, and redo all chain off this single
|
|
51
60
|
* promise so they run strictly in the order they were *invoked* — never
|
|
@@ -246,6 +255,7 @@ export class UndoScope {
|
|
|
246
255
|
this.undoStack.shift();
|
|
247
256
|
this.redoStack = [];
|
|
248
257
|
this.emitRecord(entry);
|
|
258
|
+
this.emitChange();
|
|
249
259
|
}
|
|
250
260
|
/**
|
|
251
261
|
* Subscribe to every recorded mutation. Fires synchronously at the tail of
|
|
@@ -275,6 +285,30 @@ export class UndoScope {
|
|
|
275
285
|
}
|
|
276
286
|
}
|
|
277
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Subscribe to ANY stack change (record/undo/redo/clear). Used by
|
|
290
|
+
* `useUndoScope` to re-render so `canUndo`/`canRedo` stay live across every
|
|
291
|
+
* consumer — not just the component that invoked undo/redo. Returns an
|
|
292
|
+
* unsubscribe function.
|
|
293
|
+
*/
|
|
294
|
+
onChange(listener) {
|
|
295
|
+
this.changeListeners.add(listener);
|
|
296
|
+
return () => {
|
|
297
|
+
this.changeListeners.delete(listener);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
emitChange() {
|
|
301
|
+
for (const listener of this.changeListeners) {
|
|
302
|
+
try {
|
|
303
|
+
listener();
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
if (typeof console !== 'undefined') {
|
|
307
|
+
console.error('[UndoScope] onChange listener threw', err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
278
312
|
canUndo() {
|
|
279
313
|
return this.undoStack.length > 0;
|
|
280
314
|
}
|
|
@@ -302,12 +336,21 @@ export class UndoScope {
|
|
|
302
336
|
try {
|
|
303
337
|
await applyOps(tx, ops);
|
|
304
338
|
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
// The replay was rejected (e.g. a server 409): the world didn't change,
|
|
341
|
+
// so restore the entry to the undo stack rather than silently dropping
|
|
342
|
+
// it (which would also strand it off the redo stack — invisible undo).
|
|
343
|
+
this.undoStack.push(entry);
|
|
344
|
+
this.emitChange();
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
305
347
|
finally {
|
|
306
348
|
this.replaying = false;
|
|
307
349
|
}
|
|
308
350
|
this.redoStack.push(entry);
|
|
309
351
|
if (this.redoStack.length > this.maxHistory)
|
|
310
352
|
this.redoStack.shift();
|
|
353
|
+
this.emitChange();
|
|
311
354
|
});
|
|
312
355
|
}
|
|
313
356
|
/**
|
|
@@ -327,12 +370,20 @@ export class UndoScope {
|
|
|
327
370
|
try {
|
|
328
371
|
await applyOps(tx, ops);
|
|
329
372
|
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
// Symmetric to undo: a rejected re-apply leaves state unchanged, so put
|
|
375
|
+
// the entry back on the redo stack instead of losing it.
|
|
376
|
+
this.redoStack.push(entry);
|
|
377
|
+
this.emitChange();
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
330
380
|
finally {
|
|
331
381
|
this.replaying = false;
|
|
332
382
|
}
|
|
333
383
|
this.undoStack.push(entry);
|
|
334
384
|
if (this.undoStack.length > this.maxHistory)
|
|
335
385
|
this.undoStack.shift();
|
|
386
|
+
this.emitChange();
|
|
336
387
|
});
|
|
337
388
|
}
|
|
338
389
|
/** Drop all history. Use after bootstrap / sync group change / sync error. */
|
|
@@ -340,6 +391,7 @@ export class UndoScope {
|
|
|
340
391
|
this.undoStack = [];
|
|
341
392
|
this.redoStack = [];
|
|
342
393
|
this.batch = [];
|
|
394
|
+
this.emitChange();
|
|
343
395
|
}
|
|
344
396
|
/** Introspection — for debug panels / e2e tests. */
|
|
345
397
|
size() {
|
|
@@ -353,6 +405,7 @@ export class UndoScope {
|
|
|
353
405
|
dispose() {
|
|
354
406
|
this.unsubscribe();
|
|
355
407
|
this.recordListeners.clear();
|
|
408
|
+
this.changeListeners.clear();
|
|
356
409
|
this.batch = [];
|
|
357
410
|
}
|
|
358
411
|
}
|
|
@@ -28,20 +28,30 @@ import { type SyncStoreContract } from './context.js';
|
|
|
28
28
|
/**
|
|
29
29
|
* Props for `<AbloProvider>`.
|
|
30
30
|
*
|
|
31
|
-
* The
|
|
31
|
+
* The one required prop is a prebuilt {@link Ablo} client — the client
|
|
32
|
+
* owns auth and the credential lifecycle; this provider is the reactive
|
|
33
|
+
* binding over it (Stripe's `<Elements stripe={...}>` model):
|
|
32
34
|
*
|
|
33
35
|
* ```tsx
|
|
34
|
-
*
|
|
36
|
+
* // Build once at module scope — a new instance per render tears down the socket.
|
|
37
|
+
* const ablo = Ablo({
|
|
38
|
+
* schema,
|
|
39
|
+
* getToken: () =>
|
|
40
|
+
* fetch('/api/ablo-session', { method: 'POST' })
|
|
41
|
+
* .then((r) => r.json())
|
|
42
|
+
* .then((d) => d.token),
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* <AbloProvider client={ablo}>
|
|
35
46
|
* <App />
|
|
36
47
|
* </AbloProvider>
|
|
37
48
|
* ```
|
|
38
49
|
*
|
|
39
|
-
* That's it for most apps
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* don't recognize a prop there, you don't need it.
|
|
50
|
+
* That's it for most apps. `userId` is informational; the `fallback`,
|
|
51
|
+
* `preventUnsavedChanges`, and `on*` props are opt-in app glue; and the
|
|
52
|
+
* block tagged "Optional DI (advanced)" below is escape-hatch wiring for
|
|
53
|
+
* tests and platform builders — if you don't recognize a prop there, you
|
|
54
|
+
* don't need it.
|
|
45
55
|
*/
|
|
46
56
|
export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
47
57
|
/**
|
package/dist/react/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* useCurrentUserId() — the provider's userId prop
|
|
28
28
|
*
|
|
29
29
|
* Multiplayer (always available — `<AbloProvider>` always constructs a client):
|
|
30
|
-
* useAblo((ablo) => ablo.
|
|
30
|
+
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
31
31
|
* useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
|
|
32
32
|
* usePresence() — typed presence view
|
|
33
33
|
* useIntent(name) — typed intent dispatcher
|
package/dist/react/index.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* useCurrentUserId() — the provider's userId prop
|
|
28
28
|
*
|
|
29
29
|
* Multiplayer (always available — `<AbloProvider>` always constructs a client):
|
|
30
|
-
* useAblo((ablo) => ablo.
|
|
30
|
+
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
31
31
|
* useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
|
|
32
32
|
* usePresence() — typed presence view
|
|
33
33
|
* useIntent(name) — typed intent dispatcher
|
|
@@ -52,6 +52,13 @@ export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
|
|
|
52
52
|
useEffect(() => {
|
|
53
53
|
setTick(0);
|
|
54
54
|
}, [scope]);
|
|
55
|
+
// Re-render on ANY stack change — including entries recorded from the local-
|
|
56
|
+
// mutation stream, which don't otherwise trigger a React update. Without this
|
|
57
|
+
// `canUndo`/`canRedo` go stale in every consumer that didn't itself call
|
|
58
|
+
// undo/redo (e.g. a keyboard handler whose Cmd+Z gate then never fires).
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
return scope.onChange(() => setTick((t) => t + 1));
|
|
61
|
+
}, [scope]);
|
|
55
62
|
const size = scope.size();
|
|
56
63
|
return {
|
|
57
64
|
scope,
|