@jarenjs/core 0.34.2 → 0.43.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
@@ -120,7 +120,8 @@ flowchart TB
120
120
  GeoBbox["bbox.js<br/>Bounding boxes"]
121
121
  GeoHash["geohash.js<br/>Base-32 cells"]
122
122
  GeoJson["geojson.js<br/>Typed-geometry traversal"]
123
- GeoValid["valid.js + wkt.js<br/>Validity testers"]
123
+ GeoValid["valid.js<br/>Structural judgment"]
124
+ GeoWkt["wkt.js<br/>Well-Known Text round trip"]
124
125
  GeoTree["index-tree.js<br/>Packed-Hilbert box index"]
125
126
  GeoMerc["mercator.js + simplify.js<br/>Projection out, simplification"]
126
127
  end
@@ -194,6 +195,7 @@ flowchart TB
194
195
  GeoIndex --> GeoHash
195
196
  GeoIndex --> GeoJson
196
197
  GeoIndex --> GeoValid
198
+ GeoIndex --> GeoWkt
197
199
  GeoIndex --> GeoTree
198
200
  GeoIndex --> GeoMerc
199
201
 
@@ -549,7 +551,7 @@ passed straight in without the `type` discriminator being involved.
549
551
  | `geohash.js` | the base-32 cell encoding, and its validity tester |
550
552
  | `geojson.js` | the one layer that knows the `type` discriminator |
551
553
  | `valid.js` | `isValidGeoJson` — the one-call structural judgment (rings must close), the boolean twin of the schema artifacts in `@jarenjs/json` |
552
- | `wkt.js` | `isValidWkt` a strict validity tester for Well-Known Text |
554
+ | `wkt.js` | the Well-Known Text round trip — `wktToGeoJson`, `geoJsonToWkt` and `isValidWkt`, one grammar walk with and without a builder |
553
555
  | `index-tree.js` | a static packed-Hilbert box index for spatial joins |
554
556
  | `mercator.js` | Web Mercator, the projection *out* for anything that draws |
555
557
  | `simplify.js` | Douglas-Peucker — dropping the vertices a drawing cannot show |
@@ -557,6 +559,45 @@ passed straight in without the `type` discriminator being involved.
557
559
  The three validity testers back the `geoFormats` group in
558
560
  [`@jarenjs/formats`](../formats) (`geohash`, `wkt`, `geojson`).
559
561
 
