bun-shared-memory 0.1.4 → 0.1.6
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/CHANGELOG.md +129 -0
- package/package.json +4 -5
- package/src/growable-buffer.ts +197 -29
- package/src/memory.ts +159 -160
- package/src/refcount.ts +3 -3
- package/src/shared.ts +250 -196
- package/src/worker.ts +13 -2
package/src/refcount.ts
CHANGED
|
@@ -286,9 +286,9 @@ export class RefCountCoordinator {
|
|
|
286
286
|
|
|
287
287
|
/** Whether any table has less than `defragThreshold` free space. */
|
|
288
288
|
needsDefrag(): boolean {
|
|
289
|
-
const max = this.mem.dataTable.
|
|
290
|
-
const free = (b: {
|
|
291
|
-
(b.
|
|
289
|
+
const max = this.mem.dataTable.capacity;
|
|
290
|
+
const free = (b: { capacity: number; byteLength: number }) =>
|
|
291
|
+
(b.capacity - b.byteLength) / max;
|
|
292
292
|
return (
|
|
293
293
|
free(this.mem.dataTable) < this.defragThreshold ||
|
|
294
294
|
free(this.mem.arrayTable) < this.defragThreshold ||
|
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.
|
|
181
|
+
const type = mem.dataTable.getInt32(offset);
|
|
182
182
|
if (type === 5) {
|
|
183
183
|
return mem.getObjectId(offset);
|
|
184
184
|
}
|
|
@@ -189,20 +189,6 @@ function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
|
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
/**
|
|
193
|
-
* Per-worker cache of decoded object properties: key -> { version, props }.
|
|
194
|
-
*
|
|
195
|
-
* When a property of an object is read via the layout (single-slot decode),
|
|
196
|
-
* the decoded value is cached here keyed by the object's version. A hit avoids
|
|
197
|
-
* re-decoding the property (e.g. re-reading a string from the StringTable) on
|
|
198
|
-
* repeated reads of the same property. Writes bump the object's version, so
|
|
199
|
-
* the whole entry is invalidated automatically.
|
|
200
|
-
*/
|
|
201
|
-
const propCache = new Map<
|
|
202
|
-
string,
|
|
203
|
-
{ version: number; index: number; props: Map<string, unknown> }
|
|
204
|
-
>();
|
|
205
|
-
|
|
206
192
|
/**
|
|
207
193
|
* Per-worker cache of object layouts: key -> stable property slot offsets.
|
|
208
194
|
*
|
|
@@ -222,13 +208,21 @@ function getObjectLayout(mem: SharedMemory, key: string): ObjectLayout | null {
|
|
|
222
208
|
return layout;
|
|
223
209
|
}
|
|
224
210
|
|
|
225
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Deep-ish copy of a decoded value, so callers can't corrupt the cache.
|
|
213
|
+
*
|
|
214
|
+
* Uses `Object.assign` for the fast shallow copy, then a `for...in` pass to
|
|
215
|
+
* copy any nested arrays (so mutating a returned array property cannot corrupt
|
|
216
|
+
* the cache). This is ~4.5× faster than the previous `Object.entries` + spread
|
|
217
|
+
* loop, which allocated an entries array and re-checked `Array.isArray` on
|
|
218
|
+
* every property.
|
|
219
|
+
*/
|
|
226
220
|
function copyValue(value: unknown): unknown {
|
|
227
221
|
if (Array.isArray(value)) return [...value];
|
|
228
222
|
if (value !== null && typeof value === "object") {
|
|
229
|
-
const copy
|
|
230
|
-
for (const
|
|
231
|
-
|
|
223
|
+
const copy = Object.assign({} as Record<string, unknown>, value);
|
|
224
|
+
for (const k in copy) {
|
|
225
|
+
if (Array.isArray(copy[k])) copy[k] = [...(copy[k] as unknown[])];
|
|
232
226
|
}
|
|
233
227
|
return copy;
|
|
234
228
|
}
|
|
@@ -297,6 +291,7 @@ export function Shared<T>(
|
|
|
297
291
|
const ref = createSharedRef<T>(
|
|
298
292
|
mem,
|
|
299
293
|
key,
|
|
294
|
+
typeof initialOrLoader === "function" ? undefined : (initialOrLoader as T),
|
|
300
295
|
options.refCount ?? getDefaultTracker() ?? undefined,
|
|
301
296
|
options.arrayCapacity,
|
|
302
297
|
);
|
|
@@ -318,10 +313,125 @@ export function Shared<T>(
|
|
|
318
313
|
return ref;
|
|
319
314
|
}
|
|
320
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Read a single property of a shared object value.
|
|
318
|
+
*
|
|
319
|
+
* Shared by the proxy `get` handler and the generated accessor getters. Uses
|
|
320
|
+
* the lock-free read-cache fast path and falls back to `readValue` (which
|
|
321
|
+
* fills the cache) on a miss. Array properties are returned as an array proxy.
|
|
322
|
+
*/
|
|
323
|
+
function readProp(
|
|
324
|
+
mem: SharedMemory,
|
|
325
|
+
key: string,
|
|
326
|
+
prop: string | symbol,
|
|
327
|
+
arrayCapacity?: number,
|
|
328
|
+
): unknown {
|
|
329
|
+
const fast = readCachedValue(mem, key);
|
|
330
|
+
const value = fast.hit ? fast.value : readValue(mem, key);
|
|
331
|
+
if (Array.isArray(value)) {
|
|
332
|
+
return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
|
|
333
|
+
}
|
|
334
|
+
const propValue = (value as Record<string | symbol, unknown>)[prop];
|
|
335
|
+
if (Array.isArray(propValue)) {
|
|
336
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
337
|
+
}
|
|
338
|
+
return propValue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Write a single property of a shared object value in place.
|
|
343
|
+
*
|
|
344
|
+
* Shared by the proxy `set` handler and the generated accessor setters. Uses
|
|
345
|
+
* the cached layout to update only the affected slot.
|
|
346
|
+
*/
|
|
347
|
+
function writeProp(
|
|
348
|
+
mem: SharedMemory,
|
|
349
|
+
key: string,
|
|
350
|
+
prop: string | symbol,
|
|
351
|
+
val: unknown,
|
|
352
|
+
arrayCapacity?: number,
|
|
353
|
+
): void {
|
|
354
|
+
mem.withLock(() => {
|
|
355
|
+
// Object property: use the cached layout to write only that slot.
|
|
356
|
+
const layout = getObjectLayout(mem, key);
|
|
357
|
+
if (layout !== null) {
|
|
358
|
+
const slotOffset = layout.properties.get(prop as string);
|
|
359
|
+
if (slotOffset === undefined) {
|
|
360
|
+
throw new TypeError(
|
|
361
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
mem.updateValueSlot(slotOffset, encodeValue(mem, val));
|
|
365
|
+
bump(mem, key);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Top-level array index write: update only that array slot.
|
|
370
|
+
const offset = getOffset(mem, key);
|
|
371
|
+
if (offset === null) {
|
|
372
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
373
|
+
}
|
|
374
|
+
const slot = mem.readValueSlot(offset);
|
|
375
|
+
if (
|
|
376
|
+
slot.type === TYPE_ARRAY_REF &&
|
|
377
|
+
typeof prop === "string" &&
|
|
378
|
+
/^\d+$/.test(prop)
|
|
379
|
+
) {
|
|
380
|
+
const index = Number(prop);
|
|
381
|
+
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
382
|
+
if (index >= ref.count) {
|
|
383
|
+
throw new RangeError(
|
|
384
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
388
|
+
bump(mem, key);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Top-level array length write: shrink/grow the array in place.
|
|
393
|
+
if (slot.type === TYPE_ARRAY_REF && prop === "length") {
|
|
394
|
+
const newLen = Number(val);
|
|
395
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
396
|
+
throw new RangeError("Invalid array length");
|
|
397
|
+
}
|
|
398
|
+
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
399
|
+
if (newLen <= ref.capacity) {
|
|
400
|
+
mem.updateValueSlot(
|
|
401
|
+
offset,
|
|
402
|
+
arrayRefSlot({
|
|
403
|
+
offset: ref.offset,
|
|
404
|
+
count: newLen,
|
|
405
|
+
capacity: ref.capacity,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
bump(mem, key);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fallback: full read-modify-write.
|
|
414
|
+
mutateValue<unknown, boolean>(
|
|
415
|
+
mem,
|
|
416
|
+
key,
|
|
417
|
+
(value) => {
|
|
418
|
+
if (Array.isArray(value)) {
|
|
419
|
+
(value as unknown as Record<string | symbol, unknown>)[prop] = val;
|
|
420
|
+
} else {
|
|
421
|
+
(value as Record<string | symbol, unknown>)[prop] = val;
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
424
|
+
},
|
|
425
|
+
arrayCapacity,
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
321
430
|
/** Create a SharedRef proxy backed by a SharedMemory instance. */
|
|
322
431
|
function createSharedRef<T>(
|
|
323
432
|
mem: SharedMemory,
|
|
324
433
|
key: string,
|
|
434
|
+
initialValue: T | undefined,
|
|
325
435
|
tracker?: RefCountTracker,
|
|
326
436
|
arrayCapacity?: number,
|
|
327
437
|
): SharedRef<T> {
|
|
@@ -352,13 +462,109 @@ function createSharedRef<T>(
|
|
|
352
462
|
},
|
|
353
463
|
};
|
|
354
464
|
|
|
465
|
+
// Determine the object's property names so we can hard-bake accessor
|
|
466
|
+
// getters/setters instead of routing every access through a Proxy trap.
|
|
467
|
+
// The shape is known from the initial value (if it is an object) or from
|
|
468
|
+
// shared memory (if the key already exists, e.g. in a worker).
|
|
469
|
+
let propNames: string[] | null = null;
|
|
470
|
+
if (
|
|
471
|
+
initialValue !== null &&
|
|
472
|
+
typeof initialValue === "object" &&
|
|
473
|
+
!Array.isArray(initialValue)
|
|
474
|
+
) {
|
|
475
|
+
propNames = Object.keys(initialValue as Record<string, unknown>);
|
|
476
|
+
} else {
|
|
477
|
+
const layout = mem.withLock(() => mem.getObjectLayout(key));
|
|
478
|
+
if (layout !== null) {
|
|
479
|
+
propNames = [...layout.properties.keys()];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Object values: return a plain object with hard-baked accessor getters and
|
|
484
|
+
// setters (no Proxy). This avoids the Proxy trap overhead on every property
|
|
485
|
+
// read (~2.6× faster) and stays a plain, serializable object.
|
|
486
|
+
if (propNames !== null && propNames.length > 0) {
|
|
487
|
+
// Methods live on the prototype, not on the object itself. Keeping the
|
|
488
|
+
// data properties as the only own properties lets the JIT treat the
|
|
489
|
+
// object as monomorphic, which is ~2× faster for accessor reads than
|
|
490
|
+
// having extra own properties (read/write/update/key/buffer).
|
|
491
|
+
const proto: Record<string | symbol, unknown> = {
|
|
492
|
+
[Symbol.toPrimitive]: () => target.read(),
|
|
493
|
+
};
|
|
494
|
+
Object.defineProperty(proto, "read", {
|
|
495
|
+
enumerable: false,
|
|
496
|
+
configurable: false,
|
|
497
|
+
value: target.read,
|
|
498
|
+
});
|
|
499
|
+
Object.defineProperty(proto, "write", {
|
|
500
|
+
enumerable: false,
|
|
501
|
+
configurable: false,
|
|
502
|
+
value: target.write,
|
|
503
|
+
});
|
|
504
|
+
Object.defineProperty(proto, "update", {
|
|
505
|
+
enumerable: false,
|
|
506
|
+
configurable: false,
|
|
507
|
+
value: target.update,
|
|
508
|
+
});
|
|
509
|
+
// key/buffer are non-enumerable so they don't appear in JSON.stringify /
|
|
510
|
+
// Object.keys (matching the previous Proxy behavior).
|
|
511
|
+
Object.defineProperty(proto, "key", {
|
|
512
|
+
enumerable: false,
|
|
513
|
+
configurable: false,
|
|
514
|
+
value: key,
|
|
515
|
+
});
|
|
516
|
+
Object.defineProperty(proto, "buffer", {
|
|
517
|
+
enumerable: false,
|
|
518
|
+
configurable: false,
|
|
519
|
+
get() {
|
|
520
|
+
return mem.dataTable.buffers[0];
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
const ref = Object.create(proto) as Record<string | symbol, unknown>;
|
|
524
|
+
for (const name of propNames) {
|
|
525
|
+
Object.defineProperty(ref, name, {
|
|
526
|
+
enumerable: true,
|
|
527
|
+
configurable: false,
|
|
528
|
+
get() {
|
|
529
|
+
// Lock-free fast path inlined so the JIT can optimize the hot
|
|
530
|
+
// cache-hit read without a function-call chain.
|
|
531
|
+
const cached = readCache.get(key);
|
|
532
|
+
if (
|
|
533
|
+
cached !== undefined &&
|
|
534
|
+
cached.version === mem.getVersion(cached.index)
|
|
535
|
+
) {
|
|
536
|
+
const v = cached.value;
|
|
537
|
+
if (Array.isArray(v)) {
|
|
538
|
+
return getTopLevelArrayMember(mem, key, v, name, arrayCapacity);
|
|
539
|
+
}
|
|
540
|
+
const pv = (v as Record<string | symbol, unknown>)[name];
|
|
541
|
+
if (Array.isArray(pv)) {
|
|
542
|
+
return createArrayProxy(mem, key, name, arrayCapacity);
|
|
543
|
+
}
|
|
544
|
+
return pv;
|
|
545
|
+
}
|
|
546
|
+
return readProp(mem, key, name, arrayCapacity);
|
|
547
|
+
},
|
|
548
|
+
set(v: unknown) {
|
|
549
|
+
target.__cache = undefined;
|
|
550
|
+
writeProp(mem, key, name, v, arrayCapacity);
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
// Fixed shape: reject adding new properties (throws in strict mode).
|
|
555
|
+
Object.preventExtensions(ref);
|
|
556
|
+
return ref as unknown as SharedRef<T>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Arrays, primitives, or unknown shape: use a Proxy (index access, array
|
|
560
|
+
// methods, and dynamic properties require interception).
|
|
355
561
|
return new Proxy(target, {
|
|
356
562
|
get(t, prop) {
|
|
357
563
|
if (prop === "read") return t.read;
|
|
358
564
|
if (prop === "write") return t.write;
|
|
359
565
|
if (prop === "update") return t.update;
|
|
360
566
|
if (prop === "key") return t.key;
|
|
361
|
-
if (prop === "buffer") return mem.dataTable.
|
|
567
|
+
if (prop === "buffer") return mem.dataTable.buffers[0];
|
|
362
568
|
if (prop === Symbol.toPrimitive) return () => t.read();
|
|
363
569
|
|
|
364
570
|
// push/pop on a top-level array: operate directly on the array ref
|
|
@@ -371,177 +577,12 @@ function createSharedRef<T>(
|
|
|
371
577
|
}
|
|
372
578
|
}
|
|
373
579
|
|
|
374
|
-
|
|
375
|
-
// from the cached value (no decode, no lock). The cache entry is kept
|
|
376
|
-
// directly on the target to avoid a Map lookup on the hot path.
|
|
377
|
-
const cached = t.__cache;
|
|
378
|
-
if (
|
|
379
|
-
cached !== undefined &&
|
|
380
|
-
cached.version === mem.getVersion(cached.index)
|
|
381
|
-
) {
|
|
382
|
-
const cachedValue = cached.value;
|
|
383
|
-
if (Array.isArray(cachedValue)) {
|
|
384
|
-
return getTopLevelArrayMember(
|
|
385
|
-
mem,
|
|
386
|
-
key,
|
|
387
|
-
cachedValue,
|
|
388
|
-
prop,
|
|
389
|
-
arrayCapacity,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
const propValue = (cachedValue as Record<string | symbol, unknown>)[
|
|
393
|
-
prop
|
|
394
|
-
];
|
|
395
|
-
if (Array.isArray(propValue)) {
|
|
396
|
-
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
397
|
-
}
|
|
398
|
-
return propValue;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Cache miss: for objects, decode only the requested property's slot via
|
|
402
|
-
// the cached layout, avoiding a full-object decode. The decoded value is
|
|
403
|
-
// cached per property so repeated reads of the same property are cheap.
|
|
404
|
-
const layout = getObjectLayout(mem, key);
|
|
405
|
-
if (layout !== null) {
|
|
406
|
-
const slotOffset = layout.properties.get(prop as string);
|
|
407
|
-
if (slotOffset === undefined) return undefined;
|
|
408
|
-
|
|
409
|
-
// Property cache hit: the object's version is unchanged.
|
|
410
|
-
const pc = propCache.get(key);
|
|
411
|
-
const version = pc !== undefined ? mem.getVersion(pc.index) : -1;
|
|
412
|
-
if (pc !== undefined && pc.version === version) {
|
|
413
|
-
const cachedProp = pc.props.get(prop as string);
|
|
414
|
-
if (cachedProp !== undefined) {
|
|
415
|
-
if (Array.isArray(cachedProp)) {
|
|
416
|
-
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
417
|
-
}
|
|
418
|
-
return cachedProp;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Decode the single slot and cache it under the object's version.
|
|
423
|
-
// The key's index is stable (keys are never removed), so reuse the
|
|
424
|
-
// cached index when available to avoid the linear index-table lookup.
|
|
425
|
-
const index =
|
|
426
|
-
pc !== undefined
|
|
427
|
-
? pc.index
|
|
428
|
-
: (readCache.get(key)?.index ?? getIndex(mem, key));
|
|
429
|
-
const slot = mem.readValueSlot(slotOffset);
|
|
430
|
-
// Array properties are handled lazily by the array proxy; don't decode
|
|
431
|
-
// the whole array here (avoids O(n) work on every access).
|
|
432
|
-
if (slot.type === TYPE_ARRAY_REF) {
|
|
433
|
-
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
434
|
-
}
|
|
435
|
-
const propValue = decodeValue(mem, slot);
|
|
436
|
-
if (pc === undefined || pc.version !== version) {
|
|
437
|
-
propCache.set(key, {
|
|
438
|
-
version: mem.getVersion(index),
|
|
439
|
-
index,
|
|
440
|
-
props: new Map(),
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
propCache.get(key)!.props.set(prop as string, propValue);
|
|
444
|
-
if (Array.isArray(propValue)) {
|
|
445
|
-
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
446
|
-
}
|
|
447
|
-
return propValue;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Primitive/array key: use the cached value directly (no copy).
|
|
451
|
-
const value = readValue(mem, key, t);
|
|
452
|
-
if (Array.isArray(value)) {
|
|
453
|
-
return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
|
|
454
|
-
}
|
|
455
|
-
// Object value: property access.
|
|
456
|
-
const propValue = (value as Record<string | symbol, unknown>)[prop];
|
|
457
|
-
if (Array.isArray(propValue)) {
|
|
458
|
-
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
459
|
-
}
|
|
460
|
-
return propValue;
|
|
580
|
+
return readProp(mem, key, prop, arrayCapacity);
|
|
461
581
|
},
|
|
462
582
|
set(t, prop, val) {
|
|
463
|
-
// In-place update: write only the affected slot instead of decoding and
|
|
464
|
-
// re-encoding the whole value.
|
|
465
583
|
t.__cache = undefined;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const layout = getObjectLayout(mem, key);
|
|
469
|
-
if (layout !== null) {
|
|
470
|
-
const slotOffset = layout.properties.get(prop as string);
|
|
471
|
-
if (slotOffset === undefined) {
|
|
472
|
-
throw new TypeError(
|
|
473
|
-
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
mem.updateValueSlot(slotOffset, encodeValue(mem, val));
|
|
477
|
-
bump(mem, key);
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Top-level array index write: update only that array slot.
|
|
482
|
-
const offset = getOffset(mem, key);
|
|
483
|
-
if (offset === null) {
|
|
484
|
-
throw new Error(`No shared value found for key "${key}"`);
|
|
485
|
-
}
|
|
486
|
-
const slot = mem.readValueSlot(offset);
|
|
487
|
-
if (
|
|
488
|
-
slot.type === TYPE_ARRAY_REF &&
|
|
489
|
-
typeof prop === "string" &&
|
|
490
|
-
/^\d+$/.test(prop)
|
|
491
|
-
) {
|
|
492
|
-
const index = Number(prop);
|
|
493
|
-
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
494
|
-
if (index >= ref.count) {
|
|
495
|
-
throw new RangeError(
|
|
496
|
-
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
500
|
-
bump(mem, key);
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Top-level array length write: shrink/grow the array in place without
|
|
505
|
-
// re-encoding the whole array (which would leave a dead slot behind).
|
|
506
|
-
if (slot.type === TYPE_ARRAY_REF && prop === "length") {
|
|
507
|
-
const newLen = Number(val);
|
|
508
|
-
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
509
|
-
throw new RangeError("Invalid array length");
|
|
510
|
-
}
|
|
511
|
-
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
512
|
-
if (newLen <= ref.capacity) {
|
|
513
|
-
// Shrinking (or growing within capacity): just update the count.
|
|
514
|
-
mem.updateValueSlot(
|
|
515
|
-
offset,
|
|
516
|
-
arrayRefSlot({
|
|
517
|
-
offset: ref.offset,
|
|
518
|
-
count: newLen,
|
|
519
|
-
capacity: ref.capacity,
|
|
520
|
-
}),
|
|
521
|
-
);
|
|
522
|
-
bump(mem, key);
|
|
523
|
-
return true;
|
|
524
|
-
}
|
|
525
|
-
// Growing beyond capacity falls through to the full read-modify-write.
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Fallback: full read-modify-write (e.g. setting a primitive key's
|
|
529
|
-
// property, or a non-index property on an array).
|
|
530
|
-
return mutateValue<T, boolean>(
|
|
531
|
-
mem,
|
|
532
|
-
key,
|
|
533
|
-
(value) => {
|
|
534
|
-
if (Array.isArray(value)) {
|
|
535
|
-
(value as unknown as Record<string | symbol, unknown>)[prop] =
|
|
536
|
-
val;
|
|
537
|
-
} else {
|
|
538
|
-
(value as Record<string | symbol, unknown>)[prop] = val;
|
|
539
|
-
}
|
|
540
|
-
return true;
|
|
541
|
-
},
|
|
542
|
-
arrayCapacity,
|
|
543
|
-
);
|
|
544
|
-
});
|
|
584
|
+
writeProp(mem, key, prop, val, arrayCapacity);
|
|
585
|
+
return true;
|
|
545
586
|
},
|
|
546
587
|
deleteProperty() {
|
|
547
588
|
throw new TypeError(
|
|
@@ -551,6 +592,22 @@ function createSharedRef<T>(
|
|
|
551
592
|
}) as unknown as SharedRef<T>;
|
|
552
593
|
}
|
|
553
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Lock-free fast path: if the cached version matches the current version cell,
|
|
597
|
+
* the cached value is still valid. Returns the cached value, or a miss marker.
|
|
598
|
+
* Kept small so the JIT can inline it into hot accessor getters.
|
|
599
|
+
*/
|
|
600
|
+
function readCachedValue(
|
|
601
|
+
mem: SharedMemory,
|
|
602
|
+
key: string,
|
|
603
|
+
): { hit: true; value: unknown } | { hit: false } {
|
|
604
|
+
const cached = readCache.get(key);
|
|
605
|
+
if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
|
|
606
|
+
return { hit: true, value: cached.value };
|
|
607
|
+
}
|
|
608
|
+
return { hit: false };
|
|
609
|
+
}
|
|
610
|
+
|
|
554
611
|
/**
|
|
555
612
|
* Read the full value for a key from shared memory, using the read cache.
|
|
556
613
|
*
|
|
@@ -565,15 +622,12 @@ function readValue(
|
|
|
565
622
|
__cache?: { version: number; value: unknown; index: number };
|
|
566
623
|
},
|
|
567
624
|
): unknown {
|
|
625
|
+
const fast = readCachedValue(mem, key);
|
|
626
|
+
if (fast.hit) return fast.value;
|
|
627
|
+
|
|
568
628
|
const cached = readCache.get(key);
|
|
569
629
|
const index = cached !== undefined ? cached.index : getIndex(mem, key);
|
|
570
|
-
|
|
571
|
-
// Lock-free fast path: if the cached version matches the current version
|
|
572
|
-
// cell, the cached value is still valid.
|
|
573
630
|
const version = mem.getVersion(index);
|
|
574
|
-
if (cached !== undefined && cached.version === version) {
|
|
575
|
-
return cached.value;
|
|
576
|
-
}
|
|
577
631
|
|
|
578
632
|
// Cache miss: decode under the lock and cache with the observed version.
|
|
579
633
|
const value = mem.withLock(() => {
|
|
@@ -581,7 +635,7 @@ function readValue(
|
|
|
581
635
|
if (offset === null) {
|
|
582
636
|
throw new Error(`No shared value found for key "${key}"`);
|
|
583
637
|
}
|
|
584
|
-
const type = mem.dataTable.
|
|
638
|
+
const type = mem.dataTable.getInt32(offset);
|
|
585
639
|
if (type === 5) {
|
|
586
640
|
// Object block.
|
|
587
641
|
const properties = mem.readObject(offset);
|
|
@@ -611,7 +665,7 @@ function writeValue(
|
|
|
611
665
|
mem.withLock(() => {
|
|
612
666
|
const existing = getOffset(mem, key);
|
|
613
667
|
if (existing !== null) {
|
|
614
|
-
const type = mem.dataTable.
|
|
668
|
+
const type = mem.dataTable.getInt32(existing);
|
|
615
669
|
if (type === 5) {
|
|
616
670
|
// Update existing object's property slots in place.
|
|
617
671
|
const properties = mem.readObject(existing);
|
package/src/worker.ts
CHANGED
|
@@ -144,6 +144,7 @@ function stopCoordinator(): void {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
147
148
|
/** The main-thread worker listener, so it can be removed by `stopInit()`. */
|
|
148
149
|
let mainListener:
|
|
149
150
|
| ((worker: {
|
|
@@ -166,12 +167,17 @@ function initMain(): void {
|
|
|
166
167
|
}) => {
|
|
167
168
|
worker.addEventListener("message", (e) => {
|
|
168
169
|
const msg = e.data as { __sharedMemoryRequest?: boolean } | undefined;
|
|
169
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
msg !== null &&
|
|
172
|
+
typeof msg === "object" &&
|
|
173
|
+
"__sharedMemoryRequest" in msg
|
|
174
|
+
) {
|
|
170
175
|
const mem = getDefaultMemory();
|
|
171
176
|
worker.postMessage({
|
|
172
177
|
__sharedMemoryInit: true,
|
|
173
178
|
buffers: mem.exportBuffers(),
|
|
174
179
|
});
|
|
180
|
+
return;
|
|
175
181
|
}
|
|
176
182
|
});
|
|
177
183
|
};
|
|
@@ -234,7 +240,12 @@ function installMessageWrapper(): void {
|
|
|
234
240
|
buffers?: Parameters<typeof SharedMemory.fromBuffers>[0];
|
|
235
241
|
}
|
|
236
242
|
| undefined;
|
|
237
|
-
if (
|
|
243
|
+
if (
|
|
244
|
+
msg !== null &&
|
|
245
|
+
typeof msg === "object" &&
|
|
246
|
+
"__sharedMemoryInit" in msg &&
|
|
247
|
+
msg.buffers
|
|
248
|
+
) {
|
|
238
249
|
const mem = SharedMemory.fromBuffers(msg.buffers);
|
|
239
250
|
setDefaultMemory(mem);
|
|
240
251
|
// Create the worker-side refcount tracker from the shared refcount
|