bun-shared-memory 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  *
@@ -443,14 +759,32 @@ function readProp(
443
759
  prop: string | symbol,
444
760
  arrayCapacity?: number,
445
761
  ): unknown {
446
- const fast = readCachedValue(mem, key);
447
- const value = fast.hit ? fast.value : readValue(mem, key);
762
+ // Inline the read-cache fast path (instead of calling readCachedValue, which
763
+ // returns a polymorphic {hit,value} union and allocates an object on every
764
+ // cache hit). Keeping this monomorphic lets the JIT optimize the hot read.
765
+ const cached = readCache.get(key);
766
+ const value =
767
+ cached !== undefined &&
768
+ cached.version === Atomics.load(mem.versionView, cached.cell)
769
+ ? cached.value
770
+ : readValue(mem, key);
448
771
  if (Array.isArray(value)) {
449
772
  return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
450
773
  }
451
774
  const propValue = (value as Record<string | symbol, unknown>)[prop];
452
775
  if (Array.isArray(propValue)) {
453
- return createArrayProxy(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
+ );
454
788
  }
455
789
  return propValue;
456
790
  }
@@ -484,15 +818,81 @@ function readObjectProp(
484
818
  );
485
819
  }
486
820
  const slot = mem.readValueSlot(slotOffset);
821
+ if (slot.type === TYPE_OBJECT_REF || slot.type === TYPE_KEY_REF) {
822
+ // Nested object (plain block or live shared reference): return a live
823
+ // proxy so in-place mutation stays in sync.
824
+ const value = createNestedObjectProxy(
825
+ mem,
826
+ { key, path: [{ kind: "prop", name: prop }] },
827
+ arrayCapacity,
828
+ );
829
+ fieldCache.version = version;
830
+ fieldCache.value = value;
831
+ return value;
832
+ }
487
833
  let value = decodeValue(mem, slot);
488
834
  if (Array.isArray(value)) {
489
- value = createArrayProxy(mem, key, prop, arrayCapacity);
835
+ value = createArrayProxy(
836
+ mem,
837
+ { key, path: [{ kind: "prop", name: prop }] },
838
+ arrayCapacity,
839
+ );
490
840
  }
491
841
  fieldCache.version = version;
492
842
  fieldCache.value = value;
493
843
  return value;
494
844
  }
495
845
 
