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
@@ -10,6 +10,7 @@ import normalizeBackgroundJobError from "./normalize-error.js"
10
10
  import { coordinateSharedTransactionConnection } from "../testing/shared-transaction-connection-coordinator.js"
11
11
  import stableJsonStringify from "../utils/stable-json.js"
12
12
  import {
13
+ BACKGROUND_JOB_TERMINAL_STATUSES,
13
14
  BACKGROUND_JOB_EXECUTION_MODES,
14
15
  DEFAULT_BACKGROUND_JOB_EXECUTION_MODE,
15
16
  DEFAULT_BACKGROUND_JOB_QUEUE,
@@ -18,7 +19,9 @@ import {
18
19
  normalizeBackgroundJobExecutionMode,
19
20
  normalizeBackgroundJobMaxRetries,
20
21
  normalizeBackgroundJobQueue,
22
+ normalizeBackgroundJobScheduleKey,
21
23
  normalizeBackgroundJobScheduledAtMs,
24
+ normalizeBackgroundJobStatus,
22
25
  rescheduledBackgroundJobAtMs,
23
26
  retryDelayMs
24
27
  } from "./job-semantics.js"
@@ -100,6 +103,9 @@ const JOBS_INDEX_COLUMN_NAMES = [
100
103
  ]
101
104
  const IDEMPOTENCY_KEYS_TABLE = "background_job_idempotency_keys"
102
105
  const SCHEDULE_KEYS_TABLE = "background_job_schedule_keys"
106
+ const SCHEDULE_ORDER_WATERMARKS_TABLE = "background_job_schedule_order_watermarks"
107
+ const SCHEDULE_ORDER_WATERMARK_MIGRATION_VERSION = "20260911120000"
108
+ const SCHEDULE_HISTORY_ORDER_INDEX = "index_background_jobs_schedule_history_order"
103
109
  const CONCURRENCY_TABLE = "background_job_concurrency"
104
110
  const COUNTS_REVISION_TABLE = "background_job_count_revisions"
105
111
  const COUNTS_REVISION_KEY = "counts"
@@ -918,7 +924,9 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
918
924
  previousStatus = "handed_off"
919
925
  }
920
926
 
921
- await this._insertPreparedJob(db, {preparedJob, scheduleKey: normalizedScheduleKey})
927
+ const scheduleOrder = await this._nextScheduleOrder(db, normalizedScheduleKey)
928
+
929
+ await this._insertPreparedJob(db, {preparedJob, scheduleKey: normalizedScheduleKey, scheduleOrder})
922
930
  await db.upsert({
923
931
  tableName: SCHEDULE_KEYS_TABLE,
924
932
  data: {schedule_key: normalizedScheduleKey, job_id: preparedJob.jobId},
@@ -989,6 +997,78 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
989
997
  })
990
998
  }
991
999
 
