velocious 1.0.549 → 1.0.552

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 (56) hide show
  1. package/README.md +13 -0
  2. package/build/background-jobs/json-socket.js +25 -0
  3. package/build/background-jobs/socket-request.js +48 -6
  4. package/build/background-jobs/status-reporter.js +14 -2
  5. package/build/configuration-types.js +1 -0
  6. package/build/database/advisory-lock-runner.js +17 -8
  7. package/build/database/drivers/base.js +12 -0
  8. package/build/database/drivers/mssql/index.js +52 -3
  9. package/build/database/drivers/mysql/index.js +34 -0
  10. package/build/database/structure-sql-loader.js +12 -6
  11. package/build/src/background-jobs/json-socket.d.ts +20 -0
  12. package/build/src/background-jobs/json-socket.d.ts.map +1 -1
  13. package/build/src/background-jobs/json-socket.js +25 -1
  14. package/build/src/background-jobs/socket-request.d.ts +13 -1
  15. package/build/src/background-jobs/socket-request.d.ts.map +1 -1
  16. package/build/src/background-jobs/socket-request.js +44 -7
  17. package/build/src/background-jobs/status-reporter.d.ts +12 -1
  18. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  19. package/build/src/background-jobs/status-reporter.js +14 -3
  20. package/build/src/configuration-types.d.ts +5 -0
  21. package/build/src/configuration-types.d.ts.map +1 -1
  22. package/build/src/configuration-types.js +2 -1
  23. package/build/src/database/advisory-lock-runner.d.ts +25 -8
  24. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  25. package/build/src/database/advisory-lock-runner.js +18 -9
  26. package/build/src/database/drivers/base.d.ts +9 -0
  27. package/build/src/database/drivers/base.d.ts.map +1 -1
  28. package/build/src/database/drivers/base.js +12 -1
  29. package/build/src/database/drivers/mssql/index.d.ts +9 -0
  30. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  31. package/build/src/database/drivers/mssql/index.js +45 -2
  32. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  33. package/build/src/database/drivers/mysql/index.js +35 -1
  34. package/build/src/database/structure-sql-loader.d.ts.map +1 -1
  35. package/build/src/database/structure-sql-loader.js +14 -8
  36. package/build/src/testing/test.d.ts +2 -1
  37. package/build/src/testing/test.d.ts.map +1 -1
  38. package/build/src/testing/test.js +3 -2
  39. package/build/src/testing/wait-for-event.d.ts +46 -0
  40. package/build/src/testing/wait-for-event.d.ts.map +1 -0
  41. package/build/src/testing/wait-for-event.js +64 -0
  42. package/build/testing/test.js +2 -1
  43. package/build/testing/wait-for-event.js +72 -0
  44. package/build/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +2 -2
  46. package/src/background-jobs/json-socket.js +25 -0
  47. package/src/background-jobs/socket-request.js +48 -6
  48. package/src/background-jobs/status-reporter.js +14 -2
  49. package/src/configuration-types.js +1 -0
  50. package/src/database/advisory-lock-runner.js +17 -8
  51. package/src/database/drivers/base.js +12 -0
  52. package/src/database/drivers/mssql/index.js +52 -3
  53. package/src/database/drivers/mysql/index.js +34 -0
  54. package/src/database/structure-sql-loader.js +12 -6
  55. package/src/testing/test.js +2 -1
  56. package/src/testing/wait-for-event.js +72 -0
package/README.md CHANGED
@@ -141,6 +141,19 @@ Slowest 10 tests:
141
141
 
142
142
  The report is skipped for single-test runs. See [docs/testing-guidelines.md](docs/testing-guidelines.md).
143
143
 
144
+ Prefer waiting for a real signal or condition over sleeping a fixed duration. `waitForEvent(emitter, eventName, {timeoutMs, filter})` resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's `waitFor`.
145
+
146
+ ```js
147
+ import {waitForEvent} from "velocious/build/src/testing/test.js"
148
+ import waitFor from "awaitery/build/wait-for.js"
149
+
150
+ // Event-driven: resolves as soon as the matching event fires (no polling latency).
151
+ const {result} = await waitForEvent(jobRunner, "jobFinished", {filter: (event) => event.jobId === jobId})
152
+
153
+ // Condition polling: retries the callback until it stops throwing.
154
+ await waitFor(() => expect(await Message.count()).toEqual(1))
155
+ ```
156
+
144
157
  Velocious captures console output emitted while each test executes, but does not print passing-test output by default. When a test fails, Velocious prints a truncated `Console output:` block for that failed test and saves the full captured log under `tmp/screenshots` next to failure screenshots/browser logs/HTML. Each failed test summary prints the saved console log path.
145
158
 
146
159
  Configure console output behavior in your testing config file.
