@abloatai/ablo 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/Model.d.ts +22 -0
- package/dist/Model.js +45 -0
- package/dist/ObjectPool.d.ts +14 -1
- package/dist/ObjectPool.js +8 -1
- package/dist/SyncClient.js +7 -1
- package/dist/SyncEngineContext.js +2 -0
- package/dist/ai-sdk/coordination-context.d.ts +2 -1
- package/dist/ai-sdk/coordination-context.js +3 -3
- package/dist/ai-sdk/index.d.ts +3 -4
- package/dist/ai-sdk/index.js +2 -3
- package/dist/ai-sdk/wrap.d.ts +13 -18
- package/dist/ai-sdk/wrap.js +2 -6
- package/dist/cli.cjs +253 -160
- package/dist/client/Ablo.d.ts +83 -22
- package/dist/client/Ablo.js +116 -32
- package/dist/client/ApiClient.d.ts +2 -2
- package/dist/client/ApiClient.js +27 -32
- package/dist/client/auth.d.ts +26 -0
- package/dist/client/auth.js +208 -0
- package/dist/client/createModelProxy.d.ts +16 -11
- package/dist/client/createModelProxy.js +15 -15
- package/dist/client/sessionMint.d.ts +10 -0
- package/dist/client/sessionMint.js +8 -1
- package/dist/coordination/schema.d.ts +10 -10
- package/dist/coordination/schema.js +7 -8
- package/dist/coordination/trace.d.ts +91 -0
- package/dist/coordination/trace.js +147 -0
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +1 -2
- package/dist/errors.js +6 -9
- package/dist/index.d.ts +6 -1
- package/dist/index.js +13 -0
- package/dist/interfaces/index.d.ts +45 -2
- package/dist/mutators/undoApply.d.ts +7 -2
- package/dist/mutators/undoApply.js +7 -35
- package/dist/policy/types.d.ts +18 -9
- package/dist/policy/types.js +26 -16
- package/dist/react/AbloProvider.d.ts +2 -2
- package/dist/react/useAblo.js +12 -2
- package/dist/schema/sugar.js +10 -2
- package/dist/server/read-config.d.ts +7 -0
- package/dist/sync/HydrationCoordinator.js +7 -3
- package/dist/sync/SyncWebSocket.d.ts +11 -2
- package/dist/sync/SyncWebSocket.js +67 -3
- package/dist/sync/createClaimStream.d.ts +9 -2
- package/dist/sync/createClaimStream.js +9 -7
- package/dist/sync/participants.d.ts +12 -11
- package/dist/sync/participants.js +5 -5
- package/dist/transactions/TransactionQueue.d.ts +1 -0
- package/dist/transactions/TransactionQueue.js +40 -3
- package/dist/types/streams.d.ts +57 -135
- package/dist/utils/json.d.ts +39 -0
- package/dist/utils/json.js +88 -0
- package/docs/coordination.md +16 -0
- package/docs/debugging.md +194 -0
- package/docs/index.md +1 -0
- package/package.json +1 -1
- package/dist/ai-sdk/claim-broadcast.d.ts +0 -83
- package/dist/ai-sdk/claim-broadcast.js +0 -79
package/dist/client/auth.d.ts
CHANGED
|
@@ -49,6 +49,29 @@ export interface AuthResolveInput {
|
|
|
49
49
|
export declare function readProcessEnv(): Record<string, string | undefined>;
|
|
50
50
|
export declare function resolveApiKey(input: AuthResolveInput): string | ApiKeySetter | null;
|
|
51
51
|
export declare function resolveAuthToken(input: AuthResolveInput): string | null;
|
|
52
|
+
type CliMode = 'sandbox' | 'production';
|
|
53
|
+
type StaticApiKeySource = 'option' | 'env';
|
|
54
|
+
interface StaticApiKey {
|
|
55
|
+
readonly key: string;
|
|
56
|
+
readonly source: StaticApiKeySource;
|
|
57
|
+
}
|
|
58
|
+
interface CliCredentialSnapshot {
|
|
59
|
+
readonly mode: CliMode;
|
|
60
|
+
readonly activeProfile: string;
|
|
61
|
+
readonly storedKey?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface CliKeyMismatch {
|
|
64
|
+
readonly source: StaticApiKeySource;
|
|
65
|
+
readonly configuredKeyPrefix: string;
|
|
66
|
+
readonly configuredMode?: CliMode;
|
|
67
|
+
readonly cliMode: CliMode;
|
|
68
|
+
readonly storedKeyPrefix?: string;
|
|
69
|
+
readonly kind: 'mode_mismatch' | 'key_override';
|
|
70
|
+
readonly message: string;
|
|
71
|
+
}
|
|
72
|
+
/** Infer sandbox/production from Ablo key prefixes without importing CLI code. */
|
|
73
|
+
export declare function modeFromApiKey(key: string): CliMode | undefined;
|
|
74
|
+
export declare function describeCliKeyMismatch(configured: StaticApiKey, cli: CliCredentialSnapshot): CliKeyMismatch | null;
|
|
52
75
|
/**
|
|
53
76
|
* Resolve the direct-URL connector's Postgres connection string.
|
|
54
77
|
*
|
|
@@ -63,6 +86,8 @@ export declare function resolveAuthToken(input: AuthResolveInput): string | null
|
|
|
63
86
|
*/
|
|
64
87
|
export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
|
|
65
88
|
export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
89
|
+
export declare function warnIfDatabaseUrlDeprecated(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
90
|
+
export declare function warnIfCliKeyMismatch(input: AuthResolveInput, warn?: (message: string) => void): Promise<void>;
|
|
66
91
|
export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
|
|
67
92
|
export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
|
|
68
93
|
export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
|
|
@@ -108,3 +133,4 @@ export declare function resolveBootstrapBaseUrl(input: {
|
|
|
108
133
|
readonly url: string;
|
|
109
134
|
readonly bootstrapBaseUrl?: string;
|
|
110
135
|
}): string;
|
|
136
|
+
export {};
|
package/dist/client/auth.js
CHANGED
|
@@ -28,6 +28,160 @@ export function resolveApiKey(input) {
|
|
|
28
28
|
export function resolveAuthToken(input) {
|
|
29
29
|
return input.options.authToken ?? null;
|
|
30
30
|
}
|
|
31
|
+
function keyPrefix(key) {
|
|
32
|
+
return `${key.slice(0, 12)}…`;
|
|
33
|
+
}
|
|
34
|
+
/** Infer sandbox/production from Ablo key prefixes without importing CLI code. */
|
|
35
|
+
export function modeFromApiKey(key) {
|
|
36
|
+
if (/^(sk|rk)_test_/.test(key))
|
|
37
|
+
return 'sandbox';
|
|
38
|
+
if (/^(sk|rk)_live_/.test(key))
|
|
39
|
+
return 'production';
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function resolveStaticApiKey(input) {
|
|
43
|
+
if (typeof input.options.apiKey === 'string') {
|
|
44
|
+
return { key: input.options.apiKey, source: 'option' };
|
|
45
|
+
}
|
|
46
|
+
if (input.options.apiKey !== undefined && input.options.apiKey !== null) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const envKey = input.env.ABLO_API_KEY;
|
|
50
|
+
if (typeof envKey === 'string' && envKey.length > 0) {
|
|
51
|
+
return { key: envKey, source: 'env' };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function readProfileKeys(value) {
|
|
56
|
+
if (!value || typeof value !== 'object')
|
|
57
|
+
return {};
|
|
58
|
+
const profiles = {};
|
|
59
|
+
for (const [name, raw] of Object.entries(value)) {
|
|
60
|
+
if (!raw || typeof raw !== 'object')
|
|
61
|
+
continue;
|
|
62
|
+
const row = raw;
|
|
63
|
+
const sandbox = row.sandbox;
|
|
64
|
+
const production = row.production;
|
|
65
|
+
profiles[name] = {
|
|
66
|
+
sandbox: sandbox && typeof sandbox === 'object' ? sandbox : undefined,
|
|
67
|
+
production: production && typeof production === 'object' ? production : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return profiles;
|
|
71
|
+
}
|
|
72
|
+
function legacyProfileKeys(value) {
|
|
73
|
+
if (!value)
|
|
74
|
+
return { sandbox: undefined, production: undefined };
|
|
75
|
+
const sandbox = value.sandbox;
|
|
76
|
+
const production = value.production;
|
|
77
|
+
if ((sandbox && typeof sandbox === 'object') ||
|
|
78
|
+
(production && typeof production === 'object')) {
|
|
79
|
+
return {
|
|
80
|
+
sandbox: sandbox && typeof sandbox === 'object' ? sandbox : undefined,
|
|
81
|
+
production: production && typeof production === 'object' ? production : undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (typeof value.apiKey === 'string') {
|
|
85
|
+
return { sandbox: { apiKey: value.apiKey }, production: undefined };
|
|
86
|
+
}
|
|
87
|
+
return { sandbox: undefined, production: undefined };
|
|
88
|
+
}
|
|
89
|
+
function normalizeCliMode(value) {
|
|
90
|
+
return value === 'sandbox' || value === 'production' ? value : undefined;
|
|
91
|
+
}
|
|
92
|
+
function activeProjectSlug(value) {
|
|
93
|
+
if (!value || typeof value !== 'object')
|
|
94
|
+
return undefined;
|
|
95
|
+
const slug = value.slug;
|
|
96
|
+
return typeof slug === 'string' && slug.length > 0 ? slug : undefined;
|
|
97
|
+
}
|
|
98
|
+
function importNodeBuiltin(specifier) {
|
|
99
|
+
return import(specifier);
|
|
100
|
+
}
|
|
101
|
+
async function readJsonIfPresent(path) {
|
|
102
|
+
try {
|
|
103
|
+
const { readFile } = await importNodeBuiltin('node:fs/promises');
|
|
104
|
+
const text = await readFile(path, 'utf8');
|
|
105
|
+
const parsed = JSON.parse(text);
|
|
106
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function readCliCredentialSnapshot(env) {
|
|
113
|
+
const processLike = globalThis.process;
|
|
114
|
+
if (!processLike?.versions?.node)
|
|
115
|
+
return null;
|
|
116
|
+
if (typeof window !== 'undefined')
|
|
117
|
+
return null;
|
|
118
|
+
const [{ homedir }, { join }] = await Promise.all([
|
|
119
|
+
importNodeBuiltin('node:os'),
|
|
120
|
+
importNodeBuiltin('node:path'),
|
|
121
|
+
]);
|
|
122
|
+
const dir = env.ABLO_CONFIG_DIR
|
|
123
|
+
?? (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, 'ablo') : join(homedir(), '.config', 'ablo'));
|
|
124
|
+
const [cfg, creds] = await Promise.all([
|
|
125
|
+
readJsonIfPresent(join(dir, 'config.json')),
|
|
126
|
+
readJsonIfPresent(join(dir, 'credentials.json')),
|
|
127
|
+
]);
|
|
128
|
+
const mode = normalizeCliMode(cfg?.mode) ?? normalizeCliMode(creds?.mode);
|
|
129
|
+
const activeProfile = activeProjectSlug(cfg?.activeProject) ?? 'default';
|
|
130
|
+
const profiles = {
|
|
131
|
+
...readProfileKeys(creds?.profiles),
|
|
132
|
+
...readProfileKeys(cfg?.profiles),
|
|
133
|
+
};
|
|
134
|
+
if (!profiles[activeProfile]) {
|
|
135
|
+
const legacy = { ...legacyProfileKeys(cfg), ...legacyProfileKeys(creds) };
|
|
136
|
+
if (legacy.sandbox?.apiKey || legacy.production?.apiKey) {
|
|
137
|
+
profiles[activeProfile] = legacy;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const effectiveMode = mode ?? (Object.keys(profiles).length > 0 ? 'sandbox' : undefined);
|
|
141
|
+
if (!effectiveMode)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
mode: effectiveMode,
|
|
145
|
+
activeProfile,
|
|
146
|
+
...(profiles[activeProfile]?.[effectiveMode]?.apiKey
|
|
147
|
+
? { storedKey: profiles[activeProfile][effectiveMode].apiKey }
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function describeCliKeyMismatch(configured, cli) {
|
|
152
|
+
const configuredMode = modeFromApiKey(configured.key);
|
|
153
|
+
const configuredKeyPrefix = keyPrefix(configured.key);
|
|
154
|
+
const storedKeyPrefix = cli.storedKey ? keyPrefix(cli.storedKey) : undefined;
|
|
155
|
+
const sourceLabel = configured.source === 'env' ? 'ABLO_API_KEY' : 'configured apiKey';
|
|
156
|
+
if (configuredMode && configuredMode !== cli.mode) {
|
|
157
|
+
return {
|
|
158
|
+
source: configured.source,
|
|
159
|
+
configuredKeyPrefix,
|
|
160
|
+
configuredMode,
|
|
161
|
+
cliMode: cli.mode,
|
|
162
|
+
...(storedKeyPrefix ? { storedKeyPrefix } : {}),
|
|
163
|
+
kind: 'mode_mismatch',
|
|
164
|
+
message: `${sourceLabel} is a ${configuredMode} key (${configuredKeyPrefix}) but the Ablo CLI is in ` +
|
|
165
|
+
`${cli.mode} mode${storedKeyPrefix ? ` (active stored key ${storedKeyPrefix})` : ''}. ` +
|
|
166
|
+
`Requests will use ${configuredMode}. Use the ${cli.mode} key, unset ABLO_API_KEY, ` +
|
|
167
|
+
`or run \`ablo mode ${configuredMode}\` intentionally.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (configured.source === 'env' && cli.storedKey && configured.key !== cli.storedKey) {
|
|
171
|
+
return {
|
|
172
|
+
source: configured.source,
|
|
173
|
+
configuredKeyPrefix,
|
|
174
|
+
...(configuredMode ? { configuredMode } : {}),
|
|
175
|
+
cliMode: cli.mode,
|
|
176
|
+
storedKeyPrefix,
|
|
177
|
+
kind: 'key_override',
|
|
178
|
+
message: `ABLO_API_KEY (${configuredKeyPrefix}) overrides the CLI's stored active ${cli.mode} key ` +
|
|
179
|
+
`(${storedKeyPrefix}). Requests will use the environment key. Unset ABLO_API_KEY to use ` +
|
|
180
|
+
'`ablo status` / `ablo mode` credentials.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
31
185
|
/**
|
|
32
186
|
* Resolve the direct-URL connector's Postgres connection string.
|
|
33
187
|
*
|
|
@@ -85,6 +239,60 @@ export function warnIfDatabaseUrlEnvIgnored(input, warn) {
|
|
|
85
239
|
else if (typeof console !== 'undefined')
|
|
86
240
|
console.warn('[Ablo]', message);
|
|
87
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* One-time deprecation nudge for the `databaseUrl` direct connector.
|
|
244
|
+
*
|
|
245
|
+
* `databaseUrl` registers the `dedicated` storage mode — Ablo opens a pool INTO
|
|
246
|
+
* the caller's Postgres and writes into it directly. That is the operate-their-
|
|
247
|
+
* database posture we are moving off. Ablo is Stripe-shaped: it hosts only the
|
|
248
|
+
* transaction log (the ordered sync_deltas) + coordination, never your data — your
|
|
249
|
+
* rows always live in your own database. The supported path is the signed Data
|
|
250
|
+
* Source endpoint (`dataSource(...)`), where your app owns the write and your
|
|
251
|
+
* credentials never leave it. See docs/plans/stripe-shaped-storage-posture.md.
|
|
252
|
+
*
|
|
253
|
+
* Still honored at runtime so existing integrations keep working; this only warns
|
|
254
|
+
* once per process (so it never spams) and falls back to `console.warn` when no
|
|
255
|
+
* logger is supplied (the `transport: 'http'`/`'api'` client has none).
|
|
256
|
+
*/
|
|
257
|
+
let warnedDatabaseUrlDeprecated = false;
|
|
258
|
+
export function warnIfDatabaseUrlDeprecated(input, warn) {
|
|
259
|
+
if (warnedDatabaseUrlDeprecated)
|
|
260
|
+
return;
|
|
261
|
+
if (input.options.databaseUrl == null)
|
|
262
|
+
return;
|
|
263
|
+
warnedDatabaseUrlDeprecated = true;
|
|
264
|
+
const message = '`databaseUrl` (the direct connector) is deprecated and will be removed from ' +
|
|
265
|
+
'the supported path. It lets Ablo dial into your database; we are moving off ' +
|
|
266
|
+
'that. Ablo hosts only the transaction log — your data stays in your DB. Expose ' +
|
|
267
|
+
'a signed Data Source endpoint (`dataSource(...)`) so your app owns the write, ' +
|
|
268
|
+
'or self-host the engine to keep the log in your infra too. ' +
|
|
269
|
+
'See docs/plans/stripe-shaped-storage-posture.md.';
|
|
270
|
+
if (warn)
|
|
271
|
+
warn(message);
|
|
272
|
+
else if (typeof console !== 'undefined')
|
|
273
|
+
console.warn('[Ablo]', message);
|
|
274
|
+
}
|
|
275
|
+
let warnedCliKeyMismatch = false;
|
|
276
|
+
export async function warnIfCliKeyMismatch(input, warn) {
|
|
277
|
+
if (warnedCliKeyMismatch)
|
|
278
|
+
return;
|
|
279
|
+
if (input.env.NODE_ENV === 'production')
|
|
280
|
+
return;
|
|
281
|
+
const configured = resolveStaticApiKey(input);
|
|
282
|
+
if (!configured)
|
|
283
|
+
return;
|
|
284
|
+
const cli = await readCliCredentialSnapshot(input.env);
|
|
285
|
+
if (!cli)
|
|
286
|
+
return;
|
|
287
|
+
const mismatch = describeCliKeyMismatch(configured, cli);
|
|
288
|
+
if (!mismatch)
|
|
289
|
+
return;
|
|
290
|
+
warnedCliKeyMismatch = true;
|
|
291
|
+
if (warn)
|
|
292
|
+
warn(mismatch.message);
|
|
293
|
+
else if (typeof console !== 'undefined')
|
|
294
|
+
console.warn('[Ablo]', mismatch.message);
|
|
295
|
+
}
|
|
88
296
|
export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
|
|
89
297
|
export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
90
298
|
export const ABLO_DEFAULT_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
@@ -22,7 +22,7 @@ import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
|
22
22
|
import type { JoinedParticipant } from '../sync/participants.js';
|
|
23
23
|
import type { LoadWhere } from '../query/types.js';
|
|
24
24
|
import { ModelScope } from '../types/index.js';
|
|
25
|
-
import type { Duration, Claim,
|
|
25
|
+
import type { Duration, Claim, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
|
|
26
26
|
export interface ModelClientMeta {
|
|
27
27
|
readonly key: string;
|
|
28
28
|
readonly typename: string;
|
|
@@ -90,7 +90,7 @@ export interface ModelCollaboration<T> {
|
|
|
90
90
|
range?: TargetRange;
|
|
91
91
|
meta?: Record<string, unknown>;
|
|
92
92
|
};
|
|
93
|
-
/** Human-readable phase (`'editing'`)
|
|
93
|
+
/** Human-readable phase (`'editing'`). */
|
|
94
94
|
reason: string;
|
|
95
95
|
ttl?: Duration;
|
|
96
96
|
/**
|
|
@@ -101,7 +101,7 @@ export interface ModelCollaboration<T> {
|
|
|
101
101
|
queue?: boolean;
|
|
102
102
|
/** Reject (don't wait) if the queue is already this deep when we join. */
|
|
103
103
|
maxQueueDepth?: number;
|
|
104
|
-
}): Promise<
|
|
104
|
+
}): Promise<Claim>;
|
|
105
105
|
createSnapshot(modelKey: string, id: string): Snapshot;
|
|
106
106
|
/**
|
|
107
107
|
* Current coordination state on a target — who (if anyone) holds it.
|
|
@@ -185,7 +185,7 @@ export interface ModelCollaboration<T> {
|
|
|
185
185
|
}
|
|
186
186
|
export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
187
187
|
/** Human-readable phase shown to observers while held. Defaults to
|
|
188
|
-
* `'editing'`. The same word on every claim surface
|
|
188
|
+
* `'editing'`. The same word on every claim surface. */
|
|
189
189
|
reason?: string;
|
|
190
190
|
/** Peer-visible explanation of the work being performed. */
|
|
191
191
|
description?: string;
|
|
@@ -258,7 +258,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
|
|
|
258
258
|
* `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
|
|
259
259
|
* lease id and snapshot watermark for attribution + stale protection.
|
|
260
260
|
*/
|
|
261
|
-
export type {
|
|
261
|
+
export type { Claim };
|
|
262
262
|
export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
|
|
263
263
|
/**
|
|
264
264
|
* The coordination surface for a model, exposed as a callable namespace.
|
|
@@ -312,7 +312,7 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
|
|
|
312
312
|
*/
|
|
313
313
|
reorder(params: ClaimReorderParams<T>): void;
|
|
314
314
|
/** Release a manual claim handle early. Single-write claims auto-release. */
|
|
315
|
-
release(params: ClaimLookupParams<T> |
|
|
315
|
+
release(params: ClaimLookupParams<T> | Claim<T>): Promise<void>;
|
|
316
316
|
}
|
|
317
317
|
/**
|
|
318
318
|
* The awaited form of a claim method: a synchronous return becomes a `Promise`,
|
|
@@ -321,8 +321,13 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
|
|
|
321
321
|
*/
|
|
322
322
|
export type AwaitedClaimMethod<F> = F extends (...args: infer A) => infer R ? R extends Promise<unknown> ? (...args: A) => R : (...args: A) => Promise<R> : F;
|
|
323
323
|
export interface ClaimApi<T> extends ClaimReadApi<T> {
|
|
324
|
-
/**
|
|
325
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Take a claim and get an explicit held-work handle back. Returns a
|
|
326
|
+
* {@link Claim} — `data` (and `readAt`) are guaranteed present
|
|
327
|
+
* because this door always re-reads the row under the lease, so callers use
|
|
328
|
+
* `handle.data` directly without a guard.
|
|
329
|
+
*/
|
|
330
|
+
(params: ClaimParams<T>): Promise<Claim<T>>;
|
|
326
331
|
}
|
|
327
332
|
export interface ModelRetrieveParams extends ServerRetrieveOptions {
|
|
328
333
|
readonly id: string;
|
|
@@ -330,16 +335,16 @@ export interface ModelRetrieveParams extends ServerRetrieveOptions {
|
|
|
330
335
|
export interface ModelCreateParams<T, CreateInput> extends MutationOptions {
|
|
331
336
|
readonly data: CreateInput;
|
|
332
337
|
readonly id?: string | null;
|
|
333
|
-
readonly claim?:
|
|
338
|
+
readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
|
|
334
339
|
}
|
|
335
340
|
export interface ModelUpdateParams<T> extends MutationOptions {
|
|
336
341
|
readonly id: string;
|
|
337
342
|
readonly data: Partial<T>;
|
|
338
|
-
readonly claim?:
|
|
343
|
+
readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
|
|
339
344
|
}
|
|
340
345
|
export interface ModelDeleteParams<T> extends MutationOptions {
|
|
341
346
|
readonly id: string;
|
|
342
|
-
readonly claim?:
|
|
347
|
+
readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
|
|
343
348
|
}
|
|
344
349
|
/** Options for the WebSocket-only `ablo.<model>.watch(ids, options?)`. */
|
|
345
350
|
export interface WatchOptions {
|
|
@@ -39,7 +39,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
39
39
|
// with the commit op's model name; a lease recorded under the schema key
|
|
40
40
|
// never collides with it — which silently disarmed the guard for every
|
|
41
41
|
// model whose schema key differs from its typename (i.e. nearly all of
|
|
42
|
-
// them, plural key vs singular typename). Public surfaces (
|
|
42
|
+
// them, plural key vs singular typename). Public surfaces (Claim.
|
|
43
43
|
// target.model) keep the schema key; only the wire/coordination targets
|
|
44
44
|
// use this.
|
|
45
45
|
const wireModel = registeredModelName.toLowerCase();
|
|
@@ -90,7 +90,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
90
90
|
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
91
91
|
value !== null &&
|
|
92
92
|
value.object === 'claim' &&
|
|
93
|
-
typeof value.
|
|
93
|
+
typeof value.id === 'string' &&
|
|
94
94
|
typeof value.release === 'function';
|
|
95
95
|
const claimMeta = (options) => {
|
|
96
96
|
if (!options?.description)
|
|
@@ -131,7 +131,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
131
131
|
if (!held)
|
|
132
132
|
return;
|
|
133
133
|
activeClaims.delete(id);
|
|
134
|
-
await held.lease.release();
|
|
134
|
+
await held.lease.release?.();
|
|
135
135
|
};
|
|
136
136
|
const takeClaim = async (params) => {
|
|
137
137
|
if (!collaboration) {
|
|
@@ -214,7 +214,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
214
214
|
}
|
|
215
215
|
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
216
216
|
const reason = options?.reason ?? 'editing';
|
|
217
|
-
// The self-claim's `
|
|
217
|
+
// The self-claim's `ClaimTarget` mirrors what a peer's `claim.state` would
|
|
218
218
|
// report (`state` maps `held.target.model` → `type`), so a holder and a
|
|
219
219
|
// peer see the SAME target.type for one row — the wire model token.
|
|
220
220
|
const selfTarget = {
|
|
@@ -235,7 +235,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
235
235
|
expiresAt,
|
|
236
236
|
});
|
|
237
237
|
const target = {
|
|
238
|
-
|
|
238
|
+
type: schemaKey,
|
|
239
239
|
id,
|
|
240
240
|
...(options?.field ? { field: options.field } : {}),
|
|
241
241
|
...(options?.path ? { path: options.path } : {}),
|
|
@@ -245,7 +245,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
245
245
|
const release = () => releaseClaim(id);
|
|
246
246
|
return {
|
|
247
247
|
object: 'claim',
|
|
248
|
-
|
|
248
|
+
id: lease.id,
|
|
249
249
|
readAt: snapshot.stamp,
|
|
250
250
|
target,
|
|
251
251
|
reason,
|
|
@@ -278,7 +278,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
278
278
|
if (own) {
|
|
279
279
|
return {
|
|
280
280
|
object: 'claim',
|
|
281
|
-
id: own.lease.
|
|
281
|
+
id: own.lease.id,
|
|
282
282
|
status: 'active',
|
|
283
283
|
target: own.target,
|
|
284
284
|
reason: own.reason,
|
|
@@ -403,7 +403,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
403
403
|
const effective = {
|
|
404
404
|
...opts,
|
|
405
405
|
...(autoLease ? { claim: autoLease } : {}),
|
|
406
|
-
...(isClaimHandle(claim) ? { claim: { id: claim.
|
|
406
|
+
...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
|
|
407
407
|
};
|
|
408
408
|
try {
|
|
409
409
|
syncClient.add(model, effective);
|
|
@@ -411,7 +411,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
411
411
|
return modelAsRow(model);
|
|
412
412
|
}
|
|
413
413
|
finally {
|
|
414
|
-
await autoLease?.release().catch(() => { });
|
|
414
|
+
await autoLease?.release?.().catch(() => { });
|
|
415
415
|
}
|
|
416
416
|
}),
|
|
417
417
|
update: guard(async (params) => {
|
|
@@ -422,7 +422,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
422
422
|
return await operations.update({ ...params, claim: handle });
|
|
423
423
|
}
|
|
424
424
|
finally {
|
|
425
|
-
await handle.release();
|
|
425
|
+
await handle.release?.();
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
const { id } = params;
|
|
@@ -439,7 +439,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
439
439
|
wait: 'confirmed',
|
|
440
440
|
readAt: claimed.snapshot.stamp,
|
|
441
441
|
onStale: 'reject',
|
|
442
|
-
claimRef: { id: claimed.lease.
|
|
442
|
+
claimRef: { id: claimed.lease.id },
|
|
443
443
|
...opts,
|
|
444
444
|
}
|
|
445
445
|
: {
|
|
@@ -454,7 +454,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
454
454
|
}
|
|
455
455
|
: {}),
|
|
456
456
|
...opts,
|
|
457
|
-
...(handle ? { claim: { id: handle.
|
|
457
|
+
...(handle ? { claim: { id: handle.id } } : {}),
|
|
458
458
|
};
|
|
459
459
|
// Local user update: `applyChanges` keeps change tracking ON so
|
|
460
460
|
// the edited fields land in `modifiedProperties` and actually get
|
|
@@ -473,7 +473,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
473
473
|
await operations.delete({ ...params, claim: handle });
|
|
474
474
|
}
|
|
475
475
|
finally {
|
|
476
|
-
await handle.release();
|
|
476
|
+
await handle.release?.();
|
|
477
477
|
}
|
|
478
478
|
return;
|
|
479
479
|
}
|
|
@@ -489,7 +489,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
489
489
|
wait: 'confirmed',
|
|
490
490
|
readAt: claimed.snapshot.stamp,
|
|
491
491
|
onStale: 'reject',
|
|
492
|
-
claimRef: { id: claimed.lease.
|
|
492
|
+
claimRef: { id: claimed.lease.id },
|
|
493
493
|
...opts,
|
|
494
494
|
}
|
|
495
495
|
: {
|
|
@@ -501,7 +501,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
501
501
|
}
|
|
502
502
|
: {}),
|
|
503
503
|
...opts,
|
|
504
|
-
...(handle ? { claim: { id: handle.
|
|
504
|
+
...(handle ? { claim: { id: handle.id } } : {}),
|
|
505
505
|
};
|
|
506
506
|
syncClient.delete(model, effective);
|
|
507
507
|
await waitForMutation(model, effective);
|
|
@@ -6,6 +6,16 @@ export interface MintSessionContext {
|
|
|
6
6
|
readonly apiKey: string;
|
|
7
7
|
readonly baseUrl: string;
|
|
8
8
|
readonly fetch?: typeof fetch;
|
|
9
|
+
/**
|
|
10
|
+
* Schema-key → wire typename map, supplied ONLY by the schema client
|
|
11
|
+
* (`Ablo`). A capability is scoped by the lowercased TYPENAME the Hub
|
|
12
|
+
* checks, but `can` is keyed by schema key — so `can: { documents: ['update'] }`
|
|
13
|
+
* on a model whose `typename` is overridden to `Document` must mint
|
|
14
|
+
* `document.update`, not `documents.update` (else the Hub denies it with
|
|
15
|
+
* `capability_scope_denied`). The schemaless `ApiClient` omits this map:
|
|
16
|
+
* there the `can` key already IS the wire token, so no translation applies.
|
|
17
|
+
*/
|
|
18
|
+
readonly modelTypenames?: Readonly<Record<string, string>>;
|
|
9
19
|
}
|
|
10
20
|
/**
|
|
11
21
|
* Mint a session token from an already-resolved `sk_` credential + base URL.
|
|
@@ -63,7 +63,14 @@ export async function mintSession(params, ctx) {
|
|
|
63
63
|
userMeta: params.userMeta ?? { id: res.participantId },
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
const operations = Object.entries(params.can).flatMap(([model, ops]) =>
|
|
66
|
+
const operations = Object.entries(params.can).flatMap(([model, ops]) => {
|
|
67
|
+
// Translate the schema key the developer used in `can` to the wire
|
|
68
|
+
// typename the Hub gates on — see `modelTypenames` above. Falls back to
|
|
69
|
+
// the key when no map is supplied (schemaless client) or the key isn't
|
|
70
|
+
// in it, preserving the prior behaviour exactly for those callers.
|
|
71
|
+
const ns = ctx.modelTypenames?.[model] ?? model;
|
|
72
|
+
return (ops ?? []).map((op) => `${ns.toLowerCase()}.${op}`);
|
|
73
|
+
});
|
|
67
74
|
const res = await exchangeApiKey({
|
|
68
75
|
apiKey,
|
|
69
76
|
baseUrl,
|
|
@@ -209,7 +209,7 @@ export declare const wireClaimSummarySchema: z.ZodObject<{
|
|
|
209
209
|
entityId: z.ZodString;
|
|
210
210
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
211
211
|
claimId: z.ZodString;
|
|
212
|
-
|
|
212
|
+
reason: z.ZodString;
|
|
213
213
|
declaredAt: z.ZodNumber;
|
|
214
214
|
expiresAt: z.ZodNumber;
|
|
215
215
|
}, z.core.$strip>;
|
|
@@ -227,7 +227,7 @@ export declare const claimErrorSchema: z.ZodObject<{
|
|
|
227
227
|
entityId: z.ZodString;
|
|
228
228
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
229
229
|
claimId: z.ZodString;
|
|
230
|
-
|
|
230
|
+
reason: z.ZodString;
|
|
231
231
|
declaredAt: z.ZodNumber;
|
|
232
232
|
expiresAt: z.ZodNumber;
|
|
233
233
|
}, z.core.$strip>>;
|
|
@@ -237,7 +237,7 @@ export type ClaimError = z.infer<typeof claimErrorSchema>;
|
|
|
237
237
|
/**
|
|
238
238
|
* A declared pending-mutation claim — the unit broadcast in presence
|
|
239
239
|
* `activeClaims`. Clients supply the descriptive `targetRef` fields, an
|
|
240
|
-
* `
|
|
240
|
+
* explanatory `reason`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
|
|
241
241
|
* `expiresAt` and may set `status` / `error`.
|
|
242
242
|
*
|
|
243
243
|
* `status` and `error` are OPTIONAL: this single shape serves both the
|
|
@@ -259,7 +259,7 @@ export declare const wireClaimSchema: z.ZodObject<{
|
|
|
259
259
|
field: z.ZodOptional<z.ZodString>;
|
|
260
260
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
261
261
|
claimId: z.ZodString;
|
|
262
|
-
|
|
262
|
+
reason: z.ZodString;
|
|
263
263
|
declaredAt: z.ZodNumber;
|
|
264
264
|
expiresAt: z.ZodNumber;
|
|
265
265
|
status: z.ZodOptional<z.ZodEnum<{
|
|
@@ -280,7 +280,7 @@ export declare const wireClaimSchema: z.ZodObject<{
|
|
|
280
280
|
entityId: z.ZodString;
|
|
281
281
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
282
282
|
claimId: z.ZodString;
|
|
283
|
-
|
|
283
|
+
reason: z.ZodString;
|
|
284
284
|
declaredAt: z.ZodNumber;
|
|
285
285
|
expiresAt: z.ZodNumber;
|
|
286
286
|
}, z.core.$strip>>;
|
|
@@ -313,7 +313,7 @@ export declare const claimRejectionSchema: z.ZodObject<{
|
|
|
313
313
|
entityId: z.ZodString;
|
|
314
314
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
315
315
|
claimId: z.ZodString;
|
|
316
|
-
|
|
316
|
+
reason: z.ZodString;
|
|
317
317
|
declaredAt: z.ZodNumber;
|
|
318
318
|
expiresAt: z.ZodNumber;
|
|
319
319
|
}, z.core.$strip>>;
|
|
@@ -385,7 +385,7 @@ export declare const modelClaimSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
385
385
|
}, z.core.$strip>>;
|
|
386
386
|
export type ModelClaim = z.infer<typeof modelClaimSchema>;
|
|
387
387
|
/**
|
|
388
|
-
* `claim_begin` payload (client → server). The descriptive target +
|
|
388
|
+
* `claim_begin` payload (client → server). The descriptive target + reason,
|
|
389
389
|
* plus an optional duration hint and the opt-in fair-queue flag. The server
|
|
390
390
|
* stamps the lifecycle/timestamp fields, so they are NOT part of the inbound
|
|
391
391
|
* shape — this is exactly what the WS ingest validates.
|
|
@@ -403,7 +403,7 @@ export declare const claimBeginPayloadSchema: z.ZodObject<{
|
|
|
403
403
|
field: z.ZodOptional<z.ZodString>;
|
|
404
404
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
405
405
|
claimId: z.ZodString;
|
|
406
|
-
|
|
406
|
+
reason: z.ZodString;
|
|
407
407
|
estimatedMs: z.ZodOptional<z.ZodNumber>;
|
|
408
408
|
queue: z.ZodOptional<z.ZodBoolean>;
|
|
409
409
|
}, z.core.$strip>;
|
|
@@ -585,7 +585,7 @@ export declare const presenceUpdateFrameSchema: z.ZodObject<{
|
|
|
585
585
|
field: z.ZodOptional<z.ZodString>;
|
|
586
586
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
587
587
|
claimId: z.ZodString;
|
|
588
|
-
|
|
588
|
+
reason: z.ZodString;
|
|
589
589
|
declaredAt: z.ZodNumber;
|
|
590
590
|
expiresAt: z.ZodNumber;
|
|
591
591
|
status: z.ZodOptional<z.ZodEnum<{
|
|
@@ -606,7 +606,7 @@ export declare const presenceUpdateFrameSchema: z.ZodObject<{
|
|
|
606
606
|
entityId: z.ZodString;
|
|
607
607
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
608
608
|
claimId: z.ZodString;
|
|
609
|
-
|
|
609
|
+
reason: z.ZodString;
|
|
610
610
|
declaredAt: z.ZodNumber;
|
|
611
611
|
expiresAt: z.ZodNumber;
|
|
612
612
|
}, z.core.$strip>>;
|
|
@@ -209,8 +209,8 @@ export const claimStatusSchema = z.enum([
|
|
|
209
209
|
]);
|
|
210
210
|
const wireClaimBaseSchema = targetRefSchema.extend({
|
|
211
211
|
claimId: z.string(),
|
|
212
|
-
/**
|
|
213
|
-
|
|
212
|
+
/** Human-readable phase: 'editing' | 'reviewing' | 'forecasting' … */
|
|
213
|
+
reason: z.string(),
|
|
214
214
|
/** Server-stamped declaration time (epoch ms). */
|
|
215
215
|
declaredAt: z.number(),
|
|
216
216
|
/** Server-computed TTL deadline (epoch ms). Readers treat as advisory. */
|
|
@@ -219,7 +219,7 @@ const wireClaimBaseSchema = targetRefSchema.extend({
|
|
|
219
219
|
});
|
|
220
220
|
export const wireClaimSummarySchema = wireClaimBaseSchema.pick({
|
|
221
221
|
claimId: true,
|
|
222
|
-
|
|
222
|
+
reason: true,
|
|
223
223
|
declaredAt: true,
|
|
224
224
|
expiresAt: true,
|
|
225
225
|
entityType: true,
|
|
@@ -243,7 +243,7 @@ export const claimErrorSchema = z.object({
|
|
|
243
243
|
/**
|
|
244
244
|
* A declared pending-mutation claim — the unit broadcast in presence
|
|
245
245
|
* `activeClaims`. Clients supply the descriptive `targetRef` fields, an
|
|
246
|
-
* `
|
|
246
|
+
* explanatory `reason`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
|
|
247
247
|
* `expiresAt` and may set `status` / `error`.
|
|
248
248
|
*
|
|
249
249
|
* `status` and `error` are OPTIONAL: this single shape serves both the
|
|
@@ -298,8 +298,7 @@ export const modelClaimSchema = z
|
|
|
298
298
|
id: z.string(),
|
|
299
299
|
actor: z.string(),
|
|
300
300
|
participantKind: wireParticipantKindSchema,
|
|
301
|
-
/** Human-readable phase (`'editing'`).
|
|
302
|
-
* wire carries the same value as `action` (healed on read). */
|
|
301
|
+
/** Human-readable phase (`'editing'`). */
|
|
303
302
|
reason: z.string(),
|
|
304
303
|
description: z.string().optional(),
|
|
305
304
|
field: z.string().optional(),
|
|
@@ -310,14 +309,14 @@ export const modelClaimSchema = z
|
|
|
310
309
|
})
|
|
311
310
|
.readonly();
|
|
312
311
|
/**
|
|
313
|
-
* `claim_begin` payload (client → server). The descriptive target +
|
|
312
|
+
* `claim_begin` payload (client → server). The descriptive target + reason,
|
|
314
313
|
* plus an optional duration hint and the opt-in fair-queue flag. The server
|
|
315
314
|
* stamps the lifecycle/timestamp fields, so they are NOT part of the inbound
|
|
316
315
|
* shape — this is exactly what the WS ingest validates.
|
|
317
316
|
*/
|
|
318
317
|
export const claimBeginPayloadSchema = targetRefSchema.extend({
|
|
319
318
|
claimId: z.string(),
|
|
320
|
-
|
|
319
|
+
reason: z.string(),
|
|
321
320
|
/** Hint for `expiresAt`; the server caps it. */
|
|
322
321
|
estimatedMs: z.number().optional(),
|
|
323
322
|
/**
|