velocious 1.0.698 → 1.0.700

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 (95) hide show
  1. package/README.md +2 -2
  2. package/build/configuration-types.js +23 -0
  3. package/build/configuration.js +136 -1
  4. package/build/database/advisory-lock-backend-policy.js +204 -0
  5. package/build/database/advisory-lock-file-backend.js +348 -0
  6. package/build/database/advisory-lock-runner.js +239 -16
  7. package/build/database/drivers/base.js +318 -15
  8. package/build/database/drivers/mssql/index.js +1 -1
  9. package/build/database/drivers/mysql/index.js +4 -2
  10. package/build/database/drivers/pgsql/index.js +4 -2
  11. package/build/database/drivers/sqlite/base.js +13 -129
  12. package/build/database/drivers/sqlite/index.js +41 -62
  13. package/build/database/migrator.js +120 -50
  14. package/build/database/record/attachments/store.js +84 -1
  15. package/build/environment-handlers/base.js +13 -0
  16. package/build/environment-handlers/node.js +11 -0
  17. package/build/http-server/server-lock.js +21 -115
  18. package/build/src/configuration-types.d.ts +68 -0
  19. package/build/src/configuration-types.d.ts.map +1 -1
  20. package/build/src/configuration-types.js +21 -1
  21. package/build/src/configuration.d.ts +100 -1
  22. package/build/src/configuration.d.ts.map +1 -1
  23. package/build/src/configuration.js +124 -2
  24. package/build/src/database/advisory-lock-backend-policy.d.ts +145 -0
  25. package/build/src/database/advisory-lock-backend-policy.d.ts.map +1 -0
  26. package/build/src/database/advisory-lock-backend-policy.js +184 -0
  27. package/build/src/database/advisory-lock-file-backend.d.ts +194 -0
  28. package/build/src/database/advisory-lock-file-backend.d.ts.map +1 -0
  29. package/build/src/database/advisory-lock-file-backend.js +313 -0
  30. package/build/src/database/advisory-lock-runner.d.ts +116 -15
  31. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  32. package/build/src/database/advisory-lock-runner.js +190 -17
  33. package/build/src/database/drivers/base.d.ts +125 -4
  34. package/build/src/database/drivers/base.d.ts.map +1 -1
  35. package/build/src/database/drivers/base.js +282 -17
  36. package/build/src/database/drivers/mssql/index.d.ts +1 -1
  37. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  38. package/build/src/database/drivers/mssql/index.js +2 -2
  39. package/build/src/database/drivers/mysql/index.d.ts +4 -2
  40. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  41. package/build/src/database/drivers/mysql/index.js +5 -3
  42. package/build/src/database/drivers/pgsql/index.d.ts +4 -2
  43. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  44. package/build/src/database/drivers/pgsql/index.js +5 -3
  45. package/build/src/database/drivers/sqlite/base.d.ts +11 -49
  46. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  47. package/build/src/database/drivers/sqlite/base.js +14 -114
  48. package/build/src/database/drivers/sqlite/index.d.ts +28 -19
  49. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  50. package/build/src/database/drivers/sqlite/index.js +41 -59
  51. package/build/src/database/migrator.d.ts.map +1 -1
  52. package/build/src/database/migrator.js +110 -56
  53. package/build/src/database/record/attachments/store.d.ts +0 -3
  54. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  55. package/build/src/database/record/attachments/store.js +71 -2
  56. package/build/src/environment-handlers/base.d.ts +14 -0
  57. package/build/src/environment-handlers/base.d.ts.map +1 -1
  58. package/build/src/environment-handlers/base.js +13 -1
  59. package/build/src/environment-handlers/node.d.ts +11 -0
  60. package/build/src/environment-handlers/node.d.ts.map +1 -1
  61. package/build/src/environment-handlers/node.js +11 -1
  62. package/build/src/http-server/server-lock.d.ts +13 -37
  63. package/build/src/http-server/server-lock.d.ts.map +1 -1
  64. package/build/src/http-server/server-lock.js +22 -109
  65. package/build/src/testing/test-runner.d.ts +74 -0
  66. package/build/src/testing/test-runner.d.ts.map +1 -1
  67. package/build/src/testing/test-runner.js +140 -1
  68. package/build/src/testing/test-tenant-database-pool.d.ts +183 -0
  69. package/build/src/testing/test-tenant-database-pool.d.ts.map +1 -0
  70. package/build/src/testing/test-tenant-database-pool.js +388 -0
  71. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  72. package/build/src/testing/velocious-attempt-executor.js +43 -1
  73. package/build/testing/test-runner.js +152 -0
  74. package/build/testing/test-tenant-database-pool.js +438 -0
  75. package/build/testing/velocious-attempt-executor.js +44 -0
  76. package/package.json +1 -1
  77. package/src/configuration-types.js +23 -0
  78. package/src/configuration.js +136 -1
  79. package/src/database/advisory-lock-backend-policy.js +204 -0
  80. package/src/database/advisory-lock-file-backend.js +348 -0
  81. package/src/database/advisory-lock-runner.js +239 -16
  82. package/src/database/drivers/base.js +318 -15
  83. package/src/database/drivers/mssql/index.js +1 -1
  84. package/src/database/drivers/mysql/index.js +4 -2
  85. package/src/database/drivers/pgsql/index.js +4 -2
  86. package/src/database/drivers/sqlite/base.js +13 -129
  87. package/src/database/drivers/sqlite/index.js +41 -62
  88. package/src/database/migrator.js +120 -50
  89. package/src/database/record/attachments/store.js +84 -1
  90. package/src/environment-handlers/base.js +13 -0
  91. package/src/environment-handlers/node.js +11 -0
  92. package/src/http-server/server-lock.js +21 -115
  93. package/src/testing/test-runner.js +152 -0
  94. package/src/testing/test-tenant-database-pool.js +438 -0
  95. package/src/testing/velocious-attempt-executor.js +44 -0
