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/src/worker.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { SharedMemory, type SharedMemoryOptions } from "./memory.ts";
|
|
2
|
+
import {
|
|
3
|
+
RefCountBuffer,
|
|
4
|
+
RefCountCoordinator,
|
|
5
|
+
RefCountTracker,
|
|
6
|
+
} from "./refcount.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Worker integration (M4).
|
|
10
|
+
*
|
|
11
|
+
* Lets a worker simply call `Shared()` and share data with the main thread
|
|
12
|
+
* without manually serializing buffers. The mechanism is a request/response
|
|
13
|
+
* handshake:
|
|
14
|
+
*
|
|
15
|
+
* - The main thread calls `init()` once. It listens for new workers via Bun's
|
|
16
|
+
* `process.on("worker", ...)` and answers each worker's buffer request.
|
|
17
|
+
* - A worker calls `await init()` at startup. It sends a request to the main
|
|
18
|
+
* thread, which replies with the shared buffers; the worker reconstructs the
|
|
19
|
+
* module-level singleton `SharedMemory` from them. From then on, `Shared()`
|
|
20
|
+
* in that worker uses the same buffers as the main thread.
|
|
21
|
+
*
|
|
22
|
+
* To avoid a race where a worker calls `Shared()` before the buffers arrive,
|
|
23
|
+
* the worker's message wrapper **buffers** incoming messages until the
|
|
24
|
+
* handshake completes, then forwards them to the handler registered via
|
|
25
|
+
* `onMessage()` in order.
|
|
26
|
+
*
|
|
27
|
+
* The singleton is created lazily on first use, so a standalone process (no
|
|
28
|
+
* workers) just works too.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The module-level singleton SharedMemory, shared across all workers. */
|
|
32
|
+
let defaultMemory: SharedMemory | null = null;
|
|
33
|
+
/** Set while a worker is awaiting the buffers from the main thread. */
|
|
34
|
+
let waitingForInit = false;
|
|
35
|
+
|
|
36
|
+
/** Get the singleton SharedMemory, creating it on first use. */
|
|
37
|
+
export function getDefaultMemory(): SharedMemory {
|
|
38
|
+
if (defaultMemory === null) {
|
|
39
|
+
if (waitingForInit) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Shared() called before the shared buffers arrived from the main thread. " +
|
|
42
|
+
"Await init() before using Shared().",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!Bun.isMainThread) {
|
|
46
|
+
// A worker must call `await init()` to receive the shared buffers from
|
|
47
|
+
// the main thread. Without it, creating a fresh memory here would
|
|
48
|
+
// silently give the worker its own, unshared buffers — a hard-to-debug
|
|
49
|
+
// bug where reads/writes don't propagate to the main thread.
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Shared() called in a worker without init(). " +
|
|
52
|
+
"Call `await init()` at the top of the worker to receive the shared " +
|
|
53
|
+
"buffers from the main thread before using Shared().",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
// Standalone process (or main thread): create a fresh memory.
|
|
57
|
+
defaultMemory = new SharedMemory();
|
|
58
|
+
}
|
|
59
|
+
return defaultMemory;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Set the singleton SharedMemory (e.g. from buffers received in a worker). */
|
|
63
|
+
export function setDefaultMemory(mem: SharedMemory): void {
|
|
64
|
+
defaultMemory = mem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether the singleton has been initialized yet. */
|
|
68
|
+
export function hasDefaultMemory(): boolean {
|
|
69
|
+
return defaultMemory !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize the shared-memory handshake.
|
|
74
|
+
*
|
|
75
|
+
* - **Main thread**: call once after creating the first SharedMemory. It
|
|
76
|
+
* listens for new workers and answers each worker's buffer request. If no
|
|
77
|
+
* SharedMemory exists yet, one is created from `options` (e.g. to set a
|
|
78
|
+
* total memory budget via `maxTotalBytes`).
|
|
79
|
+
* - **Worker**: `await init()` at startup. It requests the shared buffers from
|
|
80
|
+
* the main thread and reconstructs the singleton `SharedMemory` from them.
|
|
81
|
+
*
|
|
82
|
+
* Returns a promise that resolves once the handshake is set up (immediately in
|
|
83
|
+
* the main thread, after the buffers arrive in a worker).
|
|
84
|
+
*/
|
|
85
|
+
export async function init(options?: SharedMemoryOptions): Promise<void> {
|
|
86
|
+
if (Bun.isMainThread) {
|
|
87
|
+
if (!hasDefaultMemory()) {
|
|
88
|
+
setDefaultMemory(new SharedMemory(options));
|
|
89
|
+
}
|
|
90
|
+
initMain();
|
|
91
|
+
startCoordinator();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await initWorker();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stop the main-thread worker listener installed by `init()`. No-op in a
|
|
99
|
+
* worker. Useful for tests and teardown.
|
|
100
|
+
*/
|
|
101
|
+
export function stopInit(): void {
|
|
102
|
+
if (Bun.isMainThread) {
|
|
103
|
+
stopMain();
|
|
104
|
+
stopCoordinator();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The main-thread RefCountCoordinator, started by `init()`. */
|
|
109
|
+
let coordinator: RefCountCoordinator | null = null;
|
|
110
|
+
/** Cleanup function returned by `coordinator.autoRegister()`. */
|
|
111
|
+
let unregisterWorkers: (() => void) | null = null;
|
|
112
|
+
|
|
113
|
+
/** The worker-side RefCountTracker, created from the shared refcount buffer. */
|
|
114
|
+
let workerTracker: RefCountTracker | null = null;
|
|
115
|
+
|
|
116
|
+
/** Get the worker's default RefCountTracker, or null if not available. */
|
|
117
|
+
export function getDefaultTracker(): RefCountTracker | null {
|
|
118
|
+
return workerTracker;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Start the reference-counting coordinator on the main thread.
|
|
123
|
+
*
|
|
124
|
+
* Creates a `RefCountCoordinator` over the default memory (attaching the 5th
|
|
125
|
+
* refcount buffer so it is shared with workers) and starts automatic
|
|
126
|
+
* maintenance: refcount events are drained and dead space is defragmented
|
|
127
|
+
* when free space is low. Also auto-registers workers so their references are
|
|
128
|
+
* released on close/crash.
|
|
129
|
+
*/
|
|
130
|
+
function startCoordinator(): void {
|
|
131
|
+
if (coordinator !== null) return;
|
|
132
|
+
const mem = getDefaultMemory();
|
|
133
|
+
coordinator = new RefCountCoordinator(mem);
|
|
134
|
+
unregisterWorkers = coordinator.autoRegister();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Stop the coordinator's maintenance timer and worker listener. */
|
|
138
|
+
function stopCoordinator(): void {
|
|
139
|
+
if (coordinator !== null) {
|
|
140
|
+
coordinator.stopAutoMaintenance();
|
|
141
|
+
unregisterWorkers?.();
|
|
142
|
+
unregisterWorkers = null;
|
|
143
|
+
coordinator = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The main-thread worker listener, so it can be removed by `stopInit()`. */
|
|
148
|
+
let mainListener:
|
|
149
|
+
| ((worker: {
|
|
150
|
+
postMessage: (msg: unknown) => void;
|
|
151
|
+
addEventListener: (
|
|
152
|
+
type: string,
|
|
153
|
+
fn: (e: { data: unknown }) => void,
|
|
154
|
+
) => void;
|
|
155
|
+
}) => void)
|
|
156
|
+
| null = null;
|
|
157
|
+
|
|
158
|
+
/** Main thread: answer each worker's buffer request. */
|
|
159
|
+
function initMain(): void {
|
|
160
|
+
const listener = (worker: {
|
|
161
|
+
postMessage: (msg: unknown) => void;
|
|
162
|
+
addEventListener: (
|
|
163
|
+
type: string,
|
|
164
|
+
fn: (e: { data: unknown }) => void,
|
|
165
|
+
) => void;
|
|
166
|
+
}) => {
|
|
167
|
+
worker.addEventListener("message", (e) => {
|
|
168
|
+
const msg = e.data as { __sharedMemoryRequest?: boolean } | undefined;
|
|
169
|
+
if (msg && msg.__sharedMemoryRequest) {
|
|
170
|
+
const mem = getDefaultMemory();
|
|
171
|
+
worker.postMessage({
|
|
172
|
+
__sharedMemoryInit: true,
|
|
173
|
+
buffers: mem.exportBuffers(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
mainListener = listener;
|
|
179
|
+
process.on("worker", listener);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Remove the main-thread worker listener. */
|
|
183
|
+
function stopMain(): void {
|
|
184
|
+
if (mainListener) {
|
|
185
|
+
process.off("worker", mainListener);
|
|
186
|
+
mainListener = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Worker: request the buffers from the main thread and await the reply. */
|
|
191
|
+
async function initWorker(): Promise<void> {
|
|
192
|
+
waitingForInit = true;
|
|
193
|
+
installMessageWrapper();
|
|
194
|
+
|
|
195
|
+
const reply = new Promise<void>((resolve) => {
|
|
196
|
+
pendingInitResolve = resolve;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const scope = globalThis as unknown as {
|
|
200
|
+
postMessage: (msg: unknown) => void;
|
|
201
|
+
};
|
|
202
|
+
scope.postMessage({ __sharedMemoryRequest: true });
|
|
203
|
+
|
|
204
|
+
await reply;
|
|
205
|
+
waitingForInit = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Resolver for the worker's pending init handshake. */
|
|
209
|
+
let pendingInitResolve: (() => void) | null = null;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Worker side: take over `self.onmessage` and initialize the singleton
|
|
213
|
+
* SharedMemory from buffers sent by the main thread.
|
|
214
|
+
*
|
|
215
|
+
* Bun buffers incoming messages until a `message` listener is registered, so
|
|
216
|
+
* installing the wrapper at module load guarantees the init reply is processed
|
|
217
|
+
* before any other message. Until the buffers arrive, other messages are
|
|
218
|
+
* buffered; once initialized, they are forwarded to the handler registered via
|
|
219
|
+
* `onMessage()` in order.
|
|
220
|
+
*
|
|
221
|
+
* The worker's message handler must be registered via `onMessage()` (rather
|
|
222
|
+
* than assigned to `self.onmessage` directly) so that this wrapper can buffer
|
|
223
|
+
* messages until the shared buffers have arrived.
|
|
224
|
+
*/
|
|
225
|
+
function installMessageWrapper(): void {
|
|
226
|
+
const scope = globalThis as unknown as {
|
|
227
|
+
onmessage: ((e: { data: unknown }) => void) | null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const wrapper = (e: { data: unknown }) => {
|
|
231
|
+
const msg = e.data as
|
|
232
|
+
| {
|
|
233
|
+
__sharedMemoryInit?: boolean;
|
|
234
|
+
buffers?: Parameters<typeof SharedMemory.fromBuffers>[0];
|
|
235
|
+
}
|
|
236
|
+
| undefined;
|
|
237
|
+
if (msg && msg.__sharedMemoryInit && msg.buffers) {
|
|
238
|
+
const mem = SharedMemory.fromBuffers(msg.buffers);
|
|
239
|
+
setDefaultMemory(mem);
|
|
240
|
+
// Create the worker-side refcount tracker from the shared refcount
|
|
241
|
+
// buffer, so read() acquires references that the main thread's
|
|
242
|
+
// coordinator can reclaim.
|
|
243
|
+
if (msg.buffers.refCount) {
|
|
244
|
+
const rcBuffer = new RefCountBuffer(msg.buffers.refCount);
|
|
245
|
+
workerTracker = new RefCountTracker(
|
|
246
|
+
rcBuffer,
|
|
247
|
+
rcBuffer.allocateWorkerId(),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
pendingInitResolve?.();
|
|
251
|
+
pendingInitResolve = null;
|
|
252
|
+
// Forward any messages that arrived before the init reply.
|
|
253
|
+
flush();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (hasDefaultMemory() && hasHandler) {
|
|
257
|
+
messageHandler(e);
|
|
258
|
+
} else {
|
|
259
|
+
pending.push(e.data);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
scope.onmessage = wrapper;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Messages buffered until the buffers arrive and a handler is registered. */
|
|
267
|
+
const pending: unknown[] = [];
|
|
268
|
+
/** Whether a real handler has been registered via `onMessage()`. */
|
|
269
|
+
let hasHandler = false;
|
|
270
|
+
|
|
271
|
+
/** The worker's real message handler, set via `onMessage()`. */
|
|
272
|
+
let messageHandler: (e: { data: unknown }) => void = () => {};
|
|
273
|
+
|
|
274
|
+
/** Forward buffered messages once the buffers and a handler are ready. */
|
|
275
|
+
function flush(): void {
|
|
276
|
+
if (!(hasDefaultMemory() && hasHandler)) return;
|
|
277
|
+
for (const data of pending) {
|
|
278
|
+
messageHandler({ data });
|
|
279
|
+
}
|
|
280
|
+
pending.length = 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Register the worker's message handler. Call this after `await init()`;
|
|
285
|
+
* messages are buffered until the shared buffers have arrived from the main
|
|
286
|
+
* thread and a handler is registered, then forwarded here in order.
|
|
287
|
+
*/
|
|
288
|
+
export function onMessage(handler: (e: { data: unknown }) => void): void {
|
|
289
|
+
messageHandler = handler;
|
|
290
|
+
hasHandler = true;
|
|
291
|
+
flush();
|
|
292
|
+
}
|