@mappa-ai/mappa-node 1.2.2 → 1.2.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.cjs CHANGED
@@ -170,6 +170,54 @@ var JobCanceledError = class extends MappaError {
170
170
  return lines.join("\n");
171
171
  }
172
172
  };
173
+ /**
174
+ * Error thrown when SSE streaming fails after all retries.
175
+ *
176
+ * Includes recovery metadata to allow callers to resume or retry:
177
+ * - `jobId`: The job being streamed (when known)
178
+ * - `lastEventId`: Last successfully received event ID for resumption
179
+ * - `url`: The stream URL that failed
180
+ * - `retryCount`: Number of retries attempted
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * try {
185
+ * for await (const event of mappa.jobs.stream(jobId)) { ... }
186
+ * } catch (err) {
187
+ * if (err instanceof StreamError) {
188
+ * console.log(`Stream failed for job ${err.jobId}`);
189
+ * console.log(`Last event ID: ${err.lastEventId}`);
190
+ * // Can retry with: mappa.jobs.stream(err.jobId)
191
+ * }
192
+ * }
193
+ * ```
194
+ */
195
+ var StreamError = class extends MappaError {
196
+ name = "StreamError";
197
+ jobId;
198
+ lastEventId;
199
+ url;
200
+ retryCount;
201
+ constructor(message, opts) {
202
+ super(message, {
203
+ requestId: opts.requestId,
204
+ cause: opts.cause
205
+ });
206
+ this.jobId = opts.jobId;
207
+ this.lastEventId = opts.lastEventId;
208
+ this.url = opts.url;
209
+ this.retryCount = opts.retryCount;
210
+ }
211
+ toString() {
212
+ const lines = [`${this.name}: ${this.message}`];
213
+ if (this.jobId) lines.push(` Job ID: ${this.jobId}`);
214
+ if (this.lastEventId) lines.push(` Last Event ID: ${this.lastEventId}`);
215
+ if (this.url) lines.push(` URL: ${this.url}`);
216
+ lines.push(` Retry Count: ${this.retryCount}`);
217
+ if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
218
+ return lines.join("\n");
219
+ }
220
+ };
173
221
 
174
222
  //#endregion
175
223
  //#region src/resources/credits.ts
@@ -885,10 +933,19 @@ var JobsResource = class {
885
933
  } catch (error) {
886
934
  if (opts?.signal?.aborted) throw error;
887
935
  retries++;
888
- if (retries >= maxRetries) throw error;
936
+ if (retries >= maxRetries) throw new StreamError(`Stream connection failed for job ${jobId} after ${maxRetries} retries`, {
937
+ jobId,
938
+ lastEventId,
939
+ retryCount: retries,
940
+ cause: error
941
+ });
889
942
  await this.backoff(retries);
890
943
  }
891
- throw new MappaError(`Failed to get status for job ${jobId} after ${maxRetries} retries`);
944
+ throw new StreamError(`Failed to get status for job ${jobId} after ${maxRetries} retries`, {
945
+ jobId,
946
+ lastEventId,
947
+ retryCount: maxRetries
948
+ });
892
949
  }