846
+ /**
847
+ * Write a single object-property slot whose offset is already resolved
848
+ * (cached by the caller, e.g. the accessor setter). Avoids the
849
+ * `getObjectLayout` Map lookup + `properties.get` per write on the hot path.
850
+ */
851
+ function writeObjectSlot(
852
+ mem: SharedMemory,
853
+ key: string,
854
+ slotOffset: number,
855
+ val: unknown,
856
+ arrayCapacity?: number,
857
+ target?: WriteTarget,
858
+ ): void {
859
+ if (typeof val === "number") {
860
+ // Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
861
+ // and no in-place string reuse; write the float64 directly.
862
+ mem.updateNumberSlot(slotOffset, val);
863
+ bumpTarget(mem, key, target);
864
+ return;
865
+ }
866
+ if (typeof val === "boolean") {
867
+ // Fast path: booleans need no old-slot read (freeSlotRefs is a no-op)
868
+ // and no in-place string reuse; write the 4 Int32s directly.
869
+ mem.updateBooleanSlot(slotOffset, val);
870
+ bumpTarget(mem, key, target);
871
+ return;
872
+ }
873
+ if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
874
+ // Fast path: int64 bigints are written directly, avoiding the ValueSlot
875
+ // object allocation of the generic path.
876
+ mem.updateBigIntSlot(slotOffset, val);
877
+ bumpTarget(mem, key, target);
878
+ return;
879
+ }
880
+ if (typeof val === "string") {
881
+ // Fast path: short strings are written inline into the slot itself.
882
+ if (mem.updateInlineStringSlot(slotOffset, val)) {
883
+ bumpTarget(mem, key, target);
884
+ return;
885
+ }
886
+ }
887
+ const oldSlot = mem.readValueSlot(slotOffset);
888
+ freeSlotRefs(mem, oldSlot);
889
+ mem.updateValueSlot(
890
+ slotOffset,
891
+ encodeValue(mem, val, arrayCapacity, oldSlot),
892
+ );
893
+ bumpTarget(mem, key, target);
894
+ }
895
+
496
896
  /**
497
897
  * Write a single property of a shared object value in place.
498
898
  *
@@ -544,33 +944,16 @@ function writePropInner(
544
944
  `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
545
945
  );
546
946
  }
547
- if (typeof val === "number") {
548
- // Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
549
- // and no in-place string reuse; write the float64 directly.
550
- mem.updateNumberSlot(slotOffset, val);
551
- bumpTarget(mem, key, target);
552
- return;
553
- }
554
- if (typeof val === "string") {
555
- // Fast path: short strings are written inline into the slot itself.
556
- if (mem.updateInlineStringSlot(slotOffset, val)) {
557
- bumpTarget(mem, key, target);
558
- return;
559
- }
560
- }
561
- const oldSlot = mem.readValueSlot(slotOffset);
562
- freeSlotRefs(mem, oldSlot);
563
- mem.updateValueSlot(
564
- slotOffset,
565
- encodeValue(mem, val, arrayCapacity, oldSlot),
566
- );
567
- bumpTarget(mem, key, target);
947
+ writeObjectSlot(mem, key, slotOffset, val, arrayCapacity, target);
568
948
  return;
569
949
  }
570
950
  }
571
951
 
572
952
  // Top-level array index write: update only that array slot.
573
- 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);
574
957
  if (offset === null) {
575
958
  throw new Error(`No shared value found for key "${key}"`);
576
959
  }
@@ -593,7 +976,7 @@ function writePropInner(
593
976
  let ref = target?.__arrayRef;
594
977
  if (
595
978
  ref === undefined ||
596
- target!.__arrayRefLayoutVersion !== mem.getLayoutVersion()
979
+ target!.__arrayRefLayoutVersion !== cachedLv
597
980
  ) {
598
981
  ref = {
599
982
  offset: mem.dataTable.getInt32(offset + 4),
@@ -602,7 +985,7 @@ function writePropInner(
602
985
  };
603
986
  if (target !== undefined) {
604
987
  target.__arrayRef = ref;
605
- target.__arrayRefLayoutVersion = mem.getLayoutVersion();
988
+ target.__arrayRefLayoutVersion = cachedLv;
606
989
  }
607
990
  }
608
991
  if (index >= ref.count) {
@@ -622,6 +1005,13 @@ function writePropInner(
622
1005
  bumpTarget(mem, key, target);
623
1006
  return;
624
1007
  }
1008
+ if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
1009
+ // Fast path: int64 bigints are written directly, avoiding the
1010
+ // ValueSlot object allocation of the generic path.
1011
+ mem.updateArrayBigIntSlot(ref, index, val);
1012
+ bumpTarget(mem, key, target);
1013
+ return;
1014
+ }
625
1015
  if (typeof val === "string") {
626
1016
  // Fast path: short strings are written inline into the array slot.
627
1017
  if (mem.updateArrayInlineStringSlot(ref, index, val)) {
@@ -692,7 +1082,8 @@ function createSharedRef<T>(
692
1082
  // on every local write/update; cross-worker invalidation is detected by
693
1083
  // the version check in the get handler.
694
1084
  __cache: undefined as
695
- { version: number; value: unknown; index: number } | undefined,
1085
+ | { version: number; value: unknown; index: number; cell: number }
1086
+ | undefined,
696
1087
  // Cached DataTable offset for this key, valid only while the global
697
1088
  // layout version is unchanged (i.e. no defragmentation has moved it).
698
1089
  // Mirrors the module-level offsetCache to avoid a Map lookup on the
@@ -811,6 +1202,12 @@ function createSharedRef<T>(
811
1202
  // recreate the array proxy on every access. The proxy reads fresh data
812
1203
  // on each access, so it stays correct without being recreated.
813
1204
  let arrayProxy: unknown[] | undefined;
1205
+ // Cached object slot offset for this property so writes skip the
1206
+ // getObjectLayout Map lookup + properties.get on the hot path.
1207
+ // Invalidated when the layout version changes (defragmentation moves
1208
+ // the object block).
1209
+ let slotOffset: number | undefined;
1210
+ let slotLayoutVersion = -1;
814
1211
  Object.defineProperty(ref, name, {
815
1212
  enumerable: true,
816
1213
  configurable: false,
@@ -845,18 +1242,57 @@ function createSharedRef<T>(
845
1242
  },
846
1243
  set(v: unknown) {
847
1244
  target.__cache = undefined;
848
- writeProp(mem, key, name, v, arrayCapacity, target);
1245
+ mem.beginWrite();
1246
+ if (mem.isDefragmenting()) {
1247
+ // A defrag pass may move the object block: drop the cached slot
1248
+ // offset so it is re-resolved, and run the write under the mutex.
1249
+ mem.endWrite();
1250
+ slotOffset = undefined;
1251
+ slotLayoutVersion = -1;
1252
+ mem.withLock(() =>
1253
+ writePropInner(mem, key, name, v, arrayCapacity, target),
1254
+ );
1255
+ return;
1256
+ }
1257
+ try {
1258
+ // Write directly to the cached slot offset when still valid,
1259
+ // skipping the getObjectLayout Map lookup + properties.get.
1260
+ if (slotLayoutVersion !== mem.getLayoutVersion()) {
1261
+ slotOffset = undefined;
1262
+ slotLayoutVersion = mem.getLayoutVersion();
1263
+ }
1264
+ if (slotOffset === undefined) {
1265
+ const layout = getObjectLayout(mem, key);
1266
+ if (layout === null) {
1267
+ // Not an object (shouldn't happen for an object accessor).
1268
+ throw new TypeError(
1269
+ `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
1270
+ );
1271
+ }
1272
+ const so = layout.properties.get(name);
1273
+ if (so === undefined) {
1274
+ throw new TypeError(
1275
+ `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
1276
+ );
1277
+ }
1278
+ slotOffset = so;
1279
+ }
1280
+ writeObjectSlot(mem, key, slotOffset, v, arrayCapacity, target);
1281
+ } finally {
1282
+ mem.endWrite();
1283
+ }
849
1284
  },
850
1285
  });
851
1286
  }
852
1287
  // Fixed shape: reject adding new properties (throws in strict mode).
853
1288
  Object.preventExtensions(ref);
1289
+ sharedKeyMap.set(ref, key);
854
1290
  return ref as unknown as SharedRef<T>;
855
1291
  }
856
1292
 
857
1293
  // Arrays, primitives, or unknown shape: use a Proxy (index access, array
858
1294
  // methods, and dynamic properties require interception).
859
- return new Proxy(target, {
1295
+ const proxy = new Proxy(target, {
860
1296
  get(t, prop) {
861
1297
  if (prop === "read") return t.read;
862
1298
  if (prop === "write") return t.write;
@@ -868,10 +1304,10 @@ function createSharedRef<T>(
868
1304
  // push/pop on a top-level array: operate directly on the array ref
869
1305
  // without decoding the whole array (O(1) amortized).
870
1306
  if (prop === "push" || prop === "pop") {
871
- const slotOffset = findArrayRefSlot(mem, key, null);
872
- if (slotOffset !== null) {
1307
+ const resolved = resolveArrayRef(mem, { key, path: [] });
1308
+ if (resolved !== null) {
873
1309
  return (...args: unknown[]) =>
874
- mutateArrayRef(mem, key, null, prop, args);
1310
+ mutateArrayRef(mem, { key, path: [] }, prop, args);
875
1311
  }
876
1312
  }
877
1313
 
@@ -887,23 +1323,25 @@ function createSharedRef<T>(
887
1323
  "Shared values have a fixed shape: properties may not be added or removed",
888
1324
  );
889
1325
  },
890
- }) as unknown as SharedRef<T>;
1326
+ });
1327
+ sharedKeyMap.set(proxy, key);
1328
+ return proxy as unknown as SharedRef<T>;
891
1329
  }
892
1330
 
893
1331
  /**
894
- * Lock-free fast path: if the cached version matches the current version cell,
895
- * the cached value is still valid. Returns the cached value, or a miss marker.
896
- * Kept small so the JIT can inline it into hot accessor getters.
1332
+ * Return a live wrapper for a shared key that already exists (used to resolve
1333
+ * a TYPE_KEY_REF back to a reusable wrapper). Does not write anything — the
1334
+ * key must already exist in shared memory.
897
1335
  */
