bun-shared-memory 0.1.1 โ†’ 0.1.3

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/README.md CHANGED
@@ -1,4 +1,25 @@
1
- # bun-shared-memory
1
+ <div align="center">
2
+
3
+ # ๐Ÿง  bun-shared-memory
4
+
5
+ **Share data between Bun workers/threads using `SharedArrayBuffer`** โ€” zero-copy, 4-buffer architecture, Atomics-based synchronization.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/bun-shared-memory?style=for-the-badge&logo=npm&color=cb3837)](https://www.npmjs.com/package/bun-shared-memory)
8
+ [![npm downloads](https://img.shields.io/npm/dm/bun-shared-memory?style=for-the-badge&logo=npm&color=cb3837)](https://www.npmjs.com/package/bun-shared-memory)
9
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/bun-shared-memory?style=for-the-badge&color=blueviolet)](https://bundlephobia.com/package/bun-shared-memory)
10
+
11
+ [![GitHub stars](https://img.shields.io/github/stars/Kapsonfire/bun-shared-memory?style=for-the-badge&logo=github&color=181717)](https://github.com/Kapsonfire/bun-shared-memory)
12
+ [![GitHub forks](https://img.shields.io/github/forks/Kapsonfire/bun-shared-memory?style=for-the-badge&logo=github&color=181717)](https://github.com/Kapsonfire/bun-shared-memory)
13
+ [![GitHub issues](https://img.shields.io/github/issues/Kapsonfire/bun-shared-memory?style=for-the-badge&logo=github&color=181717)](https://github.com/Kapsonfire/bun-shared-memory/issues)
14
+ [![GitHub license](https://img.shields.io/github/license/Kapsonfire/bun-shared-memory?style=for-the-badge&color=green)](https://github.com/Kapsonfire/bun-shared-memory/blob/main/LICENSE)
15
+
16
+ [![Bun](https://img.shields.io/badge/Bun-%E2%89%A51.4.0-black?style=for-the-badge&logo=bun&color=14151a)](https://bun.sh)
17
+ [![TypeScript](https://img.shields.io/badge/TypeScript-7-blue?style=for-the-badge&logo=typescript&color=3178c6)](https://www.typescriptlang.org)
18
+ [![Tests](https://img.shields.io/badge/tests-passing-brightgreen?style=for-the-badge&logo=checkmarx)](https://github.com/Kapsonfire/bun-shared-memory/actions)
19
+
20
+ </div>
21
+
22
+ ---
2
23
 
3
24
  A TypeScript library for the **Bun** runtime that enables sharing data between workers/threads using `SharedArrayBuffer`.
4
25
 
@@ -7,7 +28,42 @@ A TypeScript library for the **Bun** runtime that enables sharing data between w
7
28
  > onward. Core shared-memory operations (including `SharedArrayBuffer.grow()`)
8
29
  > work on earlier versions, but the automatic worker buffer-sharing does not.
9
30
 
10
- ## Install
31
+ ## ๐Ÿ“ˆ Real-world benchmark
32
+
33
+ `bun run bench:real` compares four methodologies for serving a "get user"
34
+ request through an [Elysia](https://elysiajs.com) mini API. Requests are built
35
+ by hand and passed directly to `app.fetch()` (no HTTP server); each methodology
36
+ is benchmarked sequentially with 4 workers. See
37
+ [`bench/real-world/README.md`](bench/real-world/README.md) for setup.
38
+
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):
41
+
42
+ | Methodology | Transport | Requests | Elapsed (ms) | req/s | vs. shared |
43
+ |-------------|-----------|----------|--------------|------------|------------|
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
54
+ slower than shared memory.
55
+
56
+ ## โœจ Features
57
+
58
+ - ๐Ÿš€ **Zero-copy** shared memory between workers/threads via `SharedArrayBuffer`
59
+ - ๐Ÿงฉ **4-buffer architecture** โ€” Index Table, DataTable, append-only StringTable, ArrayTable
60
+ - ๐Ÿ”’ **Atomics-based synchronization** โ€” re-entrant `Mutex` + `Notifier`
61
+ - โšก **Read & offset caches** โ€” lock-free fast path, cross-worker invalidation via version cells
62
+ - ๐Ÿงน **Reference counting & defragmentation** โ€” automatic memory reclamation
63
+ - ๐Ÿช„ **Proxy-based API** โ€” `Shared()` with direct getter/setter access
64
+ - ๐Ÿงต **Automatic worker integration** โ€” no manual buffer serialization
65
+
66
+ ## ๐Ÿ“ฆ Install
11
67
 
12
68
  ```sh
13
69
  bun add bun-shared-memory
@@ -15,15 +71,18 @@ bun add bun-shared-memory
15
71
 
16
72
  See [SPEC.md](./SPEC.md) for the full specification.
17
73
 
18
- ## Status
74
+ ## ๐Ÿ“Š Stats
19
75
 
20
- โœ… **M3 โ€” Synchronization**: `Atomics`-based locking (`Mutex`) and notifications (`Notifier`) between workers, integrated into all shared-memory operations.
21
-
22
- โœ… **Read cache**: reads are cached per worker and invalidated cross-worker via a per-key version counter.
23
-
24
- โœ… **Reference counting & defragmentation**: workers acquire/release references to shared objects/arrays (released on GC), the main thread tracks which worker holds what, and a defragmentation pass reclaims dead space when free space is low.
25
-
26
- โœ… **M4 โ€” Worker integration**: a worker can simply call `Shared()` and automatically share the same buffers as the main thread. Both sides call `init()` โ€” the main thread answers buffer requests, each worker requests the shared buffers โ€” no manual buffer serialization needed.
76
+ | | |
77
+ |---|---|
78
+ | **Version** | [![npm version](https://img.shields.io/npm/v/bun-shared-memory?color=cb3837)](https://www.npmjs.com/package/bun-shared-memory) |
79
+ | **Downloads / month** | [![npm downloads](https://img.shields.io/npm/dm/bun-shared-memory?color=cb3837)](https://www.npmjs.com/package/bun-shared-memory) |
80
+ | **Stars** | [![GitHub stars](https://img.shields.io/github/stars/Kapsonfire/bun-shared-memory?color=181717)](https://github.com/Kapsonfire/bun-shared-memory) |
81
+ | **Forks** | [![GitHub forks](https://img.shields.io/github/forks/Kapsonfire/bun-shared-memory?color=181717)](https://github.com/Kapsonfire/bun-shared-memory) |
82
+ | **Open issues** | [![GitHub issues](https://img.shields.io/github/issues/Kapsonfire/bun-shared-memory?color=181717)](https://github.com/Kapsonfire/bun-shared-memory/issues) |
83
+ | **Open PRs** | [![GitHub pull requests](https://img.shields.io/github/issues-pr/Kapsonfire/bun-shared-memory?color=181717)](https://github.com/Kapsonfire/bun-shared-memory/pulls) |
84
+ | **License** | [![License](https://img.shields.io/github/license/Kapsonfire/bun-shared-memory?color=green)](https://github.com/Kapsonfire/bun-shared-memory/blob/main/LICENSE) |
85
+ | **Language** | [![TypeScript](https://img.shields.io/badge/TypeScript-7-blue?color=3178c6)](https://www.typescriptlang.org) |
27
86
 
28
87
  ## Data Types
29
88
 
@@ -34,8 +93,15 @@ See [SPEC.md](./SPEC.md) for the full specification.
34
93
 
35
94
  ## Usage
36
95
 
96
+ Always call `init()` first โ€” on both the main thread and in every worker. It
97
+ sets up the shared-memory handshake and (optionally) configures the buffers.
98
+
37
99
  ```ts
38
- import { Shared } from "bun-shared-memory";
100
+ import { Shared, init } from "bun-shared-memory";
101
+
102
+ // Main thread: call init() once at startup. It creates the shared memory
103
+ // (with the given buffer sizes) and answers buffer requests from workers.
104
+ await init({ initialSize: 64 * 1024, maxByteLength: 4 * 1024 * 1024 * 1024 });
39
105
 
40
106
  // Get a wrapper to a named value in shared memory.
41
107
  // If it doesn't exist, create it with the provided initial value.
@@ -67,6 +133,10 @@ const user2 = await Shared<User>("user-2", async () => {
67
133
  });
68
134
  ```
69
135
 
136
+ In a worker, `await init()` requests the shared buffers from the main thread
137
+ and reconstructs the same memory, so `Shared()` reads/writes the same data as
138
+ the main thread. See [Worker integration](#worker-integration) below.
139
+
70
140
  ## Synchronization
71
141
 
72
142
  All shared-memory reads and writes are protected by an `Atomics`-based mutex, so concurrent access from multiple workers is safe.
@@ -147,7 +217,7 @@ defragmented โ€” no manual setup needed.
147
217
  import { Shared, init } from "bun-shared-memory";
148
218
 
149
219
  // Main thread: init() starts the coordinator automatically.
150
- init();
220
+ await init();
151
221
  const user = Shared("user-1", { name: "Alice" });
152
222
  const copy = user.read(); // acquires a ref; released when `copy` is GC'd
153
223
  ```
@@ -192,8 +262,11 @@ workers, and each worker requests the shared buffers from the main thread.
192
262
  // main.ts
193
263
  import { Shared, init } from "bun-shared-memory";
194
264
 
265
+ // Always call init() first. It creates the shared memory (with the given
266
+ // buffer sizes) and answers buffer requests from every new worker.
267
+ await init({ initialSize: 64 * 1024, maxByteLength: 4 * 1024 * 1024 * 1024 });
268
+
195
269
  const user = Shared("user-1", { name: "Alice", age: 30 });
196
- init(); // answer buffer requests from every new worker
197
270
  ```
198
271
 
199
272
  ```ts
@@ -223,6 +296,11 @@ buffer-request listener and starts the refcount coordinator (auto-maintenance
223
296
  > `Shared()` in a worker without `init()` throws an error rather than silently
224
297
  > creating a separate, unshared memory.
225
298
 
299
+ The coordinator's maintenance timer is unref'd, so it does not keep the
300
+ process alive โ€” the process exits naturally once no other work is pending
301
+ (e.g. after the last worker closes). You don't need to call `stopInit()` or
302
+ `process.exit()` to terminate.
303
+
226
304
  A complete runnable example is in [`examples/worker/`](examples/worker/).
227
305
 
228
306
  ## Memory budget
@@ -233,21 +311,28 @@ index entry). A single `SharedArrayBuffer` can be at most **4 GiB**. By default
233
311
  each buffer grows independently up to its own `maxByteLength`, so the _total_
234
312
  memory can be several times that.
235
313
 
236
- To cap the **total** memory across all buffers, pass `maxTotalBytes` when
237
- creating the memory (or to `init()`):
314
+ 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`:
238
318
 
239
319
  ```ts
240
- import { SharedMemory, init } from "bun-shared-memory";
241
-
242
- // Total budget of 4 GiB shared across all buffers. Each buffer's own
243
- // maxByteLength is min(4 GiB, maxTotalBytes), and the sum of all buffers'
244
- // physical sizes is additionally capped at maxTotalBytes.
245
- const mem = new SharedMemory({ maxTotalBytes: 4 * 1024 * 1024 * 1024 });
246
-
247
- // Or set it when initializing the shared-memory handshake:
248
- await init({ maxTotalBytes: 4 * 1024 * 1024 * 1024 });
320
+ import { init } from "bun-shared-memory";
321
+
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).
324
+ await init({
325
+ initialSize: 64 * 1024,
326
+ maxByteLength: 4 * 1024 * 1024 * 1024,
327
+ maxTotalBytes: 4 * 1024 * 1024 * 1024,
328
+ });
249
329
  ```
250
330
 
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`.
335
+
251
336
  The budget is shared dynamically โ€” buffers grow as needed, but only while the
252
337
  sum of all buffers stays within `maxTotalBytes`. If a write would exceed the
253
338
  budget, a `RangeError` is thrown.
@@ -309,3 +394,21 @@ per-worker LRU cache grows dynamically (configurable via `internCapacity` /
309
394
  `internMax` on `SharedMemory`) and evicts least-recently-used entries once it
310
395
  reaches capacity. Because the StringTable is append-only, cached references stay
311
396
  valid for the lifetime of the buffer.
397
+
398
+ ### Real-world benchmark
399
+
400
+ See [๐Ÿ“ˆ Real-world benchmark](#-real-world-benchmark) at the top of this
401
+ README for the measured results.
402
+
403
+ ## ๐Ÿค Contributing
404
+
405
+ Contributions are welcome! Please open an issue or pull request.
406
+
407
+ - ๐Ÿ› [Report a bug](https://github.com/Kapsonfire/bun-shared-memory/issues/new?labels=bug)
408
+ - ๐Ÿ’ก [Request a feature](https://github.com/Kapsonfire/bun-shared-memory/issues/new?labels=enhancement)
409
+ - ๐Ÿ“– [Browse open issues](https://github.com/Kapsonfire/bun-shared-memory/issues)
410
+ - ๐Ÿ”€ [Open a pull request](https://github.com/Kapsonfire/bun-shared-memory/pulls)
411
+
412
+ ## ๐Ÿ“„ License
413
+
414
+ [MIT](./LICENSE) ยฉ [Kapsonfire](https://github.com/Kapsonfire)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-shared-memory",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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
  "module": "index.ts",
@@ -18,7 +18,10 @@
18
18
  "scripts": {
19
19
  "typecheck": "bun check",
20
20
  "test": "bun test",
21
- "bench": "bun bench/bench.ts"
21
+ "bench": "bun bench/bench.ts",
22
+ "bench:real": "bun bench/real-world/run.ts",
23
+ "bench:real:seed": "bun bench/real-world/seed.ts",
24
+ "bench:real:mysql": "docker compose -f bench/real-world/docker-compose.yml up -d"
22
25
  },
23
26
  "keywords": [
24
27
  "bun",
@@ -41,7 +44,10 @@
41
44
  "access": "public"
42
45
  },
43
46
  "devDependencies": {
44
- "@types/bun": "latest"
47
+ "@types/bun": "latest",
48
+ "elysia": "^1.4.30",
49
+ "ioredis": "^6.0.0",
50
+ "mysql2": "^3.24.6"
45
51
  },
46
52
  "peerDependencies": {
47
53
  "typescript": "^7"
package/src/refcount.ts CHANGED
@@ -316,6 +316,10 @@ export class RefCountCoordinator {
316
316
  this.mem.withLock(() => this.mem.defragment());
317
317
  }
318
318
  }, intervalMs);
319
+ // Unref the timer so it does not keep the process alive. The coordinator
320
+ // keeps draining events and defragmenting while the process runs, but the
321
+ // process can still exit naturally when no other work is pending.
322
+ (this.maintenanceTimer as unknown as { unref?: () => void }).unref?.();
319
323
  return () => this.stopAutoMaintenance();
320
324
  }
321
325