The store engine moves into the client, and Python goes with it
Reading the catalog, choosing the release that fits this machine, unpacking it,
writing the menu entry and remembering what went where all happen in process now.
There is no interpreter to find, no child process, and no JSON-lines protocol
between the two halves — `PythonEngineProcessRunner`, the runtime locator, the two
engine mappers and the version negotiation are all gone, and with them the one
unchecked cast this codebase had (engine stdout to a typed event).
What that buys a person: on Windows and on a fresh Mac the app simply works. It
used to look for `python3`, `python` and `py -3` and draw a link to python.org
where none answered.
What lands on disk is unchanged, deliberately. `config.json` and `state.json` keep
the shell engine's snake_case shape, its `<scope>:<name>` keys and its file modes,
so a machine whose library was installed by the CLI keeps it — verified against the
Python engine on the same catalog: the same 13-title listing with zero field
differences, byte-identical payloads, identical modes and an identical Info.plist,
and a re-sync over a Python-installed home that writes nothing. Remove, prune,
prune-suppression on a named sync and the v1 state migration were each exercised.
Three things worth knowing about the new code:
- the zip reader is ~150 lines over `node:zlib`, because Node has none and this
application has no runtime dependencies. It restores the executable bit from
each entry's external attributes, without which nothing installed can start,
and it refuses zip64, unknown compression and paths that escape the
destination rather than guessing;
- `SUPPORTED_WARP_ENGINE_VERSIONS` names the engine versions this client is
written against, checked against the `WarpEngine-Version` header every
response carries. `selectCatalogDialect` switches over that list exhaustively,
so adding a version fails the build — type checker and linter both — until
somebody says what its catalog reads like. An absent header is read as the
oldest version, which is what an engine before 0.4.0 is;
- refresh and the language picker are icons at the foot of the side menu now,
both named for a tooltip and a screen reader, the picker still a real
`<select>` under its glyph.
The repository is free of Python as well: the Makefile, the CI check and the
release script read package.json and the forge's JSON with Node.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+41
-22
@@ -12,9 +12,9 @@ that reaches the window is a safety question, not a style one.
|
||||
|
||||
```
|
||||
shared ← contracts and strings both sides need (no logic, no I/O)
|
||||
domain ← models, ports, errors. Knows nothing about Electron, Node or Python
|
||||
domain ← models, ports, errors. Knows nothing about Electron or Node
|
||||
application ← services and DTO mappers. Orchestrates the domain through its ports
|
||||
infrastructure ← adapters: the Python CLI, HTTP, the filesystem, Electron itself
|
||||
infrastructure ← adapters: the store engine, HTTP, the filesystem, Electron itself
|
||||
main ← the Electron host: window, IPC controllers, composition root
|
||||
preload ← the bridge, and only the bridge
|
||||
renderer ← the window: state store, views, controllers
|
||||
@@ -57,10 +57,13 @@ src/
|
||||
services/ CatalogService, StoreSelectionService, …
|
||||
mappers/ domain → DTO
|
||||
infrastructure/
|
||||
process/ Python: locating it, running it, reading its streams
|
||||
engine/ the store engine: catalog, releases, install, state
|
||||
dialects/ one per WarpEngine version's catalog shape
|
||||
launchers/ .desktop, .app bundle, .lnk — the three hosts
|
||||
archive/ ZipArchive: a zip reader over node:zlib
|
||||
files/ StoreFileSystem: atomic writes and the delete guard
|
||||
repositories/ the port implementations
|
||||
mappers/ engine JSON → domain
|
||||
http/ HttpTextClient, HttpStatusError
|
||||
http/ HttpTextClient, StoreHttpClient, HttpStatusError
|
||||
json/ JsonRecord: reading data that came from elsewhere
|
||||
config/ BuildConfiguration: what was decided when this was packaged
|
||||
electron/ ApplicationEnvironment and GameLauncher adapters
|
||||
@@ -95,9 +98,13 @@ on this list, it belongs on this list.
|
||||
### Ports and adapters
|
||||
|
||||
`domain/ports/*` are interfaces; `infrastructure/*` implements them; the composition
|
||||
root is the only file that knows which implementation is in use. This is what makes
|
||||
the Python CLI, the registry HTTP call and Electron's `shell` replaceable — by a stub
|
||||
in a test, by a local endpoint in development, by a second engine later.
|
||||
root is the only file that knows which implementation is in use. This is what makes the
|
||||
store engine, the registry HTTP call and Electron's `shell` replaceable — by a stub in a
|
||||
test, by a local endpoint in development, by a second host's engine later.
|
||||
|
||||
It has already paid for itself once: the engine used to be a Python CLI driven as a child
|
||||
process, and replacing it with one that runs in process was a new adapter behind the same
|
||||
port. Nothing in `application`, `main` or `renderer` changed shape for it.
|
||||
|
||||
### Repository and Gateway
|
||||
|
||||
@@ -105,8 +112,10 @@ Both are ports; the distinction is what is behind them.
|
||||
|
||||
- **Repository** — a store of records this application owns the shape of:
|
||||
`InstalledStoreRepository`, `PreferencesRepository`, `StoreRegistryRepository`.
|
||||
- **Gateway** — another program or service with its own protocol:
|
||||
`StoreCatalogGateway` (the engine).
|
||||
- **Gateway** — something with a protocol of its own, whether or not it is another
|
||||
process: `StoreCatalogGateway` (the store engine), which today is
|
||||
`NativeStoreCatalogGateway` in this application and was a Python CLI before it. The port
|
||||
stays async because the work is: it downloads and unpacks.
|
||||
|
||||
### Service
|
||||
|
||||
@@ -119,8 +128,11 @@ controller or a view.
|
||||
Data crossing a boundary is a DTO, and a mapper converts. Two boundaries, two
|
||||
directions:
|
||||
|
||||
- `infrastructure/mappers/Engine*Mapper` — engine JSON (snake_case) → domain model.
|
||||
These are the only files that know the engine's field names.
|
||||
- `infrastructure/engine/dialects/*` — catalog JSON → typed catalog records. These are
|
||||
the only files that know a WarpEngine version's field names.
|
||||
- `infrastructure/engine/StoreConfigurationReader`, `StoreStateRepository` — the two
|
||||
snake_case files on disk → domain models. These are the only files that know the on-disk
|
||||
field names, which are the shell engine's and stay that way.
|
||||
- `application/mappers/*DtoMapper` — domain model → DTO for the bridge. Decisions the
|
||||
window must not make live here: the absolute box-art URL, whether a title can be
|
||||
launched at all.
|
||||
@@ -176,8 +188,8 @@ never calls the bridge.
|
||||
### Error hierarchy with codes
|
||||
|
||||
`DomainError` is abstract with a `code`; subclasses name a single failure
|
||||
(`PythonMissingError`, `StoreMissingError`, `EngineInvocationError`,
|
||||
`RegistryUnavailableError`, `BusyError`). The code is what crosses the bridge.
|
||||
(`StoreMissingError`, `EngineInvocationError`, `RegistryUnavailableError`, `BusyError`).
|
||||
The code is what crosses the bridge.
|
||||
|
||||
### Frozen constant tables
|
||||
|
||||
@@ -195,7 +207,8 @@ trying it out, someone shipping a client for another site, us.
|
||||
|
||||
### Untrusted-data readers
|
||||
|
||||
Anything parsed from outside — engine stdout, the registry — goes through
|
||||
Anything parsed from outside — the catalog, a store's config, the state file, the
|
||||
registry — goes through
|
||||
`infrastructure/json/JsonRecord.ts`: `unknown` in, a typed value with a stated
|
||||
fallback out. No `as` casts on foreign data.
|
||||
|
||||
@@ -219,7 +232,7 @@ The names are a pattern, not a preference, and are checked by
|
||||
|---|---|---|
|
||||
| Domain model | plain noun, no suffix | `Game`, `InstalledStore` |
|
||||
| Port | `<Subject>Repository` / `Gateway` / `Locator` / `Installer` / `Launcher` | `StoreCatalogGateway` |
|
||||
| Adapter | `<Technology><Port>` | `PythonStoreCatalogGateway`, `HttpStoreRegistryRepository`, `FileSystemInstalledStoreRepository` |
|
||||
| Adapter | `<Technology><Port>` | `NativeStoreCatalogGateway`, `HttpStoreRegistryRepository`, `FileSystemInstalledStoreRepository` |
|
||||
| Service | `<Area>Service` | `CatalogService` |
|
||||
| Mapper | `<Subject>Mapper` / `<Subject>DtoMapper` | `EngineGameMapper`, `GameDtoMapper` |
|
||||
| Wire type | `<Subject>Dto` | `CatalogListingDto` |
|
||||
@@ -227,7 +240,7 @@ The names are a pattern, not a preference, and are checked by
|
||||
| Renderer controller | `<Area>Controller` | `StoreController` |
|
||||
| View | `<Region>View` | `SideMenuView`, `GameCardView` |
|
||||
| Factory | `<Product>Factory` | `MainWindowFactory` |
|
||||
| Error | `<Cause>Error` | `PythonMissingError` |
|
||||
| Error | `<Cause>Error` | `StoreMissingError` |
|
||||
| Callback bag | `<Owner>Callbacks` | `SideMenuViewCallbacks` |
|
||||
| Type parameter | `T`-prefixed | `TResult`, `TElement` |
|
||||
|
||||
@@ -281,12 +294,18 @@ dependencies are declared here, and that is worth more than being strippable by
|
||||
the implementation to `preload.ts`, a `handle…` method to the right controller, and the
|
||||
behaviour to a service. The compiler names every file you missed.
|
||||
|
||||
**A new engine (e.g. RetroArch).** Add an entry to `STORE_ENGINES`. The store
|
||||
discovery, the home suffix and the launcher name all read from that table; the CLI has
|
||||
the same command shape, so `PythonStoreCatalogGateway` is unchanged.
|
||||
**A new engine (e.g. RetroArch).** Add an entry to `STORE_ENGINES` — the store discovery,
|
||||
the home suffix and the launcher name all read from that table — and a `StoreCatalogGateway`
|
||||
implementation for that host. Everything above the port is unchanged.
|
||||
|
||||
**A new field from the engine.** `EngineGameMapper` reads it into the model, `GameDto`
|
||||
and `GameDtoMapper` carry it across if the window needs it, and a view renders it.
|
||||
**A new WarpEngine version.** Add it to `SUPPORTED_WARP_ENGINE_VERSIONS`. The build then
|
||||
fails in `selectCatalogDialect` until the switch says which `CatalogDialect` reads it:
|
||||
either an existing one, when the catalog's shape did not change, or a new one beside
|
||||
`SoftwareListCatalogDialect`.
|
||||
|
||||
**A new field from the catalog.** The dialect reads it into `CatalogSoftware`,
|
||||
`CatalogRelease` or `CatalogAsset`; `SelectedGame` and `Game` carry it if the survey or the
|
||||
window needs it; `GameDto` and `GameDtoMapper` take it across the bridge.
|
||||
|
||||
**A new language.** Add `<Language>Messages.ts` typed as `MessageBundle`, add the code
|
||||
to `LOCALES` and the bundle to `TranslationCatalog`. A missing key will not compile.
|
||||
|
||||
Reference in New Issue
Block a user