next-leak 0.4.2 → 0.5.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.
@@ -3657,10 +3657,10 @@ var require_ansi_styles = __commonJS({
3657
3657
  };
3658
3658
  var ansi2ansi = (n) => n;
3659
3659
  var rgb2rgb = (r, g, b) => [r, g, b];
3660
- var setLazyProperty = (object, property, get) => {
3660
+ var setLazyProperty = (object, property, get2) => {
3661
3661
  Object.defineProperty(object, property, {
3662
3662
  get: () => {
3663
- const value = get();
3663
+ const value = get2();
3664
3664
  Object.defineProperty(object, property, {
3665
3665
  value,
3666
3666
  enumerable: true,
@@ -36043,18 +36043,18 @@ var require_gen_mapping_umd = __commonJS({
36043
36043
  function cast(set) {
36044
36044
  return set;
36045
36045
  }
36046
- function get(setarr, key) {
36046
+ function get2(setarr, key) {
36047
36047
  return cast(setarr)._indexes[key];
36048
36048
  }
36049
36049
  function put(setarr, key) {
36050
- const index = get(setarr, key);
36050
+ const index = get2(setarr, key);
36051
36051
  if (index !== void 0) return index;
36052
36052
  const { array, _indexes: indexes } = cast(setarr);
36053
36053
  const length = array.push(key);
36054
36054
  return indexes[key] = length - 1;
36055
36055
  }
36056
36056
  function remove(setarr, key) {
36057
- const index = get(setarr, key);
36057
+ const index = get2(setarr, key);
36058
36058
  if (index === void 0) return;
36059
36059
  const { array, _indexes: indexes } = cast(setarr);
36060
36060
  for (let i = index + 1; i < array.length; i++) {
@@ -60212,7 +60212,7 @@ var require_family = __commonJS({
60212
60212
  });
60213
60213
  exports2._getKey = _getKey;
60214
60214
  exports2._getPattern = _getPattern;
60215
- exports2.get = get;
60215
+ exports2.get = get2;
60216
60216
  exports2.getAllNextSiblings = getAllNextSiblings;
60217
60217
  exports2.getAllPrevSiblings = getAllPrevSiblings;
60218
60218
  exports2.getAssignmentIdentifiers = getAssignmentIdentifiers;
@@ -60443,7 +60443,7 @@ var require_family = __commonJS({
60443
60443
  }
60444
60444
  return siblings;
60445
60445
  }
60446
- function get(key, context = true) {
60446
+ function get2(key, context = true) {
60447
60447
  if (context === true) context = this.context;
60448
60448
  const parts = key.split(".");
60449
60449
  if (parts.length === 1) {
@@ -65957,7 +65957,7 @@ var require_dist5 = __commonJS({
65957
65957
  var pathVar = getPathVar(env, platform);
65958
65958
  var separator = getSeparator(platform);
65959
65959
  var originalPath = env[pathVar];
65960
- return { push, unshift, get, restore };
65960
+ return { push, unshift, get: get2, restore };
65961
65961
  function push() {
65962
65962
  for (var _len = arguments.length, paths = Array(_len), _key = 0; _key < _len; _key++) {
65963
65963
  paths[_key] = arguments[_key];
@@ -65970,12 +65970,12 @@ var require_dist5 = __commonJS({
65970
65970
  }
65971
65971
  return change(false, paths);
65972
65972
  }
65973
- function get() {
65973
+ function get2() {
65974
65974
  return env[pathVar];
65975
65975
  }
65976
65976
  function restore() {
65977
65977
  env[pathVar] = originalPath;
65978
- return get();
65978
+ return get2();
65979
65979
  }
65980
65980
  function change(append, paths) {
65981
65981
  if (!append) {
@@ -65986,7 +65986,7 @@ var require_dist5 = __commonJS({
65986
65986
  addExistingPath(pathArray, env[pathVar], append);
65987
65987
  env[pathVar] = pathArray.join(separator);
65988
65988
  });
65989
- return get();
65989
+ return get2();
65990
65990
  }
65991
65991
  }
65992
65992
  function getPathArray(pathToAdd) {
@@ -86350,7 +86350,7 @@ var require_form_data = __commonJS({
86350
86350
  var CombinedStream = require_combined_stream();
86351
86351
  var util = __require("util");
86352
86352
  var path7 = __require("path");
86353
- var http = __require("http");
86353
+ var http2 = __require("http");
86354
86354
  var https = __require("https");
86355
86355
  var parseUrl = __require("url").parse;
86356
86356
  var fs = __require("fs");
@@ -86625,7 +86625,7 @@ var require_form_data = __commonJS({
86625
86625
  if (options.protocol === "https:") {
86626
86626
  request2 = https.request(options);
86627
86627
  } else {
86628
- request2 = http.request(options);
86628
+ request2 = http2.request(options);
86629
86629
  }
86630
86630
  this.getLength(function(err, length) {
86631
86631
  if (err && err !== "Unknown stream") {
@@ -93208,9 +93208,6 @@ async function runLoadPhase(options) {
93208
93208
  amount: options.amount,
93209
93209
  connections: options.connections,
93210
93210
  ...options.headers !== void 0 && { headers: options.headers },
93211
- ...options.abandonAfterMs !== void 0 && {
93212
- timeout: Math.max(Math.ceil(options.abandonAfterMs / 1e3), 1)
93213
- },
93214
93211
  ...unique && { idReplacement: true }
93215
93212
  });
93216
93213
  const result = {
@@ -93221,8 +93218,7 @@ async function runLoadPhase(options) {
93221
93218
  timeouts: raw.timeouts,
93222
93219
  durationSeconds: raw.duration
93223
93220
  };
93224
- const expected = options.abandonAfterMs === void 0 ? 0 : result.timeouts;
93225
- const failures = Math.max(options.amount - result.ok2xx - expected, 0);
93221
+ const failures = Math.max(options.amount - result.ok2xx, 0);
93226
93222
  const ratio = options.amount === 0 ? 0 : failures / options.amount;
93227
93223
  if (ratio > (options.maxErrorRatio ?? 0.01)) {
93228
93224
  const unanswered = failures - result.non2xx - result.errors - result.timeouts;
@@ -93239,6 +93235,7 @@ async function runLoadPhase(options) {
93239
93235
  import { mkdir } from "fs/promises";
93240
93236
 
93241
93237
  // src/control-client.ts
93238
+ import http from "http";
93242
93239
  import { z as z5 } from "zod";
93243
93240
  var sampleSchema = z5.object({
93244
93241
  gcExposed: z5.boolean(),
@@ -93254,19 +93251,70 @@ var ControlError = class extends Error {
93254
93251
  this.name = "ControlError";
93255
93252
  }
93256
93253
  };
93257
- async function request(port, pathname) {
93258
- let response;
93259
- try {
93260
- response = await fetch(`http://127.0.0.1:${port}${pathname}`);
93261
- } catch (cause) {
93262
- throw new ControlError(
93263
- `control channel ${pathname} on port ${port} did not answer (${cause instanceof Error ? cause.message : String(cause)}) \u2014 the measured process is gone or its event loop is blocked`
93254
+ var DEADLINES_MS = {
93255
+ "/mem": 3e4,
93256
+ "/gc": 18e4,
93257
+ "/snapshot": 18e5
93258
+ };
93259
+ var DEFAULT_DEADLINE_MS = 3e4;
93260
+ function deadlineFor(pathname) {
93261
+ const operation = pathname.split("?")[0] ?? pathname;
93262
+ return DEADLINES_MS[operation] ?? DEFAULT_DEADLINE_MS;
93263
+ }
93264
+ function get(port, pathname, deadlineMs) {
93265
+ return new Promise((resolve, reject) => {
93266
+ let settled = false;
93267
+ const settle = (outcome) => {
93268
+ if (settled) {
93269
+ return;
93270
+ }
93271
+ settled = true;
93272
+ clearTimeout(timer);
93273
+ if ("ok" in outcome) {
93274
+ resolve(outcome.ok);
93275
+ } else {
93276
+ reject(outcome.error);
93277
+ }
93278
+ };
93279
+ const request2 = http.get({ host: "127.0.0.1", port, path: pathname }, (response) => {
93280
+ const chunks = [];
93281
+ response.on("data", (chunk) => chunks.push(chunk));
93282
+ response.once(
93283
+ "end",
93284
+ () => settle({
93285
+ ok: { status: response.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") }
93286
+ })
93287
+ );
93288
+ });
93289
+ const timer = setTimeout(() => {
93290
+ settle({
93291
+ error: new ControlError(
93292
+ `control channel ${pathname} on port ${port} did not answer within ${Math.round(deadlineMs / 1e3)}s \u2014 the measured process is wedged, or this snapshot is larger than anything this tool has been validated on`
93293
+ )
93294
+ });
93295
+ request2.destroy();
93296
+ }, deadlineMs);
93297
+ timer.unref();
93298
+ request2.once(
93299
+ "error",
93300
+ (cause) => settle({
93301
+ error: new ControlError(
93302
+ `control channel ${pathname} on port ${port} did not answer (${cause.message}) \u2014 the measured process is gone or its event loop is blocked`
93303
+ )
93304
+ })
93264
93305
  );
93265
- }
93266
- if (!response.ok) {
93306
+ });
93307
+ }
93308
+ async function request(port, pathname, deadlineMs = deadlineFor(pathname)) {
93309
+ const response = await get(port, pathname, deadlineMs);
93310
+ if (response.status !== 200) {
93267
93311
  throw new ControlError(`control channel ${pathname} responded ${response.status}`);
93268
93312
  }
93269
- return response.json();
93313
+ try {
93314
+ return JSON.parse(response.body);
93315
+ } catch {
93316
+ throw new ControlError(`control channel ${pathname} answered with malformed JSON`);
93317
+ }
93270
93318
  }
93271
93319
  async function requestGc(port) {
93272
93320
  const sample = sampleSchema.parse(await request(port, "/gc"));
@@ -93444,6 +93492,7 @@ async function waitUntilSettled(controlPort, maxIdleMs, deps) {
93444
93492
  var defaultDeps = {
93445
93493
  launch: launchInstrumented,
93446
93494
  load: runLoadPhase,
93495
+ abandon: runAbandonPhase,
93447
93496
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
93448
93497
  readMemory: requestMemory
93449
93498
  };
@@ -93502,7 +93551,7 @@ async function runRitual(options, deps = defaultDeps) {
93502
93551
  });
93503
93552
  return;
93504
93553
  }
93505
- const outcome = await runAbandonPhase({
93554
+ const outcome = await deps.abandon({
93506
93555
  url: `http://127.0.0.1:${app.appPort}${options.route}`,
93507
93556
  amount,
93508
93557
  connections,
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  killActiveChildren,
10
10
  parseCliArgs,
11
11
  runMeasurement
12
- } from "./chunk-YDMJVVWG.js";
12
+ } from "./chunk-IDSNTUKC.js";
13
13
  import "./chunk-2FZXZLZW.js";
14
14
  import "./chunk-XHPUAMJG.js";
15
15
  import "./chunk-6XYFBOL2.js";
@@ -1,4 +1,6 @@
1
1
  import type { HeapSample } from "./control-server.js";
2
+ declare function deadlineFor(pathname: string): number;
3
+ declare function request(port: number, pathname: string, deadlineMs?: number): Promise<unknown>;
2
4
  /** Forces GC in the measured process and returns a settled memory sample. */
3
5
  export declare function requestGc(port: number): Promise<HeapSample>;
4
6
  /**
@@ -11,3 +13,13 @@ export declare function requestSnapshot(port: number, name: string): Promise<{
11
13
  file: string;
12
14
  sample: HeapSample;
13
15
  }>;
16
+ /** Test seam: the production deadlines would make a hung-server test take minutes. */
17
+ export declare const requestWithDeadline: typeof request;
18
+ /**
19
+ * Test seam for the deadline table itself. A mutation run showed the mapping
20
+ * could quietly send `/snapshot?name=x` to the 30-second default instead of
21
+ * its 30-minute bound — reinstating, from the inside, the exact ceiling this
22
+ * client exists to remove.
23
+ */
24
+ export declare const deadlineForOperation: typeof deadlineFor;
25
+ export {};
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sourceIndexAt,
40
40
  summarizeBaseline,
41
41
  validateTarget
42
- } from "./chunk-YDMJVVWG.js";
42
+ } from "./chunk-IDSNTUKC.js";
43
43
  import {
44
44
  renderHtmlReport
45
45
  } from "./chunk-NNDGXIGQ.js";
package/dist/load.d.ts CHANGED
@@ -5,12 +5,6 @@ export type LoadPhaseOptions = {
5
5
  connections: number;
6
6
  /** Sent with every request (compression, cookies, auth). */
7
7
  headers?: Record<string, string>;
8
- /**
9
- * Give up on each request after this many milliseconds, emulating clients
10
- * that disconnect before the response arrives. Abandoned requests are
11
- * expected, so they do not count against the error budget.
12
- */
13
- abandonAfterMs?: number;
14
8
  /**
15
9
  * Maximum tolerated ratio of non-2xx responses plus socket errors before
16
10
  * the phase fails. A route that errors under load must fail the run, not
package/dist/ritual.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { HeapSample } from "./control-server.js";
2
2
  import { launchInstrumented } from "./launcher.js";
3
+ import { runAbandonPhase } from "./abandon-load.js";
3
4
  import { runLoadPhase } from "./load.js";
4
5
  import { type TrendResult } from "./trend.js";
5
6
  export type RitualOptions = {
@@ -103,6 +104,8 @@ export type RitualResult = {
103
104
  export type RitualDeps = {
104
105
  launch: typeof launchInstrumented;
105
106
  load: typeof runLoadPhase;
107
+ /** Early-disconnect load; a seam like `load`, for the same reasons. */
108
+ abandon: typeof runAbandonPhase;
106
109
  sleep: (ms: number) => Promise<void>;
107
110
  /** GC-free read, polled while the app is under load. */
108
111
  readMemory: (port: number) => Promise<HeapSample>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-leak",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.",
5
5
  "keywords": [
6
6
  "nextjs",