898
- function readCachedValue(
1336
+ function getNestedSharedRef<T = unknown>(
899
1337
  mem: SharedMemory,
900
1338
  key: string,
901
- ): { hit: true; value: unknown } | { hit: false } {
902
- const cached = readCache.get(key);
903
- if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
904
- return { hit: true, value: cached.value };
905
- }
906
- return { hit: false };
1339
+ ): SharedRef<T> {
1340
+ const existing = registry.get(key);
1341
+ if (existing) return existing as SharedRef<T>;
1342
+ const ref = createSharedRef<T>(mem, key, undefined) as SharedRef<T>;
1343
+ registry.set(key, ref as SharedRef<unknown>);
1344
+ return ref;
907
1345
  }
908
1346
 
909
1347
  /**
@@ -917,14 +1355,22 @@ function readValue(
917
1355
  mem: SharedMemory,
918
1356
  key: string,
919
1357
  cacheTarget?: {
920
- __cache?: { version: number; value: unknown; index: number };
1358
+ __cache?: { version: number; value: unknown; index: number; cell: number };
921
1359
  },
922
1360
  ): unknown {
923
- const fast = readCachedValue(mem, key);
924
- if (fast.hit) return fast.value;
925
-
1361
+ // Inline the read-cache fast path (see readProp) to avoid the polymorphic
1362
+ // {hit,value} union returned by readCachedValue.
926
1363
  const cached = readCache.get(key);
1364
+ if (
1365
+ cached !== undefined &&
1366
+ cached.version === Atomics.load(mem.versionView, cached.cell)
1367
+ ) {
1368
+ return cached.value;
1369
+ }
1370
+
927
1371
  const index = cached !== undefined ? cached.index : getIndex(mem, key);
1372
+ const cell =
1373
+ cached !== undefined ? cached.cell : mem.getVersionCellIndex(index);
928
1374
  const version = mem.getVersion(index);
929
1375
 
930
1376
  // Cache miss: decode under the lock and cache with the observed version.
@@ -947,7 +1393,7 @@ function readValue(
947
1393
  return decodeValue(mem, mem.readValueSlot(offset));
948
1394
  });
949
1395
 
950
- const entry = { version, value, index };
1396
+ const entry = { version, value, index, cell };
951
1397
  readCache.set(key, entry);
952
1398
  if (cacheTarget) cacheTarget.__cache = entry;
953
1399
  return value;
@@ -982,7 +1428,11 @@ function writeValueInner(
982
1428
  arrayCapacity?: number,
983
1429
  target?: WriteTarget,
984
1430
  ): void {
985
- 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);
986
1436
  if (existing !== null) {
987
1437
  // Skip the DataTable type read when the slot type is known to be stable
988
1438
  // (cached on the target). Only re-read when the type is unknown.
@@ -1037,6 +1487,10 @@ function writeValueInner(
1037
1487
  // no-op) and no in-place string reuse.
1038
1488
  if (typeof value === "boolean") {
1039
1489
  mem.updateBooleanSlot(existing, value);
1490
+ } else if (value >= MIN_INT64 && value <= MAX_INT64) {
1491
+ // Fast path: int64 bigints are written directly, avoiding the
1492
+ // ValueSlot object allocation of the generic path.
1493
+ mem.updateBigIntSlot(existing, value);
1040
1494
  } else {
1041
1495
  mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
1042
1496
  }
@@ -1055,10 +1509,11 @@ function writeValueInner(
1055
1509
  const oldType = mem.dataTable.getInt32(existing);
1056
1510
  const oldA = mem.dataTable.getInt32(existing + 4);
1057
1511
  const oldB = mem.dataTable.getInt32(existing + 8);
1512
+ const oldC = mem.dataTable.getInt32(existing + 12);
1058
1513
  const ref = mem.storeStringValue(
1059
1514
  value,
1060
1515
  oldType === TYPE_STRING_VALUE_REF
1061
- ? { offset: oldA, length: oldB }
1516
+ ? { offset: oldA, length: oldB, reserved: oldC || undefined }
1062
1517
  : undefined,
1063
1518
  );
1064
1519
  mem.updateValueSlot(existing, stringValueRefSlot(ref));
@@ -1171,55 +1626,110 @@ function mutateValue<T, R>(
1171
1626
  * `prop` is null for a top-level array value, or the property name for an
1172
1627
  * array property of an object. Returns null if the value is not an array.
1173
1628
  */
