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 +127 -24
- package/package.json +9 -3
- package/src/refcount.ts +4 -0
package/README.md
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/bun-shared-memory)
|
|
8
|
+
[](https://www.npmjs.com/package/bun-shared-memory)
|
|
9
|
+
[](https://bundlephobia.com/package/bun-shared-memory)
|
|
10
|
+
|
|
11
|
+
[](https://github.com/Kapsonfire/bun-shared-memory)
|
|
12
|
+
[](https://github.com/Kapsonfire/bun-shared-memory)
|
|
13
|
+
[](https://github.com/Kapsonfire/bun-shared-memory/issues)
|
|
14
|
+
[](https://github.com/Kapsonfire/bun-shared-memory/blob/main/LICENSE)
|
|
15
|
+
|
|
16
|
+
[](https://bun.sh)
|
|
17
|
+
[](https://www.typescriptlang.org)
|
|
18
|
+
[](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
|
-
##
|
|
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
|
-
##
|
|
74
|
+
## ๐ Stats
|
|
19
75
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
76
|
+
| | |
|
|
77
|
+
|---|---|
|
|
78
|
+
| **Version** | [](https://www.npmjs.com/package/bun-shared-memory) |
|
|
79
|
+
| **Downloads / month** | [](https://www.npmjs.com/package/bun-shared-memory) |
|
|
80
|
+
| **Stars** | [](https://github.com/Kapsonfire/bun-shared-memory) |
|
|
81
|
+
| **Forks** | [](https://github.com/Kapsonfire/bun-shared-memory) |
|
|
82
|
+
| **Open issues** | [](https://github.com/Kapsonfire/bun-shared-memory/issues) |
|
|
83
|
+
| **Open PRs** | [](https://github.com/Kapsonfire/bun-shared-memory/pulls) |
|
|
84
|
+
| **License** | [](https://github.com/Kapsonfire/bun-shared-memory/blob/main/LICENSE) |
|
|
85
|
+
| **Language** | [](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
|
-
|
|
237
|
-
|
|
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 {
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
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
|
|