velocious 1.0.666 → 1.0.668

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.
Files changed (174) hide show
  1. package/README.md +40 -11
  2. package/build/background-jobs/adapter-client.js +22 -0
  3. package/build/background-jobs/adapter.js +15 -0
  4. package/build/background-jobs/client.js +147 -1
  5. package/build/background-jobs/job-semantics.js +30 -0
  6. package/build/background-jobs/job.js +26 -1
  7. package/build/background-jobs/local-adapter.js +36 -8
  8. package/build/background-jobs/local-store.js +410 -39
  9. package/build/background-jobs/main.js +69 -2
  10. package/build/background-jobs/platform-job.js +26 -1
  11. package/build/background-jobs/runtime.js +58 -0
  12. package/build/background-jobs/store.js +374 -6
  13. package/build/background-jobs/types.js +27 -2
  14. package/build/background-jobs/web/controller.js +1 -0
  15. package/build/configuration.js +30 -6
  16. package/build/database/drivers/mssql/column.js +2 -2
  17. package/build/database/drivers/mssql/table.js +19 -5
  18. package/build/database/pool/async-tracked-multi-connection.js +2 -1
  19. package/build/database/query/alter-table-base.js +8 -1
  20. package/build/database/record/attachments/store.js +2 -2
  21. package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  22. package/build/environment-handlers/node/cli/commands/test.js +17 -69
  23. package/build/frontend-model-controller.js +22 -2
  24. package/build/frontend-models/websocket-channel.js +18 -0
  25. package/build/http-server/client/websocket-session.js +41 -4
  26. package/build/http-server/websocket-channel.js +14 -0
  27. package/build/http-server/websocket-event-log-store.js +85 -12
  28. package/build/http-server/websocket-events-host.js +17 -7
  29. package/build/src/background-jobs/adapter-client.d.ts +17 -0
  30. package/build/src/background-jobs/adapter-client.d.ts.map +1 -1
  31. package/build/src/background-jobs/adapter-client.js +19 -1
  32. package/build/src/background-jobs/adapter.d.ts +15 -0
  33. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  34. package/build/src/background-jobs/adapter.js +14 -1
  35. package/build/src/background-jobs/client.d.ts +17 -0
  36. package/build/src/background-jobs/client.d.ts.map +1 -1
  37. package/build/src/background-jobs/client.js +129 -1
  38. package/build/src/background-jobs/job-semantics.d.ts +18 -0
  39. package/build/src/background-jobs/job-semantics.d.ts.map +1 -1
  40. package/build/src/background-jobs/job-semantics.js +28 -1
  41. package/build/src/background-jobs/job.d.ts +15 -0
  42. package/build/src/background-jobs/job.d.ts.map +1 -1
  43. package/build/src/background-jobs/job.js +21 -2
  44. package/build/src/background-jobs/local-adapter.d.ts +23 -8
  45. package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
  46. package/build/src/background-jobs/local-adapter.js +35 -9
  47. package/build/src/background-jobs/local-store.d.ts +120 -5
  48. package/build/src/background-jobs/local-store.d.ts.map +1 -1
  49. package/build/src/background-jobs/local-store.js +366 -40
  50. package/build/src/background-jobs/main.d.ts +24 -2
  51. package/build/src/background-jobs/main.d.ts.map +1 -1
  52. package/build/src/background-jobs/main.js +68 -3
  53. package/build/src/background-jobs/platform-job.d.ts +15 -0
  54. package/build/src/background-jobs/platform-job.d.ts.map +1 -1
  55. package/build/src/background-jobs/platform-job.js +19 -2
  56. package/build/src/background-jobs/runtime.d.ts +39 -0
  57. package/build/src/background-jobs/runtime.d.ts.map +1 -1
  58. package/build/src/background-jobs/runtime.js +49 -1
  59. package/build/src/background-jobs/store.d.ts +100 -1
  60. package/build/src/background-jobs/store.d.ts.map +1 -1
  61. package/build/src/background-jobs/store.js +334 -8
  62. package/build/src/background-jobs/types.d.ts +97 -4
  63. package/build/src/background-jobs/types.d.ts.map +1 -1
  64. package/build/src/background-jobs/types.js +28 -3
  65. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  66. package/build/src/background-jobs/web/controller.js +2 -1
  67. package/build/src/configuration.d.ts +18 -1
  68. package/build/src/configuration.d.ts.map +1 -1
  69. package/build/src/configuration.js +28 -6
  70. package/build/src/database/drivers/mssql/column.d.ts.map +1 -1
  71. package/build/src/database/drivers/mssql/column.js +2 -2
  72. package/build/src/database/drivers/mssql/table.d.ts.map +1 -1
  73. package/build/src/database/drivers/mssql/table.js +19 -5
  74. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  75. package/build/src/database/pool/async-tracked-multi-connection.js +3 -2
  76. package/build/src/database/query/alter-table-base.d.ts.map +1 -1
  77. package/build/src/database/query/alter-table-base.js +9 -2
  78. package/build/src/database/record/attachments/store.js +3 -3
  79. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
  80. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
  81. package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
  82. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  83. package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
  84. package/build/src/frontend-model-controller.d.ts.map +1 -1
  85. package/build/src/frontend-model-controller.js +18 -3
  86. package/build/src/frontend-models/websocket-channel.d.ts +9 -0
  87. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  88. package/build/src/frontend-models/websocket-channel.js +16 -1
  89. package/build/src/http-server/client/websocket-session.d.ts +25 -2
  90. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  91. package/build/src/http-server/client/websocket-session.js +40 -5
  92. package/build/src/http-server/websocket-channel.d.ts +11 -0
  93. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  94. package/build/src/http-server/websocket-channel.js +14 -1
  95. package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
  96. package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
  97. package/build/src/http-server/websocket-event-log-store.js +79 -12
  98. package/build/src/http-server/websocket-events-host.d.ts +10 -3
  99. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  100. package/build/src/http-server/websocket-events-host.js +17 -8
  101. package/build/src/testing/test-files-finder.d.ts +17 -117
  102. package/build/src/testing/test-files-finder.d.ts.map +1 -1
  103. package/build/src/testing/test-files-finder.js +29 -294
  104. package/build/src/testing/test-filter-parser.d.ts +32 -22
  105. package/build/src/testing/test-filter-parser.d.ts.map +1 -1
  106. package/build/src/testing/test-filter-parser.js +25 -217
  107. package/build/src/testing/test-profile-activity.d.ts +1 -6
  108. package/build/src/testing/test-profile-activity.d.ts.map +1 -1
  109. package/build/src/testing/test-profile-activity.js +2 -12
  110. package/build/src/testing/test-profile-output.d.ts +1 -40
  111. package/build/src/testing/test-profile-output.d.ts.map +1 -1
  112. package/build/src/testing/test-profile-output.js +2 -217
  113. package/build/src/testing/test-profiler.d.ts +15 -659
  114. package/build/src/testing/test-profiler.d.ts.map +1 -1
  115. package/build/src/testing/test-profiler.js +35 -833
  116. package/build/src/testing/test-runner.d.ts +10 -1
  117. package/build/src/testing/test-runner.d.ts.map +1 -1
  118. package/build/src/testing/test-runner.js +26 -4
  119. package/build/src/testing/test-suite-splitter.d.ts +1 -128
  120. package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
  121. package/build/src/testing/test-suite-splitter.js +2 -259
  122. package/build/src/testing/timing-manifest.d.ts +5 -93
  123. package/build/src/testing/timing-manifest.d.ts.map +1 -1
  124. package/build/src/testing/timing-manifest.js +4 -283
  125. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  126. package/build/src/testing/velocious-attempt-executor.js +8 -2
  127. package/build/testing/test-files-finder.js +32 -343
  128. package/build/testing/test-filter-parser.js +29 -248
  129. package/build/testing/test-profile-activity.js +1 -12
  130. package/build/testing/test-profile-output.js +8 -253
  131. package/build/testing/test-profiler.js +38 -895
  132. package/build/testing/test-runner.js +33 -9
  133. package/build/testing/test-suite-splitter.js +1 -301
  134. package/build/testing/timing-manifest.js +10 -344
  135. package/build/testing/velocious-attempt-executor.js +7 -1
  136. package/package.json +3 -3
  137. package/scripts/test-browser.js +30 -6
  138. package/scripts/verify-docker-dev-environment.js +13 -1
  139. package/src/background-jobs/adapter-client.js +22 -0
  140. package/src/background-jobs/adapter.js +15 -0
  141. package/src/background-jobs/client.js +147 -1
  142. package/src/background-jobs/job-semantics.js +30 -0
  143. package/src/background-jobs/job.js +26 -1
  144. package/src/background-jobs/local-adapter.js +36 -8
  145. package/src/background-jobs/local-store.js +410 -39
  146. package/src/background-jobs/main.js +69 -2
  147. package/src/background-jobs/platform-job.js +26 -1
  148. package/src/background-jobs/runtime.js +58 -0
  149. package/src/background-jobs/store.js +374 -6
  150. package/src/background-jobs/types.js +27 -2
  151. package/src/background-jobs/web/controller.js +1 -0
  152. package/src/configuration.js +30 -6
  153. package/src/database/drivers/mssql/column.js +2 -2
  154. package/src/database/drivers/mssql/table.js +19 -5
  155. package/src/database/pool/async-tracked-multi-connection.js +2 -1
  156. package/src/database/query/alter-table-base.js +8 -1
  157. package/src/database/record/attachments/store.js +2 -2
  158. package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  159. package/src/environment-handlers/node/cli/commands/test.js +17 -69
  160. package/src/frontend-model-controller.js +22 -2
  161. package/src/frontend-models/websocket-channel.js +18 -0
  162. package/src/http-server/client/websocket-session.js +41 -4
  163. package/src/http-server/websocket-channel.js +14 -0
  164. package/src/http-server/websocket-event-log-store.js +85 -12
  165. package/src/http-server/websocket-events-host.js +17 -7
  166. package/src/testing/test-files-finder.js +32 -343
  167. package/src/testing/test-filter-parser.js +29 -248
  168. package/src/testing/test-profile-activity.js +1 -12
  169. package/src/testing/test-profile-output.js +8 -253
  170. package/src/testing/test-profiler.js +38 -895
  171. package/src/testing/test-runner.js +33 -9
  172. package/src/testing/test-suite-splitter.js +1 -301
  173. package/src/testing/timing-manifest.js +10 -344
  174. package/src/testing/velocious-attempt-executor.js +7 -1
