@ix-xs/node-comfort 2.1.0 → 2.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -2
  4. package/src/Arr.js +13 -5
  5. package/src/Checker.js +56 -8
  6. package/src/Cli.js +60 -18
  7. package/src/Color.js +32 -6
  8. package/src/Crypto.js +429 -78
  9. package/src/Emitter.js +9 -9
  10. package/src/Env.js +90 -8
  11. package/src/FS.js +369 -59
  12. package/src/Func.js +1 -1
  13. package/src/Http.js +266 -75
  14. package/src/Id.js +53 -15
  15. package/src/Logger.js +100 -22
  16. package/src/Num.js +3 -2
  17. package/src/Obj.js +225 -36
  18. package/src/SQLite.js +155 -98
  19. package/src/Schema.js +281 -40
  20. package/src/Str.js +77 -29
  21. package/src/Sys.js +124 -23
  22. package/src/Time.js +98 -24
  23. package/src/Utils.js +18 -3
  24. package/src/errors.js +75 -4
  25. package/src/internal/shared.js +68 -14
  26. package/src/internal/terminal.js +49 -0
  27. package/types/index.d.ts +28 -5
  28. package/types/src/Arr.d.ts +4 -2
  29. package/types/src/Arr.d.ts.map +1 -1
  30. package/types/src/Checker.d.ts +29 -3
  31. package/types/src/Checker.d.ts.map +1 -1
  32. package/types/src/Cli.d.ts +6 -4
  33. package/types/src/Cli.d.ts.map +1 -1
  34. package/types/src/Color.d.ts +2 -2
  35. package/types/src/Color.d.ts.map +1 -1
  36. package/types/src/Crypto.d.ts +126 -28
  37. package/types/src/Crypto.d.ts.map +1 -1
  38. package/types/src/Emitter.d.ts +6 -6
  39. package/types/src/Emitter.d.ts.map +1 -1
  40. package/types/src/Env.d.ts +19 -1
  41. package/types/src/Env.d.ts.map +1 -1
  42. package/types/src/FS.d.ts +30 -23
  43. package/types/src/FS.d.ts.map +1 -1
  44. package/types/src/Func.d.ts +1 -1
  45. package/types/src/Http.d.ts +45 -8
  46. package/types/src/Http.d.ts.map +1 -1
  47. package/types/src/Id.d.ts.map +1 -1
  48. package/types/src/Logger.d.ts +11 -3
  49. package/types/src/Logger.d.ts.map +1 -1
  50. package/types/src/Num.d.ts +1 -1
  51. package/types/src/Num.d.ts.map +1 -1
  52. package/types/src/Obj.d.ts +15 -2
  53. package/types/src/Obj.d.ts.map +1 -1
  54. package/types/src/SQLite.d.ts +105 -13
  55. package/types/src/SQLite.d.ts.map +1 -1
  56. package/types/src/Schema.d.ts +61 -5
  57. package/types/src/Schema.d.ts.map +1 -1
  58. package/types/src/Str.d.ts +32 -7
  59. package/types/src/Str.d.ts.map +1 -1
  60. package/types/src/Sys.d.ts +43 -10
  61. package/types/src/Sys.d.ts.map +1 -1
  62. package/types/src/Time.d.ts +12 -7
  63. package/types/src/Time.d.ts.map +1 -1
  64. package/types/src/Utils.d.ts +16 -6
  65. package/types/src/Utils.d.ts.map +1 -1
  66. package/types/src/errors.d.ts +6 -2
  67. package/types/src/errors.d.ts.map +1 -1
  68. package/types/src/internal/shared.d.ts +7 -0
  69. package/types/src/internal/shared.d.ts.map +1 -1
  70. package/types/src/internal/terminal.d.ts +13 -0
  71. package/types/src/internal/terminal.d.ts.map +1 -0
package/src/Schema.js CHANGED
@@ -70,7 +70,30 @@ const { UNSAFE_KEYS } = require("./internal/shared.js");
70
70
  */
71
71
 
