tape-six 1.7.13 → 1.8.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.
Files changed (43) hide show
  1. package/README.md +3 -1
  2. package/TESTING.md +246 -47
  3. package/bin/tape6-server.js +19 -19
  4. package/index.d.ts +67 -0
  5. package/llms-full.txt +102 -0
  6. package/llms.txt +66 -0
  7. package/package.json +13 -4
  8. package/skills/write-tests/SKILL.md +63 -16
  9. package/src/State.js +26 -21
  10. package/src/Tester.js +59 -27
  11. package/src/deep6/env.d.ts +174 -0
  12. package/src/deep6/env.js +4 -4
  13. package/src/deep6/index.d.ts +86 -0
  14. package/src/deep6/index.js +10 -7
  15. package/src/deep6/traverse/assemble.d.ts +59 -0
  16. package/src/deep6/traverse/assemble.js +4 -3
  17. package/src/deep6/traverse/clone.d.ts +57 -0
  18. package/src/deep6/traverse/clone.js +4 -2
  19. package/src/deep6/traverse/deref.d.ts +59 -0
  20. package/src/deep6/traverse/deref.js +3 -2
  21. package/src/deep6/traverse/preprocess.d.ts +65 -0
  22. package/src/deep6/traverse/preprocess.js +2 -1
  23. package/src/deep6/traverse/walk.d.ts +219 -0
  24. package/src/deep6/traverse/walk.js +9 -4
  25. package/src/deep6/unifiers/matchCondition.d.ts +45 -0
  26. package/src/deep6/unifiers/matchCondition.js +1 -0
  27. package/src/deep6/unifiers/matchInstanceOf.d.ts +37 -0
  28. package/src/deep6/unifiers/matchInstanceOf.js +1 -0
  29. package/src/deep6/unifiers/matchString.d.ts +56 -0
  30. package/src/deep6/unifiers/matchString.js +1 -0
  31. package/src/deep6/unifiers/matchTypeOf.d.ts +37 -0
  32. package/src/deep6/unifiers/matchTypeOf.js +1 -0
  33. package/src/deep6/unifiers/ref.d.ts +52 -0
  34. package/src/deep6/unifiers/ref.js +1 -0
  35. package/src/deep6/unify.d.ts +95 -0
  36. package/src/deep6/unify.js +130 -66
  37. package/src/deep6/utils/replaceVars.d.ts +25 -0
  38. package/src/deep6/utils/replaceVars.js +23 -19
  39. package/src/response.d.ts +43 -0
  40. package/src/response.js +57 -0
  41. package/src/server.d.ts +81 -0
  42. package/src/server.js +69 -0
  43. package/src/test.js +26 -53
