@@ -20720,19 +20720,22 @@ export class ObjectStackProtocolImplementation implements
2072020720 * so each object's table is torn down once. Per-item failures are collected
2072120721 * without aborting the rest.
2072220722 *
20723- * [#21276] The steps, in order. Nothing durable happens before step 3 , so
20724- * a refusal at any of steps 1–3 leaves everything as it was:
20723+ * [#21276] The steps, in order. Nothing durable happens before step 4 , so
20724+ * a refusal at any of steps 1–4 leaves everything as it was:
2072520725 * 1. the tenant-scope refusals (`TENANT_SCOPE_REQUIRED`) — pure;
2072620726 * 2. the `sys_metadata` read — a read; a failure is thrown;
20727- * 3. the `sys_packages` delete through the `package` service — the FIRST
20727+ * 3. the registry's uninstall refusal (another package extends an object
20728+ * this one owns, ADR-0029), asked through
20729+ * `SchemaRegistry.assertPackageUninstallable` — pure; thrown as is;
20730+ * 4. the `sys_packages` delete through the `package` service — the FIRST
2072820731 * durable step; a refusal, returned or thrown, is thrown as this verb's
2072920732 * failure (see {@link packagePersistFailureError});
20730- * 4 . the per-item `sys_metadata` deletes and table teardown — each refusal
20733+ * 5 . the per-item `sys_metadata` deletes and table teardown — each refusal
2073120734 * is collected in `failed[]`;
20732- * 5 . the registry withdrawal — a refusal (another package extends an
20733- * object this one owns, ADR-0029) is logged, and the package leaves at
20734- * the next restart, since its stored row is already gone;
20735- * 6 . the uninstall cleanups — each refusal is reported in `cleanups[]`.
20735+ * 6 . the registry withdrawal — step 3 already asked its refusal; anything
20736+ * it still throws is logged, and the package leaves at the next
20737+ * restart, since its stored row is already gone;
20738+ * 7 . the uninstall cleanups — each refusal is reported in `cleanups[]`.
2073620739 */
2073720740 async deletePackage(request: DeletePackageRequest): Promise<DeletePackageResponse> {
2073820741 // [#7780] A cross-tenant uninstall must be DECLARED, never inferred from
@@ -20882,6 +20885,26 @@ export class ObjectStackProtocolImplementation implements
2088220885 throw metadataReadFailureError(e);
2088320886 }
2088420887
20888+ // [#21276] THE REGISTRY'S UNINSTALL REFUSAL, ASKED BEFORE THE STORE
20889+ // DELETE. `SchemaRegistry` refuses an uninstall when another package
20890+ // `extend`s an object this one owns (ADR-0029), and it decides that
20891+ // before it mutates (#7970). Here that refusal used to be met only at
20892+ // the registry withdrawal below, after the stored row, the metadata rows
20893+ // and the tables were already gone. `assertPackageUninstallable` asks
20894+ // the same predicate (the one copy `unregisterObjectsByPackage` itself
20895+ // calls) without performing the uninstall, so the refusal is thrown
20896+ // here, as is, with nothing removed. The HTTP door answers it through
20897+ // its `catch` around this verb: `500`, nothing changed.
20898+ //
20899+ // The registry is reached the way the withdrawal below reaches it. A
20900+ // registry that does not carry the method (an engine double, a host on
20901+ // a registry without it) is not asked, and this verb behaves as it did
20902+ // before the method existed: the refusal surfaces at the withdrawal.
20903+ const packageRegistry = (this.engine as any)?.registry;
20904+ if (typeof packageRegistry?.assertPackageUninstallable === 'function') {
20905+ packageRegistry.assertPackageUninstallable(request.packageId);
20906+ }
20907+
2088520908 // [#21276] THE STORE DELETE COMES FIRST, and its refusal is this verb's
2088620909 // refusal. Triage's ruling: refuse before withdrawing, not undo. Every
2088720910 // step above this one only reads; every step below it — the per-item
@@ -20991,18 +21014,16 @@ export class ObjectStackProtocolImplementation implements
2099121014
2099221015 // [#2747] Unregister from the in-memory SchemaRegistry too, so the
2099321016 // running kernel stops serving the package without waiting for a
20994- // restart. Best-effort: the HTTP dispatcher already unregisters
20995- // before calling us (second call is a no-op warn), and a package
20996- // with live extenders refuses unregistration — that failure is
20997- // logged, not fatal (the durable row is gone, so the next boot is
20998- // clean either way).
20999- //
21000- // [#21276] This refusal can still come AFTER the store delete above,
21001- // and it stays here. `SchemaRegistry` has no verb that answers "would
21002- // this uninstall be refused?" without performing it; a copy of its
21003- // extender predicate here would be a second place that must agree with
21004- // the first; and performing the uninstall first would withdraw the
21005- // package before the store decides.
21017+ // restart. [#21276] The HTTP door no longer unregisters before calling
21018+ // this verb; it withdraws only after this verb has answered, and skips
21019+ // that when this step already did it.
21020+ //
21021+ // [#21276] The registry's own refusal (ADR-0029 extenders) was asked
21022+ // before the store delete, through `assertPackageUninstallable`, so it
21023+ // does not arrive here. The `catch` stays as a safety net for a
21024+ // registry that lacks that method, or a throw nothing asked ahead of
21025+ // time: it is logged, not fatal, because the durable row is already
21026+ // gone and the next boot is clean either way.
2100621027 try {
2100721028 (this.engine as any)?.registry?.uninstallPackage?.(request.packageId);
2100821029 } catch (e) {
0 commit comments