Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 1071393

Browse files
fix(console,i18n): install answers say what the runtime served (objectui#12098) (#12129)
Fixes #12098 Clause-②: yes ## What changes Two readers of an install answer now say what the runtime served. - **An install-local answered `hotLoaded: false`.** The runtime installed the package, but its running kernel could not register it. This is the lenient path for a cloud-fetched manifest (objectstack-ai/objectstack#22695). The package loads at the runtime's next restart. - The package page (`doInstallLocal`) used to show `marketplace.install.localSuccess` ("should now appear in the app switcher") and refresh the app list. It now shows the new `marketplace.install.localLoadsAtRestart`: installed, loads when the runtime next restarts, not in the app switcher until then. It does not run `refreshMetadata('app')` or `emitMetadataRefresh()`. - `hotLoaded: true` keeps today's wording and refresh. - **An upgrade waits for the new version's app.** `waitForServedApp` takes a `version` option. Its type is `string | undefined` and it is required, so every caller has to decide what to pass. An app that wears `_packageVersion` counts only at that version. An app that wears none comes from a runtime that predates objectstack-ai/objectstack#22689, and it counts on its `_packageId` alone, as today. - The package page passes the version the install answer names (`installation.version`). An answer that names none waits on the id alone. "Check again" waits for the same version. - The catalog's org strip passes its answer's version the same way, so it inherits the predicate as triage asked (`6102113934`). One new locale key, `marketplace.install.localLoadsAtRestart`, is in all ten packs with a real translation. No spec type is imported. ## Premises, measured on objectui `main` `29b99490` and objectstack `main` `e84aeb36` (read through `gh api` contents) - **H1 holds.** `doInstallLocal` showed `localSuccess` on any `200`, then ran `refreshMetadata('app')` and `emitMetadataRefresh()`. It never read `hotLoaded`. `LocalInstallResult` already types `hotLoaded: boolean`, so `marketplaceApi.ts` is unchanged. - **H2 holds.** `isAppOf` matched `_packageId` alone. `_packageVersion` is declared beside `_packageId` in `packages/spec/src/kernel/metadata-protection.zod.ts` ("Owning package version."). objectstack PR #22724 (`b62260cb`) stamps it on every app the registry path registers, at the top level of each item that `GET /meta/app` serves. That is the list the wait already polls. - **H3: the page holds three versions at that moment.** They are `installResp.installation.version` (the answer), `data.package.latest_version.version` (the catalog), and `data.versions[0].version`. The wait uses the answer's version only. A guessed catalog version that was not the one installed would turn a good install into a five-minute timeout. With no version, the worst case is today's early refresh. - **H4 holds.** install-local has no rebuild window (the report on objectui#12087), and on `false` nothing of the package is served before the restart. So no wait runs: the page sets the message and returns. - **H5 holds.** The ten packs are `packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts`. - **H6 holds.** `marketplace-install-local-plugin.ts` answers `hotLoaded: hotLoadError === undefined` and adds `hotLoadError` only beside `false`. The field and its type are unchanged. ## Where it lands, and one addition beyond the claim's wording - `waitForServedApp.ts`: the predicate and its docblock. - `MarketplacePackagePage.tsx`: the `hotLoaded` reader, and the version carried into the wait and "Check again". - `MarketplacePage.tsx`: the catalog's `waitForServedApp` call passes the answer's version. The claim names this call. - **Beyond the claim's wording: the catalog's install-local branch now reads `hotLoaded` too.** - `git grep "installLocal("` outside tests finds two readers of the install-local answer: the package page and this branch. - This branch installs an org package by id with `versionId: 'latest'`. That is a cloud-fetched manifest, the lenient path's own case. - It said "Installed NAME", and its comment claimed "the live registry already holds it", before it sent the refresh pulse. - It now shows the same restart key and sends no pulse on `false`. The file is on the claim's surface. The fix is the same defect, the same key and the same shape, and adds no new gate family. - The census pin `de-quote-pairing-3876.test.ts` moves from 76 to 77: the new `de` value quotes the name („NAME“), one matched pair. - No docs page describes these messages. `content/docs` and `packages/app-shell/README.md` were searched for the install wording, `install-local`, `hotLoaded` and `_packageId`, and the only hits were unrelated. No doc edit. ## Tests - `waitForServedApp-12098.test.ts` (unit, virtual clock): - an upgrade from 1.0.0 to 1.1.0 keeps reading while 1.0.0 is served and settles on the 1.1.0 app: 4 reads, 3 sleeps; - 1.0.0 served for the whole bound gives `served: false`; - CONTROL: an app with no `_packageVersion` is accepted on the id at the first read. - `MarketplacePackagePage.servedTruth-12098.test.tsx` (the real `MetadataProvider`, the real bus, and the real `waitForServedApp` on a virtual clock): - **Acceptance 1:** a `200` with `hotLoaded: false` shows `localLoadsAtRestart`, not `localSuccess`. After the install there are 0 `GET /meta/app` reads, 0 pulses, 0 `app` seed writes and no wait. CONTROL: `hotLoaded: true` keeps `localSuccess`, reads the list and sends one pulse. - **Acceptance 2:** an update from 1.0.0 to 1.1.0 waits with `version: '1.1.0'`. While 1.0.0 is served there is no pulse, no seed write and the status stays "Deploying". At the landing interval there is one pulse, and every seed write after the install carries the 1.1.0 app. CONTROL: an app with no `_packageVersion` is accepted on the first read with no sleep. - `MarketplacePage.orgInstallWaitServed-12097.test.tsx`: - its waits now record the version (`'1.0.0'`); - its install-local fixture gains the real wire's `hotLoaded: true`. It lacked the field, which the declared type requires; - a new case pins the catalog's `hotLoaded: false` branch: the restart wording, no pulse, no seed write and no wait. - `waitForServedApp-12087.test.ts`: its six calls pass `version: undefined`, the id-only reading those cases pin. Runs, all from the repo root under the shared verify lock: - `vitest run packages/app-shell/src/console/marketplace/`: 29 files and 183 tests passed, at `b1db713`. The only later commit, `35e1bb0`, touches only `packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts`. - `vitest run packages/i18n/`: 82 files passed, 1316 tests passed and 13 skipped, at `35e1bb0`. - `pnpm --filter @object-ui/app-shell type-check` (it echoes `tsc --noEmit && tsc -p tsconfig.test.json`): exit 0 at `b1db713`, after the dependency closure was built. - `pnpm --filter @object-ui/i18n type-check`: exit 0 at `35e1bb0`. ## Reverse verification Run at `e69d49b`. Each leg went through `scripts/ablation-replace.mjs`: the anchor must hit, the mutation is proven on disk by count and blob, and the restore is proven as blob equals HEAD with `git diff HEAD` empty. The tests resolve the sources by relative import, so no `dist/` sits in the path. - **A:** the version clause in `isAppOf` set to `return true;`. Predicted 3 red, observed 3 red: the two unit upgrade cases and the page's upgrade case. Both CONTROLs stayed green. - **B:** the package page's `if (!result.hotLoaded)` set to `if (false)`. 1 red: the `hotLoaded: false` case. - **C:** the catalog's pulse guard set to `if (true)`. 1 red: the catalog `hotLoaded: false` case. - The first attempt was refused by the tool, because its replacement was a substring of the anchor. It was re-run with a distinct replacement. - **D:** the catalog's restart wording replaced by `marketplace.org.installed`. 1 red: the same case. ## Gates Gates run at `35e1bb0`, the final head, each exit 0 unless stated: - `pnpm check:i18n-keys`, `check:i18n-drift` (1 key added, 0 en values changed), `check:i18n-dead-keys`, `check:i18n-designer-parity`. - `node scripts/check-changeset-presence.mjs`, `check:changeset-claims`, `check:pending-changeset-literals`. - `check:new-line-citations` (0 new), `check:control-bytes`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:phantom-deps`. - `check:self-import`, `check:esm-specifiers`, `check:unreferenced-sources`, `check:spec-symbols`, `check:side-effects-array`, `check:unused-deps`, `check:doc-example-readers`. - Ratchets: `column-identity.ratchet.test.ts` and `one-authority-per-exported-name-6273.test.ts` (2 files, 18 tests); the root suite `vitest run scripts/__tests__/` (179 files passed, 2 skipped; 5452 tests passed). - **Eager closure,** after a head `apps/console` build at `35e1bb0`: - `check:eager-closure` passed at 3171.7 KB gzipped against a 3204.6 KB budget; - `check:eager-locale-catalogues` passed: `en` is the only resident catalogue; - the new and changed code (`waitForServedApp`, `MarketplacePackagePage`, `MarketplacePage`) sits in lazy chunks, none of them among the 290 eager files; - the eager delta is the `en` entry alone: 162 raw bytes, 37 gzip bytes. This was measured on the head chunk with and without the entry. **No base build was made**, so it is not a base-to-head bundle diff. - **eslint, narrowed:** `eslint --no-inline-config --format json` over the branch's 18 changed `.ts`/`.tsx` files gives 0 errors. The 13 warnings are the base counts unchanged: 11 in `MarketplacePackagePage.tsx` and 2 in `MarketplacePage.tsx`, measured on the base blobs. `eslint.config.js` sets no `parserOptions.project` or `projectService`, so this diff cannot move a verdict on an untouched file. Repo-wide `pnpm lint` is left to CI. ## Acceptance notes - `hotLoadError`, which is served beside `false`, is not shown. The card asks for the restart wording only. No carrier. - The wait assumes that the control plane's `installation.version` is the manifest version the runtime stamps as `_packageVersion`. The cloud repo is not readable from this seat. If the two ever differed, the wait would end in the timeout state with "Check again", not in a refresh of a wrong list. - On a `hotLoaded: false` local result, the suggested-bindings panel still mounts, as before. Whether the runtime records suggestions for a package its kernel did not register was not measured. Session: `https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD` --- _Generated by [Claude Code](https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 802f0bc commit 1071393

19 files changed

Lines changed: 565 additions & 53 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': minor
4+
---
5+
6+
The Marketplace's install answers now say what the runtime served (objectui#12098).
7+
8+
- **An install-local answered `hotLoaded: false`.** The runtime installed the package, but its running kernel could not register it (objectstack#22695, the lenient path for a cloud-fetched manifest). The package loads at the runtime's next restart. The package page used to say the app "should now appear in the app switcher" and refresh the app list. The catalog's "Your organization" strip said "Installed {name}" and sent the metadata-refresh signal. Both now say: installed, loads when the runtime next restarts, not in the app switcher until then. Neither refreshes the app list. `hotLoaded: true` is unchanged.
9+
- **An upgrade waits for the new version's app.** After a cloud install into this environment, the console waits until the runtime serves the package's app (objectui#12087). On an upgrade, the stale kernel still serves the OLD version's app under the same `_packageId`, so the wait ended on its first read and the console kept the pre-upgrade list. The runtime now stamps each app it registers with `_packageVersion` (objectstack#22689). The wait now also matches the version that the install answer names (`installation.version`). An app served without `_packageVersion` (a runtime from before that change) is accepted on its id, as before. An answer that names no version waits on the id alone. The catalog strip's wait matches the version the same way.
10+
11+
**Locale key.** `marketplace.install.localLoadsAtRestart`, in all ten packs. `TranslationKeys` gains it.
12+
13+
**Clause-②: yes (widening)**: one locale key added to `@object-ui/i18n`'s published packs, which widens `TranslationKeys`. No export, prop or schema key is added, and no spec type is imported.

‎packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx‎

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,13 @@ function installLandsInThisEnvironment(selectedEnv: string): boolean {
9393
* objectui#12087 — after an install into THIS environment, the page waits for
9494
* the runtime to serve the package's app before it refreshes the metadata
9595
* cache. `served` carries the first app of the package a user can open, or
96-
* `null` when every one is inactive or hidden.
96+
* `null` when every one is inactive or hidden. `version` is the one the install
97+
* answer names, so "Check again" waits for the same version (objectui#12098).
9798
*/
9899
type DeployState =
99-
| { phase: 'waiting'; manifestId: string }
100-
| { phase: 'served'; manifestId: string; app: ServedApp | null }
101-
| { phase: 'timeout'; manifestId: string };
100+
| { phase: 'waiting'; manifestId: string; version: string | undefined }
101+
| { phase: 'served'; manifestId: string; version: string | undefined; app: ServedApp | null }
102+
| { phase: 'timeout'; manifestId: string; version: string | undefined };
102103

103104
export function MarketplacePackagePage() {
104105
const navigate = useNavigate();
@@ -352,11 +353,16 @@ export function MarketplacePackagePage() {
352353
* refresh, the seed drop and the bus pulse all wait for it. On expiry nothing
353354
* is refreshed, and the page says the app has not appeared rather than that
354355
* the install succeeded.
356+
*
357+
* objectui#12098 — on an upgrade the stale kernel still serves the OLD
358+
* version's app under the same `_packageId`, so the wait also names the
359+
* version the install answered with (see `waitForServedApp`).
355360
*/
356-
const awaitServedInstall = async (manifestId: string) => {
357-
setDeploy({ phase: 'waiting', manifestId });
361+
const awaitServedInstall = async (manifestId: string, version: string | undefined) => {
362+
setDeploy({ phase: 'waiting', manifestId, version });
358363
const wait = await waitForServedApp({
359364
manifestId,
365+
version,
360366
// The SDK's declared list answer (`GetMetaItemsResponse`: `{ type,
361367
// items }`). Not `MetadataProvider`'s `extractItems`: a value import of
362368
// that module from this lazy page made rolldown park the provider in
@@ -367,7 +373,7 @@ export function MarketplacePackagePage() {
367373
},
368374
});
369375
if (!wait.served) {
370-
setDeploy({ phase: 'timeout', manifestId });
376+
setDeploy({ phase: 'timeout', manifestId, version });
371377
return;
372378
}
373379
// Invalidate the metadata cache so the newly-installed app's
@@ -391,7 +397,7 @@ export function MarketplacePackagePage() {
391397
// back to pick up the new metadata.
392398
}
393399
emitMetadataRefresh();
394-
setDeploy({ phase: 'served', manifestId, app: filterActiveApps(wait.apps)[0] ?? null });
400+
setDeploy({ phase: 'served', manifestId, version, app: filterActiveApps(wait.apps)[0] ?? null });
395401
};
396402

397403
const doInstall = async () => {
@@ -424,8 +430,10 @@ export function MarketplacePackagePage() {
424430
// serves the PRE-install kernel, so nothing is refreshed yet: the wait
425431
// below refreshes once the package's app is served. Not awaited, and
426432
// not cancelled on unmount: an operator who leaves this page while the
427-
// app deploys still gets the refreshed cache when it lands.
428-
void awaitServedInstall(data.package.manifest_id);
433+
// app deploys still gets the refreshed cache when it lands. The version
434+
// is the answer's own (objectui#12098), never the catalog's latest: an
435+
// answer that names none waits on the id alone, as before.
436+
void awaitServedInstall(data.package.manifest_id, installResp?.installation?.version);
429437
} else {
430438
// Another environment: nothing this console renders changed, so there
431439
// is nothing here to wait for or to refresh.
@@ -450,17 +458,24 @@ export function MarketplacePackagePage() {
450458
setLocalResult(null);
451459
try {
452460
const result = await installLocal({ packageId });
461+
const name = data?.package ? localizePackage(data.package as any, language).displayName : result.manifestId;
462+
// objectui#12098 — `hotLoaded` says whether the running kernel registered
463+
// the package (objectstack#22695). `false` is the runtime's lenient path:
464+
// installed and in its ledger, loaded at its next restart. Nothing of it
465+
// is served until then, so the app list is not refreshed as if it were,
466+
// and the page says when the app will appear.
467+
if (!result.hotLoaded) {
468+
setLocalResult({ ok: true, message: t('marketplace.install.localLoadsAtRestart', { version: result.version, name }) });
469+
return;
470+
}
453471
try {
454472
await refreshMetadata('app');
455473
} catch {
456474
// Non-fatal: the install succeeded; the app list will refresh on next navigation.
457475
}
458476
setLocalResult({
459477
ok: true,
460-
message: t('marketplace.install.localSuccess', {
461-
version: result.version,
462-
name: data?.package ? localizePackage(data.package as any, language).displayName : result.manifestId,
463-
}),
478+
message: t('marketplace.install.localSuccess', { version: result.version, name }),
464479
});
465480
emitMetadataRefresh();
466481
} catch (e: any) {
@@ -780,7 +795,7 @@ export function MarketplacePackagePage() {
780795
</Button>
781796
) : null}
782797
{deploy.phase === 'timeout' ? (
783-
<Button size="sm" variant="outline" onClick={() => void awaitServedInstall(deploy.manifestId)}>
798+
<Button size="sm" variant="outline" onClick={() => void awaitServedInstall(deploy.manifestId, deploy.version)}>
784799
{t('marketplace.install.checkAgain')}
785800
</Button>
786801
) : null}

‎packages/app-shell/src/console/marketplace/MarketplacePage.tsx‎

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,13 @@ function toLoadFailure(e: unknown): LoadFailure {
117117
* (objectui#12087), in the same words. The strip lists several packages and
118118
* this status sits above them all, so it names its package (`name`, the row's
119119
* display name). `served` carries the first app of the package a user can open,
120-
* or `null` when every one is inactive or hidden.
120+
* or `null` when every one is inactive or hidden. `version` is the one the
121+
* install answer names, which the wait matches too (objectui#12098).
121122
*/
122123
type OrgDeployState =
123-
| { phase: 'waiting'; manifestId: string; name: string }
124-
| { phase: 'served'; manifestId: string; name: string; app: ServedApp | null }
125-
| { phase: 'timeout'; manifestId: string; name: string };
124+
| { phase: 'waiting'; manifestId: string; version: string | undefined; name: string }
125+
| { phase: 'served'; manifestId: string; version: string | undefined; name: string; app: ServedApp | null }
126+
| { phase: 'timeout'; manifestId: string; version: string | undefined; name: string };
126127

127128
export function MarketplacePage() {
128129
const navigate = useNavigate();
@@ -229,12 +230,13 @@ export function MarketplacePage() {
229230
* earlier one's, and an earlier wait that settles late never overwrites it.
230231
* Its pulse still goes out, because its app IS served.
231232
*/
232-
const awaitServedOrgInstall = async (manifestId: string, name: string) => {
233+
const awaitServedOrgInstall = async (manifestId: string, version: string | undefined, name: string) => {
233234
const settle = (next: OrgDeployState) =>
234235
setOrgDeploy((cur) => (cur?.manifestId === manifestId ? next : cur));
235-
setOrgDeploy({ phase: 'waiting', manifestId, name });
236+
setOrgDeploy({ phase: 'waiting', manifestId, version, name });
236237
const wait = await waitForServedApp({
237238
manifestId,
239+
version,
238240
// The SDK's declared list answer (`GetMetaItemsResponse`: `{ type,
239241
// items }`), read the way the package page reads it.
240242
readApps: async () => {
@@ -243,11 +245,11 @@ export function MarketplacePage() {
243245
},
244246
});
245247
if (!wait.served) {
246-
settle({ phase: 'timeout', manifestId, name });
248+
settle({ phase: 'timeout', manifestId, version, name });
247249
return;
248250
}
249251
emitMetadataRefresh();
250-
settle({ phase: 'served', manifestId, name, app: filterActiveApps(wait.apps)[0] ?? null });
252+
settle({ phase: 'served', manifestId, version, name, app: filterActiveApps(wait.apps)[0] ?? null });
251253
};
252254

253255
// Install an org package into the current environment (ADR-0007 step ②).
@@ -266,22 +268,32 @@ export function MarketplacePage() {
266268
setOrgMsg(null);
267269
try {
268270
if (getRuntimeConfig().features.installLocal) {
269-
await installLocal({ packageId: pkg.manifest_id || pkg.id, versionId: 'latest' });
270-
setOrgMsg({ ok: true, text: t('marketplace.org.installed', { defaultValue: `Installed ${pkg.display_name}`, name: pkg.display_name }) });
271+
const result = await installLocal({ packageId: pkg.manifest_id || pkg.id, versionId: 'latest' });
272+
// objectui#12098 — `hotLoaded: false` is the runtime's lenient path
273+
// (objectstack#22695): installed, loaded at its next restart. Nothing
274+
// of it is served until then, so the strip says so and nothing is
275+
// refreshed, as on the package page.
276+
setOrgMsg({
277+
ok: true,
278+
text: result.hotLoaded
279+
? t('marketplace.org.installed', { defaultValue: `Installed ${pkg.display_name}`, name: pkg.display_name })
280+
: t('marketplace.install.localLoadsAtRestart', { version: result.version, name: pkg.display_name }),
281+
});
271282
await load();
272-
// The runtime hot-registered the package before it answered, so the
273-
// live registry already holds it. Pulse every mounted MetadataProvider
283+
// On `hotLoaded` the runtime registered the package before it answered,
284+
// so the live registry already holds it. Pulse every mounted MetadataProvider
274285
// so the new app appears in the nav (and its objects/views resolve)
275286
// without a manual browser refresh.
276-
emitMetadataRefresh();
287+
if (result.hotLoaded) emitMetadataRefresh();
277288
} else {
278-
await installPackage({ packageId: pkg.id, environmentId: '', seedSampleData: true });
289+
const installResp = await installPackage({ packageId: pkg.id, environmentId: '', seedSampleData: true });
279290
// objectui#12097 — this answer lands while the runtime still serves the
280291
// PRE-install kernel, so nothing is refreshed here: the wait refreshes
281292
// once the package's app is served. Not awaited, and not cancelled on
282293
// unmount, as on the package page: an operator who leaves this page
283294
// while the app deploys still gets the refreshed cache when it lands.
284-
void awaitServedOrgInstall(pkg.manifest_id, pkg.display_name);
295+
// It waits for the version the answer names (objectui#12098).
296+
void awaitServedOrgInstall(pkg.manifest_id, installResp?.installation?.version, pkg.display_name);
285297
await load();
286298
}
287299
} catch (e: any) {
@@ -393,7 +405,7 @@ export function MarketplacePage() {
393405
<Button
394406
size="sm"
395407
variant="outline"
396-
onClick={() => void awaitServedOrgInstall(orgDeploy.manifestId, orgDeploy.name)}
408+
onClick={() => void awaitServedOrgInstall(orgDeploy.manifestId, orgDeploy.version, orgDeploy.name)}
397409
>
398410
{t('marketplace.install.checkAgain')}
399411
</Button>

0 commit comments

Comments
 (0)