@osmix/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +54 -7
  3. package/dist/contracts.d.ts +2 -1
  4. package/dist/contracts.d.ts.map +1 -1
  5. package/dist/entities.d.ts +21 -1
  6. package/dist/entities.d.ts.map +1 -1
  7. package/dist/entities.js +52 -3
  8. package/dist/entities.js.map +1 -1
  9. package/dist/ids.d.ts +18 -5
  10. package/dist/ids.d.ts.map +1 -1
  11. package/dist/ids.js +68 -18
  12. package/dist/ids.js.map +1 -1
  13. package/dist/index.d.ts +6 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/indirect-kd-index.d.ts +28 -0
  18. package/dist/indirect-kd-index.d.ts.map +1 -0
  19. package/dist/indirect-kd-index.js +163 -0
  20. package/dist/indirect-kd-index.js.map +1 -0
  21. package/dist/limits.d.ts +30 -0
  22. package/dist/limits.d.ts.map +1 -0
  23. package/dist/limits.js +51 -0
  24. package/dist/limits.js.map +1 -0
  25. package/dist/nodes.d.ts +28 -13
  26. package/dist/nodes.d.ts.map +1 -1
  27. package/dist/nodes.js +181 -51
  28. package/dist/nodes.js.map +1 -1
  29. package/dist/osm.d.ts +59 -3
  30. package/dist/osm.d.ts.map +1 -1
  31. package/dist/osm.js +31 -2
  32. package/dist/osm.js.map +1 -1
  33. package/dist/relations.d.ts +10 -4
  34. package/dist/relations.d.ts.map +1 -1
  35. package/dist/relations.js +66 -28
  36. package/dist/relations.js.map +1 -1
  37. package/dist/renumber.d.ts +23 -0
  38. package/dist/renumber.d.ts.map +1 -0
  39. package/dist/renumber.js +64 -0
  40. package/dist/renumber.js.map +1 -0
  41. package/dist/stringtable.d.ts +2 -1
  42. package/dist/stringtable.d.ts.map +1 -1
  43. package/dist/stringtable.js +5 -1
  44. package/dist/stringtable.js.map +1 -1
  45. package/dist/tags.d.ts +44 -11
  46. package/dist/tags.d.ts.map +1 -1
  47. package/dist/tags.js +153 -27
  48. package/dist/tags.js.map +1 -1
  49. package/dist/typed-arrays.d.ts +63 -48
  50. package/dist/typed-arrays.d.ts.map +1 -1
  51. package/dist/typed-arrays.js +184 -107
  52. package/dist/typed-arrays.js.map +1 -1
  53. package/dist/ways.d.ts +50 -2
  54. package/dist/ways.d.ts.map +1 -1
  55. package/dist/ways.js +212 -35
  56. package/dist/ways.js.map +1 -1
  57. package/package.json +11 -12
  58. package/src/contracts.ts +2 -1
  59. package/src/entities.ts +67 -4
  60. package/src/ids.ts +72 -22
  61. package/src/index.ts +13 -7
  62. package/src/indirect-kd-index.ts +212 -0
  63. package/src/limits.ts +73 -0
  64. package/src/nodes.ts +228 -54
  65. package/src/osm.ts +90 -4
  66. package/src/relations.ts +80 -30
  67. package/src/renumber.ts +80 -0
  68. package/src/stringtable.ts +15 -1
  69. package/src/tags.ts +173 -31
  70. package/src/typed-arrays.ts +225 -113
  71. package/src/ways.ts +233 -38
  72. package/test/entity-index-errors.test.ts +41 -0
  73. package/test/ids.bench.ts +11 -7
  74. package/test/ids.test.ts +38 -0
  75. package/test/limits.test.ts +83 -0
  76. package/test/no-shared-array-buffer.test.ts +41 -0
  77. package/test/nodes-spatial-index.test.ts +261 -0
  78. package/test/osm-bbox.test.ts +30 -0
  79. package/test/relations.bench.ts +9 -4
  80. package/test/relations.test.ts +52 -29
  81. package/test/renumber.test.ts +54 -0
  82. package/test/spatial-index-rebuild.test.ts +33 -0
  83. package/test/storage-compaction.test.ts +269 -0
  84. package/test/tags-search.test.ts +13 -0
  85. package/test/typed-arrays.test.ts +178 -59
  86. package/src/types/geokdbush.d.ts +0 -32
  87. package/src/types/kdbush.d.ts +0 -47
