velocious 1.0.557 → 1.0.559
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 +17 -3
- package/build/background-jobs/main.js +35 -12
- package/build/background-jobs/store.js +262 -16
- package/build/background-jobs/web/authorization.js +5 -4
- package/build/background-jobs/web/controller.js +13 -16
- package/build/background-jobs/web/counts-channel.js +79 -0
- package/build/background-jobs/web/index.js +2 -0
- package/build/background-jobs/web/registry.js +1 -1
- package/build/background-jobs/worker.js +53 -28
- package/build/database/record/index.js +12 -0
- package/build/frontend-models/base.js +192 -41
- package/build/http-client/websocket-client.js +48 -0
- package/build/src/background-jobs/main.d.ts +14 -1
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +37 -14
- package/build/src/background-jobs/store.d.ts +79 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +235 -17
- package/build/src/background-jobs/web/authorization.d.ts +5 -3
- package/build/src/background-jobs/web/authorization.d.ts.map +1 -1
- package/build/src/background-jobs/web/authorization.js +6 -5
- package/build/src/background-jobs/web/controller.d.ts.map +1 -1
- package/build/src/background-jobs/web/controller.js +14 -15
- package/build/src/background-jobs/web/counts-channel.d.ts +31 -0
- package/build/src/background-jobs/web/counts-channel.d.ts.map +1 -0
- package/build/src/background-jobs/web/counts-channel.js +71 -0
- package/build/src/background-jobs/web/index.d.ts.map +1 -1
- package/build/src/background-jobs/web/index.js +3 -1
- package/build/src/background-jobs/web/registry.d.ts +1 -1
- package/build/src/background-jobs/web/registry.d.ts.map +1 -1
- package/build/src/background-jobs/web/registry.js +2 -2
- package/build/src/background-jobs/worker.d.ts +18 -1
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +55 -31
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +9 -1
- package/build/src/frontend-models/base.d.ts +13 -4
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +173 -44
- package/build/src/http-client/websocket-client.d.ts +3 -0
- package/build/src/http-client/websocket-client.d.ts.map +1 -1
- package/build/src/http-client/websocket-client.js +42 -1
- package/build/src/tenants/tenant.d.ts +20 -9
- package/build/src/tenants/tenant.d.ts.map +1 -1
- package/build/src/tenants/tenant.js +47 -12
- package/build/src/testing/factory/factory-registry.d.ts +22 -3
- package/build/src/testing/factory/factory-registry.d.ts.map +1 -1
- package/build/src/testing/factory/factory-registry.js +32 -9
- package/build/src/testing/factory/factory-runner.d.ts +16 -4
- package/build/src/testing/factory/factory-runner.d.ts.map +1 -1
- package/build/src/testing/factory/factory-runner.js +33 -10
- package/build/src/utils/shutdown-lifecycle.d.ts +12 -0
- package/build/src/utils/shutdown-lifecycle.d.ts.map +1 -0
- package/build/src/utils/shutdown-lifecycle.js +36 -0
- package/build/tenants/tenant.js +51 -11
- package/build/testing/factory/factory-registry.js +33 -8
- package/build/testing/factory/factory-runner.js +38 -12
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/shutdown-lifecycle.js +41 -0
- package/package.json +3 -2
- package/src/background-jobs/main.js +35 -12
- package/src/background-jobs/store.js +262 -16
- package/src/background-jobs/web/authorization.js +5 -4
- package/src/background-jobs/web/controller.js +13 -16
- package/src/background-jobs/web/counts-channel.js +79 -0
- package/src/background-jobs/web/index.js +2 -0
- package/src/background-jobs/web/registry.js +1 -1
- package/src/background-jobs/worker.js +53 -28
- package/src/database/record/index.js +12 -0
- package/src/frontend-models/base.js +192 -41
- package/src/http-client/websocket-client.js +48 -0
- package/src/tenants/tenant.js +51 -11
- package/src/testing/factory/factory-registry.js +33 -8
- package/src/testing/factory/factory-runner.js +38 -12
- package/src/utils/shutdown-lifecycle.js +41 -0
package/src/tenants/tenant.js
CHANGED
|
@@ -11,19 +11,21 @@ import TenantIterator from "./tenant-iterator.js"
|
|
|
11
11
|
* class is the single discoverable home for switching into a tenant's context, reading the
|
|
12
12
|
* current one, iterating every tenant of a database identifier, and dropping a tenant's
|
|
13
13
|
* database. Switching delegates to {@link Current} (which owns the async-context tenant
|
|
14
|
-
* state) and additionally runs the callback inside `ensureConnections`,
|
|
14
|
+
* state) and additionally runs the callback inside `ensureConnections`, initializing registered
|
|
15
|
+
* tenant-switched models whose tables exist before the callback runs. Entering a tenant therefore
|
|
15
16
|
* makes its database immediately queryable — the apartment-style "switch" semantics — without
|
|
16
|
-
* the caller establishing connections itself; iteration and drop drive the
|
|
17
|
-
* database provider hooks.
|
|
17
|
+
* the caller establishing connections or model metadata itself; iteration and drop drive the
|
|
18
|
+
* app's tenant database provider hooks.
|
|
18
19
|
*/
|
|
19
20
|
export default class Tenant {
|
|
20
21
|
/**
|
|
21
22
|
* Runs `callback` with `tenant` as the current tenant, restoring the previous tenant after.
|
|
22
23
|
* The callback runs inside `ensureConnections`, so every database identifier the tenant
|
|
23
24
|
* activates (the global database plus the tenant's database) has a checked-out connection
|
|
24
|
-
* available for the callback's duration
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
25
|
+
* available for the callback's duration. Registered tenant-switched models whose tables exist
|
|
26
|
+
* are initialized before the callback runs, so switching into a tenant makes it queryable
|
|
27
|
+
* without the caller wiring up connections or model metadata. Already-checked-out connections
|
|
28
|
+
* and in-progress model initialization promises are reused. The callback receives the active
|
|
27
29
|
* connections keyed by identifier, the same as `ensureConnections`.
|
|
28
30
|
* @template T
|
|
29
31
|
* @param {object} tenant Descriptor understood by the app's tenantDatabaseResolver.
|
|
@@ -33,7 +35,41 @@ export default class Tenant {
|
|
|
33
35
|
static async with(tenant, callback) {
|
|
34
36
|
const configuration = Current.configuration()
|
|
35
37
|
|
|
36
|
-
return await Current.withTenant(tenant, async () => await configuration.ensureConnections(
|
|
38
|
+
return await Current.withTenant(tenant, async () => await configuration.ensureConnections(async (connections) => {
|
|
39
|
+
await this._ensureCurrentTenantModelsInitialized(configuration)
|
|
40
|
+
|
|
41
|
+
return await callback(connections)
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initializes registered tenant-switched models whose tables exist in the
|
|
47
|
+
* current tenant before runtime callbacks can build synchronous query scopes.
|
|
48
|
+
* Models for absent optional tables remain deferred until they are used.
|
|
49
|
+
* @param {import("../configuration.js").default} configuration - Current configuration.
|
|
50
|
+
* @returns {Promise<void>} - Resolves when available tenant models are initialized.
|
|
51
|
+
*/
|
|
52
|
+
static async _ensureCurrentTenantModelsInitialized(configuration) {
|
|
53
|
+
for (const modelClass of Object.values(configuration.getModelClasses())) {
|
|
54
|
+
if (modelClass.isInitialized() || !modelClass.hasTenantDatabaseIdentifierResolver()) continue
|
|
55
|
+
|
|
56
|
+
const databaseIdentifier = modelClass.getTenantDatabaseIdentifier()
|
|
57
|
+
|
|
58
|
+
if (!databaseIdentifier || !configuration.isDatabaseIdentifierActive(databaseIdentifier)) continue
|
|
59
|
+
|
|
60
|
+
const connection = modelClass.connection()
|
|
61
|
+
const table = await connection.getTableByName(modelClass.tableName(), {throwError: false})
|
|
62
|
+
|
|
63
|
+
if (!table) continue
|
|
64
|
+
|
|
65
|
+
if (Object.keys(modelClass.getTranslationsMap()).length > 0) {
|
|
66
|
+
const translationsTable = await connection.getTableByName(modelClass.getTranslationsTableName(), {throwError: false})
|
|
67
|
+
|
|
68
|
+
if (!translationsTable) continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await modelClass.ensureInitialized({configuration})
|
|
72
|
+
}
|
|
37
73
|
}
|
|
38
74
|
|
|
39
75
|
/**
|
|
@@ -47,9 +83,10 @@ export default class Tenant {
|
|
|
47
83
|
/**
|
|
48
84
|
* Lists the tenants for a database identifier through the provider and runs `callback`
|
|
49
85
|
* within each tenant's context, optionally filtered and several at a time. Like
|
|
50
|
-
* {@link Tenant.with}, the callback runs inside `ensureConnections`
|
|
51
|
-
*
|
|
52
|
-
*
|
|
86
|
+
* {@link Tenant.with}, the callback runs inside `ensureConnections` after available
|
|
87
|
+
* tenant-switched models are initialized, so each tenant's database is queryable without the
|
|
88
|
+
* caller wiring up connections or model metadata. Returns how many tenants the callback ran
|
|
89
|
+
* for (after filtering).
|
|
53
90
|
* @param {{identifier: string, callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, parallel?: number, filter?: (tenant: ?) => boolean, configuration?: import("../configuration.js").default}} args - Tenant database identifier, per-tenant operation, filtering, and concurrency settings.
|
|
54
91
|
* @returns {Promise<number>} - Number of processed tenants.
|
|
55
92
|
*/
|
|
@@ -71,7 +108,10 @@ export default class Tenant {
|
|
|
71
108
|
// their callbacks, such as create, before the tenant database exists) while runtime
|
|
72
109
|
// iteration here gets the tenant's connections established the same way Tenant.with does.
|
|
73
110
|
return await iterator.run(tenants, async (callbackArgs) => {
|
|
74
|
-
await configuration.ensureConnections({name: `Tenant.each: ${identifier}`}, async () =>
|
|
111
|
+
await configuration.ensureConnections({name: `Tenant.each: ${identifier}`}, async () => {
|
|
112
|
+
await this._ensureCurrentTenantModelsInitialized(configuration)
|
|
113
|
+
await callback(callbackArgs)
|
|
114
|
+
})
|
|
75
115
|
})
|
|
76
116
|
}
|
|
77
117
|
|
|
@@ -333,17 +333,27 @@ export default class FactoryRegistry {
|
|
|
333
333
|
const {traits, overrides} = normalizeInvocationArgs(args)
|
|
334
334
|
/** @type {Array<?>} */
|
|
335
335
|
const results = []
|
|
336
|
+
/** @type {import("./factory-runner.js").CompiledPlan | undefined} */
|
|
337
|
+
let planTemplate
|
|
336
338
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
this._activeEvaluations += 1
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
for (let index = 0; index < count; index++) {
|
|
343
|
+
const invocation = await this._runFactoryInvocation({factoryName, traits, overrides, strategy, planTemplate})
|
|
344
|
+
|
|
345
|
+
planTemplate = invocation.planTemplate
|
|
346
|
+
results.push(invocation.result)
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
this._activeEvaluations -= 1
|
|
339
350
|
}
|
|
340
351
|
|
|
341
352
|
return results
|
|
342
353
|
}
|
|
343
354
|
|
|
344
355
|
/**
|
|
345
|
-
* Compiles and runs a factory invocation under a strategy
|
|
346
|
-
* evaluations for the mutation guard.
|
|
356
|
+
* Compiles and runs a factory invocation under a strategy.
|
|
347
357
|
* @param {object} args - Options.
|
|
348
358
|
* @param {string} args.factoryName - Factory name.
|
|
349
359
|
* @param {string[]} args.traits - Ordered traits.
|
|
@@ -351,7 +361,21 @@ export default class FactoryRegistry {
|
|
|
351
361
|
* @param {"attributesFor" | "build" | "create"} args.strategy - Strategy name.
|
|
352
362
|
* @returns {Promise<?>} - The strategy result.
|
|
353
363
|
*/
|
|
354
|
-
async _runFactory(
|
|
364
|
+
async _runFactory(args) {
|
|
365
|
+
return (await this._runFactoryInvocation(args)).result
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Runs one event-tracked invocation, optionally reusing declaration planning.
|
|
370
|
+
* @param {object} args - Options.
|
|
371
|
+
* @param {string} args.factoryName - Factory name.
|
|
372
|
+
* @param {string[]} args.traits - Ordered traits.
|
|
373
|
+
* @param {Record<string, ?>} args.overrides - Overrides.
|
|
374
|
+
* @param {"attributesFor" | "build" | "create"} args.strategy - Strategy name.
|
|
375
|
+
* @param {import("./factory-runner.js").CompiledPlan} [args.planTemplate] - Reusable declaration plan.
|
|
376
|
+
* @returns {Promise<{result: ?, planTemplate: import("./factory-runner.js").CompiledPlan}>} - Result and declaration plan.
|
|
377
|
+
*/
|
|
378
|
+
async _runFactoryInvocation({factoryName, traits, overrides, strategy, planTemplate}) {
|
|
355
379
|
this._activeEvaluations += 1
|
|
356
380
|
|
|
357
381
|
const invocationId = this._events.nextInvocationId()
|
|
@@ -360,12 +384,13 @@ export default class FactoryRegistry {
|
|
|
360
384
|
try {
|
|
361
385
|
this._events.emit("start", {invocationId, factory: factoryName, strategy, traits})
|
|
362
386
|
|
|
363
|
-
const
|
|
364
|
-
const
|
|
387
|
+
const compiledPlanTemplate = planTemplate || this._runner.compileTemplate(factoryName, traits)
|
|
388
|
+
const compiledPlan = this._runner.applyOverrides(compiledPlanTemplate, overrides)
|
|
389
|
+
const result = await this._strategies[strategy].run({registry: this, plan: compiledPlan})
|
|
365
390
|
|
|
366
391
|
this._events.emit("success", {invocationId, factory: factoryName, strategy, traits, durationMs: Date.now() - startedAt})
|
|
367
392
|
|
|
368
|
-
return result
|
|
393
|
+
return {result, planTemplate: compiledPlanTemplate}
|
|
369
394
|
} catch (error) {
|
|
370
395
|
this._events.emit("failure", {invocationId, factory: factoryName, strategy, traits, durationMs: Date.now() - startedAt, error})
|
|
371
396
|
|
|
@@ -48,6 +48,16 @@ export default class FactoryRunner {
|
|
|
48
48
|
* @returns {CompiledPlan} - The compiled plan.
|
|
49
49
|
*/
|
|
50
50
|
compile(factoryName, requestedTraits, overrides) {
|
|
51
|
+
return this.applyOverrides(this.compileTemplate(factoryName, requestedTraits), overrides)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compiles inheritance, traits and declarations without call-site overrides.
|
|
56
|
+
* @param {string} factoryName - Factory to run.
|
|
57
|
+
* @param {string[]} requestedTraits - Traits requested at the call site, in order.
|
|
58
|
+
* @returns {CompiledPlan} - Reusable declaration plan.
|
|
59
|
+
*/
|
|
60
|
+
compileTemplate(factoryName, requestedTraits) {
|
|
51
61
|
const chain = this._resolveChain(factoryName)
|
|
52
62
|
const target = chain[chain.length - 1]
|
|
53
63
|
const modelClass = this._resolveModelClass(chain)
|
|
@@ -67,7 +77,31 @@ export default class FactoryRunner {
|
|
|
67
77
|
this._expandTrait(traitName, target, flattened, [])
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
return this._buildPlan({flattened,
|
|
80
|
+
return this._buildPlan({flattened, modelClass, target, chainNames: chain.map((definition) => definition.name)})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Applies the current call-site overrides without mutating the reusable template.
|
|
85
|
+
* @param {CompiledPlan} planTemplate - Reusable declaration plan.
|
|
86
|
+
* @param {Record<string, ?>} overrides - Current call-site overrides.
|
|
87
|
+
* @returns {CompiledPlan} - Per-invocation plan.
|
|
88
|
+
*/
|
|
89
|
+
applyOverrides(planTemplate, overrides) {
|
|
90
|
+
const overrideKeys = Object.keys(overrides)
|
|
91
|
+
|
|
92
|
+
if (overrideKeys.length === 0) return planTemplate
|
|
93
|
+
|
|
94
|
+
const resolved = new Map(planTemplate.resolved)
|
|
95
|
+
|
|
96
|
+
for (const key of overrideKeys) {
|
|
97
|
+
const prior = resolved.get(key)
|
|
98
|
+
|
|
99
|
+
resolved.set(key, {slotKind: prior ? prior.slotKind : "attribute", value: overrides[key], isOverride: true})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._arbitrateAssociationOverrides(resolved, overrides, planTemplate.modelClass)
|
|
103
|
+
|
|
104
|
+
return {...planTemplate, resolved}
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
/**
|
|
@@ -193,16 +227,15 @@ export default class FactoryRunner {
|
|
|
193
227
|
}
|
|
194
228
|
|
|
195
229
|
/**
|
|
196
|
-
* Folds flattened declarations
|
|
230
|
+
* Folds flattened declarations into a reusable compiled plan.
|
|
197
231
|
* @param {object} args - Options.
|
|
198
232
|
* @param {Array<{decl: import("./declarations.js").Declaration}>} args.flattened - Flattened declarations.
|
|
199
|
-
* @param {Record<string, ?>} args.overrides - Call-site overrides.
|
|
200
233
|
* @param {(new (attributes?: Record<string, ?>) => ?) | null} args.modelClass - Resolved model class.
|
|
201
234
|
* @param {import("./factory-definition.js").default} args.target - Target factory definition.
|
|
202
235
|
* @param {string[]} args.chainNames - Inheritance chain names.
|
|
203
236
|
* @returns {CompiledPlan} - The compiled plan.
|
|
204
237
|
*/
|
|
205
|
-
_buildPlan({flattened,
|
|
238
|
+
_buildPlan({flattened, modelClass, target, chainNames}) {
|
|
206
239
|
/** @type {Map<string, Slot>} */
|
|
207
240
|
const resolved = new Map()
|
|
208
241
|
/** @type {Map<string, import("./declarations.js").CallbackDeclaration[]>} */
|
|
@@ -238,14 +271,6 @@ export default class FactoryRunner {
|
|
|
238
271
|
}
|
|
239
272
|
}
|
|
240
273
|
|
|
241
|
-
for (const key of Object.keys(overrides)) {
|
|
242
|
-
const prior = resolved.get(key)
|
|
243
|
-
|
|
244
|
-
resolved.set(key, {slotKind: prior ? prior.slotKind : "attribute", value: overrides[key], isOverride: true})
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this._arbitrateAssociationOverrides(resolved, overrides, modelClass)
|
|
248
|
-
|
|
249
274
|
return {
|
|
250
275
|
factoryName: target.name,
|
|
251
276
|
factoryDefinition: target,
|
|
@@ -269,6 +294,7 @@ export default class FactoryRunner {
|
|
|
269
294
|
* @returns {void}
|
|
270
295
|
*/
|
|
271
296
|
_arbitrateAssociationOverrides(resolved, overrides, modelClass) {
|
|
297
|
+
if (Object.keys(overrides).length === 0) return
|
|
272
298
|
if (typeof modelClass !== "function" || !(modelClass.prototype instanceof DatabaseRecord)) return
|
|
273
299
|
|
|
274
300
|
const backendModelClass = /** @type {typeof DatabaseRecord} */ (modelClass)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs service shutdown and its completion hook while preserving both failures.
|
|
5
|
+
* @param {object} args - Lifecycle callbacks.
|
|
6
|
+
* @param {() => Promise<void>} args.shutdown - Primary service shutdown.
|
|
7
|
+
* @param {() => void | Promise<void>} [args.onStopped] - Completion hook.
|
|
8
|
+
* @returns {Promise<void>} - Resolves after shutdown and the hook finish.
|
|
9
|
+
*/
|
|
10
|
+
export default async function shutdownLifecycle({shutdown, onStopped}) {
|
|
11
|
+
let shutdownFailed = false
|
|
12
|
+
let shutdownError
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await shutdown()
|
|
16
|
+
} catch (error) {
|
|
17
|
+
shutdownFailed = true
|
|
18
|
+
shutdownError = error
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let hookFailed = false
|
|
22
|
+
let hookError
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await onStopped?.()
|
|
26
|
+
} catch (error) {
|
|
27
|
+
hookFailed = true
|
|
28
|
+
hookError = error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (shutdownFailed && hookFailed) {
|
|
32
|
+
throw new AggregateError(
|
|
33
|
+
[shutdownError, hookError],
|
|
34
|
+
"Service shutdown and onStopped hook failed",
|
|
35
|
+
{cause: shutdownError}
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hookFailed) throw hookError
|
|
40
|
+
if (shutdownFailed) throw shutdownError
|
|
41
|
+
}
|