bun-shared-memory 0.2.0 → 0.2.1

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 CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-11-04
9
+
10
+ ### Performance
11
+
12
+ - Inline the read-cache fast path in `readProp`/`readValue` instead of calling
13
+ `readCachedValue`, which returned a polymorphic `{hit,value}` union and
14
+ allocated an object on every cache hit. Keeping the read path monomorphic
15
+ lets the JIT optimize it: number reads dropped from ~12× to ~6× slower than
16
+ native, boolean reads from ~8.4× to ~5.5×, and all array index reads
17
+ improved (array-read summary ~9.7× → ~8.7×).
18
+ - Add `updateBigIntSlot`/`updateArrayBigIntSlot` fast paths that write int64
19
+ bigints directly into the slot, avoiding the `ValueSlot` object allocation
20
+ of the generic encode path. `bigint[]` index writes dropped from ~24.8× to
21
+ ~18× slower than native.
22
+ - Extract the array-proxy set handler body into a named `writeArrayProp`
23
+ function and inline the `writeLock`, so no closure is allocated per write.
24
+ - Cache the key's version-cell index in the array proxy so writes bump the
25
+ version without a per-write Map lookup. Object array-prop index writes
26
+ dropped from ~33.9× to ~29× slower than native.
27
+ - Real-world benchmark: the shared-memory "get user" endpoint improved from
28
+ ~4.18M to ~5.79M req/s (+39%), widening the gap over the comparison
29
+ backends (sqlite 6.2×→8.5×, redis 218×→302×, mysql 233×→322× slower than
30
+ shared memory).
31
+
8
32
  ## [0.2.0] - 2026-11-04
9
33
 
10
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-shared-memory",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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
  "sideEffects": false,
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/bun": "latest",
50
- "cofusion": "^0.0.1",
50
+ "cofusion": "^0.0.2",
51
51
  "elysia": "^1.4.30"
52
52
  }
53
53
  }
package/src/memory.ts CHANGED
@@ -812,6 +812,20 @@ export class SharedMemory {
812
812
  buf.setInt32(off + 12, 0);
813
813
  }
814
814
 