1174
- 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(
1175
1637
  mem: SharedMemory,
1176
- key: string,
1177
- prop: string | null,
1178
- ): number | null {
1179
- if (prop === null) {
1638
+ locator: ArrayLocator,
1639
+ ): { slot: ValueSlot; write: (s: ValueSlot) => void } | null {
1640
+ const { key, path } = locator;
1641
+ if (path.length === 0) {
1180
1642
  const offset = getOffset(mem, key);
1181
1643
  if (offset === null) return null;
1182
1644
  const slot = mem.readValueSlot(offset);
1183
- 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) };
1184
1647
  }
1185
- const layout = getObjectLayout(mem, key);
1186
- if (layout === null) return null;
1187
- const slotOffset = layout.properties.get(prop);
1188
- if (slotOffset === undefined) return null;
1189
- const slot = mem.readValueSlot(slotOffset);
1190
- 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;
1191
1698
  }
1192
1699
 
1193
1700
  /**
1194
1701
  * O(1) in-place push/pop on a shared array ref, without decoding or
1195
- * re-encoding the whole array. `prop` is null for a top-level array, or the
1196
- * 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).
1197
1704
  */
1198
1705
  function mutateArrayRef(
1199
1706
  mem: SharedMemory,
1200
- key: string,
1201
- prop: string | null,
1707
+ locator: ArrayLocator,
1202
1708
  method: "push" | "pop",
1203
1709
  args: unknown[],
1204
1710
  ): unknown {
1205
1711
  return writeLock(mem, () => {
1206
- const slotOffset = findArrayRefSlot(mem, key, prop);
1207
- if (slotOffset === null) {
1712
+ const resolved = resolveArrayRef(mem, locator);
1713
+ if (resolved === null) {
1208
1714
  throw new TypeError("Value is not an array");
1209
1715
  }
1210
- const slot = mem.readValueSlot(slotOffset);
1211
- 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
+ };
1212
1722
 
1213
1723
  if (method === "push") {
1214
1724
  for (const arg of args) {
1215
1725
  const next = mem.appendArraySlot(ref, encodeValue(mem, arg));
1216
1726
  // Always persist the new count (and offset/capacity if moved).
1217
- mem.updateValueSlot(slotOffset, arrayRefSlot(next));
1727
+ resolved.write(arrayRefSlot(next));
1218
1728
  ref.offset = next.offset;
1219
1729
  ref.count = next.count;
1220
1730
  ref.capacity = next.capacity;
1221
1731
  }
1222
- bump(mem, key);
1732
+ bump(mem, locator.key);
1223
1733
  return ref.count;
1224
1734
  }
1225
1735
 
@@ -1237,12 +1747,48 @@ function mutateArrayRef(
1237
1747
  count: ref.count - 1,
1238
1748
  capacity: ref.capacity,
1239
1749
  };
1240
- mem.updateValueSlot(slotOffset, arrayRefSlot(next));
1241
- bump(mem, key);
1750
+ resolved.write(arrayRefSlot(next));
1751
+ bump(mem, locator.key);
1242
1752
  return result;
1243
1753
  });
1244
1754
  }
