bun-shared-memory 0.1.5 → 0.1.7

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/src/memory.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { GrowableBuffer, SharedBudget } from "./growable-buffer.ts";
1
+ import {
2
+ GrowableBuffer,
3
+ SharedBudget,
4
+ nextPowerOfTwo,
5
+ } from "./growable-buffer.ts";
2
6
  import { Mutex } from "./mutex.ts";
3
7
 
4
8
  /**
@@ -30,10 +34,11 @@ import { Mutex } from "./mutex.ts";
30
34
  * [12..15] reserved
31
35
  * then propertyCount * (nameRef(8 bytes) + valueSlot(16 bytes))
32
36
  *
33
- * Index Table entry (12 bytes):
37
+ * Index Table entry (16 bytes):
34
38
  * [0..3] key string offset
35
39
  * [4..7] key string length
36
40
  * [8..11] data offset (into DataTable)
41
+ * [12..15] key version
37
42
  */
38
43
 
39
44
  export const TYPE_NUMBER = 0;
@@ -51,11 +56,19 @@ const INDEX_ENTRY_SIZE = 16;
51
56
  const INDEX_HEADER = 4;
52
57
  /** Byte offset of the version field within an index entry. */
53
58
  const INDEX_VERSION_OFFSET = 12;
59
+ const MAX_INDEX_TABLE_BYTES = 4 * 1024 * 1024 * 1024;
54
60
  /** Size of the objectId header at the start of each array block. */
55
61
  const ARRAY_HEADER = 4;
56
62
 
57
63
  const DEFAULT_INITIAL = 1024;
58
64
  const DEFAULT_MAX = 64 * 1024 * 1024; // 64 MiB
65
+ const DEFAULT_CHUNK_SIZE = 32 * 1024 * 1024; // 32 MiB per chunk
66
+ /** Minimum size of a single chunk (a chunk is the minimum growth step). */
67
+ const MIN_CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB
68
+ /** Minimum combined memory budget (maxTotalBytes / maxByteLength). */
69
+ const MIN_MAX_MEMORY = 32 * 1024 * 1024; // 32 MiB
70
+ /** Maximum number of chunks per table (chunkSize must be large enough). */
71
+ const MAX_CHUNKS_PER_TABLE = 1024;
59
72
  const DEFAULT_INTERN_CAPACITY = 256;
60
73
  const DEFAULT_INTERN_MAX = 1 << 16; // 65536 entries
61
74
 
