bun-shared-memory 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +310 -2
- package/index.ts +1 -0
- package/package.json +43 -4
- package/src/growable-buffer.ts +110 -0
- package/src/index.ts +21 -0
- package/src/memory.ts +1016 -0
- package/src/mutex.ts +92 -0
- package/src/notifier.ts +39 -0
- package/src/refcount.ts +358 -0
- package/src/shared.ts +957 -0
- package/src/worker.ts +292 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dennis Dudek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,311 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bun-shared-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript library for the **Bun** runtime that enables sharing data between workers/threads using `SharedArrayBuffer`.
|
|
4
|
+
|
|
5
|
+
> **Requires Bun ≥ 1.4.0.** The worker integration (M4) relies on the
|
|
6
|
+
> `process.on("worker")` handshake, which only works reliably from Bun 1.4.0
|
|
7
|
+
> onward. Core shared-memory operations (including `SharedArrayBuffer.grow()`)
|
|
8
|
+
> work on earlier versions, but the automatic worker buffer-sharing does not.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
bun add bun-shared-memory
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
See [SPEC.md](./SPEC.md) for the full specification.
|
|
17
|
+
|
|
18
|
+
## Status
|
|
19
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
## Data Types
|
|
29
|
+
|
|
30
|
+
- **Allowed**: primitives (`number`, `string`, `boolean`), `bigint`, and arrays of these (mixed arrays allowed).
|
|
31
|
+
- **BigInts** of any size are supported: values that fit in 64 bits are stored as a fast int64 slot; larger values are stored as their decimal string in the StringTable.
|
|
32
|
+
- **Objects** have a **fixed shape**: once created, properties may not be added or removed — only their values can change.
|
|
33
|
+
- **Not allowed**: nested objects, nested arrays, `null`, `undefined`.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Shared } from "bun-shared-memory";
|
|
39
|
+
|
|
40
|
+
// Get a wrapper to a named value in shared memory.
|
|
41
|
+
// If it doesn't exist, create it with the provided initial value.
|
|
42
|
+
const user = Shared<User>("user-1", { name: "Alice", age: 30, tags: ["a"] });
|
|
43
|
+
|
|
44
|
+
// Access object properties directly via getters/setters
|
|
45
|
+
user.name; // "Alice"
|
|
46
|
+
user.name = "Bob"; // writes to shared memory
|
|
47
|
+
user.age = 31;
|
|
48
|
+
|
|
49
|
+
// Arrays are proxied too: index access and mutating methods work
|
|
50
|
+
user.tags.push("b");
|
|
51
|
+
user.tags[0] = "x";
|
|
52
|
+
user.tags.pop();
|
|
53
|
+
|
|
54
|
+
// Top-level arrays work the same way
|
|
55
|
+
const arr = Shared<number[]>("arr-1", [1, 2, 3]);
|
|
56
|
+
arr.push(4);
|
|
57
|
+
arr[0] = 99;
|
|
58
|
+
|
|
59
|
+
// Primitives use read()/write()/update()
|
|
60
|
+
const counter = Shared<number>("counter-1", 0);
|
|
61
|
+
counter.write(42);
|
|
62
|
+
counter.update((n) => n + 1);
|
|
63
|
+
|
|
64
|
+
// Or create it with a loader function (sync or async)
|
|
65
|
+
const user2 = await Shared<User>("user-2", async () => {
|
|
66
|
+
return await fetchUser();
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Synchronization
|
|
71
|
+
|
|
72
|
+
All shared-memory reads and writes are protected by an `Atomics`-based mutex, so concurrent access from multiple workers is safe.
|
|
73
|
+
|
|
74
|
+
### Read cache
|
|
75
|
+
|
|
76
|
+
Reads are cached per worker to avoid re-decoding the value on every access.
|
|
77
|
+
Each key has a version cell (an `Int32`) in shared memory that is incremented
|
|
78
|
+
on every write. A read does a single lock-free `Atomics.load` on the version
|
|
79
|
+
cell; if it matches the cached version, the cached value is returned without
|
|
80
|
+
taking the lock or decoding. Because the version cell is shared, a write in
|
|
81
|
+
any worker invalidates the cache in every other worker automatically.
|
|
82
|
+
|
|
83
|
+
`read()` returns a copy, so mutating the returned object cannot corrupt the
|
|
84
|
+
cache. Property access through the proxy (e.g. `user.name`) reads from the
|
|
85
|
+
cache directly.
|
|
86
|
+
|
|
87
|
+
### Offset cache
|
|
88
|
+
|
|
89
|
+
Because object properties may not be added or removed, an object's layout
|
|
90
|
+
(total DataTable offset + each property's slot offset) is static for the
|
|
91
|
+
lifetime of the object. The layout is built once (under the lock) and cached
|
|
92
|
+
per worker, so a property read decodes only the requested slot instead of the
|
|
93
|
+
whole object. The decoded property value is cached per property and
|
|
94
|
+
invalidated by the same per-key version cell, so repeated reads of the same
|
|
95
|
+
property are cheap. If a future GC/defragmentation moves an object block, the
|
|
96
|
+
layout cache must be invalidated (e.g. via a layout version cell).
|
|
97
|
+
|
|
98
|
+
### Mutex
|
|
99
|
+
|
|
100
|
+
A re-entrant cross-worker mutex. Share the same buffer across workers.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Mutex } from "bun-shared-memory";
|
|
104
|
+
|
|
105
|
+
const mutex = new Mutex();
|
|
106
|
+
// pass mutex.buffer to other workers, then:
|
|
107
|
+
mutex.lock();
|
|
108
|
+
try {
|
|
109
|
+
// critical section
|
|
110
|
+
} finally {
|
|
111
|
+
mutex.unlock();
|
|
112
|
+
}
|
|
113
|
+
// or
|
|
114
|
+
mutex.withLock(() => {
|
|
115
|
+
// critical section
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Notifier
|
|
120
|
+
|
|
121
|
+
A cross-worker notification primitive built on `Atomics.wait`/`Atomics.notify`.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { Notifier } from "bun-shared-memory";
|
|
125
|
+
|
|
126
|
+
const notifier = new Notifier();
|
|
127
|
+
// pass notifier.buffer to other workers, then:
|
|
128
|
+
notifier.wait(); // block until notified
|
|
129
|
+
notifier.notify(1); // wake one waiting worker
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Reference counting & memory reclamation
|
|
133
|
+
|
|
134
|
+
The shared buffers are append-only, so dead space (overwritten strings,
|
|
135
|
+
abandoned array blocks, replaced objects) is never reclaimed on its own. A
|
|
136
|
+
reference-counting system tracks which workers hold references to which
|
|
137
|
+
objects/arrays, and a **defragmentation** pass reclaims memory when free space
|
|
138
|
+
runs low.
|
|
139
|
+
|
|
140
|
+
When you use the worker integration (`init()`), this is wired up automatically:
|
|
141
|
+
`init()` starts a `RefCountCoordinator` on the main thread (auto-maintenance +
|
|
142
|
+
auto-registration of workers) and a `RefCountTracker` in each worker, so
|
|
143
|
+
`read()` acquires references that are released on GC and dead space is
|
|
144
|
+
defragmented — no manual setup needed.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { Shared, init } from "bun-shared-memory";
|
|
148
|
+
|
|
149
|
+
// Main thread: init() starts the coordinator automatically.
|
|
150
|
+
init();
|
|
151
|
+
const user = Shared("user-1", { name: "Alice" });
|
|
152
|
+
const copy = user.read(); // acquires a ref; released when `copy` is GC'd
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
You can also wire it up manually (e.g. when not using `init()`):
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import {
|
|
159
|
+
SharedMemory,
|
|
160
|
+
RefCountCoordinator,
|
|
161
|
+
RefCountTracker,
|
|
162
|
+
RefCountBuffer,
|
|
163
|
+
} from "bun-shared-memory";
|
|
164
|
+
|
|
165
|
+
// Main thread: coordinate refcounts and auto-register workers.
|
|
166
|
+
const mem = new SharedMemory();
|
|
167
|
+
const coord = new RefCountCoordinator(mem); // auto-maintenance starts automatically
|
|
168
|
+
coord.autoRegister(); // track workers via process.on("worker")
|
|
169
|
+
// The coordinator periodically drains acquire/release events and defragments
|
|
170
|
+
// when free space runs low — no manual calls needed.
|
|
171
|
+
|
|
172
|
+
// Worker: track references acquired by read().
|
|
173
|
+
const tracker = new RefCountTracker(new RefCountBuffer(refBuf), workerId);
|
|
174
|
+
const user = Shared(
|
|
175
|
+
"user-1",
|
|
176
|
+
{ name: "Alice" },
|
|
177
|
+
{ memory: mem, refCount: tracker },
|
|
178
|
+
);
|
|
179
|
+
const copy = user.read(); // acquires a ref; released when `copy` is GC'd
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
See [REFCOUNT.md](./REFCOUNT.md) for the full design.
|
|
183
|
+
|
|
184
|
+
## Worker integration
|
|
185
|
+
|
|
186
|
+
A worker can simply call `Shared()` and automatically share the same buffers
|
|
187
|
+
as the main thread — no manual buffer serialization needed. Both sides call
|
|
188
|
+
`init()` to set up the handshake: the main thread answers buffer requests from
|
|
189
|
+
workers, and each worker requests the shared buffers from the main thread.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// main.ts
|
|
193
|
+
import { Shared, init } from "bun-shared-memory";
|
|
194
|
+
|
|
195
|
+
const user = Shared("user-1", { name: "Alice", age: 30 });
|
|
196
|
+
init(); // answer buffer requests from every new worker
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// worker.ts
|
|
201
|
+
import { Shared, init, onMessage } from "bun-shared-memory";
|
|
202
|
+
|
|
203
|
+
await init(); // request the shared buffers from the main thread
|
|
204
|
+
|
|
205
|
+
// Register your own message handler via onMessage(); messages are buffered
|
|
206
|
+
// until the buffers have arrived.
|
|
207
|
+
onMessage((e) => {
|
|
208
|
+
const user = Shared("user-1", { name: "x" }); // reads the main thread's value
|
|
209
|
+
self.postMessage(user.read());
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`init()` is safe to call on both sides: in the main thread it registers the
|
|
214
|
+
buffer-request listener and starts the refcount coordinator (auto-maintenance
|
|
215
|
+
|
|
216
|
+
- worker auto-registration), and in a worker it performs the request/response
|
|
217
|
+
handshake, reconstructs the shared buffers, and creates a refcount tracker —
|
|
218
|
+
so reference counting works automatically. `Shared()` never overwrites a value
|
|
219
|
+
that already exists in shared memory, so a worker reading a key created by the
|
|
220
|
+
main thread gets the existing value rather than its own initial value.
|
|
221
|
+
|
|
222
|
+
> **Note:** a worker must call `await init()` before using `Shared()`. Calling
|
|
223
|
+
> `Shared()` in a worker without `init()` throws an error rather than silently
|
|
224
|
+
> creating a separate, unshared memory.
|
|
225
|
+
|
|
226
|
+
A complete runnable example is in [`examples/worker/`](examples/worker/).
|
|
227
|
+
|
|
228
|
+
## Memory budget
|
|
229
|
+
|
|
230
|
+
Shared memory is split across several growable `SharedArrayBuffer`s (the
|
|
231
|
+
index, data, string, and array tables; each key's version counter lives in its
|
|
232
|
+
index entry). A single `SharedArrayBuffer` can be at most **4 GiB**. By default
|
|
233
|
+
each buffer grows independently up to its own `maxByteLength`, so the _total_
|
|
234
|
+
memory can be several times that.
|
|
235
|
+
|
|
236
|
+
To cap the **total** memory across all buffers, pass `maxTotalBytes` when
|
|
237
|
+
creating the memory (or to `init()`):
|
|
238
|
+
|
|
239
|
+
```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 });
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The budget is shared dynamically — buffers grow as needed, but only while the
|
|
252
|
+
sum of all buffers stays within `maxTotalBytes`. If a write would exceed the
|
|
253
|
+
budget, a `RangeError` is thrown.
|
|
254
|
+
|
|
255
|
+
## Array capacity
|
|
256
|
+
|
|
257
|
+
By default an array is stored with exactly as many slots as it has elements,
|
|
258
|
+
so the first `push` must relocate the array (with doubled capacity) to make
|
|
259
|
+
room. To avoid that, reserve extra capacity up front with `arrayCapacity`:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { Shared } from "bun-shared-memory";
|
|
263
|
+
|
|
264
|
+
// Reserve 100 slots so the first ~90 pushes append in place without moving.
|
|
265
|
+
const arr = Shared<number[]>("arr-cap", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {
|
|
266
|
+
arrayCapacity: 100,
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`arrayCapacity` is the initial capacity (in elements) reserved for arrays when
|
|
271
|
+
they are first stored. `push` appends in place until the capacity is
|
|
272
|
+
exhausted, then the array is relocated with doubled capacity. It defaults to
|
|
273
|
+
the array's length.
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
bun install # install dependencies
|
|
279
|
+
bun run typecheck # type check (bun check)
|
|
280
|
+
bun test # run tests
|
|
281
|
+
bun fmt # format code
|
|
282
|
+
bun run bench # run benchmarks
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Benchmarks
|
|
286
|
+
|
|
287
|
+
`bun run bench` compares shared-memory access against native JS access for
|
|
288
|
+
every supported data type and its arrays:
|
|
289
|
+
|
|
290
|
+
- **Primitives**: `number`, `string`, `boolean`, `bigint` (read + write)
|
|
291
|
+
- **Arrays**: `number[]`, `string[]`, `boolean[]`, `bigint[]`, `mixed[]` (index read, index write)
|
|
292
|
+
- **Objects**: property read/write and array-property access
|
|
293
|
+
|
|
294
|
+
Each benchmark reports ops/sec for the native and shared paths, plus the
|
|
295
|
+
relative slowdown. Results are in `bench/`.
|
|
296
|
+
|
|
297
|
+
With the read cache and offset cache, **reads** are typically 5–12× slower than
|
|
298
|
+
native (the remaining cost is the proxy dispatch). **Writes** are slower because
|
|
299
|
+
they must serialize, write, and bump the version cell. Object property and array
|
|
300
|
+
index writes are O(1) in-place updates (only the affected slot is written).
|
|
301
|
+
Array `push`/`pop` are O(1) amortized in-place operations: arrays reserve
|
|
302
|
+
capacity padding in the ArrayTable, `push` writes into the padding until it is
|
|
303
|
+
exhausted (then the array is relocated with doubled capacity), and `pop` just
|
|
304
|
+
decrements the length.
|
|
305
|
+
|
|
306
|
+
**String interning**: repeated writes of the same string reuse a cached
|
|
307
|
+
StringTable reference instead of re-encoding and re-copying the bytes. The
|
|
308
|
+
per-worker LRU cache grows dynamically (configurable via `internCapacity` /
|
|
309
|
+
`internMax` on `SharedMemory`) and evicts least-recently-used entries once it
|
|
310
|
+
reaches capacity. Because the StringTable is append-only, cached references stay
|
|
311
|
+
valid for the lifetime of the buffer.
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Share data between Bun workers/threads using SharedArrayBuffer (zero-copy, 4-buffer architecture, Atomics-based synchronization).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"types": "index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.ts",
|
|
11
|
+
"default": "./index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.ts",
|
|
16
|
+
"src/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "bun check",
|
|
20
|
+
"test": "bun test",
|
|
21
|
+
"bench": "bun bench/bench.ts"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"bun",
|
|
25
|
+
"shared-memory",
|
|
26
|
+
"sharedarraybuffer",
|
|
27
|
+
"worker",
|
|
28
|
+
"threads",
|
|
29
|
+
"atomics",
|
|
30
|
+
"zero-copy"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"bun": ">=1.4.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "latest"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"typescript": "^7"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared memory budget shared across multiple GrowableBuffers.
|
|
3
|
+
*
|
|
4
|
+
* Tracks the sum of the physical sizes of all participating buffers so a
|
|
5
|
+
* caller can cap the *total* memory used by a set of buffers, rather than
|
|
6
|
+
* capping each buffer independently. Growth is only allowed if the new total
|
|
7
|
+
* stays within the budget.
|
|
8
|
+
*/
|
|
9
|
+
export class SharedBudget {
|
|
10
|
+
private total = 0;
|
|
11
|
+
|
|
12
|
+
constructor(private readonly max: number) {}
|
|
13
|
+
|
|
14
|
+
/** Record a buffer's initial physical size. */
|
|
15
|
+
add(size: number): void {
|
|
16
|
+
this.total += size;
|
|
17
|
+
if (this.total > this.max) {
|
|
18
|
+
throw new RangeError(
|
|
19
|
+
`Shared memory budget exceeded: initial buffers total ${this.total} bytes but the budget is ${this.max}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Attempt to grow a buffer from `oldSize` to `newSize`. Returns true and
|
|
26
|
+
* updates the total if the new total stays within the budget, false
|
|
27
|
+
* otherwise (the caller must not grow).
|
|
28
|
+
*/
|
|
29
|
+
tryGrow(oldSize: number, newSize: number): boolean {
|
|
30
|
+
const newTotal = this.total - oldSize + newSize;
|
|
31
|
+
if (newTotal > this.max) return false;
|
|
32
|
+
this.total = newTotal;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Growable SharedArrayBuffer helper.
|
|
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.
|
|
44
|
+
*/
|
|
45
|
+
export class GrowableBuffer {
|
|
46
|
+
readonly buffer: SharedArrayBuffer;
|
|
47
|
+
private view: DataView;
|
|
48
|
+
private readonly maxByteLength: number;
|
|
49
|
+
private readonly budget?: SharedBudget;
|
|
50
|
+
private used = 0;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
initialSizeOrBuffer: number | SharedArrayBuffer,
|
|
54
|
+
maxByteLength: number,
|
|
55
|
+
budget?: SharedBudget,
|
|
56
|
+
) {
|
|
57
|
+
this.buffer =
|
|
58
|
+
typeof initialSizeOrBuffer === "number"
|
|
59
|
+
? new SharedArrayBuffer(initialSizeOrBuffer, { maxByteLength })
|
|
60
|
+
: initialSizeOrBuffer;
|
|
61
|
+
this.view = new DataView(this.buffer);
|
|
62
|
+
this.maxByteLength = maxByteLength;
|
|
63
|
+
this.budget = budget;
|
|
64
|
+
this.budget?.add(this.buffer.byteLength);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Number of bytes written so far (used length, not buffer capacity). */
|
|
68
|
+
get byteLength(): number {
|
|
69
|
+
return this.used;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Ensure the buffer has room for at least `needed` bytes. */
|
|
73
|
+
ensureCapacity(needed: number): void {
|
|
74
|
+
if (needed <= this.buffer.byteLength) return;
|
|
75
|
+
if (needed > this.maxByteLength) {
|
|
76
|
+
throw new RangeError(
|
|
77
|
+
`Shared buffer capacity exceeded: need ${needed} bytes but max is ${this.maxByteLength}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
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
|
+
);
|
|
87
|
+
}
|
|
88
|
+
this.buffer.grow(newSize);
|
|
89
|
+
this.view = new DataView(this.buffer);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Mark `n` bytes as used, after writing them at the current offset. */
|
|
93
|
+
advance(n: number): void {
|
|
94
|
+
this.used += n;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 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.
|
|
102
|
+
*/
|
|
103
|
+
reset(n: number): void {
|
|
104
|
+
this.used = n;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getView(): DataView {
|
|
108
|
+
return this.view;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { Shared } from "./shared.ts";
|
|
2
|
+
export type { SharedRef, SharedOptions } from "./shared.ts";
|
|
3
|
+
export { SharedMemory } from "./memory.ts";
|
|
4
|
+
export type { SharedMemoryOptions } from "./memory.ts";
|
|
5
|
+
export { Mutex } from "./mutex.ts";
|
|
6
|
+
export { Notifier } from "./notifier.ts";
|
|
7
|
+
export {
|
|
8
|
+
RefCountBuffer,
|
|
9
|
+
RefCountCoordinator,
|
|
10
|
+
RefCountTracker,
|
|
11
|
+
} from "./refcount.ts";
|
|
12
|
+
export type { ObjectId, RefEvent } from "./refcount.ts";
|
|
13
|
+
export {
|
|
14
|
+
getDefaultMemory,
|
|
15
|
+
setDefaultMemory,
|
|
16
|
+
hasDefaultMemory,
|
|
17
|
+
getDefaultTracker,
|
|
18
|
+
init,
|
|
19
|
+
stopInit,
|
|
20
|
+
onMessage,
|
|
21
|
+
} from "./worker.ts";
|