1245
1755
 
1756
+ /**
1757
+ * Read the decoded value of the array at a locator, using the read cache for
1758
+ * the root value and navigating the path. This is the fast read path (the
1759
+ * root read hits the version-checked cache), matching the pre-nested-array
1760
+ * performance for array-property reads.
1761
+ */
1762
+ function readArrayValue(mem: SharedMemory, locator: ArrayLocator): unknown[] {
1763
+ const root = readValue(mem, locator.key);
1764
+ const arr = readPathValue(mem, locator, root);
1765
+ if (!Array.isArray(arr)) {
1766
+ throw new TypeError("Value is not an array");
1767
+ }
1768
+ return arr;
1769
+ }
1770
+
1771
+ /**
1772
+ * Navigate a decoded root value (the whole top-level value) along a locator
1773
+ * path, returning the value at the end of the path. Used by the fallback
1774
+ * read-modify-write paths.
1775
+ */
1776
+ function readPathValue(
1777
+ mem: SharedMemory,
1778
+ locator: ArrayLocator,
1779
+ rootValue: unknown,
1780
+ ): unknown {
1781
+ let current = rootValue;
1782
+ for (const step of locator.path) {
1783
+ if (step.kind === "prop") {
1784
+ current = (current as Record<string, unknown>)[step.name];
1785
+ } else {
1786
+ current = (current as unknown[])[step.index];
1787
+ }
1788
+ }
1789
+ return current;
1790
+ }
1791
+
1246
1792
  /** Get a member of a top-level array value, with read-modify-write for mutating methods. */
