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/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
- /** Convert a JS value into a ValueSlot, storing strings/arrays in tables. */
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
- ? { offset: oldSlot.a, length: oldSlot.b }
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
- for (const v of value) {
92
- if (Array.isArray(v) || (v !== null && typeof v === "object")) {
93
- throw new TypeError(
94
- "Arrays may only contain primitives and bigints (no nested arrays or objects)",
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, and arrays of these are allowed`,
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 4: // array-ref
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
- offsetCache.set(key, { offset, layoutVersion: mem.getLayoutVersion() });
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 = mem.getLayoutVersion();
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
- const layoutCache = new Map<string, ObjectLayout | null>();
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
- if (layoutCache.has(key)) return layoutCache.get(key)!;
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 cannot corrupt
333
- * the cache). This is ~4.5× faster than the previous `Object.entries` + spread
334
- * loop, which allocated an entries array and re-checked `Array.isArray` on
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)) return [...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
- if (Array.isArray(copy[k])) copy[k] = [...(copy[k] as unknown[])];
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 && cached.version === mem.getVersion(cached.index)
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(mem, key, prop as string, arrayCapacity);
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(mem, key, prop, arrayCapacity);
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
- const offset = getCachedOffset(mem, key, target);
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 !== mem.getLayoutVersion()
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 = mem.getLayoutVersion();
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 } | undefined,
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
- return new Proxy(target, {
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 slotOffset = findArrayRefSlot(mem, key, null);
959
- if (slotOffset !== null) {
1307
+ const resolved = resolveArrayRef(mem, { key, path: [] });
1308
+ if (resolved !== null) {
960
1309
  return (...args: unknown[]) =>
961
- mutateArrayRef(mem, key, null, prop, args);
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
- }) as unknown as SharedRef<T>;
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 (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
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
- const existing = getCachedOffset(mem, key, target);
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
- function findArrayRefSlot(
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
- key: string,
1255
- prop: string | null,
1256
- ): number | null {
1257
- if (prop === null) {
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
- return slot.type === TYPE_ARRAY_REF ? offset : null;
1645
+ if (slot.type !== TYPE_ARRAY_REF) return null;
1646
+ return { slot, write: (s) => mem.updateValueSlot(offset, s) };
1262
1647
  }
1263
- const layout = getObjectLayout(mem, key);
1264
- if (layout === null) return null;
1265
- const slotOffset = layout.properties.get(prop);
1266
- if (slotOffset === undefined) return null;
1267
- const slot = mem.readValueSlot(slotOffset);
1268
- return slot.type === TYPE_ARRAY_REF ? slotOffset : null;
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. `prop` is null for a top-level array, or the
1274
- * property name for an array property of an object.
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
- key: string,
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 slotOffset = findArrayRefSlot(mem, key, prop);
1285
- if (slotOffset === null) {
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 = mem.readValueSlot(slotOffset);
1289
- const ref: ArrayRef = { offset: slot.a, count: slot.b, capacity: slot.c };
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
- mem.updateValueSlot(slotOffset, arrayRefSlot(next));
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
- mem.updateValueSlot(slotOffset, arrayRefSlot(next));
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
- return value[Number(prop)];
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, null, methodName, args);
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
- /** Create a proxy for an array property of a shared object. */
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
- key: string,
1371
- prop: string,
1859
+ locator: ArrayLocator,
1372
1860
  arrayCapacity?: number,
1373
1861
  ): unknown[] {
1374
- // Cache the object's property slot offset and the array ref so index writes
1375
- // skip the per-write getObjectLayout Map lookup, properties.get, and the
1376
- // DataTable header reads. Invalidated when the layout version changes
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
- * Write a single array-property slot in place. Defined once (not per call)
1396
- * so the set handler can inline the writeLock without allocating a closure
1397
- * on every write. Mutates the cached slot offset/ref/layout version.
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 writeArrayProp(arrProp: string | symbol, val: unknown): boolean {
1400
- // Refresh the cached slot offset when the layout version changed
1401
- // (defragmentation may have moved the object block).
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 (cachedSlotOffset === undefined) {
1408
- const layout = getObjectLayout(mem, key);
1409
- if (layout === null) {
1410
- // Fallback: full read-modify-write.
1411
- return mutateValue<Record<string, unknown>, boolean>(
1412
- mem,
1413
- key,
1414
- (fresh) => {
1415
- const arr = fresh[prop] as unknown[];
1416
- (arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
1417
- return true;
1418
- },
1419
- arrayCapacity,
1420
- );
1421
- }
1422
- const so = layout.properties.get(prop);
1423
- if (so === undefined) {
1424
- throw new TypeError(
1425
- `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
1426
- );
1427
- }
1428
- cachedSlotOffset = so;
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
- const propSlot = mem.readValueSlot(cachedSlotOffset);
1431
- if (
1432
- propSlot.type === TYPE_ARRAY_REF &&
1433
- typeof arrProp === "string" &&
1434
- /^\d+$/.test(arrProp)
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
- // Use the cached ref when valid; re-read when the array may have
1438
- // grown past the cached count (push) or the layout changed.
1439
- let ref = cachedRef;
1440
- if (
1441
- ref === undefined ||
1442
- index >= ref.count ||
1443
- cachedLayoutVersion !== mem.getLayoutVersion()
1444
- ) {
1445
- ref = {
1446
- offset: propSlot.a,
1447
- count: propSlot.b,
1448
- capacity: propSlot.c,
1449
- };
1450
- cachedRef = ref;
1451
- }
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 property length write: shrink/grow in place without re-encoding
1489
- // the whole object (which would leave a dead slot).
1490
- if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
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.updateValueSlot(
1502
- cachedSlotOffset,
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<Record<string, unknown>, boolean>(
1985
+ return mutateValue<unknown[], boolean>(
1516
1986
  mem,
1517
1987
  key,
1518
1988
  (fresh) => {
1519
- const arr = fresh[prop] as unknown[];
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 an array property: operate directly on the array ref
1530
- // without decoding the whole object (O(1) amortized).
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 slotOffset = findArrayRefSlot(mem, key, prop);
1533
- if (slotOffset !== null) {
2002
+ const resolved = resolveArrayRef(mem, locator);
2003
+ if (resolved !== null) {
1534
2004
  return (...args: unknown[]) =>
1535
- mutateArrayRef(mem, key, prop, arrProp, args);
2005
+ mutateArrayRef(mem, locator, arrProp, args);
1536
2006
  }
1537
2007
  }
1538
- const current = readValue(mem, key) as Record<string, unknown>;
1539
- const arr = current[prop] as unknown[];
1540
- if (!Array.isArray(arr)) {
1541
- throw new TypeError(`Property "${prop}" is not an array`);
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
- return arr[Number(arrProp)];
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, key, prop, methodName, args);
2042
+ return mutateArrayRef(mem, locator, methodName, args);
1557
2043
  };
1558
2044
  }
1559
2045
  return (...args: unknown[]) => {
1560
- return mutateValue<Record<string, unknown>, unknown>(
2046
+ return mutateValue<unknown[], unknown>(
1561
2047
  mem,
1562
2048
  key,
1563
2049
  (fresh) => {
1564
- const freshArr = fresh[prop] as unknown[];
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 object. Inline writeLock to avoid
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()) {