562
+ **WKT is one grammar walk with two entry points.** `isValidWkt` is a format
563
+ tester — it runs per value in the validator and per keystroke in the form
564
+ layer — so it must not allocate a geometry to answer a boolean; and two
565
+ hand-maintained grammars for one syntax would drift apart. Both are avoided by
566
+ giving every scan function a sink: absent, it validates; present, it appends
567
+ the value it just recognized, and `wktToGeoJson` reads the result. A committed
568
+ corpus of 181 strings, its malformed half included, asserts
569
+ `isValidWkt(s) === (wktToGeoJson(s) !== null)` for every entry — a divergence
570
+ is a failing test, not a note. `geoJsonToWkt` writes the string back, and the
571
+ parse-write-parse direction is asserted over the corpus; the write-parse-write
572
+ direction is *not* claimed, because WKT whitespace, the `M` measure and number
573
+ spelling are normalized on the way through.
574
+
575
+ Against [`wellknown`](https://www.npmjs.com/package/wellknown), the established
576
+ WKT↔GeoJSON converter (`npm run benchmark:geo`, Node v24.19.0):
577
+
578
+ | scenario | Jaren | wellknown | ratio |
579
+ |---|---|---|---|
580
+ | parse a `POINT` | 439 ns | 1.34 µs | **3.1×** |
581
+ | parse a 2000-vertex `POLYGON` | 820 µs | 2.20 ms | **2.7×** |
582
+ | write that polygon | 160 µs | 213 µs | **1.3×** |
583
+ | validate a `POINT` | 241 ns | 1.29 µs | **5.4×** |
584
+ | validate that polygon | 493 µs | 1.94 ms | **3.9×** |
585
+
586
+ The last two rows are the ones the sink exists for: `wellknown` has no
587
+ predicate, so validating means parsing and throwing the geometry away. And the
588
+ comparison is *not* like-for-like on work done — **wellknown validates less**
589
+ and is still slower. Over the corpus it accepts 38 strings this grammar
590
+ refuses (no ring closure, no four-point ring or two-point line minimum, no
591
+ coordinate count checked against the modifier, no tag list, and text after the
592
+ geometry ignored); it refuses 22 this one accepts (`M`/`ZM` geometries, `EMPTY`
593
+ for POINT/MULTIPOINT/GEOMETRYCOLLECTION); and on 10 more the two produce
594
+ *different geometries*, because wellknown answers an EMPTY geometry for a
595
+ POLYGON/MULTILINESTRING/MULTIPOLYGON carrying a modifier and writes a
596
+ MULTIPOINT's measure into the altitude slot. Every one of those classes has a
597
+ pinned size in the benchmark's equivalence table, so a difference cannot be
598
+ quietly absorbed, and the entries behind each are listed under it. The
599
+ stringify output is byte-identical on the polygon.
600
+
560
601
  Two decisions carry the module. **Orientation is computed exactly**, through
561
602
  Shewchuk's adaptive precision arithmetic: a naive floating-point determinant
562
603
  returns the *wrong sign* on near-collinear input, which makes a containment
package/README.md CHANGED
@@ -10,7 +10,7 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
10
10
  |---|---|
11
11
  | `@jarenjs/core` | type guards and getters (`isStringType`, `isObjectClass`, `getIntegerType`, ...) |
12
12
  | `@jarenjs/core/array` | array helpers (`isUniqueArray`, `getUniqueArray`, `includesAll`, ...) |
13
- | `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject` predicate, `__proto__`-safe `setObjectMember`, `deepFreeze`, map/set merging |
13
+ | `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject` and deep `isJsonValue` predicates, `__proto__`-safe `setObjectMember`, `deepFreeze`, map/set merging |
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/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 |
@@ -75,7 +75,7 @@ Grouped by file: `email` (RFC 5321 + internationalized addresses), `host` (hostn
75
75
  ```javascript
76
76
  import {
77
77
  orient2d, haversineDistance, ringWinding, bboxOf,
78
- geohashEncode, isValidWkt, createBboxIndex,
78
+ geohashEncode, wktToGeoJson, geoJsonToWkt, createBboxIndex,
79
79
  } from '@jarenjs/core/geo';
80
80
 
81
81
  orient2d(0, 0, 1, 0, 0, 1); // > 0 — counter-clockwise, exactly
@@ -83,17 +83,20 @@ haversineDistance(4.9041, 52.3676, 2.3522, 48.8566); // 429_862 m (Amsterdam–P
83
83
  ringWinding([[0,0],[1,0],[1,1],[0,1],[0,0]]); // 1 — an RFC 7946 exterior ring
84
84
  bboxOf({ type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,52]]] }); // [4, 52, 5, 53]
85
85
  geohashEncode(4.9041, 52.3676, 5); // 'u173z' — a string, so a prefix test is proximity
86
- isValidWkt('POINT (4.9041 52.3676)'); // true — strict ISO 19125 grammar
86
+
87
+ wktToGeoJson('POINT (4.9041 52.3676)'); // { type: 'Point', coordinates: [4.9041, 52.3676] }
88
+ geoJsonToWkt({ type: 'Point', coordinates: [4.9041, 52.3676] }); // 'POINT (4.9041 52.3676)'
87
89
 
88
90
  const index = createBboxIndex(regions.map(bboxOf)); // packed-Hilbert, build once
89
91
  for (const i of index.search(...bboxOf(point))) confirm(regions[i]); // candidates, then the exact test
90
92
  ```
91
93
 
92
- Three design decisions carry the module:
94
+ Four design decisions carry the module:
93
95
 
94
96
  - **Orientation is computed exactly** (Shewchuk's adaptive predicates): a naive floating-point determinant returns the *wrong sign* on near-collinear input, which makes containment contradict itself. Every ring winding and point-in-polygon answer rests on this sign, and the deliberate cost is on the [benchmark page](https://jklarenbeek.github.io/jarenjs/#/benchmarks?suite=geo).
95
97
  - **Measurement is spherical, drawing is projected, and the two never mix.** A Euclidean norm on raw degrees is 64% wrong over 1 km at Dutch latitudes, so `haversineDistance`/`sphericalRingArea` work on the sphere (`equirectDistance` is the cheap screening form for rejecting candidates first), while `projectMercator`/`fitMercator` and `simplifyLine`/`simplifyRing` exist for renderers — never measure on a projected coordinate.
96
98
  - **Validity is a separate concern from traversal.** `eachPosition`, `bboxOf`, `centroidOf` and friends measure without judging; `isValidGeoJson` (structure plus the ring closure a JSON Schema provably cannot express), `isValidWkt` and `isValidGeohash` are the one-call judgments that back the `geoFormats` group in [`@jarenjs/formats`](../formats), next to the full GeoJSON meta-schema artifacts in [`@jarenjs/json`](../json).
99
+ - **WKT is one grammar walk with two entry points.** `isValidWkt` and `wktToGeoJson` run the *same* scan, parameterized by a sink that is absent for the predicate and present for the parser — so the `wkt` format tester (which runs per value in the validator and per keystroke in the form layer) allocates nothing, and the two cannot drift apart. A committed corpus asserts `isValidWkt(s) === (wktToGeoJson(s) !== null)` for all 181 entries, malformed half included. `geoJsonToWkt` writes the string back, and `wktToGeoJson(geoJsonToWkt(g))` returns `g`; the text direction is *not* claimed, because whitespace, the `M` measure and number spelling are normalized. Against [`wellknown`](https://www.npmjs.com/package/wellknown) the parse is 3.1× faster on a `POINT` and the yes-or-no answer 5.4× — and wellknown *validates less* while being slower, which is the honest framing; the table and every language difference are in [ARCHITECTURE](./ARCHITECTURE.md).
97
100
 
98
101
  The spatial query operators (`$distance`, `$within`, `$geohash`, spatial joins over the box index) live in the query engine in [`@jarenjs/json`](../json); the streaming map chart that draws a FeatureCollection with bounded memory lives in [`@jarenjs/charts`](../../components/charts). The full module reference is [docs/GEO.md](./docs/GEO.md).
99
102
 
@@ -1,3 +1,28 @@
1
+ /**
2
+ * True when a value is a thenable — the shape `await` and the sync-capable
3
+ * async helpers below key on. A `then` that is not a function is data.
4
+ * @param {any} value
5
+ * @returns {boolean}
6
+ */
7
+ export declare function isThenable(value: any): boolean;
8
+ /**
9
+ * Sync-capable-async composition: apply `next` to a result that may be
10
+ * a value or a promise, WITHOUT allocating a promise when it is already
11
+ * a value. This is how a seam whose implementations may answer either
12
+ * way (a database driver, an idempotency ledger) keeps its synchronous
13
+ * fast path exact while the asynchronous one composes naturally.
14
+ * @param {any} value - A value or a promise of one
15
+ * @param {(value: any) => any} next
16
+ * @returns {any} `next`'s result, promise-wrapped only if the input was
17
+ */
18
+ export declare function chain(value: any, next: (value: any) => any): any;
19
+ /**
20
+ * Lift a value-or-promise into a promise — the ONE allocation a public
21
+ * asynchronous surface pays per call over a sync-capable seam.
22
+ * @param {any} value
23
+ * @returns {Promise<any>}
24
+ */
25
+ export declare function toPromise(value: any): Promise<any>;
1
26
  /**
2
27
  * trueThat
3
28
  * acts as a dummy validator that always returns true
@@ -2,6 +2,14 @@
2
2
  * The bounding box of a list of positions, as `[west, south, east, north]`.
3
3
  * Returns null for an empty list — there is no box that bounds nothing,
4
4
  * and an all-Infinity placeholder would silently intersect everything.
5
+ *
6
+ * Null too when any position is non-finite. Narrowing would drop it —
7
+ * every comparison against `NaN` is false — and answer with a finite,
8
+ * plausible box that does not contain the input it was asked to bound.
9
+ * A pre-filter built on such a box loses matching rows silently, which
10
+ * is the one failure a two-stage spatial plan cannot survive, so a
11
+ * position that cannot be bounded refuses the box rather than leaving
12
+ * it too small.
5
13
  * @param {Array<number[]>} positions
6
14
  * @returns {number[] | null}
7
15
  */
@@ -30,3 +38,23 @@ export declare function bboxContains(box: number[], x: number, y: number): boole
30
38
  * @returns {number[]}
31
39
  */
32
40
  export declare function bboxUnion(a: number[], b: number[]): number[];
41
+ /**
42
+ * A box as the `Polygon` that covers it — the rectangle written
43
+ * counter-clockwise from its south-west corner, which is RFC 7946's
44
+ * exterior winding, and closed.
45
+ *
46
+ * A box is four numbers and a polygon is a value the rest of the
47
+ * language can measure, contain and intersect, so this is the crossing
48
+ * between the two: a geohash cell, an index probe box or a `bbox`
49
+ * member becomes something `$within` and `$area` can take.
50
+ *
51
+ * @param {number[] | null} box - [west, south, east, north]
52
+ * @returns {{ type: string, coordinates: number[][][] } | null} null for no box
53
+ * @example
54
+ * bboxPolygon([4, 52, 5, 53]);
55
+ * // { type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,53],[4,52]]] }
56
+ */
57
+ export declare function bboxPolygon(box: number[] | null): {
58
+ type: string;
59
+ coordinates: number[][][];
60
+ } | null;
@@ -57,6 +57,38 @@ export declare function initialBearing(lon1: number, lat1: number, lon2: number,
57
57
  * @returns {[number, number]} the destination as a GeoJSON position
58
58
  */
59
59
  export declare function destinationPoint(lon: number, lat: number, bearing: number, distance: number, radius?: number): [number, number];
60
+ /**
61
+ * The bounding box of a geodesic circle: every position within
62
+ * `distance` metres of `(lon, lat)` on the same sphere lies inside it.
63
+ * This is what turns "nearer than r" into a box a range index can seek.
64
+ *
65
+ * The latitude bounds are the circle's due-north and due-south points,
66
+ * but the longitude bounds are **not** its due-east and due-west ones:
67
+ * the circle reaches its extreme meridians where it runs tangent to
68
+ * them, further out than a 90° bearing travels — 0.4 % further at 80°N
69
+ * over 100 km. A box built from four bearings is therefore too small,
70
+ * and too small is the one error a conservative pre-filter cannot
71
+ * survive. The extreme half-width is `asin(sin δ / cos φ)` for angular
72
+ * radius δ at latitude φ.
73
+ *
74
+ * `null` when the circle reaches a pole (`|lat| + δ >= 90°`), where the
75
+ * longitude bound does not exist because the circle spans every
76
+ * meridian, and for any non-finite or negative input.
77
+ *
78
+ * West and east are NOT wrapped into `[-180, 180]`: a circle spanning
79
+ * the antimeridian answers a west below -180 or an east above 180, so
80
+ * a caller can tell that case apart — RFC 7946 asks producers to cut
81
+ * there, and a wrapped box would silently claim the short way round.
82
+ *
83
+ * @param {number} lon - centre longitude, degrees
84
+ * @param {number} lat - centre latitude, degrees
85
+ * @param {number} distance - radius in metres (or in `radius`'s unit)
86
+ * @param {number} [radius] - sphere radius
87
+ * @returns {[number, number, number, number] | null} `[west, south, east, north]`
88
+ * @example
89
+ * circleBounds(5, 52, 1000); // [4.98539…, 51.99101…, 5.01461…, 52.00899…]
90
+ */
91
+ export declare function circleBounds(lon: number, lat: number, distance: number, radius?: number): [number, number, number, number] | null;
60
92
  /**
61
93
  * Total great-circle length of a line of positions, in metres. An empty
62
94
  * or single-position line has length 0.
@@ -27,7 +27,10 @@ export declare function geometryOf(value: any): object | null;
27
27
  export declare function eachPosition(value: any, visit: (position: number[]) => void): void;
28
28
  /**
29
29
  * The bounding box of any GeoJSON value, as `[west, south, east, north]`.
30
- * Null when the value contains no positions.
30
+ * Null when the value contains no positions, and null when any position
31
+ * it walks is non-finite — the rule `bboxOfPositions` states, for the
32
+ * same reason: a box that does not bound its input is worse than no
33
+ * box.
31
34
  *
32
35
  * A `bbox` member already present on the value is ignored: it is an
33
36
  * optimization the producer may have got wrong, and recomputing is the
@@ -29,3 +29,31 @@ export declare function simplifyLine(line: Array<number[]>, tolerance: number):
29
29
  * @returns {Array<number[]>}
30
30
  */
31
31
  export declare function simplifyRing(ring: Array<number[]>, tolerance: number): Array<number[]>;
32
+ /**
33
+ * A whole GeoJSON value with its vertices dropped — the same value,
34
+ * reduced. Lines go through {@link simplifyLine} and rings through
35
+ * {@link simplifyRing}, so **a ring stays closed and a line keeps both
36
+ * endpoints**; points and multipoints have no run to collapse and come
37
+ * back untouched.
38
+ *
39
+ * Accepts what the traversal layer accepts: a geometry, a Feature, a
40
+ * FeatureCollection or a GeometryCollection. Foreign members, ids and
41
+ * properties ride along — the value that comes back is the value that
42
+ * went in, with fewer positions, so it can be stored or sent as-is.
43
+ * Anything this does not recognize is returned unchanged rather than
44
+ * dropped.
45
+ *
46
+ * The tolerance is in the coordinate's own units, which for GeoJSON is
47
+ * **degrees** — a planar vertex-dropping threshold, never a distance. A
48
+ * degree of longitude is not a fixed length, so calling it metres would
49
+ * be the planar-for-geodesic confusion the rest of this module exists to
50
+ * prevent.
51
+ *
52
+ * @param {any} value
53
+ * @param {number} tolerance - in degrees
54
+ * @returns {any} a new value; the input is not modified
55
+ * @example
56
+ * simplifyGeometry({ type: 'LineString', coordinates: [[0,0],[1,0.001],[2,0]] }, 0.01);
57
+ * // { type: 'LineString', coordinates: [[0,0],[2,0]] }
58
+ */
59
+ export declare function simplifyGeometry(value: any, tolerance: number): any;
@@ -4,6 +4,9 @@
4
4
  * coordinate count consistent with the `Z`/`M`/`ZM` modifier and with
5
5
  * itself, closed polygon rings, and nothing before or after.
6
6
  *
7
+ * This is the non-allocating half of the walk {@link wktToGeoJson}
8
+ * builds with, so the two accept exactly the same language.
9
+ *
7
10
  * @param {string} text
8
11
  * @returns {boolean}
9
12
  * @example
@@ -12,5 +15,67 @@
12
15
  * isValidWkt('POLYGON ((0 0, 4 0, 4 4, 1 1))'); // false (open ring)
13
16
  * isValidWkt('POINT Z (1 2)'); // false (Z wants 3)
14
17
  * isValidWkt('LINESTRING (0 0, 1 1 1)'); // false (mixed dimension)
18
+ * isValidWkt('CIRCLE EMPTY'); // false (not one of the seven)
15
19
  */
16
20
  export declare function isValidWkt(text: string): boolean;
21
+ /**
22
+ * The GeoJSON geometry a WKT string names, or `null` when the text is
23
+ * not well-formed WKT — the same judgment {@link isValidWkt} makes,
24
+ * from the same walk. `null` rather than a throw is the kernel's
25
+ * posture for "no answer" (`bboxOf`, `centroidOf`, `geohashBounds` all
26
+ * answer it), so no call site has to wrap this in a try.
27
+ *
28
+ * Three rules decide what a lenient parser would silently lose:
29
+ *
30
+ * - `EMPTY` becomes an empty coordinate array (`POINT EMPTY` →
31
+ * `{ type: 'Point', coordinates: [] }`), never `null`: unparseable
32
+ * and validly empty are different answers.
33
+ * - The `M` measure is dropped and `Z` is kept. RFC 7946 §3.1.1 defines
34
+ * a position's third element as altitude, and a measure is not one —
35
+ * writing it there would be a lie. `POINT ZM (1 2 3 4)` → `[1, 2, 3]`,
36
+ * `POINT M (1 2 3)` → `[1, 2]`.
37
+ * - Coordinate ranges are not judged. `POINT (999 999)` parses;
38
+ * `isValidGeoJson` is the value gate.
39
+ *
40
+ * @param {string} text
41
+ * @returns {object | null}
42
+ * @example
43
+ * wktToGeoJson('POINT (4.9041 52.3676)');
44
+ * // { type: 'Point', coordinates: [4.9041, 52.3676] }
45
+ * wktToGeoJson('POLYGON ((0 0, 4 0, 4 4, 0 0))');
46
+ * // { type: 'Polygon', coordinates: [[[0,0],[4,0],[4,4],[0,0]]] }
47
+ * wktToGeoJson('POINT EMPTY'); // { type: 'Point', coordinates: [] }
48
+ * wktToGeoJson('POLYGON ((0 0))'); // null
49
+ */
50
+ export declare function wktToGeoJson(text: string): object | null;
51
+ /**
52
+ * A GeoJSON value as a WKT string, or `null` for a value with no
53
+ * geometry to write. Accepts what the traversal layer accepts: a bare
54
+ * position, a geometry, a Feature or a FeatureCollection.
55
+ *
56
+ * `options.dim` is 2 (the default) or 3. At 3, a geometry whose every
57
+ * position carries a third element is written with a `Z` modifier;
58
+ * anything else is written 2D with the third elements dropped, because
59
+ * one WKT geometry carries one modifier for all of its coordinates. The
60
+ * decision is made per tagged geometry, so a GeometryCollection may mix
61
+ * 2D and 3D members — each writes its own modifier. Invalid WKT is
62
+ * never emitted.
63
+ *
64
+ * A non-finite coordinate yields `null`: a value that cannot be written
65
+ * is not written approximately.
66
+ *
67
+ * @param {any} value
68
+ * @param {{ dim?: number }} [options]
69
+ * @returns {string | null}
70
+ * @example
71
+ * geoJsonToWkt({ type: 'Point', coordinates: [4.9041, 52.3676] });
72
+ * // 'POINT (4.9041 52.3676)'
73
+ * geoJsonToWkt([4.9041, 52.3676]); // 'POINT (4.9041 52.3676)'
74
+ * geoJsonToWkt({ type: 'Point', coordinates: [] }); // 'POINT EMPTY'
75
+ * geoJsonToWkt({ type: 'Point', coordinates: [1, 2, 3] }, { dim: 3 });
76
+ * // 'POINT Z (1 2 3)'
77
+ * geoJsonToWkt({ type: 'Point', coordinates: [NaN, 2] }); // null
78
+ */
79
+ export declare function geoJsonToWkt(value: any, options?: {
80
+ dim?: number;
81
+ }): string | null;
@@ -136,6 +136,24 @@ export declare function semanticKey(value: any): string;
136
136
  * @returns {boolean}
137
137
  */
138
138
  export declare function isJsonObject(value: any): boolean;
139
+ /**
140
+ * True when a value is representable as JSON as it stands: `null`, a
141
+ * boolean, a finite number, a string, or an array/plain object (own
142
+ * enumerable members, prototype `Object.prototype` or `null`) whose
143
+ * members all are. Everything else — `undefined`, `bigint`, `symbol`,
144
+ * functions, `NaN`/`±Infinity`, class instances (`Error`, `Date`, `Map`,
145
+ * `Response`, DOM nodes ...) and any cycle — is not.
146
+ *
147
+ * This is the boundary predicate for "JSON only crosses": a host that
148
+ * hands a value into application state asks it once, before the value
149
+ * can carry an object the state validator would have to reject. It is
150
+ * TOTAL — a hostile accessor that throws makes the value not-JSON rather
151
+ * than propagating.
152
+ *
153
+ * @param {any} value
154
+ * @returns {boolean}
155
+ */
156
+ export declare function isJsonValue(value: any): boolean;
139
157
  /**
140
158
  * Assign a member so that a key named `__proto__` becomes an own data
141
159
  * property instead of reassigning the object's prototype. Every builder
@@ -21,6 +21,7 @@ export declare const CC_0 = 48;
21
21
  export declare const CC_1 = 49;
22
22
  export declare const CC_9 = 57;
23
23
  export declare const CC_COLON = 58;
24
+ export declare const CC_SEMICOLON = 59;
24
25
  export declare const CC_LT = 60;
25
26
  export declare const CC_EQ = 61;
26
27
  export declare const CC_GT = 62;
@@ -94,6 +94,28 @@ export declare function fromCodePoints(codePoints: number[]): string;
94
94
  * @returns {number} -1 when a < b, 0 when equal, 1 when a > b
95
95
  */
96
96
  export declare function compareCodePoints(a: string, b: string): number;
97
+ /**
98
+ * The exclusive upper bound of the strings beginning with `prefix`: the
99
+ * smallest string that sorts above every one of them. For a
100
+ * well-formed `value`, `value` begins with `prefix` **iff**
101
+ * `prefix <= value < codePointPrefixSuccessor(prefix)` under
102
+ * {@link compareCodePoints} — which is what lets a caller spell a
103
+ * prefix test as a half-open range, the form an ordered index can seek
104
+ * rather than test row by row.
105
+ *
106
+ * Null when there is no such string, and the caller then has no range
107
+ * to offer: the empty prefix (every string begins with it) and a
108
+ * prefix of nothing but U+10FFFF (nothing sorts above it).
109
+ *
110
+ * The bound never lands in the surrogate range, because it has to be a
111
+ * string the caller can hand on — to a comparator, a database
112
+ * parameter, a serializer. No well-formed string sorts inside that
113
+ * range, so stepping over it leaves the equivalence above intact.
114
+ *
115
+ * @param {string} prefix - The prefix to bound
116
+ * @returns {string | null} The exclusive upper bound, or null when none exists
117
+ */
118
+ export declare function codePointPrefixSuccessor(prefix: string): string | null;
97
119
  /** FNV-1a 32-bit offset basis — the seed a fresh hash starts from. */
98
120
  export declare const FNV1A_OFFSET_BASIS = 2166136261;
99
121
  /**
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @file An incremental Server-Sent-Events codec (WHATWG HTML §9.2).
3
+ * `createSseEventDecoder` yields complete events `{ event, id, data,
4
+ * retry }` from network chunks fed in any split — mid-line, mid-event,
5
+ * CR, LF or CRLF line endings; `createSseDecoder` is the data-only view
6
+ * of the same machine (the shape every OpenAI-compatible streaming
7
+ * endpoint needs: just the `data:` payloads, in order); and
8
+ * `encodeSseEvent` renders one event back to wire text.
9
+ *
10
+ * Dispatch follows the specification: field lines are `name: value`
11
+ * with one optional leading space in the value, `:` lines are comments,
12
+ * an event is dispatched on the blank line exactly when its data buffer
13
+ * is non-empty (multi-line data joins with `\n`), the last-event-id is
14
+ * a stream attribute that persists across events (an `id` field whose
15
+ * value carries U+0000 is ignored), and `retry` must be ASCII digits.
16
+ * `end()` flushes a final event from a stream that never sent its
17
+ * closing blank line.
18
+ */
19
+ export type SseEvent = {
20
+ event: string | null;
21
+ id: string | null;
22
+ data: string;
23
+ retry: number | null;
24
+ };
25
+ /**
26
+ * One decoded event. `event` is the `event:` field of the block, `null`
27
+ * when the block had none (the specification's default type "message");
28
+ * `id` is the stream's last-event-id at dispatch, `null` while the
29
+ * stream has not set one; `data` is the joined data payload; `retry` is
30
+ * the reconnection time a `retry:` field set since the previous
31
+ * dispatch, `null` otherwise.
32
+ * @typedef {Object} SseEvent
33
+ * @property {string | null} event
34
+ * @property {string | null} id
35
+ * @property {string} data
36
+ * @property {number | null} retry
37
+ */
38
+ /**
39
+ * An incremental decoder yielding complete events.
40
+ * @returns {{ feed: (chunk: string) => SseEvent[], end: () => SseEvent[] }}
41
+ * `feed` returns the events completed by this chunk; `end` flushes a
42
+ * final event from a stream that never sent its closing blank line.
43
+ */
44
+ export declare function createSseEventDecoder(): {
45
+ feed: (chunk: string) => SseEvent[];
46
+ end: () => SseEvent[];
47
+ };
48
+ /**
49
+ * The data-only view of the event decoder: `feed` yields the complete
50
+ * `data:` payloads in order and every other field is ignored — the
51
+ * shape a chat-completion stream consumer needs.
52
+ * @returns {{ feed: (chunk: string) => string[], end: () => string[] }}
53
+ */
54
+ export declare function createSseDecoder(): {
55
+ feed: (chunk: string) => string[];
56
+ end: () => string[];
57
+ };
58
+ /**
59
+ * Render one event as wire text: the optional `event:`, `id:` and
60
+ * `retry:` lines, the data split on `\n` into one `data:` line each,
61
+ * and the dispatching blank line. Throws `TypeError` for text the frame
62
+ * cannot carry: a line terminator inside `event` or `id`, U+0000 inside
63
+ * `id`, a bare carriage return inside `data` (a decoder would read it
64
+ * as a line break and corrupt the framing), or a `retry` that is not a
65
+ * non-negative integer.
66
+ * @param {{ event?: string | null, id?: string | null, data: string, retry?: number | null }} fields
67
+ * @returns {string}
68
+ */
69
+ export declare function encodeSseEvent(fields: {
70
+ event?: string | null;
71
+ id?: string | null;
72
+ data: string;
73
+ retry?: number | null;
74
+ }): string;
package/docs/GEO.md CHANGED
@@ -35,6 +35,20 @@ rejecting candidates before the real test. Also `initialBearing`,
35
35
  `destinationPoint`, `lineLength(positions)`. The ellipsoid is
36
36
  deliberately not modelled.
37
37
 
38
+ `circleBounds(lon, lat, metres)` answers the `[west, south, east,
39
+ north]` box a "nearer than r" test can be seeked with — the shape a
40
+ range index needs before the exact distance runs. Its longitude bounds
41
+ are **not** the circle's due-east and due-west points: the circle
42
+ reaches its extreme meridians where it runs tangent to them, which is
43
+ `asin(sin δ / cos φ)` from the centre and 0.4 % further than a 90°
44
+ bearing travels at 80°N over 100 km. A box built from four bearings is
45
+ too small, and a pre-filter that is too small drops matching rows
46
+ silently. It answers `null` when the circle reaches a pole (there is no
47
+ longitude bound to give), and it does not wrap west/east into
48
+ `[-180, 180]` — a circle spanning the antimeridian answers a west below
49
+ -180, which is how a caller detects the case RFC 7946 asks producers to
50
+ cut.
51
+
38
52
  ## Rings — `ring.js`
39
53
 
40
54
  `isRingClosed`, `ringSignedArea` (shoelace over `orient2d` terms —
@@ -56,6 +70,15 @@ never-matching, keeping positions aligned with the caller's array.
56
70
  Build and probe are level with Flatbush (`npm run benchmark:geo`; the
57
71
  table lives in [ARCHITECTURE](../ARCHITECTURE.md)).
58
72
 
73
+ **A box is refused rather than made too small.** `bboxOfPositions` and
74
+ `bboxOf` answer `null` for an empty input *and* whenever any position
75
+ they are asked to bound has a non-finite coordinate — they never skip
76
+ the position and bound the rest. A narrowing loop would drop it
77
+ silently (every comparison against `NaN` is false) and return a finite,
78
+ plausible box that does not contain its input, and a candidate filter
79
+ built on such a box loses matching entries with nothing to show for it.
80
+ `null` is the answer every caller already handles.
81
+
59
82
  ## GeoJSON traversal — `geojson.js`
60
83
 
61
84
  The one layer that reads the `type` discriminator; every function takes
@@ -66,7 +89,7 @@ mass), `containsPosition`, `geoDistance` (between representative
66
89
  positions), `ringsClosed`. Nothing here validates — malformed input
67
90
  yields null or 0 — because judgment lives one module over.
68
91
 
69
- ## Validity — `valid.js`, `wkt.js`, `geohash.js`
92
+ ## Validity — `valid.js`, `geohash.js`
70
93
 
71
94
  The one-call judgments backing the `geoFormats` group in
72
95
  `@jarenjs/formats`:
@@ -76,11 +99,61 @@ The one-call judgments backing the `geoFormats` group in
76
99
  invariant a JSON Schema provably cannot express. The shallow twin of
77
100
  the meta-schema artifacts in `@jarenjs/json`, which locate failures
78
101
  and (via `$query`) also check winding.
79
- - `isValidWkt(text)` — strict ISO 19125 grammar: seven tags, `Z`/`M`/
80
- `ZM` modifiers (unmodified accepts 2 or 3 coordinates, as PostGIS
81
- does), consistent counts, closed rings, `EMPTY`, no surrounding text.
82
102
  - `isValidGeohash(hash)` — non-empty, lowercase base-32 alphabet.
83
103
 
104
+ ## Well-Known Text — `wkt.js`
105
+
106
+ The round trip with the text encoding every spatial database emits.
107
+ `wktToGeoJson(text)` returns a geometry or `null`; `geoJsonToWkt(value,
108
+ options?)` returns a string or `null`; `isValidWkt(text)` answers
109
+ yes-or-no. **All three are one grammar walk**: each scan function takes
110
+ a sink that is absent for the predicate and present for the parser, so
111
+ the format tester allocates nothing and the two entry points cannot
112
+ drift. A committed corpus asserts `isValidWkt(s) === (wktToGeoJson(s)
113
+ !== null)` for every entry, malformed half included — a divergence is a
114
+ failing test.
115
+
116
+ The grammar is strict ISO 19125: the seven tags and no others,
117
+ `Z`/`M`/`ZM` modifiers (unmodified accepts 2 or 3 coordinates, as
118
+ PostGIS does), coordinate counts consistent with the modifier and with
119
+ the geometry's first point, rings of four or more positions that close,
120
+ `EMPTY`, and no text before or after.
121
+
122
+ Four rules decide what a lenient converter loses:
123
+
124
+ - **`EMPTY` is an empty coordinate array**, not `null`: `POINT EMPTY` →
125
+ `{ type: 'Point', coordinates: [] }`. Unparseable and validly empty
126
+ are different answers.
127
+ - **`Z` is kept and `M` is dropped.** RFC 7946 §3.1.1 defines a
128
+ position's third element as altitude; a WKT measure is not one, so
129
+ writing it there would be a lie. `POINT ZM (1 2 3 4)` → `[1, 2, 3]`,
130
+ `POINT M (1 2 3)` → `[1, 2]`. The measure is discarded, and it is
131
+ discarded on purpose.
132
+ - **The parser does not judge ranges.** `POINT (999 999)` parses;
133
+ `isValidGeoJson` is the value gate, and duplicating it here would make
134
+ the two entry points disagree.
135
+ - **A value that cannot be written is not written approximately.**
136
+ `geoJsonToWkt` answers `null` for a non-finite coordinate, and writes
137
+ numbers with the shortest round-tripping spelling (`String(n)`, what
138
+ `JSON.stringify` uses) — never a fixed precision, which would silently
139
+ move the point.
140
+
141
+ `geoJsonToWkt` accepts what the traversal layer accepts: a Feature
142
+ writes its geometry, a FeatureCollection a `GEOMETRYCOLLECTION` of its
143
+ features' geometries, a bare position a `POINT`. `options.dim` is 2 (the
144
+ default) or 3; at 3 a geometry whose every position carries a third
145
+ element gets a `Z` modifier, and anything else is written 2D with the
146
+ third elements dropped, because one WKT geometry carries one modifier
147
+ for all of its coordinates. The decision is made per tagged geometry, so
148
+ a `GEOMETRYCOLLECTION` may mix 2D and 3D members — invalid WKT is never
149
+ emitted.
150
+
151
+ **One direction round-trips and the other does not.**
152
+ `wktToGeoJson(geoJsonToWkt(g))` returns `g`, and that is asserted over
153
+ the whole corpus. `geoJsonToWkt(wktToGeoJson(s)) === s` is false in
154
+ general and is not claimed: whitespace, the `M` measure and number
155
+ spelling are all normalized.
156
+
84
157
  ## Geohash — `geohash.js`
85
158
 
86
159
  `geohashEncode(lon, lat, precision)`, `geohashDecode`, `geohashBounds`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/core",
3
3
  "private": false,
4
- "version": "0.34.2",
4
+ "version": "0.43.1",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
package/src/function.js CHANGED
@@ -5,6 +5,40 @@ import {
5
5
 
6
6
  // export * from './function-tools-xtra';
7
7
 
8
+ /**
9
+ * True when a value is a thenable — the shape `await` and the sync-capable
10
+ * async helpers below key on. A `then` that is not a function is data.
11
+ * @param {any} value
12
+ * @returns {boolean}
13
+ */
14
+ export function isThenable(value) {
15
+ return value !== null && typeof value === 'object' && typeof value.then === 'function';
16
+ }
17
+
18
+ /**
19
+ * Sync-capable-async composition: apply `next` to a result that may be
20
+ * a value or a promise, WITHOUT allocating a promise when it is already
21
+ * a value. This is how a seam whose implementations may answer either
22
+ * way (a database driver, an idempotency ledger) keeps its synchronous
23
+ * fast path exact while the asynchronous one composes naturally.
24
+ * @param {any} value - A value or a promise of one
25
+ * @param {(value: any) => any} next
26
+ * @returns {any} `next`'s result, promise-wrapped only if the input was
27
+ */
28
+ export function chain(value, next) {
29
+ return isThenable(value) ? value.then(next) : next(value);
30
+ }
31
+
32
+ /**
33
+ * Lift a value-or-promise into a promise — the ONE allocation a public
34
+ * asynchronous surface pays per call over a sync-capable seam.
35
+ * @param {any} value
36
+ * @returns {Promise<any>}
37
+ */
38
+ export function toPromise(value) {
39
+ return isThenable(value) ? value : Promise.resolve(value);
40
+ }
41
+
8
42
  /**
9
43
  * trueThat
10
44
  * acts as a dummy validator that always returns true