@nxtedition/shared 4.0.4 → 4.0.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/README.md CHANGED
@@ -183,7 +183,7 @@ Immediately publishes the pending write position to the reader. Unlike `uncork`,
183
183
 
184
184
  ## Benchmarks
185
185
 
186
- Measured on AMD EPYC 9355P (4.28 GHz), Node.js 25.6.0, 8 MiB ring buffer, Docker (x64-linux).
186
+ Measured on AMD EPYC 9355P (4.29 GHz), Node.js 25.6.1, 8 MiB ring buffer, Docker (x64-linux).
187
187
 
188
188
  Each benchmark writes batches of fixed-size messages from the main thread and
189
189
  reads them in a worker thread. The shared ring buffer is compared against
@@ -193,38 +193,39 @@ Node.js `postMessage` (structured clone).
193
193
 
194
194
  | Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
195
195
  | -----: | --------------: | --------------: | -------------------: | -------------------: |
196
- | 64 B | **901 MiB/s** | 410 MiB/s | 25 MiB/s | 42 MiB/s |
197
- | 256 B | **2.67 GiB/s** | 896 MiB/s | 88 MiB/s | 158 MiB/s |
198
- | 1 KiB | **4.88 GiB/s** | 1.26 GiB/s | 328 MiB/s | 498 MiB/s |
199
- | 4 KiB | **9.22 GiB/s** | 1.50 GiB/s | 1.14 GiB/s | 1.70 GiB/s |
200
- | 16 KiB | **10.90 GiB/s** | 1.56 GiB/s | 4.29 GiB/s | 6.27 GiB/s |
201
- | 64 KiB | 13.03 GiB/s | 1.55 GiB/s | 10.10 GiB/s | **15.18 GiB/s** |
196
+ | 64 B | **838 MiB/s** | 388 MiB/s | 24 MiB/s | 42 MiB/s |
197
+ | 256 B | **2.65 GiB/s** | 1.46 GiB/s | 89 MiB/s | 168 MiB/s |
198
+ | 1 KiB | **4.95 GiB/s** | 4.86 GiB/s | 339 MiB/s | 525 MiB/s |
199
+ | 4 KiB | 8.42 GiB/s | **15.11 GiB/s** | 1.12 GiB/s | 1.86 GiB/s |
200
+ | 16 KiB | 12.02 GiB/s | **33.27 GiB/s** | 4.12 GiB/s | 6.02 GiB/s |
201
+ | 64 KiB | 12.96 GiB/s | **43.66 GiB/s** | 9.33 GiB/s | 14.73 GiB/s |
202
202
 
203
203
  ### Message rate
204
204
 
205
205
  | Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
206
206
  | -----: | --------------: | --------------: | -------------------: | -------------------: |
207
- | 64 B | **14.76 M/s** | 6.72 M/s | 405 K/s | 688 K/s |
208
- | 256 B | **11.20 M/s** | 3.67 M/s | 360 K/s | 648 K/s |
209
- | 1 KiB | **5.12 M/s** | 1.32 M/s | 336 K/s | 510 K/s |
210
- | 4 KiB | **2.42 M/s** | 394 K/s | 298 K/s | 445 K/s |
211
- | 16 KiB | **714 K/s** | 102 K/s | 281 K/s | 411 K/s |
212
- | 64 KiB | 213 K/s | 25 K/s | 165 K/s | **249 K/s** |
207
+ | 64 B | **13.73 M/s** | 6.35 M/s | 391 K/s | 693 K/s |
208
+ | 256 B | **11.14 M/s** | 6.14 M/s | 366 K/s | 689 K/s |
209
+ | 1 KiB | **5.19 M/s** | 5.09 M/s | 348 K/s | 538 K/s |
210
+ | 4 KiB | 2.21 M/s | **3.96 M/s** | 295 K/s | 488 K/s |
211
+ | 16 KiB | 788 K/s | **2.18 M/s** | 270 K/s | 395 K/s |
212
+ | 64 KiB | 212 K/s | **715 K/s** | 153 K/s | 241 K/s |
213
213
 
214
214
  ### Key findings
215
215
 
216
216
  - **Small messages (64–256 B):** The shared ring buffer with `Buffer.set` delivers
217
- **14.8–11.2 M msg/s** — up to **36x faster** than `postMessage` (buffer) and
218
- **21x faster** than `postMessage` (string). Per-message overhead dominates at
217
+ **13.7–11.1 M msg/s** — up to **35x faster** than `postMessage` (buffer) and
218
+ **20x faster** than `postMessage` (string). Per-message overhead dominates at
219
219
  these sizes, and avoiding structured cloning makes the biggest difference.
