bun-shared-memory 0.1.4 → 0.1.6
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 +129 -0
- package/package.json +4 -5
- package/src/growable-buffer.ts +197 -29
- package/src/memory.ts +159 -160
- package/src/refcount.ts +3 -3
- package/src/shared.ts +250 -196
- package/src/worker.ts +13 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
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.6] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support shared memory larger than 4 GiB. The single-`SharedArrayBuffer`
|
|
13
|
+
per-buffer architecture is replaced with a chunked `GrowableBuffer`: each
|
|
14
|
+
buffer is backed by multiple equal-size chunks (32 MiB default, power of
|
|
15
|
+
two), so a buffer can exceed the 4 GiB single-SAB limit. Offsets resolve
|
|
16
|
+
O(1) via bit shifts (`offset >> chunkShift`).
|
|
17
|
+
- `SharedMemoryOptions.chunkSize` to configure the chunk size (the minimum
|
|
18
|
+
growth step).
|
|
19
|
+
- Chunks are pre-allocated at construction (4 bytes each, with
|
|
20
|
+
`maxByteLength = chunkSize`) and activated on first access via
|
|
21
|
+
`SharedArrayBuffer.grow()`. Because `grow()` is visible across all workers
|
|
22
|
+
sharing a buffer, a worker can grow a chunk and every other worker sees it
|
|
23
|
+
immediately — no postMessage broadcast is needed for chunk activation.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `exportBuffers()`/`fromBuffers()` now pass chunk lists instead of a single
|
|
28
|
+
buffer; `fromBuffers` reuses the shared `SharedArrayBuffer`s directly (no
|
|
29
|
+
copy) so writes stay visible across workers. The chunk size is derived from
|
|
30
|
+
each buffer's `maxByteLength` (preserved across `postMessage`), not its
|
|
31
|
+
current 4-byte length.
|
|
32
|
+
- Remove the 4 GiB cap on the per-buffer maximum in the `SharedMemory`
|
|
33
|
+
constructor (the chunked `GrowableBuffer` already bypasses the single-SAB
|
|
34
|
+
limit).
|
|
35
|
+
|
|
36
|
+
### Performance
|
|
37
|
+
|
|
38
|
+
- After defragmentation, unused chunks are marked inactive:
|
|
39
|
+
`GrowableBuffer.reset()` deactivates chunks beyond the compacted region so
|
|
40
|
+
the GC can reclaim their storage.
|
|
41
|
+
- The chunked accessor adds an indirect `chunks[index].view` lookup on the
|
|
42
|
+
write hot path (the price of >4 GiB support): writes ~-15%, array writes
|
|
43
|
+
~-20%. Reads are unchanged (lock-free cache path).
|
|
44
|
+
|
|
45
|
+
## [0.1.5] - 2026-11-02
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Replace the `Proxy` used for object `SharedRef`s with hard-baked
|
|
50
|
+
`Object.defineProperty` accessor getters/setters, so property reads skip the
|
|
51
|
+
Proxy trap on the hot path (~3× faster object property reads). Methods
|
|
52
|
+
(`read`/`write`/`update`/`key`/`buffer`) move to the prototype so the data
|
|
53
|
+
properties are the only own properties, keeping the JIT shape monomorphic
|
|
54
|
+
(~2× faster than having the methods as own properties). Arrays, primitives,
|
|
55
|
+
and unknown shapes still use the Proxy. Fixed shape is enforced via
|
|
56
|
+
`Object.preventExtensions` plus `configurable: false` accessors.
|
|
57
|
+
- Optimize `copyValue` (the `read()` copy) with `Object.assign` + a `for...in`
|
|
58
|
+
array pass (~3.5× faster object `read()`).
|
|
59
|
+
- Pass the initial value into `createSharedRef` so the object shape can be
|
|
60
|
+
baked at creation time.
|
|
61
|
+
|
|
62
|
+
## [0.1.4] - 2026-11-01
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- Declare `"sideEffects": false` in `package.json` so bundlers can tree-shake
|
|
67
|
+
the library (the modules have no side effects on import).
|
|
68
|
+
- Remove the `typescript` peer dependency. The library ships as TypeScript
|
|
69
|
+
source and runs natively in Bun; `bun check` is Bun's own type checker and
|
|
70
|
+
does not require the `typescript` package.
|
|
71
|
+
|
|
72
|
+
## [0.1.3] - 2026-11-01
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- Real-world benchmark (`bench/real-world/`) comparing shared memory against
|
|
77
|
+
live SQLite, MySQL, and Redis via an Elysia mini API.
|
|
78
|
+
- Redis as a fourth benchmark methodology (in Docker).
|
|
79
|
+
- Unix-socket transport for MySQL/Redis (Linux only; skipped on macOS).
|
|
80
|
+
- Benchmark results written to `benchlogs/<timestamp>-<commit>-real.md`.
|
|
81
|
+
- Real-world benchmark results and methodology documented in the README.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- MySQL/Redis connections are established before the benchmark (not per
|
|
86
|
+
request) so connection setup is not measured as overhead.
|
|
87
|
+
- The benchmark always runs both TCP and Unix-socket transports; the socket
|
|
88
|
+
run is skipped on macOS without error.
|
|
89
|
+
|
|
90
|
+
## [0.1.2] - 2026-11-01
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- Unref the maintenance timer so the process can exit naturally.
|
|
95
|
+
- Document `init()` as the first call and how to configure memory via `init()`.
|
|
96
|
+
|
|
97
|
+
## [0.1.1] - 2026-11-01
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
|
|
101
|
+
- Repository metadata and npm publishing preparation.
|
|
102
|
+
- README with package name and install instructions.
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- Optimize the read hot path: direct cache on the proxy target.
|
|
107
|
+
- Optimize `Mutex`: skip notify when no worker is waiting.
|
|
108
|
+
- Start the refcount coordinator/tracker automatically in `init()`.
|
|
109
|
+
- Document the minimum Bun version (`>= 1.4.0`).
|
|
110
|
+
|
|
111
|
+
## [0.1.0] - 2026-11-01
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
|
|
115
|
+
- Initial release: `Shared<T>` Proxy-based API over a 4-buffer shared memory
|
|
116
|
+
architecture (Index Table, DataTable, append-only StringTable, ArrayTable).
|
|
117
|
+
- Atomics-based synchronization (`Mutex`, `Notifier`).
|
|
118
|
+
- Per-key read cache with cross-worker invalidation and offset cache for
|
|
119
|
+
object property reads.
|
|
120
|
+
- O(1) in-place `push`/`pop` with capacity-based array growth.
|
|
121
|
+
- Benchmarks for all data types vs native.
|
|
122
|
+
|
|
123
|
+
[0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
|
|
124
|
+
[0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
|
|
125
|
+
[0.1.4]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.3...v0.1.4
|
|
126
|
+
[0.1.3]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.2...v0.1.3
|
|
127
|
+
[0.1.2]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.1...v0.1.2
|
|
128
|
+
[0.1.1]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.0...v0.1.1
|
|
129
|
+
[0.1.0]: https://github.com/Kapsonfire/bun-shared-memory/releases/tag/v0.1.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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,
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"index.ts",
|
|
17
|
-
"src/"
|
|
17
|
+
"src/",
|
|
18
|
+
"CHANGELOG.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"typecheck": "bun check",
|
|
@@ -46,8 +47,6 @@
|
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/bun": "latest",
|
|
49
|
-
"elysia": "^1.4.30"
|
|
50
|
-
"ioredis": "^6.0.0",
|
|
51
|
-
"mysql2": "^3.24.6"
|
|
50
|
+
"elysia": "^1.4.30"
|
|
52
51
|
}
|
|
53
52
|
}
|
package/src/growable-buffer.ts
CHANGED
|
@@ -35,33 +35,111 @@ export class SharedBudget {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Growable
|
|
38
|
+
* Growable shared memory backed by one or more SharedArrayBuffers (chunks).
|
|
39
39
|
*
|
|
40
|
-
* SharedArrayBuffer
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* A single SharedArrayBuffer can be at most 4 GiB, so a buffer that needs to
|
|
41
|
+
* grow beyond that is split into multiple chunks. Each chunk is its own
|
|
42
|
+
* SharedArrayBuffer (<= `chunkMax` bytes). Offsets are logical and continuous
|
|
43
|
+
* across chunks; access methods resolve an offset into (chunk, chunkOffset).
|
|
44
|
+
*
|
|
45
|
+
* All chunks are pre-allocated at construction with `maxByteLength = chunkSize`
|
|
46
|
+
* but only 4 bytes of actual storage. A chunk is "activated" (grown to its
|
|
47
|
+
* full `chunkSize`) on first access. Because `SharedArrayBuffer.grow()` is
|
|
48
|
+
* visible across all workers sharing the buffer, activation needs no
|
|
49
|
+
* postMessage broadcast.
|
|
44
50
|
*/
|
|
51
|
+
interface Chunk {
|
|
52
|
+
buffer: SharedArrayBuffer;
|
|
53
|
+
view: DataView;
|
|
54
|
+
/** Whether the chunk has been grown to its full size. */
|
|
55
|
+
active: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Round `n` up to the next power of two (chunk sizes must be powers of two). */
|
|
59
|
+
function nextPowerOfTwo(n: number): number {
|
|
60
|
+
if (n <= 0) return 1;
|
|
61
|
+
let p = 1;
|
|
62
|
+
while (p < n) p <<= 1;
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
export class GrowableBuffer {
|
|
46
|
-
|
|
47
|
-
private view: DataView;
|
|
67
|
+
private chunks: Chunk[] = [];
|
|
48
68
|
private readonly maxByteLength: number;
|
|
69
|
+
/** Fixed size of every chunk (the minimum growth step). */
|
|
70
|
+
readonly chunkSize: number;
|
|
71
|
+
/** log2(chunkSize), used for O(1) offset resolution via bit shifts. */
|
|
72
|
+
private readonly chunkShift: number;
|
|
49
73
|
private readonly budget?: SharedBudget;
|
|
50
74
|
private used = 0;
|
|
51
75
|
|
|
52
76
|
constructor(
|
|
53
|
-
initialSizeOrBuffer: number | SharedArrayBuffer,
|
|
77
|
+
initialSizeOrBuffer: number | SharedArrayBuffer | SharedArrayBuffer[],
|
|
54
78
|
maxByteLength: number,
|
|
55
79
|
budget?: SharedBudget,
|
|
80
|
+
chunkSize = 32 * 1024 * 1024, // 32 MiB per chunk
|
|
56
81
|
) {
|
|
57
|
-
this.buffer =
|
|
58
|
-
typeof initialSizeOrBuffer === "number"
|
|
59
|
-
? new SharedArrayBuffer(initialSizeOrBuffer, { maxByteLength })
|
|
60
|
-
: initialSizeOrBuffer;
|
|
61
|
-
this.view = new DataView(this.buffer);
|
|
62
82
|
this.maxByteLength = maxByteLength;
|
|
63
83
|
this.budget = budget;
|
|
64
|
-
|
|
84
|
+
if (typeof initialSizeOrBuffer === "number") {
|
|
85
|
+
this.chunkSize = Math.min(nextPowerOfTwo(chunkSize), maxByteLength);
|
|
86
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
87
|
+
// Pre-allocate all chunks (4 bytes each, growable to chunkSize).
|
|
88
|
+
const chunkCount = Math.max(1, Math.ceil(maxByteLength / this.chunkSize));
|
|
89
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
90
|
+
const buffer = new SharedArrayBuffer(4, {
|
|
91
|
+
maxByteLength: this.chunkSize,
|
|
92
|
+
});
|
|
93
|
+
this.chunks.push({
|
|
94
|
+
buffer,
|
|
95
|
+
view: new DataView(buffer),
|
|
96
|
+
active: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} else if (Array.isArray(initialSizeOrBuffer)) {
|
|
100
|
+
// Reconstruct from a list of chunks. Use the buffers directly (they are
|
|
101
|
+
// already shared via postMessage) so writes are visible across workers.
|
|
102
|
+
// All chunks are pre-allocated at 4 bytes with maxByteLength = chunkSize;
|
|
103
|
+
// the chunk size is derived from each buffer's maxByteLength (which is
|
|
104
|
+
// preserved across postMessage), not its current byteLength.
|
|
105
|
+
this.chunkSize = initialSizeOrBuffer[0]?.maxByteLength ?? chunkSize;
|
|
106
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
107
|
+
for (const sab of initialSizeOrBuffer) {
|
|
108
|
+
this.chunks.push({
|
|
109
|
+
buffer: sab,
|
|
110
|
+
view: new DataView(sab),
|
|
111
|
+
active: sab.byteLength === this.chunkSize,
|
|
112
|
+
});
|
|
113
|
+
this.budget?.add(sab.byteLength);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Reconstruct from a single existing buffer. Use it directly so writes
|
|
117
|
+
// are shared. Its size defines the chunk size (prefer maxByteLength so
|
|
118
|
+
// a pre-allocated 4-byte chunk is interpreted at its full size).
|
|
119
|
+
this.chunkSize =
|
|
120
|
+
initialSizeOrBuffer.maxByteLength ?? initialSizeOrBuffer.byteLength;
|
|
121
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
122
|
+
this.chunks.push({
|
|
123
|
+
buffer: initialSizeOrBuffer,
|
|
124
|
+
view: new DataView(initialSizeOrBuffer),
|
|
125
|
+
active: initialSizeOrBuffer.byteLength === this.chunkSize,
|
|
126
|
+
});
|
|
127
|
+
this.budget?.add(initialSizeOrBuffer.byteLength);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Activate the chunk at `index` by growing it to its full size. Because
|
|
133
|
+
* `SharedArrayBuffer.grow()` is visible across all workers, no broadcast is
|
|
134
|
+
* needed. No-op if the chunk is already active.
|
|
135
|
+
*/
|
|
136
|
+
private activate(index: number): void {
|
|
137
|
+
const chunk = this.chunks[index];
|
|
138
|
+
if (chunk === undefined || chunk.active) return;
|
|
139
|
+
chunk.buffer.grow(this.chunkSize);
|
|
140
|
+
chunk.view = new DataView(chunk.buffer);
|
|
141
|
+
chunk.active = true;
|
|
142
|
+
this.budget?.add(this.chunkSize);
|
|
65
143
|
}
|
|
66
144
|
|
|
67
145
|
/** Number of bytes written so far (used length, not buffer capacity). */
|
|
@@ -69,24 +147,49 @@ export class GrowableBuffer {
|
|
|
69
147
|
return this.used;
|
|
70
148
|
}
|
|
71
149
|
|
|
150
|
+
/** Total logical capacity across all chunks (active or not). */
|
|
151
|
+
get capacity(): number {
|
|
152
|
+
return this.chunks.length * this.chunkSize;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The list of SharedArrayBuffers backing this buffer (for export). */
|
|
156
|
+
get buffers(): SharedArrayBuffer[] {
|
|
157
|
+
return this.chunks.map((c) => c.buffer);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Number of chunks that have been activated (grown to full size). */
|
|
161
|
+
get activeChunkCount(): number {
|
|
162
|
+
return this.chunks.reduce((n, c) => n + (c.active ? 1 : 0), 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a logical offset into (chunk, offset within that chunk). All
|
|
167
|
+
* chunks are the same size, so this is O(1). Activates the chunk if it is
|
|
168
|
+
* not yet active.
|
|
169
|
+
*/
|
|
170
|
+
private resolve(offset: number): { chunk: Chunk; off: number } {
|
|
171
|
+
const index = offset >> this.chunkShift;
|
|
172
|
+
const chunk = this.chunks[index];
|
|
173
|
+
if (chunk === undefined) {
|
|
174
|
+
throw new RangeError(`Offset ${offset} is beyond the buffer capacity`);
|
|
175
|
+
}
|
|
176
|
+
this.activate(index);
|
|
177
|
+
return { chunk, off: offset & (this.chunkSize - 1) };
|
|
178
|
+
}
|
|
179
|
+
|
|
72
180
|
/** Ensure the buffer has room for at least `needed` bytes. */
|
|
73
181
|
ensureCapacity(needed: number): void {
|
|
74
|
-
if (needed <= this.buffer.byteLength) return;
|
|
75
182
|
if (needed > this.maxByteLength) {
|
|
76
183
|
throw new RangeError(
|
|
77
184
|
`Shared buffer capacity exceeded: need ${needed} bytes but max is ${this.maxByteLength}`,
|
|
78
185
|
);
|
|
79
186
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`Shared memory budget exceeded: growing to ${newSize} bytes would exceed the total budget`,
|
|
86
|
-
);
|
|
187
|
+
// All chunks are pre-allocated; just activate the ones needed to cover
|
|
188
|
+
// `needed`. grow() is visible across workers, so no broadcast is required.
|
|
189
|
+
const neededChunks = Math.ceil(needed / this.chunkSize);
|
|
190
|
+
for (let i = 0; i < neededChunks; i++) {
|
|
191
|
+
this.activate(i);
|
|
87
192
|
}
|
|
88
|
-
this.buffer.grow(newSize);
|
|
89
|
-
this.view = new DataView(this.buffer);
|
|
90
193
|
}
|
|
91
194
|
|
|
92
195
|
/** Mark `n` bytes as used, after writing them at the current offset. */
|
|
@@ -96,15 +199,80 @@ export class GrowableBuffer {
|
|
|
96
199
|
|
|
97
200
|
/**
|
|
98
201
|
* Reset the used length to `n` bytes. Used by defragmentation after
|
|
99
|
-
* compacting live data to the front of the buffer.
|
|
100
|
-
*
|
|
101
|
-
*
|
|
202
|
+
* compacting live data to the front of the buffer. Chunks beyond the
|
|
203
|
+
* compacted region are marked inactive (their storage is released by the
|
|
204
|
+
* GC, which replaces them with fresh chunks).
|
|
102
205
|
*/
|
|
103
206
|
reset(n: number): void {
|
|
104
207
|
this.used = n;
|
|
208
|
+
const neededChunks = Math.ceil(n / this.chunkSize);
|
|
209
|
+
for (let i = neededChunks; i < this.chunks.length; i++) {
|
|
210
|
+
this.chunks[i]!.active = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
// Typed accessors (resolve offset -> chunk)
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
getInt32(offset: number): number {
|
|
219
|
+
const index = offset >> this.chunkShift;
|
|
220
|
+
this.activate(index);
|
|
221
|
+
return this.chunks[index]!.view.getInt32(
|
|
222
|
+
offset & (this.chunkSize - 1),
|
|
223
|
+
true,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setInt32(offset: number, value: number): void {
|
|
228
|
+
const index = offset >> this.chunkShift;
|
|
229
|
+
this.activate(index);
|
|
230
|
+
this.chunks[index]!.view.setInt32(
|
|
231
|
+
offset & (this.chunkSize - 1),
|
|
232
|
+
value,
|
|
233
|
+
true,
|
|
234
|
+
);
|
|
105
235
|
}
|
|
106
236
|
|
|
107
|
-
|
|
108
|
-
|
|
237
|
+
getUint8(offset: number): number {
|
|
238
|
+
const index = offset >> this.chunkShift;
|
|
239
|
+
this.activate(index);
|
|
240
|
+
return this.chunks[index]!.view.getUint8(offset & (this.chunkSize - 1));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setUint8(offset: number, value: number): void {
|
|
244
|
+
const index = offset >> this.chunkShift;
|
|
245
|
+
this.activate(index);
|
|
246
|
+
this.chunks[index]!.view.setUint8(offset & (this.chunkSize - 1), value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Read `length` bytes starting at `offset` into a fresh Uint8Array. */
|
|
250
|
+
readBytes(offset: number, length: number): Uint8Array {
|
|
251
|
+
const out = new Uint8Array(length);
|
|
252
|
+
let pos = offset;
|
|
253
|
+
let outPos = 0;
|
|
254
|
+
while (outPos < length) {
|
|
255
|
+
const { chunk, off } = this.resolve(pos);
|
|
256
|
+
const n = Math.min(length - outPos, this.chunkSize - off);
|
|
257
|
+
out.set(new Uint8Array(chunk.buffer, off, n), outPos);
|
|
258
|
+
outPos += n;
|
|
259
|
+
pos += n;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Write `bytes` starting at `offset`. */
|
|
265
|
+
writeBytes(offset: number, bytes: Uint8Array): void {
|
|
266
|
+
let pos = offset;
|
|
267
|
+
let srcPos = 0;
|
|
268
|
+
while (srcPos < bytes.byteLength) {
|
|
269
|
+
const { chunk, off } = this.resolve(pos);
|
|
270
|
+
const n = Math.min(bytes.byteLength - srcPos, this.chunkSize - off);
|
|
271
|
+
new Uint8Array(chunk.buffer, off, n).set(
|
|
272
|
+
bytes.subarray(srcPos, srcPos + n),
|
|
273
|
+
);
|
|
274
|
+
srcPos += n;
|
|
275
|
+
pos += n;
|
|
276
|
+
}
|
|
109
277
|
}
|
|
110
278
|
}
|