@jarenjs/core 0.56.0 → 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 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.
@@ -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 is emitted only when non-zero and without trailing
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
@@ -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 must
6
- * match). Not the same as `equalsJson`, which compares JSON values
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 past the last element. A caller that may hold such a
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';
@@ -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). `epochOfRFC3339Parts(parts)` epoch ms: a
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.4× the one-pass bucket loop and 6.2× the one-pass ring sum<!--/fact-->,
321
- and against the vocabulary a consumer had instead it is <!--fact:series.kernelVsQuery-->73.6× faster than the generic query bucket and 81.2× faster than the labelled count window<!--/fact-->.
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.5 ms | 1,667 | 0.45 ms | one-pass loop | 3.4× |
327
- | `resampleSeries`, + linear fill | 1.1 ms | 1,657 | 1 ms | the same buckets, omitting | 1.0× |
328
- | `rollingSeries`, 60 s window | 7.2 ms | 100,000 | 1.2 ms | one-pass ring sum | 6.2× |
329
- | `asOfJoin`, one left row per 100 | 1.7 ms | 1,000 | 2.2 ms | one index read per row | 0.8× |
330
- | `downsampleSeries`, lttb, gap corpus | 1.6 ms | 2,000 | 1.8 ms | the same line with no holes in it | 0.9× |
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 14.7× a handful of index reads, and beats them by 1.3× once there is one left row per hundred right ones. 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-->
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 an injected zone provider costs 1.0× the integer ladder over an identical answer — near parity because it is near nothing, since the benchmark corpus spans 28 hours and holds two daily boundaries. What the suite gates instead is that the provider is consulted per boundary rather than per sample.<!--/fact-->
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; recurrence grammars
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.56.0",
4
+ "version": "0.66.1",
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
+ }
@@ -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) => D3(Math.round((p.seconds - Math.trunc(p.seconds)) * 1000)),
90
- S: (p) => '' + Math.trunc((p.seconds - Math.trunc(p.seconds)) * 10),
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 is emitted only when non-zero and without trailing
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 = fraction.toFixed(3).slice(1).replace(/0+$/, '');
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
@@ -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 - 1, parts.day,
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
- // Date.UTC maps years 0-99 into the 1900s; restore the real year
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 must
18
- * match). Not the same as `equalsJson`, which compares JSON values
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
- if (target.constructor !== source.constructor)
38
+ const constructor = Object.getPrototypeOf(target)?.constructor;
39
+ if (constructor !== Object.getPrototypeOf(source)?.constructor)
38
40
  return false;
39
41
 
40
- if (target.constructor === Object) {
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 (target.constructor === Map) {
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 (target.constructor === Array) {
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 (target.constructor === Set) {
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 (target.constructor === RegExp) {
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 past the last element. A caller that may hold such a
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
+ }
@@ -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).