@@ -0,0 +1,269 @@
1
+ import { inspectBackingBuffers } from "@osmix/shared/backing-buffers";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { OSM_CONTENT_HASH_VERSION, OSM_TRANSFER_VERSION, Osm } from "../src/osm.ts";
5
+ import StringTable from "../src/stringtable.ts";
6
+ import { Tags } from "../src/tags.ts";
7
+
8
+ describe("sparse tag storage", () => {
9
+ it("rejects duplicate and out-of-order tagged entity indexes", () => {
10
+ const tags = new Tags(new StringTable());
11
+ tags.addTagKeysAndValues(2, [0], [1]);
12
+
13
+ expect(() => tags.addTagKeysAndValues(2, [0], [1])).toThrow(/added out of order/);
14
+ expect(() => tags.addTagKeysAndValues(1, [0], [1])).toThrow(/added out of order/);
15
+ });
16
+
17
+ it("preserves tags across rank boundaries and supports more than 255 tags", () => {
18
+ const osm = new Osm({ id: "sparse-tags" });
19
+ const manyTags = Object.fromEntries(
20
+ Array.from({ length: 300 }, (_, index) => [`key-${index}`, `value-${index}`]),
21
+ );
22
+ const taggedIndexes = new Set([0, 255, 256, 511, 512]);
23
+
24
+ for (let index = 0; index <= 512; index++) {
25
+ osm.nodes.addNode({
26
+ id: index + 1,
27
+ lon: index / 10,
28
+ lat: index / 20,
29
+ tags: index === 0 ? manyTags : taggedIndexes.has(index) ? { boundary: `${index}` } : {},
30
+ });
31
+ }
32
+ osm.buildIndexes();
33
+
34
+ expect(osm.nodes.tags.cardinality(0)).toBe(300);
35
+ expect(osm.nodes.tags.cardinality(1)).toBe(0);
36
+ expect(osm.nodes.tags.cardinality(255)).toBe(1);
37
+ expect(osm.nodes.tags.cardinality(256)).toBe(1);
38
+ expect(osm.nodes.tags.cardinality(511)).toBe(1);
39
+ expect(osm.nodes.tags.cardinality(512)).toBe(1);
40
+ expect([...osm.nodes.tags.taggedEntityIndexes()]).toEqual([0, 255, 256, 511, 512]);
41
+ expect(osm.nodes.tags.taggedEntityCount).toBe(5);
42
+ expect(Object.keys(osm.nodes.getByIndex(0).tags ?? {})).toHaveLength(300);
43
+
44
+ const transferables = osm.transferables();
45
+ expect(transferables.transferVersion).toBe(OSM_TRANSFER_VERSION);
46
+ expect(transferables.contentHashVersion).toBe(OSM_CONTENT_HASH_VERSION);
47
+ expect(transferables.nodes.taggedEntityBits.byteLength).toBe(
48
+ Math.ceil(513 / 32) * Uint32Array.BYTES_PER_ELEMENT,
49
+ );
50
+ expect(transferables.nodes.tagRankCheckpoints.byteLength).toBe(
51
+ (Math.ceil(513 / 256) + 1) * Uint32Array.BYTES_PER_ELEMENT,
52
+ );
53
+ expect(transferables.nodes.tagOffsets.byteLength).toBe(
54
+ (taggedIndexes.size + 1) * Uint32Array.BYTES_PER_ELEMENT,
55
+ );
56
+ expect(osm.info().spatialIndexes).toEqual({
57
+ nodes: { all: false, tagged: false },
58
+ ways: false,
59
+ relations: false,
60
+ });
61
+
62
+ const reconstructed = new Osm(transferables);
63
+ expect([...reconstructed.nodes.tags.taggedEntityIndexes()]).toEqual([0, 255, 256, 511, 512]);
64
+ expect(reconstructed.nodes.getByIndex(512).tags).toEqual({ boundary: "512" });
65
+ });
66
+
67
+ it("stores and searches an all-tagged collection across rank boundaries", () => {
68
+ const osm = new Osm({ id: "all-tagged" });
69
+ const entityCount = 257;
70
+
71
+ for (let index = 0; index < entityCount; index++) {
72
+ osm.nodes.addNode({
73
+ id: index + 1,
74
+ lon: index / 10,
75
+ lat: index / 20,
76
+ tags: { shared: index % 2 === 0 ? "even" : "odd", position: `${index}` },
77
+ });
78
+ }
79
+ osm.buildIndexes();
80
+
81
+ expect(osm.nodes.tags.taggedEntityCount).toBe(entityCount);
82
+ expect([...osm.nodes.tags.taggedEntityIndexes()]).toEqual(
83
+ Array.from({ length: entityCount }, (_, index) => index),
84
+ );
85
+ for (const index of [0, 31, 32, 255, 256]) {
86
+ expect(osm.nodes.tags.cardinality(index)).toBe(2);
87
+ expect(osm.nodes.getByIndex(index).tags).toEqual({
88
+ shared: index % 2 === 0 ? "even" : "odd",
89
+ position: `${index}`,
90
+ });
91
+ }
92
+ expect([...osm.nodes].map(({ id }) => id)).toEqual(
93
+ Array.from({ length: entityCount }, (_, index) => index + 1),
94
+ );
95
+ expect(osm.nodes.search("shared")).toHaveLength(entityCount);
96
+ expect(osm.nodes.search("shared", "even").map(({ id }) => id)).toEqual(
97
+ Array.from({ length: 129 }, (_, index) => index * 2 + 1),
98
+ );
99
+
100
+ const transferables = osm.transferables();
101
+ expect(new Uint32Array(transferables.nodes.taggedEntityBits)).toEqual(
102
+ new Uint32Array([
103
+ 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
104
+ 0xffffffff, 1,
105
+ ]),
106
+ );
107
+ expect(new Uint32Array(transferables.nodes.tagRankCheckpoints)).toEqual(
108
+ new Uint32Array([0, 256, 257]),
109
+ );
110
+ expect(new Uint32Array(transferables.nodes.tagOffsets)).toEqual(
111
+ new Uint32Array(Array.from({ length: entityCount + 1 }, (_, index) => index * 2)),
112
+ );
113
+ expect(Tags.getBytesRequired(entityCount, entityCount)).toBe(
114
+ 9 * Uint32Array.BYTES_PER_ELEMENT +
115
+ 3 * Uint32Array.BYTES_PER_ELEMENT +
116
+ (entityCount + 1) * Uint32Array.BYTES_PER_ELEMENT,
117
+ );
118
+
119
+ const reconstructed = new Osm(transferables);
120
+ expect(reconstructed.nodes.tags.taggedEntityCount).toBe(entityCount);
121
+ expect(reconstructed.nodes.getByIndex(256).tags).toEqual({
122
+ shared: "even",
123
+ position: "256",
124
+ });
125
+ expect(reconstructed.nodes.search("shared", "odd")).toHaveLength(128);
126
+ expect([...reconstructed.nodes.tags.taggedEntityIndexes()]).toHaveLength(entityCount);
127
+ });
128
+
129
+ it("stores and round-trips an all-untagged collection with only the offset sentinel", () => {
130
+ const osm = new Osm({ id: "all-untagged" });
131
+ const entityCount = 257;
132
+
133
+ for (let index = 0; index < entityCount; index++) {
134
+ osm.nodes.addNode({ id: index + 1, lon: index / 10, lat: index / 20 });
135
+ }
136
+ osm.buildIndexes();
137
+
138
+ expect(osm.nodes.tags.taggedEntityCount).toBe(0);
139
+ expect([...osm.nodes.tags.taggedEntityIndexes()]).toEqual([]);
140
+ for (const index of [0, 31, 32, 255, 256]) {
141
+ expect(osm.nodes.tags.cardinality(index)).toBe(0);
142
+ expect(osm.nodes.getByIndex(index).tags).toBeUndefined();
143
+ }
144
+ expect([...osm.nodes].every((node) => node.tags === undefined)).toBe(true);
145
+ expect(osm.nodes.search("missing-key")).toEqual([]);
146
+
147
+ const transferables = osm.transferables();
148
+ expect(new Uint32Array(transferables.nodes.taggedEntityBits)).toEqual(new Uint32Array(9));
149
+ expect(new Uint32Array(transferables.nodes.tagRankCheckpoints)).toEqual(
150
+ new Uint32Array([0, 0, 0]),
151
+ );
152
+ expect(new Uint32Array(transferables.nodes.tagOffsets)).toEqual(new Uint32Array([0]));
153
+ expect(transferables.nodes.tagKeys.byteLength).toBe(0);
154
+ expect(transferables.nodes.tagVals.byteLength).toBe(0);
155
+ expect(Tags.getBytesRequired(entityCount, 0)).toBe(
156
+ 9 * Uint32Array.BYTES_PER_ELEMENT +
157
+ 3 * Uint32Array.BYTES_PER_ELEMENT +
158
+ Uint32Array.BYTES_PER_ELEMENT,
159
+ );
160
+
161
+ const reconstructed = new Osm(transferables);
162
+ expect(reconstructed.nodes.tags.taggedEntityCount).toBe(0);
163
+ expect([...reconstructed.nodes.tags.taggedEntityIndexes()]).toEqual([]);
164
+ expect(reconstructed.nodes.getByIndex(256)).toEqual({
165
+ id: 257,
166
+ lon: 25.6,
167
+ lat: 12.8,
168
+ });
169
+ expect(reconstructed.nodes.search("missing-key")).toEqual([]);
170
+ });
171
+
172
+ it("preserves plain load diagnostics through info and transfer", () => {
173
+ const osm = new Osm();
174
+ osm.buildIndexes();
175
+ osm.setLoadDiagnostics({
176
+ requestedProfile: "auto",
177
+ selectedProfile: "view",
178
+ reasons: [{ code: "large", level: "warning", message: "View selected" }],
179
+ bytes: {
180
+ residentTypedBuffers: 100,
181
+ projectedTypedBufferPeak: 200,
182
+ largestPlannedAllocation: 50,
183
+ storageBytes: 75,
184
+ },
185
+ budgets: { workingSet: 150, singleAllocation: 60 },
186
+ phaseTimingsMs: { parse: 10 },
187
+ counters: { taggedNodes: 0, wayReferences: 0, relationMembers: 0 },
188
+ });
189
+
190
+ expect(osm.info().loadDiagnostics?.selectedProfile).toBe("view");
191
+ expect(new Osm(osm.transferables()).info().loadDiagnostics).toEqual(osm.info().loadDiagnostics);
192
+ });
193
+ });
194
+
195
+ describe("indexed way references", () => {
196
+ it("stores resolved node indexes and losslessly preserves sparse missing IDs", () => {
197
+ const osm = new Osm({ id: "way-refs" });
198
+ osm.nodes.addNode({ id: 10, lon: 1, lat: 2 });
199
+ osm.nodes.addNode({ id: 20, lon: 3, lat: 4 });
200
+ osm.ways.addWay({ id: 100, refs: [999, 10, 998] });
201
+ osm.ways.addWay({ id: 101, refs: [10, 997, 20] });
202
+ osm.buildIndexes();
203
+
204
+ expect(osm.ways.getRefIds(0)).toEqual([999, 10, 998]);
205
+ expect(osm.ways.getRefIds(1)).toEqual([10, 997, 20]);
206
+ expect(() => osm.ways.getCoordinates(0)).toThrow("Node 999 not found for way geometry");
207
+
208
+ const transferables = osm.transferables();
209
+ expect(transferables.ways.refs.byteLength).toBe(6 * Uint32Array.BYTES_PER_ELEMENT);
210
+ expect(new Uint32Array(transferables.ways.missingRefPositions)).toEqual(
211
+ new Uint32Array([0, 2, 4]),
212
+ );
213
+ expect(new Float64Array(transferables.ways.missingRefIds)).toEqual(
214
+ new Float64Array([999, 998, 997]),
215
+ );
216
+
217
+ const reconstructed = new Osm(transferables);
218
+ expect(reconstructed.ways.getRefIds(0)).toEqual([999, 10, 998]);
219
+ expect(reconstructed.ways.getRefIds(1)).toEqual([10, 997, 20]);
220
+ });
221
+
222
+ it("produces the same content hash for pending IDs and directly indexed refs", () => {
223
+ const deferred = new Osm({ id: "deferred" });
224
+ deferred.nodes.addNode({ id: 10, lon: 1, lat: 2 });
225
+ deferred.nodes.addNode({ id: 20, lon: 3, lat: 4 });
226
+ deferred.ways.addWay({ id: 100, refs: [10, 20] });
227
+ deferred.buildIndexes();
228
+
229
+ const direct = new Osm({ id: "direct" });
230
+ direct.nodes.addNode({ id: 10, lon: 1, lat: 2 });
231
+ direct.nodes.addNode({ id: 20, lon: 3, lat: 4 });
232
+ direct.nodes.buildIndex();
233
+ direct.ways.addWays(
234
+ [{ id: 100, keys: [], vals: [], refs: [10, 10] }],
235
+ new Uint32Array(),
236
+ undefined,
237
+ (id) => direct.nodes.ids.getIndexFromId(id),
238
+ );
239
+ direct.buildIndexes();
240
+
241
+ expect(direct.ways.getRefIds(0)).toEqual([10, 20]);
242
+ expect(direct.contentHash()).toBe(deferred.contentHash());
243
+ });
244
+ });
245
+
246
+ describe("empty spatial capabilities", () => {
247
+ it("marks requested empty way and relation indexes as built", () => {
248
+ const osm = new Osm({ id: "empty-spatial-indexes" });
249
+ osm.buildIndexes();
250
+
251
+ osm.ways.buildSpatialIndex();
252
+ osm.relations.buildSpatialIndex();
253
+
254
+ expect(osm.ways.hasSpatialIndex()).toBe(true);
255
+ expect(osm.relations.hasSpatialIndex()).toBe(true);
256
+ expect(osm.info().spatialIndexes).toMatchObject({
257
+ ways: true,
258
+ relations: true,
259
+ });
260
+
261
+ const transferables = osm.transferables();
262
+ const inspection = inspectBackingBuffers(transferables);
263
+ expect(inspection.arrayBuffers).toBe(0);
264
+
265
+ const restored = new Osm(transferables);
266
+ expect(restored.ways.hasSpatialIndex()).toBe(true);
267
+ expect(restored.relations.hasSpatialIndex()).toBe(true);
268
+ });
269
+ });
@@ -35,6 +35,19 @@ describe("Tags.search on Nodes", () => {
35
35
  const none = nodes.search("highway");
36
36
  expect(none).toEqual([]);
37
37
  });
