@nxtedition/shared 3.0.1 → 4.0.0
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/README.md +106 -34
- package/lib/bench-worker.d.mts +1 -0
- package/lib/bench.d.mts +1 -0
- package/lib/index.d.ts +42 -29
- package/lib/index.js +238 -213
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Reads are zero-copy: the reader callback receives a `DataView` directly into the
|
|
|
12
12
|
|
|
13
13
|
## Platform Assumptions
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
All messages are aligned on 4-byte boundaries. Message length headers are read and written via `Int32Array` indexing rather than `DataView`, avoiding per-access endianness checks on the hot path.
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -23,25 +23,25 @@ npm install @nxtedition/shared
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
25
|
```js
|
|
26
|
-
import {
|
|
26
|
+
import { State, Reader, Writer } from '@nxtedition/shared'
|
|
27
27
|
|
|
28
|
-
// Allocate shared memory (pass
|
|
29
|
-
const
|
|
28
|
+
// Allocate shared memory (pass state.buffer to a worker thread)
|
|
29
|
+
const state = new State(1024 * 1024) // 1 MB ring buffer
|
|
30
30
|
|
|
31
31
|
// --- Writer side (e.g. main thread) ---
|
|
32
|
-
const w =
|
|
32
|
+
const w = new Writer(state)
|
|
33
33
|
|
|
34
34
|
const payload = Buffer.from('hello world')
|
|
35
35
|
w.writeSync(payload.length, (data) => {
|
|
36
|
-
payload.copy(data.buffer, data.
|
|
37
|
-
return data.
|
|
36
|
+
payload.copy(data.buffer, data.byteOffset)
|
|
37
|
+
return data.byteOffset + payload.length
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
// --- Reader side (e.g. worker thread) ---
|
|
41
|
-
const r =
|
|
41
|
+
const r = new Reader(state)
|
|
42
42
|
|
|
43
43
|
r.readSome((data) => {
|
|
44
|
-
const msg = data.buffer.subarray(data.
|
|
44
|
+
const msg = data.buffer.subarray(data.byteOffset, data.byteOffset + data.byteLength).toString()
|
|
45
45
|
console.log(msg) // 'hello world'
|
|
46
46
|
})
|
|
47
47
|
```
|
|
@@ -53,8 +53,8 @@ w.cork(() => {
|
|
|
53
53
|
for (const item of items) {
|
|
54
54
|
const buf = Buffer.from(JSON.stringify(item))
|
|
55
55
|
w.writeSync(buf.length, (data) => {
|
|
56
|
-
buf.copy(data.buffer, data.
|
|
57
|
-
return data.
|
|
56
|
+
buf.copy(data.buffer, data.byteOffset)
|
|
57
|
+
return data.byteOffset + buf.length
|
|
58
58
|
})
|
|
59
59
|
}
|
|
60
60
|
})
|
|
@@ -66,8 +66,8 @@ w.cork(() => {
|
|
|
66
66
|
```js
|
|
67
67
|
const buf = Buffer.from('data')
|
|
68
68
|
const ok = w.tryWrite(buf.length, (data) => {
|
|
69
|
-
buf.copy(data.buffer, data.
|
|
70
|
-
return data.
|
|
69
|
+
buf.copy(data.buffer, data.byteOffset)
|
|
70
|
+
return data.byteOffset + buf.length
|
|
71
71
|
})
|
|
72
72
|
if (!ok) {
|
|
73
73
|
// Buffer is full — the reader hasn't caught up yet
|
|
@@ -78,28 +78,29 @@ if (!ok) {
|
|
|
78
78
|
|
|
79
79
|
```js
|
|
80
80
|
// main.js
|
|
81
|
-
import {
|
|
81
|
+
import { State, Writer } from '@nxtedition/shared'
|
|
82
82
|
import { Worker } from 'node:worker_threads'
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const state = new State(1024 * 1024)
|
|
85
85
|
const worker = new Worker('./reader-worker.js', {
|
|
86
|
-
workerData:
|
|
86
|
+
workerData: state.buffer,
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
-
const w =
|
|
89
|
+
const w = new Writer(state)
|
|
90
90
|
// ... write messages
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
```js
|
|
94
94
|
// reader-worker.js
|
|
95
|
-
import {
|
|
95
|
+
import { State, Reader } from '@nxtedition/shared'
|
|
96
96
|
import { workerData } from 'node:worker_threads'
|
|
97
97
|
|
|
98
|
-
const
|
|
98
|
+
const state = new State(workerData)
|
|
99
|
+
const r = new Reader(state)
|
|
99
100
|
|
|
100
101
|
function poll() {
|
|
101
102
|
const count = r.readSome((data) => {
|
|
102
|
-
// process data.buffer at data.
|
|
103
|
+
// process data.buffer at data.byteOffset..data.byteOffset+data.byteLength
|
|
103
104
|
})
|
|
104
105
|
setImmediate(poll)
|
|
105
106
|
}
|
|
@@ -108,31 +109,31 @@ poll()
|
|
|
108
109
|
|
|
109
110
|
## API
|
|
110
111
|
|
|
111
|
-
### `
|
|
112
|
+
### `new State(size: number)` / `new State(buffer: SharedArrayBuffer)`
|
|
112
113
|
|
|
113
|
-
Allocates
|
|
114
|
+
Allocates or wraps a shared memory buffer for the ring buffer. The first 128 bytes are reserved for state (read/write pointers); the rest is the data region.
|
|
114
115
|
|
|
115
|
-
- **size** —
|
|
116
|
-
-
|
|
116
|
+
- **size** — Data capacity in bytes (must be a positive integer, max ~2 GB)
|
|
117
|
+
- **buffer** — An existing `SharedArrayBuffer` to wrap
|
|
117
118
|
|
|
118
|
-
### `
|
|
119
|
+
### `new Reader(state: State)`
|
|
119
120
|
|
|
120
121
|
Creates a reader for the ring buffer.
|
|
121
122
|
|
|
122
|
-
#### `reader.readSome(next)
|
|
123
|
+
#### `reader.readSome(next)`
|
|
123
124
|
|
|
124
125
|
Reads a batch of messages. Calls `next(data)` for each message, where `data` has:
|
|
125
126
|
|
|
126
127
|
- `buffer: Buffer` — The underlying shared buffer
|
|
127
128
|
- `view: DataView` — A DataView over the shared buffer
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
129
|
+
- `byteOffset: number` — Start offset of the message payload
|
|
130
|
+
- `byteLength: number` — Length of the message payload in bytes
|
|
130
131
|
|
|
131
132
|
Return `false` from the callback to stop reading early. Returns the number of messages processed.
|
|
132
133
|
|
|
133
134
|
Messages are batched: up to 1024 items or 256 KiB per call.
|
|
134
135
|
|
|
135
|
-
### `
|
|
136
|
+
### `new Writer(state: State, options?)`
|
|
136
137
|
|
|
137
138
|
Creates a writer for the ring buffer.
|
|
138
139
|
|
|
@@ -141,22 +142,93 @@ Creates a writer for the ring buffer.
|
|
|
141
142
|
- `yield?: () => void` — Called when the writer must wait for the reader to catch up. Useful to prevent deadlocks when the writer thread also drives the reader.
|
|
142
143
|
- `logger?: { warn(obj, msg): void }` — Logger for yield warnings (pino-compatible).
|
|
143
144
|
|
|
144
|
-
#### `writer.writeSync(len, fn
|
|
145
|
+
#### `writer.writeSync(len, fn)`
|
|
145
146
|
|
|
146
147
|
Synchronously writes a message. Blocks (via `Atomics.wait`) until buffer space is available.
|
|
147
148
|
|
|
148
149
|
- **len** — Maximum payload size in bytes. Writing beyond `len` bytes in the callback is undefined behavior.
|
|
149
|
-
- **fn(data) → number** — Write callback. Write payload into `data.buffer` starting at `data.
|
|
150
|
-
- **timeout** — Max wait time in ms (default: 60000). Throws on timeout.
|
|
150
|
+
- **fn(data) → number** — Write callback. Write payload into `data.buffer` starting at `data.byteOffset`. **Must return the end position** (`data.byteOffset + bytesWritten`), not the byte count.
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
Throws on timeout (default: 60000 ms).
|
|
153
|
+
|
|
154
|
+
#### `writer.tryWrite(len, fn)`
|
|
153
155
|
|
|
154
156
|
Non-blocking write attempt. Returns `false` if the buffer is full. The `fn` callback follows the same contract as `writeSync`.
|
|
155
157
|
|
|
156
|
-
#### `writer.cork(callback)
|
|
158
|
+
#### `writer.cork(callback)`
|
|
157
159
|
|
|
158
160
|
Batches multiple writes within the callback. The write pointer is only published to the reader when `cork` returns, reducing atomic operation overhead.
|
|
159
161
|
|
|
162
|
+
## Benchmarks
|
|
163
|
+
|
|
164
|
+
Measured on Apple M3 Pro (3.51 GHz), Node.js 25.6.1, 8 MiB ring buffer.
|
|
165
|
+
|
|
166
|
+
Each benchmark writes batches of fixed-size messages from the main thread and
|
|
167
|
+
reads them in a worker thread. The shared ring buffer is compared against
|
|
168
|
+
Node.js `postMessage` (structured clone). Hardware performance counters were
|
|
169
|
+
collected with [`@mitata/counters`](https://github.com/evanwashere/mitata).
|
|
170
|
+
|
|
171
|
+
### Throughput
|
|
172
|
+
|
|
173
|
+
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
174
|
+
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
175
|
+
| 64 B | **1.64 GiB/s** | 753 MiB/s | 96 MiB/s | 120 MiB/s |
|
|
176
|
+
| 256 B | **3.13 GiB/s** | 2.46 GiB/s | 360 MiB/s | 464 MiB/s |
|
|
177
|
+
| 1 KiB | 4.81 GiB/s | **7.56 GiB/s** | 1.31 GiB/s | 1.68 GiB/s |
|
|
178
|
+
| 4 KiB | 5.04 GiB/s | **16.44 GiB/s** | 3.91 GiB/s | 5.13 GiB/s |
|
|
179
|
+
| 16 KiB | 5.10 GiB/s | **22.24 GiB/s** | 9.07 GiB/s | 7.60 GiB/s |
|
|
180
|
+
| 64 KiB | 5.33 GiB/s | **12.42 GiB/s** | 8.85 GiB/s | 13.20 GiB/s |
|
|
181
|
+
|
|
182
|
+
### Message rate
|
|
183
|
+
|
|
184
|
+
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
185
|
+
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
186
|
+
| 64 B | **27.53 M/s** | 12.33 M/s | 1.57 M/s | 1.97 M/s |
|
|
187
|
+
| 256 B | **13.14 M/s** | 10.31 M/s | 1.47 M/s | 1.90 M/s |
|
|
188
|
+
| 1 KiB | 5.04 M/s | **7.93 M/s** | 1.38 M/s | 1.77 M/s |
|
|
189
|
+
| 4 KiB | 1.32 M/s | **4.31 M/s** | 1.02 M/s | 1.35 M/s |
|
|
190
|
+
| 16 KiB | 334 K/s | **1.46 M/s** | 594 K/s | 498 K/s |
|
|
191
|
+
| 64 KiB | 87 K/s | **203 K/s** | 145 K/s | 216 K/s |
|
|
192
|
+
|
|
193
|
+
### CPU efficiency (instructions per cycle)
|
|
194
|
+
|
|
195
|
+
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
196
|
+
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
197
|
+
| 64 B | 5.45 | 5.62 | 3.89 | 3.35 |
|
|
198
|
+
| 256 B | 4.52 | 5.76 | 3.82 | 3.18 |
|
|
199
|
+
| 1 KiB | 4.18 | **6.25** | 3.65 | 3.16 |
|
|
200
|
+
| 4 KiB | 3.79 | **6.65** | 3.51 | 2.92 |
|
|
201
|
+
| 16 KiB | 3.73 | **6.12** | 2.91 | 2.63 |
|
|
202
|
+
| 64 KiB | 3.93 | **4.30** | 2.44 | 2.92 |
|
|
203
|
+
|
|
204
|
+
### Key findings
|
|
205
|
+
|
|
206
|
+
- **Small messages (64-256 B):** The shared ring buffer with `Buffer.copy` delivers
|
|
207
|
+
up to **18x higher message rate** and **14x higher throughput** than `postMessage`.
|
|
208
|
+
Per-message overhead dominates at these sizes, and avoiding structured cloning makes
|
|
209
|
+
the biggest difference. The `Int32Array` meta path is especially effective here —
|
|
210
|
+
27.5 M msg/s at 64 B with 5.45 IPC.
|
|
211
|
+
|
|
212
|
+
- **Large messages (1-64 KiB):** The shared ring buffer with string encoding
|
|
213
|
+
(`Buffer.write`) reaches up to **22 GiB/s** — roughly **2-4x faster** than
|
|
214
|
+
`postMessage`. V8's ASCII fast path for UTF-8 encoding is heavily vectorized
|
|
215
|
+
(6-7 IPC on Apple M3 Pro), which explains why string writes outperform raw
|
|
216
|
+
`Buffer.copy` at larger sizes.
|
|
217
|
+
|
|
218
|
+
- **CPU efficiency:** The shared ring buffer consistently achieves higher IPC
|
|
219
|
+
(4-7) compared to `postMessage` (2-4), indicating less time spent stalled on
|
|
220
|
+
memory or synchronization.
|
|
221
|
+
|
|
222
|
+
- **Caveat:** The string benchmark uses ASCII-only content. Multi-byte UTF-8
|
|
223
|
+
strings will not hit V8's vectorized fast path and will be significantly slower.
|
|
224
|
+
|
|
225
|
+
### Running the benchmark
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
# Hardware counters require elevated privileges on macOS
|
|
229
|
+
sudo node --allow-natives-syntax packages/shared/src/bench.mjs
|
|
230
|
+
```
|
|
231
|
+
|
|
160
232
|
## License
|
|
161
233
|
|
|
162
234
|
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/bench.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,44 +1,57 @@
|
|
|
1
|
-
export interface SharedBuffers {
|
|
2
|
-
sharedState: SharedArrayBuffer;
|
|
3
|
-
sharedBuffer: SharedArrayBuffer;
|
|
4
|
-
}
|
|
5
1
|
export interface BufferRegion {
|
|
6
2
|
buffer: Buffer;
|
|
7
3
|
view: DataView;
|
|
8
|
-
offset: number;
|
|
9
|
-
length: number;
|
|
10
4
|
byteOffset: number;
|
|
11
5
|
byteLength: number;
|
|
12
6
|
}
|
|
13
|
-
/**
|
|
14
|
-
* Allocates the shared memory buffers.
|
|
15
|
-
*/
|
|
16
|
-
export declare function alloc(size: number): SharedBuffers;
|
|
17
|
-
export interface Reader {
|
|
18
|
-
readSome<U>(next: (data: BufferRegion, opaque: U) => void | boolean, opaque: U): number;
|
|
19
|
-
readSome(next: (data: BufferRegion) => void | boolean): number;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Creates a reader for the ring buffer.
|
|
23
|
-
*/
|
|
24
|
-
export declare function reader({ sharedState, sharedBuffer }: SharedBuffers): Reader;
|
|
25
7
|
export interface WriterOptions {
|
|
26
8
|
yield?: () => void;
|
|
27
9
|
logger?: {
|
|
28
10
|
warn(obj: object, msg: string): void;
|
|
29
11
|
};
|
|
30
12
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Shared ring buffer state. Allocates or wraps a SharedArrayBuffer where the
|
|
15
|
+
* first 128 bytes are reserved for read/write pointers and the rest is the
|
|
16
|
+
* data region.
|
|
17
|
+
*/
|
|
18
|
+
export declare class State {
|
|
19
|
+
readonly buffer: SharedArrayBuffer;
|
|
20
|
+
constructor(size: number);
|
|
21
|
+
constructor(buffer: SharedArrayBuffer);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Reader for the ring buffer.
|
|
25
|
+
*/
|
|
26
|
+
export declare class Reader {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(state: State);
|
|
29
|
+
readSome<U>(next: (data: BufferRegion, opaque?: U) => void | boolean, opaque?: U): number;
|
|
40
30
|
}
|
|
41
31
|
/**
|
|
42
|
-
*
|
|
32
|
+
* Writer for the ring buffer.
|
|
43
33
|
*/
|
|
44
|
-
export declare
|
|
34
|
+
export declare class Writer {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(state: State, { yield: onYield, logger }?: WriterOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Synchronously writes a message. Blocks (via `Atomics.wait`) until buffer space is available.
|
|
39
|
+
* Writing more than "len" bytes in the callback will cause undefined behavior.
|
|
40
|
+
*/
|
|
41
|
+
writeSync<U>(len: number, fn: (data: BufferRegion, opaque?: U) => number, opaque?: U): void;
|
|
42
|
+
/**
|
|
43
|
+
* Non-blocking write attempt. Returns `false` if the buffer is full.
|
|
44
|
+
* Writing more than "len" bytes in the callback will cause undefined behavior.
|
|
45
|
+
*/
|
|
46
|
+
tryWrite<U>(len: number, fn: (data: BufferRegion, opaque?: U) => number, opaque?: U): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Batches multiple writes within the callback. The write pointer is only
|
|
49
|
+
* published to the reader when cork returns, reducing atomic operation overhead.
|
|
50
|
+
*/
|
|
51
|
+
cork<T>(callback?: () => T): T | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Publishes the pending write position to the reader.
|
|
54
|
+
*/
|
|
55
|
+
uncork(): void;
|
|
56
|
+
flushSync(): void;
|
|
57
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -7,90 +7,106 @@
|
|
|
7
7
|
const WRITE_INDEX = 0
|
|
8
8
|
const READ_INDEX = 16
|
|
9
9
|
|
|
10
|
+
// The first 128 bytes of the buffer are reserved for state (read/write pointers).
|
|
11
|
+
// Data starts at byte offset 128.
|
|
12
|
+
const STATE_BYTES = 128
|
|
13
|
+
|
|
10
14
|
// High-Water Mark for batching operations to reduce the frequency
|
|
11
15
|
// of expensive atomic writes.
|
|
12
16
|
const HWM_BYTES = 256 * 1024 // 256 KiB
|
|
13
17
|
const HWM_COUNT = 1024 // 1024 items
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
|
|
28
25
|
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
29
31
|
/**
|
|
30
|
-
* Allocates
|
|
32
|
+
* Shared ring buffer state. Allocates or wraps a SharedArrayBuffer where the
|
|
33
|
+
* first 128 bytes are reserved for read/write pointers and the rest is the
|
|
34
|
+
* data region.
|
|
31
35
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
export class State {
|
|
37
|
+
buffer
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
constructor(sizeOrBuffer ) {
|
|
42
|
+
if (sizeOrBuffer instanceof SharedArrayBuffer) {
|
|
43
|
+
if (sizeOrBuffer.byteLength < STATE_BYTES + 8) {
|
|
44
|
+
throw new RangeError('SharedArrayBuffer too small for ring buffer state')
|
|
45
|
+
}
|
|
46
|
+
if (sizeOrBuffer.byteLength >= 2 ** 31) {
|
|
47
|
+
throw new RangeError('Shared buffer size exceeds maximum of 2GB')
|
|
48
|
+
}
|
|
49
|
+
this.buffer = sizeOrBuffer
|
|
50
|
+
} else {
|
|
51
|
+
const size = sizeOrBuffer
|
|
52
|
+
if (!Number.isInteger(size)) {
|
|
53
|
+
throw new TypeError('size must be a positive integer')
|
|
54
|
+
}
|
|
55
|
+
if (size <= 0) {
|
|
56
|
+
throw new RangeError('size must be a positive integer')
|
|
57
|
+
}
|
|
58
|
+
if (size >= 2 ** 31 - 11) {
|
|
59
|
+
throw new RangeError('size exceeds maximum of 2GB minus header size')
|
|
60
|
+
}
|
|
61
|
+
// 128 bytes for state + data region (rounded up to 4-byte boundary for Int32Array).
|
|
62
|
+
this.buffer = new SharedArrayBuffer(STATE_BYTES + ((size + 8 + 3) & ~3))
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
67
|
/**
|
|
58
|
-
*
|
|
68
|
+
* Reader for the ring buffer.
|
|
59
69
|
*/
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
export class Reader {
|
|
71
|
+
#state
|
|
72
|
+
#size
|
|
73
|
+
#int32
|
|
74
|
+
#data
|
|
75
|
+
#readPos
|
|
76
|
+
|
|
77
|
+
constructor(state ) {
|
|
78
|
+
const sharedBuffer = state.buffer
|
|
79
|
+
const size = sharedBuffer.byteLength - STATE_BYTES
|
|
80
|
+
|
|
81
|
+
this.#state = new Int32Array(sharedBuffer, 0, STATE_BYTES >> 2)
|
|
82
|
+
this.#size = size
|
|
83
|
+
this.#int32 = new Int32Array(sharedBuffer, STATE_BYTES)
|
|
84
|
+
|
|
85
|
+
// This object is reused to avoid creating new objects in a hot path.
|
|
86
|
+
// This helps V8 maintain a stable hidden class for the object,
|
|
87
|
+
// which is a key optimization (zero-copy read).
|
|
88
|
+
this.#data = {
|
|
89
|
+
buffer: Buffer.from(sharedBuffer, STATE_BYTES, size),
|
|
90
|
+
view: new DataView(sharedBuffer, STATE_BYTES, size),
|
|
91
|
+
byteOffset: 0,
|
|
92
|
+
byteLength: 0,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Local copy of the pointer. The `| 0` is a hint to the V8 JIT
|
|
96
|
+
// compiler that this is a 32-bit integer, enabling optimizations.
|
|
97
|
+
this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
const size = sharedBuffer.byteLength
|
|
73
|
-
const buffer = Buffer.from(sharedBuffer)
|
|
74
|
-
const view = new DataView(sharedBuffer)
|
|
75
|
-
|
|
76
|
-
// This object is reused to avoid creating new objects in a hot path.
|
|
77
|
-
// This helps V8 maintain a stable hidden class for the object,
|
|
78
|
-
// which is a key optimization (zero-copy read).
|
|
79
|
-
const data = { buffer, view, offset: 0, length: 0, byteOffset: 0, byteLength: 0 }
|
|
80
|
-
|
|
81
|
-
// Local copies of the pointers. The `| 0` is a hint to the V8 JIT
|
|
82
|
-
// compiler that these are 32-bit integers, enabling optimizations.
|
|
83
|
-
let readPos = Atomics.load(state, READ_INDEX) | 0
|
|
84
|
-
let writePos = Atomics.load(state, WRITE_INDEX) | 0
|
|
85
|
-
|
|
86
|
-
function readSome (
|
|
87
|
-
next ,
|
|
88
|
-
opaque ,
|
|
89
|
-
) {
|
|
100
|
+
readSome (next , opaque ) {
|
|
90
101
|
let count = 0
|
|
91
102
|
let bytes = 0
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
const state = this.#state
|
|
105
|
+
const int32 = this.#int32
|
|
106
|
+
const size = this.#size
|
|
107
|
+
const data = this.#data
|
|
108
|
+
let readPos = this.#readPos
|
|
109
|
+
let writePos = state[WRITE_INDEX] | 0
|
|
94
110
|
|
|
95
111
|
// First, check if the local writePos matches the readPos.
|
|
96
112
|
// If so, refresh it from shared memory in case the writer has added data.
|
|
@@ -100,8 +116,8 @@ export function reader({ sharedState, sharedBuffer } ) {
|
|
|
100
116
|
|
|
101
117
|
// Process messages in a batch to minimize loop and atomic operation overhead.
|
|
102
118
|
while (count < HWM_COUNT && bytes < HWM_BYTES && readPos !== writePos) {
|
|
119
|
+
const dataLen = int32[readPos >> 2] | 0
|
|
103
120
|
const dataPos = readPos + 4
|
|
104
|
-
const dataLen = view.getInt32(dataPos - 4, true) | 0
|
|
105
121
|
|
|
106
122
|
bytes += 4
|
|
107
123
|
|
|
@@ -120,16 +136,16 @@ export function reader({ sharedState, sharedBuffer } ) {
|
|
|
120
136
|
throw new Error('Data exceeds buffer size')
|
|
121
137
|
}
|
|
122
138
|
|
|
123
|
-
|
|
139
|
+
// Advance by aligned length so next header is on a 4-byte boundary.
|
|
140
|
+
const alignedLen = (dataLen + 3) & ~3
|
|
141
|
+
readPos += 4 + alignedLen
|
|
124
142
|
|
|
125
|
-
bytes +=
|
|
143
|
+
bytes += alignedLen
|
|
126
144
|
count += 1
|
|
127
145
|
|
|
128
146
|
// This is a "zero-copy" operation. We don't copy the data out.
|
|
129
147
|
// Instead, we pass a "view" into the shared buffer.
|
|
130
|
-
data.offset = dataPos
|
|
131
148
|
data.byteOffset = dataPos
|
|
132
|
-
data.length = dataLen
|
|
133
149
|
data.byteLength = dataLen
|
|
134
150
|
|
|
135
151
|
if (next(data, opaque) === false) {
|
|
@@ -138,6 +154,8 @@ export function reader({ sharedState, sharedBuffer } ) {
|
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
this.#readPos = readPos
|
|
158
|
+
|
|
141
159
|
// IMPORTANT: The reader only updates its shared `readPos` after a batch
|
|
142
160
|
// is processed. This significantly reduces atomic operation overhead.
|
|
143
161
|
if (bytes > 0) {
|
|
@@ -146,190 +164,169 @@ export function reader({ sharedState, sharedBuffer } ) {
|
|
|
146
164
|
|
|
147
165
|
return count
|
|
148
166
|
}
|
|
149
|
-
|
|
150
|
-
return { readSome }
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
169
|
/**
|
|
170
|
-
*
|
|
170
|
+
* Writer for the ring buffer.
|
|
171
171
|
*/
|
|
172
|
-
export
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
172
|
+
export class Writer {
|
|
173
|
+
#state
|
|
174
|
+
#size
|
|
175
|
+
#int32
|
|
176
|
+
#data
|
|
177
|
+
#readPos
|
|
178
|
+
#writePos
|
|
179
|
+
#yielding
|
|
180
|
+
#corked
|
|
181
|
+
#pending
|
|
182
|
+
#onYield
|
|
183
|
+
#logger
|
|
184
|
+
#uncorkBound
|
|
185
|
+
|
|
186
|
+
constructor(state , { yield: onYield, logger } = {}) {
|
|
187
|
+
const sharedBuffer = state.buffer
|
|
188
|
+
|
|
189
|
+
if (onYield != null && typeof onYield !== 'function') {
|
|
190
|
+
throw new TypeError('onYield must be a function')
|
|
191
|
+
}
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
// This helps V8 maintain a stable hidden class for the object,
|
|
193
|
-
// which is a key optimization (zero-copy read).
|
|
194
|
-
const data = { buffer, view, offset: 0, length: 0, byteOffset: 0, byteLength: 0 }
|
|
193
|
+
const size = sharedBuffer.byteLength - STATE_BYTES
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
let writePos = Atomics.load(state, WRITE_INDEX) | 0
|
|
195
|
+
this.#state = new Int32Array(sharedBuffer, 0, STATE_BYTES >> 2)
|
|
196
|
+
this.#size = size
|
|
197
|
+
this.#int32 = new Int32Array(sharedBuffer, STATE_BYTES)
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
// This object is reused to avoid creating new objects in a hot path.
|
|
200
|
+
// This helps V8 maintain a stable hidden class for the object,
|
|
201
|
+
// which is a key optimization (zero-copy read).
|
|
202
|
+
this.#data = {
|
|
203
|
+
buffer: Buffer.from(sharedBuffer, STATE_BYTES, size),
|
|
204
|
+
view: new DataView(sharedBuffer, STATE_BYTES, size),
|
|
205
|
+
byteOffset: 0,
|
|
206
|
+
byteLength: 0,
|
|
207
|
+
}
|
|
204
208
|
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
// Local copies of the pointers. The `| 0` is a hint to the V8 JIT
|
|
210
|
+
// compiler that these are 32-bit integers, enabling optimizations.
|
|
211
|
+
this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
|
|
212
|
+
this.#writePos = Atomics.load(this.#state, WRITE_INDEX) | 0
|
|
213
|
+
|
|
214
|
+
this.#yielding = 0
|
|
215
|
+
this.#corked = 0
|
|
216
|
+
this.#pending = 0
|
|
217
|
+
this.#onYield = onYield
|
|
218
|
+
this.#logger = logger
|
|
219
|
+
this.#uncorkBound = this.uncork.bind(this)
|
|
207
220
|
}
|
|
208
221
|
|
|
209
222
|
/**
|
|
210
223
|
* Pauses the writer thread to wait for the reader to catch up.
|
|
211
224
|
*/
|
|
212
|
-
|
|
213
|
-
if (yielding > 128) {
|
|
225
|
+
#yield(delay ) {
|
|
226
|
+
if (this.#yielding > 128) {
|
|
214
227
|
throw new Error('Detected possible deadlock: writer yielding too many times')
|
|
215
228
|
}
|
|
216
229
|
|
|
217
230
|
// First, ensure the very latest write position is visible to the reader.
|
|
218
|
-
|
|
231
|
+
this.flushSync()
|
|
219
232
|
|
|
220
|
-
if (onYield) {
|
|
221
|
-
yielding += 1
|
|
233
|
+
if (this.#onYield) {
|
|
234
|
+
this.#yielding += 1
|
|
222
235
|
try {
|
|
223
236
|
// Call the user-provided yield function, if any. This can be important
|
|
224
237
|
// if the writer is waiting for the reader to process data which would
|
|
225
238
|
// otherwise deadlock.
|
|
226
|
-
onYield()
|
|
239
|
+
this.#onYield()
|
|
227
240
|
} finally {
|
|
228
|
-
yielding -= 1
|
|
241
|
+
this.#yielding -= 1
|
|
229
242
|
}
|
|
230
243
|
}
|
|
231
244
|
|
|
232
245
|
// Atomics.wait is the most efficient way to pause. It puts the thread
|
|
233
246
|
// to sleep, consuming no CPU, until the reader changes the READ_INDEX.
|
|
234
247
|
if (delay > 0) {
|
|
235
|
-
Atomics.wait(state, READ_INDEX, readPos, delay)
|
|
248
|
+
Atomics.wait(this.#state, READ_INDEX, this.#readPos, delay)
|
|
236
249
|
} else {
|
|
237
250
|
// @ts-expect-error Atomics.pause is Stage 3, available in Node.js 25+
|
|
238
251
|
Atomics.pause()
|
|
239
252
|
}
|
|
240
253
|
|
|
241
254
|
// After waking up, refresh the local view of the reader's position.
|
|
242
|
-
readPos = Atomics.load(state, READ_INDEX) | 0
|
|
255
|
+
this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
/**
|
|
246
259
|
* Tries to acquire enough space in the buffer for a new message.
|
|
247
260
|
*/
|
|
248
|
-
|
|
249
|
-
// Total space required: payload + its 4-byte length header + a potential
|
|
261
|
+
#acquire(len ) {
|
|
262
|
+
// Total space required: aligned payload + its 4-byte length header + a potential
|
|
250
263
|
// 4-byte header for the *next* message (for wrap-around check).
|
|
251
|
-
const required = len + 4 + 4
|
|
264
|
+
const required = ((len + 3) & ~3) + 4 + 4
|
|
265
|
+
const size = this.#size
|
|
266
|
+
const state = this.#state
|
|
267
|
+
const int32 = this.#int32
|
|
252
268
|
|
|
253
|
-
if (writePos >= readPos) {
|
|
269
|
+
if (this.#writePos >= this.#readPos) {
|
|
254
270
|
// Case 1: The writer is ahead of the reader. [ 0 - R ... W - size ]
|
|
255
271
|
// There is free space from W to the end (s) and from 0 to R.
|
|
256
272
|
|
|
257
|
-
if (size - writePos >= required) {
|
|
273
|
+
if (size - this.#writePos >= required) {
|
|
258
274
|
// Enough space at the end of the buffer.
|
|
259
275
|
return true
|
|
260
276
|
}
|
|
261
277
|
|
|
262
|
-
readPos = state[READ_INDEX] | 0
|
|
263
|
-
if (readPos === 0) {
|
|
264
|
-
|
|
278
|
+
this.#readPos = state[READ_INDEX] | 0
|
|
279
|
+
if (this.#readPos === 0) {
|
|
280
|
+
this.#yield(0)
|
|
265
281
|
}
|
|
266
282
|
|
|
267
283
|
// Not enough space at the end. Check if there's space at the beginning.
|
|
268
|
-
if (readPos === 0) {
|
|
284
|
+
if (this.#readPos === 0) {
|
|
269
285
|
// Reader is at the beginning, so no space to wrap around into.
|
|
270
286
|
return false
|
|
271
287
|
}
|
|
272
288
|
|
|
273
289
|
// Mark the current position with a wrap-around signal (-1).
|
|
274
|
-
|
|
290
|
+
int32[this.#writePos >> 2] = -1
|
|
275
291
|
|
|
276
292
|
// Reset writer position to the beginning.
|
|
277
|
-
writePos = 0
|
|
293
|
+
this.#writePos = 0
|
|
278
294
|
|
|
279
|
-
if (writePos + 4 > size) {
|
|
295
|
+
if (this.#writePos + 4 > size) {
|
|
280
296
|
// assertion
|
|
281
|
-
throw new Error(
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Write position ${this.#writePos} with next header exceeds buffer size ${size}`,
|
|
299
|
+
)
|
|
282
300
|
}
|
|
283
|
-
if (writePos === readPos) {
|
|
301
|
+
if (this.#writePos === this.#readPos) {
|
|
284
302
|
// assertion
|
|
285
|
-
throw new Error(
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
305
|
+
)
|
|
286
306
|
}
|
|
287
307
|
|
|
288
|
-
Atomics.store(state, WRITE_INDEX, writePos)
|
|
308
|
+
Atomics.store(state, WRITE_INDEX, this.#writePos)
|
|
289
309
|
}
|
|
290
310
|
|
|
291
311
|
// Case 2: The writer has wrapped around. [ 0 ... W - R ... s ]
|
|
292
312
|
// The only free space is between W and R.
|
|
293
313
|
|
|
294
|
-
readPos = state[READ_INDEX] | 0
|
|
295
|
-
if (readPos - writePos < required) {
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return readPos - writePos >= required
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* "Uncorks" the stream by publishing the pending write position.
|
|
304
|
-
* This is called from a microtask to batch atomic stores.
|
|
305
|
-
*/
|
|
306
|
-
function _uncork() {
|
|
307
|
-
corked -= 1
|
|
308
|
-
if (corked === 0) {
|
|
309
|
-
_flush()
|
|
314
|
+
this.#readPos = state[READ_INDEX] | 0
|
|
315
|
+
if (this.#readPos - this.#writePos < required) {
|
|
316
|
+
this.#yield(0)
|
|
310
317
|
}
|
|
311
|
-
}
|
|
312
318
|
|
|
313
|
-
|
|
314
|
-
if (pending > 0) {
|
|
315
|
-
Atomics.store(state, WRITE_INDEX, writePos)
|
|
316
|
-
pending = 0
|
|
317
|
-
}
|
|
319
|
+
return this.#readPos - this.#writePos >= required
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
/**
|
|
321
323
|
* Performs the actual write into the buffer after space has been acquired.
|
|
322
324
|
*/
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
) {
|
|
328
|
-
const dataPos = writePos + 4
|
|
329
|
-
|
|
330
|
-
data.offset = dataPos
|
|
325
|
+
#write (dataCap , fn , opaque ) {
|
|
326
|
+
const dataPos = this.#writePos + 4
|
|
327
|
+
const data = this.#data
|
|
328
|
+
|
|
331
329
|
data.byteOffset = dataPos
|
|
332
|
-
data.length = dataCap
|
|
333
330
|
data.byteLength = dataCap
|
|
334
331
|
|
|
335
332
|
// The user-provided function writes the data and returns the final position.
|
|
@@ -347,56 +344,54 @@ export function writer(
|
|
|
347
344
|
throw new RangeError(`"fn" returned a number ${dataLen} that exceeds capacity ${dataCap}`)
|
|
348
345
|
}
|
|
349
346
|
|
|
347
|
+
const size = this.#size
|
|
350
348
|
if (dataPos + dataLen > size) {
|
|
351
349
|
// assertion
|
|
352
350
|
throw new Error(`Data position ${dataPos} with length ${dataLen} exceeds buffer size ${size}`)
|
|
353
351
|
}
|
|
354
352
|
|
|
355
|
-
const
|
|
353
|
+
const alignedLen = (dataLen + 3) & ~3
|
|
354
|
+
const nextPos = this.#writePos + 4 + alignedLen
|
|
356
355
|
|
|
357
356
|
if (nextPos + 4 > size) {
|
|
358
357
|
// assertion
|
|
359
358
|
throw new Error(`Write position ${nextPos} with next header exceeds buffer size ${size}`)
|
|
360
359
|
}
|
|
361
|
-
if (nextPos === readPos) {
|
|
360
|
+
if (nextPos === this.#readPos) {
|
|
362
361
|
// assertion
|
|
363
|
-
throw new Error(`Write position ${nextPos} cannot equal read position ${readPos}`)
|
|
362
|
+
throw new Error(`Write position ${nextPos} cannot equal read position ${this.#readPos}`)
|
|
364
363
|
}
|
|
365
364
|
|
|
366
365
|
// Write the actual length of the data into the 4-byte header.
|
|
367
|
-
|
|
368
|
-
writePos += 4 +
|
|
369
|
-
pending += 4 +
|
|
366
|
+
this.#int32[this.#writePos >> 2] = dataLen
|
|
367
|
+
this.#writePos += 4 + alignedLen
|
|
368
|
+
this.#pending += 4 + alignedLen
|
|
370
369
|
|
|
371
370
|
// This is the "corking" optimization. Instead of calling Atomics.store
|
|
372
371
|
// on every write, we batch them. We either write when a certain
|
|
373
372
|
// amount of data is pending (HWM_BYTES) or at the end of the current
|
|
374
373
|
// event loop tick. This drastically reduces atomic operation overhead.
|
|
375
|
-
if (pending >= HWM_BYTES) {
|
|
376
|
-
Atomics.store(state, WRITE_INDEX, writePos)
|
|
377
|
-
pending = 0
|
|
378
|
-
} else if (corked === 0) {
|
|
379
|
-
corked += 1
|
|
380
|
-
setImmediate(
|
|
374
|
+
if (this.#pending >= HWM_BYTES) {
|
|
375
|
+
Atomics.store(this.#state, WRITE_INDEX, this.#writePos)
|
|
376
|
+
this.#pending = 0
|
|
377
|
+
} else if (this.#corked === 0) {
|
|
378
|
+
this.#corked += 1
|
|
379
|
+
setImmediate(this.#uncorkBound)
|
|
381
380
|
}
|
|
382
381
|
}
|
|
383
382
|
|
|
384
383
|
/**
|
|
385
|
-
*
|
|
386
|
-
* wait until space is available.
|
|
384
|
+
* Synchronously writes a message. Blocks (via `Atomics.wait`) until buffer space is available.
|
|
387
385
|
* Writing more than "len" bytes in the callback will cause undefined behavior.
|
|
388
386
|
*/
|
|
389
|
-
|
|
390
|
-
len ,
|
|
391
|
-
fn ,
|
|
392
|
-
opaque ,
|
|
393
|
-
) {
|
|
387
|
+
writeSync (len , fn , opaque ) {
|
|
394
388
|
if (typeof len !== 'number') {
|
|
395
389
|
throw new TypeError('"len" must be a non-negative number')
|
|
396
390
|
}
|
|
397
391
|
if (len < 0) {
|
|
398
392
|
throw new RangeError(`"len" ${len} is negative`)
|
|
399
393
|
}
|
|
394
|
+
const size = this.#size
|
|
400
395
|
if (len >= 2 ** 31 || len > size - 8) {
|
|
401
396
|
throw new Error(`"len" ${len} exceeds maximum allowed size ${size - 8}`)
|
|
402
397
|
}
|
|
@@ -404,48 +399,53 @@ export function writer(
|
|
|
404
399
|
throw new TypeError('"fn" must be a function')
|
|
405
400
|
}
|
|
406
401
|
|
|
407
|
-
if (!
|
|
402
|
+
if (!this.#acquire(len)) {
|
|
408
403
|
const startTime = performance.now()
|
|
409
404
|
let yieldCount = 0
|
|
410
405
|
let yieldTime = 0
|
|
411
|
-
for (let n = 0; !
|
|
406
|
+
for (let n = 0; !this.#acquire(len); n++) {
|
|
412
407
|
if (performance.now() - startTime > 60e3) {
|
|
413
408
|
throw new Error('Timeout while waiting for space in the buffer')
|
|
414
409
|
}
|
|
415
|
-
|
|
410
|
+
this.#yield(3)
|
|
416
411
|
yieldCount += 1
|
|
417
412
|
yieldTime += 3
|
|
418
413
|
}
|
|
419
414
|
const elapsedTime = performance.now() - startTime
|
|
420
|
-
logger?.warn(
|
|
421
|
-
{
|
|
415
|
+
this.#logger?.warn(
|
|
416
|
+
{
|
|
417
|
+
yieldLength: len,
|
|
418
|
+
readPos: this.#readPos,
|
|
419
|
+
writePos: this.#writePos,
|
|
420
|
+
elapsedTime,
|
|
421
|
+
yieldCount,
|
|
422
|
+
yieldTime,
|
|
423
|
+
},
|
|
422
424
|
'yielded',
|
|
423
425
|
)
|
|
424
426
|
}
|
|
425
427
|
|
|
426
|
-
|
|
428
|
+
this.#write(len, fn, opaque)
|
|
427
429
|
|
|
428
|
-
if (writePos === readPos) {
|
|
429
|
-
throw new Error(
|
|
430
|
+
if (this.#writePos === this.#readPos) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
433
|
+
)
|
|
430
434
|
}
|
|
431
435
|
}
|
|
432
436
|
|
|
433
437
|
/**
|
|
434
|
-
*
|
|
438
|
+
* Non-blocking write attempt. Returns `false` if the buffer is full.
|
|
435
439
|
* Writing more than "len" bytes in the callback will cause undefined behavior.
|
|
436
440
|
*/
|
|
437
|
-
|
|
438
|
-
function tryWrite (
|
|
439
|
-
len ,
|
|
440
|
-
fn ,
|
|
441
|
-
opaque ,
|
|
442
|
-
) {
|
|
441
|
+
tryWrite (len , fn , opaque ) {
|
|
443
442
|
if (typeof len !== 'number') {
|
|
444
443
|
throw new TypeError('"len" must be a non-negative number')
|
|
445
444
|
}
|
|
446
445
|
if (len < 0) {
|
|
447
446
|
throw new RangeError(`"len" ${len} is negative`)
|
|
448
447
|
}
|
|
448
|
+
const size = this.#size
|
|
449
449
|
if (len >= 2 ** 31 || len > size - 8) {
|
|
450
450
|
throw new Error(`"len" ${len} exceeds maximum allowed size ${size - 8}`)
|
|
451
451
|
}
|
|
@@ -453,29 +453,54 @@ export function writer(
|
|
|
453
453
|
throw new TypeError('"fn" must be a function')
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
if (!
|
|
456
|
+
if (!this.#acquire(len)) {
|
|
457
457
|
return false
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
|
|
460
|
+
this.#write(len, fn, opaque)
|
|
461
461
|
|
|
462
|
-
if (writePos === readPos) {
|
|
463
|
-
throw new Error(
|
|
462
|
+
if (this.#writePos === this.#readPos) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
465
|
+
)
|
|
464
466
|
}
|
|
465
467
|
|
|
466
468
|
return true
|
|
467
469
|
}
|
|
468
470
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Batches multiple writes within the callback. The write pointer is only
|
|
473
|
+
* published to the reader when cork returns, reducing atomic operation overhead.
|
|
474
|
+
*/
|
|
475
|
+
cork (callback ) {
|
|
476
|
+
this.#corked += 1
|
|
471
477
|
if (callback != null) {
|
|
472
478
|
try {
|
|
473
479
|
return callback()
|
|
474
480
|
} finally {
|
|
475
|
-
|
|
481
|
+
this.uncork()
|
|
476
482
|
}
|
|
477
483
|
}
|
|
478
484
|
}
|
|
479
485
|
|
|
480
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Publishes the pending write position to the reader.
|
|
488
|
+
*/
|
|
489
|
+
uncork() {
|
|
490
|
+
if (this.#corked === 0) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
this.#corked -= 1
|
|
495
|
+
if (this.#corked === 0) {
|
|
496
|
+
this.flushSync()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
flushSync() {
|
|
501
|
+
if (this.#pending > 0) {
|
|
502
|
+
Atomics.store(this.#state, WRITE_INDEX, this.#writePos)
|
|
503
|
+
this.#pending = 0
|
|
504
|
+
}
|
|
505
|
+
}
|
|
481
506
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"oxlint-tsgolint": "^0.13.0",
|
|
27
27
|
"rimraf": "^6.1.3",
|
|
28
28
|
"typescript": "^5.9.3"
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"gitHead": "18d5bdc8855ad9012882b03cab144a9625b8c4b8"
|
|
30
31
|
}
|