@mappa-ai/mappa-node 1.2.2 → 1.2.4
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 +180 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -8
- package/dist/index.d.mts +116 -8
- package/dist/index.mjs +179 -56
- 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.
|
|
@@ -1517,7 +1614,7 @@ function isObject(v) {
|
|
|
1517
1614
|
}
|
|
1518
1615
|
/**
|
|
1519
1616
|
* Async signature verification using WebCrypto (works in modern Node and browsers).
|
|
1520
|
-
* Signature scheme
|
|
1617
|
+
* Signature scheme:
|
|
1521
1618
|
* headers["mappa-signature"] = "t=1700000000,v1=<hex_hmac_sha256>"
|
|
1522
1619
|
* Signed payload: `${t}.${rawBody}`
|
|
1523
1620
|
*/
|
|
@@ -1535,20 +1632,24 @@ var WebhooksResource = class {
|
|
|
1535
1632
|
if (!timingSafeEqualHex(await hmacHex(params.secret, signed), parts.v1)) throw new Error("Invalid signature");
|
|
1536
1633
|
return { ok: true };
|
|
1537
1634
|
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Parse and validate a webhook event payload.
|
|
1637
|
+
*
|
|
1638
|
+
* @param payload - Raw JSON string from the webhook request body
|
|
1639
|
+
* @returns Parsed and validated webhook event
|
|
1640
|
+
* @throws Error if payload is invalid or missing required fields
|
|
1641
|
+
*/
|
|
1538
1642
|
parseEvent(payload) {
|
|
1539
1643
|
const raw = JSON.parse(payload);
|
|
1540
1644
|
if (!isObject(raw)) throw new Error("Invalid webhook payload: not an object");
|
|
1541
1645
|
const obj = raw;
|
|
1542
|
-
const id = obj.id;
|
|
1543
1646
|
const type = obj.type;
|
|
1544
|
-
const
|
|
1545
|
-
if (typeof id !== "string") throw new Error("Invalid webhook payload: id must be a string");
|
|
1647
|
+
const timestamp = obj.timestamp;
|
|
1546
1648
|
if (typeof type !== "string") throw new Error("Invalid webhook payload: type must be a string");
|
|
1547
|
-
if (typeof
|
|
1649
|
+
if (typeof timestamp !== "string") throw new Error("Invalid webhook payload: timestamp must be a string");
|
|
1548
1650
|
return {
|
|
1549
|
-
id,
|
|
1550
1651
|
type,
|
|
1551
|
-
|
|
1652
|
+
timestamp,
|
|
1552
1653
|
data: "data" in obj ? obj.data : void 0
|
|
1553
1654
|
};
|
|
1554
1655
|
}
|
|
@@ -1711,6 +1812,28 @@ function isMappaError(err) {
|
|
|
1711
1812
|
function isInsufficientCreditsError(err) {
|
|
1712
1813
|
return err instanceof InsufficientCreditsError;
|
|
1713
1814
|
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Type guard for stream connection errors.
|
|
1817
|
+
*
|
|
1818
|
+
* Use this to detect streaming failures and access recovery metadata
|
|
1819
|
+
* like `jobId`, `lastEventId`, and `retryCount`.
|
|
1820
|
+
*
|
|
1821
|
+
* @example
|
|
1822
|
+
* ```typescript
|
|
1823
|
+
* try {
|
|
1824
|
+
* await mappa.reports.generate({ ... });
|
|
1825
|
+
* } catch (err) {
|
|
1826
|
+
* if (isStreamError(err)) {
|
|
1827
|
+
* console.log(`Stream failed for job ${err.jobId}`);
|
|
1828
|
+
* console.log(`Last event ID: ${err.lastEventId}`);
|
|
1829
|
+
* console.log(`Retries attempted: ${err.retryCount}`);
|
|
1830
|
+
* }
|
|
1831
|
+
* }
|
|
1832
|
+
* ```
|
|
1833
|
+
*/
|
|
1834
|
+
function isStreamError(err) {
|
|
1835
|
+
return err instanceof StreamError;
|
|
1836
|
+
}
|
|
1714
1837
|
|
|
1715
1838
|
//#endregion
|
|
1716
1839
|
exports.ApiError = ApiError;
|
|
@@ -1721,6 +1844,7 @@ exports.JobFailedError = JobFailedError;
|
|
|
1721
1844
|
exports.Mappa = Mappa;
|
|
1722
1845
|
exports.MappaError = MappaError;
|
|
1723
1846
|
exports.RateLimitError = RateLimitError;
|
|
1847
|
+
exports.StreamError = StreamError;
|
|
1724
1848
|
exports.ValidationError = ValidationError;
|
|
1725
1849
|
exports.hasEntity = hasEntity;
|
|
1726
1850
|
exports.isInsufficientCreditsError = isInsufficientCreditsError;
|
|
@@ -1728,5 +1852,6 @@ exports.isJsonReport = isJsonReport;
|
|
|
1728
1852
|
exports.isMappaError = isMappaError;
|
|
1729
1853
|
exports.isMarkdownReport = isMarkdownReport;
|
|
1730
1854
|
exports.isPdfReport = isPdfReport;
|
|
1855
|
+
exports.isStreamError = isStreamError;
|
|
1731
1856
|
exports.isUrlReport = isUrlReport;
|
|
1732
1857
|
//# sourceMappingURL=index.cjs.map
|