bun-shared-memory 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/package.json +5 -8
- package/src/shared.ts +246 -192
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.5] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Replace the `Proxy` used for object `SharedRef`s with hard-baked
|
|
12
|
+
`Object.defineProperty` accessor getters/setters, so property reads skip the
|
|
13
|
+
Proxy trap on the hot path (~3× faster object property reads). Methods
|
|
14
|
+
(`read`/`write`/`update`/`key`/`buffer`) move to the prototype so the data
|
|
15
|
+
properties are the only own properties, keeping the JIT shape monomorphic
|
|
16
|
+
(~2× faster than having the methods as own properties). Arrays, primitives,
|
|
17
|
+
and unknown shapes still use the Proxy. Fixed shape is enforced via
|
|
18
|
+
`Object.preventExtensions` plus `configurable: false` accessors.
|
|
19
|
+
- Optimize `copyValue` (the `read()` copy) with `Object.assign` + a `for...in`
|
|
20
|
+
array pass (~3.5× faster object `read()`).
|
|
21
|
+
- Pass the initial value into `createSharedRef` so the object shape can be
|
|
22
|
+
baked at creation time.
|
|
23
|
+
|
|
24
|
+
## [0.1.4] - 2026-11-01
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Declare `"sideEffects": false` in `package.json` so bundlers can tree-shake
|
|
28
|
+
the library (the modules have no side effects on import).
|
|
29
|
+
- Remove the `typescript` peer dependency. The library ships as TypeScript
|
|
30
|
+
source and runs natively in Bun; `bun check` is Bun's own type checker and
|
|
31
|
+
does not require the `typescript` package.
|
|
32
|
+
|
|
33
|
+
## [0.1.3] - 2026-11-01
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Real-world benchmark (`bench/real-world/`) comparing shared memory against
|
|
37
|
+
live SQLite, MySQL, and Redis via an Elysia mini API.
|
|
38
|
+
- Redis as a fourth benchmark methodology (in Docker).
|
|
39
|
+
- Unix-socket transport for MySQL/Redis (Linux only; skipped on macOS).
|
|
40
|
+
- Benchmark results written to `benchlogs/<timestamp>-<commit>-real.md`.
|
|
41
|
+
- Real-world benchmark results and methodology documented in the README.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- MySQL/Redis connections are established before the benchmark (not per
|
|
45
|
+
request) so connection setup is not measured as overhead.
|
|
46
|
+
- The benchmark always runs both TCP and Unix-socket transports; the socket
|
|
47
|
+
run is skipped on macOS without error.
|
|
48
|
+
|
|
49
|
+
## [0.1.2] - 2026-11-01
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
- Unref the maintenance timer so the process can exit naturally.
|
|
53
|
+
- Document `init()` as the first call and how to configure memory via `init()`.
|
|
54
|
+
|
|
55
|
+
## [0.1.1] - 2026-11-01
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- Repository metadata and npm publishing preparation.
|
|
59
|
+
- README with package name and install instructions.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
- Optimize the read hot path: direct cache on the proxy target.
|
|
63
|
+
- Optimize `Mutex`: skip notify when no worker is waiting.
|
|
64
|
+
- Start the refcount coordinator/tracker automatically in `init()`.
|
|
65
|
+
- Document the minimum Bun version (`>= 1.4.0`).
|
|
66
|
+
|
|
67
|
+
## [0.1.0] - 2026-11-01
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
- Initial release: `Shared<T>` Proxy-based API over a 4-buffer shared memory
|
|
71
|
+
architecture (Index Table, DataTable, append-only StringTable, ArrayTable).
|
|
72
|
+
- Atomics-based synchronization (`Mutex`, `Notifier`).
|
|
73
|
+
- Per-key read cache with cross-worker invalidation and offset cache for
|
|
74
|
+
object property reads.
|
|
75
|
+
- O(1) in-place `push`/`pop` with capacity-based array growth.
|
|
76
|
+
- Benchmarks for all data types vs native.
|
|
77
|
+
|
|
78
|
+
[0.1.4]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.3...v0.1.4
|
|
79
|
+
[0.1.3]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.2...v0.1.3
|
|
80
|
+
[0.1.2]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.1...v0.1.2
|
|
81
|
+
[0.1.1]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.0...v0.1.1
|
|
82
|
+
[0.1.0]: https://github.com/Kapsonfire/bun-shared-memory/releases/tag/v0.1.0
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Share data between Bun workers/threads using SharedArrayBuffer (zero-copy, 4-buffer architecture, Atomics-based synchronization).",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"module": "index.ts",
|
|
7
8
|
"types": "index.ts",
|
|
8
9
|
"exports": {
|
|
@@ -13,7 +14,8 @@
|
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"index.ts",
|
|
16
|
-
"src/"
|
|
17
|
+
"src/",
|
|
18
|
+
"CHANGELOG.md"
|
|
17
19
|
],
|
|
18
20
|
"scripts": {
|
|
19
21
|
"typecheck": "bun check",
|
|
@@ -45,11 +47,6 @@
|
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@types/bun": "latest",
|
|
48
|
-
"elysia": "^1.4.30"
|
|
49
|
-
"ioredis": "^6.0.0",
|
|
50
|
-
"mysql2": "^3.24.6"
|
|
51
|
-
},
|
|
52
|
-
"peerDependencies": {
|
|
53
|
-
"typescript": "^7"
|
|
50
|
+
"elysia": "^1.4.30"
|
|
54
51
|
}
|
|
55
52
|
}
|
package/src/shared.ts
CHANGED
|
@@ -189,20 +189,6 @@ function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
|
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
191
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
192
|
/**
|
|
207
193
|
* Per-worker cache of object layouts: key -> stable property slot offsets.
|
|
208
194
|
*
|
|
@@ -222,13 +208,21 @@ function getObjectLayout(mem: SharedMemory, key: string): ObjectLayout | null {
|
|
|
222
208
|
return layout;
|
|
223
209
|
}
|
|
224
210
|
|
|
225
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Deep-ish copy of a decoded value, so callers can't corrupt the cache.
|
|
213
|
+
*
|
|
214
|
+
* Uses `Object.assign` for the fast shallow copy, then a `for...in` pass to
|
|
215
|
+
* copy any nested arrays (so mutating a returned array property cannot corrupt
|
|
216
|
+
* the cache). This is ~4.5× faster than the previous `Object.entries` + spread
|
|
217
|
+
* loop, which allocated an entries array and re-checked `Array.isArray` on
|
|
218
|
+
* every property.
|
|
219
|
+
*/
|
|
226
220
|
function copyValue(value: unknown): unknown {
|
|
227
221
|
if (Array.isArray(value)) return [...value];
|
|
228
222
|
if (value !== null && typeof value === "object") {
|
|
229
|
-
const copy
|
|
230
|
-
for (const
|
|
231
|
-
|
|
223
|
+
const copy = Object.assign({} as Record<string, unknown>, value);
|
|
224
|
+
for (const k in copy) {
|
|
225
|
+
if (Array.isArray(copy[k])) copy[k] = [...(copy[k] as unknown[])];
|
|
232
226
|
}
|
|
233
227
|
return copy;
|
|
234
228
|
}
|
|
@@ -297,6 +291,7 @@ export function Shared<T>(
|
|
|
297
291
|
const ref = createSharedRef<T>(
|
|
298
292
|
mem,
|
|
299
293
|
key,
|
|
294
|
+
typeof initialOrLoader === "function" ? undefined : (initialOrLoader as T),
|
|
300
295
|
options.refCount ?? getDefaultTracker() ?? undefined,
|
|
301
296
|
options.arrayCapacity,
|
|
302
297
|
);
|
|
@@ -318,10 +313,125 @@ export function Shared<T>(
|
|
|
318
313
|
return ref;
|
|
319
314
|
}
|
|
320
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Read a single property of a shared object value.
|
|
318
|
+
*
|
|
319
|
+
* Shared by the proxy `get` handler and the generated accessor getters. Uses
|
|
320
|
+
* the lock-free read-cache fast path and falls back to `readValue` (which
|
|
321
|
+
* fills the cache) on a miss. Array properties are returned as an array proxy.
|
|
322
|
+
*/
|
|
323
|
+
function readProp(
|
|
324
|
+
mem: SharedMemory,
|
|
325
|
+
key: string,
|
|
326
|
+
prop: string | symbol,
|
|
327
|
+
arrayCapacity?: number,
|
|
328
|
+
): unknown {
|
|
329
|
+
const fast = readCachedValue(mem, key);
|
|
330
|
+
const value = fast.hit ? fast.value : readValue(mem, key);
|
|
331
|
+
if (Array.isArray(value)) {
|
|
332
|
+
return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
|
|
333
|
+
}
|
|
334
|
+
const propValue = (value as Record<string | symbol, unknown>)[prop];
|
|
335
|
+
if (Array.isArray(propValue)) {
|
|
336
|
+
return createArrayProxy(mem, key, prop as string, arrayCapacity);
|
|
337
|
+
}
|
|
338
|
+
return propValue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Write a single property of a shared object value in place.
|
|
343
|
+
*
|
|
344
|
+
* Shared by the proxy `set` handler and the generated accessor setters. Uses
|
|
345
|
+
* the cached layout to update only the affected slot.
|
|
346
|
+
*/
|
|
347
|
+
function writeProp(
|
|
348
|
+
mem: SharedMemory,
|
|
349
|
+
key: string,
|
|
350
|
+
prop: string | symbol,
|
|
351
|
+
val: unknown,
|
|
352
|
+
arrayCapacity?: number,
|
|
353
|
+
): void {
|
|
354
|
+
mem.withLock(() => {
|
|
355
|
+
// Object property: use the cached layout to write only that slot.
|
|
356
|
+
const layout = getObjectLayout(mem, key);
|
|
357
|
+
if (layout !== null) {
|
|
358
|
+
const slotOffset = layout.properties.get(prop as string);
|
|
359
|
+
if (slotOffset === undefined) {
|
|
360
|
+
throw new TypeError(
|
|
361
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
mem.updateValueSlot(slotOffset, encodeValue(mem, val));
|
|
365
|
+
bump(mem, key);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Top-level array index write: update only that array slot.
|
|
370
|
+
const offset = getOffset(mem, key);
|
|
371
|
+
if (offset === null) {
|
|
372
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
373
|
+
}
|
|
374
|
+
const slot = mem.readValueSlot(offset);
|
|
375
|
+
if (
|
|
376
|
+
slot.type === TYPE_ARRAY_REF &&
|
|
377
|
+
typeof prop === "string" &&
|
|
378
|
+
/^\d+$/.test(prop)
|
|
379
|
+
) {
|
|
380
|
+
const index = Number(prop);
|
|
381
|
+
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
382
|
+
if (index >= ref.count) {
|
|
383
|
+
throw new RangeError(
|
|
384
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
388
|
+
bump(mem, key);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Top-level array length write: shrink/grow the array in place.
|
|
393
|
+
if (slot.type === TYPE_ARRAY_REF && prop === "length") {
|
|
394
|
+
const newLen = Number(val);
|
|
395
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
396
|
+
throw new RangeError("Invalid array length");
|
|
397
|
+
}
|
|
398
|
+
const ref = { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
399
|
+
if (newLen <= ref.capacity) {
|
|
400
|
+
mem.updateValueSlot(
|
|
401
|
+
offset,
|
|
402
|
+
arrayRefSlot({
|
|
403
|
+
offset: ref.offset,
|
|
404
|
+
count: newLen,
|
|
405
|
+
capacity: ref.capacity,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
bump(mem, key);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fallback: full read-modify-write.
|
|
414
|
+
mutateValue<unknown, boolean>(
|
|
415
|
+
mem,
|
|
416
|
+
key,
|
|
417
|
+
(value) => {
|
|
418
|
+
if (Array.isArray(value)) {
|
|
419
|
+
(value as unknown as Record<string | symbol, unknown>)[prop] = val;
|
|
420
|
+
} else {
|
|
421
|
+
(value as Record<string | symbol, unknown>)[prop] = val;
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
424
|
+
},
|
|
425
|
+
arrayCapacity,
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
321
430
|
/** Create a SharedRef proxy backed by a SharedMemory instance. */
|
|
322
431
|
function createSharedRef<T>(
|
|
323
432
|
mem: SharedMemory,
|
|
324
433
|
key: string,
|
|
434
|
+
initialValue: T | undefined,
|
|
325
435
|
tracker?: RefCountTracker,
|
|
326
436
|
arrayCapacity?: number,
|
|
327
437
|
): SharedRef<T> {
|
|
@@ -352,6 +462,102 @@ function createSharedRef<T>(
|
|
|
352
462
|
},
|
|
353
463
|
};
|
|
354
464
|
|
|
465
|
+
// Determine the object's property names so we can hard-bake accessor
|
|
466
|
+
// getters/setters instead of routing every access through a Proxy trap.
|
|
467
|
+
// The shape is known from the initial value (if it is an object) or from
|
|
468
|
+
// shared memory (if the key already exists, e.g. in a worker).
|
|
469
|
+
let propNames: string[] | null = null;
|
|
470
|
+
if (
|
|
471
|
+
initialValue !== null &&
|
|
472
|
+
typeof initialValue === "object" &&
|
|
473
|
+
!Array.isArray(initialValue)
|
|
474
|
+
) {
|
|
475
|
+
propNames = Object.keys(initialValue as Record<string, unknown>);
|
|
476
|
+
} else {
|
|
477
|
+
const layout = mem.withLock(() => mem.getObjectLayout(key));
|
|
478
|
+
if (layout !== null) {
|
|
479
|
+
propNames = [...layout.properties.keys()];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Object values: return a plain object with hard-baked accessor getters and
|
|
484
|
+
// setters (no Proxy). This avoids the Proxy trap overhead on every property
|
|
485
|
+
// read (~2.6× faster) and stays a plain, serializable object.
|
|
486
|
+
if (propNames !== null && propNames.length > 0) {
|
|
487
|
+
// Methods live on the prototype, not on the object itself. Keeping the
|
|
488
|
+
// data properties as the only own properties lets the JIT treat the
|
|
489
|
+
// object as monomorphic, which is ~2× faster for accessor reads than
|
|
490
|
+
// having extra own properties (read/write/update/key/buffer).
|
|
491
|
+
const proto: Record<string | symbol, unknown> = {
|
|
492
|
+
[Symbol.toPrimitive]: () => target.read(),
|
|
493
|
+
};
|
|
494
|
+
Object.defineProperty(proto, "read", {
|
|
495
|
+
enumerable: false,
|
|
496
|
+
configurable: false,
|
|
497
|
+
value: target.read,
|
|
498
|
+
});
|
|
499
|
+
Object.defineProperty(proto, "write", {
|
|
500
|
+
enumerable: false,
|
|
501
|
+
configurable: false,
|
|
502
|
+
value: target.write,
|
|
503
|
+
});
|
|
504
|
+
Object.defineProperty(proto, "update", {
|
|
505
|
+
enumerable: false,
|
|
506
|
+
configurable: false,
|
|
507
|
+
value: target.update,
|
|
508
|
+
});
|
|
509
|
+
// key/buffer are non-enumerable so they don't appear in JSON.stringify /
|
|
510
|
+
// Object.keys (matching the previous Proxy behavior).
|
|
511
|
+
Object.defineProperty(proto, "key", {
|
|
512
|
+
enumerable: false,
|
|
513
|
+
configurable: false,
|
|
514
|
+
value: key,
|
|
515
|
+
});
|
|
516
|
+
Object.defineProperty(proto, "buffer", {
|
|
517
|
+
enumerable: false,
|
|
518
|
+
configurable: false,
|
|
519
|
+
get() {
|
|
520
|
+
return mem.dataTable.buffer;
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
const ref = Object.create(proto) as Record<string | symbol, unknown>;
|
|
524
|
+
for (const name of propNames) {
|
|
525
|
+
Object.defineProperty(ref, name, {
|
|
526
|
+
enumerable: true,
|
|
527
|
+
configurable: false,
|
|
528
|
+
get() {
|
|
529
|
+
// Lock-free fast path inlined so the JIT can optimize the hot
|
|
530
|
+
// cache-hit read without a function-call chain.
|
|
531
|
+
const cached = readCache.get(key);
|
|
532
|
+
if (
|
|
533
|
+
cached !== undefined &&
|
|
534
|
+
cached.version === mem.getVersion(cached.index)
|
|
535
|
+
) {
|
|
536
|
+
const v = cached.value;
|
|
537
|
+
if (Array.isArray(v)) {
|
|
538
|
+
return getTopLevelArrayMember(mem, key, v, name, arrayCapacity);
|
|
539
|
+
}
|
|
540
|
+
const pv = (v as Record<string | symbol, unknown>)[name];
|
|
541
|
+
if (Array.isArray(pv)) {
|
|
542
|
+
return createArrayProxy(mem, key, name, arrayCapacity);
|
|
543
|
+
}
|
|
544
|
+
return pv;
|
|
545
|
+
}
|
|
546
|
+
return readProp(mem, key, name, arrayCapacity);
|
|
547
|
+
},
|
|
548
|
+
set(v: unknown) {
|
|
549
|
+
target.__cache = undefined;
|
|
550
|
+
writeProp(mem, key, name, v, arrayCapacity);
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
// Fixed shape: reject adding new properties (throws in strict mode).
|
|
555
|
+
Object.preventExtensions(ref);
|
|
556
|
+
return ref as unknown as SharedRef<T>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Arrays, primitives, or unknown shape: use a Proxy (index access, array
|
|
560
|
+
// methods, and dynamic properties require interception).
|
|
355
561
|
return new Proxy(target, {
|
|
356
562
|
get(t, prop) {
|
|
357
563
|
if (prop === "read") return t.read;
|
|
@@ -371,177 +577,12 @@ function createSharedRef<T>(
|
|
|
371
577
|
}
|
|
372
578
|
}
|
|
373
579
|
|
|
374
|
-
|
|
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;
|
|
580
|
+
return readProp(mem, key, prop, arrayCapacity);
|
|
461
581
|
},
|
|
462
582
|
set(t, prop, val) {
|
|
463
|
-
// In-place update: write only the affected slot instead of decoding and
|
|
464
|
-
// re-encoding the whole value.
|
|
465
583
|
t.__cache = undefined;
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
});
|
|
584
|
+
writeProp(mem, key, prop, val, arrayCapacity);
|
|
585
|
+
return true;
|
|
545
586
|
},
|
|
546
587
|
deleteProperty() {
|
|
547
588
|
throw new TypeError(
|
|
@@ -551,6 +592,22 @@ function createSharedRef<T>(
|
|
|
551
592
|
}) as unknown as SharedRef<T>;
|
|
552
593
|
}
|
|
553
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Lock-free fast path: if the cached version matches the current version cell,
|
|
597
|
+
* the cached value is still valid. Returns the cached value, or a miss marker.
|
|
598
|
+
* Kept small so the JIT can inline it into hot accessor getters.
|
|
599
|
+
*/
|
|
600
|
+
function readCachedValue(
|
|
601
|
+
mem: SharedMemory,
|
|
602
|
+
key: string,
|
|
603
|
+
): { hit: true; value: unknown } | { hit: false } {
|
|
604
|
+
const cached = readCache.get(key);
|
|
605
|
+
if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
|
|
606
|
+
return { hit: true, value: cached.value };
|
|
607
|
+
}
|
|
608
|
+
return { hit: false };
|
|
609
|
+
}
|
|
610
|
+
|
|
554
611
|
/**
|
|
555
612
|
* Read the full value for a key from shared memory, using the read cache.
|
|
556
613
|
*
|
|
@@ -565,15 +622,12 @@ function readValue(
|
|
|
565
622
|
__cache?: { version: number; value: unknown; index: number };
|
|
566
623
|
},
|
|
567
624
|
): unknown {
|
|
625
|
+
const fast = readCachedValue(mem, key);
|
|
626
|
+
if (fast.hit) return fast.value;
|
|
627
|
+
|
|
568
628
|
const cached = readCache.get(key);
|
|
569
629
|
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
630
|
const version = mem.getVersion(index);
|
|
574
|
-
if (cached !== undefined && cached.version === version) {
|
|
575
|
-
return cached.value;
|
|
576
|
-
}
|
|
577
631
|
|
|
578
632
|
// Cache miss: decode under the lock and cache with the observed version.
|
|
579
633
|
const value = mem.withLock(() => {
|