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/CHANGELOG.md CHANGED
@@ -5,9 +5,99 @@ 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.7] - 2026-11-02
9
+
10
+ ### Added
11
+
12
+ - Keep the Index Table in a single growable `SharedArrayBuffer` (capped at
13
+ 4 GiB) instead of the chunked logic, so it stays contiguous and cheap to
14
+ address. `SharedMemoryOptions.indexTableMaxByteLength` configures its cap
15
+ (default 4 GiB).
16
+ - `maxTotalBytes` now caps the combined physical size of DataTable,
17
+ StringTable, and ArrayTable only (the Index Table is excluded). The budget
18
+ is enforced by an atomic `SharedBudget` counter shared across workers that
19
+ reserves memory before each chunk growth, so the three tables cannot exceed
20
+ the limit even when multiple workers write concurrently.
21
+ - Enforce size limits regardless of the configured values: `chunkSize` is at
22
+ least 4 MiB, `maxTotalBytes`/`maxByteLength` are at least 32 MiB, and
23
+ `chunkSize` is raised as needed so no table needs more than 1024 chunks.
24
+ `SharedMemory.maxByteLength` and `SharedMemory.maxTotalBytes` are now public
25
+ read-only fields.
26
+
27
+ ### Performance
28
+
29
+ - Reclaim memory from inactive chunks after defragmentation. A
30
+ `SharedArrayBuffer` cannot be shrunk, so `GrowableBuffer` replaces each
31
+ inactive chunk with a fresh 4-byte `SharedArrayBuffer`, releasing the old
32
+ full-size buffer so its memory is freed. `defragment()` returns the new
33
+ chunk lists and the main thread broadcasts them to workers via a
34
+ `__chunkUpdate` message; workers apply them with `applyChunkUpdate()`. This
35
+ only runs during defragmentation (rare), so the hot path is unaffected.
36
+ - Optimize the budget write hot path: `GrowableBuffer` tracks
37
+ `nextChunkToActivate` (the first not-yet-active chunk) so `ensureCapacity`
38
+ only activates new chunks instead of re-scanning every already-active chunk
39
+ on each write (was O(n) per write as the dataset grew). Writes ~9% faster.
40
+
41
+ ### Changed
42
+
43
+ - Stabilize the real-world benchmark against CPU contention. The shared-memory
44
+ methodology finishes in tens of milliseconds, so it is sensitive to load
45
+ from other processes (IDE, language servers). Each worker now runs several
46
+ measured samples (default 5) and reports its fastest; each methodology is
47
+ run several times (default 3) and the fastest run is kept. Because
48
+ contention can only slow a run down, the minimum is the best estimate of
49
+ true throughput.
50
+
51
+ ### Fixed
52
+
53
+ - Resolve chunk offsets with an unsigned right shift (`offset >>> chunkShift`)
54
+ instead of a signed one. A signed 32-bit shift turns offsets at or above
55
+ 2 GiB negative, so reads/writes past 2 GiB (e.g. in a >4 GiB chunked buffer)
56
+ hit the wrong chunk or threw. The Index Table and the Data/String/Array
57
+ tables can now be addressed at any offset below the 4 GiB single-SAB cap.
58
+ Reported by [mizulu](https://github.com/mizulu).
59
+
60
+ ## [0.1.6] - 2026-11-02
61
+
62
+ ### Added
63
+
64
+ - Support shared memory larger than 4 GiB. The single-`SharedArrayBuffer`
65
+ per-buffer architecture is replaced with a chunked `GrowableBuffer`: each
66
+ buffer is backed by multiple equal-size chunks (32 MiB default, power of
67
+ two), so a buffer can exceed the 4 GiB single-SAB limit. Offsets resolve
68
+ O(1) via bit shifts (`offset >> chunkShift`).
69
+ - `SharedMemoryOptions.chunkSize` to configure the chunk size (the minimum
70
+ growth step).
71
+ - Chunks are pre-allocated at construction (4 bytes each, with
72
+ `maxByteLength = chunkSize`) and activated on first access via
73
+ `SharedArrayBuffer.grow()`. Because `grow()` is visible across all workers
74
+ sharing a buffer, a worker can grow a chunk and every other worker sees it
75
+ immediately — no postMessage broadcast is needed for chunk activation.
76
+
77
+ ### Changed
78
+
79
+ - `exportBuffers()`/`fromBuffers()` now pass chunk lists instead of a single
80
+ buffer; `fromBuffers` reuses the shared `SharedArrayBuffer`s directly (no
81
+ copy) so writes stay visible across workers. The chunk size is derived from
82
+ each buffer's `maxByteLength` (preserved across `postMessage`), not its
83
+ current 4-byte length.
84
+ - Remove the 4 GiB cap on the per-buffer maximum in the `SharedMemory`
85
+ constructor (the chunked `GrowableBuffer` already bypasses the single-SAB
86
+ limit).
87
+
88
+ ### Performance
89
+
90
+ - After defragmentation, unused chunks are marked inactive:
91
+ `GrowableBuffer.reset()` deactivates chunks beyond the compacted region so
92
+ the GC can reclaim their storage.
93
+ - The chunked accessor adds an indirect `chunks[index].view` lookup on the
94
+ write hot path (the price of >4 GiB support): writes ~-15%, array writes
95
+ ~-20%. Reads are unchanged (lock-free cache path).
96
+
8
97
  ## [0.1.5] - 2026-11-02
9
98
 
10
99
  ### Changed
100
+
11
101
  - Replace the `Proxy` used for object `SharedRef`s with hard-baked
12
102
  `Object.defineProperty` accessor getters/setters, so property reads skip the
13
103
  Proxy trap on the hot path (~3× faster object property reads). Methods
@@ -24,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
24
114
  ## [0.1.4] - 2026-11-01
25
115
 
26
116
  ### Changed
117
+
27
118
  - Declare `"sideEffects": false` in `package.json` so bundlers can tree-shake
28
119
  the library (the modules have no side effects on import).
29
120
  - Remove the `typescript` peer dependency. The library ships as TypeScript
@@ -33,6 +124,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
124
  ## [0.1.3] - 2026-11-01
34
125
 
35
126
  ### Added
127
+
36
128
  - Real-world benchmark (`bench/real-world/`) comparing shared memory against
37
129
  live SQLite, MySQL, and Redis via an Elysia mini API.
38
130
  - Redis as a fourth benchmark methodology (in Docker).
@@ -41,6 +133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
133
  - Real-world benchmark results and methodology documented in the README.
42
134
 
43
135
  ### Changed
136
+
44
137
  - MySQL/Redis connections are established before the benchmark (not per
45
138
  request) so connection setup is not measured as overhead.
46
139
  - The benchmark always runs both TCP and Unix-socket transports; the socket
@@ -49,16 +142,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
49
142
  ## [0.1.2] - 2026-11-01
50
143
 
51
144
  ### Changed
145
+
52
146
  - Unref the maintenance timer so the process can exit naturally.
53
147
  - Document `init()` as the first call and how to configure memory via `init()`.
54
148
 
55
149
  ## [0.1.1] - 2026-11-01
56
150
 
57
151
  ### Added
152
+
58
153
  - Repository metadata and npm publishing preparation.
59
154
  - README with package name and install instructions.
60
155
 
61
156
  ### Changed
157
+
62
158
  - Optimize the read hot path: direct cache on the proxy target.
63
159
  - Optimize `Mutex`: skip notify when no worker is waiting.
64
160
  - Start the refcount coordinator/tracker automatically in `init()`.
@@ -67,6 +163,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
67
163
  ## [0.1.0] - 2026-11-01
68
164
 
69
165
  ### Added
166
+
70
167
  - Initial release: `Shared<T>` Proxy-based API over a 4-buffer shared memory
71
168
  architecture (Index Table, DataTable, append-only StringTable, ArrayTable).
72
169
  - Atomics-based synchronization (`Mutex`, `Notifier`).
@@ -75,6 +172,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
75
172
  - O(1) in-place `push`/`pop` with capacity-based array growth.
76
173
  - Benchmarks for all data types vs native.
77
174
 
175
+ [0.1.7]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.6...v0.1.7
176
+ [0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
177
+ [0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
78
178
  [0.1.4]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.3...v0.1.4
79
179
  [0.1.3]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.2...v0.1.3
80
180
  [0.1.2]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.1...v0.1.2
package/README.md CHANGED
@@ -37,20 +37,21 @@ is benchmarked sequentially with 4 workers. See
37
37
  [`bench/real-world/README.md`](bench/real-world/README.md) for setup.
38
38
 
39
39
  Measured on Linux (Bun 1.4.4, 100,000 requests per methodology, sorted by
40
- speed; MySQL/Redis benchmarked over both TCP and Unix socket):
40
+ speed; MySQL/Redis benchmarked over both TCP and Unix socket; each methodology
41
+ is the minimum over 3 runs × 5 samples to filter CPU-contention noise):
41
42
 
42
43
  | Methodology | Transport | Requests | Elapsed (ms) | req/s | vs. shared |
43
44
  |-------------|-----------|----------|--------------|------------|------------|
44
- | `shared` | — | 100,000 | 52 | 1,925,042 | — |
45
- | `sqlite` | — | 100,000 | 187 | 535,469 | 3.6× slower |
46
- | `redis` | socket | 100,000 | 1,688 | 59,233 | 32.5× slower |
47
- | `mysql` | socket | 100,000 | 1,823 | 54,861 | 35.1× slower |
48
- | `redis` | tcp | 100,000 | 2,863 | 34,923 | 55.1× slower |
49
- | `mysql` | tcp | 100,000 | 3,281 | 30,476 | 63.2× slower |
50
-
51
- Shared memory is the fastest by a wide margin: ~3.6× faster than a live SQLite
52
- read and ~33–63× faster than live MySQL/Redis reads. Unix sockets are
53
- significantly faster than TCP for MySQL/Redis (~1.7×), but both remain far
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
+
52
+ Shared memory is the fastest by a wide margin: ~9.2× faster than a live SQLite
53
+ read and ~61–145× faster than live MySQL/Redis reads. Unix sockets are
54
+ significantly faster than TCP for MySQL/Redis (~1.8×), but both remain far
54
55
  slower than shared memory.
55
56
 
56
57
  ## ✨ Features
@@ -305,22 +306,24 @@ A complete runnable example is in [`examples/worker/`](examples/worker/).
305
306
 
306
307
  ## Memory budget
307
308
 
308
- Shared memory is split across several growable `SharedArrayBuffer`s (the
309
- index, data, string, and array tables; each key's version counter lives in its
310
- index entry). A single `SharedArrayBuffer` can be at most **4 GiB**. By default
311
- each buffer grows independently up to its own `maxByteLength`, so the _total_
312
- memory can be several times that.
309
+ The Index Table (including each key's version counter) uses one growable
310
+ `SharedArrayBuffer`, capped at **4 GiB** by default (configurable via
311
+ `indexTableMaxByteLength`). The DataTable, StringTable, and ArrayTable use
312
+ chunks and can individually exceed 4 GiB. By default each table grows up to
313
+ its own `maxByteLength`, so the _total_ memory can be several times that.
313
314
 
314
315
  Configure the buffers when calling `init()` — pass `initialSize` (each
315
- buffer's starting size, avoiding a grow on first use) and `maxByteLength`
316
- (each buffer's cap). To cap the **total** memory across all buffers, pass
317
- `maxTotalBytes`:
316
+ table's starting size, avoiding a grow on first use) and `maxByteLength`
317
+ (each table's cap). To cap the combined physical memory of DataTable,
318
+ StringTable, and ArrayTable, pass `maxTotalBytes`. The Index Table is excluded
319
+ from this budget. Its growth and chunk growth in workers reserve memory from
320
+ the same atomic budget before allocating, so the three tables cannot exceed it.
318
321
 
319
322
  ```ts
320
323
  import { init } from "bun-shared-memory";
321
324
 
322
- // Each buffer starts at 64 KiB and grows up to 4 GiB; the sum of all buffers'
323
- // physical sizes is additionally capped at 4 GiB (maxTotalBytes).
325
+ // The Index Table grows up to 4 GiB independently. DataTable, StringTable, and
326
+ // ArrayTable together are capped at 4 GiB.
324
327
  await init({
325
328
  initialSize: 64 * 1024,
326
329
  maxByteLength: 4 * 1024 * 1024 * 1024,
@@ -328,14 +331,24 @@ await init({
328
331
  });
329
332
  ```
330
333
 
331
- `maxTotalBytes` caps the **sum** of all buffers' physical sizes, so the total
332
- stays within the budget. If a write would exceed the budget, a `RangeError` is
333
- thrown. Without `maxTotalBytes`, each buffer simply grows up to its own
334
- `maxByteLength`.
334
+ `maxTotalBytes` caps the **sum** of the physical sizes of DataTable,
335
+ StringTable, and ArrayTable, including across workers. If a write would exceed
336
+ the budget, a `RangeError` is thrown before the chunk grows. Without
337
+ `maxTotalBytes`, each table simply grows up to its own `maxByteLength`.
335
338
 
336
- The budget is shared dynamically — buffers grow as needed, but only while the
337
- sum of all buffers stays within `maxTotalBytes`. If a write would exceed the
338
- budget, a `RangeError` is thrown.
339
+ ### Size limits
340
+
341
+ The following limits are enforced regardless of the configured values:
342
+
343
+ - **`chunkSize` is at least 4 MiB.** A smaller value is raised to 4 MiB.
344
+ - **`maxTotalBytes` / `maxByteLength` are at least 32 MiB.** A smaller value
345
+ is raised to 32 MiB.
346
+ - **No table uses more than 1024 chunks.** `chunkSize` is raised as needed so
347
+ that `ceil(maxByteLength / chunkSize) ≤ 1024` (e.g. an 8 GiB table with a
348
+ 4 MiB chunk would need 2048 chunks, so the chunk size is raised to 8 MiB).
349
+
350
+ These keep the chunk count bounded (so the pre-allocated chunk list stays
351
+ small) and avoid pathological tiny chunks.
339
352
 
340
353
  ## Array capacity
341
354
 
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.7",
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,