72
72
  /**
73
- * @typedef {{ issues: import("./errors").ValidationIssue[], path: Array<string | number> }} ParseContext
73
+ * Options for `parse()`, `safeParse()` and `is()`.
74
+ * @typedef {object} ParseOptions
75
+ * @property {number} [maxDepth] How many levels of objects and arrays a value may have. A value nested deeper is refused with a `"too_deep"` issue instead of running out of stack. Defaults to `1000`.
76
+ */
77
+
78
+ /**
79
+ * The state of a parse. `abortEarly` is set by `is()` and while a union
80
+ * tries its options: containers then stop at the first issue, and set
81
+ * `aborted` when they do. `memo` keeps what unions found on objects for
82
+ * the rest of the parse.
83
+ * @typedef {{ issues: import("./errors").ValidationIssue[], path: Array<string | number>, maxDepth: number, abortEarly: boolean, aborted: boolean, memo: { unions: WeakMap<object, Map<Schema<any>, _UnionMemo>> | undefined } | undefined }} ParseContext
84
+ */
85
+
86
+ /**
87
+ * How an option of a union failed when tried, with paths relative to the
88
+ * union. `aborted` says the issues are only the first ones.
89
+ * @typedef {{ issues: import("./errors").ValidationIssue[], aborted: boolean }} _Attempt
90
+ */
91
+
92
+ /**
93
+ * What a union found on a value: how each option failed when tried, and
94
+ * what it reported while another union tried its options (`fast`) or for
95
+ * the final report (`full`).
96
+ * @typedef {{ tried: _Attempt[], fast: _Attempt | undefined, full: _Attempt | undefined }} _UnionMemo
74
97
  */
75
98
 
76
99
  /**
@@ -93,6 +116,56 @@ const INVALID = Symbol("invalid");
93
116
  const _TRUE = ["true", "1", "yes", "on", "y"];
94
117
  const _FALSE = ["false", "0", "no", "off", "n", ""];
95
118
  const _DATE = /^\d{4}-\d{2}-\d{2}$/;
119
+ const _MAX_DEPTH = 1000;
120
+ // What a URL can't hold as written. new URL() would encode it, but the string returned is the raw one.
121
+ const _URL_UNSAFE = /[\u0000- \u007f<>"`\\]/;
122
+
123
+ /**
124
+ * A fresh context for a parse.
125
+ * @param {ParseOptions | undefined} options
126
+ * @returns {ParseContext}
127
+ */
128
+ const _context = (options) => ({ issues: [], path: [], maxDepth: options?.maxDepth ?? _MAX_DEPTH, abortEarly: false, aborted: false, memo: undefined });
129
+
130
+ /**
131
+ * A value for a message: JSON, except bigints (`5n`) and what JSON can't
132
+ * write, like a cycle.
133
+ * @param {unknown} value
134
+ * @returns {string}
135
+ */
136
+ const _show = (value) => {
137
+ if (typeof value === "bigint") return `${value}n`;
138
+ try {
139
+ return JSON.stringify(value) ?? String(value);
140
+ } catch {
141
+ try {
142
+ return JSON.stringify(value, (_key, v) => (typeof v === "bigint" ? `${v}n` : v));
143
+ } catch {
144
+ return _typeName(value);
145
+ }
146
+ }
147
+ };
148
+
149
+ /** @param {unknown} error */
150
+ const _isStackOverflow = (error) => error instanceof RangeError && /call stack/i.test(error.message);
151
+
152
+ /**
153
+ * Runs a schema from the top. A value nested so deep that the stack runs
154
+ * out becomes a `"too_deep"` issue instead of an exception.
155
+ * @param {Schema<any>} schema
156
+ * @param {unknown} value
157
+ * @param {ParseContext} ctx
158
+ * @returns {unknown}
159
+ */
160
+ const _runRoot = (schema, value, ctx) => {
161
+ try {
162
+ return /** @type {any} */ (schema)._run(value, ctx);
163
+ } catch (error) {
164
+ if (!_isStackOverflow(error)) throw error;
165
+ ctx.issues.push({ path: [...ctx.path], code: "too_deep", message: "Nested too deeply" });
166
+ return INVALID;
167
+ }
168
+ };
96
169
 
