bun-shared-memory 0.2.1 → 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 +54 -3
- package/README.md +28 -12
- package/package.json +1 -1
- package/src/growable-buffer.ts +35 -0
- package/src/memory.ts +124 -9
- package/src/shared.ts +643 -156
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
|
*
|
|
@@ -448,7 +764,8 @@ function readProp(
|
|
|
448
764
|
// cache hit). Keeping this monomorphic lets the JIT optimize the hot read.
|
|
449
765
|
const cached = readCache.get(key);
|
|
450
766
|
const value =
|
|
451
|
-
cached !== undefined &&
|
|
767
|
+
cached !== undefined &&
|
|
768
|
+
cached.version === Atomics.load(mem.versionView, cached.cell)
|
|
452
769
|
? cached.value
|
|
453
770
|
: readValue(mem, key);
|
|
454
771
|
if (Array.isArray(value)) {
|
|
@@ -456,7 +773,18 @@ function readProp(
|
|
|
456
773
|
}
|
|
457
774
|
const propValue = (value as Record<string | symbol, unknown>)[prop];
|
|
458
775
|
if (Array.isArray(propValue)) {
|
|
459
|
-
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
|
+
);
|
|
460
788
|
}
|
|
461
789
|
return propValue;
|
|
462
790
|
}
|
|
@@ -490,9 +818,25 @@ function readObjectProp(
|
|
|
490
818
|
);
|
|
491
819
|
}
|
|
492
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
|
+
}
|
|
493
833
|
let value = decodeValue(mem, slot);
|
|
494
834
|
if (Array.isArray(value)) {
|
|
495
|
-
value = createArrayProxy(
|
|
835
|
+
value = createArrayProxy(
|
|
836
|
+
mem,
|
|
837
|
+
{ key, path: [{ kind: "prop", name: prop }] },
|
|
838
|
+
arrayCapacity,
|
|
839
|
+
);
|
|
496
840
|
}
|
|
497
841
|
fieldCache.version = version;
|
|
498
842
|
fieldCache.value = value;
|
|
@@ -606,7 +950,10 @@ function writePropInner(
|
|
|
606
950
|
}
|
|
607
951
|
|
|
608
952
|
// Top-level array index write: update only that array slot.
|
|
609
|
-
|
|
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);
|
|
610
957
|
if (offset === null) {
|
|
611
958
|
throw new Error(`No shared value found for key "${key}"`);
|
|
612
959
|
}
|
|
@@ -629,7 +976,7 @@ function writePropInner(
|
|
|
629
976
|
let ref = target?.__arrayRef;
|
|
630
977
|
if (
|
|
631
978
|
ref === undefined ||
|
|
632
|
-
target!.__arrayRefLayoutVersion !==
|
|
979
|
+
target!.__arrayRefLayoutVersion !== cachedLv
|
|
633
980
|
) {
|
|
634
981
|
ref = {
|
|
635
982
|
offset: mem.dataTable.getInt32(offset + 4),
|
|
@@ -638,7 +985,7 @@ function writePropInner(
|
|
|
638
985
|
};
|
|
639
986
|
if (target !== undefined) {
|
|
640
987
|
target.__arrayRef = ref;
|
|
641
|
-
target.__arrayRefLayoutVersion =
|
|
988
|
+
target.__arrayRefLayoutVersion = cachedLv;
|
|
642
989
|
}
|
|
643
990
|
}
|
|
644
991
|
if (index >= ref.count) {
|
|
@@ -735,7 +1082,8 @@ function createSharedRef<T>(
|
|
|
735
1082
|
// on every local write/update; cross-worker invalidation is detected by
|
|
736
1083
|
// the version check in the get handler.
|
|
737
1084
|
__cache: undefined as
|
|
738
|
-
{ version: number; value: unknown; index: number
|
|
1085
|
+
| { version: number; value: unknown; index: number; cell: number }
|
|
1086
|
+
| undefined,
|
|
739
1087
|
// Cached DataTable offset for this key, valid only while the global
|
|
740
1088
|
// layout version is unchanged (i.e. no defragmentation has moved it).
|
|
741
1089
|
// Mirrors the module-level offsetCache to avoid a Map lookup on the
|
|
@@ -938,12 +1286,13 @@ function createSharedRef<T>(
|
|
|
938
1286
|
}
|
|
939
1287
|
// Fixed shape: reject adding new properties (throws in strict mode).
|
|
940
1288
|
Object.preventExtensions(ref);
|
|
1289
|
+
sharedKeyMap.set(ref, key);
|
|
941
1290
|
return ref as unknown as SharedRef<T>;
|
|
942
1291
|
}
|
|
943
1292
|
|
|
944
1293
|
// Arrays, primitives, or unknown shape: use a Proxy (index access, array
|
|
945
1294
|
// methods, and dynamic properties require interception).
|
|
946
|
-
|
|
1295
|
+
const proxy = new Proxy(target, {
|
|
947
1296
|
get(t, prop) {
|
|
948
1297
|
if (prop === "read") return t.read;
|
|
949
1298
|
if (prop === "write") return t.write;
|
|
@@ -955,10 +1304,10 @@ function createSharedRef<T>(
|
|
|
955
1304
|
// push/pop on a top-level array: operate directly on the array ref
|
|
956
1305
|
// without decoding the whole array (O(1) amortized).
|
|
957
1306
|
if (prop === "push" || prop === "pop") {
|
|
958
|
-
const
|
|
959
|
-
if (
|
|
1307
|
+
const resolved = resolveArrayRef(mem, { key, path: [] });
|
|
1308
|
+
if (resolved !== null) {
|
|
960
1309
|
return (...args: unknown[]) =>
|
|
961
|
-
mutateArrayRef(mem, key,
|
|
1310
|
+
mutateArrayRef(mem, { key, path: [] }, prop, args);
|
|
962
1311
|
}
|
|
963
1312
|
}
|
|
964
1313
|
|
|
@@ -974,7 +1323,25 @@ function createSharedRef<T>(
|
|
|
974
1323
|
"Shared values have a fixed shape: properties may not be added or removed",
|
|
975
1324
|
);
|
|
976
1325
|
},
|
|
977
|
-
})
|
|
1326
|
+
});
|
|
1327
|
+
sharedKeyMap.set(proxy, key);
|
|
1328
|
+
return proxy as unknown as SharedRef<T>;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
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.
|
|
1335
|
+
*/
|
|
1336
|
+
function getNestedSharedRef<T = unknown>(
|
|
1337
|
+
mem: SharedMemory,
|
|
1338
|
+
key: string,
|
|
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;
|
|
978
1345
|
}
|
|
979
1346
|
|
|
980
1347
|
/**
|
|
@@ -988,17 +1355,22 @@ function readValue(
|
|
|
988
1355
|
mem: SharedMemory,
|
|
989
1356
|
key: string,
|
|
990
1357
|
cacheTarget?: {
|
|
991
|
-
__cache?: { version: number; value: unknown; index: number };
|
|
1358
|
+
__cache?: { version: number; value: unknown; index: number; cell: number };
|
|
992
1359
|
},
|
|
993
1360
|
): unknown {
|
|
994
1361
|
// Inline the read-cache fast path (see readProp) to avoid the polymorphic
|
|
995
1362
|
// {hit,value} union returned by readCachedValue.
|
|
996
1363
|
const cached = readCache.get(key);
|
|
997
|
-
if (
|
|
1364
|
+
if (
|
|
1365
|
+
cached !== undefined &&
|
|
1366
|
+
cached.version === Atomics.load(mem.versionView, cached.cell)
|
|
1367
|
+
) {
|
|
998
1368
|
return cached.value;
|
|
999
1369
|
}
|
|
1000
1370
|
|
|
1001
1371
|
const index = cached !== undefined ? cached.index : getIndex(mem, key);
|
|
1372
|
+
const cell =
|
|
1373
|
+
cached !== undefined ? cached.cell : mem.getVersionCellIndex(index);
|
|
1002
1374
|
const version = mem.getVersion(index);
|
|
1003
1375
|
|
|
1004
1376
|
// Cache miss: decode under the lock and cache with the observed version.
|
|
@@ -1021,7 +1393,7 @@ function readValue(
|
|
|
1021
1393
|
return decodeValue(mem, mem.readValueSlot(offset));
|
|
1022
1394
|
});
|
|
1023
1395
|
|
|
1024
|
-
const entry = { version, value, index };
|
|
1396
|
+
const entry = { version, value, index, cell };
|
|
1025
1397
|
readCache.set(key, entry);
|
|
1026
1398
|
if (cacheTarget) cacheTarget.__cache = entry;
|
|
1027
1399
|
return value;
|
|
@@ -1056,7 +1428,11 @@ function writeValueInner(
|
|
|
1056
1428
|
arrayCapacity?: number,
|
|
1057
1429
|
target?: WriteTarget,
|
|
1058
1430
|
): void {
|
|
1059
|
-
|
|
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);
|
|
1060
1436
|
if (existing !== null) {
|
|
1061
1437
|
// Skip the DataTable type read when the slot type is known to be stable
|
|
1062
1438
|
// (cached on the target). Only re-read when the type is unknown.
|
|
@@ -1133,10 +1509,11 @@ function writeValueInner(
|
|
|
1133
1509
|
const oldType = mem.dataTable.getInt32(existing);
|
|
1134
1510
|
const oldA = mem.dataTable.getInt32(existing + 4);
|
|
1135
1511
|
const oldB = mem.dataTable.getInt32(existing + 8);
|
|
1512
|
+
const oldC = mem.dataTable.getInt32(existing + 12);
|
|
1136
1513
|
const ref = mem.storeStringValue(
|
|
1137
1514
|
value,
|
|
1138
1515
|
oldType === TYPE_STRING_VALUE_REF
|
|
1139
|
-
? { offset: oldA, length: oldB }
|
|
1516
|
+
? { offset: oldA, length: oldB, reserved: oldC || undefined }
|
|
1140
1517
|
: undefined,
|
|
1141
1518
|
);
|
|
1142
1519
|
mem.updateValueSlot(existing, stringValueRefSlot(ref));
|
|
@@ -1249,55 +1626,110 @@ function mutateValue<T, R>(
|
|
|
1249
1626
|
* `prop` is null for a top-level array value, or the property name for an
|
|
1250
1627
|
* array property of an object. Returns null if the value is not an array.
|
|
1251
1628
|
*/
|
|
1252
|
-
|
|
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(
|
|
1253
1637
|
mem: SharedMemory,
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
if (
|
|
1638
|
+
locator: ArrayLocator,
|
|
1639
|
+
): { slot: ValueSlot; write: (s: ValueSlot) => void } | null {
|
|
1640
|
+
const { key, path } = locator;
|
|
1641
|
+
if (path.length === 0) {
|
|
1258
1642
|
const offset = getOffset(mem, key);
|
|
1259
1643
|
if (offset === null) return null;
|
|
1260
1644
|
const slot = mem.readValueSlot(offset);
|
|
1261
|
-
|
|
1645
|
+
if (slot.type !== TYPE_ARRAY_REF) return null;
|
|
1646
|
+
return { slot, write: (s) => mem.updateValueSlot(offset, s) };
|
|
1262
1647
|
}
|
|
1263
|
-
const
|
|
1264
|
-
if (
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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;
|
|
1269
1698
|
}
|
|
1270
1699
|
|
|
1271
1700
|
/**
|
|
1272
1701
|
* O(1) in-place push/pop on a shared array ref, without decoding or
|
|
1273
|
-
* re-encoding the whole array. `
|
|
1274
|
-
*
|
|
1702
|
+
* re-encoding the whole array. `locator` identifies the array (top-level,
|
|
1703
|
+
* object property, or nested).
|
|
1275
1704
|
*/
|
|
1276
1705
|
function mutateArrayRef(
|
|
1277
1706
|
mem: SharedMemory,
|
|
1278
|
-
|
|
1279
|
-
prop: string | null,
|
|
1707
|
+
locator: ArrayLocator,
|
|
1280
1708
|
method: "push" | "pop",
|
|
1281
1709
|
args: unknown[],
|
|
1282
1710
|
): unknown {
|
|
1283
1711
|
return writeLock(mem, () => {
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1712
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
1713
|
+
if (resolved === null) {
|
|
1286
1714
|
throw new TypeError("Value is not an array");
|
|
1287
1715
|
}
|
|
1288
|
-
const slot =
|
|
1289
|
-
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
|
+
};
|
|
1290
1722
|
|
|
1291
1723
|
if (method === "push") {
|
|
1292
1724
|
for (const arg of args) {
|
|
1293
1725
|
const next = mem.appendArraySlot(ref, encodeValue(mem, arg));
|
|
1294
1726
|
// Always persist the new count (and offset/capacity if moved).
|
|
1295
|
-
|
|
1727
|
+
resolved.write(arrayRefSlot(next));
|
|
1296
1728
|
ref.offset = next.offset;
|
|
1297
1729
|
ref.count = next.count;
|
|
1298
1730
|
ref.capacity = next.capacity;
|
|
1299
1731
|
}
|
|
1300
|
-
bump(mem, key);
|
|
1732
|
+
bump(mem, locator.key);
|
|
1301
1733
|
return ref.count;
|
|
1302
1734
|
}
|
|
1303
1735
|
|
|
@@ -1315,12 +1747,48 @@ function mutateArrayRef(
|
|
|
1315
1747
|
count: ref.count - 1,
|
|
1316
1748
|
capacity: ref.capacity,
|
|
1317
1749
|
};
|
|
1318
|
-
|
|
1319
|
-
bump(mem, key);
|
|
1750
|
+
resolved.write(arrayRefSlot(next));
|
|
1751
|
+
bump(mem, locator.key);
|
|
1320
1752
|
return result;
|
|
1321
1753
|
});
|
|
1322
1754
|
}
|
|
1323
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
|
+
|
|
1324
1792
|
/** Get a member of a top-level array value, with read-modify-write for mutating methods. */
|
|
1325
1793
|
function getTopLevelArrayMember(
|
|
1326
1794
|
mem: SharedMemory,
|
|
@@ -1331,7 +1799,23 @@ function getTopLevelArrayMember(
|
|
|
1331
1799
|
): unknown {
|
|
1332
1800
|
if (prop === "length") return value.length;
|
|
1333
1801
|
if (typeof prop === "string" && /^\d+$/.test(prop)) {
|
|
1334
|
-
|
|
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;
|
|
1335
1819
|
}
|
|
1336
1820
|
if (typeof prop === "symbol") {
|
|
1337
1821
|
if (prop === Symbol.iterator) return value[Symbol.iterator].bind(value);
|
|
@@ -1342,7 +1826,7 @@ function getTopLevelArrayMember(
|
|
|
1342
1826
|
// push/pop are O(1) in-place operations on the array ref.
|
|
1343
1827
|
if (methodName === "push" || methodName === "pop") {
|
|
1344
1828
|
return (...args: unknown[]) => {
|
|
1345
|
-
return mutateArrayRef(mem, key,
|
|
1829
|
+
return mutateArrayRef(mem, { key, path: [] }, methodName, args);
|
|
1346
1830
|
};
|
|
1347
1831
|
}
|
|
1348
1832
|
return (...args: unknown[]) => {
|
|
@@ -1364,18 +1848,21 @@ function getTopLevelArrayMember(
|
|
|
1364
1848
|
return method;
|
|
1365
1849
|
}
|
|
1366
1850
|
|
|
1367
|
-
/**
|
|
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
|
+
*/
|
|
1368
1857
|
function createArrayProxy(
|
|
1369
1858
|
mem: SharedMemory,
|
|
1370
|
-
|
|
1371
|
-
prop: string,
|
|
1859
|
+
locator: ArrayLocator,
|
|
1372
1860
|
arrayCapacity?: number,
|
|
1373
1861
|
): unknown[] {
|
|
1374
|
-
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
// (defragmentation or array growth via moveArray).
|
|
1378
|
-
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).
|
|
1379
1866
|
let cachedRef:
|
|
1380
1867
|
{ offset: number; count: number; capacity: number } | undefined;
|
|
1381
1868
|
let cachedLayoutVersion = -1;
|
|
@@ -1392,63 +1879,42 @@ function createArrayProxy(
|
|
|
1392
1879
|
}
|
|
1393
1880
|
|
|
1394
1881
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
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).
|
|
1398
1885
|
*/
|
|
1399
|
-
function
|
|
1400
|
-
|
|
1401
|
-
|
|
1886
|
+
function getRef(minCount = 0): {
|
|
1887
|
+
offset: number;
|
|
1888
|
+
count: number;
|
|
1889
|
+
capacity: number;
|
|
1890
|
+
} {
|
|
1402
1891
|
if (cachedLayoutVersion !== mem.getLayoutVersion()) {
|
|
1403
|
-
cachedSlotOffset = undefined;
|
|
1404
1892
|
cachedRef = undefined;
|
|
1405
1893
|
cachedLayoutVersion = mem.getLayoutVersion();
|
|
1406
1894
|
}
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
mem,
|
|
1413
|
-
key,
|
|
1414
|
-
(fresh) => {
|
|
1415
|
-
const arr = fresh[prop] as unknown[];
|
|
1416
|
-
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
1417
|
-
return true;
|
|
1418
|
-
},
|
|
1419
|
-
arrayCapacity,
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
const so = layout.properties.get(prop);
|
|
1423
|
-
if (so === undefined) {
|
|
1424
|
-
throw new TypeError(
|
|
1425
|
-
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
cachedSlotOffset = so;
|
|
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");
|
|
1429
1900
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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)) {
|
|
1436
1916
|
const index = Number(arrProp);
|
|
1437
|
-
|
|
1438
|
-
// grown past the cached count (push) or the layout changed.
|
|
1439
|
-
let ref = cachedRef;
|
|
1440
|
-
if (
|
|
1441
|
-
ref === undefined ||
|
|
1442
|
-
index >= ref.count ||
|
|
1443
|
-
cachedLayoutVersion !== mem.getLayoutVersion()
|
|
1444
|
-
) {
|
|
1445
|
-
ref = {
|
|
1446
|
-
offset: propSlot.a,
|
|
1447
|
-
count: propSlot.b,
|
|
1448
|
-
capacity: propSlot.c,
|
|
1449
|
-
};
|
|
1450
|
-
cachedRef = ref;
|
|
1451
|
-
}
|
|
1917
|
+
const ref = getRef(index + 1);
|
|
1452
1918
|
if (index >= ref.count) {
|
|
1453
1919
|
throw new RangeError(
|
|
1454
1920
|
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
@@ -1481,42 +1947,46 @@ function createArrayProxy(
|
|
|
1481
1947
|
}
|
|
1482
1948
|
}
|
|
1483
1949
|
freeSlotRefs(mem, mem.readArraySlot(ref, index));
|
|
1484
|
-
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
1950
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val, arrayCapacity));
|
|
1485
1951
|
bumpCached();
|
|
1486
1952
|
return true;
|
|
1487
1953
|
}
|
|
1488
|
-
// Array
|
|
1489
|
-
//
|
|
1490
|
-
if (
|
|
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") {
|
|
1491
1957
|
const newLen = Number(val);
|
|
1492
1958
|
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
1493
1959
|
throw new RangeError("Invalid array length");
|
|
1494
1960
|
}
|
|
1495
|
-
const ref =
|
|
1496
|
-
offset: propSlot.a,
|
|
1497
|
-
count: propSlot.b,
|
|
1498
|
-
capacity: propSlot.c,
|
|
1499
|
-
};
|
|
1961
|
+
const ref = getRef();
|
|
1500
1962
|
if (newLen <= ref.capacity) {
|
|
1501
|
-
mem
|
|
1502
|
-
|
|
1963
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
1964
|
+
if (resolved === null) {
|
|
1965
|
+
throw new TypeError("Value is not an array");
|
|
1966
|
+
}
|
|
1967
|
+
resolved.write(
|
|
1503
1968
|
arrayRefSlot({
|
|
1504
1969
|
offset: ref.offset,
|
|
1505
1970
|
count: newLen,
|
|
1506
1971
|
capacity: ref.capacity,
|
|
1507
1972
|
}),
|
|
1508
1973
|
);
|
|
1974
|
+
cachedRef = {
|
|
1975
|
+
offset: ref.offset,
|
|
1976
|
+
count: newLen,
|
|
1977
|
+
capacity: ref.capacity,
|
|
1978
|
+
};
|
|
1509
1979
|
bumpCached();
|
|
1510
1980
|
return true;
|
|
1511
1981
|
}
|
|
1512
1982
|
// Growing beyond capacity falls through to the full read-modify-write.
|
|
1513
1983
|
}
|
|
1514
1984
|
// Fallback: full read-modify-write.
|
|
1515
|
-
return mutateValue<
|
|
1985
|
+
return mutateValue<unknown[], boolean>(
|
|
1516
1986
|
mem,
|
|
1517
1987
|
key,
|
|
1518
1988
|
(fresh) => {
|
|
1519
|
-
const arr = fresh
|
|
1989
|
+
const arr = readPathValue(mem, locator, fresh) as unknown[];
|
|
1520
1990
|
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
1521
1991
|
return true;
|
|
1522
1992
|
},
|
|
@@ -1526,25 +1996,41 @@ function createArrayProxy(
|
|
|
1526
1996
|
|
|
1527
1997
|
return new Proxy([] as unknown[], {
|
|
1528
1998
|
get(_target, arrProp) {
|
|
1529
|
-
// push/pop on
|
|
1530
|
-
//
|
|
1999
|
+
// push/pop on the array: operate directly on the array ref without
|
|
2000
|
+
// decoding the whole value (O(1) amortized).
|
|
1531
2001
|
if (arrProp === "push" || arrProp === "pop") {
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
2002
|
+
const resolved = resolveArrayRef(mem, locator);
|
|
2003
|
+
if (resolved !== null) {
|
|
1534
2004
|
return (...args: unknown[]) =>
|
|
1535
|
-
mutateArrayRef(mem,
|
|
2005
|
+
mutateArrayRef(mem, locator, arrProp, args);
|
|
1536
2006
|
}
|
|
1537
2007
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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;
|
|
1542
2012
|
}
|
|
1543
|
-
if (arrProp === "length") return arr.length;
|
|
1544
2013
|
if (typeof arrProp === "string" && /^\d+$/.test(arrProp)) {
|
|
1545
|
-
|
|
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;
|
|
1546
2031
|
}
|
|
1547
2032
|
if (typeof arrProp === "symbol") {
|
|
2033
|
+
const arr = readArrayValue(mem, locator);
|
|
1548
2034
|
if (arrProp === Symbol.iterator) return arr[Symbol.iterator].bind(arr);
|
|
1549
2035
|
return (arr as unknown as Record<symbol, unknown>)[arrProp];
|
|
1550
2036
|
}
|
|
@@ -1553,15 +2039,15 @@ function createArrayProxy(
|
|
|
1553
2039
|
// push/pop are O(1) in-place operations on the array ref.
|
|
1554
2040
|
if (methodName === "push" || methodName === "pop") {
|
|
1555
2041
|
return (...args: unknown[]) => {
|
|
1556
|
-
return mutateArrayRef(mem,
|
|
2042
|
+
return mutateArrayRef(mem, locator, methodName, args);
|
|
1557
2043
|
};
|
|
1558
2044
|
}
|
|
1559
2045
|
return (...args: unknown[]) => {
|
|
1560
|
-
return mutateValue<
|
|
2046
|
+
return mutateValue<unknown[], unknown>(
|
|
1561
2047
|
mem,
|
|
1562
2048
|
key,
|
|
1563
2049
|
(fresh) => {
|
|
1564
|
-
const freshArr = fresh
|
|
2050
|
+
const freshArr = readPathValue(mem, locator, fresh) as unknown[];
|
|
1565
2051
|
const method = (
|
|
1566
2052
|
freshArr as unknown as Record<
|
|
1567
2053
|
string,
|
|
@@ -1574,13 +2060,14 @@ function createArrayProxy(
|
|
|
1574
2060
|
);
|
|
1575
2061
|
};
|
|
1576
2062
|
}
|
|
2063
|
+
const arr = readArrayValue(mem, locator);
|
|
1577
2064
|
const method = (arr as unknown as Record<string, unknown>)[arrProp];
|
|
1578
2065
|
if (typeof method === "function") return method.bind(arr);
|
|
1579
2066
|
return method;
|
|
1580
2067
|
},
|
|
1581
2068
|
set(_target, arrProp, val) {
|
|
1582
2069
|
// In-place update: write only the affected array slot instead of
|
|
1583
|
-
// decoding and re-encoding the whole
|
|
2070
|
+
// decoding and re-encoding the whole value. Inline writeLock to avoid
|
|
1584
2071
|
// a closure allocation on the hot path.
|
|
1585
2072
|
mem.beginWrite();
|
|
1586
2073
|
if (mem.isDefragmenting()) {
|