|
| 1 | +--- |
| 2 | +"@objectstack/core": minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(core): both kernels agree that a duplicate plugin registration OVERWRITES, and say so out loud (#9864) |
| 6 | + |
| 7 | +Registering two plugins under the same `name` used to mean two different things |
| 8 | +depending on which kernel was running: |
| 9 | + |
| 10 | +| kernel | behaviour before | |
| 11 | +|---|---| |
| 12 | +| `ObjectKernel` (what `os serve` runs) | accepted and overwrote, with **no check and no distinguishing log line** — `Plugin registered: <name>@<version>` printed twice, reading as two plugins running | |
| 13 | +| `LiteKernel` (tests, serverless, edge) | threw `[Kernel] Plugin '<name>' already registered` | |
| 14 | + |
| 15 | +Under the maintainer's ruling (2026-08-19, option B) both kernels now apply one |
| 16 | +declared contract: **duplicate registration by `name` overwrites — last-one-wins |
| 17 | +— and emits a `warn` naming the plugin and both versions.** |
| 18 | + |
| 19 | +``` |
| 20 | +WARN Plugin superseded: 'com.objectstack.audit' — the later registration (v2.0.0) |
| 21 | + REPLACED the earlier one (v1.0.0). Only the later instance is initialized and |
| 22 | + started; the earlier one is discarded without ever running init(). Duplicate |
| 23 | + registration by name is last-one-wins on both kernels by declared contract |
| 24 | + (#9864) — register the plugin once if that is not what you meant. |
| 25 | +``` |
| 26 | + |
| 27 | +**This declares and warns about behaviour that already shipped; it does not fix a |
| 28 | +user-visible bug.** The overwrite is load-bearing today — it is exactly what lets |
| 29 | +a stack's own `plugins` entry supersede a plugin the CLI auto-registered earlier |
| 30 | +in the same boot (`AuditPlugin`, #9863) — and every boot path that worked before |
| 31 | +works the same way now. What changes is that the behaviour is declared, audible, |
| 32 | +and pinned against **both** kernels |
| 33 | +(`packages/core/src/plugin-registration.contract.test.ts`) rather than being an |
| 34 | +accident of whichever kernel a reader happened to open. This was the fourth |
| 35 | +measured instance of one contract implemented twice across the two kernels |
| 36 | +(#5170, #5282, #8357 adjacent). |
| 37 | + |
| 38 | +**What this changes for a caller** |
| 39 | + |
| 40 | +- `LiteKernel.use()` no longer throws on a duplicate name. FROM: catch |
| 41 | + `[Kernel] Plugin '<name>' already registered` to detect a double registration. |
| 42 | + TO: there is no throw to catch — a duplicate is a `warn` and the later instance |
| 43 | + wins. Code that registered a plugin twice and relied on the refusal should |
| 44 | + register it once instead. |
| 45 | +- `ObjectKernel` emits one `warn` where it previously emitted nothing, and |
| 46 | + **suppresses** its `Plugin registered:` line for the superseding registration, |
| 47 | + so the count of those lines equals the number of plugins that actually boot. |
| 48 | +- The level is part of the contract: `warn`, never `info`. The CLI's default |
| 49 | + kernel level is `warn`, and its boot-quiet window replays `warn` while |
| 50 | + discarding in-window `info` — an `info` notice would be invisible on exactly |
| 51 | + the boot path where this was measured. |
| 52 | + |
| 53 | +**Measured, not assumed:** the displaced instance holds nothing that needs |
| 54 | +teardown. Registration is legal only while the kernel is `idle`, so a supersede |
| 55 | +can only ever displace a plugin that has never been initialized; `init()`, |
| 56 | +`start()` and `destroy()` all run later, over a registry the displaced entry has |
| 57 | +already left. `PluginLoader.loadPlugin()` — which `ObjectKernel` runs first — is |
| 58 | +pure validation plus a name-keyed map write of its own, and invokes nothing on |
| 59 | +the plugin. Calling `destroy()` on the displaced instance would be the bug, not |
| 60 | +the fix: it is the paired teardown for an `init()` that never ran. |
0 commit comments