815
+ /**
816
+ * Fast-path: write an int64 bigint into an array slot directly, avoiding
817
+ * the ValueSlot object allocation and the 4 separate Int32 writes.
818
+ */
819
+ updateArrayBigIntSlot(ref: ArrayRef, index: number, value: bigint): void {
820
+ slotView.setBigInt64(0, value, true);
821
+ const buf = this.arrayTable;
822
+ const off = ref.offset + ARRAY_HEADER + index * SLOT_SIZE;
823
+ buf.setInt32(off, TYPE_BIGINT);
824
+ buf.setInt32(off + 4, slotView.getInt32(0, true));
825
+ buf.setInt32(off + 8, slotView.getInt32(4, true));
826
+ buf.setInt32(off + 12, 0);
827
+ }
828
+
815
829
  /**
816
830
  * Fast-path: write a short string inline into an array slot (up to 11
817
831
  * UTF-8 bytes), avoiding a heap allocation and the string-table write.
@@ -970,6 +984,19 @@ export class SharedMemory {
970
984
  buf.setInt32(offset + 12, 0);
971
985
  }
972
986
 
987
+ /**
988
+ * Fast-path: write an int64 bigint slot directly into the DataTable,
989
+ * avoiding the ValueSlot object allocation of the generic path.
990
+ */
991
+ updateBigIntSlot(offset: number, value: bigint): void {
992
+ slotView.setBigInt64(0, value, true);
993
+ const buf = this.dataTable;
994
+ buf.setInt32(offset, TYPE_BIGINT);
995
+ buf.setInt32(offset + 4, slotView.getInt32(0, true));
996
+ buf.setInt32(offset + 8, slotView.getInt32(4, true));
997
+ buf.setInt32(offset + 12, 0);
998
+ }
999
+
973
1000
  /**
974
1001
  * Fast-path: write a short string inline into the slot itself (up to 11
975
1002
  * UTF-8 bytes), avoiding a heap allocation and the string-table write.
package/src/shared.ts CHANGED
@@ -443,8 +443,14 @@ function readProp(
443
443
  prop: string | symbol,
444
444
  arrayCapacity?: number,
445
445
  ): unknown {
446
- const fast = readCachedValue(mem, key);
447
- const value = fast.hit ? fast.value : readValue(mem, key);
446
+ // Inline the read-cache fast path (instead of calling readCachedValue, which
447
+ // returns a polymorphic {hit,value} union and allocates an object on every
448
+ // cache hit). Keeping this monomorphic lets the JIT optimize the hot read.
449
+ const cached = readCache.get(key);
450
+ const value =
451
+ cached !== undefined && cached.version === mem.getVersion(cached.index)
452
+ ? cached.value
453
+ : readValue(mem, key);
448
454
  if (Array.isArray(value)) {
449
455
  return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
450
456
  }
@@ -493,6 +499,56 @@ function readObjectProp(
493
499
  return value;
494
500
  }
495
501
 
502
+ /**
503
+ * Write a single object-property slot whose offset is already resolved
504
+ * (cached by the caller, e.g. the accessor setter). Avoids the
505
+ * `getObjectLayout` Map lookup + `properties.get` per write on the hot path.
506
+ */
507
+ function writeObjectSlot(
508
+ mem: SharedMemory,
509
+ key: string,
510
+ slotOffset: number,
511
+ val: unknown,
512
+ arrayCapacity?: number,
513
+ target?: WriteTarget,
514
+ ): void {
515
+ if (typeof val === "number") {
516
+ // Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
517
+ // and no in-place string reuse; write the float64 directly.
518
+ mem.updateNumberSlot(slotOffset, val);
519
+ bumpTarget(mem, key, target);
520
+ return;
521
+ }
522
+ if (typeof val === "boolean") {
523
+ // Fast path: booleans need no old-slot read (freeSlotRefs is a no-op)
524
+ // and no in-place string reuse; write the 4 Int32s directly.
525
+ mem.updateBooleanSlot(slotOffset, val);
526
+ bumpTarget(mem, key, target);
527
+ return;
528
+ }
529
+ if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
530
+ // Fast path: int64 bigints are written directly, avoiding the ValueSlot
531
+ // object allocation of the generic path.
532
+ mem.updateBigIntSlot(slotOffset, val);
533
+ bumpTarget(mem, key, target);
534
+ return;
535
+ }
536
+ if (typeof val === "string") {
537
+ // Fast path: short strings are written inline into the slot itself.
538
+ if (mem.updateInlineStringSlot(slotOffset, val)) {
539
+ bumpTarget(mem, key, target);
540
+ return;
541
+ }
542
+ }
543
+ const oldSlot = mem.readValueSlot(slotOffset);
544
+ freeSlotRefs(mem, oldSlot);
545
+ mem.updateValueSlot(
546
+ slotOffset,
547
+ encodeValue(mem, val, arrayCapacity, oldSlot),
548
+ );
549
+ bumpTarget(mem, key, target);
550
+ }
551
+
496
552
  /**
497
553
  * Write a single property of a shared object value in place.
498
554
  *
@@ -544,27 +600,7 @@ function writePropInner(
544
600
  `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
545
601
  );
546
602
  }
547
- if (typeof val === "number") {
548
- // Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
549
- // and no in-place string reuse; write the float64 directly.
550
- mem.updateNumberSlot(slotOffset, val);
551
- bumpTarget(mem, key, target);
552
- return;
553
- }
554
- if (typeof val === "string") {
555
- // Fast path: short strings are written inline into the slot itself.
556
- if (mem.updateInlineStringSlot(slotOffset, val)) {
557
- bumpTarget(mem, key, target);
558
- return;
559
- }
560
- }
561
- const oldSlot = mem.readValueSlot(slotOffset);
562
- freeSlotRefs(mem, oldSlot);
563
- mem.updateValueSlot(
564
- slotOffset,
565
- encodeValue(mem, val, arrayCapacity, oldSlot),
566
- );
567
- bumpTarget(mem, key, target);
603
+ writeObjectSlot(mem, key, slotOffset, val, arrayCapacity, target);
568
604
  return;
569
605
  }
570
606
  }
@@ -622,6 +658,13 @@ function writePropInner(
622
658
  bumpTarget(mem, key, target);
623
659
  return;
624
660
  }
661
+ if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
662
+ // Fast path: int64 bigints are written directly, avoiding the
663
+ // ValueSlot object allocation of the generic path.
664
+ mem.updateArrayBigIntSlot(ref, index, val);
665
+ bumpTarget(mem, key, target);
666
+ return;
667
+ }
625
668
  if (typeof val === "string") {
626
669
  // Fast path: short strings are written inline into the array slot.
627
670
  if (mem.updateArrayInlineStringSlot(ref, index, val)) {
@@ -811,6 +854,12 @@ function createSharedRef<T>(
811
854
  // recreate the array proxy on every access. The proxy reads fresh data
812
855
  // on each access, so it stays correct without being recreated.
813
856
  let arrayProxy: unknown[] | undefined;
857
+ // Cached object slot offset for this property so writes skip the
858
+ // getObjectLayout Map lookup + properties.get on the hot path.
859
+ // Invalidated when the layout version changes (defragmentation moves
860
+ // the object block).
861
+ let slotOffset: number | undefined;
862
+ let slotLayoutVersion = -1;
814
863
  Object.defineProperty(ref, name, {
815
864
  enumerable: true,
816
865
  configurable: false,
@@ -845,7 +894,45 @@ function createSharedRef<T>(
845
894
  },
846
895
  set(v: unknown) {
847
896
  target.__cache = undefined;
848
- writeProp(mem, key, name, v, arrayCapacity, target);
897
+ mem.beginWrite();
898
+ if (mem.isDefragmenting()) {
899
+ // A defrag pass may move the object block: drop the cached slot
900
+ // offset so it is re-resolved, and run the write under the mutex.
901
+ mem.endWrite();
902
+ slotOffset = undefined;
903
+ slotLayoutVersion = -1;
904
+ mem.withLock(() =>
905
+ writePropInner(mem, key, name, v, arrayCapacity, target),
906
+ );
907
+ return;
908
+ }
909
+ try {
910
+ // Write directly to the cached slot offset when still valid,
911
+ // skipping the getObjectLayout Map lookup + properties.get.
912
+ if (slotLayoutVersion !== mem.getLayoutVersion()) {
913
+ slotOffset = undefined;
914
+ slotLayoutVersion = mem.getLayoutVersion();
915
+ }
916
+ if (slotOffset === undefined) {
917
+ const layout = getObjectLayout(mem, key);
918
+ if (layout === null) {
919
+ // Not an object (shouldn't happen for an object accessor).
920
+ throw new TypeError(
921
+ `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
922
+ );
923
+ }
924
+ const so = layout.properties.get(name);
925
+ if (so === undefined) {
926
+ throw new TypeError(
927
+ `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
928
+ );
929
+ }
930
+ slotOffset = so;
931
+ }
932
+ writeObjectSlot(mem, key, slotOffset, v, arrayCapacity, target);
933
+ } finally {
934
+ mem.endWrite();
935
+ }
849
936
  },
850
937
  });
851
938
  }
@@ -890,22 +977,6 @@ function createSharedRef<T>(
890
977
  }) as unknown as SharedRef<T>;
891
978
  }
892
979
 
893
- /**
894
- * Lock-free fast path: if the cached version matches the current version cell,
895
- * the cached value is still valid. Returns the cached value, or a miss marker.
896
- * Kept small so the JIT can inline it into hot accessor getters.
897
- */
898
- function readCachedValue(
899
- mem: SharedMemory,
900
- key: string,
901
- ): { hit: true; value: unknown } | { hit: false } {
902
- const cached = readCache.get(key);
903
- if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
904
- return { hit: true, value: cached.value };
905
- }
906
- return { hit: false };
907
- }
908
-
909
980
  /**
910
981
  * Read the full value for a key from shared memory, using the read cache.
911
982
  *
@@ -920,10 +991,13 @@ function readValue(
920
991
  __cache?: { version: number; value: unknown; index: number };
921
992
  },
922
993
  ): unknown {
923
- const fast = readCachedValue(mem, key);
924
- if (fast.hit) return fast.value;
925
-
994
+ // Inline the read-cache fast path (see readProp) to avoid the polymorphic
995
+ // {hit,value} union returned by readCachedValue.
926
996
  const cached = readCache.get(key);
997
+ if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
998
+ return cached.value;
999
+ }
1000
+
927
1001
  const index = cached !== undefined ? cached.index : getIndex(mem, key);
928
1002
  const version = mem.getVersion(index);
929
1003
 
@@ -1037,6 +1111,10 @@ function writeValueInner(
1037
1111
  // no-op) and no in-place string reuse.
1038
1112
  if (typeof value === "boolean") {
1039
1113
  mem.updateBooleanSlot(existing, value);
1114
+ } else if (value >= MIN_INT64 && value <= MAX_INT64) {
1115
+ // Fast path: int64 bigints are written directly, avoiding the
1116
+ // ValueSlot object allocation of the generic path.
1117
+ mem.updateBigIntSlot(existing, value);
1040
1118
  } else {
1041
1119
  mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
1042
1120
  }
@@ -1301,6 +1379,151 @@ function createArrayProxy(
1301
1379
  let cachedRef:
1302
1380
  { offset: number; count: number; capacity: number } | undefined;
1303
1381
  let cachedLayoutVersion = -1;
1382
+ // Cached version-cell index for this key, so writes can bump the version
1383
+ // cell without a Map lookup on every write. Stable for the key's lifetime.
1384
+ let cachedIndex: number | undefined;
1385
+
1386
+ /** Bump the key's version cell using the cached index (resolved lazily). */
1387
+ function bumpCached(): void {
1388
+ if (cachedIndex === undefined) {
1389
+ cachedIndex = getIndex(mem, key);
1390
+ }
1391
+ mem.bumpVersion(cachedIndex);
1392
+ }
1393
+
1394
+ /**
1395
+ * Write a single array-property slot in place. Defined once (not per call)
1396
+ * so the set handler can inline the writeLock without allocating a closure
1397
+ * on every write. Mutates the cached slot offset/ref/layout version.
1398
+ */
1399
+ function writeArrayProp(arrProp: string | symbol, val: unknown): boolean {
1400
+ // Refresh the cached slot offset when the layout version changed
1401
+ // (defragmentation may have moved the object block).
1402
+ if (cachedLayoutVersion !== mem.getLayoutVersion()) {
1403
+ cachedSlotOffset = undefined;
1404
+ cachedRef = undefined;
1405
+ cachedLayoutVersion = mem.getLayoutVersion();
1406
+ }
1407
+ if (cachedSlotOffset === undefined) {
1408
+ const layout = getObjectLayout(mem, key);
1409
+ if (layout === null) {
1410
+ // Fallback: full read-modify-write.
1411
+ return mutateValue<Record<string, unknown>, boolean>(
1412
+ mem,
1413
+ key,
1414
+ (fresh) => {
1415
+ const arr = fresh[prop] as unknown[];
1416
+ (arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
1417
+ return true;
1418
+ },
1419
+ arrayCapacity,
1420
+ );
1421
+ }
1422
+ const so = layout.properties.get(prop);
1423
+ if (so === undefined) {
1424
+ throw new TypeError(
1425
+ `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
1426
+ );
1427
+ }
1428
+ cachedSlotOffset = so;
1429
+ }
1430
+ const propSlot = mem.readValueSlot(cachedSlotOffset);
1431
+ if (
1432
+ propSlot.type === TYPE_ARRAY_REF &&
1433
+ typeof arrProp === "string" &&
1434
+ /^\d+$/.test(arrProp)
1435
+ ) {
1436
+ const index = Number(arrProp);
1437
+ // Use the cached ref when valid; re-read when the array may have
1438
+ // grown past the cached count (push) or the layout changed.
1439
+ let ref = cachedRef;
1440
+ if (
1441
+ ref === undefined ||
1442
+ index >= ref.count ||
1443
+ cachedLayoutVersion !== mem.getLayoutVersion()
1444
+ ) {
1445
+ ref = {
1446
+ offset: propSlot.a,
1447
+ count: propSlot.b,
1448
+ capacity: propSlot.c,
1449
+ };
1450
+ cachedRef = ref;
1451
+ }
1452
+ if (index >= ref.count) {
1453
+ throw new RangeError(
1454
+ `Index ${index} out of bounds for array of length ${ref.count}`,
1455
+ );
1456
+ }
1457
+ if (typeof val === "number") {
1458
+ // Fast path: numbers need no old-slot read (freeSlotRefs is a no-op).
1459
+ mem.updateArrayNumberSlot(ref, index, val);
1460
+ bumpCached();
1461
+ return true;
1462
+ }
1463
+ if (typeof val === "boolean") {
1464
+ // Fast path: booleans need no old-slot read (freeSlotRefs is a no-op).
1465
+ mem.updateArrayBooleanSlot(ref, index, val);
1466
+ bumpCached();
1467
+ return true;
1468
+ }
1469
+ if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
1470
+ // Fast path: int64 bigints are written directly, avoiding the
1471
+ // ValueSlot object allocation of the generic path.
1472
+ mem.updateArrayBigIntSlot(ref, index, val);
1473
+ bumpCached();
1474
+ return true;
1475
+ }
1476
+ if (typeof val === "string") {
1477
+ // Fast path: short strings are written inline into the slot.
1478
+ if (mem.updateArrayInlineStringSlot(ref, index, val)) {
1479
+ bumpCached();
1480
+ return true;
1481
+ }
1482
+ }
1483
+ freeSlotRefs(mem, mem.readArraySlot(ref, index));
1484
+ mem.updateArraySlot(ref, index, encodeValue(mem, val));
1485
+ bumpCached();
1486
+ return true;
1487
+ }
1488
+ // Array property length write: shrink/grow in place without re-encoding
1489
+ // the whole object (which would leave a dead slot).
1490
+ if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
1491
+ const newLen = Number(val);
1492
+ if (!Number.isInteger(newLen) || newLen < 0) {
1493
+ throw new RangeError("Invalid array length");
1494
+ }
1495
+ const ref = {
1496
+ offset: propSlot.a,
1497
+ count: propSlot.b,
1498
+ capacity: propSlot.c,
1499
+ };
1500
+ if (newLen <= ref.capacity) {
1501
+ mem.updateValueSlot(
1502
+ cachedSlotOffset,
1503
+ arrayRefSlot({
1504
+ offset: ref.offset,
1505
+ count: newLen,
1506
+ capacity: ref.capacity,
1507
+ }),
1508
+ );
1509
+ bumpCached();
1510
+ return true;
1511
+ }
1512
+ // Growing beyond capacity falls through to the full read-modify-write.
1513
+ }
1514
+ // Fallback: full read-modify-write.
1515
+ return mutateValue<Record<string, unknown>, boolean>(
1516
+ mem,
1517
+ key,
1518
+ (fresh) => {
1519
+ const arr = fresh[prop] as unknown[];
1520
+ (arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
1521
+ return true;
1522
+ },
1523
+ arrayCapacity,
1524
+ );
1525
+ }
1526
+
1304
1527
  return new Proxy([] as unknown[], {
1305
1528
  get(_target, arrProp) {
1306
1529
  // push/pop on an array property: operate directly on the array ref
@@ -1357,130 +1580,18 @@ function createArrayProxy(
1357
1580
  },
1358
1581
  set(_target, arrProp, val) {
1359
1582
  // In-place update: write only the affected array slot instead of
1360
- // decoding and re-encoding the whole object.
1361
- return writeLock(mem, () => {
1362
- // Refresh the cached slot offset when the layout version changed
1363
- // (defragmentation may have moved the object block).
1364
- if (cachedLayoutVersion !== mem.getLayoutVersion()) {
1365
- cachedSlotOffset = undefined;
1366
- cachedRef = undefined;
1367
- cachedLayoutVersion = mem.getLayoutVersion();
1368
- }
1369
- if (cachedSlotOffset === undefined) {
1370
- const layout = getObjectLayout(mem, key);
1371
- if (layout === null) {
1372
- // Fallback: full read-modify-write.
1373
- return mutateValue<Record<string, unknown>, boolean>(
1374
- mem,
1375
- key,
1376
- (fresh) => {
1377
- const arr = fresh[prop] as unknown[];
1378
- (arr as unknown as Record<string | symbol, unknown>)[arrProp] =
1379
- val;
1380
- return true;
1381
- },
1382
- arrayCapacity,
1383
- );
1384
- }
1385
- const so = layout.properties.get(prop);
1386
- if (so === undefined) {
1387
- throw new TypeError(
1388
- `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
1389
- );
1390
- }
1391
- cachedSlotOffset = so;
1392
- }
1393
- const propSlot = mem.readValueSlot(cachedSlotOffset);
1394
- if (
1395
- propSlot.type === TYPE_ARRAY_REF &&
1396
- typeof arrProp === "string" &&
1397
- /^\d+$/.test(arrProp)
1398
- ) {
1399
- const index = Number(arrProp);
1400
- // Use the cached ref when valid; re-read when the array may have
1401
- // grown past the cached count (push) or the layout changed.
1402
- let ref = cachedRef;
1403
- if (
1404
- ref === undefined ||
1405
- index >= ref.count ||
1406
- cachedLayoutVersion !== mem.getLayoutVersion()
1407
- ) {
1408
- ref = {
1409
- offset: propSlot.a,
1410
- count: propSlot.b,
1411
- capacity: propSlot.c,
1412
- };
1413
- cachedRef = ref;
1414
- }
1415
- if (index >= ref.count) {
1416
- throw new RangeError(
1417
- `Index ${index} out of bounds for array of length ${ref.count}`,
1418
- );
1419
- }
1420
- if (typeof val === "number") {
1421
- // Fast path: numbers need no old-slot read (freeSlotRefs is a
1422
- // no-op) and no in-place string reuse.
1423
- mem.updateArrayNumberSlot(ref, index, val);
1424
- bump(mem, key);
1425
- return true;
1426
- }
1427
- if (typeof val === "boolean") {
1428
- // Fast path: booleans need no old-slot read (freeSlotRefs is a
1429
- // no-op).
1430
- mem.updateArrayBooleanSlot(ref, index, val);
1431
- bump(mem, key);
1432
- return true;
1433
- }
1434
- if (typeof val === "string") {
1435
- // Fast path: short strings are written inline into the slot.
1436
- if (mem.updateArrayInlineStringSlot(ref, index, val)) {
1437
- bump(mem, key);
1438
- return true;
1439
- }
1440
- }
1441
- freeSlotRefs(mem, mem.readArraySlot(ref, index));
1442
- mem.updateArraySlot(ref, index, encodeValue(mem, val));
1443
- bump(mem, key);
1444
- return true;
1445
- }
1446
- // Array property length write: shrink/grow in place without
1447
- // re-encoding the whole object (which would leave a dead slot).
1448
- if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
1449
- const newLen = Number(val);
1450
- if (!Number.isInteger(newLen) || newLen < 0) {
1451
- throw new RangeError("Invalid array length");
1452
- }
1453
- const ref = {
1454
- offset: propSlot.a,
1455
- count: propSlot.b,
1456
- capacity: propSlot.c,
1457
- };
1458
- if (newLen <= ref.capacity) {
1459
- mem.updateValueSlot(
1460
- cachedSlotOffset,
1461
- arrayRefSlot({
1462
- offset: ref.offset,
1463
- count: newLen,
1464
- capacity: ref.capacity,
1465
- }),
1466
- );
1467
- bump(mem, key);
1468
- return true;
1469
- }
1470
- // Growing beyond capacity falls through to the full read-modify-write.
1471
- }
1472
- // Fallback: full read-modify-write.
1473
- return mutateValue<Record<string, unknown>, boolean>(
1474
- mem,
1475
- key,
1476
- (fresh) => {
1477
- const arr = fresh[prop] as unknown[];
1478
- (arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
1479
- return true;
1480
- },
1481
- arrayCapacity,
1482
- );
1483
- });
1583
+ // decoding and re-encoding the whole object. Inline writeLock to avoid
1584
+ // a closure allocation on the hot path.
1585
+ mem.beginWrite();
1586
+ if (mem.isDefragmenting()) {
1587
+ mem.endWrite();
1588
+ return mem.withLock(() => writeArrayProp(arrProp, val));
1589
+ }
1590
+ try {
1591
+ return writeArrayProp(arrProp, val);
1592
+ } finally {
1593
+ mem.endWrite();
1594
+ }
1484
1595
  },
1485
1596
  });
1486
1597
  }