meocord 4.1.2 → 4.2.1

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 (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -1,4 +1,6 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import { storeOperationsSettled } from './cooldown-runner.js';
3
+ import { RunningCalls } from './running-calls.js';
2
4
 
3
5
  /** The entry of a unit whose instance is `instance`. */ function lifecycleEntry(unit, instance) {
4
6
  return {
@@ -11,11 +13,45 @@ import { storeOperationsSettled } from './cooldown-runner.js';
11
13
  }
12
14
  };
13
15
  }
16
+ /**
17
+ * The ready hooks' pass, which shutdown waits for before it reads which units to shut down, so a unit whose `onReady`
18
+ * finishes meanwhile is shut down too. An `onReady` still running that asks the app to stop is not waited for; a timer
19
+ * or callback an `onReady` that has settled left behind excuses nothing.
20
+ */ class ReadyPass {
21
+ /** Runs `pass`, the ready hooks, as the pass shutdown waits for. */ run(pass) {
22
+ return this.passes.run('onReady', pass);
23
+ }
24
+ /** Excuses the pass when this runs within the `onReady` in progress, as one that stops the app does. */ excuseCurrent() {
25
+ if (this.current && this.hooks.getStore() === this.current) this.passes.excuseCurrent();
26
+ }
27
+ /** Settles once the pass has, unless excused. */ settled() {
28
+ return this.passes.settled();
29
+ }
30
+ /** The unit whose `onReady` a wait would still wait for, if any. */ running() {
31
+ return this.passes.running().length > 0 ? [
32
+ this.unit ?? 'a unit'
33
+ ] : [];
34
+ }
35
+ constructor(){
36
+ this.passes = new RunningCalls();
37
+ /** The `onReady` each async context belongs to. */ this.hooks = new AsyncLocalStorage();
38
+ /** Runs a unit's `onReady` as the one in progress. */ this.runOnReady = async (unit, run)=>{
39
+ const hook = {};
40
+ this.current = hook;
41
+ this.unit = unit.name;
42
+ try {
43
+ await this.hooks.run(hook, run);
44
+ } finally{
45
+ if (this.current === hook) this.current = undefined;
46
+ }
47
+ };
48
+ }
49
+ }
14
50
  /**
15
51
  * Resolves each unit and runs its `onReady` one at a time, in the units' dependency order. A failure is
16
52
  * reported and the next unit still runs. Each unit whose `onReady` settled, or that has none, is pushed
17
53
  * to `entries` as it finishes, so a shutdown that starts meanwhile closes exactly those.
18
- */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled } = {}) {
54
+ */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
19
55
  const failed = new Set();
20
56
  // For each unit, the failed units it depends on, directly or through another dependency
21
57
  const failedUpstream = new Map();
@@ -23,6 +59,9 @@ import { storeOperationsSettled } from './cooldown-runner.js';
23
59
  unit.token,
24
60
  unit
25
61
  ]));
62
+ // The token each instance was first reached by: one instance two tokens reach, an alias or one value provided twice,
63
+ // runs its hooks once
64
+ const firstToken = new Map();
26
65
  for (const unit of units){
27
66
  if (stopped()) break;
28
67
  const upstream = new Set();
@@ -41,9 +80,17 @@ import { storeOperationsSettled } from './cooldown-runner.js';
41
80
  settled?.(unit);
42
81
  continue;
43
82
  }
83
+ const first = firstToken.get(instance);
84
+ if (first !== undefined) {
85
+ // Failed or not as the token that ran its hooks did, for what depends on this one
86
+ if (failed.has(first)) failed.add(unit.token);
87
+ settled?.(unit);
88
+ continue;
89
+ }
90
+ firstToken.set(instance, unit.token);
44
91
  const entry = lifecycleEntry(unit, instance);
45
- // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready skips
46
- // the one still starting, and those not reached yet
92
+ // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready waits for the one still
93
+ // starting, within its time, and skips those not reached yet
47
94
  if (typeof instance.onReady !== 'function') {
48
95
  entries.push(entry);
49
96
  settled?.(unit);
@@ -54,7 +101,8 @@ import { storeOperationsSettled } from './cooldown-runner.js';
54
101
  ]);
