@mappa-ai/mappa-node 1.2.1 → 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 +169 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -20
- package/dist/index.d.mts +107 -20
- package/dist/index.mjs +168 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|