@astralform/js 4.0.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 CHANGED
@@ -61,9 +61,15 @@ const session = new ChatSession({
61
61
  userId: "user-123", // Required — identifies the end user
62
62
  baseURL: "http://localhost:8000", // Optional — defaults to https://api.astralform.ai
63
63
  fetch: customFetch, // Optional — custom fetch implementation
64
+ timeoutMs: 30_000, // Optional — REST request deadline, defaults to 30s
64
65
  });
65
66
  ```
66
67
 
68
+ `timeoutMs` bounds each REST request end to end — connect, headers, and the
69
+ body read — rejecting with `ConnectionError` when it expires. File uploads and
70
+ the SSE stream are exempt: both are long-running by design, and the stream
71
+ carries its own `AbortSignal`.
72
+
67
73
  ## Events
68
74
 
69
75
  Subscribe to events with `.on()`, which returns an unsubscribe function. The SDK forwards a typed `ChatEvent` for every wire event — consumers build their own block / message state from the stream.
package/dist/index.cjs CHANGED
@@ -298,6 +298,7 @@ async function* streamJobSSE(options) {
298
298
 
299
299
  // src/client.ts
300
300
  var DEFAULT_BASE_URL = "https://api.astralform.ai";
301
+ var DEFAULT_TIMEOUT_MS = 3e4;
301
302
  function validateBaseURL(url) {
302
303
  const cleaned = url.replace(/\/+$/, "");
303
304
  try {
@@ -348,6 +349,7 @@ var AstralformClient = class {
348
349
  }
349
350
  this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);
350
351
  this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
352
+ this.timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
351
353
  }
352
354
  /**
353
355
  * Replace the current OIDC access token without reconstructing the client.
@@ -438,11 +440,48 @@ var AstralformClient = class {
438
440
  "Content-Type": "application/json"
439
441
  };
440
442
  }
443
+ /**
444
+ * Run one REST exchange under a single deadline covering connect, headers,
445
+ * AND the body read. The body read is the part that matters: `json()` used
446
+ * to sit outside every guard, so a response whose headers arrived but whose
447
+ * body stalled hung forever — silently stranding callers that await it
448
+ * (a stalled `getMessages` used to leave `StreamManager.restore()` parked
449
+ * before it ever fetched the events it renders from).
450
+ *
451
+ * The controller is created per request and is deliberately NOT the
452
+ * session's — that one means "the user cancelled this turn" and is null
453
+ * outside a live turn. Aborting frees the socket; the race guarantees a
454
+ * rejection even when an injected `fetch` ignores the signal.
455
+ */
456
+ async withDeadline(run) {
457
+ const controller = new AbortController();
458
+ const timedOut = () => new ConnectionError(`Request timed out after ${this.timeoutMs}ms`);
459
+ let timer;
460
+ const deadline = new Promise((_resolve, reject) => {
461
+ timer = setTimeout(() => {
462
+ controller.abort();
463
+ reject(timedOut());
464
+ }, this.timeoutMs);
465
+ });
466
+ try {
467
+ return await Promise.race([run(controller.signal), deadline]);
468
+ } catch (err) {
469
+ if (controller.signal.aborted) throw timedOut();
470
+ throw err;
471
+ } finally {
472
+ clearTimeout(timer);
473
+ }
474
+ }
441
475
  async request(method, path, body) {
476
+ return this.withDeadline((signal) => this.send(method, path, body, signal));
477
+ }
478
+ /** Fetch + status handling. Always called inside `withDeadline`. */
479
+ async send(method, path, body, signal) {
442
480
  const response = await this.fetchFn(`${this.baseURL}${path}`, {
443
481
  method,
444
482
  headers: this.headers,
445
- body: body !== void 0 ? JSON.stringify(body) : void 0
483
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
484
+ signal
446
485
  }).catch((err) => {
447
486
  throw new ConnectionError(
448
487
  err instanceof Error ? err.message : "Failed to connect"
@@ -451,13 +490,21 @@ var AstralformClient = class {
451
490
  await this.handleError(response);
452
491
  return response;
453
492
  }
493
+ // DO NOT refactor these back into `request()` + `.json()`. Parsing the body
494
+ // INSIDE the raced callback is the entire fix: `json()` outside the deadline
495
+ // is the original bug (headers arrive, body stalls, caller hangs forever).
496
+ // `request()` survives for `del()`, which never reads the body.
454
497
  async get(path) {
455
- const response = await this.request("GET", path);
456
- return response.json();
498
+ return this.withDeadline(async (signal) => {
499
+ const response = await this.send("GET", path, void 0, signal);
500
+ return await response.json();
501
+ });
457
502
  }
458
503
  async post(path, body) {
459
- const response = await this.request("POST", path, body);
460
- return response.json();
504
+ return this.withDeadline(async (signal) => {
505
+ const response = await this.send("POST", path, body, signal);
506
+ return await response.json();
507
+ });
461
508
  }
462
509
  async del(path) {
463
510
  await this.request("DELETE", path);
@@ -1348,6 +1395,7 @@ var ChatSession = class {
1348
1395
  upload_ids: options?.uploadIds,
1349
1396
  agent_name: options?.agentName,
1350
1397
  plan_mode: options?.planMode,
1398
+ goal: options?.goal,
1351
1399
  // Per-request model choice (client-side model selection).
1352
1400
  provider: options?.provider,
1353
1401
  model: options?.model,
@@ -1911,6 +1959,7 @@ var StreamManager = class {
1911
1959
  agentName: options?.agentName,
1912
1960
  uploadIds: options?.uploadIds,
1913
1961
  planMode: options?.planMode,
1962
+ goal: options?.goal,
1914
1963
  provider: options?.provider,
1915
1964
  model: options?.model,
1916
1965
  reasoningEffort: options?.reasoningEffort,