38
+
39
+ it("returns no matches for a shared string-table key absent from the collection", () => {
40
+ const st = new StringTable();
41
+ const nodes = new Nodes(st);
42
+ nodes.addNode({ id: 1, lon: -1, lat: 1, tags: { curb: "yes" } });
43
+ nodes.buildIndex();
44
+
45
+ const ways = new Ways(st, nodes);
46
+ ways.addWay({ id: 10, refs: [1], tags: { highway: "residential" } });
47
+ ways.buildIndex();
48
+
49
+ expect(nodes.search("highway")).toEqual([]);
50
+ });
38
51
  });
39
52
 
40
53
  describe("Tags.search on Ways", () => {
@@ -1,6 +1,45 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { BufferConstructor, ResizeableTypedArray } from "../src/typed-arrays";
3
+ import {
4
+ BufferConstructor,
5
+ ResizeableTypedArray,
6
+ TypedBufferAllocationError,
7
+ } from "../src/typed-arrays";
8
+
9
+ const INITIAL_BYTES = 64 * 1024;
10
+
11
+ class FailingCreateArrayBuffer extends ArrayBuffer {
12
+ constructor() {
13
+ super(0);
14
+ throw new RangeError("create failed");
15
+ }
16
+ }
17
+
18
+ /** Allocates the first buffer, but can neither resize it nor allocate a larger one. */
19
+ class FailingGrowArrayBuffer extends ArrayBuffer {
20
+ constructor(byteLength: number, options?: { maxByteLength?: number }) {
21
+ if (byteLength > INITIAL_BYTES) throw new RangeError("grow failed");
22
+ super(byteLength, options);
23
+ }
24
+
25
+ override resize(): void {
26
+ throw new RangeError("resize failed");
27
+ }
28
+ }
29
+
30
+ class FailingCompactArrayBuffer extends ArrayBuffer {
31
+ override transferToFixedLength(): ArrayBuffer {
32
+ throw new RangeError("compact failed");
33
+ }
34
+ }
35
+
36
+ /** Refuses any `maxByteLength` reservation, like a runtime out of address space. */
37
+ class NoReserveArrayBuffer extends ArrayBuffer {
38
+ constructor(byteLength: number, options?: { maxByteLength?: number }) {
39
+ if (options?.maxByteLength !== undefined) throw new RangeError("no reservation");
40
+ super(byteLength);
41
+ }
42
+ }
4
43
 
5
44
  describe("typed array helpers", () => {
6
45
  it("selects the appropriate buffer constructor", () => {
@@ -12,24 +51,73 @@ describe("typed array helpers", () => {
12
51
  });
13
52
 
14
53
  describe("ResizeableTypedArray", () => {
15
- it("push stores values and returns the index", () => {
16
- const arr = new ResizeableTypedArray(Float64Array);
54
+ it("reports structured create allocation failures", () => {
55
+ expect(
56
+ () =>
57
+ new ResizeableTypedArray(
58
+ Float64Array,
59
+ FailingCreateArrayBuffer as unknown as ArrayBufferConstructor,
60
+ ),
61
+ ).toThrowError(
62
+ expect.objectContaining({
63
+ name: "TypedBufferAllocationError",
64
+ code: "TYPED_BUFFER_ALLOCATION_FAILED",
65
+ operation: "create",
66
+ typedArray: "Float64Array",
67
+ bufferType: "array-buffer",
68
+ elementCount: INITIAL_BYTES / 8,
69
+ bytesPerElement: 8,
70
+ requiredBytes: INITIAL_BYTES,
71
+ }),
72
+ );
73
+ });
17
74
 
18
- const first = arr.push(1.5);
19
- const second = arr.push(2.5);
75
+ it("reports structured grow allocation failures", () => {
76
+ const arr = new ResizeableTypedArray(
77
+ Uint8Array,
78
+ FailingGrowArrayBuffer as unknown as ArrayBufferConstructor,
79
+ );
80
+ arr.pushMany(new Uint8Array(INITIAL_BYTES));
81
+
82
+ expect(() => arr.push(1)).toThrowError(
83
+ expect.objectContaining({
84
+ operation: "grow",
85
+ typedArray: "Uint8Array",
86
+ elementCount: INITIAL_BYTES * 2,
87
+ requiredBytes: INITIAL_BYTES * 2,
88
+ }),
89
+ );
90
+ expect(arr.length).toBe(INITIAL_BYTES);
91
+ });
20
92
 
21
- expect(first).toBe(0);
22
- expect(second).toBe(1);
23
- expect(arr.length).toBe(2);
24
- expect(arr.at(0)).toBe(1.5);
25
- expect(arr.at(1)).toBe(2.5);
93
+ it("reports structured compact allocation failures", () => {
94
+ const arr = new ResizeableTypedArray(
95
+ Float64Array,
96
+ FailingCompactArrayBuffer as unknown as ArrayBufferConstructor,
97
+ );
98
+ arr.pushMany([1, 2, 3]);
99
+
100
+ try {
101
+ arr.compact();
102
+ expect.unreachable("Expected compact to fail");
103
+ } catch (error) {
104
+ expect(error).toBeInstanceOf(TypedBufferAllocationError);
105
+ expect(error).toMatchObject({
106
+ operation: "compact",
107
+ typedArray: "Float64Array",
108
+ elementCount: 3,
109
+ bytesPerElement: 8,
110
+ requiredBytes: 24,
111
+ });
112
+ expect((error as Error).cause).toBeInstanceOf(RangeError);
113
+ }
26
114
  });
27
115
 
28
- it("should push many values", () => {
116
+ it("push stores values and returns the index", () => {
29
117
  const arr = new ResizeableTypedArray(Float64Array);
30
118
 
31
- arr.pushMany([1.5, 2.5]);
32
-
119
+ expect(arr.push(1.5)).toBe(0);
120
+ expect(arr.push(2.5)).toBe(1);
33
121
  expect(arr.length).toBe(2);
34
122
  expect(arr.at(0)).toBe(1.5);
35
123
  expect(arr.at(1)).toBe(2.5);
@@ -37,98 +125,129 @@ describe("typed array helpers", () => {
37
125
 
38
126
  it("at supports negative indices and guards bounds", () => {
39
127
  const arr = new ResizeableTypedArray(Int16Array);
40
-
41
128
  arr.pushMany([10, 20, 30]);
42
129
 
43
- expect(arr.length).toBe(3);
44
- expect(Array.from(arr.slice(0, arr.length))).toEqual([10, 20, 30]);
45
130
  expect(arr.at(-1)).toBe(30);
46
131
  expect(arr.at(-3)).toBe(10);
47
132
  expect(() => arr.at(-4)).toThrow(/Index out of bounds/);
48
133
  expect(() => arr.at(3)).toThrow(/Index out of bounds/);
49
134
  });
50
135
 
51
- it("pushMany appends arrays while preserving order", () => {
136
+ it("pushMany appends arrays while preserving order, across growth", () => {
52
137
  const arr = new ResizeableTypedArray(Uint32Array);
53
-
54
138
  arr.pushMany([1, 2, 3]);
55
139
  arr.pushMany(Uint32Array.from([4, 5]));
56
-
57
- expect(arr.length).toBe(5);
58
- expect(Array.from(arr.slice(0, arr.length))).toEqual([1, 2, 3, 4, 5]);
140
+ expect(Array.from(arr.array)).toEqual([1, 2, 3, 4, 5]);
141
+
142
+ // One call that needs several doublings grows once, to at least the needed size.
143
+ const many = Uint32Array.from({ length: 100_000 }, (_, i) => i);
144
+ arr.pushMany(many);
145
+ expect(arr.length).toBe(100_005);
146
+ expect(arr.at(-1)).toBe(99_999);
147
+ expect(arr.capacity).toBeGreaterThanOrEqual(100_005);
59
148
  });
60
149
 
61
- it("slice returns a copy of the requested segment", () => {
150
+ it("exposes only stored items through array, iteration and slice", () => {
62
151
  const arr = new ResizeableTypedArray(Uint8Array);
63
-
64
152
  arr.pushMany([1, 2, 3, 4]);
65
153
 
66
- const segment = arr.slice(1, 3);
154
+ expect(arr.capacity).toBeGreaterThan(4);
155
+ expect(arr.array.length).toBe(4);
156
+ expect([...arr]).toEqual([1, 2, 3, 4]);
67
157
 
158
+ const segment = arr.slice(1, 3);
68
159
  expect(segment).toBeInstanceOf(Uint8Array);
69
160
  expect(Array.from(segment)).toEqual([2, 3]);
70
-
71
161
  segment[0] = 99;
72
162
  expect(arr.at(1)).toBe(2);
73
163
  });
74
164
 
165
+ it("set grows and extends the length", () => {
166
+ const arr = new ResizeableTypedArray(Float32Array);
167
+ arr.set(100_000, 5);
168
+ expect(arr.length).toBe(100_001);
169
+ expect(arr.at(100_000)).toBe(5);
170
+ expect(arr.at(0)).toBe(0);
171
+ });
172
+
75
173
  it("compact trims unused capacity", () => {
76
174
  const arr = new ResizeableTypedArray(Float64Array);
77
-
78
175
  arr.pushMany([5, 6, 7]);
79
176
 
80
- const previousCapacity = arr.array.length;
81
177
  const compacted = arr.compact();
82
- const expectedByteLength = arr.length * Float64Array.BYTES_PER_ELEMENT;
83
178
 
84
179
  expect(compacted).toBe(arr.array);
85
- expect(compacted.length).toBe(arr.length);
86
- expect(compacted.length).toBeLessThan(previousCapacity);
87
- expect(arr.array.buffer.byteLength).toBe(expectedByteLength);
180
+ expect(compacted.length).toBe(3);
181
+ expect(arr.capacity).toBe(3);
182
+ expect(arr.buffer.byteLength).toBe(3 * Float64Array.BYTES_PER_ELEMENT);
88
183
  });
89
184
 
90
- it("from reuses an existing buffer and grows when needed", () => {
91
- const backing = new ArrayBuffer(16);
185
+ it("grows again after compact", () => {
186
+ const arr = new ResizeableTypedArray(Uint16Array);
187
+ arr.pushMany([1, 2]);
188
+ arr.compact();
189
+ arr.push(3);
190
+ expect(Array.from(arr.array)).toEqual([1, 2, 3]);
191
+ });
192
+
193
+ it("from wraps a transferred buffer without allocating, and can grow past 2 MiB", () => {
194
+ // A finished column larger than 2 MiB, as received from another worker.
195
+ const backing = new ArrayBuffer(4 * 2 ** 20);
92
196
  new Uint32Array(backing).set([1, 2, 3, 4]);
93
197
 
94
198
  const arr = ResizeableTypedArray.from(Uint32Array, backing);
199
+ expect(arr.buffer).toBe(backing);
200
+ expect(arr.length).toBe(2 ** 20);
201
+ expect(arr.at(3)).toBe(4);
95
202
 
96
- arr.bufferSize = arr.buffer.byteLength;
97
- arr.maxByteLength = Math.max(arr.maxByteLength, arr.bufferSize * 2);
98
- arr.items = arr.array.length;
203
+ arr.push(5);
204
+ expect(arr.length).toBe(2 ** 20 + 1);
205
+ expect(arr.at(-1)).toBe(5);
206
+ expect(arr.at(3)).toBe(4);
207
+ });
99
208
 
100
- expect(arr.length).toBe(4);
101
- expect(Array.from(arr.slice(0, arr.length))).toEqual([1, 2, 3, 4]);
209
+ it("falls back to reallocating when the runtime refuses a reservation", () => {
210
+ const arr = new ResizeableTypedArray(
211
+ Uint8Array,
212
+ NoReserveArrayBuffer as unknown as ArrayBufferConstructor,
213
+ );
214
+ const first = arr.buffer;
215
+ arr.pushMany(new Uint8Array(INITIAL_BYTES + 1).fill(7));
216
+ expect(arr.buffer).not.toBe(first);
217
+ expect(arr.length).toBe(INITIAL_BYTES + 1);
218
+ expect(arr.at(-1)).toBe(7);
219
+ });
102
220
 
103
- arr.push(5);
221
+ it("projects the capacity a column reaches: doubling, then 1.5× above 256 MiB", () => {
222
+ const MiB = 2 ** 20;
223
+ expect(ResizeableTypedArray.capacityBytesFor(10, 8)).toBe(INITIAL_BYTES);
224
+ expect(ResizeableTypedArray.capacityBytesFor(3 * MiB, 8)).toBe(4 * MiB);
225
+ expect(ResizeableTypedArray.capacityBytesFor(300 * MiB, 8)).toBe(384 * MiB);
226
+ // Just over 2 GiB no longer asks for 4 GiB.
227
+ const capacity = ResizeableTypedArray.capacityBytesFor(2 * 1024 * MiB + 8, 8);
228
+ expect(capacity).toBeLessThan(3 * 1024 * MiB);
229
+ expect(capacity).toBeGreaterThanOrEqual(2 * 1024 * MiB + 8);
230
+ });
104
231
 
105
- expect(arr.length).toBe(5);
106
- expect(arr.array.length).toBe(8);
107
- expect(Array.from(arr.slice(0, arr.length))).toEqual([1, 2, 3, 4, 5]);
232
+ it("grows in place without copying", () => {
233
+ const arr = new ResizeableTypedArray(Float64Array);
234
+ const first = arr.buffer;
235
+ for (let i = 0; i < 100_000; i++) arr.push(i);
236
+ expect(arr.buffer).toBe(first);
237
+ expect(arr.at(99_999)).toBe(99_999);
108
238
  });
109
239
 
110
240
  if (typeof SharedArrayBuffer !== "undefined") {
111
- it("grows shared buffers in place", () => {
112
- const backing = new SharedArrayBuffer(8, {
113
- maxByteLength: 32,
114
- });
115
- const seed = new Uint8Array(backing);
116
- seed.set([1, 2, 3, 4, 5, 6, 7, 8]);
241
+ it("grows a transferred growable shared buffer in place", () => {
242
+ const backing = new SharedArrayBuffer(8, { maxByteLength: 32 });
243
+ new Uint8Array(backing).set([1, 2, 3, 4, 5, 6, 7, 8]);
117
244
 
118
245
  const arr = ResizeableTypedArray.from(Uint8Array, backing);
119
-
120
- arr.bufferSize = arr.buffer.byteLength;
121
- arr.maxByteLength = backing.maxByteLength;
122
- arr.items = arr.array.length;
123
-
124
- const originalBuffer = arr.buffer;
125
-
126
246
  arr.push(9);
127
247
 
128
- expect(arr.buffer).toBe(originalBuffer);
129
- expect(arr.array.length).toBe(16);
130
- expect(arr.buffer.byteLength).toBe(16);
131
- expect(Array.from(arr.slice(0, 9))).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
248
+ expect(arr.buffer).toBe(backing);
249
+ expect(arr.capacity).toBe(16);
250
+ expect(Array.from(arr.array)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
132
251
  });
133
252
  }
134
253
  });
@@ -1,32 +0,0 @@
1
- declare module "geokdbush" {
2
- import type KDBush from "kdbush";
3
-
4
- /**
5
- * Returns an array of the closest points from a given location in order of increasing distance.
6
- * @param index - A KDBush index with geographic coordinates (lng, lat) in degrees.
7
- * @param lng - Query point longitude in degrees.
8
- * @param lat - Query point latitude in degrees.
9
- * @param maxResults - Maximum number of points to return (default: Infinity).
10
- * @param maxDistance - Maximum distance in kilometers (default: Infinity).
11
- * @param predicate - Optional filter function called with index of each point.
12
- * @returns Array of point indices from the index, sorted by distance.
13
- */
14
- export function around(
15
- index: KDBush,
16
- lng: number,
17
- lat: number,
18
- maxResults?: number,
19
- maxDistance?: number,
20
- predicate?: (index: number) => boolean,
21
- ): number[];
22
-
23
- /**
24
- * Returns the great circle distance between two locations in kilometers.
25
- * @param lng1 - First point longitude in degrees.
26
- * @param lat1 - First point latitude in degrees.
27
- * @param lng2 - Second point longitude in degrees.
28
- * @param lat2 - Second point latitude in degrees.
29
- * @returns Distance in kilometers.
30
- */
31
- export function distance(lng1: number, lat1: number, lng2: number, lat2: number): number;
32
- }
@@ -1,47 +0,0 @@
1
- declare module "kdbush" {
2
- import type {
3
- BufferConstructor,
4
- BufferType,
5
- TypedArray,
6
- TypedArrayConstructor,
7
- } from "../typed-arrays.ts";
8
-
9
- export default class KDBush {
10
- constructor(
11
- length: number,
12
- nodeSize?: number,
13
- ArrayType?: TypedArrayConstructor,
14
- BufferConstructor?: BufferConstructor,
15
- );
16
-
17
- /** Number of items in the index */
18
- numItems: number;
19
-
20
- /** Size of the KD-tree node */
21
- nodeSize: number;
22
-
23
- /** Serialized index data buffer */
24
- data: BufferType;
25
-
26
- /** Internal array of indices (used by geokdbush) */
27
- ids: Uint16Array | Uint32Array;
28
-
29
- /** Internal array of coordinates [x0, y0, x1, y1, ...] (used by geokdbush) */
30
- coords: TypedArray;
31
-
32
- /** Add a point before calling finish */
33
- add(x: number, y: number): number;
34
-
35
- /** Finalize the index after adding all points */
36
- finish(): this;
37
-
38
- /** Query points within a bounding box; returns internal point indexes */
39
- range(minX: number, minY: number, maxX: number, maxY: number): number[];
40
-
41
- /** Query points within a radius from (x, y); returns internal point indexes */
42
- within(x: number, y: number, radius: number): number[];
43
-
44
- /** Reconstruct an index from a serialized buffer */
45
- static from(data: BufferType): KDBush;
46
- }
47
- }