@@ -11,6 +11,7 @@ import Logger from "../logger.js"
11
11
  * @property {Date | string} created_at - Creation time.
12
12
  * @property {string} id - Event id.
13
13
  * @property {string} payload_json - Serialized payload.
14
+ * @property {string | null} params_json - Serialized broadcast params, or null when the publish carried none.
14
15
  * @property {number | string} sequence - Sequence number.
15
16
  */
16
17
  /**
@@ -18,6 +19,16 @@ import Logger from "../logger.js"
18
19
  * @typedef {object} WebsocketReplayChannelRow
19
20
  * @property {string} channel - Channel name.
20
21
  */
22
+ /**
23
+ * Normalized persisted websocket event.
24
+ * @typedef {object} WebsocketPersistedEvent
25
+ * @property {string} channel - Channel name.
26
+ * @property {string} createdAt - ISO creation time.
27
+ * @property {string} id - Event id.
28
+ * @property {Record<string, ReturnType<typeof JSON.parse>> | null} params - Persisted broadcast params, or null when the publish carried none.
29
+ * @property {ReturnType<typeof JSON.parse>} payload - Event payload.
30
+ * @property {number} sequence - Sequence number.
31
+ */
21
32
  const EVENTS_TABLE = "websocket_channel_events"
22
33
  const REPLAY_CHANNELS_TABLE = "websocket_replay_channels"
