@platformos/platformos-mcp-supervisor 0.0.2 → 0.2.0
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/README.md +105 -0
- package/dist/adapter-input.d.ts +113 -0
- package/dist/adapter-input.js +210 -0
- package/dist/adapter-input.spec.d.ts +1 -0
- package/dist/adapter-input.spec.js +275 -0
- package/dist/bin/args.d.ts +15 -2
- package/dist/bin/args.js +31 -3
- package/dist/bin/args.spec.d.ts +1 -0
- package/dist/bin/args.spec.js +35 -0
- package/dist/bin/platformos-mcp-supervisor.js +6 -2
- package/dist/check-docs.d.ts +21 -0
- package/dist/check-docs.js +38 -0
- package/dist/check-docs.spec.d.ts +1 -0
- package/dist/check-docs.spec.js +87 -0
- package/dist/context.d.ts +34 -0
- package/dist/context.js +12 -0
- package/dist/cost-model.d.ts +188 -0
- package/dist/cost-model.js +197 -0
- package/dist/cost-model.spec.d.ts +1 -0
- package/dist/cost-model.spec.js +91 -0
- package/dist/deadline.d.ts +27 -0
- package/dist/deadline.js +40 -0
- package/dist/deadline.spec.d.ts +1 -0
- package/dist/deadline.spec.js +83 -0
- package/dist/enrich/enrich.d.ts +105 -0
- package/dist/enrich/enrich.js +136 -0
- package/dist/enrich/enrich.spec.d.ts +1 -0
- package/dist/enrich/enrich.spec.js +184 -0
- package/dist/enrich/symbol.d.ts +16 -0
- package/dist/enrich/symbol.js +58 -0
- package/dist/enrich/symbol.spec.d.ts +1 -0
- package/dist/enrich/symbol.spec.js +89 -0
- package/dist/impact/dependants.d.ts +75 -0
- package/dist/impact/dependants.js +104 -0
- package/dist/impact/diff.d.ts +27 -0
- package/dist/impact/diff.js +33 -0
- package/dist/impact/impact.d.ts +21 -0
- package/dist/impact/impact.js +160 -0
- package/dist/impact/impact.spec.d.ts +1 -0
- package/dist/impact/impact.spec.js +357 -0
- package/dist/impact/project-scan.d.ts +39 -0
- package/dist/impact/project-scan.js +62 -0
- package/dist/impact/project-scan.spec.d.ts +1 -0
- package/dist/impact/project-scan.spec.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/lint/fixes.d.ts +28 -0
- package/dist/lint/fixes.js +30 -0
- package/dist/lint/lint-batch.d.ts +100 -0
- package/dist/lint/lint-batch.js +98 -0
- package/dist/lint/lint-batch.spec.d.ts +1 -0
- package/dist/lint/lint-batch.spec.js +503 -0
- package/dist/lint/lint.d.ts +11 -1
- package/dist/lint/lint.js +9 -3
- package/dist/result/assemble.d.ts +11 -9
- package/dist/result/assemble.js +63 -16
- package/dist/result/assemble.spec.d.ts +1 -0
- package/dist/result/assemble.spec.js +319 -0
- package/dist/result/blocking.d.ts +92 -0
- package/dist/result/blocking.js +116 -0
- package/dist/result/blocking.spec.d.ts +1 -0
- package/dist/result/blocking.spec.js +201 -0
- package/dist/result/impact-states.d.ts +19 -0
- package/dist/result/impact-states.js +10 -0
- package/dist/result/response-budget.d.ts +9 -0
- package/dist/result/response-budget.js +133 -0
- package/dist/result/response-budget.spec.d.ts +1 -0
- package/dist/result/response-budget.spec.js +222 -0
- package/dist/result/types.d.ts +212 -104
- package/dist/result/types.js +4 -13
- package/dist/transport/instructions-coverage.spec.d.ts +1 -0
- package/dist/transport/instructions-coverage.spec.js +176 -0
- package/dist/transport/instructions.d.ts +1 -0
- package/dist/transport/instructions.js +150 -0
- package/dist/transport/process-guards.d.ts +43 -0
- package/dist/transport/process-guards.js +63 -0
- package/dist/transport/process-guards.spec.d.ts +1 -0
- package/dist/transport/process-guards.spec.js +200 -0
- package/dist/transport/server.d.ts +6 -1
- package/dist/transport/server.js +21 -10
- package/dist/transport/validate-code.d.ts +50 -15
- package/dist/transport/validate-code.js +238 -33
- package/dist/transport/validate-code.spec.d.ts +1 -0
- package/dist/transport/validate-code.spec.js +2303 -0
- package/dist/validate/batch-bounds.d.ts +34 -0
- package/dist/validate/batch-bounds.js +65 -0
- package/dist/validate/batch-coherence.d.ts +21 -0
- package/dist/validate/batch-coherence.js +54 -0
- package/dist/validate/batch-coherence.spec.d.ts +1 -0
- package/dist/validate/batch-coherence.spec.js +111 -0
- package/dist/validate/validate-buffers.d.ts +27 -0
- package/dist/validate/validate-buffers.js +319 -0
- package/package.json +8 -7
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a lint COSTS — and therefore how much work one request may admit, and how long the
|
|
3
|
+
* server waits for it.
|
|
4
|
+
*
|
|
5
|
+
* The byte caps and the deadline are ONE relationship, written here as arithmetic and
|
|
6
|
+
* derived rather than chosen. Adding per-node work moves {@link LINT_MS_PER_KIB} and every
|
|
7
|
+
* dependent bound follows; `cost-model.spec.ts` fails if any of them stops fitting. A batch
|
|
8
|
+
* admitted past its deadline returns `timed_out` for EVERY file in the request, so the two
|
|
9
|
+
* drifting apart fails quietly and in the expensive direction.
|
|
10
|
+
*
|
|
11
|
+
* THE DEADLINE SCALES WITH THE REQUEST because a fixed one and a meaningful batch cap
|
|
12
|
+
* cannot both exist: hold it at 60 s and the largest admissible batch is ~133 KiB, smaller
|
|
13
|
+
* than a single legal buffer. Scaling costs little — this deadline is a backstop for ASYNC
|
|
14
|
+
* stalls (see `deadline.ts`) rather than a CPU bound.
|
|
15
|
+
*
|
|
16
|
+
* Every number here is MEASURED, and the measurement is named. A plausible constant is how
|
|
17
|
+
* this file rots back into unrelated magic numbers.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Wall-clock cost of linting one KiB of buffer, on an idle machine, warm cache.
|
|
21
|
+
*
|
|
22
|
+
* Measured end to end (`runValidateCode`, the whole request path), not against the parse
|
|
23
|
+
* alone. Reproduce with `scripts/measure-lint-cost.mjs`, which builds a synthetic project
|
|
24
|
+
* and prints the rate for a single buffer and for batch shapes; the slowest observed was
|
|
25
|
+
* ~51 ms/KiB, on CLEAN markup rather than the diagnostic-dense shape.
|
|
26
|
+
*
|
|
27
|
+
* DO NOT LOWER THIS TO MATCH A FASTER MEASUREMENT. The rate varies by several times with
|
|
28
|
+
* the project and the markup, so the constant is deliberately set above the slowest
|
|
29
|
+
* observed value. Being wrong FAST costs a longer wait before a stall is declared; being
|
|
30
|
+
* wrong SLOW admits a request that cannot finish and returns `timed_out` for every file.
|
|
31
|
+
*/
|
|
32
|
+
export declare const LINT_MS_PER_KIB = 75;
|
|
33
|
+
/**
|
|
34
|
+
* How much slower the same lint runs on a contended machine than on an idle one.
|
|
35
|
+
*
|
|
36
|
+
* Measured at ~2.3x (128 KiB: ~10 s idle, ~23 s under load), held at 3 because it bounds
|
|
37
|
+
* someone else's machine. Being wrong low produces a false `timed_out` — a silent
|
|
38
|
+
* no-answer — while being wrong high costs only a longer wait before declaring a stall.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LOAD_FACTOR = 3;
|
|
41
|
+
/**
|
|
42
|
+
* How far beyond the expected loaded worst case the deadline sits. The deadline must never
|
|
43
|
+
* fire on work that was legal to admit; it exists to catch a stall.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEADLINE_MARGIN = 2;
|
|
46
|
+
/** Deadline granted per KiB of admitted work: idle cost, under load, with margin. */
|
|
47
|
+
export declare const DEADLINE_MS_PER_KIB: number;
|
|
48
|
+
/**
|
|
49
|
+
* Deadline floor, applied to every request however small.
|
|
50
|
+
*
|
|
51
|
+
* Small requests are not dominated by their buffers: a cold first call also builds the app,
|
|
52
|
+
* loads the config and reconciles the graph (~0.8 s idle, ~1.6 s loaded on a 162-file
|
|
53
|
+
* project). The floor covers that fixed cost with room to spare.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MIN_LINT_DEADLINE_MS = 60000;
|
|
56
|
+
/**
|
|
57
|
+
* Deadline ceiling, and therefore the origin of {@link maxBytesWithin}'s answer for the
|
|
58
|
+
* batch cap.
|
|
59
|
+
*
|
|
60
|
+
* POLICY rather than measurement: how long the server holds a request open before
|
|
61
|
+
* concluding it has stalled. Two minutes is past the point where an agent has any use for
|
|
62
|
+
* the answer, and every extra second of ceiling buys a larger admissible batch.
|
|
63
|
+
*/
|
|
64
|
+
export declare const MAX_LINT_DEADLINE_MS = 120000;
|
|
65
|
+
/**
|
|
66
|
+
* How much of the AGENT'S CONTEXT one `validate_code` answer may spend.
|
|
67
|
+
*
|
|
68
|
+
* POLICY, like {@link MAX_LINT_DEADLINE_MS}, and the one bound here that is not about the
|
|
69
|
+
* server's own work. Unbounded, a legal call was measured returning ~336 000 tokens — more
|
|
70
|
+
* than most context windows hold, with nothing in the payload saying so.
|
|
71
|
+
*
|
|
72
|
+
* At ~153 bytes per diagnostic this budget holds roughly 200 findings. Measured real calls
|
|
73
|
+
* cost tens to hundreds of tokens, so the cap cannot touch them; what it defends against is
|
|
74
|
+
* the tail — a generated file, or one cascading syntax error in a large partial.
|
|
75
|
+
*/
|
|
76
|
+
export declare const RESPONSE_TOKEN_BUDGET = 8000;
|
|
77
|
+
/**
|
|
78
|
+
* Bytes per token, for converting the budget above into something countable.
|
|
79
|
+
*
|
|
80
|
+
* A crude ESTIMATE — this JSON is mostly ASCII prose, where ~4 bytes/token is the usual
|
|
81
|
+
* rule of thumb, and the real tokenizer is the client's. Being wrong by 25% moves the cap
|
|
82
|
+
* between ~150 and ~250 diagnostics, which changes nothing about which calls it affects.
|
|
83
|
+
*/
|
|
84
|
+
export declare const BYTES_PER_TOKEN = 4;
|
|
85
|
+
/**
|
|
86
|
+
* The bound actually enforced: serialized bytes of diagnostics one response may
|
|
87
|
+
* carry, across the WHOLE request rather than per file.
|
|
88
|
+
*
|
|
89
|
+
* Per-request because per-file alone leaves the worst case multiplied by the file
|
|
90
|
+
* count — 50 files each just under a per-file cap is 50x the intended bound, and the
|
|
91
|
+
* batch form exists precisely so agents send many files at once.
|
|
92
|
+
*/
|
|
93
|
+
export declare const MAX_RESPONSE_DIAGNOSTIC_BYTES: number;
|
|
94
|
+
/**
|
|
95
|
+
* What one file costs in a response BEFORE any diagnostics: its status, gate, `impact`
|
|
96
|
+
* object, empty lists, and — when findings were withheld — the truncation note.
|
|
97
|
+
*
|
|
98
|
+
* Measured, not assumed: a 50-file batch of clean files serializes to 234 bytes per file,
|
|
99
|
+
* and the note adds ~150. Held at 512 because this multiplies by the file count.
|
|
100
|
+
*/
|
|
101
|
+
export declare const RESPONSE_ENVELOPE_BYTES_PER_FILE = 512;
|
|
102
|
+
/**
|
|
103
|
+
* The whole response bound, stated as arithmetic rather than enforced directly.
|
|
104
|
+
*
|
|
105
|
+
* The allocator bounds DIAGNOSTICS, the only unbounded dimension; the envelope is O(files)
|
|
106
|
+
* and already bounded by the batch file cap. `transport/validate-code.spec.ts` measures the
|
|
107
|
+
* worst legal request against this.
|
|
108
|
+
*
|
|
109
|
+
* Takes the file count rather than importing `MAX_BATCH_FILES`, which lives in
|
|
110
|
+
* `validate/batch-bounds.ts` and already imports this module.
|
|
111
|
+
*/
|
|
112
|
+
export declare function maxResponseBytes(fileCount: number): number;
|
|
113
|
+
/**
|
|
114
|
+
* How long to wait for a lint that was handed `bytes` of buffer.
|
|
115
|
+
*
|
|
116
|
+
* Never below {@link MIN_LINT_DEADLINE_MS}. Callers do not clamp to
|
|
117
|
+
* {@link MAX_LINT_DEADLINE_MS} — the byte caps do that upstream by refusing the request
|
|
118
|
+
* outright, so the ceiling is enforced where the work is admitted, not where it is timed.
|
|
119
|
+
*/
|
|
120
|
+
export declare function lintDeadlineMs(bytes: number): number;
|
|
121
|
+
/**
|
|
122
|
+
* The most bytes whose {@link lintDeadlineMs} still fits inside `deadlineMs`.
|
|
123
|
+
*
|
|
124
|
+
* The inverse of the function above, floored to a whole KiB so the answer is exact at the
|
|
125
|
+
* boundary — one byte more and the deadline exceeds `deadlineMs`. Used to derive the batch
|
|
126
|
+
* cap from the deadline ceiling instead of from a round multiple of the per-buffer bound.
|
|
127
|
+
*/
|
|
128
|
+
export declare function maxBytesWithin(deadlineMs: number): number;
|
|
129
|
+
/**
|
|
130
|
+
* How many DEPENDANTS one request will lint when computing impact.
|
|
131
|
+
*
|
|
132
|
+
* THE REAL BOUND ON IMPACT, and not a nicety. `IMPACT_DEADLINE_MS` cannot bound this work:
|
|
133
|
+
* a lint is synchronous CPU on the one event loop, so no timer preempts it (see
|
|
134
|
+
* `deadline.ts`). Bounding the INPUT is the only defence, exactly as `MAX_BUFFER_BYTES` is
|
|
135
|
+
* for the primary lint.
|
|
136
|
+
*
|
|
137
|
+
* DERIVED from the deadline it has to fit inside, on the worst project scale:
|
|
138
|
+
*
|
|
139
|
+
* impact deadline 2 000 ms
|
|
140
|
+
* less the project read at 10k files ~ 850 ms
|
|
141
|
+
* leaves for both lint passes ~1 150 ms
|
|
142
|
+
*
|
|
143
|
+
* Measured twice-lint cost, and the same {@link LOAD_FACTOR} the lint deadline uses:
|
|
144
|
+
*
|
|
145
|
+
* 100 dependants 258 ms idle 774 ms loaded -> 1 624 ms total, FITS
|
|
146
|
+
* 200 dependants 487 ms idle 1 460 ms loaded -> over
|
|
147
|
+
* 311 dependants 716 ms idle 2 147 ms loaded -> over on its own
|
|
148
|
+
*
|
|
149
|
+
* 311 is not hypothetical: it is the most-depended-on file measured on a real 2 615-file
|
|
150
|
+
* application (`lib/current_profile`). The same measurement puts 99.37% of targets at 100
|
|
151
|
+
* dependants or fewer — p50 1, p90 4, p99 59 — so the bound is hit rarely and, when it is,
|
|
152
|
+
* IT IS REPORTED (`unchecked_dependants`) rather than silently shortening the analysis.
|
|
153
|
+
*/
|
|
154
|
+
export declare const MAX_DEPENDANTS_LINTED = 100;
|
|
155
|
+
/**
|
|
156
|
+
* Cost of DISCOVERING dependants, per KiB of candidate text.
|
|
157
|
+
*
|
|
158
|
+
* Discovery parses every file whose text contains the target's name and resolves its
|
|
159
|
+
* references through the graph, so it scales with the BYTES it must parse rather than with
|
|
160
|
+
* the file count. Measured on a real 2 615-file application across four very differently
|
|
161
|
+
* shaped targets: 12.82, 12.52, 8.59 and 14.60 ms/KiB. Held above the slowest observed, for
|
|
162
|
+
* the same reason {@link LINT_MS_PER_KIB} is.
|
|
163
|
+
*/
|
|
164
|
+
export declare const DISCOVERY_MS_PER_KIB = 15;
|
|
165
|
+
/**
|
|
166
|
+
* How much candidate text one target's discovery will parse before giving up.
|
|
167
|
+
*
|
|
168
|
+
* WHY A BOUND AT ALL, and why it is not {@link MAX_DEPENDANTS_LINTED}: that one bounds the
|
|
169
|
+
* two lint passes, which happen AFTER discovery. Discovery itself was unbounded, and the
|
|
170
|
+
* most-referenced file on a real application (`lib/current_profile`, 312 candidates) cost
|
|
171
|
+
* 4.7 s and then returned `unavailable` anyway — seconds spent to reach "I do not know".
|
|
172
|
+
* The candidate byte total is known from a substring scan, before a single parse, so the
|
|
173
|
+
* same answer costs milliseconds instead.
|
|
174
|
+
*
|
|
175
|
+
* DERIVED from what has to fit inside `IMPACT_DEADLINE_MS`:
|
|
176
|
+
*
|
|
177
|
+
* impact deadline 2 000 ms
|
|
178
|
+
* less the project read ~ 235 ms
|
|
179
|
+
* less both lint passes at the dependant bound ~ 258 ms
|
|
180
|
+
* leaves for discovery ~1 507 ms
|
|
181
|
+
* at {@link DISCOVERY_MS_PER_KIB} ~100 KiB
|
|
182
|
+
*
|
|
183
|
+
* Held at 64 KiB rather than 100, because 100 spends the entire remaining budget and leaves
|
|
184
|
+
* nothing for a larger project's read. Measured against the same application: candidate text
|
|
185
|
+
* is p50 1 KiB, p90 13, p95 23, p99 143 — so 64 KiB is nearly 3x the p95 and turns away
|
|
186
|
+
* 2.51% of targets, each in a few milliseconds instead of several seconds.
|
|
187
|
+
*/
|
|
188
|
+
export declare const MAX_CANDIDATE_BYTES: number;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a lint COSTS — and therefore how much work one request may admit, and how long the
|
|
3
|
+
* server waits for it.
|
|
4
|
+
*
|
|
5
|
+
* The byte caps and the deadline are ONE relationship, written here as arithmetic and
|
|
6
|
+
* derived rather than chosen. Adding per-node work moves {@link LINT_MS_PER_KIB} and every
|
|
7
|
+
* dependent bound follows; `cost-model.spec.ts` fails if any of them stops fitting. A batch
|
|
8
|
+
* admitted past its deadline returns `timed_out` for EVERY file in the request, so the two
|
|
9
|
+
* drifting apart fails quietly and in the expensive direction.
|
|
10
|
+
*
|
|
11
|
+
* THE DEADLINE SCALES WITH THE REQUEST because a fixed one and a meaningful batch cap
|
|
12
|
+
* cannot both exist: hold it at 60 s and the largest admissible batch is ~133 KiB, smaller
|
|
13
|
+
* than a single legal buffer. Scaling costs little — this deadline is a backstop for ASYNC
|
|
14
|
+
* stalls (see `deadline.ts`) rather than a CPU bound.
|
|
15
|
+
*
|
|
16
|
+
* Every number here is MEASURED, and the measurement is named. A plausible constant is how
|
|
17
|
+
* this file rots back into unrelated magic numbers.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Wall-clock cost of linting one KiB of buffer, on an idle machine, warm cache.
|
|
21
|
+
*
|
|
22
|
+
* Measured end to end (`runValidateCode`, the whole request path), not against the parse
|
|
23
|
+
* alone. Reproduce with `scripts/measure-lint-cost.mjs`, which builds a synthetic project
|
|
24
|
+
* and prints the rate for a single buffer and for batch shapes; the slowest observed was
|
|
25
|
+
* ~51 ms/KiB, on CLEAN markup rather than the diagnostic-dense shape.
|
|
26
|
+
*
|
|
27
|
+
* DO NOT LOWER THIS TO MATCH A FASTER MEASUREMENT. The rate varies by several times with
|
|
28
|
+
* the project and the markup, so the constant is deliberately set above the slowest
|
|
29
|
+
* observed value. Being wrong FAST costs a longer wait before a stall is declared; being
|
|
30
|
+
* wrong SLOW admits a request that cannot finish and returns `timed_out` for every file.
|
|
31
|
+
*/
|
|
32
|
+
export const LINT_MS_PER_KIB = 75;
|
|
33
|
+
/**
|
|
34
|
+
* How much slower the same lint runs on a contended machine than on an idle one.
|
|
35
|
+
*
|
|
36
|
+
* Measured at ~2.3x (128 KiB: ~10 s idle, ~23 s under load), held at 3 because it bounds
|
|
37
|
+
* someone else's machine. Being wrong low produces a false `timed_out` — a silent
|
|
38
|
+
* no-answer — while being wrong high costs only a longer wait before declaring a stall.
|
|
39
|
+
*/
|
|
40
|
+
export const LOAD_FACTOR = 3;
|
|
41
|
+
/**
|
|
42
|
+
* How far beyond the expected loaded worst case the deadline sits. The deadline must never
|
|
43
|
+
* fire on work that was legal to admit; it exists to catch a stall.
|
|
44
|
+
*/
|
|
45
|
+
export const DEADLINE_MARGIN = 2;
|
|
46
|
+
/** Deadline granted per KiB of admitted work: idle cost, under load, with margin. */
|
|
47
|
+
export const DEADLINE_MS_PER_KIB = LINT_MS_PER_KIB * LOAD_FACTOR * DEADLINE_MARGIN;
|
|
48
|
+
/**
|
|
49
|
+
* Deadline floor, applied to every request however small.
|
|
50
|
+
*
|
|
51
|
+
* Small requests are not dominated by their buffers: a cold first call also builds the app,
|
|
52
|
+
* loads the config and reconciles the graph (~0.8 s idle, ~1.6 s loaded on a 162-file
|
|
53
|
+
* project). The floor covers that fixed cost with room to spare.
|
|
54
|
+
*/
|
|
55
|
+
export const MIN_LINT_DEADLINE_MS = 60_000;
|
|
56
|
+
/**
|
|
57
|
+
* Deadline ceiling, and therefore the origin of {@link maxBytesWithin}'s answer for the
|
|
58
|
+
* batch cap.
|
|
59
|
+
*
|
|
60
|
+
* POLICY rather than measurement: how long the server holds a request open before
|
|
61
|
+
* concluding it has stalled. Two minutes is past the point where an agent has any use for
|
|
62
|
+
* the answer, and every extra second of ceiling buys a larger admissible batch.
|
|
63
|
+
*/
|
|
64
|
+
export const MAX_LINT_DEADLINE_MS = 120_000;
|
|
65
|
+
/**
|
|
66
|
+
* How much of the AGENT'S CONTEXT one `validate_code` answer may spend.
|
|
67
|
+
*
|
|
68
|
+
* POLICY, like {@link MAX_LINT_DEADLINE_MS}, and the one bound here that is not about the
|
|
69
|
+
* server's own work. Unbounded, a legal call was measured returning ~336 000 tokens — more
|
|
70
|
+
* than most context windows hold, with nothing in the payload saying so.
|
|
71
|
+
*
|
|
72
|
+
* At ~153 bytes per diagnostic this budget holds roughly 200 findings. Measured real calls
|
|
73
|
+
* cost tens to hundreds of tokens, so the cap cannot touch them; what it defends against is
|
|
74
|
+
* the tail — a generated file, or one cascading syntax error in a large partial.
|
|
75
|
+
*/
|
|
76
|
+
export const RESPONSE_TOKEN_BUDGET = 8_000;
|
|
77
|
+
/**
|
|
78
|
+
* Bytes per token, for converting the budget above into something countable.
|
|
79
|
+
*
|
|
80
|
+
* A crude ESTIMATE — this JSON is mostly ASCII prose, where ~4 bytes/token is the usual
|
|
81
|
+
* rule of thumb, and the real tokenizer is the client's. Being wrong by 25% moves the cap
|
|
82
|
+
* between ~150 and ~250 diagnostics, which changes nothing about which calls it affects.
|
|
83
|
+
*/
|
|
84
|
+
export const BYTES_PER_TOKEN = 4;
|
|
85
|
+
/**
|
|
86
|
+
* The bound actually enforced: serialized bytes of diagnostics one response may
|
|
87
|
+
* carry, across the WHOLE request rather than per file.
|
|
88
|
+
*
|
|
89
|
+
* Per-request because per-file alone leaves the worst case multiplied by the file
|
|
90
|
+
* count — 50 files each just under a per-file cap is 50x the intended bound, and the
|
|
91
|
+
* batch form exists precisely so agents send many files at once.
|
|
92
|
+
*/
|
|
93
|
+
export const MAX_RESPONSE_DIAGNOSTIC_BYTES = RESPONSE_TOKEN_BUDGET * BYTES_PER_TOKEN;
|
|
94
|
+
/**
|
|
95
|
+
* What one file costs in a response BEFORE any diagnostics: its status, gate, `impact`
|
|
96
|
+
* object, empty lists, and — when findings were withheld — the truncation note.
|
|
97
|
+
*
|
|
98
|
+
* Measured, not assumed: a 50-file batch of clean files serializes to 234 bytes per file,
|
|
99
|
+
* and the note adds ~150. Held at 512 because this multiplies by the file count.
|
|
100
|
+
*/
|
|
101
|
+
export const RESPONSE_ENVELOPE_BYTES_PER_FILE = 512;
|
|
102
|
+
/**
|
|
103
|
+
* The whole response bound, stated as arithmetic rather than enforced directly.
|
|
104
|
+
*
|
|
105
|
+
* The allocator bounds DIAGNOSTICS, the only unbounded dimension; the envelope is O(files)
|
|
106
|
+
* and already bounded by the batch file cap. `transport/validate-code.spec.ts` measures the
|
|
107
|
+
* worst legal request against this.
|
|
108
|
+
*
|
|
109
|
+
* Takes the file count rather than importing `MAX_BATCH_FILES`, which lives in
|
|
110
|
+
* `validate/batch-bounds.ts` and already imports this module.
|
|
111
|
+
*/
|
|
112
|
+
export function maxResponseBytes(fileCount) {
|
|
113
|
+
return MAX_RESPONSE_DIAGNOSTIC_BYTES + fileCount * RESPONSE_ENVELOPE_BYTES_PER_FILE;
|
|
114
|
+
}
|
|
115
|
+
/** KiB in `bytes`, rounded UP — a partial KiB still costs a full unit of work. */
|
|
116
|
+
const kib = (bytes) => Math.ceil(bytes / 1024);
|
|
117
|
+
/**
|
|
118
|
+
* How long to wait for a lint that was handed `bytes` of buffer.
|
|
119
|
+
*
|
|
120
|
+
* Never below {@link MIN_LINT_DEADLINE_MS}. Callers do not clamp to
|
|
121
|
+
* {@link MAX_LINT_DEADLINE_MS} — the byte caps do that upstream by refusing the request
|
|
122
|
+
* outright, so the ceiling is enforced where the work is admitted, not where it is timed.
|
|
123
|
+
*/
|
|
124
|
+
export function lintDeadlineMs(bytes) {
|
|
125
|
+
return Math.max(MIN_LINT_DEADLINE_MS, kib(bytes) * DEADLINE_MS_PER_KIB);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The most bytes whose {@link lintDeadlineMs} still fits inside `deadlineMs`.
|
|
129
|
+
*
|
|
130
|
+
* The inverse of the function above, floored to a whole KiB so the answer is exact at the
|
|
131
|
+
* boundary — one byte more and the deadline exceeds `deadlineMs`. Used to derive the batch
|
|
132
|
+
* cap from the deadline ceiling instead of from a round multiple of the per-buffer bound.
|
|
133
|
+
*/
|
|
134
|
+
export function maxBytesWithin(deadlineMs) {
|
|
135
|
+
return Math.floor(deadlineMs / DEADLINE_MS_PER_KIB) * 1024;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* How many DEPENDANTS one request will lint when computing impact.
|
|
139
|
+
*
|
|
140
|
+
* THE REAL BOUND ON IMPACT, and not a nicety. `IMPACT_DEADLINE_MS` cannot bound this work:
|
|
141
|
+
* a lint is synchronous CPU on the one event loop, so no timer preempts it (see
|
|
142
|
+
* `deadline.ts`). Bounding the INPUT is the only defence, exactly as `MAX_BUFFER_BYTES` is
|
|
143
|
+
* for the primary lint.
|
|
144
|
+
*
|
|
145
|
+
* DERIVED from the deadline it has to fit inside, on the worst project scale:
|
|
146
|
+
*
|
|
147
|
+
* impact deadline 2 000 ms
|
|
148
|
+
* less the project read at 10k files ~ 850 ms
|
|
149
|
+
* leaves for both lint passes ~1 150 ms
|
|
150
|
+
*
|
|
151
|
+
* Measured twice-lint cost, and the same {@link LOAD_FACTOR} the lint deadline uses:
|
|
152
|
+
*
|
|
153
|
+
* 100 dependants 258 ms idle 774 ms loaded -> 1 624 ms total, FITS
|
|
154
|
+
* 200 dependants 487 ms idle 1 460 ms loaded -> over
|
|
155
|
+
* 311 dependants 716 ms idle 2 147 ms loaded -> over on its own
|
|
156
|
+
*
|
|
157
|
+
* 311 is not hypothetical: it is the most-depended-on file measured on a real 2 615-file
|
|
158
|
+
* application (`lib/current_profile`). The same measurement puts 99.37% of targets at 100
|
|
159
|
+
* dependants or fewer — p50 1, p90 4, p99 59 — so the bound is hit rarely and, when it is,
|
|
160
|
+
* IT IS REPORTED (`unchecked_dependants`) rather than silently shortening the analysis.
|
|
161
|
+
*/
|
|
162
|
+
export const MAX_DEPENDANTS_LINTED = 100;
|
|
163
|
+
/**
|
|
164
|
+
* Cost of DISCOVERING dependants, per KiB of candidate text.
|
|
165
|
+
*
|
|
166
|
+
* Discovery parses every file whose text contains the target's name and resolves its
|
|
167
|
+
* references through the graph, so it scales with the BYTES it must parse rather than with
|
|
168
|
+
* the file count. Measured on a real 2 615-file application across four very differently
|
|
169
|
+
* shaped targets: 12.82, 12.52, 8.59 and 14.60 ms/KiB. Held above the slowest observed, for
|
|
170
|
+
* the same reason {@link LINT_MS_PER_KIB} is.
|
|
171
|
+
*/
|
|
172
|
+
export const DISCOVERY_MS_PER_KIB = 15;
|
|
173
|
+
/**
|
|
174
|
+
* How much candidate text one target's discovery will parse before giving up.
|
|
175
|
+
*
|
|
176
|
+
* WHY A BOUND AT ALL, and why it is not {@link MAX_DEPENDANTS_LINTED}: that one bounds the
|
|
177
|
+
* two lint passes, which happen AFTER discovery. Discovery itself was unbounded, and the
|
|
178
|
+
* most-referenced file on a real application (`lib/current_profile`, 312 candidates) cost
|
|
179
|
+
* 4.7 s and then returned `unavailable` anyway — seconds spent to reach "I do not know".
|
|
180
|
+
* The candidate byte total is known from a substring scan, before a single parse, so the
|
|
181
|
+
* same answer costs milliseconds instead.
|
|
182
|
+
*
|
|
183
|
+
* DERIVED from what has to fit inside `IMPACT_DEADLINE_MS`:
|
|
184
|
+
*
|
|
185
|
+
* impact deadline 2 000 ms
|
|
186
|
+
* less the project read ~ 235 ms
|
|
187
|
+
* less both lint passes at the dependant bound ~ 258 ms
|
|
188
|
+
* leaves for discovery ~1 507 ms
|
|
189
|
+
* at {@link DISCOVERY_MS_PER_KIB} ~100 KiB
|
|
190
|
+
*
|
|
191
|
+
* Held at 64 KiB rather than 100, because 100 spends the entire remaining budget and leaves
|
|
192
|
+
* nothing for a larger project's read. Measured against the same application: candidate text
|
|
193
|
+
* is p50 1 KiB, p90 13, p95 23, p99 143 — so 64 KiB is nearly 3x the p95 and turns away
|
|
194
|
+
* 2.51% of targets, each in a few milliseconds instead of several seconds.
|
|
195
|
+
*/
|
|
196
|
+
export const MAX_CANDIDATE_BYTES = 64 * 1024;
|
|
197
|
+
//# sourceMappingURL=cost-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEADLINE_MARGIN, DEADLINE_MS_PER_KIB, LINT_MS_PER_KIB, LOAD_FACTOR, MAX_LINT_DEADLINE_MS, MIN_LINT_DEADLINE_MS, lintDeadlineMs, maxBytesWithin, } from './cost-model.js';
|
|
3
|
+
import { MAX_BUFFER_BYTES } from './adapter-input.js';
|
|
4
|
+
import { MAX_BATCH_BYTES, MAX_BATCH_FILES } from './validate/batch-bounds.js';
|
|
5
|
+
/**
|
|
6
|
+
* The arithmetic, and the invariants that make it safe.
|
|
7
|
+
*
|
|
8
|
+
* WHAT THIS FILE IS FOR. The defect it exists to prevent is not a wrong number —
|
|
9
|
+
* it is two numbers that stopped being related to each other without anything
|
|
10
|
+
* noticing. `MAX_BATCH_BYTES` was `4 * MAX_BUFFER_BYTES` and the deadline was 60 s,
|
|
11
|
+
* and they were compatible until `FilterArity` moved throughput; after that the
|
|
12
|
+
* worst legal batch could not finish inside the deadline it was given, and every
|
|
13
|
+
* file in such a request comes back `timed_out` — no validation, no error, no sign.
|
|
14
|
+
*
|
|
15
|
+
* So the assertions below are mostly RELATIONSHIPS, not values. A relationship
|
|
16
|
+
* fails the moment someone changes one side of it, which is precisely when it needs
|
|
17
|
+
* to be looked at. The few literal values that are pinned are pinned because they
|
|
18
|
+
* are what makes the relationships legible in a failure message.
|
|
19
|
+
*/
|
|
20
|
+
describe('Unit: the lint cost model', () => {
|
|
21
|
+
it('composes the per-KiB deadline from its three named factors', () => {
|
|
22
|
+
// Pinned as a composition, not as 450: a reader who changes LOAD_FACTOR should
|
|
23
|
+
// see this follow, and a reader who changes this alone should see it break.
|
|
24
|
+
expect([LINT_MS_PER_KIB, LOAD_FACTOR, DEADLINE_MARGIN, DEADLINE_MS_PER_KIB]).toEqual([
|
|
25
|
+
75,
|
|
26
|
+
3,
|
|
27
|
+
2,
|
|
28
|
+
75 * 3 * 2,
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
it('charges a partial KiB as a whole one', () => {
|
|
32
|
+
// Rounding DOWN would grant a deadline smaller than the work admitted, which is
|
|
33
|
+
// the failure this module exists to prevent — so the rounding direction is part
|
|
34
|
+
// of the contract, not an implementation detail.
|
|
35
|
+
expect([lintDeadlineMs(300 * 1024), lintDeadlineMs(300 * 1024 - 1023)]).toEqual([
|
|
36
|
+
300 * DEADLINE_MS_PER_KIB,
|
|
37
|
+
300 * DEADLINE_MS_PER_KIB,
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
it('never returns less than the floor, however small the request', () => {
|
|
41
|
+
expect([lintDeadlineMs(0), lintDeadlineMs(1), lintDeadlineMs(1024)]).toEqual([
|
|
42
|
+
MIN_LINT_DEADLINE_MS,
|
|
43
|
+
MIN_LINT_DEADLINE_MS,
|
|
44
|
+
MIN_LINT_DEADLINE_MS,
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
it('inverts exactly: one KiB more than maxBytesWithin overshoots the deadline', () => {
|
|
48
|
+
const bytes = maxBytesWithin(MAX_LINT_DEADLINE_MS);
|
|
49
|
+
expect([lintDeadlineMs(bytes) <= MAX_LINT_DEADLINE_MS, lintDeadlineMs(bytes + 1024)]).toEqual([
|
|
50
|
+
true,
|
|
51
|
+
lintDeadlineMs(bytes) + DEADLINE_MS_PER_KIB,
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('Unit: the bounds derived from the cost model', () => {
|
|
56
|
+
it('admits the worst legal BATCH inside the deadline that batch is granted', () => {
|
|
57
|
+
// The regression, stated directly. Before this derivation MAX_BATCH_BYTES was
|
|
58
|
+
// 512 KiB against a fixed 60 s deadline: 512 KiB of measured work is ~38 s idle
|
|
59
|
+
// and ~115 s loaded, so the worst legal batch could not finish in time and every
|
|
60
|
+
// file in it returned `timed_out`.
|
|
61
|
+
expect(lintDeadlineMs(MAX_BATCH_BYTES) <= MAX_LINT_DEADLINE_MS).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('admits the worst legal single BUFFER inside the deadline it is granted', () => {
|
|
64
|
+
expect(lintDeadlineMs(MAX_BUFFER_BYTES) <= MAX_LINT_DEADLINE_MS).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it('leaves the single-file path on exactly the deadline it had before scaling', () => {
|
|
67
|
+
// Every real single-file call is far below MAX_BUFFER_BYTES, and even a maximal
|
|
68
|
+
// one lands on the floor. Scaling the deadline therefore changed nothing about
|
|
69
|
+
// the path that carries essentially all traffic — which is why it was safe to
|
|
70
|
+
// introduce. If this ever fails, single-file latency behaviour has moved and the
|
|
71
|
+
// 60 s figure quoted in `context.ts` and in the timeout message is stale.
|
|
72
|
+
expect(lintDeadlineMs(MAX_BUFFER_BYTES)).toEqual(MIN_LINT_DEADLINE_MS);
|
|
73
|
+
});
|
|
74
|
+
it('never refuses a batch for being smaller than one legal single buffer', () => {
|
|
75
|
+
// A batch cap below the per-buffer cap would refuse a one-file batch containing
|
|
76
|
+
// a file the single-file form accepts — the same content, refused for the shape
|
|
77
|
+
// of the request. This is also what forces the deadline to scale: at a fixed
|
|
78
|
+
// 60 s the derived batch cap is ~133 KiB, and this invariant fails.
|
|
79
|
+
expect(MAX_BATCH_BYTES > MAX_BUFFER_BYTES).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('states the derived cap, so a change to the model shows up as a number', () => {
|
|
82
|
+
expect([MAX_BATCH_BYTES, MAX_BATCH_BYTES / 1024, MAX_BATCH_FILES]).toEqual([272_384, 266, 50]);
|
|
83
|
+
});
|
|
84
|
+
it('bounds the whole request even at the file-count cap', () => {
|
|
85
|
+
// MAX_BATCH_FILES alone cannot bound the work: 50 files just under the buffer
|
|
86
|
+
// cap would be ~6 MiB. The byte cap is what binds, and it binds well before the
|
|
87
|
+
// file cap does — this records which of the two is load-bearing.
|
|
88
|
+
expect(MAX_BATCH_FILES * MAX_BUFFER_BYTES > MAX_BATCH_BYTES).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=cost-model.spec.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deadline wrapper for the request path.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS DOES AND DOES NOT BUY. The lint cannot be interrupted mid-flight: parsing is
|
|
5
|
+
* synchronous CPU work on the one event loop, so no timer preempts it. Racing a deadline
|
|
6
|
+
* returns control to the CALLER while the work runs on to completion. That fixes the
|
|
7
|
+
* failure mode that hurts — the agent stops waiting — but it is a BACKSTOP, not the primary
|
|
8
|
+
* defence, which is bounding the input (`MAX_BUFFER_BYTES`). True cancellation would need
|
|
9
|
+
* the lint on a worker thread.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Returned instead of a value when the deadline expires. A unique symbol rather
|
|
13
|
+
* than `undefined`/`null` so it can never collide with a legitimate result.
|
|
14
|
+
*/
|
|
15
|
+
export declare const TIMED_OUT: unique symbol;
|
|
16
|
+
export type TimedOut = typeof TIMED_OUT;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve with `work`'s value, or with {@link TIMED_OUT} once `ms` elapses.
|
|
19
|
+
*
|
|
20
|
+
* The timer is cleared on BOTH outcomes, and `unref`'d besides, so a pending deadline can
|
|
21
|
+
* never hold the process open.
|
|
22
|
+
*
|
|
23
|
+
* A rejection from `work` propagates unchanged. NOTE that when the deadline wins, `work` is
|
|
24
|
+
* left running and unobserved — the caller MUST attach a handler to it, or a later
|
|
25
|
+
* rejection becomes an unhandled rejection.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withDeadline<T>(work: Promise<T>, ms: number): Promise<T | TimedOut>;
|
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deadline wrapper for the request path.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS DOES AND DOES NOT BUY. The lint cannot be interrupted mid-flight: parsing is
|
|
5
|
+
* synchronous CPU work on the one event loop, so no timer preempts it. Racing a deadline
|
|
6
|
+
* returns control to the CALLER while the work runs on to completion. That fixes the
|
|
7
|
+
* failure mode that hurts — the agent stops waiting — but it is a BACKSTOP, not the primary
|
|
8
|
+
* defence, which is bounding the input (`MAX_BUFFER_BYTES`). True cancellation would need
|
|
9
|
+
* the lint on a worker thread.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Returned instead of a value when the deadline expires. A unique symbol rather
|
|
13
|
+
* than `undefined`/`null` so it can never collide with a legitimate result.
|
|
14
|
+
*/
|
|
15
|
+
export const TIMED_OUT = Symbol('platformos-supervisor:timed-out');
|
|
16
|
+
/**
|
|
17
|
+
* Resolve with `work`'s value, or with {@link TIMED_OUT} once `ms` elapses.
|
|
18
|
+
*
|
|
19
|
+
* The timer is cleared on BOTH outcomes, and `unref`'d besides, so a pending deadline can
|
|
20
|
+
* never hold the process open.
|
|
21
|
+
*
|
|
22
|
+
* A rejection from `work` propagates unchanged. NOTE that when the deadline wins, `work` is
|
|
23
|
+
* left running and unobserved — the caller MUST attach a handler to it, or a later
|
|
24
|
+
* rejection becomes an unhandled rejection.
|
|
25
|
+
*/
|
|
26
|
+
export function withDeadline(work, ms) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
29
|
+
// Node-only: `unref` keeps a pending deadline from holding the event loop.
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
work.then((value) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve(value);
|
|
34
|
+
}, (error) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=deadline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { TIMED_OUT, withDeadline } from './deadline.js';
|
|
3
|
+
/**
|
|
4
|
+
* TASK-15. `withDeadline` is a backstop, and its contract is narrow on purpose:
|
|
5
|
+
* add a deadline, change nothing else. In particular a rejection must pass through
|
|
6
|
+
* untouched, because the lint is the primary gate and swallowing its failure would
|
|
7
|
+
* turn a real error into a silent pass.
|
|
8
|
+
*/
|
|
9
|
+
const after = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
|
10
|
+
const rejectAfter = (ms, error) => new Promise((_, reject) => setTimeout(() => reject(error), ms));
|
|
11
|
+
describe('Unit: withDeadline', () => {
|
|
12
|
+
it('resolves with the value when the work finishes first', async () => {
|
|
13
|
+
await expect(withDeadline(after(1, 'done'), 1_000)).resolves.toEqual('done');
|
|
14
|
+
});
|
|
15
|
+
it('resolves with TIMED_OUT when the deadline wins', async () => {
|
|
16
|
+
await expect(withDeadline(after(1_000, 'too late'), 5)).resolves.toBe(TIMED_OUT);
|
|
17
|
+
});
|
|
18
|
+
it('resolves with TIMED_OUT for work that never settles at all', async () => {
|
|
19
|
+
// The exact shape that used to hang the tool for the life of the session.
|
|
20
|
+
await expect(withDeadline(new Promise(() => { }), 5)).resolves.toBe(TIMED_OUT);
|
|
21
|
+
});
|
|
22
|
+
it('propagates a rejection unchanged rather than converting it to TIMED_OUT', async () => {
|
|
23
|
+
// The lint is the primary gate: a genuine failure must stay a failure.
|
|
24
|
+
const failure = new Error('lint exploded');
|
|
25
|
+
await expect(withDeadline(rejectAfter(1, failure), 1_000)).rejects.toThrow(failure);
|
|
26
|
+
});
|
|
27
|
+
it('preserves a falsy result, which a null/undefined sentinel would have destroyed', async () => {
|
|
28
|
+
// TIMED_OUT is a unique Symbol precisely so these stay distinguishable.
|
|
29
|
+
await expect(withDeadline(Promise.resolve(undefined), 1_000)).resolves.toBeUndefined();
|
|
30
|
+
await expect(withDeadline(Promise.resolve(null), 1_000)).resolves.toBeNull();
|
|
31
|
+
await expect(withDeadline(Promise.resolve(0), 1_000)).resolves.toEqual(0);
|
|
32
|
+
await expect(withDeadline(Promise.resolve([]), 1_000)).resolves.toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
it('clears the timer when the work wins, so nothing is left pending', async () => {
|
|
35
|
+
const clearSpy = vi.spyOn(globalThis, 'clearTimeout');
|
|
36
|
+
try {
|
|
37
|
+
await withDeadline(after(1, 'done'), 60_000);
|
|
38
|
+
// A 60 s timer left armed would hold a stdio server open past its work.
|
|
39
|
+
expect(clearSpy).toHaveBeenCalled();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
clearSpy.mockRestore();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('clears the timer when the work REJECTS, not only when it resolves', async () => {
|
|
46
|
+
const clearSpy = vi.spyOn(globalThis, 'clearTimeout');
|
|
47
|
+
try {
|
|
48
|
+
await expect(withDeadline(rejectAfter(1, new Error('x')), 60_000)).rejects.toThrow();
|
|
49
|
+
expect(clearSpy).toHaveBeenCalled();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearSpy.mockRestore();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it('unrefs the deadline timer so a pending one cannot hold the process open', async () => {
|
|
56
|
+
const unref = vi.fn();
|
|
57
|
+
const setSpy = vi
|
|
58
|
+
.spyOn(globalThis, 'setTimeout')
|
|
59
|
+
.mockImplementation((() => ({ unref })));
|
|
60
|
+
try {
|
|
61
|
+
void withDeadline(Promise.resolve('immediate'), 60_000);
|
|
62
|
+
expect(unref).toHaveBeenCalledOnce();
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setSpy.mockRestore();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
it('does not stop the abandoned work from continuing — the documented limitation', async () => {
|
|
69
|
+
// Parsing is synchronous CPU work; nothing here can preempt it. The deadline
|
|
70
|
+
// frees the CALLER, it does not cancel. Pinned so the limitation is not
|
|
71
|
+
// mistaken for a bug later, and so nobody assumes cancellation they don't have.
|
|
72
|
+
let finished = false;
|
|
73
|
+
const work = after(30, 'eventually').then((value) => {
|
|
74
|
+
finished = true;
|
|
75
|
+
return value;
|
|
76
|
+
});
|
|
77
|
+
expect(await withDeadline(work, 5)).toBe(TIMED_OUT);
|
|
78
|
+
expect(finished).toBe(false);
|
|
79
|
+
await work;
|
|
80
|
+
expect(finished).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=deadline.spec.js.map
|