@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
@@ -7,6 +7,8 @@
7
7
  * @module
8
8
  */
9
9
 
10
+ import { isSharedArrayBuffer } from "@osmix/shared/backing-buffers";
11
+
10
12
  /**
11
13
  * Use SharedArrayBuffer if the runtime supports it, otherwise fall back to ArrayBuffer.
12
14
  * SharedArrayBuffer enables zero-copy transfer between workers.
@@ -38,8 +40,56 @@ export type TypedArray<B extends BufferType = BufferType> =
38
40
  * Constructor interface for typed arrays.
39
41
  */
40
42
  export interface TypedArrayConstructor<T extends TypedArray<BufferType> = TypedArray<BufferType>> {
41
- new (buffer: BufferType): T;
43
+ new (buffer: BufferType, byteOffset?: number, length?: number): T;
42
44
  readonly BYTES_PER_ELEMENT: number;
45
+ readonly name: string;
46
+ }
47
+
48
+ export type TypedBufferAllocationOperation = "create" | "grow" | "compact";
49
+ export type TypedBufferType = "array-buffer" | "shared-array-buffer";
50
+
51
+ /** Structured allocation failure suitable for transport across worker boundaries. */
52
+ export class TypedBufferAllocationError extends Error {
53
+ readonly code = "TYPED_BUFFER_ALLOCATION_FAILED";
54
+ readonly stage = "typed-buffer-allocation";
55
+ readonly operation: TypedBufferAllocationOperation;
56
+ readonly typedArray: string;
57
+ readonly bufferType: TypedBufferType;
58
+ readonly elementCount: number;
59
+ readonly bytesPerElement: number;
60
+ readonly requiredBytes: number;
61
+
62
+ constructor(
63
+ args: {
64
+ operation: TypedBufferAllocationOperation;
65
+ typedArray: string;
66
+ bufferType: TypedBufferType;
67
+ elementCount: number;
68
+ bytesPerElement: number;
69
+ requiredBytes: number;
70
+ },
71
+ cause: unknown,
72
+ ) {
73
+ super(
74
+ `${args.typedArray} ${args.operation} requires ${args.requiredBytes.toLocaleString()} bytes in a single ${args.bufferType === "shared-array-buffer" ? "SharedArrayBuffer" : "ArrayBuffer"}.`,
75
+ { cause },
76
+ );
77
+ this.name = "TypedBufferAllocationError";
78
+ this.operation = args.operation;
79
+ this.typedArray = args.typedArray;
80
+ this.bufferType = args.bufferType;
81
+ this.elementCount = args.elementCount;
82
+ this.bytesPerElement = args.bytesPerElement;
83
+ this.requiredBytes = args.requiredBytes;
84
+ }
85
+ }
86
+
87
+ type BufferConstructorType = SharedArrayBufferConstructor | ArrayBufferConstructor;
88
+
89
+ function getBufferType(BC: BufferConstructorType): TypedBufferType {
90
+ return typeof SharedArrayBuffer !== "undefined" && BC === SharedArrayBuffer
91
+ ? "shared-array-buffer"
92
+ : "array-buffer";
43
93
  }
44
94
 
45
95
  /**
@@ -51,63 +101,60 @@ export interface TypedArrayConstructor<T extends TypedArray<BufferType> = TypedA
51
101
  */
52
102
  export const IdArrayType = Float64Array;
53
103
 
54
- /**
55
- * Uint32Array for storing array indices.
56
- *
57
- * Internal indices into typed arrays never exceed 2^32 elements,
58
- * so Uint32Array provides the best balance of range and memory efficiency.
59
- */
60
- export const IndexArrayType = Uint32Array;
104
+ const MiB = 2 ** 20;
105
+
106
+ /** Starting capacity of a new column. Growth is in place, so small columns stay small. */
107
+ const INITIAL_BYTES = 64 * 1024;
61
108
 