1000
+ /**
1001
+ * Reads stable ownership and optional latest terminal history in one fenced transaction.
1002
+ * @param {string} scheduleKey - Stable logical schedule key.
1003
+ * @param {{includeLatestTerminal?: boolean}} [options] - Lookup options.
1004
+ * @returns {Promise<import("./types.js").BackgroundJobScheduledLookupResult>} - Normalized public jobs.
1005
+ */
1006
+ async getScheduledJob(scheduleKey, {includeLatestTerminal = false} = {}) {
1007
+ await this.ensureReady()
1008
+
1009
+ const normalizedScheduleKey = this._normalizeScheduleKey(scheduleKey)
1010
+
1011
+ if (typeof includeLatestTerminal !== "boolean") {
1012
+ throw VelociousError.safe("background job includeLatestTerminal must be a boolean")
1013
+ }
1014
+
1015
+ return await this._serializedCountMutation(async (db) => {
1016
+ return await this._scheduledJobLookup(db, {
1017
+ includeLatestTerminal,
1018
+ scheduleKey: normalizedScheduleKey
1019
+ })
1020
+ }, {
1021
+ advisoryLock: {
1022
+ failureMessage: "Failed to acquire background job schedule-key lock",
1023
+ name: this._scheduleKeyLockName(normalizedScheduleKey)
1024
+ }
1025
+ })
1026
+ }
1027
+
1028
+ /**
1029
+ * Moves only a future queued stable owner to the current time.
1030
+ * @param {string} scheduleKey - Stable logical schedule key.
1031
+ * @returns {Promise<import("./types.js").BackgroundJobWakeResult>} - Exact wake outcome.
1032
+ */
1033
+ async wakeScheduled(scheduleKey) {
1034
+ await this.ensureReady()
1035
+
1036
+ const normalizedScheduleKey = this._normalizeScheduleKey(scheduleKey)
1037
+
1038
+ return await this._serializedCountMutation(async (db) => {
1039
+ const job = await this._scheduledOwnerJob(db, normalizedScheduleKey)
1040
+
1041
+ if (!job || (job.status !== "queued" && job.status !== "handed_off")) return {jobId: null, outcome: "not_found"}
1042
+ if (job.status === "handed_off") return {jobId: job.id, outcome: "handed_off"}
1043
+
1044
+ const nowMs = this.clock.now()
1045
+
1046
+ if (Number(job.scheduledAtMs) <= nowMs) return {jobId: job.id, outcome: "already_due"}
1047
+
1048
+ const affectedRows = await this._updateAffectedRows(db, {
1049
+ tableName: JOBS_TABLE,
1050
+ data: {scheduled_at_ms: nowMs},
1051
+ conditions: {id: job.id, scheduled_at_ms: job.scheduledAtMs, status: "queued"}
1052
+ })
1053
+
1054
+ if (affectedRows === 1) return {jobId: job.id, outcome: "woken"}
1055
+
1056
+ const currentJob = await this._scheduledOwnerJob(db, normalizedScheduleKey)
1057
+
1058
+ if (currentJob?.status === "handed_off") return {jobId: currentJob.id, outcome: "handed_off"}
1059
+ if (currentJob?.status === "queued" && Number(currentJob.scheduledAtMs) <= nowMs) {
1060
+ return {jobId: currentJob.id, outcome: "already_due"}
1061
+ }
1062
+
1063
+ return {jobId: null, outcome: "not_found"}
1064
+ }, {
1065
+ advisoryLock: {
1066
+ failureMessage: "Failed to acquire background job schedule-key lock",
1067
+ name: this._scheduleKeyLockName(normalizedScheduleKey)
1068
+ }
1069
+ })
1070
+ }
1071
+
992
1072
  /**
993
1073
  * Runs next available job.
994
1074
  * @param {object} [args] - Options.
@@ -1764,6 +1844,22 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1764
1844
  if (await db.tableExists(MAIL_DELIVERY_OPERATIONS_TABLE)) await db.query(`DELETE FROM ${db.quoteTable(MAIL_DELIVERY_OPERATIONS_TABLE)}`)
1765
1845
  if (await db.tableExists(IDEMPOTENCY_KEYS_TABLE)) await db.query(`DELETE FROM ${db.quoteTable(IDEMPOTENCY_KEYS_TABLE)}`)
1766
1846
  if (await db.tableExists(SCHEDULE_KEYS_TABLE)) await db.query(`DELETE FROM ${db.quoteTable(SCHEDULE_KEYS_TABLE)}`)
1847
+ if (await db.tableExists(SCHEDULE_ORDER_WATERMARKS_TABLE)) {
1848
+ const watermarkRows = await db
1849
+ .newQuery()
1850
+ .from(SCHEDULE_ORDER_WATERMARKS_TABLE)
1851
+ .select("schedule_key")
1852
+ .results()
1853
+
1854
+ for (const watermarkRow of watermarkRows) {
1855
+ await db.delete({
1856
+ tableName: SCHEDULE_ORDER_WATERMARKS_TABLE,
1857
+ conditions: {
1858
+ schedule_key: String(/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (watermarkRow).schedule_key)
1859
+ }
1860
+ })
1861
+ }
1862
+ }
1767
1863
  await db.query(`DELETE FROM ${db.quoteTable(JOBS_TABLE)}`)
1768
1864
  if (await db.tableExists(CONCURRENCY_TABLE)) await db.query(`DELETE FROM ${db.quoteTable(CONCURRENCY_TABLE)}`)
1769
1865
  const deltas = Object.fromEntries(Object.entries(snapshot.counts).map(([key, value]) => [key, -value]))
@@ -1858,9 +1954,10 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1858
1954
  * @param {object} args - Insert input.
1859
1955
  * @param {PreparedBackgroundJob} args.preparedJob - Prepared job.
1860
1956
  * @param {string | null} args.scheduleKey - Historical stable key.
1957
+ * @param {number | null} [args.scheduleOrder] - Monotonic stable ownership order.
1861
1958
  * @returns {Promise<void>} - Resolves after insertion.
1862
1959
  */
