bun-shared-memory 0.1.8 → 0.2.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/CHANGELOG.md +76 -0
- package/README.md +18 -18
- package/package.json +2 -1
- package/src/growable-buffer.ts +179 -0
- package/src/memory.ts +371 -41
- package/src/refcount.ts +31 -8
- package/src/shared.ts +643 -168
package/src/shared.ts
CHANGED
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
SharedMemory,
|
|
3
3
|
TYPE_ARRAY_REF,
|
|
4
4
|
TYPE_BIGINT_STR,
|
|
5
|
+
TYPE_INLINE_STRING,
|
|
6
|
+
TYPE_STRING_REF,
|
|
7
|
+
TYPE_STRING_VALUE_REF,
|
|
5
8
|
arrayRefSlot,
|
|
6
9
|
bigintSlot,
|
|
7
10
|
bigintStrSlot,
|
|
@@ -9,6 +12,7 @@ import {
|
|
|
9
12
|
decodeSlot,
|
|
10
13
|
numberSlot,
|
|
11
14
|
stringRefSlot,
|
|
15
|
+
stringValueRefSlot,
|
|
12
16
|
type ArrayRef,
|
|
13
17
|
type ObjectLayout,
|
|
14
18
|
type StringRef,
|
|
@@ -32,6 +36,26 @@ const MUTATING_METHODS = new Set([
|
|
|
32
36
|
|
|
33
37
|
type Primitive = number | string | boolean | bigint;
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Run a write operation. Writes are lock-free by default: they increment the
|
|
41
|
+
* in-flight write counter, and only fall back to the shared mutex if a
|
|
42
|
+
* defragmentation pass is running (rare). This keeps the hot write path free
|
|
43
|
+
* of the mutex's re-entrancy tracking while still preventing a write from
|
|
44
|
+
* racing with a defragmentation pass.
|
|
45
|
+
*/
|
|
46
|
+
function writeLock<T>(mem: SharedMemory, fn: () => T): T {
|
|
47
|
+
mem.beginWrite();
|
|
48
|
+
if (mem.isDefragmenting()) {
|
|
49
|
+
mem.endWrite();
|
|
50
|
+
return mem.withLock(fn);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return fn();
|
|
54
|
+
} finally {
|
|
55
|
+
mem.endWrite();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
/** Inclusive bounds of a signed 64-bit integer (stored as a fast int64 slot). */
|
|
36
60
|
const MIN_INT64 = -(2n ** 63n);
|
|
37
61
|
const MAX_INT64 = 2n ** 63n - 1n;
|
|
@@ -41,6 +65,7 @@ function encodeValue(
|
|
|
41
65
|
mem: SharedMemory,
|
|
42
66
|
value: unknown,
|
|
43
67
|
arrayCapacity?: number,
|
|
68
|
+
oldSlot?: ValueSlot,
|
|
44
69
|
): ValueSlot {
|
|
45
70
|
switch (typeof value) {
|
|
46
71
|
case "number":
|
|
@@ -55,7 +80,12 @@ function encodeValue(
|
|
|
55
80
|
}
|
|
56
81
|
return bigintStrSlot(mem.storeString(value.toString()));
|
|
57
82
|
case "string":
|
|
58
|
-
|
|
83
|
+
// Reuse the existing string block in place when the new string fits.
|
|
84
|
+
const oldRef =
|
|
85
|
+
oldSlot?.type === TYPE_STRING_VALUE_REF
|
|
86
|
+
? { offset: oldSlot.a, length: oldSlot.b }
|
|
87
|
+
: undefined;
|
|
88
|
+
return stringValueRefSlot(mem.storeStringValue(value, oldRef));
|
|
59
89
|
default:
|
|
60
90
|
if (Array.isArray(value)) {
|
|
61
91
|
for (const v of value) {
|
|
@@ -77,8 +107,12 @@ function encodeValue(
|
|
|
77
107
|
/** Decode a ValueSlot into a JS value, reading strings/arrays from tables. */
|
|
78
108
|
function decodeValue(mem: SharedMemory, slot: ValueSlot): unknown {
|
|
79
109
|
switch (slot.type) {
|
|
80
|
-
case
|
|
110
|
+
case TYPE_STRING_REF: // property-name/string-table ref
|
|
81
111
|
return mem.readString({ offset: slot.a, length: slot.b });
|
|
112
|
+
case TYPE_STRING_VALUE_REF: // string value in the ArrayTable
|
|
113
|
+
return mem.readStringValue({ offset: slot.a, length: slot.b });
|
|
114
|
+
case TYPE_INLINE_STRING: // short string stored inline in the slot
|
|
115
|
+
return mem.readInlineString(slot.a, slot.b, slot.c);
|
|
82
116
|
case TYPE_BIGINT_STR: // bigint stored as decimal string
|
|
83
117
|
return BigInt(mem.readString({ offset: slot.a, length: slot.b }));
|
|
84
118
|
case 4: // array-ref
|
|
@@ -90,6 +124,18 @@ function decodeValue(mem: SharedMemory, slot: ValueSlot): unknown {
|
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Free the blocks referenced by a value slot (an array block or a string),
|
|
129
|
+
* so they can be reused by the slab allocator. Called before a slot is
|
|
130
|
+
* overwritten with a new value. No-op for primitive slots.
|
|
131
|
+
*
|
|
132
|
+
* No-op: string-value/array/object blocks are no longer freed on the hot
|
|
133
|
+
* path (they are reclaimed by defragmentation instead).
|
|
134
|
+
*/
|
|
135
|
+
function freeSlotRefs(_mem: SharedMemory, _slot: ValueSlot): void {
|
|
136
|
+
// Intentionally a no-op; dead blocks are reclaimed by defrag.
|
|
137
|
+
}
|
|
138
|
+
|
|
93
139
|
/**
|
|
94
140
|
* A reference to a value stored in shared memory.
|
|
95
141
|
*
|
|
@@ -137,6 +183,18 @@ const readCache = new Map<
|
|
|
137
183
|
*/
|
|
138
184
|
const indexCache = new Map<string, number>();
|
|
139
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Per-worker cache of a key's current DataTable offset, valid only for the
|
|
188
|
+
* layout version it was resolved under. The offset changes on
|
|
189
|
+
* GC/defragmentation, so it is re-validated against the global layout version
|
|
190
|
+
* on each access (a single Atomics.load). This avoids the Index-Table read on
|
|
191
|
+
* every write, which is the dominant cost of the write hot path.
|
|
192
|
+
*/
|
|
193
|
+
const offsetCache = new Map<
|
|
194
|
+
string,
|
|
195
|
+
{ offset: number; layoutVersion: number }
|
|
196
|
+
>();
|
|
197
|
+
|
|
140
198
|
/** Resolve a key's stable index, caching it after the first lookup. */
|
|
141
199
|
function getIndex(mem: SharedMemory, key: string): number {
|
|
142
200
|
const cached = indexCache.get(key);
|
|
@@ -166,6 +224,65 @@ function getOffset(mem: SharedMemory, key: string): number | null {
|
|
|
166
224
|
return mem.lookupByIndex(index);
|
|
167
225
|
}
|
|
168
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The per-key cache fields on a SharedRef proxy target that the write hot
|
|
229
|
+
* paths use to skip Map lookups and DataTable reads. `__offset`/`__type` are
|
|
230
|
+
* valid only while `__layoutVersion` matches the global layout version;
|
|
231
|
+
* `__arrayRef` additionally requires `__arrayRefLayoutVersion` to match (it is
|
|
232
|
+
* bumped on defragmentation and on array growth via `moveArray`).
|
|
233
|
+
*/
|
|
234
|
+
interface WriteTarget {
|
|
235
|
+
__offset?: number;
|
|
236
|
+
__layoutVersion: number;
|
|
237
|
+
__type?: number;
|
|
238
|
+
__arrayRef?: { offset: number; count: number; capacity: number } | undefined;
|
|
239
|
+
__arrayRefLayoutVersion: number;
|
|
240
|
+
__index?: number | undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a key's current DataTable offset, caching it across writes. The
|
|
245
|
+
* cached offset is only reused when the global layout version is unchanged
|
|
246
|
+
* (i.e. no defragmentation has moved the data since it was cached); otherwise
|
|
247
|
+
* it is re-resolved from the Index Table.
|
|
248
|
+
*/
|
|
249
|
+
function getCachedOffset(
|
|
250
|
+
mem: SharedMemory,
|
|
251
|
+
key: string,
|
|
252
|
+
target?: WriteTarget,
|
|
253
|
+
): number | null {
|
|
254
|
+
if (target !== undefined && target.__offset !== undefined) {
|
|
255
|
+
if (target.__layoutVersion === mem.getLayoutVersion()) {
|
|
256
|
+
return target.__offset;
|
|
257
|
+
}
|
|
258
|
+
target.__offset = undefined;
|
|
259
|
+
target.__type = undefined;
|
|
260
|
+
target.__arrayRef = undefined;
|
|
261
|
+
}
|
|
262
|
+
const cached = offsetCache.get(key);
|
|
263
|
+
if (cached !== undefined) {
|
|
264
|
+
if (cached.layoutVersion === mem.getLayoutVersion()) {
|
|
265
|
+
if (target !== undefined) {
|
|
266
|
+
target.__offset = cached.offset;
|
|
267
|
+
target.__layoutVersion = cached.layoutVersion;
|
|
268
|
+
}
|
|
269
|
+
return cached.offset;
|
|
270
|
+
}
|
|
271
|
+
offsetCache.delete(key);
|
|
272
|
+
}
|
|
273
|
+
const offset = getOffset(mem, key);
|
|
274
|
+
if (offset !== null) {
|
|
275
|
+
offsetCache.set(key, { offset, layoutVersion: mem.getLayoutVersion() });
|
|
276
|
+
if (target !== undefined) {
|
|
277
|
+
target.__offset = offset;
|
|
278
|
+
target.__layoutVersion = mem.getLayoutVersion();
|
|
279
|
+
target.__type = undefined;
|
|
280
|
+
target.__arrayRef = undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return offset;
|
|
284
|
+
}
|
|
285
|
+
|
|
169
286
|
/** Whether a decoded value is a reference-countable object/array. */
|
|
170
287
|
function isTrackable(value: unknown): boolean {
|
|
171
288
|
return value !== null && typeof value === "object";
|
|
@@ -338,6 +455,44 @@ function readProp(
|
|
|
338
455
|
return propValue;
|
|
339
456
|
}
|
|
340
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Read a single object property for the per-property accessor cache, decoding
|
|
460
|
+
* only the requested slot (not the whole object) and caching the decoded value
|
|
461
|
+
* in `fieldCache` under the observed version. Array properties are cached as
|
|
462
|
+
* an array proxy so subsequent reads hit the fast path.
|
|
463
|
+
*
|
|
464
|
+
* This is the miss path for the object accessor getters; the hit path returns
|
|
465
|
+
* `fieldCache.value` directly.
|
|
466
|
+
*/
|
|
467
|
+
function readObjectProp(
|
|
468
|
+
mem: SharedMemory,
|
|
469
|
+
key: string,
|
|
470
|
+
prop: string,
|
|
471
|
+
fieldCache: { version: number; value: unknown },
|
|
472
|
+
version: number,
|
|
473
|
+
arrayCapacity?: number,
|
|
474
|
+
): unknown {
|
|
475
|
+
const layout = getObjectLayout(mem, key);
|
|
476
|
+
if (layout === null) {
|
|
477
|
+
// Not an object (shouldn't happen for an object accessor); fall back.
|
|
478
|
+
return readProp(mem, key, prop, arrayCapacity);
|
|
479
|
+
}
|
|
480
|
+
const slotOffset = layout.properties.get(prop);
|
|
481
|
+
if (slotOffset === undefined) {
|
|
482
|
+
throw new TypeError(
|
|
483
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
487
|
+
let value = decodeValue(mem, slot);
|
|
488
|
+
if (Array.isArray(value)) {
|
|
489
|
+
value = createArrayProxy(mem, key, prop, arrayCapacity);
|
|
490
|
+
}
|
|
491
|
+
fieldCache.version = version;
|
|
492
|
+
fieldCache.value = value;
|
|
493
|
+
return value;
|
|
494
|
+
}
|
|
495
|
+
|
|
341
496
|
/**
|
|
342
497
|
* Write a single property of a shared object value in place.
|
|
343
498
|
*
|
|
@@ -350,8 +505,36 @@ function writeProp(
|
|
|
350
505
|
prop: string | symbol,
|
|
351
506
|
val: unknown,
|
|
352
507
|
arrayCapacity?: number,
|
|
508
|
+
target?: WriteTarget,
|
|
353
509
|
): void {
|
|
354
|
-
|
|
510
|
+
// Inline writeLock to avoid a closure allocation on the hot path.
|
|
511
|
+
mem.beginWrite();
|
|
512
|
+
if (mem.isDefragmenting()) {
|
|
513
|
+
mem.endWrite();
|
|
514
|
+
mem.withLock(() =>
|
|
515
|
+
writePropInner(mem, key, prop, val, arrayCapacity, target),
|
|
516
|
+
);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
writePropInner(mem, key, prop, val, arrayCapacity, target);
|
|
521
|
+
} finally {
|
|
522
|
+
mem.endWrite();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function writePropInner(
|
|
527
|
+
mem: SharedMemory,
|
|
528
|
+
key: string,
|
|
529
|
+
prop: string | symbol,
|
|
530
|
+
val: unknown,
|
|
531
|
+
arrayCapacity?: number,
|
|
532
|
+
target?: WriteTarget,
|
|
533
|
+
): void {
|
|
534
|
+
// If the slot type is already cached on the target, we can skip the
|
|
535
|
+
// getObjectLayout Map lookup for top-level arrays (which are never objects).
|
|
536
|
+
const knownType = target?.__type;
|
|
537
|
+
if (knownType !== TYPE_ARRAY_REF) {
|
|
355
538
|
// Object property: use the cached layout to write only that slot.
|
|
356
539
|
const layout = getObjectLayout(mem, key);
|
|
357
540
|
if (layout !== null) {
|
|
@@ -361,70 +544,136 @@ function writeProp(
|
|
|
361
544
|
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
362
545
|
);
|
|
363
546
|
}
|
|
364
|
-
|
|
365
|
-
|
|
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);
|
|
366
568
|
return;
|
|
367
569
|
}
|
|
570
|
+
}
|
|
368
571
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
572
|
+
// Top-level array index write: update only that array slot.
|
|
573
|
+
const offset = getCachedOffset(mem, key, target);
|
|
574
|
+
if (offset === null) {
|
|
575
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
576
|
+
}
|
|
577
|
+
const slotType =
|
|
578
|
+
target !== undefined && target.__type !== undefined
|
|
579
|
+
? target.__type
|
|
580
|
+
: mem.dataTable.getInt32(offset);
|
|
581
|
+
if (target !== undefined && target.__type === undefined) {
|
|
582
|
+
target.__type = slotType;
|
|
583
|
+
}
|
|
584
|
+
if (
|
|
585
|
+
slotType === TYPE_ARRAY_REF &&
|
|
586
|
+
typeof prop === "string" &&
|
|
587
|
+
/^\d+$/.test(prop)
|
|
588
|
+
) {
|
|
589
|
+
const index = Number(prop);
|
|
590
|
+
// Use the cached ArrayRef when it is still valid (same layout version,
|
|
591
|
+
// i.e. no defragmentation or array growth has moved it). Otherwise
|
|
592
|
+
// re-read the 3 header Int32s and refresh the cache.
|
|
593
|
+
let ref = target?.__arrayRef;
|
|
375
594
|
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
/^\d+$/.test(prop)
|
|
595
|
+
ref === undefined ||
|
|
596
|
+
target!.__arrayRefLayoutVersion !== mem.getLayoutVersion()
|
|
379
597
|
) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
598
|
+
ref = {
|
|
599
|
+
offset: mem.dataTable.getInt32(offset + 4),
|
|
600
|
+
count: mem.dataTable.getInt32(offset + 8),
|
|
601
|
+
capacity: mem.dataTable.getInt32(offset + 12),
|
|
602
|
+
};
|
|
603
|
+
if (target !== undefined) {
|
|
604
|
+
target.__arrayRef = ref;
|
|
605
|
+
target.__arrayRefLayoutVersion = mem.getLayoutVersion();
|
|
386
606
|
}
|
|
387
|
-
|
|
388
|
-
|
|
607
|
+
}
|
|
608
|
+
if (index >= ref.count) {
|
|
609
|
+
throw new RangeError(
|
|
610
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (typeof val === "number") {
|
|
614
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op).
|
|
615
|
+
mem.updateArrayNumberSlot(ref, index, val);
|
|
616
|
+
bumpTarget(mem, key, target);
|
|
389
617
|
return;
|
|
390
618
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
mem
|
|
401
|
-
offset,
|
|
402
|
-
arrayRefSlot({
|
|
403
|
-
offset: ref.offset,
|
|
404
|
-
count: newLen,
|
|
405
|
-
capacity: ref.capacity,
|
|
406
|
-
}),
|
|
407
|
-
);
|
|
408
|
-
bump(mem, key);
|
|
619
|
+
if (typeof val === "boolean") {
|
|
620
|
+
// Fast path: booleans need no old-slot read (freeSlotRefs is a no-op).
|
|
621
|
+
mem.updateArrayBooleanSlot(ref, index, val);
|
|
622
|
+
bumpTarget(mem, key, target);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (typeof val === "string") {
|
|
626
|
+
// Fast path: short strings are written inline into the array slot.
|
|
627
|
+
if (mem.updateArrayInlineStringSlot(ref, index, val)) {
|
|
628
|
+
bumpTarget(mem, key, target);
|
|
409
629
|
return;
|
|
410
630
|
}
|
|
411
631
|
}
|
|
632
|
+
freeSlotRefs(mem, mem.readArraySlot(ref, index));
|
|
633
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
634
|
+
bumpTarget(mem, key, target);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
412
637
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
638
|
+
// Top-level array length write: shrink/grow the array in place.
|
|
639
|
+
if (slotType === TYPE_ARRAY_REF && prop === "length") {
|
|
640
|
+
const newLen = Number(val);
|
|
641
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
642
|
+
throw new RangeError("Invalid array length");
|
|
643
|
+
}
|
|
644
|
+
const ref = {
|
|
645
|
+
offset: mem.dataTable.getInt32(offset + 4),
|
|
646
|
+
count: mem.dataTable.getInt32(offset + 8),
|
|
647
|
+
capacity: mem.dataTable.getInt32(offset + 12),
|
|
648
|
+
};
|
|
649
|
+
if (newLen <= ref.capacity) {
|
|
650
|
+
mem.updateValueSlot(
|
|
651
|
+
offset,
|
|
652
|
+
arrayRefSlot({
|
|
653
|
+
offset: ref.offset,
|
|
654
|
+
count: newLen,
|
|
655
|
+
capacity: ref.capacity,
|
|
656
|
+
}),
|
|
657
|
+
);
|
|
658
|
+
bumpTarget(mem, key, target);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Fallback: full read-modify-write.
|
|
664
|
+
mutateValue<unknown, boolean>(
|
|
665
|
+
mem,
|
|
666
|
+
key,
|
|
667
|
+
(value) => {
|
|
668
|
+
if (Array.isArray(value)) {
|
|
669
|
+
(value as unknown as Record<string | symbol, unknown>)[prop] = val;
|
|
670
|
+
} else {
|
|
671
|
+
(value as Record<string | symbol, unknown>)[prop] = val;
|
|
672
|
+
}
|
|
673
|
+
return true;
|
|
674
|
+
},
|
|
675
|
+
arrayCapacity,
|
|
676
|
+
);
|
|
428
677
|
}
|
|
429
678
|
|
|
430
679
|
/** Create a SharedRef proxy backed by a SharedMemory instance. */
|
|
@@ -444,6 +693,26 @@ function createSharedRef<T>(
|
|
|
444
693
|
// the version check in the get handler.
|
|
445
694
|
__cache: undefined as
|
|
446
695
|
{ version: number; value: unknown; index: number } | undefined,
|
|
696
|
+
// Cached DataTable offset for this key, valid only while the global
|
|
697
|
+
// layout version is unchanged (i.e. no defragmentation has moved it).
|
|
698
|
+
// Mirrors the module-level offsetCache to avoid a Map lookup on the
|
|
699
|
+
// write hot path.
|
|
700
|
+
__offset: undefined as number | undefined,
|
|
701
|
+
__layoutVersion: -1,
|
|
702
|
+
// Cached slot type for this key, so the write hot path can skip the
|
|
703
|
+
// DataTable type read when the value's type is known to be stable.
|
|
704
|
+
__type: undefined as number | undefined,
|
|
705
|
+
// Cached ArrayRef for a top-level array value, so the write hot path can
|
|
706
|
+
// skip re-reading the 3 header Int32s on every index write. Valid only
|
|
707
|
+
// while the global layout version is unchanged (no defragmentation) and
|
|
708
|
+
// while the array has not grown past the cached count (checked by the
|
|
709
|
+
// caller via `index < ref.count`).
|
|
710
|
+
__arrayRef: undefined as
|
|
711
|
+
{ offset: number; count: number; capacity: number } | undefined,
|
|
712
|
+
__arrayRefLayoutVersion: -1,
|
|
713
|
+
// Cached version-cell index for this key, so the write hot path can bump
|
|
714
|
+
// the version cell without a Map lookup. Stable for the key's lifetime.
|
|
715
|
+
__index: undefined as number | undefined,
|
|
447
716
|
read: () => {
|
|
448
717
|
const value = copyValue(readValue(mem, key, target)) as T;
|
|
449
718
|
if (tracker && isTrackable(value)) {
|
|
@@ -454,7 +723,7 @@ function createSharedRef<T>(
|
|
|
454
723
|
},
|
|
455
724
|
write: (value: T) => {
|
|
456
725
|
target.__cache = undefined;
|
|
457
|
-
writeValue(mem, key, value, arrayCapacity);
|
|
726
|
+
writeValue(mem, key, value, arrayCapacity, target);
|
|
458
727
|
},
|
|
459
728
|
update: (fn: (current: T) => T) => {
|
|
460
729
|
target.__cache = undefined;
|
|
@@ -521,33 +790,62 @@ function createSharedRef<T>(
|
|
|
521
790
|
},
|
|
522
791
|
});
|
|
523
792
|
const ref = Object.create(proto) as Record<string | symbol, unknown>;
|
|
793
|
+
// Resolve the object's stable version-cell index lazily (the value may not
|
|
794
|
+
// be written to shared memory until after this accessor is built) and cache
|
|
795
|
+
// it so the hot getter can load the version cell directly without a Map
|
|
796
|
+
// lookup or an index→cell computation on every read. The cell index is
|
|
797
|
+
// stable for the key's lifetime (the Index Table is never reordered).
|
|
798
|
+
let cellIndex: number | null = null;
|
|
524
799
|
for (const name of propNames) {
|
|
800
|
+
// Per-property cache slot bound to this getter closure (mirrors
|
|
801
|
+
// cofusion's per-field `desc.cachedVer`/`desc.cachedVal`). On a cache
|
|
802
|
+
// hit the getter returns the already-decoded value directly, avoiding
|
|
803
|
+
// the `readCache` Map hash lookup and the whole-object `v[name]`
|
|
804
|
+
// extraction on the hot path.
|
|
805
|
+
const fieldCache: { version: number; value: unknown } = {
|
|
806
|
+
version: -1,
|
|
807
|
+
value: undefined,
|
|
808
|
+
};
|
|
809
|
+
// Stable array proxy for this property, cached across writes. A write
|
|
810
|
+
// bumps the version and invalidates `fieldCache`, which would otherwise
|
|
811
|
+
// recreate the array proxy on every access. The proxy reads fresh data
|
|
812
|
+
// on each access, so it stays correct without being recreated.
|
|
813
|
+
let arrayProxy: unknown[] | undefined;
|
|
525
814
|
Object.defineProperty(ref, name, {
|
|
526
815
|
enumerable: true,
|
|
527
816
|
configurable: false,
|
|
528
817
|
get() {
|
|
529
818
|
// Lock-free fast path inlined so the JIT can optimize the hot
|
|
530
819
|
// cache-hit read without a function-call chain.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
return pv;
|
|
820
|
+
if (cellIndex === null) {
|
|
821
|
+
cellIndex = mem.getVersionCellIndex(getIndex(mem, key));
|
|
822
|
+
}
|
|
823
|
+
const version = Atomics.load(mem.versionView, cellIndex);
|
|
824
|
+
if (fieldCache.version === version) {
|
|
825
|
+
return fieldCache.value;
|
|
826
|
+
}
|
|
827
|
+
// If this property is an array, return the stable cached proxy
|
|
828
|
+
// (which reads fresh data on each access) instead of recreating it
|
|
829
|
+
// on every write.
|
|
830
|
+
if (arrayProxy !== undefined) {
|
|
831
|
+
return arrayProxy;
|
|
545
832
|
}
|
|
546
|
-
|
|
833
|
+
const value = readObjectProp(
|
|
834
|
+
mem,
|
|
835
|
+
key,
|
|
836
|
+
name,
|
|
837
|
+
fieldCache,
|
|
838
|
+
version,
|
|
839
|
+
arrayCapacity,
|
|
840
|
+
);
|
|
841
|
+
if (Array.isArray(value)) {
|
|
842
|
+
arrayProxy = value;
|
|
843
|
+
}
|
|
844
|
+
return value;
|
|
547
845
|
},
|
|
548
846
|
set(v: unknown) {
|
|
549
847
|
target.__cache = undefined;
|
|
550
|
-
writeProp(mem, key, name, v, arrayCapacity);
|
|
848
|
+
writeProp(mem, key, name, v, arrayCapacity, target);
|
|
551
849
|
},
|
|
552
850
|
});
|
|
553
851
|
}
|
|
@@ -581,7 +879,7 @@ function createSharedRef<T>(
|
|
|
581
879
|
},
|
|
582
880
|
set(t, prop, val) {
|
|
583
881
|
t.__cache = undefined;
|
|
584
|
-
writeProp(mem, key, prop, val, arrayCapacity);
|
|
882
|
+
writeProp(mem, key, prop, val, arrayCapacity, t);
|
|
585
883
|
return true;
|
|
586
884
|
},
|
|
587
885
|
deleteProperty() {
|
|
@@ -661,61 +959,152 @@ function writeValue(
|
|
|
661
959
|
key: string,
|
|
662
960
|
value: unknown,
|
|
663
961
|
arrayCapacity?: number,
|
|
962
|
+
target?: WriteTarget,
|
|
664
963
|
): void {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
964
|
+
// Inline writeLock to avoid a closure allocation on the hot path.
|
|
965
|
+
mem.beginWrite();
|
|
966
|
+
if (mem.isDefragmenting()) {
|
|
967
|
+
mem.endWrite();
|
|
968
|
+
mem.withLock(() => writeValueInner(mem, key, value, arrayCapacity, target));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
writeValueInner(mem, key, value, arrayCapacity, target);
|
|
973
|
+
} finally {
|
|
974
|
+
mem.endWrite();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function writeValueInner(
|
|
979
|
+
mem: SharedMemory,
|
|
980
|
+
key: string,
|
|
981
|
+
value: unknown,
|
|
982
|
+
arrayCapacity?: number,
|
|
983
|
+
target?: WriteTarget,
|
|
984
|
+
): void {
|
|
985
|
+
const existing = getCachedOffset(mem, key, target);
|
|
986
|
+
if (existing !== null) {
|
|
987
|
+
// Skip the DataTable type read when the slot type is known to be stable
|
|
988
|
+
// (cached on the target). Only re-read when the type is unknown.
|
|
989
|
+
let type = target?.__type;
|
|
990
|
+
if (type === undefined) {
|
|
991
|
+
type = mem.dataTable.getInt32(existing);
|
|
992
|
+
if (target !== undefined) target.__type = type;
|
|
993
|
+
}
|
|
994
|
+
if (type === 5) {
|
|
995
|
+
// Update existing object's property slots in place.
|
|
996
|
+
const properties = mem.readObject(existing);
|
|
997
|
+
const valueObj = value as Record<string, unknown>;
|
|
998
|
+
for (let i = 0; i < properties.length; i++) {
|
|
999
|
+
const [name] = properties[i]!;
|
|
1000
|
+
if (!(name in valueObj)) {
|
|
1001
|
+
throw new TypeError(
|
|
1002
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
684
1003
|
);
|
|
685
1004
|
}
|
|
686
|
-
//
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1005
|
+
// Free the old referenced blocks (array/string) before overwriting.
|
|
1006
|
+
const oldPropSlot = properties[i]![1];
|
|
1007
|
+
freeSlotRefs(mem, oldPropSlot);
|
|
1008
|
+
mem.updateObjectSlot(
|
|
1009
|
+
existing,
|
|
1010
|
+
i,
|
|
1011
|
+
encodeValue(mem, valueObj[name], arrayCapacity, oldPropSlot),
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
// Check for extra properties.
|
|
1015
|
+
const existingNames = new Set(properties.map(([n]) => n));
|
|
1016
|
+
for (const name of Object.keys(valueObj)) {
|
|
1017
|
+
if (!existingNames.has(name)) {
|
|
1018
|
+
throw new TypeError(
|
|
1019
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
1020
|
+
);
|
|
694
1021
|
}
|
|
695
|
-
bump(mem, key);
|
|
696
|
-
return;
|
|
697
1022
|
}
|
|
698
|
-
|
|
699
|
-
mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
|
|
700
|
-
bump(mem, key);
|
|
1023
|
+
bumpTarget(mem, key, target);
|
|
701
1024
|
return;
|
|
702
1025
|
}
|
|
703
|
-
|
|
704
|
-
if (
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1026
|
+
// Update a single value slot in place.
|
|
1027
|
+
if (typeof value === "number") {
|
|
1028
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
|
|
1029
|
+
// and no in-place string reuse; write the float64 directly.
|
|
1030
|
+
mem.updateNumberSlot(existing, value);
|
|
1031
|
+
bumpTarget(mem, key, target);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const isPrimitive = typeof value === "boolean" || typeof value === "bigint";
|
|
1035
|
+
if (isPrimitive) {
|
|
1036
|
+
// Fast path: primitives need no old-slot read (freeSlotRefs is a
|
|
1037
|
+
// no-op) and no in-place string reuse.
|
|
1038
|
+
if (typeof value === "boolean") {
|
|
1039
|
+
mem.updateBooleanSlot(existing, value);
|
|
1040
|
+
} else {
|
|
1041
|
+
mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
|
|
1042
|
+
}
|
|
1043
|
+
bumpTarget(mem, key, target);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (typeof value === "string") {
|
|
1047
|
+
// Fast path: short strings are written inline into the slot itself,
|
|
1048
|
+
// avoiding a heap allocation and the string-table write. Longer strings
|
|
1049
|
+
// read the old string ref directly (no ValueSlot object allocation) so
|
|
1050
|
+
// storeStringValue can reuse the block in place.
|
|
1051
|
+
if (mem.updateInlineStringSlot(existing, value)) {
|
|
1052
|
+
bumpTarget(mem, key, target);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const oldType = mem.dataTable.getInt32(existing);
|
|
1056
|
+
const oldA = mem.dataTable.getInt32(existing + 4);
|
|
1057
|
+
const oldB = mem.dataTable.getInt32(existing + 8);
|
|
1058
|
+
const ref = mem.storeStringValue(
|
|
1059
|
+
value,
|
|
1060
|
+
oldType === TYPE_STRING_VALUE_REF
|
|
1061
|
+
? { offset: oldA, length: oldB }
|
|
1062
|
+
: undefined,
|
|
709
1063
|
);
|
|
710
|
-
|
|
711
|
-
mem
|
|
1064
|
+
mem.updateValueSlot(existing, stringValueRefSlot(ref));
|
|
1065
|
+
bumpTarget(mem, key, target);
|
|
712
1066
|
return;
|
|
713
1067
|
}
|
|
1068
|
+
const oldSlot = mem.readValueSlot(existing);
|
|
1069
|
+
freeSlotRefs(mem, oldSlot);
|
|
1070
|
+
mem.updateValueSlot(
|
|
1071
|
+
existing,
|
|
1072
|
+
encodeValue(mem, value, arrayCapacity, oldSlot),
|
|
1073
|
+
);
|
|
1074
|
+
bumpTarget(mem, key, target);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
714
1077
|
|
|
715
|
-
|
|
716
|
-
|
|
1078
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1079
|
+
// Create a new object.
|
|
1080
|
+
const valueObj = value as Record<string, unknown>;
|
|
1081
|
+
const props: Array<[string, ValueSlot]> = Object.entries(valueObj).map(
|
|
1082
|
+
([name, v]) => [name, encodeValue(mem, v, arrayCapacity)],
|
|
1083
|
+
);
|
|
1084
|
+
const offset = mem.createObject(props);
|
|
717
1085
|
mem.insert(key, offset);
|
|
718
|
-
|
|
1086
|
+
const lv = mem.getLayoutVersion();
|
|
1087
|
+
offsetCache.set(key, { offset, layoutVersion: lv });
|
|
1088
|
+
if (target !== undefined) {
|
|
1089
|
+
target.__offset = offset;
|
|
1090
|
+
target.__layoutVersion = lv;
|
|
1091
|
+
target.__type = undefined;
|
|
1092
|
+
target.__arrayRef = undefined;
|
|
1093
|
+
}
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Create a single value slot (primitive or array).
|
|
1098
|
+
const offset = mem.storeValueSlot(encodeValue(mem, value, arrayCapacity));
|
|
1099
|
+
mem.insert(key, offset);
|
|
1100
|
+
const lv = mem.getLayoutVersion();
|
|
1101
|
+
offsetCache.set(key, { offset, layoutVersion: lv });
|
|
1102
|
+
if (target !== undefined) {
|
|
1103
|
+
target.__offset = offset;
|
|
1104
|
+
target.__layoutVersion = lv;
|
|
1105
|
+
target.__type = undefined;
|
|
1106
|
+
target.__arrayRef = undefined;
|
|
1107
|
+
}
|
|
719
1108
|
}
|
|
720
1109
|
|
|
721
1110
|
/** Bump the version cell for a key, invalidating read caches. */
|
|
@@ -723,6 +1112,25 @@ function bump(mem: SharedMemory, key: string): void {
|
|
|
723
1112
|
mem.bumpVersion(getIndex(mem, key));
|
|
724
1113
|
}
|
|
725
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* Bump the version cell for a key using the target's cached index, avoiding
|
|
1117
|
+
* the `getIndex` Map lookup on the write hot path. Falls back to `bump` when
|
|
1118
|
+
* the index is not yet cached.
|
|
1119
|
+
*/
|
|
1120
|
+
function bumpTarget(
|
|
1121
|
+
mem: SharedMemory,
|
|
1122
|
+
key: string,
|
|
1123
|
+
target?: WriteTarget,
|
|
1124
|
+
): void {
|
|
1125
|
+
if (target !== undefined && target.__index !== undefined) {
|
|
1126
|
+
mem.bumpVersion(target.__index);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const index = getIndex(mem, key);
|
|
1130
|
+
if (target !== undefined) target.__index = index;
|
|
1131
|
+
mem.bumpVersion(index);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
726
1134
|
/** Atomically update the value for a key. */
|
|
727
1135
|
function updateValue<T>(
|
|
728
1136
|
mem: SharedMemory,
|
|
@@ -730,7 +1138,7 @@ function updateValue<T>(
|
|
|
730
1138
|
fn: (current: T) => T,
|
|
731
1139
|
arrayCapacity?: number,
|
|
732
1140
|
): T {
|
|
733
|
-
return mem
|
|
1141
|
+
return writeLock(mem, () => {
|
|
734
1142
|
const current = readValue(mem, key) as T;
|
|
735
1143
|
const next = fn(current);
|
|
736
1144
|
writeValue(mem, key, next, arrayCapacity);
|
|
@@ -749,7 +1157,7 @@ function mutateValue<T, R>(
|
|
|
749
1157
|
fn: (current: T) => R,
|
|
750
1158
|
arrayCapacity?: number,
|
|
751
1159
|
): R {
|
|
752
|
-
return mem
|
|
1160
|
+
return writeLock(mem, () => {
|
|
753
1161
|
const current = readValue(mem, key) as T;
|
|
754
1162
|
const result = fn(current);
|
|
755
1163
|
writeValue(mem, key, current, arrayCapacity);
|
|
@@ -794,7 +1202,7 @@ function mutateArrayRef(
|
|
|
794
1202
|
method: "push" | "pop",
|
|
795
1203
|
args: unknown[],
|
|
796
1204
|
): unknown {
|
|
797
|
-
return mem
|
|
1205
|
+
return writeLock(mem, () => {
|
|
798
1206
|
const slotOffset = findArrayRefSlot(mem, key, prop);
|
|
799
1207
|
if (slotOffset === null) {
|
|
800
1208
|
throw new TypeError("Value is not an array");
|
|
@@ -822,6 +1230,8 @@ function mutateArrayRef(
|
|
|
822
1230
|
// Read the last element before decrementing.
|
|
823
1231
|
const lastSlot = mem.readArraySlot(ref, ref.count - 1);
|
|
824
1232
|
const result = decodeValue(mem, lastSlot);
|
|
1233
|
+
// Free the removed element's referenced blocks (array/string).
|
|
1234
|
+
freeSlotRefs(mem, lastSlot);
|
|
825
1235
|
const next = {
|
|
826
1236
|
offset: ref.offset,
|
|
827
1237
|
count: ref.count - 1,
|
|
@@ -883,6 +1293,14 @@ function createArrayProxy(
|
|
|
883
1293
|
prop: string,
|
|
884
1294
|
arrayCapacity?: number,
|
|
885
1295
|
): unknown[] {
|
|
1296
|
+
// Cache the object's property slot offset and the array ref so index writes
|
|
1297
|
+
// skip the per-write getObjectLayout Map lookup, properties.get, and the
|
|
1298
|
+
// DataTable header reads. Invalidated when the layout version changes
|
|
1299
|
+
// (defragmentation or array growth via moveArray).
|
|
1300
|
+
let cachedSlotOffset: number | undefined;
|
|
1301
|
+
let cachedRef:
|
|
1302
|
+
{ offset: number; count: number; capacity: number } | undefined;
|
|
1303
|
+
let cachedLayoutVersion = -1;
|
|
886
1304
|
return new Proxy([] as unknown[], {
|
|
887
1305
|
get(_target, arrProp) {
|
|
888
1306
|
// push/pop on an array property: operate directly on the array ref
|
|
@@ -940,59 +1358,116 @@ function createArrayProxy(
|
|
|
940
1358
|
set(_target, arrProp, val) {
|
|
941
1359
|
// In-place update: write only the affected array slot instead of
|
|
942
1360
|
// decoding and re-encoding the whole object.
|
|
943
|
-
return mem
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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)) {
|
|
966
1437
|
bump(mem, key);
|
|
967
1438
|
return true;
|
|
968
1439
|
}
|
|
969
|
-
// Array property length write: shrink/grow in place without
|
|
970
|
-
// re-encoding the whole object (which would leave a dead slot).
|
|
971
|
-
if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
|
|
972
|
-
const newLen = Number(val);
|
|
973
|
-
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
974
|
-
throw new RangeError("Invalid array length");
|
|
975
|
-
}
|
|
976
|
-
const ref = {
|
|
977
|
-
offset: propSlot.a,
|
|
978
|
-
count: propSlot.b,
|
|
979
|
-
capacity: propSlot.c,
|
|
980
|
-
};
|
|
981
|
-
if (newLen <= ref.capacity) {
|
|
982
|
-
mem.updateValueSlot(
|
|
983
|
-
slotOffset,
|
|
984
|
-
arrayRefSlot({
|
|
985
|
-
offset: ref.offset,
|
|
986
|
-
count: newLen,
|
|
987
|
-
capacity: ref.capacity,
|
|
988
|
-
}),
|
|
989
|
-
);
|
|
990
|
-
bump(mem, key);
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
// Growing beyond capacity falls through to the full read-modify-write.
|
|
994
|
-
}
|
|
995
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.
|
|
996
1471
|
}
|
|
997
1472
|
// Fallback: full read-modify-write.
|
|
998
1473
|
return mutateValue<Record<string, unknown>, boolean>(
|