1247
1793
  function getTopLevelArrayMember(
1248
1794
  mem: SharedMemory,
@@ -1253,7 +1799,23 @@ function getTopLevelArrayMember(
1253
1799
  ): unknown {
1254
1800
  if (prop === "length") return value.length;
1255
1801
  if (typeof prop === "string" && /^\d+$/.test(prop)) {
1256
- 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;
1257
1819
  }
1258
1820
  if (typeof prop === "symbol") {
1259
1821
  if (prop === Symbol.iterator) return value[Symbol.iterator].bind(value);
@@ -1264,7 +1826,7 @@ function getTopLevelArrayMember(
1264
1826
  // push/pop are O(1) in-place operations on the array ref.
1265
1827
  if (methodName === "push" || methodName === "pop") {
1266
1828
  return (...args: unknown[]) => {
1267
- return mutateArrayRef(mem, key, null, methodName, args);
1829
+ return mutateArrayRef(mem, { key, path: [] }, methodName, args);
1268
1830
  };
1269
1831
  }
1270
1832
  return (...args: unknown[]) => {
@@ -1286,42 +1848,189 @@ function getTopLevelArrayMember(
1286
1848
  return method;
1287
1849
  }
1288
1850
 
1289
- /** 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
+ */
1290
1857
  function createArrayProxy(
1291
1858
  mem: SharedMemory,
1292
- key: string,
1293
- prop: string,
1859
+ locator: ArrayLocator,
1294
1860
  arrayCapacity?: number,
1295
1861
  ): unknown[] {
1296
- // Cache the object's property slot offset and the array ref so index writes
1297
- // skip the per-write getObjectLayout Map lookup, properties.get, and the
1298
- // DataTable header reads. Invalidated when the layout version changes
1299
- // (defragmentation or array growth via moveArray).
1300
- let cachedSlotOffset: number | undefined;
1862
+ const { key } = locator;
1863
+ // Cache the resolved array ref and the version-cell index so index writes
1864
+ // skip the per-write path navigation and Map lookups. Invalidated when the
1865
+ // layout version changes (defragmentation or array growth via moveArray).
1301
1866
  let cachedRef:
1302
1867
  { offset: number; count: number; capacity: number } | undefined;
1303
1868
  let cachedLayoutVersion = -1;
1869
+ // Cached version-cell index for this key, so writes can bump the version
1870
+ // cell without a Map lookup on every write. Stable for the key's lifetime.
1871
+ let cachedIndex: number | undefined;
1872
+
1873
+ /** Bump the key's version cell using the cached index (resolved lazily). */
1874
+ function bumpCached(): void {
1875
+ if (cachedIndex === undefined) {
1876
+ cachedIndex = getIndex(mem, key);
1877
+ }
1878
+ mem.bumpVersion(cachedIndex);
1879
+ }
1880
+
1881
+ /**
1882
+ * Resolve the array ref, refreshing the cache when the layout changed or
1883
+ * when the cached count is too small for a requested index (the array may
1884
+ * have grown in place via `push` without a layout-version bump).
1885
+ */
1886
+ function getRef(minCount = 0): {
1887
+ offset: number;
1888
+ count: number;
1889
+ capacity: number;
1890
+ } {
1891
+ if (cachedLayoutVersion !== mem.getLayoutVersion()) {
1892
+ cachedRef = undefined;
1893
+ cachedLayoutVersion = mem.getLayoutVersion();
1894
+ }
1895
+ if (cachedRef !== undefined && cachedRef.count >= minCount)
1896
+ return cachedRef;
1897
+ const resolved = resolveArrayRef(mem, locator);
1898
+ if (resolved === null) {
1899
+ throw new TypeError("Value is not an array");
1900
+ }
1901
+ cachedRef = {
1902
+ offset: resolved.slot.a,
1903
+ count: resolved.slot.b,
1904
+ capacity: resolved.slot.c,
1905
+ };
1906
+ return cachedRef;
1907
+ }
1908
+
1909
+ /**
1910
+ * Write a single array slot in place. Defined once (not per call) so the
1911
+ * set handler can inline the writeLock without allocating a closure on
1912
+ * every write.
1913
+ */
1914
+ function writeArrayProp(arrProp: string | symbol, val: unknown): boolean {
1915
+ if (typeof arrProp === "string" && /^\d+$/.test(arrProp)) {
1916
+ const index = Number(arrProp);
1917
+ const ref = getRef(index + 1);
1918
+ if (index >= ref.count) {
1919
+ throw new RangeError(
1920
+ `Index ${index} out of bounds for array of length ${ref.count}`,
1921
+ );
1922
+ }
1923
+ if (typeof val === "number") {
1924
+ // Fast path: numbers need no old-slot read (freeSlotRefs is a no-op).
1925
+ mem.updateArrayNumberSlot(ref, index, val);
1926
+ bumpCached();
1927
+ return true;
1928
+ }
1929
+ if (typeof val === "boolean") {
1930
+ // Fast path: booleans need no old-slot read (freeSlotRefs is a no-op).
1931
+ mem.updateArrayBooleanSlot(ref, index, val);
1932
+ bumpCached();
1933
+ return true;
1934
+ }
1935
+ if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
1936
+ // Fast path: int64 bigints are written directly, avoiding the
1937
+ // ValueSlot object allocation of the generic path.
1938
+ mem.updateArrayBigIntSlot(ref, index, val);
1939
+ bumpCached();
1940
+ return true;
1941
+ }
1942
+ if (typeof val === "string") {
1943
+ // Fast path: short strings are written inline into the slot.
1944
+ if (mem.updateArrayInlineStringSlot(ref, index, val)) {
1945
+ bumpCached();
1946
+ return true;
1947
+ }
1948
+ }
1949
+ freeSlotRefs(mem, mem.readArraySlot(ref, index));
1950
+ mem.updateArraySlot(ref, index, encodeValue(mem, val, arrayCapacity));
1951
+ bumpCached();
1952
+ return true;
1953
+ }
1954
+ // Array length write: shrink/grow in place without re-encoding the whole
1955
+ // value (which would leave a dead slot).
1956
+ if (arrProp === "length") {
1957
+ const newLen = Number(val);
1958
+ if (!Number.isInteger(newLen) || newLen < 0) {
1959
+ throw new RangeError("Invalid array length");
1960
+ }
1961
+ const ref = getRef();
1962
+ if (newLen <= ref.capacity) {
1963
+ const resolved = resolveArrayRef(mem, locator);
1964
+ if (resolved === null) {
1965
+ throw new TypeError("Value is not an array");
1966
+ }
1967
+ resolved.write(
1968
+ arrayRefSlot({
1969
+ offset: ref.offset,
1970
+ count: newLen,
1971
+ capacity: ref.capacity,
1972
+ }),
1973
+ );
1974
+ cachedRef = {
1975
+ offset: ref.offset,
1976
+ count: newLen,
1977
+ capacity: ref.capacity,
1978
+ };
1979
+ bumpCached();
1980
+ return true;
1981
+ }
1982
+ // Growing beyond capacity falls through to the full read-modify-write.
1983
+ }
1984
+ // Fallback: full read-modify-write.
1985
+ return mutateValue<unknown[], boolean>(
1986
+ mem,
1987
+ key,
1988
+ (fresh) => {
1989
+ const arr = readPathValue(mem, locator, fresh) as unknown[];
1990
+ (arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
1991
+ return true;
1992
+ },
1993
+ arrayCapacity,
1994
+ );
1995
+ }
1996
+
1304
1997
  return new Proxy([] as unknown[], {
1305
1998
  get(_target, arrProp) {
1306
- // push/pop on an array property: operate directly on the array ref
1307
- // 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).
1308
2001
  if (arrProp === "push" || arrProp === "pop") {
1309
- const slotOffset = findArrayRefSlot(mem, key, prop);
1310
- if (slotOffset !== null) {
2002
+ const resolved = resolveArrayRef(mem, locator);
2003
+ if (resolved !== null) {
1311
2004
  return (...args: unknown[]) =>
1312
- mutateArrayRef(mem, key, prop, arrProp, args);
2005
+ mutateArrayRef(mem, locator, arrProp, args);
1313
2006
  }
1314
2007
  }
1315
- const current = readValue(mem, key) as Record<string, unknown>;
1316
- const arr = current[prop] as unknown[];
1317
- if (!Array.isArray(arr)) {
1318
- 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;
1319
2012
  }
1320
- if (arrProp === "length") return arr.length;
1321
2013
  if (typeof arrProp === "string" && /^\d+$/.test(arrProp)) {
1322
- 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;
1323
2031
  }
1324
2032
  if (typeof arrProp === "symbol") {
2033
+ const arr = readArrayValue(mem, locator);
1325
2034
  if (arrProp === Symbol.iterator) return arr[Symbol.iterator].bind(arr);
1326
2035
  return (arr as unknown as Record<symbol, unknown>)[arrProp];
1327
2036
  }
@@ -1330,15 +2039,15 @@ function createArrayProxy(
1330
2039
  // push/pop are O(1) in-place operations on the array ref.
1331
2040
  if (methodName === "push" || methodName === "pop") {
1332
2041
  return (...args: unknown[]) => {
1333
- return mutateArrayRef(mem, key, prop, methodName, args);
2042
+ return mutateArrayRef(mem, locator, methodName, args);
1334
2043
  };
1335
2044
  }
1336
2045
  return (...args: unknown[]) => {
1337
- return mutateValue<Record<string, unknown>, unknown>(
2046
+ return mutateValue<unknown[], unknown>(
1338
2047
  mem,
1339
2048
  key,
1340
2049
  (fresh) => {
1341
- const freshArr = fresh[prop] as unknown[];
2050
+ const freshArr = readPathValue(mem, locator, fresh) as unknown[];
1342
2051
  const method = (
1343
2052
  freshArr as unknown as Record<
1344
2053
  string,
@@ -1351,136 +2060,25 @@ function createArrayProxy(
1351
2060
  );
1352
2061
  };
1353
2062
  }
2063
+ const arr = readArrayValue(mem, locator);
1354
2064
  const method = (arr as unknown as Record<string, unknown>)[arrProp];
1355
2065
  if (typeof method === "function") return method.bind(arr);
1356
2066
  return method;
1357
2067
  },
1358
2068
  set(_target, arrProp, val) {
1359
2069
  // In-place update: write only the affected array slot instead of
1360
- // decoding and re-encoding the whole object.
1361
- return writeLock(mem, () => {
1362
- // Refresh the cached slot offset when the layout version changed
1363
- // (defragmentation may have moved the object block).
1364
- if (cachedLayoutVersion !== mem.getLayoutVersion()) {
1365
- cachedSlotOffset = undefined;
1366
- cachedRef = undefined;
1367
- cachedLayoutVersion = mem.getLayoutVersion();
1368
- }
1369
- if (cachedSlotOffset === undefined) {
1370
- const layout = getObjectLayout(mem, key);
1371
- if (layout === null) {
1372
- // Fallback: full read-modify-write.
1373
- return mutateValue<Record<string, unknown>, boolean>(
1374
- mem,
1375
- key,
1376
- (fresh) => {
1377
- const arr = fresh[prop] as unknown[];
1378
- (arr as unknown as Record<string | symbol, unknown>)[arrProp] =
1379
- val;
1380
- return true;
1381
- },
1382
- arrayCapacity,
1383
- );
1384
- }
1385
- const so = layout.properties.get(prop);
1386
- if (so === undefined) {
1387
- throw new TypeError(
1388
- `Value for key "${key}" has a fixed shape: properties may not be added or removed`,
1389
- );
1390
- }
1391
- cachedSlotOffset = so;
1392
- }
1393
- const propSlot = mem.readValueSlot(cachedSlotOffset);
1394
- if (
1395
- propSlot.type === TYPE_ARRAY_REF &&
1396
- typeof arrProp === "string" &&
1397
- /^\d+$/.test(arrProp)
1398
- ) {
1399
- const index = Number(arrProp);
1400
- // Use the cached ref when valid; re-read when the array may have
1401
- // grown past the cached count (push) or the layout changed.
1402
- let ref = cachedRef;
1403
- if (
1404
- ref === undefined ||
1405
- index >= ref.count ||
1406
- cachedLayoutVersion !== mem.getLayoutVersion()
1407
- ) {
1408
- ref = {
1409
- offset: propSlot.a,
1410
- count: propSlot.b,
1411
- capacity: propSlot.c,
1412
- };
1413
- cachedRef = ref;
1414
- }
1415
- if (index >= ref.count) {
1416
- throw new RangeError(
1417
- `Index ${index} out of bounds for array of length ${ref.count}`,
1418
- );
1419
- }
1420
- if (typeof val === "number") {
1421
- // Fast path: numbers need no old-slot read (freeSlotRefs is a
1422
- // no-op) and no in-place string reuse.
1423
- mem.updateArrayNumberSlot(ref, index, val);
1424
- bump(mem, key);
1425
- return true;
1426
- }
1427
- if (typeof val === "boolean") {
1428
- // Fast path: booleans need no old-slot read (freeSlotRefs is a
1429
- // no-op).
1430
- mem.updateArrayBooleanSlot(ref, index, val);
1431
- bump(mem, key);
1432
- return true;
1433
- }
1434
- if (typeof val === "string") {
1435
- // Fast path: short strings are written inline into the slot.
1436
- if (mem.updateArrayInlineStringSlot(ref, index, val)) {
1437
- bump(mem, key);
1438
- return true;
1439
- }
1440
- }
1441
- freeSlotRefs(mem, mem.readArraySlot(ref, index));
1442
- mem.updateArraySlot(ref, index, encodeValue(mem, val));
1443
- bump(mem, key);
1444
- return true;
1445
- }
1446
- // Array property length write: shrink/grow in place without
1447
- // re-encoding the whole object (which would leave a dead slot).
1448
- if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
1449
- const newLen = Number(val);
1450
- if (!Number.isInteger(newLen) || newLen < 0) {
1451
- throw new RangeError("Invalid array length");
1452
- }
1453
- const ref = {
1454
- offset: propSlot.a,
1455
- count: propSlot.b,
1456
- capacity: propSlot.c,
1457
- };
1458
- if (newLen <= ref.capacity) {
1459
- mem.updateValueSlot(
1460
- cachedSlotOffset,
1461
- arrayRefSlot({
1462
- offset: ref.offset,
1463
- count: newLen,
1464
- capacity: ref.capacity,
1465
- }),
1466
- );
1467
- bump(mem, key);
1468
- return true;
1469
- }
1470
- // Growing beyond capacity falls through to the full read-modify-write.
1471
- }
1472
- // Fallback: full read-modify-write.
1473
- return mutateValue<Record<string, unknown>, boolean>(
1474
- mem,
1475
- key,
1476
- (fresh) => {
1477
- const arr = fresh[prop] as unknown[];
1478
- (arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
1479
- return true;
1480
- },
1481
- arrayCapacity,
1482
- );
1483
- });
2070
+ // decoding and re-encoding the whole value. Inline writeLock to avoid
2071
+ // a closure allocation on the hot path.
2072
+ mem.beginWrite();
2073
+ if (mem.isDefragmenting()) {
2074
+ mem.endWrite();
2075
+ return mem.withLock(() => writeArrayProp(arrProp, val));
2076
+ }
2077
+ try {
2078
+ return writeArrayProp(arrProp, val);
2079
+ } finally {
2080
+ mem.endWrite();
2081
+ }
1484
2082
  },
1485
2083
  });
1486
2084
  }