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/src/refcount.ts CHANGED
@@ -161,6 +161,13 @@ export class RefCountCoordinator {
161
161
  /** Free-space threshold (fraction of maxByteLength) that triggers defrag. */
162
162
  private readonly defragThreshold: number;
163
163
  private maintenanceTimer: ReturnType<typeof setInterval> | null = null;
164
+ /** Called after a defragmentation pass with the new chunk lists. */
165
+ private readonly onDefragment?: (buffers: {
166
+ indexTable: SharedArrayBuffer[];
167
+ dataTable: SharedArrayBuffer[];
168
+ stringTable: SharedArrayBuffer[];
169
+ arrayTable: SharedArrayBuffer[];
170
+ }) => void;
164
171
 
165
172
  constructor(
166
173
  mem: SharedMemory,
@@ -169,9 +176,16 @@ export class RefCountCoordinator {
169
176
  defragThreshold?: number;
170
177
  autoMaintenance?: boolean;
171
178
  maintenanceInterval?: number;
179
+ onDefragment?: (buffers: {
180
+ indexTable: SharedArrayBuffer[];
181
+ dataTable: SharedArrayBuffer[];
182
+ stringTable: SharedArrayBuffer[];
183
+ arrayTable: SharedArrayBuffer[];
184
+ }) => void;
172
185
  } = {},
173
186
  ) {
174
187
  this.mem = mem;
188
+ this.onDefragment = opts.onDefragment;
175
189
  // Reuse the memory's shared refCount buffer if present, so the same SAB is
176
190
  // exported to workers; otherwise create a fresh one and attach it to the
177
191
  // memory so it can be shared.
@@ -286,9 +300,9 @@ export class RefCountCoordinator {
286
300
 
287
301
  /** Whether any table has less than `defragThreshold` free space. */
288
302
  needsDefrag(): boolean {
289
- const max = this.mem.dataTable.buffer.maxByteLength;
290
- const free = (b: { buffer: SharedArrayBuffer; byteLength: number }) =>
291
- (b.buffer.byteLength - b.byteLength) / max;
303
+ const max = this.mem.dataTable.capacity;
304
+ const free = (b: { capacity: number; byteLength: number }) =>
305
+ (b.capacity - b.byteLength) / max;
292
306
  return (
293
307
  free(this.mem.dataTable) < this.defragThreshold ||
294
308
  free(this.mem.arrayTable) < this.defragThreshold ||
@@ -313,7 +327,8 @@ export class RefCountCoordinator {
313
327
  this.maintenanceTimer = setInterval(() => {
314
328
  this.drainEvents();
315
329
  if (this.needsDefrag()) {
316
- this.mem.withLock(() => this.mem.defragment());
330
+ const buffers = this.mem.withLock(() => this.mem.defragment());
331
+ this.onDefragment?.(buffers);
317
332
  }
318
333
  }, intervalMs);
319
334
  // Unref the timer so it does not keep the process alive. The coordinator
package/src/shared.ts CHANGED
@@ -178,7 +178,7 @@ function isTrackable(value: unknown): boolean {
178
178
  function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
179
179
  const offset = getOffset(mem, key);
180
180
  if (offset === null) return null;
181
- const type = mem.dataTable.getView().getInt32(offset, true);
181
+ const type = mem.dataTable.getInt32(offset);
182
182
  if (type === 5) {
183
183
  return mem.getObjectId(offset);
184
184
  }
@@ -517,7 +517,7 @@ function createSharedRef<T>(
517
517
  enumerable: false,
518
518
  configurable: false,
519
519
  get() {
520
- return mem.dataTable.buffer;
520
+ return mem.dataTable.buffers[0];
521
521
  },
522
522
  });
523
523
  const ref = Object.create(proto) as Record<string | symbol, unknown>;
@@ -564,7 +564,7 @@ function createSharedRef<T>(
564
564
  if (prop === "write") return t.write;
565
565
  if (prop === "update") return t.update;
566
566
  if (prop === "key") return t.key;
567
- if (prop === "buffer") return mem.dataTable.buffer;
567
+ if (prop === "buffer") return mem.dataTable.buffers[0];
568
568
  if (prop === Symbol.toPrimitive) return () => t.read();
569
569
 
570
570
  // push/pop on a top-level array: operate directly on the array ref
@@ -635,7 +635,7 @@ function readValue(
635
635
  if (offset === null) {
636
636
  throw new Error(`No shared value found for key "${key}"`);
637
637
  }
638
- const type = mem.dataTable.getView().getInt32(offset, true);
638
+ const type = mem.dataTable.getInt32(offset);
639
639
  if (type === 5) {
640
640
  // Object block.
641
641
  const properties = mem.readObject(offset);
@@ -665,7 +665,7 @@ function writeValue(
665
665
  mem.withLock(() => {
666
666
  const existing = getOffset(mem, key);
667
667
  if (existing !== null) {
668
- const type = mem.dataTable.getView().getInt32(existing, true);
668
+ const type = mem.dataTable.getInt32(existing);
669
669
  if (type === 5) {
670
670
  // Update existing object's property slots in place.
671
671
  const properties = mem.readObject(existing);
package/src/worker.ts CHANGED
@@ -130,7 +130,9 @@ export function getDefaultTracker(): RefCountTracker | null {
130
130
  function startCoordinator(): void {
131
131
  if (coordinator !== null) return;
132
132
  const mem = getDefaultMemory();
133
- coordinator = new RefCountCoordinator(mem);
133
+ coordinator = new RefCountCoordinator(mem, undefined, {
134
+ onDefragment: (buffers) => broadcastChunkUpdate(buffers),
135
+ });
134
136
  unregisterWorkers = coordinator.autoRegister();
135
137
  }
136
138
 
@@ -144,6 +146,7 @@ function stopCoordinator(): void {
144
146
  }
145
147
  }
146
148
 
149
+ // ---------------------------------------------------------------------------
147
150
  /** The main-thread worker listener, so it can be removed by `stopInit()`. */
148
151
  let mainListener:
149
152
  | ((worker: {
@@ -155,6 +158,12 @@ let mainListener:
155
158
  }) => void)
156
159
  | null = null;
157
160
 
161
+ /** Live workers on the main thread, for broadcasting chunk updates. */
162
+ const liveWorkers = new Set<{
163
+ postMessage: (msg: unknown) => void;
164
+ addEventListener: (type: string, fn: (e: { data: unknown }) => void) => void;
165
+ }>();
166
+
158
167
  /** Main thread: answer each worker's buffer request. */
159
168
  function initMain(): void {
160
169
  const listener = (worker: {
@@ -164,14 +173,20 @@ function initMain(): void {
164
173
  fn: (e: { data: unknown }) => void,
165
174
  ) => void;
166
175
  }) => {
176
+ liveWorkers.add(worker);
167
177
  worker.addEventListener("message", (e) => {
168
178
  const msg = e.data as { __sharedMemoryRequest?: boolean } | undefined;
169
- if (msg && msg.__sharedMemoryRequest) {
179
+ if (
180
+ msg !== null &&
181
+ typeof msg === "object" &&
182
+ "__sharedMemoryRequest" in msg
183
+ ) {
170
184
  const mem = getDefaultMemory();
171
185
  worker.postMessage({
172
186
  __sharedMemoryInit: true,
173
187
  buffers: mem.exportBuffers(),
174
188
  });
189
+ return;
175
190
  }
176
191
  });
177
192
  };
@@ -185,6 +200,22 @@ function stopMain(): void {
185
200
  process.off("worker", mainListener);
186
201
  mainListener = null;
187
202
  }
203
+ liveWorkers.clear();
204
+ }
205
+
206
+ /**
207
+ * Broadcast a chunk update (new chunk lists after a defragmentation pass) to
208
+ * all live workers. Workers apply it via `applyChunkUpdate()`.
209
+ */
210
+ function broadcastChunkUpdate(buffers: {
211
+ indexTable: SharedArrayBuffer[];
212
+ dataTable: SharedArrayBuffer[];
213
+ stringTable: SharedArrayBuffer[];
214
+ arrayTable: SharedArrayBuffer[];
215
+ }): void {
216
+ for (const worker of liveWorkers) {
217
+ worker.postMessage({ __chunkUpdate: true, buffers });
218
+ }
188
219
  }
189
220
 
190
221
  /** Worker: request the buffers from the main thread and await the reply. */
@@ -233,8 +264,22 @@ function installMessageWrapper(): void {
233
264
  __sharedMemoryInit?: boolean;
234
265
  buffers?: Parameters<typeof SharedMemory.fromBuffers>[0];
235
266
  }
267
+ | {
268
+ __chunkUpdate?: boolean;
269
+ buffers?: {
270
+ indexTable: SharedArrayBuffer[];
271
+ dataTable: SharedArrayBuffer[];
272
+ stringTable: SharedArrayBuffer[];
273
+ arrayTable: SharedArrayBuffer[];
274
+ };
275
+ }
236
276
  | undefined;
237
- if (msg && msg.__sharedMemoryInit && msg.buffers) {
277
+ if (
278
+ msg !== null &&
279
+ typeof msg === "object" &&
280
+ "__sharedMemoryInit" in msg &&
281
+ msg.buffers
282
+ ) {
238
283
  const mem = SharedMemory.fromBuffers(msg.buffers);
239
284
  setDefaultMemory(mem);
240
285
  // Create the worker-side refcount tracker from the shared refcount
@@ -253,6 +298,18 @@ function installMessageWrapper(): void {
253
298
  flush();
254
299
  return;
255
300
  }
301
+ if (
302
+ msg !== null &&
303
+ typeof msg === "object" &&
304
+ "__chunkUpdate" in msg &&
305
+ msg.buffers &&
306
+ hasDefaultMemory()
307
+ ) {
308
+ // The main thread replaced inactive chunks after a defragmentation
309
+ // pass; apply the new chunk lists so this worker sees the replacement.
310
+ getDefaultMemory().applyChunkUpdate(msg.buffers);
311
+ return;
312
+ }
256
313
  if (hasDefaultMemory() && hasHandler) {
257
314
  messageHandler(e);
258
315
  } else {