@@ -1,6 +1,24 @@
1
1
  // @ts-check
2
2
 
3
3
  import timeout, {TimeoutError} from "awaitery/build/timeout.js"
4
+ import {DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS} from "./advisory-lock-bound.js"
5
+ import {
6
+ AdvisoryLockBackendUnavailableError,
7
+ resolveAdvisoryLockBackendPolicy,
8
+ refuseAdvisoryLockTierBelowMinimum
9
+ } from "./advisory-lock-backend-policy.js"
10
+
11
+ /**
12
+ * Provenance of one advisory-lock acquisition (STC-210 design §3): every
13
+ * held lock reports the tier that actually served it, so a fallback is
14
+ * visible to the callback, in logs, and in `getAdvisoryLockBackendStatus()`.
15
+ * @typedef {object} AdvisoryLockBackendProvenance
16
+ * @property {import("./advisory-lock-backend-policy.js").AdvisoryLockTier} backend - Tier that served the acquisition.
17
+ * @property {string} [driverType] - Database driver type when the driver tier served it.
18
+ * @property {boolean} fencingSupported - Whether this tier carries fencing tokens (design §8); the file tier reports false rather than pretending.
19
+ * @property {string} [holderId] - Backend holder id for the file tier.
20
+ * @property {string} [lockName] - Lock name this provenance belongs to.
21
+ */
4
22
 
5
23
  /**
6
24
  * Thrown when an advisory lock could not be acquired before `timeoutMs` elapsed.
@@ -77,19 +95,41 @@ export default class AdvisoryLockRunner {
77
95
  * cleanup can release the lock even if callback database work is stuck.
78
96
  * @template T
79
97
  * @param {string} name - Lock name.
80
- * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
98
+ * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>} callback - Callback to invoke while the lock is held.
81
99
  * @param {{timeoutMs?: number | null, holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Lock and hold timeout options.
82
100
  * @returns {Promise<T>} - Resolves with the callback result.
83
101
  */