@@ -172,22 +185,65 @@ export function decodeSlot(slot: ValueSlot): unknown {
172
185
 
173
186
  export interface SharedMemoryOptions {
174
187
  initialSize?: number;
188
+ /** Per-buffer maximum byte length (clamped to at least 32 MiB). */
175
189
  maxByteLength?: number;
176
- /** Total memory budget shared across all buffers (see below). */
190
+ /**
191
+ * Total physical memory budget shared by DataTable, StringTable, and
192
+ * ArrayTable (clamped to at least 32 MiB).
193
+ */
177
194
  maxTotalBytes?: number;
195
+ /** Internal worker reconstruction option; normally inferred from buffers. */
196
+ indexTableMaxByteLength?: number;
197
+ /**
198
+ * Size of each growable chunk (the minimum growth step). Default 32 MiB,
199
+ * clamped to at least 4 MiB and raised as needed so no table needs more
200
+ * than 1024 chunks.
201
+ */
202
+ chunkSize?: number;
178
203
  lock?: Mutex;
179
204
  internCapacity?: number;
180
205
  internMax?: number;
181
206
  buffers?: {
182
- indexTable?: SharedArrayBuffer;
183
- dataTable?: SharedArrayBuffer;
184
- stringTable?: SharedArrayBuffer;
185
- arrayTable?: SharedArrayBuffer;
207
+ indexTable?: SharedArrayBuffer | SharedArrayBuffer[];
208
+ dataTable?: SharedArrayBuffer | SharedArrayBuffer[];
209
+ stringTable?: SharedArrayBuffer | SharedArrayBuffer[];
210
+ arrayTable?: SharedArrayBuffer | SharedArrayBuffer[];
186
211
  layoutVersion?: SharedArrayBuffer;
187
212
  refCount?: SharedArrayBuffer;
213
+ budget?: SharedArrayBuffer;
188
214
  };
189
215
  }
190
216
 
217
+ /** Build a GrowableBuffer from an optional single SAB, chunk list, or nothing. */
218
+ function makeBuffer(
219
+ buf: SharedArrayBuffer | SharedArrayBuffer[] | undefined,
220
+ initial: number,
221
+ maxByteLength: number,
222
+ budget?: SharedBudget,
223
+ chunkSize?: number,
224
+ singleBuffer = false,
225
+ chargeInitial = true,
226
+ ): GrowableBuffer {
227
+ if (buf === undefined) {
228
+ return new GrowableBuffer(
229
+ initial,
230
+ maxByteLength,
231
+ budget,
232
+ chunkSize,
233
+ singleBuffer,
234
+ chargeInitial,
235
+ );
236
+ }
237
+ return new GrowableBuffer(
238
+ buf,
239
+ maxByteLength,
240
+ budget,
241
+ chunkSize,
242
+ singleBuffer,
243
+ chargeInitial,
244
+ );
245
+ }
246
+
191
247
  /**
192
248
  * Manages the four shared buffers and provides operations to store and read
193
249
  * strings, arrays, and objects.
@@ -197,6 +253,12 @@ export class SharedMemory {
197
253
  readonly dataTable: GrowableBuffer;
198
254
  readonly stringTable: GrowableBuffer;
199
255
  readonly arrayTable: GrowableBuffer;
256
+ /** Per-buffer maximum byte length (clamped to at least 32 MiB). */
257
+ readonly maxByteLength: number;
258
+ private readonly indexTableMaxByteLength: number;
259
+ /** Combined memory budget for Data/String/Array tables, or null if unset. */
260
+ readonly maxTotalBytes: number | null;
261
+ private readonly budgetBuffer: SharedArrayBuffer | null;
200
262
  /**
201
263
  * Cached Int32Array view over the Index Table, used to read/increment each
202
264
  * key's version cell (stored as the 4th field of each index entry). Rebuilt
@@ -246,44 +308,86 @@ export class SharedMemory {
246
308
 
247
309
  constructor(options?: SharedMemoryOptions) {
248
310
  const initial = options?.initialSize ?? DEFAULT_INITIAL;
249
- // A single SharedArrayBuffer can be at most 4 GiB. `maxTotalBytes` is a
250
- // *total* budget shared across all buffers: each buffer's own maxByteLength
251
- // is capped at min(4 GiB, maxTotalBytes), and the sum of all buffers'
252
- // physical sizes is additionally capped at maxTotalBytes. Without
253
- // maxTotalBytes, each buffer simply grows up to its own maxByteLength.
311
+ // `maxTotalBytes` caps the combined physical size of DataTable,
312
+ // StringTable, and ArrayTable. IndexTable has its independent 4 GiB cap.
313
+ // Without maxTotalBytes, each buffer grows up to its own maxByteLength.
314
+ // The Index Table remains a single growable SharedArrayBuffer and is
315
+ // capped at 4 GiB. The other tables use chunks and can exceed that limit.
254
316
  const maxTotal = options?.maxTotalBytes;
255
- const perBufferMax = Math.min(
256
- 4 * 1024 * 1024 * 1024,
317
+ if (
318
+ maxTotal !== undefined &&
319
+ (!Number.isSafeInteger(maxTotal) || maxTotal < 0)
320
+ ) {
321
+ throw new RangeError("maxTotalBytes must be a non-negative safe integer");
322
+ }
323
+ // Enforce the minimum memory budget: maxTotalBytes / maxByteLength are
324
+ // never below 32 MiB.
325
+ const perBufferMax = Math.max(
257
326
  maxTotal ?? options?.maxByteLength ?? DEFAULT_MAX,
327
+ MIN_MAX_MEMORY,
328
+ );
329
+ const indexTableMax = Math.min(
330
+ options?.indexTableMaxByteLength ?? options?.maxByteLength ?? DEFAULT_MAX,
331
+ MAX_INDEX_TABLE_BYTES,
332
+ );
333
+ // Enforce the chunk-size limits: at least 4 MiB, and large enough that no
334
+ // table needs more than MAX_CHUNKS_PER_TABLE chunks.
335
+ const userChunkSize = options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
336
+ const chunkSize = Math.max(
337
+ userChunkSize,
338
+ MIN_CHUNK_SIZE,
339
+ nextPowerOfTwo(perBufferMax / MAX_CHUNKS_PER_TABLE),
258
340
  );
259
- const budget =
260
- maxTotal !== undefined ? new SharedBudget(maxTotal) : undefined;
261
341
  const buffers = options?.buffers;
262
- this.indexTable = new GrowableBuffer(
263
- buffers?.indexTable ??
264
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
265
- perBufferMax,
266
- budget,
342
+ // The budget is the clamped maxTotalBytes (never below MIN_MAX_MEMORY), so
343
+ // it stays consistent with perBufferMax.
344
+ const budgetMax = maxTotal !== undefined ? perBufferMax : undefined;
345
+ const budgetBuffer = buffers?.budget;
346
+ const budget =
347
+ budgetMax !== undefined
348
+ ? new SharedBudget(budgetMax, budgetBuffer)
349
+ : undefined;
350
+ const chargeInitial = budgetBuffer === undefined;
351
+ this.maxByteLength = perBufferMax;
352
+ this.indexTableMaxByteLength = indexTableMax;
353
+ this.maxTotalBytes = budgetMax ?? null;
354
+ this.budgetBuffer = budget?.buffer ?? null;
355
+ this.indexTable = makeBuffer(
356
+ buffers?.indexTable,
357
+ initial,
358
+ indexTableMax,
359
+ undefined,
360
+ chunkSize,
361
+ true,
267
362
  );
268
- this.dataTable = new GrowableBuffer(
269
- buffers?.dataTable ??
270
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
363
+ this.dataTable = makeBuffer(
364
+ buffers?.dataTable,
365
+ initial,
271
366
  perBufferMax,
272
367
  budget,
368
+ chunkSize,
369
+ false,
370
+ chargeInitial,
273
371
  );
274
- this.stringTable = new GrowableBuffer(
275
- buffers?.stringTable ??
276
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
372
+ this.stringTable = makeBuffer(
373
+ buffers?.stringTable,
374
+ initial,
277
375
  perBufferMax,
278
376
  budget,
377
+ chunkSize,
378
+ false,
379
+ chargeInitial,
279
380
  );
280
- this.arrayTable = new GrowableBuffer(
281
- buffers?.arrayTable ??
282
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
381
+ this.arrayTable = makeBuffer(
382
+ buffers?.arrayTable,
383
+ initial,
283
384
  perBufferMax,
284
385
  budget,
386
+ chunkSize,
387
+ false,
388
+ chargeInitial,
285
389
  );
286
- this.versionView = new Int32Array(this.indexTable.buffer);
390
+ this.versionView = new Int32Array(this.indexTable.buffers[0]!);
287
391
  this.layoutVersion = new Int32Array(
288
392
  buffers?.layoutVersion ??
289
393
  new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
@@ -299,6 +403,24 @@ export class SharedMemory {
299
403
  return this.lock.withLock(fn);
300
404
  }
301
405
 
406
+ /**
407
+ * Apply a buffer update received from the main thread after a
408
+ * defragmentation pass. Replaces each table's buffers and rebuilds the
409
+ * version view. Called by workers on the `__chunkUpdate` message.
410
+ */
411
+ applyChunkUpdate(buffers: {
412
+ indexTable: SharedArrayBuffer[];
413
+ dataTable: SharedArrayBuffer[];
414
+ stringTable: SharedArrayBuffer[];
415
+ arrayTable: SharedArrayBuffer[];
416
+ }): void {
417
+ this.indexTable.applyChunks(buffers.indexTable);
418
+ this.dataTable.applyChunks(buffers.dataTable);
419
+ this.stringTable.applyChunks(buffers.stringTable);
420
+ this.arrayTable.applyChunks(buffers.arrayTable);
421
+ this.versionView = new Int32Array(this.indexTable.buffers[0]!);
422
+ }
423
+
302
424
  /**
303
425
  * Attach the reference-counting buffer (5th SAB) to this memory so it is
304
426
  * included in `exportBuffers()`. No-op if a buffer is already attached.
@@ -313,24 +435,37 @@ export class SharedMemory {
313
435
  /**
314
436
  * Export all shared buffers (including the lock) so they can be sent to a
315
437
  * worker via `postMessage` and reconstructed there with `fromBuffers()`.
438
+ *
439
+ * Tables are exported as lists for the chunked buffers. The Index Table is
440
+ * a single growable SharedArrayBuffer, exported as a one-element list. The
441
+ * optional shared budget counter is included so workers reserve atomically
442
+ * against the same three-table limit.
316
443
  */
317
444
  exportBuffers(): {
318
- indexTable: SharedArrayBuffer;
319
- dataTable: SharedArrayBuffer;
320
- stringTable: SharedArrayBuffer;
321
- arrayTable: SharedArrayBuffer;
445
+ indexTable: SharedArrayBuffer[];
446
+ dataTable: SharedArrayBuffer[];
447
+ stringTable: SharedArrayBuffer[];
448
+ arrayTable: SharedArrayBuffer[];
322
449
  layoutVersion: SharedArrayBuffer;
323
450
  lock: SharedArrayBuffer;
324
451
  refCount: SharedArrayBuffer | null;
452
+ maxByteLength: number;
453
+ indexTableMaxByteLength: number;
454
+ maxTotalBytes: number | null;
455
+ budget: SharedArrayBuffer | null;
325
456
  } {
326
457
  return {
327
- indexTable: this.indexTable.buffer,
328
- dataTable: this.dataTable.buffer,
329
- stringTable: this.stringTable.buffer,
330
- arrayTable: this.arrayTable.buffer,
458
+ indexTable: this.indexTable.buffers,
459
+ dataTable: this.dataTable.buffers,
460
+ stringTable: this.stringTable.buffers,
461
+ arrayTable: this.arrayTable.buffers,
331
462
  layoutVersion: this.layoutVersion.buffer as SharedArrayBuffer,
332
463
  lock: this.lock.buffer as SharedArrayBuffer,
333
464
  refCount: this.refCountBuffer,
465
+ maxByteLength: this.maxByteLength,
466
+ indexTableMaxByteLength: this.indexTableMaxByteLength,
467
+ maxTotalBytes: this.maxTotalBytes,
468
+ budget: this.budgetBuffer,
334
469
  };
335
470
  }
336
471
 
@@ -339,15 +474,29 @@ export class SharedMemory {
339
474
  * `exportBuffers()` (e.g. received from the main thread in a worker).
340
475
  */
341
476
  static fromBuffers(buffers: {
342
- indexTable: SharedArrayBuffer;
343
- dataTable: SharedArrayBuffer;
344
- stringTable: SharedArrayBuffer;
345
- arrayTable: SharedArrayBuffer;
477
+ indexTable: SharedArrayBuffer | SharedArrayBuffer[];
478
+ dataTable: SharedArrayBuffer | SharedArrayBuffer[];
479
+ stringTable: SharedArrayBuffer | SharedArrayBuffer[];
480
+ arrayTable: SharedArrayBuffer | SharedArrayBuffer[];
346
481
  layoutVersion: SharedArrayBuffer;
347
482
  lock: SharedArrayBuffer;
348
483
  refCount?: SharedArrayBuffer | null;
484
+ maxByteLength?: number;
485
+ indexTableMaxByteLength?: number;
486
+ maxTotalBytes?: number | null;
487
+ budget?: SharedArrayBuffer | null;
349
488
  }): SharedMemory {
489
+ const indexBuffer = Array.isArray(buffers.indexTable)
490
+ ? buffers.indexTable[0]
491
+ : buffers.indexTable;
350
492
  return new SharedMemory({
493
+ maxByteLength: buffers.maxByteLength ?? DEFAULT_MAX,
494
+ indexTableMaxByteLength:
495
+ buffers.indexTableMaxByteLength ??
496
+ indexBuffer?.maxByteLength ??
497
+ indexBuffer?.byteLength ??
498
+ DEFAULT_MAX,
499
+ maxTotalBytes: buffers.maxTotalBytes ?? undefined,
351
500
  buffers: {
352
501
  indexTable: buffers.indexTable,
353
502
  dataTable: buffers.dataTable,
@@ -355,6 +504,7 @@ export class SharedMemory {
355
504
  arrayTable: buffers.arrayTable,
356
505
  layoutVersion: buffers.layoutVersion,
357
506
  refCount: buffers.refCount ?? undefined,
507
+ budget: buffers.budget ?? undefined,
358
508
  },
359
509
  lock: new Mutex(buffers.lock),
360
510
  });
@@ -393,10 +543,7 @@ export class SharedMemory {
393
543
  const bytes = this.encoder.encode(value);
394
544
  const offset = this.stringTable.byteLength;
395
545
  this.stringTable.ensureCapacity(offset + bytes.byteLength);
396
- const view = this.stringTable.getView();
397
- for (let i = 0; i < bytes.byteLength; i++) {
398
- view.setUint8(offset + i, bytes[i]!);
399
- }
546
+ this.stringTable.writeBytes(offset, bytes);
400
547
  this.stringTable.advance(bytes.byteLength);
401
548
  const ref: StringRef = { offset, length: bytes.byteLength };
402
549
 
@@ -431,12 +578,7 @@ export class SharedMemory {
431
578
 
432
579
  /** Read a string from the StringTable by reference. */
433
580
  readString(ref: StringRef): string {
434
- const view = this.stringTable.getView();
435
- const bytes = new Uint8Array(
436
- this.stringTable.buffer,
437
- ref.offset,
438
- ref.length,
439
- );
581
+ const bytes = this.stringTable.readBytes(ref.offset, ref.length);
440
582
  return this.decoder.decode(bytes);
441
583
  }
442
584
 
@@ -458,11 +600,14 @@ export class SharedMemory {
458
600
  const cap = Math.max(capacity ?? 0, count, 1);
459
601
  const offset = this.arrayTable.byteLength;
460
602
  this.arrayTable.ensureCapacity(offset + ARRAY_HEADER + cap * SLOT_SIZE);
461
- const view = this.arrayTable.getView();
462
603
  const objectId = this.nextObjectId++;
463
- view.setInt32(offset, objectId, true);
604
+ this.arrayTable.setInt32(offset, objectId);
464
605
  for (let i = 0; i < count; i++) {
465
- writeSlot(view, offset + ARRAY_HEADER + i * SLOT_SIZE, slots[i]!);
606
+ writeSlot(
607
+ this.arrayTable,
608
+ offset + ARRAY_HEADER + i * SLOT_SIZE,
609
+ slots[i]!,
610
+ );
466
611
  }
467
612
  this.arrayTable.advance(ARRAY_HEADER + cap * SLOT_SIZE);
468
613
  return { offset, count, capacity: cap };
@@ -470,10 +615,11 @@ export class SharedMemory {
470
615
 
471
616
  /** Read an array of value slots from the ArrayTable by reference. */
472
617
  readArray(ref: ArrayRef): ValueSlot[] {
473
- const view = this.arrayTable.getView();
474
618
  const slots: ValueSlot[] = [];
475
619
  for (let i = 0; i < ref.count; i++) {
476
- slots.push(readSlot(view, ref.offset + ARRAY_HEADER + i * SLOT_SIZE));
620
+ slots.push(
621
+ readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
622
+ );
477
623
  }
478
624
  return slots;
479
625
  }
@@ -481,7 +627,7 @@ export class SharedMemory {
481
627
  /** Update a single value slot of an array in the ArrayTable in place. */
482
628
  updateArraySlot(ref: ArrayRef, index: number, slot: ValueSlot): void {
483
629
  writeSlot(
484
- this.arrayTable.getView(),
630
+ this.arrayTable,
485
631
  ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
486
632
  slot,
487
633
  );
@@ -490,14 +636,14 @@ export class SharedMemory {
490
636
  /** Read a single value slot of an array from the ArrayTable. */
491
637
  readArraySlot(ref: ArrayRef, index: number): ValueSlot {
492
638
  return readSlot(
493
- this.arrayTable.getView(),
639
+ this.arrayTable,
494
640
  ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
495
641
  );
496
642
  }
497
643
 
498
644
  /** Read the stable objectId of an array block in the ArrayTable. */
499
645
  getArrayObjectId(offset: number): number {
500
- return this.arrayTable.getView().getInt32(offset, true);
646
+ return this.arrayTable.getInt32(offset);
501
647
  }
502
648
 
503
649
  /**
@@ -510,14 +656,13 @@ export class SharedMemory {
510
656
  this.arrayTable.ensureCapacity(
511
657
  newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
512
658
  );
513
- const view = this.arrayTable.getView();
514
659
  const objectId = this.getArrayObjectId(ref.offset);
515
- view.setInt32(newOffset, objectId, true);
660
+ this.arrayTable.setInt32(newOffset, objectId);
516
661
  for (let i = 0; i < ref.count; i++) {
517
662
  writeSlot(
518
- view,
663
+ this.arrayTable,
519
664
  newOffset + ARRAY_HEADER + i * SLOT_SIZE,
520
- readSlot(view, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
665
+ readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
521
666
  );
522
667
  }
523
668
  this.arrayTable.advance(ARRAY_HEADER + newCapacity * SLOT_SIZE);
@@ -533,7 +678,7 @@ export class SharedMemory {
533
678
  appendArraySlot(ref: ArrayRef, slot: ValueSlot): ArrayRef {
534
679
  if (ref.count < ref.capacity) {
535
680
  writeSlot(
536
- this.arrayTable.getView(),
681
+ this.arrayTable,
537
682
  ref.offset + ARRAY_HEADER + ref.count * SLOT_SIZE,
538
683
  slot,
539
684
  );
@@ -545,7 +690,7 @@ export class SharedMemory {
545
690
  }
546
691
  const moved = this.moveArray(ref, Math.max(ref.capacity * 2, 1));
547
692
  writeSlot(
548
- this.arrayTable.getView(),
693
+ this.arrayTable,
549
694
  moved.offset + ARRAY_HEADER + moved.count * SLOT_SIZE,
550
695
  slot,
551
696
  );
@@ -564,19 +709,19 @@ export class SharedMemory {
564
709
  storeValueSlot(slot: ValueSlot): number {
565
710
  const offset = this.dataTable.byteLength;
566
711
  this.dataTable.ensureCapacity(offset + SLOT_SIZE);
567
- writeSlot(this.dataTable.getView(), offset, slot);
712
+ writeSlot(this.dataTable, offset, slot);
568
713
  this.dataTable.advance(SLOT_SIZE);
569
714
  return offset;
570
715
  }
571
716
 
572
717
  /** Read a single value slot from the DataTable. */
573
718
  readValueSlot(offset: number): ValueSlot {
574
- return readSlot(this.dataTable.getView(), offset);
719
+ return readSlot(this.dataTable, offset);
575
720
  }
576
721
 
577
722
  /** Update a single value slot in the DataTable. */
578
723
  updateValueSlot(offset: number, slot: ValueSlot): void {
579
- writeSlot(this.dataTable.getView(), offset, slot);
724
+ writeSlot(this.dataTable, offset, slot);
580
725
  }
581
726
 
582
727
  /**
@@ -590,20 +735,19 @@ export class SharedMemory {
590
735
  OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
591
736
  const offset = this.dataTable.byteLength;
592
737
  this.dataTable.ensureCapacity(offset + blockSize);
593
- const view = this.dataTable.getView();
594
738
 
595
739
  const objectId = this.nextObjectId++;
596
- view.setInt32(offset, TYPE_OBJECT, true);
597
- view.setInt32(offset + 4, properties.length, true);
598
- view.setInt32(offset + 8, objectId, true);
740
+ this.dataTable.setInt32(offset, TYPE_OBJECT);
741
+ this.dataTable.setInt32(offset + 4, properties.length);
742
+ this.dataTable.setInt32(offset + 8, objectId);
599
743
 
600
744
  let cursor = offset + OBJECT_HEADER;
601
745
  for (const [name, slot] of properties) {
602
746
  const nameRef = this.storeString(name);
603
- view.setInt32(cursor, nameRef.offset, true);
604
- view.setInt32(cursor + 4, nameRef.length, true);
747
+ this.dataTable.setInt32(cursor, nameRef.offset);
748
+ this.dataTable.setInt32(cursor + 4, nameRef.length);
605
749
  cursor += NAME_REF_SIZE;
606
- writeSlot(view, cursor, slot);
750
+ writeSlot(this.dataTable, cursor, slot);
607
751
  cursor += SLOT_SIZE;
608
752
  }
609
753
  this.dataTable.advance(blockSize);
@@ -612,16 +756,15 @@ export class SharedMemory {
612
756
 
613
757
  /** Read an object's properties from the DataTable. */
614
758
  readObject(offset: number): Array<[string, ValueSlot]> {
615
- const view = this.dataTable.getView();
616
- const count = view.getInt32(offset + 4, true);
759
+ const count = this.dataTable.getInt32(offset + 4);
617
760
  const properties: Array<[string, ValueSlot]> = [];
618
761
  let cursor = offset + OBJECT_HEADER;
619
762
  for (let i = 0; i < count; i++) {
620
- const nameOffset = view.getInt32(cursor, true);
621
- const nameLength = view.getInt32(cursor + 4, true);
763
+ const nameOffset = this.dataTable.getInt32(cursor);
764
+ const nameLength = this.dataTable.getInt32(cursor + 4);
622
765
  const name = this.readString({ offset: nameOffset, length: nameLength });
623
766
  cursor += NAME_REF_SIZE;
624
- const slot = readSlot(view, cursor);
767
+ const slot = readSlot(this.dataTable, cursor);
625
768
  cursor += SLOT_SIZE;
626
769
  properties.push([name, slot]);
627
770
  }
@@ -630,7 +773,7 @@ export class SharedMemory {
630
773
 
631
774
  /** Read the stable objectId of an object block in the DataTable. */
632
775
  getObjectId(offset: number): number {
633
- return this.dataTable.getView().getInt32(offset + 8, true);
776
+ return this.dataTable.getInt32(offset + 8);
634
777
  }
635
778
 
636
779
  /**
@@ -647,14 +790,13 @@ export class SharedMemory {
647
790
  getObjectLayout(key: string): ObjectLayout | null {
648
791
  const offset = this.lookup(key);
649
792
  if (offset === null) return null;
650
- const view = this.dataTable.getView();
651
- if (view.getInt32(offset, true) !== TYPE_OBJECT) return null;
652
- const count = view.getInt32(offset + 4, true);
793
+ if (this.dataTable.getInt32(offset) !== TYPE_OBJECT) return null;
794
+ const count = this.dataTable.getInt32(offset + 4);
653
795
  const properties = new Map<string, number>();
654
796
  let cursor = offset + OBJECT_HEADER;
655
797
  for (let i = 0; i < count; i++) {
656
- const nameOffset = view.getInt32(cursor, true);
657
- const nameLength = view.getInt32(cursor + 4, true);
798
+ const nameOffset = this.dataTable.getInt32(cursor);
799
+ const nameLength = this.dataTable.getInt32(cursor + 4);
658
800
  const name = this.readString({ offset: nameOffset, length: nameLength });
659
801
  cursor += NAME_REF_SIZE;
660
802
  properties.set(name, cursor);
@@ -669,13 +811,12 @@ export class SharedMemory {
669
811
  propertyIndex: number,
670
812
  slot: ValueSlot,
671
813
  ): void {
672
- const view = this.dataTable.getView();
673
814
  const slotOffset =
674
815
  offset +
675
816
  OBJECT_HEADER +
676
817
  propertyIndex * (NAME_REF_SIZE + SLOT_SIZE) +
677
818
  NAME_REF_SIZE;
678
- writeSlot(view, slotOffset, slot);
819
+ writeSlot(this.dataTable, slotOffset, slot);
679
820
  }
680
821
 
681
822
  // ---------------------------------------------------------------------------
@@ -691,11 +832,10 @@ export class SharedMemory {
691
832
 
692
833
  /** Read the DataTable offset for a known stable index, or null if absent. */
693
834
  lookupByIndex(index: number): number | null {
694
- const view = this.indexTable.getView();
695
- const count = view.getInt32(0, true);
835
+ const count = this.indexTable.getInt32(0);
696
836
  if (index >= count) return null;
697
837
  const entryOffset = INDEX_HEADER + index * INDEX_ENTRY_SIZE;
698
- return view.getInt32(entryOffset + 8, true);
838
+ return this.indexTable.getInt32(entryOffset + 8);
699
839
  }
700
840
 
701
841
  /**
@@ -706,12 +846,11 @@ export class SharedMemory {
706
846
  * used to address the key's version cell.
707
847
  */
708
848
  lookupIndex(key: string): number | null {
709
- const view = this.indexTable.getView();
710
- const count = view.getInt32(0, true);
849
+ const count = this.indexTable.getInt32(0);
711
850
  for (let i = 0; i < count; i++) {
712
851
  const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
713
- const keyOffset = view.getInt32(entryOffset, true);
714
- const keyLength = view.getInt32(entryOffset + 4, true);
852
+ const keyOffset = this.indexTable.getInt32(entryOffset);
853
+ const keyLength = this.indexTable.getInt32(entryOffset + 4);
715
854
  const storedKey = this.readString({
716
855
  offset: keyOffset,
717
856
  length: keyLength,
@@ -736,21 +875,21 @@ export class SharedMemory {
736
875
  /** Insert a key → dataOffset mapping into the Index Table. */
737
876
  insert(key: string, dataOffset: number): void {
738
877
  const keyRef = this.storeString(key);
739
- const view = this.indexTable.getView();
740
- const count = view.getInt32(0, true);
878
+ const count = this.indexTable.getInt32(0);
741
879
  const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
742
880
  this.indexTable.ensureCapacity(entryOffset + INDEX_ENTRY_SIZE);
743
- view.setInt32(entryOffset, keyRef.offset, true);
744
- view.setInt32(entryOffset + 4, keyRef.length, true);
745
- view.setInt32(entryOffset + 8, dataOffset, true);
881
+ this.indexTable.setInt32(entryOffset, keyRef.offset);
882
+ this.indexTable.setInt32(entryOffset + 4, keyRef.length);
883
+ this.indexTable.setInt32(entryOffset + 8, dataOffset);
746
884
  // The 4th field is the key's version cell, initialized to 0.
747
- view.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0, true);
748
- view.setInt32(0, count + 1, true);
885
+ this.indexTable.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0);
886
+ this.indexTable.setInt32(0, count + 1);
749
887
  this.indexTable.advance(INDEX_ENTRY_SIZE);
750
888
 
751
- // Rebuild the version view if the buffer grew (grow preserves the SAB in
752
- // place, but the Int32Array view must be recreated over the new buffer).
753
- this.versionView = new Int32Array(this.indexTable.buffer);
889
+ // Rebuild the version view over the first chunk (the Index Table is small
890
+ // and stays within the first chunk for typical workloads; version cells
891
+ // beyond the first chunk are not addressable via the single Int32Array).
892
+ this.versionView = new Int32Array(this.indexTable.buffers[0]!);
754
893
  }
755
894
 
756
895
  // ---------------------------------------------------------------------------
@@ -786,7 +925,12 @@ export class SharedMemory {
786
925
  *
787
926
  * Must be called under the shared lock (or on a single thread).
788
927
  */
789
- defragment(): void {
928
+ defragment(): {
929
+ indexTable: SharedArrayBuffer[];
930
+ dataTable: SharedArrayBuffer[];
931
+ stringTable: SharedArrayBuffer[];
932
+ arrayTable: SharedArrayBuffer[];
933
+ } {
790
934
  const stringRemap = new Map<number, number>();
791
935
  const arrayRemap = new Map<number, number>();
792
936
  const objectRemap = new Map<number, number>();
@@ -812,9 +956,10 @@ export class SharedMemory {
812
956
  const markArray = (offset: number, count: number) => {
813
957
  if (liveArrays.has(offset)) return;
814
958
  liveArrays.add(offset);
815
- const view = this.arrayTable.getView();
816
959
  for (let i = 0; i < count; i++) {
817
- markValueSlot(readSlot(view, offset + ARRAY_HEADER + i * SLOT_SIZE));
960
+ markValueSlot(
961
+ readSlot(this.arrayTable, offset + ARRAY_HEADER + i * SLOT_SIZE),
962
+ );
818
963
  }
819
964
  };
820
965
 
@@ -822,40 +967,37 @@ export class SharedMemory {
822
967
  if (liveObjects.has(offset)) return;
823
968
  liveObjects.add(offset);
824
969
  liveDataOffsets.add(offset);
825
- const view = this.dataTable.getView();
826
- const count = view.getInt32(offset + 4, true);
970
+ const count = this.dataTable.getInt32(offset + 4);
827
971
  let cursor = offset + OBJECT_HEADER;
828
972
  for (let i = 0; i < count; i++) {
829
- const nameOffset = view.getInt32(cursor, true);
830
- const nameLength = view.getInt32(cursor + 4, true);
973
+ const nameOffset = this.dataTable.getInt32(cursor);
974
+ const nameLength = this.dataTable.getInt32(cursor + 4);
831
975
  liveStrings.add(nameOffset);
832
976
  cursor += NAME_REF_SIZE;
833
- markValueSlot(readSlot(view, cursor));
977
+ markValueSlot(readSlot(this.dataTable, cursor));
834
978
  cursor += SLOT_SIZE;
835
979
  }
836
980
  };
837
981
 
838
982
  const markValue = (dataOffset: number) => {
839
- const view = this.dataTable.getView();
840
- if (view.getInt32(dataOffset, true) === TYPE_OBJECT) {
983
+ if (this.dataTable.getInt32(dataOffset) === TYPE_OBJECT) {
841
984
  markObject(dataOffset);
842
985
  } else {
843
986
  liveDataOffsets.add(dataOffset);
844
- markValueSlot(readSlot(view, dataOffset));
987
+ markValueSlot(readSlot(this.dataTable, dataOffset));
845
988
  }
846
989
  };
847
990
 
848
991
  // Phase 1: Mark live data by walking the Index Table.
849
- const indexView = this.indexTable.getView();
850
- const indexCount = indexView.getInt32(0, true);
992
+ const indexCount = this.indexTable.getInt32(0);
851
993
  const indexEntries: Array<{ keyRef: StringRef; dataOffset: number }> = [];
852
994
  for (let i = 0; i < indexCount; i++) {
853
995
  const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
854
996
  const keyRef = {
855
- offset: indexView.getInt32(entryOffset, true),
856
- length: indexView.getInt32(entryOffset + 4, true),
997
+ offset: this.indexTable.getInt32(entryOffset),
998
+ length: this.indexTable.getInt32(entryOffset + 4),
857
999
  };
858
- const dataOffset = indexView.getInt32(entryOffset + 8, true);
1000
+ const dataOffset = this.indexTable.getInt32(entryOffset + 8);
859
1001
  indexEntries.push({ keyRef, dataOffset });
860
1002
  liveStrings.add(keyRef.offset);
861
1003
  markValue(dataOffset);
@@ -864,14 +1006,13 @@ export class SharedMemory {
864
1006
  // Phase 2: Compact the DataTable (objects and single value slots).
865
1007
  const sortedData = [...liveDataOffsets].sort((a, b) => a - b);
866
1008
  let newDataOffset = 0;
867
- const dataView = this.dataTable.getView();
868
1009
  for (const oldOffset of sortedData) {
869
1010
  const size =
870
- dataView.getInt32(oldOffset, true) === TYPE_OBJECT
1011
+ this.dataTable.getInt32(oldOffset) === TYPE_OBJECT
871
1012
  ? OBJECT_HEADER +
872
- dataView.getInt32(oldOffset + 4, true) * (NAME_REF_SIZE + SLOT_SIZE)
1013
+ this.dataTable.getInt32(oldOffset + 4) * (NAME_REF_SIZE + SLOT_SIZE)
873
1014
  : SLOT_SIZE;
874
- copyBytes(dataView, newDataOffset, oldOffset, size);
1015
+ copyBytes(this.dataTable, newDataOffset, oldOffset, size);
875
1016
  objectRemap.set(oldOffset, newDataOffset);
876
1017
  newDataOffset += size;
877
1018
  }
@@ -880,7 +1021,6 @@ export class SharedMemory {
880
1021
  // Phase 3: Compact the ArrayTable.
881
1022
  const sortedArrays = [...liveArrays].sort((a, b) => a - b);
882
1023
  let newArrayOffset = 0;
883
- const arrayView = this.arrayTable.getView();
884
1024
  for (const oldOffset of sortedArrays) {
885
1025
  // Determine the array's capacity by scanning forward to the next live
886
1026
  // array or the end of the used region.
@@ -888,7 +1028,7 @@ export class SharedMemory {
888
1028
  const end = next !== undefined ? next : this.arrayTable.byteLength;
889
1029
  const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
890
1030
  copyBytes(
891
- arrayView,
1031
+ this.arrayTable,
892
1032
  newArrayOffset,
893
1033
  oldOffset,
894
1034
  ARRAY_HEADER + capacity * SLOT_SIZE,
@@ -901,12 +1041,11 @@ export class SharedMemory {
901
1041
  // Phase 4: Compact the StringTable.
902
1042
  const sortedStrings = [...liveStrings].sort((a, b) => a - b);
903
1043
  let newStringOffset = 0;
904
- const stringView = this.stringTable.getView();
905
1044
  for (const oldOffset of sortedStrings) {
906
1045
  const next = sortedStrings[sortedStrings.indexOf(oldOffset) + 1];
907
1046
  const end = next !== undefined ? next : this.stringTable.byteLength;
908
1047
  const length = end - oldOffset;
909
- copyBytes(stringView, newStringOffset, oldOffset, length);
1048
+ copyBytes(this.stringTable, newStringOffset, oldOffset, length);
910
1049
  stringRemap.set(oldOffset, newStringOffset);
911
1050
  newStringOffset += length;
912
1051
  }
@@ -916,29 +1055,29 @@ export class SharedMemory {
916
1055
  for (let i = 0; i < indexCount; i++) {
917
1056
  const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
918
1057
  const { keyRef, dataOffset } = indexEntries[i]!;
919
- indexView.setInt32(entryOffset, stringRemap.get(keyRef.offset)!, true);
920
- indexView.setInt32(entryOffset + 4, keyRef.length, true);
921
- indexView.setInt32(entryOffset + 8, objectRemap.get(dataOffset)!, true);
1058
+ this.indexTable.setInt32(entryOffset, stringRemap.get(keyRef.offset)!);
1059
+ this.indexTable.setInt32(entryOffset + 4, keyRef.length);
1060
+ this.indexTable.setInt32(entryOffset + 8, objectRemap.get(dataOffset)!);
922
1061
  }
923
1062
 
924
1063
  // Phase 6: Rewrite object property names and value slots, and array slots.
925
1064
  for (const oldOffset of sortedData) {
926
- if (dataView.getInt32(oldOffset, true) !== TYPE_OBJECT) continue;
1065
+ if (this.dataTable.getInt32(oldOffset) !== TYPE_OBJECT) continue;
927
1066
  const newOff = objectRemap.get(oldOffset)!;
928
- const count = dataView.getInt32(newOff + 4, true);
1067
+ const count = this.dataTable.getInt32(newOff + 4);
929
1068
  let cursor = newOff + OBJECT_HEADER;
930
1069
  for (let i = 0; i < count; i++) {
931
- const nameOffset = dataView.getInt32(cursor, true);
932
- dataView.setInt32(cursor, stringRemap.get(nameOffset)!, true);
1070
+ const nameOffset = this.dataTable.getInt32(cursor);
1071
+ this.dataTable.setInt32(cursor, stringRemap.get(nameOffset)!);
933
1072
  cursor += NAME_REF_SIZE;
934
- remapSlot(dataView, cursor, stringRemap, arrayRemap);
1073
+ remapSlot(this.dataTable, cursor, stringRemap, arrayRemap);
935
1074
  cursor += SLOT_SIZE;
936
1075
  }
937
1076
  }
938
1077
  for (const oldOffset of sortedData) {
939
- if (dataView.getInt32(oldOffset, true) === TYPE_OBJECT) continue;
1078
+ if (this.dataTable.getInt32(oldOffset) === TYPE_OBJECT) continue;
940
1079
  const newOff = objectRemap.get(oldOffset)!;
941
- remapSlot(dataView, newOff, stringRemap, arrayRemap);
1080
+ remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
942
1081
  }
943
1082
  for (const oldOffset of sortedArrays) {
944
1083
  const newOff = arrayRemap.get(oldOffset)!;
@@ -947,7 +1086,7 @@ export class SharedMemory {
947
1086
  const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
948
1087
  for (let i = 0; i < capacity; i++) {
949
1088
  remapSlot(
950
- arrayView,
1089
+ this.arrayTable,
951
1090
  newOff + ARRAY_HEADER + i * SLOT_SIZE,
952
1091
  stringRemap,
953
1092
  arrayRemap,
@@ -957,34 +1096,43 @@ export class SharedMemory {
957
1096
 
958
1097
  // Phase 7: Invalidate all per-worker caches.
959
1098
  this.bumpLayoutVersion();
1099
+
1100
+ // Phase 8: Reclaim memory from inactive chunks (beyond the compacted
1101
+ // region). The Index Table is a single growable SAB and is never replaced.
1102
+ return {
1103
+ indexTable: this.indexTable.releaseInactiveChunks(),
1104
+ dataTable: this.dataTable.releaseInactiveChunks(),
1105
+ stringTable: this.stringTable.releaseInactiveChunks(),
1106
+ arrayTable: this.arrayTable.releaseInactiveChunks(),
1107
+ };
960
1108
  }
961
1109
  }
962
1110
 
963
- function writeSlot(view: DataView, offset: number, slot: ValueSlot): void {
964
- view.setInt32(offset, slot.type, true);
965
- view.setInt32(offset + 4, slot.a, true);
966
- view.setInt32(offset + 8, slot.b, true);
967
- view.setInt32(offset + 12, slot.c, true);
1111
+ function writeSlot(buf: GrowableBuffer, offset: number, slot: ValueSlot): void {
1112
+ buf.setInt32(offset, slot.type);
1113
+ buf.setInt32(offset + 4, slot.a);
1114
+ buf.setInt32(offset + 8, slot.b);
1115
+ buf.setInt32(offset + 12, slot.c);
968
1116
  }
969
1117
 
970
- function readSlot(view: DataView, offset: number): ValueSlot {
1118
+ function readSlot(buf: GrowableBuffer, offset: number): ValueSlot {
971
1119
  return {
972
- type: view.getInt32(offset, true),
973
- a: view.getInt32(offset + 4, true),
974
- b: view.getInt32(offset + 8, true),
975
- c: view.getInt32(offset + 12, true),
1120
+ type: buf.getInt32(offset),
1121
+ a: buf.getInt32(offset + 4),
1122
+ b: buf.getInt32(offset + 8),
1123
+ c: buf.getInt32(offset + 12),
976
1124
  };
977
1125
  }
978
1126
 
979
1127
  /** Copy `len` bytes from `src` to `dst` in the same buffer (dst <= src). */
980
1128
  function copyBytes(
981
- view: DataView,
1129
+ buf: GrowableBuffer,
982
1130
  dst: number,
983
1131
  src: number,
984
1132
  len: number,
985
1133
  ): void {
986
1134
  for (let i = 0; i < len; i++) {
987
- view.setUint8(dst + i, view.getUint8(src + i));
1135
+ buf.setUint8(dst + i, buf.getUint8(src + i));
988
1136
  }
989
1137
  }
990
1138
 
@@ -994,23 +1142,15 @@ function copyBytes(
994
1142
  * via `arrayRemap`.
995
1143
  */
996
1144
  function remapSlot(
997
- view: DataView,
1145
+ buf: GrowableBuffer,
998
1146
  offset: number,
999
1147
  stringRemap: Map<number, number>,
1000
1148
  arrayRemap: Map<number, number>,
1001
1149
  ): void {
1002
- const type = view.getInt32(offset, true);
1150
+ const type = buf.getInt32(offset);
1003
1151
  if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
1004
- view.setInt32(
1005
- offset + 4,
1006
- stringRemap.get(view.getInt32(offset + 4, true))!,
1007
- true,
1008
- );
1152
+ buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
1009
1153
  } else if (type === TYPE_ARRAY_REF) {
1010
- view.setInt32(
1011
- offset + 4,
1012
- arrayRemap.get(view.getInt32(offset + 4, true))!,
1013
- true,
1014
- );
1154
+ buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
1015
1155
  }
1016
1156
  }