bun-shared-memory 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,9 +5,47 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.6] - 2026-11-02
9
+
10
+ ### Added
11
+
12
+ - Support shared memory larger than 4 GiB. The single-`SharedArrayBuffer`
13
+ per-buffer architecture is replaced with a chunked `GrowableBuffer`: each
14
+ buffer is backed by multiple equal-size chunks (32 MiB default, power of
15
+ two), so a buffer can exceed the 4 GiB single-SAB limit. Offsets resolve
16
+ O(1) via bit shifts (`offset >> chunkShift`).
17
+ - `SharedMemoryOptions.chunkSize` to configure the chunk size (the minimum
18
+ growth step).
19
+ - Chunks are pre-allocated at construction (4 bytes each, with
20
+ `maxByteLength = chunkSize`) and activated on first access via
21
+ `SharedArrayBuffer.grow()`. Because `grow()` is visible across all workers
22
+ sharing a buffer, a worker can grow a chunk and every other worker sees it
23
+ immediately — no postMessage broadcast is needed for chunk activation.
24
+
25
+ ### Changed
26
+
27
+ - `exportBuffers()`/`fromBuffers()` now pass chunk lists instead of a single
28
+ buffer; `fromBuffers` reuses the shared `SharedArrayBuffer`s directly (no
29
+ copy) so writes stay visible across workers. The chunk size is derived from
30
+ each buffer's `maxByteLength` (preserved across `postMessage`), not its
31
+ current 4-byte length.
32
+ - Remove the 4 GiB cap on the per-buffer maximum in the `SharedMemory`
33
+ constructor (the chunked `GrowableBuffer` already bypasses the single-SAB
34
+ limit).
35
+
36
+ ### Performance
37
+
38
+ - After defragmentation, unused chunks are marked inactive:
39
+ `GrowableBuffer.reset()` deactivates chunks beyond the compacted region so
40
+ the GC can reclaim their storage.
41
+ - The chunked accessor adds an indirect `chunks[index].view` lookup on the
42
+ write hot path (the price of >4 GiB support): writes ~-15%, array writes
43
+ ~-20%. Reads are unchanged (lock-free cache path).
44
+
8
45
  ## [0.1.5] - 2026-11-02
9
46
 
10
47
  ### Changed
48
+
11
49
  - Replace the `Proxy` used for object `SharedRef`s with hard-baked
12
50
  `Object.defineProperty` accessor getters/setters, so property reads skip the
13
51
  Proxy trap on the hot path (~3× faster object property reads). Methods
@@ -24,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
24
62
  ## [0.1.4] - 2026-11-01
25
63
 
26
64
  ### Changed
65
+
27
66
  - Declare `"sideEffects": false` in `package.json` so bundlers can tree-shake
28
67
  the library (the modules have no side effects on import).
29
68
  - Remove the `typescript` peer dependency. The library ships as TypeScript
@@ -33,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
72
  ## [0.1.3] - 2026-11-01
34
73
 
35
74
  ### Added
75
+
36
76
  - Real-world benchmark (`bench/real-world/`) comparing shared memory against
37
77
  live SQLite, MySQL, and Redis via an Elysia mini API.
38
78
  - Redis as a fourth benchmark methodology (in Docker).
@@ -41,6 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
81
  - Real-world benchmark results and methodology documented in the README.
42
82
 
43
83
  ### Changed
84
+
44
85
  - MySQL/Redis connections are established before the benchmark (not per
45
86
  request) so connection setup is not measured as overhead.
46
87
  - The benchmark always runs both TCP and Unix-socket transports; the socket
@@ -49,16 +90,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
49
90
  ## [0.1.2] - 2026-11-01
50
91
 
51
92
  ### Changed
93
+
52
94
  - Unref the maintenance timer so the process can exit naturally.
53
95
  - Document `init()` as the first call and how to configure memory via `init()`.
54
96
 
55
97
  ## [0.1.1] - 2026-11-01
56
98
 
57
99
  ### Added
100
+
58
101
  - Repository metadata and npm publishing preparation.
59
102
  - README with package name and install instructions.
60
103
 
61
104
  ### Changed
105
+
62
106
  - Optimize the read hot path: direct cache on the proxy target.
63
107
  - Optimize `Mutex`: skip notify when no worker is waiting.
64
108
  - Start the refcount coordinator/tracker automatically in `init()`.
@@ -67,6 +111,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
67
111
  ## [0.1.0] - 2026-11-01
68
112
 
69
113
  ### Added
114
+
70
115
  - Initial release: `Shared<T>` Proxy-based API over a 4-buffer shared memory
71
116
  architecture (Index Table, DataTable, append-only StringTable, ArrayTable).
72
117
  - Atomics-based synchronization (`Mutex`, `Notifier`).
@@ -75,6 +120,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
75
120
  - O(1) in-place `push`/`pop` with capacity-based array growth.
76
121
  - Benchmarks for all data types vs native.
77
122
 