84
102
  async withAdvisoryLock(name, callback, args = {}) {
103
+ const fileBackend = await this.fileBackendForName(name)
104
+
105
+ if (fileBackend) {
106
+ const acquired = await fileBackend.acquire(name, {timeoutMs: this.fileAcquireTimeoutMs(args)})
107
+
108
+ if (!acquired) {
109
+ throw new AdvisoryLockTimeoutError(`Timed out waiting for advisory lock ${JSON.stringify(name)} on the file tier`, {name})
110
+ }
111
+
112
+ const lockBackend = this.fileProvenance(fileBackend, name)
113
+
114
+ this.configuration.recordAdvisoryLockAcquisition({backend: lockBackend.backend, fencingSupported: lockBackend.fencingSupported, lockName: name})
115
+
116
+ return await this.runLockedCallback({callback, holdTimeoutMs: args.holdTimeoutMs, lockBackend, name, release: async () => await fileBackend.release(name)})
117
+ }
118
+
85
119
  return await this.withLockConnection(args, async (connection) => {
120
+ const lockBackend = this.driverProvenance(connection)
121
+
122
+ this.assertDriverTierEligible(name, lockBackend)
123
+
86
124
  const acquired = await connection.acquireAdvisoryLock(name, args)
87
125
 
88
126
  if (!acquired) {
89
127
  throw new AdvisoryLockTimeoutError(`Timed out waiting for advisory lock ${JSON.stringify(name)}`, {name})
90
128
  }
91
129
 
92
- return await this.runLockedCallback({callback, connection, holdTimeoutMs: args.holdTimeoutMs, name})
130
+ this.configuration.recordAdvisoryLockAcquisition({backend: lockBackend.backend, fencingSupported: lockBackend.fencingSupported, lockName: name})
131
+
132
+ return await this.runLockedCallback({callback, holdTimeoutMs: args.holdTimeoutMs, lockBackend, name, release: async () => await connection.releaseAdvisoryLock(name)})
93
133
  })
94
134
  }
95
135
 
