@jarenjs/core 0.56.0 → 0.67.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.
- package/ARCHITECTURE.md +35 -1
- package/README.md +106 -1
- package/dist/types/async.d.ts +103 -0
- package/dist/types/dates/format.d.ts +2 -1
- package/dist/types/object.d.ts +4 -3
- package/dist/types/runtime.d.ts +89 -0
- package/dist/types/series/index.d.ts +7 -0
- package/dist/types/string.d.ts +13 -0
- package/docs/DATES.md +3 -1
- package/docs/SERIES.md +25 -10
- package/package.json +5 -1
- package/src/async.js +207 -0
- package/src/dates/format.js +15 -5
- package/src/dates/rfc3339.js +5 -7
- package/src/object.js +15 -13
- package/src/runtime.js +130 -0
- package/src/series/index.js +7 -0
- package/src/string.js +31 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -161,13 +161,17 @@ flowchart TB
|
|
|
161
161
|
|
|
162
162
|
subgraph FunctionModule["Function Utilities"]
|
|
163
163
|
FunctionUtil["function.js<br/>trueThat, falseThat"]
|
|
164
|
-
AsyncUtil["async.js<br/>Bounded ordered asynchronous map"]
|
|
164
|
+
AsyncUtil["async.js<br/>Bounded ordered asynchronous map, awaited sink"]
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
subgraph RandomModule["Randomness"]
|
|
168
168
|
RandomUtil["random.js<br/>Seeded generator, integer draw, shuffle, distinct draw"]
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
subgraph RuntimeModule["Host runtime"]
|
|
172
|
+
RuntimeRecord["runtime.js<br/>The runtime record: clock, identifiers, randomness, zone provider"]
|
|
173
|
+
end
|
|
174
|
+
|
|
171
175
|
subgraph StatsModule["Statistics"]
|
|
172
176
|
StatsUtil["stats.js<br/>Mean, sample variance, median, named quantile"]
|
|
173
177
|
end
|
|
@@ -834,6 +838,36 @@ findSlots(shifts, { duration: 'PT30M' }).length; // 16
|
|
|
834
838
|
createIntervalIndex(shifts).at(4 * 3600_000); // the second shift only: [start, end)
|
|
835
839
|
```
|
|
836
840
|
|
|
841
|
+
### 5e. Runtime record (`runtime.js`)
|
|
842
|
+
|
|
843
|
+
The one record a host hands to every subsystem that needs a host fact:
|
|
844
|
+
`createRuntime({ now, uuid, random, zoneProvider })`, frozen, defaulting
|
|
845
|
+
member for member to the platform's own (`Date.now`, `crypto.randomUUID`,
|
|
846
|
+
`Math.random`, and no zone provider — a named zone stays a refusal). The
|
|
847
|
+
store (query deadlines included), the job engine, the migration runner, the
|
|
848
|
+
contract bindings and the contract memory ledger take it as `runtime`; a
|
|
849
|
+
subsystem's own explicit option wins over the record's member,
|
|
850
|
+
which wins over the built-in default, so adopting the record changes nothing
|
|
851
|
+
observable and a deterministic run — a fixed clock, a seeded generator, a
|
|
852
|
+
counting identifier — is configured once. It is a type plus a freeze: this
|
|
853
|
+
module reads no clock and draws no number of its own, and it reaches hosts,
|
|
854
|
+
never query compilation, because a compiled query is cached by document
|
|
855
|
+
identity and there is no `now` in the kernel for it to feed.
|
|
856
|
+
|
|
857
|
+
```javascript
|
|
858
|
+
import { createRuntime } from '@jarenjs/core/runtime';
|
|
859
|
+
import { mulberry32 } from '@jarenjs/core/random';
|
|
860
|
+
|
|
861
|
+
let n = 0;
|
|
862
|
+
const runtime = createRuntime({
|
|
863
|
+
now: () => 1_700_000_000_000,
|
|
864
|
+
uuid: () => `id-${++n}`,
|
|
865
|
+
random: mulberry32(2026),
|
|
866
|
+
});
|
|
867
|
+
runtime.zoneProvider; // null — the one member left at its default
|
|
868
|
+
createRuntime({ clock: Date.now }); // TypeError: a runtime record has 'now', 'uuid', 'random', 'zoneProvider', not 'clock'
|
|
869
|
+
```
|
|
870
|
+
|
|
837
871
|
### 6. Text Module (`text/`)
|
|
838
872
|
|
|
839
873
|
Comprehensive string format validation organized by domain.
|
package/README.md
CHANGED
|
@@ -14,8 +14,9 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
14
14
|
| `@jarenjs/core/string` | Unicode string helpers (`countCodePoints`, `compareCodePoints`, ...), cached regex compilation, the suite's one content hash (`fnv1a` and the `hashContent` fingerprint over it) and `kebabCase` |
|
|
15
15
|
| `@jarenjs/core/cache` | the bounded LRU (`createBoundedCache`), the reference-keyed `createWeakCache`, and `createSemanticCache` — keyed by what a value IS, for caches whose entries decide a result |
|
|
16
16
|
| `@jarenjs/core/random` | the suite's one seeded generator (`mulberry32`, pinned sequence, ToUint32 seed) and the draws built on it: `randomInt` over a half-open range, in-place Fisher–Yates `shuffle`, and `drawDistinct` — `k` distinct indices from one stream |
|
|
17
|
+
| `@jarenjs/core/runtime` | the runtime record — `createRuntime({ now, uuid, random, zoneProvider })`, frozen, defaulting member for member to the platform's own (`Date.now`, `crypto.randomUUID`, `Math.random`, no zone provider) — that the store (its query deadlines included), the jobs engine, the migration runner, every contract binding and the contract memory ledger take as `runtime`, so a deterministic run is configured once; a subsystem's own explicit option wins over the record, and the record reaches hosts, never query compilation |
|
|
17
18
|
| `@jarenjs/core/stats` | descriptive statistics over a sample: `mean`, sample `variance`/`stddev`, the midpoint `median`, and `quantile(values, p, { method })` — `p` on 0..1 under a NAMED rule, `'nearest-rank'` or `'linear'`, because a default would decide silently; an empty sample answers `undefined`, never `0` |
|
|
18
|
-
| `@jarenjs/core/async` | `mapConcurrent(items, limit, worker, { signal })` — the bounded ordered asynchronous map: never more than `limit` workers in flight, results in input order, a rejection or an abort stops dispatch and drains the lanes before the map rejects, so nothing is still running when it settles |
|
|
19
|
+
| `@jarenjs/core/async` | `mapConcurrent(items, limit, worker, { signal })` — the bounded ordered asynchronous map: never more than `limit` workers in flight, results in input order, a rejection or an abort stops dispatch and drains the lanes before the map rejects, so nothing is still running when it settles; and `createAwaitedSink(sink)` — the suite's one write serializer: every `write` waits for the previous one to settle (a sink may answer a promise — a socket waiting for `drain`, a stream waiting for the consumer's pull), `end` waits for every write, `abort` rejects what is still queued at once, one failure stops everything behind it, and a synchronous sink stays on a no-promise fast path |
|
|
19
20
|
| `@jarenjs/core/chunk` | cutting a value down to size: `sizeOf` (the suite's one size rule — a string is its length, anything else its JSON), `excerpt`, `truncate`, and `chunkText` by size, line or separator with offsets that locate a piece in its source |
|
|
20
21
|
| `@jarenjs/core/scan` | char-code constants and predicates for recursive-descent parsers |
|
|
21
22
|
| `@jarenjs/core/message` | the message template/catalog compiler shared by the validator and the form layer |
|
|
@@ -190,6 +191,110 @@ t({ comparison: '>=', limit: 18 }); // 'must be >= 18'
|
|
|
190
191
|
|
|
191
192
|
The JSON addressing and query standards — JSON validation helpers, JSON Pointer (RFC 6901), the compiling JSONPath engine (RFC 9535), and the Jaren JSON Query language with its XQuery front-end — live in [`@jarenjs/json`](../json). This package supplies their foundations: the char-code scanner (`@jarenjs/core/scan`), `equalsJson` deep equality, code-point ordering, and the I-Regexp (RFC 9485) toolbox.
|
|
192
193
|
|
|
194
|
+
## Exports
|
|
195
|
+
|
|
196
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
197
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
198
|
+
|
|
199
|
+
<!--fact:exports.core-->
|
|
200
|
+
| Import | Kind | Declarations |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `@jarenjs/core` | JavaScript | declared |
|
|
203
|
+
| `@jarenjs/core/array` | JavaScript | declared |
|
|
204
|
+
| `@jarenjs/core/async` | JavaScript | declared |
|
|
205
|
+
| `@jarenjs/core/bigint` | JavaScript | declared |
|
|
206
|
+
| `@jarenjs/core/cache` | JavaScript | declared |
|
|
207
|
+
| `@jarenjs/core/chunk` | JavaScript | declared |
|
|
208
|
+
| `@jarenjs/core/color` | JavaScript | declared |
|
|
209
|
+
| `@jarenjs/core/dates` | JavaScript | declared |
|
|
210
|
+
| `@jarenjs/core/errors` | JavaScript | declared |
|
|
211
|
+
| `@jarenjs/core/dates/civil` | JavaScript | declared |
|
|
212
|
+
| `@jarenjs/core/dates/duration` | JavaScript | declared |
|
|
213
|
+
| `@jarenjs/core/dates/format` | JavaScript | declared |
|
|
214
|
+
| `@jarenjs/core/dates/index` | JavaScript | declared |
|
|
215
|
+
| `@jarenjs/core/dates/parse` | JavaScript | declared |
|
|
216
|
+
| `@jarenjs/core/dates/rfc3339` | JavaScript | declared |
|
|
217
|
+
| `@jarenjs/core/dates/ticks` | JavaScript | declared |
|
|
218
|
+
| `@jarenjs/core/float` | JavaScript | declared |
|
|
219
|
+
| `@jarenjs/core/geo` | JavaScript | declared |
|
|
220
|
+
| `@jarenjs/core/geo/angle` | JavaScript | declared |
|
|
221
|
+
| `@jarenjs/core/geo/bbox` | JavaScript | declared |
|
|
222
|
+
| `@jarenjs/core/geo/distance` | JavaScript | declared |
|
|
223
|
+
| `@jarenjs/core/geo/geohash` | JavaScript | declared |
|
|
224
|
+
| `@jarenjs/core/geo/geojson` | JavaScript | declared |
|
|
225
|
+
| `@jarenjs/core/geo/index` | JavaScript | declared |
|
|
226
|
+
| `@jarenjs/core/geo/index-tree` | JavaScript | declared |
|
|
227
|
+
| `@jarenjs/core/geo/mercator` | JavaScript | declared |
|
|
228
|
+
| `@jarenjs/core/geo/predicates` | JavaScript | declared |
|
|
229
|
+
| `@jarenjs/core/geo/ring` | JavaScript | declared |
|
|
230
|
+
| `@jarenjs/core/geo/simplify` | JavaScript | declared |
|
|
231
|
+
| `@jarenjs/core/geo/valid` | JavaScript | declared |
|
|
232
|
+
| `@jarenjs/core/geo/wkt` | JavaScript | declared |
|
|
233
|
+
| `@jarenjs/core/vector` | JavaScript | declared |
|
|
234
|
+
| `@jarenjs/core/function` | JavaScript | declared |
|
|
235
|
+
| `@jarenjs/core/integer` | JavaScript | declared |
|
|
236
|
+
| `@jarenjs/core/message` | JavaScript | declared |
|
|
237
|
+
| `@jarenjs/core/number` | JavaScript | declared |
|
|
238
|
+
| `@jarenjs/core/object` | JavaScript | declared |
|
|
239
|
+
| `@jarenjs/core/random` | JavaScript | declared |
|
|
240
|
+
| `@jarenjs/core/runtime` | JavaScript | declared |
|
|
241
|
+
| `@jarenjs/core/scan` | JavaScript | declared |
|
|
242
|
+
| `@jarenjs/core/schema` | JavaScript | declared |
|
|
243
|
+
| `@jarenjs/core/series` | JavaScript | declared |
|
|
244
|
+
| `@jarenjs/core/series/asof` | JavaScript | declared |
|
|
245
|
+
| `@jarenjs/core/series/bucket` | JavaScript | declared |
|
|
246
|
+
| `@jarenjs/core/series/downsample` | JavaScript | declared |
|
|
247
|
+
| `@jarenjs/core/series/index` | JavaScript | declared |
|
|
248
|
+
| `@jarenjs/core/series/interval` | JavaScript | declared |
|
|
249
|
+
| `@jarenjs/core/series/interval-index` | JavaScript | declared |
|
|
250
|
+
| `@jarenjs/core/series/normalize` | JavaScript | declared |
|
|
251
|
+
| `@jarenjs/core/series/rolling` | JavaScript | declared |
|
|
252
|
+
| `@jarenjs/core/series/selector` | JavaScript | declared |
|
|
253
|
+
| `@jarenjs/core/series/zone` | JavaScript | declared |
|
|
254
|
+
| `@jarenjs/core/stats` | JavaScript | declared |
|
|
255
|
+
| `@jarenjs/core/string` | JavaScript | declared |
|
|
256
|
+
| `@jarenjs/core/text` | JavaScript | declared |
|
|
257
|
+
| `@jarenjs/core/text/base64` | JavaScript | declared |
|
|
258
|
+
| `@jarenjs/core/text/basic` | JavaScript | declared |
|
|
259
|
+
| `@jarenjs/core/text/email` | JavaScript | declared |
|
|
260
|
+
| `@jarenjs/core/text/host` | JavaScript | declared |
|
|
261
|
+
| `@jarenjs/core/text/i18n` | JavaScript | declared |
|
|
262
|
+
| `@jarenjs/core/text/identifiers` | JavaScript | declared |
|
|
263
|
+
| `@jarenjs/core/text/index` | JavaScript | declared |
|
|
264
|
+
| `@jarenjs/core/text/iregexp` | JavaScript | declared |
|
|
265
|
+
| `@jarenjs/core/text/misc` | JavaScript | declared |
|
|
266
|
+
| `@jarenjs/core/text/punycode` | JavaScript | declared |
|
|
267
|
+
| `@jarenjs/core/text/sse` | JavaScript | declared |
|
|
268
|
+
| `@jarenjs/core/math` | JavaScript | declared |
|
|
269
|
+
| `@jarenjs/core/math/float64` | JavaScript | declared |
|
|
270
|
+
| `@jarenjs/core/math/format` | JavaScript | declared |
|
|
271
|
+
| `@jarenjs/core/math/index` | JavaScript | declared |
|
|
272
|
+
| `@jarenjs/core/math/int32` | JavaScript | declared |
|
|
273
|
+
| `@jarenjs/core/math/mat4` | JavaScript | declared |
|
|
274
|
+
| `@jarenjs/core/math/project` | JavaScript | declared |
|
|
275
|
+
| `@jarenjs/core/math/solve` | JavaScript | declared |
|
|
276
|
+
| `@jarenjs/core/math/vec2f64` | JavaScript | declared |
|
|
277
|
+
| `@jarenjs/core/math/vec2i32` | JavaScript | declared |
|
|
278
|
+
| `@jarenjs/core/math/vec3f64` | JavaScript | declared |
|
|
279
|
+
| `@jarenjs/core/math/word` | JavaScript | declared |
|
|
280
|
+
| `@jarenjs/core/finance` | JavaScript | declared |
|
|
281
|
+
| `@jarenjs/core/finance/amortization` | JavaScript | declared |
|
|
282
|
+
| `@jarenjs/core/finance/bond` | JavaScript | declared |
|
|
283
|
+
| `@jarenjs/core/finance/cashflow` | JavaScript | declared |
|
|
284
|
+
| `@jarenjs/core/finance/depreciation` | JavaScript | declared |
|
|
285
|
+
| `@jarenjs/core/finance/index` | JavaScript | declared |
|
|
286
|
+
| `@jarenjs/core/finance/indicators` | JavaScript | declared |
|
|
287
|
+
| `@jarenjs/core/finance/interest` | JavaScript | declared |
|
|
288
|
+
| `@jarenjs/core/finance/returns` | JavaScript | declared |
|
|
289
|
+
| `@jarenjs/core/finance/tvm` | JavaScript | declared |
|
|
290
|
+
| `@jarenjs/core/convert` | JavaScript | declared |
|
|
291
|
+
| `@jarenjs/core/convert/convert` | JavaScript | declared |
|
|
292
|
+
| `@jarenjs/core/convert/currency` | JavaScript | declared |
|
|
293
|
+
| `@jarenjs/core/convert/index` | JavaScript | declared |
|
|
294
|
+
| `@jarenjs/core/convert/registry` | JavaScript | declared |
|
|
295
|
+
| `@jarenjs/core/package.json` | metadata | — |
|
|
296
|
+
<!--/fact-->
|
|
297
|
+
|
|
193
298
|
## Development
|
|
194
299
|
|
|
195
300
|
Unit tests live in `test/core/` at the repository root (`npm run test:core`). This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document, with per-module references under [docs/](./docs/) (`MATH`, `CONVERT`, `FINANCE`, `DATES`, `GEO`); see the repository [README](../../README.md) and [ARCHITECTURE](../../docs/ARCHITECTURE.md) for the monorepo picture, and the [ROADMAP](../../docs/ROADMAP.md) for planned work.
|
package/dist/types/async.d.ts
CHANGED
|
@@ -37,3 +37,106 @@
|
|
|
37
37
|
export declare function mapConcurrent<T, R>(items: readonly T[], limit: number, worker: (item: T, index: number) => Promise<R> | R, options?: {
|
|
38
38
|
signal?: AbortSignal;
|
|
39
39
|
}): Promise<R[]>;
|
|
40
|
+
export type SinkLike<T> = {
|
|
41
|
+
write: (chunk: T) => unknown;
|
|
42
|
+
end?: () => unknown;
|
|
43
|
+
abort?: (reason: unknown) => unknown;
|
|
44
|
+
};
|
|
45
|
+
export type AwaitedSink<T> = {
|
|
46
|
+
/**
|
|
47
|
+
* - queue one
|
|
48
|
+
* chunk behind every earlier write; `undefined` when the underlying
|
|
49
|
+
* write answered synchronously with nothing pending (the fast path),
|
|
50
|
+
* else a promise that settles when the underlying write did
|
|
51
|
+
*/
|
|
52
|
+
write: (chunk: T) => Promise<void> | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* - queue the underlying `end` once
|
|
55
|
+
* after every write; repeated calls answer the same promise
|
|
56
|
+
*/
|
|
57
|
+
end: () => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* - stop accepting
|
|
60
|
+
* writes, reject the writes still queued, and call the underlying
|
|
61
|
+
* `abort` once, immediately; repeated calls answer the same promise
|
|
62
|
+
*/
|
|
63
|
+
abort: (reason?: unknown) => Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* - whether a write rejected or an abort
|
|
66
|
+
* happened; nothing reaches the underlying sink after that
|
|
67
|
+
*/
|
|
68
|
+
failed: () => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* - whether `end` or `abort` was called
|
|
71
|
+
*/
|
|
72
|
+
closed: () => boolean;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The underlying sink an awaited sink serializes: `write` answers a
|
|
76
|
+
* value (the chunk is written) or a promise (the chunk is written when
|
|
77
|
+
* it settles — a socket waiting for `drain`, a stream waiting for the
|
|
78
|
+
* consumer's next pull); `end` and `abort` are optional and may answer
|
|
79
|
+
* either way too.
|
|
80
|
+
* @template T
|
|
81
|
+
* @typedef {Object} SinkLike
|
|
82
|
+
* @property {(chunk: T) => unknown} write
|
|
83
|
+
* @property {() => unknown} [end]
|
|
84
|
+
* @property {(reason: unknown) => unknown} [abort]
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* The serialized, awaited view of a sink.
|
|
88
|
+
* @template T
|
|
89
|
+
* @typedef {Object} AwaitedSink
|
|
90
|
+
* @property {(chunk: T) => Promise<void> | undefined} write - queue one
|
|
91
|
+
* chunk behind every earlier write; `undefined` when the underlying
|
|
92
|
+
* write answered synchronously with nothing pending (the fast path),
|
|
93
|
+
* else a promise that settles when the underlying write did
|
|
94
|
+
* @property {() => Promise<void>} end - queue the underlying `end` once
|
|
95
|
+
* after every write; repeated calls answer the same promise
|
|
96
|
+
* @property {(reason?: unknown) => Promise<void>} abort - stop accepting
|
|
97
|
+
* writes, reject the writes still queued, and call the underlying
|
|
98
|
+
* `abort` once, immediately; repeated calls answer the same promise
|
|
99
|
+
* @property {() => boolean} failed - whether a write rejected or an abort
|
|
100
|
+
* happened; nothing reaches the underlying sink after that
|
|
101
|
+
* @property {() => boolean} closed - whether `end` or `abort` was called
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* Serialize a sink: every write waits for the previous one, `end` waits
|
|
105
|
+
* for every write, and one failure stops everything after it. This is
|
|
106
|
+
* the one place the suite decides what "the next chunk" means for a
|
|
107
|
+
* sink that may answer a promise — a Node response whose `write()` said
|
|
108
|
+
* `false` (wait for `drain`), a Web `ReadableStream` bridge that waits
|
|
109
|
+
* for the consumer's pull, a stream runner whose carrier hooks may be
|
|
110
|
+
* asynchronous — so backpressure is a property of the sink, never of
|
|
111
|
+
* the code writing into it.
|
|
112
|
+
*
|
|
113
|
+
* The contract, in full:
|
|
114
|
+
*
|
|
115
|
+
* - writes reach the underlying sink in call order and never overlap:
|
|
116
|
+
* the next underlying write begins only after the previous one
|
|
117
|
+
* settled;
|
|
118
|
+
* - a synchronous sink stays on a no-extra-promise fast path: while
|
|
119
|
+
* nothing is pending and the underlying write answers a non-thenable,
|
|
120
|
+
* `write` answers `undefined`; the first thenable answer opens the
|
|
121
|
+
* queue, and an idle queue returns to the fast path;
|
|
122
|
+
* - `end()` runs the underlying `end` once, after every write queued
|
|
123
|
+
* before it; `abort(reason)` runs the underlying `abort` once, at once
|
|
124
|
+
* (an abort is urgent — the pending underlying write is not waited
|
|
125
|
+
* for), and every write still queued rejects with the reason;
|
|
126
|
+
* - a write that throws or rejects (including a throwing `then` getter
|
|
127
|
+
* on its answer) fails the sink: its own promise
|
|
128
|
+
* rejects, every write queued behind it rejects with the same reason
|
|
129
|
+
* without reaching the underlying sink, later writes reject at once,
|
|
130
|
+
* and `end()` rejects too — the stream did not end cleanly;
|
|
131
|
+
* - `end` and `abort` are mutually terminal: the first one decides, the
|
|
132
|
+
* other one (and every repeat) answers the first one's promise; a
|
|
133
|
+
* write after either rejects;
|
|
134
|
+
* - `write` never throws — a refusal is a rejected promise, so a caller
|
|
135
|
+
* handles one shape.
|
|
136
|
+
*
|
|
137
|
+
* @template T
|
|
138
|
+
* @param {SinkLike<T>} sink
|
|
139
|
+
* @returns {AwaitedSink<T>}
|
|
140
|
+
* @throws {TypeError} when `sink` has no `write` function
|
|
141
|
+
*/
|
|
142
|
+
export declare function createAwaitedSink<T>(sink: SinkLike<T>): AwaitedSink<T>;
|
|
@@ -45,7 +45,8 @@ export declare function compileDateFormat(pattern: string, names?: DateNames): (
|
|
|
45
45
|
* `full-time`, and the offset is the record's own rather than UTC.
|
|
46
46
|
*
|
|
47
47
|
* The round trip preserves the *value*, not necessarily the spelling: a
|
|
48
|
-
* fractional second
|
|
48
|
+
* fractional second keeps the parser's six-digit precision and is emitted
|
|
49
|
+
* only when non-zero and without trailing
|
|
49
50
|
* zeros, so `…:05.250Z` comes back `…:05.25Z`. The parts record holds
|
|
50
51
|
* the fraction as a number, so the original digit count is not
|
|
51
52
|
* recoverable — a consumer that must reproduce the input byte for byte
|
package/dist/types/object.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Deep equality comparison for arbitrary values.
|
|
3
3
|
*
|
|
4
4
|
* Generic JavaScript equality: understands Maps, Sets, RegExps,
|
|
5
|
-
* functions, typed arrays and class instances (constructors
|
|
6
|
-
* match). Not the same as
|
|
5
|
+
* functions, typed arrays and class instances (prototype constructors
|
|
6
|
+
* must match; an own `constructor` member is data). Not the same as
|
|
7
|
+
* `equalsJson`, which compares JSON values
|
|
7
8
|
* only and is the hot-path variant — keep both.
|
|
8
9
|
* @param {any} target
|
|
9
10
|
* @param {any} source
|
|
@@ -111,7 +112,7 @@ export declare function contentKey(value: any): string;
|
|
|
111
112
|
* functions, symbols, cycles, and non-plain objects (a `Date`, `Map`,
|
|
112
113
|
* `RegExp` or class instance, all of which serialize to `{}`), plus the
|
|
113
114
|
* two members a serialization cannot show — a symbol key, and an own
|
|
114
|
-
* array property
|
|
115
|
+
* array property that is not an element index. A caller that may hold such a
|
|
115
116
|
* value must treat the refusal as "not cacheable" and compute afresh —
|
|
116
117
|
* never as "reuse whatever shares the key".
|
|
117
118
|
*
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The runtime record: the host facts every subsystem that needs
|
|
3
|
+
* one used to take separately — the clock, secure identifiers,
|
|
4
|
+
* randomness and the zone provider — as one frozen record a host builds
|
|
5
|
+
* once and hands to the store, the jobs engine, the migration runner and
|
|
6
|
+
* the http binding. Nothing here IS a clock, a random source or a zone
|
|
7
|
+
* database: the defaults are the platform's own (`Date.now`,
|
|
8
|
+
* `crypto.randomUUID`, `Math.random`, and no zone provider, which keeps
|
|
9
|
+
* a named zone the refusal it always was), so adopting the record changes
|
|
10
|
+
* nothing a consumer can observe. What it buys is that a deterministic
|
|
11
|
+
* run — a fixed clock, a seeded generator, a counting identifier — is
|
|
12
|
+
* configured in one place, and that four subsystems can no longer
|
|
13
|
+
* disagree about what time it is.
|
|
14
|
+
*
|
|
15
|
+
* Precedence is fixed: a subsystem's own explicit option wins over the
|
|
16
|
+
* record's member, which wins over the built-in default. The options are
|
|
17
|
+
* published surface, and a record that silently overrode them would be a
|
|
18
|
+
* breaking change dressed as an ergonomic.
|
|
19
|
+
*
|
|
20
|
+
* The record reaches hosts, never query compilation. A compiled query is
|
|
21
|
+
* cached by document identity and saved as a rule, so the current instant
|
|
22
|
+
* enters it as data — an external — and there is no `now` operator for
|
|
23
|
+
* this record to feed.
|
|
24
|
+
*/
|
|
25
|
+
export type Runtime = {
|
|
26
|
+
/**
|
|
27
|
+
* - the clock, in epoch milliseconds
|
|
28
|
+
*/
|
|
29
|
+
now: () => number;
|
|
30
|
+
/**
|
|
31
|
+
* - a fresh identifier; secure by default
|
|
32
|
+
*/
|
|
33
|
+
uuid: () => string;
|
|
34
|
+
/**
|
|
35
|
+
* - uniform in `[0, 1)`
|
|
36
|
+
*/
|
|
37
|
+
random: () => number;
|
|
38
|
+
/**
|
|
39
|
+
* - the tzdb a named zone is read through, or `null`: a named zone is
|
|
40
|
+
* then a refusal, never a quiet UTC
|
|
41
|
+
*/
|
|
42
|
+
zoneProvider: import('./series/zone.js').ZoneProvider | null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The record every host-facing subsystem takes as `runtime`.
|
|
46
|
+
* @typedef {Object} Runtime
|
|
47
|
+
* @property {() => number} now - the clock, in epoch milliseconds
|
|
48
|
+
* @property {() => string} uuid - a fresh identifier; secure by default
|
|
49
|
+
* @property {() => number} random - uniform in `[0, 1)`
|
|
50
|
+
* @property {import('./series/zone.js').ZoneProvider | null} zoneProvider
|
|
51
|
+
* - the tzdb a named zone is read through, or `null`: a named zone is
|
|
52
|
+
* then a refusal, never a quiet UTC
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* The four members a runtime record carries, so a subsystem, a test and
|
|
56
|
+
* a document all spell the host facts the same way.
|
|
57
|
+
*/
|
|
58
|
+
export declare const RUNTIME_MEMBERS: readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* Build a runtime record: the platform defaults, with any member
|
|
61
|
+
* overridden. The result is frozen, so a subsystem that was handed one
|
|
62
|
+
* can hand it on without a copy.
|
|
63
|
+
*
|
|
64
|
+
* The record is closed: a member it does not have is a refusal naming
|
|
65
|
+
* the four it does, because `clock` for `now` quietly ignored would be a
|
|
66
|
+
* deterministic run that is not.
|
|
67
|
+
*
|
|
68
|
+
* @param {Partial<Runtime>} [overrides]
|
|
69
|
+
* @returns {Readonly<Runtime>}
|
|
70
|
+
* @throws {TypeError} for a member that is not a function, a
|
|
71
|
+
* `zoneProvider` that is neither `null` nor a provider, or a member
|
|
72
|
+
* the record does not have
|
|
73
|
+
* @example
|
|
74
|
+
* createRuntime(); // the platform's own
|
|
75
|
+
* createRuntime({ now: () => 1_700_000_000_000 }); // a fixed clock, the rest default
|
|
76
|
+
* createRuntime({ uuid: () => `id-${++n}`, random: mulberry32(1), zoneProvider });
|
|
77
|
+
*/
|
|
78
|
+
export declare function createRuntime(overrides?: Partial<Runtime>): Readonly<Runtime>;
|
|
79
|
+
/**
|
|
80
|
+
* The record a subsystem reads its `runtime` option through: nothing
|
|
81
|
+
* given is the platform default, and anything given is validated and
|
|
82
|
+
* frozen — so every member a subsystem reads is a function, and a
|
|
83
|
+
* malformed record is refused where it was passed rather than where it
|
|
84
|
+
* was first called.
|
|
85
|
+
* @param {Partial<Runtime> | undefined | null} [candidate] - a subsystem's `options.runtime`
|
|
86
|
+
* @returns {Readonly<Runtime>}
|
|
87
|
+
* @throws {TypeError} as `createRuntime` does
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveRuntime(candidate?: Partial<Runtime> | undefined | null): Readonly<Runtime>;
|
|
@@ -2,6 +2,13 @@ export { toEpoch, normalizeSeries, canonicalSeries, normalizeIntervals, lowerBou
|
|
|
2
2
|
export { containsInstant, overlapsInterval, intersectInterval, mergeIntervals, subtractIntervals, gapsWithin, coverageOf, findSlots, MERGE_MEMBERS, SLOTS_MEMBERS, } from './interval.js';
|
|
3
3
|
export { createIntervalIndex } from './interval-index.js';
|
|
4
4
|
export { resolveClock, CLOCK_MEMBERS } from './zone.js';
|
|
5
|
+
export type ZoneProvider = import('./zone.js').ZoneProvider;
|
|
6
|
+
/**
|
|
7
|
+
* The wall clock a caller supplies for a named zone - the shape a host
|
|
8
|
+
* passes as `provider`, named here so it can be spelled where the seam
|
|
9
|
+
* is crossed.
|
|
10
|
+
* @typedef {import('./zone.js').ZoneProvider} ZoneProvider
|
|
11
|
+
*/
|
|
5
12
|
export { compileBuckets, resampleSeries, RESAMPLE_MEMBERS } from './bucket.js';
|
|
6
13
|
export { rollingSeries, ROLLING_MEMBERS } from './rolling.js';
|
|
7
14
|
export { asOfJoin, ASOF_MEMBERS } from './asof.js';
|
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/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
|
|
@@ -317,26 +331,27 @@ 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 <!--fact:series.kernelVsCeiling-->3.
|
|
321
|
-
and against the vocabulary a consumer had instead it is <!--fact:series.kernelVsQuery-->
|
|
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.
|
|
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× |
|
|
331
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. <!--fact:series.asofShape-->The as-of join costs
|
|
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. <!--fact:series.zoneCost-->Walking every boundary through
|
|
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.67.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
"types": "./dist/types/random.d.ts",
|
|
113
113
|
"default": "./src/random.js"
|
|
114
114
|
},
|
|
115
|
+
"./runtime": {
|
|
116
|
+
"types": "./dist/types/runtime.d.ts",
|
|
117
|
+
"default": "./src/runtime.js"
|
|
118
|
+
},
|
|
115
119
|
"./scan": {
|
|
116
120
|
"types": "./dist/types/scan.d.ts",
|
|
117
121
|
"default": "./src/scan.js"
|
package/src/async.js
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* about what the worker does; those are the caller's policies around it.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { isThenable } from './function.js';
|
|
31
|
+
|
|
30
32
|
/**
|
|
31
33
|
* @template T, R
|
|
32
34
|
* @param {readonly T[]} items
|
|
@@ -76,3 +78,208 @@ export async function mapConcurrent(items, limit, worker, options = {}) {
|
|
|
76
78
|
if (stop.length > 0) throw stop[0];
|
|
77
79
|
return results;
|
|
78
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
|
|
package/src/object.js
CHANGED
|
@@ -14,8 +14,9 @@ const hasOwn = Object.hasOwn;
|
|
|
14
14
|
* Deep equality comparison for arbitrary values.
|
|
15
15
|
*
|
|
16
16
|
* Generic JavaScript equality: understands Maps, Sets, RegExps,
|
|
17
|
-
* functions, typed arrays and class instances (constructors
|
|
18
|
-
* match). Not the same as
|
|
17
|
+
* functions, typed arrays and class instances (prototype constructors
|
|
18
|
+
* must match; an own `constructor` member is data). Not the same as
|
|
19
|
+
* `equalsJson`, which compares JSON values
|
|
19
20
|
* only and is the hot-path variant — keep both.
|
|
20
21
|
* @param {any} target
|
|
21
22
|
* @param {any} source
|
|
@@ -34,23 +35,24 @@ export function equalsDeep(target, source) {
|
|
|
34
35
|
if (isScalarType(target))
|
|
35
36
|
return false;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
const constructor = Object.getPrototypeOf(target)?.constructor;
|
|
39
|
+
if (constructor !== Object.getPrototypeOf(source)?.constructor)
|
|
38
40
|
return false;
|
|
39
41
|
|
|
40
|
-
if (
|
|
42
|
+
if (constructor === Object) {
|
|
41
43
|
const tks = Object.keys(target);
|
|
42
44
|
const sks = Object.keys(source);
|
|
43
45
|
if (tks.length !== sks.length)
|
|
44
46
|
return false;
|
|
45
47
|
for (let i = 0; i < tks.length; ++i) {
|
|
46
48
|
const key = tks[i];
|
|
47
|
-
if (!equalsDeep(target[key], source[key]))
|
|
49
|
+
if (!hasOwn(source, key) || !equalsDeep(target[key], source[key]))
|
|
48
50
|
return false;
|
|
49
51
|
}
|
|
50
52
|
return true;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
if (
|
|
55
|
+
if (constructor === Map) {
|
|
54
56
|
if (target.size !== source.size)
|
|
55
57
|
return false;
|
|
56
58
|
for (const [key, value] of target) {
|
|
@@ -62,7 +64,7 @@ export function equalsDeep(target, source) {
|
|
|
62
64
|
return true;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
if (
|
|
67
|
+
if (constructor === Array) {
|
|
66
68
|
if (target.length !== source.length)
|
|
67
69
|
return false;
|
|
68
70
|
for (let i = 0; i < target.length; ++i) {
|
|
@@ -72,7 +74,7 @@ export function equalsDeep(target, source) {
|
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
if (
|
|
77
|
+
if (constructor === Set) {
|
|
76
78
|
if (target.size !== source.size)
|
|
77
79
|
return false;
|
|
78
80
|
for (const value of target) {
|
|
@@ -82,7 +84,7 @@ export function equalsDeep(target, source) {
|
|
|
82
84
|
return true;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
if (
|
|
87
|
+
if (constructor === RegExp) {
|
|
86
88
|
return target.toString() === source.toString();
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -104,7 +106,7 @@ export function equalsDeep(target, source) {
|
|
|
104
106
|
if (tkeys.length === 0) return true;
|
|
105
107
|
for (let i = 0; i < tkeys.length; ++i) {
|
|
106
108
|
const key = tkeys[i];
|
|
107
|
-
if (!equalsDeep(target[key], source[key]))
|
|
109
|
+
if (!hasOwn(source, key) || !equalsDeep(target[key], source[key]))
|
|
108
110
|
return false;
|
|
109
111
|
}
|
|
110
112
|
return true;
|
|
@@ -375,7 +377,7 @@ function semanticToken(value, path, open) {
|
|
|
375
377
|
// an own property beyond the elements would vanish positionally
|
|
376
378
|
for (const key of Object.keys(items)) {
|
|
377
379
|
const index = Number(key);
|
|
378
|
-
if (!Number.isInteger(index) || index < 0 || index >= items.length)
|
|
380
|
+
if (!Number.isInteger(index) || index < 0 || index >= items.length || String(index) !== key)
|
|
379
381
|
refuse(`the extra array property ${JSON.stringify(key)}`);
|
|
380
382
|
}
|
|
381
383
|
out = '[';
|
|
@@ -422,7 +424,7 @@ function semanticToken(value, path, open) {
|
|
|
422
424
|
* functions, symbols, cycles, and non-plain objects (a `Date`, `Map`,
|
|
423
425
|
* `RegExp` or class instance, all of which serialize to `{}`), plus the
|
|
424
426
|
* two members a serialization cannot show — a symbol key, and an own
|
|
425
|
-
* array property
|
|
427
|
+
* array property that is not an element index. A caller that may hold such a
|
|
426
428
|
* value must treat the refusal as "not cacheable" and compute afresh —
|
|
427
429
|
* never as "reuse whatever shares the key".
|
|
428
430
|
*
|
|
@@ -634,4 +636,4 @@ export function mergeSet(set, ...iterables) {
|
|
|
634
636
|
set.add(item);
|
|
635
637
|
}
|
|
636
638
|
}
|
|
637
|
-
}
|
|
639
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The runtime record: the host facts every subsystem that needs
|
|
4
|
+
* one used to take separately — the clock, secure identifiers,
|
|
5
|
+
* randomness and the zone provider — as one frozen record a host builds
|
|
6
|
+
* once and hands to the store, the jobs engine, the migration runner and
|
|
7
|
+
* the http binding. Nothing here IS a clock, a random source or a zone
|
|
8
|
+
* database: the defaults are the platform's own (`Date.now`,
|
|
9
|
+
* `crypto.randomUUID`, `Math.random`, and no zone provider, which keeps
|
|
10
|
+
* a named zone the refusal it always was), so adopting the record changes
|
|
11
|
+
* nothing a consumer can observe. What it buys is that a deterministic
|
|
12
|
+
* run — a fixed clock, a seeded generator, a counting identifier — is
|
|
13
|
+
* configured in one place, and that four subsystems can no longer
|
|
14
|
+
* disagree about what time it is.
|
|
15
|
+
*
|
|
16
|
+
* Precedence is fixed: a subsystem's own explicit option wins over the
|
|
17
|
+
* record's member, which wins over the built-in default. The options are
|
|
18
|
+
* published surface, and a record that silently overrode them would be a
|
|
19
|
+
* breaking change dressed as an ergonomic.
|
|
20
|
+
*
|
|
21
|
+
* The record reaches hosts, never query compilation. A compiled query is
|
|
22
|
+
* cached by document identity and saved as a rule, so the current instant
|
|
23
|
+
* enters it as data — an external — and there is no `now` operator for
|
|
24
|
+
* this record to feed.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The record every host-facing subsystem takes as `runtime`.
|
|
29
|
+
* @typedef {Object} Runtime
|
|
30
|
+
* @property {() => number} now - the clock, in epoch milliseconds
|
|
31
|
+
* @property {() => string} uuid - a fresh identifier; secure by default
|
|
32
|
+
* @property {() => number} random - uniform in `[0, 1)`
|
|
33
|
+
* @property {import('./series/zone.js').ZoneProvider | null} zoneProvider
|
|
34
|
+
* - the tzdb a named zone is read through, or `null`: a named zone is
|
|
35
|
+
* then a refusal, never a quiet UTC
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The four members a runtime record carries, so a subsystem, a test and
|
|
40
|
+
* a document all spell the host facts the same way.
|
|
41
|
+
*/
|
|
42
|
+
export const RUNTIME_MEMBERS = Object.freeze(['now', 'uuid', 'random', 'zoneProvider']);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The platform's own answers — exactly what every subsystem fell back
|
|
46
|
+
* to before the record existed, member for member, so a run that never
|
|
47
|
+
* builds a record behaves as it always did.
|
|
48
|
+
* @type {Readonly<Runtime>}
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_RUNTIME = Object.freeze({
|
|
51
|
+
now: Date.now,
|
|
52
|
+
// `randomUUID` is a method of the platform's Crypto object and refuses
|
|
53
|
+
// to run unbound, so the default is a call rather than a reference
|
|
54
|
+
uuid: () => globalThis.crypto.randomUUID(),
|
|
55
|
+
random: Math.random,
|
|
56
|
+
zoneProvider: null,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether a value is the pair of functions the temporal kernel's clock
|
|
61
|
+
* seam takes for a named zone.
|
|
62
|
+
* @param {any} value
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isZoneProvider(value) {
|
|
66
|
+
return value !== null && typeof value === 'object'
|
|
67
|
+
&& typeof value.toParts === 'function' && typeof value.toEpoch === 'function';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a runtime record: the platform defaults, with any member
|
|
72
|
+
* overridden. The result is frozen, so a subsystem that was handed one
|
|
73
|
+
* can hand it on without a copy.
|
|
74
|
+
*
|
|
75
|
+
* The record is closed: a member it does not have is a refusal naming
|
|
76
|
+
* the four it does, because `clock` for `now` quietly ignored would be a
|
|
77
|
+
* deterministic run that is not.
|
|
78
|
+
*
|
|
79
|
+
* @param {Partial<Runtime>} [overrides]
|
|
80
|
+
* @returns {Readonly<Runtime>}
|
|
81
|
+
* @throws {TypeError} for a member that is not a function, a
|
|
82
|
+
* `zoneProvider` that is neither `null` nor a provider, or a member
|
|
83
|
+
* the record does not have
|
|
84
|
+
* @example
|
|
85
|
+
* createRuntime(); // the platform's own
|
|
86
|
+
* createRuntime({ now: () => 1_700_000_000_000 }); // a fixed clock, the rest default
|
|
87
|
+
* createRuntime({ uuid: () => `id-${++n}`, random: mulberry32(1), zoneProvider });
|
|
88
|
+
*/
|
|
89
|
+
export function createRuntime(overrides = undefined) {
|
|
90
|
+
if (overrides === undefined || overrides === null)
|
|
91
|
+
return DEFAULT_RUNTIME;
|
|
92
|
+
if (typeof overrides !== 'object')
|
|
93
|
+
throw new TypeError('a runtime record is an object');
|
|
94
|
+
/** @type {any} */
|
|
95
|
+
const record = { ...DEFAULT_RUNTIME };
|
|
96
|
+
for (const key of Object.keys(overrides)) {
|
|
97
|
+
if (!RUNTIME_MEMBERS.includes(key)) {
|
|
98
|
+
throw new TypeError(`a runtime record has ${
|
|
99
|
+
RUNTIME_MEMBERS.map((m) => `'${m}'`).join(', ')}, not '${key}'`);
|
|
100
|
+
}
|
|
101
|
+
const value = /** @type {any} */ (overrides)[key];
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
if (key === 'zoneProvider') {
|
|
105
|
+
if (value !== null && !isZoneProvider(value)) {
|
|
106
|
+
throw new TypeError('runtime.zoneProvider is null or a provider with'
|
|
107
|
+
+ ' toParts(epoch, zone) and toEpoch(parts, zone, disambiguation)');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (typeof value !== 'function') {
|
|
111
|
+
throw new TypeError(`runtime.${key} is a function`);
|
|
112
|
+
}
|
|
113
|
+
record[key] = value;
|
|
114
|
+
}
|
|
115
|
+
return Object.freeze(record);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The record a subsystem reads its `runtime` option through: nothing
|
|
120
|
+
* given is the platform default, and anything given is validated and
|
|
121
|
+
* frozen — so every member a subsystem reads is a function, and a
|
|
122
|
+
* malformed record is refused where it was passed rather than where it
|
|
123
|
+
* was first called.
|
|
124
|
+
* @param {Partial<Runtime> | undefined | null} [candidate] - a subsystem's `options.runtime`
|
|
125
|
+
* @returns {Readonly<Runtime>}
|
|
126
|
+
* @throws {TypeError} as `createRuntime` does
|
|
127
|
+
*/
|
|
128
|
+
export function resolveRuntime(candidate = undefined) {
|
|
129
|
+
return candidate === DEFAULT_RUNTIME ? DEFAULT_RUNTIME : createRuntime(candidate);
|
|
130
|
+
}
|
package/src/series/index.js
CHANGED
|
@@ -75,6 +75,13 @@ export { createIntervalIndex } from './interval-index.js';
|
|
|
75
75
|
|
|
76
76
|
export { resolveClock, CLOCK_MEMBERS } from './zone.js';
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The wall clock a caller supplies for a named zone - the shape a host
|
|
80
|
+
* passes as `provider`, named here so it can be spelled where the seam
|
|
81
|
+
* is crossed.
|
|
82
|
+
* @typedef {import('./zone.js').ZoneProvider} ZoneProvider
|
|
83
|
+
*/
|
|
84
|
+
|
|
78
85
|
export { compileBuckets, resampleSeries, RESAMPLE_MEMBERS } from './bucket.js';
|
|
79
86
|
|
|
80
87
|
export { rollingSeries, ROLLING_MEMBERS } from './rolling.js';
|
package/src/string.js
CHANGED
|
@@ -166,6 +166,37 @@ export function countCharCode(str, code, start = 0, end = str.length) {
|
|
|
166
166
|
return n;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The UTF-8 byte length of a slice of a string — the one measure every
|
|
171
|
+
* byte bound in the suite counts (a body limit, a page, a change
|
|
172
|
+
* record, a parser's record or token limit) — computed without encoding
|
|
173
|
+
* a copy: one byte below U+0080, two below U+0800, four for a surrogate
|
|
174
|
+
* pair (one code point), three otherwise; a lone surrogate counts three,
|
|
175
|
+
* as its replacement would.
|
|
176
|
+
* @param {string} str
|
|
177
|
+
* @param {number} [start] - Inclusive start offset (defaults to 0)
|
|
178
|
+
* @param {number} [end] - Exclusive end offset (defaults to full length)
|
|
179
|
+
* @returns {number} The UTF-8 bytes of `str[start, end)`
|
|
180
|
+
*/
|
|
181
|
+
export function utf8ByteLength(str, start = 0, end = str.length) {
|
|
182
|
+
let bytes = 0;
|
|
183
|
+
for (let i = start; i < end; i++) {
|
|
184
|
+
const code = str.charCodeAt(i);
|
|
185
|
+
if (code < 0x80) bytes += 1;
|
|
186
|
+
else if (code < 0x800) bytes += 2;
|
|
187
|
+
else if (code >= 0xD800 && code <= 0xDBFF && i + 1 < end) {
|
|
188
|
+
const next = str.charCodeAt(i + 1);
|
|
189
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
190
|
+
bytes += 4;
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
else bytes += 3;
|
|
194
|
+
}
|
|
195
|
+
else bytes += 3;
|
|
196
|
+
}
|
|
197
|
+
return bytes;
|
|
198
|
+
}
|
|
199
|
+
|
|
169
200
|
/**
|
|
170
201
|
* Count the Unicode code points of a string (surrogate-pair aware;
|
|
171
202
|
* a lone surrogate counts as one code point).
|