bun-shared-memory 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +18 -18
- package/package.json +2 -1
- package/src/growable-buffer.ts +179 -0
- package/src/memory.ts +371 -41
- package/src/refcount.ts +31 -8
- package/src/shared.ts +643 -168
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,80 @@ 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.2.0] - 2026-11-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Slab allocator for string values and arrays. Overwriting a value no longer
|
|
13
|
+
leaves the old block as dead space: freed blocks are pushed onto a shared
|
|
14
|
+
free list (one per table, in a `SharedArrayBuffer`) and reused by later
|
|
15
|
+
allocations, so the tables grow far less over time and defragmentation runs
|
|
16
|
+
less often. The free list is shared across workers via
|
|
17
|
+
`exportBuffers()`/`fromBuffers()`.
|
|
18
|
+
- Inline short strings. Strings up to 11 UTF-8 bytes are packed directly into
|
|
19
|
+
the 16-byte value slot (type + length in the top 4 bits of `c`, bytes in
|
|
20
|
+
`a`/`b`/`c`), avoiding a heap allocation and the string-table write on every
|
|
21
|
+
write. Longer strings still use the slab-allocated ArrayTable with in-place
|
|
22
|
+
reuse.
|
|
23
|
+
- A complete multi-worker todo-list example (`examples/todo-list/`) that
|
|
24
|
+
serves both the frontend and the API on a single port via Elysia workers.
|
|
25
|
+
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
- Cache each object property getter's stable version-cell index (resolved
|
|
29
|
+
lazily on first read) and load the version directly via
|
|
30
|
+
`Atomics.load(mem.versionView, cellIndex)`, replacing the per-read
|
|
31
|
+
`index === null` check and the `getVersion` → `versionCellIndex` multiply +
|
|
32
|
+
divide. Object property reads improved a further 3–17%.
|
|
33
|
+
- Cache the top-level array ref and version-cell index on the `SharedRef`
|
|
34
|
+
target so index writes skip the per-write DataTable header reads, Map
|
|
35
|
+
lookups, and the generic encode path. `moveArray` now bumps the layout
|
|
36
|
+
version so the cached ref is invalidated on array growth. Closes the
|
|
37
|
+
remaining array-write gap: number[]/boolean[]/string[] index writes are now
|
|
38
|
+
at or above native speed.
|
|
39
|
+
- Cache the array proxy for an object's array property on the accessor getter
|
|
40
|
+
so it isn't recreated on every write (a write bumps the version and
|
|
41
|
+
invalidates the field cache), and cache the property slot offset + array ref
|
|
42
|
+
in the array proxy so index writes skip the per-write `getObjectLayout` Map
|
|
43
|
+
lookup, `properties.get`, and DataTable header reads. Object array-prop
|
|
44
|
+
index writes dropped from ~254× to ~33× slower than native.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Raise the defragmentation trigger: a table now triggers a defrag pass when
|
|
49
|
+
its used length exceeds 80% of capacity (was: when free space fell below
|
|
50
|
+
10%), and only when at least 64 blocks have been freed. Because writes are
|
|
51
|
+
lock-free bump allocations, the used length grows monotonically even when
|
|
52
|
+
the live data set stays the same size; the new threshold defragments only
|
|
53
|
+
when the buffer is genuinely fragmented. During a defrag pass, lock-free
|
|
54
|
+
writes fall back to the mutex and in-flight writes are awaited before
|
|
55
|
+
compacting.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Fix slab-allocator block corruption in `free()`. `alloc()` padded blocks to
|
|
60
|
+
`MIN_BLOCK_SIZE` internally but callers advanced the cursor by the unpadded
|
|
61
|
+
size, so blocks overlapped and `free()`'s metadata overwrote the neighbour
|
|
62
|
+
block. `alloc()` now returns the actual reserved (padded) size and all
|
|
63
|
+
callers advance by it. Free-list metadata was also reduced from 12 to 8
|
|
64
|
+
bytes (next pointer + size) and the magic marker dropped (double-frees are
|
|
65
|
+
now detected by walking the free list), lowering `MIN_BLOCK_SIZE` from 12 to
|
|
66
|
+
8 so small string blocks no longer inflate the ArrayTable during
|
|
67
|
+
defragmentation.
|
|
68
|
+
|
|
69
|
+
## [0.1.8] - 2026-11-02
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- Share each `GrowableBuffer`'s append cursor across workers. The cursor was a
|
|
74
|
+
private per-instance field, so when a worker reconstructed `SharedMemory`
|
|
75
|
+
via `fromBuffers()` its cursor stayed `0` and the first write in that worker
|
|
76
|
+
wrote at offset `0`, overwriting existing data (e.g. a key string) and
|
|
77
|
+
corrupting the shared value. The cursor now lives in a `SharedArrayBuffer`
|
|
78
|
+
(as a `BigInt64Array`) read/written with `Atomics`, so every worker appends
|
|
79
|
+
at the correct offset. Reported while building the multi-worker todo-list
|
|
80
|
+
example.
|
|
81
|
+
|
|
8
82
|
## [0.1.7] - 2026-11-02
|
|
9
83
|
|
|
10
84
|
### Added
|
|
@@ -172,6 +246,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
172
246
|
- O(1) in-place `push`/`pop` with capacity-based array growth.
|
|
173
247
|
- Benchmarks for all data types vs native.
|
|
174
248
|
|
|
249
|
+
[0.2.0]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.8...v0.2.0
|
|
250
|
+
[0.1.8]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.7...v0.1.8
|
|
175
251
|
[0.1.7]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.6...v0.1.7
|
|
176
252
|
[0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
|
|
177
253
|
[0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
|
package/README.md
CHANGED
|
@@ -40,14 +40,14 @@ Measured on Linux (Bun 1.4.4, 100,000 requests per methodology, sorted by
|
|
|
40
40
|
speed; MySQL/Redis benchmarked over both TCP and Unix socket; each methodology
|
|
41
41
|
is the minimum over 3 runs × 5 samples to filter CPU-contention noise):
|
|
42
42
|
|
|
43
|
-
| Methodology | Transport | Requests | Elapsed (ms) | req/s
|
|
44
|
-
|
|
45
|
-
| `shared` | — | 100,000 | 21 | 4,876,492
|
|
46
|
-
| `sqlite` | — | 100,000 | 188 | 531,116
|
|
47
|
-
| `redis` | socket | 100,000 | 1,262 | 79,229
|
|
48
|
-
| `mysql` | socket | 100,000 | 1,605 | 62,322
|
|
49
|
-
| `redis` | tcp | 100,000 | 2,817 | 35,497
|
|
50
|
-
| `mysql` | tcp | 100,000 | 2,976 | 33,602
|
|
43
|
+
| Methodology | Transport | Requests | Elapsed (ms) | req/s | vs. shared |
|
|
44
|
+
| ----------- | --------- | -------- | ------------ | --------- | ------------- |
|
|
45
|
+
| `shared` | — | 100,000 | 21 | 4,876,492 | — |
|
|
46
|
+
| `sqlite` | — | 100,000 | 188 | 531,116 | 9.2× slower |
|
|
47
|
+
| `redis` | socket | 100,000 | 1,262 | 79,229 | 61.5× slower |
|
|
48
|
+
| `mysql` | socket | 100,000 | 1,605 | 62,322 | 78.2× slower |
|
|
49
|
+
| `redis` | tcp | 100,000 | 2,817 | 35,497 | 137.4× slower |
|
|
50
|
+
| `mysql` | tcp | 100,000 | 2,976 | 33,602 | 145.1× slower |
|
|
51
51
|
|
|
52
52
|
Shared memory is the fastest by a wide margin: ~9.2× faster than a live SQLite
|
|
53
53
|
read and ~61–145× faster than live MySQL/Redis reads. Unix sockets are
|
|
@@ -74,16 +74,16 @@ See [SPEC.md](./SPEC.md) for the full specification.
|
|
|
74
74
|
|
|
75
75
|
## 📊 Stats
|
|
76
76
|
|
|
77
|
-
|
|
|
78
|
-
|
|
79
|
-
| **Version**
|
|
80
|
-
| **Downloads / month** | [](https://www.npmjs.com/package/bun-shared-memory)
|
|
81
|
-
| **Stars**
|
|
82
|
-
| **Forks**
|
|
83
|
-
| **Open issues**
|
|
84
|
-
| **Open PRs**
|
|
85
|
-
| **License**
|
|
86
|
-
| **Language**
|
|
77
|
+
| | |
|
|
78
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| **Version** | [](https://www.npmjs.com/package/bun-shared-memory) |
|
|
80
|
+
| **Downloads / month** | [](https://www.npmjs.com/package/bun-shared-memory) |
|
|
81
|
+
| **Stars** | [](https://github.com/Kapsonfire/bun-shared-memory) |
|
|
82
|
+
| **Forks** | [](https://github.com/Kapsonfire/bun-shared-memory) |
|
|
83
|
+
| **Open issues** | [](https://github.com/Kapsonfire/bun-shared-memory/issues) |
|
|
84
|
+
| **Open PRs** | [](https://github.com/Kapsonfire/bun-shared-memory/pulls) |
|
|
85
|
+
| **License** | [](https://github.com/Kapsonfire/bun-shared-memory/blob/main/LICENSE) |
|
|
86
|
+
| **Language** | [](https://www.typescriptlang.org) |
|
|
87
87
|
|
|
88
88
|
## Data Types
|
|
89
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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,
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/bun": "latest",
|
|
50
|
+
"cofusion": "^0.0.1",
|
|
50
51
|
"elysia": "^1.4.30"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/growable-buffer.ts
CHANGED
|
@@ -93,6 +93,16 @@ export function largestPowerOfTwo(n: number): number {
|
|
|
93
93
|
return 2 ** Math.floor(Math.log2(n));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Minimum size of a slab-allocated block. The free-list metadata written by
|
|
98
|
+
* `free` (next pointer + size = 8 bytes) must fit inside the block, otherwise
|
|
99
|
+
* it would overwrite the following block. All blocks are padded up to at
|
|
100
|
+
* least this size.
|
|
101
|
+
*/
|
|
102
|
+
const MIN_BLOCK_SIZE = 8;
|
|
103
|
+
|
|
104
|
+
export { MIN_BLOCK_SIZE };
|
|
105
|
+
|
|
96
106
|
export class GrowableBuffer {
|
|
97
107
|
private chunks: Chunk[] = [];
|
|
98
108
|
private readonly maxByteLength: number;
|
|
@@ -109,6 +119,17 @@ export class GrowableBuffer {
|
|
|
109
119
|
* existing data at offset 0.
|
|
110
120
|
*/
|
|
111
121
|
private readonly cursor: BigInt64Array;
|
|
122
|
+
/**
|
|
123
|
+
* Shared head of the free list. Stored in a SharedArrayBuffer so all workers
|
|
124
|
+
* that reconstruct this buffer from `fromBuffers()` see the same free list
|
|
125
|
+
* and reuse freed blocks instead of appending at the end.
|
|
126
|
+
*
|
|
127
|
+
* The free list is a singly-linked list of freed blocks. Each freed block
|
|
128
|
+
* stores, in its first 4 bytes, the byte offset of the next free block (or
|
|
129
|
+
* -1 for the end of the list). The head is the offset of the first free
|
|
130
|
+
* block, or -1 when the list is empty.
|
|
131
|
+
*/
|
|
132
|
+
private readonly freeListHead: Int32Array;
|
|
112
133
|
/** Smallest chunk index that is not yet active (see `activate`). */
|
|
113
134
|
private nextChunkToActivate = 0;
|
|
114
135
|
|
|
@@ -120,12 +141,20 @@ export class GrowableBuffer {
|
|
|
120
141
|
singleBuffer = false,
|
|
121
142
|
chargeInitial = true,
|
|
122
143
|
cursorBuffer?: SharedArrayBuffer,
|
|
144
|
+
freeListBuffer?: SharedArrayBuffer,
|
|
123
145
|
) {
|
|
124
146
|
this.singleBuffer = singleBuffer;
|
|
125
147
|
this.budget = budget;
|
|
126
148
|
this.cursor = new BigInt64Array(
|
|
127
149
|
cursorBuffer ?? new SharedArrayBuffer(BigInt64Array.BYTES_PER_ELEMENT),
|
|
128
150
|
);
|
|
151
|
+
this.freeListHead = new Int32Array(
|
|
152
|
+
freeListBuffer ?? new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
|
|
153
|
+
);
|
|
154
|
+
// Initialize the free list head to -1 (empty) on first construction.
|
|
155
|
+
if (freeListBuffer === undefined) {
|
|
156
|
+
Atomics.store(this.freeListHead, 0, -1);
|
|
157
|
+
}
|
|
129
158
|
if (singleBuffer && typeof initialSizeOrBuffer !== "number") {
|
|
130
159
|
const buffer = Array.isArray(initialSizeOrBuffer)
|
|
131
160
|
? initialSizeOrBuffer[0]
|
|
@@ -285,6 +314,11 @@ export class GrowableBuffer {
|
|
|
285
314
|
return this.cursor.buffer as SharedArrayBuffer;
|
|
286
315
|
}
|
|
287
316
|
|
|
317
|
+
/** The shared free-list head buffer (for export to workers). */
|
|
318
|
+
get freeListBuffer(): SharedArrayBuffer {
|
|
319
|
+
return this.freeListHead.buffer as SharedArrayBuffer;
|
|
320
|
+
}
|
|
321
|
+
|
|
288
322
|
/** Total logical capacity across all chunks (active or not). */
|
|
289
323
|
get capacity(): number {
|
|
290
324
|
return this.singleBuffer
|
|
@@ -391,6 +425,148 @@ export class GrowableBuffer {
|
|
|
391
425
|
Atomics.add(this.cursor, 0, BigInt(n));
|
|
392
426
|
}
|
|
393
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Atomically reserve `size` bytes at the end of the buffer and return the
|
|
430
|
+
* start offset. This is the lock-free bump-allocation primitive: the shared
|
|
431
|
+
* cursor is incremented with a single `Atomics.add`, so concurrent workers
|
|
432
|
+
* each get a distinct, non-overlapping range without a mutex. The caller
|
|
433
|
+
* writes its data into `[offset, offset + size)`.
|
|
434
|
+
*
|
|
435
|
+
* Chunk growth is preserved: `ensureCapacity` activates any chunks needed to
|
|
436
|
+
* cover the reserved range. Because `SharedArrayBuffer.grow()` is visible
|
|
437
|
+
* across workers, activation needs no broadcast.
|
|
438
|
+
*
|
|
439
|
+
* Unlike `alloc`, this never consults the free list, so freed blocks are not
|
|
440
|
+
* reused on this path (dead space is reclaimed by defragmentation).
|
|
441
|
+
*/
|
|
442
|
+
reserve(size: number): number {
|
|
443
|
+
// Lock-free bump allocation with chunk-boundary alignment. The shared
|
|
444
|
+
// cursor is advanced with a compare-exchange loop so concurrent workers
|
|
445
|
+
// each get a distinct, non-overlapping range without a mutex. If the
|
|
446
|
+
// block would cross a chunk boundary, the offset is aligned up to the next
|
|
447
|
+
// chunk so the caller's typed writes (setInt32 etc.) never straddle two
|
|
448
|
+
// chunks (those accessors only write within a single chunk).
|
|
449
|
+
while (true) {
|
|
450
|
+
const current = Number(Atomics.load(this.cursor, 0));
|
|
451
|
+
let offset = current;
|
|
452
|
+
if (!this.singleBuffer) {
|
|
453
|
+
const within = current & (this.chunkSize - 1);
|
|
454
|
+
if (within + size > this.chunkSize) {
|
|
455
|
+
offset = current + (this.chunkSize - within);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const next = offset + size;
|
|
459
|
+
if (
|
|
460
|
+
Atomics.compareExchange(
|
|
461
|
+
this.cursor,
|
|
462
|
+
0,
|
|
463
|
+
BigInt(current),
|
|
464
|
+
BigInt(next),
|
|
465
|
+
) === BigInt(current)
|
|
466
|
+
) {
|
|
467
|
+
this.ensureCapacity(next);
|
|
468
|
+
return offset;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Allocate `size` bytes, reusing a freed block from the free list when one
|
|
475
|
+
* of at least `size` bytes is available (first-fit), otherwise appending at
|
|
476
|
+
* the end of the buffer.
|
|
477
|
+
*
|
|
478
|
+
* Returns the byte offset of the allocated block and the actual reserved
|
|
479
|
+
* (padded) size. The caller must write the block's data and then call
|
|
480
|
+
* `advance(reserved)` only when the block was appended (not reused), because
|
|
481
|
+
* a reused block is already counted as used.
|
|
482
|
+
*
|
|
483
|
+
* @returns the offset, the reserved size, and whether the block was freshly
|
|
484
|
+
* appended (true) or reused from the free list (false).
|
|
485
|
+
*/
|
|
486
|
+
alloc(size: number): { offset: number; appended: boolean; size: number } {
|
|
487
|
+
// Pad small blocks up to MIN_BLOCK_SIZE so the free-list metadata written
|
|
488
|
+
// by `free` always fits inside the block and never overwrites a neighbour.
|
|
489
|
+
size = Math.max(size, MIN_BLOCK_SIZE);
|
|
490
|
+
// First-fit search over the free list for a block of at least `size`.
|
|
491
|
+
let prev = -1;
|
|
492
|
+
let cur = Atomics.load(this.freeListHead, 0);
|
|
493
|
+
while (cur !== -1) {
|
|
494
|
+
const next = this.getInt32(cur);
|
|
495
|
+
if (this.blockSize(cur) >= size) {
|
|
496
|
+
// Unlink `cur` from the free list.
|
|
497
|
+
if (prev === -1) {
|
|
498
|
+
Atomics.store(this.freeListHead, 0, next);
|
|
499
|
+
} else {
|
|
500
|
+
this.setInt32(prev, next);
|
|
501
|
+
}
|
|
502
|
+
return { offset: cur, appended: false, size };
|
|
503
|
+
}
|
|
504
|
+
prev = cur;
|
|
505
|
+
cur = next;
|
|
506
|
+
}
|
|
507
|
+
// No reusable block; append at the end.
|
|
508
|
+
const offset = this.byteLength;
|
|
509
|
+
this.ensureCapacity(offset + size);
|
|
510
|
+
return { offset, appended: true, size };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Free a block previously allocated with `alloc`, pushing it onto the free
|
|
515
|
+
* list so it can be reused. Detects double frees: freeing a block that is
|
|
516
|
+
* already on the free list throws an Error.
|
|
517
|
+
*
|
|
518
|
+
* The block's first 8 bytes are overwritten with the free-list metadata
|
|
519
|
+
* (next pointer + size), so the caller must not read the block's data after
|
|
520
|
+
* freeing it.
|
|
521
|
+
*/
|
|
522
|
+
free(offset: number, size: number): void {
|
|
523
|
+
// Pad to MIN_BLOCK_SIZE to match the size `alloc` actually reserved, so
|
|
524
|
+
// `blockSize` reports the real block size for reuse decisions.
|
|
525
|
+
size = Math.max(size, MIN_BLOCK_SIZE);
|
|
526
|
+
// Double-free detection: a block already on the free list must not be
|
|
527
|
+
// freed again. Walk the list (free is not a hot path, so the O(n) scan is
|
|
528
|
+
// acceptable).
|
|
529
|
+
let cur = Atomics.load(this.freeListHead, 0);
|
|
530
|
+
while (cur !== -1) {
|
|
531
|
+
if (cur === offset) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Double free detected at offset ${offset} (size ${size})`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
cur = this.getInt32(cur);
|
|
537
|
+
}
|
|
538
|
+
const head = Atomics.load(this.freeListHead, 0);
|
|
539
|
+
this.setInt32(offset, head);
|
|
540
|
+
this.setInt32(offset + 4, size);
|
|
541
|
+
Atomics.store(this.freeListHead, 0, offset);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Read the size of a freed block, stored in bytes [4..7] when it was
|
|
546
|
+
* freed. Used by `alloc` to check whether a freed block is large enough.
|
|
547
|
+
*/
|
|
548
|
+
private blockSize(offset: number): number {
|
|
549
|
+
return this.getInt32(offset + 4);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Walk the free list and return the number of freed blocks and the total
|
|
554
|
+
* bytes they occupy. Used to measure fragmentation: many small freed blocks
|
|
555
|
+
* scattered through the buffer indicate high fragmentation, which a
|
|
556
|
+
* defragmentation pass can compact.
|
|
557
|
+
*/
|
|
558
|
+
freeListStats(): { count: number; bytes: number } {
|
|
559
|
+
let count = 0;
|
|
560
|
+
let bytes = 0;
|
|
561
|
+
let cur = Atomics.load(this.freeListHead, 0);
|
|
562
|
+
while (cur !== -1) {
|
|
563
|
+
count++;
|
|
564
|
+
bytes += this.blockSize(cur);
|
|
565
|
+
cur = this.getInt32(cur);
|
|
566
|
+
}
|
|
567
|
+
return { count, bytes };
|
|
568
|
+
}
|
|
569
|
+
|
|
394
570
|
/**
|
|
395
571
|
* Reset the used length to `n` bytes. Used by defragmentation after
|
|
396
572
|
* compacting live data to the front of the buffer. Chunks beyond the
|
|
@@ -399,6 +575,9 @@ export class GrowableBuffer {
|
|
|
399
575
|
*/
|
|
400
576
|
reset(n: number): void {
|
|
401
577
|
Atomics.store(this.cursor, 0, BigInt(n));
|
|
578
|
+
// After compaction the free list's offsets are invalid (blocks moved), so
|
|
579
|
+
// clear it. Freed blocks are re-added lazily on the next `free`.
|
|
580
|
+
Atomics.store(this.freeListHead, 0, -1);
|
|
402
581
|
if (this.singleBuffer) return;
|
|
403
582
|
const neededChunks = Math.ceil(n / this.chunkSize);
|
|
404
583
|
for (let i = 0; i < this.chunks.length; i++) {
|