23
34
  const DEFAULT_RETENTION_MS = 10 * 60 * 1000
@@ -100,22 +111,40 @@ export default class VelociousHttpServerWebsocketEventLogStore {
100
111
 
101
112
  /**
102
113
  * Runs schema present.
103
- * @returns {Promise<boolean>} - Whether both event-log tables physically exist.
114
+ * @returns {Promise<boolean>} - Whether both event-log tables exist and the events table carries every required column.
104
115
  */
105
116
  async _schemaPresent() {
106
117
  return await this._withDb(async (db) =>
107
- await db.tableExists(EVENTS_TABLE) && await db.tableExists(REPLAY_CHANNELS_TABLE)
118
+ (await db.tableExists(EVENTS_TABLE) && await db.tableExists(REPLAY_CHANNELS_TABLE))
119
+ && (await this._columnPresent(db, EVENTS_TABLE, "params_json"))
108
120
  )
109
121
  }
110
122
 
123
+ /**
124
+ * Runs column present.
125
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
126
+ * @param {string} tableName - Table name.
127
+ * @param {string} columnName - Column name.
128
+ * @returns {Promise<boolean>} - Whether the column exists on the table.
129
+ */
130
+ async _columnPresent(db, tableName, columnName) {
131
+ const tables = await db.getTables()
132
+ const table = tables.find((candidate) => candidate.getName() == tableName)
133
+
134
+ if (!table) return false
135
+
136
+ return (await table.getColumnByName(columnName)) !== undefined
137
+ }
138
+
111
139
  /**
112
140
  * Runs append event.
113
141
  * @param {object} args - Options.
114
142
  * @param {string} args.channel - Channel name.
115
143
  * @param {ReturnType<typeof JSON.parse>} args.payload - Event payload.
116
- * @returns {Promise<{channel: string, createdAt: string, id: string, payload: ReturnType<typeof JSON.parse>}>} - Persisted event row.
144
+ * @param {Record<string, ReturnType<typeof JSON.parse>> | null} [args.params] - Broadcast params to persist for stream-scoped replay, or null when the publish carried none.
145
+ * @returns {Promise<WebsocketPersistedEvent>} - Persisted event row.
117
146
  */
118
- async appendEvent({channel, payload}) {
147
+ async appendEvent({channel, params = null, payload}) {
119
148
  await this.ensureReady()
120
149
 
121
150
  const id = randomUUID()
@@ -128,10 +157,11 @@ export default class VelociousHttpServerWebsocketEventLogStore {
128
157
  channel,
129
158
  created_at: createdAt,
130
159
  id,
160
+ params_json: params === null ? null : JSON.stringify(params),
131
161
  payload_json: JSON.stringify(payload)
132
162
  }
133
163
  })
134
- return {channel, createdAt: createdAt.toISOString(), id, payload}
164
+ return {channel, createdAt: createdAt.toISOString(), id, params, payload}
135
165
  })
136
166
  }
137
167
 
