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.
Files changed (75) hide show
  1. package/README.md +17 -3
  2. package/build/background-jobs/main.js +35 -12
  3. package/build/background-jobs/store.js +262 -16
  4. package/build/background-jobs/web/authorization.js +5 -4
  5. package/build/background-jobs/web/controller.js +13 -16
  6. package/build/background-jobs/web/counts-channel.js +79 -0
  7. package/build/background-jobs/web/index.js +2 -0
  8. package/build/background-jobs/web/registry.js +1 -1
  9. package/build/background-jobs/worker.js +53 -28
  10. package/build/database/record/index.js +12 -0
  11. package/build/frontend-models/base.js +192 -41
  12. package/build/http-client/websocket-client.js +48 -0
  13. package/build/src/background-jobs/main.d.ts +14 -1
  14. package/build/src/background-jobs/main.d.ts.map +1 -1
  15. package/build/src/background-jobs/main.js +37 -14
  16. package/build/src/background-jobs/store.d.ts +79 -0
  17. package/build/src/background-jobs/store.d.ts.map +1 -1
  18. package/build/src/background-jobs/store.js +235 -17
  19. package/build/src/background-jobs/web/authorization.d.ts +5 -3
  20. package/build/src/background-jobs/web/authorization.d.ts.map +1 -1
  21. package/build/src/background-jobs/web/authorization.js +6 -5
  22. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  23. package/build/src/background-jobs/web/controller.js +14 -15
  24. package/build/src/background-jobs/web/counts-channel.d.ts +31 -0
  25. package/build/src/background-jobs/web/counts-channel.d.ts.map +1 -0
  26. package/build/src/background-jobs/web/counts-channel.js +71 -0
  27. package/build/src/background-jobs/web/index.d.ts.map +1 -1
  28. package/build/src/background-jobs/web/index.js +3 -1
  29. package/build/src/background-jobs/web/registry.d.ts +1 -1
  30. package/build/src/background-jobs/web/registry.d.ts.map +1 -1
  31. package/build/src/background-jobs/web/registry.js +2 -2
  32. package/build/src/background-jobs/worker.d.ts +18 -1
  33. package/build/src/background-jobs/worker.d.ts.map +1 -1
  34. package/build/src/background-jobs/worker.js +55 -31
  35. package/build/src/database/record/index.d.ts.map +1 -1
  36. package/build/src/database/record/index.js +9 -1
  37. package/build/src/frontend-models/base.d.ts +13 -4
  38. package/build/src/frontend-models/base.d.ts.map +1 -1
  39. package/build/src/frontend-models/base.js +173 -44
  40. package/build/src/http-client/websocket-client.d.ts +3 -0
  41. package/build/src/http-client/websocket-client.d.ts.map +1 -1
  42. package/build/src/http-client/websocket-client.js +42 -1
  43. package/build/src/tenants/tenant.d.ts +20 -9
  44. package/build/src/tenants/tenant.d.ts.map +1 -1
  45. package/build/src/tenants/tenant.js +47 -12
  46. package/build/src/testing/factory/factory-registry.d.ts +22 -3
  47. package/build/src/testing/factory/factory-registry.d.ts.map +1 -1
  48. package/build/src/testing/factory/factory-registry.js +32 -9
  49. package/build/src/testing/factory/factory-runner.d.ts +16 -4
  50. package/build/src/testing/factory/factory-runner.d.ts.map +1 -1
  51. package/build/src/testing/factory/factory-runner.js +33 -10
  52. package/build/src/utils/shutdown-lifecycle.d.ts +12 -0
  53. package/build/src/utils/shutdown-lifecycle.d.ts.map +1 -0
  54. package/build/src/utils/shutdown-lifecycle.js +36 -0
  55. package/build/tenants/tenant.js +51 -11
  56. package/build/testing/factory/factory-registry.js +33 -8
  57. package/build/testing/factory/factory-runner.js +38 -12
  58. package/build/tsconfig.tsbuildinfo +1 -1
  59. package/build/utils/shutdown-lifecycle.js +41 -0
  60. package/package.json +3 -2
  61. package/src/background-jobs/main.js +35 -12
  62. package/src/background-jobs/store.js +262 -16
  63. package/src/background-jobs/web/authorization.js +5 -4
  64. package/src/background-jobs/web/controller.js +13 -16
  65. package/src/background-jobs/web/counts-channel.js +79 -0
  66. package/src/background-jobs/web/index.js +2 -0
  67. package/src/background-jobs/web/registry.js +1 -1
  68. package/src/background-jobs/worker.js +53 -28
  69. package/src/database/record/index.js +12 -0
  70. package/src/frontend-models/base.js +192 -41
  71. package/src/http-client/websocket-client.js +48 -0
  72. package/src/tenants/tenant.js +51 -11
  73. package/src/testing/factory/factory-registry.js +33 -8
  74. package/src/testing/factory/factory-runner.js +38 -12
  75. package/src/utils/shutdown-lifecycle.js +41 -0
@@ -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`, so entering a tenant
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 app's tenant
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: switching into a tenant makes it queryable without
25
- * the caller wiring up connections. Already-checked-out connections are reused, so nesting
26
- * `Tenant.with` calls does not open redundant connections. The callback receives the active
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(callback))
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` so each tenant's
51
- * database is queryable without the caller wiring up connections. Returns how many tenants
52
- * the callback ran for (after filtering).
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 () => await callback(callbackArgs))
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
- for (let index = 0; index < count; index++) {
338
- results.push(await this._runFactory({factoryName, traits, overrides, strategy}))
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, tracking active
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({factoryName, traits, overrides, strategy}) {
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 plan = this._runner.compile(factoryName, traits, overrides)
364
- const result = await this._strategies[strategy].run({registry: this, plan})
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, overrides, modelClass, target, chainNames: chain.map((definition) => definition.name)})
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 plus overrides into a compiled plan.
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, overrides, modelClass, target, chainNames}) {
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
+ }