@ambarltd/core 0.1.17

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 (46) hide show
  1. package/README.md +10 -0
  2. package/dist/callable.d.ts +1 -0
  3. package/dist/callable.js +6 -0
  4. package/dist/future.d.ts +102 -0
  5. package/dist/future.js +164 -0
  6. package/dist/helpers/object.d.ts +9 -0
  7. package/dist/helpers/object.js +31 -0
  8. package/dist/json/decoder.d.ts +99 -0
  9. package/dist/json/decoder.js +213 -0
  10. package/dist/json/encoder.d.ts +50 -0
  11. package/dist/json/encoder.js +115 -0
  12. package/dist/json/schema.d.ts +78 -0
  13. package/dist/json/schema.js +168 -0
  14. package/dist/json/types.d.ts +6 -0
  15. package/dist/json/types.js +1 -0
  16. package/dist/list.d.ts +35 -0
  17. package/dist/list.js +130 -0
  18. package/dist/maybe.d.ts +104 -0
  19. package/dist/maybe.js +106 -0
  20. package/dist/remote-data.d.ts +117 -0
  21. package/dist/remote-data.js +148 -0
  22. package/dist/result.d.ts +75 -0
  23. package/dist/result.js +126 -0
  24. package/dist/router.d.ts +117 -0
  25. package/dist/router.js +106 -0
  26. package/dist/test.d.ts +63 -0
  27. package/dist/test.js +470 -0
  28. package/dist/time.d.ts +118 -0
  29. package/dist/time.js +387 -0
  30. package/dist/tracing/opentelemetry.d.ts +27 -0
  31. package/dist/tracing/opentelemetry.js +215 -0
  32. package/dist/tracing/proxy.d.ts +20 -0
  33. package/dist/tracing/proxy.js +103 -0
  34. package/dist/tracing/simple.d.ts +10 -0
  35. package/dist/tracing/simple.js +224 -0
  36. package/dist/tracing.d.ts +29 -0
  37. package/dist/tracing.js +55 -0
  38. package/dist/trampoline.d.ts +24 -0
  39. package/dist/trampoline.js +46 -0
  40. package/dist/tree-map.d.ts +73 -0
  41. package/dist/tree-map.js +169 -0
  42. package/dist/tree-set.d.ts +63 -0
  43. package/dist/tree-set.js +114 -0
  44. package/dist/types.d.ts +21 -0
  45. package/dist/types.js +1 -0
  46. package/package.json +49 -0