220
220
 
221
- - **Medium to large messages (1–16 KiB):** `Buffer.set` via the ring buffer
222
- maintains its lead, reaching **10.9 GiB/s** at 16 KiB **1.7–5.4x faster**
223
- than the best `postMessage` variant.
221
+ - **Medium messages (1 KiB):** `Buffer.set` and string are nearly identical
222
+ (**4.95 vs 4.86 GiB/s**), both **~9x faster** than the best `postMessage`
223
+ variant.
224
224
 
225
- - **Very large messages (64 KiB):** `postMessage` (string) overtakes the shared
226
- buffer at **15.2 GiB/s** vs **13.0 GiB/s**. At this size, structured cloning
227
- overhead is amortized and the kernel's optimized `memcpy` dominates.
225
+ - **Large messages (4–64 KiB):** Shared string overtakes `Buffer.set` and
226
+ scales to **43.7 GiB/s** at 64 KiB — **3.4x faster** than `Buffer.set` and
227
+ **3.0x faster** than `postMessage` (string). At every size, the shared ring
228
+ buffer outperforms `postMessage`.
228
229
 
229
230
  - **Caveat:** The string benchmark uses ASCII-only content. Multi-byte UTF-8
230
231
  strings will not hit V8's vectorized fast path and will be significantly slower.
package/lib/index.js CHANGED
@@ -16,6 +16,8 @@ const STATE_BYTES = 128
16
16
  const HWM_BYTES = 256 * 1024 // 256 KiB
17
17
  const HWM_COUNT = 1024 // 1024 items
18
18
 
19
+ const isProduction = process.env.NODE_ENV === 'production'
20
+
19
21
 
20
22
 
21
23
 