@@ -102,22 +142,45 @@ export default class AdvisoryLockRunner {
102
142
  * cancellation.
103
143
  * @template T
104
144
  * @param {string} name - Lock name.
105
- * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
145
+ * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>} callback - Callback to invoke while the lock is held.
106
146
  * @param {{holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Hold timeout options.
107
147
  * @returns {Promise<T>} - Resolves with the callback result.
108
148
  */
109
149
  async withAdvisoryLockOrFail(name, callback, args = {}) {
150
+ const fileBackend = await this.fileBackendForName(name)
151
+
152
+ if (fileBackend) {
153
+ const acquired = await fileBackend.tryAcquire(name)
154
+
155
+ if (!acquired) {
156
+ throw new AdvisoryLockBusyError(`Advisory lock ${JSON.stringify(name)} is already held on the file tier`, {name})
157
+ }
158
+
159
+ const lockBackend = this.fileProvenance(fileBackend, name)
160
+
161
+ this.configuration.recordAdvisoryLockAcquisition({backend: lockBackend.backend, fencingSupported: lockBackend.fencingSupported, lockName: name})
162
+
163
+ return await this.runLockedCallback({callback, holdTimeoutMs: args.holdTimeoutMs, lockBackend, name, release: async () => await fileBackend.release(name)})
164
+ }
165
+
110
166
  return await this.withLockConnection(args, async (connection) => {
167
+ const lockBackend = this.driverProvenance(connection)
168
+
169
+ this.assertDriverTierEligible(name, lockBackend)
170
+
111
171
  const acquired = await connection.tryAcquireAdvisoryLock(name)
112
172
 
113
173
  if (!acquired) {
114
174
  throw new AdvisoryLockBusyError(`Advisory lock ${JSON.stringify(name)} is already held`, {name})
115
175
  }
116
176
 
117
- return await this.runLockedCallback({callback, connection, holdTimeoutMs: args.holdTimeoutMs, name})
177
+ this.configuration.recordAdvisoryLockAcquisition({backend: lockBackend.backend, fencingSupported: lockBackend.fencingSupported, lockName: name})
178
+
179
+ return await this.runLockedCallback({callback, holdTimeoutMs: args.holdTimeoutMs, lockBackend, name, release: async () => await connection.releaseAdvisoryLock(name)})
118
180
  })
119
181
  }
120
182
 
183
+ /**
121
184
  /**
122
185
  * Runs a callback if the advisory lock can be acquired without waiting;
123
186
  * otherwise returns a `{acquired: false}` defer result instead of throwing.
@@ -133,33 +196,172 @@ export default class AdvisoryLockRunner {
133
196
  * file lock), so a caller never branches on the backend.
134
197
  * @template T
135
198
  * @param {string} name - Lock name.
136
- * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
199
+ * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>} callback - Callback to invoke while the lock is held.
137
200
  * @param {{holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Hold timeout options.
138
201
  * @returns {Promise<{acquired: true, result: T} | {acquired: false, reason: "contended", lockName: string}>} - Either the held-callback result or the defer outcome.
139
202
  */
140
203
  async withAdvisoryLockOrDefer(name, callback, args = {}) {
204
+ const fileBackend = await this.fileBackendForName(name)
205
+
206
+ if (fileBackend) {
207
+ const acquired = await fileBackend.acquire(name, {timeoutMs: 0})
208
+
209
+ if (!acquired) return {acquired: false, reason: "contended", lockName: name}
210
+
211
+ const lockBackend = this.fileProvenance(fileBackend, name)
212
+
213
+ this.configuration.recordAdvisoryLockAcquisition({backend: lockBackend.backend, fencingSupported: lockBackend.fencingSupported, lockName: name})
214
+
215
+ const result = await this.runLockedCallback({callback, holdTimeoutMs: args.holdTimeoutMs, lockBackend, name, release: async () => await fileBackend.release(name)})
216
+
217
+ return {acquired: true, result}
218
+ }
219
+
141
220
  return await this.withLockConnection(args, async (connection) => {
142
221
  const acquired = await connection.tryAcquireAdvisoryLock(name)
143
222
 
144
223
  if (!acquired) return {acquired: false, reason: "contended", lockName: name}
145
224
 
146
- const result = await this.runLockedCallback({callback, connection, holdTimeoutMs: args.holdTimeoutMs, name})
225
+ const lockBackend = this.driverProvenance(connection)
226
+
227
+ this.assertDriverTierEligible(name, lockBackend)
228
+
229
+ this.configuration.recordAdvisoryLockAcquisition({backend: lockBackend.backend, fencingSupported: lockBackend.fencingSupported, lockName: name})
230
+
231
+ const result = await this.runLockedCallback({callback, holdTimeoutMs: args.holdTimeoutMs, lockBackend, name, release: async () => await connection.releaseAdvisoryLock(name)})
147
232
 
148
233
  return {acquired: true, result}
149
234
  })
150
235
  }
151
236
 
152
237
  /**
153
- * Runs the lock holder callback and releases the lock from its owning connection.
238
+ * Runs the lock holder callback and releases the lock from its owning backend.
154
239
  * @template T
155
- * @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.
240
+ * @param {{callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>, holdTimeoutMs?: number | null, lockBackend: AdvisoryLockBackendProvenance, name: string, release: () => Promise<unknown>}} args - Locked callback args.
156
241
  * @returns {Promise<T>} - Resolves with the callback result.
157
242
  */
158
- async runLockedCallback({callback, connection, holdTimeoutMs, name}) {
243
+ async runLockedCallback({callback, holdTimeoutMs, lockBackend, name, release}) {
159
244
  try {
160
- return await AdvisoryLockRunner.runWithAdvisoryLockHoldTimeout(name, callback, holdTimeoutMs)
245
+ return await AdvisoryLockRunner.runWithAdvisoryLockHoldTimeout(name, callback, holdTimeoutMs, lockBackend)
161
246
  } finally {
162
- await connection.releaseAdvisoryLock(name)
247
+ await release()
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Resolves the backend policy for one lock name and returns the file
253
+ * backend when the name explicitly opts into the file tier
254
+ * (`preferredTier: "file"` plus `advisoryLocks.backends.file.enabled: true`).
255
+ * A name whose declared minimum tier is cross-host is refused loudly
256
+ * (`AdvisoryLockMinTierRefusedError`) before any contact with the file
257
+ * tier. A name that prefers the file tier while the backend is disabled
258
+ * moves to the driver path (a stronger-or-equal tier), never the other way
259
+ * around. Nothing here ever auto-enables the file tier.
260
+ * @param {string} name - Lock name.
261
+ * @returns {Promise<import("./advisory-lock-file-backend.js").default | null>} - Resolves with the file backend serving this name, or null for the driver path.
262
+ */
263
+ async fileBackendForName(name) {
264
+ const policy = resolveAdvisoryLockBackendPolicy(name, this.configuration.getAdvisoryLockPolicy())
265
+
266
+ if (policy.preferredTier !== "file") return null
267
+
268
+ if (!advisoryLockPolicyAllowsFileTier(policy)) {
269
+ refuseAdvisoryLockTierBelowMinimum({
270
+ configuration: this.configuration,
271
+ lockName: name,
272
+ minTier: policy.minTier,
273
+ requestedBackend: "file"
274
+ })
275
+ }
276
+
277
+ const backend = await this.configuration.getAdvisoryLockFileBackend()
278
+
279
+ if (!backend) {
280
+ // Preferred tier unavailable: fail *upward* to the driver path (a
281
+ // stronger-or-equal tier), never downward. If the driver cannot satisfy
282
+ // the minimum either, `assertDriverTierEligible` throws
283
+ // `AdvisoryLockBackendUnavailableError` on the driver path.
284
+ return null
285
+ }
286
+
287
+ return backend
288
+ }
289
+
290
+ /**
291
+ * Fail-closed check on the driver path (design §9): an undeclared lock name
292
+ * keeps today's behavior (any driver serves it — full backward
293
+ * compatibility), but a name with an explicit policy entry is refused with
294
+ * `AdvisoryLockBackendUnavailableError` when the configured driver cannot
295
+ * satisfy its minimum tier (e.g. `minTier: "driver-native"` on a SQLite
296
+ * driver, which has no native advisory-lock primitive).
297
+ * @param {string} name - Lock name.
298
+ * @param {AdvisoryLockBackendProvenance} lockBackend - Tier the driver path would serve with.
299
+ * @returns {void}
300
+ */
301
+ assertDriverTierEligible(name, lockBackend) {
302
+ const policy = resolveAdvisoryLockBackendPolicy(name, this.configuration.getAdvisoryLockPolicy())
303
+
304
+ if (!policy.entry) return
305
+
306
+ const tier = lockBackend.backend
307
+
308
+ if (tier === "process-local" && !policy.allowedTiers.includes("process-local")) {
309
+ throw new AdvisoryLockBackendUnavailableError(
310
+ `Advisory lock ${JSON.stringify(name)} requires minimum tier ${JSON.stringify(policy.minTier)} but the configured driver (${String(lockBackend.driverType)}) only offers the process-local tier`,
311
+ {lockName: name, minTier: policy.minTier, reasons: [`driver tier ${tier} below minTier ${policy.minTier}`]}
312
+ )
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Resolves the wait bound for a file-tier acquisition: an explicit caller
318
+ * `timeoutMs` (including an explicit `null` opt-out) wins, otherwise the
319
+ * same framework default that bounds driver acquisitions applies.
320
+ * @param {{timeoutMs?: number | null}} args - Lock options.
321
+ * @returns {number | null} - Wait bound in milliseconds, or `null` for an indefinite wait.
322
+ */
323
+ fileAcquireTimeoutMs(args) {
324
+ if (args.timeoutMs !== undefined) return args.timeoutMs
325
+
326
+ return DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS
327
+ }
328
+
329
+ /**
330
+ * Builds the acquisition provenance for a file-tier hold. The file tier
331
+ * cannot carry fencing tokens, so the limitation is reported on every
332
+ * acquisition rather than silently pretended away (design §8).
333
+ * @param {import("./advisory-lock-file-backend.js").default} backend - Backend that served the acquisition.
334
+ * @param {string} name - Lock name.
335
+ * @returns {AdvisoryLockBackendProvenance} - Acquisition provenance.
336
+ */
337
+ fileProvenance(backend, name) {
338
+ return {backend: "file", fencingSupported: false, holderId: backend.holderId, lockName: name}
339
+ }
340
+
341
+ /**
342
+ * Builds the acquisition provenance for a driver-served hold. SQLite has no
343
+ * native advisory-lock primitive, so its emulation tier is reported as
344
+ * `process-local` rather than pretending to be database-native.
345
+ * @param {import("./drivers/base.js").default} connection - Connection that served the acquisition.
346
+ * @returns {AdvisoryLockBackendProvenance} - Acquisition provenance.
347
+ */
348
+ driverProvenance(connection) {
349
+ let driverType = null
350
+
351
+ try {
352
+ driverType = connection.getType()
353
+ } catch {
354
+ // Test stub drivers may not implement getType(); the tier alone identifies the backend.
355
+ }
356
+
357
+ const tier = typeof connection.advisoryLockBackendTier === "function"
358
+ ? connection.advisoryLockBackendTier()
359
+ : (driverType === "sqlite" ? "process-local" : "driver-native")
360
+
361
+ return {
362
+ backend: /** @type {import("./advisory-lock-backend-policy.js").AdvisoryLockTier} */ (tier),
363
+ driverType: driverType ?? undefined,
364
+ fencingSupported: tier === "driver-native"
163
365
  }
164
366
  }
165
367
 
@@ -218,16 +420,18 @@ export default class AdvisoryLockRunner {
218
420
  *
219
421
  * The callback receives a `TimeoutControl` from awaitery, enabling cooperative
220
422
  * cancellation via `control.check()`, `control.signal`, `control.timedOut`,
221
- * and `control.remaining()`.
423
+ * and `control.remaining()`, plus `lockBackend` provenance for the tier that
424
+ * served the acquisition.
222
425
  * @template T
223
426
  * @param {string} name - Lock name (for the error message).
224
- * @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback holding the lock.
427
+ * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>} callback - Callback holding the lock.
225
428
  * @param {number | null} [holdTimeoutMs] - Max hold time; falsy disables the timeout.
429
+ * @param {AdvisoryLockBackendProvenance} [lockBackend] - Provenance of the acquisition serving this hold.
226
430
  * @returns {Promise<T>} - Resolves with the callback result.
227
431
  */
228
- static async runWithAdvisoryLockHoldTimeout(name, callback, holdTimeoutMs) {
432
+ static async runWithAdvisoryLockHoldTimeout(name, callback, holdTimeoutMs, lockBackend) {
229
433
  if (!holdTimeoutMs || holdTimeoutMs <= 0) {
230
- return await callback()
434
+ return await callback({lockBackend: lockBackend ?? AdvisoryLockRunner.unknownBackendProvenance()})
231
435
  }
232
436
 
233
437
  let callbackSettled = false
@@ -235,7 +439,7 @@ export default class AdvisoryLockRunner {
235
439
  try {
236
440
  return await timeout({timeout: holdTimeoutMs}, async ({control}) => {
237
441
  try {
238
- return await callback({control})
442
+ return await callback({control, lockBackend: lockBackend ?? AdvisoryLockRunner.unknownBackendProvenance()})
239
443
  } finally {
240
444
  callbackSettled = true
241
445
  }
@@ -248,6 +452,25 @@ export default class AdvisoryLockRunner {
248
452
  throw error
249
453
  }
250
454
  }
455
+
456
+ /**
457
+ * Neutral provenance for direct `runWithAdvisoryLockHoldTimeout` callers that
458
+ * hold a lock acquired outside this runner.
459
+ * @returns {AdvisoryLockBackendProvenance} - Provenance with an unknown tier.
460
+ */
461
+ static unknownBackendProvenance() {
462
+ return {backend: "process-local", fencingSupported: false}
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Runs advisory lock policy allows file tier.
468
+ * @param {import("./advisory-lock-backend-policy.js").ResolvedAdvisoryLockBackendPolicy} policy - Resolved policy.
469
+ * @returns {boolean} - Whether the file tier may serve this name.
470
+ */
471
+ function advisoryLockPolicyAllowsFileTier(policy) {
472
+ return policy.allowedTiers.includes("file")
251
473
  }
252
474
 
253
475
  export {AdvisoryLockBusyError, AdvisoryLockHoldTimeoutError, AdvisoryLockTimeoutError}
476
+ export {AdvisoryLockBackendUnavailableError, AdvisoryLockMinTierRefusedError} from "./advisory-lock-backend-policy.js"