1863
- async _insertPreparedJob(db, {preparedJob, scheduleKey}) {
1960
+ async _insertPreparedJob(db, {preparedJob, scheduleKey, scheduleOrder = null}) {
1864
1961
  const {concurrency} = preparedJob
1865
1962
 
1866
1963
  if (concurrency) {
@@ -1885,6 +1982,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1885
1982
  scheduled_at_ms: preparedJob.scheduledAtMs,
1886
1983
  created_at_ms: preparedJob.createdAtMs,
1887
1984
  schedule_key: scheduleKey,
1985
+ schedule_order: scheduleOrder,
1888
1986
  concurrency_key: concurrency?.concurrencyKey || null,
1889
1987
  max_concurrency: concurrency?.maxConcurrency || null,
1890
1988
  timeout_ms: preparedJob.timeoutMs,
@@ -1936,9 +2034,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1936
2034
  * @returns {string} - Validated key.
1937
2035
  */
1938
2036
  _normalizeScheduleKey(scheduleKey) {
1939
- if (typeof scheduleKey === "string" && scheduleKey.length > 0 && scheduleKey.length <= 255) return scheduleKey
1940
-
1941
- throw VelociousError.safe("background job scheduleKey must be a non-empty string of at most 255 characters")
2037
+ return normalizeBackgroundJobScheduleKey(scheduleKey)
1942
2038
  }
1943
2039
 
1944
2040
  /**
@@ -2118,6 +2214,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2118
2214
  table.bigint("scheduled_at_ms", {null: false, index: true})
2119
2215
  table.bigint("created_at_ms", {null: false, index: true})
2120
2216
  table.string("schedule_key", {null: true, index: true})
2217
+ table.bigint("schedule_order", {null: true})
2121
2218
  table.bigint("handed_off_at_ms", {null: true, index: true})
2122
2219
  table.string("handoff_id", {null: true})
2123
2220
  table.bigint("completed_at_ms", {null: true})
@@ -2224,6 +2321,8 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2224
2321
 
2225
2322
  await this._ensureQueueColumn(db)
2226
2323
  await this._ensureScheduleKeyColumn(db)
2324
+ await this._ensureScheduleOrderColumn(db)
2325
+ await this._ensureScheduleOrderWatermarksTable(db)
2227
2326
  await this._ensureJobTimeoutColumn(db)
2228
2327
  await this._ensureChildAcceptanceColumns(db)
2229
2328
  await this._ensureJobsTableIndexesOnce(db)
@@ -2375,6 +2474,117 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2375
2474
  }
2376
2475
  }
2377
2476
 
2477
+ /**
2478
+ * Idempotently adds monotonic schedule ownership history and its lookup index.
2479
+ * Existing rows remain null and use the documented legacy fallback ordering.
2480
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2481
+ * @returns {Promise<void>} - Resolves when ensured.
2482
+ */
2483
+ async _ensureScheduleOrderColumn(db) {
2484
+ const lockName = `${MIGRATION_SCOPE}:schedule_order_column`
2485
+ const acquired = await db.acquireAdvisoryLock(lockName)
2486
+
2487
+ if (!acquired) throw new Error("Failed to acquire background jobs schedule-order schema lock")
2488
+
2489
+ try {
2490
+ db.clearSchemaCache()
2491
+ let table = await db.getTableByNameOrFail(JOBS_TABLE)
2492
+
2493
+ if (!(await table.getColumnByName("schedule_order"))) {
2494
+ const tableData = new TableData(JOBS_TABLE)
2495
+
2496
+ tableData.bigint("schedule_order", {null: true})
2497
+ for (const sql of await db.alterTableSQLs(tableData)) await db.query(sql)
2498
+ db.clearSchemaCache()
2499
+ table = await db.getTableByNameOrFail(JOBS_TABLE)
2500
+ }
2501
+
2502
+ const indexNames = new Set((await table.getIndexes()).map((index) => index.getName()))
2503
+
2504
+ if (!indexNames.has(SCHEDULE_HISTORY_ORDER_INDEX)) {
2505
+ const sqls = await db.createIndexSQLs({
2506
+ columns: ["schedule_key", "schedule_order", "created_at_ms", "id"],
2507
+ ifNotExists: db.getType() === "sqlite",
2508
+ name: SCHEDULE_HISTORY_ORDER_INDEX,
2509
+ tableName: JOBS_TABLE
2510
+ })
2511
+
2512
+ for (const sql of sqls) await db.query(sql)
2513
+ db.clearSchemaCache()
2514
+ }
2515
+ } finally {
2516
+ await db.releaseAdvisoryLock(lockName)
2517
+ }
2518
+ }
2519
+
2520
+ /**
2521
+ * Creates the retention-independent schedule-order high-water table and
2522
+ * initializes it from the greatest retained ordered row for every key.
2523
+ * Legacy rows whose order is null deliberately do not establish a watermark.
2524
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2525
+ * @returns {Promise<void>} - Resolves when ensured and backfilled.
2526
+ */
2527
+ async _ensureScheduleOrderWatermarksTable(db) {
2528
+ const migrationVersion = SCHEDULE_ORDER_WATERMARK_MIGRATION_VERSION
2529
+ const migrationKey = this._migrationKey(migrationVersion)
2530
+ const tableExists = await db.tableExists(SCHEDULE_ORDER_WATERMARKS_TABLE)
2531
+
2532
+ if (tableExists && await this._hasMigration(db, migrationVersion)) return
2533
+
2534
+ const acquired = await db.acquireAdvisoryLock(migrationKey)
2535
+
2536
+ if (!acquired) throw new Error("Failed to acquire background jobs schedule-order watermark schema lock")
2537
+
2538
+ try {
2539
+ db.clearSchemaCache()
2540
+ const lockedTableExists = await db.tableExists(SCHEDULE_ORDER_WATERMARKS_TABLE)
2541
+ const alreadyApplied = await this._hasMigration(db, migrationVersion)
2542
+
2543
+ if (!lockedTableExists) {
2544
+ const table = new TableData(SCHEDULE_ORDER_WATERMARKS_TABLE, {ifNotExists: true})
2545
+
2546
+ table.string("schedule_key", {primaryKey: true})
2547
+ table.bigint("high_water_mark", {null: false})
2548
+ await db.createTable(table)
2549
+ db.clearSchemaCache()
2550
+ }
2551
+
2552
+ // Rebuild a missing table even when its migration ledger survived. The
2553
+ // retained job rows are the only compatible source for that recovery;
2554
+ // once rows are pruned, normal schema durability protects the watermark.
2555
+ if (!lockedTableExists || !alreadyApplied) await this._backfillScheduleOrderWatermarks(db)
2556
+ if (!alreadyApplied) await this._recordMigration(db, migrationVersion)
2557
+ } finally {
2558
+ await db.releaseAdvisoryLock(migrationKey)
2559
+ }
2560
+ }
2561
+
2562
+ /**
2563
+ * Backfills each key from its greatest retained non-legacy ownership order.
2564
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2565
+ * @returns {Promise<void>} - Resolves after all retained keys are represented.
2566
+ */
2567
+ async _backfillScheduleOrderWatermarks(db) {
2568
+ const keyRows = await db
2569
+ .newQuery()
2570
+ .from(JOBS_TABLE)
2571
+ .select("schedule_key")
2572
+ .whereNot({schedule_key: null})
2573
+ .whereNot({schedule_order: null})
2574
+ .distinct()
2575
+ .results()
2576
+
2577
+ for (const keyRow of keyRows) {
2578
+ const scheduleKey = String(/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (keyRow).schedule_key)
2579
+ const retainedOrder = await this._greatestRetainedScheduleOrder(db, scheduleKey)
2580
+ const currentWatermark = await this._scheduleOrderWatermark(db, scheduleKey)
2581
+
2582
+ if (retainedOrder === null) continue
2583
+ if (currentWatermark !== null && currentWatermark >= retainedOrder) continue
2584
+ await this._writeScheduleOrderWatermark(db, {scheduleKey, scheduleOrder: retainedOrder})
2585
+ }
2586
+ }
2587
+
2378
2588
  /**
2379
2589
  * Idempotently adds the `queue` column to an existing jobs table. Existing
2380
2590
  * rows read back as the default queue (see {@link _normalizeJobRow}), so no
@@ -2557,6 +2767,163 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2557
2767
  return this._normalizeJobRow(rows[0])
2558
2768
  }
2559
2769
 
2770
+ /**
2771
+ * Reads the job currently named by one stable owner row.
2772
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2773
+ * @param {string} scheduleKey - Validated stable schedule key.
2774
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Normalized owner job.
2775
+ */
2776
+ async _scheduledOwnerJob(db, scheduleKey) {
2777
+ const ownerRows = await db
2778
+ .newQuery()
2779
+ .from(SCHEDULE_KEYS_TABLE)
2780
+ .where({schedule_key: scheduleKey})
2781
+ .limit(1)
2782
+ .results()
2783
+ const ownerRow = ownerRows[0]
2784
+
2785
+ if (!ownerRow) return null
2786
+
2787
+ return await this._getJobRowById(db, String(ownerRow.job_id))
2788
+ }
2789
+
2790
+ /**
2791
+ * Assigns the next ownership order while the caller holds the schedule-key
2792
+ * advisory lock and count-revision transaction fence. The independent
2793
+ * watermark survives both ownership release and terminal-history pruning.
2794
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2795
+ * @param {string} scheduleKey - Validated stable schedule key.
2796
+ * @returns {Promise<number>} - Next monotonic ownership order.
2797
+ */
2798
+ async _nextScheduleOrder(db, scheduleKey) {
2799
+ const durableOrder = await this._scheduleOrderWatermark(db, scheduleKey)
2800
+ const retainedOrder = await this._greatestRetainedScheduleOrder(db, scheduleKey)
2801
+ let currentOrder = durableOrder
2802
+
2803
+ if (retainedOrder !== null && (currentOrder === null || retainedOrder > currentOrder)) currentOrder = retainedOrder
2804
+ const nextOrder = currentOrder === null ? 1 : currentOrder + 1
2805
+
2806
+ if (!Number.isSafeInteger(nextOrder)) {
2807
+ throw new Error(`Background job schedule ownership order exhausted for ${scheduleKey}`)
2808
+ }
2809
+
2810
+ await this._writeScheduleOrderWatermark(db, {scheduleKey, scheduleOrder: nextOrder})
2811
+
2812
+ return nextOrder
2813
+ }
2814
+
2815
+ /**
2816
+ * Finds the greatest retained non-legacy ownership order for migration and
2817
+ * rolling-upgrade compatibility. It is never the sole durability boundary.
2818
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2819
+ * @param {string} scheduleKey - Validated stable schedule key.
2820
+ * @returns {Promise<number | null>} - Greatest retained order, or null.
2821
+ */
2822
+ async _greatestRetainedScheduleOrder(db, scheduleKey) {
2823
+ const rows = await db
2824
+ .newQuery()
2825
+ .from(JOBS_TABLE)
2826
+ .select("schedule_order")
2827
+ .where({schedule_key: scheduleKey})
2828
+ .whereNot({schedule_order: null})
2829
+ .order("schedule_order DESC")
2830
+ .limit(1)
2831
+ .results()
2832
+ const row = rows[0]
2833
+
2834
+ if (!row) return null
2835
+
2836
+ return this._validatedScheduleOrder(
2837
+ /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row).schedule_order
2838
+ )
2839
+ }
2840
+
2841
+ /**
2842
+ * Reads and validates one retention-independent schedule-order watermark.
2843
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2844
+ * @param {string} scheduleKey - Validated stable schedule key.
2845
+ * @returns {Promise<number | null>} - Current watermark, or null before first ownership.
2846
+ */
2847
+ async _scheduleOrderWatermark(db, scheduleKey) {
2848
+ const rows = await db
2849
+ .newQuery()
2850
+ .from(SCHEDULE_ORDER_WATERMARKS_TABLE)
2851
+ .select("high_water_mark")
2852
+ .where({schedule_key: scheduleKey})
2853
+ .limit(1)
2854
+ .results()
2855
+ const row = rows[0]
2856
+
2857
+ if (!row) return null
2858
+
2859
+ return this._validatedScheduleOrder(
2860
+ /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row).high_water_mark
2861
+ )
2862
+ }
2863
+
2864
+ /**
2865
+ * Persists one schedule-order watermark without exposing it as a job row.
2866
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2867
+ * @param {object} args - Watermark identity and value.
2868
+ * @param {string} args.scheduleKey - Validated stable schedule key.
2869
+ * @param {number} args.scheduleOrder - Validated monotonic order.
2870
+ * @returns {Promise<void>} - Resolves after persistence.
2871
+ */
2872
+ async _writeScheduleOrderWatermark(db, {scheduleKey, scheduleOrder}) {
2873
+ await db.upsert({
2874
+ tableName: SCHEDULE_ORDER_WATERMARKS_TABLE,
2875
+ data: {high_water_mark: scheduleOrder, schedule_key: scheduleKey},
2876
+ conflictColumns: ["schedule_key"],
2877
+ updateColumns: ["high_water_mark"]
2878
+ })
2879
+ }
2880
+
2881
+ /**
2882
+ * Validates an ownership order loaded from durable storage.
2883
+ * @param {ReturnType<typeof JSON.parse>} value - Stored order.
2884
+ * @returns {number} - Positive safe integer ownership order.
2885
+ */
2886
+ _validatedScheduleOrder(value) {
2887
+ const scheduleOrder = this._normalizeNumber(value)
2888
+
2889
+ if (scheduleOrder === null || !Number.isSafeInteger(scheduleOrder) || scheduleOrder < 1) {
2890
+ throw new Error(`Invalid background job schedule ownership order: ${scheduleOrder}`)
2891
+ }
2892
+
2893
+ return scheduleOrder
2894
+ }
2895
+
2896
+ /**
2897
+ * Builds a stable-schedule lookup exclusively from normalized job rows.
2898
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2899
+ * @param {object} args - Lookup options.
2900
+ * @param {boolean} args.includeLatestTerminal - Whether terminal history is requested.
2901
+ * @param {string} args.scheduleKey - Validated stable schedule key.
2902
+ * @returns {Promise<import("./types.js").BackgroundJobScheduledLookupResult>} - Normalized public jobs.
2903
+ */
2904
+ async _scheduledJobLookup(db, {includeLatestTerminal, scheduleKey}) {
2905
+ const ownerJob = await this._scheduledOwnerJob(db, scheduleKey)
2906
+ const currentJob = ownerJob && (ownerJob.status === "queued" || ownerJob.status === "handed_off") ? ownerJob : null
2907
+
2908
+ if (!includeLatestTerminal) return {currentJob, latestTerminalJob: null}
2909
+
2910
+ const terminalStatuses = BACKGROUND_JOB_TERMINAL_STATUSES.map((status) => db.quote(status)).join(", ")
2911
+ const terminalRows = await db
2912
+ .newQuery()
2913
+ .from(JOBS_TABLE)
2914
+ .where({schedule_key: scheduleKey})
2915
+ .where(`${db.quoteColumn("status")} IN (${terminalStatuses})`)
2916
+ .order(`CASE WHEN ${db.quoteColumn("schedule_order")} IS NULL THEN 0 ELSE 1 END DESC`)
2917
+ .order("schedule_order DESC")
2918
+ .order("created_at_ms DESC")
2919
+ .order("id DESC")
2920
+ .limit(1)
2921
+ .results()
2922
+ const latestTerminalJob = terminalRows[0] ? this._normalizeJobRow(terminalRows[0]) : null
2923
+
2924
+ return {currentJob, latestTerminalJob}
2925
+ }
2926
+
2560
2927
  /**
2561
2928
  * Releases ownership only when the key still points at the expected job.
2562
2929
  * @param {import("../database/drivers/base.js").default} db - Database connection.
@@ -2738,7 +3105,8 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2738
3105
  executionMode,
2739
3106
  queue: row.queue ? String(row.queue) : DEFAULT_BACKGROUND_JOB_QUEUE,
2740
3107
  scheduleKey: row.schedule_key ? String(row.schedule_key) : null,
2741
- status: row.status ? String(row.status) : "queued",
3108
+ scheduleOrder: this._normalizeNumber(row.schedule_order),
3109
+ status: normalizeBackgroundJobStatus(row.status ? String(row.status) : "queued"),
2742
3110
  attempts: this._normalizeNumber(row.attempts),
2743
3111
  maxRetries: this._normalizeNumber(row.max_retries),
2744
3112
  scheduledAtMs: this._normalizeNumber(row.scheduled_at_ms),
@@ -6,6 +6,9 @@
6
6
  /** @typedef {"candidate" | "active" | "retired"} BackgroundJobsGenerationInitialState */
7
7
  /** @typedef {"starting" | "candidate" | "active" | "retiring" | "retired" | "stopped"} BackgroundJobsGenerationLifecycleState */
8
8
  /** @typedef {"missing-generation" | "unexpected-generation" | "malformed-generation" | "generation-mismatch" | "worker-admission-retired" | "worker-has-no-recoverable-handoffs"} BackgroundJobsGenerationRejectionReason */
9
+ /** @typedef {"queued" | "handed_off"} BackgroundJobActiveStatus */
10
+ /** @typedef {"cancelled" | "completed" | "failed" | "orphaned"} BackgroundJobTerminalStatus */
11
+ /** @typedef {BackgroundJobActiveStatus | BackgroundJobTerminalStatus} BackgroundJobStatus */
9
12
  /** @typedef {"exit" | "process-error" | "ipc-send"} PooledRunnerFailureOrigin */
10
13
  /** @typedef {"starting" | "running" | "retiring"} PooledRunnerLifecycleState */
11
14
  /** @typedef {"unexpected" | "job-timeout" | "worker-shutdown-timeout"} PooledRunnerTerminationReason */
@@ -96,6 +99,8 @@
96
99
  * @property {(args: {jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: BackgroundJobOptions, producerInvocationId?: string, producerProof?: BackgroundJobProducerProof}) => Promise<string>} enqueue - Enqueues a job.
97
100
  * @property {(args: {scheduleKey: string, jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: BackgroundJobOptions}) => Promise<BackgroundJobReplacementResult>} replaceScheduled - Replaces a stable schedule.
98
101
  * @property {(args: {scheduleKey: string}) => Promise<BackgroundJobCancellationResult>} cancelScheduled - Cancels a stable schedule.
102
+ * @property {(args: {scheduleKey: string, includeLatestTerminal?: boolean}) => Promise<BackgroundJobScheduledLookupResult>} getScheduledJob - Reads stable schedule ownership and history.
103
+ * @property {(args: {scheduleKey: string}) => Promise<BackgroundJobWakeResult>} wakeScheduled - Expedites a stable schedule owner.
99
104
  */
100
105
  /**
101
106
  * @typedef {object} BackgroundJobHandoff
@@ -154,7 +159,8 @@
154
159
  * @property {BackgroundJobExecutionMode} executionMode - How the job should run.
155
160
  * @property {string} queue - Queue name (defaults to `"default"`).
156
161
  * @property {string | null} scheduleKey - Stable logical schedule key retained for history.
157
- * @property {string} status - Current job status.
162
+ * @property {number | null} scheduleOrder - Transaction-assigned monotonic ownership order for this schedule key; Node preserves its high-water mark across terminal-history pruning. Null for legacy/non-scheduled rows.
163
+ * @property {BackgroundJobStatus} status - Current job status.
158
164
  * @property {number | null} attempts - Failure attempts count.
159
165
  * @property {number | null} maxRetries - Max retry attempts.
160
166
  * @property {number | null} scheduledAtMs - Next scheduled time in ms.
@@ -191,6 +197,19 @@
191
197
  * @property {string | null} jobId - Detached owner's job id, when one was active.
192
198
  * @property {BackgroundJobCancellationOutcome} outcome - Truthful best-effort outcome.
193
199
  */
200
+ /**
201
+ * @typedef {object} BackgroundJobScheduledLookupResult
202
+ * @property {BackgroundJobRow | null} currentJob - Current queued or handed-off owner.
203
+ * @property {BackgroundJobRow | null} latestTerminalJob - Latest terminal history when requested.
204
+ */
205
+ /**
206
+ * @typedef {"woken" | "already_due" | "handed_off" | "not_found"} BackgroundJobWakeOutcome
207
+ */
208
+ /**
209
+ * @typedef {object} BackgroundJobWakeResult
210
+ * @property {string | null} jobId - Current owner's durable job id, when found.
211
+ * @property {BackgroundJobWakeOutcome} outcome - Exact wake outcome.
212
+ */
194
213
  /**
195
214
  * @typedef {object} BackgroundJobFailureEvent
196
215
  * @property {BackgroundJobRow} job - Updated job row after failure handling.
@@ -222,6 +241,12 @@
222
241
  * @typedef {{type: "cancel-scheduled", scheduleKey: string}} BackgroundJobCancelScheduledMessage
223
242
  * @typedef {{type: "schedule-cancelled", jobId: string | null, outcome: BackgroundJobCancellationOutcome}} BackgroundJobScheduleCancelledMessage
224
243
  * @typedef {{type: "cancel-scheduled-error", error?: string}} BackgroundJobCancelScheduledErrorMessage
244
+ * @typedef {{type: "get-scheduled-job", scheduleKey: string, includeLatestTerminal?: boolean}} BackgroundJobGetScheduledMessage
245
+ * @typedef {{type: "scheduled-job", currentJob: BackgroundJobRow | null, latestTerminalJob: BackgroundJobRow | null}} BackgroundJobScheduledMessage
246
+ * @typedef {{type: "get-scheduled-job-error", error?: string}} BackgroundJobGetScheduledErrorMessage
247
+ * @typedef {{type: "wake-scheduled", scheduleKey: string}} BackgroundJobWakeScheduledMessage
248
+ * @typedef {{type: "schedule-woken", jobId: string | null, outcome: BackgroundJobWakeOutcome}} BackgroundJobScheduleWokenMessage
249
+ * @typedef {{type: "wake-scheduled-error", error?: string}} BackgroundJobWakeScheduledErrorMessage
225
250
  * @typedef {{type: "job", payload: BackgroundJobPayload}} BackgroundJobJobMessage
226
251
  * @typedef {{type: "job-accepted", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number}} BackgroundJobAcceptedMessage
227
252
  * @typedef {{type: "job-complete", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobCompleteMessage
@@ -231,7 +256,7 @@
231
256
  * @typedef {{type: "job-update-error", jobId: string, error?: string}} BackgroundJobUpdateErrorMessage
232
257
  */
233
258
  /**
234
- * @typedef {BackgroundJobHelloMessage | BackgroundJobGenerationAcceptedMessage | BackgroundJobGenerationRejectedMessage | BackgroundJobReadyMessage | BackgroundJobDrainingMessage | BackgroundJobHeartbeatMessage | BackgroundJobEnqueueMessage | BackgroundJobEnqueuedMessage | BackgroundJobEnqueueErrorMessage | BackgroundJobReplaceScheduledMessage | BackgroundJobScheduleReplacedMessage | BackgroundJobReplaceScheduledErrorMessage | BackgroundJobCancelScheduledMessage | BackgroundJobScheduleCancelledMessage | BackgroundJobCancelScheduledErrorMessage | BackgroundJobJobMessage | BackgroundJobAcceptedMessage | BackgroundJobCompleteMessage | BackgroundJobFailedMessage | BackgroundJobRescheduleMessage | BackgroundJobUpdatedMessage | BackgroundJobUpdateErrorMessage} BackgroundJobSocketMessage
259
+ * @typedef {BackgroundJobHelloMessage | BackgroundJobGenerationAcceptedMessage | BackgroundJobGenerationRejectedMessage | BackgroundJobReadyMessage | BackgroundJobDrainingMessage | BackgroundJobHeartbeatMessage | BackgroundJobEnqueueMessage | BackgroundJobEnqueuedMessage | BackgroundJobEnqueueErrorMessage | BackgroundJobReplaceScheduledMessage | BackgroundJobScheduleReplacedMessage | BackgroundJobReplaceScheduledErrorMessage | BackgroundJobCancelScheduledMessage | BackgroundJobScheduleCancelledMessage | BackgroundJobCancelScheduledErrorMessage | BackgroundJobGetScheduledMessage | BackgroundJobScheduledMessage | BackgroundJobGetScheduledErrorMessage | BackgroundJobWakeScheduledMessage | BackgroundJobScheduleWokenMessage | BackgroundJobWakeScheduledErrorMessage | BackgroundJobJobMessage | BackgroundJobAcceptedMessage | BackgroundJobCompleteMessage | BackgroundJobFailedMessage | BackgroundJobRescheduleMessage | BackgroundJobUpdatedMessage | BackgroundJobUpdateErrorMessage} BackgroundJobSocketMessage
235
260
  */
236
261
 
237
262
  export const nothing = {}
@@ -207,6 +207,7 @@ export default class VelociousBackgroundJobsWebController extends Controller {
207
207
  maxRetries: job.maxRetries,
208
208
  orphanedAtMs: job.orphanedAtMs,
209
209
  scheduleKey: job.scheduleKey,
210
+ scheduleOrder: job.scheduleOrder,
210
211
  scheduledAtMs: job.scheduledAtMs,
211
212
  status: job.status,
212
213
  workerId: job.workerId
@@ -408,6 +408,13 @@ export default class VelociousConfiguration {
408
408
  * @type {Map<string, typeof import("./http-server/websocket-channel.js").default>} */
409
409
  this._websocketChannelClasses = new Map()
410
410
 
411
+ /**
412
+ * Channel types registered with `{liveOnly: true}`: their traffic is
413
+ * never persisted for replay, and `markChannelInterested` rejects the
414
+ * name.
415
+ * @type {Set<string>} */
416
+ this._liveOnlyWebsocketChannels = new Set()
417
+
411
418
  /**
412
419
  * Stores the websocket channel subscriptions value.
413
420
  * @type {Map<string, Set<import("./http-server/websocket-channel.js").default>>} - channelType → live subscriptions across all sessions.
@@ -1126,6 +1133,7 @@ export default class VelociousConfiguration {
1126
1133
  }
1127
1134
 
1128
1135
  return {
1136
+ liveOnlyChannels: Array.from(this._liveOnlyWebsocketChannels),
1129
1137
  pausedSessions: this._pausedWebsocketSessions.size,
1130
1138
  registeredChannels: Array.from(this._websocketChannelClasses.keys()),
1131
1139
  registeredConnections: Array.from(this._websocketConnectionClasses.keys()),
@@ -2966,12 +2974,15 @@ export default class VelociousConfiguration {
2966
2974
  * Clients subscribe via `{type: "channel-subscribe", channelType: name, ...}`.
2967
2975
  * @param {string} name - Client-facing channel type name.
2968
2976
  * @param {typeof import("./http-server/websocket-channel.js").default} ChannelClass - Websocket channel class.
2977
+ * @param {{liveOnly?: boolean}} [options] - Registration options.
2969
2978
  * @returns {void}
2970
2979
  */
2971
- registerWebsocketChannel(name, ChannelClass) {
2980
+ registerWebsocketChannel(name, ChannelClass, {liveOnly = false} = {}) {
2972
2981
  if (!name) throw new Error("Channel name is required")
2973
2982
  if (!ChannelClass) throw new Error("ChannelClass is required")
2974
2983
  this._websocketChannelClasses.set(name, ChannelClass)
2984
+
2985
+ if (liveOnly) this._liveOnlyWebsocketChannels.add(name)
2975
2986
  }
2976
2987
 
2977
2988
  /**
@@ -2983,6 +2994,17 @@ export default class VelociousConfiguration {
2983
2994
  return this._websocketChannelClasses.get(name)
2984
2995
  }
2985
2996
 
2997
+ /**
2998
+ * Whether a channel type was registered with `{liveOnly: true}`.
2999
+ * Live-only channels are never persisted for replay: the event-log
3000
+ * store's `markChannelInterested` throws for their names.
3001
+ * @param {string} name - Channel type name to look up.
3002
+ * @returns {boolean} - Whether the channel is declared live-only.
3003
+ */
3004
+ isWebsocketChannelLiveOnly(name) {
3005
+ return this._liveOnlyWebsocketChannels.has(name)
3006
+ }
3007
+
2986
3008
  /**
2987
3009
  * Tracks a live channel subscription in the global routing registry.
2988
3010
  * Called by the session when `canSubscribe()` resolves truthy; the
@@ -3336,11 +3358,13 @@ export default class VelociousConfiguration {
3336
3358
  }
3337
3359
  const previousDelivery = this._localBroadcastDeliveryTails.get(subscription)
3338
3360
  const delivery = this.withoutCurrentConnectionContexts(() => {
3339
- return (previousDelivery || Promise.resolve())
3340
- .then(() => this._deliverWebsocketChannelBroadcast(subscription, body, deliveryMetadata))
3341
- .catch((error) => {
3342
- console.error(`broadcastToChannel: ${name} subscription ${subscription.subscriptionId} deliverBroadcast threw`, error)
3343
- })
3361
+ return this.runWithTestSharedConnectionContexts(() => {
3362
+ return (previousDelivery || Promise.resolve())
3363
+ .then(() => this._deliverWebsocketChannelBroadcast(subscription, body, deliveryMetadata))
3364
+ .catch((error) => {
3365
+ console.error(`broadcastToChannel: ${name} subscription ${subscription.subscriptionId} deliverBroadcast threw`, error)
3366
+ })
3367
+ })
3344
3368
  })
3345
3369
 
3346
3370
  this._localBroadcastDeliveryTails.set(subscription, delivery)
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseColumn from "../base-column.js"
4
- import {digg} from "diggerize"
4
+ import { digg } from "diggerize"
5
5
 
6
6
  export default class VelociousDatabaseDriversMssqlColumn extends BaseColumn {
7
7
  /**
@@ -37,6 +37,6 @@ export default class VelociousDatabaseDriversMssqlColumn extends BaseColumn {
37
37
  }
38
38
  }
39
39
 
40
- getPrimaryKey() { return digg(this, "data", "isIdentity") === 1 }
40
+ getPrimaryKey() { return digg(this, "data", "isPrimaryKey") === 1 }
41
41
  getType() { return digg(this, "data", "DATA_TYPE") }
42
42
  }