62
109
  /**
63
- * Initial buffer size for ResizeableTypedArray.
64
- * 1 MiB provides reasonable initial capacity while avoiding excessive memory allocation.
110
+ * Address space a growing column reserves so that it grows in place, without copying.
111
+ * Reserved pages hold no memory until written. Chromium's V8 sandbox has room for about 250
112
+ * reservations of this size, and a load builds about 35 columns at once. A reservation that
113
+ * fails falls back to growing by reallocation.
65
114
  */
66
- export const DEFAULT_BUFFER_SIZE = 2 ** 20; // 1 MiB
115
+ const GROWTH_RESERVE_BYTES = 4 * 2 ** 30;
116
+
117
+ /** Above this size a column grows by 1.5× instead of 2×, so it overshoots less. */
118
+ const DOUBLING_LIMIT_BYTES = 256 * MiB;
67
119
 
68
120
  /**
69
121
  * Auto-expanding typed array wrapper.
70
122
  *
71
- * - `push()` appends elements, doubling buffer size as needed.
72
- * - `compact()` shrinks buffer to fit data.
73
- * - Supports growable SharedArrayBuffer and resizable ArrayBuffer.
123
+ * - `push()` / `pushMany()` append, growing the buffer in place when the runtime supports
124
+ * growable buffers, otherwise by reallocating.
125
+ * - `array` is a view of the stored items only; `compact()` shrinks the buffer to fit them.
126
+ * - `from()` wraps a finished (transferred) buffer, which can still grow.
74
127
  */
