bun-shared-memory 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +310 -2
- package/index.ts +1 -0
- package/package.json +47 -4
- package/src/growable-buffer.ts +110 -0
- package/src/index.ts +21 -0
- package/src/memory.ts +1016 -0
- package/src/mutex.ts +92 -0
- package/src/notifier.ts +39 -0
- package/src/refcount.ts +358 -0
- package/src/shared.ts +957 -0
- package/src/worker.ts +292 -0
package/src/shared.ts
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SharedMemory,
|
|
3
|
+
TYPE_ARRAY_REF,
|
|
4
|
+
TYPE_BIGINT_STR,
|
|
5
|
+
arrayRefSlot,
|
|
6
|
+
bigintSlot,
|
|
7
|
+
bigintStrSlot,
|
|
8
|
+
booleanSlot,
|
|
9
|
+
decodeSlot,
|
|
10
|
+
numberSlot,
|
|
11
|
+
stringRefSlot,
|
|
12
|
+
type ArrayRef,
|
|
13
|
+
type ObjectLayout,
|
|
14
|
+
type StringRef,
|
|
15
|
+
type ValueSlot,
|
|
16
|
+
} from "./memory.ts";
|
|
17
|
+
import type { RefCountTracker } from "./refcount.ts";
|
|
18
|
+
import { getDefaultMemory, getDefaultTracker } from "./worker.ts";
|
|
19
|
+
|
|
20
|
+
/** Array methods that mutate the array in place. */
|
|
21
|
+
const MUTATING_METHODS = new Set([
|
|
22
|
+
"push",
|
|
23
|
+
"pop",
|
|
24
|
+
"shift",
|
|
25
|
+
"unshift",
|
|
26
|
+
"splice",
|
|
27
|
+
"sort",
|
|
28
|
+
"reverse",
|
|
29
|
+
"fill",
|
|
30
|
+
"copyWithin",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
type Primitive = number | string | boolean | bigint;
|
|
34
|
+
|
|
35
|
+
/** Inclusive bounds of a signed 64-bit integer (stored as a fast int64 slot). */
|
|
36
|
+
const MIN_INT64 = -(2n ** 63n);
|
|
37
|
+
const MAX_INT64 = 2n ** 63n - 1n;
|
|
38
|
+
|
|
39
|
+
/** Convert a JS value into a ValueSlot, storing strings/arrays in tables. */
|
|
40
|
+
function encodeValue(
|
|
41
|
+
mem: SharedMemory,
|
|
42
|
+
value: unknown,
|
|
43
|
+
arrayCapacity?: number,
|
|
44
|
+
): ValueSlot {
|
|
45
|
+
switch (typeof value) {
|
|
46
|
+
case "number":
|
|
47
|
+
return numberSlot(value);
|
|
48
|
+
case "boolean":
|
|
49
|
+
return booleanSlot(value);
|
|
50
|
+
case "bigint":
|
|
51
|
+
// Bigints that fit in 64 bits are stored as a fast int64 slot; larger
|
|
52
|
+
// ones are stored as their decimal string in the StringTable.
|
|
53
|
+
if (value >= MIN_INT64 && value <= MAX_INT64) {
|
|
54
|
+
return bigintSlot(value);
|
|
55
|
+
}
|
|
56
|
+
return bigintStrSlot(mem.storeString(value.toString()));
|
|
57
|
+
case "string":
|
|
58
|
+
return stringRefSlot(mem.storeString(value));
|
|
59
|
+
default:
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
for (const v of value) {
|
|
62
|
+
if (Array.isArray(v) || (v !== null && typeof v === "object")) {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
"Arrays may only contain primitives and bigints (no nested arrays or objects)",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const slots = value.map((v) => encodeValue(mem, v, arrayCapacity));
|
|
69
|
+
return arrayRefSlot(mem.storeArray(slots, arrayCapacity));
|
|
70
|
+
}
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
`Cannot store value of type ${typeof value}: only primitives, bigints, and arrays of these are allowed`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Decode a ValueSlot into a JS value, reading strings/arrays from tables. */
|
|
78
|
+
function decodeValue(mem: SharedMemory, slot: ValueSlot): unknown {
|
|
79
|
+
switch (slot.type) {
|
|
80
|
+
case 3: // string-ref
|
|
81
|
+
return mem.readString({ offset: slot.a, length: slot.b });
|
|
82
|
+
case TYPE_BIGINT_STR: // bigint stored as decimal string
|
|
83
|
+
return BigInt(mem.readString({ offset: slot.a, length: slot.b }));
|
|
84
|
+
case 4: // array-ref
|
|
85
|
+
return mem
|
|
86
|
+
.readArray({ offset: slot.a, count: slot.b, capacity: slot.c })
|
|
87
|
+
.map((s) => decodeValue(mem, s));
|
|
88
|
+
default:
|
|
89
|
+
return decodeSlot(slot);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A reference to a value stored in shared memory.
|
|
95
|
+
*
|
|
96
|
+
* Exposes direct property access to the shared value. For objects, properties
|
|
97
|
+
* are accessed directly (e.g. `user.name`). For arrays, index access and
|
|
98
|
+
* mutating methods (push, pop, splice, ...) work directly. All access is
|
|
99
|
+
* synchronized with an Atomics-based lock.
|
|
100
|
+
*/
|
|
101
|
+
export type SharedRef<T> = [T] extends [object]
|
|
102
|
+
? T & {
|
|
103
|
+
read(): T;
|
|
104
|
+
write(value: T): void;
|
|
105
|
+
update(fn: (current: T) => T): T;
|
|
106
|
+
readonly key: string;
|
|
107
|
+
readonly buffer: SharedArrayBuffer;
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
read(): T;
|
|
111
|
+
write(value: T): void;
|
|
112
|
+
update(fn: (current: T) => T): T;
|
|
113
|
+
readonly key: string;
|
|
114
|
+
readonly buffer: SharedArrayBuffer;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Per-worker read cache: key -> { version, value, index }.
|
|
119
|
+
*
|
|
120
|
+
* Each worker has its own cache (module-level, not shared). A read hits the
|
|
121
|
+
* cache when the key's version cell (in shared memory) matches the cached
|
|
122
|
+
* version. Writes bump the version cell, so any worker's cache is invalidated
|
|
123
|
+
* automatically on the next read. The key's stable index is cached too, so a
|
|
124
|
+
* cache hit avoids the linear index-table lookup.
|
|
125
|
+
*/
|
|
126
|
+
const readCache = new Map<
|
|
127
|
+
string,
|
|
128
|
+
{ version: number; value: unknown; index: number }
|
|
129
|
+
>();
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Per-worker cache of a key's stable index in the Index Table.
|
|
133
|
+
*
|
|
134
|
+
* Keys are never removed, so a key's index is stable for its lifetime. Once
|
|
135
|
+
* resolved, it can be cached forever, avoiding the linear index-table lookup
|
|
136
|
+
* (which decodes every stored key string) on every access.
|
|
137
|
+
*/
|
|
138
|
+
const indexCache = new Map<string, number>();
|
|
139
|
+
|
|
140
|
+
/** Resolve a key's stable index, caching it after the first lookup. */
|
|
141
|
+
function getIndex(mem: SharedMemory, key: string): number {
|
|
142
|
+
const cached = indexCache.get(key);
|
|
143
|
+
if (cached !== undefined) return cached;
|
|
144
|
+
const index = mem.lookupIndex(key);
|
|
145
|
+
if (index === null) {
|
|
146
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
147
|
+
}
|
|
148
|
+
indexCache.set(key, index);
|
|
149
|
+
return index;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a key's current DataTable offset using the cached stable index.
|
|
154
|
+
* The offset can change on GC/defragmentation, so it is re-read from the
|
|
155
|
+
* Index Table each time (a single Int32 read), but the expensive linear
|
|
156
|
+
* key-string scan is avoided. Returns null if the key is not present.
|
|
157
|
+
*/
|
|
158
|
+
function getOffset(mem: SharedMemory, key: string): number | null {
|
|
159
|
+
const cached = indexCache.get(key);
|
|
160
|
+
if (cached !== undefined) {
|
|
161
|
+
return mem.lookupByIndex(cached);
|
|
162
|
+
}
|
|
163
|
+
const index = mem.lookupIndex(key);
|
|
164
|
+
if (index === null) return null;
|
|
165
|
+
indexCache.set(key, index);
|
|
166
|
+
return mem.lookupByIndex(index);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Whether a decoded value is a reference-countable object/array. */
|
|
170
|
+
function isTrackable(value: unknown): boolean {
|
|
171
|
+
return value !== null && typeof value === "object";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the stable objectId of a key's top-level value, or null if it is a
|
|
176
|
+
* primitive (which is stored inline and not reference-counted).
|
|
177
|
+
*/
|
|
178
|
+
function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
|
|
179
|
+
const offset = getOffset(mem, key);
|
|
180
|
+
if (offset === null) return null;
|
|
181
|
+
const type = mem.dataTable.getView().getInt32(offset, true);
|
|
182
|
+
if (type === 5) {
|
|
183
|
+
return mem.getObjectId(offset);
|
|
184
|
+
}
|
|
185
|
+
const slot = mem.readValueSlot(offset);
|
|
186
|
+
if (slot.type === TYPE_ARRAY_REF) {
|
|
187
|
+
return mem.getArrayObjectId(slot.a);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Per-worker cache of decoded object properties: key -> { version, props }.
|
|
194
|
+
*
|
|
195
|
+
* When a property of an object is read via the layout (single-slot decode),
|
|
196
|
+
* the decoded value is cached here keyed by the object's version. A hit avoids
|
|
197
|
+
* re-decoding the property (e.g. re-reading a string from the StringTable) on
|
|
198
|
+
* repeated reads of the same property. Writes bump the object's version, so
|
|
199
|
+
* the whole entry is invalidated automatically.
|
|
200
|
+
*/
|
|
201
|
+
const propCache = new Map<
|
|
202
|
+
string,
|
|
203
|
+
{ version: number; index: number; props: Map<string, unknown> }
|
|
204
|
+
>();
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Per-worker cache of object layouts: key -> stable property slot offsets.
|
|
208
|
+
*
|
|
209
|
+
* Because object properties may not be added or removed, the layout is static
|
|
210
|
+
* for the lifetime of the object. It is built once (under the lock) and cached
|
|
211
|
+
* so individual property reads can decode only the requested slot instead of
|
|
212
|
+
* the whole object. If a future GC/defragmentation moves an object block, this
|
|
213
|
+
* cache must be invalidated (e.g. via a layout version cell).
|
|
214
|
+
*/
|
|
215
|
+
const layoutCache = new Map<string, ObjectLayout | null>();
|
|
216
|
+
|
|
217
|
+
/** Get (and cache) the stable layout of an object value, or null if not an object. */
|
|
218
|
+
function getObjectLayout(mem: SharedMemory, key: string): ObjectLayout | null {
|
|
219
|
+
if (layoutCache.has(key)) return layoutCache.get(key)!;
|
|
220
|
+
const layout = mem.withLock(() => mem.getObjectLayout(key));
|
|
221
|
+
layoutCache.set(key, layout);
|
|
222
|
+
return layout;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Deep-ish copy of a decoded value, so callers can't corrupt the cache. */
|
|
226
|
+
function copyValue(value: unknown): unknown {
|
|
227
|
+
if (Array.isArray(value)) return [...value];
|
|
228
|
+
if (value !== null && typeof value === "object") {
|
|
229
|
+
const copy: Record<string, unknown> = {};
|
|
230
|
+
for (const [k, v] of Object.entries(value)) {
|
|
231
|
+
copy[k] = Array.isArray(v) ? [...v] : v;
|
|
232
|
+
}
|
|
233
|
+
return copy;
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Options for creating a shared value. */
|
|
239
|
+
export interface SharedOptions {
|
|
240
|
+
/** Initial size of each shared buffer in bytes. */
|
|
241
|
+
initialSize?: number;
|
|
242
|
+
/** Maximum size of each shared buffer in bytes. */
|
|
243
|
+
maxByteLength?: number;
|
|
244
|
+
/**
|
|
245
|
+
* An existing SharedMemory instance to use. When provided, the value is
|
|
246
|
+
* stored in (and read from) this shared memory, allowing multiple workers
|
|
247
|
+
* to access the same buffers.
|
|
248
|
+
*/
|
|
249
|
+
memory?: SharedMemory;
|
|
250
|
+
/**
|
|
251
|
+
* A worker-side reference-count tracker. When provided, `read()` acquires a
|
|
252
|
+
* reference to the returned object/array, released when it is GC'd.
|
|
253
|
+
*/
|
|
254
|
+
refCount?: RefCountTracker;
|
|
255
|
+
/**
|
|
256
|
+
* Initial capacity (in elements) reserved for arrays when they are first
|
|
257
|
+
* stored. Arrays are created with this many slots, so subsequent `push`
|
|
258
|
+
* calls can append in place without growing/moving the array until the
|
|
259
|
+
* capacity is exhausted. Defaults to the array's length.
|
|
260
|
+
*/
|
|
261
|
+
arrayCapacity?: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type Loader<T> = () => T | Promise<T>;
|
|
265
|
+
|
|
266
|
+
const registry = new Map<string, SharedRef<unknown>>();
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get a wrapper to a named value in shared memory.
|
|
270
|
+
*
|
|
271
|
+
* If the value does not exist yet, it is created using the provided initial
|
|
272
|
+
* value or loader function (which may be async). If it already exists, the
|
|
273
|
+
* existing wrapper is returned and the initial value/loader is ignored.
|
|
274
|
+
*
|
|
275
|
+
* Returns a Promise when a loader is provided (since it may be async), and a
|
|
276
|
+
* SharedRef directly when an initial value is provided.
|
|
277
|
+
*/
|
|
278
|
+
export function Shared<T>(
|
|
279
|
+
key: string,
|
|
280
|
+
loader: Loader<T>,
|
|
281
|
+
options?: SharedOptions,
|
|
282
|
+
): Promise<SharedRef<T>>;
|
|
283
|
+
export function Shared<T>(
|
|
284
|
+
key: string,
|
|
285
|
+
initial: T,
|
|
286
|
+
options?: SharedOptions,
|
|
287
|
+
): SharedRef<T>;
|
|
288
|
+
export function Shared<T>(
|
|
289
|
+
key: string,
|
|
290
|
+
initialOrLoader: T | Loader<T>,
|
|
291
|
+
options: SharedOptions = {},
|
|
292
|
+
): SharedRef<T> | Promise<SharedRef<T>> {
|
|
293
|
+
const existing = registry.get(key);
|
|
294
|
+
if (existing) return existing as SharedRef<T>;
|
|
295
|
+
|
|
296
|
+
const mem = options.memory ?? getDefaultMemory();
|
|
297
|
+
const ref = createSharedRef<T>(
|
|
298
|
+
mem,
|
|
299
|
+
key,
|
|
300
|
+
options.refCount ?? getDefaultTracker() ?? undefined,
|
|
301
|
+
options.arrayCapacity,
|
|
302
|
+
);
|
|
303
|
+
registry.set(key, ref as SharedRef<unknown>);
|
|
304
|
+
|
|
305
|
+
if (typeof initialOrLoader === "function") {
|
|
306
|
+
return Promise.resolve((initialOrLoader as Loader<T>)()).then((value) => {
|
|
307
|
+
ref.write(value);
|
|
308
|
+
return ref;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// If the key already exists in shared memory (e.g. it was created by the
|
|
313
|
+
// main thread and this is a worker sharing the same buffers), don't
|
|
314
|
+
// overwrite it with the initial value.
|
|
315
|
+
if (getOffset(mem, key) === null) {
|
|
316
|
+
ref.write(initialOrLoader as T);
|
|
317
|
+
}
|
|
318
|
+
return ref;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Create a SharedRef proxy backed by a SharedMemory instance. */
|
|
322
|
+
function createSharedRef<T>(
|
|
323
|
+
mem: SharedMemory,
|
|
324
|
+
key: string,
|
|
325
|
+
tracker?: RefCountTracker,
|
|
326
|
+
arrayCapacity?: number,
|
|
327
|
+
): SharedRef<T> {
|
|
328
|
+
const target = {
|
|
329
|
+
key,
|
|
330
|
+
mem,
|
|
331
|
+
// Fast-path cache: mirrors the module-level readCache entry for this key
|
|
332
|
+
// so the proxy get handler can read it without a Map lookup. Invalidated
|
|
333
|
+
// on every local write/update; cross-worker invalidation is detected by
|
|
334
|
+
// the version check in the get handler.
|
|
335
|
+
__cache: undefined as
|
|
336
|
+
{ version: number; value: unknown; index: number } | undefined,
|
|
337
|
+
read: () => {
|
|
338
|
+
const value = copyValue(readValue(mem, key, target)) as T;
|
|
339
|
+
if (tracker && isTrackable(value)) {
|
|
340
|
+
const objectId = getTopLevelObjectId(mem, key);
|
|
341
|
+
if (objectId !== null) tracker.track(value as object, objectId);
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
},
|
|
345
|
+
write: (value: T) => {
|
|
346
|
+
target.__cache = undefined;
|
|
347
|
+
writeValue(mem, key, value, arrayCapacity);
|
|
348
|
+
},
|
|
349
|
+
update: (fn: (current: T) => T) => {
|
|
350
|
+
target.__cache = undefined;
|
|
351
|
+
return updateValue(mem, key, fn, arrayCapacity);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return new Proxy(target, {
|
|
356
|
+
get(t, prop) {
|
|
357
|
+
if (prop === "read") return t.read;
|
|
358
|
+
if (prop === "write") return t.write;
|
|
359
|
+
if (prop === "update") return t.update;
|
|
360
|
+
if (prop === "key") return t.key;
|
|
361
|
+
if (prop === "buffer") return mem.dataTable.buffer;
|
|
362
|
+
if (prop === Symbol.toPrimitive) return () => t.read();
|
|
363
|
+
|
|
364
|
+
// push/pop on a top-level array: operate directly on the array ref
|
|
365
|
+
// without decoding the whole array (O(1) amortized).
|
|
366
|
+
if (prop === "push" || prop === "pop") {
|
|
367
|
+
const slotOffset = findArrayRefSlot(mem, key, null);
|
|
368
|
+
if (slotOffset !== null) {
|
|
369
|
+
return (...args: unknown[]) =>
|
|
370
|
+
mutateArrayRef(mem, key, null, prop, args);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Fast path: if the full value is cached and valid, read the property
|
|
375
|
+
// from the cached value (no decode, no lock). The cache entry is kept
|
|
376
|
+
// directly on the target to avoid a Map lookup on the hot path.
|
|
377
|
+
const cached = t.__cache;
|
|
378
|
+
if (
|
|
379
|
+
cached !== undefined &&
|
|
380
|
+
cached.version === mem.getVersion(cached.index)
|
|
381
|
+
) {
|
|
382
|
+
const cachedValue = cached.value;
|
|
383
|
+
if (Array.isArray(cachedValue)) {
|
|
384
|
+
return getTopLevelArrayMember(
|
|
385
|
+
mem,
|
|
386
|
+
key,
|
|
387
|
+
cachedValue,
|
|
388
|
+
prop,
|
|
389
|
+
arrayCapacity,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const propValue = (cachedValue as Record<string | symbol, unknown>)[
|
|
393
|
+
prop
|
|
394
|
+
];
|
|
395
|
+
if (Array.isArray(propValue)) {
|
|
396
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
397
|
+
}
|
|
398
|
+
return propValue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Cache miss: for objects, decode only the requested property's slot via
|
|
402
|
+
// the cached layout, avoiding a full-object decode. The decoded value is
|
|
403
|
+
// cached per property so repeated reads of the same property are cheap.
|
|
404
|
+
const layout = getObjectLayout(mem, key);
|
|
405
|
+
if (layout !== null) {
|
|
406
|
+
const slotOffset = layout.properties.get(prop as string);
|
|
407
|
+
if (slotOffset === undefined) return undefined;
|
|
408
|
+
|
|
409
|
+
// Property cache hit: the object's version is unchanged.
|
|
410
|
+
const pc = propCache.get(key);
|
|
411
|
+
const version = pc !== undefined ? mem.getVersion(pc.index) : -1;
|
|
412
|
+
if (pc !== undefined && pc.version === version) {
|
|
413
|
+
const cachedProp = pc.props.get(prop as string);
|
|
414
|
+
if (cachedProp !== undefined) {
|
|
415
|
+
if (Array.isArray(cachedProp)) {
|
|
416
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
417
|
+
}
|
|
418
|
+
return cachedProp;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Decode the single slot and cache it under the object's version.
|
|
423
|
+
// The key's index is stable (keys are never removed), so reuse the
|
|
424
|
+
// cached index when available to avoid the linear index-table lookup.
|
|
425
|
+
const index =
|
|
426
|
+
pc !== undefined
|
|
427
|
+
? pc.index
|
|
428
|
+
: (readCache.get(key)?.index ?? getIndex(mem, key));
|
|
429
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
430
|
+
// Array properties are handled lazily by the array proxy; don't decode
|
|
431
|
+
// the whole array here (avoids O(n) work on every access).
|
|
432
|
+
if (slot.type === TYPE_ARRAY_REF) {
|
|
433
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
434
|
+
}
|
|
435
|
+
const propValue = decodeValue(mem, slot);
|
|
436
|
+
if (pc === undefined || pc.version !== version) {
|
|
437
|
+
propCache.set(key, {
|
|
438
|
+
version: mem.getVersion(index),
|
|
439
|
+
index,
|
|
440
|
+
props: new Map(),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
propCache.get(key)!.props.set(prop as string, propValue);
|
|
444
|
+
if (Array.isArray(propValue)) {
|
|
445
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
446
|
+
}
|
|
447
|
+
return propValue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Primitive/array key: use the cached value directly (no copy).
|
|
451
|
+
const value = readValue(mem, key, t);
|
|
452
|
+
if (Array.isArray(value)) {
|
|
453
|
+
return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
|
|
454
|
+
}
|
|
455
|
+
// Object value: property access.
|
|
456
|
+
const propValue = (value as Record<string | symbol, unknown>)[prop];
|
|
457
|
+
if (Array.isArray(propValue)) {
|
|
458
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
459
|
+
}
|
|
460
|
+
return propValue;
|
|
461
|
+
},
|
|
462
|
+
set(t, prop, val) {
|
|
463
|
+
// In-place update: write only the affected slot instead of decoding and
|
|
464
|
+
// re-encoding the whole value.
|
|
465
|
+
t.__cache = undefined;
|
|
466
|
+
return mem.withLock(() => {
|
|
467
|
+
// Object property: use the cached layout to write only that slot.
|
|
468
|
+
const layout = getObjectLayout(mem, key);
|
|
469
|
+
if (layout !== null) {
|
|
470
|
+
const slotOffset = layout.properties.get(prop as string);
|
|
471
|
+
if (slotOffset === undefined) {
|
|
472
|
+
throw new TypeError(
|
|
473
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
mem.updateValueSlot(slotOffset, encodeValue(mem, val));
|
|
477
|
+
bump(mem, key);
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Top-level array index write: update only that array slot.
|
|
482
|
+
const offset = getOffset(mem, key);
|
|
483
|
+
if (offset === null) {
|
|
484
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
485
|
+
}
|
|
486
|
+
const slot = mem.readValueSlot(offset);
|
|
487
|
+
if (
|
|
488
|
+
slot.type === TYPE_ARRAY_REF &&
|
|
489
|
+
typeof prop === "string" &&
|
|
490
|
+
/^\d+$/.test(prop)
|
|
491
|
+
) {
|
|
492
|
+
const index = Number(prop);
|
|
493
|
+
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
494
|
+
if (index >= ref.count) {
|
|
495
|
+
throw new RangeError(
|
|
496
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
500
|
+
bump(mem, key);
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Top-level array length write: shrink/grow the array in place without
|
|
505
|
+
// re-encoding the whole array (which would leave a dead slot behind).
|
|
506
|
+
if (slot.type === TYPE_ARRAY_REF && prop === "length") {
|
|
507
|
+
const newLen = Number(val);
|
|
508
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
509
|
+
throw new RangeError("Invalid array length");
|
|
510
|
+
}
|
|
511
|
+
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
512
|
+
if (newLen <= ref.capacity) {
|
|
513
|
+
// Shrinking (or growing within capacity): just update the count.
|
|
514
|
+
mem.updateValueSlot(
|
|
515
|
+
offset,
|
|
516
|
+
arrayRefSlot({
|
|
517
|
+
offset: ref.offset,
|
|
518
|
+
count: newLen,
|
|
519
|
+
capacity: ref.capacity,
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
bump(mem, key);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
// Growing beyond capacity falls through to the full read-modify-write.
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Fallback: full read-modify-write (e.g. setting a primitive key's
|
|
529
|
+
// property, or a non-index property on an array).
|
|
530
|
+
return mutateValue<T, boolean>(
|
|
531
|
+
mem,
|
|
532
|
+
key,
|
|
533
|
+
(value) => {
|
|
534
|
+
if (Array.isArray(value)) {
|
|
535
|
+
(value as unknown as Record<string | symbol, unknown>)[prop] =
|
|
536
|
+
val;
|
|
537
|
+
} else {
|
|
538
|
+
(value as Record<string | symbol, unknown>)[prop] = val;
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
},
|
|
542
|
+
arrayCapacity,
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
deleteProperty() {
|
|
547
|
+
throw new TypeError(
|
|
548
|
+
"Shared values have a fixed shape: properties may not be added or removed",
|
|
549
|
+
);
|
|
550
|
+
},
|
|
551
|
+
}) as unknown as SharedRef<T>;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Read the full value for a key from shared memory, using the read cache.
|
|
556
|
+
*
|
|
557
|
+
* When `cacheTarget` is provided (the proxy target), the cache entry is also
|
|
558
|
+
* mirrored onto `cacheTarget.__cache` so the proxy get handler can read it
|
|
559
|
+
* without a Map lookup on the hot path.
|
|
560
|
+
*/
|
|
561
|
+
function readValue(
|
|
562
|
+
mem: SharedMemory,
|
|
563
|
+
key: string,
|
|
564
|
+
cacheTarget?: {
|
|
565
|
+
__cache?: { version: number; value: unknown; index: number };
|
|
566
|
+
},
|
|
567
|
+
): unknown {
|
|
568
|
+
const cached = readCache.get(key);
|
|
569
|
+
const index = cached !== undefined ? cached.index : getIndex(mem, key);
|
|
570
|
+
|
|
571
|
+
// Lock-free fast path: if the cached version matches the current version
|
|
572
|
+
// cell, the cached value is still valid.
|
|
573
|
+
const version = mem.getVersion(index);
|
|
574
|
+
if (cached !== undefined && cached.version === version) {
|
|
575
|
+
return cached.value;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Cache miss: decode under the lock and cache with the observed version.
|
|
579
|
+
const value = mem.withLock(() => {
|
|
580
|
+
const offset = getOffset(mem, key);
|
|
581
|
+
if (offset === null) {
|
|
582
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
583
|
+
}
|
|
584
|
+
const type = mem.dataTable.getView().getInt32(offset, true);
|
|
585
|
+
if (type === 5) {
|
|
586
|
+
// Object block.
|
|
587
|
+
const properties = mem.readObject(offset);
|
|
588
|
+
const obj: Record<string, unknown> = {};
|
|
589
|
+
for (const [name, slot] of properties) {
|
|
590
|
+
obj[name] = decodeValue(mem, slot);
|
|
591
|
+
}
|
|
592
|
+
return obj;
|
|
593
|
+
}
|
|
594
|
+
// Single value slot (primitive or array).
|
|
595
|
+
return decodeValue(mem, mem.readValueSlot(offset));
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const entry = { version, value, index };
|
|
599
|
+
readCache.set(key, entry);
|
|
600
|
+
if (cacheTarget) cacheTarget.__cache = entry;
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Write a full value for a key to shared memory. */
|
|
605
|
+
function writeValue(
|
|
606
|
+
mem: SharedMemory,
|
|
607
|
+
key: string,
|
|
608
|
+
value: unknown,
|
|
609
|
+
arrayCapacity?: number,
|
|
610
|
+
): void {
|
|
611
|
+
mem.withLock(() => {
|
|
612
|
+
const existing = getOffset(mem, key);
|
|
613
|
+
if (existing !== null) {
|
|
614
|
+
const type = mem.dataTable.getView().getInt32(existing, true);
|
|
615
|
+
if (type === 5) {
|
|
616
|
+
// Update existing object's property slots in place.
|
|
617
|
+
const properties = mem.readObject(existing);
|
|
618
|
+
const valueObj = value as Record<string, unknown>;
|
|
619
|
+
for (let i = 0; i < properties.length; i++) {
|
|
620
|
+
const [name] = properties[i]!;
|
|
621
|
+
if (!(name in valueObj)) {
|
|
622
|
+
throw new TypeError(
|
|
623
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
mem.updateObjectSlot(
|
|
627
|
+
existing,
|
|
628
|
+
i,
|
|
629
|
+
encodeValue(mem, valueObj[name], arrayCapacity),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
// Check for extra properties.
|
|
633
|
+
const existingNames = new Set(properties.map(([n]) => n));
|
|
634
|
+
for (const name of Object.keys(valueObj)) {
|
|
635
|
+
if (!existingNames.has(name)) {
|
|
636
|
+
throw new TypeError(
|
|
637
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
bump(mem, key);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
// Update a single value slot in place.
|
|
645
|
+
mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
|
|
646
|
+
bump(mem, key);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
651
|
+
// Create a new object.
|
|
652
|
+
const valueObj = value as Record<string, unknown>;
|
|
653
|
+
const props: Array<[string, ValueSlot]> = Object.entries(valueObj).map(
|
|
654
|
+
([name, v]) => [name, encodeValue(mem, v, arrayCapacity)],
|
|
655
|
+
);
|
|
656
|
+
const offset = mem.createObject(props);
|
|
657
|
+
mem.insert(key, offset);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Create a single value slot (primitive or array).
|
|
662
|
+
const offset = mem.storeValueSlot(encodeValue(mem, value, arrayCapacity));
|
|
663
|
+
mem.insert(key, offset);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Bump the version cell for a key, invalidating read caches. */
|
|
668
|
+
function bump(mem: SharedMemory, key: string): void {
|
|
669
|
+
mem.bumpVersion(getIndex(mem, key));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Atomically update the value for a key. */
|
|
673
|
+
function updateValue<T>(
|
|
674
|
+
mem: SharedMemory,
|
|
675
|
+
key: string,
|
|
676
|
+
fn: (current: T) => T,
|
|
677
|
+
arrayCapacity?: number,
|
|
678
|
+
): T {
|
|
679
|
+
return mem.withLock(() => {
|
|
680
|
+
const current = readValue(mem, key) as T;
|
|
681
|
+
const next = fn(current);
|
|
682
|
+
writeValue(mem, key, next, arrayCapacity);
|
|
683
|
+
return next;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Atomically read-modify-write a value under a single lock acquisition.
|
|
689
|
+
* Unlike `updateValue`, the transform may return a different value than the
|
|
690
|
+
* one written (e.g. array methods return the popped element).
|
|
691
|
+
*/
|
|
692
|
+
function mutateValue<T, R>(
|
|
693
|
+
mem: SharedMemory,
|
|
694
|
+
key: string,
|
|
695
|
+
fn: (current: T) => R,
|
|
696
|
+
arrayCapacity?: number,
|
|
697
|
+
): R {
|
|
698
|
+
return mem.withLock(() => {
|
|
699
|
+
const current = readValue(mem, key) as T;
|
|
700
|
+
const result = fn(current);
|
|
701
|
+
writeValue(mem, key, current, arrayCapacity);
|
|
702
|
+
return result;
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Find the DataTable offset of the array-ref slot for a key's array.
|
|
708
|
+
*
|
|
709
|
+
* `prop` is null for a top-level array value, or the property name for an
|
|
710
|
+
* array property of an object. Returns null if the value is not an array.
|
|
711
|
+
*/
|
|
712
|
+
function findArrayRefSlot(
|
|
713
|
+
mem: SharedMemory,
|
|
714
|
+
key: string,
|
|
715
|
+
prop: string | null,
|
|
716
|
+
): number | null {
|
|
717
|
+
if (prop === null) {
|
|
718
|
+
const offset = getOffset(mem, key);
|
|
719
|
+
if (offset === null) return null;
|
|
720
|
+
const slot = mem.readValueSlot(offset);
|
|
721
|
+
return slot.type === TYPE_ARRAY_REF ? offset : null;
|
|
722
|
+
}
|
|
723
|
+
const layout = getObjectLayout(mem, key);
|
|
724
|
+
if (layout === null) return null;
|
|
725
|
+
const slotOffset = layout.properties.get(prop);
|
|
726
|
+
if (slotOffset === undefined) return null;
|
|
727
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
728
|
+
return slot.type === TYPE_ARRAY_REF ? slotOffset : null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* O(1) in-place push/pop on a shared array ref, without decoding or
|
|
733
|
+
* re-encoding the whole array. `prop` is null for a top-level array, or the
|
|
734
|
+
* property name for an array property of an object.
|
|
735
|
+
*/
|
|
736
|
+
function mutateArrayRef(
|
|
737
|
+
mem: SharedMemory,
|
|
738
|
+
key: string,
|
|
739
|
+
prop: string | null,
|
|
740
|
+
method: "push" | "pop",
|
|
741
|
+
args: unknown[],
|
|
742
|
+
): unknown {
|
|
743
|
+
return mem.withLock(() => {
|
|
744
|
+
const slotOffset = findArrayRefSlot(mem, key, prop);
|
|
745
|
+
if (slotOffset === null) {
|
|
746
|
+
throw new TypeError("Value is not an array");
|
|
747
|
+
}
|
|
748
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
749
|
+
const ref: ArrayRef = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
750
|
+
|
|
751
|
+
if (method === "push") {
|
|
752
|
+
for (const arg of args) {
|
|
753
|
+
const next = mem.appendArraySlot(ref, encodeValue(mem, arg));
|
|
754
|
+
// Always persist the new count (and offset/capacity if moved).
|
|
755
|
+
mem.updateValueSlot(slotOffset, arrayRefSlot(next));
|
|
756
|
+
ref.offset = next.offset;
|
|
757
|
+
ref.count = next.count;
|
|
758
|
+
ref.capacity = next.capacity;
|
|
759
|
+
}
|
|
760
|
+
bump(mem, key);
|
|
761
|
+
return ref.count;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// pop
|
|
765
|
+
if (ref.count === 0) {
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
// Read the last element before decrementing.
|
|
769
|
+
const lastSlot = mem.readArraySlot(ref, ref.count - 1);
|
|
770
|
+
const result = decodeValue(mem, lastSlot);
|
|
771
|
+
const next = {
|
|
772
|
+
offset: ref.offset,
|
|
773
|
+
count: ref.count - 1,
|
|
774
|
+
capacity: ref.capacity,
|
|
775
|
+
};
|
|
776
|
+
mem.updateValueSlot(slotOffset, arrayRefSlot(next));
|
|
777
|
+
bump(mem, key);
|
|
778
|
+
return result;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Get a member of a top-level array value, with read-modify-write for mutating methods. */
|
|
783
|
+
function getTopLevelArrayMember(
|
|
784
|
+
mem: SharedMemory,
|
|
785
|
+
key: string,
|
|
786
|
+
value: unknown[],
|
|
787
|
+
prop: string | symbol,
|
|
788
|
+
arrayCapacity?: number,
|
|
789
|
+
): unknown {
|
|
790
|
+
if (prop === "length") return value.length;
|
|
791
|
+
if (typeof prop === "string" && /^\d+$/.test(prop)) {
|
|
792
|
+
return value[Number(prop)];
|
|
793
|
+
}
|
|
794
|
+
if (typeof prop === "symbol") {
|
|
795
|
+
if (prop === Symbol.iterator) return value[Symbol.iterator].bind(value);
|
|
796
|
+
return (value as unknown as Record<symbol, unknown>)[prop];
|
|
797
|
+
}
|
|
798
|
+
if (MUTATING_METHODS.has(prop)) {
|
|
799
|
+
const methodName = prop;
|
|
800
|
+
// push/pop are O(1) in-place operations on the array ref.
|
|
801
|
+
if (methodName === "push" || methodName === "pop") {
|
|
802
|
+
return (...args: unknown[]) => {
|
|
803
|
+
return mutateArrayRef(mem, key, null, methodName, args);
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return (...args: unknown[]) => {
|
|
807
|
+
return mutateValue<unknown[], unknown>(
|
|
808
|
+
mem,
|
|
809
|
+
key,
|
|
810
|
+
(fresh) => {
|
|
811
|
+
const method = (
|
|
812
|
+
fresh as unknown as Record<string, (...a: unknown[]) => unknown>
|
|
813
|
+
)[methodName]!;
|
|
814
|
+
return method.apply(fresh, args);
|
|
815
|
+
},
|
|
816
|
+
arrayCapacity,
|
|
817
|
+
);
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const method = (value as unknown as Record<string, unknown>)[prop];
|
|
821
|
+
if (typeof method === "function") return method.bind(value);
|
|
822
|
+
return method;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Create a proxy for an array property of a shared object. */
|
|
826
|
+
function createArrayProxy(
|
|
827
|
+
mem: SharedMemory,
|
|
828
|
+
key: string,
|
|
829
|
+
prop: string,
|
|
830
|
+
arrayCapacity?: number,
|
|
831
|
+
): unknown[] {
|
|
832
|
+
return new Proxy([] as unknown[], {
|
|
833
|
+
get(_target, arrProp) {
|
|
834
|
+
// push/pop on an array property: operate directly on the array ref
|
|
835
|
+
// without decoding the whole object (O(1) amortized).
|
|
836
|
+
if (arrProp === "push" || arrProp === "pop") {
|
|
837
|
+
const slotOffset = findArrayRefSlot(mem, key, prop);
|
|
838
|
+
if (slotOffset !== null) {
|
|
839
|
+
return (...args: unknown[]) =>
|
|
840
|
+
mutateArrayRef(mem, key, prop, arrProp, args);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const current = readValue(mem, key) as Record<string, unknown>;
|
|
844
|
+
const arr = current[prop] as unknown[];
|
|
845
|
+
if (!Array.isArray(arr)) {
|
|
846
|
+
throw new TypeError(`Property "${prop}" is not an array`);
|
|
847
|
+
}
|
|
848
|
+
if (arrProp === "length") return arr.length;
|
|
849
|
+
if (typeof arrProp === "string" && /^\d+$/.test(arrProp)) {
|
|
850
|
+
return arr[Number(arrProp)];
|
|
851
|
+
}
|
|
852
|
+
if (typeof arrProp === "symbol") {
|
|
853
|
+
if (arrProp === Symbol.iterator) return arr[Symbol.iterator].bind(arr);
|
|
854
|
+
return (arr as unknown as Record<symbol, unknown>)[arrProp];
|
|
855
|
+
}
|
|
856
|
+
if (MUTATING_METHODS.has(arrProp)) {
|
|
857
|
+
const methodName = arrProp;
|
|
858
|
+
// push/pop are O(1) in-place operations on the array ref.
|
|
859
|
+
if (methodName === "push" || methodName === "pop") {
|
|
860
|
+
return (...args: unknown[]) => {
|
|
861
|
+
return mutateArrayRef(mem, key, prop, methodName, args);
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
return (...args: unknown[]) => {
|
|
865
|
+
return mutateValue<Record<string, unknown>, unknown>(
|
|
866
|
+
mem,
|
|
867
|
+
key,
|
|
868
|
+
(fresh) => {
|
|
869
|
+
const freshArr = fresh[prop] as unknown[];
|
|
870
|
+
const method = (
|
|
871
|
+
freshArr as unknown as Record<
|
|
872
|
+
string,
|
|
873
|
+
(...a: unknown[]) => unknown
|
|
874
|
+
>
|
|
875
|
+
)[methodName]!;
|
|
876
|
+
return method.apply(freshArr, args);
|
|
877
|
+
},
|
|
878
|
+
arrayCapacity,
|
|
879
|
+
);
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const method = (arr as unknown as Record<string, unknown>)[arrProp];
|
|
883
|
+
if (typeof method === "function") return method.bind(arr);
|
|
884
|
+
return method;
|
|
885
|
+
},
|
|
886
|
+
set(_target, arrProp, val) {
|
|
887
|
+
// In-place update: write only the affected array slot instead of
|
|
888
|
+
// decoding and re-encoding the whole object.
|
|
889
|
+
return mem.withLock(() => {
|
|
890
|
+
const layout = getObjectLayout(mem, key);
|
|
891
|
+
if (layout !== null) {
|
|
892
|
+
const slotOffset = layout.properties.get(prop);
|
|
893
|
+
if (slotOffset !== undefined) {
|
|
894
|
+
const propSlot = mem.readValueSlot(slotOffset);
|
|
895
|
+
if (
|
|
896
|
+
propSlot.type === TYPE_ARRAY_REF &&
|
|
897
|
+
typeof arrProp === "string" &&
|
|
898
|
+
/^\d+$/.test(arrProp)
|
|
899
|
+
) {
|
|
900
|
+
const index = Number(arrProp);
|
|
901
|
+
const ref = {
|
|
902
|
+
offset: propSlot.a,
|
|
903
|
+
count: propSlot.b,
|
|
904
|
+
capacity: propSlot.c,
|
|
905
|
+
};
|
|
906
|
+
if (index >= ref.count) {
|
|
907
|
+
throw new RangeError(
|
|
908
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
912
|
+
bump(mem, key);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
// Array property length write: shrink/grow in place without
|
|
916
|
+
// re-encoding the whole object (which would leave a dead slot).
|
|
917
|
+
if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
|
|
918
|
+
const newLen = Number(val);
|
|
919
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
920
|
+
throw new RangeError("Invalid array length");
|
|
921
|
+
}
|
|
922
|
+
const ref = {
|
|
923
|
+
offset: propSlot.a,
|
|
924
|
+
count: propSlot.b,
|
|
925
|
+
capacity: propSlot.c,
|
|
926
|
+
};
|
|
927
|
+
if (newLen <= ref.capacity) {
|
|
928
|
+
mem.updateValueSlot(
|
|
929
|
+
slotOffset,
|
|
930
|
+
arrayRefSlot({
|
|
931
|
+
offset: ref.offset,
|
|
932
|
+
count: newLen,
|
|
933
|
+
capacity: ref.capacity,
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
bump(mem, key);
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
// Growing beyond capacity falls through to the full read-modify-write.
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// Fallback: full read-modify-write.
|
|
944
|
+
return mutateValue<Record<string, unknown>, boolean>(
|
|
945
|
+
mem,
|
|
946
|
+
key,
|
|
947
|
+
(fresh) => {
|
|
948
|
+
const arr = fresh[prop] as unknown[];
|
|
949
|
+
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
950
|
+
return true;
|
|
951
|
+
},
|
|
952
|
+
arrayCapacity,
|
|
953
|
+
);
|
|
954
|
+
});
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
}
|