manifest 7.2.0 → 7.2.1
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/CONTRACT.md +3 -1
- package/README.md +58 -32
- package/dist/bin.cjs +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-NHEF7GUP.js → chunk-OXOZQLZ3.js} +24 -1
- package/dist/{chunk-GC5H22R2.js → chunk-RO6CIYR2.js} +29 -3
- package/dist/index.cjs +48 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/register.cjs +48 -3
- package/dist/register.js +2 -2
- package/docs/guide.md +1 -1
- package/package.json +1 -1
package/CONTRACT.md
CHANGED
|
@@ -71,4 +71,6 @@ Every call the SDK sees and does not send to `POST /v1/heal`, whatever its statu
|
|
|
71
71
|
|
|
72
72
|
Metadata only. The URL carries scheme, host, port and path: no query string, userinfo or fragment. No headers and no request or response body are sent, and a response body is never read to record a call. A call sent to `/v1/heal` is not also tracked.
|
|
73
73
|
|
|
74
|
-
Recording is an in-memory append and never delays or fails the caller's request. A batch is sent when 500 calls are queued or every five seconds, at most once per second, one send at a time, up to 500 calls per request, with a five-second deadline. At most 5,000 calls are buffered; newer calls are dropped past that. A network error, timeout, 429 or 5xx is retried once, then the batch is dropped; any other answer, including 404 from a server without the route, drops it. HTTP 403 with `{"error":"project_disabled"}` suspends sending for five minutes, as for healing. Methods are upper-cased; a record whose method exceeds 16 characters or whose URL exceeds 4,096 is not sent. Buffered calls are sent once when the event loop drains (`beforeExit`), for at most two seconds: a send still in flight then is aborted and the rest is dropped, so a script never hangs on an unreachable server. A process killed
|
|
74
|
+
Recording is an in-memory append and never delays or fails the caller's request. A batch is sent when 500 calls are queued or every five seconds, at most once per second, one send at a time, up to 500 calls per request, with a five-second deadline. At most 5,000 calls are buffered; newer calls are dropped past that. A network error, timeout, 429 or 5xx is retried once, then the batch is dropped; any other answer, including 404 from a server without the route, drops it. HTTP 403 with `{"error":"project_disabled"}` suspends sending for five minutes, as for healing. Methods are upper-cased; a record whose method exceeds 16 characters or whose URL exceeds 4,096 is not sent. Buffered calls are sent once when the event loop drains (`beforeExit`), for at most two seconds: a send still in flight then is aborted and the rest is dropped, so a script never hangs on an unreachable server. A process killed first can lose them.
|
|
75
|
+
|
|
76
|
+
On a serverless platform (`VERCEL`, `AWS_LAMBDA_FUNCTION_NAME` or `K_SERVICE` set), the function freezes as soon as it responds, so neither the interval nor the exit flush would run. There, each call is sent as soon as it is recorded, still at most once per second and one send at a time, and the send and every outcome report are handed to the platform's `waitUntil` (Vercel's request context, or Next.js's own), so the function stays alive until they are delivered. Where no `waitUntil` exists, the send still starts during the invocation.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Manifest for Node.js
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**The API resilience layer for your Node.js apps.**
|
|
8
8
|
|
|
9
9
|
[](https://github.com/mnfst/manifest-node/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/manifest)
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
## What is Manifest
|
|
18
18
|
|
|
19
|
-
Manifest is
|
|
19
|
+
Manifest is the API resilience layer for your apps and agents. It works with every API they call: external services, your internal APIs and MCP tools.
|
|
20
20
|
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* 🗺️ **See every API your app depends on**, and how reliable each one is.
|
|
22
|
+
* 🎯 **Repair failed API requests on the fly**, so your app keeps working.
|
|
23
|
+
* 🛠️ **Know what to fix in your code**, with a prompt for your coding agent.
|
|
24
24
|
|
|
25
25
|
## How it works
|
|
26
26
|
|
|
@@ -46,47 +46,73 @@ The prompt adds the one-line install to your entry file and stops to let you pas
|
|
|
46
46
|
|
|
47
47
|
### Start with code
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
npm install manifest
|
|
51
|
-
```
|
|
49
|
+
1. Create a project in your [Manifest dashboard](https://dashboard.manifest.build) and copy its project key.
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
import { manifest } from 'manifest';
|
|
51
|
+
2. Install the SDK:
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
53
|
+
```sh
|
|
54
|
+
npm install manifest
|
|
55
|
+
```
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
3. Call `manifest()` from the first import of the file that starts your app, before any client is constructed:
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
```js
|
|
60
|
+
import { manifest } from 'manifest';
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
manifest(); // Once, at startup.
|
|
63
|
+
// Keep making your API calls as usual.
|
|
64
|
+
```
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
TypeScript, ESM and CommonJS. Zero dependencies.
|
|
67
|
+
|
|
68
|
+
4. Set your key in the environment of your app:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
export MNFST_KEY='your-project-key'
|
|
72
|
+
```
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
5. Restart your app, then check the install from your project directory:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npx manifest doctor
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It resolves the installed SDK version, masks and validates the key, checks that Manifest loads before your app, and prints the runtime coverage.
|
|
81
|
+
|
|
82
|
+
Some clients keep the `fetch` they saw when they were built, and a client built at import time runs before your call. Where that happens, or where the start command is not yours to change, preload the SDK instead:
|
|
72
83
|
|
|
73
84
|
```sh
|
|
74
85
|
node -r manifest/register app.js
|
|
75
86
|
```
|
|
76
87
|
|
|
77
|
-
|
|
88
|
+
### n8n
|
|
78
89
|
|
|
79
|
-
|
|
90
|
+
Manifest runs in a self-hosted n8n with Docker Compose, through this SDK. n8n Cloud cannot load it. Run the steps in this order:
|
|
80
91
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```
|
|
92
|
+
1. Install the SDK from the folder of your `docker-compose.yml`:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
docker compose exec n8n npm install --prefix /home/node/.n8n/manifest manifest
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
2. Add these two lines to the `environment` of your n8n service, and of every worker service in queue mode:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
MNFST_KEY: your-project-key
|
|
102
|
+
NODE_OPTIONS: --require /home/node/.n8n/manifest/node_modules/manifest/dist/register.cjs
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
3. Restart n8n:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
docker compose up -d
|
|
109
|
+
```
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
n8n does not start when `NODE_OPTIONS` names a file that is not installed yet, so keep this order.
|
|
86
112
|
|
|
87
113
|
## Try it
|
|
88
114
|
|
|
89
|
-
Send a request that
|
|
115
|
+
Send a request that fails with a 4xx error, such as a value the API rejects:
|
|
90
116
|
|
|
91
117
|
```js
|
|
92
118
|
import { manifest } from 'manifest';
|
|
@@ -100,13 +126,13 @@ manifest({
|
|
|
100
126
|
const res = await fetch('https://api.example.com/orders', {
|
|
101
127
|
method: 'POST',
|
|
102
128
|
headers: { 'content-type': 'application/json' },
|
|
103
|
-
body: JSON.stringify({ limit: 500 }), //
|
|
129
|
+
body: JSON.stringify({ limit: 500 }), // rejected by the API
|
|
104
130
|
});
|
|
105
|
-
console.log(res.status);
|
|
131
|
+
console.log(res.status);
|
|
106
132
|
```
|
|
107
133
|
|
|
108
|
-
|
|
134
|
+
The failed request appears in your [Manifest dashboard](https://dashboard.manifest.build), grouped with others like it in an issue. Once Manifest has a patch for that error, the next request that fails the same way is repaired and retried: `onHeal` reports `patched` or `unverified` with the retry's status code, and your app receives the answer to the retry.
|
|
109
135
|
|
|
110
136
|
## More
|
|
111
137
|
|
|
112
|
-
[Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Python SDK](https://github.com/mnfst/manifest-python) · [Website](https://manifest.build)
|
|
138
|
+
[Documentation](https://docs.manifest.build) · [Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Python SDK](https://github.com/mnfst/manifest-python) · [PHP SDK](https://github.com/mnfst/manifest-php) · [Website](https://manifest.build)
|
package/dist/bin.cjs
CHANGED
|
@@ -33,7 +33,7 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
33
33
|
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
34
|
|
|
35
35
|
// src/api.ts
|
|
36
|
-
var VERSION = "7.2.
|
|
36
|
+
var VERSION = "7.2.1";
|
|
37
37
|
|
|
38
38
|
// src/cli.ts
|
|
39
39
|
var DEFAULT_URL = "https://api.manifest.build";
|
package/dist/bin.js
CHANGED
|
@@ -293,8 +293,28 @@ async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAP
|
|
|
293
293
|
return { response, body: errorBody(captured.bytes), truncated: !captured.complete || Buffer.byteLength(Buffer.from(captured.bytes).toString("utf8")) > limit, complete: captured.complete };
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// src/serverless.ts
|
|
297
|
+
var CONTEXTS = [/* @__PURE__ */ Symbol.for("@vercel/request-context"), /* @__PURE__ */ Symbol.for("@next/request-context")];
|
|
298
|
+
function isServerless(env = process.env) {
|
|
299
|
+
return Boolean(env.VERCEL || env.AWS_LAMBDA_FUNCTION_NAME || env.K_SERVICE);
|
|
300
|
+
}
|
|
301
|
+
function keepAlive(promise) {
|
|
302
|
+
try {
|
|
303
|
+
const globals = globalThis;
|
|
304
|
+
for (const symbol of CONTEXTS) {
|
|
305
|
+
const waitUntil = globals[symbol]?.get?.()?.waitUntil;
|
|
306
|
+
if (typeof waitUntil === "function") {
|
|
307
|
+
waitUntil(promise.catch(() => {
|
|
308
|
+
}));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
296
316
|
// src/api.ts
|
|
297
|
-
var VERSION = "7.2.
|
|
317
|
+
var VERSION = "7.2.1";
|
|
298
318
|
var MAX_HEALS_IN_FLIGHT = 8;
|
|
299
319
|
var warn = (message) => process.emitWarning(message, { code: "MNFST" });
|
|
300
320
|
var HealApi = class {
|
|
@@ -452,6 +472,7 @@ var HealApi = class {
|
|
|
452
472
|
const promise = this.send(id, outcome);
|
|
453
473
|
this.pending.add(promise);
|
|
454
474
|
void promise.then(() => this.pending.delete(promise));
|
|
475
|
+
keepAlive(promise);
|
|
455
476
|
}
|
|
456
477
|
async send(id, outcome) {
|
|
457
478
|
const controller = new AbortController();
|
|
@@ -492,6 +513,8 @@ export {
|
|
|
492
513
|
REQUEST_LIMIT,
|
|
493
514
|
captureRequest,
|
|
494
515
|
captureResponse,
|
|
516
|
+
isServerless,
|
|
517
|
+
keepAlive,
|
|
495
518
|
VERSION,
|
|
496
519
|
warn,
|
|
497
520
|
HealApi
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
captureRequest,
|
|
6
6
|
captureResponse,
|
|
7
7
|
isObject,
|
|
8
|
+
isServerless,
|
|
9
|
+
keepAlive,
|
|
8
10
|
mergeBody,
|
|
9
11
|
parseRequestBody,
|
|
10
12
|
safeHeaders,
|
|
@@ -13,7 +15,7 @@ import {
|
|
|
13
15
|
trackedUrl,
|
|
14
16
|
travelingBody,
|
|
15
17
|
warn
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-OXOZQLZ3.js";
|
|
17
19
|
|
|
18
20
|
// src/http.ts
|
|
19
21
|
import http from "http";
|
|
@@ -33,6 +35,8 @@ var CallBuffer = class {
|
|
|
33
35
|
this.send = send;
|
|
34
36
|
this.flushAt = options.flushAt ?? 500;
|
|
35
37
|
this.minGapMs = options.minGapMs ?? 1e3;
|
|
38
|
+
this.immediate = options.immediate ?? false;
|
|
39
|
+
this.keepAlive = options.keepAlive ?? keepAlive;
|
|
36
40
|
this.timer = setInterval(() => void this.kick(), options.intervalMs ?? 5e3);
|
|
37
41
|
this.timer.unref();
|
|
38
42
|
}
|
|
@@ -46,10 +50,14 @@ var CallBuffer = class {
|
|
|
46
50
|
timer;
|
|
47
51
|
flushAt;
|
|
48
52
|
minGapMs;
|
|
53
|
+
immediate;
|
|
54
|
+
keepAlive;
|
|
55
|
+
draining = null;
|
|
49
56
|
record(call) {
|
|
50
57
|
if (this.queue.length >= MAX_BUFFER) return;
|
|
51
58
|
this.queue.push(call);
|
|
52
|
-
if (this.
|
|
59
|
+
if (this.immediate) this.drain();
|
|
60
|
+
else if (this.queue.length >= this.flushAt) void this.kick();
|
|
53
61
|
}
|
|
54
62
|
size() {
|
|
55
63
|
return this.queue.length;
|
|
@@ -80,6 +88,21 @@ var CallBuffer = class {
|
|
|
80
88
|
stop() {
|
|
81
89
|
clearInterval(this.timer);
|
|
82
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Serverless: the function may freeze as soon as its response is sent, before
|
|
93
|
+
* any timer fires. So send now, through `flush()` (same gap, same single send
|
|
94
|
+
* in flight), and let the platform wait for it. Calls recorded meanwhile ride
|
|
95
|
+
* the same drain; one that lands as it settles starts the next.
|
|
96
|
+
*/
|
|
97
|
+
drain() {
|
|
98
|
+
if (this.draining) return;
|
|
99
|
+
const run = this.flush().finally(() => {
|
|
100
|
+
this.draining = null;
|
|
101
|
+
if (this.queue.length > 0) this.drain();
|
|
102
|
+
});
|
|
103
|
+
this.draining = run;
|
|
104
|
+
this.keepAlive(run);
|
|
105
|
+
}
|
|
83
106
|
kick() {
|
|
84
107
|
if (this.inFlight || this.queue.length === 0) return;
|
|
85
108
|
if (performance.now() - this.lastSentAt < this.minGapMs) return;
|
|
@@ -127,7 +150,10 @@ var Runtime = class {
|
|
|
127
150
|
this.options = options;
|
|
128
151
|
this.original = original;
|
|
129
152
|
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
130
|
-
this.tracker = new CallBuffer(
|
|
153
|
+
this.tracker = new CallBuffer(
|
|
154
|
+
(batch, signal) => this.api.sendRequests(batch, signal),
|
|
155
|
+
{ immediate: isServerless() }
|
|
156
|
+
);
|
|
131
157
|
}
|
|
132
158
|
options;
|
|
133
159
|
original;
|
package/dist/index.cjs
CHANGED
|
@@ -340,8 +340,28 @@ async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAP
|
|
|
340
340
|
// src/runtime.ts
|
|
341
341
|
var import_node_crypto = require("crypto");
|
|
342
342
|
|
|
343
|
+
// src/serverless.ts
|
|
344
|
+
var CONTEXTS = [/* @__PURE__ */ Symbol.for("@vercel/request-context"), /* @__PURE__ */ Symbol.for("@next/request-context")];
|
|
345
|
+
function isServerless(env = process.env) {
|
|
346
|
+
return Boolean(env.VERCEL || env.AWS_LAMBDA_FUNCTION_NAME || env.K_SERVICE);
|
|
347
|
+
}
|
|
348
|
+
function keepAlive(promise) {
|
|
349
|
+
try {
|
|
350
|
+
const globals2 = globalThis;
|
|
351
|
+
for (const symbol of CONTEXTS) {
|
|
352
|
+
const waitUntil = globals2[symbol]?.get?.()?.waitUntil;
|
|
353
|
+
if (typeof waitUntil === "function") {
|
|
354
|
+
waitUntil(promise.catch(() => {
|
|
355
|
+
}));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
343
363
|
// src/api.ts
|
|
344
|
-
var VERSION = "7.2.
|
|
364
|
+
var VERSION = "7.2.1";
|
|
345
365
|
var MAX_HEALS_IN_FLIGHT = 8;
|
|
346
366
|
var warn = (message) => process.emitWarning(message, { code: "MNFST" });
|
|
347
367
|
var HealApi = class {
|
|
@@ -499,6 +519,7 @@ var HealApi = class {
|
|
|
499
519
|
const promise = this.send(id, outcome);
|
|
500
520
|
this.pending.add(promise);
|
|
501
521
|
void promise.then(() => this.pending.delete(promise));
|
|
522
|
+
keepAlive(promise);
|
|
502
523
|
}
|
|
503
524
|
async send(id, outcome) {
|
|
504
525
|
const controller = new AbortController();
|
|
@@ -534,6 +555,8 @@ var CallBuffer = class {
|
|
|
534
555
|
this.send = send;
|
|
535
556
|
this.flushAt = options.flushAt ?? 500;
|
|
536
557
|
this.minGapMs = options.minGapMs ?? 1e3;
|
|
558
|
+
this.immediate = options.immediate ?? false;
|
|
559
|
+
this.keepAlive = options.keepAlive ?? keepAlive;
|
|
537
560
|
this.timer = setInterval(() => void this.kick(), options.intervalMs ?? 5e3);
|
|
538
561
|
this.timer.unref();
|
|
539
562
|
}
|
|
@@ -547,10 +570,14 @@ var CallBuffer = class {
|
|
|
547
570
|
timer;
|
|
548
571
|
flushAt;
|
|
549
572
|
minGapMs;
|
|
573
|
+
immediate;
|
|
574
|
+
keepAlive;
|
|
575
|
+
draining = null;
|
|
550
576
|
record(call) {
|
|
551
577
|
if (this.queue.length >= MAX_BUFFER) return;
|
|
552
578
|
this.queue.push(call);
|
|
553
|
-
if (this.
|
|
579
|
+
if (this.immediate) this.drain();
|
|
580
|
+
else if (this.queue.length >= this.flushAt) void this.kick();
|
|
554
581
|
}
|
|
555
582
|
size() {
|
|
556
583
|
return this.queue.length;
|
|
@@ -581,6 +608,21 @@ var CallBuffer = class {
|
|
|
581
608
|
stop() {
|
|
582
609
|
clearInterval(this.timer);
|
|
583
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Serverless: the function may freeze as soon as its response is sent, before
|
|
613
|
+
* any timer fires. So send now, through `flush()` (same gap, same single send
|
|
614
|
+
* in flight), and let the platform wait for it. Calls recorded meanwhile ride
|
|
615
|
+
* the same drain; one that lands as it settles starts the next.
|
|
616
|
+
*/
|
|
617
|
+
drain() {
|
|
618
|
+
if (this.draining) return;
|
|
619
|
+
const run = this.flush().finally(() => {
|
|
620
|
+
this.draining = null;
|
|
621
|
+
if (this.queue.length > 0) this.drain();
|
|
622
|
+
});
|
|
623
|
+
this.draining = run;
|
|
624
|
+
this.keepAlive(run);
|
|
625
|
+
}
|
|
584
626
|
kick() {
|
|
585
627
|
if (this.inFlight || this.queue.length === 0) return;
|
|
586
628
|
if (performance.now() - this.lastSentAt < this.minGapMs) return;
|
|
@@ -628,7 +670,10 @@ var Runtime = class {
|
|
|
628
670
|
this.options = options;
|
|
629
671
|
this.original = original;
|
|
630
672
|
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
631
|
-
this.tracker = new CallBuffer(
|
|
673
|
+
this.tracker = new CallBuffer(
|
|
674
|
+
(batch, signal) => this.api.sendRequests(batch, signal),
|
|
675
|
+
{ immediate: isServerless() }
|
|
676
|
+
);
|
|
632
677
|
}
|
|
633
678
|
options;
|
|
634
679
|
original;
|
package/dist/index.d.cts
CHANGED
|
@@ -12,7 +12,7 @@ interface HealEvent {
|
|
|
12
12
|
operations?: unknown[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
declare const VERSION = "7.2.
|
|
15
|
+
declare const VERSION = "7.2.1";
|
|
16
16
|
|
|
17
17
|
/** Install once, before libraries capture their own reference to global fetch. */
|
|
18
18
|
declare function manifest(options?: ManifestOptions): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ interface HealEvent {
|
|
|
12
12
|
operations?: unknown[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
declare const VERSION = "7.2.
|
|
15
|
+
declare const VERSION = "7.2.1";
|
|
16
16
|
|
|
17
17
|
/** Install once, before libraries capture their own reference to global fetch. */
|
|
18
18
|
declare function manifest(options?: ManifestOptions): void;
|
package/dist/index.js
CHANGED
package/dist/register.cjs
CHANGED
|
@@ -327,8 +327,28 @@ async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAP
|
|
|
327
327
|
// src/runtime.ts
|
|
328
328
|
var import_node_crypto = require("crypto");
|
|
329
329
|
|
|
330
|
+
// src/serverless.ts
|
|
331
|
+
var CONTEXTS = [/* @__PURE__ */ Symbol.for("@vercel/request-context"), /* @__PURE__ */ Symbol.for("@next/request-context")];
|
|
332
|
+
function isServerless(env = process.env) {
|
|
333
|
+
return Boolean(env.VERCEL || env.AWS_LAMBDA_FUNCTION_NAME || env.K_SERVICE);
|
|
334
|
+
}
|
|
335
|
+
function keepAlive(promise) {
|
|
336
|
+
try {
|
|
337
|
+
const globals2 = globalThis;
|
|
338
|
+
for (const symbol of CONTEXTS) {
|
|
339
|
+
const waitUntil = globals2[symbol]?.get?.()?.waitUntil;
|
|
340
|
+
if (typeof waitUntil === "function") {
|
|
341
|
+
waitUntil(promise.catch(() => {
|
|
342
|
+
}));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
330
350
|
// src/api.ts
|
|
331
|
-
var VERSION = "7.2.
|
|
351
|
+
var VERSION = "7.2.1";
|
|
332
352
|
var MAX_HEALS_IN_FLIGHT = 8;
|
|
333
353
|
var warn = (message) => process.emitWarning(message, { code: "MNFST" });
|
|
334
354
|
var HealApi = class {
|
|
@@ -486,6 +506,7 @@ var HealApi = class {
|
|
|
486
506
|
const promise = this.send(id, outcome);
|
|
487
507
|
this.pending.add(promise);
|
|
488
508
|
void promise.then(() => this.pending.delete(promise));
|
|
509
|
+
keepAlive(promise);
|
|
489
510
|
}
|
|
490
511
|
async send(id, outcome) {
|
|
491
512
|
const controller = new AbortController();
|
|
@@ -521,6 +542,8 @@ var CallBuffer = class {
|
|
|
521
542
|
this.send = send;
|
|
522
543
|
this.flushAt = options.flushAt ?? 500;
|
|
523
544
|
this.minGapMs = options.minGapMs ?? 1e3;
|
|
545
|
+
this.immediate = options.immediate ?? false;
|
|
546
|
+
this.keepAlive = options.keepAlive ?? keepAlive;
|
|
524
547
|
this.timer = setInterval(() => void this.kick(), options.intervalMs ?? 5e3);
|
|
525
548
|
this.timer.unref();
|
|
526
549
|
}
|
|
@@ -534,10 +557,14 @@ var CallBuffer = class {
|
|
|
534
557
|
timer;
|
|
535
558
|
flushAt;
|
|
536
559
|
minGapMs;
|
|
560
|
+
immediate;
|
|
561
|
+
keepAlive;
|
|
562
|
+
draining = null;
|
|
537
563
|
record(call) {
|
|
538
564
|
if (this.queue.length >= MAX_BUFFER) return;
|
|
539
565
|
this.queue.push(call);
|
|
540
|
-
if (this.
|
|
566
|
+
if (this.immediate) this.drain();
|
|
567
|
+
else if (this.queue.length >= this.flushAt) void this.kick();
|
|
541
568
|
}
|
|
542
569
|
size() {
|
|
543
570
|
return this.queue.length;
|
|
@@ -568,6 +595,21 @@ var CallBuffer = class {
|
|
|
568
595
|
stop() {
|
|
569
596
|
clearInterval(this.timer);
|
|
570
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Serverless: the function may freeze as soon as its response is sent, before
|
|
600
|
+
* any timer fires. So send now, through `flush()` (same gap, same single send
|
|
601
|
+
* in flight), and let the platform wait for it. Calls recorded meanwhile ride
|
|
602
|
+
* the same drain; one that lands as it settles starts the next.
|
|
603
|
+
*/
|
|
604
|
+
drain() {
|
|
605
|
+
if (this.draining) return;
|
|
606
|
+
const run = this.flush().finally(() => {
|
|
607
|
+
this.draining = null;
|
|
608
|
+
if (this.queue.length > 0) this.drain();
|
|
609
|
+
});
|
|
610
|
+
this.draining = run;
|
|
611
|
+
this.keepAlive(run);
|
|
612
|
+
}
|
|
571
613
|
kick() {
|
|
572
614
|
if (this.inFlight || this.queue.length === 0) return;
|
|
573
615
|
if (performance.now() - this.lastSentAt < this.minGapMs) return;
|
|
@@ -615,7 +657,10 @@ var Runtime = class {
|
|
|
615
657
|
this.options = options;
|
|
616
658
|
this.original = original;
|
|
617
659
|
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
618
|
-
this.tracker = new CallBuffer(
|
|
660
|
+
this.tracker = new CallBuffer(
|
|
661
|
+
(batch, signal) => this.api.sendRequests(batch, signal),
|
|
662
|
+
{ immediate: isServerless() }
|
|
663
|
+
);
|
|
619
664
|
}
|
|
620
665
|
options;
|
|
621
666
|
original;
|
package/dist/register.js
CHANGED
package/docs/guide.md
CHANGED
|
@@ -124,7 +124,7 @@ Outcome reports are best effort, limited to 64 concurrent requests with five-sec
|
|
|
124
124
|
|
|
125
125
|
## Data sent to Manifest
|
|
126
126
|
|
|
127
|
-
**Every call (metadata only).** For each call that is not healed, whatever its status, the SDK sends its method, URL without the query string, userinfo or fragment, status code, response time and time of the call. No headers and no bodies. Calls are batched and sent in the background, at most once per second; recording one never slows the call.
|
|
127
|
+
**Every call (metadata only).** For each call that is not healed, whatever its status, the SDK sends its method, URL without the query string, userinfo or fragment, status code, response time and time of the call. No headers and no bodies. Calls are batched and sent in the background, at most once per second; recording one never slows the call. On Vercel, AWS Lambda and Cloud Run, each call is sent as soon as it is recorded, and the function is kept alive until it is delivered.
|
|
128
128
|
|
|
129
129
|
**Healable failures (full capture).** Failed URLs, request headers, JSON or form-urlencoded bodies, and raw error responses go to the configured server. Known credential names in query parameters and headers are masked; credential-named top-level request body fields are withheld and restored on retry. Exception prose is not sent for transport failures.
|
|
130
130
|
|