75
128
  export class ResizeableTypedArray<TA extends TypedArray> {
76
129
  /** The typed array constructor for this instance */
77
- ArrayType: TypedArrayConstructor<TA>;
78
- /** The current typed array view into the buffer */
79
- array: TA;
80
- /** Number of items actually stored (may be less than array.length) */
81
- items = 0;
82
-
83
- /** The underlying ArrayBuffer or SharedArrayBuffer */
84
- buffer: BufferType;
85
- /** Current buffer size in bytes */
86
- bufferSize: number;
87
- /** Maximum byte length for growable buffers */
88
- maxByteLength: number;
89
-
130
+ readonly ArrayType: TypedArrayConstructor<TA>;
90
131
  /** Buffer constructor (SharedArrayBuffer or ArrayBuffer) */
91
- BC: SharedArrayBufferConstructor | ArrayBufferConstructor;
132
+ private readonly BC: BufferConstructorType;
133
+ /** View of the whole buffer: stored items, then spare capacity. */
134
+ private view: TA;
135
+ /** Number of items actually stored (may be less than the capacity). */
136
+ private items = 0;
92
137
 
93
138
  /**
94
- * Reconstruct a ResizeableTypedArray from an existing buffer.
95
- *
96
- * Used after transferring buffers between workers. The resulting array
97
- * is considered "compacted" with items = array.length.
98
- *
99
- * @param ArrayType - The typed array constructor.
100
- * @param buffer - The existing buffer to wrap.
101
- * @returns A new ResizeableTypedArray wrapping the buffer.
139
+ * The buffer size a new column reaches when it is filled one `push` at a time to
140
+ * `requiredBytes`, for projecting peak memory before a load.
141
+ */
142
+ static capacityBytesFor(requiredBytes: number, bytesPerElement: number): number {
143
+ let bytes = INITIAL_BYTES;
144
+ while (bytes < requiredBytes) {
145
+ bytes = nextByteLength(bytes, bytes + bytesPerElement, bytesPerElement);
146
+ }
147
+ return bytes;
148
+ }
149
+
150
+ /**
151
+ * Wrap an existing buffer, as after transferring it between workers. Every element of the
152
+ * buffer is a stored item. Nothing is allocated until the array grows.
102
153
  */
103
154
  static from<TA extends TypedArray>(ArrayType: TypedArrayConstructor<TA>, buffer: BufferType) {
104
- const rta = new ResizeableTypedArray<TA>(
105
- ArrayType,
106
- buffer instanceof SharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer,
107
- );
108
- rta.buffer = buffer;
109
- rta.array = new ArrayType(buffer);
110
- rta.items = rta.array.length;
155
+ const BC = isSharedArrayBuffer(buffer) ? SharedArrayBuffer : ArrayBuffer;
156
+ const rta = new ResizeableTypedArray<TA>(ArrayType, BC, new ArrayType(buffer));
157
+ rta.items = rta.view.length;
111
158
  return rta;
112
159
  }
113
160
 
@@ -119,85 +166,62 @@ export class ResizeableTypedArray<TA extends TypedArray> {
119
166
  */
120
167
  constructor(
121
168
  ArrayType: TypedArrayConstructor<TA>,
122
- BC: SharedArrayBufferConstructor | ArrayBufferConstructor = BufferConstructor,
169
+ BC: BufferConstructorType = BufferConstructor,
170
+ /** @internal A view to wrap instead of allocating; used by `from()`. */
171
+ view?: TA,
123
172
  ) {
124
173
  this.ArrayType = ArrayType;
125
- this.bufferSize = DEFAULT_BUFFER_SIZE;
126
- this.maxByteLength = DEFAULT_BUFFER_SIZE * 2;
127
174
  this.BC = BC;
128
- this.buffer = new BC(this.bufferSize, {
129
- maxByteLength: this.maxByteLength,
130
- });
131
- this.array = new this.ArrayType(this.buffer);
175
+ this.view = view ?? new ArrayType(this.allocate("create", INITIAL_BYTES, GROWTH_RESERVE_BYTES));
132
176
  }
133
177
 
134
- /**
135
- * Iterate over the array values.
136
- */
137
- [Symbol.iterator](): ArrayIterator<number> {
138
- return this.array[Symbol.iterator]();
178
+ /** The stored items. Before `compact()`, a view that excludes the spare capacity. */
179
+ get array(): TA {
180
+ return this.items === this.view.length ? this.view : (this.view.subarray(0, this.items) as TA);
139
181
  }
140
182
 
141
- /**
142
- * Double the buffer capacity.
143
- * Uses in-place grow/resize if supported, otherwise allocates new buffer and copies.
144
- */
145
- expandArray() {
146
- this.bufferSize *= 2;
147
- if (this.bufferSize > this.buffer.maxByteLength) {
148
- // Need a completely new buffer with larger maxByteLength
149
- this.maxByteLength *= 2;
150
- const newBuffer = new this.BC(this.bufferSize, {
151
- maxByteLength: this.maxByteLength,
152
- });
153
- const newArray = new this.ArrayType(newBuffer);
154
- newArray.set(this.array);
155
- this.buffer = newBuffer;
156
- this.array = newArray;
157
- } else {
158
- // Can grow/resize the existing buffer in place
159
- if (this.buffer instanceof SharedArrayBuffer && this.buffer.growable) {
160
- this.buffer.grow(this.bufferSize);
161
- } else if (this.buffer instanceof ArrayBuffer && this.buffer.resizable) {
162
- this.buffer.resize(this.bufferSize);
163
- } else {
164
- throw Error("Buffer is not growable or resizable");
165
- }
166
- }
183
+ /** The underlying buffer, including any spare capacity. */
184
+ get buffer(): BufferType {
185
+ return this.view.buffer as BufferType;
186
+ }
187
+
188
+ get length() {
189
+ return this.items;
190
+ }
191
+
192
+ /** Number of items the current buffer can hold before it must grow. */
193
+ get capacity() {
194
+ return this.view.length;
195
+ }
196
+
197
+ /** Iterate over the stored values. */
198
+ [Symbol.iterator](): ArrayIterator<number> {
199
+ return this.array[Symbol.iterator]();
167
200
  }
168
201
 
169
202
  /**
170
203
  * Get the value at an index. Handles negative indices.
171
204
  */
172
205
  at(index: number): number {
173
- if (index < -this.length || index >= this.length)
174
- throw Error(`Index out of bounds: ${index}. Length: ${this.length}`);
175
- if (index < 0) return this.at(this.length + index);
176
- const result = this.array[index];
177
- if (result === undefined) throw Error(`No value at index: ${index}`);
178
- return result;
206
+ if (index >= 0 && index < this.items) return this.view[index]!;
207
+ if (index < 0 && index >= -this.items) return this.view[this.items + index]!;
208
+ throw Error(`Index out of bounds: ${index}. Length: ${this.items}`);
179
209
  }
180
210
 
181
211
  /**
182
- * Get a slice of the array.
212
+ * Get a copy of a range of the stored items.
183
213
  */
184
214
  slice(start: number, end: number) {
185
215
  return this.array.slice(start, end);
186
216
  }
187
217
 
188
- get length() {
189
- return this.items;
190
- }
191
-
192
218
  /**
193
219
  * Push a value to the end of the array.
194
220
  */
195
221
  push(value: number): number {
196
- if (this.length >= this.array.length) {
197
- this.expandArray();
198
- }
199
- this.array[this.items++] = value;
200
- return this.length - 1;
222
+ if (this.items === this.view.length) this.ensureCapacity(this.items + 1);
223
+ this.view[this.items] = value;
224
+ return this.items++;
201
225
  }
202
226
 
203
227
  /**
@@ -205,19 +229,19 @@ export class ResizeableTypedArray<TA extends TypedArray> {
205
229
  */
206
230
  set(index: number, value: number) {
207
231
  if (index < 0) throw Error("Index out of bounds");
208
- if (index >= this.length) {
209
- while (index >= this.array.length) this.expandArray();
232
+ if (index >= this.items) {
233
+ this.ensureCapacity(index + 1);
210
234
  this.items = index + 1;
211
235
  }
212
- this.array[index] = value;
236
+ this.view[index] = value;
213
237
  }
214
238
 
215
239
  /**
216
240
  * Push multiple values to the end of the array.
217
241
  */
218
- pushMany(values: number[] | TypedArray) {
219
- while (this.length + values.length > this.array.length) this.expandArray();
220
- this.array.set(values, this.length);
242
+ pushMany(values: ArrayLike<number>) {
243
+ this.ensureCapacity(this.items + values.length);
244
+ this.view.set(values, this.items);
221
245
  this.items += values.length;
222
246
  }
223
247
 
@@ -226,16 +250,104 @@ export class ResizeableTypedArray<TA extends TypedArray> {
226
250
  * Buffer becomes fixed-length after compacting.
227
251
  */
228
252
  compact() {
229
- if (this.buffer instanceof SharedArrayBuffer) {
230
- // SharedArrayBuffer uses slice() to create a new fixed-size buffer
231
- this.buffer = this.buffer.slice(0, this.length * this.ArrayType.BYTES_PER_ELEMENT);
232
- } else {
233
- // ArrayBuffer uses transferToFixedLength() to detach and resize
234
- this.buffer = this.buffer.transferToFixedLength(
235
- this.length * this.ArrayType.BYTES_PER_ELEMENT,
236
- );
253
+ const requiredBytes = this.items * this.ArrayType.BYTES_PER_ELEMENT;
254
+ const buffer = this.buffer;
255
+ let compacted: BufferType;
256
+ try {
257
+ compacted = isSharedArrayBuffer(buffer)
258
+ ? // A SharedArrayBuffer cannot shrink: copy the items into a fixed-size buffer.
259
+ buffer.slice(0, requiredBytes)
260
+ : buffer.transferToFixedLength(requiredBytes);
261
+ } catch (cause) {
262
+ throw this.allocationError("compact", requiredBytes, cause);
263
+ }
264
+ this.view = new this.ArrayType(compacted);
265
+ return this.view;
266
+ }
267
+
268
+ /** Make room for `count` items: grow in place if the buffer can, otherwise reallocate. */
269
+ private ensureCapacity(count: number) {
270
+ if (count <= this.view.length) return;
271
+ const bytesPerElement = this.ArrayType.BYTES_PER_ELEMENT;
272
+ const buffer = this.buffer;
273
+ const byteLength = nextByteLength(buffer.byteLength, count * bytesPerElement, bytesPerElement);
274
+ if (buffer.maxByteLength >= byteLength && growInPlace(buffer, byteLength)) {
275
+ // A resized buffer keeps a length-tracking view: take a fresh fixed-length one.
276
+ this.view = new this.ArrayType(buffer, 0, byteLength / bytesPerElement);
277
+ return;
278
+ }
279
+ const next = this.allocate("grow", byteLength, GROWTH_RESERVE_BYTES);
280
+ const view = new this.ArrayType(next, 0, byteLength / bytesPerElement);
281
+ view.set(this.array);
282
+ this.view = view;
283
+ }
284
+
285
+ /**
286
+ * Allocate `byteLength` bytes that can grow in place up to `reserveBytes` when the runtime
287
+ * allows. A failed reservation falls back to a buffer that doubles once, then to fixed size.
288
+ */
289
+ private allocate(
290
+ operation: TypedBufferAllocationOperation,
291
+ byteLength: number,
292
+ reserveBytes: number,
293
+ ): BufferType {
294
+ for (const maxByteLength of [Math.max(reserveBytes, byteLength), byteLength * 2]) {
295
+ try {
296
+ return new this.BC(byteLength, { maxByteLength });
297
+ } catch {
298
+ // The runtime refused the reservation: try a smaller one, then a fixed buffer.
299
+ }
300
+ }
301
+ try {
302
+ return new this.BC(byteLength);
303
+ } catch (cause) {
304
+ throw this.allocationError(operation, byteLength, cause);
305
+ }
306
+ }
307
+
308
+ private allocationError(
309
+ operation: TypedBufferAllocationOperation,
310
+ requiredBytes: number,
311
+ cause: unknown,
312
+ ): TypedBufferAllocationError {
313
+ return new TypedBufferAllocationError(
314
+ {
315
+ operation,
316
+ typedArray: this.ArrayType.name,
317
+ bufferType: getBufferType(this.BC),
318
+ elementCount: Math.ceil(requiredBytes / this.ArrayType.BYTES_PER_ELEMENT),
319
+ bytesPerElement: this.ArrayType.BYTES_PER_ELEMENT,
320
+ requiredBytes,
321
+ },
322
+ cause,
323
+ );
324
+ }
325
+ }
326
+
327
+ /**
328
+ * The next buffer size for a column that needs `requiredBytes`: double while small, then grow
329
+ * by 1.5×, and never less than required. Rounded up to a multiple of 8 bytes so every element
330
+ * type divides it.
331
+ */
332
+ function nextByteLength(currentBytes: number, requiredBytes: number, bytesPerElement: number) {
333
+ const current = Math.max(currentBytes, bytesPerElement);
334
+ const grown = current < DOUBLING_LIMIT_BYTES ? current * 2 : Math.ceil(current * 1.5);
335
+ return Math.ceil(Math.max(grown, requiredBytes) / 8) * 8;
336
+ }
337
+
338
+ /** Grow a growable or resizable buffer to `byteLength`. False if it cannot grow in place. */
339
+ function growInPlace(buffer: BufferType, byteLength: number): boolean {
340
+ try {
341
+ if (isSharedArrayBuffer(buffer)) {
342
+ if (!buffer.growable) return false;
343
+ buffer.grow(byteLength);
344
+ return true;
237
345
  }
238
- this.array = new this.ArrayType(this.buffer);
239
- return this.array;
346
+ if (!buffer.resizable) return false;
347
+ buffer.resize(byteLength);
348
+ return true;
349
+ } catch {
350
+ // Committing the pages failed: fall back to reallocating, which reports its own error.
351
+ return false;
240
352
  }
241
353
  }