velocious 1.0.644 → 1.0.645
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.
- package/README.md +11 -9
- package/build/authorization/ability.js +36 -13
- package/build/background-jobs/adapter.js +9 -0
- package/build/background-jobs/main.js +35 -10
- package/build/background-jobs/store.js +217 -38
- package/build/background-jobs/types.js +15 -0
- package/build/configuration-types.js +1 -1
- package/build/configuration.js +45 -9
- package/build/database/drivers/base-column.js +8 -0
- package/build/database/drivers/mssql/sql/update.js +1 -3
- package/build/database/drivers/mysql/sql/update.js +1 -3
- package/build/database/drivers/pgsql/column.js +22 -0
- package/build/database/drivers/pgsql/sql/update.js +1 -3
- package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
- package/build/database/drivers/sqlite/sql/update.js +1 -3
- package/build/database/query/index.js +2 -2
- package/build/database/query/model-class-query.js +26 -10
- package/build/database/query/preloader/has-many.js +4 -1
- package/build/database/query/query-data.js +2 -1
- package/build/database/query/update-base.js +14 -0
- package/build/database/query/with-count.js +3 -1
- package/build/database/record/acts-as-list.js +37 -13
- package/build/database/record/attachments/store.js +336 -26
- package/build/database/record/auditing.js +6 -3
- package/build/database/record/index.js +197 -42
- package/build/database/record/instance-relationships/belongs-to.js +2 -1
- package/build/database/record/validators/uniqueness.js +8 -5
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
- package/build/frontend-model-controller.js +136 -53
- package/build/frontend-model-resource/base-resource.js +87 -37
- package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
- package/build/frontend-models/base.js +307 -59
- package/build/frontend-models/preloader.js +6 -3
- package/build/frontend-models/query.js +15 -6
- package/build/frontend-models/resource-definition.js +21 -2
- package/build/frontend-models/use-destroyed-event.js +1 -1
- package/build/frontend-models/use-model-class-event.js +2 -2
- package/build/frontend-models/use-updated-event.js +1 -1
- package/build/frontend-models/websocket-channel.js +256 -93
- package/build/frontend-models/websocket-publishers.js +218 -18
- package/build/http-server/client/websocket-session.js +13 -1
- package/build/http-server/websocket-channel.js +15 -2
- package/build/src/authorization/ability.d.ts +9 -3
- package/build/src/authorization/ability.d.ts.map +1 -1
- package/build/src/authorization/ability.js +33 -14
- package/build/src/background-jobs/adapter.d.ts +6 -0
- package/build/src/background-jobs/adapter.d.ts.map +1 -1
- package/build/src/background-jobs/adapter.js +9 -1
- package/build/src/background-jobs/main.d.ts +6 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +34 -11
- package/build/src/background-jobs/store.d.ts +66 -12
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +191 -35
- package/build/src/background-jobs/types.d.ts +55 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +16 -1
- package/build/src/configuration-types.d.ts +3 -3
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +2 -2
- package/build/src/configuration.d.ts +18 -8
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +38 -7
- package/build/src/database/drivers/base-column.d.ts +5 -0
- package/build/src/database/drivers/base-column.d.ts.map +1 -1
- package/build/src/database/drivers/base-column.js +8 -1
- package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/sql/update.js +2 -4
- package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/sql/update.js +2 -4
- package/build/src/database/drivers/pgsql/column.d.ts +5 -0
- package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/column.js +19 -1
- package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/sql/update.js +2 -4
- package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
- package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/sql/update.js +2 -4
- package/build/src/database/query/index.d.ts +4 -4
- package/build/src/database/query/index.d.ts.map +1 -1
- package/build/src/database/query/index.js +3 -3
- package/build/src/database/query/model-class-query.d.ts +8 -2
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +25 -11
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +4 -2
- package/build/src/database/query/query-data.d.ts.map +1 -1
- package/build/src/database/query/query-data.js +3 -2
- package/build/src/database/query/update-base.d.ts +7 -0
- package/build/src/database/query/update-base.d.ts.map +1 -1
- package/build/src/database/query/update-base.js +13 -1
- package/build/src/database/query/with-count.d.ts.map +1 -1
- package/build/src/database/query/with-count.js +3 -2
- package/build/src/database/record/acts-as-list.d.ts.map +1 -1
- package/build/src/database/record/acts-as-list.js +35 -14
- package/build/src/database/record/attachments/store.d.ts +67 -0
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +301 -25
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +7 -4
- package/build/src/database/record/index.d.ts +78 -26
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +171 -42
- package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
- package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
- package/build/src/database/record/validators/uniqueness.js +8 -6
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -12
- package/build/src/frontend-model-controller.d.ts +29 -8
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +122 -51
- package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +83 -37
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
- package/build/src/frontend-models/base.d.ts +53 -20
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +275 -59
- package/build/src/frontend-models/preloader.d.ts.map +1 -1
- package/build/src/frontend-models/preloader.js +6 -4
- package/build/src/frontend-models/query.d.ts +2 -2
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +14 -7
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +20 -3
- package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
- package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-destroyed-event.js +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-model-class-event.js +3 -3
- package/build/src/frontend-models/use-updated-event.d.ts +1 -1
- package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-updated-event.js +2 -2
- package/build/src/frontend-models/websocket-channel.d.ts +60 -41
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +225 -85
- package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
- package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-publishers.js +190 -19
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +11 -2
- package/build/src/http-server/websocket-channel.d.ts +20 -8
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +15 -3
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +4 -3
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +3 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +6 -5
- package/build/src/sync/sync-publisher.d.ts.map +1 -1
- package/build/src/sync/sync-publisher.js +4 -3
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
- package/build/src/testing/test-profiler.d.ts +4 -4
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +3 -3
- package/build/src/testing/test-runner.d.ts +271 -117
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +496 -427
- package/build/src/testing/test.d.ts +11 -72
- package/build/src/testing/test.d.ts.map +1 -1
- package/build/src/testing/test.js +131 -284
- package/build/src/testing/velocious-attempt-executor.d.ts +14 -29
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-attempt-executor.js +52 -46
- package/build/src/testing/velocious-runner-reporter.d.ts +29 -34
- package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -1
- package/build/src/testing/velocious-runner-reporter.js +145 -57
- package/build/src/testing/velocious-suite-hook-executor.d.ts +5 -23
- package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-suite-hook-executor.js +22 -47
- package/build/src/testing/velocious-test-arguments.d.ts +11 -6
- package/build/src/testing/velocious-test-arguments.d.ts.map +1 -1
- package/build/src/testing/velocious-test-arguments.js +18 -10
- package/build/src/utils/model-primary-key.d.ts +73 -0
- package/build/src/utils/model-primary-key.d.ts.map +1 -0
- package/build/src/utils/model-primary-key.js +144 -0
- package/build/sync/sync-api-client.js +3 -2
- package/build/sync/sync-client.js +2 -1
- package/build/sync/sync-envelope-replay-service.js +5 -4
- package/build/sync/sync-publisher.js +3 -2
- package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/build/testing/test-profiler.js +2 -2
- package/build/testing/test-runner.js +493 -449
- package/build/testing/test.js +164 -306
- package/build/testing/velocious-attempt-executor.js +55 -45
- package/build/testing/velocious-runner-reporter.js +156 -58
- package/build/testing/velocious-suite-hook-executor.js +22 -50
- package/build/testing/velocious-test-arguments.js +19 -10
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/model-primary-key.js +171 -0
- package/package.json +5 -4
- package/src/authorization/ability.js +36 -13
- package/src/background-jobs/adapter.js +9 -0
- package/src/background-jobs/main.js +35 -10
- package/src/background-jobs/store.js +217 -38
- package/src/background-jobs/types.js +15 -0
- package/src/configuration-types.js +1 -1
- package/src/configuration.js +45 -9
- package/src/database/drivers/base-column.js +8 -0
- package/src/database/drivers/mssql/sql/update.js +1 -3
- package/src/database/drivers/mysql/sql/update.js +1 -3
- package/src/database/drivers/pgsql/column.js +22 -0
- package/src/database/drivers/pgsql/sql/update.js +1 -3
- package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
- package/src/database/drivers/sqlite/sql/update.js +1 -3
- package/src/database/query/index.js +2 -2
- package/src/database/query/model-class-query.js +26 -10
- package/src/database/query/preloader/has-many.js +4 -1
- package/src/database/query/query-data.js +2 -1
- package/src/database/query/update-base.js +14 -0
- package/src/database/query/with-count.js +3 -1
- package/src/database/record/acts-as-list.js +37 -13
- package/src/database/record/attachments/store.js +336 -26
- package/src/database/record/auditing.js +6 -3
- package/src/database/record/index.js +197 -42
- package/src/database/record/instance-relationships/belongs-to.js +2 -1
- package/src/database/record/validators/uniqueness.js +8 -5
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
- package/src/frontend-model-controller.js +136 -53
- package/src/frontend-model-resource/base-resource.js +87 -37
- package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
- package/src/frontend-models/base.js +307 -59
- package/src/frontend-models/preloader.js +6 -3
- package/src/frontend-models/query.js +15 -6
- package/src/frontend-models/resource-definition.js +21 -2
- package/src/frontend-models/use-destroyed-event.js +1 -1
- package/src/frontend-models/use-model-class-event.js +2 -2
- package/src/frontend-models/use-updated-event.js +1 -1
- package/src/frontend-models/websocket-channel.js +256 -93
- package/src/frontend-models/websocket-publishers.js +218 -18
- package/src/http-server/client/websocket-session.js +13 -1
- package/src/http-server/websocket-channel.js +15 -2
- package/src/sync/sync-api-client.js +3 -2
- package/src/sync/sync-client.js +2 -1
- package/src/sync/sync-envelope-replay-service.js +5 -4
- package/src/sync/sync-publisher.js +3 -2
- package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/src/testing/test-profiler.js +2 -2
- package/src/testing/test-runner.js +493 -449
- package/src/testing/test.js +164 -306
- package/src/testing/velocious-attempt-executor.js +55 -45
- package/src/testing/velocious-runner-reporter.js +156 -58
- package/src/testing/velocious-suite-hook-executor.js +22 -50
- package/src/testing/velocious-test-arguments.js +19 -10
- package/src/utils/model-primary-key.js +171 -0
- package/build/src/testing/testing-package-adapter.d.ts +0 -9
- package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
- package/build/src/testing/testing-package-adapter.js +0 -69
- package/build/testing/testing-package-adapter.js +0 -76
- package/src/testing/testing-package-adapter.js +0 -76
|
@@ -189,9 +189,10 @@ export default class VelociousAuthorizationAbility {
|
|
|
189
189
|
* @param {string} args.action - Requested action.
|
|
190
190
|
* @param {typeof import("../database/record/index.js").default} args.modelClass - Model class.
|
|
191
191
|
* @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} args.query - Query.
|
|
192
|
+
* @param {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [args.ruleQueryFactory] - Optional factory for the queries that evaluate individual conditional rules.
|
|
192
193
|
* @returns {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} - Authorized query.
|
|
193
194
|
*/
|
|
194
|
-
applyToQuery({action, modelClass, query}) {
|
|
195
|
+
applyToQuery({action, modelClass, query, ruleQueryFactory}) {
|
|
195
196
|
this.loadAbilitiesForModelClass(modelClass)
|
|
196
197
|
|
|
197
198
|
const applicableRules = this.rulesFor({action, modelClass})
|
|
@@ -203,18 +204,18 @@ export default class VelociousAuthorizationAbility {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
if (allowRules.some((rule) => !rule.conditions)) {
|
|
206
|
-
this.applyDenyRules({action, denyRules, modelClass, query})
|
|
207
|
+
this.applyDenyRules({action, denyRules, modelClass, query, ruleQueryFactory})
|
|
207
208
|
return query
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
const allowSqlParts = this.conditionSqlParts({action, modelClass, query, rules: allowRules})
|
|
211
|
+
const allowSqlParts = this.conditionSqlParts({action, modelClass, query, ruleQueryFactory, rules: allowRules})
|
|
211
212
|
|
|
212
213
|
if (allowSqlParts.length === 0) {
|
|
213
214
|
return query.where("1=0")
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
query.where(`(${allowSqlParts.join(" OR ")})`)
|
|
217
|
-
this.applyDenyRules({action, denyRules, modelClass, query})
|
|
218
|
+
this.applyDenyRules({action, denyRules, modelClass, query, ruleQueryFactory})
|
|
218
219
|
|
|
219
220
|
return query
|
|
220
221
|
}
|
|
@@ -240,19 +241,20 @@ export default class VelociousAuthorizationAbility {
|
|
|
240
241
|
* @param {string} args.action - Action.
|
|
241
242
|
* @param {typeof import("../database/record/index.js").default} args.modelClass - Model class.
|
|
242
243
|
* @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} args.query - Base query.
|
|
244
|
+
* @param {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [args.ruleQueryFactory] - Optional conditional-rule query factory.
|
|
243
245
|
* @param {AbilityRuleType[]} args.rules - Rules.
|
|
244
246
|
* @returns {string[]} - SQL condition parts.
|
|
245
247
|
*/
|
|
246
|
-
conditionSqlParts({action, modelClass, query, rules}) {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
248
|
+
conditionSqlParts({action, modelClass, query, ruleQueryFactory, rules}) {
|
|
249
|
+
const primaryKey = modelClass.primaryKey()
|
|
250
|
+
const primaryKeyAttributes = Array.isArray(primaryKey) ? primaryKey : [primaryKey]
|
|
251
|
+
const quotedBaseTable = query.driver.quoteTable(query.getTableReferenceForJoin())
|
|
250
252
|
const sqlParts = []
|
|
251
253
|
|
|
252
254
|
for (const rule of rules) {
|
|
253
255
|
if (!rule.conditions) continue
|
|
254
256
|
|
|
255
|
-
const scopedQuery = modelClass._newQuery()
|
|
257
|
+
const scopedQuery = ruleQueryFactory ? ruleQueryFactory() : modelClass._newQuery()
|
|
256
258
|
const resultQuery = this.applyRuleCondition({
|
|
257
259
|
action,
|
|
258
260
|
conditions: rule.conditions,
|
|
@@ -260,7 +262,16 @@ export default class VelociousAuthorizationAbility {
|
|
|
260
262
|
query: scopedQuery
|
|
261
263
|
})
|
|
262
264
|
const finalQuery = resultQuery || scopedQuery
|
|
263
|
-
|
|
265
|
+
|
|
266
|
+
if (ruleQueryFactory && finalQuery !== scopedQuery) {
|
|
267
|
+
const finalFroms = finalQuery.getFroms()
|
|
268
|
+
|
|
269
|
+
finalFroms.splice(0, finalFroms.length, ...scopedQuery.getFroms())
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const quotedScopedTable = query.driver.quoteTable(finalQuery.getTableReferenceForJoin())
|
|
273
|
+
const primaryKeyColumns = primaryKeyAttributes.map((attributeName) => modelClass.getColumnNameForAttributeName(attributeName))
|
|
274
|
+
const selectedPkSql = primaryKeyColumns.map((columnName) => `${quotedScopedTable}.${query.driver.quoteColumn(columnName)}`)
|
|
264
275
|
|
|
265
276
|
if (finalQuery._distinct) {
|
|
266
277
|
query.distinct(true)
|
|
@@ -268,7 +279,18 @@ export default class VelociousAuthorizationAbility {
|
|
|
268
279
|
|
|
269
280
|
finalQuery.select(selectedPkSql)
|
|
270
281
|
|
|
271
|
-
|
|
282
|
+
if (Array.isArray(primaryKey)) {
|
|
283
|
+
const authorizedRowsAlias = query.driver.quoteTable("velocious_authorized_rows")
|
|
284
|
+
const identitySql = primaryKeyColumns.map((columnName) => {
|
|
285
|
+
const quotedColumn = query.driver.quoteColumn(columnName)
|
|
286
|
+
|
|
287
|
+
return `${authorizedRowsAlias}.${quotedColumn} = ${quotedBaseTable}.${quotedColumn}`
|
|
288
|
+
}).join(" AND ")
|
|
289
|
+
|
|
290
|
+
sqlParts.push(`EXISTS (SELECT 1 FROM (${finalQuery.toSql()}) AS ${authorizedRowsAlias} WHERE ${identitySql})`)
|
|
291
|
+
} else {
|
|
292
|
+
sqlParts.push(`${quotedBaseTable}.${query.driver.quoteColumn(primaryKeyColumns[0])} IN (${finalQuery.toSql()})`)
|
|
293
|
+
}
|
|
272
294
|
}
|
|
273
295
|
|
|
274
296
|
return sqlParts
|
|
@@ -281,9 +303,10 @@ export default class VelociousAuthorizationAbility {
|
|
|
281
303
|
* @param {AbilityRuleType[]} args.denyRules - Deny rules.
|
|
282
304
|
* @param {typeof import("../database/record/index.js").default} args.modelClass - Model class.
|
|
283
305
|
* @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} args.query - Query.
|
|
306
|
+
* @param {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [args.ruleQueryFactory] - Optional conditional-rule query factory.
|
|
284
307
|
* @returns {void} - No return value.
|
|
285
308
|
*/
|
|
286
|
-
applyDenyRules({action, denyRules, modelClass, query}) {
|
|
309
|
+
applyDenyRules({action, denyRules, modelClass, query, ruleQueryFactory}) {
|
|
287
310
|
if (denyRules.length === 0) return
|
|
288
311
|
|
|
289
312
|
if (denyRules.some((rule) => !rule.conditions)) {
|
|
@@ -291,7 +314,7 @@ export default class VelociousAuthorizationAbility {
|
|
|
291
314
|
return
|
|
292
315
|
}
|
|
293
316
|
|
|
294
|
-
const denySqlParts = this.conditionSqlParts({action, modelClass, query, rules: denyRules})
|
|
317
|
+
const denySqlParts = this.conditionSqlParts({action, modelClass, query, ruleQueryFactory, rules: denyRules})
|
|
295
318
|
|
|
296
319
|
if (denySqlParts.length > 0) {
|
|
297
320
|
query.where(`NOT (${denySqlParts.join(" OR ")})`)
|
|
@@ -48,6 +48,15 @@ export default class BackgroundJobsAdapter {
|
|
|
48
48
|
*/
|
|
49
49
|
async reconcileQueueConcurrency() { throw new Error("BackgroundJobsAdapter#reconcileQueueConcurrency is not implemented") }
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Repairs drift in adapter-owned durable active concurrency counts. Adapters
|
|
53
|
+
* without duplicate active-count persistence can keep this no-op result.
|
|
54
|
+
* @returns {Promise<import("./types.js").BackgroundJobConcurrencyReconciliation>} - Repair summary.
|
|
55
|
+
*/
|
|
56
|
+
async reconcileActiveConcurrency() {
|
|
57
|
+
return {candidateCount: 0, checkedCount: 0, repairedCount: 0, repairs: [], repairsTruncatedCount: 0}
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
/**
|
|
52
61
|
* Enqueues a job.
|
|
53
62
|
* @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} _args - Job request.
|
|
@@ -2063,23 +2063,25 @@ export default class BackgroundJobsMain {
|
|
|
2063
2063
|
handoffs.set(job.id, handoff.handoffId)
|
|
2064
2064
|
|
|
2065
2065
|
try {
|
|
2066
|
+
const dispatchedJob = handoff.job || job
|
|
2067
|
+
|
|
2066
2068
|
worker.send({
|
|
2067
2069
|
type: "job",
|
|
2068
2070
|
payload: {
|
|
2069
|
-
id:
|
|
2070
|
-
jobName:
|
|
2071
|
-
args:
|
|
2071
|
+
id: dispatchedJob.id,
|
|
2072
|
+
jobName: dispatchedJob.jobName,
|
|
2073
|
+
args: dispatchedJob.args,
|
|
2072
2074
|
handoffId: handoff.handoffId,
|
|
2073
2075
|
workerId: worker.workerId,
|
|
2074
2076
|
handedOffAtMs: handoff.handedOffAtMs,
|
|
2075
2077
|
options: {
|
|
2076
|
-
concurrencyKey:
|
|
2077
|
-
executionMode:
|
|
2078
|
-
maxConcurrency:
|
|
2079
|
-
maxRetries:
|
|
2080
|
-
queue:
|
|
2081
|
-
scheduledAtMs:
|
|
2082
|
-
...(
|
|
2078
|
+
concurrencyKey: dispatchedJob.concurrencyKey || undefined,
|
|
2079
|
+
executionMode: dispatchedJob.executionMode,
|
|
2080
|
+
maxConcurrency: dispatchedJob.maxConcurrency ?? undefined,
|
|
2081
|
+
maxRetries: dispatchedJob.maxRetries ?? undefined,
|
|
2082
|
+
queue: dispatchedJob.queue,
|
|
2083
|
+
scheduledAtMs: dispatchedJob.scheduledAtMs ?? undefined,
|
|
2084
|
+
...(dispatchedJob.timeoutMs === null ? {} : {timeoutMs: dispatchedJob.timeoutMs})
|
|
2083
2085
|
}
|
|
2084
2086
|
}
|
|
2085
2087
|
})
|
|
@@ -2354,6 +2356,29 @@ export default class BackgroundJobsMain {
|
|
|
2354
2356
|
errorEvents.emit("framework-error", payload)
|
|
2355
2357
|
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
2356
2358
|
}
|
|
2359
|
+
|
|
2360
|
+
if (this.lifecycleState === "active") await this._reconcileActiveConcurrency()
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/**
|
|
2364
|
+
* Repairs durable admission counters on the active main's maintenance cadence
|
|
2365
|
+
* and immediately retries dispatch when capacity was recovered.
|
|
2366
|
+
* @returns {Promise<void>} - Resolves after repair and any resulting drain.
|
|
2367
|
+
*/
|
|
2368
|
+
async _reconcileActiveConcurrency() {
|
|
2369
|
+
try {
|
|
2370
|
+
const result = await this.store.reconcileActiveConcurrency()
|
|
2371
|
+
|
|
2372
|
+
if (result.repairedCount > 0) await this._drain()
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
2375
|
+
const payload = {context: {generationId: this.generationId, stage: "background-job-concurrency-reconciliation"}, error: normalizedError}
|
|
2376
|
+
const errorEvents = this.configuration.getErrorEvents()
|
|
2377
|
+
|
|
2378
|
+
this.logger.error(() => ["Failed to reconcile background job active-concurrency counts:", normalizedError])
|
|
2379
|
+
errorEvents.emit("framework-error", payload)
|
|
2380
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
2381
|
+
}
|
|
2357
2382
|
}
|
|
2358
2383
|
|
|
2359
2384
|
/**
|
|
@@ -56,6 +56,20 @@ import {
|
|
|
56
56
|
* @property {{failureMessage: string, name: string}} [advisoryLock] - Session lock held around the transaction.
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* BackgroundJobConcurrencyCountRow type.
|
|
61
|
+
* @typedef {object} BackgroundJobConcurrencyCountRow
|
|
62
|
+
* @property {number | string} active_count - Persisted or aggregated active count.
|
|
63
|
+
* @property {string} concurrency_key - Durable cap identity.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* BackgroundJobQueuedConcurrency type.
|
|
68
|
+
* @typedef {object} BackgroundJobQueuedConcurrency
|
|
69
|
+
* @property {string | null} concurrencyKey - Current concurrency key for queued work.
|
|
70
|
+
* @property {number | null} maxConcurrency - Current concurrency cap for queued work.
|
|
71
|
+
*/
|
|
72
|
+
|
|
59
73
|
const MIGRATIONS_TABLE = "velocious_internal_migrations"
|
|
60
74
|
const MIGRATION_SCOPE = "background_jobs"
|
|
61
75
|
const MIGRATION_VERSION = "20250215000000"
|
|
@@ -89,6 +103,8 @@ const SCHEDULE_KEYS_TABLE = "background_job_schedule_keys"
|
|
|
89
103
|
const CONCURRENCY_TABLE = "background_job_concurrency"
|
|
90
104
|
const COUNTS_REVISION_TABLE = "background_job_count_revisions"
|
|
91
105
|
const COUNTS_REVISION_KEY = "counts"
|
|
106
|
+
const CONCURRENCY_RECONCILIATION_LOCK = "background-jobs:queue-concurrency-reconcile"
|
|
107
|
+
const CONCURRENCY_REPAIR_SAMPLE_LIMIT = 10
|
|
92
108
|
export const BACKGROUND_JOB_COUNTS_CHANNEL = "velocious-background-job-counts"
|
|
93
109
|
export const BACKGROUND_JOB_COUNT_BUCKETS = ["all", "queued", "handed_off", "completed", "failed", "orphaned"]
|
|
94
110
|
const COUNTED_JOB_STATUSES = BACKGROUND_JOB_COUNT_BUCKETS.slice(1)
|
|
@@ -222,8 +238,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
222
238
|
await this.ensureReady()
|
|
223
239
|
|
|
224
240
|
await this._withDb(async (db) => {
|
|
225
|
-
const
|
|
226
|
-
const acquired = await db.acquireAdvisoryLock(lockName)
|
|
241
|
+
const acquired = await db.acquireAdvisoryLock(CONCURRENCY_RECONCILIATION_LOCK)
|
|
227
242
|
|
|
228
243
|
if (!acquired) throw new Error("Failed to acquire background job queue-concurrency reconcile lock")
|
|
229
244
|
|
|
@@ -236,7 +251,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
236
251
|
// the counts (adoption itself is idempotent).
|
|
237
252
|
this._queueConcurrencyReconciled = true
|
|
238
253
|
} finally {
|
|
239
|
-
await db.releaseAdvisoryLock(
|
|
254
|
+
await db.releaseAdvisoryLock(CONCURRENCY_RECONCILIATION_LOCK)
|
|
240
255
|
}
|
|
241
256
|
})
|
|
242
257
|
|
|
@@ -246,6 +261,44 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
246
261
|
])
|
|
247
262
|
}
|
|
248
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Repairs durable active-count drift while a main process remains live. The
|
|
266
|
+
* initial snapshot is read-only; only suspected mismatches take their
|
|
267
|
+
* counter lock and re-count inside the serialized transaction path.
|
|
268
|
+
* @returns {Promise<import("./types.js").BackgroundJobConcurrencyReconciliation>} - Repair summary.
|
|
269
|
+
*/
|
|
270
|
+
async reconcileActiveConcurrency() {
|
|
271
|
+
const databaseIdentifier = this.getDatabaseIdentifier()
|
|
272
|
+
const startedAtMs = Date.now()
|
|
273
|
+
|
|
274
|
+
await this.ensureReady()
|
|
275
|
+
|
|
276
|
+
const result = await this._serializedConnectionMutation(
|
|
277
|
+
async (db) => await this._reconcileConcurrency(db),
|
|
278
|
+
{
|
|
279
|
+
advisoryLock: {
|
|
280
|
+
failureMessage: "Failed to acquire background job active-concurrency reconcile lock",
|
|
281
|
+
name: CONCURRENCY_RECONCILIATION_LOCK
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (result.repairedCount > 0) {
|
|
287
|
+
await this.logger.warn(() => [
|
|
288
|
+
"Repaired background jobs active-concurrency count drift",
|
|
289
|
+
{
|
|
290
|
+
databaseIdentifier,
|
|
291
|
+
durationMs: Date.now() - startedAtMs,
|
|
292
|
+
repairedCount: result.repairedCount,
|
|
293
|
+
repairs: result.repairs,
|
|
294
|
+
repairsTruncatedCount: result.repairsTruncatedCount
|
|
295
|
+
}
|
|
296
|
+
])
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
301
|
+
|
|
249
302
|
/**
|
|
250
303
|
* Runs enqueue.
|
|
251
304
|
* @param {object} args - Options.
|
|
@@ -949,8 +1002,11 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
949
1002
|
const handedOffAtMs = Date.now()
|
|
950
1003
|
|
|
951
1004
|
return await this._serializedCountMutation(async (db) => {
|
|
952
|
-
const
|
|
953
|
-
if (!
|
|
1005
|
+
const selectedJob = await this._getJobRowById(db, jobId)
|
|
1006
|
+
if (!selectedJob || selectedJob.status !== "queued") return null
|
|
1007
|
+
const queuedJob = await this._reconcileQueuedJobConcurrency(db, selectedJob)
|
|
1008
|
+
|
|
1009
|
+
if (!queuedJob) return null
|
|
954
1010
|
if (queuedJob.concurrencyKey && !(await this._reserveConcurrency(db, queuedJob.concurrencyKey))) return null
|
|
955
1011
|
const affectedRows = await this._updateAffectedRows(db, {
|
|
956
1012
|
tableName: JOBS_TABLE,
|
|
@@ -960,7 +1016,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
960
1016
|
handoff_id: handoffId,
|
|
961
1017
|
worker_id: workerId || null
|
|
962
1018
|
},
|
|
963
|
-
conditions: {id: jobId, status: "queued"}
|
|
1019
|
+
conditions: {concurrency_key: queuedJob.concurrencyKey, id: jobId, status: "queued"}
|
|
964
1020
|
})
|
|
965
1021
|
|
|
966
1022
|
if (affectedRows !== 1) {
|
|
@@ -969,7 +1025,16 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
969
1025
|
}
|
|
970
1026
|
|
|
971
1027
|
await this._recordStatusTransition(db, "queued", "handed_off")
|
|
972
|
-
|
|
1028
|
+
/** @type {import("./types.js").BackgroundJobRow} */
|
|
1029
|
+
const handedOffJob = {
|
|
1030
|
+
...queuedJob,
|
|
1031
|
+
handedOffAtMs,
|
|
1032
|
+
handoffId,
|
|
1033
|
+
status: "handed_off",
|
|
1034
|
+
workerId: workerId || null
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return {handedOffAtMs, handoffId, job: handedOffJob}
|
|
973
1038
|
})
|
|
974
1039
|
}
|
|
975
1040
|
|
|
@@ -2355,6 +2420,41 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2355
2420
|
})
|
|
2356
2421
|
}
|
|
2357
2422
|
|
|
2423
|
+
/**
|
|
2424
|
+
* Applies the active generation's queue policy immediately before handoff.
|
|
2425
|
+
* Explicit concurrency remains owned by the enqueue request.
|
|
2426
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
2427
|
+
* @param {import("./types.js").BackgroundJobRow} job - Queued job snapshot.
|
|
2428
|
+
* @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Reconciled job, or null when its queued-state fence lost.
|
|
2429
|
+
*/
|
|
2430
|
+
async _reconcileQueuedJobConcurrency(db, job) {
|
|
2431
|
+
if (job.concurrencyKey && !job.concurrencyKey.startsWith(QUEUE_CONCURRENCY_KEY_PREFIX)) {
|
|
2432
|
+
return job
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
const concurrency = this._resolveConcurrency({}, job.queue)
|
|
2436
|
+
/** @type {BackgroundJobQueuedConcurrency} */
|
|
2437
|
+
const current = concurrency
|
|
2438
|
+
? {concurrencyKey: concurrency.concurrencyKey, maxConcurrency: concurrency.maxConcurrency}
|
|
2439
|
+
: {concurrencyKey: null, maxConcurrency: null}
|
|
2440
|
+
|
|
2441
|
+
if (concurrency) await this._ensureQueueConcurrencyKey(db, concurrency)
|
|
2442
|
+
if (job.concurrencyKey === current.concurrencyKey && job.maxConcurrency === current.maxConcurrency) return job
|
|
2443
|
+
|
|
2444
|
+
const affectedRows = await this._updateAffectedRows(db, {
|
|
2445
|
+
tableName: JOBS_TABLE,
|
|
2446
|
+
data: {
|
|
2447
|
+
concurrency_key: current.concurrencyKey,
|
|
2448
|
+
max_concurrency: current.maxConcurrency
|
|
2449
|
+
},
|
|
2450
|
+
conditions: {concurrency_key: job.concurrencyKey, id: job.id, status: "queued"}
|
|
2451
|
+
})
|
|
2452
|
+
|
|
2453
|
+
if (affectedRows !== 1) return null
|
|
2454
|
+
|
|
2455
|
+
return {...job, concurrencyKey: current.concurrencyKey, maxConcurrency: current.maxConcurrency}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2358
2458
|
/**
|
|
2359
2459
|
* Reads the configured max concurrency for a queue from the background-jobs config.
|
|
2360
2460
|
* @param {string} queue - Queue name.
|
|
@@ -2763,14 +2863,19 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2763
2863
|
/**
|
|
2764
2864
|
* Rebuilds durable counts from active handoffs.
|
|
2765
2865
|
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
2766
|
-
* @
|
|
2866
|
+
* @param {{insideTransaction?: boolean}} [options] - Reuse an enclosing transaction.
|
|
2867
|
+
* @returns {Promise<import("./types.js").BackgroundJobConcurrencyReconciliation>} - Repair summary.
|
|
2767
2868
|
*/
|
|
2768
|
-
async _reconcileConcurrency(db) {
|
|
2769
|
-
if (!(await db.tableExists(CONCURRENCY_TABLE)))
|
|
2869
|
+
async _reconcileConcurrency(db, {insideTransaction = false} = {}) {
|
|
2870
|
+
if (!(await db.tableExists(CONCURRENCY_TABLE))) {
|
|
2871
|
+
return {candidateCount: 0, checkedCount: 0, repairedCount: 0, repairs: [], repairsTruncatedCount: 0}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2770
2874
|
const activeRows = await db
|
|
2771
2875
|
.newQuery()
|
|
2772
2876
|
.from(JOBS_TABLE)
|
|
2773
2877
|
.select("concurrency_key")
|
|
2878
|
+
.select("COUNT(*) AS active_count")
|
|
2774
2879
|
.where({status: "handed_off"})
|
|
2775
2880
|
.where(`${db.quoteColumn("concurrency_key")} IS NOT NULL`)
|
|
2776
2881
|
.group("concurrency_key")
|
|
@@ -2779,18 +2884,49 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2779
2884
|
.newQuery()
|
|
2780
2885
|
.from(CONCURRENCY_TABLE)
|
|
2781
2886
|
.select("concurrency_key")
|
|
2887
|
+
.select("active_count")
|
|
2782
2888
|
.where(`${db.quoteColumn("active_count")} != 0`)
|
|
2783
2889
|
.results()
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
)
|
|
2890
|
+
/** @type {Map<string, number>} */
|
|
2891
|
+
const activeCounts = new Map()
|
|
2892
|
+
/** @type {Map<string, number>} */
|
|
2893
|
+
const persistedCounts = new Map()
|
|
2789
2894
|
|
|
2790
|
-
for (const
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2895
|
+
for (const rawRow of activeRows) {
|
|
2896
|
+
const row = /** @type {BackgroundJobConcurrencyCountRow} */ (rawRow)
|
|
2897
|
+
activeCounts.set(row.concurrency_key, this._validatedConcurrencyCount(row.active_count, row.concurrency_key))
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
for (const rawRow of staleRows) {
|
|
2901
|
+
const row = /** @type {BackgroundJobConcurrencyCountRow} */ (rawRow)
|
|
2902
|
+
persistedCounts.set(row.concurrency_key, this._validatedConcurrencyCount(row.active_count, row.concurrency_key))
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const concurrencyKeys = [...new Set([...activeCounts.keys(), ...persistedCounts.keys()])].sort()
|
|
2906
|
+
const candidateKeys = concurrencyKeys.filter((concurrencyKey) => {
|
|
2907
|
+
return (activeCounts.get(concurrencyKey) || 0) !== (persistedCounts.get(concurrencyKey) || 0)
|
|
2908
|
+
})
|
|
2909
|
+
/** @type {import("./types.js").BackgroundJobConcurrencyRepair[]} */
|
|
2910
|
+
const repairs = []
|
|
2911
|
+
let repairedCount = 0
|
|
2912
|
+
|
|
2913
|
+
for (const concurrencyKey of candidateKeys) {
|
|
2914
|
+
const repair = insideTransaction
|
|
2915
|
+
? await this._reconcileConcurrencyKey(db, concurrencyKey)
|
|
2916
|
+
: await this._transactionResult(db, async () => await this._reconcileConcurrencyKey(db, concurrencyKey))
|
|
2917
|
+
|
|
2918
|
+
if (!repair) continue
|
|
2919
|
+
|
|
2920
|
+
repairedCount++
|
|
2921
|
+
if (repairs.length < CONCURRENCY_REPAIR_SAMPLE_LIMIT) repairs.push(repair)
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
return {
|
|
2925
|
+
candidateCount: candidateKeys.length,
|
|
2926
|
+
checkedCount: concurrencyKeys.length,
|
|
2927
|
+
repairedCount,
|
|
2928
|
+
repairs,
|
|
2929
|
+
repairsTruncatedCount: repairedCount - repairs.length
|
|
2794
2930
|
}
|
|
2795
2931
|
}
|
|
2796
2932
|
|
|
@@ -2799,29 +2935,57 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2799
2935
|
* lock order used by handoff and completion transitions.
|
|
2800
2936
|
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
2801
2937
|
* @param {string} concurrencyKey - Counter key.
|
|
2802
|
-
* @returns {Promise<
|
|
2938
|
+
* @returns {Promise<import("./types.js").BackgroundJobConcurrencyRepair | null>} - Applied repair.
|
|
2803
2939
|
*/
|
|
2804
2940
|
async _reconcileConcurrencyKey(db, concurrencyKey) {
|
|
2805
2941
|
await this._lockConcurrencyRow(db, concurrencyKey)
|
|
2942
|
+
const persistedRows = await db
|
|
2943
|
+
.newQuery()
|
|
2944
|
+
.from(CONCURRENCY_TABLE)
|
|
2945
|
+
.select("active_count")
|
|
2946
|
+
.select("concurrency_key")
|
|
2947
|
+
.where({concurrency_key: concurrencyKey})
|
|
2948
|
+
.limit(1)
|
|
2949
|
+
.results()
|
|
2950
|
+
|
|
2951
|
+
if (!persistedRows[0]) throw new Error(`Missing background job concurrency counter for ${concurrencyKey}`)
|
|
2952
|
+
|
|
2953
|
+
const persistedRow = /** @type {BackgroundJobConcurrencyCountRow} */ (persistedRows[0])
|
|
2954
|
+
const previousActiveCount = this._validatedConcurrencyCount(persistedRow.active_count, concurrencyKey)
|
|
2806
2955
|
const rows = await db
|
|
2807
2956
|
.newQuery()
|
|
2808
2957
|
.from(JOBS_TABLE)
|
|
2809
2958
|
.select("COUNT(*) AS active_count")
|
|
2810
2959
|
.where({concurrency_key: concurrencyKey, status: "handed_off"})
|
|
2811
2960
|
.results()
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
)
|
|
2961
|
+
const countRow = /** @type {{active_count: number | string}} */ (rows[0])
|
|
2962
|
+
const activeCount = this._validatedConcurrencyCount(countRow.active_count, concurrencyKey)
|
|
2815
2963
|
|
|
2816
|
-
if (activeCount ===
|
|
2817
|
-
throw new Error(`Invalid reconciled background job concurrency count for ${concurrencyKey}: ${activeCount}`)
|
|
2818
|
-
}
|
|
2964
|
+
if (activeCount === previousActiveCount) return null
|
|
2819
2965
|
|
|
2820
2966
|
await db.update({
|
|
2821
2967
|
tableName: CONCURRENCY_TABLE,
|
|
2822
2968
|
data: {active_count: activeCount},
|
|
2823
2969
|
conditions: {concurrency_key: concurrencyKey}
|
|
2824
2970
|
})
|
|
2971
|
+
|
|
2972
|
+
return {activeCount, concurrencyKey, previousActiveCount}
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* Validates a database count before it participates in reconciliation.
|
|
2977
|
+
* @param {number | string} value - Raw count.
|
|
2978
|
+
* @param {string} concurrencyKey - Counter key for diagnostics.
|
|
2979
|
+
* @returns {number} - Safe non-negative count.
|
|
2980
|
+
*/
|
|
2981
|
+
_validatedConcurrencyCount(value, concurrencyKey) {
|
|
2982
|
+
const count = this._normalizeNumber(value)
|
|
2983
|
+
|
|
2984
|
+
if (count === null || !Number.isSafeInteger(count) || count < 0) {
|
|
2985
|
+
throw new Error(`Invalid reconciled background job concurrency count for ${concurrencyKey}: ${count}`)
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
return count
|
|
2825
2989
|
}
|
|
2826
2990
|
|
|
2827
2991
|
/**
|
|
@@ -2836,11 +3000,12 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2836
3000
|
* while a backlog exists otherwise leaves persisted rows stale: pre-cap jobs
|
|
2837
3001
|
* keep a null key and bypass the cap, post-removal jobs stay capped under a
|
|
2838
3002
|
* now-unconfigured key, and a changed numeric cap stays stale until the next
|
|
2839
|
-
* enqueue. Bring
|
|
2840
|
-
* queue's stored cap, adopt not-yet-keyed
|
|
2841
|
-
*
|
|
2842
|
-
*
|
|
2843
|
-
*
|
|
3003
|
+
* enqueue. Bring queued durable state in line with config: sync each configured
|
|
3004
|
+
* queue's stored cap, adopt not-yet-keyed queued jobs onto their queue key,
|
|
3005
|
+
* and release queued jobs from queue keys whose queue is no longer capped.
|
|
3006
|
+
* Existing handoffs retain the policy and reservation they started with, so
|
|
3007
|
+
* reconciliation cannot race their completion/retry transitions. Runs before
|
|
3008
|
+
* {@link _reconcileConcurrency} so any pre-existing active counts are exact.
|
|
2844
3009
|
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
2845
3010
|
* @returns {Promise<void>} - Resolves when reconciled.
|
|
2846
3011
|
*/
|
|
@@ -2853,7 +3018,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2853
3018
|
const keyColumn = db.quoteColumn("concurrency_key")
|
|
2854
3019
|
const capColumn = db.quoteColumn("max_concurrency")
|
|
2855
3020
|
const queueColumn = db.quoteColumn("queue")
|
|
2856
|
-
const
|
|
3021
|
+
const queued = `${db.quoteColumn("status")} = ${db.quote("queued")}`
|
|
2857
3022
|
/** @type {Set<string>} */
|
|
2858
3023
|
const cappedQueues = new Set()
|
|
2859
3024
|
|
|
@@ -2868,7 +3033,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2868
3033
|
await this._ensureQueueConcurrencyKey(db, {concurrencyKey, maxConcurrency: cap})
|
|
2869
3034
|
await db.query(
|
|
2870
3035
|
`UPDATE ${jobsTable} SET ${keyColumn} = ${db.quote(concurrencyKey)}, ${capColumn} = ${Number(cap)} ` +
|
|
2871
|
-
`WHERE ${queueColumn} = ${db.quote(queue)} AND ${keyColumn} IS NULL AND ${
|
|
3036
|
+
`WHERE ${queueColumn} = ${db.quote(queue)} AND ${keyColumn} IS NULL AND ${queued}`
|
|
2872
3037
|
)
|
|
2873
3038
|
}
|
|
2874
3039
|
|
|
@@ -2887,7 +3052,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2887
3052
|
|
|
2888
3053
|
await db.query(
|
|
2889
3054
|
`UPDATE ${jobsTable} SET ${keyColumn} = NULL, ${capColumn} = NULL ` +
|
|
2890
|
-
`WHERE ${keyColumn} = ${db.quote(concurrencyKey)} AND ${
|
|
3055
|
+
`WHERE ${keyColumn} = ${db.quote(concurrencyKey)} AND ${queued}`
|
|
2891
3056
|
)
|
|
2892
3057
|
}
|
|
2893
3058
|
}
|
|
@@ -3025,15 +3190,29 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
3025
3190
|
}
|
|
3026
3191
|
|
|
3027
3192
|
/**
|
|
3028
|
-
*
|
|
3029
|
-
* connection. Cross-process ordering remains the responsibility of durable
|
|
3030
|
-
* row/advisory locks and unique constraints acquired around the callback.
|
|
3193
|
+
* Runs a serialized callback inside one transaction.
|
|
3031
3194
|
* @template T
|
|
3032
3195
|
* @param {(db: import("../database/drivers/base.js").default) => Promise<T>} callback - Transaction callback.
|
|
3033
3196
|
* @param {BackgroundJobTransactionSerializationOptions} [options] - Serialization options.
|
|
3034
3197
|
* @returns {Promise<T>} Callback result.
|
|
3035
3198
|
*/
|
|
3036
3199
|
async _serializedTransactionMutation(callback, options = {}) {
|
|
3200
|
+
return await this._serializedConnectionMutation(
|
|
3201
|
+
async (db) => await this._transactionResult(db, async () => await callback(db)),
|
|
3202
|
+
options
|
|
3203
|
+
)
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
/**
|
|
3207
|
+
* Admits mutation callbacks to the process-local FIFO before they check out a
|
|
3208
|
+
* connection. Cross-process ordering remains the responsibility of durable
|
|
3209
|
+
* row/advisory locks and unique constraints acquired around the callback.
|
|
3210
|
+
* @template T
|
|
3211
|
+
* @param {(db: import("../database/drivers/base.js").default) => Promise<T>} callback - Connection callback.
|
|
3212
|
+
* @param {BackgroundJobTransactionSerializationOptions} [options] - Serialization options.
|
|
3213
|
+
* @returns {Promise<T>} Callback result.
|
|
3214
|
+
*/
|
|
3215
|
+
async _serializedConnectionMutation(callback, options = {}) {
|
|
3037
3216
|
const identifier = this.getDatabaseIdentifier() || "default"
|
|
3038
3217
|
const previous = transactionMutationChains.get(identifier) || Promise.resolve()
|
|
3039
3218
|
let resolveRun = () => {}
|
|
@@ -3057,7 +3236,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
3057
3236
|
}
|
|
3058
3237
|
|
|
3059
3238
|
try {
|
|
3060
|
-
return await
|
|
3239
|
+
return await callback(db)
|
|
3061
3240
|
} finally {
|
|
3062
3241
|
if (advisoryLock) await db.releaseAdvisoryLock(advisoryLock.name)
|
|
3063
3242
|
}
|
|
@@ -51,6 +51,20 @@
|
|
|
51
51
|
* @property {number} maxConcurrency - Positive cap.
|
|
52
52
|
* @property {boolean} queueDerived - Whether queue configuration owns the cap.
|
|
53
53
|
*/
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} BackgroundJobConcurrencyRepair
|
|
56
|
+
* @property {number} activeCount - Exact handed-off job count persisted by the repair.
|
|
57
|
+
* @property {string} concurrencyKey - Durable cap identity.
|
|
58
|
+
* @property {number} previousActiveCount - Persisted count replaced by the repair.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} BackgroundJobConcurrencyReconciliation
|
|
62
|
+
* @property {number} candidateCount - Snapshot mismatches rechecked under their counter locks.
|
|
63
|
+
* @property {number} checkedCount - Active or nonzero durable counters compared in the initial snapshot.
|
|
64
|
+
* @property {number} repairedCount - Counters whose persisted values were changed.
|
|
65
|
+
* @property {BackgroundJobConcurrencyRepair[]} repairs - Bounded deterministic sample of applied repairs.
|
|
66
|
+
* @property {number} repairsTruncatedCount - Applied repairs omitted from the sample.
|
|
67
|
+
*/
|
|
54
68
|
/**
|
|
55
69
|
* @typedef {object} PreparedLocalBackgroundJob
|
|
56
70
|
* @property {string} argsDigest - Fixed-width digest of the serialized arguments.
|
|
@@ -78,6 +92,7 @@
|
|
|
78
92
|
* @typedef {object} BackgroundJobHandoff
|
|
79
93
|
* @property {string} handoffId - Unique handoff lease id.
|
|
80
94
|
* @property {number} handedOffAtMs - Time handed to a worker in ms.
|
|
95
|
+
* @property {BackgroundJobRow} [job] - Exact committed job snapshot when the adapter changes dispatch data during the claim.
|
|
81
96
|
*/
|
|
82
97
|
/**
|
|
83
98
|
* @typedef {object} BackgroundJobHandoffSnapshot
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
* @property {string[]} [builtInMemberCommands] - Built-in member command names (`find`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
451
451
|
* @property {string} [modelName] - Frontend model name override.
|
|
452
452
|
* @property {string[]} [relationships] - Relationship names to expose in frontend models. Type and target model are inferred from the backend model's registered relationships.
|
|
453
|
-
* @property {string} [primaryKey] - Primary key attribute name.
|
|
453
|
+
* @property {string | string[]} [primaryKey] - Primary key attribute name or ordered composite primary-key attribute names.
|
|
454
454
|
* @property {FrontendModelResourceServerConfiguration} [server] - Optional legacy backend behavior overrides for built-in frontend actions.
|
|
455
455
|
* @property {FrontendModelResourceSyncConfiguration | boolean} [sync] - Optional safe local/offline sync policy metadata. `policy` participates in the hash but is not exposed to generated frontend config/manifest.
|
|
456
456
|
*/
|