@objectstack/plugin-reports 17.1.0 → 17.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,134 @@
1
1
  # @objectstack/plugin-reports
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an
8
+ optional `error` now declare a **non-optional** `warn`, so a durability report
9
+ always has somewhere to land (#9754, #10556).
10
+
11
+ `minor`, not `major`: during the launch window this stack ships breaking changes
12
+ as `minor` — every publishable package versions in lockstep, so a `major` would
13
+ promote the whole release. `patch` would be wrong in the other direction, because
14
+ this *can* break a consumer's build.
15
+
16
+ `error` stays optional on every one of these types — hosts legitimately inject
17
+ reduced sinks, and requiring `error` was measured and rejected as #9754 option C.
18
+ What changes is that its *absence* now has a declared, guaranteed destination.
19
+ Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the
20
+ type cannot reach, so **no runtime behaviour changes**: nothing that printed
21
+ before stops printing, and nothing silent starts printing.
22
+
23
+ ### Who has to change, and what to do
24
+
25
+ Only a caller that hands one of these sinks an object with **no `warn` method** —
26
+ for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no
27
+ rename, no removal, and no stored value or metadata key to rewrite. Every
28
+ construction site inside this repo already supplied one, so the in-repo cost was
29
+ zero; the compile error is reserved for the callers that were silently discarding
30
+ these reports.
31
+
32
+ The affected types, by package:
33
+
34
+ - `@objectstack/cloud-connection` — the internal `PluginContext['logger']`
35
+ - `@objectstack/metadata-protocol` — `IndexMigrationLogger`
36
+ - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks`
37
+ - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger`
38
+ - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal
39
+ `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']`
40
+ - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions`
41
+ - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']`
42
+ - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`,
43
+ `rule-hooks` and `record-share-cascade`
44
+ - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions`
45
+ - `@objectstack/service-knowledge` — `KnowledgeLogger`
46
+
47
+ `AuthManagerOptions['logger']` is the one most likely to be reached from outside:
48
+ `AuthManager` is public surface, its `logger` option stays optional, and a logger
49
+ that *is* supplied must now carry `warn`. The only non-test construction site in
50
+ this repo passes the kernel `Logger`, whose `warn` is already required.
51
+
52
+ `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger
53
+ field to `{}`. The field is now honestly optional rather than holding an empty
54
+ object that declared it could report and discarded everything. Behaviour is
55
+ unchanged in both directions.
56
+
57
+ <!-- adr-0087: not-required (runtime-interface-only packages/plugins/plugin-auth/src/auth-manager.ts#AuthManagerOptions, packages/plugins/plugin-auth/src/reconcile-membership.ts#ReconcileMembershipDeps, packages/metadata-protocol/src/migrations/partial-index-probe.ts#IndexMigrationLogger, packages/plugins/plugin-audit/src/auth-event-audit.ts#AuthEventAuditLogger, packages/plugins/plugin-audit/src/read-audit.ts#ReadAuditLogger, packages/plugins/plugin-reports/src/report-service.ts#ReportServiceOptions, packages/services/service-knowledge/src/knowledge-service.ts#KnowledgeLogger) every tightened type is a plain TypeScript logger interface -- no Zod projection, no metadata surface, and none is referenced by one -- so `objectstack migrate meta` has nothing to rewrite. Nothing is removed or renamed and no stored value moves; the only consumer action is adding a `warn` member at a construction site the compiler names. -->
58
+
59
+ ### Patch Changes
60
+
61
+ - 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel
62
+ actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and
63
+ `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and
64
+ `LiteKernel.destroy()`, which walk the plugins in reverse calling
65
+ `plugin.destroy()`, walked straight past every plugin whose teardown was spelled
66
+ `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still
67
+ armed, the REST/OpenAPI/Slack connectors still registered on the automation
68
+ engine, the approvals SLA escalation job still scheduled, and the knowledge
69
+ event-sync subscription still open.
70
+
71
+ Each teardown body now lives in `destroy()`. `stop()` is retained as a
72
+ delegating alias with its parameter made optional, so an embedder that learned
73
+ to call it directly — precisely because the kernel never did — keeps working
74
+ unchanged. No export is removed and the `Plugin` interface is untouched.
75
+
76
+ Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as
77
+ fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted
78
+ from the merge queue.
79
+ - Updated dependencies [8f04d9a]
80
+ - Updated dependencies [6936d07]
81
+ - Updated dependencies [59eb04d]
82
+ - Updated dependencies [9f05b7d]
83
+ - Updated dependencies [3b2af5e]
84
+ - Updated dependencies [7d2d112]
85
+ - Updated dependencies [5fa0d72]
86
+ - Updated dependencies [02b3b07]
87
+ - Updated dependencies [914c413]
88
+ - Updated dependencies [55809a0]
89
+ - Updated dependencies [ee2ff45]
90
+ - Updated dependencies [47cd3ec]
91
+ - Updated dependencies [52db1d1]
92
+ - Updated dependencies [5649efb]
93
+ - Updated dependencies [9d7d2de]
94
+ - Updated dependencies [c815c50]
95
+ - Updated dependencies [795ea05]
96
+ - Updated dependencies [2306a76]
97
+ - Updated dependencies [e5ea701]
98
+ - Updated dependencies [a40dcc1]
99
+ - Updated dependencies [def0d3e]
100
+ - Updated dependencies [8d0bb79]
101
+ - Updated dependencies [5acb58d]
102
+ - Updated dependencies [2e3cf95]
103
+ - Updated dependencies [4c93387]
104
+ - Updated dependencies [504c8d5]
105
+ - Updated dependencies [a037f7c]
106
+ - Updated dependencies [3ee8ddf]
107
+ - Updated dependencies [16cef97]
108
+ - Updated dependencies [a79bd35]
109
+ - Updated dependencies [6ceaa4b]
110
+ - Updated dependencies [15ea214]
111
+ - Updated dependencies [de19489]
112
+ - Updated dependencies [c684d00]
113
+ - Updated dependencies [923c424]
114
+ - Updated dependencies [0ab81d1]
115
+ - Updated dependencies [1ec36b7]
116
+ - Updated dependencies [5f2e54c]
117
+ - Updated dependencies [189373b]
118
+ - Updated dependencies [35ad101]
119
+ - Updated dependencies [ceb33a9]
120
+ - Updated dependencies [dccbcec]
121
+ - Updated dependencies [73d9795]
122
+ - Updated dependencies [8012960]
123
+ - Updated dependencies [266654d]
124
+ - Updated dependencies [f34f56b]
125
+ - Updated dependencies [f399618]
126
+ - Updated dependencies [75e9301]
127
+ - Updated dependencies [2810695]
128
+ - @objectstack/platform-objects@17.2.0
129
+ - @objectstack/spec@17.2.0
130
+ - @objectstack/core@17.2.0
131
+
3
132
  ## 17.1.0
4
133
 
5
134
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -76,9 +76,16 @@ interface ReportServiceOptions {
76
76
  engine: ReportEngine;
77
77
  email?: ReportEmail;
78
78
  clock?: ReportClock;
79
+ /**
80
+ * `warn` is GUARANTEED, `error` is not (#9754). Hosts do inject reduced sinks,
81
+ * so `error` stays optional — but a durability report that degrades to `warn`
82
+ * needs `warn` to actually be there, and this service degrades to it in eight
83
+ * places below. The whole `logger` stays optional; what is no longer
84
+ * representable is a sink that HAS the field and still prints nothing.
85
+ */
79
86
  logger?: {
80
87
  info?: (msg: any, ...rest: any[]) => void;
81
- warn?: (msg: any, ...rest: any[]) => void;
88
+ warn: (msg: any, ...rest: any[]) => void;
82
89
  error?: (msg: any, ...rest: any[]) => void;
83
90
  };
84
91
  /** Cap rows per report to protect both DB and email size. */
@@ -109,7 +116,22 @@ declare class ReportService implements IReportService {
109
116
  private readonly engine;
110
117
  private readonly email?;
111
118
  private readonly clock;
112
- private readonly logger;
119
+ /**
120
+ * Optional, and deliberately NOT defaulted to `{}` (#10556).
121
+ *
122
+ * `ReportServiceOptions['logger']` guarantees a `warn` channel under #9754, so `{}` stopped being a
123
+ * legal value of the type — which is the gate working: an empty object is a
124
+ * sink that declares it can report and then discards everything. The repair
125
+ * is to say what is TRUE — there may be no logger at all — rather than to
126
+ * mint a sink that lies. Runtime behaviour is unchanged in both directions:
127
+ * absent logger and `{}` both printed nothing before, and print nothing now.
128
+ *
129
+ * ⛔ What this deliberately does NOT decide: whether an absent host sink should
130
+ * instead default to a `console`-backed one. That is the open design call the
131
+ * #9754 ledger records against `plugin-security`'s `= {}` field, and it is a
132
+ * maintainer decision — not something to settle here to make a checker green.
133
+ */
134
+ private readonly logger?;
113
135
  private readonly maxRows;
114
136
  private readonly resolveOwnerContext?;
115
137
  private readonly canExportFn?;
@@ -221,10 +243,56 @@ declare class ReportsServicePlugin implements Plugin {
221
243
  private intervalHandle?;
222
244
  private jobName?;
223
245
  private jobService?;
246
+ /**
247
+ * Captured in `init()` because `destroy()` — the hook the kernel actually
248
+ * calls — takes NO `PluginContext` (`Plugin.destroy?(): Promise<void> | void`
249
+ * in core's `types.ts`). Teardown still needs somewhere to report a failed
250
+ * job cancellation, so the logger is held rather than resolved late.
251
+ */
252
+ private logger?;
224
253
  constructor(options?: ReportsPluginOptions);
225
254
  init(ctx: PluginContext): Promise<void>;
226
255
  start(ctx: PluginContext): Promise<void>;
227
- stop(ctx: PluginContext): Promise<void>;
256
+ /**
257
+ * The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
258
+ * ONLY teardown entry point `ObjectKernel.performShutdown()` and
259
+ * `LiteKernel.destroy()` invoke.
260
+ *
261
+ * [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. The body below was
262
+ * correct in every respect except its NAME: `Plugin` declares `init()`,
263
+ * `start?()` and `destroy?()` and no `stop()`, so the kernel walked straight
264
+ * past this plugin at shutdown and the dispatcher went on ticking after
265
+ * `await kernel.shutdown()` had resolved. The asymmetry is what hid it:
266
+ * `start()` IS on the interface and does fire, so a `start`/`stop` pair reads
267
+ * as symmetric in review.
268
+ *
269
+ * WHY IT STAYED INVISIBLE, AND WHERE THE BILL LANDS. `start()` `unref()`s the
270
+ * interval, so a long-lived host process still exits and nothing complains in
271
+ * production. Under vitest the worker is alive throughout teardown, so a tick
272
+ * fires after the test file is over, reads through a driver the suite already
273
+ * disconnected, and a console fallback warns; `console.*` inside a worker is
274
+ * an RPC (`onUserConsoleLog`) and one issued after `rpcDone()` snapshotted the
275
+ * pending set is rejected as `EnvironmentTeardownError`. Nothing awaits that
276
+ * promise, so it lands as an unhandled rejection and fails a run in which
277
+ * every test passed. That is the identical defect #9371 fixed in
278
+ * `MessagingServicePlugin` — measured there as two green PRs (334/334 and
279
+ * 337/337) evicted from the merge queue.
280
+ *
281
+ * The job-service branch matters as much as the timer: when `service-job` is
282
+ * installed the dispatcher is a scheduled job rather than a `setInterval`, so
283
+ * a teardown the kernel never reaches leaks whichever of the two this
284
+ * deployment happens to use.
285
+ */
286
+ destroy(): Promise<void>;
287
+ /**
288
+ * Retained alias for {@link destroy}. Kept because it is public API of an
289
+ * exported class, and removing it would break an embedder who learned to call
290
+ * it directly precisely BECAUSE the kernel never did. The parameter is now
291
+ * optional and ignored: `destroy()` takes no context, so teardown reads the
292
+ * logger captured in `init()`. Prefer kernel shutdown; direct callers keep
293
+ * working unchanged.
294
+ */
295
+ stop(_ctx?: PluginContext): Promise<void>;
228
296
  }
229
297
 
230
298
  export { type ReportClock, type ReportEmail, type ReportEngine, ReportService, type ReportServiceOptions, type ReportsPluginOptions, ReportsServicePlugin, renderReport };
package/dist/index.d.ts CHANGED
@@ -76,9 +76,16 @@ interface ReportServiceOptions {
76
76
  engine: ReportEngine;
77
77
  email?: ReportEmail;
78
78
  clock?: ReportClock;
79
+ /**
80
+ * `warn` is GUARANTEED, `error` is not (#9754). Hosts do inject reduced sinks,
81
+ * so `error` stays optional — but a durability report that degrades to `warn`
82
+ * needs `warn` to actually be there, and this service degrades to it in eight
83
+ * places below. The whole `logger` stays optional; what is no longer
84
+ * representable is a sink that HAS the field and still prints nothing.
85
+ */
79
86
  logger?: {
80
87
  info?: (msg: any, ...rest: any[]) => void;
81
- warn?: (msg: any, ...rest: any[]) => void;
88
+ warn: (msg: any, ...rest: any[]) => void;
82
89
  error?: (msg: any, ...rest: any[]) => void;
83
90
  };
84
91
  /** Cap rows per report to protect both DB and email size. */
@@ -109,7 +116,22 @@ declare class ReportService implements IReportService {
109
116
  private readonly engine;
110
117
  private readonly email?;
111
118
  private readonly clock;
112
- private readonly logger;
119
+ /**
120
+ * Optional, and deliberately NOT defaulted to `{}` (#10556).
121
+ *
122
+ * `ReportServiceOptions['logger']` guarantees a `warn` channel under #9754, so `{}` stopped being a
123
+ * legal value of the type — which is the gate working: an empty object is a
124
+ * sink that declares it can report and then discards everything. The repair
125
+ * is to say what is TRUE — there may be no logger at all — rather than to
126
+ * mint a sink that lies. Runtime behaviour is unchanged in both directions:
127
+ * absent logger and `{}` both printed nothing before, and print nothing now.
128
+ *
129
+ * ⛔ What this deliberately does NOT decide: whether an absent host sink should
130
+ * instead default to a `console`-backed one. That is the open design call the
131
+ * #9754 ledger records against `plugin-security`'s `= {}` field, and it is a
132
+ * maintainer decision — not something to settle here to make a checker green.
133
+ */
134
+ private readonly logger?;
113
135
  private readonly maxRows;
114
136
  private readonly resolveOwnerContext?;
115
137
  private readonly canExportFn?;
@@ -221,10 +243,56 @@ declare class ReportsServicePlugin implements Plugin {
221
243
  private intervalHandle?;
222
244
  private jobName?;
223
245
  private jobService?;
246
+ /**
247
+ * Captured in `init()` because `destroy()` — the hook the kernel actually
248
+ * calls — takes NO `PluginContext` (`Plugin.destroy?(): Promise<void> | void`
249
+ * in core's `types.ts`). Teardown still needs somewhere to report a failed
250
+ * job cancellation, so the logger is held rather than resolved late.
251
+ */
252
+ private logger?;
224
253
  constructor(options?: ReportsPluginOptions);
225
254
  init(ctx: PluginContext): Promise<void>;
226
255
  start(ctx: PluginContext): Promise<void>;
227
- stop(ctx: PluginContext): Promise<void>;
256
+ /**
257
+ * The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
258
+ * ONLY teardown entry point `ObjectKernel.performShutdown()` and
259
+ * `LiteKernel.destroy()` invoke.
260
+ *
261
+ * [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. The body below was
262
+ * correct in every respect except its NAME: `Plugin` declares `init()`,
263
+ * `start?()` and `destroy?()` and no `stop()`, so the kernel walked straight
264
+ * past this plugin at shutdown and the dispatcher went on ticking after
265
+ * `await kernel.shutdown()` had resolved. The asymmetry is what hid it:
266
+ * `start()` IS on the interface and does fire, so a `start`/`stop` pair reads
267
+ * as symmetric in review.
268
+ *
269
+ * WHY IT STAYED INVISIBLE, AND WHERE THE BILL LANDS. `start()` `unref()`s the
270
+ * interval, so a long-lived host process still exits and nothing complains in
271
+ * production. Under vitest the worker is alive throughout teardown, so a tick
272
+ * fires after the test file is over, reads through a driver the suite already
273
+ * disconnected, and a console fallback warns; `console.*` inside a worker is
274
+ * an RPC (`onUserConsoleLog`) and one issued after `rpcDone()` snapshotted the
275
+ * pending set is rejected as `EnvironmentTeardownError`. Nothing awaits that
276
+ * promise, so it lands as an unhandled rejection and fails a run in which
277
+ * every test passed. That is the identical defect #9371 fixed in
278
+ * `MessagingServicePlugin` — measured there as two green PRs (334/334 and
279
+ * 337/337) evicted from the merge queue.
280
+ *
281
+ * The job-service branch matters as much as the timer: when `service-job` is
282
+ * installed the dispatcher is a scheduled job rather than a `setInterval`, so
283
+ * a teardown the kernel never reaches leaks whichever of the two this
284
+ * deployment happens to use.
285
+ */
286
+ destroy(): Promise<void>;
287
+ /**
288
+ * Retained alias for {@link destroy}. Kept because it is public API of an
289
+ * exported class, and removing it would break an embedder who learned to call
290
+ * it directly precisely BECAUSE the kernel never did. The parameter is now
291
+ * optional and ignored: `destroy()` takes no context, so teardown reads the
292
+ * logger captured in `init()`. Prefer kernel shutdown; direct callers keep
293
+ * working unchanged.
294
+ */
295
+ stop(_ctx?: PluginContext): Promise<void>;
228
296
  }
229
297
 
230
298
  export { type ReportClock, type ReportEmail, type ReportEngine, ReportService, type ReportServiceOptions, type ReportsPluginOptions, ReportsServicePlugin, renderReport };
package/dist/index.js CHANGED
@@ -154,7 +154,7 @@ var ReportService = class {
154
154
  this.engine = opts.engine;
155
155
  this.email = opts.email;
156
156
  this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
157
- this.logger = opts.logger ?? {};
157
+ this.logger = opts.logger;
158
158
  this.maxRows = Math.max(1, opts.maxRows ?? 5e3);
159
159
  this.resolveOwnerContext = opts.resolveOwnerContext;
160
160
  this.canExportFn = opts.canExport;
@@ -186,7 +186,7 @@ var ReportService = class {
186
186
  try {
187
187
  allowed = await this.canExportFn(object, context);
188
188
  } catch (err) {
189
- this.logger.warn?.("ReportService: canExport check failed \u2014 denying export", err);
189
+ this.logger?.warn?.("ReportService: canExport check failed \u2014 denying export", err);
190
190
  allowed = false;
191
191
  }
192
192
  if (!allowed) {
@@ -382,7 +382,7 @@ var ReportService = class {
382
382
  updated_at: ranAt
383
383
  }, { context: SYSTEM_CTX });
384
384
  } catch (err) {
385
- this.logger.warn?.("ReportService: failed to stamp last_run_at", err);
385
+ this.logger?.warn?.("ReportService: failed to stamp last_run_at", err);
386
386
  }
387
387
  }
388
388
  return {
@@ -485,7 +485,7 @@ var ReportService = class {
485
485
  const report = rowFromSaved(row);
486
486
  const ownerId = report.owner_id;
487
487
  const runContext = ownerId && this.resolveOwnerContext ? await this.resolveOwnerContext(ownerId).catch((err) => {
488
- this.logger.warn?.("ReportService.dispatchDue: owner context resolution failed", err);
488
+ this.logger?.warn?.("ReportService.dispatchDue: owner context resolution failed", err);
489
489
  return null;
490
490
  }) : null;
491
491
  if (!runContext) {
@@ -531,7 +531,7 @@ var ReportService = class {
531
531
  });
532
532
  }
533
533
  } else if (!this.email) {
534
- this.logger.warn?.("ReportService.dispatchDue: no email service \u2014 schedule fired but mail not sent");
534
+ this.logger?.warn?.("ReportService.dispatchDue: no email service \u2014 schedule fired but mail not sent");
535
535
  }
536
536
  await this.advanceSchedule(schedule, ts);
537
537
  fired++;
@@ -541,7 +541,7 @@ var ReportService = class {
541
541
  last_status: "failed",
542
542
  last_error: String(err?.message ?? err ?? "unknown").slice(0, 500)
543
543
  });
544
- this.logger.error?.("ReportService.dispatchDue: schedule failed", err);
544
+ this.logger?.error?.("ReportService.dispatchDue: schedule failed", err);
545
545
  }
546
546
  }
547
547
  return { fired, failed, skipped };
@@ -562,9 +562,9 @@ var ReportService = class {
562
562
  try {
563
563
  const next = new import_croner.Cron(cron, { timezone: schedule.timezone || "UTC" }).nextRun(from);
564
564
  if (next) return next;
565
- this.logger.warn?.(`ReportService: cron '${cron}' has no next occurrence; falling back to interval`);
565
+ this.logger?.warn?.(`ReportService: cron '${cron}' has no next occurrence; falling back to interval`);
566
566
  } catch (err) {
567
- this.logger.warn?.(`ReportService: invalid cron '${cron}'; falling back to interval`, err);
567
+ this.logger?.warn?.(`ReportService: invalid cron '${cron}'; falling back to interval`, err);
568
568
  }
569
569
  }
570
570
  const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;
@@ -589,7 +589,7 @@ var ReportService = class {
589
589
  updated_at: this.clock.now().toISOString()
590
590
  }, { context: SYSTEM_CTX });
591
591
  } catch (err) {
592
- this.logger.warn?.("ReportService: failed to mark schedule", err);
592
+ this.logger?.warn?.("ReportService: failed to mark schedule", err);
593
593
  }
594
594
  }
595
595
  };
@@ -605,6 +605,7 @@ var ReportsServicePlugin = class {
605
605
  this.options = options;
606
606
  }
607
607
  async init(ctx) {
608
+ this.logger = ctx.logger;
608
609
  ctx.getService("manifest").register({
609
610
  id: "com.objectstack.service.reports",
610
611
  name: "Reports Service",
@@ -696,16 +697,59 @@ var ReportsServicePlugin = class {
696
697
  ctx.logger.info("ReportsServicePlugin: dispatcher registered (setInterval fallback)", { intervalMs });
697
698
  });
698
699
  }
699
- async stop(ctx) {
700
+ /**
701
+ * The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
702
+ * ONLY teardown entry point `ObjectKernel.performShutdown()` and
703
+ * `LiteKernel.destroy()` invoke.
704
+ *
705
+ * [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. The body below was
706
+ * correct in every respect except its NAME: `Plugin` declares `init()`,
707
+ * `start?()` and `destroy?()` and no `stop()`, so the kernel walked straight
708
+ * past this plugin at shutdown and the dispatcher went on ticking after
709
+ * `await kernel.shutdown()` had resolved. The asymmetry is what hid it:
710
+ * `start()` IS on the interface and does fire, so a `start`/`stop` pair reads
711
+ * as symmetric in review.
712
+ *
713
+ * WHY IT STAYED INVISIBLE, AND WHERE THE BILL LANDS. `start()` `unref()`s the
714
+ * interval, so a long-lived host process still exits and nothing complains in
715
+ * production. Under vitest the worker is alive throughout teardown, so a tick
716
+ * fires after the test file is over, reads through a driver the suite already
717
+ * disconnected, and a console fallback warns; `console.*` inside a worker is
718
+ * an RPC (`onUserConsoleLog`) and one issued after `rpcDone()` snapshotted the
719
+ * pending set is rejected as `EnvironmentTeardownError`. Nothing awaits that
720
+ * promise, so it lands as an unhandled rejection and fails a run in which
721
+ * every test passed. That is the identical defect #9371 fixed in
722
+ * `MessagingServicePlugin` — measured there as two green PRs (334/334 and
723
+ * 337/337) evicted from the merge queue.
724
+ *
725
+ * The job-service branch matters as much as the timer: when `service-job` is
726
+ * installed the dispatcher is a scheduled job rather than a `setInterval`, so
727
+ * a teardown the kernel never reaches leaks whichever of the two this
728
+ * deployment happens to use.
729
+ */
730
+ async destroy() {
700
731
  if (this.intervalHandle) clearInterval(this.intervalHandle);
701
732
  this.intervalHandle = void 0;
702
733
  if (this.jobService && this.jobName && typeof this.jobService.cancel === "function") {
703
734
  try {
704
735
  await this.jobService.cancel(this.jobName);
705
736
  } catch (err) {
706
- ctx.logger.warn("ReportsServicePlugin: failed to cancel job", err);
737
+ this.logger?.warn("ReportsServicePlugin: failed to cancel job", err);
707
738
  }
708
739
  }
740
+ this.jobService = void 0;
741
+ this.jobName = void 0;
742
+ }
743
+ /**
744
+ * Retained alias for {@link destroy}. Kept because it is public API of an
745
+ * exported class, and removing it would break an embedder who learned to call
746
+ * it directly precisely BECAUSE the kernel never did. The parameter is now
747
+ * optional and ignored: `destroy()` takes no context, so teardown reads the
748
+ * logger captured in `init()`. Prefer kernel shutdown; direct callers keep
749
+ * working unchanged.
750
+ */
751
+ async stop(_ctx) {
752
+ await this.destroy();
709
753
  }
710
754
  };
711
755
  // Annotate the CommonJS export names for ESM import in node: