@qoniai/qoni 0.4.1 → 0.4.3

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/dist/index.d.cts CHANGED
@@ -224,6 +224,7 @@ interface RawRequestInput extends TokenInput {
224
224
  headers?: Record<string, string>;
225
225
  }
226
226
  interface RequestPayload {
227
+ signal?: AbortSignal;
227
228
  query?: JsonObject;
228
229
  body?: JsonObject | BodyInit;
229
230
  headers?: Record<string, string>;
@@ -638,7 +639,7 @@ interface RunWaitOptions {
638
639
  * with the token + run addressing already bound.
639
640
  */
640
641
  interface RunWireOps {
641
- getDetail(): Promise<JsonObject>;
642
+ getDetail(signal?: AbortSignal): Promise<JsonObject>;
642
643
  streamEvents(opts: RunEventsOptions): AsyncIterable<QoniEvent<unknown>>;
643
644
  /**
644
645
  * Invoke an interaction action by posting to its declared endpoint. The
@@ -648,7 +649,7 @@ interface RunWireOps {
648
649
  postAction(action: Action, body: JsonObject | undefined): Promise<void>;
649
650
  cancel(reason?: string): Promise<JsonObject>;
650
651
  /** Products without artifacts leave this undefined → `result.artifacts` is []. */
651
- listArtifacts?(): Promise<Artifact[]>;
652
+ listArtifacts?(signal?: AbortSignal): Promise<Artifact[]>;
652
653
  }
653
654
  /**
654
655
  * Handle to a single run. Generic over the product's event set (分型): a
@@ -782,6 +783,7 @@ declare function createDoAnythingNamespace(transport: QoniTransport): {
782
783
  createRun: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
783
784
  getRun: <T = unknown>(input: RuntimeTokenInput & {
784
785
  runId: string;
786
+ signal?: AbortSignal;
785
787
  }) => Promise<QoniResponse<T>>;
786
788
  events: <T = unknown>(input: RuntimeTokenInput & {
787
789
  runId: string;
@@ -862,6 +864,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
862
864
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
863
865
  get: <T = unknown>(input: RuntimeTokenInput & {
864
866
  runId: string;
867
+ signal?: AbortSignal;
865
868
  }) => Promise<QoniResponse<T>>;
866
869
  events: <T = unknown>(input: RuntimeTokenInput & {
867
870
  runId: string;
@@ -885,6 +888,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
885
888
  } & JsonObject) => Promise<QoniResponse<T>>;
886
889
  listArtifacts: <T = unknown>(input: RuntimeTokenInput & {
887
890
  runId: string;
891
+ signal?: AbortSignal;
888
892
  }) => Promise<QoniResponse<T>>;
889
893
  getArtifact: <T = unknown>(input: RuntimeTokenInput & {
890
894
  runId: string;
@@ -1190,6 +1194,7 @@ declare function createWebSearchNamespace(transport: QoniTransport): {
1190
1194
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
1191
1195
  get: <T = unknown>(input: RuntimeTokenInput & {
1192
1196
  runId: string;
1197
+ signal?: AbortSignal;
1193
1198
  }) => Promise<QoniResponse<T>>;
1194
1199
  events: <T = unknown>(input: RuntimeTokenInput & {
1195
1200
  runId: string;
package/dist/index.d.ts CHANGED
@@ -224,6 +224,7 @@ interface RawRequestInput extends TokenInput {
224
224
  headers?: Record<string, string>;
225
225
  }
226
226
  interface RequestPayload {
227
+ signal?: AbortSignal;
227
228
  query?: JsonObject;
228
229
  body?: JsonObject | BodyInit;
229
230
  headers?: Record<string, string>;
@@ -638,7 +639,7 @@ interface RunWaitOptions {
638
639
  * with the token + run addressing already bound.
639
640
  */
640
641
  interface RunWireOps {
641
- getDetail(): Promise<JsonObject>;
642
+ getDetail(signal?: AbortSignal): Promise<JsonObject>;
642
643
  streamEvents(opts: RunEventsOptions): AsyncIterable<QoniEvent<unknown>>;
643
644
  /**
644
645
  * Invoke an interaction action by posting to its declared endpoint. The
@@ -648,7 +649,7 @@ interface RunWireOps {
648
649
  postAction(action: Action, body: JsonObject | undefined): Promise<void>;
649
650
  cancel(reason?: string): Promise<JsonObject>;
650
651
  /** Products without artifacts leave this undefined → `result.artifacts` is []. */
651
- listArtifacts?(): Promise<Artifact[]>;
652
+ listArtifacts?(signal?: AbortSignal): Promise<Artifact[]>;
652
653
  }
653
654
  /**
654
655
  * Handle to a single run. Generic over the product's event set (分型): a
@@ -782,6 +783,7 @@ declare function createDoAnythingNamespace(transport: QoniTransport): {
782
783
  createRun: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
783
784
  getRun: <T = unknown>(input: RuntimeTokenInput & {
784
785
  runId: string;
786
+ signal?: AbortSignal;
785
787
  }) => Promise<QoniResponse<T>>;
786
788
  events: <T = unknown>(input: RuntimeTokenInput & {
787
789
  runId: string;
@@ -862,6 +864,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
862
864
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
863
865
  get: <T = unknown>(input: RuntimeTokenInput & {
864
866
  runId: string;
867
+ signal?: AbortSignal;
865
868
  }) => Promise<QoniResponse<T>>;
866
869
  events: <T = unknown>(input: RuntimeTokenInput & {
867
870
  runId: string;
@@ -885,6 +888,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
885
888
  } & JsonObject) => Promise<QoniResponse<T>>;
886
889
  listArtifacts: <T = unknown>(input: RuntimeTokenInput & {
887
890
  runId: string;
891
+ signal?: AbortSignal;
888
892
  }) => Promise<QoniResponse<T>>;
889
893
  getArtifact: <T = unknown>(input: RuntimeTokenInput & {
890
894
  runId: string;
@@ -1190,6 +1194,7 @@ declare function createWebSearchNamespace(transport: QoniTransport): {
1190
1194
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
1191
1195
  get: <T = unknown>(input: RuntimeTokenInput & {
1192
1196
  runId: string;
1197
+ signal?: AbortSignal;
1193
1198
  }) => Promise<QoniResponse<T>>;
1194
1199
  events: <T = unknown>(input: RuntimeTokenInput & {
1195
1200
  runId: string;
package/dist/index.js CHANGED
@@ -349,8 +349,14 @@ function decodeDataUri(uri) {
349
349
  const match = /^data:([^;,]*)?(;base64)?,(.*)$/s.exec(uri);
350
350
  if (!match) return void 0;
351
351
  const [, mime, isBase64, body] = match;
352
- const bytes = isBase64 ? new Uint8Array(Buffer.from(body, "base64")) : new TextEncoder().encode(decodeURIComponent(body));
353
- return { bytes, mime: mime || "application/octet-stream" };
352
+ const encoded = isBase64 ? body.replace(/\s/g, "") : body;
353
+ if (isBase64 && (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded) || encoded.length % 4 === 1 || encoded.includes("=") && encoded.length % 4 !== 0)) return void 0;
354
+ try {
355
+ const bytes = isBase64 ? new Uint8Array(Buffer.from(encoded, "base64")) : new TextEncoder().encode(decodeURIComponent(encoded));
356
+ return bytes.length ? { bytes, mime: mime || "application/octet-stream" } : void 0;
357
+ } catch {
358
+ return void 0;
359
+ }
354
360
  }
355
361
  function asString(value) {
356
362
  return typeof value === "string" && value ? value : void 0;
@@ -768,11 +774,11 @@ var RunHandle = class {
768
774
  }
769
775
  /** Drive the run to a terminal state and return the settled result. */
770
776
  async wait(opts = {}) {
771
- const { signal, timedOut } = withTimeout(opts.signal, opts.timeoutMs);
777
+ const { signal, timedOut, dispose } = withTimeout(opts.signal, opts.timeoutMs);
772
778
  let lastEventId;
773
779
  let eventIdAtLastClose;
774
780
  let sawCleanClose = false;
775
- let terminalPayload;
781
+ let terminalDetail;
776
782
  let terminalDoneReason;
777
783
  let screenshotIndex = 0;
778
784
  try {
@@ -780,24 +786,28 @@ var RunHandle = class {
780
786
  for await (const event of this.events({
781
787
  signal,
782
788
  lastEventId,
783
- onWireEvent: opts.onWireEvent,
789
+ onWireEvent: (wire) => waitWithSignal(() => {
790
+ if (wire.id !== void 0) lastEventId = wire.id;
791
+ return opts.onWireEvent?.(wire);
792
+ }, signal),
784
793
  onReconnect: opts.onReconnect
785
794
  })) {
786
- if (event.raw.id) lastEventId = event.raw.id;
787
- await opts.onEvent?.(event);
795
+ await waitWithSignal(() => opts.onEvent?.(event), signal);
788
796
  if (event.type === "screenshot") {
789
- await opts.onScreenshot?.(event.image, screenshotIndex++);
797
+ await waitWithSignal(() => opts.onScreenshot?.(event.image, screenshotIndex++), signal);
790
798
  } else if (event.type === "interaction") {
791
- await opts.onInteraction?.(this.interactionHandle(event.data), event);
799
+ await waitWithSignal(() => opts.onInteraction?.(this.interactionHandle(event.data), event), signal);
792
800
  }
793
801
  if (event.isTerminal) {
794
- terminalPayload = isJsonObject(event.raw.data) ? event.raw.data : void 0;
795
802
  if (event.type === "done") terminalDoneReason = event.data.terminalReason || void 0;
796
803
  break settled;
797
804
  }
798
805
  }
799
- const detail2 = await this.ops.getDetail();
800
- if (isTerminalRunStatus(detail2.status)) break;
806
+ const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
807
+ if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
808
+ terminalDetail = detail2;
809
+ break;
810
+ }
801
811
  if (sawCleanClose && lastEventId === eventIdAtLastClose) {
802
812
  throw new QoniError(
803
813
  `run event stream ended before a terminal event while the run is still "${String(detail2.status)}" \u2014 re-attach or poll status()`,
@@ -807,6 +817,11 @@ var RunHandle = class {
807
817
  sawCleanClose = true;
808
818
  eventIdAtLastClose = lastEventId;
809
819
  }
820
+ const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
821
+ this.#adoptSessionRef(detail);
822
+ const result = settleRunResult(this.id, detail, [], terminalDoneReason);
823
+ result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
824
+ return result;
810
825
  } catch (err) {
811
826
  if (timedOut() && !opts.signal?.aborted) {
812
827
  throw new QoniTimeoutError(
@@ -815,17 +830,9 @@ var RunHandle = class {
815
830
  );
816
831
  }
817
832
  throw err;
833
+ } finally {
834
+ dispose();
818
835
  }
819
- let detail;
820
- try {
821
- detail = await this.ops.getDetail();
822
- } catch (err) {
823
- if (!terminalPayload) throw err;
824
- detail = terminalPayload;
825
- }
826
- this.#adoptSessionRef(detail);
827
- const artifacts = this.ops.listArtifacts ? await this.ops.listArtifacts() : [];
828
- return settleRunResult(this.id, detail, artifacts, terminalDoneReason);
829
836
  }
830
837
  /**
831
838
  * Wrap an {@link Interaction} (from `event.data` of an `interaction` event)
@@ -878,15 +885,12 @@ function normalizeRunStatus(id, detail) {
878
885
  function settleRunResult(runId, detail, artifacts, fallbackTerminalReason) {
879
886
  const terminalReason = (typeof detail.terminal_reason === "string" ? detail.terminal_reason : void 0) ?? fallbackTerminalReason;
880
887
  const isTaskSuccessful = typeof detail.is_task_successful === "boolean" ? detail.is_task_successful : void 0;
881
- let status;
882
- if (isTerminalRunStatus(detail.status)) {
883
- status = detail.status;
884
- } else if (terminalReason === "canceled") {
885
- status = "canceled";
886
- } else if (terminalReason === "failed" || terminalReason === "expired" || isTaskSuccessful === false) {
887
- status = "failed";
888
- } else {
889
- status = "succeeded";
888
+ const status = normalizeRunStatus(runId, detail).status;
889
+ if (!isTerminalRunStatus(status)) {
890
+ throw new QoniError(
891
+ "run detail did not confirm a terminal state \u2014 re-attach or poll status()",
892
+ { code: "upstream.failed", retryable: true, body: detail }
893
+ );
890
894
  }
891
895
  return {
892
896
  runId,
@@ -913,19 +917,40 @@ function isVideoFrame(wire) {
913
917
  return type === "browser_video_frame" || type === "run.browser_video_frame";
914
918
  }
915
919
  function withTimeout(signal, timeoutMs) {
916
- if (!timeoutMs) return { signal, timedOut: () => false };
920
+ if (!timeoutMs) return { signal, timedOut: () => false, dispose: () => {
921
+ } };
917
922
  const controller = new AbortController();
918
923
  let fired = false;
919
924
  const timer = setTimeout(() => {
920
925
  fired = true;
921
926
  controller.abort();
922
927
  }, timeoutMs);
923
- controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
928
+ const onAbort = () => controller.abort(signal?.reason);
929
+ const dispose = () => {
930
+ clearTimeout(timer);
931
+ signal?.removeEventListener("abort", onAbort);
932
+ };
933
+ controller.signal.addEventListener("abort", dispose, { once: true });
924
934
  if (signal) {
925
- if (signal.aborted) controller.abort();
926
- else signal.addEventListener("abort", () => controller.abort(), { once: true });
935
+ if (signal.aborted) onAbort();
936
+ else signal.addEventListener("abort", onAbort, { once: true });
927
937
  }
928
- return { signal: controller.signal, timedOut: () => fired };
938
+ return { signal: controller.signal, timedOut: () => fired, dispose };
939
+ }
940
+ function waitWithSignal(operation, signal) {
941
+ if (!signal) return Promise.resolve().then(operation);
942
+ return new Promise((resolve, reject) => {
943
+ const onAbort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
944
+ if (signal.aborted) {
945
+ onAbort();
946
+ return;
947
+ }
948
+ signal.addEventListener("abort", onAbort, { once: true });
949
+ Promise.resolve().then(() => {
950
+ signal.throwIfAborted();
951
+ return operation();
952
+ }).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
953
+ });
929
954
  }
930
955
 
931
956
  // src/deep-research.ts
@@ -940,6 +965,7 @@ function createDeepResearchNamespace(transport) {
940
965
  `/deep_research/runs/${encodeURIComponent(input.runId)}`,
941
966
  input.token,
942
967
  {
968
+ signal: input.signal,
943
969
  requiredScopes: ["webagent.deep_research:read"]
944
970
  }
945
971
  ),
@@ -985,6 +1011,7 @@ function createDeepResearchNamespace(transport) {
985
1011
  `/deep_research/runs/${encodeURIComponent(input.runId)}/artifacts`,
986
1012
  input.token,
987
1013
  {
1014
+ signal: input.signal,
988
1015
  requiredScopes: ["webagent.deep_research:read"]
989
1016
  }
990
1017
  ),
@@ -999,7 +1026,7 @@ function createDeepResearchNamespace(transport) {
999
1026
  };
1000
1027
  function buildOps(token, runId) {
1001
1028
  return {
1002
- getDetail: async () => asRecord((await api.get({ token, runId })).data),
1029
+ getDetail: async (signal) => asRecord((await api.get({ token, runId, signal })).data),
1003
1030
  streamEvents: (opts) => api.events({
1004
1031
  token,
1005
1032
  runId,
@@ -1019,8 +1046,8 @@ function createDeepResearchNamespace(transport) {
1019
1046
  );
1020
1047
  },
1021
1048
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
1022
- listArtifacts: async () => {
1023
- const res = await api.listArtifacts({ token, runId });
1049
+ listArtifacts: async (signal) => {
1050
+ const res = await api.listArtifacts({ token, runId, signal });
1024
1051
  const items = Array.isArray(res.data?.items) ? res.data.items : [];
1025
1052
  return items.filter(isJsonObject).map((item) => toArtifact(item, token, runId));
1026
1053
  }
@@ -1140,6 +1167,7 @@ function createDoAnythingNamespace(transport) {
1140
1167
  `/do_anything/runs/${encodeURIComponent(input.runId)}`,
1141
1168
  input.token,
1142
1169
  {
1170
+ signal: input.signal,
1143
1171
  requiredScopes: ["webagent.do_anything:read"]
1144
1172
  }
1145
1173
  ),
@@ -1197,7 +1225,7 @@ function createDoAnythingNamespace(transport) {
1197
1225
  };
1198
1226
  function buildOps(token, runId, capture) {
1199
1227
  return {
1200
- getDetail: async () => asRecord2((await api.getRun({ token, runId })).data),
1228
+ getDetail: async (signal) => asRecord2((await api.getRun({ token, runId, signal })).data),
1201
1229
  streamEvents: (opts) => api.events({
1202
1230
  token,
1203
1231
  runId,
@@ -1791,6 +1819,7 @@ function createWebSearchNamespace(transport) {
1791
1819
  `/web_search/runs/${encodeURIComponent(input.runId)}`,
1792
1820
  input.token,
1793
1821
  {
1822
+ signal: input.signal,
1794
1823
  requiredScopes: ["webagent.web_search:read"]
1795
1824
  }
1796
1825
  ),
@@ -1816,7 +1845,7 @@ function createWebSearchNamespace(transport) {
1816
1845
  };
1817
1846
  function buildOps(token, runId) {
1818
1847
  return {
1819
- getDetail: async () => asRecord4((await api.get({ token, runId })).data),
1848
+ getDetail: async (signal) => asRecord4((await api.get({ token, runId, signal })).data),
1820
1849
  streamEvents: (opts) => api.events({
1821
1850
  token,
1822
1851
  runId,
@@ -2089,7 +2118,9 @@ var Qoni = class {
2089
2118
  }
2090
2119
  }
2091
2120
  async webAgentJson(method, path, token, payload = {}) {
2121
+ payload.signal?.throwIfAborted();
2092
2122
  const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
2123
+ payload.signal?.throwIfAborted();
2093
2124
  const baseUrl = await this.baseUrlFor("webagent");
2094
2125
  return this.jsonRequest(
2095
2126
  baseUrl,
@@ -2100,9 +2131,11 @@ var Qoni = class {
2100
2131
  // Binary GET against the WebAgent API — artifact content downloads. JSON
2101
2132
  // error payloads still map to typed QoniErrors.
2102
2133
  async webAgentBytes(path, token, payload = {}) {
2134
+ payload.signal?.throwIfAborted();
2103
2135
  const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
2136
+ payload.signal?.throwIfAborted();
2104
2137
  const baseUrl = await this.baseUrlFor("webagent");
2105
- const response = await this.fetchWithTimeout(
2138
+ return this.fetchWithTimeout(
2106
2139
  urlWithBasePath(baseUrl, this.webAgentPath(runtimeToken, path), payload.query),
2107
2140
  {
2108
2141
  method: "GET",
@@ -2111,20 +2144,22 @@ var Qoni = class {
2111
2144
  authorization: `Bearer ${runtimeToken}`,
2112
2145
  ...payload.headers || {}
2113
2146
  }
2114
- }
2147
+ },
2148
+ async (response, signal) => {
2149
+ if (!response.ok) {
2150
+ throw errorFromPayload(
2151
+ response.status,
2152
+ await readJson(response, signal),
2153
+ `Qoni request failed with HTTP ${response.status}`
2154
+ );
2155
+ }
2156
+ return {
2157
+ bytes: await readBody(response, signal),
2158
+ mime: response.headers.get("content-type") || "application/octet-stream"
2159
+ };
2160
+ },
2161
+ payload.signal
2115
2162
  );
2116
- if (!response.ok) {
2117
- throw errorFromPayload(
2118
- response.status,
2119
- await readJson(response),
2120
- `Qoni request failed with HTTP ${response.status}`
2121
- );
2122
- }
2123
- const buffer = await response.arrayBuffer();
2124
- return {
2125
- bytes: new Uint8Array(buffer),
2126
- mime: response.headers.get("content-type") || "application/octet-stream"
2127
- };
2128
2163
  }
2129
2164
  async *webAgentSSE(path, token, payload = {}) {
2130
2165
  const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
@@ -2164,15 +2199,16 @@ var Qoni = class {
2164
2199
  });
2165
2200
  }
2166
2201
  async jsonRequest(baseUrl, pathname, options) {
2167
- const response = await this.fetchWithTimeout(
2202
+ const { response, payload } = await this.fetchWithTimeout(
2168
2203
  urlWithBasePath(baseUrl, pathname, options.query),
2169
2204
  {
2170
2205
  method: options.method,
2171
2206
  headers: requestHeaders(options),
2172
2207
  body: requestBody(options.body)
2173
- }
2208
+ },
2209
+ async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) }),
2210
+ options.signal
2174
2211
  );
2175
- const payload = await readJson(response);
2176
2212
  if (!response.ok) {
2177
2213
  throw errorFromPayload(
2178
2214
  response.status,
@@ -2222,45 +2258,55 @@ var Qoni = class {
2222
2258
  }
2223
2259
  }
2224
2260
  async *sseConnectOnce(baseUrl, pathname, options) {
2225
- const response = await this.fetchWithTimeout(
2226
- urlWithBasePath(baseUrl, pathname, options.query),
2227
- {
2228
- method: "GET",
2229
- headers: {
2230
- accept: "text/event-stream",
2231
- authorization: `Bearer ${options.token}`,
2232
- ...options.headers || {},
2233
- ...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
2234
- }
2235
- },
2236
- options.signal,
2237
- { streamBody: true }
2238
- );
2239
- if (!response.ok) {
2240
- throw errorFromPayload(
2241
- response.status,
2242
- await readJson(response),
2243
- `Qoni stream failed with HTTP ${response.status}`
2244
- );
2245
- }
2246
- if (!response.body) {
2247
- throw errorFromPayload(502, void 0, "Qoni stream response has no body");
2248
- }
2249
- const reader = response.body.getReader();
2261
+ let cleanup;
2262
+ let reader;
2263
+ const onAbort = () => {
2264
+ void reader?.cancel(options.signal?.reason).catch(() => void 0);
2265
+ };
2250
2266
  const decoder = new TextDecoder();
2251
2267
  let buffer = "";
2252
2268
  try {
2269
+ const response = await this.fetchWithTimeout(
2270
+ urlWithBasePath(baseUrl, pathname, options.query),
2271
+ {
2272
+ method: "GET",
2273
+ headers: {
2274
+ accept: "text/event-stream",
2275
+ authorization: `Bearer ${options.token}`,
2276
+ ...options.headers || {},
2277
+ ...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
2278
+ }
2279
+ },
2280
+ async (response2, signal) => {
2281
+ if (!response2.ok) {
2282
+ throw errorFromPayload(
2283
+ response2.status,
2284
+ await readJson(response2, signal),
2285
+ `Qoni stream failed with HTTP ${response2.status}`
2286
+ );
2287
+ }
2288
+ return response2;
2289
+ },
2290
+ options.signal,
2291
+ { streamCleanup: (dispose) => {
2292
+ cleanup = dispose;
2293
+ } }
2294
+ );
2295
+ if (!response.body) {
2296
+ throw errorFromPayload(502, void 0, "Qoni stream response has no body");
2297
+ }
2298
+ reader = response.body.getReader();
2299
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2253
2300
  while (true) {
2254
- if (options.signal?.aborted) {
2255
- await reader.cancel().catch(() => void 0);
2256
- throw options.signal.reason || new DOMException("Aborted", "AbortError");
2257
- }
2301
+ options.signal?.throwIfAborted();
2258
2302
  const { done, value } = await reader.read();
2303
+ options.signal?.throwIfAborted();
2259
2304
  if (done) break;
2260
2305
  buffer += decoder.decode(value, { stream: true });
2261
2306
  const chunks = buffer.split(/\r?\n\r?\n/);
2262
2307
  buffer = chunks.pop() || "";
2263
2308
  for (const chunk of chunks) {
2309
+ options.signal?.throwIfAborted();
2264
2310
  const event = parseSSEEvent(chunk);
2265
2311
  if (event) yield event;
2266
2312
  }
@@ -2268,14 +2314,16 @@ var Qoni = class {
2268
2314
  const finalEvent = parseSSEEvent(buffer);
2269
2315
  if (finalEvent) yield finalEvent;
2270
2316
  } finally {
2271
- await reader.cancel().catch(() => void 0);
2317
+ cleanup?.();
2318
+ options.signal?.removeEventListener("abort", onAbort);
2319
+ void reader?.cancel().catch(() => void 0);
2272
2320
  try {
2273
- reader.releaseLock();
2321
+ reader?.releaseLock();
2274
2322
  } catch {
2275
2323
  }
2276
2324
  }
2277
2325
  }
2278
- async fetchWithTimeout(input, init, signal, opts) {
2326
+ async fetchWithTimeout(input, init, consume, signal, opts) {
2279
2327
  if (signal?.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
2280
2328
  const controller = new AbortController();
2281
2329
  const resolved = resolveLocalGenauthRequest(input);
@@ -2290,17 +2338,39 @@ var Qoni = class {
2290
2338
  didTimeout = true;
2291
2339
  controller.abort();
2292
2340
  }, this.timeoutMs);
2341
+ let response;
2293
2342
  try {
2294
- return await this.fetchImpl(resolved.url, { ...requestInit, signal: controller.signal });
2343
+ response = await new Promise((resolve, reject) => {
2344
+ const onAbort = () => reject(controller.signal.reason);
2345
+ controller.signal.addEventListener("abort", onAbort, { once: true });
2346
+ Promise.resolve().then(() => {
2347
+ controller.signal.throwIfAborted();
2348
+ return this.fetchImpl(resolved.url, { ...requestInit, signal: controller.signal });
2349
+ }).then((response2) => {
2350
+ if (controller.signal.aborted) {
2351
+ void response2.body?.cancel(controller.signal.reason).catch(() => void 0);
2352
+ reject(controller.signal.reason);
2353
+ } else {
2354
+ resolve(response2);
2355
+ }
2356
+ }, reject).finally(() => controller.signal.removeEventListener("abort", onAbort));
2357
+ });
2358
+ const result = await consume(response, controller.signal);
2359
+ controller.signal.throwIfAborted();
2360
+ return result;
2295
2361
  } catch (error) {
2296
- if (isAbortError(error) && didTimeout || isConnectTimeoutError(error)) {
2362
+ void response?.body?.cancel().catch(() => void 0);
2363
+ if (signal?.aborted) throw signal.reason;
2364
+ if (didTimeout || isConnectTimeoutError(error)) {
2297
2365
  throw timeoutError(error);
2298
2366
  }
2299
2367
  if (isFetchNetworkError(error)) throw networkError(error);
2300
2368
  throw error;
2301
2369
  } finally {
2302
2370
  clearTimeout(timeout);
2303
- if (!opts?.streamBody) signal?.removeEventListener("abort", abortFromSignal);
2371
+ const cleanup = () => signal?.removeEventListener("abort", abortFromSignal);
2372
+ if (opts?.streamCleanup) opts.streamCleanup(cleanup);
2373
+ else cleanup();
2304
2374
  }
2305
2375
  }
2306
2376
  async baseUrlFor(service) {
@@ -2356,7 +2426,7 @@ var Qoni = class {
2356
2426
  pathname,
2357
2427
  {}
2358
2428
  );
2359
- const response = await this.fetchWithTimeout(
2429
+ const { response, payload } = await this.fetchWithTimeout(
2360
2430
  urlWithBasePath(this.host, pathname),
2361
2431
  {
2362
2432
  method: "GET",
@@ -2364,9 +2434,9 @@ var Qoni = class {
2364
2434
  accept: "application/json",
2365
2435
  ...headers
2366
2436
  }
2367
- }
2437
+ },
2438
+ async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) })
2368
2439
  );
2369
- const payload = await readJson(response);
2370
2440
  if (!response.ok) {
2371
2441
  throw errorFromPayload(
2372
2442
  response.status,
@@ -2460,18 +2530,52 @@ function requestBody(body) {
2460
2530
  function isBodyInit(value) {
2461
2531
  return typeof value === "string" || value instanceof ArrayBuffer || value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams;
2462
2532
  }
2463
- async function readJson(response) {
2533
+ async function readBody(response, signal) {
2534
+ if (!response.body) {
2535
+ signal.throwIfAborted();
2536
+ return new Uint8Array();
2537
+ }
2538
+ const reader = response.body.getReader();
2539
+ const onAbort = () => {
2540
+ void reader.cancel(signal.reason).catch(() => void 0);
2541
+ };
2542
+ signal.addEventListener("abort", onAbort, { once: true });
2543
+ try {
2544
+ const chunks = [];
2545
+ let size = 0;
2546
+ for (; ; ) {
2547
+ signal.throwIfAborted();
2548
+ const { done, value } = await reader.read();
2549
+ signal.throwIfAborted();
2550
+ if (done) break;
2551
+ chunks.push(value);
2552
+ size += value.byteLength;
2553
+ }
2554
+ const bytes = new Uint8Array(size);
2555
+ let offset = 0;
2556
+ for (const chunk of chunks) {
2557
+ bytes.set(chunk, offset);
2558
+ offset += chunk.byteLength;
2559
+ }
2560
+ return bytes;
2561
+ } finally {
2562
+ signal.removeEventListener("abort", onAbort);
2563
+ void reader.cancel().catch(() => void 0);
2564
+ reader.releaseLock();
2565
+ }
2566
+ }
2567
+ async function readJson(response, signal) {
2464
2568
  const contentType = response.headers.get("content-type")?.toLowerCase() || "";
2465
- if (contentType.includes("ndjson") || contentType.includes("jsonl")) {
2466
- const body = await response.text();
2467
- if (!body.trim()) return [];
2468
- try {
2569
+ const body = new TextDecoder().decode(await readBody(response, signal));
2570
+ try {
2571
+ if (contentType.includes("ndjson") || contentType.includes("jsonl")) {
2572
+ if (!body.trim()) return [];
2469
2573
  return body.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
2470
- } catch {
2471
- return void 0;
2472
2574
  }
2575
+ return JSON.parse(body);
2576
+ } catch {
2577
+ return void 0;
2473
2578
  }
2474
- return response.json().catch(() => void 0);
2475
2579
  }
2476
2580
  function normalizeResponse(payload, _service) {
2477
2581
  if (isJsonObject(payload) && ("data" in payload || "meta" in payload)) {
@@ -2714,6 +2818,7 @@ function isRetryableStreamError(err) {
2714
2818
  if (err && typeof err === "object") {
2715
2819
  const e = err;
2716
2820
  if (e.name === "AbortError") return false;
2821
+ if (e.status === 408) return true;
2717
2822
  if (typeof e.retryable === "boolean") return e.retryable;
2718
2823
  if (typeof e.status === "number") {
2719
2824
  return e.status >= 500 || e.status === 408 || e.status === 429;
@@ -2820,9 +2925,6 @@ function normalizeDelegateAgentTokenResponse(response) {
2820
2925
  }
2821
2926
  };
2822
2927
  }
2823
- function isAbortError(error) {
2824
- return error instanceof DOMException && error.name === "AbortError";
2825
- }
2826
2928
  function isConnectTimeoutError(error) {
2827
2929
  const cause = errorCause(error);
2828
2930
  return errorCode(cause) === "UND_ERR_CONNECT_TIMEOUT" || errorName(cause) === "ConnectTimeoutError";