@@ -59,7 +61,6 @@ export class Reader {
59
61
  #size
60
62
  #int32
61
63
  #data
62
- #readPos
63
64
 
64
65
  constructor(sharedBuffer ) {
65
66
  const size = sharedBuffer.byteLength - STATE_BYTES
@@ -77,10 +78,6 @@ export class Reader {
77
78
  byteOffset: 0,
78
79
  byteLength: 0,
79
80
  }
80
-
81
- // Local copy of the pointer. The `| 0` is a hint to the V8 JIT
82
- // compiler that this is a 32-bit integer, enabling optimizations.
83
- this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
84
81
  }
85
82
 
86
83
 
@@ -93,14 +90,9 @@ export class Reader {
93
90
  const int32 = this.#int32
94
91
  const size = this.#size
95
92
  const data = this.#data
96
- let readPos = this.#readPos
97
- let writePos = state[WRITE_INDEX] | 0
98
93
 
99
- // First, check if the local writePos matches the readPos.
100
- // If so, refresh it from shared memory in case the writer has added data.
101
- if (readPos === writePos) {
102
- writePos = Atomics.load(state, WRITE_INDEX) | 0
103
- }
94
+ let readPos = state[READ_INDEX] | 0
95
+ let writePos = state[WRITE_INDEX] | 0
104
96
 
105
97
  // Process messages in a batch to minimize loop and atomic operation overhead.
106
98
  while (count < HWM_COUNT && bytes < HWM_BYTES && readPos !== writePos) {
@@ -115,7 +107,7 @@ export class Reader {
115
107
  readPos = 0
116
108
  // After wrapping, we must re-check against the writer's position.
117
109
  // It's possible the writer is now at a position > 0.
118
- writePos = Atomics.load(state, WRITE_INDEX) | 0
110
+ writePos = state[WRITE_INDEX] | 0
119
111
  } else {
120
112
  if (dataLen < 0) {
121
113
  throw new Error('Invalid data length')
@@ -142,12 +134,10 @@ export class Reader {
142
134
  }
143
135
  }
144
136
 
145
- this.#readPos = readPos
146
-
147
137
  // IMPORTANT: The reader only updates its shared `readPos` after a batch
148
- // is processed. This significantly reduces atomic operation overhead.
138
+ // is processed. This significantly reduces shared memory overhead.
149
139
  if (bytes > 0) {
150
- Atomics.store(state, READ_INDEX, readPos)
140
+ state[READ_INDEX] = readPos | 0
151
141
  }
152
142
 
153
143
  return count
@@ -194,8 +184,8 @@ export class Writer {
194
184
 
195
185
  // Local copies of the pointers. The `| 0` is a hint to the V8 JIT
196
186
  // compiler that these are 32-bit integers, enabling optimizations.
197
- this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
198
- this.#writePos = Atomics.load(this.#state, WRITE_INDEX) | 0
187
+ this.#readPos = this.#state[READ_INDEX] | 0
188
+ this.#writePos = this.#state[WRITE_INDEX] | 0
199
189
 
200
190
  this.#yielding = 0
201
191
  this.#corked = 0
@@ -233,12 +223,11 @@ export class Writer {
233
223
  if (delay > 0) {
234
224
  Atomics.wait(this.#state, READ_INDEX, this.#readPos, delay)
235
225
  } else {
236
- // @ts-expect-error Atomics.pause is Stage 3, available in Node.js 25+
237
226
  Atomics.pause()
238
227
  }
239
228
 
240
229
  // After waking up, refresh the local view of the reader's position.
241
- this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
230
+ this.#readPos = this.#state[READ_INDEX] | 0
242
231
  }
243
232
 
244
233
  /**
@@ -262,12 +251,12 @@ export class Writer {
262
251
  }
263
252
 
264
253
  this.#readPos = state[READ_INDEX] | 0
265
- if (this.#readPos === 0) {
254
+ if (this.#readPos < 4) {
266
255
  this.#yield(0)
267
256
  }
268
257
 
269
258
  // Not enough space at the end. Check if there's space at the beginning.
270
- if (this.#readPos === 0) {
259
+ if (this.#readPos < 4) {
271
260
  // Reader is at the beginning, so no space to wrap around into.
272
261
  return false
273
262
  }
@@ -278,13 +267,13 @@ export class Writer {
278
267
  // Reset writer position to the beginning.
279
268
  this.#writePos = 0
280
269
 
281
- if (this.#writePos + 4 > size) {
270
+ if (!isProduction && this.#writePos + 4 > size) {
282
271
  // assertion
283
272
  throw new Error(
284
273
  `Write position ${this.#writePos} with next header exceeds buffer size ${size}`,
285
274
  )
286
275
  }
287
- if (this.#writePos === this.#readPos) {
276
+ if (!isProduction && this.#writePos === this.#readPos) {
288
277
  // assertion
289
278
  throw new Error(
290
279
  `Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
@@ -292,6 +281,7 @@ export class Writer {
292
281
  }
293
282
 
294
283
  Atomics.store(state, WRITE_INDEX, this.#writePos)
284
+ this.#pending = 0
295
285
  }
296
286
 
297
287
  // Case 2: The writer has wrapped around. [ 0 ... W - R ... s ]
@@ -331,7 +321,7 @@ export class Writer {
331
321
  }
332
322
 
333
323
  const size = this.#size
334
- if (dataPos + dataLen > size) {
324
+ if (!isProduction && dataPos + dataLen > size) {
335
325
  // assertion
336
326
  throw new Error(`Data position ${dataPos} with length ${dataLen} exceeds buffer size ${size}`)
337
327
  }
@@ -339,11 +329,11 @@ export class Writer {
339
329
  const alignedLen = (dataLen + 3) & ~3
340
330
  const nextPos = this.#writePos + 4 + alignedLen
341
331
 
342
- if (nextPos + 4 > size) {
332
+ if (!isProduction && nextPos + 4 > size) {
343
333
  // assertion
344
334
  throw new Error(`Write position ${nextPos} with next header exceeds buffer size ${size}`)
345
335
  }
346
- if (nextPos === this.#readPos) {
336
+ if (!isProduction && nextPos === this.#readPos) {
347
337
  // assertion
348
338
  throw new Error(`Write position ${nextPos} cannot equal read position ${this.#readPos}`)
349
339
  }
@@ -415,7 +405,8 @@ export class Writer {
415
405
 
416
406
  this.#write(len, fn, opaque)
417
407
 
418
- if (this.#writePos === this.#readPos) {
408
+ if (!isProduction && this.#writePos === this.#readPos) {
409
+ // assertion
419
410
  throw new Error(
420
411
  `Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
421
412
  )
@@ -449,7 +440,8 @@ export class Writer {
449
440
 
450
441
  this.#write(len, fn, opaque)
451
442
 
452
- if (this.#writePos === this.#readPos) {
443
+ if (!isProduction && this.#writePos === this.#readPos) {
444
+ // assertion
453
445
  throw new Error(
454
446
  `Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
455
447
  )
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/shared",
3
- "version": "4.0.4",
3
+ "version": "4.0.5",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -27,5 +27,5 @@
27
27
  "rimraf": "^6.1.3",
28
28
  "typescript": "^5.9.3"
29
29
  },
30
- "gitHead": "6c260546e469c6cb70b5d35a20d3b0efcbf9629b"
30
+ "gitHead": "f6592f0be62fecc161237610ae6454ec6585548b"
31
31
  }