123
+ [0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
124
+ [0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
78
125
  [0.1.4]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.3...v0.1.4
79
126
  [0.1.3]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.2...v0.1.3
80
127
  [0.1.2]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.1...v0.1.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-shared-memory",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Share data between Bun workers/threads using SharedArrayBuffer (zero-copy, 4-buffer architecture, Atomics-based synchronization).",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -35,33 +35,111 @@ export class SharedBudget {
35
35
  }
36
36
 
37
37
  /**
38
- * Growable SharedArrayBuffer helper.
38
+ * Growable shared memory backed by one or more SharedArrayBuffers (chunks).
39
39
  *
40
- * SharedArrayBuffer supports `grow(newLength)` when created with
41
- * `maxByteLength`. Growth preserves existing data, can only grow up to
42
- * `maxByteLength`, and cannot shrink. All workers sharing the buffer see the
43
- * growth immediately.
40
+ * A single SharedArrayBuffer can be at most 4 GiB, so a buffer that needs to
41
+ * grow beyond that is split into multiple chunks. Each chunk is its own
42
+ * SharedArrayBuffer (<= `chunkMax` bytes). Offsets are logical and continuous
43
+ * across chunks; access methods resolve an offset into (chunk, chunkOffset).
44
+ *
45
+ * All chunks are pre-allocated at construction with `maxByteLength = chunkSize`
46
+ * but only 4 bytes of actual storage. A chunk is "activated" (grown to its
47
+ * full `chunkSize`) on first access. Because `SharedArrayBuffer.grow()` is
48
+ * visible across all workers sharing the buffer, activation needs no
49
+ * postMessage broadcast.
44
50
  */
51
+ interface Chunk {
52
+ buffer: SharedArrayBuffer;
53
+ view: DataView;
54
+ /** Whether the chunk has been grown to its full size. */
55
+ active: boolean;
56
+ }
57
+
58
+ /** Round `n` up to the next power of two (chunk sizes must be powers of two). */
59
+ function nextPowerOfTwo(n: number): number {
60
+ if (n <= 0) return 1;
61
+ let p = 1;
62
+ while (p < n) p <<= 1;
63
+ return p;
64
+ }
65
+
45
66
  export class GrowableBuffer {
46
- readonly buffer: SharedArrayBuffer;
47
- private view: DataView;
67
+ private chunks: Chunk[] = [];
48
68
  private readonly maxByteLength: number;
69
+ /** Fixed size of every chunk (the minimum growth step). */
70
+ readonly chunkSize: number;
71
+ /** log2(chunkSize), used for O(1) offset resolution via bit shifts. */
72
+ private readonly chunkShift: number;
49
73
  private readonly budget?: SharedBudget;
50
74
  private used = 0;
51
75
 
52
76
  constructor(
53
- initialSizeOrBuffer: number | SharedArrayBuffer,
77
+ initialSizeOrBuffer: number | SharedArrayBuffer | SharedArrayBuffer[],
54
78
  maxByteLength: number,
55
79
  budget?: SharedBudget,
80
+ chunkSize = 32 * 1024 * 1024, // 32 MiB per chunk
56
81
  ) {
57
- this.buffer =
58
- typeof initialSizeOrBuffer === "number"
59
- ? new SharedArrayBuffer(initialSizeOrBuffer, { maxByteLength })
60
- : initialSizeOrBuffer;
61
- this.view = new DataView(this.buffer);
62
82
  this.maxByteLength = maxByteLength;
63
83
  this.budget = budget;
64
- this.budget?.add(this.buffer.byteLength);
84
+ if (typeof initialSizeOrBuffer === "number") {
85
+ this.chunkSize = Math.min(nextPowerOfTwo(chunkSize), maxByteLength);
86
+ this.chunkShift = Math.log2(this.chunkSize);
87
+ // Pre-allocate all chunks (4 bytes each, growable to chunkSize).
88
+ const chunkCount = Math.max(1, Math.ceil(maxByteLength / this.chunkSize));
89
+ for (let i = 0; i < chunkCount; i++) {
90
+ const buffer = new SharedArrayBuffer(4, {
91
+ maxByteLength: this.chunkSize,
92
+ });
93
+ this.chunks.push({
94
+ buffer,
95
+ view: new DataView(buffer),
96
+ active: false,
97
+ });
98
+ }
99
+ } else if (Array.isArray(initialSizeOrBuffer)) {
100
+ // Reconstruct from a list of chunks. Use the buffers directly (they are
101
+ // already shared via postMessage) so writes are visible across workers.
102
+ // All chunks are pre-allocated at 4 bytes with maxByteLength = chunkSize;
103
+ // the chunk size is derived from each buffer's maxByteLength (which is
104
+ // preserved across postMessage), not its current byteLength.
105
+ this.chunkSize = initialSizeOrBuffer[0]?.maxByteLength ?? chunkSize;
106
+ this.chunkShift = Math.log2(this.chunkSize);
107
+ for (const sab of initialSizeOrBuffer) {
108
+ this.chunks.push({
109
+ buffer: sab,
110
+ view: new DataView(sab),
111
+ active: sab.byteLength === this.chunkSize,
112
+ });
113
+ this.budget?.add(sab.byteLength);
114
+ }
115
+ } else {
116
+ // Reconstruct from a single existing buffer. Use it directly so writes
117
+ // are shared. Its size defines the chunk size (prefer maxByteLength so
118
+ // a pre-allocated 4-byte chunk is interpreted at its full size).
119
+ this.chunkSize =
120
+ initialSizeOrBuffer.maxByteLength ?? initialSizeOrBuffer.byteLength;
121
+ this.chunkShift = Math.log2(this.chunkSize);
122
+ this.chunks.push({
123
+ buffer: initialSizeOrBuffer,
124
+ view: new DataView(initialSizeOrBuffer),
125
+ active: initialSizeOrBuffer.byteLength === this.chunkSize,
126
+ });
127
+ this.budget?.add(initialSizeOrBuffer.byteLength);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Activate the chunk at `index` by growing it to its full size. Because
133
+ * `SharedArrayBuffer.grow()` is visible across all workers, no broadcast is
134
+ * needed. No-op if the chunk is already active.
135
+ */
136
+ private activate(index: number): void {
137
+ const chunk = this.chunks[index];
138
+ if (chunk === undefined || chunk.active) return;
139
+ chunk.buffer.grow(this.chunkSize);
140
+ chunk.view = new DataView(chunk.buffer);
141
+ chunk.active = true;
142
+ this.budget?.add(this.chunkSize);
65
143
  }
66
144
 
67
145
  /** Number of bytes written so far (used length, not buffer capacity). */
@@ -69,24 +147,49 @@ export class GrowableBuffer {
69
147
  return this.used;
70
148
  }
71
149
 
150
+ /** Total logical capacity across all chunks (active or not). */
151
+ get capacity(): number {
152
+ return this.chunks.length * this.chunkSize;
153
+ }
154
+
155
+ /** The list of SharedArrayBuffers backing this buffer (for export). */
156
+ get buffers(): SharedArrayBuffer[] {
157
+ return this.chunks.map((c) => c.buffer);
158
+ }
159
+
160
+ /** Number of chunks that have been activated (grown to full size). */
161
+ get activeChunkCount(): number {
162
+ return this.chunks.reduce((n, c) => n + (c.active ? 1 : 0), 0);
163
+ }
164
+
165
+ /**
166
+ * Resolve a logical offset into (chunk, offset within that chunk). All
167
+ * chunks are the same size, so this is O(1). Activates the chunk if it is
168
+ * not yet active.
169
+ */
170
+ private resolve(offset: number): { chunk: Chunk; off: number } {
171
+ const index = offset >> this.chunkShift;
172
+ const chunk = this.chunks[index];
173
+ if (chunk === undefined) {
174
+ throw new RangeError(`Offset ${offset} is beyond the buffer capacity`);
175
+ }
176
+ this.activate(index);
177
+ return { chunk, off: offset & (this.chunkSize - 1) };
178
+ }
179
+
72
180
  /** Ensure the buffer has room for at least `needed` bytes. */
73
181
  ensureCapacity(needed: number): void {
74
- if (needed <= this.buffer.byteLength) return;
75
182
  if (needed > this.maxByteLength) {
76
183
  throw new RangeError(
77
184
  `Shared buffer capacity exceeded: need ${needed} bytes but max is ${this.maxByteLength}`,
78
185
  );
79
186
  }
80
- let newSize = this.buffer.byteLength * 2;
81
- while (newSize < needed) newSize *= 2;
82
- if (newSize > this.maxByteLength) newSize = this.maxByteLength;
83
- if (this.budget && !this.budget.tryGrow(this.buffer.byteLength, newSize)) {
84
- throw new RangeError(
85
- `Shared memory budget exceeded: growing to ${newSize} bytes would exceed the total budget`,
86
- );
187
+ // All chunks are pre-allocated; just activate the ones needed to cover
188
+ // `needed`. grow() is visible across workers, so no broadcast is required.
189
+ const neededChunks = Math.ceil(needed / this.chunkSize);
190
+ for (let i = 0; i < neededChunks; i++) {
191
+ this.activate(i);
87
192
  }
88
- this.buffer.grow(newSize);
89
- this.view = new DataView(this.buffer);
90
193
  }
91
194
 
92
195
  /** Mark `n` bytes as used, after writing them at the current offset. */
@@ -96,15 +199,80 @@ export class GrowableBuffer {
96
199
 
97
200
  /**
98
201
  * Reset the used length to `n` bytes. Used by defragmentation after
99
- * compacting live data to the front of the buffer. The underlying
100
- * SharedArrayBuffer is not shrunk (it cannot be), but the tail beyond `n`
101
- * is treated as free space for future writes.
202
+ * compacting live data to the front of the buffer. Chunks beyond the
203
+ * compacted region are marked inactive (their storage is released by the
204
+ * GC, which replaces them with fresh chunks).
102
205
  */
103
206
  reset(n: number): void {
104
207
  this.used = n;
208
+ const neededChunks = Math.ceil(n / this.chunkSize);
209
+ for (let i = neededChunks; i < this.chunks.length; i++) {
210
+ this.chunks[i]!.active = false;
211
+ }
212
+ }
213
+
214
+ // -------------------------------------------------------------------------
215
+ // Typed accessors (resolve offset -> chunk)
216
+ // -------------------------------------------------------------------------
217
+
218
+ getInt32(offset: number): number {
219
+ const index = offset >> this.chunkShift;
220
+ this.activate(index);
221
+ return this.chunks[index]!.view.getInt32(
222
+ offset & (this.chunkSize - 1),
223
+ true,
224
+ );
225
+ }
226
+
227
+ setInt32(offset: number, value: number): void {
228
+ const index = offset >> this.chunkShift;
229
+ this.activate(index);
230
+ this.chunks[index]!.view.setInt32(
231
+ offset & (this.chunkSize - 1),
232
+ value,
233
+ true,
234
+ );
105
235
  }
106
236
 
107
- getView(): DataView {
108
- return this.view;
237
+ getUint8(offset: number): number {
238
+ const index = offset >> this.chunkShift;
239
+ this.activate(index);
240
+ return this.chunks[index]!.view.getUint8(offset & (this.chunkSize - 1));
241
+ }
242
+
243
+ setUint8(offset: number, value: number): void {
244
+ const index = offset >> this.chunkShift;
245
+ this.activate(index);
246
+ this.chunks[index]!.view.setUint8(offset & (this.chunkSize - 1), value);
247
+ }
248
+
249
+ /** Read `length` bytes starting at `offset` into a fresh Uint8Array. */
250
+ readBytes(offset: number, length: number): Uint8Array {
251
+ const out = new Uint8Array(length);
252
+ let pos = offset;
253
+ let outPos = 0;
254
+ while (outPos < length) {
255
+ const { chunk, off } = this.resolve(pos);
256
+ const n = Math.min(length - outPos, this.chunkSize - off);
257
+ out.set(new Uint8Array(chunk.buffer, off, n), outPos);
258
+ outPos += n;
259
+ pos += n;
260
+ }
261
+ return out;
262
+ }
263
+
264
+ /** Write `bytes` starting at `offset`. */
265
+ writeBytes(offset: number, bytes: Uint8Array): void {
266
+ let pos = offset;
267
+ let srcPos = 0;
268
+ while (srcPos < bytes.byteLength) {
269
+ const { chunk, off } = this.resolve(pos);
270
+ const n = Math.min(bytes.byteLength - srcPos, this.chunkSize - off);
271
+ new Uint8Array(chunk.buffer, off, n).set(
272
+ bytes.subarray(srcPos, srcPos + n),
273
+ );
274
+ srcPos += n;
275
+ pos += n;
276
+ }
109
277
  }
110
278
  }
package/src/memory.ts CHANGED
@@ -175,19 +175,35 @@ export interface SharedMemoryOptions {
175
175
  maxByteLength?: number;
176
176
  /** Total memory budget shared across all buffers (see below). */
177
177
  maxTotalBytes?: number;
178
+ /** Size of each growable chunk (the minimum growth step). Default 32 MiB. */
179
+ chunkSize?: number;
178
180
  lock?: Mutex;
179
181
  internCapacity?: number;
180
182
  internMax?: number;
181
183
  buffers?: {
182
- indexTable?: SharedArrayBuffer;
183
- dataTable?: SharedArrayBuffer;
184
- stringTable?: SharedArrayBuffer;
185
- arrayTable?: SharedArrayBuffer;
184
+ indexTable?: SharedArrayBuffer | SharedArrayBuffer[];
185
+ dataTable?: SharedArrayBuffer | SharedArrayBuffer[];
186
+ stringTable?: SharedArrayBuffer | SharedArrayBuffer[];
187
+ arrayTable?: SharedArrayBuffer | SharedArrayBuffer[];
186
188
  layoutVersion?: SharedArrayBuffer;
187
189
  refCount?: SharedArrayBuffer;
188
190
  };
189
191
  }
190
192
 
193
+ /** Build a GrowableBuffer from an optional single SAB, chunk list, or nothing. */
194
+ function makeBuffer(
195
+ buf: SharedArrayBuffer | SharedArrayBuffer[] | undefined,
196
+ initial: number,
197
+ maxByteLength: number,
198
+ budget?: SharedBudget,
199
+ chunkSize?: number,
200
+ ): GrowableBuffer {
201
+ if (buf === undefined) {
202
+ return new GrowableBuffer(initial, maxByteLength, budget, chunkSize);
203
+ }
204
+ return new GrowableBuffer(buf, maxByteLength, budget, chunkSize);
205
+ }
206
+
191
207
  /**
192
208
  * Manages the four shared buffers and provides operations to store and read
193
209
  * strings, arrays, and objects.
@@ -246,44 +262,48 @@ export class SharedMemory {
246
262
 
247
263
  constructor(options?: SharedMemoryOptions) {
248
264
  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.
265
+ // `maxTotalBytes` is a *total* budget shared across all buffers: each
266
+ // buffer's own maxByteLength is capped at maxTotalBytes, and the sum of
267
+ // all buffers' physical sizes is additionally capped at maxTotalBytes.
268
+ // Without maxTotalBytes, each buffer grows up to its own maxByteLength.
269
+ // A single SharedArrayBuffer is limited to 4 GiB, but the chunked
270
+ // GrowableBuffer splits a buffer across multiple chunks, so a buffer can
271
+ // exceed 4 GiB.
254
272
  const maxTotal = options?.maxTotalBytes;
255
- const perBufferMax = Math.min(
256
- 4 * 1024 * 1024 * 1024,
257
- maxTotal ?? options?.maxByteLength ?? DEFAULT_MAX,
258
- );
273
+ const perBufferMax = maxTotal ?? options?.maxByteLength ?? DEFAULT_MAX;
259
274
  const budget =
260
275
  maxTotal !== undefined ? new SharedBudget(maxTotal) : undefined;
276
+ const chunkSize = options?.chunkSize;
261
277
  const buffers = options?.buffers;
262
- this.indexTable = new GrowableBuffer(
263
- buffers?.indexTable ??
264
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
278
+ this.indexTable = makeBuffer(
279
+ buffers?.indexTable,
280
+ initial,
265
281
  perBufferMax,
266
282
  budget,
283
+ chunkSize,
267
284
  );
268
- this.dataTable = new GrowableBuffer(
269
- buffers?.dataTable ??
270
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
285
+ this.dataTable = makeBuffer(
286
+ buffers?.dataTable,
287
+ initial,
271
288
  perBufferMax,
272
289
  budget,
290
+ chunkSize,
273
291
  );
274
- this.stringTable = new GrowableBuffer(
275
- buffers?.stringTable ??
276
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
292
+ this.stringTable = makeBuffer(
293
+ buffers?.stringTable,
294
+ initial,
277
295
  perBufferMax,
278
296
  budget,
297
+ chunkSize,
279
298
  );
280
- this.arrayTable = new GrowableBuffer(
281
- buffers?.arrayTable ??
282
- new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
299
+ this.arrayTable = makeBuffer(
300
+ buffers?.arrayTable,
301
+ initial,
283
302
  perBufferMax,
284
303
  budget,
304
+ chunkSize,
285
305
  );
286
- this.versionView = new Int32Array(this.indexTable.buffer);
306
+ this.versionView = new Int32Array(this.indexTable.buffers[0]!);
287
307
  this.layoutVersion = new Int32Array(
288
308
  buffers?.layoutVersion ??
289
309
  new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
@@ -313,21 +333,24 @@ export class SharedMemory {
313
333
  /**
314
334
  * Export all shared buffers (including the lock) so they can be sent to a
315
335
  * worker via `postMessage` and reconstructed there with `fromBuffers()`.
336
+ *
337
+ * Each table is exported as a list of SharedArrayBuffers (chunks), since a
338
+ * single SharedArrayBuffer is limited to 4 GiB.
316
339
  */
317
340
  exportBuffers(): {
318
- indexTable: SharedArrayBuffer;
319
- dataTable: SharedArrayBuffer;
320
- stringTable: SharedArrayBuffer;
321
- arrayTable: SharedArrayBuffer;
341
+ indexTable: SharedArrayBuffer[];
342
+ dataTable: SharedArrayBuffer[];
343
+ stringTable: SharedArrayBuffer[];
344
+ arrayTable: SharedArrayBuffer[];
322
345
  layoutVersion: SharedArrayBuffer;
323
346
  lock: SharedArrayBuffer;
324
347
  refCount: SharedArrayBuffer | null;
325
348
  } {
326
349
  return {
327
- indexTable: this.indexTable.buffer,
328
- dataTable: this.dataTable.buffer,
329
- stringTable: this.stringTable.buffer,
330
- arrayTable: this.arrayTable.buffer,
350
+ indexTable: this.indexTable.buffers,
351
+ dataTable: this.dataTable.buffers,
352
+ stringTable: this.stringTable.buffers,
353
+ arrayTable: this.arrayTable.buffers,
331
354
  layoutVersion: this.layoutVersion.buffer as SharedArrayBuffer,
332
355
  lock: this.lock.buffer as SharedArrayBuffer,
333
356
  refCount: this.refCountBuffer,
@@ -339,10 +362,10 @@ export class SharedMemory {
339
362
  * `exportBuffers()` (e.g. received from the main thread in a worker).
340
363
  */
341
364
  static fromBuffers(buffers: {
342
- indexTable: SharedArrayBuffer;
343
- dataTable: SharedArrayBuffer;
344
- stringTable: SharedArrayBuffer;
345
- arrayTable: SharedArrayBuffer;
365
+ indexTable: SharedArrayBuffer | SharedArrayBuffer[];
366
+ dataTable: SharedArrayBuffer | SharedArrayBuffer[];
367
+ stringTable: SharedArrayBuffer | SharedArrayBuffer[];
368
+ arrayTable: SharedArrayBuffer | SharedArrayBuffer[];
346
369
  layoutVersion: SharedArrayBuffer;
347
370
  lock: SharedArrayBuffer;
348
371
  refCount?: SharedArrayBuffer | null;
@@ -393,10 +416,7 @@ export class SharedMemory {
393
416
  const bytes = this.encoder.encode(value);
394
417
  const offset = this.stringTable.byteLength;
395
418
  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
- }
419
+ this.stringTable.writeBytes(offset, bytes);
400
420
  this.stringTable.advance(bytes.byteLength);
401
421
  const ref: StringRef = { offset, length: bytes.byteLength };
402
422
 
@@ -431,12 +451,7 @@ export class SharedMemory {
431
451
 
432
452
  /** Read a string from the StringTable by reference. */
433
453
  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
- );
454
+ const bytes = this.stringTable.readBytes(ref.offset, ref.length);
440
455
  return this.decoder.decode(bytes);
441
456
  }
442
457
 
@@ -458,11 +473,14 @@ export class SharedMemory {
458
473
  const cap = Math.max(capacity ?? 0, count, 1);
459
474
  const offset = this.arrayTable.byteLength;
460
475
  this.arrayTable.ensureCapacity(offset + ARRAY_HEADER + cap * SLOT_SIZE);
461
- const view = this.arrayTable.getView();
462
476
  const objectId = this.nextObjectId++;
463
- view.setInt32(offset, objectId, true);
477
+ this.arrayTable.setInt32(offset, objectId);
464
478
  for (let i = 0; i < count; i++) {
465
- writeSlot(view, offset + ARRAY_HEADER + i * SLOT_SIZE, slots[i]!);
479
+ writeSlot(
480
+ this.arrayTable,
481
+ offset + ARRAY_HEADER + i * SLOT_SIZE,
482
+ slots[i]!,
483
+ );
466
484
  }
467
485
  this.arrayTable.advance(ARRAY_HEADER + cap * SLOT_SIZE);
468
486
  return { offset, count, capacity: cap };
@@ -470,10 +488,11 @@ export class SharedMemory {
470
488
 
471
489
  /** Read an array of value slots from the ArrayTable by reference. */
472
490
  readArray(ref: ArrayRef): ValueSlot[] {
473
- const view = this.arrayTable.getView();
474
491
  const slots: ValueSlot[] = [];
475
492
  for (let i = 0; i < ref.count; i++) {
476
- slots.push(readSlot(view, ref.offset + ARRAY_HEADER + i * SLOT_SIZE));
493
+ slots.push(
494
+ readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
495
+ );
477
496
  }
478
497
  return slots;
479
498
  }
@@ -481,7 +500,7 @@ export class SharedMemory {
481
500
  /** Update a single value slot of an array in the ArrayTable in place. */
482
501
  updateArraySlot(ref: ArrayRef, index: number, slot: ValueSlot): void {
483
502
  writeSlot(
484
- this.arrayTable.getView(),
503
+ this.arrayTable,
485
504
  ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
486
505
  slot,
487
506
  );
@@ -490,14 +509,14 @@ export class SharedMemory {
490
509
  /** Read a single value slot of an array from the ArrayTable. */
491
510
  readArraySlot(ref: ArrayRef, index: number): ValueSlot {
492
511
  return readSlot(
493
- this.arrayTable.getView(),
512
+ this.arrayTable,
494
513
  ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
495
514
  );
496
515
  }
497
516
 
498
517
  /** Read the stable objectId of an array block in the ArrayTable. */
499
518
  getArrayObjectId(offset: number): number {
500
- return this.arrayTable.getView().getInt32(offset, true);
519
+ return this.arrayTable.getInt32(offset);
501
520
  }
502
521
 
503
522
  /**
@@ -510,14 +529,13 @@ export class SharedMemory {
510
529
  this.arrayTable.ensureCapacity(
511
530
  newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
512
531
  );
513
- const view = this.arrayTable.getView();
514
532
  const objectId = this.getArrayObjectId(ref.offset);
515
- view.setInt32(newOffset, objectId, true);
533
+ this.arrayTable.setInt32(newOffset, objectId);
516
534
  for (let i = 0; i < ref.count; i++) {
517
535
  writeSlot(
518
- view,
536
+ this.arrayTable,
519
537
  newOffset + ARRAY_HEADER + i * SLOT_SIZE,
520
- readSlot(view, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
538
+ readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
521
539
  );
522
540
  }
523
541
  this.arrayTable.advance(ARRAY_HEADER + newCapacity * SLOT_SIZE);
@@ -533,7 +551,7 @@ export class SharedMemory {
533
551
  appendArraySlot(ref: ArrayRef, slot: ValueSlot): ArrayRef {
534
552
  if (ref.count < ref.capacity) {
535
553
  writeSlot(
536
- this.arrayTable.getView(),
554
+ this.arrayTable,
537
555
  ref.offset + ARRAY_HEADER + ref.count * SLOT_SIZE,
538
556
  slot,
539
557
  );
@@ -545,7 +563,7 @@ export class SharedMemory {
545
563
  }
546
564
  const moved = this.moveArray(ref, Math.max(ref.capacity * 2, 1));
547
565
  writeSlot(
548
- this.arrayTable.getView(),
566
+ this.arrayTable,
549
567
  moved.offset + ARRAY_HEADER + moved.count * SLOT_SIZE,
550
568
  slot,
551
569
  );
@@ -564,19 +582,19 @@ export class SharedMemory {
564
582
  storeValueSlot(slot: ValueSlot): number {
565
583
  const offset = this.dataTable.byteLength;
566
584
  this.dataTable.ensureCapacity(offset + SLOT_SIZE);
567
- writeSlot(this.dataTable.getView(), offset, slot);
585
+ writeSlot(this.dataTable, offset, slot);
568
586
  this.dataTable.advance(SLOT_SIZE);
569
587
  return offset;
570
588
  }
571
589
 
572
590
  /** Read a single value slot from the DataTable. */
573
591
  readValueSlot(offset: number): ValueSlot {
574
- return readSlot(this.dataTable.getView(), offset);
592
+ return readSlot(this.dataTable, offset);
575
593
  }
576
594
 
577
595
  /** Update a single value slot in the DataTable. */
578
596
  updateValueSlot(offset: number, slot: ValueSlot): void {
579
- writeSlot(this.dataTable.getView(), offset, slot);
597
+ writeSlot(this.dataTable, offset, slot);
580
598
  }
581
599
 
582
600
  /**
@@ -590,20 +608,19 @@ export class SharedMemory {
590
608
  OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
591
609
  const offset = this.dataTable.byteLength;
592
610
  this.dataTable.ensureCapacity(offset + blockSize);
593
- const view = this.dataTable.getView();
594
611
 
595
612
  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);
613
+ this.dataTable.setInt32(offset, TYPE_OBJECT);
614
+ this.dataTable.setInt32(offset + 4, properties.length);
615
+ this.dataTable.setInt32(offset + 8, objectId);
599
616
 
600
617
  let cursor = offset + OBJECT_HEADER;
601
618
  for (const [name, slot] of properties) {
602
619
  const nameRef = this.storeString(name);
603
- view.setInt32(cursor, nameRef.offset, true);
604
- view.setInt32(cursor + 4, nameRef.length, true);
620
+ this.dataTable.setInt32(cursor, nameRef.offset);
621
+ this.dataTable.setInt32(cursor + 4, nameRef.length);
605
622
  cursor += NAME_REF_SIZE;
606
- writeSlot(view, cursor, slot);
623
+ writeSlot(this.dataTable, cursor, slot);
607
624
  cursor += SLOT_SIZE;
608
625
  }
609
626
  this.dataTable.advance(blockSize);
@@ -612,16 +629,15 @@ export class SharedMemory {
612
629
 
613
630
  /** Read an object's properties from the DataTable. */
614
631
  readObject(offset: number): Array<[string, ValueSlot]> {
615
- const view = this.dataTable.getView();
616
- const count = view.getInt32(offset + 4, true);
632
+ const count = this.dataTable.getInt32(offset + 4);
617
633
  const properties: Array<[string, ValueSlot]> = [];
618
634
  let cursor = offset + OBJECT_HEADER;
619
635
  for (let i = 0; i < count; i++) {
620
- const nameOffset = view.getInt32(cursor, true);
621
- const nameLength = view.getInt32(cursor + 4, true);
636
+ const nameOffset = this.dataTable.getInt32(cursor);
637
+ const nameLength = this.dataTable.getInt32(cursor + 4);
622
638
  const name = this.readString({ offset: nameOffset, length: nameLength });
623
639
  cursor += NAME_REF_SIZE;
624
- const slot = readSlot(view, cursor);
640
+ const slot = readSlot(this.dataTable, cursor);
625
641
  cursor += SLOT_SIZE;
626
642
  properties.push([name, slot]);
627
643
  }
@@ -630,7 +646,7 @@ export class SharedMemory {
630
646
 
631
647
  /** Read the stable objectId of an object block in the DataTable. */
632
648
  getObjectId(offset: number): number {
633
- return this.dataTable.getView().getInt32(offset + 8, true);
649
+ return this.dataTable.getInt32(offset + 8);
634
650
  }
635
651
 
636
652
  /**
@@ -647,14 +663,13 @@ export class SharedMemory {
647
663
  getObjectLayout(key: string): ObjectLayout | null {
648
664
  const offset = this.lookup(key);
649
665
  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);
666
+ if (this.dataTable.getInt32(offset) !== TYPE_OBJECT) return null;
667
+ const count = this.dataTable.getInt32(offset + 4);
653
668
  const properties = new Map<string, number>();
654
669
  let cursor = offset + OBJECT_HEADER;
655
670
  for (let i = 0; i < count; i++) {
656
- const nameOffset = view.getInt32(cursor, true);
657
- const nameLength = view.getInt32(cursor + 4, true);
671
+ const nameOffset = this.dataTable.getInt32(cursor);
672
+ const nameLength = this.dataTable.getInt32(cursor + 4);
658
673
  const name = this.readString({ offset: nameOffset, length: nameLength });
659
674
  cursor += NAME_REF_SIZE;
660
675
  properties.set(name, cursor);
@@ -669,13 +684,12 @@ export class SharedMemory {
669
684
  propertyIndex: number,
670
685
  slot: ValueSlot,
671
686
  ): void {
672
- const view = this.dataTable.getView();
673
687
  const slotOffset =
674
688
  offset +
675
689
  OBJECT_HEADER +
676
690
  propertyIndex * (NAME_REF_SIZE + SLOT_SIZE) +
677
691
  NAME_REF_SIZE;
678
- writeSlot(view, slotOffset, slot);
692
+ writeSlot(this.dataTable, slotOffset, slot);
679
693
  }
680
694
 
681
695
  // ---------------------------------------------------------------------------
@@ -691,11 +705,10 @@ export class SharedMemory {
691
705
 
692
706
  /** Read the DataTable offset for a known stable index, or null if absent. */
693
707
  lookupByIndex(index: number): number | null {
694
- const view = this.indexTable.getView();
695
- const count = view.getInt32(0, true);
708
+ const count = this.indexTable.getInt32(0);
696
709
  if (index >= count) return null;
697
710
  const entryOffset = INDEX_HEADER + index * INDEX_ENTRY_SIZE;
698
- return view.getInt32(entryOffset + 8, true);
711
+ return this.indexTable.getInt32(entryOffset + 8);
699
712
  }
700
713
 
701
714
  /**
@@ -706,12 +719,11 @@ export class SharedMemory {
706
719
  * used to address the key's version cell.
707
720
  */
708
721
  lookupIndex(key: string): number | null {
709
- const view = this.indexTable.getView();
710
- const count = view.getInt32(0, true);
722
+ const count = this.indexTable.getInt32(0);
711
723
  for (let i = 0; i < count; i++) {
712
724
  const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
713
- const keyOffset = view.getInt32(entryOffset, true);
714
- const keyLength = view.getInt32(entryOffset + 4, true);
725
+ const keyOffset = this.indexTable.getInt32(entryOffset);
726
+ const keyLength = this.indexTable.getInt32(entryOffset + 4);
715
727
  const storedKey = this.readString({
716
728
  offset: keyOffset,
717
729
  length: keyLength,
@@ -736,21 +748,21 @@ export class SharedMemory {
736
748
  /** Insert a key → dataOffset mapping into the Index Table. */
737
749
  insert(key: string, dataOffset: number): void {
738
750
  const keyRef = this.storeString(key);
739
- const view = this.indexTable.getView();
740
- const count = view.getInt32(0, true);
751
+ const count = this.indexTable.getInt32(0);
741
752
  const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
742
753
  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);
754
+ this.indexTable.setInt32(entryOffset, keyRef.offset);
755
+ this.indexTable.setInt32(entryOffset + 4, keyRef.length);
756
+ this.indexTable.setInt32(entryOffset + 8, dataOffset);
746
757
  // 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);
758
+ this.indexTable.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0);
759
+ this.indexTable.setInt32(0, count + 1);
749
760
  this.indexTable.advance(INDEX_ENTRY_SIZE);
750
761
 
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);
762
+ // Rebuild the version view over the first chunk (the Index Table is small
763
+ // and stays within the first chunk for typical workloads; version cells
764
+ // beyond the first chunk are not addressable via the single Int32Array).
765
+ this.versionView = new Int32Array(this.indexTable.buffers[0]!);
754
766
  }
755
767
 
756
768
  // ---------------------------------------------------------------------------
@@ -812,9 +824,10 @@ export class SharedMemory {
812
824
  const markArray = (offset: number, count: number) => {
813
825
  if (liveArrays.has(offset)) return;
814
826
  liveArrays.add(offset);
815
- const view = this.arrayTable.getView();
816
827
  for (let i = 0; i < count; i++) {
817
- markValueSlot(readSlot(view, offset + ARRAY_HEADER + i * SLOT_SIZE));
828
+ markValueSlot(
829
+ readSlot(this.arrayTable, offset + ARRAY_HEADER + i * SLOT_SIZE),
830
+ );
818
831
  }
819
832
  };
820
833
 
@@ -822,40 +835,37 @@ export class SharedMemory {
822
835
  if (liveObjects.has(offset)) return;
823
836
  liveObjects.add(offset);
824
837
  liveDataOffsets.add(offset);
825
- const view = this.dataTable.getView();
826
- const count = view.getInt32(offset + 4, true);
838
+ const count = this.dataTable.getInt32(offset + 4);
827
839
  let cursor = offset + OBJECT_HEADER;
828
840
  for (let i = 0; i < count; i++) {
829
- const nameOffset = view.getInt32(cursor, true);
830
- const nameLength = view.getInt32(cursor + 4, true);
841
+ const nameOffset = this.dataTable.getInt32(cursor);
842
+ const nameLength = this.dataTable.getInt32(cursor + 4);
831
843
  liveStrings.add(nameOffset);
832
844
  cursor += NAME_REF_SIZE;
833
- markValueSlot(readSlot(view, cursor));
845
+ markValueSlot(readSlot(this.dataTable, cursor));
834
846
  cursor += SLOT_SIZE;
835
847
  }
836
848
  };
837
849
 
838
850
  const markValue = (dataOffset: number) => {
839
- const view = this.dataTable.getView();
840
- if (view.getInt32(dataOffset, true) === TYPE_OBJECT) {
851
+ if (this.dataTable.getInt32(dataOffset) === TYPE_OBJECT) {
841
852
  markObject(dataOffset);
842
853
  } else {
843
854
  liveDataOffsets.add(dataOffset);
844
- markValueSlot(readSlot(view, dataOffset));
855
+ markValueSlot(readSlot(this.dataTable, dataOffset));
845
856
  }
846
857
  };
847
858
 
848
859
  // Phase 1: Mark live data by walking the Index Table.
849
- const indexView = this.indexTable.getView();
850
- const indexCount = indexView.getInt32(0, true);
860
+ const indexCount = this.indexTable.getInt32(0);
851
861
  const indexEntries: Array<{ keyRef: StringRef; dataOffset: number }> = [];
852
862
  for (let i = 0; i < indexCount; i++) {
853
863
  const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
854
864
  const keyRef = {
855
- offset: indexView.getInt32(entryOffset, true),
856
- length: indexView.getInt32(entryOffset + 4, true),
865
+ offset: this.indexTable.getInt32(entryOffset),
866
+ length: this.indexTable.getInt32(entryOffset + 4),
857
867
  };
858
- const dataOffset = indexView.getInt32(entryOffset + 8, true);
868
+ const dataOffset = this.indexTable.getInt32(entryOffset + 8);
859
869
  indexEntries.push({ keyRef, dataOffset });
860
870
  liveStrings.add(keyRef.offset);
861
871
  markValue(dataOffset);
@@ -864,14 +874,13 @@ export class SharedMemory {
864
874
  // Phase 2: Compact the DataTable (objects and single value slots).
865
875
  const sortedData = [...liveDataOffsets].sort((a, b) => a - b);
866
876
  let newDataOffset = 0;
867
- const dataView = this.dataTable.getView();
868
877
  for (const oldOffset of sortedData) {
869
878
  const size =
870
- dataView.getInt32(oldOffset, true) === TYPE_OBJECT
879
+ this.dataTable.getInt32(oldOffset) === TYPE_OBJECT
871
880
  ? OBJECT_HEADER +
872
- dataView.getInt32(oldOffset + 4, true) * (NAME_REF_SIZE + SLOT_SIZE)
881
+ this.dataTable.getInt32(oldOffset + 4) * (NAME_REF_SIZE + SLOT_SIZE)
873
882
  : SLOT_SIZE;
874
- copyBytes(dataView, newDataOffset, oldOffset, size);
883
+ copyBytes(this.dataTable, newDataOffset, oldOffset, size);
875
884
  objectRemap.set(oldOffset, newDataOffset);
876
885
  newDataOffset += size;
877
886
  }
@@ -880,7 +889,6 @@ export class SharedMemory {
880
889
  // Phase 3: Compact the ArrayTable.
881
890
  const sortedArrays = [...liveArrays].sort((a, b) => a - b);
882
891
  let newArrayOffset = 0;
883
- const arrayView = this.arrayTable.getView();
884
892
  for (const oldOffset of sortedArrays) {
885
893
  // Determine the array's capacity by scanning forward to the next live
886
894
  // array or the end of the used region.
@@ -888,7 +896,7 @@ export class SharedMemory {
888
896
  const end = next !== undefined ? next : this.arrayTable.byteLength;
889
897
  const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
890
898
  copyBytes(
891
- arrayView,
899
+ this.arrayTable,
892
900
  newArrayOffset,
893
901
  oldOffset,
894
902
  ARRAY_HEADER + capacity * SLOT_SIZE,
@@ -901,12 +909,11 @@ export class SharedMemory {
901
909
  // Phase 4: Compact the StringTable.
902
910
  const sortedStrings = [...liveStrings].sort((a, b) => a - b);
903
911
  let newStringOffset = 0;
904
- const stringView = this.stringTable.getView();
905
912
  for (const oldOffset of sortedStrings) {
906
913
  const next = sortedStrings[sortedStrings.indexOf(oldOffset) + 1];
907
914
  const end = next !== undefined ? next : this.stringTable.byteLength;
908
915
  const length = end - oldOffset;
909
- copyBytes(stringView, newStringOffset, oldOffset, length);
916
+ copyBytes(this.stringTable, newStringOffset, oldOffset, length);
910
917
  stringRemap.set(oldOffset, newStringOffset);
911
918
  newStringOffset += length;
912
919
  }
@@ -916,29 +923,29 @@ export class SharedMemory {
916
923
  for (let i = 0; i < indexCount; i++) {
917
924
  const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
918
925
  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);
926
+ this.indexTable.setInt32(entryOffset, stringRemap.get(keyRef.offset)!);
927
+ this.indexTable.setInt32(entryOffset + 4, keyRef.length);
928
+ this.indexTable.setInt32(entryOffset + 8, objectRemap.get(dataOffset)!);
922
929
  }
923
930
 
924
931
  // Phase 6: Rewrite object property names and value slots, and array slots.
925
932
  for (const oldOffset of sortedData) {
926
- if (dataView.getInt32(oldOffset, true) !== TYPE_OBJECT) continue;
933
+ if (this.dataTable.getInt32(oldOffset) !== TYPE_OBJECT) continue;
927
934
  const newOff = objectRemap.get(oldOffset)!;
928
- const count = dataView.getInt32(newOff + 4, true);
935
+ const count = this.dataTable.getInt32(newOff + 4);
929
936
  let cursor = newOff + OBJECT_HEADER;
930
937
  for (let i = 0; i < count; i++) {
931
- const nameOffset = dataView.getInt32(cursor, true);
932
- dataView.setInt32(cursor, stringRemap.get(nameOffset)!, true);
938
+ const nameOffset = this.dataTable.getInt32(cursor);
939
+ this.dataTable.setInt32(cursor, stringRemap.get(nameOffset)!);
933
940
  cursor += NAME_REF_SIZE;
934
- remapSlot(dataView, cursor, stringRemap, arrayRemap);
941
+ remapSlot(this.dataTable, cursor, stringRemap, arrayRemap);
935
942
  cursor += SLOT_SIZE;
936
943
  }
937
944
  }
938
945
  for (const oldOffset of sortedData) {
939
- if (dataView.getInt32(oldOffset, true) === TYPE_OBJECT) continue;
946
+ if (this.dataTable.getInt32(oldOffset) === TYPE_OBJECT) continue;
940
947
  const newOff = objectRemap.get(oldOffset)!;
941
- remapSlot(dataView, newOff, stringRemap, arrayRemap);
948
+ remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
942
949
  }
943
950
  for (const oldOffset of sortedArrays) {
944
951
  const newOff = arrayRemap.get(oldOffset)!;
@@ -947,7 +954,7 @@ export class SharedMemory {
947
954
  const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
948
955
  for (let i = 0; i < capacity; i++) {
949
956
  remapSlot(
950
- arrayView,
957
+ this.arrayTable,
951
958
  newOff + ARRAY_HEADER + i * SLOT_SIZE,
952
959
  stringRemap,
953
960
  arrayRemap,
@@ -960,31 +967,31 @@ export class SharedMemory {
960
967
  }
961
968
  }
962
969
 
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);
970
+ function writeSlot(buf: GrowableBuffer, offset: number, slot: ValueSlot): void {
971
+ buf.setInt32(offset, slot.type);
972
+ buf.setInt32(offset + 4, slot.a);
973
+ buf.setInt32(offset + 8, slot.b);
974
+ buf.setInt32(offset + 12, slot.c);
968
975
  }
969
976
 
970
- function readSlot(view: DataView, offset: number): ValueSlot {
977
+ function readSlot(buf: GrowableBuffer, offset: number): ValueSlot {
971
978
  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),
979
+ type: buf.getInt32(offset),
980
+ a: buf.getInt32(offset + 4),
981
+ b: buf.getInt32(offset + 8),
982
+ c: buf.getInt32(offset + 12),
976
983
  };
977
984
  }
978
985
 
979
986
  /** Copy `len` bytes from `src` to `dst` in the same buffer (dst <= src). */
980
987
  function copyBytes(
981
- view: DataView,
988
+ buf: GrowableBuffer,
982
989
  dst: number,
983
990
  src: number,
984
991
  len: number,
985
992
  ): void {
986
993
  for (let i = 0; i < len; i++) {
987
- view.setUint8(dst + i, view.getUint8(src + i));
994
+ buf.setUint8(dst + i, buf.getUint8(src + i));
988
995
  }
989
996
  }
990
997
 
@@ -994,23 +1001,15 @@ function copyBytes(
994
1001
  * via `arrayRemap`.
995
1002
  */
996
1003
  function remapSlot(
997
- view: DataView,
1004
+ buf: GrowableBuffer,
998
1005
  offset: number,
999
1006
  stringRemap: Map<number, number>,
1000
1007
  arrayRemap: Map<number, number>,
1001
1008
  ): void {
1002
- const type = view.getInt32(offset, true);
1009
+ const type = buf.getInt32(offset);
1003
1010
  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
- );
1011
+ buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
1009
1012
  } else if (type === TYPE_ARRAY_REF) {
1010
- view.setInt32(
1011
- offset + 4,
1012
- arrayRemap.get(view.getInt32(offset + 4, true))!,
1013
- true,
1014
- );
1013
+ buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
1015
1014
  }
1016
1015
  }
package/src/refcount.ts CHANGED
@@ -286,9 +286,9 @@ export class RefCountCoordinator {
286
286
 
287
287
  /** Whether any table has less than `defragThreshold` free space. */
288
288
  needsDefrag(): boolean {
289
- const max = this.mem.dataTable.buffer.maxByteLength;
290
- const free = (b: { buffer: SharedArrayBuffer; byteLength: number }) =>
291
- (b.buffer.byteLength - b.byteLength) / max;
289
+ const max = this.mem.dataTable.capacity;
290
+ const free = (b: { capacity: number; byteLength: number }) =>
291
+ (b.capacity - b.byteLength) / max;
292
292
  return (
293
293
  free(this.mem.dataTable) < this.defragThreshold ||
294
294
  free(this.mem.arrayTable) < this.defragThreshold ||
package/src/shared.ts CHANGED
@@ -178,7 +178,7 @@ function isTrackable(value: unknown): boolean {
178
178
  function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
179
179
  const offset = getOffset(mem, key);
180
180
  if (offset === null) return null;
181
- const type = mem.dataTable.getView().getInt32(offset, true);
181
+ const type = mem.dataTable.getInt32(offset);
182
182
  if (type === 5) {
183
183
  return mem.getObjectId(offset);
184
184
  }
@@ -517,7 +517,7 @@ function createSharedRef<T>(
517
517
  enumerable: false,
518
518
  configurable: false,
519
519
  get() {
520
- return mem.dataTable.buffer;
520
+ return mem.dataTable.buffers[0];
521
521
  },
522
522
  });
523
523
  const ref = Object.create(proto) as Record<string | symbol, unknown>;
@@ -564,7 +564,7 @@ function createSharedRef<T>(
564
564
  if (prop === "write") return t.write;
565
565
  if (prop === "update") return t.update;
566
566
  if (prop === "key") return t.key;
567
- if (prop === "buffer") return mem.dataTable.buffer;
567
+ if (prop === "buffer") return mem.dataTable.buffers[0];
568
568
  if (prop === Symbol.toPrimitive) return () => t.read();
569
569
 
570
570
  // push/pop on a top-level array: operate directly on the array ref
@@ -635,7 +635,7 @@ function readValue(
635
635
  if (offset === null) {
636
636
  throw new Error(`No shared value found for key "${key}"`);
637
637
  }
638
- const type = mem.dataTable.getView().getInt32(offset, true);
638
+ const type = mem.dataTable.getInt32(offset);
639
639
  if (type === 5) {
640
640
  // Object block.
641
641
  const properties = mem.readObject(offset);
@@ -665,7 +665,7 @@ function writeValue(
665
665
  mem.withLock(() => {
666
666
  const existing = getOffset(mem, key);
667
667
  if (existing !== null) {
668
- const type = mem.dataTable.getView().getInt32(existing, true);
668
+ const type = mem.dataTable.getInt32(existing);
669
669
  if (type === 5) {
670
670
  // Update existing object's property slots in place.
671
671
  const properties = mem.readObject(existing);
package/src/worker.ts CHANGED
@@ -144,6 +144,7 @@ function stopCoordinator(): void {
144
144
  }
145
145
  }
146
146
 
147
+ // ---------------------------------------------------------------------------
147
148
  /** The main-thread worker listener, so it can be removed by `stopInit()`. */
148
149
  let mainListener:
149
150
  | ((worker: {
@@ -166,12 +167,17 @@ function initMain(): void {
166
167
  }) => {
167
168
  worker.addEventListener("message", (e) => {
168
169
  const msg = e.data as { __sharedMemoryRequest?: boolean } | undefined;
169
- if (msg && msg.__sharedMemoryRequest) {
170
+ if (
171
+ msg !== null &&
172
+ typeof msg === "object" &&
173
+ "__sharedMemoryRequest" in msg
174
+ ) {
170
175
  const mem = getDefaultMemory();
171
176
  worker.postMessage({
172
177
  __sharedMemoryInit: true,
173
178
  buffers: mem.exportBuffers(),
174
179
  });
180
+ return;
175
181
  }
176
182
  });
177
183
  };
@@ -234,7 +240,12 @@ function installMessageWrapper(): void {
234
240
  buffers?: Parameters<typeof SharedMemory.fromBuffers>[0];
235
241
  }
236
242
  | undefined;
237
- if (msg && msg.__sharedMemoryInit && msg.buffers) {
243
+ if (
244
+ msg !== null &&
245
+ typeof msg === "object" &&
246
+ "__sharedMemoryInit" in msg &&
247
+ msg.buffers
248
+ ) {
238
249
  const mem = SharedMemory.fromBuffers(msg.buffers);
239
250
  setDefaultMemory(mem);
240
251
  // Create the worker-side refcount tracker from the shared refcount