bun-shared-memory 0.2.0 → 0.2.2
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 +75 -0
- package/README.md +28 -12
- package/package.json +2 -2
- package/src/growable-buffer.ts +35 -0
- package/src/memory.ts +151 -9
- package/src/shared.ts +849 -251
package/src/shared.ts
CHANGED
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
TYPE_ARRAY_REF,
|
|
4
4
|
TYPE_BIGINT_STR,
|
|
5
5
|
TYPE_INLINE_STRING,
|
|
6
|
+
TYPE_KEY_REF,
|
|
7
|
+
TYPE_NULL,
|
|
8
|
+
TYPE_OBJECT_REF,
|
|
6
9
|
TYPE_STRING_REF,
|
|
7
10
|
TYPE_STRING_VALUE_REF,
|
|
8
11
|
arrayRefSlot,
|
|
@@ -10,7 +13,10 @@ import {
|
|
|
10
13
|
bigintStrSlot,
|
|
11
14
|
booleanSlot,
|
|
12
15
|
decodeSlot,
|
|
16
|
+
keyRefSlot,
|
|
17
|
+
nullSlot,
|
|
13
18
|
numberSlot,
|
|
19
|
+
objectRefSlot,
|
|
14
20
|
stringRefSlot,
|
|
15
21
|
stringValueRefSlot,
|
|
16
22
|
type ArrayRef,
|
|
@@ -36,6 +42,25 @@ const MUTATING_METHODS = new Set([
|
|
|
36
42
|
|
|
37
43
|
type Primitive = number | string | boolean | bigint;
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A single step in the path to a nested array. The first step may be a
|
|
47
|
+
* property name (an array property of a shared object); all subsequent steps
|
|
48
|
+
* are array indices.
|
|
49
|
+
*/
|
|
50
|
+
type ArrayStep =
|
|
51
|
+
{ kind: "prop"; name: string } | { kind: "index"; index: number };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Locates an array within shared memory. `path` is empty for a top-level
|
|
55
|
+
* array value, `[{kind:"prop",name}]` for an array property of a shared
|
|
56
|
+
* object, and longer for nested arrays (e.g. `[{kind:"index",index:0}]` for
|
|
57
|
+
* `arr[0]`).
|
|
58
|
+
*/
|
|
59
|
+
interface ArrayLocator {
|
|
60
|
+
key: string;
|
|
61
|
+
path: ArrayStep[];
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
/**
|
|
40
65
|
* Run a write operation. Writes are lock-free by default: they increment the
|
|
41
66
|
* in-flight write counter, and only fall back to the shared mutex if a
|
|
@@ -60,12 +85,22 @@ function writeLock<T>(mem: SharedMemory, fn: () => T): T {
|
|
|
60
85
|
const MIN_INT64 = -(2n ** 63n);
|
|
61
86
|
const MAX_INT64 = 2n ** 63n - 1n;
|
|
62
87
|
|
|
63
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Convert a JS value into a ValueSlot, storing strings/arrays/objects in
|
|
90
|
+
* tables.
|
|
91
|
+
*
|
|
92
|
+
* `visited` tracks plain objects being encoded in the current recursion to
|
|
93
|
+
* break cyclic plain-object graphs (which have no representation in the
|
|
94
|
+
* object-block structure). Nested `Shared` objects are stored as live key
|
|
95
|
+
* references instead of being copied, so self/mutual references never recurse
|
|
96
|
+
* and changes stay in sync.
|
|
97
|
+
*/
|
|
64
98
|
function encodeValue(
|
|
65
99
|
mem: SharedMemory,
|
|
66
100
|
value: unknown,
|
|
67
101
|
arrayCapacity?: number,
|
|
68
102
|
oldSlot?: ValueSlot,
|
|
103
|
+
visited?: Set<object>,
|
|
69
104
|
): ValueSlot {
|
|
70
105
|
switch (typeof value) {
|
|
71
106
|
case "number":
|
|
@@ -83,23 +118,56 @@ function encodeValue(
|
|
|
83
118
|
// Reuse the existing string block in place when the new string fits.
|
|
84
119
|
const oldRef =
|
|
85
120
|
oldSlot?.type === TYPE_STRING_VALUE_REF
|
|
86
|
-
? {
|
|
121
|
+
? {
|
|
122
|
+
offset: oldSlot.a,
|
|
123
|
+
length: oldSlot.b,
|
|
124
|
+
reserved: oldSlot.c || undefined,
|
|
125
|
+
}
|
|
87
126
|
: undefined;
|
|
88
127
|
return stringValueRefSlot(mem.storeStringValue(value, oldRef));
|
|
89
128
|
default:
|
|
129
|
+
if (value === null) {
|
|
130
|
+
// `null` is a supported value (stored as TYPE_NULL). `undefined` is
|
|
131
|
+
// intentionally still rejected below.
|
|
132
|
+
return nullSlot();
|
|
133
|
+
}
|
|
90
134
|
if (Array.isArray(value)) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
const slots = value.map((v) => encodeValue(mem, v, arrayCapacity));
|
|
135
|
+
// Nested arrays are stored as array-refs in the parent array's slots
|
|
136
|
+
// (recursively). Elements may be any encodable value (primitives,
|
|
137
|
+
// nested arrays, nested objects, or live shared references).
|
|
138
|
+
const slots = value.map((v) =>
|
|
139
|
+
encodeValue(mem, v, arrayCapacity, undefined, visited),
|
|
140
|
+
);
|
|
99
141
|
return arrayRefSlot(mem.storeArray(slots, arrayCapacity));
|
|
100
142
|
}
|
|
143
|
+
if (value !== null && typeof value === "object") {
|
|
144
|
+
// A nested SharedRef: store a live reference to its key so the two
|
|
145
|
+
// stay in sync and self/mutual references don't recurse.
|
|
146
|
+
const sharedKey = sharedKeyMap.get(value as object);
|
|
147
|
+
if (sharedKey !== undefined) {
|
|
148
|
+
const index = getIndex(mem, sharedKey);
|
|
149
|
+
return keyRefSlot(index);
|
|
150
|
+
}
|
|
151
|
+
// A plain nested object: store it as a nested object block.
|
|
152
|
+
if (visited === undefined) visited = new Set<object>();
|
|
153
|
+
if (visited.has(value as object)) {
|
|
154
|
+
throw new TypeError(
|
|
155
|
+
`Cannot store cyclic plain object: it is already being written`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
visited.add(value as object);
|
|
159
|
+
const valueObj = value as Record<string, unknown>;
|
|
160
|
+
const props: Array<[string, ValueSlot]> = Object.entries(valueObj).map(
|
|
161
|
+
([name, v]) => [
|
|
162
|
+
name,
|
|
163
|
+
encodeValue(mem, v, arrayCapacity, undefined, visited),
|
|
164
|
+
],
|
|
165
|
+
);
|
|
166
|
+
const offset = mem.createObject(props);
|
|
167
|
+
return objectRefSlot(offset);
|
|
168
|
+
}
|
|
101
169
|
throw new TypeError(
|
|
102
|
-
`Cannot store value of type ${typeof value}: only primitives, bigints,
|
|
170
|
+
`Cannot store value of type ${typeof value}: only primitives, bigints, arrays, and objects are allowed`,
|
|
103
171
|
);
|
|
104
172
|
}
|
|
105
173
|
}
|
|
@@ -115,10 +183,26 @@ function decodeValue(mem: SharedMemory, slot: ValueSlot): unknown {
|
|
|
115
183
|
return mem.readInlineString(slot.a, slot.b, slot.c);
|
|
116
184
|
case TYPE_BIGINT_STR: // bigint stored as decimal string
|
|
117
185
|
return BigInt(mem.readString({ offset: slot.a, length: slot.b }));
|
|
118
|
-
case
|
|
186
|
+
case TYPE_ARRAY_REF: // array-ref
|
|
119
187
|
return mem
|
|
120
188
|
.readArray({ offset: slot.a, count: slot.b, capacity: slot.c })
|
|
121
189
|
.map((s) => decodeValue(mem, s));
|
|
190
|
+
case TYPE_OBJECT_REF: {
|
|
191
|
+
// Nested object block: decode its properties recursively.
|
|
192
|
+
const properties = mem.readObject(slot.a);
|
|
193
|
+
const obj: Record<string, unknown> = {};
|
|
194
|
+
for (const [name, s] of properties) {
|
|
195
|
+
obj[name] = decodeValue(mem, s);
|
|
196
|
+
}
|
|
197
|
+
return obj;
|
|
198
|
+
}
|
|
199
|
+
case TYPE_KEY_REF: {
|
|
200
|
+
// Live reference to another shared key: return a live wrapper so the
|
|
201
|
+
// two stay in sync. Resolving to a reference (not its contents) is what
|
|
202
|
+
// prevents infinite recursion on self/mutual references.
|
|
203
|
+
const key = mem.getKeyByIndex(slot.a);
|
|
204
|
+
return key === null ? undefined : getNestedSharedRef(mem, key);
|
|
205
|
+
}
|
|
122
206
|
default:
|
|
123
207
|
return decodeSlot(slot);
|
|
124
208
|
}
|
|
@@ -171,7 +255,7 @@ export type SharedRef<T> = [T] extends [object]
|
|
|
171
255
|
*/
|
|
172
256
|
const readCache = new Map<
|
|
173
257
|
string,
|
|
174
|
-
{ version: number; value: unknown; index: number }
|
|
258
|
+
{ version: number; value: unknown; index: number; cell: number }
|
|
175
259
|
>();
|
|
176
260
|
|
|
177
261
|
/**
|
|
@@ -250,9 +334,10 @@ function getCachedOffset(
|
|
|
250
334
|
mem: SharedMemory,
|
|
251
335
|
key: string,
|
|
252
336
|
target?: WriteTarget,
|
|
337
|
+
lv?: number,
|
|
253
338
|
): number | null {
|
|
254
339
|
if (target !== undefined && target.__offset !== undefined) {
|
|
255
|
-
if (target.__layoutVersion === mem.getLayoutVersion()) {
|
|
340
|
+
if (target.__layoutVersion === (lv ?? mem.getLayoutVersion())) {
|
|
256
341
|
return target.__offset;
|
|
257
342
|
}
|
|
258
343
|
target.__offset = undefined;
|
|
@@ -261,7 +346,7 @@ function getCachedOffset(
|
|
|
261
346
|
}
|
|
262
347
|
const cached = offsetCache.get(key);
|
|
263
348
|
if (cached !== undefined) {
|
|
264
|
-
if (cached.layoutVersion === mem.getLayoutVersion()) {
|
|
349
|
+
if (cached.layoutVersion === (lv ?? mem.getLayoutVersion())) {
|
|
265
350
|
if (target !== undefined) {
|
|
266
351
|
target.__offset = cached.offset;
|
|
267
352
|
target.__layoutVersion = cached.layoutVersion;
|
|
@@ -272,10 +357,11 @@ function getCachedOffset(
|
|
|
272
357
|
}
|
|
273
358
|
const offset = getOffset(mem, key);
|
|
274
359
|
if (offset !== null) {
|
|
275
|
-
|
|
360
|
+
const currentLv = lv ?? mem.getLayoutVersion();
|
|
361
|
+
offsetCache.set(key, { offset, layoutVersion: currentLv });
|
|
276
362
|
if (target !== undefined) {
|
|
277
363
|
target.__offset = offset;
|
|
278
|
-
target.__layoutVersion =
|
|
364
|
+
target.__layoutVersion = currentLv;
|
|
279
365
|
target.__type = undefined;
|
|
280
366
|
target.__arrayRef = undefined;
|
|
281
367
|
}
|
|
@@ -315,31 +401,253 @@ function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
|
|
|
315
401
|
* the whole object. If a future GC/defragmentation moves an object block, this
|
|
316
402
|
* cache must be invalidated (e.g. via a layout version cell).
|
|
317
403
|
*/
|
|
318
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Per-worker cache of a key's object layout: key -> { layout, layoutVersion }.
|
|
406
|
+
*
|
|
407
|
+
* Because object properties may not be added or removed, the layout is static
|
|
408
|
+
* for the lifetime of the object, but a defragmentation pass can move the
|
|
409
|
+
* object block (changing its property slot offsets). The cache is therefore
|
|
410
|
+
* re-validated against the global layout version on each access (a single
|
|
411
|
+
* Atomics.load), exactly like the offset cache.
|
|
412
|
+
*/
|
|
413
|
+
const layoutCache = new Map<
|
|
414
|
+
string,
|
|
415
|
+
{ layout: ObjectLayout | null; version: number }
|
|
416
|
+
>();
|
|
319
417
|
|
|
320
418
|
/** Get (and cache) the stable layout of an object value, or null if not an object. */
|
|
321
419
|
function getObjectLayout(mem: SharedMemory, key: string): ObjectLayout | null {
|
|
322
|
-
|
|
420
|
+
const cached = layoutCache.get(key);
|
|
421
|
+
if (cached !== undefined && cached.version === mem.getLayoutVersion()) {
|
|
422
|
+
return cached.layout;
|
|
423
|
+
}
|
|
323
424
|
const layout = mem.withLock(() => mem.getObjectLayout(key));
|
|
324
|
-
layoutCache.set(key, layout);
|
|
425
|
+
layoutCache.set(key, { layout, version: mem.getLayoutVersion() });
|
|
325
426
|
return layout;
|
|
326
427
|
}
|
|
327
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The resolved anchor of a nested object: either a plain object block in the
|
|
431
|
+
* DataTable (kind `object`) or a live reference to another shared key (kind
|
|
432
|
+
* `shared`).
|
|
433
|
+
*/
|
|
434
|
+
type NestedObjectAnchor =
|
|
435
|
+
| { kind: "object"; offset: number }
|
|
436
|
+
| { kind: "shared"; key: string; index: number };
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Navigate a locator path to the object value at its end, returning an anchor
|
|
440
|
+
* that can be read/written in place. Returns null if the value at the locator
|
|
441
|
+
* is not an object (or doesn't exist).
|
|
442
|
+
*
|
|
443
|
+
* The path may mix object-property and array-index steps (e.g. a nested object
|
|
444
|
+
* inside an array, or an object property of a nested object). A `TYPE_OBJECT_REF`
|
|
445
|
+
* resolves to an object block; a `TYPE_KEY_REF` resolves to the referenced
|
|
446
|
+
* shared key (so changes stay in sync). Re-navigated on each access so
|
|
447
|
+
* defragmentation-moved offsets are always current.
|
|
448
|
+
*/
|
|
449
|
+
function resolveNestedObject(
|
|
450
|
+
mem: SharedMemory,
|
|
451
|
+
locator: ArrayLocator,
|
|
452
|
+
): NestedObjectAnchor | null {
|
|
453
|
+
const { key, path } = locator;
|
|
454
|
+
if (path.length === 0) {
|
|
455
|
+
const offset = getOffset(mem, key);
|
|
456
|
+
if (offset === null) return null;
|
|
457
|
+
return mem.dataTable.getInt32(offset) === 5 /* TYPE_OBJECT */
|
|
458
|
+
? { kind: "object", offset }
|
|
459
|
+
: null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
type Curr = { kind: "obj"; offset: number } | { kind: "arr"; ref: ArrayRef };
|
|
463
|
+
// Establish the root (top-level object block or array).
|
|
464
|
+
let curr: Curr | null = null;
|
|
465
|
+
const rootOffset = getOffset(mem, key);
|
|
466
|
+
if (rootOffset === null) return null;
|
|
467
|
+
if (mem.dataTable.getInt32(rootOffset) === 5 /* TYPE_OBJECT */) {
|
|
468
|
+
curr = { kind: "obj", offset: rootOffset };
|
|
469
|
+
} else {
|
|
470
|
+
const rootSlot = mem.readValueSlot(rootOffset);
|
|
471
|
+
if (rootSlot.type !== TYPE_ARRAY_REF) return null;
|
|
472
|
+
curr = {
|
|
473
|
+
kind: "arr",
|
|
474
|
+
ref: { offset: rootSlot.a, count: rootSlot.b, capacity: rootSlot.c },
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let lastSlot: ValueSlot | null = null;
|
|
479
|
+
for (let i = 0; i < path.length; i++) {
|
|
480
|
+
const step = path[i]!;
|
|
481
|
+
if (curr === null) return null;
|
|
482
|
+
let s: ValueSlot;
|
|
483
|
+
if (step.kind === "prop") {
|
|
484
|
+
if (curr.kind !== "obj") return null;
|
|
485
|
+
const layout = mem.getObjectLayoutAtOffset(curr.offset);
|
|
486
|
+
if (layout === null) return null;
|
|
487
|
+
const so = layout.properties.get(step.name);
|
|
488
|
+
if (so === undefined) return null;
|
|
489
|
+
s = mem.readValueSlot(so);
|
|
490
|
+
} else {
|
|
491
|
+
if (curr.kind !== "arr") return null;
|
|
492
|
+
if (step.index >= curr.ref.count) return null;
|
|
493
|
+
s = mem.readArraySlot(curr.ref, step.index);
|
|
494
|
+
}
|
|
495
|
+
lastSlot = s;
|
|
496
|
+
// Descend into a container for the next step.
|
|
497
|
+
if (s.type === TYPE_OBJECT_REF) {
|
|
498
|
+
curr = { kind: "obj", offset: s.a };
|
|
499
|
+
} else if (s.type === TYPE_ARRAY_REF) {
|
|
500
|
+
curr = { kind: "arr", ref: { offset: s.a, count: s.b, capacity: s.c } };
|
|
501
|
+
} else if (i < path.length - 1) {
|
|
502
|
+
return null; // hit a primitive before the end of the path
|
|
503
|
+
} else {
|
|
504
|
+
curr = null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (lastSlot === null) return null;
|
|
509
|
+
if (lastSlot.type === TYPE_OBJECT_REF) {
|
|
510
|
+
return { kind: "object", offset: lastSlot.a };
|
|
511
|
+
}
|
|
512
|
+
if (lastSlot.type === TYPE_KEY_REF) {
|
|
513
|
+
const k = mem.getKeyByIndex(lastSlot.a);
|
|
514
|
+
return k === null ? null : { kind: "shared", key: k, index: lastSlot.a };
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Create a live proxy for the nested object at `locator` (a plain nested
|
|
521
|
+
* object block or a reference to another shared key). Property reads/writes go
|
|
522
|
+
* through to shared memory in place, so changes stay in sync. If the nested
|
|
523
|
+
* value is a reference to another shared key, the shared wrapper for that key
|
|
524
|
+
* is returned directly.
|
|
525
|
+
*/
|
|
526
|
+
function createNestedObjectProxy(
|
|
527
|
+
mem: SharedMemory,
|
|
528
|
+
locator: ArrayLocator,
|
|
529
|
+
arrayCapacity?: number,
|
|
530
|
+
): Record<string | symbol, unknown> | null {
|
|
531
|
+
const anchor = resolveNestedObject(mem, locator);
|
|
532
|
+
if (anchor === null) return null;
|
|
533
|
+
if (anchor.kind === "shared") {
|
|
534
|
+
return getNestedSharedRef(mem, anchor.key) as Record<
|
|
535
|
+
string | symbol,
|
|
536
|
+
unknown
|
|
537
|
+
>;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const proxy = new Proxy({} as Record<string | symbol, unknown>, {
|
|
541
|
+
get(_t, prop) {
|
|
542
|
+
if (prop === "read") {
|
|
543
|
+
return () => copyValue(readValueAtLocator(mem, locator));
|
|
544
|
+
}
|
|
545
|
+
if (prop === "key") return locator.key;
|
|
546
|
+
if (prop === "buffer") return mem.dataTable.buffers[0];
|
|
547
|
+
const offset = resolveAnchorOffset(mem, locator);
|
|
548
|
+
if (offset === null) return undefined;
|
|
549
|
+
const layout = mem.withLock(() => mem.getObjectLayoutAtOffset(offset));
|
|
550
|
+
if (layout === null) return undefined;
|
|
551
|
+
const slotOffset = layout.properties.get(prop as string);
|
|
552
|
+
if (slotOffset === undefined) return undefined;
|
|
553
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
554
|
+
if (slot.type === TYPE_OBJECT_REF || slot.type === TYPE_KEY_REF) {
|
|
555
|
+
return createNestedObjectProxy(
|
|
556
|
+
mem,
|
|
557
|
+
{
|
|
558
|
+
key: locator.key,
|
|
559
|
+
path: [...locator.path, { kind: "prop", name: prop as string }],
|
|
560
|
+
},
|
|
561
|
+
arrayCapacity,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
let value = decodeValue(mem, slot);
|
|
565
|
+
if (Array.isArray(value)) {
|
|
566
|
+
return createArrayProxy(
|
|
567
|
+
mem,
|
|
568
|
+
{
|
|
569
|
+
key: locator.key,
|
|
570
|
+
path: [...locator.path, { kind: "prop", name: prop as string }],
|
|
571
|
+
},
|
|
572
|
+
arrayCapacity,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return value;
|
|
576
|
+
},
|
|
577
|
+
set(_t, prop, val) {
|
|
578
|
+
const offset = resolveAnchorOffset(mem, locator);
|
|
579
|
+
if (offset === null) {
|
|
580
|
+
throw new TypeError("Value is not an object");
|
|
581
|
+
}
|
|
582
|
+
const layout = mem.withLock(() => mem.getObjectLayoutAtOffset(offset));
|
|
583
|
+
if (layout === null) {
|
|
584
|
+
throw new TypeError("Value is not an object");
|
|
585
|
+
}
|
|
586
|
+
const slotOffset = layout.properties.get(prop as string);
|
|
587
|
+
if (slotOffset === undefined) {
|
|
588
|
+
throw new TypeError(
|
|
589
|
+
`Nested object has a fixed shape: properties may not be added or removed`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
writeObjectSlot(mem, locator.key, slotOffset, val, arrayCapacity);
|
|
593
|
+
return true;
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
return proxy;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Re-resolve a locator that is known to point at a plain object block and
|
|
601
|
+
* return its current DataTable offset, or null if it no longer points at one
|
|
602
|
+
* (e.g. the value was replaced). Re-navigates from the root so
|
|
603
|
+
* defragmentation-moved offsets are always current.
|
|
604
|
+
*/
|
|
605
|
+
function resolveAnchorOffset(
|
|
606
|
+
mem: SharedMemory,
|
|
607
|
+
locator: ArrayLocator,
|
|
608
|
+
): number | null {
|
|
609
|
+
const anchor = resolveNestedObject(mem, locator);
|
|
610
|
+
return anchor !== null && anchor.kind === "object" ? anchor.offset : null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Decode the (plain) value at a locator by reading the root through the read
|
|
615
|
+
* cache and navigating the path. Used by the nested object proxy's `read()`.
|
|
616
|
+
*/
|
|
617
|
+
function readValueAtLocator(mem: SharedMemory, locator: ArrayLocator): unknown {
|
|
618
|
+
const root = readValue(mem, locator.key);
|
|
619
|
+
return readPathValue(mem, locator, root);
|
|
620
|
+
}
|
|
621
|
+
|
|
328
622
|
/**
|
|
329
623
|
* Deep-ish copy of a decoded value, so callers can't corrupt the cache.
|
|
330
624
|
*
|
|
331
625
|
* Uses `Object.assign` for the fast shallow copy, then a `for...in` pass to
|
|
332
|
-
* copy any nested arrays (so mutating a returned array property
|
|
333
|
-
* the cache). This is ~4.5× faster than the previous
|
|
334
|
-
* loop, which allocated an entries array and
|
|
335
|
-
* every property.
|
|
626
|
+
* recursively copy any nested arrays (so mutating a returned array property
|
|
627
|
+
* cannot corrupt the cache). This is ~4.5× faster than the previous
|
|
628
|
+
* `Object.entries` + spread loop, which allocated an entries array and
|
|
629
|
+
* re-checked `Array.isArray` on every property.
|
|
336
630
|
*/
|
|
337
|
-
function copyValue(value: unknown): unknown {
|
|
338
|
-
if (Array.isArray(value))
|
|
631
|
+
function copyValue(value: unknown, seen?: Map<object, unknown>): unknown {
|
|
632
|
+
if (Array.isArray(value)) {
|
|
633
|
+
// Lazily create the cycle-tracking map only when the first container is
|
|
634
|
+
// reached, so primitives keep the zero-allocation fast path.
|
|
635
|
+
const map = (seen ??= new Map<object, unknown>());
|
|
636
|
+
const cached = map.get(value);
|
|
637
|
+
if (cached !== undefined) return cached;
|
|
638
|
+
const copy: unknown[] = [];
|
|
639
|
+
map.set(value, copy);
|
|
640
|
+
for (const v of value) copy.push(copyValue(v, map));
|
|
641
|
+
return copy;
|
|
642
|
+
}
|
|
339
643
|
if (value !== null && typeof value === "object") {
|
|
644
|
+
const map = (seen ??= new Map<object, unknown>());
|
|
645
|
+
const cached = map.get(value);
|
|
646
|
+
if (cached !== undefined) return cached;
|
|
340
647
|
const copy = Object.assign({} as Record<string, unknown>, value);
|
|
648
|
+
map.set(value, copy);
|
|
341
649
|
for (const k in copy) {
|
|
342
|
-
|
|
650
|
+
copy[k] = copyValue(copy[k], map);
|
|
343
651
|
}
|
|
344
652
|
return copy;
|
|
345
653
|
}
|
|
@@ -376,6 +684,14 @@ type Loader<T> = () => T | Promise<T>;
|
|
|
376
684
|
|
|
377
685
|
const registry = new Map<string, SharedRef<unknown>>();
|
|
378
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Maps each SharedRef wrapper object to its shared key. Used by `encodeValue`
|
|
689
|
+
* to embed a nested `Shared` object as a live reference (TYPE_KEY_REF) so
|
|
690
|
+
* changes stay in sync, instead of copying or recursing into it (which would
|
|
691
|
+
* loop forever on self/mutual references).
|
|
692
|
+
*/
|
|
693
|
+
const sharedKeyMap = new WeakMap<object, string>();
|
|
694
|
+
|
|
379
695
|
/**
|
|
380
696
|
* Get a wrapper to a named value in shared memory.
|
|
381
697
|
*
|
|
@@ -443,14 +759,32 @@ function readProp(
|
|
|
443
759
|
prop: string | symbol,
|
|
444
760
|
arrayCapacity?: number,
|
|
445
761
|
): unknown {
|
|
446
|
-
|
|
447
|
-
|
|
762
|
+
// Inline the read-cache fast path (instead of calling readCachedValue, which
|
|
763
|
+
// returns a polymorphic {hit,value} union and allocates an object on every
|
|
764
|
+
// cache hit). Keeping this monomorphic lets the JIT optimize the hot read.
|
|
765
|
+
const cached = readCache.get(key);
|
|
766
|
+
const value =
|
|
767
|
+
cached !== undefined &&
|
|
768
|
+
cached.version === Atomics.load(mem.versionView, cached.cell)
|
|
769
|
+
? cached.value
|
|
770
|
+
: readValue(mem, key);
|
|
448
771
|
if (Array.isArray(value)) {
|
|
449
772
|
return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
|
|
450
773
|
}
|
|
451
774
|
const propValue = (value as Record<string | symbol, unknown>)[prop];
|
|
452
775
|
if (Array.isArray(propValue)) {
|
|
453
|
-
return createArrayProxy(
|
|
776
|
+
return createArrayProxy(
|
|
777
|
+
mem,
|
|
778
|
+
{ key, path: [{ kind: "prop", name: prop as string }] },
|
|
779
|
+
arrayCapacity,
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (propValue !== null && typeof propValue === "object") {
|
|
783
|
+
return createNestedObjectProxy(
|
|
784
|
+
mem,
|
|
785
|
+
{ key, path: [{ kind: "prop", name: prop as string }] },
|
|
786
|
+
arrayCapacity,
|
|
787
|
+
);
|
|
454
788
|
}
|
|
455
789
|
return propValue;
|
|
456
790
|
}
|
|
@@ -484,15 +818,81 @@ function readObjectProp(
|
|
|
484
818
|
);
|
|
485
819
|
}
|
|
486
820
|
const slot = mem.readValueSlot(slotOffset);
|
|
821
|
+
if (slot.type === TYPE_OBJECT_REF || slot.type === TYPE_KEY_REF) {
|
|
822
|
+
// Nested object (plain block or live shared reference): return a live
|
|
823
|
+
// proxy so in-place mutation stays in sync.
|
|
824
|
+
const value = createNestedObjectProxy(
|
|
825
|
+
mem,
|
|
826
|
+
{ key, path: [{ kind: "prop", name: prop }] },
|
|
827
|
+
arrayCapacity,
|
|
828
|
+
);
|
|
829
|
+
fieldCache.version = version;
|
|
830
|
+
fieldCache.value = value;
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
487
833
|
let value = decodeValue(mem, slot);
|
|
488
834
|
if (Array.isArray(value)) {
|
|
489
|
-
value = createArrayProxy(
|
|
835
|
+
value = createArrayProxy(
|
|
836
|
+
mem,
|
|
837
|
+
{ key, path: [{ kind: "prop", name: prop }] },
|
|
838
|
+
arrayCapacity,
|
|
839
|
+
);
|
|
490
840
|
}
|
|
491
841
|
fieldCache.version = version;
|
|
492
842
|
fieldCache.value = value;
|
|
493
843
|
return value;
|
|
494
844
|
}
|
|
495
845
|
|
|
846
|
+
/**
|
|
847
|
+
* Write a single object-property slot whose offset is already resolved
|
|
848
|
+
* (cached by the caller, e.g. the accessor setter). Avoids the
|
|
849
|
+
* `getObjectLayout` Map lookup + `properties.get` per write on the hot path.
|
|
850
|
+
*/
|
|
851
|
+
function writeObjectSlot(
|
|
852
|
+
mem: SharedMemory,
|
|
853
|
+
key: string,
|
|
854
|
+
slotOffset: number,
|
|
855
|
+
val: unknown,
|
|
856
|
+
arrayCapacity?: number,
|
|
857
|
+
target?: WriteTarget,
|
|
858
|
+
): void {
|
|
859
|
+
if (typeof val === "number") {
|
|
860
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
|
|
861
|
+
// and no in-place string reuse; write the float64 directly.
|
|
862
|
+
mem.updateNumberSlot(slotOffset, val);
|
|
863
|
+
bumpTarget(mem, key, target);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (typeof val === "boolean") {
|
|
867
|
+
// Fast path: booleans need no old-slot read (freeSlotRefs is a no-op)
|
|
868
|
+
// and no in-place string reuse; write the 4 Int32s directly.
|
|
869
|
+
mem.updateBooleanSlot(slotOffset, val);
|
|
870
|
+
bumpTarget(mem, key, target);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
|
|
874
|
+
// Fast path: int64 bigints are written directly, avoiding the ValueSlot
|
|
875
|
+
// object allocation of the generic path.
|
|
876
|
+
mem.updateBigIntSlot(slotOffset, val);
|
|
877
|
+
bumpTarget(mem, key, target);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (typeof val === "string") {
|
|
881
|
+
// Fast path: short strings are written inline into the slot itself.
|
|
882
|
+
if (mem.updateInlineStringSlot(slotOffset, val)) {
|
|
883
|
+
bumpTarget(mem, key, target);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const oldSlot = mem.readValueSlot(slotOffset);
|
|
888
|
+
freeSlotRefs(mem, oldSlot);
|
|
889
|
+
mem.updateValueSlot(
|
|
890
|
+
slotOffset,
|
|
891
|
+
encodeValue(mem, val, arrayCapacity, oldSlot),
|
|
892
|
+
);
|
|
893
|
+
bumpTarget(mem, key, target);
|
|
894
|
+
}
|
|
895
|
+
|
|
496
896
|
/**
|
|
497
897
|
* Write a single property of a shared object value in place.
|
|
498
898
|
*
|
|
@@ -544,33 +944,16 @@ function writePropInner(
|
|
|
544
944
|
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
545
945
|
);
|
|
546
946
|
}
|
|
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);
|
|
947
|
+
writeObjectSlot(mem, key, slotOffset, val, arrayCapacity, target);
|
|
568
948
|
return;
|
|
569
949
|
}
|
|
570
950
|
}
|
|
571
951
|
|
|
572
952
|
// Top-level array index write: update only that array slot.
|
|
573
|
-
|
|
953
|
+
// Within a lock-free write the layout version is stable, so we read it
|
|
954
|
+
// once and reuse it for all checks below, saving 2 Atomics.load ops.
|
|
955
|
+
const cachedLv = mem.getLayoutVersion();
|
|
956
|
+
const offset = getCachedOffset(mem, key, target, cachedLv);
|
|
574
957
|
if (offset === null) {
|
|
575
958
|
throw new Error(`No shared value found for key "${key}"`);
|
|
576
959
|
}
|
|
@@ -593,7 +976,7 @@ function writePropInner(
|
|
|
593
976
|
let ref = target?.__arrayRef;
|
|
594
977
|
if (
|
|
595
978
|
ref === undefined ||
|
|
596
|
-
target!.__arrayRefLayoutVersion !==
|
|
979
|
+
target!.__arrayRefLayoutVersion !== cachedLv
|
|
597
980
|
) {
|
|
598
981
|
ref = {
|
|
599
982
|
offset: mem.dataTable.getInt32(offset + 4),
|
|
@@ -602,7 +985,7 @@ function writePropInner(
|
|
|
602
985
|
};
|
|
603
986
|
if (target !== undefined) {
|
|
604
987
|
target.__arrayRef = ref;
|
|
605
|
-
target.__arrayRefLayoutVersion =
|
|
988
|
+
target.__arrayRefLayoutVersion = cachedLv;
|
|
606
989
|
}
|
|
607
990
|
}
|
|
608
991
|
if (index >= ref.count) {
|
|
@@ -622,6 +1005,13 @@ function writePropInner(
|
|
|
622
1005
|
bumpTarget(mem, key, target);
|
|
623
1006
|
return;
|
|
624
1007
|
}
|
|
1008
|
+
if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
|
|
1009
|
+
// Fast path: int64 bigints are written directly, avoiding the
|
|
1010
|
+
// ValueSlot object allocation of the generic path.
|
|
1011
|
+
mem.updateArrayBigIntSlot(ref, index, val);
|
|
1012
|
+
bumpTarget(mem, key, target);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
625
1015
|
if (typeof val === "string") {
|
|
626
1016
|
// Fast path: short strings are written inline into the array slot.
|
|
627
1017
|
if (mem.updateArrayInlineStringSlot(ref, index, val)) {
|
|
@@ -692,7 +1082,8 @@ function createSharedRef<T>(
|
|
|
692
1082
|
// on every local write/update; cross-worker invalidation is detected by
|
|
693
1083
|
// the version check in the get handler.
|
|
694
1084
|
__cache: undefined as
|
|
695
|
-
{ version: number; value: unknown; index: number
|
|
1085
|
+
| { version: number; value: unknown; index: number; cell: number }
|
|
1086
|
+
| undefined,
|
|
696
1087
|
// Cached DataTable offset for this key, valid only while the global
|
|
697
1088
|
// layout version is unchanged (i.e. no defragmentation has moved it).
|
|
698
1089
|
// Mirrors the module-level offsetCache to avoid a Map lookup on the
|
|
@@ -811,6 +1202,12 @@ function createSharedRef<T>(
|
|
|
811
1202
|
// recreate the array proxy on every access. The proxy reads fresh data
|
|
812
1203
|
// on each access, so it stays correct without being recreated.
|
|
813
1204
|
let arrayProxy: unknown[] | undefined;
|
|
1205
|
+
// Cached object slot offset for this property so writes skip the
|
|
1206
|
+
// getObjectLayout Map lookup + properties.get on the hot path.
|
|
1207
|
+
// Invalidated when the layout version changes (defragmentation moves
|
|
1208
|
+
// the object block).
|
|
1209
|
+
let slotOffset: number | undefined;
|
|
1210
|
+
let slotLayoutVersion = -1;
|
|
814
1211
|
Object.defineProperty(ref, name, {
|
|
815
1212
|
enumerable: true,
|
|
816
1213
|
configurable: false,
|
|
@@ -845,18 +1242,57 @@ function createSharedRef<T>(
|
|
|
845
1242
|
},
|
|
846
1243
|
set(v: unknown) {
|
|
847
1244
|
target.__cache = undefined;
|
|
848
|
-
|
|
1245
|
+
mem.beginWrite();
|
|
1246
|
+
if (mem.isDefragmenting()) {
|
|
1247
|
+
// A defrag pass may move the object block: drop the cached slot
|
|
1248
|
+
// offset so it is re-resolved, and run the write under the mutex.
|
|
1249
|
+
mem.endWrite();
|
|
1250
|
+
slotOffset = undefined;
|
|
1251
|
+
slotLayoutVersion = -1;
|
|
1252
|
+
mem.withLock(() =>
|
|
1253
|
+
writePropInner(mem, key, name, v, arrayCapacity, target),
|
|
1254
|
+
);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
// Write directly to the cached slot offset when still valid,
|
|
1259
|
+
// skipping the getObjectLayout Map lookup + properties.get.
|
|
1260
|
+
if (slotLayoutVersion !== mem.getLayoutVersion()) {
|
|
1261
|
+
slotOffset = undefined;
|
|
1262
|
+
slotLayoutVersion = mem.getLayoutVersion();
|
|
1263
|
+
}
|
|
1264
|
+
if (slotOffset === undefined) {
|
|
1265
|
+
const layout = getObjectLayout(mem, key);
|
|
1266
|
+
if (layout === null) {
|
|
1267
|
+
// Not an object (shouldn't happen for an object accessor).
|
|
1268
|
+
throw new TypeError(
|
|
1269
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
const so = layout.properties.get(name);
|
|
1273
|
+
if (so === undefined) {
|
|
1274
|
+
throw new TypeError(
|
|
1275
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
slotOffset = so;
|
|
1279
|
+
}
|
|
1280
|
+
writeObjectSlot(mem, key, slotOffset, v, arrayCapacity, target);
|
|
1281
|
+
} finally {
|
|
1282
|
+
mem.endWrite();
|
|
1283
|
+
}
|
|
849
1284
|
},
|
|
850
1285
|
});
|
|
851
1286
|
}
|
|
852
1287
|
// Fixed shape: reject adding new properties (throws in strict mode).
|
|
853
1288
|
Object.preventExtensions(ref);
|
|
1289
|
+
sharedKeyMap.set(ref, key);
|
|
854
1290
|
return ref as unknown as SharedRef<T>;
|
|
855
1291
|
}
|
|
856
1292
|
|
|
857
1293
|
// Arrays, primitives, or unknown shape: use a Proxy (index access, array
|
|
858
1294
|
// methods, and dynamic properties require interception).
|
|
859
|
-
|
|
1295
|
+
const proxy = new Proxy(target, {
|
|
860
1296
|
get(t, prop) {
|
|
861
1297
|
if (prop === "read") return t.read;
|
|
862
1298
|
if (prop === "write") return t.write;
|
|
@@ -868,10 +1304,10 @@ function createSharedRef<T>(
|
|
|
868
1304
|
// push/pop on a top-level array: operate directly on the array ref
|
|
869
1305
|
// without decoding the whole array (O(1) amortized).
|
|
870
1306
|
if (prop === "push" || prop === "pop") {
|
|
871
|
-
const
|
|
872
|
-
if (
|
|
1307
|
+
const resolved = resolveArrayRef(mem, { key, path: [] });
|
|
1308
|
+
if (resolved !== null) {
|
|
873
1309
|
return (...args: unknown[]) =>
|
|
874
|
-
mutateArrayRef(mem, key,
|
|
1310
|
+
mutateArrayRef(mem, { key, path: [] }, prop, args);
|
|
875
1311
|
}
|
|
876
1312
|
}
|
|
877
1313
|
|
|
@@ -887,23 +1323,25 @@ function createSharedRef<T>(
|
|
|
887
1323
|
"Shared values have a fixed shape: properties may not be added or removed",
|
|
888
1324
|
);
|
|
889
1325
|
},
|
|
890
|
-
})
|
|
1326
|
+
});
|
|
1327
|
+
sharedKeyMap.set(proxy, key);
|
|
1328
|
+
return proxy as unknown as SharedRef<T>;
|
|
891
1329
|
}
|
|
892
1330
|
|
|
893
1331
|
/**
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
1332
|
+
* Return a live wrapper for a shared key that already exists (used to resolve
|
|
1333
|
+
* a TYPE_KEY_REF back to a reusable wrapper). Does not write anything — the
|
|
1334
|
+
* key must already exist in shared memory.
|
|
897
1335
|
*/
|
|
898
|
-
function
|
|
1336
|
+
function getNestedSharedRef<T = unknown>(
|
|
899
1337
|
mem: SharedMemory,
|
|
900
1338
|
key: string,
|
|
901
|
-
):
|
|
902
|
-
const
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
return
|
|
1339
|
+
): SharedRef<T> {
|
|
1340
|
+
const existing = registry.get(key);
|
|
1341
|
+
if (existing) return existing as SharedRef<T>;
|
|
1342
|
+
const ref = createSharedRef<T>(mem, key, undefined) as SharedRef<T>;
|
|
1343
|
+
registry.set(key, ref as SharedRef<unknown>);
|
|
1344
|
+
return ref;
|
|
907
1345
|
}
|
|
908
1346
|
|
|
909
1347
|
/**
|
|
@@ -917,14 +1355,22 @@ function readValue(
|
|
|
917
1355
|
mem: SharedMemory,
|
|
918
1356
|
key: string,
|
|
919
1357
|
cacheTarget?: {
|
|
920
|
-
__cache?: { version: number; value: unknown; index: number };
|
|
1358
|
+
__cache?: { version: number; value: unknown; index: number; cell: number };
|
|
921
1359
|
},
|
|
922
1360
|
): unknown {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1361
|
+
// Inline the read-cache fast path (see readProp) to avoid the polymorphic
|
|
1362
|
+
// {hit,value} union returned by readCachedValue.
|
|
926
1363
|
const cached = readCache.get(key);
|
|
1364
|
+
if (
|
|
1365
|
+
cached !== undefined &&
|
|
1366
|
+
cached.version === Atomics.load(mem.versionView, cached.cell)
|
|
1367
|
+
) {
|
|
1368
|
+
return cached.value;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
927
1371
|
const index = cached !== undefined ? cached.index : getIndex(mem, key);
|
|
1372
|
+
const cell =
|
|
1373
|
+
cached !== undefined ? cached.cell : mem.getVersionCellIndex(index);
|
|
928
1374
|
const version = mem.getVersion(index);
|
|
929
1375
|
|
|
930
1376
|
// Cache miss: decode under the lock and cache with the observed version.
|
|
@@ -947,7 +1393,7 @@ function readValue(
|
|
|
947
1393
|
return decodeValue(mem, mem.readValueSlot(offset));
|
|
948
1394
|
});
|
|
949
1395
|
|
|
950
|
-
const entry = { version, value, index };
|
|
1396
|
+
const entry = { version, value, index, cell };
|
|
951
1397
|
readCache.set(key, entry);
|
|
952
1398
|
if (cacheTarget) cacheTarget.__cache = entry;
|
|
953
1399
|
return value;
|
|
@@ -982,7 +1428,11 @@ function writeValueInner(
|
|
|
982
1428
|
arrayCapacity?: number,
|
|
983
1429
|
target?: WriteTarget,
|
|
984
1430
|
): void {
|
|
985
|
-
|
|
1431
|
+
// Within a lock-free write the layout version is stable (defragmentation
|
|
1432
|
+
// can only start when the write counter reaches 0), so we read it once
|
|
1433
|
+
// and reuse it for all checks below.
|
|
1434
|
+
const cachedLv = mem.getLayoutVersion();
|
|
1435
|
+
const existing = getCachedOffset(mem, key, target, cachedLv);
|
|
986
1436
|
if (existing !== null) {
|
|
987
1437
|
// Skip the DataTable type read when the slot type is known to be stable
|
|
988
1438
|
// (cached on the target). Only re-read when the type is unknown.
|
|
@@ -1037,6 +1487,10 @@ function writeValueInner(
|
|
|
1037
1487
|
// no-op) and no in-place string reuse.
|
|
1038
1488
|
if (typeof value === "boolean") {
|
|
1039
1489
|
mem.updateBooleanSlot(existing, value);
|
|
1490
|
+
} else if (value >= MIN_INT64 && value <= MAX_INT64) {
|
|
1491
|
+
// Fast path: int64 bigints are written directly, avoiding the
|
|
1492
|
+
// ValueSlot object allocation of the generic path.
|
|
1493
|
+
mem.updateBigIntSlot(existing, value);
|
|
1040
1494
|
} else {
|
|
1041
1495
|
mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
|
|
1042
1496
|
}
|
|
@@ -1055,10 +1509,11 @@ function writeValueInner(
|
|
|
1055
1509
|
const oldType = mem.dataTable.getInt32(existing);
|
|
1056
1510
|
const oldA = mem.dataTable.getInt32(existing + 4);
|
|
1057
1511
|
const oldB = mem.dataTable.getInt32(existing + 8);
|
|
1512
|
+
const oldC = mem.dataTable.getInt32(existing + 12);
|
|
1058
1513
|
const ref = mem.storeStringValue(
|
|
1059
1514
|
value,
|
|
1060
1515
|
oldType === TYPE_STRING_VALUE_REF
|
|
1061
|
-
? { offset: oldA, length: oldB }
|
|
1516
|
+
? { offset: oldA, length: oldB, reserved: oldC || undefined }
|
|
1062
1517
|
: undefined,
|
|
1063
1518
|
);
|
|
1064
1519
|
mem.updateValueSlot(existing, stringValueRefSlot(ref));
|
|
@@ -1171,55 +1626,110 @@ function mutateValue<T, R>(
|
|
|
1171
1626
|
* `prop` is null for a top-level array value, or the property name for an
|
|
1172
1627
|
* array property of an object. Returns null if the value is not an array.
|
|
1173
1628
|
*/
|
|
1174
|
-
|
|
1629
|
+
/**
|
|
1630
|
+
* Resolve the array-ref slot for a locator, returning the current array-ref
|
|
1631
|
+
* ValueSlot and a `write` function that persists a new array-ref slot back to
|
|
1632
|
+
* its location (DataTable offset for a top-level array or object property,
|
|
1633
|
+
* ArrayTable slot for a nested array). Returns null if the value at the
|
|
1634
|
+
* locator is not an array.
|
|
1635
|
+
*/
|
|
1636
|
+
function resolveArrayRef(
|
|
1175
1637
|
mem: SharedMemory,
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (
|
|
1638
|
+
locator: ArrayLocator,
|
|
1639
|
+
): { slot: ValueSlot; write: (s: ValueSlot) => void } | null {
|
|
1640
|
+
const { key, path } = locator;
|
|
1641
|
+
if (path.length === 0) {
|
|
1180
1642
|
const offset = getOffset(mem, key);
|
|
1181
1643
|
if (offset === null) return null;
|
|
1182
1644
|
const slot = mem.readValueSlot(offset);
|
|
1183
|
-
|
|
1645
|
+
if (slot.type !== TYPE_ARRAY_REF) return null;
|
|
1646
|
+
return { slot, write: (s) => mem.updateValueSlot(offset, s) };
|
|
1184
1647
|
}
|
|
1185
|
-
const
|
|
1186
|
-
if (
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1648
|
+
const first = path[0]!;
|
|
1649
|
+
if (first.kind === "prop") {
|
|
1650
|
+
const layout = getObjectLayout(mem, key);
|
|
1651
|
+
if (layout === null) return null;
|
|
1652
|
+
const slotOffset = layout.properties.get(first.name);
|
|
1653
|
+
if (slotOffset === undefined) return null;
|
|
1654
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
1655
|
+
if (slot.type !== TYPE_ARRAY_REF) return null;
|
|
1656
|
+
if (path.length === 1) {
|
|
1657
|
+
return { slot, write: (s) => mem.updateValueSlot(slotOffset, s) };
|
|
1658
|
+
}
|
|
1659
|
+
return resolveNestedArrayRef(mem, slot, path.slice(1));
|
|
1660
|
+
}
|
|
1661
|
+
// First step is an index: the root must be a top-level array.
|
|
1662
|
+
const rootOffset = getOffset(mem, key);
|
|
1663
|
+
if (rootOffset === null) return null;
|
|
1664
|
+
const rootSlot = mem.readValueSlot(rootOffset);
|
|
1665
|
+
if (rootSlot.type !== TYPE_ARRAY_REF) return null;
|
|
1666
|
+
return resolveNestedArrayRef(mem, rootSlot, path);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Navigate into a nested array starting from a parent array-ref slot. `steps`
|
|
1671
|
+
* are all index steps. Returns the resolved array-ref slot and a writer, or
|
|
1672
|
+
* null if any step is not an array or is out of bounds.
|
|
1673
|
+
*/
|
|
1674
|
+
function resolveNestedArrayRef(
|
|
1675
|
+
mem: SharedMemory,
|
|
1676
|
+
parentSlot: ValueSlot,
|
|
1677
|
+
steps: ArrayStep[],
|
|
1678
|
+
): { slot: ValueSlot; write: (s: ValueSlot) => void } | null {
|
|
1679
|
+
let ref: ArrayRef = {
|
|
1680
|
+
offset: parentSlot.a,
|
|
1681
|
+
count: parentSlot.b,
|
|
1682
|
+
capacity: parentSlot.c,
|
|
1683
|
+
};
|
|
1684
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1685
|
+
const step = steps[i]!;
|
|
1686
|
+
if (step.kind !== "index") return null;
|
|
1687
|
+
if (step.index >= ref.count) return null;
|
|
1688
|
+
const slot = mem.readArraySlot(ref, step.index);
|
|
1689
|
+
if (slot.type !== TYPE_ARRAY_REF) return null;
|
|
1690
|
+
if (i === steps.length - 1) {
|
|
1691
|
+
const slotRef = ref;
|
|
1692
|
+
const index = step.index;
|
|
1693
|
+
return { slot, write: (s) => mem.updateArraySlot(slotRef, index, s) };
|
|
1694
|
+
}
|
|
1695
|
+
ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1191
1698
|
}
|
|
1192
1699
|
|
|
1193
1700
|
/**
|
|
1194
1701
|
* O(1) in-place push/pop on a shared array ref, without decoding or
|
|
1195
|
-
* re-encoding the whole array. `
|
|
1196
|
-
*
|
|
1702
|
+
* re-encoding the whole array. `locator` identifies the array (top-level,
|
|
1703
|
+
* object property, or nested).
|
|
1197
1704
|
*/
|
|
1198
1705
|
function mutateArrayRef(
|
|
1199
1706
|
mem: SharedMemory,
|
|
1200
|
-
|
|
1201
|
-
prop: string | null,
|
|
1707
|
+
locator: ArrayLocator,
|
|
1202
1708
|
method: "push" | "pop",
|
|
1203
1709
|
args: unknown[],
|
|
1204
1710
|
): unknown {
|
|
1205
1711
|
return writeLock(mem, () => {
|
|
1206
|
-
const
|
|
1207
|
-
if (
|
|
1712
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
1713
|
+
if (resolved === null) {
|
|
1208
1714
|
throw new TypeError("Value is not an array");
|
|
1209
1715
|
}
|
|
1210
|
-
const slot =
|
|
1211
|
-
const ref: ArrayRef = {
|
|
1716
|
+
const slot = resolved.slot;
|
|
1717
|
+
const ref: ArrayRef = {
|
|
1718
|
+
offset: slot.a,
|
|
1719
|
+
count: slot.b,
|
|
1720
|
+
capacity: slot.c,
|
|
1721
|
+
};
|
|
1212
1722
|
|
|
1213
1723
|
if (method === "push") {
|
|
1214
1724
|
for (const arg of args) {
|
|
1215
1725
|
const next = mem.appendArraySlot(ref, encodeValue(mem, arg));
|
|
1216
1726
|
// Always persist the new count (and offset/capacity if moved).
|
|
1217
|
-
|
|
1727
|
+
resolved.write(arrayRefSlot(next));
|
|
1218
1728
|
ref.offset = next.offset;
|
|
1219
1729
|
ref.count = next.count;
|
|
1220
1730
|
ref.capacity = next.capacity;
|
|
1221
1731
|
}
|
|
1222
|
-
bump(mem, key);
|
|
1732
|
+
bump(mem, locator.key);
|
|
1223
1733
|
return ref.count;
|
|
1224
1734
|
}
|
|
1225
1735
|
|
|
@@ -1237,12 +1747,48 @@ function mutateArrayRef(
|
|
|
1237
1747
|
count: ref.count - 1,
|
|
1238
1748
|
capacity: ref.capacity,
|
|
1239
1749
|
};
|
|
1240
|
-
|
|
1241
|
-
bump(mem, key);
|
|
1750
|
+
resolved.write(arrayRefSlot(next));
|
|
1751
|
+
bump(mem, locator.key);
|
|
1242
1752
|
return result;
|
|
1243
1753
|
});
|
|
1244
1754
|
}
|
|
1245
1755
|
|
|
1756
|
+
/**
|
|
1757
|
+
* Read the decoded value of the array at a locator, using the read cache for
|
|
1758
|
+
* the root value and navigating the path. This is the fast read path (the
|
|
1759
|
+
* root read hits the version-checked cache), matching the pre-nested-array
|
|
1760
|
+
* performance for array-property reads.
|
|
1761
|
+
*/
|
|
1762
|
+
function readArrayValue(mem: SharedMemory, locator: ArrayLocator): unknown[] {
|
|
1763
|
+
const root = readValue(mem, locator.key);
|
|
1764
|
+
const arr = readPathValue(mem, locator, root);
|
|
1765
|
+
if (!Array.isArray(arr)) {
|
|
1766
|
+
throw new TypeError("Value is not an array");
|
|
1767
|
+
}
|
|
1768
|
+
return arr;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Navigate a decoded root value (the whole top-level value) along a locator
|
|
1773
|
+
* path, returning the value at the end of the path. Used by the fallback
|
|
1774
|
+
* read-modify-write paths.
|
|
1775
|
+
*/
|
|
1776
|
+
function readPathValue(
|
|
1777
|
+
mem: SharedMemory,
|
|
1778
|
+
locator: ArrayLocator,
|
|
1779
|
+
rootValue: unknown,
|
|
1780
|
+
): unknown {
|
|
1781
|
+
let current = rootValue;
|
|
1782
|
+
for (const step of locator.path) {
|
|
1783
|
+
if (step.kind === "prop") {
|
|
1784
|
+
current = (current as Record<string, unknown>)[step.name];
|
|
1785
|
+
} else {
|
|
1786
|
+
current = (current as unknown[])[step.index];
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return current;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1246
1792
|
/** Get a member of a top-level array value, with read-modify-write for mutating methods. */
|
|
1247
1793
|
function getTopLevelArrayMember(
|
|
1248
1794
|
mem: SharedMemory,
|
|
@@ -1253,7 +1799,23 @@ function getTopLevelArrayMember(
|
|
|
1253
1799
|
): unknown {
|
|
1254
1800
|
if (prop === "length") return value.length;
|
|
1255
1801
|
if (typeof prop === "string" && /^\d+$/.test(prop)) {
|
|
1256
|
-
|
|
1802
|
+
const index = Number(prop);
|
|
1803
|
+
const element = value[index];
|
|
1804
|
+
if (Array.isArray(element)) {
|
|
1805
|
+
return createArrayProxy(
|
|
1806
|
+
mem,
|
|
1807
|
+
{ key, path: [{ kind: "index", index }] },
|
|
1808
|
+
arrayCapacity,
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
if (element !== null && typeof element === "object") {
|
|
1812
|
+
return createNestedObjectProxy(
|
|
1813
|
+
mem,
|
|
1814
|
+
{ key, path: [{ kind: "index", index }] },
|
|
1815
|
+
arrayCapacity,
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
return element;
|
|
1257
1819
|
}
|
|
1258
1820
|
if (typeof prop === "symbol") {
|
|
1259
1821
|
if (prop === Symbol.iterator) return value[Symbol.iterator].bind(value);
|
|
@@ -1264,7 +1826,7 @@ function getTopLevelArrayMember(
|
|
|
1264
1826
|
// push/pop are O(1) in-place operations on the array ref.
|
|
1265
1827
|
if (methodName === "push" || methodName === "pop") {
|
|
1266
1828
|
return (...args: unknown[]) => {
|
|
1267
|
-
return mutateArrayRef(mem, key,
|
|
1829
|
+
return mutateArrayRef(mem, { key, path: [] }, methodName, args);
|
|
1268
1830
|
};
|
|
1269
1831
|
}
|
|
1270
1832
|
return (...args: unknown[]) => {
|
|
@@ -1286,42 +1848,189 @@ function getTopLevelArrayMember(
|
|
|
1286
1848
|
return method;
|
|
1287
1849
|
}
|
|
1288
1850
|
|
|
1289
|
-
/**
|
|
1851
|
+
/**
|
|
1852
|
+
* Create a proxy for an array at a locator (a top-level array, an array
|
|
1853
|
+
* property of a shared object, or a nested array). Index access and mutating
|
|
1854
|
+
* methods work directly; nested array elements are returned as their own
|
|
1855
|
+
* proxy so they can be mutated in place.
|
|
1856
|
+
*/
|
|
1290
1857
|
function createArrayProxy(
|
|
1291
1858
|
mem: SharedMemory,
|
|
1292
|
-
|
|
1293
|
-
prop: string,
|
|
1859
|
+
locator: ArrayLocator,
|
|
1294
1860
|
arrayCapacity?: number,
|
|
1295
1861
|
): unknown[] {
|
|
1296
|
-
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
// (defragmentation or array growth via moveArray).
|
|
1300
|
-
let cachedSlotOffset: number | undefined;
|
|
1862
|
+
const { key } = locator;
|
|
1863
|
+
// Cache the resolved array ref and the version-cell index so index writes
|
|
1864
|
+
// skip the per-write path navigation and Map lookups. Invalidated when the
|
|
1865
|
+
// layout version changes (defragmentation or array growth via moveArray).
|
|
1301
1866
|
let cachedRef:
|
|
1302
1867
|
{ offset: number; count: number; capacity: number } | undefined;
|
|
1303
1868
|
let cachedLayoutVersion = -1;
|
|
1869
|
+
// Cached version-cell index for this key, so writes can bump the version
|
|
1870
|
+
// cell without a Map lookup on every write. Stable for the key's lifetime.
|
|
1871
|
+
let cachedIndex: number | undefined;
|
|
1872
|
+
|
|
1873
|
+
/** Bump the key's version cell using the cached index (resolved lazily). */
|
|
1874
|
+
function bumpCached(): void {
|
|
1875
|
+
if (cachedIndex === undefined) {
|
|
1876
|
+
cachedIndex = getIndex(mem, key);
|
|
1877
|
+
}
|
|
1878
|
+
mem.bumpVersion(cachedIndex);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Resolve the array ref, refreshing the cache when the layout changed or
|
|
1883
|
+
* when the cached count is too small for a requested index (the array may
|
|
1884
|
+
* have grown in place via `push` without a layout-version bump).
|
|
1885
|
+
*/
|
|
1886
|
+
function getRef(minCount = 0): {
|
|
1887
|
+
offset: number;
|
|
1888
|
+
count: number;
|
|
1889
|
+
capacity: number;
|
|
1890
|
+
} {
|
|
1891
|
+
if (cachedLayoutVersion !== mem.getLayoutVersion()) {
|
|
1892
|
+
cachedRef = undefined;
|
|
1893
|
+
cachedLayoutVersion = mem.getLayoutVersion();
|
|
1894
|
+
}
|
|
1895
|
+
if (cachedRef !== undefined && cachedRef.count >= minCount)
|
|
1896
|
+
return cachedRef;
|
|
1897
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
1898
|
+
if (resolved === null) {
|
|
1899
|
+
throw new TypeError("Value is not an array");
|
|
1900
|
+
}
|
|
1901
|
+
cachedRef = {
|
|
1902
|
+
offset: resolved.slot.a,
|
|
1903
|
+
count: resolved.slot.b,
|
|
1904
|
+
capacity: resolved.slot.c,
|
|
1905
|
+
};
|
|
1906
|
+
return cachedRef;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* Write a single array slot in place. Defined once (not per call) so the
|
|
1911
|
+
* set handler can inline the writeLock without allocating a closure on
|
|
1912
|
+
* every write.
|
|
1913
|
+
*/
|
|
1914
|
+
function writeArrayProp(arrProp: string | symbol, val: unknown): boolean {
|
|
1915
|
+
if (typeof arrProp === "string" && /^\d+$/.test(arrProp)) {
|
|
1916
|
+
const index = Number(arrProp);
|
|
1917
|
+
const ref = getRef(index + 1);
|
|
1918
|
+
if (index >= ref.count) {
|
|
1919
|
+
throw new RangeError(
|
|
1920
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if (typeof val === "number") {
|
|
1924
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op).
|
|
1925
|
+
mem.updateArrayNumberSlot(ref, index, val);
|
|
1926
|
+
bumpCached();
|
|
1927
|
+
return true;
|
|
1928
|
+
}
|
|
1929
|
+
if (typeof val === "boolean") {
|
|
1930
|
+
// Fast path: booleans need no old-slot read (freeSlotRefs is a no-op).
|
|
1931
|
+
mem.updateArrayBooleanSlot(ref, index, val);
|
|
1932
|
+
bumpCached();
|
|
1933
|
+
return true;
|
|
1934
|
+
}
|
|
1935
|
+
if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
|
|
1936
|
+
// Fast path: int64 bigints are written directly, avoiding the
|
|
1937
|
+
// ValueSlot object allocation of the generic path.
|
|
1938
|
+
mem.updateArrayBigIntSlot(ref, index, val);
|
|
1939
|
+
bumpCached();
|
|
1940
|
+
return true;
|
|
1941
|
+
}
|
|
1942
|
+
if (typeof val === "string") {
|
|
1943
|
+
// Fast path: short strings are written inline into the slot.
|
|
1944
|
+
if (mem.updateArrayInlineStringSlot(ref, index, val)) {
|
|
1945
|
+
bumpCached();
|
|
1946
|
+
return true;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
freeSlotRefs(mem, mem.readArraySlot(ref, index));
|
|
1950
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val, arrayCapacity));
|
|
1951
|
+
bumpCached();
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
// Array length write: shrink/grow in place without re-encoding the whole
|
|
1955
|
+
// value (which would leave a dead slot).
|
|
1956
|
+
if (arrProp === "length") {
|
|
1957
|
+
const newLen = Number(val);
|
|
1958
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
1959
|
+
throw new RangeError("Invalid array length");
|
|
1960
|
+
}
|
|
1961
|
+
const ref = getRef();
|
|
1962
|
+
if (newLen <= ref.capacity) {
|
|
1963
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
1964
|
+
if (resolved === null) {
|
|
1965
|
+
throw new TypeError("Value is not an array");
|
|
1966
|
+
}
|
|
1967
|
+
resolved.write(
|
|
1968
|
+
arrayRefSlot({
|
|
1969
|
+
offset: ref.offset,
|
|
1970
|
+
count: newLen,
|
|
1971
|
+
capacity: ref.capacity,
|
|
1972
|
+
}),
|
|
1973
|
+
);
|
|
1974
|
+
cachedRef = {
|
|
1975
|
+
offset: ref.offset,
|
|
1976
|
+
count: newLen,
|
|
1977
|
+
capacity: ref.capacity,
|
|
1978
|
+
};
|
|
1979
|
+
bumpCached();
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
// Growing beyond capacity falls through to the full read-modify-write.
|
|
1983
|
+
}
|
|
1984
|
+
// Fallback: full read-modify-write.
|
|
1985
|
+
return mutateValue<unknown[], boolean>(
|
|
1986
|
+
mem,
|
|
1987
|
+
key,
|
|
1988
|
+
(fresh) => {
|
|
1989
|
+
const arr = readPathValue(mem, locator, fresh) as unknown[];
|
|
1990
|
+
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
1991
|
+
return true;
|
|
1992
|
+
},
|
|
1993
|
+
arrayCapacity,
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1304
1997
|
return new Proxy([] as unknown[], {
|
|
1305
1998
|
get(_target, arrProp) {
|
|
1306
|
-
// push/pop on
|
|
1307
|
-
//
|
|
1999
|
+
// push/pop on the array: operate directly on the array ref without
|
|
2000
|
+
// decoding the whole value (O(1) amortized).
|
|
1308
2001
|
if (arrProp === "push" || arrProp === "pop") {
|
|
1309
|
-
const
|
|
1310
|
-
if (
|
|
2002
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
2003
|
+
if (resolved !== null) {
|
|
1311
2004
|
return (...args: unknown[]) =>
|
|
1312
|
-
mutateArrayRef(mem,
|
|
2005
|
+
mutateArrayRef(mem, locator, arrProp, args);
|
|
1313
2006
|
}
|
|
1314
2007
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2008
|
+
if (arrProp === "length") {
|
|
2009
|
+
// Use the read cache (version-checked), so an in-place `push` that
|
|
2010
|
+
// grew the array is reflected (the version bump invalidates the cache).
|
|
2011
|
+
return readArrayValue(mem, locator).length;
|
|
1319
2012
|
}
|
|
1320
|
-
if (arrProp === "length") return arr.length;
|
|
1321
2013
|
if (typeof arrProp === "string" && /^\d+$/.test(arrProp)) {
|
|
1322
|
-
|
|
2014
|
+
const index = Number(arrProp);
|
|
2015
|
+
const element = readArrayValue(mem, locator)[index];
|
|
2016
|
+
if (Array.isArray(element)) {
|
|
2017
|
+
return createArrayProxy(
|
|
2018
|
+
mem,
|
|
2019
|
+
{ key, path: [...locator.path, { kind: "index", index }] },
|
|
2020
|
+
arrayCapacity,
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
if (element !== null && typeof element === "object") {
|
|
2024
|
+
return createNestedObjectProxy(
|
|
2025
|
+
mem,
|
|
2026
|
+
{ key, path: [...locator.path, { kind: "index", index }] },
|
|
2027
|
+
arrayCapacity,
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
return element;
|
|
1323
2031
|
}
|
|
1324
2032
|
if (typeof arrProp === "symbol") {
|
|
2033
|
+
const arr = readArrayValue(mem, locator);
|
|
1325
2034
|
if (arrProp === Symbol.iterator) return arr[Symbol.iterator].bind(arr);
|
|
1326
2035
|
return (arr as unknown as Record<symbol, unknown>)[arrProp];
|
|
1327
2036
|
}
|
|
@@ -1330,15 +2039,15 @@ function createArrayProxy(
|
|
|
1330
2039
|
// push/pop are O(1) in-place operations on the array ref.
|
|
1331
2040
|
if (methodName === "push" || methodName === "pop") {
|
|
1332
2041
|
return (...args: unknown[]) => {
|
|
1333
|
-
return mutateArrayRef(mem,
|
|
2042
|
+
return mutateArrayRef(mem, locator, methodName, args);
|
|
1334
2043
|
};
|
|
1335
2044
|
}
|
|
1336
2045
|
return (...args: unknown[]) => {
|
|
1337
|
-
return mutateValue<
|
|
2046
|
+
return mutateValue<unknown[], unknown>(
|
|
1338
2047
|
mem,
|
|
1339
2048
|
key,
|
|
1340
2049
|
(fresh) => {
|
|
1341
|
-
const freshArr = fresh
|
|
2050
|
+
const freshArr = readPathValue(mem, locator, fresh) as unknown[];
|
|
1342
2051
|
const method = (
|
|
1343
2052
|
freshArr as unknown as Record<
|
|
1344
2053
|
string,
|
|
@@ -1351,136 +2060,25 @@ function createArrayProxy(
|
|
|
1351
2060
|
);
|
|
1352
2061
|
};
|
|
1353
2062
|
}
|
|
2063
|
+
const arr = readArrayValue(mem, locator);
|
|
1354
2064
|
const method = (arr as unknown as Record<string, unknown>)[arrProp];
|
|
1355
2065
|
if (typeof method === "function") return method.bind(arr);
|
|
1356
2066
|
return method;
|
|
1357
2067
|
},
|
|
1358
2068
|
set(_target, arrProp, val) {
|
|
1359
2069
|
// In-place update: write only the affected array slot instead of
|
|
1360
|
-
// decoding and re-encoding the whole
|
|
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
|
-
});
|
|
2070
|
+
// decoding and re-encoding the whole value. Inline writeLock to avoid
|
|
2071
|
+
// a closure allocation on the hot path.
|
|
2072
|
+
mem.beginWrite();
|
|
2073
|
+
if (mem.isDefragmenting()) {
|
|
2074
|
+
mem.endWrite();
|
|
2075
|
+
return mem.withLock(() => writeArrayProp(arrProp, val));
|
|
2076
|
+
}
|
|
2077
|
+
try {
|
|
2078
|
+
return writeArrayProp(arrProp, val);
|
|
2079
|
+
} finally {
|
|
2080
|
+
mem.endWrite();
|
|
2081
|
+
}
|
|
1484
2082
|
},
|
|
1485
2083
|
});
|
|
1486
2084
|
}
|