@jarenjs/core 0.49.2 → 0.66.1
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/ARCHITECTURE.md +52 -9
- package/README.md +109 -1
- package/dist/types/async.d.ts +142 -0
- package/dist/types/dates/format.d.ts +2 -1
- package/dist/types/object.d.ts +4 -3
- package/dist/types/random.d.ts +80 -0
- package/dist/types/runtime.d.ts +89 -0
- package/dist/types/series/index.d.ts +7 -0
- package/dist/types/stats.d.ts +73 -0
- package/dist/types/string.d.ts +13 -0
- package/docs/DATES.md +3 -1
- package/docs/GEO.md +2 -2
- package/docs/SERIES.md +28 -13
- package/package.json +17 -1
- package/src/async.js +285 -0
- package/src/dates/format.js +15 -5
- package/src/dates/rfc3339.js +5 -7
- package/src/object.js +15 -13
- package/src/random.js +125 -0
- package/src/runtime.js +130 -0
- package/src/series/index.js +7 -0
- package/src/stats.js +133 -0
- package/src/string.js +31 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Descriptive statistics over a sample of numbers: the mean, the
|
|
3
|
+
* sample variance and its root, the median, and a quantile that will
|
|
4
|
+
* not answer until told which quantile it is being asked for.
|
|
5
|
+
*
|
|
6
|
+
* Before this file the suite computed these in two places with two
|
|
7
|
+
* quantile rules — linear interpolation in the query layer's statistics
|
|
8
|
+
* pack, nearest rank in the benchmark harness — and each was the right
|
|
9
|
+
* rule for its consumer: an interpolated percentile is what an analyst
|
|
10
|
+
* expects of `$percentile`, while a benchmark row of eleven readings
|
|
11
|
+
* should not publish a latency nobody measured. Both rules stay; the
|
|
12
|
+
* definitions move here so that there is one of each, and `quantile`
|
|
13
|
+
* makes the caller name its method, because "the 95th percentile" of a
|
|
14
|
+
* small sample is a different number under every one of the seven
|
|
15
|
+
* common definitions and a default would decide silently.
|
|
16
|
+
*
|
|
17
|
+
* Every function answers `undefined` for a sample it cannot summarize —
|
|
18
|
+
* an empty one, or fewer than two values for a variance — rather than
|
|
19
|
+
* `NaN` or `0`: a number that was not measured must not format as one.
|
|
20
|
+
* The caller's array is never reordered; a quantile sorts a copy.
|
|
21
|
+
* Values are numbers by contract and are not checked one by one.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The arithmetic mean.
|
|
25
|
+
* @param {readonly number[]} values
|
|
26
|
+
* @returns {number | undefined} `undefined` for an empty sample
|
|
27
|
+
*/
|
|
28
|
+
export declare function mean(values: readonly number[]): number | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The SAMPLE variance, with Bessel's correction (`n − 1`): the sample is
|
|
31
|
+
* taken as drawn from a population it did not enumerate, which is what a
|
|
32
|
+
* benchmark's rounds and a query's rows both are.
|
|
33
|
+
* @param {readonly number[]} values
|
|
34
|
+
* @returns {number | undefined} `undefined` for fewer than two values
|
|
35
|
+
*/
|
|
36
|
+
export declare function variance(values: readonly number[]): number | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The sample standard deviation — the root of {@link variance}.
|
|
39
|
+
* @param {readonly number[]} values
|
|
40
|
+
* @returns {number | undefined} `undefined` where the variance is
|
|
41
|
+
*/
|
|
42
|
+
export declare function stddev(values: readonly number[]): number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The median: the middle value, or the mean of the two middle values
|
|
45
|
+
* when the sample has an even count.
|
|
46
|
+
*
|
|
47
|
+
* This is NOT `quantile(values, 0.5, …)` under either method, and on an
|
|
48
|
+
* even count the three disagree: for `[1, 2, 3, 4]` the median is `2.5`,
|
|
49
|
+
* the nearest-rank p50 is `2`, and the linear p50 is `2.5` only because
|
|
50
|
+
* that sample happens to be evenly spaced. A consumer publishing a "p50"
|
|
51
|
+
* beside a "p95" wants `quantile` with its method named; a consumer
|
|
52
|
+
* asking for the median wants this.
|
|
53
|
+
* @param {readonly number[]} values
|
|
54
|
+
* @returns {number | undefined} `undefined` for an empty sample
|
|
55
|
+
*/
|
|
56
|
+
export declare function median(values: readonly number[]): number | undefined;
|
|
57
|
+
export type QuantileMethod = 'nearest-rank' | 'linear';
|
|
58
|
+
/**
|
|
59
|
+
* The `p`-quantile of a sample, `p` on `[0, 1]`, under a NAMED method.
|
|
60
|
+
*
|
|
61
|
+
* The method is required, not defaulted: on a small sample the common
|
|
62
|
+
* definitions disagree by whole values, and a caller who did not say
|
|
63
|
+
* which one it wanted has published a number it cannot explain.
|
|
64
|
+
* @param {readonly number[]} values
|
|
65
|
+
* @param {number} p - the probability, `0` (the minimum) to `1` (the maximum)
|
|
66
|
+
* @param {{ method: QuantileMethod }} options
|
|
67
|
+
* @returns {number | undefined} `undefined` for an empty sample
|
|
68
|
+
* @throws {TypeError} when `method` is absent or not one of {@link QuantileMethod}
|
|
69
|
+
* @throws {RangeError} when `p` is not a number in `[0, 1]`
|
|
70
|
+
*/
|
|
71
|
+
export declare function quantile(values: readonly number[], p: number, options: {
|
|
72
|
+
method: QuantileMethod;
|
|
73
|
+
}): number | undefined;
|
package/dist/types/string.d.ts
CHANGED
|
@@ -54,6 +54,19 @@ export declare function getStringLength(str: string, useGrapheme?: boolean): num
|
|
|
54
54
|
* @returns {number} Number of occurrences in [start, end)
|
|
55
55
|
*/
|
|
56
56
|
export declare function countCharCode(str: string, code: number, start?: number, end?: number): number;
|
|
57
|
+
/**
|
|
58
|
+
* The UTF-8 byte length of a slice of a string — the one measure every
|
|
59
|
+
* byte bound in the suite counts (a body limit, a page, a change
|
|
60
|
+
* record, a parser's record or token limit) — computed without encoding
|
|
61
|
+
* a copy: one byte below U+0080, two below U+0800, four for a surrogate
|
|
62
|
+
* pair (one code point), three otherwise; a lone surrogate counts three,
|
|
63
|
+
* as its replacement would.
|
|
64
|
+
* @param {string} str
|
|
65
|
+
* @param {number} [start] - Inclusive start offset (defaults to 0)
|
|
66
|
+
* @param {number} [end] - Exclusive end offset (defaults to full length)
|
|
67
|
+
* @returns {number} The UTF-8 bytes of `str[start, end)`
|
|
68
|
+
*/
|
|
69
|
+
export declare function utf8ByteLength(str: string, start?: number, end?: number): number;
|
|
57
70
|
/**
|
|
58
71
|
* Count the Unicode code points of a string (surrogate-pair aware;
|
|
59
72
|
* a lone surrogate counts as one code point).
|
package/docs/DATES.md
CHANGED
|
@@ -21,7 +21,9 @@ Parsing: `parseRFC3339Parts(str)` → a flat **parts** record
|
|
|
21
21
|
`{year, month, day, hours, minutes, seconds, offset}`. Absent fields are
|
|
22
22
|
`-1` sentinels; `offset` is minutes east of UTC, `0` for `Z` and `null`
|
|
23
23
|
when the string carried none. The inverse is `formatRFC3339Parts(parts)`
|
|
24
|
-
(round-trips the offset
|
|
24
|
+
(round-trips the offset and up to six fractional-second digits, dropping
|
|
25
|
+
trailing zeros). The `SSS` formatting token writes the first three fraction
|
|
26
|
+
digits. `epochOfRFC3339Parts(parts)` → epoch ms: a
|
|
25
27
|
date-only reads as UTC midnight, an offset shifts to its instant, and a
|
|
26
28
|
time-only has **no** instant — it returns `NaN` rather than inventing a
|
|
27
29
|
day. `getDateTypeOf*` variants produce a `Date` for callers that want
|
package/docs/GEO.md
CHANGED
|
@@ -22,10 +22,10 @@ the *wrong sign* on near-collinear input, which makes containment
|
|
|
22
22
|
contradict itself. `orient2dFast` is the naive form, exported for
|
|
23
23
|
callers that provably do not care. Every winding and containment answer
|
|
24
24
|
in this module rests on this sign; its cost is the deliberate
|
|
25
|
-
point-in-polygon loss on the benchmark page (<!--
|
|
25
|
+
point-in-polygon loss on the benchmark page (<!--fact:geo.pip2000-->0.5×<!--/fact--> against
|
|
26
26
|
Turf at 2000 vertices, kept on purpose). Every loss the kernel carries is
|
|
27
27
|
published in [ARCHITECTURE](../ARCHITECTURE.md), derived from the committed
|
|
28
|
-
measurement: <!--
|
|
28
|
+
measurement: <!--fact:geo.losses-->three rows lose to a rival: point in polygon (2000-vertex) at 0.5× (turf), bounding box (2000-vertex) at 0.9× (turf), index build (100k boxes) at 0.8× (flatbush)<!--/fact-->.
|
|
29
29
|
|
|
30
30
|
## Distance — `distance.js`
|
|
31
31
|
|
package/docs/SERIES.md
CHANGED
|
@@ -177,6 +177,20 @@ with no provider is a refusal, never a quiet fall back to UTC — which is
|
|
|
177
177
|
right for Amsterdam for none of the year and *looks* right for eight
|
|
178
178
|
months of it.
|
|
179
179
|
|
|
180
|
+
The seam is crossed, not closed. `@jarenjs/locales/intl-zones` ships
|
|
181
|
+
`createIntlZoneProvider()`, a provider over the host's own ICU data —
|
|
182
|
+
the one copy of the tzdb that is already installed and already
|
|
183
|
+
maintained — that a caller passes as `provider`. It is opt-in and on
|
|
184
|
+
its own subpath, because ICU output moves between hosts and may not be
|
|
185
|
+
anyone's default; it never reads the host's own zone, because every
|
|
186
|
+
call names its zone and there is no default that asks the process
|
|
187
|
+
where it is; and it answers a gap and a fold exactly, proved by a
|
|
188
|
+
transition corpus rather than by examples. Nothing about the seam
|
|
189
|
+
moves: this kernel still bundles no zone data and reaches for no
|
|
190
|
+
`Intl`, a named zone with no provider is still a refusal, and a host
|
|
191
|
+
with a tzdb of its own still passes that instead. The provider's own
|
|
192
|
+
document is [`@jarenjs/locales`' README](../../locales/README.md).
|
|
193
|
+
|
|
180
194
|
## Buckets, resampling and fill — `bucket.js`
|
|
181
195
|
|
|
182
196
|
Two questions that are always asked together and are not the same
|
|
@@ -311,32 +325,33 @@ downsampleSeries(readings, { target: 2000 }); // → { points, sourceCount, re
|
|
|
311
325
|
|
|
312
326
|
## What it costs
|
|
313
327
|
|
|
314
|
-
Measured by `benchmark/series.js` over <!--
|
|
328
|
+
Measured by `benchmark/series.js` over <!--fact:series.corpus-->100,000 samples at 1-second spacing, Node v24.19.0<!--/fact-->,
|
|
315
329
|
which gates every timing on equivalence first: no number below is printed
|
|
316
330
|
unless the kernel answered the identical rows the references did.
|
|
317
331
|
|
|
318
332
|
The kernel is not the ceiling and does not claim to be. A one-pass loop
|
|
319
333
|
written for one question validates nothing, normalizes nothing and
|
|
320
|
-
returns a bare pair. Against those loops the kernel costs <!--
|
|
321
|
-
and against the vocabulary a consumer had instead it is <!--
|
|
334
|
+
returns a bare pair. Against those loops the kernel costs <!--fact:series.kernelVsCeiling-->3.3× the one-pass bucket loop and 6.0× the one-pass ring sum<!--/fact-->,
|
|
335
|
+
and against the vocabulary a consumer had instead it is <!--fact:series.kernelVsQuery-->76.7× faster than the generic query bucket and 78.5× faster than the labelled count window<!--/fact-->.
|
|
322
336
|
|
|
323
|
-
<!--
|
|
337
|
+
<!--fact:series.kernelTable-->
|
|
324
338
|
| operation | median | rows | against | what that is | ratio |
|
|
325
339
|
|---|---:|---:|---:|---|---:|
|
|
326
|
-
| `resampleSeries`, 60 s buckets | 1.
|
|
327
|
-
| `resampleSeries`, + linear fill | 1.1 ms | 1,657 | 1 ms | the same buckets, omitting | 1.
|
|
328
|
-
| `rollingSeries`, 60 s window | 7.
|
|
329
|
-
| `asOfJoin`, one left row per 100 | 1.7 ms | 1,000 |
|
|
330
|
-
| `downsampleSeries`, lttb, gap corpus | 1.
|
|
331
|
-
<!--/
|
|
340
|
+
| `resampleSeries`, 60 s buckets | 1.6 ms | 1,667 | 0.47 ms | one-pass loop | 3.3× |
|
|
341
|
+
| `resampleSeries`, + linear fill | 1.1 ms | 1,657 | 1 ms | the same buckets, omitting | 1.1× |
|
|
342
|
+
| `rollingSeries`, 60 s window | 7.6 ms | 100,000 | 1.3 ms | one-pass ring sum | 6.0× |
|
|
343
|
+
| `asOfJoin`, one left row per 100 | 1.7 ms | 1,000 | 1.7 ms | one index read per row | 1.0× |
|
|
344
|
+
| `downsampleSeries`, lttb, gap corpus | 1.7 ms | 2,000 | 1.9 ms | the same line with no holes in it | 0.9× |
|
|
345
|
+
<!--/fact-->
|
|
332
346
|
|
|
333
|
-
A row that loses stays in, and the two shapes of the same join are published side by side rather than the flattering one alone. <!--
|
|
347
|
+
A row that loses stays in, and the two shapes of the same join are published side by side rather than the flattering one alone. <!--fact:series.asofShape-->The as-of join costs 19.7× a handful of index reads, and narrows to 1.0× the same join once there is one left row per hundred right ones — still the statement's win, published as one. The reason is the shape rather than the engine: a b-tree pays per probe, and a sorted walk pays for the whole right side whether it was asked one question or a thousand.<!--/fact-->
|
|
334
348
|
|
|
335
|
-
And the seam has a price that this corpus cannot charge it. <!--
|
|
349
|
+
And the seam has a price that this corpus cannot charge it. <!--fact:series.zoneCost-->Walking every boundary through the shipped Intl zone provider costs 1.1× the integer ladder over an identical answer — a handful of ICU reads against the whole ladder, since the benchmark corpus spans 28 hours and holds two daily boundaries. What the suite gates is that the provider is consulted per boundary rather than per sample.<!--/fact-->
|
|
336
350
|
|
|
337
351
|
## Not here
|
|
338
352
|
|
|
339
|
-
Named time zones are injected, never bundled
|
|
353
|
+
Named time zones are injected, never bundled — the provider over host
|
|
354
|
+
ICU lives opt-in in `@jarenjs/locales`, not here; recurrence grammars
|
|
340
355
|
(RRULE, iCalendar) and any kind of scheduling solver are somebody else's
|
|
341
356
|
layer. This module supplies the algebra those are built from, and
|
|
342
357
|
nothing that needs a clock, a locale or a zone database to be correct.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/core",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.66.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"types": "./dist/types/array.d.ts",
|
|
41
41
|
"default": "./src/array.js"
|
|
42
42
|
},
|
|
43
|
+
"./async": {
|
|
44
|
+
"types": "./dist/types/async.d.ts",
|
|
45
|
+
"default": "./src/async.js"
|
|
46
|
+
},
|
|
43
47
|
"./bigint": {
|
|
44
48
|
"types": "./dist/types/bigint.d.ts",
|
|
45
49
|
"default": "./src/bigint.js"
|
|
@@ -104,6 +108,14 @@
|
|
|
104
108
|
"types": "./dist/types/object.d.ts",
|
|
105
109
|
"default": "./src/object.js"
|
|
106
110
|
},
|
|
111
|
+
"./random": {
|
|
112
|
+
"types": "./dist/types/random.d.ts",
|
|
113
|
+
"default": "./src/random.js"
|
|
114
|
+
},
|
|
115
|
+
"./runtime": {
|
|
116
|
+
"types": "./dist/types/runtime.d.ts",
|
|
117
|
+
"default": "./src/runtime.js"
|
|
118
|
+
},
|
|
107
119
|
"./scan": {
|
|
108
120
|
"types": "./dist/types/scan.d.ts",
|
|
109
121
|
"default": "./src/scan.js"
|
|
@@ -120,6 +132,10 @@
|
|
|
120
132
|
"types": "./dist/types/series/*.d.ts",
|
|
121
133
|
"default": "./src/series/*.js"
|
|
122
134
|
},
|
|
135
|
+
"./stats": {
|
|
136
|
+
"types": "./dist/types/stats.d.ts",
|
|
137
|
+
"default": "./src/stats.js"
|
|
138
|
+
},
|
|
123
139
|
"./string": {
|
|
124
140
|
"types": "./dist/types/string.d.ts",
|
|
125
141
|
"default": "./src/string.js"
|
package/src/async.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The bounded ordered asynchronous map: run a worker over a list
|
|
4
|
+
* with never more than `limit` calls in flight, and answer the results
|
|
5
|
+
* in the list's order. Before this file the same twelve lines lived in
|
|
6
|
+
* the AI package's program runner and in the benchmark harness, and a
|
|
7
|
+
* downstream consumer had written them a third time; a pool that exists
|
|
8
|
+
* once is one whose edge behavior can be pinned once.
|
|
9
|
+
*
|
|
10
|
+
* The contract, in full:
|
|
11
|
+
*
|
|
12
|
+
* - results are in INPUT order, whatever order the workers finish in;
|
|
13
|
+
* - never more than `limit` workers are in flight; `limit` must be a
|
|
14
|
+
* number of at least 1 (`Infinity` is allowed and means unbounded) —
|
|
15
|
+
* anything else is a `TypeError`, never a silent clamp, because a
|
|
16
|
+
* limit of 0 is a bug in the caller and "sequential" is spelled 1;
|
|
17
|
+
* - a worker rejection stops dispatch: no item starts after it, the
|
|
18
|
+
* workers already in flight are awaited, and only then does the map
|
|
19
|
+
* reject with that first rejection. A worker that throws
|
|
20
|
+
* synchronously is a rejection;
|
|
21
|
+
* - an abort does the same, rejecting with the signal's reason; a signal
|
|
22
|
+
* that is already aborted rejects before any worker runs;
|
|
23
|
+
* - so when the returned promise settles, NO worker is still running —
|
|
24
|
+
* the caller can close whatever the workers were using.
|
|
25
|
+
*
|
|
26
|
+
* The map does not retry, rate-limit, delay per origin or know anything
|
|
27
|
+
* about what the worker does; those are the caller's policies around it.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { isThenable } from './function.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template T, R
|
|
34
|
+
* @param {readonly T[]} items
|
|
35
|
+
* @param {number} limit - workers in flight at once; a number >= 1, `Infinity` for unbounded
|
|
36
|
+
* @param {(item: T, index: number) => Promise<R> | R} worker
|
|
37
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
38
|
+
* @returns {Promise<R[]>} the results, in input order
|
|
39
|
+
* @throws {TypeError} (as a rejection) when `limit` is not a number >= 1
|
|
40
|
+
*/
|
|
41
|
+
export async function mapConcurrent(items, limit, worker, options = {}) {
|
|
42
|
+
if (typeof limit !== 'number' || !(limit >= 1))
|
|
43
|
+
throw new TypeError(`mapConcurrent needs a limit of at least 1, got ${String(limit)}`);
|
|
44
|
+
const { signal } = options;
|
|
45
|
+
if (signal?.aborted) throw signal.reason;
|
|
46
|
+
const count = items.length;
|
|
47
|
+
/** @type {R[]} */
|
|
48
|
+
const results = new Array(count);
|
|
49
|
+
if (count === 0) return results;
|
|
50
|
+
|
|
51
|
+
let next = 0;
|
|
52
|
+
// the first failure or abort, kept as a one-element list: the lanes
|
|
53
|
+
// stop dispatching the moment it is set and drain what they hold
|
|
54
|
+
/** @type {unknown[]} */
|
|
55
|
+
const stop = [];
|
|
56
|
+
/** @type {(() => void) | undefined} */
|
|
57
|
+
let onAbort;
|
|
58
|
+
if (signal !== undefined) {
|
|
59
|
+
onAbort = () => { if (stop.length === 0) stop.push(signal.reason); };
|
|
60
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lane = async () => {
|
|
64
|
+
while (stop.length === 0) {
|
|
65
|
+
const index = next++;
|
|
66
|
+
if (index >= count) return;
|
|
67
|
+
try {
|
|
68
|
+
results[index] = await worker(items[index], index);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (stop.length === 0) stop.push(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const lanes = Array.from({ length: Math.min(limit, count) }, lane);
|
|
76
|
+
await Promise.all(lanes);
|
|
77
|
+
if (onAbort !== undefined) signal?.removeEventListener('abort', onAbort);
|
|
78
|
+
if (stop.length > 0) throw stop[0];
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The underlying sink an awaited sink serializes: `write` answers a
|
|
84
|
+
* value (the chunk is written) or a promise (the chunk is written when
|
|
85
|
+
* it settles — a socket waiting for `drain`, a stream waiting for the
|
|
86
|
+
* consumer's next pull); `end` and `abort` are optional and may answer
|
|
87
|
+
* either way too.
|
|
88
|
+
* @template T
|
|
89
|
+
* @typedef {Object} SinkLike
|
|
90
|
+
* @property {(chunk: T) => unknown} write
|
|
91
|
+
* @property {() => unknown} [end]
|
|
92
|
+
* @property {(reason: unknown) => unknown} [abort]
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The serialized, awaited view of a sink.
|
|
97
|
+
* @template T
|
|
98
|
+
* @typedef {Object} AwaitedSink
|
|
99
|
+
* @property {(chunk: T) => Promise<void> | undefined} write - queue one
|
|
100
|
+
* chunk behind every earlier write; `undefined` when the underlying
|
|
101
|
+
* write answered synchronously with nothing pending (the fast path),
|
|
102
|
+
* else a promise that settles when the underlying write did
|
|
103
|
+
* @property {() => Promise<void>} end - queue the underlying `end` once
|
|
104
|
+
* after every write; repeated calls answer the same promise
|
|
105
|
+
* @property {(reason?: unknown) => Promise<void>} abort - stop accepting
|
|
106
|
+
* writes, reject the writes still queued, and call the underlying
|
|
107
|
+
* `abort` once, immediately; repeated calls answer the same promise
|
|
108
|
+
* @property {() => boolean} failed - whether a write rejected or an abort
|
|
109
|
+
* happened; nothing reaches the underlying sink after that
|
|
110
|
+
* @property {() => boolean} closed - whether `end` or `abort` was called
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Serialize a sink: every write waits for the previous one, `end` waits
|
|
115
|
+
* for every write, and one failure stops everything after it. This is
|
|
116
|
+
* the one place the suite decides what "the next chunk" means for a
|
|
117
|
+
* sink that may answer a promise — a Node response whose `write()` said
|
|
118
|
+
* `false` (wait for `drain`), a Web `ReadableStream` bridge that waits
|
|
119
|
+
* for the consumer's pull, a stream runner whose carrier hooks may be
|
|
120
|
+
* asynchronous — so backpressure is a property of the sink, never of
|
|
121
|
+
* the code writing into it.
|
|
122
|
+
*
|
|
123
|
+
* The contract, in full:
|
|
124
|
+
*
|
|
125
|
+
* - writes reach the underlying sink in call order and never overlap:
|
|
126
|
+
* the next underlying write begins only after the previous one
|
|
127
|
+
* settled;
|
|
128
|
+
* - a synchronous sink stays on a no-extra-promise fast path: while
|
|
129
|
+
* nothing is pending and the underlying write answers a non-thenable,
|
|
130
|
+
* `write` answers `undefined`; the first thenable answer opens the
|
|
131
|
+
* queue, and an idle queue returns to the fast path;
|
|
132
|
+
* - `end()` runs the underlying `end` once, after every write queued
|
|
133
|
+
* before it; `abort(reason)` runs the underlying `abort` once, at once
|
|
134
|
+
* (an abort is urgent — the pending underlying write is not waited
|
|
135
|
+
* for), and every write still queued rejects with the reason;
|
|
136
|
+
* - a write that throws or rejects (including a throwing `then` getter
|
|
137
|
+
* on its answer) fails the sink: its own promise
|
|
138
|
+
* rejects, every write queued behind it rejects with the same reason
|
|
139
|
+
* without reaching the underlying sink, later writes reject at once,
|
|
140
|
+
* and `end()` rejects too — the stream did not end cleanly;
|
|
141
|
+
* - `end` and `abort` are mutually terminal: the first one decides, the
|
|
142
|
+
* other one (and every repeat) answers the first one's promise; a
|
|
143
|
+
* write after either rejects;
|
|
144
|
+
* - `write` never throws — a refusal is a rejected promise, so a caller
|
|
145
|
+
* handles one shape.
|
|
146
|
+
*
|
|
147
|
+
* @template T
|
|
148
|
+
* @param {SinkLike<T>} sink
|
|
149
|
+
* @returns {AwaitedSink<T>}
|
|
150
|
+
* @throws {TypeError} when `sink` has no `write` function
|
|
151
|
+
*/
|
|
152
|
+
export function createAwaitedSink(sink) {
|
|
153
|
+
if (sink === null || typeof sink !== 'object' || typeof sink.write !== 'function') {
|
|
154
|
+
throw new TypeError('createAwaitedSink needs a sink with a write function');
|
|
155
|
+
}
|
|
156
|
+
/** whether an underlying operation is in flight (it answered a thenable) */
|
|
157
|
+
let running = false;
|
|
158
|
+
/**
|
|
159
|
+
* The operations waiting for the one in flight, in call order.
|
|
160
|
+
* @type {{ op: () => unknown, resolve: () => void, reject: (reason: unknown) => void }[]}
|
|
161
|
+
*/
|
|
162
|
+
const queued = [];
|
|
163
|
+
let failed = false;
|
|
164
|
+
/** @type {unknown} */
|
|
165
|
+
let failure;
|
|
166
|
+
let closed = false;
|
|
167
|
+
/** @type {Promise<void> | null} the first end()/abort() settlement */
|
|
168
|
+
let terminal = null;
|
|
169
|
+
|
|
170
|
+
/** @param {unknown} reason */
|
|
171
|
+
const fail = (reason) => {
|
|
172
|
+
if (failed) return;
|
|
173
|
+
failed = true;
|
|
174
|
+
failure = reason;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Run what is queued, one at a time, until one answers a thenable. */
|
|
178
|
+
const next = () => {
|
|
179
|
+
while (!running && queued.length > 0) {
|
|
180
|
+
const item = /** @type {NonNullable<typeof queued[0]>} */ (queued.shift());
|
|
181
|
+
if (failed) {
|
|
182
|
+
item.reject(failure);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
let answer;
|
|
186
|
+
try {
|
|
187
|
+
answer = item.op();
|
|
188
|
+
if (!isThenable(answer)) {
|
|
189
|
+
item.resolve();
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
fail(err);
|
|
195
|
+
item.reject(err);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
running = true;
|
|
199
|
+
Promise.resolve(answer).then(() => {
|
|
200
|
+
running = false;
|
|
201
|
+
item.resolve();
|
|
202
|
+
next();
|
|
203
|
+
}, (err) => {
|
|
204
|
+
running = false;
|
|
205
|
+
fail(err);
|
|
206
|
+
item.reject(err);
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Run one operation behind the queue.
|
|
214
|
+
* @param {() => unknown} op
|
|
215
|
+
* @returns {Promise<void> | undefined}
|
|
216
|
+
*/
|
|
217
|
+
const run = (op) => {
|
|
218
|
+
if (running || queued.length > 0) {
|
|
219
|
+
return new Promise((resolve, reject) => { queued.push({ op, resolve, reject }); });
|
|
220
|
+
}
|
|
221
|
+
let answer;
|
|
222
|
+
try {
|
|
223
|
+
answer = op();
|
|
224
|
+
if (!isThenable(answer)) return undefined;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
fail(err);
|
|
228
|
+
return Promise.reject(err);
|
|
229
|
+
}
|
|
230
|
+
running = true;
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
Promise.resolve(answer).then(() => {
|
|
233
|
+
running = false;
|
|
234
|
+
resolve();
|
|
235
|
+
next();
|
|
236
|
+
}, (err) => {
|
|
237
|
+
running = false;
|
|
238
|
+
fail(err);
|
|
239
|
+
reject(err);
|
|
240
|
+
next();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
write(chunk) {
|
|
247
|
+
if (failed) return Promise.reject(failure);
|
|
248
|
+
if (closed) return Promise.reject(new Error('createAwaitedSink: write after end'));
|
|
249
|
+
return run(() => sink.write(chunk));
|
|
250
|
+
},
|
|
251
|
+
end() {
|
|
252
|
+
if (terminal !== null) return terminal;
|
|
253
|
+
closed = true;
|
|
254
|
+
if (failed) {
|
|
255
|
+
terminal = Promise.reject(failure);
|
|
256
|
+
return terminal;
|
|
257
|
+
}
|
|
258
|
+
const answer = run(() => (typeof sink.end === 'function' ? sink.end() : undefined));
|
|
259
|
+
terminal = answer === undefined ? Promise.resolve() : answer;
|
|
260
|
+
return terminal;
|
|
261
|
+
},
|
|
262
|
+
abort(reason) {
|
|
263
|
+
if (terminal !== null) return terminal;
|
|
264
|
+
closed = true;
|
|
265
|
+
const why = reason === undefined ? new Error('createAwaitedSink: aborted') : reason;
|
|
266
|
+
fail(why);
|
|
267
|
+
// what is still queued rejects now — an abort does not wait for
|
|
268
|
+
// the operation in flight, whose late settlement is then ignored
|
|
269
|
+
const waiting = queued.splice(0, queued.length);
|
|
270
|
+
for (let i = 0; i < waiting.length; i++) waiting[i].reject(why);
|
|
271
|
+
let answer;
|
|
272
|
+
try {
|
|
273
|
+
answer = typeof sink.abort === 'function' ? sink.abort(why) : undefined;
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
terminal = Promise.reject(err);
|
|
277
|
+
return terminal;
|
|
278
|
+
}
|
|
279
|
+
terminal = Promise.resolve(answer).then(() => undefined);
|
|
280
|
+
return terminal;
|
|
281
|
+
},
|
|
282
|
+
failed: () => failed,
|
|
283
|
+
closed: () => closed,
|
|
284
|
+
};
|
|
285
|
+
}
|
package/src/dates/format.js
CHANGED
|
@@ -40,6 +40,13 @@ import {
|
|
|
40
40
|
const D2 = (n) => (n < 10 ? '0' + n : '' + n);
|
|
41
41
|
const D3 = (n) => (n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n);
|
|
42
42
|
|
|
43
|
+
// The parser retains up to six fractional digits. Round the whole seconds
|
|
44
|
+
// before extracting them, so subtraction does not expose binary noise.
|
|
45
|
+
/** @param {number} seconds */
|
|
46
|
+
function fractionDigits(seconds) {
|
|
47
|
+
return seconds.toFixed(6).slice(-6);
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
function pad(n, width) {
|
|
44
51
|
const s = '' + n;
|
|
45
52
|
return s.length >= width ? s : '0'.repeat(width - s.length) + s;
|
|
@@ -86,8 +93,8 @@ const TOKENS = {
|
|
|
86
93
|
m: (p) => '' + p.minutes,
|
|
87
94
|
ss: (p) => D2(Math.trunc(p.seconds)),
|
|
88
95
|
s: (p) => '' + Math.trunc(p.seconds),
|
|
89
|
-
SSS: (p) =>
|
|
90
|
-
S: (p) =>
|
|
96
|
+
SSS: (p) => fractionDigits(p.seconds).slice(0, 3),
|
|
97
|
+
S: (p) => fractionDigits(p.seconds).slice(0, 1),
|
|
91
98
|
a: (p, n) => n.meridiem[p.hours < 12 ? 0 : 1],
|
|
92
99
|
XXX: (p) => offsetText(p.offset, true, true),
|
|
93
100
|
XX: (p) => offsetText(p.offset, false, true),
|
|
@@ -203,7 +210,8 @@ const FORMAT_TIME = compileDateFormat('HH:mm:ssXXX');
|
|
|
203
210
|
* `full-time`, and the offset is the record's own rather than UTC.
|
|
204
211
|
*
|
|
205
212
|
* The round trip preserves the *value*, not necessarily the spelling: a
|
|
206
|
-
* fractional second
|
|
213
|
+
* fractional second keeps the parser's six-digit precision and is emitted
|
|
214
|
+
* only when non-zero and without trailing
|
|
207
215
|
* zeros, so `…:05.250Z` comes back `…:05.25Z`. The parts record holds
|
|
208
216
|
* the fraction as a number, so the original digit count is not
|
|
209
217
|
* recoverable — a consumer that must reproduce the input byte for byte
|
|
@@ -222,9 +230,11 @@ export function formatRFC3339Parts(parts) {
|
|
|
222
230
|
if (fraction === 0)
|
|
223
231
|
return base;
|
|
224
232
|
// splice the fraction in after the seconds, before the offset
|
|
225
|
-
const digits =
|
|
233
|
+
const digits = fractionDigits(parts.seconds).replace(/0+$/, '');
|
|
234
|
+
if (digits.length === 0)
|
|
235
|
+
return base;
|
|
226
236
|
const cut = base.length - (FORMAT_OFFSET_LEN(parts));
|
|
227
|
-
return base.slice(0, cut) + digits + base.slice(cut);
|
|
237
|
+
return base.slice(0, cut) + '.' + digits + base.slice(cut);
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
// how many characters the rendered offset takes, so the fraction can be
|
package/src/dates/rfc3339.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isObjectOfClass,
|
|
6
6
|
} from '../index.js';
|
|
7
7
|
// the calendar rules live in civil.js, which owns the day tables
|
|
8
|
-
import { isLeapYear, daysInMonth } from './civil.js';
|
|
8
|
+
import { isLeapYear, daysInMonth, daysFromCivil } from './civil.js';
|
|
9
9
|
|
|
10
10
|
export { isLeapYear };
|
|
11
11
|
|
|
@@ -365,18 +365,16 @@ export function epochOfRFC3339Parts(parts) {
|
|
|
365
365
|
return NaN;
|
|
366
366
|
const seconds = parts.seconds < 0 ? 0 : parts.seconds;
|
|
367
367
|
const whole = Math.floor(seconds);
|
|
368
|
+
// Civil day numbers preserve year zero's leap day and carries across
|
|
369
|
+
// year 99; Date.UTC's 1900-based interpretation of 0-99 cannot.
|
|
368
370
|
const ms = Date.UTC(
|
|
369
|
-
parts.year, parts.month
|
|
371
|
+
1970, 0, 1 + daysFromCivil(parts.year, parts.month, parts.day),
|
|
370
372
|
parts.hours < 0 ? 0 : parts.hours,
|
|
371
373
|
parts.minutes < 0 ? 0 : parts.minutes,
|
|
372
374
|
whole, Math.round((seconds - whole) * 1000));
|
|
373
375
|
if (ms !== ms)
|
|
374
376
|
return NaN;
|
|
375
|
-
|
|
376
|
-
const utc = new Date(ms);
|
|
377
|
-
if (parts.year >= 0 && parts.year < 100)
|
|
378
|
-
utc.setUTCFullYear(parts.year);
|
|
379
|
-
return utc.getTime() - (parts.offset === null ? 0 : parts.offset) * 60000;
|
|
377
|
+
return ms - (parts.offset === null ? 0 : parts.offset) * 60000;
|
|
380
378
|
}
|
|
381
379
|
//#endregion
|
|
382
380
|
|