A registry record is a name and a catalog
`config` — added this morning in 2.1.0 — is gone, and `storeRepositoryUrl` with it, along with the two store repositories they pointed at. 2.1.0 had the registry say how each store behaves. Wrong shape: how a store behaves is fixed per installed client, and this application is the only thing that can see the machine it runs on. A copy of that on a server was a second authority over decisions this side had already made correctly — including which directories the store may delete from — and two authorities are a way to disagree. Keeping two stores on one machine apart needs none of it. It is a subfolder, derived here: the store id is a slug of the catalog host, the home is `<id>-desktop`, the games folder is `<id>`, and that folder is the only subtree the store will ever delete from. Derived from the *catalog* on purpose — the catalog is what a store is, so two records naming the same one are the same store and land in the same place, which makes installing twice idempotent instead of a way to orphan what is already there. Existing installations keep their identity: a store home is recognised by its own `config.json`, so one installed as `ttg` stays `ttg` in `ttg-desktop` with its games where they are. Only a new install derives its id. `StoreProvisioningService` no longer re-reads the registry before installing. That existed to keep the renderer from supplying a config, and with no config in the record there is nothing left to protect: a name and a catalog have no paths in them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,6 @@ export class RegistryStoreDtoMapper {
|
||||
return {
|
||||
name: store.name,
|
||||
catalogUrl: store.catalogUrl,
|
||||
storeRepositoryUrl: store.storeRepositoryUrl,
|
||||
storeId: deriveStoreId(store)
|
||||
}
|
||||
}
|
||||
@@ -16,20 +15,8 @@ export class RegistryStoreDtoMapper {
|
||||
return stores.map((store: RegistryStore): RegistryStoreDto => this.toDto(store))
|
||||
}
|
||||
|
||||
/**
|
||||
* The window hands a record back when asking for an install — as an identity only.
|
||||
*
|
||||
* There is no `config` here on purpose. A store's config decides where files are
|
||||
* written and which subtree the store may delete from, so it must not be something
|
||||
* the window can supply; `StoreProvisioningService` reads the real record from the
|
||||
* registry instead. That is the same rule as `GameDto` carrying no paths.
|
||||
*/
|
||||
/** The window hands a record straight back when asking for an install. */
|
||||
public toModel (dto: RegistryStoreDto): RegistryStore {
|
||||
return {
|
||||
name: dto.name,
|
||||
catalogUrl: dto.catalogUrl,
|
||||
storeRepositoryUrl: dto.storeRepositoryUrl,
|
||||
config: null
|
||||
}
|
||||
return { name: dto.name, catalogUrl: dto.catalogUrl }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,45 +30,21 @@ export class StoreProvisioningService {
|
||||
return this.registry.listStores()
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the chosen store.
|
||||
*
|
||||
* The window's choice is taken at face value, which is safe because a record is only a
|
||||
* name and a catalog: there is no path in it and nothing that decides what may be
|
||||
* deleted. The store's own configuration is written by the installer from the engine's
|
||||
* defaults, so the renderer cannot influence where anything lands.
|
||||
*/
|
||||
public async installStore (
|
||||
chosen: RegistryStore,
|
||||
store: RegistryStore,
|
||||
progress?: EngineProgressListener
|
||||
): Promise<InstalledStore> {
|
||||
const store = await this.resolveFromRegistry(chosen, progress)
|
||||
const home = this.stores.resolveDefaultHome(deriveStoreId(store))
|
||||
const installed = await this.installer.installEngine(home, store, progress)
|
||||
return this.selection.adoptStore(installed)
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry's own record for the store that was chosen.
|
||||
*
|
||||
* The window is handed stores to display and hands one back to install, but what it
|
||||
* hands back is not what gets used. A store's config decides where files are written
|
||||
* and which subtree the store may later delete from, so it cannot be something the
|
||||
* window supplies — the choice is treated as an identity, a name and a catalog, and
|
||||
* the record behind it is read again here.
|
||||
*
|
||||
* A store that has since left the registry, or a registry that cannot be reached, is
|
||||
* not a reason to refuse the install: it proceeds on the engine's defaults, which is
|
||||
* what a store with no config gets anyway.
|
||||
*/
|
||||
private async resolveFromRegistry (
|
||||
chosen: RegistryStore,
|
||||
progress?: EngineProgressListener
|
||||
): Promise<RegistryStore> {
|
||||
try {
|
||||
const listed = await this.registry.listStores()
|
||||
const found = listed.find((store: RegistryStore): boolean =>
|
||||
store.catalogUrl === chosen.catalogUrl && store.name === chosen.name)
|
||||
if (found !== undefined) return found
|
||||
progress?.onLog?.(
|
||||
`${chosen.name} is no longer in the registry — installing on the engine's defaults`)
|
||||
} catch (error: unknown) {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
progress?.onLog?.(
|
||||
`the registry could not be read again (${reason}) — installing on the engine's defaults`)
|
||||
}
|
||||
return { ...chosen, config: null }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user