@@ -24,18 +24,17 @@ class Wrap extends Unifier {
24
24
  value = isWrapped ? val.object : val;
25
25
  if (!value || typeof value != 'object' || Array.isArray(this.object) !== Array.isArray(value)) return false;
26
26
  if (Array.isArray(this.object)) {
27
- if (!Array.isArray(value)) return false;
28
27
  return isWrapped
29
28
  ? unifyObjects(this.object, this.type, this, val.object, val.type, val, ls, rs, env)
30
29
  : unifyObjects(this.object, this.type, this, val, env.openArrays ? 'open' : 'exact', null, ls, rs, env);
31
30
  }
32
- if (typeof Map == 'function' && this.object instanceof Map) {
31
+ if (this.object instanceof Map) {
33
32
  if (!(value instanceof Map)) return false;
34
33
  return isWrapped
35
34
  ? unifyMaps(this.object, this.type, this, val.object, val.type, val, ls, rs, env)
36
35
  : unifyMaps(this.object, this.type, this, val, env.openMaps ? 'open' : 'exact', null, ls, rs, env);
37
36
  }
38
- if (typeof Set == 'function' && this.object instanceof Set) {
37
+ if (this.object instanceof Set) {
39
38
  if (!(value instanceof Set)) return false;
40
39
  return isWrapped
41
40
  ? unifySets(this.object, this.type, this, val.object, val.type, val, ls, rs, env)
@@ -61,11 +60,12 @@ const registry = [
61
60
  Date,
62
61
  (l, r) => l instanceof Date && r instanceof Date && l.getTime() == r.getTime(),
63
62
  RegExp,
64
- (l, r) =>
65
- l instanceof RegExp && r instanceof RegExp && l.source == r.source && l.global == r.global && l.multiline == r.multiline && l.ignoreCase == r.ignoreCase
63
+ (l, r) => l instanceof RegExp && r instanceof RegExp && l.source == r.source && l.flags == r.flags
66
64
  ],
67
65
  filters = [];
68
66
 
67
+ typeof URL == 'function' && registry.push(URL, (l, r) => l instanceof URL && r instanceof URL && l.href == r.href);
68
+
69
69
  // possible well-known constructors
70
70
 
71
71
  const unifyTypedArrays = Type => (l, r, ls, rs, env) => {
@@ -99,9 +99,10 @@ const unifyDataView = (l, r, ls, rs, env) => {
99
99
  };
100
100
  typeof DataView == 'function' && registry.push(DataView, unifyDataView);
101
101
 
102
+ const unifyUint8Array = unifyTypedArrays(Uint8Array);
102
103
  const unifyArrayBuffer = (l, r, ls, rs, env) => {
103
104
  if (!(l instanceof ArrayBuffer) || !(r instanceof ArrayBuffer) || l.byteLength != r.byteLength) return false;
104
- return unifyTypedArrays(Uint8Array)(new Uint8Array(l), new Uint8Array(r), ls, rs, env);
105
+ return unifyUint8Array(new Uint8Array(l), new Uint8Array(r), ls, rs, env);
105
106
  };
106
107
  typeof ArrayBuffer == 'function' && typeof Uint8Array == 'function' && registry.push(ArrayBuffer, unifyArrayBuffer);
107
108
 
@@ -161,22 +162,28 @@ const mapOps = {
161
162
  }
162
163
  }
163
164
  };
164
- mapOps.exact.exact.compare = mapOps.exact.open.compare = mapOps.exact.soft.compare = (l, r, ls, rs) => {
165
- for (const [key, value] of r) {
166
- if (!l.has(key)) return false;
167
- ls.push(l.get(key));
168
- rs.push(value);
169
- }
170
- return true;
171
- };
172
- mapOps.open.open.compare = mapOps.open.soft.compare = mapOps.soft.soft.compare = (l, r, ls, rs) => {
173
- for (const [key, value] of r) {
174
- if (!l.has(key)) continue;
175
- ls.push(l.get(key));
176
- rs.push(value);
177
- }
178
- return true;
179
- };
165
+ mapOps.exact.exact.compare =
166
+ mapOps.exact.open.compare =
167
+ mapOps.exact.soft.compare =
168
+ (l, r, ls, rs) => {
169
+ for (const [key, value] of r) {
170
+ if (!l.has(key)) return false;
171
+ ls.push(l.get(key));
172
+ rs.push(value);
173
+ }
174
+ return true;
175
+ };
176
+ mapOps.open.open.compare =
177
+ mapOps.open.soft.compare =
178
+ mapOps.soft.soft.compare =
179
+ (l, r, ls, rs) => {
180
+ for (const [key, value] of r) {
181
+ if (!l.has(key)) continue;
182
+ ls.push(l.get(key));
183
+ rs.push(value);
184
+ }
185
+ return true;
186
+ };
180
187
  mapOps.exact.soft.update = mapOps.open.soft.update = function () {
181
188
  for (const [key, value] of this.l) {
182
189
  !this.r.has(key) && this.r.set(key, value);
@@ -186,7 +193,19 @@ mapOps.exact.soft.update = mapOps.open.soft.update = function () {
186
193
  const unifyMaps = (l, lt, lm, r, rt, rm, ls, rs, env) => {
187
194
  const ols = ls;
188
195
  if (lt > rt) {
189
- [l, lt, lm, ls, r, rt, rm, rs] = [r, rt, rm, rs, l, lt, lm, ls];
196
+ let tmp;
197
+ tmp = l;
198
+ l = r;
199
+ r = tmp;
200
+ tmp = lt;
201
+ lt = rt;
202
+ rt = tmp;
203
+ tmp = lm;
204
+ lm = rm;
205
+ rm = tmp;
206
+ tmp = ls;
207
+ ls = rs;
208
+ rs = tmp;
190
209
  }
191
210
  const ops = mapOps[lt][rt];
192
211
  if (ops.precheck && !ops.precheck(l, r)) return false;
@@ -231,8 +250,13 @@ const setOps = {
231
250
  }
232
251
  }
233
252
  };
234
- setOps.exact.exact.compare = setOps.exact.open.compare = setOps.exact.soft.compare = setOps.open.open.compare = setOps.open.soft.compare = setOps.soft.soft.compare = () =>
235
- true;
253
+ setOps.exact.exact.compare =
254
+ setOps.exact.open.compare =
255
+ setOps.exact.soft.compare =
256
+ setOps.open.open.compare =
257
+ setOps.open.soft.compare =
258
+ setOps.soft.soft.compare =
259
+ () => true;
236
260
  setOps.exact.soft.update = setOps.open.soft.update = function () {
237
261
  for (const key of this.l) {
238
262
  this.r.add(key);
@@ -242,7 +266,19 @@ setOps.exact.soft.update = setOps.open.soft.update = function () {
242
266
  const unifySets = (l, lt, lm, r, rt, rm, ls, rs, env) => {
243
267
  const ols = ls;
244
268
  if (lt > rt) {
245
- [l, lt, lm, ls, r, rt, rm, rs] = [r, rt, rm, rs, l, lt, lm, ls];
269
+ let tmp;
270
+ tmp = l;
271
+ l = r;
272
+ r = tmp;
273
+ tmp = lt;
274
+ lt = rt;
275
+ rt = tmp;
276
+ tmp = lm;
277
+ lm = rm;
278
+ rm = tmp;
279
+ tmp = ls;
280
+ ls = rs;
281
+ rs = tmp;
246
282
  }
247
283
  const ops = setOps[lt][rt];
248
284
  if (ops.precheck && !ops.precheck(l, r)) return false;
@@ -267,7 +303,10 @@ const objectOps = {
267
303
  rKeys = rKeys.concat(Object.getOwnPropertySymbols(r));
268
304
  }
269
305
  if (lKeys.length != rKeys.length) return false;
270
- return lKeys.every(k => hasOwnProperty.call(r, k));
306
+ for (let i = 0; i < lKeys.length; ++i) {
307
+ if (!hasOwnProperty.call(r, lKeys[i])) return false;
308
+ }
309
+ return true;
271
310
  }
272
311
  },
273
312
  open: {},
@@ -298,29 +337,34 @@ const objectOps = {
298
337
  }
299
338
  }
300
339
  };
301
- objectOps.exact.exact.compare = objectOps.exact.open.compare = objectOps.exact.soft.compare = (l, r, ls, rs, env) => {
302
- let keys = Object.keys(r);
303
- if (env.symbols) keys = keys.concat(Object.getOwnPropertySymbols(r));
304
- return keys.every(k => {
305
- if (hasOwnProperty.call(l, k)) {
306
- ls.push(l[k]);
307
- rs.push(r[k]);
340
+ objectOps.exact.exact.compare =
341
+ objectOps.exact.open.compare =
342
+ objectOps.exact.soft.compare =
343
+ (l, r, ls, rs, env) => {
344
+ let keys = Object.keys(r);
345
+ if (env.symbols) keys = keys.concat(Object.getOwnPropertySymbols(r));
346
+ for (let i = 0; i < keys.length; ++i) {
347
+ const k = keys[i];
348
+ if (!hasOwnProperty.call(l, k)) return false;
349
+ ls.push(l[k]);
350
+ rs.push(r[k]);
351
+ }
308
352
  return true;
309
- }
310
- return false;
311
- });
312
- };
313
- objectOps.open.open.compare = objectOps.open.soft.compare = objectOps.soft.soft.compare = (l, r, ls, rs, env) => {
314
- let keys = Object.keys(r);
315
- if (env.symbols) keys = keys.concat(Object.getOwnPropertySymbols(r));
316
- for (const k of keys) {
317
- if (hasOwnProperty.call(l, k)) {
318
- ls.push(l[k]);
319
- rs.push(r[k]);
320
- }
321
- }
322
- return true;
323
- };
353
+ };
354
+ objectOps.open.open.compare =
355
+ objectOps.open.soft.compare =
356
+ objectOps.soft.soft.compare =
357
+ (l, r, ls, rs, env) => {
358
+ let keys = Object.keys(r);
359
+ if (env.symbols) keys = keys.concat(Object.getOwnPropertySymbols(r));
360
+ for (const k of keys) {
361
+ if (hasOwnProperty.call(l, k)) {
362
+ ls.push(l[k]);
363
+ rs.push(r[k]);
364
+ }
365
+ }
366
+ return true;
367
+ };
324
368
  objectOps.exact.soft.update = objectOps.open.soft.update = function () {
325
369
  let keys = Object.keys(this.l);
326
370
  if (this.e.symbols) keys = keys.concat(Object.getOwnPropertySymbols(this.l));
@@ -332,7 +376,19 @@ objectOps.exact.soft.update = objectOps.open.soft.update = function () {
332
376
  const unifyObjects = (l, lt, lm, r, rt, rm, ls, rs, env) => {
333
377
  const ols = ls;
334
378
  if (lt > rt) {
335
- [l, lt, lm, ls, r, rt, rm, rs] = [r, rt, rm, rs, l, lt, lm, ls];
379
+ let tmp;
380
+ tmp = l;
381
+ l = r;
382
+ r = tmp;
383
+ tmp = lt;
384
+ lt = rt;
385
+ rt = tmp;
386
+ tmp = lm;
387
+ lm = rm;
388
+ rm = tmp;
389
+ tmp = ls;
390
+ ls = rs;
391
+ rs = tmp;
336
392
  }
337
393
  const ops = objectOps[lt][rt];
338
394
  if (ops.precheck && !ops.precheck(l, r, env)) return false;
@@ -354,36 +410,39 @@ const unify = (l, r, env, options) => {
354
410
  env = Object.assign(env, options);
355
411
  // options: openObjects, openArrays, openMaps, openSets, circular, loose, ignoreFunctions, signedZero, symbols.
356
412
  const ls = [l],
357
- rs = [r],
358
- lSeen = new Map(),
413
+ rs = [r];
414
+ let lSeen, rSeen;
415
+ if (env.circular) {
416
+ lSeen = new Map();
359
417
  rSeen = new Map();
418
+ }
360
419
  main: while (ls.length) {
361
420
  // perform a command, or extract a pair
362
421
  l = ls.pop();
363
- if (l instanceof Command) {
422
+ if (typeof l == 'object' && l instanceof Command) {
364
423
  l.f();
365
424
  continue;
366
425
  }
367
426
  r = rs.pop();
368
427
  // direct equality
369
428
  if (l === r) {
370
- if (env.circular && l && typeof l == 'object' && lSeen.has(l) ^ rSeen.has(r)) return null;
429
+ if (lSeen && l && typeof l == 'object' && lSeen.has(l) ^ rSeen.has(r)) return null;
371
430
  if (env.signedZero && l === 0 && 1 / l !== 1 / r) return null;
372
431
  continue;
373
432
  }
374
433
  // anyvar
375
434
  if (l === _ || r === _) continue;
376
435
  // process variables (variables have priority)
377
- if (l instanceof Variable) {
436
+ if (typeof l == 'object' && l instanceof Variable) {
378
437
  if (l.unify(r, ls, rs, env)) continue;
379
438
  return null;
380
439
  }
381
- if (r instanceof Variable) {
440
+ if (typeof r == 'object' && r instanceof Variable) {
382
441
  if (r.unify(l, ls, rs, env)) continue;
383
442
  return null;
384
443
  }
385
444
  // process circular dependencies
386
- if (env.circular) {
445
+ if (lSeen) {
387
446
  const lIndex = lSeen.get(l);
388
447
  if (typeof lIndex == 'number') {
389
448
  if (lIndex === rSeen.get(r)) continue main;
@@ -396,11 +455,11 @@ const unify = (l, r, env, options) => {
396
455
  r && typeof r == 'object' && rSeen.set(r, rSeen.size);
397
456
  }
398
457
  // invoke custom unifiers
399
- if (l instanceof Unifier) {
458
+ if (typeof l == 'object' && l instanceof Unifier) {
400
459
  if (l.unify(r, ls, rs, env)) continue;
401
460
  return null;
402
461
  }
403
- if (r instanceof Unifier) {
462
+ if (typeof r == 'object' && r instanceof Unifier) {
404
463
  if (r.unify(l, ls, rs, env)) continue;
405
464
  return null;
406
465
  }
@@ -414,12 +473,17 @@ const unify = (l, r, env, options) => {
414
473
  if (typeof l == 'number' && isNaN(l) && isNaN(r)) continue;
415
474
  // cut off impossible combinations
416
475
  if (typeof l != 'object' || !l || !r) return null;
417
- // process registered constructors
418
- const registry = unify.registry;
419
- for (let i = 0; i < registry.length; i += 2) {
420
- if (l instanceof registry[i] || r instanceof registry[i]) {
421
- if (registry[i + 1](l, r, ls, rs, env)) continue main;
422
- return null;
476
+ // fast path: plain objects and arrays skip registry
477
+ const lp = Object.getPrototypeOf(l),
478
+ rp = Object.getPrototypeOf(r);
479
+ if (!((lp === Object.prototype || lp === null || lp === Array.prototype) && (rp === Object.prototype || rp === null || rp === Array.prototype))) {
480
+ // process registered constructors
481
+ const registry = unify.registry;
482
+ for (let i = 0; i < registry.length; i += 2) {
483
+ if (l instanceof registry[i] || r instanceof registry[i]) {
484
+ if (registry[i + 1](l, r, ls, rs, env)) continue main;
485
+ return null;
486
+ }
423
487
  }
424
488
  }
425
489
  // process registered filters
@@ -442,5 +506,5 @@ const unify = (l, r, env, options) => {
442
506
  unify.registry = registry;
443
507
  unify.filters = filters;
444
508
 
445
- export {_, Env, Unifier, isUnifier, Variable, variable, isVariable, _ as any, open, soft, isOpen, isSoft, isWrapped};
509
+ export {_, Env, Unifier, isUnifier, Variable, variable, isVariable, _ as any, open, soft, isOpen, isSoft, isWrapped, unify};
446
510
  export default unify;
@@ -0,0 +1,25 @@
1
+ // Type definitions for deep6 replaceVars utility
2
+ // Generated from src/utils/replaceVars.js
3
+
4
+ import type {Env, Variable} from '../env.js';
5
+
6
+ /**
7
+ * Creates a tagged template function that substitutes variables from an environment
8
+ *
9
+ * Template interpolations can be Variable instances (resolved via `.get(env)`),
10
+ * strings/numbers/symbols (looked up in `env.values`), or other values (coerced to string).
11
+ *
12
+ * @param env - Unification environment with variable bindings
13
+ * @returns A tagged template literal function
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const x = variable('x');
18
+ * const env = unify({a: x}, {a: 42});
19
+ * const t = replaceVars(env);
20
+ * t`The answer is ${x}!`; // "The answer is 42!"
21
+ * ```
22
+ */
23
+ export declare const replaceVars: (env: Env) => (strings: TemplateStringsArray, ...vars: unknown[]) => string;
24
+
25
+ export default replaceVars;
@@ -1,24 +1,28 @@
1
1
  import {isVariable} from '../env.js';
2
2
 
3
- const replaceVars = env => (strings, ...vars) => {
4
- let buffer = strings[0];
5
- for(let i = 0; i < vars.length;) {
6
- const v = vars[i];
7
- if (isVariable(v)) {
8
- buffer += v.get(env);
9
- } else switch (typeof v) {
10
- case 'string':
11
- case 'number':
12
- case 'symbol':
13
- buffer += env.values[v];
14
- break;
15
- default:
16
- buffer += v;
17
- break;
3
+ const replaceVars =
4
+ env =>
5
+ (strings, ...vars) => {
6
+ let buffer = strings[0];
7
+ for (let i = 0; i < vars.length; ) {
8
+ const v = vars[i];
9
+ if (isVariable(v)) {
10
+ buffer += v.get(env);
11
+ } else
12
+ switch (typeof v) {
13
+ case 'string':
14
+ case 'number':
15
+ case 'symbol':
16
+ buffer += env.values[v];
17
+ break;
18
+ default:
19
+ buffer += v;
20
+ break;
21
+ }
22
+ buffer += strings[++i];
18
23
  }
19
- buffer += strings[++i];
20
- }
21
- return buffer;
22
- };
24
+ return buffer;
25
+ };
23
26
 
27
+ export {replaceVars};
24
28
  export default replaceVars;
@@ -0,0 +1,43 @@
1
+ import type {IncomingMessage} from 'node:http';
2
+
3
+ /**
4
+ * Anything `asText` / `asJson` / `asBytes` / `header` / `headers` accept.
5
+ * Either a W3C `Response` (from `fetch`) or a Node `http.IncomingMessage`.
6
+ */
7
+ export type ResponseLike = Response | IncomingMessage;
8
+
9
+ /**
10
+ * Read the response body as a UTF-8 string. Works with both `Response`
11
+ * (delegates to `res.text()`) and `http.IncomingMessage` (drains the
12
+ * stream and decodes).
13
+ */
14
+ export function asText(res: ResponseLike): Promise<string>;
15
+
16
+ /**
17
+ * Read the response body and parse as JSON. Equivalent to
18
+ * `JSON.parse(await asText(res))`; uses `res.json()` directly when
19
+ * available.
20
+ */
21
+ export function asJson<T = unknown>(res: ResponseLike): Promise<T>;
22
+
23
+ /**
24
+ * Read the response body as raw bytes (`Uint8Array`). For
25
+ * `http.IncomingMessage`, drains the stream; for `Response`, delegates
26
+ * to `arrayBuffer()`.
27
+ */
28
+ export function asBytes(res: ResponseLike): Promise<Uint8Array>;
29
+
30
+ /**
31
+ * Read a single response header by name (case-insensitive). Returns
32
+ * `null` if the header isn't present (matching `Response.headers.get`
33
+ * semantics). For `http.IncomingMessage` headers that are arrays
34
+ * (e.g. `set-cookie`), values are joined with `, `.
35
+ */
36
+ export function header(res: ResponseLike, name: string): string | null;
37
+
38
+ /**
39
+ * Return all response headers as a plain object with lowercase keys.
40
+ * Array-valued headers (e.g. `set-cookie` on `IncomingMessage`) are
41
+ * joined with `, ` to match `Response.headers` shape.
42
+ */
43
+ export function headers(res: ResponseLike): Record<string, string>;
@@ -0,0 +1,57 @@
1
+ // @ts-self-types="./response.d.ts"
2
+
3
+ const isWebResponse = res =>
4
+ typeof res?.text === 'function' && typeof res?.headers?.get === 'function';
5
+
6
+ const drainAsBuffer = res =>
7
+ new Promise((resolve, reject) => {
8
+ const chunks = [];
9
+ res.on('data', chunk => chunks.push(chunk));
10
+ res.on('end', () => {
11
+ try {
12
+ resolve(Buffer.concat(chunks));
13
+ } catch (error) {
14
+ reject(error);
15
+ }
16
+ });
17
+ res.on('error', reject);
18
+ });
19
+
20
+ export const asText = async res => {
21
+ if (isWebResponse(res)) return await res.text();
22
+ const buffer = await drainAsBuffer(res);
23
+ return buffer.toString('utf8');
24
+ };
25
+
26
+ export const asJson = async res => {
27
+ if (isWebResponse(res)) return await res.json();
28
+ return JSON.parse(await asText(res));
29
+ };
30
+
31
+ export const asBytes = async res => {
32
+ if (isWebResponse(res)) {
33
+ const ab = await res.arrayBuffer();
34
+ return new Uint8Array(ab);
35
+ }
36
+ const buffer = await drainAsBuffer(res);
37
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
38
+ };
39
+
40
+ export const header = (res, name) => {
41
+ if (isWebResponse(res)) return res.headers.get(name);
42
+ const value = res.headers?.[name.toLowerCase()];
43
+ return value === undefined ? null : Array.isArray(value) ? value.join(', ') : value;
44
+ };
45
+
46
+ export const headers = res => {
47
+ const out = {};
48
+ if (isWebResponse(res)) {
49
+ for (const [k, v] of res.headers) out[k.toLowerCase()] = v;
50
+ return out;
51
+ }
52
+ if (!res.headers) return out;
53
+ for (const [k, v] of Object.entries(res.headers)) {
54
+ out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : v;
55
+ }
56
+ return out;
57
+ };
@@ -0,0 +1,81 @@
1
+ import type {RequestListener, Server} from 'node:http';
2
+
3
+ /**
4
+ * Result of starting a server. Properties reflect the actual bound address
5
+ * (e.g. `port` is the OS-assigned port when `0` was requested).
6
+ */
7
+ export interface ServerLifecycle {
8
+ /** The underlying `http.Server` instance. */
9
+ server: Server;
10
+ /** Base URL of the bound server, e.g. `"http://127.0.0.1:54321"`. */
11
+ base: string;
12
+ /** Actual port the server is listening on. */
13
+ port: number;
14
+ /** Host the server is bound to. */
15
+ host: string;
16
+ /**
17
+ * Close the server. Idempotent. Calls `server.closeAllConnections()`
18
+ * (when available) so keep-alive sockets don't delay teardown.
19
+ */
20
+ close(): Promise<void>;
21
+ }
22
+
23
+ /**
24
+ * Options for `startServer` / `withServer` / `setupServer`.
25
+ */
26
+ export interface ServerOptions {
27
+ /**
28
+ * Host to bind to. Defaults to `'127.0.0.1'` (explicit IPv4 — avoids
29
+ * dual-stack ambiguity on macOS where `'localhost'` may resolve to `::1`).
30
+ */
31
+ host?: string;
32
+ /** Port to bind to. Defaults to `0` (OS-assigned). */
33
+ port?: number;
34
+ }
35
+
36
+ /**
37
+ * Procedural primitive: start an `http.Server`, return a lifecycle handle.
38
+ *
39
+ * Races `'listening'` against `'error'` — port-busy / `EACCES` rejects,
40
+ * never hangs.
41
+ */
42
+ export function startServer(server: Server, opts?: ServerOptions): Promise<ServerLifecycle>;
43
+
44
+ /**
45
+ * Scoped resource: spin up a server with `serverHandler`, run `clientHandler`
46
+ * with the base URL, tear the server down in `finally`. Cleanup runs whether
47
+ * `clientHandler` resolves, rejects, or throws synchronously.
48
+ *
49
+ * `serverHandler` is the per-request callback (Node calls it once per
50
+ * incoming request). `clientHandler` is the per-scope test body (called once,
51
+ * with the base URL). Either side may be the SUT — naming reflects role on
52
+ * the wire, not which side is being tested.
53
+ */
54
+ export function withServer<T>(
55
+ serverHandler: RequestListener,
56
+ clientHandler: (base: string, lifecycle: ServerLifecycle) => Promise<T> | T,
57
+ opts?: ServerOptions
58
+ ): Promise<T>;
59
+
60
+ /**
61
+ * Hook-registering helper for suite-shared servers. Registers `beforeAll` to
62
+ * start the server and `afterAll` to close it; returns a live-getter context
63
+ * whose properties reflect the running server's state at access time.
64
+ *
65
+ * Per-test state reset (e.g. clearing a `recorded[]` array) stays user-side:
66
+ * compose your own `beforeEach`. `setupServer` owns the suite lifecycle; the
67
+ * caller owns suite state.
68
+ *
69
+ * Don't destructure the returned object at module load time — its properties
70
+ * read the live lifecycle on each access; destructuring captures stale
71
+ * `undefined` values.
72
+ */
73
+ export function setupServer(
74
+ serverHandler: RequestListener,
75
+ opts?: ServerOptions
76
+ ): Readonly<{
77
+ readonly server: Server | undefined;
78
+ readonly base: string | undefined;
79
+ readonly port: number | undefined;
80
+ readonly host: string | undefined;
81
+ }>;
package/src/server.js ADDED
@@ -0,0 +1,69 @@
1
+ // @ts-self-types="./server.d.ts"
2
+
3
+ import http from 'node:http';
4
+ import {once} from 'node:events';
5
+
6
+ import {beforeAll, afterAll} from './test.js';
7
+
8
+ const DEFAULT_HOST = '127.0.0.1';
9
+
10
+ export const startServer = async (server, {host = DEFAULT_HOST, port = 0} = {}) => {
11
+ const listening = once(server, 'listening');
12
+ const failure = once(server, 'error').then(([err]) => {
13
+ throw err;
14
+ });
15
+ server.listen(port, host);
16
+ await Promise.race([listening, failure]);
17
+
18
+ const addr = server.address();
19
+ const actualPort = typeof addr === 'object' && addr ? addr.port : port;
20
+ const base = `http://${host}:${actualPort}`;
21
+
22
+ let closed = false;
23
+ const close = async () => {
24
+ if (closed) return;
25
+ closed = true;
26
+ server.closeAllConnections?.();
27
+ server.close();
28
+ await once(server, 'close');
29
+ };
30
+
31
+ return {server, base, port: actualPort, host, close};
32
+ };
33
+
34
+ export const withServer = async (serverHandler, clientHandler, opts) => {
35
+ const server = http.createServer(serverHandler);
36
+ const lifecycle = await startServer(server, opts);
37
+ try {
38
+ return await clientHandler(lifecycle.base, lifecycle);
39
+ } finally {
40
+ await lifecycle.close();
41
+ }
42
+ };
43
+
44
+ export const setupServer = (serverHandler, opts) => {
45
+ let lifecycle = null;
46
+ beforeAll(async () => {
47
+ lifecycle = await startServer(http.createServer(serverHandler), opts);
48
+ });
49
+ afterAll(async () => {
50
+ if (lifecycle) {
51
+ await lifecycle.close();
52
+ lifecycle = null;
53
+ }
54
+ });
55
+ return Object.freeze({
56
+ get server() {
57
+ return lifecycle?.server;
58
+ },
59
+ get base() {
60
+ return lifecycle?.base;
61
+ },
62
+ get port() {
63
+ return lifecycle?.port;
64
+ },
65
+ get host() {
66
+ return lifecycle?.host;
67
+ }
68
+ });
69
+ };