@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.
- package/CHANGELOG.md +31 -0
- package/README.md +54 -7
- package/dist/contracts.d.ts +2 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/entities.d.ts +21 -1
- package/dist/entities.d.ts.map +1 -1
- package/dist/entities.js +52 -3
- package/dist/entities.js.map +1 -1
- package/dist/ids.d.ts +18 -5
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +68 -18
- package/dist/ids.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/indirect-kd-index.d.ts +28 -0
- package/dist/indirect-kd-index.d.ts.map +1 -0
- package/dist/indirect-kd-index.js +163 -0
- package/dist/indirect-kd-index.js.map +1 -0
- package/dist/limits.d.ts +30 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +51 -0
- package/dist/limits.js.map +1 -0
- package/dist/nodes.d.ts +28 -13
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +181 -51
- package/dist/nodes.js.map +1 -1
- package/dist/osm.d.ts +59 -3
- package/dist/osm.d.ts.map +1 -1
- package/dist/osm.js +31 -2
- package/dist/osm.js.map +1 -1
- package/dist/relations.d.ts +10 -4
- package/dist/relations.d.ts.map +1 -1
- package/dist/relations.js +66 -28
- package/dist/relations.js.map +1 -1
- package/dist/renumber.d.ts +23 -0
- package/dist/renumber.d.ts.map +1 -0
- package/dist/renumber.js +64 -0
- package/dist/renumber.js.map +1 -0
- package/dist/stringtable.d.ts +2 -1
- package/dist/stringtable.d.ts.map +1 -1
- package/dist/stringtable.js +5 -1
- package/dist/stringtable.js.map +1 -1
- package/dist/tags.d.ts +44 -11
- package/dist/tags.d.ts.map +1 -1
- package/dist/tags.js +153 -27
- package/dist/tags.js.map +1 -1
- package/dist/typed-arrays.d.ts +63 -48
- package/dist/typed-arrays.d.ts.map +1 -1
- package/dist/typed-arrays.js +184 -107
- package/dist/typed-arrays.js.map +1 -1
- package/dist/ways.d.ts +50 -2
- package/dist/ways.d.ts.map +1 -1
- package/dist/ways.js +212 -35
- package/dist/ways.js.map +1 -1
- package/package.json +11 -12
- package/src/contracts.ts +2 -1
- package/src/entities.ts +67 -4
- package/src/ids.ts +72 -22
- package/src/index.ts +13 -7
- package/src/indirect-kd-index.ts +212 -0
- package/src/limits.ts +73 -0
- package/src/nodes.ts +228 -54
- package/src/osm.ts +90 -4
- package/src/relations.ts +80 -30
- package/src/renumber.ts +80 -0
- package/src/stringtable.ts +15 -1
- package/src/tags.ts +173 -31
- package/src/typed-arrays.ts +225 -113
- package/src/ways.ts +233 -38
- package/test/entity-index-errors.test.ts +41 -0
- package/test/ids.bench.ts +11 -7
- package/test/ids.test.ts +38 -0
- package/test/limits.test.ts +83 -0
- package/test/no-shared-array-buffer.test.ts +41 -0
- package/test/nodes-spatial-index.test.ts +261 -0
- package/test/osm-bbox.test.ts +30 -0
- package/test/relations.bench.ts +9 -4
- package/test/relations.test.ts +52 -29
- package/test/renumber.test.ts +54 -0
- package/test/spatial-index-rebuild.test.ts +33 -0
- package/test/storage-compaction.test.ts +269 -0
- package/test/tags-search.test.ts +13 -0
- package/test/typed-arrays.test.ts +178 -59
- package/src/types/geokdbush.d.ts +0 -32
- 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
|
+
});
|
package/test/tags-search.test.ts
CHANGED
|
@@ -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 {
|
|
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("
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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("
|
|
116
|
+
it("push stores values and returns the index", () => {
|
|
29
117
|
const arr = new ResizeableTypedArray(Float64Array);
|
|
30
118
|
|
|
31
|
-
arr.
|
|
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
|
-
|
|
58
|
-
|
|
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("
|
|
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
|
-
|
|
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(
|
|
86
|
-
expect(
|
|
87
|
-
expect(arr.
|
|
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("
|
|
91
|
-
const
|
|
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.
|
|
97
|
-
arr.
|
|
98
|
-
arr.
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
112
|
-
const backing = new SharedArrayBuffer(8, {
|
|
113
|
-
|
|
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(
|
|
129
|
-
expect(arr.
|
|
130
|
-
expect(arr.
|
|
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
|
});
|
package/src/types/geokdbush.d.ts
DELETED
|
@@ -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
|
-
}
|
package/src/types/kdbush.d.ts
DELETED
|
@@ -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
|
-
}
|