97
170
  /**
98
171
  * What a failed `safeParse()` returns. The error is only built if it's
@@ -150,6 +223,47 @@ const _typeIssue = (ctx, expected, value, message) => {
150
223
  _issue(ctx, "invalid_type", message ? _message(message, value) : value === undefined ? "Required" : `Expected ${expected}, received ${received}`, { expected, received });
151
224
  };
152
225
 
226
+ /**
227
+ * Refuses a container nested deeper than allowed. The path holds one key
228
+ * per level, so its length is the depth.
229
+ * @param {ParseContext} ctx
230
+ * @returns {boolean} `true` when the issue was added.
231
+ */
232
+ const _tooDeep = (ctx) => {
233
+ if (ctx.path.length < ctx.maxDepth) return false;
234
+ _issue(ctx, "too_deep", `Nested more than ${ctx.maxDepth} levels deep`, { maximum: ctx.maxDepth });
235
+ return true;
236
+ };
237
+
238
+ /**
239
+ * Issues with their path made relative to a depth.
240
+ * @param {import("./errors").ValidationIssue[]} issues
241
+ * @param {number} depth
242
+ * @returns {import("./errors").ValidationIssue[]}
243
+ */
244
+ const _from = (issues, depth) => (depth === 0 ? issues : issues.map((issue) => ({ ...issue, path: issue.path.slice(depth) })));
245
+
246
+ /**
247
+ * Relative issues placed under a path.
248
+ * @param {import("./errors").ValidationIssue[]} issues
249
+ * @param {Array<string | number>} path
250
+ * @returns {import("./errors").ValidationIssue[]}
251
+ */
252
+ const _at = (issues, path) => issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] }));
253
+
254
+ /**
255
+ * Ends a container early while a union tries an option: one issue is
256
+ * enough to know the option doesn't fit.
257
+ * @param {ParseContext} ctx
258
+ * @param {number} before
259
+ * @returns {boolean}
260
+ */
261
+ const _stop = (ctx, before) => {
262
+ if (!ctx.abortEarly || ctx.issues.length === before) return false;
263
+ ctx.aborted = true;
264
+ return true;
265
+ };
266
+
153
267
  /**
154
268
  * Runs a nested schema.
155
269
  * @param {Schema<any>} schema
@@ -159,6 +273,18 @@ const _typeIssue = (ctx, expected, value, message) => {
159
273
  */
160
274
  const _runChild = (schema, value, ctx) => /** @type {any} */ (schema)._run(value, ctx);
161
275
 
276
+ /**
277
+ * Reads a key of an object. What every object inherits, like toString,
278
+ * is not data.
279
+ * @param {Record<string, unknown>} input
280
+ * @param {string} key
281
+ * @returns {unknown}
282
+ */
283
+ const _read = (input, key) => {
284
+ const raw = input[key];
285
+ return typeof raw === "function" && !Object.hasOwn(input, key) ? undefined : raw;
286
+ };
287
+
162
288
  /**
163
289
  * The base of every schema.
164
290
  *
@@ -232,7 +358,11 @@ class Schema {
232
358
  if (ctx.issues.length === before) {
233
359
  for (const step of this._steps) {
234
360
  if (step.kind === "check") {
235
- if (!step.test(out)) _issue(ctx, step.code, _message(step.message, out), step.details);
361
+ if (!step.test(out)) {
362
+ _issue(ctx, step.code, _message(step.message, out), step.details);
363
+ // A value already too long is refused: the checks after, a regex above all, could be very slow on it.
364
+ if ((typeof out === "string" || Array.isArray(out)) && (step.code === "too_big" || (step.code === "invalid_length" && out.length > Number(step.details?.exact)))) break;
365
+ }
236
366
  } else if (ctx.issues.length > before) {
237
367
  // What follows needs a value that passed everything before it.
238
368
  if (step.kind !== "refine") break;
@@ -267,12 +397,13 @@ class Schema {
267
397
  * const port = s.coerce.number().int().min(1).max(65535).parse(process.env.PORT);
268
398
  *
269
399
  * @param {unknown} value
400
+ * @param {ParseOptions} [options]
270
401
  * @returns {T}
271
402
  * @throws {ValidationError} Listing every issue with its path.
272
403
  */