@@ -44,6 +44,19 @@ export default class JsonSocket extends EventEmitter {
44
44
  * the main's liveness sweep to drop a wedged/silent worker.
45
45
  * @type {number | undefined} */
46
46
  this.lastSeenAt = undefined
47
+ /**
48
+ * Internal test-only observability counter — NOT public API. Number of times
49
+ * `destroy()` has run, incremented immediately before the raw socket
50
+ * `destroy()` call so specs can assert the actual teardown method that ran
51
+ * rather than a self-reported flag. Do not read or depend on this outside tests.
52
+ * @type {number} */
53
+ this._destroyCallCount = 0
54
+ /**
55
+ * Internal test-only observability counter — NOT public API. Number of times
56
+ * `close()` has run, incremented immediately before the raw socket `end()`
57
+ * call. Do not read or depend on this outside tests.
58
+ * @type {number} */
59
+ this._closeCallCount = 0
47
60
  this.buffer = ""
48
61
  this.socket.setEncoding("utf8")
49
62
  this.socket.on("data", (chunk) => this._onData(String(chunk)))
@@ -91,6 +104,18 @@ export default class JsonSocket extends EventEmitter {
91
104
  * @returns {void}
92
105
  */
93
106
  close() {
107
+ this._closeCallCount++
94
108
  this.socket.end()
95
109
  }
110
+
111
+ /**
112
+ * Forcibly destroys the underlying socket. Unlike {@link close}, which
113
+ * half-closes gracefully via `end()`, this tears the connection down
114
+ * immediately so a stalled/aborted request does not leave the socket alive.
115
+ * @returns {void}
116
+ */
117
+ destroy() {
118
+ this._destroyCallCount++
119
+ this.socket.destroy()
120
+ }
96
121
  }
@@ -15,6 +15,16 @@ export default class BackgroundJobsSocketRequest {
15
15
  this.host = host
16
16
  this.port = port
17
17
  this.role = role
18
+ /**
19
+ * Internal test-only observability reference — NOT public API. Holds the
20
+ * JsonSocket wrapper this request created so the timeout spec can inspect the
21
+ * wrapper's own `destroy()`/`close()` call counters — direct evidence of which
22
+ * teardown method actually ran, not a self-reported flag. Retains the single
23
+ * (already torn-down) wrapper for the request's lifetime. Do not expose or
24
+ * depend on this outside tests.
25
+ * @type {JsonSocket | undefined}
26
+ */
27
+ this._jsonSocket = undefined
18
28
  }
19
29
 
20
30
  /**
@@ -23,28 +33,60 @@ export default class BackgroundJobsSocketRequest {
23
33
  * @param {object} args - Options.
24
34
  * @param {(jsonSocket: JsonSocket) => void} args.onConnect - Called after the socket connects.
25
35
  * @param {(args: {message: import("./types.js").BackgroundJobSocketMessage, resolve: (value: T) => void, reject: (error: Error) => void}) => void} args.onMessage - Message handler.
36
+ * @param {AbortSignal} [args.signal] - Aborts the request; on abort the pending socket is destroyed and the promise rejects with the signal reason when it is an Error, otherwise with a generic abort Error.
26
37
  * @returns {Promise<T>} - Resolved request value.
27
38
  */
28
- async run({onConnect, onMessage}) {
39
+ async run({onConnect, onMessage, signal}) {
29
40
  const socket = net.createConnection({host: this.host, port: this.port})
30
41
  const jsonSocket = new JsonSocket(socket)
31
42
 
43
+ this._jsonSocket = jsonSocket
44
+
32
45
  return await new Promise((resolve, reject) => {
33
46
  let finished = false
34
47
  /**
35
48
  * Finish.
49
+ * @param {object} options - Options.
50
+ * @param {boolean} [options.destroy] - Destroy the socket instead of gracefully closing it.
36
51
  * @param {() => void} callback - Finish callback.
37
52
  */
38
- const finish = (callback) => {
53
+ const finish = ({destroy = false} = {}, callback) => {
39
54
  if (finished) return
40
55
  finished = true
56
+ if (signal) signal.removeEventListener("abort", onAbort)
41
57
  jsonSocket.removeAllListeners()
42
- jsonSocket.close()
58
+
59
+ if (destroy) {
60
+ jsonSocket.destroy()
61
+ } else {
62
+ jsonSocket.close()
63
+ }
64
+
43
65
  callback()
44
66
  }
45
67
 
68
+ /**
69
+ * Handles a cooperative abort: tears down the pending socket and rejects
70
+ * with the signal reason when it is an Error.
71
+ * @returns {void}
72
+ */
73
+ const onAbort = () => {
74
+ const reason = signal?.reason
75
+
76
+ finish({destroy: true}, () => reject(reason instanceof Error ? reason : new Error("Background job socket request aborted")))
77
+ }
78
+
79
+ if (signal) {
80
+ if (signal.aborted) {
81
+ onAbort()
82
+ return
83
+ }
84
+
85
+ signal.addEventListener("abort", onAbort)
86
+ }
87
+
46
88
  jsonSocket.on("error", (error) => {
47
- finish(() => reject(error))
89
+ finish({}, () => reject(error))
48
90
  })
49
91
 
50
92
  /**
@@ -54,8 +96,8 @@ export default class BackgroundJobsSocketRequest {
54
96
  jsonSocket.on("message", (message) => {
55
97
  onMessage({
56
98
  message,
57
- resolve: (value) => finish(() => resolve(value)),
58
- reject: (error) => finish(() => reject(error))
99
+ resolve: (value) => finish({}, () => resolve(value)),
100
+ reject: (error) => finish({}, () => reject(error))
59
101
  })
60
102
  })
61
103
 
@@ -15,11 +15,20 @@ export default class BackgroundJobsStatusReporter {
15
15
  * @param {import("../configuration.js").default} args.configuration - Configuration.
16
16
  * @param {string} [args.host] - Host.
17
17
  * @param {number} [args.port] - Port.
18
+ * @param {number} [args.attemptTimeoutMs] - Per-attempt socket-request timeout in milliseconds (default: 5000).
18
19
  */
19
- constructor({configuration, host, port}) {
20
+ constructor({configuration, host, port, attemptTimeoutMs = 5000}) {
20
21
  this.configuration = configuration
21
22
  this.host = host
22
23
  this.port = port
24
+ this.attemptTimeoutMs = attemptTimeoutMs
25
+ /**
26
+ * Internal test-only observability state — NOT public API. References the most
27
+ * recent socket request so the timeout spec can inspect how its socket was torn
28
+ * down. Do not expose or depend on this outside tests.
29
+ * @type {BackgroundJobsSocketRequest | undefined}
30
+ */
31
+ this._lastRequest = undefined
23
32
  this.logger = new Logger(this)
24
33
  }
25
34
 
@@ -39,10 +48,13 @@ export default class BackgroundJobsStatusReporter {
39
48
  const host = this.host || config.host
40
49
  const port = typeof this.port === "number" ? this.port : config.port
41
50
 
42
- await timeout({timeout: 5000}, async () => {
51
+ await timeout({timeout: this.attemptTimeoutMs}, async ({control}) => {
43
52
  const request = new BackgroundJobsSocketRequest({host, port, role: "reporter"})
44
53
 
54
+ this._lastRequest = request
55
+
45
56
  await request.run({
57
+ signal: control.signal,
46
58
  onConnect: (jsonSocket) => {
47
59
  jsonSocket.send({
48
60
  type: status === "completed" ? "job-complete" : "job-failed",
@@ -72,6 +72,7 @@
72
72
  * @property {function() : ?} [getConnection] - Custom connection factory override.
73
73
  * @property {string} [host] - Database host.
74
74
  * @property {boolean} [migrations] - Whether migrations are enabled for this database.
75
+ * @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
75
76
  * @property {string} [password] - Password for the database user.
76
77
  * @property {number} [port] - Database port.
77
78
  * @property {string} [primaryKeyType] - Default type for implicit migration primary keys and references. Defaults to `uuid`.
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import timeout from "awaitery/build/timeout.js"
3
+ import timeout, {TimeoutError} from "awaitery/build/timeout.js"
4
4
 
5
5
  /**
6
6
  * Thrown when an advisory lock could not be acquired before `timeoutMs` elapsed.
@@ -67,9 +67,12 @@ export default class AdvisoryLockRunner {
67
67
 
68
68
  /**
69
69
  * Runs a callback after acquiring the advisory lock, waiting up to `timeoutMs`.
70
+ * When a `holdTimeoutMs` is set the callback receives a `TimeoutControl` from
71
+ * awaitery for cooperative cancellation (`control.check()`, `control.signal`,
72
+ * `control.timedOut`, `control.remaining()`).
70
73
  * @template T
71
74
  * @param {string} name - Lock name.
72
- * @param {() => Promise<T>} callback - Callback to invoke while the lock is held.
75
+ * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
73
76
  * @param {{timeoutMs?: number | null, holdTimeoutMs?: number | null}} [args] - Lock and hold timeout options.
74
77
  * @returns {Promise<T>} - Resolves with the callback result.
75
78
  */
@@ -87,9 +90,11 @@ export default class AdvisoryLockRunner {
87
90
 
88
91
  /**
89
92
  * Runs a callback only if the advisory lock can be acquired immediately.
93
+ * When a `holdTimeoutMs` is set the callback receives a `TimeoutControl` from
94
+ * awaitery for cooperative cancellation.
90
95
  * @template T
91
96
  * @param {string} name - Lock name.
92
- * @param {() => Promise<T>} callback - Callback to invoke while the lock is held.
97
+ * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
93
98
  * @param {{holdTimeoutMs?: number | null}} [args] - Hold timeout options.
94
99
  * @returns {Promise<T>} - Resolves with the callback result.
95
100
  */
@@ -108,7 +113,7 @@ export default class AdvisoryLockRunner {
108
113
  /**
109
114
  * Runs the lock holder callback and releases the lock from its owning connection.
110
115
  * @template T
111
- * @param {{callback: () => Promise<T>, connection: import("./drivers/base.js").default, holdTimeoutMs?: number | null, name: string}} args - Locked callback args.
116
+ * @param {{callback: (args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>, connection: import("./drivers/base.js").default, holdTimeoutMs?: number | null, name: string}} args - Locked callback args.
112
117
  * @returns {Promise<T>} - Resolves with the callback result.
113
118
  */
114
119
  async runLockedCallback({callback, connection, holdTimeoutMs, name}) {
@@ -160,9 +165,13 @@ export default class AdvisoryLockRunner {
160
165
  * Runs `callback`, rejecting with `AdvisoryLockHoldTimeoutError` if it has
161
166
  * not settled within `holdTimeoutMs`. The callback is not cancelled; callers
162
167
  * use a dedicated advisory-lock connection so the lock can still be released.
168
+ *
169
+ * The callback receives a `TimeoutControl` from awaitery, enabling cooperative
170
+ * cancellation via `control.check()`, `control.signal`, `control.timedOut`,
171
+ * and `control.remaining()`.
163
172
  * @template T
164
173
  * @param {string} name - Lock name (for the error message).
165
- * @param {() => Promise<T>} callback - Callback holding the lock.
174
+ * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback holding the lock.
166
175
  * @param {number | null} [holdTimeoutMs] - Max hold time; falsy disables the timeout.
167
176
  * @returns {Promise<T>} - Resolves with the callback result.
168
177
  */
@@ -174,15 +183,15 @@ export default class AdvisoryLockRunner {
174
183
  let callbackSettled = false
175
184
 
176
185
  try {
177
- return await timeout({timeout: holdTimeoutMs}, async () => {
186
+ return await timeout({timeout: holdTimeoutMs}, async ({control}) => {
178
187
  try {
179
- return await callback()
188
+ return await callback({control})
180
189
  } finally {
181
190
  callbackSettled = true
182
191
  }
183
192
  })
184
193
  } catch (error) {
185
- if (!callbackSettled) {
194
+ if (!callbackSettled || error instanceof TimeoutError) {
186
195
  throw new AdvisoryLockHoldTimeoutError(`Advisory lock ${JSON.stringify(name)} held longer than ${holdTimeoutMs}ms`, {name})
187
196
  }
188
197
 
@@ -583,6 +583,18 @@ export default class VelociousDatabaseDriversBase {
583
583
  return null
584
584
  }
585
585
 
586
+ /**
587
+ * Executes a whole multi-statement structure SQL script in a single round-trip when
588
+ * the driver supports it, running on this connection (so the caller's foreign-key
589
+ * handling applies). Returns true if it ran the whole script; false when the caller
590
+ * should run the statements individually. The base driver has no batch path.
591
+ * @param {string} _structureSql - Full multi-statement structure SQL.
592
+ * @returns {Promise<boolean>} - Whether the script was executed as one batch.
593
+ */
594
+ async execStructureScript(_structureSql) {
595
+ return false
596
+ }
597
+
586
598
  /**
587
599
  * Runs get table by name.
588
600
  * @param {string} name - Name.
@@ -23,6 +23,17 @@ import Upsert from "./sql/upsert.js"
23
23
  import Update from "./sql/update.js"
24
24
  import UUID from "pure-uuid"
25
25
 
26
+ /**
27
+ * SQL Server error number raised by `sp_releaseapplock` when the current
28
+ * session does not hold the requested application lock. Releasing a lock the
29
+ * session no longer holds is a normal race (a shared connection's final
30
+ * check-in may already have auto-released it), which the cross-driver
31
+ * `releaseAdvisoryLock` contract models by resolving to `false`. We translate
32
+ * this specific error into that result rather than letting it escape.
33
+ * @type {number}
34
+ */
35
+ const APPLOCK_NOT_HELD_ERROR_NUMBER = 1223
36
+
26
37
  export default class VelociousDatabaseDriversMssql extends Base{
27
38
  async connect() {
28
39
  const args = this.getArgs()
@@ -640,18 +651,56 @@ export default class VelociousDatabaseDriversMssql extends Base{
640
651
 
641
652
  /**
642
653
  * Runs release advisory lock.
654
+ *
655
+ * `sp_releaseapplock` returns 0 when the lock was released, but SQL Server
656
+ * raises error {@link APPLOCK_NOT_HELD_ERROR_NUMBER} instead of returning a
657
+ * failure code when the session does not currently hold the lock. That
658
+ * error aborts the batch before the trailing `SELECT` can run, so we catch
659
+ * it and resolve to `false` to honor the cross-driver contract for an
660
+ * already-unheld lock.
643
661
  * @param {string} name - Lock name.
644
662
  * @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
645
663
  */
646
664
  async _releaseAdvisoryLock(name) {
647
- const rows = await this.query(
648
- `DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_releaseapplock @Resource = ${this.quote(name)}, @LockOwner = 'Session'; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
649
- )
665
+ let rows
666
+
667
+ try {
668
+ rows = await this.query(
669
+ `DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_releaseapplock @Resource = ${this.quote(name)}, @LockOwner = 'Session'; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
670
+ )
671
+ } catch (error) {
672
+ if (this._isApplockNotHeldError(error)) return false
673
+
674
+ throw error
675
+ }
676
+
650
677
  const result = Number(rows?.[0]?.velocious_advisory_lock_result)
651
678
 
652
679
  return result === 0
653
680
  }
654
681
 
682
+ /**
683
+ * Detects the SQL Server "application lock is not currently held" error
684
+ * raised by `sp_releaseapplock`. It walks the wrapped-error cause chain
685
+ * because `query` re-wraps the driver's `RequestError` in a plain `Error`,
686
+ * and matches on the stable numeric error number rather than the message.
687
+ * @param {unknown} error - Error thrown while releasing the lock.
688
+ * @returns {boolean} - True if the error means the lock was not held by this session.
689
+ */
690
+ _isApplockNotHeldError(error) {
691
+ let current = error
692
+
693
+ while (current instanceof Error) {
694
+ const errorNumber = /** @type {{number?: unknown}} */ (current).number
695
+
696
+ if (typeof errorNumber === "number" && errorNumber === APPLOCK_NOT_HELD_ERROR_NUMBER) return true
697
+
698
+ current = current.cause
699
+ }
700
+
701
+ return false
702
+ }
703
+
655
704
  /**
656
705
  * Returns true if any session currently holds the application lock.
657
706
  *
@@ -190,6 +190,10 @@ export default class VelociousDatabaseDriversMysql extends Base{
190
190
 
191
191
  if ("username" in args) connectArgs["user"] = args["username"]
192
192
  if ("charset" in args) connectArgs["charset"] = args["charset"]
193
+ // Opt-in only. Lets a whole structure SQL dump run in one round-trip via
194
+ // {@link execStructureScript}; off by default so ordinary queries keep rejecting
195
+ // stacked statements.
196
+ if ("multipleStatements" in args) connectArgs["multipleStatements"] = Boolean(digg(args, "multipleStatements"))
193
197
 
194
198
  return connectArgs
195
199
  }
@@ -417,6 +421,36 @@ export default class VelociousDatabaseDriversMysql extends Base{
417
421
  })
418
422
  }
419
423
 
424
+ /**
425
+ * Executes a full multi-statement structure SQL script in one round-trip when the
426
+ * connection was configured with `multipleStatements: true`. Runs on the pooled
427
+ * connection so the caller's `SET FOREIGN_KEY_CHECKS = 0` applies. Returns false so
428
+ * the caller runs statements individually when multi-statement queries are off.
429
+ * @param {string} structureSql - Full multi-statement structure SQL.
430
+ * @returns {Promise<boolean>} - Whether the script was executed as one batch.
431
+ */
432
+ async execStructureScript(structureSql) {
433
+ if (!this.getArgs().multipleStatements) return false
434
+
435
+ // The batched pool call below bypasses Base#query, so re-run the same read-only
436
+ // write guard the per-statement path applies before executing the dump.
437
+ this._assertWritableQuery(structureSql)
438
+
439
+ if (!this.pool) await this.connect()
440
+ if (!this.pool) throw new Error("MySQL pool failed to initialize")
441
+
442
+ const pool = this.pool
443
+
444
+ await new Promise((resolve, reject) => {
445
+ pool.query(structureSql, (error) => {
446
+ if (error) reject(error)
447
+ else resolve(undefined)
448
+ })
449
+ })
450
+
451
+ return true
452
+ }
453
+
420
454
  /**
421
455
  * Runs query to sql.
422
456
  * @param {import("../../query/index.js").default} query - Query instance.
@@ -31,17 +31,23 @@ export default class StructureSqlLoader {
31
31
  await db.disableForeignKeys()
32
32
 
33
33
  try {
34
- if (executableConnection) {
35
- await executableConnection.exec(structureSql)
36
- } else {
37
- for (const statement of statements) {
38
- await db.query(statement)
34
+ // Prefer a single round-trip for the whole dump: a driver-native multi-statement
35
+ // batch (e.g. MySQL with `multipleStatements`) first, then a native `exec`
36
+ // connection (e.g. SQLite), and finally per-statement execution. Running the
37
+ // whole dump at once is far faster than issuing every CREATE separately.
38
+ if (!await db.execStructureScript(structureSql)) {
39
+ if (executableConnection) {
40
+ await executableConnection.exec(structureSql)
41
+ } else {
42
+ for (const statement of statements) {
43
+ await db.query(statement)
44
+ }
39
45
  }
40
46
  }
41
47
  } finally {
42
48
  await db.enableForeignKeys()
43
49
 
44
- // The native `exec` path mutates the schema outside `Base#query`, so the
50
+ // The batch / native `exec` paths mutate the schema outside `Base#query`, so the
45
51
  // usual post-DDL schema-cache invalidation never runs. Clear it here so a
46
52
  // caller that read schema metadata before provisioning (e.g. an empty table
47
53
  // list) does not keep seeing the pre-load schema afterwards. Harmless for the
@@ -40,6 +40,19 @@ export default class JsonSocket extends JsonSocket_base {
40
40
  * the main's liveness sweep to drop a wedged/silent worker.
41
41
  * @type {number | undefined} */
42
42
  lastSeenAt: number | undefined;
43
+ /**
44
+ * Internal test-only observability counter — NOT public API. Number of times
45
+ * `destroy()` has run, incremented immediately before the raw socket
46
+ * `destroy()` call so specs can assert the actual teardown method that ran
47
+ * rather than a self-reported flag. Do not read or depend on this outside tests.
48
+ * @type {number} */
49
+ _destroyCallCount: number;
50
+ /**
51
+ * Internal test-only observability counter — NOT public API. Number of times
52
+ * `close()` has run, incremented immediately before the raw socket `end()`
53
+ * call. Do not read or depend on this outside tests.
54
+ * @type {number} */
55
+ _closeCallCount: number;
43
56
  buffer: string;
44
57
  /**
45
58
  * Runs on data.
@@ -58,6 +71,13 @@ export default class JsonSocket extends JsonSocket_base {
58
71
  * @returns {void}
59
72
  */
60
73
  close(): void;
74
+ /**
75
+ * Forcibly destroys the underlying socket. Unlike {@link close}, which
76
+ * half-closes gracefully via `end()`, this tears the connection down
77
+ * immediately so a stalled/aborted request does not leave the socket alive.
78
+ * @returns {void}
79
+ */
80
+ destroy(): void;
61
81
  }
62
82
  export {};
63
83
  //# sourceMappingURL=json-socket.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-socket.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/json-socket.js"],"names":[],"mappings":";AAIA;IACE;;;OAGG;IACH,oBAFW,OAAO,KAAK,EAAE,MAAM,EA4C9B;IAxCC,kCAAoB;IACpB;;oCAEgC;IAChC,UADU,MAAM,GAAG,SAAS,CACH;IACzB;;yBAEqB;IACrB,4BADU,OAAO,CACsB;IACvC;;yBAEqB;IACrB,oBADU,OAAO,CACa;IAC9B;;yBAEqB;IACrB,mBADU,OAAO,CACY;IAC7B,sBAAsB;IACtB,mBADW,OAAO,CACY;IAC9B;;yBAEqB;IACrB,mBADU,OAAO,CACY;IAC7B;;;;;yBAKqB;IACrB,mBADU,OAAO,CACa;IAC9B;;;oCAGgC;IAChC,YADU,MAAM,GAAG,SAAS,CACD;IAC3B,eAAgB;IAOlB;;;;OAIG;IACH,eAHW,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;OAIG;IACH,cAHW,OAAC,GACC,IAAI,CAIhB;IAED;;;OAGG;IACH,SAFa,IAAI,CAIhB;CACF"}
1
+ {"version":3,"file":"json-socket.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/json-socket.js"],"names":[],"mappings":";AAIA;IACE;;;OAGG;IACH,oBAFW,OAAO,KAAK,EAAE,MAAM,EAyD9B;IArDC,kCAAoB;IACpB;;oCAEgC;IAChC,UADU,MAAM,GAAG,SAAS,CACH;IACzB;;yBAEqB;IACrB,4BADU,OAAO,CACsB;IACvC;;yBAEqB;IACrB,oBADU,OAAO,CACa;IAC9B;;yBAEqB;IACrB,mBADU,OAAO,CACY;IAC7B,sBAAsB;IACtB,mBADW,OAAO,CACY;IAC9B;;yBAEqB;IACrB,mBADU,OAAO,CACY;IAC7B;;;;;yBAKqB;IACrB,mBADU,OAAO,CACa;IAC9B;;;oCAGgC;IAChC,YADU,MAAM,GAAG,SAAS,CACD;IAC3B;;;;;wBAKoB;IACpB,mBADU,MAAM,CACU;IAC1B;;;;wBAIoB;IACpB,iBADU,MAAM,CACQ;IACxB,eAAgB;IAOlB;;;;OAIG;IACH,eAHW,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;OAIG;IACH,cAHW,OAAC,GACC,IAAI,CAIhB;IAED;;;OAGG;IACH,SAFa,IAAI,CAKhB;IAED;;;;;OAKG;IACH,WAFa,IAAI,CAKhB;CACF"}
@@ -42,6 +42,19 @@ export default class JsonSocket extends EventEmitter {
42
42
  * the main's liveness sweep to drop a wedged/silent worker.
43
43
  * @type {number | undefined} */
44
44
  this.lastSeenAt = undefined;
45
+ /**
46
+ * Internal test-only observability counter — NOT public API. Number of times
47
+ * `destroy()` has run, incremented immediately before the raw socket
48
+ * `destroy()` call so specs can assert the actual teardown method that ran
49
+ * rather than a self-reported flag. Do not read or depend on this outside tests.
50
+ * @type {number} */
51
+ this._destroyCallCount = 0;
52
+ /**
53
+ * Internal test-only observability counter — NOT public API. Number of times
54
+ * `close()` has run, incremented immediately before the raw socket `end()`
55
+ * call. Do not read or depend on this outside tests.
56
+ * @type {number} */
57
+ this._closeCallCount = 0;
45
58
  this.buffer = "";
46
59
  this.socket.setEncoding("utf8");
47
60
  this.socket.on("data", (chunk) => this._onData(String(chunk)));
@@ -85,7 +98,18 @@ export default class JsonSocket extends EventEmitter {
85
98
  * @returns {void}
86
99
  */
87
100
  close() {
101
+ this._closeCallCount++;
88
102
  this.socket.end();
89
103
  }
104
+ /**
105
+ * Forcibly destroys the underlying socket. Unlike {@link close}, which
106
+ * half-closes gracefully via `end()`, this tears the connection down
107
+ * immediately so a stalled/aborted request does not leave the socket alive.
108
+ * @returns {void}
109
+ */
110
+ destroy() {
111
+ this._destroyCallCount++;
112
+ this.socket.destroy();
113
+ }
90
114
  }
91
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNvbi1zb2NrZXQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYmFja2dyb3VuZC1qb2JzL2pzb24tc29ja2V0LmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWixPQUFPLFlBQVksTUFBTSwyQkFBMkIsQ0FBQTtBQUVwRCxNQUFNLENBQUMsT0FBTyxPQUFPLFVBQVcsU0FBUSxZQUFZO0lBQ2xEOzs7T0FHRztJQUNILFlBQVksTUFBTTtRQUNoQixLQUFLLEVBQUUsQ0FBQTtRQUNQLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFBO1FBQ3BCOzt3Q0FFZ0M7UUFDaEMsSUFBSSxDQUFDLFFBQVEsR0FBRyxTQUFTLENBQUE7UUFDekI7OzZCQUVxQjtRQUNyQixJQUFJLENBQUMsMEJBQTBCLEdBQUcsS0FBSyxDQUFBO1FBQ3ZDOzs2QkFFcUI7UUFDckIsSUFBSSxDQUFDLGtCQUFrQixHQUFHLElBQUksQ0FBQTtRQUM5Qjs7NkJBRXFCO1FBQ3JCLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFJLENBQUE7UUFDN0Isc0JBQXNCO1FBQ3RCLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxLQUFLLENBQUE7UUFDOUI7OzZCQUVxQjtRQUNyQixJQUFJLENBQUMsaUJBQWlCLEdBQUcsSUFBSSxDQUFBO1FBQzdCOzs7Ozs2QkFLcUI7UUFDckIsSUFBSSxDQUFDLGlCQUFpQixHQUFHLEtBQUssQ0FBQTtRQUM5Qjs7O3dDQUdnQztRQUNoQyxJQUFJLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQTtRQUMzQixJQUFJLENBQUMsTUFBTSxHQUFHLEVBQUUsQ0FBQTtRQUNoQixJQUFJLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxNQUFNLENBQUMsQ0FBQTtRQUMvQixJQUFJLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQTtRQUM5RCxJQUFJLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFBO1FBQ2pELElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQTtJQUMvRCxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNILE9BQU8sQ0FBQyxLQUFLO1FBQ1gsSUFBSSxDQUFDLE1BQU0sSUFBSSxLQUFLLENBQUE7UUFFcEIsT0FBTyxJQUFJLEVBQUUsQ0FBQztZQUNaLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFBO1lBQzlDLElBQUksWUFBWSxLQUFLLENBQUMsQ0FBQztnQkFBRSxNQUFLO1lBRTlCLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxZQUFZLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQTtZQUN0RCxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLFlBQVksR0FBRyxDQUFDLENBQUMsQ0FBQTtZQUVqRCxJQUFJLENBQUMsSUFBSTtnQkFBRSxTQUFRO1lBRW5CLElBQUksQ0FBQztnQkFDSCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFBO2dCQUNoQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxPQUFPLENBQUMsQ0FBQTtZQUMvQixDQUFDO1lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztnQkFDZixJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsQ0FBQTtZQUMzQixDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsSUFBSSxDQUFDLE9BQU87UUFDVixJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFBO0lBQ25ELENBQUM7SUFFRDs7O09BR0c7SUFDSCxLQUFLO1FBQ0gsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQTtJQUNuQixDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuaW1wb3J0IEV2ZW50RW1pdHRlciBmcm9tIFwiLi4vdXRpbHMvZXZlbnQtZW1pdHRlci5qc1wiXG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEpzb25Tb2NrZXQgZXh0ZW5kcyBFdmVudEVtaXR0ZXIge1xuICAvKipcbiAgICogUnVucyBjb25zdHJ1Y3Rvci5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCJuZXRcIikuU29ja2V0fSBzb2NrZXQgLSBTb2NrZXQgaW5zdGFuY2UuXG4gICAqL1xuICBjb25zdHJ1Y3Rvcihzb2NrZXQpIHtcbiAgICBzdXBlcigpXG4gICAgdGhpcy5zb2NrZXQgPSBzb2NrZXRcbiAgICAvKipcbiAgICAgKiBOYXJyb3dzIHRoZSBydW50aW1lIHZhbHVlIHRvIHRoZSBkb2N1bWVudGVkIHR5cGUuXG4gICAgICogQHR5cGUge3N0cmluZyB8IHVuZGVmaW5lZH0gKi9cbiAgICB0aGlzLndvcmtlcklkID0gdW5kZWZpbmVkXG4gICAgLyoqXG4gICAgICogTmFycm93cyB0aGUgcnVudGltZSB2YWx1ZSB0byB0aGUgZG9jdW1lbnRlZCB0eXBlLlxuICAgICAqIEB0eXBlIHtib29sZWFufSAqL1xuICAgIHRoaXMuc3VwcG9ydHNIYW5kb2ZmSWRSZXBvcnRpbmcgPSBmYWxzZVxuICAgIC8qKlxuICAgICAqIE5hcnJvd3MgdGhlIHJ1bnRpbWUgdmFsdWUgdG8gdGhlIGRvY3VtZW50ZWQgdHlwZS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn0gKi9cbiAgICB0aGlzLmFjY2VwdHNTcGF3bmVkSm9icyA9IHRydWVcbiAgICAvKipcbiAgICAgKiBOYXJyb3dzIHRoZSBydW50aW1lIHZhbHVlIHRvIHRoZSBkb2N1bWVudGVkIHR5cGUuXG4gICAgICogQHR5cGUge2Jvb2xlYW59ICovXG4gICAgdGhpcy5hY2NlcHRzRm9ya2VkSm9icyA9IHRydWVcbiAgICAvKiogQHR5cGUge2Jvb2xlYW59ICovXG4gICAgdGhpcy5hY2NlcHRzUG9vbGVkSm9icyA9IGZhbHNlXG4gICAgLyoqXG4gICAgICogTmFycm93cyB0aGUgcnVudGltZSB2YWx1ZSB0byB0aGUgZG9jdW1lbnRlZCB0eXBlLlxuICAgICAqIEB0eXBlIHtib29sZWFufSAqL1xuICAgIHRoaXMuYWNjZXB0c0lubGluZUpvYnMgPSB0cnVlXG4gICAgLyoqXG4gICAgICogV2hldGhlciB0aGlzIHdvcmtlciBhZHZlcnRpc2VkIGhlYXJ0YmVhdCBzdXBwb3J0IGluIGl0cyBoZWxsby4gT25seVxuICAgICAqIGhlYXJ0YmVhdC1jYXBhYmxlIHdvcmtlcnMgYXJlIHN1YmplY3QgdG8gdGhlIG1haW4ncyBzdGFsZS1saXZlbmVzc1xuICAgICAqIGV2aWN0aW9uOyBhIGxlZ2FjeSB3b3JrZXIgKGUuZy4gbWlkIHJvbGxpbmcgZGVwbG95KSBpcyBleGVtcHQgc28gaXRzXG4gICAgICogYWN0aXZlIGxlYXNlcyBhcmUgbm90IHJlbGVhc2VkIHdoaWxlIGl0IGlzIHN0aWxsIHJ1bm5pbmcgdGhlbS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn0gKi9cbiAgICB0aGlzLnN1cHBvcnRzSGVhcnRiZWF0ID0gZmFsc2VcbiAgICAvKipcbiAgICAgKiBMYXN0IHRpbWUgKG1zKSB0aGUgbWFpbiBzYXcgYW55IG1lc3NhZ2UgZnJvbSB0aGlzIHdvcmtlciBzb2NrZXQ7IHVzZWQgYnlcbiAgICAgKiB0aGUgbWFpbidzIGxpdmVuZXNzIHN3ZWVwIHRvIGRyb3AgYSB3ZWRnZWQvc2lsZW50IHdvcmtlci5cbiAgICAgKiBAdHlwZSB7bnVtYmVyIHwgdW5kZWZpbmVkfSAqL1xuICAgIHRoaXMubGFzdFNlZW5BdCA9IHVuZGVmaW5lZFxuICAgIHRoaXMuYnVmZmVyID0gXCJcIlxuICAgIHRoaXMuc29ja2V0LnNldEVuY29kaW5nKFwidXRmOFwiKVxuICAgIHRoaXMuc29ja2V0Lm9uKFwiZGF0YVwiLCAoY2h1bmspID0+IHRoaXMuX29uRGF0YShTdHJpbmcoY2h1bmspKSlcbiAgICB0aGlzLnNvY2tldC5vbihcImNsb3NlXCIsICgpID0+IHRoaXMuZW1pdChcImNsb3NlXCIpKVxuICAgIHRoaXMuc29ja2V0Lm9uKFwiZXJyb3JcIiwgKGVycm9yKSA9PiB0aGlzLmVtaXQoXCJlcnJvclwiLCBlcnJvcikpXG4gIH1cblxuICAvKipcbiAgICogUnVucyBvbiBkYXRhLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gY2h1bmsgLSBEYXRhIGNodW5rLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIF9vbkRhdGEoY2h1bmspIHtcbiAgICB0aGlzLmJ1ZmZlciArPSBjaHVua1xuXG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIGNvbnN0IG5ld2xpbmVJbmRleCA9IHRoaXMuYnVmZmVyLmluZGV4T2YoXCJcXG5cIilcbiAgICAgIGlmIChuZXdsaW5lSW5kZXggPT09IC0xKSBicmVha1xuXG4gICAgICBjb25zdCBsaW5lID0gdGhpcy5idWZmZXIuc2xpY2UoMCwgbmV3bGluZUluZGV4KS50cmltKClcbiAgICAgIHRoaXMuYnVmZmVyID0gdGhpcy5idWZmZXIuc2xpY2UobmV3bGluZUluZGV4ICsgMSlcblxuICAgICAgaWYgKCFsaW5lKSBjb250aW51ZVxuXG4gICAgICB0cnkge1xuICAgICAgICBjb25zdCBtZXNzYWdlID0gSlNPTi5wYXJzZShsaW5lKVxuICAgICAgICB0aGlzLmVtaXQoXCJtZXNzYWdlXCIsIG1lc3NhZ2UpXG4gICAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgICB0aGlzLmVtaXQoXCJlcnJvclwiLCBlcnJvcilcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUnVucyBzZW5kLlxuICAgKiBAcGFyYW0gez99IG1lc3NhZ2UgLSBNZXNzYWdlIHRvIHNlbmQuXG4gICAqIEByZXR1cm5zIHt2b2lkfVxuICAgKi9cbiAgc2VuZChtZXNzYWdlKSB7XG4gICAgdGhpcy5zb2NrZXQud3JpdGUoYCR7SlNPTi5zdHJpbmdpZnkobWVzc2FnZSl9XFxuYClcbiAgfVxuXG4gIC8qKlxuICAgKiBSdW5zIGNsb3NlLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIGNsb3NlKCkge1xuICAgIHRoaXMuc29ja2V0LmVuZCgpXG4gIH1cbn1cbiJdfQ==
115
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNvbi1zb2NrZXQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYmFja2dyb3VuZC1qb2JzL2pzb24tc29ja2V0LmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWixPQUFPLFlBQVksTUFBTSwyQkFBMkIsQ0FBQTtBQUVwRCxNQUFNLENBQUMsT0FBTyxPQUFPLFVBQVcsU0FBUSxZQUFZO0lBQ2xEOzs7T0FHRztJQUNILFlBQVksTUFBTTtRQUNoQixLQUFLLEVBQUUsQ0FBQTtRQUNQLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFBO1FBQ3BCOzt3Q0FFZ0M7UUFDaEMsSUFBSSxDQUFDLFFBQVEsR0FBRyxTQUFTLENBQUE7UUFDekI7OzZCQUVxQjtRQUNyQixJQUFJLENBQUMsMEJBQTBCLEdBQUcsS0FBSyxDQUFBO1FBQ3ZDOzs2QkFFcUI7UUFDckIsSUFBSSxDQUFDLGtCQUFrQixHQUFHLElBQUksQ0FBQTtRQUM5Qjs7NkJBRXFCO1FBQ3JCLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFJLENBQUE7UUFDN0Isc0JBQXNCO1FBQ3RCLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxLQUFLLENBQUE7UUFDOUI7OzZCQUVxQjtRQUNyQixJQUFJLENBQUMsaUJBQWlCLEdBQUcsSUFBSSxDQUFBO1FBQzdCOzs7Ozs2QkFLcUI7UUFDckIsSUFBSSxDQUFDLGlCQUFpQixHQUFHLEtBQUssQ0FBQTtRQUM5Qjs7O3dDQUdnQztRQUNoQyxJQUFJLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQTtRQUMzQjs7Ozs7NEJBS29CO1FBQ3BCLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxDQUFDLENBQUE7UUFDMUI7Ozs7NEJBSW9CO1FBQ3BCLElBQUksQ0FBQyxlQUFlLEdBQUcsQ0FBQyxDQUFBO1FBQ3hCLElBQUksQ0FBQyxNQUFNLEdBQUcsRUFBRSxDQUFBO1FBQ2hCLElBQUksQ0FBQyxNQUFNLENBQUMsV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFBO1FBQy9CLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFBO1FBQzlELElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUE7UUFDakQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFBO0lBQy9ELENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsT0FBTyxDQUFDLEtBQUs7UUFDWCxJQUFJLENBQUMsTUFBTSxJQUFJLEtBQUssQ0FBQTtRQUVwQixPQUFPLElBQUksRUFBRSxDQUFDO1lBQ1osTUFBTSxZQUFZLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7WUFDOUMsSUFBSSxZQUFZLEtBQUssQ0FBQyxDQUFDO2dCQUFFLE1BQUs7WUFFOUIsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLFlBQVksQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFBO1lBQ3RELElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsWUFBWSxHQUFHLENBQUMsQ0FBQyxDQUFBO1lBRWpELElBQUksQ0FBQyxJQUFJO2dCQUFFLFNBQVE7WUFFbkIsSUFBSSxDQUFDO2dCQUNILE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUE7Z0JBQ2hDLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLE9BQU8sQ0FBQyxDQUFBO1lBQy9CLENBQUM7WUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO2dCQUNmLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLEtBQUssQ0FBQyxDQUFBO1lBQzNCLENBQUM7UUFDSCxDQUFDO0lBQ0gsQ0FBQztJQUVEOzs7O09BSUc7SUFDSCxJQUFJLENBQUMsT0FBTztRQUNWLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7SUFDbkQsQ0FBQztJQUVEOzs7T0FHRztJQUNILEtBQUs7UUFDSCxJQUFJLENBQUMsZUFBZSxFQUFFLENBQUE7UUFDdEIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQTtJQUNuQixDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSCxPQUFPO1FBQ0wsSUFBSSxDQUFDLGlCQUFpQixFQUFFLENBQUE7UUFDeEIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUN2QixDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuaW1wb3J0IEV2ZW50RW1pdHRlciBmcm9tIFwiLi4vdXRpbHMvZXZlbnQtZW1pdHRlci5qc1wiXG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEpzb25Tb2NrZXQgZXh0ZW5kcyBFdmVudEVtaXR0ZXIge1xuICAvKipcbiAgICogUnVucyBjb25zdHJ1Y3Rvci5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCJuZXRcIikuU29ja2V0fSBzb2NrZXQgLSBTb2NrZXQgaW5zdGFuY2UuXG4gICAqL1xuICBjb25zdHJ1Y3Rvcihzb2NrZXQpIHtcbiAgICBzdXBlcigpXG4gICAgdGhpcy5zb2NrZXQgPSBzb2NrZXRcbiAgICAvKipcbiAgICAgKiBOYXJyb3dzIHRoZSBydW50aW1lIHZhbHVlIHRvIHRoZSBkb2N1bWVudGVkIHR5cGUuXG4gICAgICogQHR5cGUge3N0cmluZyB8IHVuZGVmaW5lZH0gKi9cbiAgICB0aGlzLndvcmtlcklkID0gdW5kZWZpbmVkXG4gICAgLyoqXG4gICAgICogTmFycm93cyB0aGUgcnVudGltZSB2YWx1ZSB0byB0aGUgZG9jdW1lbnRlZCB0eXBlLlxuICAgICAqIEB0eXBlIHtib29sZWFufSAqL1xuICAgIHRoaXMuc3VwcG9ydHNIYW5kb2ZmSWRSZXBvcnRpbmcgPSBmYWxzZVxuICAgIC8qKlxuICAgICAqIE5hcnJvd3MgdGhlIHJ1bnRpbWUgdmFsdWUgdG8gdGhlIGRvY3VtZW50ZWQgdHlwZS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn0gKi9cbiAgICB0aGlzLmFjY2VwdHNTcGF3bmVkSm9icyA9IHRydWVcbiAgICAvKipcbiAgICAgKiBOYXJyb3dzIHRoZSBydW50aW1lIHZhbHVlIHRvIHRoZSBkb2N1bWVudGVkIHR5cGUuXG4gICAgICogQHR5cGUge2Jvb2xlYW59ICovXG4gICAgdGhpcy5hY2NlcHRzRm9ya2VkSm9icyA9IHRydWVcbiAgICAvKiogQHR5cGUge2Jvb2xlYW59ICovXG4gICAgdGhpcy5hY2NlcHRzUG9vbGVkSm9icyA9IGZhbHNlXG4gICAgLyoqXG4gICAgICogTmFycm93cyB0aGUgcnVudGltZSB2YWx1ZSB0byB0aGUgZG9jdW1lbnRlZCB0eXBlLlxuICAgICAqIEB0eXBlIHtib29sZWFufSAqL1xuICAgIHRoaXMuYWNjZXB0c0lubGluZUpvYnMgPSB0cnVlXG4gICAgLyoqXG4gICAgICogV2hldGhlciB0aGlzIHdvcmtlciBhZHZlcnRpc2VkIGhlYXJ0YmVhdCBzdXBwb3J0IGluIGl0cyBoZWxsby4gT25seVxuICAgICAqIGhlYXJ0YmVhdC1jYXBhYmxlIHdvcmtlcnMgYXJlIHN1YmplY3QgdG8gdGhlIG1haW4ncyBzdGFsZS1saXZlbmVzc1xuICAgICAqIGV2aWN0aW9uOyBhIGxlZ2FjeSB3b3JrZXIgKGUuZy4gbWlkIHJvbGxpbmcgZGVwbG95KSBpcyBleGVtcHQgc28gaXRzXG4gICAgICogYWN0aXZlIGxlYXNlcyBhcmUgbm90IHJlbGVhc2VkIHdoaWxlIGl0IGlzIHN0aWxsIHJ1bm5pbmcgdGhlbS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn0gKi9cbiAgICB0aGlzLnN1cHBvcnRzSGVhcnRiZWF0ID0gZmFsc2VcbiAgICAvKipcbiAgICAgKiBMYXN0IHRpbWUgKG1zKSB0aGUgbWFpbiBzYXcgYW55IG1lc3NhZ2UgZnJvbSB0aGlzIHdvcmtlciBzb2NrZXQ7IHVzZWQgYnlcbiAgICAgKiB0aGUgbWFpbidzIGxpdmVuZXNzIHN3ZWVwIHRvIGRyb3AgYSB3ZWRnZWQvc2lsZW50IHdvcmtlci5cbiAgICAgKiBAdHlwZSB7bnVtYmVyIHwgdW5kZWZpbmVkfSAqL1xuICAgIHRoaXMubGFzdFNlZW5BdCA9IHVuZGVmaW5lZFxuICAgIC8qKlxuICAgICAqIEludGVybmFsIHRlc3Qtb25seSBvYnNlcnZhYmlsaXR5IGNvdW50ZXIg4oCUIE5PVCBwdWJsaWMgQVBJLiBOdW1iZXIgb2YgdGltZXNcbiAgICAgKiBgZGVzdHJveSgpYCBoYXMgcnVuLCBpbmNyZW1lbnRlZCBpbW1lZGlhdGVseSBiZWZvcmUgdGhlIHJhdyBzb2NrZXRcbiAgICAgKiBgZGVzdHJveSgpYCBjYWxsIHNvIHNwZWNzIGNhbiBhc3NlcnQgdGhlIGFjdHVhbCB0ZWFyZG93biBtZXRob2QgdGhhdCByYW5cbiAgICAgKiByYXRoZXIgdGhhbiBhIHNlbGYtcmVwb3J0ZWQgZmxhZy4gRG8gbm90IHJlYWQgb3IgZGVwZW5kIG9uIHRoaXMgb3V0c2lkZSB0ZXN0cy5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfSAqL1xuICAgIHRoaXMuX2Rlc3Ryb3lDYWxsQ291bnQgPSAwXG4gICAgLyoqXG4gICAgICogSW50ZXJuYWwgdGVzdC1vbmx5IG9ic2VydmFiaWxpdHkgY291bnRlciDigJQgTk9UIHB1YmxpYyBBUEkuIE51bWJlciBvZiB0aW1lc1xuICAgICAqIGBjbG9zZSgpYCBoYXMgcnVuLCBpbmNyZW1lbnRlZCBpbW1lZGlhdGVseSBiZWZvcmUgdGhlIHJhdyBzb2NrZXQgYGVuZCgpYFxuICAgICAqIGNhbGwuIERvIG5vdCByZWFkIG9yIGRlcGVuZCBvbiB0aGlzIG91dHNpZGUgdGVzdHMuXG4gICAgICogQHR5cGUge251bWJlcn0gKi9cbiAgICB0aGlzLl9jbG9zZUNhbGxDb3VudCA9IDBcbiAgICB0aGlzLmJ1ZmZlciA9IFwiXCJcbiAgICB0aGlzLnNvY2tldC5zZXRFbmNvZGluZyhcInV0ZjhcIilcbiAgICB0aGlzLnNvY2tldC5vbihcImRhdGFcIiwgKGNodW5rKSA9PiB0aGlzLl9vbkRhdGEoU3RyaW5nKGNodW5rKSkpXG4gICAgdGhpcy5zb2NrZXQub24oXCJjbG9zZVwiLCAoKSA9PiB0aGlzLmVtaXQoXCJjbG9zZVwiKSlcbiAgICB0aGlzLnNvY2tldC5vbihcImVycm9yXCIsIChlcnJvcikgPT4gdGhpcy5lbWl0KFwiZXJyb3JcIiwgZXJyb3IpKVxuICB9XG5cbiAgLyoqXG4gICAqIFJ1bnMgb24gZGF0YS5cbiAgICogQHBhcmFtIHtzdHJpbmd9IGNodW5rIC0gRGF0YSBjaHVuay5cbiAgICogQHJldHVybnMge3ZvaWR9XG4gICAqL1xuICBfb25EYXRhKGNodW5rKSB7XG4gICAgdGhpcy5idWZmZXIgKz0gY2h1bmtcblxuICAgIHdoaWxlICh0cnVlKSB7XG4gICAgICBjb25zdCBuZXdsaW5lSW5kZXggPSB0aGlzLmJ1ZmZlci5pbmRleE9mKFwiXFxuXCIpXG4gICAgICBpZiAobmV3bGluZUluZGV4ID09PSAtMSkgYnJlYWtcblxuICAgICAgY29uc3QgbGluZSA9IHRoaXMuYnVmZmVyLnNsaWNlKDAsIG5ld2xpbmVJbmRleCkudHJpbSgpXG4gICAgICB0aGlzLmJ1ZmZlciA9IHRoaXMuYnVmZmVyLnNsaWNlKG5ld2xpbmVJbmRleCArIDEpXG5cbiAgICAgIGlmICghbGluZSkgY29udGludWVcblxuICAgICAgdHJ5IHtcbiAgICAgICAgY29uc3QgbWVzc2FnZSA9IEpTT04ucGFyc2UobGluZSlcbiAgICAgICAgdGhpcy5lbWl0KFwibWVzc2FnZVwiLCBtZXNzYWdlKVxuICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgdGhpcy5lbWl0KFwiZXJyb3JcIiwgZXJyb3IpXG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIFJ1bnMgc2VuZC5cbiAgICogQHBhcmFtIHs/fSBtZXNzYWdlIC0gTWVzc2FnZSB0byBzZW5kLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIHNlbmQobWVzc2FnZSkge1xuICAgIHRoaXMuc29ja2V0LndyaXRlKGAke0pTT04uc3RyaW5naWZ5KG1lc3NhZ2UpfVxcbmApXG4gIH1cblxuICAvKipcbiAgICogUnVucyBjbG9zZS5cbiAgICogQHJldHVybnMge3ZvaWR9XG4gICAqL1xuICBjbG9zZSgpIHtcbiAgICB0aGlzLl9jbG9zZUNhbGxDb3VudCsrXG4gICAgdGhpcy5zb2NrZXQuZW5kKClcbiAgfVxuXG4gIC8qKlxuICAgKiBGb3JjaWJseSBkZXN0cm95cyB0aGUgdW5kZXJseWluZyBzb2NrZXQuIFVubGlrZSB7QGxpbmsgY2xvc2V9LCB3aGljaFxuICAgKiBoYWxmLWNsb3NlcyBncmFjZWZ1bGx5IHZpYSBgZW5kKClgLCB0aGlzIHRlYXJzIHRoZSBjb25uZWN0aW9uIGRvd25cbiAgICogaW1tZWRpYXRlbHkgc28gYSBzdGFsbGVkL2Fib3J0ZWQgcmVxdWVzdCBkb2VzIG5vdCBsZWF2ZSB0aGUgc29ja2V0IGFsaXZlLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIGRlc3Ryb3koKSB7XG4gICAgdGhpcy5fZGVzdHJveUNhbGxDb3VudCsrXG4gICAgdGhpcy5zb2NrZXQuZGVzdHJveSgpXG4gIH1cbn1cbiJdfQ==
@@ -14,21 +14,33 @@ export default class BackgroundJobsSocketRequest {
14
14
  host: string;
15
15
  port: number;
16
16
  role: "client" | "reporter";
17
+ /**
18
+ * Internal test-only observability reference — NOT public API. Holds the
19
+ * JsonSocket wrapper this request created so the timeout spec can inspect the
20
+ * wrapper's own `destroy()`/`close()` call counters — direct evidence of which
21
+ * teardown method actually ran, not a self-reported flag. Retains the single
22
+ * (already torn-down) wrapper for the request's lifetime. Do not expose or
23
+ * depend on this outside tests.
24
+ * @type {JsonSocket | undefined}
25
+ */
26
+ _jsonSocket: JsonSocket | undefined;
17
27
  /**
18
28
  * Runs run.
19
29
  * @template T
20
30
  * @param {object} args - Options.
21
31
  * @param {(jsonSocket: JsonSocket) => void} args.onConnect - Called after the socket connects.
22
32
  * @param {(args: {message: import("./types.js").BackgroundJobSocketMessage, resolve: (value: T) => void, reject: (error: Error) => void}) => void} args.onMessage - Message handler.
33
+ * @param {AbortSignal} [args.signal] - Aborts the request; on abort the pending socket is destroyed and the promise rejects with the signal reason when it is an Error, otherwise with a generic abort Error.
23
34
  * @returns {Promise<T>} - Resolved request value.
24
35
  */
25
- run<T>({ onConnect, onMessage }: {
36
+ run<T>({ onConnect, onMessage, signal }: {
26
37
  onConnect: (jsonSocket: JsonSocket) => void;
27
38
  onMessage: (args: {
28
39
  message: import("./types.js").BackgroundJobSocketMessage;
29
40
  resolve: (value: T) => void;
30
41
  reject: (error: Error) => void;
31
42
  }) => void;
43
+ signal?: AbortSignal | undefined;
32
44
  }): Promise<T>;
33
45
  }
34
46
  import JsonSocket from "./json-socket.js";
@@ -1 +1 @@
1
- {"version":3,"file":"socket-request.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/socket-request.js"],"names":[],"mappings":"AAKA;IACE;;;;;;OAMG;IACH,kCAJG;QAAqB,IAAI,EAAjB,MAAM;QACO,IAAI,EAAjB,MAAM;QACsB,IAAI,EAAhC,QAAQ,GAAG,UAAU;KAC/B,EAKA;IAHC,aAAgB;IAChB,aAAgB;IAChB,4BAAgB;IAGlB;;;;;;;OAOG;IACH,IANa,CAAC,4BAEX;QAA+C,SAAS,EAAhD,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI;QAC8G,SAAS,EAAvJ,CAAC,IAAI,EAAE;YAAC,OAAO,EAAE,OAAO,YAAY,EAAE,0BAA0B,CAAC;YAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;YAAC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;SAAC,KAAK,IAAI;KAC/I,GAAU,OAAO,CAAC,CAAC,CAAC,CAyCtB;CACF;uBAhEsB,kBAAkB"}
1
+ {"version":3,"file":"socket-request.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/socket-request.js"],"names":[],"mappings":"AAKA;IACE;;;;;;OAMG;IACH,kCAJG;QAAqB,IAAI,EAAjB,MAAM;QACO,IAAI,EAAjB,MAAM;QACsB,IAAI,EAAhC,QAAQ,GAAG,UAAU;KAC/B,EAeA;IAbC,aAAgB;IAChB,aAAgB;IAChB,4BAAgB;IAChB;;;;;;;;OAQG;IACH,aAFU,UAAU,GAAG,SAAS,CAEJ;IAG9B;;;;;;;;OAQG;IACH,IAPa,CAAC,oCAEX;QAA+C,SAAS,EAAhD,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI;QAC8G,SAAS,EAAvJ,CAAC,IAAI,EAAE;YAAC,OAAO,EAAE,OAAO,YAAY,EAAE,0BAA0B,CAAC;YAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;YAAC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;SAAC,KAAK,IAAI;QACpH,MAAM;KACjC,GAAU,OAAO,CAAC,CAAC,CAAC,CAwEtB;CACF;uBA1GsB,kBAAkB"}