893
950
  /**
894
951
  * Map an SSE event to a JobEvent.
@@ -1270,6 +1327,25 @@ function shouldRetry(opts, err) {
1270
1327
  if (err instanceof TypeError) return { retry: true };
1271
1328
  return { retry: false };
1272
1329
  }
1330
+ /**
1331
+ * Checks if an error is a network-level failure that's safe to retry.
1332
+ * Includes socket failures, DNS errors, and fetch TypeErrors.
1333
+ */
1334
+ function isNetworkError(err) {
1335
+ if (err instanceof TypeError) return true;
1336
+ if (err && typeof err === "object") {
1337
+ const code = err.code;
1338
+ if (typeof code === "string") return [
1339
+ "FailedToOpenSocket",
1340
+ "ECONNREFUSED",
1341
+ "ECONNRESET",
1342
+ "ETIMEDOUT",
1343
+ "ENOTFOUND",
1344
+ "EAI_AGAIN"
1345
+ ].includes(code);
1346
+ }
1347
+ return false;
1348
+ }
1273
1349
  var Transport = class {
1274
1350
  fetchImpl;
1275
1351
  constructor(opts) {
@@ -1281,10 +1357,14 @@ var Transport = class {
1281
1357
  *
1282
1358
  * Uses native `fetch` with streaming response body (not browser-only `EventSource`).
1283
1359
  * Parses SSE format manually from the `ReadableStream`.
1360
+ *
1361
+ * Automatically retries on network failures (socket errors, DNS failures, etc.)
1362
+ * up to `maxRetries` times with exponential backoff.
1284
1363
  */
1285
1364
  async *streamSSE(path, opts) {
1286
1365
  const url = buildUrl(this.opts.baseUrl, path);
1287
1366
  const requestId = randomId("req");
1367
+ const maxRetries = Math.max(0, this.opts.maxRetries);
1288
1368
  const headers = {
1289
1369
  Accept: "text/event-stream",
1290
1370
  "Cache-Control": "no-cache",
@@ -1294,57 +1374,74 @@ var Transport = class {
1294
1374
  ...this.opts.defaultHeaders ?? {}
1295
1375
  };
1296
1376
  if (opts?.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
1297
- const controller = new AbortController();
1298
- const timeout = setTimeout(() => controller.abort(makeAbortError()), this.opts.timeoutMs);
1299
- if (hasAbortSignal(opts?.signal)) {
1300
- const signal = opts?.signal;
1301
- if (signal?.aborted) {
1302
- clearTimeout(timeout);
1303
- throw makeAbortError();
1304
- }
1305
- signal?.addEventListener("abort", () => controller.abort(makeAbortError()), { once: true });
1306
- }
1307
- this.opts.telemetry?.onRequest?.({
1308
- method: "GET",
1309
- url,
1310
- requestId
1311
- });
1312
1377
  let res;
1313
- try {
1314
- res = await this.fetchImpl(url, {
1378
+ let lastError;
1379
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1380
+ const controller = new AbortController();
1381
+ const timeout = setTimeout(() => controller.abort(makeAbortError()), this.opts.timeoutMs);
1382
+ if (hasAbortSignal(opts?.signal)) {
1383
+ const signal = opts?.signal;
1384
+ if (signal?.aborted) {
1385
+ clearTimeout(timeout);
1386
+ throw makeAbortError();
1387
+ }
1388
+ signal?.addEventListener("abort", () => controller.abort(makeAbortError()), { once: true });
1389
+ }
1390
+ this.opts.telemetry?.onRequest?.({
1315
1391
  method: "GET",
1316
- headers,
1317
- signal: controller.signal
1318
- });
1319
- } catch (err) {
1320
- clearTimeout(timeout);
1321
- this.opts.telemetry?.onError?.({
1322
- url,
1323
- requestId,
1324
- error: err
1325
- });
1326
- throw err;
1327
- }
1328
- if (!res.ok) {
1329
- clearTimeout(timeout);
1330
- const { parsed } = await readBody(res);
1331
- const apiErr = coerceApiError(res, parsed);
1332
- this.opts.telemetry?.onError?.({
1333
1392
  url,
1334
- requestId,
1335
- error: apiErr
1393
+ requestId
1336
1394
  });
1337
- throw apiErr;
1338
- }
1339
- if (!res.body) {
1340
- clearTimeout(timeout);
1341
- throw new MappaError("SSE response has no body");
1342
- }
1343
- try {
1344
- yield* this.parseSSEStream(res.body);
1345
- } finally {
1346
- clearTimeout(timeout);
1395
+ try {
1396
+ res = await this.fetchImpl(url, {
1397
+ method: "GET",
1398
+ headers,
1399
+ signal: controller.signal
1400
+ });
1401
+ clearTimeout(timeout);
1402
+ if (!res.ok) {
1403
+ const { parsed } = await readBody(res);
1404
+ const apiErr = coerceApiError(res, parsed);
1405
+ this.opts.telemetry?.onError?.({
1406
+ url,
1407
+ requestId,
1408
+ error: apiErr,
1409
+ context: {
1410
+ attempt,
1411
+ lastEventId: opts?.lastEventId
1412
+ }
1413
+ });
1414
+ if (res.status >= 500 && res.status <= 599 && attempt < maxRetries) {
1415
+ const delay = jitter(backoffMs(attempt + 1, 500, 4e3));
1416
+ await new Promise((r) => setTimeout(r, delay));
1417
+ continue;
1418
+ }
1419
+ throw apiErr;
1420
+ }
1421
+ break;
1422
+ } catch (err) {
1423
+ clearTimeout(timeout);
1424
+ lastError = err;
1425
+ this.opts.telemetry?.onError?.({
1426
+ url,
1427
+ requestId,
1428
+ error: err,
1429
+ context: {
1430
+ attempt,
1431
+ lastEventId: opts?.lastEventId
1432
+ }
1433
+ });
1434
+ if (isNetworkError(err) && attempt < maxRetries) {
1435
+ const delay = jitter(backoffMs(attempt + 1, 500, 4e3));
1436
+ await new Promise((r) => setTimeout(r, delay));
1437
+ continue;
1438
+ }
1439
+ throw err;
1440
+ }
1347
1441
  }
1442
+ if (!res) throw lastError;
1443
+ if (!res.body) throw new MappaError("SSE response has no body");
1444
+ yield* this.parseSSEStream(res.body);
1348
1445
  }
1349
1446
  /**
1350
1447
  * Parse SSE events from a ReadableStream.
@@ -1711,6 +1808,28 @@ function isMappaError(err) {
1711
1808
  function isInsufficientCreditsError(err) {
1712
1809
  return err instanceof InsufficientCreditsError;
1713
1810
  }
1811
+ /**
1812
+ * Type guard for stream connection errors.
1813
+ *
1814
+ * Use this to detect streaming failures and access recovery metadata
1815
+ * like `jobId`, `lastEventId`, and `retryCount`.
1816
+ *
1817
+ * @example
1818
+ * ```typescript
1819
+ * try {
1820
+ * await mappa.reports.generate({ ... });
1821
+ * } catch (err) {
1822
+ * if (isStreamError(err)) {
1823
+ * console.log(`Stream failed for job ${err.jobId}`);
1824
+ * console.log(`Last event ID: ${err.lastEventId}`);
1825
+ * console.log(`Retries attempted: ${err.retryCount}`);
1826
+ * }
1827
+ * }
1828
+ * ```
1829
+ */
1830
+ function isStreamError(err) {
1831
+ return err instanceof StreamError;
1832
+ }
1714
1833
 
1715
1834
  //#endregion
1716
1835
  exports.ApiError = ApiError;
@@ -1721,6 +1840,7 @@ exports.JobFailedError = JobFailedError;
1721
1840
  exports.Mappa = Mappa;
1722
1841
  exports.MappaError = MappaError;
1723
1842
  exports.RateLimitError = RateLimitError;
1843
+ exports.StreamError = StreamError;
1724
1844
  exports.ValidationError = ValidationError;
1725
1845
  exports.hasEntity = hasEntity;
1726
1846
  exports.isInsufficientCreditsError = isInsufficientCreditsError;
@@ -1728,5 +1848,6 @@ exports.isJsonReport = isJsonReport;
1728
1848
  exports.isMappaError = isMappaError;
1729
1849
  exports.isMarkdownReport = isMarkdownReport;
1730
1850
  exports.isPdfReport = isPdfReport;
1851
+ exports.isStreamError = isStreamError;
1731
1852
  exports.isUrlReport = isUrlReport;
1732
1853
  //# sourceMappingURL=index.cjs.map