@astralform/js 4.1.0 → 4.2.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/README.md +6 -0
- package/dist/index.cjs +52 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +52 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -120,6 +120,19 @@ interface AstralformBaseConfig {
|
|
|
120
120
|
baseURL?: string;
|
|
121
121
|
/** Supply a custom fetch (SSR, testing, custom interceptors). */
|
|
122
122
|
fetch?: typeof globalThis.fetch;
|
|
123
|
+
/**
|
|
124
|
+
* Abort a REST request — connect, headers, AND body read — after this many
|
|
125
|
+
* milliseconds. Defaults to 30_000. Does not apply to `uploadFile` (large
|
|
126
|
+
* files on slow uplinks) or to SSE streaming, which is long-lived by design
|
|
127
|
+
* and carries its own `AbortSignal`.
|
|
128
|
+
*
|
|
129
|
+
* Note that unlike axios and friends, `0` does NOT mean "no timeout": any
|
|
130
|
+
* non-positive or non-finite value falls back to the default. REST calls
|
|
131
|
+
* cannot opt out of the deadline — an unbounded REST request is the bug
|
|
132
|
+
* this exists to prevent, and it fails silently (a stalled body strands the
|
|
133
|
+
* caller forever with nothing to react to).
|
|
134
|
+
*/
|
|
135
|
+
timeoutMs?: number;
|
|
123
136
|
}
|
|
124
137
|
interface AstralformApiKeyConfig extends AstralformBaseConfig {
|
|
125
138
|
/** Agent API key (`sk_live_...` or `sk_test_...`). */
|
|
@@ -773,6 +786,7 @@ interface ConversationAsset {
|
|
|
773
786
|
declare class AstralformClient {
|
|
774
787
|
private readonly baseURL;
|
|
775
788
|
private readonly fetchFn;
|
|
789
|
+
private readonly timeoutMs;
|
|
776
790
|
/**
|
|
777
791
|
* Auth state is mutable so callers can rotate access tokens or switch
|
|
778
792
|
* agent context without re-instantiating the client. API-key mode is
|
|
@@ -819,7 +833,23 @@ declare class AstralformClient {
|
|
|
819
833
|
*/
|
|
820
834
|
private get authHeaders();
|
|
821
835
|
private get headers();
|
|
836
|
+
/**
|
|
837
|
+
* Run one REST exchange under a single deadline covering connect, headers,
|
|
838
|
+
* AND the body read. The body read is the part that matters: `json()` used
|
|
839
|
+
* to sit outside every guard, so a response whose headers arrived but whose
|
|
840
|
+
* body stalled hung forever — silently stranding callers that await it
|
|
841
|
+
* (a stalled `getMessages` used to leave `StreamManager.restore()` parked
|
|
842
|
+
* before it ever fetched the events it renders from).
|
|
843
|
+
*
|
|
844
|
+
* The controller is created per request and is deliberately NOT the
|
|
845
|
+
* session's — that one means "the user cancelled this turn" and is null
|
|
846
|
+
* outside a live turn. Aborting frees the socket; the race guarantees a
|
|
847
|
+
* rejection even when an injected `fetch` ignores the signal.
|
|
848
|
+
*/
|
|
849
|
+
private withDeadline;
|
|
822
850
|
private request;
|
|
851
|
+
/** Fetch + status handling. Always called inside `withDeadline`. */
|
|
852
|
+
private send;
|
|
823
853
|
get<T>(path: string): Promise<T>;
|
|
824
854
|
post<T>(path: string, body: unknown): Promise<T>;
|
|
825
855
|
private del;
|
package/dist/index.d.ts
CHANGED
|
@@ -120,6 +120,19 @@ interface AstralformBaseConfig {
|
|
|
120
120
|
baseURL?: string;
|
|
121
121
|
/** Supply a custom fetch (SSR, testing, custom interceptors). */
|
|
122
122
|
fetch?: typeof globalThis.fetch;
|
|
123
|
+
/**
|
|
124
|
+
* Abort a REST request — connect, headers, AND body read — after this many
|
|
125
|
+
* milliseconds. Defaults to 30_000. Does not apply to `uploadFile` (large
|
|
126
|
+
* files on slow uplinks) or to SSE streaming, which is long-lived by design
|
|
127
|
+
* and carries its own `AbortSignal`.
|
|
128
|
+
*
|
|
129
|
+
* Note that unlike axios and friends, `0` does NOT mean "no timeout": any
|
|
130
|
+
* non-positive or non-finite value falls back to the default. REST calls
|
|
131
|
+
* cannot opt out of the deadline — an unbounded REST request is the bug
|
|
132
|
+
* this exists to prevent, and it fails silently (a stalled body strands the
|
|
133
|
+
* caller forever with nothing to react to).
|
|
134
|
+
*/
|
|
135
|
+
timeoutMs?: number;
|
|
123
136
|
}
|
|
124
137
|
interface AstralformApiKeyConfig extends AstralformBaseConfig {
|
|
125
138
|
/** Agent API key (`sk_live_...` or `sk_test_...`). */
|
|
@@ -773,6 +786,7 @@ interface ConversationAsset {
|
|
|
773
786
|
declare class AstralformClient {
|
|
774
787
|
private readonly baseURL;
|
|
775
788
|
private readonly fetchFn;
|
|
789
|
+
private readonly timeoutMs;
|
|
776
790
|
/**
|
|
777
791
|
* Auth state is mutable so callers can rotate access tokens or switch
|
|
778
792
|
* agent context without re-instantiating the client. API-key mode is
|
|
@@ -819,7 +833,23 @@ declare class AstralformClient {
|
|
|
819
833
|
*/
|
|
820
834
|
private get authHeaders();
|
|
821
835
|
private get headers();
|
|
836
|
+
/**
|
|
837
|
+
* Run one REST exchange under a single deadline covering connect, headers,
|
|
838
|
+
* AND the body read. The body read is the part that matters: `json()` used
|
|
839
|
+
* to sit outside every guard, so a response whose headers arrived but whose
|
|
840
|
+
* body stalled hung forever — silently stranding callers that await it
|
|
841
|
+
* (a stalled `getMessages` used to leave `StreamManager.restore()` parked
|
|
842
|
+
* before it ever fetched the events it renders from).
|
|
843
|
+
*
|
|
844
|
+
* The controller is created per request and is deliberately NOT the
|
|
845
|
+
* session's — that one means "the user cancelled this turn" and is null
|
|
846
|
+
* outside a live turn. Aborting frees the socket; the race guarantees a
|
|
847
|
+
* rejection even when an injected `fetch` ignores the signal.
|
|
848
|
+
*/
|
|
849
|
+
private withDeadline;
|
|
822
850
|
private request;
|
|
851
|
+
/** Fetch + status handling. Always called inside `withDeadline`. */
|
|
852
|
+
private send;
|
|
823
853
|
get<T>(path: string): Promise<T>;
|
|
824
854
|
post<T>(path: string, body: unknown): Promise<T>;
|
|
825
855
|
private del;
|
package/dist/index.js
CHANGED
|
@@ -252,6 +252,7 @@ async function* streamJobSSE(options) {
|
|
|
252
252
|
|
|
253
253
|
// src/client.ts
|
|
254
254
|
var DEFAULT_BASE_URL = "https://api.astralform.ai";
|
|
255
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
255
256
|
function validateBaseURL(url) {
|
|
256
257
|
const cleaned = url.replace(/\/+$/, "");
|
|
257
258
|
try {
|
|
@@ -302,6 +303,7 @@ var AstralformClient = class {
|
|
|
302
303
|
}
|
|
303
304
|
this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);
|
|
304
305
|
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
306
|
+
this.timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
305
307
|
}
|
|
306
308
|
/**
|
|
307
309
|
* Replace the current OIDC access token without reconstructing the client.
|
|
@@ -392,11 +394,48 @@ var AstralformClient = class {
|
|
|
392
394
|
"Content-Type": "application/json"
|
|
393
395
|
};
|
|
394
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Run one REST exchange under a single deadline covering connect, headers,
|
|
399
|
+
* AND the body read. The body read is the part that matters: `json()` used
|
|
400
|
+
* to sit outside every guard, so a response whose headers arrived but whose
|
|
401
|
+
* body stalled hung forever — silently stranding callers that await it
|
|
402
|
+
* (a stalled `getMessages` used to leave `StreamManager.restore()` parked
|
|
403
|
+
* before it ever fetched the events it renders from).
|
|
404
|
+
*
|
|
405
|
+
* The controller is created per request and is deliberately NOT the
|
|
406
|
+
* session's — that one means "the user cancelled this turn" and is null
|
|
407
|
+
* outside a live turn. Aborting frees the socket; the race guarantees a
|
|
408
|
+
* rejection even when an injected `fetch` ignores the signal.
|
|
409
|
+
*/
|
|
410
|
+
async withDeadline(run) {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const timedOut = () => new ConnectionError(`Request timed out after ${this.timeoutMs}ms`);
|
|
413
|
+
let timer;
|
|
414
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
415
|
+
timer = setTimeout(() => {
|
|
416
|
+
controller.abort();
|
|
417
|
+
reject(timedOut());
|
|
418
|
+
}, this.timeoutMs);
|
|
419
|
+
});
|
|
420
|
+
try {
|
|
421
|
+
return await Promise.race([run(controller.signal), deadline]);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (controller.signal.aborted) throw timedOut();
|
|
424
|
+
throw err;
|
|
425
|
+
} finally {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
395
429
|
async request(method, path, body) {
|
|
430
|
+
return this.withDeadline((signal) => this.send(method, path, body, signal));
|
|
431
|
+
}
|
|
432
|
+
/** Fetch + status handling. Always called inside `withDeadline`. */
|
|
433
|
+
async send(method, path, body, signal) {
|
|
396
434
|
const response = await this.fetchFn(`${this.baseURL}${path}`, {
|
|
397
435
|
method,
|
|
398
436
|
headers: this.headers,
|
|
399
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
437
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
438
|
+
signal
|
|
400
439
|
}).catch((err) => {
|
|
401
440
|
throw new ConnectionError(
|
|
402
441
|
err instanceof Error ? err.message : "Failed to connect"
|
|
@@ -405,13 +444,21 @@ var AstralformClient = class {
|
|
|
405
444
|
await this.handleError(response);
|
|
406
445
|
return response;
|
|
407
446
|
}
|
|
447
|
+
// DO NOT refactor these back into `request()` + `.json()`. Parsing the body
|
|
448
|
+
// INSIDE the raced callback is the entire fix: `json()` outside the deadline
|
|
449
|
+
// is the original bug (headers arrive, body stalls, caller hangs forever).
|
|
450
|
+
// `request()` survives for `del()`, which never reads the body.
|
|
408
451
|
async get(path) {
|
|
409
|
-
|
|
410
|
-
|
|
452
|
+
return this.withDeadline(async (signal) => {
|
|
453
|
+
const response = await this.send("GET", path, void 0, signal);
|
|
454
|
+
return await response.json();
|
|
455
|
+
});
|
|
411
456
|
}
|
|
412
457
|
async post(path, body) {
|
|
413
|
-
|
|
414
|
-
|
|
458
|
+
return this.withDeadline(async (signal) => {
|
|
459
|
+
const response = await this.send("POST", path, body, signal);
|
|
460
|
+
return await response.json();
|
|
461
|
+
});
|
|
415
462
|
}
|
|
416
463
|
async del(path) {
|
|
417
464
|
await this.request("DELETE", path);
|