@woosh/meep-engine 2.121.13 → 2.122.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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "description": "Fully featured ECS game engine written in JavaScript",
6
6
  "type": "module",
7
7
  "author": "Alexander Goldring",
8
- "version": "2.121.13",
8
+ "version": "2.122.0",
9
9
  "main": "build/meep.module.js",
10
10
  "module": "build/meep.module.js",
11
11
  "exports": {
@@ -0,0 +1,113 @@
1
+ export const ALLOCATOR_NO_SPACE: 4294967295;
2
+ /**
3
+ * Fast hard realtime O(1) offset allocator with minimal fragmentation.
4
+ *
5
+ * Uses 256 bins with 8 bit floating point distribution (3 bit mantissa + 5 bit exponent) and a two level bitfield to find the next available bin using 2x LZCNT instructions to make all operations O(1). Bin sizes following the floating point distribution ensures hard bounds for memory overhead percentage regardless of size class. Pow2 bins would waste up to +100% memory (+50% on average). Our float bins waste up to +12.5% (+6.25% on average).
6
+ *
7
+ * The allocation metadata is stored in a separate data structure, making this allocator suitable for sub-allocating any resources, such as GPU heaps, buffers and arrays. Returns an offset to the first element of the allocated contiguous range.
8
+ *
9
+ * @see "A comparison of memory allocators for real-time applications" by Miguel Masmano et al
10
+ * @example
11
+ * const allocator = new Allocator(12345); // Allocator with 12345 contiguous elements in total
12
+ *
13
+ * const a = allocator.allocate(1337); // Allocate a 1337 element contiguous range
14
+ * const offset_a = a.offset; // Provides offset to the first element of the range
15
+ * do_something(offset_a);
16
+ *
17
+ * const b = allocator.allocate(123); // Allocate a 123 element contiguous range
18
+ * const offset_b = b.offset; // Provides offset to the first element of the range
19
+ * do_something(offset_b);
20
+ *
21
+ * allocator.free(a); // Free allocation a
22
+ * allocator.free(b); // Free allocation b
23
+ */
24
+ export class OffsetAllocator {
25
+ /**
26
+ *
27
+ * @param {number} size
28
+ * @param {number} maxAllocs
29
+ */
30
+ constructor(size: number, maxAllocs?: number);
31
+ /**
32
+ * Total managed space
33
+ * uint32
34
+ * @type {number}
35
+ */
36
+ size: number;
37
+ /**
38
+ * Limit on number of allocations
39
+ * uint32
40
+ * @type {number}
41
+ */
42
+ maxAllocs: number;
43
+ /**
44
+ * uint32
45
+ * @type {number}
46
+ */
47
+ freeStorage: number;
48
+ /**
49
+ * uint32
50
+ * @type {number}
51
+ */
52
+ usedBinsTop: number;
53
+ /**
54
+ *
55
+ * @type {Uint8Array}
56
+ */
57
+ usedBins: Uint8Array;
58
+ /**
59
+ *
60
+ * @type {Uint32Array}
61
+ */
62
+ binIndices: Uint32Array;
63
+ /**
64
+ *
65
+ * @type {RowFirstTable}
66
+ */
67
+ nodes: RowFirstTable;
68
+ /**
69
+ * @type {Uint32Array}
70
+ */
71
+ freeNodes: Uint32Array;
72
+ /**
73
+ * uint32
74
+ * @type {number}
75
+ */
76
+ freeOffset: number;
77
+ /**
78
+ *
79
+ * @param {number} size
80
+ * @returns {Allocation}
81
+ */
82
+ allocate(size: number): Allocation;
83
+ /**
84
+ *
85
+ * @param {Allocation} allocation
86
+ */
87
+ free(allocation: Allocation): void;
88
+ reset(): void;
89
+ /**
90
+ * Useful for debug and UI
91
+ * @return {StorageReport}
92
+ */
93
+ storageReport(): StorageReport;
94
+ #private;
95
+ }
96
+ import { RowFirstTable } from "../../collection/table/RowFirstTable.js";
97
+ declare class Allocation {
98
+ /**
99
+ *
100
+ * @param {number} offset
101
+ * @param {number} metadata
102
+ * @return {Allocation}
103
+ */
104
+ static from(offset: number, metadata: number): Allocation;
105
+ offset: number;
106
+ metadata: number;
107
+ }
108
+ declare class StorageReport {
109
+ totalFreeSpace: number;
110
+ largestFreeRegion: number;
111
+ }
112
+ export {};
113
+ //# sourceMappingURL=OffsetAllocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OffsetAllocator.d.ts","sourceRoot":"","sources":["../../../../../src/core/binary/allocator/OffsetAllocator.js"],"names":[],"mappings":"AAaA,iCAAkC,UAAU,CAAC;AAmF7C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAwDI;;;;OAIG;IACH,kBAHW,MAAM,cACN,MAAM,EAYhB;IAtED;;;;OAIG;IACH,MAFU,MAAM,CAEP;IAET;;;;OAIG;IACH,WAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,UAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,YAFU,WAAW,CAEuB;IAE5C;;;OAGG;IACH,OAFU,aAAa,CAEmB;IAE1C;;OAEG;IACH,WAFU,WAAW,CAEX;IAEV;;;OAGG;IACH,YAFU,MAAM,CAED;IAmBf;;;;OAIG;IACH,eAHW,MAAM,GACJ,UAAU,CAkGtB;IAED;;;OAGG;IACH,iBAFW,UAAU,QAsFpB;IAqID,cA4BC;IAED;;;OAGG;IACH,iBAFY,aAAa,CA2BxB;;CACJ;8BA9jB6B,yCAAyC;AAgDvE;IAII;;;;;OAKG;IACH,oBAJW,MAAM,YACN,MAAM,GACL,UAAU,CASrB;IAhBD,eAA4B;IAC5B,iBAA8B;CAgBjC;AAoBD;IACI,uBAAkB;IAClB,0BAAqB;CACxB"}
@@ -0,0 +1,576 @@
1
+ import { assert } from "../../assert.js";
2
+ import { RowFirstTable } from "../../collection/table/RowFirstTable.js";
3
+ import { RowFirstTableSpec } from "../../collection/table/RowFirstTableSpec.js";
4
+ import { ctz32 } from "../ctz32.js";
5
+ import { floatToUint, uintToFloatRoundDown, uintToFloatRoundUp } from "../float/SmallFloat.js";
6
+ import { msb_32 } from "../msb_32.js";
7
+ import { BinaryDataType } from "../type/BinaryDataType.js";
8
+
9
+ const NUM_TOP_BINS = 32;
10
+ const BINS_PER_LEAF = 8;
11
+ const TOP_BINS_INDEX_SHIFT = 3;
12
+ const LEAF_BINS_INDEX_MASK = 0x7;
13
+ const NUM_LEAF_BINS = NUM_TOP_BINS * BINS_PER_LEAF;
14
+ export const ALLOCATOR_NO_SPACE = 0xffffffff;
15
+ const NODE_UNUSED = 0xffffffff
16
+
17
+ /**
18
+ * Can potentially use u16, but number of allocation slots will be limited to 65k, so a severe limitation for little gain
19
+ * @type {BinaryDataType}
20
+ */
21
+ const NODE_INDEX_DATA_TYPE = BinaryDataType.Uint32;
22
+
23
+ const DEBUG_VERBOSE = false;
24
+
25
+ /**
26
+ * @see https://github.com/sebbbi/OffsetAllocator/blob/3610a7377088b1e8c8f1525f458c96038a4e6fc0/offsetAllocator.hpp#L70
27
+ * @type {RowFirstTableSpec}
28
+ */
29
+ const NODE_TABLE_SPEC = RowFirstTableSpec.get([
30
+ BinaryDataType.Uint32, // Data Offset
31
+ BinaryDataType.Uint32, // Data Size
32
+ NODE_INDEX_DATA_TYPE, // Bin List Prev
33
+ NODE_INDEX_DATA_TYPE, // Bin List Next
34
+ NODE_INDEX_DATA_TYPE, // Neighbour Prev
35
+ NODE_INDEX_DATA_TYPE, // Neighbour Next
36
+ BinaryDataType.Uint8, // FLAGS
37
+ ]);
38
+
39
+ const NODE_FLAGS_USED = 1;
40
+
41
+
42
+ const NODE_FIELD_DATA_OFFSET = 0;
43
+ const NODE_FIELD_DATA_SIZE = 1;
44
+ const NODE_FIELD_BIN_LIST_PREV = 2;
45
+ const NODE_FIELD_BIN_LIST_NEXT = 3;
46
+ const NODE_FIELD_NEIGHBOR_PREV = 4;
47
+ const NODE_FIELD_NEIGHBOR_NEXT = 5;
48
+ const NODE_FIELD_FLAGS = 6;
49
+
50
+ class Allocation {
51
+ offset = ALLOCATOR_NO_SPACE;
52
+ metadata = ALLOCATOR_NO_SPACE;
53
+
54
+ /**
55
+ *
56
+ * @param {number} offset
57
+ * @param {number} metadata
58
+ * @return {Allocation}
59
+ */
60
+ static from(offset, metadata) {
61
+ const r = new Allocation();
62
+
63
+ r.offset = offset;
64
+ r.metadata = metadata;
65
+
66
+ return r;
67
+ }
68
+ }
69
+
70
+ /**
71
+ *
72
+ * @param {number} bitMask
73
+ * @param {number} startBitIndex
74
+ * @return {number}
75
+ */
76
+ function findLowestSetBitAfter(bitMask, startBitIndex) {
77
+ const maskBeforeStartIndex = (1 << startBitIndex) - 1;
78
+ const maskAfterStartIndex = ~maskBeforeStartIndex;
79
+ const bitsAfter = bitMask & maskAfterStartIndex;
80
+
81
+ if (bitsAfter === 0) {
82
+ return ALLOCATOR_NO_SPACE;
83
+ }
84
+
85
+ return ctz32(bitsAfter);
86
+ }
87
+
88
+ class StorageReport {
89
+ totalFreeSpace = 0
90
+ largestFreeRegion = 0
91
+ }
92
+
93
+ /*
94
+ Largely a port of https://github.com/sebbbi/OffsetAllocator by "Sebastian Aaltonen"
95
+ */
96
+
97
+ /**
98
+ * Fast hard realtime O(1) offset allocator with minimal fragmentation.
99
+ *
100
+ * Uses 256 bins with 8 bit floating point distribution (3 bit mantissa + 5 bit exponent) and a two level bitfield to find the next available bin using 2x LZCNT instructions to make all operations O(1). Bin sizes following the floating point distribution ensures hard bounds for memory overhead percentage regardless of size class. Pow2 bins would waste up to +100% memory (+50% on average). Our float bins waste up to +12.5% (+6.25% on average).
101
+ *
102
+ * The allocation metadata is stored in a separate data structure, making this allocator suitable for sub-allocating any resources, such as GPU heaps, buffers and arrays. Returns an offset to the first element of the allocated contiguous range.
103
+ *
104
+ * @see "A comparison of memory allocators for real-time applications" by Miguel Masmano et al
105
+ * @example
106
+ * const allocator = new Allocator(12345); // Allocator with 12345 contiguous elements in total
107
+ *
108
+ * const a = allocator.allocate(1337); // Allocate a 1337 element contiguous range
109
+ * const offset_a = a.offset; // Provides offset to the first element of the range
110
+ * do_something(offset_a);
111
+ *
112
+ * const b = allocator.allocate(123); // Allocate a 123 element contiguous range
113
+ * const offset_b = b.offset; // Provides offset to the first element of the range
114
+ * do_something(offset_b);
115
+ *
116
+ * allocator.free(a); // Free allocation a
117
+ * allocator.free(b); // Free allocation b
118
+ */
119
+ export class OffsetAllocator {
120
+ /**
121
+ * Total managed space
122
+ * uint32
123
+ * @type {number}
124
+ */
125
+ size = 0;
126
+
127
+ /**
128
+ * Limit on number of allocations
129
+ * uint32
130
+ * @type {number}
131
+ */
132
+ maxAllocs = 0;
133
+
134
+ /**
135
+ * uint32
136
+ * @type {number}
137
+ */
138
+ freeStorage = 0;
139
+
140
+ /**
141
+ * uint32
142
+ * @type {number}
143
+ */
144
+ usedBinsTop = 0;
145
+
146
+ /**
147
+ *
148
+ * @type {Uint8Array}
149
+ */
150
+ usedBins = new Uint8Array(NUM_TOP_BINS);
151
+
152
+ /**
153
+ *
154
+ * @type {Uint32Array}
155
+ */
156
+ binIndices = new Uint32Array(NUM_LEAF_BINS);
157
+
158
+ /**
159
+ *
160
+ * @type {RowFirstTable}
161
+ */
162
+ nodes = new RowFirstTable(NODE_TABLE_SPEC)
163
+
164
+ /**
165
+ * @type {Uint32Array}
166
+ */
167
+ freeNodes;
168
+
169
+ /**
170
+ * uint32
171
+ * @type {number}
172
+ */
173
+ freeOffset = 0;
174
+
175
+ /**
176
+ *
177
+ * @param {number} size
178
+ * @param {number} maxAllocs
179
+ */
180
+ constructor(size, maxAllocs = 128 * 1024) {
181
+ assert.isNonNegativeInteger(size, 'size');
182
+ assert.isNonNegativeInteger(maxAllocs, 'maxAllocs');
183
+
184
+ this.size = size;
185
+ this.maxAllocs = maxAllocs;
186
+
187
+ this.freeNodes = new Uint32Array(this.maxAllocs);
188
+
189
+ this.reset();
190
+ }
191
+
192
+ /**
193
+ *
194
+ * @param {number} size
195
+ * @returns {Allocation}
196
+ */
197
+ allocate(size) {
198
+ assert.isNonNegativeInteger(size, 'size');
199
+
200
+ // Out of allocations?
201
+ if (this.freeOffset === 0) {
202
+ return Allocation.from(ALLOCATOR_NO_SPACE, ALLOCATOR_NO_SPACE);
203
+ }
204
+
205
+ // Round up to bin index to ensure that alloc >= bin
206
+ // Gives us min bin index that fits the size
207
+ let minBinIndex = uintToFloatRoundUp(size);
208
+
209
+ const minTopBinIndex = minBinIndex >>> TOP_BINS_INDEX_SHIFT;
210
+ const minLeafBinIndex = minBinIndex & LEAF_BINS_INDEX_MASK;
211
+
212
+ let topBinIndex = minTopBinIndex;
213
+ let leafBinIndex = ALLOCATOR_NO_SPACE;
214
+
215
+ // If top bin exists, scan its leaf bin. This can fail (NO_SPACE).
216
+ if ((this.usedBinsTop & (1 << topBinIndex)) !== 0) {
217
+ leafBinIndex = findLowestSetBitAfter(this.usedBins[topBinIndex], minLeafBinIndex);
218
+ }
219
+
220
+ // If we didn't find space in top bin, we search top bin from +1
221
+ if (leafBinIndex === ALLOCATOR_NO_SPACE) {
222
+ topBinIndex = findLowestSetBitAfter(this.usedBinsTop, minTopBinIndex + 1);
223
+
224
+ // Out of space?
225
+ if (topBinIndex === ALLOCATOR_NO_SPACE) {
226
+ return Allocation.from(ALLOCATOR_NO_SPACE, ALLOCATOR_NO_SPACE);
227
+ }
228
+
229
+ // All leaf bins here fit the alloc, since the top bin was rounded up. Start leaf search from bit 0.
230
+ // NOTE: This search can't fail since at least one leaf bit was set because the top bit was set.
231
+ leafBinIndex = ctz32(this.usedBins[topBinIndex]);
232
+ }
233
+
234
+ let bin_index = ((topBinIndex << TOP_BINS_INDEX_SHIFT) | leafBinIndex) >>> 0; // ">>> 0" shift is to force UINT32
235
+
236
+ // Pop the top node of the bin. Bin top = node.next.
237
+ let node_index = this.binIndices[bin_index];
238
+
239
+ const nodes = this.nodes;
240
+
241
+ const nodeTotalSize = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
242
+ nodes.writeCellValue(node_index, NODE_FIELD_DATA_SIZE, size);
243
+ nodes.writeCellValue(node_index, NODE_FIELD_FLAGS, NODE_FLAGS_USED); //mark as USED
244
+
245
+ const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
246
+ this.binIndices[bin_index] = node_bin_list_next;
247
+
248
+ if (node_bin_list_next !== NODE_UNUSED) {
249
+ nodes.writeCellValue(node_bin_list_next, NODE_FIELD_BIN_LIST_PREV, NODE_UNUSED);
250
+ }
251
+
252
+ this.freeStorage -= nodeTotalSize;
253
+
254
+ if (DEBUG_VERBOSE) {
255
+ console.log(`Free storage: ${this.freeStorage} (-${nodeTotalSize}) (allocate)`);
256
+ }
257
+
258
+ // Bin empty?
259
+ if (this.binIndices[bin_index] === NODE_UNUSED) {
260
+ // Remove a leaf bin mask bit
261
+ this.usedBins[topBinIndex] &= ~(1 << leafBinIndex);
262
+
263
+ // All leaf bins empty?
264
+ if (this.usedBins[topBinIndex] === 0) {
265
+ // Remove a top bin mask bit
266
+ this.usedBinsTop &= ~(1 << topBinIndex);
267
+ }
268
+ }
269
+
270
+ // Push back reminder N elements to a lower bin
271
+ const reminderSize = nodeTotalSize - size;
272
+
273
+ const node_data_offset = nodes.readCellValue(node_index, NODE_FIELD_DATA_OFFSET);
274
+
275
+ if (reminderSize > 0) {
276
+ const new_node_index = this.#insertNodeIntoBin(reminderSize, node_data_offset + size);
277
+
278
+ const node_neighbor_next = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
279
+
280
+ // Link nodes next to each other so that we can merge them later if both are free
281
+ // And update the old next neighbor to point to the new node (in middle)
282
+ if (node_neighbor_next !== NODE_UNUSED) {
283
+ nodes.writeCellValue(node_neighbor_next, NODE_FIELD_NEIGHBOR_PREV, new_node_index);
284
+ }
285
+ nodes.writeCellValue(new_node_index, NODE_FIELD_NEIGHBOR_PREV, node_index);
286
+ nodes.writeCellValue(new_node_index, NODE_FIELD_NEIGHBOR_NEXT, node_neighbor_next);
287
+
288
+ nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT, new_node_index);
289
+ }
290
+
291
+
292
+ return Allocation.from(node_data_offset, node_index);
293
+ }
294
+
295
+ /**
296
+ *
297
+ * @param {Allocation} allocation
298
+ */
299
+ free(allocation) {
300
+ assert.notEqual(allocation.metadata, ALLOCATOR_NO_SPACE, 'Invalid allocation');
301
+ // if (!m_nodes) return;
302
+
303
+ const node_index = allocation.metadata;
304
+
305
+ const nodes = this.nodes;
306
+ const node_flags = nodes.readCellValue(node_index, NODE_FIELD_FLAGS);
307
+
308
+ // Double delete check
309
+ assert.notEqual(node_flags & NODE_FLAGS_USED, 0, 'Node is not in use');
310
+
311
+ // Merge with neighbors...
312
+ let offset = nodes.readCellValue(node_index, NODE_FIELD_DATA_OFFSET)
313
+ let size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
314
+
315
+ const node_neighbour_prev = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV);
316
+
317
+ if (
318
+ (node_neighbour_prev !== NODE_UNUSED)
319
+ && ((nodes.readCellValue(node_neighbour_prev, NODE_FIELD_FLAGS) & NODE_FLAGS_USED) === 0) // not used
320
+ ) {
321
+ // Previous (contiguous) free node: Change offset to previous node offset. Sum sizes
322
+
323
+ const previous_offset = nodes.readCellValue(node_neighbour_prev, NODE_FIELD_DATA_OFFSET);
324
+ const previous_size = nodes.readCellValue(node_neighbour_prev, NODE_FIELD_DATA_SIZE);
325
+
326
+ offset = previous_offset;
327
+ size += previous_size;
328
+
329
+ // Remove node from the bin linked list and put it in the freelist
330
+ this.#removeNodeFromBin(node_neighbour_prev);
331
+
332
+ assert.equal(nodes.readCellValue(node_neighbour_prev, NODE_FIELD_NEIGHBOR_NEXT), node_index);
333
+
334
+ nodes.writeCellValue(
335
+ node_index, NODE_FIELD_NEIGHBOR_PREV,
336
+ nodes.readCellValue(node_neighbour_prev, NODE_FIELD_NEIGHBOR_PREV)
337
+ );
338
+ }
339
+
340
+ const node_neighbour_next = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
341
+
342
+ if (
343
+ (node_neighbour_next !== NODE_UNUSED)
344
+ && ((nodes.readCellValue(node_neighbour_next, NODE_FIELD_FLAGS) & NODE_FLAGS_USED) === 0) // not used
345
+ ) {
346
+ // Next (contiguous) free node: Offset remains the same. Sum sizes.
347
+
348
+ size += nodes.readCellValue(node_neighbour_next, NODE_FIELD_DATA_SIZE);
349
+
350
+ // Remove node from the bin linked list and put it in the freelist
351
+ this.#removeNodeFromBin(node_neighbour_next);
352
+
353
+ assert.equal(nodes.readCellValue(node_neighbour_next, NODE_FIELD_NEIGHBOR_PREV), node_index);
354
+
355
+ nodes.writeCellValue(
356
+ node_index, NODE_FIELD_NEIGHBOR_NEXT,
357
+ nodes.readCellValue(node_neighbour_next, NODE_FIELD_NEIGHBOR_NEXT)
358
+ );
359
+ }
360
+
361
+ const neighborNext = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
362
+ const neighborPrev = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV);
363
+
364
+ // Insert the removed node to freelist
365
+ if (DEBUG_VERBOSE) {
366
+ console.log(`Putting node ${node_index} into freelist[${this.freeOffset + 1}] (free)`);
367
+ }
368
+
369
+ this.freeNodes[++this.freeOffset] = node_index;
370
+
371
+ // Insert the (combined) free node to bin
372
+ const combinedNodeIndex = this.#insertNodeIntoBin(size, offset);
373
+
374
+ // Connect neighbors with the new combined node
375
+ if (neighborNext !== NODE_UNUSED) {
376
+ nodes.writeCellValue(combinedNodeIndex, NODE_FIELD_NEIGHBOR_NEXT, neighborNext);
377
+ nodes.writeCellValue(neighborNext, NODE_FIELD_NEIGHBOR_PREV, combinedNodeIndex);
378
+ }
379
+ if (neighborPrev !== NODE_UNUSED) {
380
+ nodes.writeCellValue(combinedNodeIndex, NODE_FIELD_NEIGHBOR_PREV, neighborPrev);
381
+ nodes.writeCellValue(neighborPrev, NODE_FIELD_NEIGHBOR_NEXT, combinedNodeIndex);
382
+ }
383
+ }
384
+
385
+ /**
386
+ *
387
+ * @param {number} size
388
+ * @param {number} data_offset
389
+ * @returns {number}
390
+ */
391
+ #insertNodeIntoBin(size, data_offset) {
392
+ assert.isNonNegativeInteger(size, 'size');
393
+ assert.isNonNegativeInteger(data_offset, 'size');
394
+
395
+ // Round down to bin index to ensure that bin >= alloc
396
+ const bin_index = uintToFloatRoundDown(size);
397
+
398
+ const top_bin_index = bin_index >>> TOP_BINS_INDEX_SHIFT;
399
+ const leafBinIndex = bin_index & LEAF_BINS_INDEX_MASK;
400
+
401
+ // Bin was empty before?
402
+ if (this.binIndices[bin_index] === NODE_UNUSED) {
403
+ // Set bin mask bits
404
+ this.usedBins[top_bin_index] |= 1 << leafBinIndex;
405
+ this.usedBinsTop |= 1 << top_bin_index;
406
+ }
407
+
408
+ // Take a freelist node and insert on top of the bin linked list (next = old top)
409
+ const top_node_index = this.binIndices[bin_index];
410
+ const node_index = this.freeNodes[this.freeOffset--];
411
+
412
+ if (DEBUG_VERBOSE) {
413
+ console.log(`Getting node ${node_index} from freelist[${this.freeOffset + 1}]`);
414
+ }
415
+
416
+ const nodes = this.nodes;
417
+
418
+ // initialize node
419
+
420
+ nodes.writeCellValue(node_index, NODE_FIELD_DATA_OFFSET, data_offset);
421
+ nodes.writeCellValue(node_index, NODE_FIELD_DATA_SIZE, size);
422
+
423
+ nodes.writeCellValue(node_index, NODE_FIELD_BIN_LIST_PREV, NODE_UNUSED);
424
+ nodes.writeCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT, top_node_index);
425
+
426
+ nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV, NODE_UNUSED);
427
+ nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT, NODE_UNUSED);
428
+
429
+ nodes.writeCellValue(node_index, NODE_FIELD_FLAGS, 0);
430
+
431
+ if (top_node_index !== NODE_UNUSED) {
432
+ nodes.writeCellValue(top_node_index, NODE_FIELD_BIN_LIST_PREV, node_index);
433
+ }
434
+
435
+ this.binIndices[bin_index] = node_index;
436
+
437
+ this.freeStorage += size;
438
+
439
+ if (DEBUG_VERBOSE) {
440
+ console.log(`Free storage: ${this.freeStorage} (+${size}) (insertNodeIntoBin)`);
441
+ }
442
+
443
+ return node_index;
444
+ }
445
+
446
+ /**
447
+ *
448
+ * @param {number} node_index
449
+ */
450
+ #removeNodeFromBin(node_index) {
451
+ assert.isNonNegativeInteger(node_index, 'node_index');
452
+
453
+ const nodes = this.nodes;
454
+ const node_bin_list_prev = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_PREV);
455
+ if (node_bin_list_prev !== NODE_UNUSED) {
456
+ // Easy case: We have previous node. Just remove this node from the middle of the list.
457
+ const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
458
+ nodes.writeCellValue(
459
+ node_bin_list_prev, NODE_FIELD_BIN_LIST_NEXT,
460
+ node_bin_list_next
461
+ );
462
+
463
+ if (node_bin_list_next !== NODE_UNUSED) {
464
+ nodes.writeCellValue(
465
+ node_bin_list_next, NODE_FIELD_BIN_LIST_PREV,
466
+ node_bin_list_prev
467
+ )
468
+ }
469
+ } else {
470
+ // Hard case: We are the first node in a bin. Find the bin.
471
+
472
+ // Round down to bin index to ensure that bin >= alloc
473
+ const node_data_size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
474
+ const binIndex = uintToFloatRoundDown(node_data_size);
475
+
476
+ const topBinIndex = binIndex >> TOP_BINS_INDEX_SHIFT;
477
+ const leafBinIndex = binIndex & LEAF_BINS_INDEX_MASK;
478
+
479
+ const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
480
+ this.binIndices[binIndex] = node_bin_list_next;
481
+ if (node_bin_list_next !== NODE_UNUSED) {
482
+ nodes.writeCellValue(
483
+ node_bin_list_next, NODE_FIELD_BIN_LIST_PREV,
484
+ NODE_UNUSED
485
+ );
486
+ }
487
+
488
+ // Bin empty?
489
+ if (this.binIndices[binIndex] === NODE_UNUSED) {
490
+ // Remove a leaf bin mask bit
491
+ this.usedBins[topBinIndex] &= ~(1 << leafBinIndex);
492
+
493
+ // All leaf bins empty?
494
+ if (this.usedBins[topBinIndex] === 0) {
495
+ // Remove a top bin mask bit
496
+ this.usedBinsTop &= ~(1 << topBinIndex);
497
+ }
498
+ }
499
+ }
500
+
501
+ // Insert the node to freelist
502
+ if (DEBUG_VERBOSE) {
503
+ console.log(`Putting node ${node_index} into freelist[${this.freeOffset + 1}] (removeNodeFromBin)`);
504
+ }
505
+
506
+ this.freeNodes[++this.freeOffset] = node_index;
507
+
508
+ const node_data_size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
509
+ this.freeStorage -= node_data_size;
510
+
511
+ if (DEBUG_VERBOSE) {
512
+ console.log(`Free storage: ${this.freeStorage} (-${node_data_size}) (removeNodeFromBin)`);
513
+ }
514
+ }
515
+
516
+ reset() {
517
+ this.freeStorage = 0;
518
+ this.usedBinsTop = 0;
519
+
520
+ const maxAllocs = this.maxAllocs;
521
+
522
+ this.freeOffset = maxAllocs - 1;
523
+
524
+ for (let i = 0; i < NUM_TOP_BINS; i++) {
525
+ this.usedBins[i] = 0;
526
+ }
527
+ for (let i = 0; i < NUM_LEAF_BINS; i++) {
528
+ this.binIndices[i] = NODE_UNUSED;
529
+ }
530
+
531
+ // drop data from the node table
532
+ this.nodes.length = 0;
533
+ this.nodes.setCapacity(maxAllocs);
534
+
535
+ // Freelist is a stack. Nodes in inverse order so that [0] pops first.
536
+ for (let i = 0; i < maxAllocs; i++) {
537
+ this.freeNodes[i] = maxAllocs - i - 1;
538
+ }
539
+
540
+ // Start state: Whole storage as one big node
541
+ // Algorithm will split remainders and push them back as smaller nodes
542
+ this.#insertNodeIntoBin(this.size, 0);
543
+
544
+ }
545
+
546
+ /**
547
+ * Useful for debug and UI
548
+ * @return {StorageReport}
549
+ */
550
+ storageReport() {
551
+ let largestFreeRegion = 0;
552
+ let freeStorage = 0;
553
+
554
+ // Out of allocations? -> Zero free space
555
+ if (this.freeOffset > 0) {
556
+ freeStorage = this.freeStorage;
557
+
558
+ if (this.usedBinsTop !== 0) {
559
+ const topBinIndex = msb_32(this.usedBinsTop);
560
+ const leafBinIndex = msb_32(this.usedBins[topBinIndex]);
561
+
562
+ largestFreeRegion = floatToUint((topBinIndex << TOP_BINS_INDEX_SHIFT) | leafBinIndex);
563
+
564
+ assert.greaterThanOrEqual(freeStorage, largestFreeRegion);
565
+ }
566
+
567
+ }
568
+
569
+ const report = new StorageReport();
570
+
571
+ report.largestFreeRegion = largestFreeRegion;
572
+ report.totalFreeSpace = freeStorage;
573
+
574
+ return report;
575
+ }
576
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bin sizes follow floating point (exponent + mantissa) distribution (piecewise linear log approx)
3
+ * This ensures that for each size class, the average overhead percentage stays the same
4
+ * @param {number} size
5
+ * @returns {number} uint32
6
+ */
7
+ export function uintToFloatRoundUp(size: number): number;
8
+ /**
9
+ *
10
+ * @param {number} size
11
+ * @return {number}
12
+ */
13
+ export function uintToFloatRoundDown(size: number): number;
14
+ /**
15
+ *
16
+ * @param {number} floatValue
17
+ * @return {number}
18
+ */
19
+ export function floatToUint(floatValue: number): number;
20
+ //# sourceMappingURL=SmallFloat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SmallFloat.d.ts","sourceRoot":"","sources":["../../../../../src/core/binary/float/SmallFloat.js"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,yCAHW,MAAM,GACJ,MAAM,CA0BlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACL,MAAM,CAoBjB;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACL,MAAM,CAcjB"}
@@ -0,0 +1,84 @@
1
+ import { assert } from "../../assert.js";
2
+ import { msb_32 } from "../msb_32.js";
3
+
4
+ // TODO this has some overlap with `frexp` function behavior, perhaps we can generalize
5
+
6
+ const MANTISSA_BITS = 3;
7
+ const MANTISSA_VALUE = 1 << MANTISSA_BITS;
8
+ const MANTISSA_MASK = MANTISSA_VALUE - 1;
9
+
10
+ /**
11
+ * Bin sizes follow floating point (exponent + mantissa) distribution (piecewise linear log approx)
12
+ * This ensures that for each size class, the average overhead percentage stays the same
13
+ * @param {number} size
14
+ * @returns {number} uint32
15
+ */
16
+ export function uintToFloatRoundUp(size) {
17
+ let exp = 0;
18
+ let mantissa = 0;
19
+
20
+ if (size < MANTISSA_VALUE) {
21
+ // Denorm: 0..(MANTISSA_VALUE-1)
22
+ mantissa = size;
23
+ } else {
24
+ // Normalized: Hidden high bit always 1. Not stored. Just like float.
25
+ const highestSetBit = msb_32(size);
26
+
27
+ const mantissaStartBit = highestSetBit - MANTISSA_BITS;
28
+ exp = mantissaStartBit + 1;
29
+ mantissa = (size >>> mantissaStartBit) & MANTISSA_MASK;
30
+
31
+ const lowBitsMask = (1 << mantissaStartBit) - 1;
32
+
33
+ // Round up!
34
+ if ((size & lowBitsMask) !== 0) {
35
+ mantissa++;
36
+ }
37
+ }
38
+
39
+ return (exp << MANTISSA_BITS) + mantissa; // + allows mantissa->exp overflow for round up
40
+ }
41
+
42
+ /**
43
+ *
44
+ * @param {number} size
45
+ * @return {number}
46
+ */
47
+ export function uintToFloatRoundDown(size) {
48
+ let exp = 0;
49
+ let mantissa = 0;
50
+
51
+ if (size < MANTISSA_VALUE) {
52
+ // Denorm: 0..(MANTISSA_VALUE-1)
53
+ mantissa = size;
54
+ } else {
55
+ // Normalized: Hidden high bit always 1. Not stored. Just like float.
56
+ const highestSetBit = msb_32(size);
57
+
58
+ const mantissaStartBit = highestSetBit - MANTISSA_BITS;
59
+
60
+ exp = mantissaStartBit + 1;
61
+ mantissa = (size >>> mantissaStartBit) & MANTISSA_MASK;
62
+ }
63
+
64
+ return (exp << MANTISSA_BITS) | mantissa;
65
+ }
66
+
67
+ /**
68
+ *
69
+ * @param {number} floatValue
70
+ * @return {number}
71
+ */
72
+ export function floatToUint(floatValue) {
73
+ assert.isNonNegativeInteger(floatValue, 'floatValue');
74
+
75
+ let exponent = floatValue >>> MANTISSA_BITS;
76
+ let mantissa = floatValue & MANTISSA_MASK;
77
+ if (exponent === 0) {
78
+ // Denorms
79
+ return mantissa;
80
+ } else {
81
+ const x = (mantissa | MANTISSA_VALUE) << (exponent - 1);
82
+ return x >>> 0; // convert to unsigned in case of sign overflow
83
+ }
84
+ }
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Compact binary table storage
3
- * Very efficient in terms of memory usage and allocation
4
- * You can think of it as an SQL table but only for numbers
2
+ * Compact binary table storage.
3
+ * Very efficient in terms of memory usage and allocation.
4
+ * Read and write speeds are optimized through code generation.
5
+ * You can think of it as an SQL table but only for numeric types.
6
+ *
7
+ * @copyright Company Named Limited (c) 2025
8
+ * @author Alex Goldring
5
9
  */
