bun-shared-memory 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -3
- package/README.md +28 -12
- package/package.json +1 -1
- package/src/growable-buffer.ts +35 -0
- package/src/memory.ts +124 -9
- package/src/shared.ts +643 -156
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.2] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`null` is now a supported shared value**. A new `TYPE_NULL` slot type
|
|
15
|
+
allows `null` to be stored and read anywhere — top-level, object property,
|
|
16
|
+
array element, nested object/array. `undefined` remains intentionally
|
|
17
|
+
unsupported.
|
|
18
|
+
- **Nested objects and live `Shared` references**: an object property (or array
|
|
19
|
+
element) may now be another plain object, read lazily and mutated in place
|
|
20
|
+
(e.g. `doc.profile.meta.level++` writes directly to shared memory). Two new
|
|
21
|
+
slot types back this: `TYPE_OBJECT_REF` (a nested object block in the
|
|
22
|
+
DataTable) and `TYPE_KEY_REF` (a live reference to another shared key).
|
|
23
|
+
- **Nested `Shared` values**: storing a `Shared` object inside another object
|
|
24
|
+
or array keeps a live reference, so both wrappers stay in sync (no copying).
|
|
25
|
+
- **Nested arrays**: arrays may contain other arrays (stored recursively as
|
|
26
|
+
array-refs in the ArrayTable). `arr[0][1] = x`, `arr[0].push(...)`, etc.
|
|
27
|
+
- **Defragmentation** remaps nested object references and rewrites their
|
|
28
|
+
property slots; the per-worker object-layout cache is re-validated against
|
|
29
|
+
the global layout version so it stays correct across a defrag.
|
|
30
|
+
|
|
31
|
+
### Performance
|
|
32
|
+
|
|
33
|
+
- Cache the key's **version-cell index in the read cache**, so cache-hit reads
|
|
34
|
+
skip the `versionCellIndex` arithmetic and go straight to
|
|
35
|
+
`Atomics.load(view, cached.cell)`.
|
|
36
|
+
- Hoist **`getLayoutVersion`** inside lock-free writes: within a write (counter
|
|
37
|
+
> 0) the layout version cannot change, so read it once and reuse it for all
|
|
38
|
+
cached-offset and ArrayRef checks, saving 2–3 `Atomics.load` ops per write.
|
|
39
|
+
Array writes improved from ~37× to ~33× slower than native.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Fix `copyBytes` in the defrag compactor so overlapping source/destination
|
|
44
|
+
ranges (a small string-value block compacting forward over the source region
|
|
45
|
+
of a later array block) are copied overlap-safely. This previously could
|
|
46
|
+
corrupt array contents after a defragmentation that involved interleaved
|
|
47
|
+
string-value and array blocks.
|
|
48
|
+
|
|
8
49
|
## [0.2.1] - 2026-11-04
|
|
9
50
|
|
|
10
51
|
### Performance
|
|
@@ -24,11 +65,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
24
65
|
- Cache the key's version-cell index in the array proxy so writes bump the
|
|
25
66
|
version without a per-write Map lookup. Object array-prop index writes
|
|
26
67
|
dropped from ~33.9× to ~29× slower than native.
|
|
27
|
-
- Real-world benchmark: the shared-memory "get user" endpoint improved
|
|
28
|
-
~
|
|
29
|
-
backends (sqlite
|
|
68
|
+
- Real-world benchmark: the shared-memory "get user" endpoint improved to
|
|
69
|
+
~5.15M req/s, widening the gap over the comparison
|
|
70
|
+
backends (sqlite 9.9×, redis 76.5×, mysql 82.0× slower than
|
|
30
71
|
shared memory).
|
|
31
72
|
|
|
73
|
+
### Benchmark
|
|
74
|
+
|
|
75
|
+
- Increase `SAMPLES_PER_WORKER` from 5 to 15 for more stable per-run statistics.
|
|
76
|
+
- Increase `RUNS_PER_METHOD` from 3 to 5 for better CPU-contention filtering.
|
|
77
|
+
- Increase warmup from 1,000 to 5,000 iterations so the JIT and DB caches
|
|
78
|
+
are fully warmed before measurement.
|
|
79
|
+
- Add `?pool=8` to MySQL connection strings to avoid per-request connect overhead.
|
|
80
|
+
- Add priming queries (`SELECT LIMIT 1` for MySQL, `PING` for Redis) so the
|
|
81
|
+
first measured request does not hit cold-connect latency.
|
|
82
|
+
|
|
32
83
|
## [0.2.0] - 2026-11-04
|
|
33
84
|
|
|
34
85
|
### Added
|
package/README.md
CHANGED
|
@@ -38,20 +38,20 @@ is benchmarked sequentially with 4 workers. See
|
|
|
38
38
|
|
|
39
39
|
Measured on Linux (Bun 1.4.4, 100,000 requests per methodology, sorted by
|
|
40
40
|
speed; MySQL/Redis benchmarked over both TCP and Unix socket; each methodology
|
|
41
|
-
is the minimum over
|
|
41
|
+
is the minimum over 5 runs × 15 samples to filter CPU-contention noise):
|
|
42
42
|
|
|
43
43
|
| Methodology | Transport | Requests | Elapsed (ms) | req/s | vs. shared |
|
|
44
44
|
| ----------- | --------- | -------- | ------------ | --------- | ------------- |
|
|
45
|
-
| `shared` | — | 100,000 |
|
|
46
|
-
| `sqlite` | — | 100,000 |
|
|
47
|
-
| `redis` | socket | 100,000 | 1,
|
|
48
|
-
| `mysql` | socket | 100,000 | 1,
|
|
49
|
-
| `redis` | tcp | 100,000 | 2,
|
|
50
|
-
| `mysql` | tcp | 100,000 |
|
|
51
|
-
|
|
52
|
-
Shared memory is the fastest by a wide margin: ~9.
|
|
53
|
-
read and ~61–
|
|
54
|
-
significantly faster than TCP for MySQL/Redis (~1.8×), but both remain far
|
|
45
|
+
| `shared` | — | 100,000 | 19 | 5,154,977 | — |
|
|
46
|
+
| `sqlite` | — | 100,000 | 192 | 520,329 | 9.9× slower |
|
|
47
|
+
| `redis` | socket | 100,000 | 1,484 | 67,385 | 76.5× slower |
|
|
48
|
+
| `mysql` | socket | 100,000 | 1,591 | 62,865 | 82.0× slower |
|
|
49
|
+
| `redis` | tcp | 100,000 | 2,617 | 38,205 | 134.9× slower |
|
|
50
|
+
| `mysql` | tcp | 100,000 | 3,133 | 31,922 | 161.5× slower |
|
|
51
|
+
|
|
52
|
+
Shared memory is the fastest by a wide margin: ~9.9× faster than a live SQLite
|
|
53
|
+
read and ~61–161× faster than live MySQL/Redis reads. Unix sockets are
|
|
54
|
+
significantly faster than TCP for MySQL/Redis (~1.7–1.8×), but both remain far
|
|
55
55
|
slower than shared memory.
|
|
56
56
|
|
|
57
57
|
## ✨ Features
|
|
@@ -90,7 +90,10 @@ See [SPEC.md](./SPEC.md) for the full specification.
|
|
|
90
90
|
- **Allowed**: primitives (`number`, `string`, `boolean`), `bigint`, and arrays of these (mixed arrays allowed).
|
|
91
91
|
- **BigInts** of any size are supported: values that fit in 64 bits are stored as a fast int64 slot; larger values are stored as their decimal string in the StringTable.
|
|
92
92
|
- **Objects** have a **fixed shape**: once created, properties may not be added or removed — only their values can change.
|
|
93
|
-
- **
|
|
93
|
+
- **Nested objects and arrays** are supported: an object or array can contain other plain objects and arrays (read lazily, mutated in place).
|
|
94
|
+
- **Nested `Shared` references**: a `Shared` object can be stored inside another object or array as a _live reference_ — both stay in sync (mutating either updates the same shared value). Self-references and mutual references are supported as long as the property exists in the object's initial fixed shape.
|
|
95
|
+
- **`null` is allowed** as a value (top-level, object property, or array element).
|
|
96
|
+
- **Not allowed**: `undefined`.
|
|
94
97
|
|
|
95
98
|
## Usage
|
|
96
99
|
|
|
@@ -123,6 +126,19 @@ const arr = Shared<number[]>("arr-1", [1, 2, 3]);
|
|
|
123
126
|
arr.push(4);
|
|
124
127
|
arr[0] = 99;
|
|
125
128
|
|
|
129
|
+
// Nested objects are read lazily and mutated in place
|
|
130
|
+
const doc = Shared("doc-1", {
|
|
131
|
+
profile: { name: "Alice", meta: { level: 3 } },
|
|
132
|
+
});
|
|
133
|
+
doc.profile.meta.level++;
|
|
134
|
+
doc.profile.meta.level; // 4
|
|
135
|
+
|
|
136
|
+
// Nested Shared values are live references that stay in sync
|
|
137
|
+
const settings = Shared("settings-1", { theme: "dark" });
|
|
138
|
+
const app = Shared("app-1", { settings });
|
|
139
|
+
app.settings.theme = "light";
|
|
140
|
+
settings.theme; // "light" (same shared value)
|
|
141
|
+
|
|
126
142
|
// Primitives use read()/write()/update()
|
|
127
143
|
const counter = Shared<number>("counter-1", 0);
|
|
128
144
|
counter.write(42);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Share data between Bun workers/threads using SharedArrayBuffer (zero-copy, 4-buffer architecture, Atomics-based synchronization).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/src/growable-buffer.ts
CHANGED
|
@@ -425,6 +425,41 @@ export class GrowableBuffer {
|
|
|
425
425
|
Atomics.add(this.cursor, 0, BigInt(n));
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Grow a block in place by `extra` bytes when the space immediately after it
|
|
430
|
+
* is free (i.e. the block is the last allocated one, so its end equals the
|
|
431
|
+
* current used length). This lets a caller extend a block without leaving
|
|
432
|
+
* the old prefix as dead space.
|
|
433
|
+
*
|
|
434
|
+
* The cursor is advanced with a compare-exchange loop, so concurrent workers
|
|
435
|
+
* cannot allocate into the space being claimed: if another worker has
|
|
436
|
+
* reserved bytes after `endOffset`, the cursor no longer equals `endOffset`
|
|
437
|
+
* and this returns false, letting the caller fall back to a fresh
|
|
438
|
+
* allocation.
|
|
439
|
+
*
|
|
440
|
+
* @returns true if the block was grown in place, false if the space after it
|
|
441
|
+
* is not free (caller must allocate a new block).
|
|
442
|
+
*/
|
|
443
|
+
growInPlace(endOffset: number, extra: number): boolean {
|
|
444
|
+
if (extra <= 0) return true;
|
|
445
|
+
while (true) {
|
|
446
|
+
const current = Number(Atomics.load(this.cursor, 0));
|
|
447
|
+
if (current !== endOffset) return false;
|
|
448
|
+
const next = current + extra;
|
|
449
|
+
if (
|
|
450
|
+
Atomics.compareExchange(
|
|
451
|
+
this.cursor,
|
|
452
|
+
0,
|
|
453
|
+
BigInt(current),
|
|
454
|
+
BigInt(next),
|
|
455
|
+
) === BigInt(current)
|
|
456
|
+
) {
|
|
457
|
+
this.ensureCapacity(next);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
428
463
|
/**
|
|
429
464
|
* Atomically reserve `size` bytes at the end of the buffer and return the
|
|
430
465
|
* start offset. This is the lock-free bump-allocation primitive: the shared
|
package/src/memory.ts
CHANGED
|
@@ -56,6 +56,21 @@ export const TYPE_STRING_VALUE_REF = 7;
|
|
|
56
56
|
* the a/b/c fields), avoiding a heap allocation and the string-table write.
|
|
57
57
|
*/
|
|
58
58
|
export const TYPE_INLINE_STRING = 8;
|
|
59
|
+
/**
|
|
60
|
+
* A nested object block stored in the DataTable. `a` is the DataTable byte
|
|
61
|
+
* offset of the object block (same layout as a top-level object).
|
|
62
|
+
*/
|
|
63
|
+
export const TYPE_OBJECT_REF = 9;
|
|
64
|
+
/**
|
|
65
|
+
* A live reference to another shared key (used for nested `Shared` objects so
|
|
66
|
+
* changes stay in sync). `a` is the referenced key's stable Index Table index.
|
|
67
|
+
*/
|
|
68
|
+
export const TYPE_KEY_REF = 10;
|
|
69
|
+
/**
|
|
70
|
+
* The `null` value. No payload in a/b/c — the presence of the slot's type is
|
|
71
|
+
* the entire value. `undefined` is intentionally still not representable.
|
|
72
|
+
*/
|
|
73
|
+
export const TYPE_NULL = 11;
|
|
59
74
|
|
|
60
75
|
const SLOT_SIZE = 16;
|
|
61
76
|
const OBJECT_HEADER = 16;
|
|
@@ -96,6 +111,12 @@ const slotView = new DataView(new ArrayBuffer(8));
|
|
|
96
111
|
export interface StringRef {
|
|
97
112
|
offset: number;
|
|
98
113
|
length: number;
|
|
114
|
+
/**
|
|
115
|
+
* Reserved block size in the ArrayTable (>= ARRAY_HEADER + length). Only
|
|
116
|
+
* set for string values stored in the ArrayTable; used to grow a block in
|
|
117
|
+
* place when a string is overwritten with a longer one.
|
|
118
|
+
*/
|
|
119
|
+
reserved?: number;
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
export interface ArrayRef {
|
|
@@ -159,7 +180,12 @@ export function stringRefSlot(ref: StringRef): ValueSlot {
|
|
|
159
180
|
|
|
160
181
|
/** Encode a string value (in the ArrayTable) into a ValueSlot. */
|
|
161
182
|
export function stringValueRefSlot(ref: StringRef): ValueSlot {
|
|
162
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
type: TYPE_STRING_VALUE_REF,
|
|
185
|
+
a: ref.offset,
|
|
186
|
+
b: ref.length,
|
|
187
|
+
c: ref.reserved ?? 0,
|
|
188
|
+
};
|
|
163
189
|
}
|
|
164
190
|
|
|
165
191
|
/** Encode a bigint stored as a decimal string into a ValueSlot. */
|
|
@@ -172,6 +198,21 @@ export function arrayRefSlot(ref: ArrayRef): ValueSlot {
|
|
|
172
198
|
return { type: TYPE_ARRAY_REF, a: ref.offset, b: ref.count, c: ref.capacity };
|
|
173
199
|
}
|
|
174
200
|
|
|
201
|
+
/** Encode a reference to a nested object block (DataTable offset). */
|
|
202
|
+
export function objectRefSlot(offset: number): ValueSlot {
|
|
203
|
+
return { type: TYPE_OBJECT_REF, a: offset, b: 0, c: 0 };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Encode a live reference to another shared key (by stable Index Table index). */
|
|
207
|
+
export function keyRefSlot(index: number): ValueSlot {
|
|
208
|
+
return { type: TYPE_KEY_REF, a: index, b: 0, c: 0 };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Encode the `null` value. */
|
|
212
|
+
export function nullSlot(): ValueSlot {
|
|
213
|
+
return { type: TYPE_NULL, a: 0, b: 0, c: 0 };
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
/** Decode a ValueSlot into a JS value (primitives) or a ref object. */
|
|
176
217
|
export function decodeSlot(slot: ValueSlot): unknown {
|
|
177
218
|
switch (slot.type) {
|
|
@@ -191,8 +232,14 @@ export function decodeSlot(slot: ValueSlot): unknown {
|
|
|
191
232
|
return { offset: slot.a, length: slot.b };
|
|
192
233
|
case TYPE_STRING_VALUE_REF:
|
|
193
234
|
return { offset: slot.a, length: slot.b };
|
|
235
|
+
case TYPE_NULL:
|
|
236
|
+
return null;
|
|
194
237
|
case TYPE_ARRAY_REF:
|
|
195
238
|
return { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
239
|
+
case TYPE_OBJECT_REF:
|
|
240
|
+
return { offset: slot.a };
|
|
241
|
+
case TYPE_KEY_REF:
|
|
242
|
+
return { index: slot.a };
|
|
196
243
|
default:
|
|
197
244
|
throw new Error(`Unknown slot type ${slot.type}`);
|
|
198
245
|
}
|
|
@@ -675,13 +722,34 @@ export class SharedMemory {
|
|
|
675
722
|
// unbounded ArrayTable growth. The block's objectId header is preserved.
|
|
676
723
|
if (oldRef !== undefined && size <= ARRAY_HEADER + oldRef.length) {
|
|
677
724
|
this.arrayTable.writeBytes(oldRef.offset + ARRAY_HEADER, bytes);
|
|
678
|
-
return {
|
|
725
|
+
return {
|
|
726
|
+
offset: oldRef.offset,
|
|
727
|
+
length: bytes.byteLength,
|
|
728
|
+
reserved: oldRef.reserved ?? size,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
// The new string is longer than the old block. If the old block is the
|
|
732
|
+
// last allocated block (its reserved end is the current used length), grow
|
|
733
|
+
// it in place so repeated writes to the same property don't leak dead
|
|
734
|
+
// blocks. This also covers the shrink-then-grow case: a block that was
|
|
735
|
+
// reserved for a longer string but currently holds a shorter one can be
|
|
736
|
+
// grown back into its own reserved tail.
|
|
737
|
+
if (oldRef !== undefined && oldRef.reserved !== undefined) {
|
|
738
|
+
const reservedEnd = oldRef.offset + oldRef.reserved;
|
|
739
|
+
if (this.arrayTable.growInPlace(reservedEnd, size - oldRef.reserved)) {
|
|
740
|
+
this.arrayTable.writeBytes(oldRef.offset + ARRAY_HEADER, bytes);
|
|
741
|
+
return {
|
|
742
|
+
offset: oldRef.offset,
|
|
743
|
+
length: bytes.byteLength,
|
|
744
|
+
reserved: Math.max(oldRef.reserved, size),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
679
747
|
}
|
|
680
748
|
const offset = this.arrayTable.reserve(size);
|
|
681
749
|
const objectId = this.nextObjectId++;
|
|
682
750
|
this.arrayTable.setInt32(offset, objectId);
|
|
683
751
|
this.arrayTable.writeBytes(offset + ARRAY_HEADER, bytes);
|
|
684
|
-
return { offset, length: bytes.byteLength };
|
|
752
|
+
return { offset, length: bytes.byteLength, reserved: size };
|
|
685
753
|
}
|
|
686
754
|
|
|
687
755
|
/** Read a string value from the ArrayTable by reference. */
|
|
@@ -1117,6 +1185,14 @@ export class SharedMemory {
|
|
|
1117
1185
|
getObjectLayout(key: string): ObjectLayout | null {
|
|
1118
1186
|
const offset = this.lookup(key);
|
|
1119
1187
|
if (offset === null) return null;
|
|
1188
|
+
return this.getObjectLayoutAtOffset(offset);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Get the stable layout of an object block at a DataTable offset (top-level
|
|
1193
|
+
* or nested). Returns null if the offset is not an object block.
|
|
1194
|
+
*/
|
|
1195
|
+
getObjectLayoutAtOffset(offset: number): ObjectLayout | null {
|
|
1120
1196
|
if (this.dataTable.getInt32(offset) !== TYPE_OBJECT) return null;
|
|
1121
1197
|
const count = this.dataTable.getInt32(offset + 4);
|
|
1122
1198
|
const properties = new Map<string, number>();
|
|
@@ -1189,6 +1265,20 @@ export class SharedMemory {
|
|
|
1189
1265
|
return null;
|
|
1190
1266
|
}
|
|
1191
1267
|
|
|
1268
|
+
/**
|
|
1269
|
+
* Read the key string for a known stable index. Used to resolve a live
|
|
1270
|
+
* shared-key reference (TYPE_KEY_REF) back to its key so a wrapper can be
|
|
1271
|
+
* reconstructed. Returns null if the index is out of range.
|
|
1272
|
+
*/
|
|
1273
|
+
getKeyByIndex(index: number): string | null {
|
|
1274
|
+
const count = this.indexTable.getInt32(0);
|
|
1275
|
+
if (index >= count) return null;
|
|
1276
|
+
const entryOffset = INDEX_HEADER + index * INDEX_ENTRY_SIZE;
|
|
1277
|
+
const keyOffset = this.indexTable.getInt32(entryOffset);
|
|
1278
|
+
const keyLength = this.indexTable.getInt32(entryOffset + 4);
|
|
1279
|
+
return this.readString({ offset: keyOffset, length: keyLength });
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1192
1282
|
/**
|
|
1193
1283
|
* Int32Array index of a key's version cell within the Index Table buffer.
|
|
1194
1284
|
* Stable for the lifetime of the key (the Index Table is never reordered or
|
|
@@ -1339,12 +1429,21 @@ export class SharedMemory {
|
|
|
1339
1429
|
stringValueBlocks.add(slot.a);
|
|
1340
1430
|
arrayBlockSizes.set(
|
|
1341
1431
|
slot.a,
|
|
1342
|
-
Math.max(ARRAY_HEADER + slot.b, MIN_BLOCK_SIZE),
|
|
1432
|
+
Math.max(slot.c || ARRAY_HEADER + slot.b, MIN_BLOCK_SIZE),
|
|
1343
1433
|
);
|
|
1344
1434
|
break;
|
|
1345
1435
|
case TYPE_ARRAY_REF:
|
|
1346
1436
|
markArray(slot.a, slot.b, slot.c);
|
|
1347
1437
|
break;
|
|
1438
|
+
case TYPE_OBJECT_REF:
|
|
1439
|
+
// Nested object block lives in the DataTable; mark it recursively.
|
|
1440
|
+
markObject(slot.a);
|
|
1441
|
+
break;
|
|
1442
|
+
case TYPE_KEY_REF:
|
|
1443
|
+
// A live reference to another shared key. The referenced key is
|
|
1444
|
+
// marked separately when the Index Table is walked (Phase 1), so
|
|
1445
|
+
// nothing to do here.
|
|
1446
|
+
break;
|
|
1348
1447
|
}
|
|
1349
1448
|
};
|
|
1350
1449
|
|
|
@@ -1460,14 +1559,14 @@ export class SharedMemory {
|
|
|
1460
1559
|
const nameOffset = this.dataTable.getInt32(cursor);
|
|
1461
1560
|
this.dataTable.setInt32(cursor, stringRemap.get(nameOffset)!);
|
|
1462
1561
|
cursor += NAME_REF_SIZE;
|
|
1463
|
-
remapSlot(this.dataTable, cursor, stringRemap, arrayRemap);
|
|
1562
|
+
remapSlot(this.dataTable, cursor, stringRemap, arrayRemap, objectRemap);
|
|
1464
1563
|
cursor += SLOT_SIZE;
|
|
1465
1564
|
}
|
|
1466
1565
|
}
|
|
1467
1566
|
for (const oldOffset of sortedData) {
|
|
1468
1567
|
if (this.dataTable.getInt32(oldOffset) === TYPE_OBJECT) continue;
|
|
1469
1568
|
const newOff = objectRemap.get(oldOffset)!;
|
|
1470
|
-
remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
|
|
1569
|
+
remapSlot(this.dataTable, newOff, stringRemap, arrayRemap, objectRemap);
|
|
1471
1570
|
}
|
|
1472
1571
|
for (const oldOffset of sortedArrays) {
|
|
1473
1572
|
if (stringValueBlocks.has(oldOffset)) continue;
|
|
@@ -1480,6 +1579,7 @@ export class SharedMemory {
|
|
|
1480
1579
|
newOff + ARRAY_HEADER + i * SLOT_SIZE,
|
|
1481
1580
|
stringRemap,
|
|
1482
1581
|
arrayRemap,
|
|
1582
|
+
objectRemap,
|
|
1483
1583
|
);
|
|
1484
1584
|
}
|
|
1485
1585
|
}
|
|
@@ -1514,15 +1614,25 @@ function readSlot(buf: GrowableBuffer, offset: number): ValueSlot {
|
|
|
1514
1614
|
};
|
|
1515
1615
|
}
|
|
1516
1616
|
|
|
1517
|
-
/** Copy `len` bytes from `src` to `dst` in the same buffer
|
|
1617
|
+
/** Copy `len` bytes from `src` to `dst` in the same buffer, overlap-safe. */
|
|
1518
1618
|
function copyBytes(
|
|
1519
1619
|
buf: GrowableBuffer,
|
|
1520
1620
|
dst: number,
|
|
1521
1621
|
src: number,
|
|
1522
1622
|
len: number,
|
|
1523
1623
|
): void {
|
|
1524
|
-
|
|
1525
|
-
|
|
1624
|
+
// `dst` may be greater than `src` when a small block compacts forward over
|
|
1625
|
+
// the tail of a later block still holding its old data (e.g. string-value
|
|
1626
|
+
// blocks interleaved with array blocks). Copy backwards in that case, like
|
|
1627
|
+
// memmove, so overlapping source/destination ranges aren't corrupted.
|
|
1628
|
+
if (dst > src) {
|
|
1629
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
1630
|
+
buf.setUint8(dst + i, buf.getUint8(src + i));
|
|
1631
|
+
}
|
|
1632
|
+
} else {
|
|
1633
|
+
for (let i = 0; i < len; i++) {
|
|
1634
|
+
buf.setUint8(dst + i, buf.getUint8(src + i));
|
|
1635
|
+
}
|
|
1526
1636
|
}
|
|
1527
1637
|
}
|
|
1528
1638
|
|
|
@@ -1536,11 +1646,16 @@ function remapSlot(
|
|
|
1536
1646
|
offset: number,
|
|
1537
1647
|
stringRemap: Map<number, number>,
|
|
1538
1648
|
arrayRemap: Map<number, number>,
|
|
1649
|
+
objectRemap?: Map<number, number>,
|
|
1539
1650
|
): void {
|
|
1540
1651
|
const type = buf.getInt32(offset);
|
|
1541
1652
|
if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
|
|
1542
1653
|
buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
|
|
1543
1654
|
} else if (type === TYPE_ARRAY_REF || type === TYPE_STRING_VALUE_REF) {
|
|
1544
1655
|
buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
|
|
1656
|
+
} else if (type === TYPE_OBJECT_REF && objectRemap !== undefined) {
|
|
1657
|
+
buf.setInt32(offset + 4, objectRemap.get(buf.getInt32(offset + 4))!);
|
|
1545
1658
|
}
|
|
1659
|
+
// TYPE_KEY_REF (live shared-key reference) needs no remap: it stores the
|
|
1660
|
+
// key's stable Index Table index, which never changes.
|
|
1546
1661
|
}
|