package/dist/test.js ADDED
@@ -0,0 +1,470 @@
1
+ import { object } from "@optique/core/constructs";
2
+ import { multiple } from "@optique/core/modifiers";
3
+ import { option } from "@optique/core/primitives";
4
+ import { run as runCli } from "@optique/run";
5
+ import { message } from "@optique/core/message";
6
+ import { randomUUID } from "node:crypto";
7
+ export { run, expect, group, test, parseArgs };
8
+ class Test {
9
+ name;
10
+ fun;
11
+ id;
12
+ constructor(name, fun) {
13
+ this.name = name;
14
+ this.fun = fun;
15
+ this.id = randomUUID();
16
+ }
17
+ }
18
+ class Group {
19
+ name;
20
+ entries;
21
+ constructor(name, entries) {
22
+ this.name = name;
23
+ this.entries = entries;
24
+ }
25
+ }
26
+ const test = (x, y) => new Test(x, y);
27
+ const group = (x, y) => new Group(x, y);
28
+ const INDENT = " ";
29
+ const USE_COLORS = process.stdout.isTTY;
30
+ const PROGRESS_UPDATE_INTERVAL = 500;
31
+ function red(txt) {
32
+ return USE_COLORS ? `\x1b[31m${txt}\x1b[0m` : txt;
33
+ }
34
+ function green(txt) {
35
+ return USE_COLORS ? `\x1b[32m${txt}\x1b[0m` : txt;
36
+ }
37
+ function gray(txt) {
38
+ return USE_COLORS ? `\x1b[90m${txt}\x1b[0m` : txt;
39
+ }
40
+ function italic(txt) {
41
+ return USE_COLORS ? `\x1b[3m${txt}\x1b[0m` : txt;
42
+ }
43
+ function yellow(txt) {
44
+ return USE_COLORS ? `\x1b[33m${txt}\x1b[0m` : txt;
45
+ }
46
+ function collectTestInfos(group, parentPath, startOrder) {
47
+ const currentPath = [...parentPath, group.name];
48
+ let currentOrder = startOrder;
49
+ const allInfos = new Map();
50
+ for (const entry of group.entries) {
51
+ if (entry instanceof Test) {
52
+ const fullName = currentPath.concat(entry.name).join(" / ");
53
+ allInfos.set(entry.id, {
54
+ fullName,
55
+ order: currentOrder,
56
+ state: { type: "not-started" },
57
+ });
58
+ currentOrder++;
59
+ }
60
+ else if (entry instanceof Group) {
61
+ const result = collectTestInfos(entry, currentPath, currentOrder);
62
+ for (const [key, value] of result.infos) {
63
+ allInfos.set(key, value);
64
+ }
65
+ currentOrder = result.nextOrder;
66
+ }
67
+ }
68
+ return { infos: allInfos, nextOrder: currentOrder };
69
+ }
70
+ function buildProgressDisplay(executionInfos) {
71
+ const total = executionInfos.size;
72
+ const stats = Array.from(executionInfos.values()).reduce((acc, info) => {
73
+ if (info.state.type === "finished") {
74
+ return {
75
+ ...acc,
76
+ completed: acc.completed + 1,
77
+ successes: info.state.failure === null ? acc.successes + 1 : acc.successes,
78
+ failures: info.state.failure !== null ? acc.failures + 1 : acc.failures,
79
+ };
80
+ }
81
+ else if (info.state.type === "started") {
82
+ const elapsed = Date.now() - info.state.start;
83
+ return {
84
+ ...acc,
85
+ running: [
86
+ ...acc.running,
87
+ {
88
+ name: info.fullName,
89
+ duration: prettyTime(elapsed),
90
+ startTime: info.state.start,
91
+ },
92
+ ],
93
+ };
94
+ }
95
+ return acc;
96
+ }, {
97
+ completed: 0,
98
+ successes: 0,
99
+ failures: 0,
100
+ running: [],
101
+ });
102
+ const recentRunning = stats.running.sort((a, b) => a.startTime - b.startTime).slice(-6);
103
+ return [
104
+ gray(`Running ${stats.completed}/${total} ... (${green(`✓ ${stats.successes}`)} / ${red(`✗ ${stats.failures}`)})`),
105
+ ...recentRunning.map(({ name, duration }) => ` ${name} - ${gray(duration)}`),
106
+ ];
107
+ }
108
+ function startProgressDisplay(executionInfos) {
109
+ let lastHeight = 0;
110
+ let stopped = false;
111
+ const render = () => {
112
+ if (stopped)
113
+ return;
114
+ const lines = buildProgressDisplay(executionInfos);
115
+ if (lastHeight > 0) {
116
+ process.stdout.write(`\x1b[${lastHeight}A`);
117
+ }
118
+ for (const line of lines) {
119
+ process.stdout.write("\r");
120
+ process.stdout.write("\x1b[2K");
121
+ process.stdout.write(line);
122
+ process.stdout.write("\n");
123
+ }
124
+ if (lines.length < lastHeight) {
125
+ const extraLines = lastHeight - lines.length;
126
+ for (let i = 0; i < extraLines; i++) {
127
+ process.stdout.write("\r\x1b[2K\n");
128
+ }
129
+ process.stdout.write(`\x1b[${extraLines}A`);
130
+ }
131
+ lastHeight = lines.length;
132
+ };
133
+ render();
134
+ const interval = setInterval(render, PROGRESS_UPDATE_INTERVAL);
135
+ return {
136
+ stop: () => {
137
+ stopped = true;
138
+ clearInterval(interval);
139
+ },
140
+ clear: () => {
141
+ if (lastHeight > 0) {
142
+ process.stdout.write(`\x1b[${lastHeight}A`);
143
+ process.stdout.write("\x1b[0J");
144
+ lastHeight = 0;
145
+ }
146
+ },
147
+ };
148
+ }
149
+ /**
150
+ * Run a test suite. This should be the entry point of a test program.
151
+ *
152
+ * A sane, simple testing framework. Instead of a complicated test setup which
153
+ * finds and compiles files, we do the simplest obvious thing: a function that
154
+ * takes a list of tests. To run the tests execute a Node.js program that calls
155
+ * this `run` function.
156
+ *
157
+ * ```ts
158
+ * import { run, test, group, expect, parseArgs } from "@ambarltd/core/test";
159
+ *
160
+ * run(parseArgs(), [
161
+ * group("trivial tests", [
162
+ * test("referential equality", () => expect.equals(1, 3)),
163
+ * test("structural equality", () => expect.json_equals({}, {})),
164
+ * test("async test", async () => {
165
+ * const n = await fetchNumberFromTheInternet();
166
+ * expect.equals(1, n);
167
+ * }),
168
+ * ]),
169
+ * ]);
170
+ * ```
171
+ *
172
+ * If you want tests in different files, just import them like you would
173
+ * in a normal program.
174
+ *
175
+ * ```ts
176
+ * import { run } from "@ambarltd/core/test";
177
+ * import * as unit from "@test/unitTests";
178
+ * import * as integration from "@test/integrationTests";
179
+ *
180
+ * run(parseArgs(), [unit.tests, integration.tests]);
181
+ * ```
182
+ */
183
+ async function run(options, groups) {
184
+ const filters = options.filters || [];
185
+ const selectedGroups = filterGroups(groups, filters);
186
+ if (filters.length > 0 && selectedGroups.length === 0) {
187
+ console.log("No tests matched the provided filters.");
188
+ process.exit(0);
189
+ }
190
+ const collectedData = selectedGroups.reduce((acc, group) => {
191
+ const result = collectTestInfos(group, [], acc.nextOrder);
192
+ const mergedMap = new Map([...acc.infos, ...result.infos]);
193
+ return { infos: mergedMap, nextOrder: result.nextOrder };
194
+ }, { infos: new Map(), nextOrder: 0 });
195
+ const executionInfos = collectedData.infos;
196
+ const totalTests = executionInfos.size;
197
+ const before = Date.now();
198
+ const progressDisplay = startProgressDisplay(executionInfos);
199
+ const results = await Promise.all(selectedGroups.map(group => runGroup(group, executionInfos)));
200
+ progressDisplay.stop();
201
+ progressDisplay.clear();
202
+ process.stdout.write(`\r\x1b[K`);
203
+ const after = Date.now();
204
+ logResults(results);
205
+ const failures = getFailures(results);
206
+ const failed = failures.length > 0;
207
+ console.log("");
208
+ if (!failed) {
209
+ console.log(green("✓ Success"));
210
+ }
211
+ else {
212
+ console.log("\nFailures:\n");
213
+ failures.forEach(logFailure);
214
+ console.log(red("✗ Failed"));
215
+ }
216
+ console.log(`Tests: ${totalTests - failures.length} passed, ${totalTests} total`);
217
+ console.log(`Duration: ${prettyTime(after - before)}`);
218
+ process.exit(failed ? 1 : 0);
219
+ }
220
+ function filterGroups(groups, filters) {
221
+ if (filters.length === 0) {
222
+ return groups;
223
+ }
224
+ return groups.flatMap(group => {
225
+ const filtered = filterGroup(group, filters, []);
226
+ return filtered ? [filtered] : [];
227
+ });
228
+ }
229
+ function logResults(results) {
230
+ function showLeaf(lvl, name, duration, failure) {
231
+ const color = failure === null ? green : red;
232
+ const symbol = failure === null ? "✓" : "✗";
233
+ console.log(`${INDENT.repeat(lvl)}${color(symbol)} ${name} ${gray(`(${prettyTime(duration)})`)}`);
234
+ }
235
+ function showNode(lvl, name, children) {
236
+ console.log(`${INDENT.repeat(lvl)}${italic(yellow(name))}`);
237
+ showTrees(lvl + 1, children);
238
+ }
239
+ function showTrees(lvl, trees) {
240
+ trees.forEach(tree => {
241
+ if (tree.type === "node") {
242
+ showNode(lvl, tree.name, tree.children);
243
+ }
244
+ else {
245
+ showLeaf(lvl, tree.name, tree.duration, tree.failure);
246
+ }
247
+ });
248
+ }
249
+ showTrees(0, results);
250
+ }
251
+ function getFailures(results) {
252
+ function fromOne(path, duration, reason) {
253
+ return reason === null ? [] : [{ path, reason, duration }];
254
+ }
255
+ function fromMany(path, rs) {
256
+ return rs.flatMap(r => {
257
+ const rpath = `${path}${path ? " / " : ""}${r.name}`;
258
+ return r.type === "node" ? fromMany(rpath, r.children) : fromOne(rpath, r.duration, r.failure);
259
+ });
260
+ }
261
+ return fromMany("", results);
262
+ }
263
+ function prettyTime(ms) {
264
+ const second = 1000;
265
+ const seconds = ms / second;
266
+ return `${seconds}s`;
267
+ }
268
+ function indented(str) {
269
+ return INDENT + str.split("\n").join(`\n${INDENT}`);
270
+ }
271
+ function logFailure({ path, reason, duration }) {
272
+ console.log(red(`- ${path}`));
273
+ console.log(indented(`Duration: ${prettyTime(duration)}`));
274
+ console.log(indented(reason));
275
+ console.log();
276
+ }
277
+ function filterGroup(group, filters, parents) {
278
+ const currentPath = joinPath([...parents, group.name]);
279
+ if (matchesAnyFilter(filters, currentPath)) {
280
+ return group;
281
+ }
282
+ const matchedEntries = [];
283
+ const pathSegments = [...parents, group.name];
284
+ for (const entry of group.entries) {
285
+ if (entry instanceof Group) {
286
+ const filteredChild = filterGroup(entry, filters, pathSegments);
287
+ if (filteredChild !== null) {
288
+ matchedEntries.push(filteredChild);
289
+ }
290
+ continue;
291
+ }
292
+ if (matchesAnyFilter(filters, joinPath([...pathSegments, entry.name]))) {
293
+ matchedEntries.push(entry);
294
+ }
295
+ }
296
+ if (matchedEntries.length === 0) {
297
+ return null;
298
+ }
299
+ return new Group(group.name, matchedEntries);
300
+ }
301
+ function matchesAnyFilter(filters, path) {
302
+ return filters.some(filter => filter.test(path));
303
+ }
304
+ function joinPath(parts) {
305
+ return parts.join(" / ");
306
+ }
307
+ async function runGroup(group, executionInfos) {
308
+ const children = await Promise.all(group.entries.map(entry => {
309
+ if (entry instanceof Test) {
310
+ return runTest(entry, executionInfos);
311
+ }
312
+ else {
313
+ return runGroup(entry, executionInfos);
314
+ }
315
+ }));
316
+ return {
317
+ type: "node",
318
+ name: group.name,
319
+ children,
320
+ };
321
+ }
322
+ async function runTest(test, executionInfos) {
323
+ const info = executionInfos.get(test.id);
324
+ const start = Date.now();
325
+ if (info) {
326
+ executionInfos.set(test.id, {
327
+ ...info,
328
+ state: { type: "started", start },
329
+ });
330
+ }
331
+ const failure = await execute(test);
332
+ const end = Date.now();
333
+ if (info) {
334
+ executionInfos.set(test.id, {
335
+ ...info,
336
+ state: { type: "finished", start, end, failure },
337
+ });
338
+ }
339
+ return {
340
+ type: "leaf",
341
+ name: test.name,
342
+ failure,
343
+ duration: end - start,
344
+ };
345
+ }
346
+ async function execute(test) {
347
+ try {
348
+ const r = test.fun();
349
+ if (r instanceof Promise) {
350
+ await r;
351
+ }
352
+ return null;
353
+ }
354
+ catch (e) {
355
+ const err = e;
356
+ return err.stack ? err.stack : err.message;
357
+ }
358
+ }
359
+ // Expectations
360
+ class ExpectationFailure extends Error {
361
+ constructor(message) {
362
+ super(message);
363
+ this.name = "ExpectationFailure";
364
+ }
365
+ }
366
+ const expect = {
367
+ fail: function fail(reason) {
368
+ throw new ExpectationFailure(reason);
369
+ },
370
+ equals: function equals(a, b, label) {
371
+ if (a === b) {
372
+ return;
373
+ }
374
+ const lbl = label ? `[${label}] ` : "";
375
+ throw new ExpectationFailure(`${lbl} expected ${a} to equal ${b}`);
376
+ },
377
+ deep_equals: function equals(ra, rb) {
378
+ const a = stringify(ra);
379
+ const b = stringify(rb);
380
+ if (a === b) {
381
+ return;
382
+ }
383
+ throw new ExpectationFailure(`expected '${a}' to equal '${b}'`);
384
+ },
385
+ greater_than: function greater_than(a, b) {
386
+ if (a > b) {
387
+ return;
388
+ }
389
+ throw new ExpectationFailure(`expected ${a} to be greater than ${b}`);
390
+ },
391
+ not_equals: function not_equals(a, b) {
392
+ if (a !== b) {
393
+ return;
394
+ }
395
+ throw new ExpectationFailure(`expected ${a} to not equal ${b}`);
396
+ },
397
+ json_equals: function json_equals(a, b) {
398
+ const j_a = JSON.stringify(a);
399
+ const j_b = JSON.stringify(b);
400
+ if (j_a == j_b) {
401
+ return;
402
+ }
403
+ throw new ExpectationFailure(`expected ${j_a} to equal ${j_b}`);
404
+ },
405
+ contains: function contains(needle, haystack) {
406
+ if (haystack.includes(needle)) {
407
+ return;
408
+ }
409
+ throw new ExpectationFailure(`expected '${haystack}' to contain '${needle}'`);
410
+ },
411
+ throws: function (f, g) {
412
+ try {
413
+ f();
414
+ }
415
+ catch (e) {
416
+ g(e);
417
+ return;
418
+ }
419
+ throw new ExpectationFailure("did not throw");
420
+ },
421
+ };
422
+ function stringify(v) {
423
+ const str = JSON.stringify(v);
424
+ if (str.startsWith("Object") || str.startsWith("[Function")) {
425
+ throw new Error("Value is not meaningfully stringifiable");
426
+ }
427
+ return str;
428
+ }
429
+ function extractPattern(input) {
430
+ if (input.startsWith("/") && input.lastIndexOf("/") > 0) {
431
+ const lastSlash = input.lastIndexOf("/");
432
+ const pattern = input.slice(1, lastSlash);
433
+ const flags = input.slice(lastSlash + 1);
434
+ return { pattern, flags };
435
+ }
436
+ return { pattern: input, flags: "" };
437
+ }
438
+ const regex = {
439
+ metavar: "REGEX",
440
+ parse(input) {
441
+ try {
442
+ const { pattern, flags } = extractPattern(input);
443
+ return { success: true, value: new RegExp(pattern, flags) };
444
+ }
445
+ catch (err) {
446
+ const reason = err instanceof Error ? err.message : String(err);
447
+ return {
448
+ success: false,
449
+ error: message `Invalid regular expression ${input}: ${reason}`,
450
+ };
451
+ }
452
+ },
453
+ format(value) {
454
+ return value.toString();
455
+ },
456
+ };
457
+ const cliParser = object({
458
+ matches: multiple(option("-m", "--match", regex)),
459
+ });
460
+ function parseArgs(argv = process.argv.slice(2)) {
461
+ const config = runCli(cliParser, {
462
+ args: argv,
463
+ programName: "backend-tests",
464
+ help: "both",
465
+ aboveError: "usage",
466
+ });
467
+ return {
468
+ filters: config.matches,
469
+ };
470
+ }
package/dist/time.d.ts ADDED
@@ -0,0 +1,118 @@
1
+ import * as s from "./json/schema";
2
+ import { type Maybe } from "./maybe";
3
+ type Timezone = string;
4
+ /** Date + time stored as milliseconds passed since 00:00:00 UTC on January 1, 1970. */
5
+ declare class POSIX {
6
+ readonly value: number;
7
+ static fromDate(d: Date): POSIX;
8
+ static fromDuration(d: Duration): POSIX;
9
+ static now(): POSIX;
10
+ /** Takes number of milliseconds since epoch. */
11
+ constructor(value: number);
12
+ /** Time since Unix epoch (00:00:00 UTC on January 1, 1970). */
13
+ sinceEpoch(): Duration;
14
+ toDate(): Date;
15
+ isAfter(other: POSIX): boolean;
16
+ greaterThan(other: POSIX): boolean;
17
+ compare(other: POSIX): number;
18
+ addDuration(d: Duration): POSIX;
19
+ subtractDuration(d: Duration): POSIX;
20
+ difference(other: POSIX): Duration;
21
+ static fromLocalDateAndTime(date: DateOnly, time: TimeOfDay, timezone: Timezone): POSIX;
22
+ toUTCDateAndTime(): {
23
+ date: DateOnly;
24
+ time: TimeOfDay;
25
+ };
26
+ toLocalDateAndTime(timezone: Timezone): {
27
+ date: DateOnly;
28
+ time: TimeOfDay;
29
+ };
30
+ /** Parse PostgreSQL TIMESTAMPTZ string (e.g., "2026-03-17 10:30:00+00"). */
31
+ static fromSQLTimestamp(str: string): POSIX | null;
32
+ /** Convert to PostgreSQL TIMESTAMPTZ string. */
33
+ toSQLTimestamp(): string;
34
+ static schema: s.Schema<POSIX>;
35
+ }
36
+ declare class DateOnly {
37
+ readonly year: number;
38
+ readonly month: number;
39
+ readonly day: number;
40
+ constructor(year: number, month: number, day: number);
41
+ static todayUTC(): DateOnly;
42
+ static todayLocal(timezone: Timezone): DateOnly;
43
+ static fromDate(date: Date): DateOnly;
44
+ pretty(): string;
45
+ static schema: s.Schema<DateOnly>;
46
+ greaterThan(other: DateOnly): boolean;
47
+ compare(other: DateOnly): number;
48
+ addMonths(months: number): DateOnly;
49
+ }
50
+ declare class TimeOfDay {
51
+ readonly seconds: number;
52
+ constructor(seconds: number);
53
+ static fromParts({ hours, minutes, seconds }: {
54
+ hours: number;
55
+ minutes: number;
56
+ seconds: number;
57
+ }): TimeOfDay;
58
+ pretty(): string;
59
+ getSubSecondPrecision(): number;
60
+ }
61
+ /** A length of time. */
62
+ declare class Duration {
63
+ private readonly millis;
64
+ private constructor();
65
+ static milliseconds(n: number): Duration;
66
+ static seconds(n: number): Duration;
67
+ static minutes(n: number): Duration;
68
+ static hours(n: number): Duration;
69
+ static days(n: number): Duration;
70
+ static weeks(n: number): Duration;
71
+ asMilliseconds(): number;
72
+ asSeconds(): number;
73
+ asMinutes(): number;
74
+ asHours(): number;
75
+ asDays(): number;
76
+ asWeeks(): number;
77
+ add(other: Duration): Duration;
78
+ subtract(other: Duration): Duration;
79
+ multiplyBy(n: number): Duration;
80
+ divideBy(n: number): Duration;
81
+ greaterThan(other: Duration): boolean;
82
+ compare(other: Duration): number;
83
+ /** Quantisation. Divide a duration into buckets of a fixed length. */
84
+ bucketsOf(length: Duration): {
85
+ count: number;
86
+ remainderStart: Duration;
87
+ remainder: Duration;
88
+ };
89
+ /** Returns a non-negative duration. */
90
+ absolute(): Duration;
91
+ parts(): {
92
+ days: number;
93
+ hours: number;
94
+ minutes: number;
95
+ seconds: number;
96
+ milliseconds: number;
97
+ };
98
+ /** Formats duration as ISO-8601 string (e.g., "P1DT2H30M45.123S"). */
99
+ toISO8601(): string;
100
+ /**
101
+ * Formats a duration to a friendly, human readable string. First argument (`verbosity`) can be
102
+ * either "short" (e.g. 1w 3d 5h 2m 0s 20ms) or long (e.g. 1 week, 3 days, 5 hours, 2 minutes, 20 milliseconds).
103
+ *
104
+ * Options allow for truncation after minutes or seconds. Additionally, if the truncated duration would
105
+ * evaluate to zero but not _exactly_ zero (e.g. a duration of 20 seconds when truncated to minutes will
106
+ * result in 0m), there is an option `show_less_than_when_close_to_zero` which will prefix either "<" (short)
107
+ * or "less than" long in this case, which is recommended when users are actively "watching the clock".
108
+ */
109
+ toFormatted(verbosity: "short" | "long", options?: {
110
+ truncateAfter?: "minutes" | "seconds";
111
+ onTruncation?: "show_less_than_when_close_to_zero";
112
+ }): string;
113
+ /** Parses ISO-8601 duration string (e.g., "P1DT2H30M45.123S"). */
114
+ static fromISO8601(str: string): Maybe<Duration>;
115
+ /** Default: ISO-8601 string (standard, human-readable, interoperable). */
116
+ static schema: s.Schema<Duration>;
117
+ }
118
+ export { type Timezone, DateOnly, TimeOfDay, POSIX, Duration };