6
10
  export class RowFirstTable {
7
11
  /**
@@ -44,26 +48,8 @@ export class RowFirstTable {
44
48
  added: Signal;
45
49
  };
46
50
  /**
47
- * @deprecated access directly via spec instead
48
- * @return {BinaryDataType[]}
49
- */
50
- get types(): BinaryDataType[];
51
- /**
52
- * @deprecated access {@link RowFirstTableSpec#bytesPerRecord} directly instead
53
- * @return {number}
54
- */
55
- get bytesPerRecord(): number;
56
- /**
57
- * @deprecated use {@link RowFirstTable.spec.readRowMethod} instead
58
- * @return {function(DataView, number, number[]): void}
59
- */
60
- get readRowMethod(): (arg0: DataView, arg1: number, arg2: number[]) => void;
61
- /**
62
- @deprecated use {@link RowFirstTable.spec.writeRowMethod} instead
63
- * @return {function(DataView, number, number[]): void}
64
- */
65
- get writeRowMethod(): (arg0: DataView, arg1: number, arg2: number[]) => void;
66
- /**
51
+ * Useful for deserialization.
52
+ * This is an unsafe method, avoid using it if you are not sure.
67
53
  * NOTE: capacity is set automatically
68
54
  * NOTE: length is not set automatically, you have to do that yourself
69
55
  * @param {ArrayBuffer|SharedArrayBuffer} buffer
@@ -130,7 +116,7 @@ export class RowFirstTable {
130
116
  */
131
117
  addRow(values: Array<number>): number;
132
118
  /**
133
- *
119
+ * @deprecated Use {@link addRow} and {@link setCapacity} instead
134
120
  * @param {number} count number of rows to be added
135
121
  * @param {function(row_index:number, row:Array.<number>):*} valueSupplier supplier of row values, called with row index and an empty row to be filled
136
122
  */
@@ -141,17 +127,6 @@ export class RowFirstTable {
141
127
  * @param {number} target
142
128
  */
143
129
  copyRow(source: number, target: number): void;
144
- /**
145
- * Copy data from another table. Specs must match.
146
- * NOTE: does not send onAdded signal
147
- * @param {RowFirstTable} other
148
- */
149
- copy(other: RowFirstTable): void;
150
- /**
151
- * @param {RowFirstTable} other
152
- * @returns {boolean}
153
- */
154
- equals(other: RowFirstTable): boolean;
155
130
  /**
156
131
  * Read a single row of values from the table
157
132
  * @param {number} index
@@ -165,6 +140,14 @@ export class RowFirstTable {
165
140
  * @param {number[]} record
166
141
  */
167
142
  writeRow(index: number, record: number[]): void;
143
+ /**
144
+ * Sets memory region of the row to 0s.
145
+ * Useful for initializing dirty rows for re-use.
146
+ * If you have a specific value in mind - use {@link writeRow} method instead
147
+ * NOTE: All numeric types will produce a 0 after this, so every cell of this row will be 0
148
+ * @param {number} index
149
+ */
150
+ clearRow(index: number): void;
168
151
  /**
169
152
  * Reverse order of rows, row-0 will end up at and previously last row will become the first row etc.
170
153
  */
@@ -174,31 +157,27 @@ export class RowFirstTable {
174
157
  */
175
158
  clear(): void;
176
159
  /**
177
- * @deprecated access {@link RowFirstTableSpec#getColumnCount} from spec instead
178
- * @returns {number}
179
- */
180
- getNumColumns(): number;
181
- /**
182
- *
160
+ * Utility method. Returns table contents as a 2d array of [row][cell] form.
161
+ * Primarily useful for data conversion and debugging.
183
162
  * @returns {number[][]}
184
163
  */
185
164
  toRowArray(): number[][];
186
165
  /**
187
- * Print the table to console
166
+ * Print the table to console.
167
+ * Useful for debugging.
188
168
  */
189
169
  printToConsole(): void;
190
170
  /**
191
- * @deprecated
192
- */
193
- initialize(): void;
194
- /**
195
- * @deprecated use {@link RowFirstTable#readRow} instead
171
+ * Copy data from another table. Specs must match.
172
+ * NOTE: does not dispatch {@link onAdded} signal
173
+ * @param {RowFirstTable} other
196
174
  */
197
- getRow: (index: number, result?: number[]) => number[];
175
+ copy(other: RowFirstTable): void;
198
176
  /**
199
- @deprecated use {@link RowFirstTable#writeRow} instead
177
+ * @param {RowFirstTable} other
178
+ * @returns {boolean}
200
179
  */
201
- setRow: (index: number, record: number[]) => void;
180
+ equals(other: RowFirstTable): boolean;
202
181
  }
203
182
  import Signal from "../../events/signal/Signal.js";
204
183
  //# sourceMappingURL=RowFirstTable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RowFirstTable.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/table/RowFirstTable.js"],"names":[],"mappings":"AA+BA;;;;GAIG;AACH;IACI;;;;;OAKG;IACH,kBAJW,iBAAiB,iBACjB,OAAO,EAiDjB;IAzCG;;;OAGG;IACH,MAFU,iBAAiB,CAEX;IAEhB;;;OAGG;IACH,MAFU,WAAW,CAE4D;IAEjF;;;OAGG;IACH,QAFU,MAAM,CAED;IAEf;;;OAGG;IACH,UAFU,MAAM,CAEgB;IAEhC;;;OAGG;IACH,UAFU,QAAQ,CAEqB;IAEvC;;;OAGG;IACH,IAFU;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAOxB;IAIL;;;OAGG;IACH,aAFY,cAAc,EAAE,CAI3B;IAED;;;OAGG;IACH,sBAFY,MAAM,CAIjB;IAED;;;OAGG;IACH,qBAFY,CAAS,IAAQ,EAAR,QAAQ,EAAE,IAAM,EAAN,MAAM,EAAE,IAAQ,EAAR,MAAM,EAAE,KAAG,IAAI,CAIrD;IAED;;;OAGG;IACH,sBAFY,CAAS,IAAQ,EAAR,QAAQ,EAAE,IAAM,EAAN,MAAM,EAAE,IAAQ,EAAR,MAAM,EAAE,KAAG,IAAI,CAIrD;IAGD;;;;OAIG;IACH,yBAFW,WAAW,GAAC,iBAAiB,EAQvC;IAED;;;OAGG;IACH,QAFa,MAAM,CA+BlB;IAED;;;OAGG;IACH,sBAFW,MAAM,QAgDhB;IAED;;OAEG;IACH,aAEC;IAED;;;OAGG;IACH,kBAFW,MAAM,QAsBhB;IAED;;;;;OAKG;IACH,0BAJW,MAAM,gBACN,MAAM,SACN,MAAM,QAmBhB;IAED;;;;;OAKG;IACH,yBAJW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;IAED;;;;OAIG;IACH,kBAHW,MAAM,aACN,MAAM,QAyBhB;IAED;;;;;;OAMG;IACH,kBAHW,MAAM,aACN,MAAM,QAwBhB;IAED;;;;;OAKG;IACH,kBAFY,MAAM,CAYjB;IAED;;;;OAIG;IACH,eAHW,KAAK,CAAE,MAAM,CAAC,GACZ,MAAM,CAYlB;IAED;;;;OAIG;IACH,eAHW,MAAM,4BA8ChB;IAED;;;;OAIG;IACH,gBAHW,MAAM,UACN,MAAM,QAwBhB;IAED;;;;OAIG;IACH,YAFW,aAAa,QAmBvB;IAED;;;OAGG;IACH,cAHW,aAAa,GACX,OAAO,CAmCnB;IAED;;;;;OAKG;IACH,eAJW,MAAM,WACN,MAAM,EAAE,GACN,MAAM,EAAE,CAUpB;IAED;;;;OAIG;IACH,gBAHW,MAAM,UACN,MAAM,EAAE,QAUlB;IAED;;OAEG;IACH,qBA4BC;IAED;;OAEG;IACH,cAIC;IAED;;;OAGG;IACH,iBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,cAFa,MAAM,EAAE,EAAE,CAetB;IAED;;OAEG;IACH,uBAIC;IAED;;OAEG;IACH,mBAEC;IAGL;;OAEG;IACH,gBAtHe,MAAM,WACN,MAAM,EAAE,KACN,MAAM,EAAE,CAoHK;IAC9B;;OAEG;IACH,gBA1Ge,MAAM,UACN,MAAM,EAAE,UAyGO;CAT7B;mBAlpBkB,+BAA+B"}
1
+ {"version":3,"file":"RowFirstTable.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/table/RowFirstTable.js"],"names":[],"mappings":"AA+BA;;;;;;;;GAQG;AACH;IACI;;;;;OAKG;IACH,kBAJW,iBAAiB,iBACjB,OAAO,EAsDjB;IAzCG;;;OAGG;IACH,MAFU,iBAAiB,CAEX;IAEhB;;;OAGG;IACH,MAFU,WAAW,CAE4D;IAEjF;;;OAGG;IACH,QAFU,MAAM,CAED;IAEf;;;OAGG;IACH,UAFU,MAAM,CAEgB;IAEhC;;;OAGG;IACH,UAFU,QAAQ,CAEqB;IAEvC;;;OAGG;IACH,IAFU;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAOxB;IAKL;;;;;;OAMG;IACH,yBAFW,WAAW,GAAC,iBAAiB,EAQvC;IAED;;;OAGG;IACH,QAFa,MAAM,CA+BlB;IAED;;;OAGG;IACH,sBAFW,MAAM,QAgDhB;IAED;;OAEG;IACH,aAEC;IAED;;;OAGG;IACH,kBAFW,MAAM,QAsBhB;IAED;;;;;OAKG;IACH,0BAJW,MAAM,gBACN,MAAM,SACN,MAAM,QAmBhB;IAED;;;;;OAKG;IACH,yBAJW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;IAED;;;;OAIG;IACH,kBAHW,MAAM,aACN,MAAM,QAyBhB;IAED;;;;;;OAMG;IACH,kBAHW,MAAM,aACN,MAAM,QAwBhB;IAED;;;;;OAKG;IACH,kBAFY,MAAM,CAYjB;IAED;;;;OAIG;IACH,eAHW,KAAK,CAAE,MAAM,CAAC,GACZ,MAAM,CAYlB;IAED;;;;OAIG;IACH,eAHW,MAAM,4BA8ChB;IAED;;;;OAIG;IACH,gBAHW,MAAM,UACN,MAAM,QAwBhB;IAGD;;;;;OAKG;IACH,eAJW,MAAM,WACN,MAAM,EAAE,GACN,MAAM,EAAE,CAUpB;IAED;;;;OAIG;IACH,gBAHW,MAAM,UACN,MAAM,EAAE,QAUlB;IAED;;;;;;OAMG;IACH,gBAFW,MAAM,QAgBhB;IAED;;OAEG;IACH,qBA4BC;IAED;;OAEG;IACH,cAIC;IAED;;;;OAIG;IACH,cAFa,MAAM,EAAE,EAAE,CAetB;IAED;;;OAGG;IACH,uBAIC;IAED;;;;OAIG;IACH,YAFW,aAAa,QAmBvB;IAED;;;OAGG;IACH,cAHW,aAAa,GACX,OAAO,CAwCnB;CACJ;mBA7oBkB,+BAA+B"}
@@ -30,9 +30,13 @@ const ALLOCATION_GROW_MINIMUM_STEP = 32;
30
30
  const ALLOCATION_SHRINK_FACTOR = 0.5;
31
31
 
32
32
  /**
33
- * Compact binary table storage
34
- * Very efficient in terms of memory usage and allocation
35
- * You can think of it as an SQL table but only for numbers
33
+ * Compact binary table storage.
34
+ * Very efficient in terms of memory usage and allocation.
35
+ * Read and write speeds are optimized through code generation.
36
+ * You can think of it as an SQL table but only for numeric types.
37
+ *
38
+ * @copyright Company Named Limited (c) 2025
39
+ * @author Alex Goldring
36
40
  */
37
41
  export class RowFirstTable {
38
42
  /**
@@ -41,11 +45,16 @@ export class RowFirstTable {
41
45
  * @param {boolean} [shared_array] should we use SharedArrayBuffer instead of ArrayBuffer?
42
46
  * @constructor
43
47
  */
44
- constructor(spec, shared_array = false) {
48
+ constructor(
49
+ spec,
50
+ shared_array = false
51
+ ) {
45
52
  assert.defined(spec, 'spec');
46
53
  assert.notNull(spec, 'spec');
47
54
  assert.equal(spec.isRowFirstTableSpec, true, 'spec.isRowFirstTableSpec !== true');
48
55
 
56
+ assert.isBoolean(shared_array, 'shared_array');
57
+
49
58
  /**
50
59
  *
51
60
  * @type {RowFirstTableSpec}
@@ -89,40 +98,10 @@ export class RowFirstTable {
89
98
 
90
99
  }
91
100
 
92
- /**
93
- * @deprecated access directly via spec instead
94
- * @return {BinaryDataType[]}
95
- */
96
- get types() {
97
- return this.spec.types;
98
- }
99
-
100
- /**
101
- * @deprecated access {@link RowFirstTableSpec#bytesPerRecord} directly instead
102
- * @return {number}
103
- */
104
- get bytesPerRecord() {
105
- return this.spec.bytesPerRecord;
106
- }
107
-
108
- /**
109
- * @deprecated use {@link RowFirstTable.spec.readRowMethod} instead
110
- * @return {function(DataView, number, number[]): void}
111
- */
112
- get readRowMethod() {
113
- return this.spec.readRowMethod;
114
- }
115
-
116
- /**
117
- @deprecated use {@link RowFirstTable.spec.writeRowMethod} instead
118
- * @return {function(DataView, number, number[]): void}
119
- */
120
- get writeRowMethod() {
121
- return this.spec.writeRowMethod;
122
- }
123
-
124
101
 
125
102
  /**
103
+ * Useful for deserialization.
104
+ * This is an unsafe method, avoid using it if you are not sure.
126
105
  * NOTE: capacity is set automatically
127
106
  * NOTE: length is not set automatically, you have to do that yourself
128
107
  * @param {ArrayBuffer|SharedArrayBuffer} buffer
@@ -402,7 +381,7 @@ export class RowFirstTable {
402
381
  }
403
382
 
404
383
  /**
405
- *
384
+ * @deprecated Use {@link addRow} and {@link setCapacity} instead
406
385
  * @param {number} count number of rows to be added
407
386
  * @param {function(row_index:number, row:Array.<number>):*} valueSupplier supplier of row values, called with row index and an empty row to be filled
408
387
  */
@@ -480,68 +459,6 @@ export class RowFirstTable {
480
459
  }
481
460
  }
482
461
 
483
- /**
484
- * Copy data from another table. Specs must match.
485
- * NOTE: does not send onAdded signal
486
- * @param {RowFirstTable} other
487
- */
488
- copy(other) {
489
-
490
- // check that the spec is equivalent
491
- if (!this.spec.equals(other.spec)) {
492
- throw new Error('Different table specs');
493
- }
494
-
495
- const record_count = other.length;
496
-
497
- this.resize(record_count);
498
-
499
- this.length = other.length;
500
-
501
- // copy data
502
- const data_byte_count = other.spec.bytesPerRecord * record_count;
503
-
504
- array_buffer_copy(other.data, 0, this.data, 0, data_byte_count);
505
- }
506
-
507
- /**
508
- * @param {RowFirstTable} other
509
- * @returns {boolean}
510
- */
511
- equals(other) {
512
- if (other === this) {
513
- // shortcut
514
- return true;
515
- }
516
-
517
- if (other === undefined || other === null) {
518
- // this should not generally not happen
519
- return false;
520
- }
521
-
522
- if (!this.spec.equals(other.spec)) {
523
- return false;
524
- }
525
-
526
- const this_length = this.length;
527
-
528
- if (this_length !== other.length) {
529
- return false;
530
- }
531
-
532
- const total_bytes = this_length * this.spec.bytesPerRecord;
533
-
534
- for (let i = 0; i < total_bytes; i++) {
535
- const a = this.dataView.getUint8(i);
536
- const b = other.dataView.getUint8(i);
537
-
538
- if (a !== b) {
539
- return false;
540
- }
541
- }
542
-
543
- return true;
544
- }
545
462
 
546
463
  /**
547
464
  * Read a single row of values from the table
@@ -574,6 +491,29 @@ export class RowFirstTable {
574
491
  spec.writeRowMethod(this.dataView, spec.bytesPerRecord * index, record);
575
492
  }
576
493
 
494
+ /**
495
+ * Sets memory region of the row to 0s.
496
+ * Useful for initializing dirty rows for re-use.
497
+ * If you have a specific value in mind - use {@link writeRow} method instead
498
+ * NOTE: All numeric types will produce a 0 after this, so every cell of this row will be 0
499
+ * @param {number} index
500
+ */
501
+ clearRow(index) {
502
+ assert.isNonNegativeInteger(index, 'index');
503
+
504
+ const spec = this.spec;
505
+
506
+ const bytesPerRecord = spec.bytesPerRecord;
507
+
508
+ const address = index * bytesPerRecord;
509
+
510
+ const dataView = this.dataView;
511
+
512
+ for (let i = 0; i < bytesPerRecord; i++) {
513
+ dataView.setUint8(address + i, 0);
514
+ }
515
+ }
516
+
577
517
  /**
578
518
  * Reverse order of rows, row-0 will end up at and previously last row will become the first row etc.
579
519
  */
@@ -617,15 +557,8 @@ export class RowFirstTable {
617
557
  }
618
558
 
619
559
  /**
620
- * @deprecated access {@link RowFirstTableSpec#getColumnCount} from spec instead
621
- * @returns {number}
622
- */
623
- getNumColumns() {
624
- return this.spec.getColumnCount();
625
- }
626
-
627
- /**
628
- *
560
+ * Utility method. Returns table contents as a 2d array of [row][cell] form.
561
+ * Primarily useful for data conversion and debugging.
629
562
  * @returns {number[][]}
630
563
  */
631
564
  toRowArray() {
@@ -644,7 +577,8 @@ export class RowFirstTable {
644
577
  }
645
578
 
646
579
  /**
647
- * Print the table to console
580
+ * Print the table to console.
581
+ * Useful for debugging.
648
582
  */
649
583
  printToConsole() {
650
584
  const rows = this.toRowArray();
@@ -653,18 +587,70 @@ export class RowFirstTable {
653
587
  }
654
588
 
655
589
  /**
656
- * @deprecated
590
+ * Copy data from another table. Specs must match.
591
+ * NOTE: does not dispatch {@link onAdded} signal
592
+ * @param {RowFirstTable} other
657
593
  */
658
- initialize() {
659
- console.warn('deprecated, no longer serves any purpose. Table no longer requires initialization')
594
+ copy(other) {
595
+
596
+ // check that the spec is equivalent
597
+ if (!this.spec.equals(other.spec)) {
598
+ throw new Error('Different table specs');
599
+ }
600
+
601
+ const record_count = other.length;
602
+
603
+ this.resize(record_count);
604
+
605
+ this.length = other.length;
606
+
607
+ // copy data
608
+ const data_byte_count = other.spec.bytesPerRecord * record_count;
609
+
610
+ array_buffer_copy(other.data, 0, this.data, 0, data_byte_count);
660
611
  }
661
- }
662
612
 
663
- /**
664
- * @deprecated use {@link RowFirstTable#readRow} instead
665
- */
666
- RowFirstTable.prototype.getRow = RowFirstTable.prototype.readRow;
667
- /**
668
- @deprecated use {@link RowFirstTable#writeRow} instead
669
- */
670
- RowFirstTable.prototype.setRow = RowFirstTable.prototype.writeRow;
613
+ /**
614
+ * @param {RowFirstTable} other
615
+ * @returns {boolean}
616
+ */
617
+ equals(other) {
618
+ if (other === this) {
619
+ // shortcut
620
+ return true;
621
+ }
622
+
623
+ if (other === undefined || other === null) {
624
+ // this should not generally not happen
625
+ return false;
626
+ }
627
+
628
+ if (!this.spec.equals(other.spec)) {
629
+ return false;
630
+ }
631
+
632
+ const this_length = this.length;
633
+
634
+ if (this_length !== other.length) {
635
+ return false;
636
+ }
637
+
638
+ const total_bytes = this_length * this.spec.bytesPerRecord;
639
+
640
+ // TODO can use `is_array_buffer_equals` to speed this up
641
+
642
+ const this_data_view = this.dataView;
643
+ const other_data_view = other.dataView;
644
+
645
+ for (let i = 0; i < total_bytes; i++) {
646
+ const a = this_data_view.getUint8(i);
647
+ const b = other_data_view.getUint8(i);
648
+
649
+ if (a !== b) {
650
+ return false;
651
+ }
652
+ }
653
+
654
+ return true;
655
+ }
656
+ }