273
- parse(value) {
274
- const ctx = { issues: [], path: [] };
275
- const out = this._run(value, ctx);
404
+ parse(value, options) {
405
+ const ctx = _context(options);
406
+ const out = _runRoot(this, value, ctx);
276
407
  if (ctx.issues.length) throw new ValidationError(ctx.issues);
277
408
  return /** @type {T} */ (out);
278
409
  }
@@ -286,12 +417,12 @@ class Schema {
286
417
  * saveUser(result.data);
287
418
  *
288
419
  * @param {unknown} value
420
+ * @param {ParseOptions} [options]
289
421
  * @returns {SafeParseResult<T>}
290
422
  */
291
- safeParse(value) {
292
- /** @type {ParseContext} */
293
- const ctx = { issues: [], path: [] };
294
- const out = this._run(value, ctx);
423
+ safeParse(value, options) {
424
+ const ctx = _context(options);
425
+ const out = _runRoot(this, value, ctx);
295
426
  if (ctx.issues.length) return _failure(ctx.issues);
296
427
  return { success: true, data: /** @type {T} */ (out) };
297
428
  }
@@ -303,12 +434,14 @@ class Schema {
303
434
  * if (Email.is(input)) sendMail(input); // input is a string here
304
435
  *
305
436
  * @param {unknown} value
437
+ * @param {ParseOptions} [options]
306
438
  * @returns {value is T}
307
439
  */
308
- is(value) {
309
- /** @type {ParseContext} */
310
- const ctx = { issues: [], path: [] };
311
- this._run(value, ctx);
440
+ is(value, options) {
441
+ const ctx = _context(options);
442
+ // No report to build: the first issue answers.
443
+ ctx.abortEarly = true;
444
+ _runRoot(this, value, ctx);
312
445
  return ctx.issues.length === 0;
313
446
  }
314
447
 
@@ -544,7 +677,14 @@ class StringSchema extends Schema {
544
677
  */
545
678
  _check(value, ctx) {
546
679
  let v = value;
547
- if (this._coerce && v !== undefined && v !== null && typeof v !== "string") v = String(v);
680
+ if (this._coerce && v !== undefined && v !== null && typeof v !== "string") {
681
+ try {
682
+ v = String(v);
683
+ } catch (error) {
684
+ // An array nested too deep to join, or an object without toString().
685
+ if (_isStackOverflow(error)) return _issue(ctx, "too_deep", "Nested too deeply");
686
+ }
687
+ }
548
688
  if (typeof v !== "string") return _typeIssue(ctx, "string", value, this._typeMessage);
549
689
  return v;
550
690
  }
@@ -612,14 +752,16 @@ class StringSchema extends Schema {
612
752
  }
613
753
 
614
754
  /**
615
- * An absolute URL.
755
+ * An absolute URL. The string is returned as written, so spaces, control
756
+ * characters, double quotes, backticks, `<`, `>` and backslashes are
757
+ * refused: a URL that needs them must come percent-encoded.
616
758
  * @param {Message | { message?: Message, protocols?: string[] }} [options] A message, or allowed protocols (http and https by default).
617
759
  * @returns {this}
618
760
  */
619
761
  url(options = "Invalid URL") {
620
762
  const opts = typeof options === "object" ? options : { message: options };
621
763
  const { isURL } = require("./Checker.js");
622
- return this._rule((v) => isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" }, { format: "uri" });
764
+ return this._rule((v) => !_URL_UNSAFE.test(v) && isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" }, { format: "uri" });
623
765
  }
624
766
 
625
767
  /**
@@ -1104,9 +1246,9 @@ class LiteralSchema extends Schema {
1104
1246
  _check(value, ctx) {
1105
1247
  // A Set treats 0 and -0 as the same value: tell them apart, as before.
1106
1248
  if (this._set.has(value) && (value !== 0 || this._values.some((v) => Object.is(v, value)))) return value;
1107
- const list = this._values.map((v) => JSON.stringify(v)).join(" | ");
1249
+ const list = this._values.map(_show).join(" | ");
1108
1250
  const code = this._values.length > 1 ? "invalid_enum_value" : "invalid_literal";
1109
- return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${JSON.stringify(value) ?? String(value)}`, { expected: list, received: _typeName(value) });
1251
+ return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${_show(value)}`, { expected: list, received: _typeName(value) });
1110
1252
  }
1111
1253
 
1112
1254
  /**
@@ -1183,13 +1325,16 @@ class ArraySchema extends Schema {
1183
1325
  */
1184
1326
  _check(value, ctx) {
1185
1327
  if (!Array.isArray(value)) return _typeIssue(ctx, "array", value, this._typeMessage);
1328
+ if (_tooDeep(ctx)) return INVALID;
1186
1329
  const item = this._item;
1187
1330
  const path = ctx.path;
1331
+ const before = ctx.issues.length;
1188
1332
  const out = new Array(value.length);
1189
1333
  for (let i = 0; i < value.length; i++) {
1190
1334
  path.push(i);
1191
1335
  out[i] = _runChild(item, value[i], ctx);
1192
1336
  path.pop();
1337
+ if (_stop(ctx, before)) return INVALID;
1193
1338
  }
1194
1339
  return out;
1195
1340
  }
@@ -1295,6 +1440,10 @@ class ObjectSchema extends Schema {
1295
1440
  this._keys = Object.keys(shape).filter((key) => key !== "__proto__");
1296
1441
  /** @protected @type {Array<Schema<any>>} */
1297
1442
  this._schemas = this._keys.map((key) => shape[key]);
1443
+ // Keys holding a literal or an enum tell the options of a union apart: they are checked first there.
1444
+ /** @protected @type {number[]} */
1445
+ this._literals = [];
1446
+ for (let i = 0; i < this._schemas.length; i++) if (this._schemas[i] instanceof LiteralSchema) this._literals.push(i);
1298
1447
  }
1299
1448
 
1300
1449
  /**
@@ -1306,20 +1455,35 @@ class ObjectSchema extends Schema {
1306
1455
  */
1307
1456
  _check(value, ctx) {
1308
1457
  if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
1458
+ if (_tooDeep(ctx)) return INVALID;
1309
1459
  const input = /** @type {Record<string, unknown>} */ (value);
1310
1460
  const keys = this._keys;
1311
1461
  const schemas = this._schemas;
1312
1462
  const path = ctx.path;
1463
+ const before = ctx.issues.length;
1464
+ /** @type {unknown[] | undefined} */
1465
+ let early;
1466
+ if (ctx.abortEarly && this._literals.length) {
1467
+ early = new Array(keys.length);
1468
+ for (const i of this._literals) {
1469
+ path.push(keys[i]);
1470
+ early[i] = _runChild(schemas[i], _read(input, keys[i]), ctx);
1471
+ path.pop();
1472
+ if (_stop(ctx, before)) return INVALID;
1473
+ }
1474
+ }
1313
1475
  /** @type {Record<string, unknown>} */
1314
1476
  const out = {};
1315
1477
  for (let i = 0; i < keys.length; i++) {
1316
1478
  const key = keys[i];
1317
- let raw = input[key];
1318
- // What every object inherits, like toString, is not data.
1319
- if (typeof raw === "function" && !Object.hasOwn(input, key)) raw = undefined;
1320
- path.push(key);
1321
- const result = _runChild(schemas[i], raw, ctx);
1322
- path.pop();
1479
+ let result;
1480
+ if (early !== undefined && i in early) result = early[i];
1481
+ else {
1482
+ path.push(key);
1483
+ result = _runChild(schemas[i], _read(input, key), ctx);
1484
+ path.pop();
1485
+ if (_stop(ctx, before)) return INVALID;
1486
+ }
1323
1487
  if (result !== undefined || Object.hasOwn(input, key)) out[key] = result;
1324
1488
  }
1325
1489
  if (this._unknown === "strip") return out;
@@ -1510,24 +1674,95 @@ class UnionSchema extends Schema {
1510
1674
  * @protected
1511
1675
  */
1512
1676
  _check(value, ctx) {
1513
- /** @type {import("./errors").ValidationIssue[][]} */
1514
- const failures = [];
1515
- for (const option of this._options) {
1516
- const attempt = { issues: [], path: ctx.path };
1517
- const out = _runChild(option, value, attempt);
1518
- if (!attempt.issues.length) return out;
1519
- failures.push(attempt.issues);
1677
+ const options = this._options;
1678
+ const root = ctx.path.length;
1679
+ // What a union found on an object is remembered for the rest of the
1680
+ // parse, with paths relative to the union: the options of an outer
1681
+ // union often lead to the same nested value.
1682
+ const isObject = value !== null && typeof value === "object";
1683
+ const outermost = ctx.memo === undefined;
1684
+ /** @type {_UnionMemo | undefined} */
1685
+ let memo;
1686
+ if (isObject) {
1687
+ memo = (ctx.memo ??= { unions: undefined }).unions?.get(value)?.get(this);
1688
+ const saved = ctx.abortEarly ? memo?.fast : memo?.full;
1689
+ if (saved) {
1690
+ ctx.issues.push(..._at(saved.issues, ctx.path));
1691
+ if (saved.aborted) ctx.aborted = true;
1692
+ return INVALID;
1693
+ }
1520
1694
  }
1521
- const typeOnly = failures.every((issues) => issues.length === 1 && issues[0].code === "invalid_type" && issues[0].path.length === ctx.path.length);
1522
- if (typeOnly) {
1523
- const expected = failures.map((issues) => issues[0].expected).join(" | ");
1524
- return _issue(ctx, "invalid_union", `Expected ${expected}, received ${_typeName(value)}`, { expected, received: _typeName(value) });
1695
+ // Each option is first tried in a context that stops at its first
1696
+ // issue. Trying every option in full made each level of a recursive
1697
+ // union cost twice the one below, so a small input could take seconds.
1698
+ /** @type {_Attempt[]} */
1699
+ let tried;
1700
+ if (memo) tried = memo.tried.map((attempt) => ({ issues: _at(attempt.issues, ctx.path), aborted: attempt.aborted }));
1701
+ else {
1702
+ tried = [];
1703
+ for (let i = 0; i < options.length; i++) {
1704
+ /** @type {ParseContext} */
1705
+ const attempt = { issues: [], path: ctx.path, maxDepth: ctx.maxDepth, abortEarly: true, aborted: false, memo: ctx.memo };
1706
+ const out = _runChild(options[i], value, attempt);
1707
+ if (!attempt.issues.length) return out;
1708
+ tried.push(attempt);
1709
+ }
1710
+ }
1711
+ /** @type {import("./errors").ValidationIssue[]} */
1712
+ let issues;
1713
+ let aborted = false;
1714
+ if (ctx.abortEarly) {
1715
+ // Another union is trying its options, or is() only asks yes or no: one issue is enough, unless every one is already known.
1716
+ aborted = tried.some((attempt) => attempt.aborted);
1717
+ issues = aborted ? [tried[0].issues[0]] : this._report(tried.map((attempt) => attempt.issues), value, ctx.path);
1718
+ } else {
1719
+ // The options that stopped early run again, in full, for the report.
1720
+ /** @type {import("./errors").ValidationIssue[][]} */
1721
+ const full = [];
1722
+ for (let i = 0; i < tried.length; i++) {
1723
+ if (!tried[i].aborted) {
1724
+ full.push(tried[i].issues);
1725
+ continue;
1726
+ }
1727
+ /** @type {ParseContext} */
1728
+ const again = { issues: [], path: ctx.path, maxDepth: ctx.maxDepth, abortEarly: false, aborted: false, memo: ctx.memo };
1729
+ const out = _runChild(options[i], value, again);
1730
+ if (!again.issues.length) return out;
1731
+ full.push(again.issues);
1732
+ }
1733
+ issues = this._report(full, value, ctx.path);
1734
+ }
1735
+ // Only a union inside another one can meet the same value again.
1736
+ if (isObject && !outermost && ctx.memo) {
1737
+ const unions = (ctx.memo.unions ??= new WeakMap());
1738
+ let known = unions.get(value);
1739
+ if (!known) unions.set(value, (known = new Map()));
1740
+ memo ??= { tried: tried.map((attempt) => ({ issues: _from(attempt.issues, root), aborted: attempt.aborted })), fast: undefined, full: undefined };
1741
+ known.set(this, memo);
1742
+ memo[ctx.abortEarly ? "fast" : "full"] = { issues: _from(issues, root), aborted };
1525
1743
  }
1526
- const best = failures.reduce((a, b) => (b.length < a.length ? b : a));
1527
- ctx.issues.push(...best);
1744
+ ctx.issues.push(...issues);
1745
+ if (aborted) ctx.aborted = true;
1528
1746
  return INVALID;
1529
1747
  }
1530
1748
 
1749
+ /**
1750
+ * The issues to report once every option failed: a single one when no
1751
+ * option had the right type, or those of the option with the fewest.
1752
+ * @param {import("./errors").ValidationIssue[][]} failures
1753
+ * @param {unknown} value
1754
+ * @param {Array<string | number>} path Where the union is.
1755
+ * @returns {import("./errors").ValidationIssue[]}
1756
+ * @protected
1757
+ */
1758
+ _report(failures, value, path) {
1759
+ if (failures.every((issues) => issues.length === 1 && issues[0].code === "invalid_type" && issues[0].path.length === path.length)) {
1760
+ const expected = failures.map((issues) => issues[0].expected).join(" | ");
1761
+ return [{ path: [...path], code: "invalid_union", message: `Expected ${expected}, received ${_typeName(value)}`, expected, received: _typeName(value) }];
1762
+ }
1763
+ return failures.reduce((a, b) => (b.length < a.length ? b : a));
1764
+ }
1765
+
1531
1766
  /**
1532
1767
  * @param {_JSONContext} ctx
1533
1768
  * @returns {JSONSchema}
@@ -1562,7 +1797,7 @@ class DiscriminatedUnionSchema extends Schema {
1562
1797
  const values = /** @type {any} */ (option)?._shape?.[key]?._values;
1563
1798
  if (!Array.isArray(values)) throw new TypeError(`discriminatedUnion(): every option must be an object with a literal "${key}"`);
1564
1799
  for (const value of values) {
1565
- if (this._byValue.has(value)) throw new TypeError(`discriminatedUnion(): two options have "${key}" set to ${JSON.stringify(value)}`);
1800
+ if (this._byValue.has(value)) throw new TypeError(`discriminatedUnion(): two options have "${key}" set to ${_show(value)}`);
1566
1801
  this._byValue.set(value, option);
1567
1802
  }
1568
1803
  }
@@ -1580,9 +1815,9 @@ class DiscriminatedUnionSchema extends Schema {
1580
1815
  const tag = /** @type {Record<string, unknown>} */ (value)[this._key];
1581
1816
  const option = this._byValue.get(tag);
1582
1817
  if (option) return _runChild(option, value, ctx);
1583
- const expected = [...this._byValue.keys()].map((known) => JSON.stringify(known)).join(" | ");
1818
+ const expected = [...this._byValue.keys()].map(_show).join(" | ");
1584
1819
  ctx.path.push(this._key);
1585
- _issue(ctx, "invalid_union_discriminator", `Expected ${expected}, received ${JSON.stringify(tag) ?? String(tag)}`, { expected, received: _typeName(tag) });
1820
+ _issue(ctx, "invalid_union_discriminator", `Expected ${expected}, received ${_show(tag)}`, { expected, received: _typeName(tag) });
1586
1821
  ctx.path.pop();
1587
1822
  return INVALID;
1588
1823
  }
@@ -1624,12 +1859,15 @@ class TupleSchema extends Schema {
1624
1859
  if (value.length !== this._items.length) {
1625
1860
  return _issue(ctx, "invalid_length", `Expected ${this._items.length} items, received ${value.length}`);
1626
1861
  }
1862
+ if (_tooDeep(ctx)) return INVALID;
1627
1863
  const path = ctx.path;
1864
+ const before = ctx.issues.length;
1628
1865
  const out = new Array(this._items.length);
1629
1866
  for (let i = 0; i < out.length; i++) {
1630
1867
  path.push(i);
1631
1868
  out[i] = _runChild(this._items[i], value[i], ctx);
1632
1869
  path.pop();
1870
+ if (_stop(ctx, before)) return INVALID;
1633
1871
  }
1634
1872
  return out;
1635
1873
  }
@@ -1673,8 +1911,10 @@ class RecordSchema extends Schema {
1673
1911
  */
1674
1912
  _check(value, ctx) {
1675
1913
  if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
1914
+ if (_tooDeep(ctx)) return INVALID;
1676
1915
  const input = /** @type {Record<string, unknown>} */ (value);
1677
1916
  const path = ctx.path;
1917
+ const before = ctx.issues.length;
1678
1918
  /** @type {Record<string, unknown>} */
1679
1919
  const out = {};
1680
1920
  for (const key of Object.keys(input)) {
@@ -1683,6 +1923,7 @@ class RecordSchema extends Schema {
1683
1923
  const k = this._keys ? _runChild(this._keys, key, ctx) : key;
1684
1924
  const v = _runChild(this._values, input[key], ctx);
1685
1925
  path.pop();
1926
+ if (_stop(ctx, before)) return INVALID;
1686
1927
  if (typeof k === "string" && !UNSAFE_KEYS.has(k)) out[k] = v;
1687
1928
  }
1688
1929
  return out;