akribes 0.21.17
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 +30 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/client.d.ts +240 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +272 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +196 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +274 -0
- package/dist/errors.js.map +1 -0
- package/dist/execution/index.d.ts +3 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +3 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/replay.d.ts +37 -0
- package/dist/execution/replay.d.ts.map +1 -0
- package/dist/execution/replay.js +59 -0
- package/dist/execution/replay.js.map +1 -0
- package/dist/execution/steps.d.ts +327 -0
- package/dist/execution/steps.d.ts.map +1 -0
- package/dist/execution/steps.js +1068 -0
- package/dist/execution/steps.js.map +1 -0
- package/dist/http.d.ts +53 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +141 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/runStream.d.ts +176 -0
- package/dist/runStream.d.ts.map +1 -0
- package/dist/runStream.js +408 -0
- package/dist/runStream.js.map +1 -0
- package/dist/sse.d.ts +46 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +218 -0
- package/dist/sse.js.map +1 -0
- package/dist/sub/bench.d.ts +182 -0
- package/dist/sub/bench.d.ts.map +1 -0
- package/dist/sub/bench.js +420 -0
- package/dist/sub/bench.js.map +1 -0
- package/dist/sub/channels.d.ts +22 -0
- package/dist/sub/channels.d.ts.map +1 -0
- package/dist/sub/channels.js +32 -0
- package/dist/sub/channels.js.map +1 -0
- package/dist/sub/clients.d.ts +79 -0
- package/dist/sub/clients.d.ts.map +1 -0
- package/dist/sub/clients.js +190 -0
- package/dist/sub/clients.js.map +1 -0
- package/dist/sub/documents.d.ts +113 -0
- package/dist/sub/documents.d.ts.map +1 -0
- package/dist/sub/documents.js +329 -0
- package/dist/sub/documents.js.map +1 -0
- package/dist/sub/evals.d.ts +71 -0
- package/dist/sub/evals.d.ts.map +1 -0
- package/dist/sub/evals.js +86 -0
- package/dist/sub/evals.js.map +1 -0
- package/dist/sub/events.d.ts +65 -0
- package/dist/sub/events.d.ts.map +1 -0
- package/dist/sub/events.js +154 -0
- package/dist/sub/events.js.map +1 -0
- package/dist/sub/executions.d.ts +255 -0
- package/dist/sub/executions.d.ts.map +1 -0
- package/dist/sub/executions.js +322 -0
- package/dist/sub/executions.js.map +1 -0
- package/dist/sub/mcp.d.ts +51 -0
- package/dist/sub/mcp.d.ts.map +1 -0
- package/dist/sub/mcp.js +42 -0
- package/dist/sub/mcp.js.map +1 -0
- package/dist/sub/projects.d.ts +73 -0
- package/dist/sub/projects.d.ts.map +1 -0
- package/dist/sub/projects.js +101 -0
- package/dist/sub/projects.js.map +1 -0
- package/dist/sub/scripts.d.ts +58 -0
- package/dist/sub/scripts.d.ts.map +1 -0
- package/dist/sub/scripts.js +82 -0
- package/dist/sub/scripts.js.map +1 -0
- package/dist/sub/tokens.d.ts +126 -0
- package/dist/sub/tokens.d.ts.map +1 -0
- package/dist/sub/tokens.js +105 -0
- package/dist/sub/tokens.js.map +1 -0
- package/dist/sub/versions.d.ts +29 -0
- package/dist/sub/versions.d.ts.map +1 -0
- package/dist/sub/versions.js +52 -0
- package/dist/sub/versions.js.map +1 -0
- package/dist/tokenSafety.d.ts +15 -0
- package/dist/tokenSafety.d.ts.map +1 -0
- package/dist/tokenSafety.js +24 -0
- package/dist/tokenSafety.js.map +1 -0
- package/dist/types.d.ts +1147 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +132 -0
- package/dist/types.js.map +1 -0
- package/dist/workflowEvents.d.ts +297 -0
- package/dist/workflowEvents.d.ts.map +1 -0
- package/dist/workflowEvents.js +612 -0
- package/dist/workflowEvents.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** Heartbeat backoff curve — SDK-wide canonical (#1182).
|
|
2
|
+
* Exponential with full jitter, base 1s, cap 30s. The first failure waits
|
|
3
|
+
* ~1s before retrying, the second ~2s, ..., capped at ~30s. Jitter spreads
|
|
4
|
+
* reconnect attempts when many clients lose their token at once.
|
|
5
|
+
*
|
|
6
|
+
* Exported for reuse in the SSE reconnect path and for tests. */
|
|
7
|
+
export function heartbeatBackoffMs(consecutiveFailures) {
|
|
8
|
+
if (consecutiveFailures <= 0)
|
|
9
|
+
return 0;
|
|
10
|
+
const base = 1_000;
|
|
11
|
+
const cap = 30_000;
|
|
12
|
+
const exp = Math.min(base * 2 ** (consecutiveFailures - 1), cap);
|
|
13
|
+
return Math.floor(Math.random() * exp);
|
|
14
|
+
}
|
|
15
|
+
export class ClientsClient {
|
|
16
|
+
http;
|
|
17
|
+
projectId;
|
|
18
|
+
clientId;
|
|
19
|
+
clientName;
|
|
20
|
+
options;
|
|
21
|
+
heartbeatTimer;
|
|
22
|
+
inflightHeartbeat;
|
|
23
|
+
consecutiveFailures = 0;
|
|
24
|
+
lastStatus;
|
|
25
|
+
paused = false;
|
|
26
|
+
contractState = {
|
|
27
|
+
schemas: new Map(),
|
|
28
|
+
brokenScripts: new Set(),
|
|
29
|
+
};
|
|
30
|
+
constructor(http, projectId, clientId, clientName, options = {}) {
|
|
31
|
+
this.http = http;
|
|
32
|
+
this.projectId = projectId;
|
|
33
|
+
this.clientId = clientId;
|
|
34
|
+
this.clientName = clientName;
|
|
35
|
+
this.options = options;
|
|
36
|
+
}
|
|
37
|
+
async init(interests, opts) {
|
|
38
|
+
if (!this.clientId || !this.clientName) {
|
|
39
|
+
throw new Error('clientId and clientName are required for init(). Pass id and name to AkribesClient constructor.');
|
|
40
|
+
}
|
|
41
|
+
const res = await (await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${this.projectId}/clients`, {
|
|
42
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ id: this.clientId, name: this.clientName, interests }),
|
|
44
|
+
signal: opts?.signal,
|
|
45
|
+
})).json();
|
|
46
|
+
// Populate contract state from response
|
|
47
|
+
this.contractState.brokenScripts.clear();
|
|
48
|
+
if (res.interests) {
|
|
49
|
+
for (const interest of res.interests) {
|
|
50
|
+
this.contractState.schemas.set(interest.script_name, interest.input_schema);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Reset paused state from any prior `auth_failed` so a fresh `init()`
|
|
54
|
+
// (e.g. after `setToken()` with a new token) revives the heartbeat.
|
|
55
|
+
this.paused = false;
|
|
56
|
+
this.consecutiveFailures = 0;
|
|
57
|
+
this.startHeartbeat();
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
/** Resume a heartbeat that was paused after an auth failure. Call this
|
|
61
|
+
* after rotating the underlying token via {@link AkribesClient.setToken}. */
|
|
62
|
+
resumeHeartbeat() {
|
|
63
|
+
if (!this.heartbeatTimer && !this.inflightHeartbeat) {
|
|
64
|
+
this.paused = false;
|
|
65
|
+
this.consecutiveFailures = 0;
|
|
66
|
+
this.startHeartbeat();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fireStatus(status) {
|
|
70
|
+
if (this.lastStatus === status)
|
|
71
|
+
return;
|
|
72
|
+
this.lastStatus = status;
|
|
73
|
+
try {
|
|
74
|
+
this.options.onHeartbeatStatus?.(status);
|
|
75
|
+
}
|
|
76
|
+
catch { /* swallow */ }
|
|
77
|
+
}
|
|
78
|
+
startHeartbeat() {
|
|
79
|
+
// Use a recursive setTimeout chain rather than setInterval so we can vary
|
|
80
|
+
// the next delay based on the previous result — the canonical SDK-wide
|
|
81
|
+
// pattern (#1182). Base interval 30s on success, exponential-with-jitter
|
|
82
|
+
// capped at 30s on failure (added on TOP of the base interval).
|
|
83
|
+
const tick = () => {
|
|
84
|
+
this.inflightHeartbeat = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const res = await this.http.authFetch(`${this.http.getBaseUrl()}/heartbeat`, {
|
|
87
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ client_id: this.clientId }),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
if (res.status === 401 || res.status === 403) {
|
|
92
|
+
// Revoked / expired token. Stop ticking — a 401/403 will not
|
|
93
|
+
// self-heal by waiting, only by `setToken()` + `resumeHeartbeat()`.
|
|
94
|
+
// Surfaces via the typed callback (#1220) instead of an endless
|
|
95
|
+
// console.warn loop.
|
|
96
|
+
this.fireStatus('auth_failed');
|
|
97
|
+
this.paused = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.consecutiveFailures += 1;
|
|
101
|
+
this.fireStatus('unreachable');
|
|
102
|
+
console.warn(`[Akribes SDK] heartbeat rejected: HTTP ${res.status}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.consecutiveFailures = 0;
|
|
106
|
+
this.fireStatus('ok');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
this.consecutiveFailures += 1;
|
|
111
|
+
this.fireStatus('unreachable');
|
|
112
|
+
console.warn('[Akribes SDK] heartbeat failed:', e);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.inflightHeartbeat = undefined;
|
|
116
|
+
if (!this.paused) {
|
|
117
|
+
const backoff = heartbeatBackoffMs(this.consecutiveFailures);
|
|
118
|
+
this.heartbeatTimer = setTimeout(tick, 30_000 + backoff);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.heartbeatTimer = undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
};
|
|
126
|
+
if (this.heartbeatTimer)
|
|
127
|
+
clearTimeout(this.heartbeatTimer);
|
|
128
|
+
this.heartbeatTimer = setTimeout(tick, 30_000);
|
|
129
|
+
}
|
|
130
|
+
async list(opts) {
|
|
131
|
+
return (await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${this.projectId}/clients`, opts)).json();
|
|
132
|
+
}
|
|
133
|
+
async delete(id, opts) {
|
|
134
|
+
await this.http.fetchOk(`${this.http.getBaseUrl()}/clients/${encodeURIComponent(id)}`, {
|
|
135
|
+
method: 'DELETE', signal: opts?.signal,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// ── Lock management ─────────────────────────────────────────────────
|
|
139
|
+
async listLocks(scriptName, opts) {
|
|
140
|
+
return (await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${this.projectId}/scripts/${encodeURIComponent(scriptName)}/locks`, opts)).json();
|
|
141
|
+
}
|
|
142
|
+
async revokeLock(scriptName, lockId, opts) {
|
|
143
|
+
await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${this.projectId}/scripts/${encodeURIComponent(scriptName)}/locks/${lockId}`, { method: 'DELETE', signal: opts?.signal });
|
|
144
|
+
}
|
|
145
|
+
async rebindLock(scriptName, lockId, versionId, opts) {
|
|
146
|
+
return (await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${this.projectId}/scripts/${encodeURIComponent(scriptName)}/locks/${lockId}/rebind`, {
|
|
147
|
+
method: 'PATCH',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({ version_id: versionId ?? null }),
|
|
150
|
+
signal: opts?.signal,
|
|
151
|
+
})).json();
|
|
152
|
+
}
|
|
153
|
+
// ── Flat cross-project lock helpers (#1133) ───────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// The methods above operate on `this.projectId`. The flat helpers below
|
|
156
|
+
// take an explicit `projectId` so admin tools spanning multiple projects
|
|
157
|
+
// can manage locks without spinning up a fresh project-scoped client.
|
|
158
|
+
// Mirrors Rust's `list_locks_for` / `delete_lock` / `update_lock`.
|
|
159
|
+
/** List contract locks for `scriptName` in `projectId`. Cross-project
|
|
160
|
+
* variant of {@link listLocks}; the implicit project_id on this client
|
|
161
|
+
* is ignored. */
|
|
162
|
+
async listLocksFor(projectId, scriptName, opts) {
|
|
163
|
+
return (await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${projectId}/scripts/${encodeURIComponent(scriptName)}/locks`, opts)).json();
|
|
164
|
+
}
|
|
165
|
+
/** Delete (revoke) a single lock in `projectId`. Cross-project variant
|
|
166
|
+
* of {@link revokeLock}. */
|
|
167
|
+
async deleteLock(projectId, scriptName, lockId, opts) {
|
|
168
|
+
await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${projectId}/scripts/${encodeURIComponent(scriptName)}/locks/${lockId}`, { method: 'DELETE', signal: opts?.signal });
|
|
169
|
+
}
|
|
170
|
+
/** Update (rebind) a single lock to a new version in `projectId`.
|
|
171
|
+
* Cross-project variant of {@link rebindLock}. Pass `versionId =
|
|
172
|
+
* undefined` to rebind the lock to the channel's current version. */
|
|
173
|
+
async updateLock(projectId, scriptName, lockId, versionId, opts) {
|
|
174
|
+
return (await this.http.fetchOk(`${this.http.getBaseUrl()}/projects/${projectId}/scripts/${encodeURIComponent(scriptName)}/locks/${lockId}/rebind`, {
|
|
175
|
+
method: 'PATCH',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({ version_id: versionId ?? null }),
|
|
178
|
+
signal: opts?.signal,
|
|
179
|
+
})).json();
|
|
180
|
+
}
|
|
181
|
+
async destroy() {
|
|
182
|
+
if (this.heartbeatTimer)
|
|
183
|
+
clearTimeout(this.heartbeatTimer);
|
|
184
|
+
this.heartbeatTimer = undefined;
|
|
185
|
+
this.paused = true;
|
|
186
|
+
if (this.inflightHeartbeat)
|
|
187
|
+
await this.inflightHeartbeat;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.js","sourceRoot":"","sources":["../../src/sub/clients.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { HttpClient } from '../http';
|
|
2
|
+
export type ConversionStatus = 'text' | 'ready' | 'converting' | 'pending' | 'failed' | 'unknown';
|
|
3
|
+
export type UploadResult = {
|
|
4
|
+
document_id: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
content_hash: string;
|
|
7
|
+
conversion_status: ConversionStatus;
|
|
8
|
+
};
|
|
9
|
+
export type ClaimOutcome = {
|
|
10
|
+
status: 'hit';
|
|
11
|
+
result: UploadResult;
|
|
12
|
+
} | {
|
|
13
|
+
status: 'miss';
|
|
14
|
+
};
|
|
15
|
+
export type IngestPhase = 'claiming' | 'uploading' | 'converting' | 'ready';
|
|
16
|
+
/** Per-page progress while a conversion is in flight on the server.
|
|
17
|
+
* `done` and `total` are page counts (not chunks). `total = 0` means the
|
|
18
|
+
* server hasn't yet rasterized far enough to know — render an indeterminate
|
|
19
|
+
* bar in that case. */
|
|
20
|
+
export type IngestProgress = {
|
|
21
|
+
done: number;
|
|
22
|
+
total: number;
|
|
23
|
+
};
|
|
24
|
+
export type IngestOptions = {
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
onPhase?: (phase: IngestPhase) => void;
|
|
27
|
+
/** Fires periodically while the server is converting. Polls a separate
|
|
28
|
+
* metadata endpoint — never carries markdown, just page counts.
|
|
29
|
+
* Frequency: every ~750 ms during the converting phase. */
|
|
30
|
+
onProgress?: (p: IngestProgress) => void;
|
|
31
|
+
/** Maximum time to keep polling a still-converting blob, in milliseconds.
|
|
32
|
+
* When omitted, falls back to the client-level
|
|
33
|
+
* `AkribesClientOptions.ingestPollTimeoutMs` (which itself defaults to
|
|
34
|
+
* {@link DEFAULT_INGEST_POLL_TIMEOUT_MS} = 300 s). Use this per-call
|
|
35
|
+
* option to override for a specific ingest, e.g. unusually large PDFs. */
|
|
36
|
+
pollTimeoutMs?: number;
|
|
37
|
+
};
|
|
38
|
+
/** Default poll budget for {@link DocumentsClient.ingest}, in milliseconds.
|
|
39
|
+
*
|
|
40
|
+
* 20 minutes. The previous 5 min default still surfaced
|
|
41
|
+
* `IngestTimeoutError`s on perfectly healthy big-PDF conversions while the
|
|
42
|
+
* server quietly kept working — that's a UX bug, not a hang signal. Real
|
|
43
|
+
* hangs are caught by the server's own watchdogs (see
|
|
44
|
+
* `wait_for_blob_ready`); the SDK timeout exists only as a final hard cap
|
|
45
|
+
* so callers don't poll forever on a process leak. UIs are expected to
|
|
46
|
+
* surface live progress via `onProgress` and offer a Cancel button rather
|
|
47
|
+
* than waiting for this deadline.
|
|
48
|
+
*
|
|
49
|
+
* Override via:
|
|
50
|
+
* - per-ingest: {@link IngestOptions.pollTimeoutMs}
|
|
51
|
+
* - per-client: `AkribesClientOptions.ingestPollTimeoutMs`
|
|
52
|
+
* - process-wide: env var `AKRIBES_SDK_INGEST_TIMEOUT_SECS` (Node/Bun only).
|
|
53
|
+
*/
|
|
54
|
+
export declare const DEFAULT_INGEST_POLL_TIMEOUT_MS: number;
|
|
55
|
+
/** Read `AKRIBES_SDK_INGEST_TIMEOUT_SECS` from the host's process env (Node/Bun
|
|
56
|
+
* expose `process.env`; browser bundles don't and will skip this).
|
|
57
|
+
* Returns `undefined` if unset, zero, or unparseable — the caller falls back
|
|
58
|
+
* to the next-lower-precedence value. */
|
|
59
|
+
export declare function ingestPollTimeoutMsFromEnv(): number | undefined;
|
|
60
|
+
/** Thrown when the conversion pipeline fails. Two trigger paths:
|
|
61
|
+
*
|
|
62
|
+
* 1. The server returned a 4xx/5xx with `error_type: "conversion_failed"`
|
|
63
|
+
* during claim/upload (e.g. the VLM rejected the file, pdfium couldn't
|
|
64
|
+
* read it, the conversion service was down). `document_id` is empty
|
|
65
|
+
* because no document was created.
|
|
66
|
+
* 2. The server returned 200 with `conversion_status: "failed"` from claim
|
|
67
|
+
* (the bytes have a poisoned blob row that hasn't been reclaimed yet).
|
|
68
|
+
*
|
|
69
|
+
* The `message` is server-supplied and user-facing — surface it directly in
|
|
70
|
+
* the UI instead of swapping in a generic fallback. `reason` is a stable
|
|
71
|
+
* machine-readable string for branching:
|
|
72
|
+
* `service_unavailable`, `service_rejected`, `file_too_large`,
|
|
73
|
+
* `invalid_file`, `extraction_failed`. */
|
|
74
|
+
export declare class DocumentConversionError extends Error {
|
|
75
|
+
readonly document_id: string;
|
|
76
|
+
readonly reason?: string;
|
|
77
|
+
constructor(document_id: string, message: string, reason?: string);
|
|
78
|
+
}
|
|
79
|
+
/** Thrown when `ingest()` polls past its `pollTimeoutMs` deadline. */
|
|
80
|
+
export declare class IngestTimeoutError extends Error {
|
|
81
|
+
readonly document_id: string;
|
|
82
|
+
readonly elapsed_ms: number;
|
|
83
|
+
constructor(message: string, document_id: string, elapsed_ms: number);
|
|
84
|
+
}
|
|
85
|
+
/** Thrown for protocol-level problems: unknown `conversion_status` strings,
|
|
86
|
+
* `crypto.subtle` unavailable, etc. Signals schema drift or missing browser
|
|
87
|
+
* features — surfacing loudly is the point. */
|
|
88
|
+
export declare class IngestProtocolError extends Error {
|
|
89
|
+
readonly received_status?: string;
|
|
90
|
+
constructor(message: string, received_status?: string);
|
|
91
|
+
}
|
|
92
|
+
export declare class DocumentsClient {
|
|
93
|
+
private http;
|
|
94
|
+
private projectId;
|
|
95
|
+
/** Resolved client-level default poll timeout. Per-call
|
|
96
|
+
* {@link IngestOptions.pollTimeoutMs} still wins. */
|
|
97
|
+
private readonly defaultPollTimeoutMs;
|
|
98
|
+
constructor(http: HttpClient, projectId: number, defaultPollTimeoutMs?: number);
|
|
99
|
+
/** The poll timeout this client applies when an `ingest()` caller doesn't
|
|
100
|
+
* pass `pollTimeoutMs`. Mostly useful for tests / diagnostics. */
|
|
101
|
+
getDefaultPollTimeoutMs(): number;
|
|
102
|
+
/** Fetch a snapshot of the server-side conversion progress for a content
|
|
103
|
+
* hash. Returns `null` if the server has no in-flight conversion (either
|
|
104
|
+
* it's terminal already, or there's nothing to show). Cheap (a few-byte
|
|
105
|
+
* JSON response off an in-memory map). */
|
|
106
|
+
progress(content_hash: string): Promise<IngestProgress | null>;
|
|
107
|
+
claim(content_hash: string, filename: string): Promise<ClaimOutcome>;
|
|
108
|
+
upload(filename: string, bytes: Uint8Array, opts?: {
|
|
109
|
+
signal?: AbortSignal;
|
|
110
|
+
}): Promise<UploadResult>;
|
|
111
|
+
ingest(filename: string, bytes: Uint8Array, opts?: IngestOptions): Promise<UploadResult>;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/sub/documents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// Document ingest sub-client. Mirrors the Rust akribes-sdk-rust
|
|
2
|
+
// `DocumentsClient` surface (claim / upload / ingest), plus an `onPhase`
|
|
3
|
+
// progress callback for browser UI.
|
|
4
|
+
//
|
|
5
|
+
// See `docs/superpowers/specs/2026-04-25-studio-documents-ingest-design.md`.
|
|
6
|
+
import { AkribesHttpError } from '../errors';
|
|
7
|
+
/** Translate `AkribesHttpError`s carrying the server's `conversion_failed`
|
|
8
|
+
* envelope into a typed `DocumentConversionError`. Other HTTP errors are
|
|
9
|
+
* re-thrown verbatim so callers can still inspect `status` / `body`. */
|
|
10
|
+
function rethrowConversionFailure(e) {
|
|
11
|
+
if (e instanceof AkribesHttpError && e.errorType === 'conversion_failed') {
|
|
12
|
+
// serverMessage is the user-facing copy assembled by the server's
|
|
13
|
+
// `client_facing(reason)` helper. Pass it through unchanged.
|
|
14
|
+
throw new DocumentConversionError('', e.serverMessage ?? e.message, e.reason);
|
|
15
|
+
}
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
/** Default poll budget for {@link DocumentsClient.ingest}, in milliseconds.
|
|
19
|
+
*
|
|
20
|
+
* 20 minutes. The previous 5 min default still surfaced
|
|
21
|
+
* `IngestTimeoutError`s on perfectly healthy big-PDF conversions while the
|
|
22
|
+
* server quietly kept working — that's a UX bug, not a hang signal. Real
|
|
23
|
+
* hangs are caught by the server's own watchdogs (see
|
|
24
|
+
* `wait_for_blob_ready`); the SDK timeout exists only as a final hard cap
|
|
25
|
+
* so callers don't poll forever on a process leak. UIs are expected to
|
|
26
|
+
* surface live progress via `onProgress` and offer a Cancel button rather
|
|
27
|
+
* than waiting for this deadline.
|
|
28
|
+
*
|
|
29
|
+
* Override via:
|
|
30
|
+
* - per-ingest: {@link IngestOptions.pollTimeoutMs}
|
|
31
|
+
* - per-client: `AkribesClientOptions.ingestPollTimeoutMs`
|
|
32
|
+
* - process-wide: env var `AKRIBES_SDK_INGEST_TIMEOUT_SECS` (Node/Bun only).
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_INGEST_POLL_TIMEOUT_MS = 20 * 60 * 1000;
|
|
35
|
+
/** Read `AKRIBES_SDK_INGEST_TIMEOUT_SECS` from the host's process env (Node/Bun
|
|
36
|
+
* expose `process.env`; browser bundles don't and will skip this).
|
|
37
|
+
* Returns `undefined` if unset, zero, or unparseable — the caller falls back
|
|
38
|
+
* to the next-lower-precedence value. */
|
|
39
|
+
export function ingestPollTimeoutMsFromEnv() {
|
|
40
|
+
// Browsers don't have `process`; guard so this file is tree-shake-safe.
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const proc = globalThis.process;
|
|
43
|
+
const raw = proc?.env?.AKRIBES_SDK_INGEST_TIMEOUT_SECS;
|
|
44
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
45
|
+
return undefined;
|
|
46
|
+
const n = Number(raw);
|
|
47
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
return Math.floor(n) * 1000;
|
|
50
|
+
}
|
|
51
|
+
/** Thrown when the conversion pipeline fails. Two trigger paths:
|
|
52
|
+
*
|
|
53
|
+
* 1. The server returned a 4xx/5xx with `error_type: "conversion_failed"`
|
|
54
|
+
* during claim/upload (e.g. the VLM rejected the file, pdfium couldn't
|
|
55
|
+
* read it, the conversion service was down). `document_id` is empty
|
|
56
|
+
* because no document was created.
|
|
57
|
+
* 2. The server returned 200 with `conversion_status: "failed"` from claim
|
|
58
|
+
* (the bytes have a poisoned blob row that hasn't been reclaimed yet).
|
|
59
|
+
*
|
|
60
|
+
* The `message` is server-supplied and user-facing — surface it directly in
|
|
61
|
+
* the UI instead of swapping in a generic fallback. `reason` is a stable
|
|
62
|
+
* machine-readable string for branching:
|
|
63
|
+
* `service_unavailable`, `service_rejected`, `file_too_large`,
|
|
64
|
+
* `invalid_file`, `extraction_failed`. */
|
|
65
|
+
export class DocumentConversionError extends Error {
|
|
66
|
+
document_id;
|
|
67
|
+
reason;
|
|
68
|
+
constructor(document_id, message, reason) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'DocumentConversionError';
|
|
71
|
+
this.document_id = document_id;
|
|
72
|
+
this.reason = reason;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Thrown when `ingest()` polls past its `pollTimeoutMs` deadline. */
|
|
76
|
+
export class IngestTimeoutError extends Error {
|
|
77
|
+
document_id;
|
|
78
|
+
elapsed_ms;
|
|
79
|
+
constructor(message, document_id, elapsed_ms) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = 'IngestTimeoutError';
|
|
82
|
+
this.document_id = document_id;
|
|
83
|
+
this.elapsed_ms = elapsed_ms;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Thrown for protocol-level problems: unknown `conversion_status` strings,
|
|
87
|
+
* `crypto.subtle` unavailable, etc. Signals schema drift or missing browser
|
|
88
|
+
* features — surfacing loudly is the point. */
|
|
89
|
+
export class IngestProtocolError extends Error {
|
|
90
|
+
received_status;
|
|
91
|
+
constructor(message, received_status) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.name = 'IngestProtocolError';
|
|
94
|
+
this.received_status = received_status;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Compute a hex-encoded SHA-256 digest of the given bytes. Throws
|
|
98
|
+
* `IngestProtocolError` if `crypto.subtle` isn't available (insecure
|
|
99
|
+
* context — e.g. plain HTTP deploy). */
|
|
100
|
+
async function sha256Hex(bytes) {
|
|
101
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
102
|
+
throw new IngestProtocolError('SHA-256 unavailable: ingest requires a secure context (HTTPS or localhost)');
|
|
103
|
+
}
|
|
104
|
+
// Cast: TypeScript 5.7+ narrows `Uint8Array<ArrayBufferLike>` which BufferSource
|
|
105
|
+
// excludes. Our bytes are always regular (non-shared) buffers, so the cast is safe.
|
|
106
|
+
const buf = await crypto.subtle.digest('SHA-256', bytes);
|
|
107
|
+
return Array.from(new Uint8Array(buf))
|
|
108
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
109
|
+
.join('');
|
|
110
|
+
}
|
|
111
|
+
/** Validate a `conversion_status` string from the wire. Throws
|
|
112
|
+
* `IngestProtocolError` on `'unknown'` — schema drift signal. */
|
|
113
|
+
function assertKnownStatus(s) {
|
|
114
|
+
if (s === 'unknown') {
|
|
115
|
+
throw new IngestProtocolError('received unknown conversion_status from server (schema drift)', 'unknown');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Throw `DocumentConversionError` if the server reports a `failed` status. */
|
|
119
|
+
function ensureNotFailed(r) {
|
|
120
|
+
if (r.conversion_status === 'failed') {
|
|
121
|
+
throw new DocumentConversionError(r.document_id, `document ${r.document_id} conversion failed on the server — re-upload or call reconvert`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export class DocumentsClient {
|
|
125
|
+
http;
|
|
126
|
+
projectId;
|
|
127
|
+
/** Resolved client-level default poll timeout. Per-call
|
|
128
|
+
* {@link IngestOptions.pollTimeoutMs} still wins. */
|
|
129
|
+
defaultPollTimeoutMs;
|
|
130
|
+
constructor(http, projectId, defaultPollTimeoutMs) {
|
|
131
|
+
this.http = http;
|
|
132
|
+
this.projectId = projectId;
|
|
133
|
+
this.defaultPollTimeoutMs = defaultPollTimeoutMs ?? DEFAULT_INGEST_POLL_TIMEOUT_MS;
|
|
134
|
+
}
|
|
135
|
+
/** The poll timeout this client applies when an `ingest()` caller doesn't
|
|
136
|
+
* pass `pollTimeoutMs`. Mostly useful for tests / diagnostics. */
|
|
137
|
+
getDefaultPollTimeoutMs() {
|
|
138
|
+
return this.defaultPollTimeoutMs;
|
|
139
|
+
}
|
|
140
|
+
/** Fetch a snapshot of the server-side conversion progress for a content
|
|
141
|
+
* hash. Returns `null` if the server has no in-flight conversion (either
|
|
142
|
+
* it's terminal already, or there's nothing to show). Cheap (a few-byte
|
|
143
|
+
* JSON response off an in-memory map). */
|
|
144
|
+
async progress(content_hash) {
|
|
145
|
+
const url = `${this.http.getBaseUrl()}/projects/${this.projectId}/documents/by-hash/${content_hash}/progress`;
|
|
146
|
+
const res = await this.http.fetchOk(url);
|
|
147
|
+
const wire = (await res.json());
|
|
148
|
+
if (wire.state === 'idle')
|
|
149
|
+
return null;
|
|
150
|
+
return { done: wire.done_pages, total: wire.total_pages };
|
|
151
|
+
}
|
|
152
|
+
async claim(content_hash, filename) {
|
|
153
|
+
const url = `${this.http.getBaseUrl()}/projects/${this.projectId}/documents/claim`;
|
|
154
|
+
const res = await this.http.fetchOk(url, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ content_hash, filename }),
|
|
158
|
+
}).catch(rethrowConversionFailure);
|
|
159
|
+
const wire = await res.json();
|
|
160
|
+
if (wire.status === 'hit') {
|
|
161
|
+
return {
|
|
162
|
+
status: 'hit',
|
|
163
|
+
result: {
|
|
164
|
+
document_id: wire.document_id,
|
|
165
|
+
filename: wire.filename,
|
|
166
|
+
content_hash: wire.content_hash,
|
|
167
|
+
conversion_status: wire.conversion_status,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { status: 'miss' };
|
|
172
|
+
}
|
|
173
|
+
async upload(filename, bytes, opts) {
|
|
174
|
+
const form = new FormData();
|
|
175
|
+
form.append('file', new Blob([bytes]), filename);
|
|
176
|
+
const url = `${this.http.getBaseUrl()}/projects/${this.projectId}/documents`;
|
|
177
|
+
// POST /documents blocks for the full server-side conversion. Bun
|
|
178
|
+
// enforces an internal 5-minute fetch timeout that AbortSignal cannot
|
|
179
|
+
// override; on dense documents the fetch will throw `TimeoutError` while
|
|
180
|
+
// the server keeps working. `ingest()` catches that and falls back to
|
|
181
|
+
// claim-polling, so we just propagate the error here.
|
|
182
|
+
const res = await this.http.fetchOk(url, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
body: form,
|
|
185
|
+
signal: opts?.signal,
|
|
186
|
+
}).catch(rethrowConversionFailure);
|
|
187
|
+
return (await res.json());
|
|
188
|
+
}
|
|
189
|
+
async ingest(filename, bytes, opts) {
|
|
190
|
+
const { signal, onPhase, onProgress, pollTimeoutMs = this.defaultPollTimeoutMs } = opts ?? {};
|
|
191
|
+
onPhase?.('claiming');
|
|
192
|
+
const hash = await sha256Hex(bytes);
|
|
193
|
+
const initial = await this.claim(hash, filename);
|
|
194
|
+
// Side-channel progress poller. Runs concurrently with the (blocking)
|
|
195
|
+
// upload fetch, hits the in-memory progress map endpoint every ~750 ms
|
|
196
|
+
// and pushes updates through `onProgress`. Stops as soon as `done`
|
|
197
|
+
// becomes 0 again (server cleared the entry → conversion finished) or
|
|
198
|
+
// the abort flag is set.
|
|
199
|
+
let progressPollerActive = false;
|
|
200
|
+
const stopProgressPoller = () => { progressPollerActive = false; };
|
|
201
|
+
const startProgressPoller = () => {
|
|
202
|
+
if (!onProgress || progressPollerActive)
|
|
203
|
+
return;
|
|
204
|
+
progressPollerActive = true;
|
|
205
|
+
void (async () => {
|
|
206
|
+
// Two distinct null cases:
|
|
207
|
+
// 1) Server hasn't created the entry yet (pdfium still parsing).
|
|
208
|
+
// Keep polling — entry shows up once the page count is known.
|
|
209
|
+
// 2) Server cleared the entry (conversion finished). Stop.
|
|
210
|
+
// We only treat null as "finished" *after* we've seen a non-null
|
|
211
|
+
// snapshot at least once.
|
|
212
|
+
let everSawProgress = false;
|
|
213
|
+
while (progressPollerActive && !signal?.aborted) {
|
|
214
|
+
try {
|
|
215
|
+
const snap = await this.progress(hash);
|
|
216
|
+
if (snap) {
|
|
217
|
+
everSawProgress = true;
|
|
218
|
+
onProgress(snap);
|
|
219
|
+
}
|
|
220
|
+
else if (everSawProgress) {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Don't fail the upload because progress polling glitched.
|
|
226
|
+
}
|
|
227
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
};
|
|
231
|
+
let result;
|
|
232
|
+
if (initial.status === 'miss') {
|
|
233
|
+
onPhase?.('uploading');
|
|
234
|
+
startProgressPoller();
|
|
235
|
+
// The blocking upload connection can die before the server finishes
|
|
236
|
+
// converting for several distinct reasons, all of which mean
|
|
237
|
+
// "fetch is gone, bytes are still on the server, fall back to
|
|
238
|
+
// claim-polling so the conversion result can still be surfaced":
|
|
239
|
+
//
|
|
240
|
+
// 1. Bun's fetch enforces an internal 5-minute timeout that even
|
|
241
|
+
// AbortSignal.timeout(longer) cannot extend. Surfaces as
|
|
242
|
+
// `DOMException: TimeoutError`. (Server-side caller only.)
|
|
243
|
+
// 2. Browsers (Firefox in particular) drop the connection mid-
|
|
244
|
+
// request when an idle keepalive elapses on a hop between
|
|
245
|
+
// browser and aura-server (ISP NAT reaper, Traefik upstream
|
|
246
|
+
// pool, OS conntrack). Firefox surfaces this as
|
|
247
|
+
// `TypeError: NetworkError when attempting to fetch resource`,
|
|
248
|
+
// Chrome as `TypeError: Failed to fetch`, Safari as
|
|
249
|
+
// `TypeError: Load failed`. None of these are `TimeoutError`,
|
|
250
|
+
// so a narrow timeout-only catch lets them propagate to the
|
|
251
|
+
// UI as a misleading "upload failed" while the server is in
|
|
252
|
+
// fact still busy converting.
|
|
253
|
+
//
|
|
254
|
+
// Detection: treat any `TypeError` from `fetch()` whose message
|
|
255
|
+
// matches the well-known network-failure phrases as recoverable.
|
|
256
|
+
// This is the same shape the WHATWG spec mandates fetch throw on
|
|
257
|
+
// network errors, so the heuristic is stable across browsers.
|
|
258
|
+
try {
|
|
259
|
+
result = await this.upload(filename, bytes, { signal });
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
const isFetchTimeout = e?.name === 'TimeoutError' ||
|
|
263
|
+
(e instanceof DOMException && /timed? *out/i.test(e.message));
|
|
264
|
+
const isFetchNetworkError = e instanceof TypeError &&
|
|
265
|
+
/network ?error|failed to fetch|load failed/i.test(e?.message ?? '');
|
|
266
|
+
if (!isFetchTimeout && !isFetchNetworkError)
|
|
267
|
+
throw e;
|
|
268
|
+
// Switch to converting phase and let the existing poll loop below take
|
|
269
|
+
// over by simulating a Hit-converting initial state.
|
|
270
|
+
onPhase?.('converting');
|
|
271
|
+
result = {
|
|
272
|
+
document_id: '',
|
|
273
|
+
filename,
|
|
274
|
+
content_hash: hash,
|
|
275
|
+
conversion_status: 'converting',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Hit path — may be terminal (return immediately) or non-terminal (poll).
|
|
281
|
+
assertKnownStatus(initial.result.conversion_status);
|
|
282
|
+
ensureNotFailed(initial.result);
|
|
283
|
+
result = initial.result;
|
|
284
|
+
}
|
|
285
|
+
// Poll until terminal. Reached when:
|
|
286
|
+
// - Hit returned a non-terminal status (concurrent uploader still
|
|
287
|
+
// processing), or
|
|
288
|
+
// - Miss + upload's fetch died at Bun's 5-min timeout (we converted to
|
|
289
|
+
// a synthetic converting state above).
|
|
290
|
+
if (result.conversion_status === 'converting' || result.conversion_status === 'pending') {
|
|
291
|
+
onPhase?.('converting');
|
|
292
|
+
// Hit-converting branch hasn't started the poller yet.
|
|
293
|
+
startProgressPoller();
|
|
294
|
+
const startedAt = Date.now();
|
|
295
|
+
const deadline = startedAt + pollTimeoutMs;
|
|
296
|
+
let backoffMs = 250;
|
|
297
|
+
while (result.conversion_status === 'converting' || result.conversion_status === 'pending') {
|
|
298
|
+
if (signal?.aborted) {
|
|
299
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
300
|
+
}
|
|
301
|
+
if (Date.now() >= deadline) {
|
|
302
|
+
const elapsed = Date.now() - startedAt;
|
|
303
|
+
throw new IngestTimeoutError('Document conversion did not finish within 20 minutes. The server is still working in the background. Refresh the page in a few minutes to check, or contact support.', result.document_id, elapsed);
|
|
304
|
+
}
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
306
|
+
backoffMs = Math.min(backoffMs * 2, 2000);
|
|
307
|
+
const pollOutcome = await this.claim(hash, filename);
|
|
308
|
+
if (pollOutcome.status === 'miss') {
|
|
309
|
+
// GC reclaimed the blob mid-poll; re-upload to repopulate.
|
|
310
|
+
onPhase?.('uploading');
|
|
311
|
+
const uploaded = await this.upload(filename, bytes, { signal });
|
|
312
|
+
assertKnownStatus(uploaded.conversion_status);
|
|
313
|
+
ensureNotFailed(uploaded);
|
|
314
|
+
onPhase?.('ready');
|
|
315
|
+
return uploaded;
|
|
316
|
+
}
|
|
317
|
+
assertKnownStatus(pollOutcome.result.conversion_status);
|
|
318
|
+
ensureNotFailed(pollOutcome.result);
|
|
319
|
+
result = pollOutcome.result;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
assertKnownStatus(result.conversion_status);
|
|
323
|
+
ensureNotFailed(result);
|
|
324
|
+
stopProgressPoller();
|
|
325
|
+
onPhase?.('ready');
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=documents.js.map
|