queen-mq 2.6.0 → 2.7.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.
@@ -19,13 +19,22 @@
19
19
  * - build push_items / run foreach effects
20
20
  * - commit cycle (state ops + push items + ack) atomically
21
21
  *
22
- * flush timer:
22
+ * flush timer, processing-time windows with a reducer (broker 2.7):
23
+ * each cycle notes when the partition's earliest open window closes ->
24
+ * when that time comes, claim the partition (POST /streams/v1/claim: a
25
+ * lease with an empty batch) -> close what is ripe on the broker's
26
+ * clock -> commit a cycle that acks the empty batch (count 0) under
27
+ * the claim's lease. A sweep (POST /streams/v1/state/partitions) finds
28
+ * the windows nobody tracks: after a restart, or left by another runner.
29
+ *
30
+ * flush timer, event-time windows or a broker without the claim route:
23
31
  * periodically wake -> for partitions touched recently, query state
24
32
  * for ripe windows -> apply post-reducer ops + foreach + sink push,
25
33
  * commit no-ack cycle (state deletes + sink push, no source ack).
26
34
  *
27
35
  * stop():
28
- * - signal abort, wait for in-flight cycle and flush, return.
36
+ * - signal abort, wait for in-flight cycle and flush (a flush at most
37
+ * 10 s), return.
29
38
  *
30
39
  * Retry / fault model unchanged from v0.1: cycle commit failures roll
31
40
  * back; Queen redelivers via lease/retry. Stateless / reducer / foreach
@@ -46,9 +55,40 @@ const DEFAULT_MAX_PARTITIONS = 4
46
55
  const DEFAULT_IDLE_SLEEP_MS = 100
47
56
  // Track partitions we've touched within this rolling window. Drives the
48
57
  // idle-flush sweep — anything older than this is assumed to be owned by a
49
- // different worker and we don't speculatively flush it.
58
+ // different worker and we don't speculatively flush it. Only for a broker
59
+ // without POST /streams/v1/claim (before 2.7), and for event-time windows.
50
60
  const PARTITION_RECENCY_MS = 5 * 60 * 1000 // 5 minutes
51
61
 
62
+ // Closes under a claim (broker 2.7, issue #153).
63
+ //
64
+ // How often a runner asks the broker for the partitions holding the query's
65
+ // state: after a restart, a scale-down, or for partitions another runner
66
+ // left, these are the windows nobody tracks.
67
+ const SWEEP_EVERY_MS = 30_000
68
+ // How old a partition's newest state row must be before a later sweep takes
69
+ // it on: a younger one is some runner's live work, which tracks it already.
70
+ // The first sweep after a start takes every partition: this runner tracks
71
+ // nothing yet.
72
+ const SWEEP_UNTOUCHED_MS = 30_000
73
+ // Partitions per page of the sweep.
74
+ const SWEEP_PAGE = 1000
75
+ // Partitions per claim, which is also the most one flush tick closes: a
76
+ // sweep that finds thousands of quiet partitions (after an outage) works
77
+ // through them a batch per tick instead of all at once.
78
+ const CLAIM_BATCH = 100
79
+ // The lease a claim asks for. A close is one state read and one commit.
80
+ const CLAIM_LEASE_S = 30
81
+ // How soon to try again a partition that someone else holds, or whose claim
82
+ // or close failed.
83
+ const CLAIM_RETRY_MS = 1000
84
+ // How long stop() waits for a flush in flight.
85
+ const STOP_FLUSH_WAIT_MS = 10_000
86
+
87
+ // Whether the broker serves POST /streams/v1/claim.
88
+ const CLAIMS_UNKNOWN = 'unknown'
89
+ const CLAIMS_YES = 'yes'
90
+ const CLAIMS_NO = 'no' // an older broker: the unleased flush of recent partitions
91
+
52
92
  const WATERMARK_STATE_KEY = '__wm__' // reserved internal key for per-partition watermark
53
93
 