55
102
  const slow = slowAfterMs !== undefined && report.slow ? setTimeout(()=>report.slow(unit), slowAfterMs) : undefined;
56
103
  try {
57
- await instance.onReady(client, info);
104
+ const onReady = ()=>instance.onReady(client, info);
105
+ await (runOnReady ? runOnReady(unit, onReady) : onReady());
58
106
  } catch (error) {
59
107
  failed.add(unit.token);
60
108
  report.hookFailed(unit, error);
@@ -67,12 +115,14 @@ import { storeOperationsSettled } from './cooldown-runner.js';
67
115
  }
68
116
  /**
69
117
  * Runs the entries' `onShutdown` hooks one at a time, in reverse, so a unit stops before those it
70
- * depends on. A failure is reported and the next hook still runs.
71
- */ async function runShutdownHooks(entries, hookFailed) {
118
+ * depends on. A failure is reported and the next hook still runs. An instance already in `ran` is not shut down again,
119
+ * so one that two entries reach stops once.
120
+ */ async function runShutdownHooks(entries, hookFailed, ran = new Set()) {
72
121
  for (const { name, instance } of [
73
122
  ...entries
74
123
  ].reverse()){
75
- if (typeof instance.onShutdown !== 'function') continue;
124
+ if (typeof instance.onShutdown !== 'function' || ran.has(instance)) continue;
125
+ ran.add(instance);
76
126
  try {
77
127
  await instance.onShutdown();
78
128
  } catch (error) {
@@ -80,54 +130,91 @@ import { storeOperationsSettled } from './cooldown-runner.js';
80
130
  }
81
131
  }
82
132
  }
83
- /**
84
- * Shuts down the entries there are when it begins, as a bot and a testing module both do. When the cooldown store, or
85
- * anything it injects directly or through another, has an `onShutdown`, the calls under way finish first, then the
86
- * store operations they started, so the store's last writes and releases still reach it. The other hooks then run in
87
- * reverse dependency order, and the store and what it injects last. The whole sequence is waited for at most
88
- * `timeoutMs`.
89
- */ async function runShutdownSequence(container, entries, { drainCalls, timeoutMs, hookFailed, warn }) {
90
- // A unit whose onReady settles while the calls drain was still starting when shutdown began, so it is not shut down
91
- entries = [
92
- ...entries
93
- ];
94
- const byToken = new Map(entries.map((entry)=>[
95
- entry.token,
96
- entry
97
- ]));
98
- // The store and everything it reaches through what it injects
99
- const storeSide = new Set();
100
- const reach = (entry)=>{
101
- if (!entry || storeSide.has(entry)) return;
102
- storeSide.add(entry);
103
- for (const dependency of entry.dependencies)reach(byToken.get(dependency));
104
- };
105
- for (const entry of entries)if (entry.cooldownStore) reach(entry);
106
- const needsStore = [
107
- ...storeSide
108
- ].some((entry)=>typeof entry.instance.onShutdown === 'function');
109
- const sequence = (async ()=>{
110
- if (needsStore) {
111
- await drainCalls();
112
- await storeOperationsSettled(container);
113
- }
114
- // Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
115
- await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed);
116
- await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed);
117
- })();
133
+ /** The part of `timeoutMs` the drain leaves for the hooks: a quarter, at least 1 s, and never more than half. */ function hooksReserveMs(timeoutMs) {
134
+ return Math.min(Math.max(timeoutMs / 4, 1000), timeoutMs / 2);
135
+ }
136
+ /** A timer whose promise resolves with `value` after `ms`, and `cancel` to clear it. */ function after(ms, value) {
118
137
  let timer;
119
- const timedOut = new Promise((resolve)=>{
120
- timer = setTimeout(()=>resolve('timeout'), timeoutMs);
138
+ const promise = new Promise((resolve)=>{
139
+ timer = setTimeout(()=>resolve(value), ms);
121
140
  });
141
+ return {
142
+ promise,
143
+ cancel: ()=>clearTimeout(timer)
144
+ };
145
+ }
146
+ /**
147
+ * Shuts down the entries there are once an `onReady` in progress has settled, as a bot and a testing module both do.
148
+ * The calls under way finish first, on every shutdown with `drainAlways`, and otherwise when the cooldown store, or
149
+ * anything it injects directly or through another, has an `onShutdown`; then the store operations they started, so the
150
+ * store's last writes and releases still reach it. The other hooks then run in reverse dependency order, and the store
151
+ * and what it injects last.
152
+ *
153
+ * The whole sequence is waited for at most `timeoutMs`. The wait for the `onReady` and the drain stop early enough to
154
+ * leave the hooks {@link hooksReserveMs}, with a warning naming what still runs, and the hooks run in the time left.
155
+ */ async function runShutdownSequence(container, readEntries, { drainCalls, drainAlways = false, runningCalls, readyPass, timeoutMs, hookFailed, warn }) {
156
+ const whole = after(timeoutMs, 'timeout');
157
+ const reserve = hooksReserveMs(timeoutMs);
158
+ const drainEnds = after(timeoutMs - reserve, false);
159
+ // At once on a shutdown that always drains, so no call starts while an onReady finishes
160
+ let draining = drainAlways ? drainCalls().then(()=>true) : undefined;
122
161
  try {
162
+ const readying = readyPass?.running() ?? [];
163
+ if (readying.length > 0 && !await Promise.race([
164
+ readyPass.settled().then(()=>true),
165
+ drainEnds.promise
166
+ ])) {
167
+ warn(`onReady in ${readying.join(', ')} is still running after ${timeoutMs - reserve} ms; shutting down without waiting for it.`);
168
+ }
169
+ // Read once the onReady in progress has settled, so its unit is among them
170
+ const entries = [
171
+ ...readEntries()
172
+ ];
173
+ const byToken = new Map(entries.map((entry)=>[
174
+ entry.token,
175
+ entry
176
+ ]));
177
+ // The store and everything it reaches through what it injects
178
+ const storeSide = new Set();
179
+ const reach = (entry)=>{
180
+ if (!entry || storeSide.has(entry)) return;
181
+ storeSide.add(entry);
182
+ for (const dependency of entry.dependencies)reach(byToken.get(dependency));
183
+ };
184
+ for (const entry of entries)if (entry.cooldownStore) reach(entry);
185
+ const needsStore = [
186
+ ...storeSide
187
+ ].some((entry)=>typeof entry.instance.onShutdown === 'function');
188
+ let drained = true;
189
+ if (needsStore) draining ?? (draining = drainCalls().then(()=>true));
190
+ if (draining) {
191
+ drained = await Promise.race([
192
+ draining,
193
+ drainEnds.promise
194
+ ]);
195
+ if (!drained) {
196
+ const running = runningCalls?.() ?? [];
197
+ warn(`Calls still running after ${timeoutMs - reserve} ms${running.length > 0 ? `: ${running.join(', ')}` : ''}; ` + `running the onShutdown hooks in the ${reserve} ms left.`);
198
+ }
199
+ }
200
+ drainEnds.cancel();
201
+ const sequence = (async ()=>{
202
+ // Only after the calls settled: a call still running may never let its store operations settle
203
+ if (needsStore && drained) await storeOperationsSettled(container);
204
+ // Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
205
+ const ran = new Set();
206
+ await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed, ran);
207
+ await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed, ran);
208
+ })();
123
209
  if (await Promise.race([
124
210
  sequence,
125
- timedOut
211
+ whole.promise
126
212
  ]) === 'timeout') {
127
213
  warn(`onShutdown hooks did not finish within ${timeoutMs} ms; shutting down anyway.`);
128
214
  }
129
215
  } finally{
130
- clearTimeout(timer);
216
+ drainEnds.cancel();
217
+ whole.cancel();
131
218
  }
132
219
  }
133
220
  /** Settles once every promise in `calls` has, those added while waiting included. */ async function callsSettled(calls) {
@@ -136,4 +223,4 @@ import { storeOperationsSettled } from './cooldown-runner.js';
136
223
  ]);
137
224
  }
138
225
 
139
- export { callsSettled, lifecycleEntry, runReadyHooks, runShutdownHooks, runShutdownSequence };
226
+ export { ReadyPass, callsSettled, hooksReserveMs, lifecycleEntry, runReadyHooks, runShutdownHooks, runShutdownSequence };