velocious 1.0.643 → 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 +21 -10
- 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/assets/cache.d.ts +288 -0
- package/build/src/sync/assets/cache.d.ts.map +1 -0
- package/build/src/sync/assets/cache.js +883 -0
- package/build/src/sync/assets/types.d.ts +199 -0
- package/build/src/sync/assets/types.d.ts.map +1 -0
- package/build/src/sync/assets/types.js +56 -0
- 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 +279 -117
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +503 -933
- 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 +54 -0
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-attempt-executor.js +408 -0
- package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
- package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
- package/build/src/testing/velocious-runner-reporter.js +240 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-suite-hook-executor.js +41 -0
- package/build/src/testing/velocious-test-arguments.d.ts +39 -0
- package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
- package/build/src/testing/velocious-test-arguments.js +55 -0
- 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/src/utils/sha256-bytes-hex.d.ts +7 -0
- package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
- package/build/src/utils/sha256-bytes-hex.js +117 -0
- package/build/src/utils/sha256-hex.d.ts +2 -4
- package/build/src/utils/sha256-hex.d.ts.map +1 -1
- package/build/src/utils/sha256-hex.js +6 -120
- package/build/sync/assets/cache.js +998 -0
- package/build/sync/assets/types.js +62 -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 +496 -990
- package/build/testing/test.js +164 -306
- package/build/testing/velocious-attempt-executor.js +441 -0
- package/build/testing/velocious-runner-reporter.js +264 -0
- package/build/testing/velocious-suite-hook-executor.js +43 -0
- package/build/testing/velocious-test-arguments.js +64 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/model-primary-key.js +171 -0
- package/build/utils/sha256-bytes-hex.js +132 -0
- package/build/utils/sha256-hex.js +7 -135
- 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/assets/cache.js +998 -0
- package/src/sync/assets/types.js +62 -0
- 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 +496 -990
- package/src/testing/test.js +164 -306
- package/src/testing/velocious-attempt-executor.js +441 -0
- package/src/testing/velocious-runner-reporter.js +264 -0
- package/src/testing/velocious-suite-hook-executor.js +43 -0
- package/src/testing/velocious-test-arguments.js +64 -0
- package/src/utils/model-primary-key.js +171 -0
- package/src/utils/sha256-bytes-hex.js +132 -0
- package/src/utils/sha256-hex.js +7 -135
- 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
|
@@ -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
|
*/
|
package/build/configuration.js
CHANGED
|
@@ -229,6 +229,9 @@ export default class VelociousConfiguration {
|
|
|
229
229
|
* @type {Set<import("./database/drivers/base.js").default>} */
|
|
230
230
|
_advisoryLockConnections = new Set()
|
|
231
231
|
|
|
232
|
+
/** @type {Map<string, number>} */
|
|
233
|
+
_schemaCacheGenerationsByReuseKey = new Map()
|
|
234
|
+
|
|
232
235
|
/**
|
|
233
236
|
* Runs current.
|
|
234
237
|
* @returns {VelociousConfiguration} - The current.
|
|
@@ -403,6 +406,13 @@ export default class VelociousConfiguration {
|
|
|
403
406
|
* @type {Set<Promise<void>>} */
|
|
404
407
|
this._localBroadcastDeliveries = new Set()
|
|
405
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Latest local broadcast delivery per subscription. Chaining subsequent
|
|
411
|
+
* deliveries preserves lifecycle event order without coupling separate
|
|
412
|
+
* subscribers to one another.
|
|
413
|
+
* @type {WeakMap<import("./http-server/websocket-channel.js").default, Promise<void>>} */
|
|
414
|
+
this._localBroadcastDeliveryTails = new WeakMap()
|
|
415
|
+
|
|
406
416
|
/**
|
|
407
417
|
* Stores the websocket sessions value.
|
|
408
418
|
* @type {Set<import("./http-server/client/websocket-session.js").default>} - Live websocket sessions, including paused sessions within the grace window.
|
|
@@ -1137,6 +1147,11 @@ export default class VelociousConfiguration {
|
|
|
1137
1147
|
* @returns {void} - No return value.
|
|
1138
1148
|
*/
|
|
1139
1149
|
clearSchemaCachesForReuseKey(reuseKey) {
|
|
1150
|
+
this._schemaCacheGenerationsByReuseKey.set(
|
|
1151
|
+
reuseKey,
|
|
1152
|
+
this.schemaCacheGenerationForReuseKey(reuseKey) + 1
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1140
1155
|
for (const pool of Object.values(this.databasePools)) {
|
|
1141
1156
|
if (pool.getConfigurationReuseKey() === reuseKey) {
|
|
1142
1157
|
pool.clearSchemaCache()
|
|
@@ -1144,6 +1159,15 @@ export default class VelociousConfiguration {
|
|
|
1144
1159
|
}
|
|
1145
1160
|
}
|
|
1146
1161
|
|
|
1162
|
+
/**
|
|
1163
|
+
* Returns the current schema-cache generation for one physical database.
|
|
1164
|
+
* @param {string} reuseKey - Connection reuse key identifying the shared database.
|
|
1165
|
+
* @returns {number} - Current schema-cache generation.
|
|
1166
|
+
*/
|
|
1167
|
+
schemaCacheGenerationForReuseKey(reuseKey) {
|
|
1168
|
+
return this._schemaCacheGenerationsByReuseKey.get(reuseKey) || 0
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1147
1171
|
/**
|
|
1148
1172
|
* Invalidates record metadata owned by one closed/deleted physical tenant
|
|
1149
1173
|
* database while preserving every other tenant generation.
|
|
@@ -3249,7 +3273,7 @@ export default class VelociousConfiguration {
|
|
|
3249
3273
|
* @param {string} name - Channel name.
|
|
3250
3274
|
* @param {Record<string, ReturnType<typeof JSON.parse>>} broadcastParams - Params passed to each subscription's `matches()`.
|
|
3251
3275
|
* @param {ReturnType<typeof JSON.parse>} body - Message body delivered via `sendMessage()`.
|
|
3252
|
-
* @param {
|
|
3276
|
+
* @param {import("./http-server/websocket-channel.js").WebsocketBroadcastMetadata} [meta] - Optional event metadata for replay tracking.
|
|
3253
3277
|
* @returns {void}
|
|
3254
3278
|
*/
|
|
3255
3279
|
_broadcastToChannelLocal(name, broadcastParams, body, meta) {
|
|
@@ -3273,25 +3297,37 @@ export default class VelociousConfiguration {
|
|
|
3273
3297
|
|
|
3274
3298
|
if (!matches) continue
|
|
3275
3299
|
|
|
3300
|
+
const deliveryMetadata = {
|
|
3301
|
+
broadcastParams,
|
|
3302
|
+
...(meta?.eventId ? {eventId: meta.eventId} : {})
|
|
3303
|
+
}
|
|
3304
|
+
const previousDelivery = this._localBroadcastDeliveryTails.get(subscription)
|
|
3276
3305
|
const delivery = this.withoutCurrentConnectionContexts(() => {
|
|
3277
|
-
return Promise
|
|
3278
|
-
.
|
|
3279
|
-
.then(() => this._deliverWebsocketChannelBroadcast(subscription, body, {eventId: meta?.eventId}))
|
|
3306
|
+
return (previousDelivery || Promise.resolve())
|
|
3307
|
+
.then(() => this._deliverWebsocketChannelBroadcast(subscription, body, deliveryMetadata))
|
|
3280
3308
|
.catch((error) => {
|
|
3281
3309
|
console.error(`broadcastToChannel: ${name} subscription ${subscription.subscriptionId} deliverBroadcast threw`, error)
|
|
3282
3310
|
})
|
|
3283
3311
|
})
|
|
3284
3312
|
|
|
3313
|
+
this._localBroadcastDeliveryTails.set(subscription, delivery)
|
|
3314
|
+
|
|
3285
3315
|
// Keep the fire-and-forget delivery (never awaited at broadcast time) but
|
|
3286
3316
|
// track it so `awaitPendingBroadcasts` can drain it before settling. Remove
|
|
3287
3317
|
// on settle; the failure handler also satisfies the promise so a rejected
|
|
3288
3318
|
// delivery never becomes an unhandled rejection.
|
|
3289
3319
|
this._localBroadcastDeliveries.add(delivery)
|
|
3290
3320
|
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3321
|
+
/**
|
|
3322
|
+
* Removes a settled delivery from local tracking.
|
|
3323
|
+
* @returns {void}
|
|
3324
|
+
*/
|
|
3325
|
+
const forgetDelivery = () => {
|
|
3326
|
+
this._localBroadcastDeliveries.delete(delivery)
|
|
3327
|
+
if (this._localBroadcastDeliveryTails.get(subscription) === delivery) this._localBroadcastDeliveryTails.delete(subscription)
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
delivery.then(forgetDelivery, forgetDelivery)
|
|
3295
3331
|
}
|
|
3296
3332
|
}
|
|
3297
3333
|
|
|
@@ -3314,7 +3350,7 @@ export default class VelociousConfiguration {
|
|
|
3314
3350
|
* Runs deliver websocket channel broadcast.
|
|
3315
3351
|
* @param {import("./http-server/websocket-channel.js").default} subscription - Channel subscription.
|
|
3316
3352
|
* @param {import("./http-server/websocket-channel.js").WebsocketJsonValue} body - Broadcast body.
|
|
3317
|
-
* @param {
|
|
3353
|
+
* @param {import("./http-server/websocket-channel.js").WebsocketBroadcastMetadata} meta - Broadcast metadata.
|
|
3318
3354
|
* @returns {void | Promise<void>} Broadcast delivery result.
|
|
3319
3355
|
*/
|
|
3320
3356
|
_deliverWebsocketChannelBroadcast(subscription, body, meta) {
|
|
@@ -171,6 +171,14 @@ export default class VelociousDatabaseDriversBaseColumn {
|
|
|
171
171
|
return match[1].toLowerCase()
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Returns the database-native type name used in SQL casts.
|
|
176
|
+
* @returns {string} - Database-native type name.
|
|
177
|
+
*/
|
|
178
|
+
getDatabaseType() {
|
|
179
|
+
return this.getType()
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
/**
|
|
175
183
|
* Runs get type.
|
|
176
184
|
* @abstract
|
|
@@ -20,9 +20,7 @@ export default class VelociousDatabaseConnectionDriversMssqlSqlUpdate extends Up
|
|
|
20
20
|
for (let columnName in this.conditions) {
|
|
21
21
|
if (count > 0) sql += " AND "
|
|
22
22
|
|
|
23
|
-
sql += this.
|
|
24
|
-
sql += " = "
|
|
25
|
-
sql += this.formatValue(this.conditions[columnName])
|
|
23
|
+
sql += this.formatCondition(columnName, this.conditions[columnName])
|
|
26
24
|
count++
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -22,9 +22,7 @@ export default class VelociousDatabaseConnectionDriversMysqlSqlUpdate extends Up
|
|
|
22
22
|
for (let columnName in this.conditions) {
|
|
23
23
|
if (count > 0) sql += " AND "
|
|
24
24
|
|
|
25
|
-
sql += this.
|
|
26
|
-
sql += " = "
|
|
27
|
-
sql += this.formatValue(this.conditions[columnName])
|
|
25
|
+
sql += this.formatCondition(columnName, this.conditions[columnName])
|
|
28
26
|
count++
|
|
29
27
|
}
|
|
30
28
|
|
|
@@ -33,6 +33,28 @@ export default class VelociousDatabaseDriversPgsqlColumn extends BaseColumn {
|
|
|
33
33
|
return digg(this, "data", "column_default")
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Returns the concrete PostgreSQL type name used in SQL casts.
|
|
38
|
+
* @returns {string} - Schema-qualified domain or UDT name, or the ordinary data type.
|
|
39
|
+
*/
|
|
40
|
+
getDatabaseType() {
|
|
41
|
+
const domainName = this.data.domain_name
|
|
42
|
+
const domainSchema = this.data.domain_schema
|
|
43
|
+
const dataType = this.data.data_type
|
|
44
|
+
const udtName = this.data.udt_name
|
|
45
|
+
const udtSchema = this.data.udt_schema
|
|
46
|
+
|
|
47
|
+
if (typeof domainName === "string" && typeof domainSchema === "string") {
|
|
48
|
+
return `${this.getDriver().quoteColumn(domainSchema)}.${this.getDriver().quoteColumn(domainName)}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ((dataType === "USER-DEFINED" || dataType === "ARRAY") && typeof udtName === "string" && typeof udtSchema === "string") {
|
|
52
|
+
return `${this.getDriver().quoteColumn(udtSchema)}.${this.getDriver().quoteColumn(udtName)}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this.getType()
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
getMaxLength() {
|
|
37
59
|
return digg(this, "data", "character_maximum_length")
|
|
38
60
|
}
|
|
@@ -22,9 +22,7 @@ export default class VelociousDatabaseConnectionDriversPgsqlSqlUpdate extends Up
|
|
|
22
22
|
for (let columnName in this.conditions) {
|
|
23
23
|
if (count > 0) sql += " AND "
|
|
24
24
|
|
|
25
|
-
sql += this.
|
|
26
|
-
sql += " = "
|
|
27
|
-
sql += this.formatValue(this.conditions[columnName])
|
|
25
|
+
sql += this.formatCondition(columnName, this.conditions[columnName])
|
|
28
26
|
count++
|
|
29
27
|
}
|
|
30
28
|
|
|
@@ -110,7 +110,17 @@ export default class VelociousDatabaseConnectionDriversSqliteSqlAlterTable exten
|
|
|
110
110
|
if (alterColumn.getDefault() === undefined) alterColumn.setDefault(currentColumn.getDefault())
|
|
111
111
|
if (!alterColumn.getIndex()) alterColumn.setIndex(currentColumn.getIndex())
|
|
112
112
|
if (!alterColumn.getForeignKey()) alterColumn.setForeignKey(currentColumn.getForeignKey())
|
|
113
|
-
|
|
113
|
+
const alterColumnType = alterColumn.getType()
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
alterColumn.getMaxLength() === undefined
|
|
117
|
+
&& (
|
|
118
|
+
alterColumnType === undefined
|
|
119
|
+
|| (alterColumnType !== "text" && alterColumnType === currentColumn.getType())
|
|
120
|
+
)
|
|
121
|
+
) {
|
|
122
|
+
alterColumn.setMaxLength(currentColumn.getMaxLength())
|
|
123
|
+
}
|
|
114
124
|
alterColumn.setPrimaryKey(alterColumn.getPrimaryKey() || currentColumn.getPrimaryKey())
|
|
115
125
|
if (!alterColumn.getType()) alterColumn.setType(currentColumn.getType())
|
|
116
126
|
|