54
94
  export class Runner {
@@ -100,6 +140,14 @@ export class Runner {
100
140
  this._partitionMutexes = new Map() // partitionId -> tail promise
101
141
  this._recentPartitions = new Map() // partitionId -> { partitionName, touchedAt }
102
142
  this._partitionWatermarks = new Map() // partitionId -> wmMs (cache; the broker is source of truth)
143
+ // Processing-time windows still open, by partition: what the idle flush
144
+ // claims and closes once their time has come. atMs is on the broker's
145
+ // clock (processing time is the messages' createdAt).
146
+ this._due = new Map() // partitionId -> { partitionName, atMs }
147
+ this._claims = CLAIMS_UNKNOWN
148
+ this._nextSweepAt = 0 // the first flush tick sweeps
149
+ this._sweepUntouchedMs = 0 // the first sweep takes every partition
150
+ this._flushPromise = null // the flush tick in flight, for stop()
103
151
  this._stats = {
104
152
  cyclesTotal: 0,
105
153
  flushCyclesTotal: 0,
@@ -157,6 +205,9 @@ export class Runner {
157
205
  }
158
206
 
159
207
  this._loopPromise = this._loop()
208
+ // The first flush tick runs now rather than one interval later: its sweep
209
+ // finds the windows a previous runner left open.
210
+ if (this._flushTimers.length > 0) this._flushTick().catch(() => {})
160
211
  return this
161
212
  }
162
213
 
@@ -165,8 +216,17 @@ export class Runner {
165
216
  for (const t of this._flushTimers) clearInterval(t)
166
217
  this._flushTimers = []
167
218
  if (this._loopPromise) await this._loopPromise
168
- // Allow any in-flight flush to drain.
169
- while (this._flushInFlight) await sleep(20)
219
+ // Allow any in-flight flush to drain. A close cut short would keep its
220
+ // claim on the partition until the lease ran out, so it gets up to 10 s;
221
+ // past that stop() returns and the flush ends on its own.
222
+ if (this._flushPromise) {
223
+ let timer
224
+ await Promise.race([
225
+ this._flushPromise.catch(() => {}),
226
+ new Promise(resolve => { timer = setTimeout(resolve, STOP_FLUSH_WAIT_MS) })
227
+ ])
228
+ clearTimeout(timer)
229
+ }
170
230
  }
171
231
 
172
232
  metrics() {
@@ -187,7 +247,10 @@ export class Runner {
187
247
  const groups = this._groupByPartition(messages)
188
248
  for (const group of groups) {
189
249
  if (this._isStopped()) break
190
- this._touchPartition(group.partitionId, group.partitionName)
250
+ // The recent map feeds only the unleased flush.
251
+ if (!this._leasedCloses() || this._claims !== CLAIMS_YES) {
252
+ this._touchPartition(group.partitionId, group.partitionName)
253
+ }
191
254
  try {
192
255
  await this._processPartitionCycle(group)
193
256
  this._stats.cyclesTotal++
@@ -373,12 +436,14 @@ export class Runner {
373
436
  // 3. Window operator + reducer (if present).
374
437
  let stateOps = []
375
438
  let emits = []
439
+ let nextCloseMs = null
376
440
  if (stages.window) {
377
441
  const result = stages.window.windowKind === 'session'
378
- ? await this._processSessionWindow(stages, envelopes, partitionId)
442
+ ? await this._processSessionWindow(stages, envelopes, partitionId, orderedMessages)
379
443
  : await this._processStatelessWindow(stages, envelopes, partitionId, orderedMessages)
380
444
  stateOps = result.stateOps
381
445
  emits = result.emits
446
+ nextCloseMs = result.nextCloseMs ?? null
382
447
  } else {
383
448
  // No window: each remaining envelope becomes an emit.
384
449
  emits = envelopes.map(env => ({ key: env.key, value: env.value, sourceMsg: env.msg }))
@@ -427,6 +492,9 @@ export class Runner {
427
492
  pushItems,
428
493
  ack
429
494
  })
495
+ // What stays open closes when its time comes, under a claim.
496
+ // A broker without claims never closes these, so they are not kept.
497
+ if (this._leasedCloses() && this._claims !== CLAIMS_NO) this._setDue(partitionId, partitionName, nextCloseMs)
430
498
  }
431
499
 
432
500
  // --------------------------------------------------------------- gate path
@@ -756,7 +824,7 @@ export class Runner {
756
824
 
757
825
  // ----------------------------------------------------- session window path
758
826
 
759
- async _processSessionWindow(stages, envelopes, partitionId) {
827
+ async _processSessionWindow(stages, envelopes, partitionId, orderedMessages) {
760
828
  const window = stages.window
761
829
  const reducer = stages.reducer
762
830
  if (!reducer) {
@@ -822,8 +890,17 @@ export class Runner {
822
890
  this._partitionWatermarks.set(partitionId, advancedWm)
823
891
  nowMs = advancedWm
824
892
  } else {
825
- // Use max event time observed in the batch; idle-flush uses Date.now().
826
- nowMs = maxObservedEventTime > -Infinity ? maxObservedEventTime : Date.now()
893
+ // Processing time: the batch's newest createdAt. That is the broker's
894
+ // clock, the one the claimed closes use too; this host's Date.now()
895
+ // could close a session early when it runs ahead, and would close
896
+ // every session of an old backlog at once. Date.now() only when no
897
+ // message carries a createdAt.
898
+ let newest = -Infinity
899
+ for (const m of orderedMessages || []) {
900
+ const t = Date.parse(m.createdAt || '')
901
+ if (Number.isFinite(t) && t > newest) newest = t
902
+ }
903
+ nowMs = newest > -Infinity ? newest : Date.now()
827
904
  }
828
905
 
829
906
  const reducerFn = reducer.fn
@@ -886,26 +963,53 @@ export class Runner {
886
963
  // ------------------------------------------------ idle flush (per-window)
887
964
 
888
965
  async _flushTick() {
966
+ // A tick that would overlap the one still running is skipped.
889
967
  if (this._stopped || this._flushInFlight) return
890
968
  const stages = this.stream.stages
891
969
  if (!stages.window) return
892
970
  this._flushInFlight = true
971
+ const run = this._flushTickInner()
972
+ this._flushPromise = run
893
973
  try {
894
- const partitions = Array.from(this._recentPartitions.entries())
895
- for (const [partitionId, info] of partitions) {
896
- if (this._isStopped()) break
897
- try {
898
- await this._flushPartition(partitionId, info.partitionName)
899
- } catch (err) {
900
- this._reportError(err, { phase: 'flush', partition: partitionId })
901
- }
902
- }
903
- this._stats.lastFlushAt = new Date().toISOString()
974
+ await run
904
975
  } finally {
905
976
  this._flushInFlight = false
906
977
  }
907
978
  }
908
979
 
980
+ async _flushTickInner() {
981
+ if (!this._leasedCloses() || this._claims === CLAIMS_NO) {
982
+ await this._flushRecent()
983
+ } else {
984
+ if (Date.now() >= this._nextSweepAt) {
985
+ await this._sweep(this._sweepUntouchedMs)
986
+ this._nextSweepAt = Date.now() + SWEEP_EVERY_MS
987
+ this._sweepUntouchedMs = SWEEP_UNTOUCHED_MS
988
+ }
989
+ // Not when stopping: a claim we would not close holds its partition
990
+ // until the lease runs out.
991
+ if (this._claims !== CLAIMS_NO && !this._isStopped()) await this._closeDue()
992
+ }
993
+ this._stats.lastFlushAt = new Date().toISOString()
994
+ }
995
+
996
+ /**
997
+ * The unleased flush of the partitions this process popped in the last
998
+ * five minutes: event-time windows, and every window on a broker without
999
+ * the claim route.
1000
+ */
1001
+ async _flushRecent() {
1002
+ const partitions = Array.from(this._recentPartitions.entries())
1003
+ for (const [partitionId, info] of partitions) {
1004
+ if (this._isStopped()) break
1005
+ try {
1006
+ await this._flushPartition(partitionId, info.partitionName)
1007
+ } catch (err) {
1008
+ this._reportError(err, { phase: 'flush', partition: partitionId })
1009
+ }
1010
+ }
1011
+ }
1012
+
909
1013
  async _flushPartition(partitionId, partitionName) {
910
1014
  // Mutual exclusion with the pop/cycle loop (see _partitionMutexes).
911
1015
  return this._withPartitionLock(partitionId, () => this._flushPartitionInner(partitionId, partitionName))
@@ -1068,6 +1172,238 @@ export class Runner {
1068
1172
  return map
1069
1173
  }
1070
1174
 
1175
+ // ------------------------------------------------ closes under a claim
1176
+ //
1177
+ // A processing-time window closes when its time comes even if its
1178
+ // partition gets no more messages. The runner keeps, per partition, when
1179
+ // the earliest open window closes (from each cycle it commits, and from
1180
+ // the sweep), claims the partition when that time has come
1181
+ // (POST /streams/v1/claim: a lease with an empty batch, granted only when
1182
+ // nobody holds the partition and the group has read all of it), and
1183
+ // closes the windows in a cycle that acks the empty batch. The broker
1184
+ // refuses that cycle whole when the lease is gone, so two runners never
1185
+ // close one window, and a close never deletes a window another runner
1186
+ // just extended. The clock is the broker's, the one createdAt comes from.
1187
+
1188
+ /**
1189
+ * Whether this stream closes its windows under claims: processing-time
1190
+ * windows with a reducer and an idle flush. Event-time windows close on
1191
+ * the watermark, which only events move.
1192
+ */
1193
+ _leasedCloses() {
1194
+ const { window, reducer } = this.stream.stages
1195
+ return !!(window && !window.eventTimeFn && window.idleFlushMs > 0 && reducer)
1196
+ }
1197
+
1198
+ /** Track (or forget, atMs null) when the partition's earliest open window closes. */
1199
+ _setDue(partitionId, partitionName, atMs) {
1200
+ if (!partitionId) return
1201
+ if (typeof atMs === 'number' && Number.isFinite(atMs)) {
1202
+ this._due.set(partitionId, { partitionName, atMs })
1203
+ } else {
1204
+ this._due.delete(partitionId)
1205
+ }
1206
+ }
1207
+
1208
+ /** Try a tracked partition again no sooner than atMs. */
1209
+ _retryDue(partitionId, atMs) {
1210
+ const d = this._due.get(partitionId)
1211
+ if (d) d.atMs = Math.max(d.atMs, atMs)
1212
+ }
1213
+
1214
+ _claimsOff() {
1215
+ if (this._claims !== CLAIMS_NO) {
1216
+ this.logger.info('broker has no stream claims; idle flush of recent partitions', { queryId: this.queryId })
1217
+ }
1218
+ this._claims = CLAIMS_NO
1219
+ // Nothing left that would never be closed.
1220
+ this._due.clear()
1221
+ }
1222
+
1223
+ /**
1224
+ * The partitions holding the query's state that nobody touched for
1225
+ * `untouchedMs` (0: all of them) and this runner does not track yet:
1226
+ * tracked as due now, so the next ticks claim them a batch at a time.
1227
+ */
1228
+ async _sweep(untouchedMs) {
1229
+ let after = null
1230
+ for (;;) {
1231
+ if (this._isStopped()) return
1232
+ const body = { query_id: this._serverQueryId, limit: SWEEP_PAGE }
1233
+ if (untouchedMs > 0) body.untouched_for_ms = untouchedMs
1234
+ if (after) body.after = after
1235
+ let res
1236
+ try {
1237
+ res = await this.http.post('/streams/v1/state/partitions', body)
1238
+ } catch (err) {
1239
+ // A broker before 2.7: no claims either.
1240
+ if (err && err.status === 404) return this._claimsOff()
1241
+ this._reportError(err, { phase: 'sweep' })
1242
+ return
1243
+ }
1244
+ if (!res) return
1245
+ if (res.success === false) {
1246
+ this._reportError(new Error(res.error || 'state/partitions failed'), { phase: 'sweep' })
1247
+ return
1248
+ }
1249
+ const now = Date.now()
1250
+ for (const p of res.partitions || []) {
1251
+ if (!p || !p.partition_id || this._due.has(p.partition_id)) continue
1252
+ this._due.set(p.partition_id, { partitionName: p.partition || '', atMs: now })
1253
+ }
1254
+ if (!res.next) return
1255
+ after = res.next
1256
+ }
1257
+ }
1258
+
1259
+ /** Claim the partitions whose windows are due, and close them. */
1260
+ async _closeDue() {
1261
+ const window = this.stream.stages.window
1262
+ const now = Date.now()
1263
+ const ready = []
1264
+ for (const [partitionId, d] of this._due) {
1265
+ if (d.atMs <= now) ready.push([partitionId, d.atMs])
1266
+ }
1267
+ if (ready.length === 0) return
1268
+ ready.sort((a, b) => a[1] - b[1])
1269
+ const partitionIds = ready.slice(0, CLAIM_BATCH).map(([partitionId]) => partitionId)
1270
+
1271
+ let res
1272
+ try {
1273
+ res = await this.http.post('/streams/v1/claim', {
1274
+ query_id: this._serverQueryId,
1275
+ partition_ids: partitionIds,
1276
+ consumer_group: this.consumerGroup,
1277
+ lease_seconds: CLAIM_LEASE_S
1278
+ })
1279
+ } catch (err) {
1280
+ if (err && err.status === 404) return this._claimsOff()
1281
+ this._reportError(err, { phase: 'claim' })
1282
+ const retryAt = Date.now() + CLAIM_RETRY_MS
1283
+ for (const partitionId of partitionIds) this._retryDue(partitionId, retryAt)
1284
+ return
1285
+ }
1286
+ if (!res) return
1287
+ this._claims = CLAIMS_YES
1288
+ if (res.success === false) {
1289
+ this._reportError(new Error(res.error || 'claim refused'), { phase: 'claim' })
1290
+ return
1291
+ }
1292
+
1293
+ for (const p of res.partitions || []) {
1294
+ const partitionId = p.partition_id
1295
+ let partitionName = p.partition
1296
+ if (!partitionName) {
1297
+ const d = this._due.get(partitionId)
1298
+ partitionName = d ? d.partitionName : ''
1299
+ }
1300
+ if (p.status === 'claimed') {
1301
+ // Stopping: the claims not closed yet lapse with their lease,
1302
+ // nothing is lost.
1303
+ if (this._isStopped()) continue
1304
+ const clockMs = Number.isFinite(res.now_ms) ? res.now_ms : Date.now()
1305
+ try {
1306
+ await this._closeClaimed(partitionId, partitionName, res.lease_id, clockMs)
1307
+ } catch (err) {
1308
+ // Its claim lapses with the lease; the next try comes after it.
1309
+ this._reportError(err, { phase: 'flush', partition: partitionId })
1310
+ this._retryDue(partitionId, Date.now() + CLAIM_RETRY_MS)
1311
+ }
1312
+ } else if (p.status === 'busy') {
1313
+ // Someone holds it (a cycle, maybe another runner's): come back
1314
+ // after an idle-flush interval.
1315
+ this._retryDue(partitionId, Date.now() + Math.max(window.idleFlushMs, CLAIM_RETRY_MS))
1316
+ } else {
1317
+ // backlog: messages wait, and the cycle that takes them tracks the
1318
+ // partition again. missing: nothing to close there.
1319
+ this._setDue(partitionId, partitionName, null)
1320
+ }
1321
+ }
1322
+ }
1323
+
1324
+ /**
1325
+ * Close what is ripe on a claimed partition at `clockMs` (the broker's
1326
+ * time at the claim), in a cycle that acks the claim's empty batch, and
1327
+ * track when the windows left open close. The due time is set while the
1328
+ * partition lock is held, as a cycle sets it: set after, it could
1329
+ * overwrite the newer time of a cycle that ran in between.
1330
+ */
1331
+ _closeClaimed(partitionId, partitionName, leaseId, clockMs) {
1332
+ // Mutual exclusion with the pop/cycle loop (see _partitionMutexes).
1333
+ return this._withPartitionLock(partitionId, async () => {
1334
+ const stages = this.stream.stages
1335
+ const window = stages.window
1336
+ const reducer = stages.reducer
1337
+ // Every row: the windows left open say when to come back.
1338
+ const loadedAll = await getState({
1339
+ http: this.http,
1340
+ queryId: this._serverQueryId,
1341
+ partitionId,
1342
+ keys: []
1343
+ })
1344
+ let result
1345
+ if (window.windowKind === 'session') {
1346
+ const sessionState = new Map()
1347
+ for (const [key, value] of loadedAll.entries()) {
1348
+ if (key.startsWith(window.operatorTag + '\u001fopen\u001f')) sessionState.set(key, value)
1349
+ }
1350
+ result = await window.runSession({
1351
+ envelopes: [],
1352
+ loadedState: sessionState,
1353
+ reducerFn: reducer.fn,
1354
+ reducerInitial: () => reducer._initial ? reducer._initial() : 0,
1355
+ nowMs: clockMs
1356
+ })
1357
+ } else {
1358
+ result = await reducer.run({
1359
+ envelopes: [],
1360
+ loadedState: loadedAll,
1361
+ streamTimeMs: clockMs,
1362
+ operatorTag: window.operatorTag,
1363
+ gracePeriodMs: window.gracePeriodMs
1364
+ })
1365
+ }
1366
+ const closed = result.emits.length > 0
1367
+ const emits = await this._applyPostStages(stages.post, result.emits, partitionId, partitionName)
1368
+ const pushItems = await this._terminal(emits, partitionName, partitionId)
1369
+ // The empty batch's ack: count 0 keeps the cursor where it is and the
1370
+ // lease goes back, also when nothing was ripe (another runner closed
1371
+ // it first).
1372
+ await commitCycle({
1373
+ http: this.http,
1374
+ queryId: this._serverQueryId,
1375
+ partitionId,
1376
+ consumerGroup: this.consumerGroup,
1377
+ stateOps: result.stateOps,
1378
+ pushItems,
1379
+ ack: { transactionId: '', leaseId, status: 'completed', count: 0 },
1380
+ releaseLease: true
1381
+ })
1382
+ if (closed) this._stats.flushCyclesTotal++
1383
+ this._stats.stateOpsTotal += result.stateOps.length
1384
+ this._stats.pushItemsTotal += pushItems.length
1385
+ this._setDue(partitionId, partitionName, result.nextCloseMs ?? null)
1386
+ })
1387
+ }
1388
+
1389
+ /** Sink push items for the emits, or run foreach on them (then none). */
1390
+ async _terminal(emits, partitionName, partitionId) {
1391
+ const sink = this.stream.stages.sink
1392
+ if (sink && sink.kind === 'sink') {
1393
+ return sink.buildPushItems(
1394
+ emits.map(e => ({ key: e.key, value: e.value, sourceMsg: e.sourceMsg || null })),
1395
+ partitionName
1396
+ )
1397
+ }
1398
+ if (sink && sink.kind === 'foreach') {
1399
+ await sink.runEffects(emits.map(e => ({
1400
+ value: e.value,
1401
+ ctx: this._buildEmitCtx(e, partitionName, partitionId)
1402
+ })))
1403
+ }
1404
+ return []
1405
+ }
1406
+
1071
1407
  // --------------------------------------------------------------- helpers
1072
1408
 
1073
1409
  _extractQueueName(qb) {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "description": "Partitioned message queue on a replicated broker log — broker client + fluent streaming SDK (windows, joins, gates) in one package",
6
6
  "main": "client-v2/index.js",
7
7
  "scripts": {
8
8
  "test": "npm run test:unit && node test-v2/run.js human",
9
- "test:unit": "node --test test-v2/streams-unit/configHash.test.js test-v2/streams-unit/operators.test.js test-v2/streams-unit/cycle.test.js test-v2/streams-unit/eventTime.test.js test-v2/streams-unit/ack.test.js test-v2/streams-unit/gate.test.js test-v2/http-unit/retry429.test.js test-v2/http-unit/hostHeader.test.js test-v2/http-unit/pushStatus.test.js test-v2/http-unit/renew.test.js test-v2/consumer-unit/handlerError.test.js test-v2/consumer-unit/nackScope.test.js test-v2/consumer-unit/stopOnAbort.test.js test-v2/kv-unit/kvWire.test.js test-v2/kv-unit/timerWire.test.js test-v2/kv-unit/txnWire.test.js test-v2/kv-unit/locksWire.test.js test-v2/buffer-unit/buffer.test.js test-v2/conflation-unit/conflationWire.test.js test-v2/autopilot-unit/autopilotWire.test.js test-v2/ephemeral-unit/ephemeralWire.test.js test-v2/ephemeral-unit/ephemeralBuffer.test.js test-v2/ephemeral-unit/durableSinkPin.test.js test-v2/runner-unit/fatalExit.test.js test-v2/pop-unit/popDefaults.test.js test-v2/admin-unit/removedRoutes.test.js test-v2/consumer-unit/supervision.test.js test-v2/wire-unit/pushBinary.test.js test-v2/wire-unit/popFrames.test.js test-v2/wire-unit/transactionFrames.test.js test-v2/reads-unit/offsetReads.test.js",
9
+ "test:unit": "node --test test-v2/streams-unit/configHash.test.js test-v2/streams-unit/operators.test.js test-v2/streams-unit/cycle.test.js test-v2/streams-unit/eventTime.test.js test-v2/streams-unit/ack.test.js test-v2/streams-unit/gate.test.js test-v2/streams-unit/claims.test.js test-v2/http-unit/retry429.test.js test-v2/http-unit/hostHeader.test.js test-v2/http-unit/pushStatus.test.js test-v2/http-unit/renew.test.js test-v2/consumer-unit/handlerError.test.js test-v2/consumer-unit/nackScope.test.js test-v2/consumer-unit/stopOnAbort.test.js test-v2/kv-unit/kvWire.test.js test-v2/kv-unit/timerWire.test.js test-v2/kv-unit/txnWire.test.js test-v2/kv-unit/locksWire.test.js test-v2/buffer-unit/buffer.test.js test-v2/conflation-unit/conflationWire.test.js test-v2/autopilot-unit/autopilotWire.test.js test-v2/ephemeral-unit/ephemeralWire.test.js test-v2/ephemeral-unit/ephemeralBuffer.test.js test-v2/ephemeral-unit/durableSinkPin.test.js test-v2/runner-unit/fatalExit.test.js test-v2/pop-unit/popDefaults.test.js test-v2/admin-unit/removedRoutes.test.js test-v2/consumer-unit/supervision.test.js test-v2/wire-unit/pushBinary.test.js test-v2/wire-unit/popFrames.test.js test-v2/wire-unit/transactionFrames.test.js test-v2/reads-unit/offsetReads.test.js test-v2/consumer-unit/transientPop.test.js",
10
10
  "test:unit:e2e": "node --test test-v2/streams-unit/e2e.test.js",
11
11
  "test:integration": "node test-v2/run.js human",
12
12
  "test:streams": "node test-v2/run.js stream",