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 +24 -0
- package/package.json +2 -2
- package/src/memory.ts +27 -0
- package/src/shared.ts +278 -167
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.
|
|
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.
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
924
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
}
|