@@ -139,8 +169,13 @@ export default class VelociousHttpServerWebsocketEventLogStore {
139
169
  * Runs mark channel interested.
140
170
  * @param {string} channel - Channel name.
141
171
  * @returns {Promise<void>} - Resolves when the channel interest was persisted.
172
+ * @throws {Error} When the channel is registered live-only, which forbids replay persistence.
142
173
  */
143
174
  async markChannelInterested(channel) {
175
+ if (this.configuration.isWebsocketChannelLiveOnly(channel)) {
176
+ throw new Error(`Websocket channel "${channel}" is registered live-only and cannot be marked interested in replay persistence`)
177
+ }
178
+
144
179
  await this.ensureReady()
145
180
 
146
181
  const interestedUntil = new Date(Date.now() + this.retentionMs)
@@ -158,6 +193,12 @@ export default class VelociousHttpServerWebsocketEventLogStore {
158
193
  * @returns {Promise<boolean>} - Whether the channel should be persisted for replay.
159
194
  */
160
195
  async shouldPersistChannel(channel) {
196
+ // A channel re-registered live-only can still hold cached or durable
197
+ // interest state from before the re-registration; the live-only
198
+ // contract must hold at the persistence decision, not only at
199
+ // interest marking.
200
+ if (this.configuration.isWebsocketChannelLiveOnly(channel)) return false
201
+
161
202
  if (this._channelInterestCached(channel)) return true
162
203
  if (this._interestedChannels.size === 0) return false
163
204
 
@@ -197,7 +238,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
197
238
  * @param {object} args - Options.
198
239
  * @param {string} args.channel - Channel name.
199
240
  * @param {string} args.id - Event id.
200
- * @returns {Promise<{channel: string, createdAt: string, id: string, payload: ReturnType<typeof JSON.parse>, sequence: number} | null>} - Event row or null.
241
+ * @returns {Promise<WebsocketPersistedEvent | null>} - Event row or null.
201
242
  */
202
243
  async getEventById({channel, id}) {
203
244
  await this.ensureReady()
@@ -237,7 +278,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
237
278
  * @param {string} args.channel - Channel name.
238
279
  * @param {number} args.sequence - Lower bound sequence.
239
280
  * @param {number | null | undefined} [args.upToSequence] - Inclusive ceiling sequence.
240
- * @returns {Promise<Array<{channel: string, createdAt: string, id: string, payload: ReturnType<typeof JSON.parse>, sequence: number}>>} - Ordered events.
281
+ * @returns {Promise<WebsocketPersistedEvent[]>} - Ordered events.
241
282
  */
242
283
  async getEventsAfter({channel, sequence, upToSequence}) {
243
284
  await this.ensureReady()
@@ -312,24 +353,55 @@ export default class VelociousHttpServerWebsocketEventLogStore {
312
353
  * @returns {Promise<void>} - Resolves when complete.
313
354
  */
314
355
  async _ensureEventsTable(db) {
315
- this.logger.info("Applying websocket event-log schema")
316
-
317
356
  if (await db.tableExists(EVENTS_TABLE)) {
318
- this.logger.info("Websocket event-log table already exists - skipping create")
357
+ await this._ensureEventsTableColumns(db)
319
358
  return
320
359
  }
321
360
 
361
+ this.logger.info("Applying websocket event-log schema")
362
+
322
363
  const eventTable = new TableData(EVENTS_TABLE, {ifNotExists: true})
323
364
 
324
365
  eventTable.integer("sequence", {autoIncrement: true, null: false, primaryKey: true})
325
366
  eventTable.string("id", {index: true, null: false})
326
367
  eventTable.string("channel", {index: true, null: false})
368
+ eventTable.text("params_json", {null: true})
327
369
  eventTable.text("payload_json", {null: false})
328
370
  eventTable.datetime("created_at", {index: true, null: false})
329
371
 
330
372
  await db.createTable(eventTable)
331
373
  }
332
374
 
375
+ /**
376
+ * Adds columns the current schema requires to an events table created by an
377
+ * older framework version, so upgrades keep working without a manual ALTER.
378
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
379
+ * @returns {Promise<void>} - Resolves when the events table schema is current.
380
+ */
381
+ async _ensureEventsTableColumns(db) {
382
+ if (await this._columnPresent(db, EVENTS_TABLE, "params_json")) return
383
+
384
+ this.logger.info("Adding params_json column to websocket event-log table")
385
+
386
+ const tableData = new TableData(EVENTS_TABLE)
387
+
388
+ tableData.addColumn("params_json", {isNewColumn: true, null: true, type: "text"})
389
+
390
+ try {
391
+ for (const sql of await db.alterTableSQLs(tableData)) {
392
+ await db.query(sql)
393
+ }
394
+ } catch (error) {
395
+ // A concurrent process can add the column between the presence check
396
+ // and the ALTER (multi-worker or rolling upgrade); the
397
+ // duplicate-column failure is then the expected outcome, not a real
398
+ // error. Anything else re-checks as absent and rethrows.
399
+ if (await this._columnPresent(db, EVENTS_TABLE, "params_json")) return
400
+
401
+ throw error
402
+ }
403
+ }
404
+
333
405
  /**
334
406
  * Runs ensure replay channels table.
335
407
  * @param {import("../database/drivers/base.js").default} db - Database connection.
@@ -352,7 +424,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
352
424
  * @param {string} args.channel - Channel name.
353
425
  * @param {import("../database/drivers/base.js").default} args.db - Database connection.
354
426
  * @param {string} args.id - Event id.
355
- * @returns {Promise<{channel: string, createdAt: string, id: string, payload: ReturnType<typeof JSON.parse>, sequence: number} | null>} - Event row or null.
427
+ * @returns {Promise<WebsocketPersistedEvent | null>} - Event row or null.
356
428
  */
357
429
  async _getEventById({channel, db, id}) {
358
430
  const rows = /** @type {WebsocketEventRow[]} */ (await db
@@ -370,7 +442,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
370
442
  /**
371
443
  * Runs normalize event row.
372
444
  * @param {WebsocketEventRow} row - Raw row.
373
- * @returns {{channel: string, createdAt: string, id: string, payload: ReturnType<typeof JSON.parse>, sequence: number}} - Normalized row.
445
+ * @returns {WebsocketPersistedEvent} - Normalized row.
374
446
  */
375
447
  _normalizeEventRow(row) {
376
448
  const createdAtValue = row.created_at
@@ -379,6 +451,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
379
451
  channel: row.channel,
380
452
  createdAt: createdAtValue instanceof Date ? createdAtValue.toISOString() : new Date(createdAtValue).toISOString(),
381
453
  id: row.id,
454
+ params: row.params_json === null || row.params_json === undefined ? null : JSON.parse(row.params_json),
382
455
  payload: JSON.parse(row.payload_json),
383
456
  sequence: Number(row.sequence)
384
457
  }
@@ -116,7 +116,7 @@ export class VelociousHttpServerWebsocketEventsHost {
116
116
  // Other channels chain onto their own tails and are not delayed.
117
117
  this._queuePublish({
118
118
  callback: async () => {
119
- const persistedEvent = await this._persistV2EventIfNeeded({body, channel, configuration})
119
+ const persistedEvent = await this._persistV2EventIfNeeded({body, broadcastParams, channel, configuration})
120
120
  const dispatchedTargets = new Set()
121
121
 
122
122
  for (const handler of this.broadcastHandlersByConfiguration.get(configuration) || []) {
@@ -159,7 +159,9 @@ export class VelociousHttpServerWebsocketEventsHost {
159
159
 
160
160
  if (configuration) {
161
161
  queuedPublish = previousTail.then(() => configuration.withoutCurrentTestDatabaseAccessScope(() => {
162
- return configuration.withoutCurrentConnectionContexts(callback)
162
+ return configuration.withoutCurrentConnectionContexts(() => {
163
+ return configuration.runWithTestSharedConnectionContexts(callback)
164
+ })
163
165
  }))
164
166
  } else {
165
167
  queuedPublish = previousTail.then(callback)
@@ -191,12 +193,13 @@ export class VelociousHttpServerWebsocketEventsHost {
191
193
  * Runs persist v2 event if needed.
192
194
  * @param {object} args - Options.
193
195
  * @param {ReturnType<typeof JSON.parse>} args.body - Event body.
196
+ * @param {Record<string, ReturnType<typeof JSON.parse>>} args.broadcastParams - Routing filter params.
194
197
  * @param {string} args.channel - Channel name.
195
198
  * @param {import("../configuration.js").default} args.configuration - Originating configuration.
196
199
  * @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata when storage is enabled.
197
200
  */
198
- async _persistV2EventIfNeeded({body, channel, configuration}) {
199
- return await this._persistChannelEventIfNeeded({channel, payload: body, configuration})
201
+ async _persistV2EventIfNeeded({body, broadcastParams, channel, configuration}) {
202
+ return await this._persistChannelEventIfNeeded({broadcastParams, channel, configuration, payload: body})
200
203
  }
201
204
 
202
205
  /**
@@ -211,14 +214,18 @@ export class VelociousHttpServerWebsocketEventsHost {
211
214
  }
212
215
 
213
216
  /**
214
- * Runs persist channel event if needed.
217
+ * Runs persist channel event if needed. Persists the broadcast's routing
218
+ * params (sanitized through the channel class's
219
+ * `replayableBroadcastParams`) so replay can re-apply `matches()` per
220
+ * stream instead of delivering the whole channel's log.
215
221
  * @param {object} args - Options object.
222
+ * @param {Record<string, ReturnType<typeof JSON.parse>> | null} [args.broadcastParams] - Broadcast params to persist for stream-scoped replay.
216
223
  * @param {string} args.channel - Channel name.
217
224
  * @param {ReturnType<typeof JSON.parse>} args.payload - Payload data.
218
225
  * @param {import("../configuration.js").default} [args.configuration] - Configuration owning the event store.
219
226
  * @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata.
220
227
  */
221
- async _persistChannelEventIfNeeded({channel, payload, configuration}) {
228
+ async _persistChannelEventIfNeeded({broadcastParams = null, channel, configuration, payload}) {
222
229
  const handler = this.handlers.values().next().value
223
230
  const eventConfiguration = configuration || handler?.configuration
224
231
 
@@ -229,7 +236,10 @@ export class VelociousHttpServerWebsocketEventsHost {
229
236
 
230
237
  if (!shouldPersist) return null
231
238
 
232
- const persistedEvent = await websocketEventLogStore.appendEvent({channel, payload})
239
+ const ChannelClass = eventConfiguration.getWebsocketChannelClass(channel)
240
+ const params = ChannelClass ? ChannelClass.replayableBroadcastParams(broadcastParams) : broadcastParams
241
+
242
+ const persistedEvent = await websocketEventLogStore.appendEvent({channel, params, payload})
233
243
 
234
244
  return {
235
245
  createdAt: persistedEvent.createdAt,
@@ -1,364 +1,53 @@
1
1
  // @ts-check
2
2
 
3
- import fs from "fs/promises"
4
- import path from "path"
3
+ import {
4
+ discoverTestFiles,
5
+ lineFiltersFromCandidates
6
+ } from "@velocious/testing/node"
5
7
 
6
- import fileExists from "../utils/file-exists.js"
7
- import Logger from "../logger.js"
8
8
  import restArgsError from "../utils/rest-args-error.js"
9
9
 
10
- // Incredibly complex class to find files in multiple simultanious running promises to do it as fast as possible.
11
- export default class TestFilesFinder {
12
- static IGNORED_NAMES = [".git", "node_modules"]
10
+ const VELOCIOUS_NODE_TEST_PATTERN = /(?<!\.browser)-(?:spec|test)\.(?:m)?js$/u
13
11
 
12
+ /** Compatibility facade for Velocious's published test-file finder path. */
13
+ export default class TestFilesFinder {
14
14
  /**
15
- * Runs constructor.
16
- * @param {object} args - Options object.
17
- * @param {string} args.directory - Directory path.
18
- * @param {string[]} [args.directories] - Directories.
19
- * @param {string[]} args.processArgs - Process args.
15
+ * Creates a Velocious-compatible package discovery adapter.
16
+ * @param {object} args - Discovery options.
17
+ * @param {string} args.directory - Discovery base directory.
18
+ * @param {string[]} [args.directories] - Default directories.
19
+ * @param {RegExp} [args.filePattern] - Test-file pattern override.
20
+ * @param {string[]} args.processArgs - Process arguments including the command name.
20
21
  */
21
- constructor({directory, directories, processArgs, ...restArgs}) {
22
+ constructor({directory, directories, filePattern, processArgs, ...restArgs}) {
22
23
  restArgsError(restArgs)
23
-
24
- this.directory = path.resolve(directory)
25
- this.logger = new Logger(this)
26
-
27
- if (directories) {
28
- this.directories = directories.map((entry) => path.resolve(entry))
29
- } else {
30
- this.directories = [
31
- `${this.directory}/__tests__`,
32
- `${this.directory}/tests`,
33
- `${this.directory}/spec`
34
- ]
35
- }
36
-
37
- this.findingCount = 0
38
- this.processArgs = processArgs
39
-
40
- /**
41
- * Narrows the runtime value to the documented type.
42
- * @type {string[]} */
43
- this.foundFiles = []
44
-
45
- /**
46
- * Narrows the runtime value to the documented type.
47
- * @type {Record<number, Promise<void>>} */
48
- this.findingPromises = {}
49
-
50
- /**
51
- * Narrows the runtime value to the documented type.
52
- * @type {string[]} */
53
- this.testArgs = this.processArgs.filter((processArg, index) => index != 0)
54
-
55
- /**
56
- * Narrows the runtime value to the documented type.
57
- * @type {string[]} */
58
- this.directoryArgs = []
59
-
60
- /**
61
- * Narrows the runtime value to the documented type.
62
- * @type {string[]} */
63
- this.directoryFullPaths = []
64
-
65
- /**
66
- * Narrows the runtime value to the documented type.
67
- * @type {string[]} */
68
- this.fileArgs = []
69
-
70
- /**
71
- * Narrows the runtime value to the documented type.
72
- * @type {string[]} */
73
- this.explicitFiles = []
74
-
75
- /**
76
- * Narrows the runtime value to the documented type.
77
- * @type {Record<string, number[]>} */
78
- this.lineFiltersByFile = {}
79
-
80
- this._argsPrepared = false
24
+ this.directory = directory
25
+ this.directories = directories
26
+ this.filePattern = filePattern
27
+ this.candidates = processArgs.slice(1).filter((argument) => argument !== "--")
28
+ /** @type {Record<string, number[]>} */
29
+ this.lineFiltersByFile = lineFiltersFromCandidates({
30
+ cwd: directory,
31
+ candidates: this.candidates
32
+ })
81
33
  }
82
34
 
83
35
  /**
84
- * Runs find test files.
85
- * @returns {Promise<string[]>} - Resolves with the test files.
36
+ * Discovers test files through the package implementation.
37
+ * @returns {Promise<string[]>} - Discovered absolute file paths.
86
38
  */
87
39
  async findTestFiles() {
88
- await this.prepareArgs()
89
-
90
- if (this.explicitFiles.length > 0 && this.directoryArgs.length === 0) {
91
- return Array.from(new Set(this.explicitFiles))
92
- }
93
-
94
- await this.withFindingCount(async () => {
95
- const hasExplicitArgs = this.directoryFullPaths.length > 0 || this.fileArgs.length > 0
96
- const directoriesToScan = hasExplicitArgs ? this.directoryFullPaths : this.directories
97
-
98
- for (const directory of directoriesToScan) {
99
- if (await fileExists(directory)) {
100
- await this.findTestFilesInDir(directory)
101
- }
102
- }
40
+ return await discoverTestFiles({
41
+ cwd: this.directory,
42
+ candidates: this.candidates,
43
+ directories: this.directories,
44
+ filePattern: this.filePattern || VELOCIOUS_NODE_TEST_PATTERN
103
45
  })
104
-
105
- await this.waitForFindingPromises()
106
-
107
- if (this.explicitFiles.length > 0) {
108
- this.foundFiles.push(...this.explicitFiles)
109
- }
110
-
111
- return Array.from(new Set(this.foundFiles))
112
46
  }
113
47
 
114
48
  /**
115
- * Runs get line filters by file.
116
- * @returns {Record<string, number[]>} - Line filters by file.
49
+ * Gets candidate-derived line filters.
50
+ * @returns {Record<string, number[]>} - Line filters keyed by absolute file path.
117
51
  */
118
52
  getLineFiltersByFile() { return this.lineFiltersByFile }
119
-
120
- /**
121
- * Runs finding promises length.
122
- * @returns {number} - The ing promises length.
123
- */
124
- findingPromisesLength() { return Object.keys(this.findingPromises).length }
125
-
126
- async waitForFindingPromises() {
127
- while (this.findingPromisesLength() > 0) {
128
- await this.waitForFindingPromisesIteration()
129
- }
130
- }
131
-
132
- /**
133
- * Runs wait for finding promises iteration.
134
- * @returns {Promise<void>} - Resolves when complete.
135
- */
136
- async waitForFindingPromisesIteration() {
137
- const unfinishedPromises = []
138
-
139
- for (const findingPromiseId in this.findingPromises) {
140
- const findingPromise = this.findingPromises[findingPromiseId]
141
-
142
- unfinishedPromises.push(findingPromise)
143
- }
144
-
145
- await Promise.all(unfinishedPromises)
146
- }
147
-
148
- /**
149
- * Runs with finding count.
150
- * @param {() => Promise<void>} callback - Callback function.
151
- * @returns {Promise<void>} - Resolves when complete.
152
- */
153
- withFindingCount(callback) {
154
- return new Promise((resolve) => {
155
- const findingPromise = callback()
156
- const findingCount = this.findingCount
157
-
158
- this.findingCount += 1
159
- this.findingPromises[findingCount] = findingPromise
160
-
161
- findingPromise.finally(() => {
162
- delete this.findingPromises[findingCount]
163
-
164
- resolve(undefined)
165
- })
166
- })
167
- }
168
-
169
- /**
170
- * Runs find test files in dir.
171
- * @param {string} dir - Dir.
172
- * @returns {Promise<void>} - Resolves when complete.
173
- */
174
- async findTestFilesInDir(dir) {
175
- await this.withFindingCount(async () => {
176
- const files = await fs.readdir(dir)
177
-
178
- for (const file of files) {
179
- if (TestFilesFinder.IGNORED_NAMES.includes(file)) {
180
- continue
181
- }
182
-
183
- const fullPath = `${dir}/${file}`
184
- const localPath = fullPath.replace(`${this.directory}/`, "")
185
- const isDir = (await fs.stat(fullPath)).isDirectory()
186
-
187
- if (isDir) {
188
- this.findTestFilesInDir(fullPath)
189
- } else {
190
- if (this.isFileMatchingRequirements(file, localPath)) {
191
- this.foundFiles.push(fullPath)
192
- }
193
- }
194
- }
195
- })
196
- }
197
-
198
- /**
199
- * Runs is file matching requirements.
200
- * @param {string} file - File.
201
- * @param {string} localPath - Local path.
202
- * @returns {boolean} - Whether file matching requirements.
203
- */
204
- isFileMatchingRequirements(file, localPath) {
205
- if (this.directoryArgs.length > 0) {
206
- for (const directoryArg of this.directoryArgs) {
207
- if (localPath.startsWith(directoryArg) && this.looksLikeTestFile(file)) {
208
- this.logger.debug("Found test file because matching dir and looks like this file:", file)
209
- return true
210
- }
211
- }
212
- }
213
-
214
- if (this.fileArgs.length > 0) {
215
- for (const fileArg of this.fileArgs) {
216
- if (fileArg == localPath) {
217
- this.logger.debug("Found test file because matching file arg:", file)
218
- return true
219
- }
220
- }
221
- }
222
-
223
- if (this.fileArgs.length == 0 && this.directoryArgs.length == 0 && this.looksLikeTestFile(file)) {
224
- this.logger.debug("Found test file because looks like this file:", file)
225
- return true
226
- }
227
-
228
- return false
229
- }
230
-
231
- /**
232
- * Runs looks like test file.
233
- * @param {string} file - File.
234
- * @returns {boolean} - Whether looks like test file.
235
- */
236
- looksLikeTestFile(file) {
237
- return Boolean(file.match(/-(spec|test)\.(m|)js$/))
238
- }
239
-
240
- /**
241
- * Runs prepare args.
242
- * @returns {Promise<void>} - Resolves when test args are prepared.
243
- */
244
- async prepareArgs() {
245
- if (this._argsPrepared) return
246
-
247
- for (const testArg of this.testArgs) {
248
- if (testArg === "--") continue
249
-
250
- const {cleanArg, line} = this.splitLineArg(testArg)
251
- const forceDirectory = testArg.endsWith("/") || testArg.endsWith(path.sep)
252
- const fullPath = path.isAbsolute(cleanArg) ? cleanArg : path.resolve(this.directory, cleanArg)
253
- const baseName = path.basename(this.directory)
254
- const hasBasePrefix = cleanArg === baseName || cleanArg.startsWith(`${baseName}/`) || cleanArg.startsWith(`${baseName}${path.sep}`)
255
- const basePrefixedFullPath = (!path.isAbsolute(cleanArg) && hasBasePrefix) ? path.resolve(path.dirname(this.directory), cleanArg) : null
256
- const fullPathCandidates = basePrefixedFullPath ? [basePrefixedFullPath] : [fullPath]
257
-
258
- if (forceDirectory) {
259
- const preferredLocalPath = this.toLocalPath(basePrefixedFullPath || fullPath)
260
- this.directoryArgs.push(this.ensureTrailingSlash(preferredLocalPath))
261
- this.directoryFullPaths.push(path.resolve(basePrefixedFullPath || fullPath))
262
- continue
263
- }
264
-
265
- try {
266
- let stats
267
- let resolvedFullPath
268
-
269
- for (const candidatePath of fullPathCandidates) {
270
- try {
271
- stats = await fs.stat(candidatePath)
272
- resolvedFullPath = candidatePath
273
- break
274
- } catch {
275
- // Keep searching
276
- }
277
- }
278
-
279
- if (!stats || !resolvedFullPath) throw new Error("Path not found")
280
- const localPath = this.toLocalPath(resolvedFullPath)
281
-
282
- if (stats.isDirectory()) {
283
- this.directoryArgs.push(this.ensureTrailingSlash(localPath))
284
- this.directoryFullPaths.push(resolvedFullPath)
285
- } else if (stats.isFile()) {
286
- this.fileArgs.push(localPath)
287
- this.explicitFiles.push(resolvedFullPath)
288
-
289
- if (line) {
290
- this.addLineFilter(resolvedFullPath, line)
291
- }
292
- }
293
- } catch {
294
- const fallbackLocalPath = this.toLocalPath(basePrefixedFullPath || fullPath)
295
- this.fileArgs.push(fallbackLocalPath)
296
-
297
- if (line) {
298
- this.addLineFilter(basePrefixedFullPath || fullPath, line)
299
- }
300
- }
301
- }
302
-
303
- this._argsPrepared = true
304
- }
305
-
306
- /**
307
- * Runs add line filter.
308
- * @param {string} filePath - File path.
309
- * @param {number} line - Line number.
310
- * @returns {void} - No return value.
311
- */
312
- addLineFilter(filePath, line) {
313
- const fullPath = path.resolve(filePath)
314
-
315
- if (!this.lineFiltersByFile[fullPath]) {
316
- this.lineFiltersByFile[fullPath] = []
317
- }
318
-
319
- if (!this.lineFiltersByFile[fullPath].includes(line)) {
320
- this.lineFiltersByFile[fullPath].push(line)
321
- }
322
- }
323
-
324
- /**
325
- * Runs split line arg.
326
- * @param {string} testArg - Test arg.
327
- * @returns {{cleanArg: string, line?: number}} - Cleaned arg and line.
328
- */
329
- splitLineArg(testArg) {
330
- const match = testArg.match(/^(.*):(\d+)$/)
331
-
332
- if (!match) {
333
- return {cleanArg: testArg}
334
- }
335
-
336
- const line = Number(match[2])
337
-
338
- if (!Number.isFinite(line)) {
339
- return {cleanArg: testArg}
340
- }
341
-
342
- return {cleanArg: match[1], line}
343
- }
344
-
345
- /**
346
- * Runs ensure trailing slash.
347
- * @param {string} localPath - Local path.
348
- * @returns {string} - Normalized local path with trailing slash.
349
- */
350
- ensureTrailingSlash(localPath) {
351
- if (localPath === "") return localPath
352
- return localPath.endsWith("/") ? localPath : `${localPath}/`
353
- }
354
-
355
- /**
356
- * Runs to local path.
357
- * @param {string} fullPath - Full path.
358
- * @returns {string} - Local path relative to the base directory.
359
- */
360
- toLocalPath(fullPath) {
361
- const relativePath = path.relative(this.directory, fullPath)
362
- return relativePath.split(path.sep).join("/")
363
- }
364
53
  }