Update the documentation of the README files in the packages and in the Website service target.

This commit is contained in:
2026-08-13 00:55:34 +02:00
parent 70c0cc5303
commit 964e7fa707
4 changed files with 9 additions and 9 deletions
+3 -3
View File
@@ -16,9 +16,9 @@ The shared [Hummingbird](https://github.com/hummingbird-project/hummingbird) too
## Design rules
The package holds only what every service can reuse; anything a service owns is injected, never referenced:
- **No site-specific content.** No page markup, no asset catalog, no `Bundle.module` lookups. A type that needs a service's content takes it as a parameter: the `bundle:` whose String Catalog names the supported languages (`LocalizationMiddleware`, `LocalizedHTMLCollectionResponse`, `NotFoundMiddleware`), the `document:` closure that builds a page for a locale, and a `Page` conformer's `metadata` — along with the `summary`, `canonicalURL`, `socialCard`, `structuredData`, and `analytics` its other head tags render, each omitted unless the page provides it. URLs arrive fully formed and absolute: composing them from an origin and a versioned asset path stays with the page.
- **Types own their format; `Page` renders generically.** Each optional head concern derives its own render-ready representation — `SocialCard.tags`, `StructuredData.payload`, `Analytics.attributes` — and the `Page` scaffolding applies it without knowing the vocabulary (Open Graph, schema.org, or the Umami convention). Page ``scripts`` and the tracker render as `defer`red head tags, with a `preconnect` hint for the tracker's cross-origin host.
- **Services fill the gaps once, via extensions.** A service restores its convenient call sites with retroactive extensions — the Website's `Page+Defaults`, `LocalizationMiddleware+Defaults`, and `NotFoundMiddleware+Defaults` are the pattern. The open schema.org vocabularies extend the same way: the package declares only the `Property.Name` and `Node.Kind` constants every service shares, and a service adds what its own node shapes need.
- **No site-specific content.** No page markup, no asset catalog, no `Bundle.module` lookups. A type that needs a service's content takes it as a parameter: the `bundle:` whose String Catalog names the supported languages, the `document:` closure building a page for a locale, and a `Page` conformer's `metadata` plus its optional head concerns (`summary`, `canonicalURL`, `socialCard`, `structuredData`, `analytics`). URLs arrive fully formed and absolute; composing them stays with the page.
- **Types own their format; `Page` renders generically.** Each head concern derives its own render-ready representation — `SocialCard.tags`, `StructuredData.payload`, `Analytics.attributes` — and `Page` applies it without knowing the vocabulary. Page ``scripts`` and the tracker render as `defer`red head tags, with a `preconnect` hint for the tracker's cross-origin host.
- **Services fill the gaps once, via extensions.** A service restores its convenient call sites with retroactive extensions — the Website's `*+Defaults` extensions are the pattern. The open schema.org vocabularies extend the same way: the package declares the shared `Property.Name` and `Node.Kind` constants, and a service adds its own.
- **Method structs.** Single-operation types such as `FingerprintAssets` hold their lifetime-fixed configuration in `init` and take only per-call inputs in `callAsFunction`.
## Layout
+2 -2
View File
@@ -10,10 +10,10 @@ The server-side localization toolkit the **Loud** services build on: locale-expl
| Diagnostics | `CatalogState`, the outcome of reading the catalog (`loaded`, `missing`, or `undecodable`) |
## Design rules
- **The String Catalog is the single source of truth.** Supported languages, the default language, and every string come from the bundle's `Localizable.xcstrings`, so adding a language is a translation-only change — `LanguageList` and `Negotiate` pick up a new locale with no code change.
- **The String Catalog is the single source of truth.** Supported languages, the default language, and every string come from the bundle's `Localizable.xcstrings`, so adding a language is a translation-only change.
- **The locale is always explicit.** A server has no single "current" locale, so every lookup names the locale to resolve in; nothing reads process-wide locale state.
- **Raw `.xcstrings` parsing, for Linux parity.** The catalog is decoded from its JSON rather than through Foundation's compiled-catalog APIs, which are unavailable or non-functional on Linux. Consumers must `.copy` the resource verbatim (never `.process` it) so it ships as raw JSON everywhere. Only simple `stringUnit` values are decoded — plural and device variations are not.
- **Resolution never fails.** A missing entry falls back to the source-language string, then to the key itself; a missing or undecodable catalog degrades the language list to the default. Check `catalogState` once at startup and warn when it is not `.loaded`, before visitors see raw keys.
- **Resolution never fails.** A missing entry falls back to the source-language string, then to the key itself; a missing or undecodable catalog degrades the language list to the default. Check `catalogState` at startup and warn when it is not `.loaded`.
- **Method structs.** `Localize` and `Negotiate` hold their lifetime-fixed configuration (the bundle) in `init` and take only per-call inputs in `callAsFunction`.
- **One decoded catalog per bundle.** Catalogs are immutable at runtime, so every `Localize`, `Negotiate`, and `LanguageList` bound to the same bundle and table shares one cached `StringCatalog`.
+1 -1
View File
@@ -15,7 +15,7 @@ The [Fluent](https://github.com/hummingbird-project/hummingbird-fluent)-based da
- **One default database.** `Service` registers the selected backend as the *default* database, so repositories resolve it with a plain `fluent.db()` and stay agnostic of which driver is in use.
- **Migrations are declared once, and append-only.** `PrepareDB` registers every migration in the order it must run; alter the schema by adding a migration, never by editing one that has already run. Registering does not apply them: the in-memory backend migrates on startup, while a shared PostgreSQL database is migrated out of band (the executable's migrate-and-exit mode), so booting instances never race.
- **Models never cross a concurrency boundary.** FluentKit models are mutable reference types, so they stay internal to the package and repositories return `Sendable` value-type snapshots (e.g. `Example`) instead.
- **Readiness never throws, and never hangs.** `Probe` runs a `SELECT 1` — the cheapest statement both backends understand, independent of any schema — and maps every failure to `false`, so callers translate it straight into a readiness response. The query is raced against a deadline (2 seconds unless the probe is built with another), so a database that hangs rather than refuses yields a prompt "not ready" instead of a hanging readiness endpoint.
- **Readiness never throws, and never hangs.** `Probe` runs a schema-independent `SELECT 1`, maps every failure to `false`, and races the query against a deadline (2 seconds by default) — a hanging database yields a prompt "not ready" instead of a stalled endpoint.
- **A single connection for the in-memory store.** The SQLite backend is capped at one connection per event loop, so every query reaches the same in-memory database instead of each pooled connection getting a private one.
- **Method structs.** `Service`, `PrepareDB`, and `Probe` hold their lifetime-fixed configuration in `init` and take only per-call inputs in `callAsFunction`.
+3 -3
View File
@@ -111,9 +111,9 @@ A dotted config key maps to an environment variable by upper-casing, splitting c
| `database.password` | `DATABASE_PASSWORD` | _(empty)_ | Database password. Provide via the environment/a secret — never commit it. |
| `database.tls` | `DATABASE_TLS` | `prefer` | TLS posture when connecting: `off`, `prefer`, or `require`. Ignored for `inMemory`. |
| `database.pool.maxPerEventLoop` | `DATABASE_POOL_MAX_PER_EVENT_LOOP` | `4` | Maximum pooled connections per event loop. Ignored for `inMemory`. |
| `database.pool.timeout` | `DATABASE_POOL_TIMEOUT` | `10` | Longest wait, in seconds, for a pooled connection to become available before the query fails. Ignored for `inMemory`. |
| `database.pool.timeout` | `DATABASE_POOL_TIMEOUT` | `10` | Seconds a query waits for a pooled connection before failing. Ignored for `inMemory`. |
> **Connection budget:** the pool holds `database.pool.maxPerEventLoop` connections *per event loop*, and the event loop group runs one loop per core. An 8-core instance can therefore open 32, and each replica that many again — three replicas exhaust PostgreSQL's default `max_connections` of 100. Size this against the server's limit, not against the number alone. When every connection is busy, a query waits up to `database.pool.timeout` for one to free up and then fails — this bounds how long requests stall on an exhausted pool.
> **Connection budget:** the pool holds `database.pool.maxPerEventLoop` connections *per event loop*, and the event loop group runs one loop per core. An 8-core instance can therefore open 32, and each replica that many again — three replicas exhaust PostgreSQL's default `max_connections` of 100. Size this against the server's limit, not against the number alone. On an exhausted pool, a query waits up to `database.pool.timeout` before failing.
See [Persistence](#persistence-1) below for the workflow.
@@ -210,7 +210,7 @@ The containerised run needs no `DATABASE_HOST`: `docker-compose.override.yml` pi
> **Note:** `db-reset` deletes `Tests/DB` itself, because Compose's `--volumes` flag cannot clear a bind mount. Use it to start from an empty database — for instance after changing `DATABASE_PASSWORD`, which is only read when the cluster is first initialised.
### Health checks
`GET /health` is a liveness check (process is up, no dependency check). `GET /health/ready` runs `SELECT 1` against the database and returns `200` when reachable or `503` otherwise — so an orchestrator restarts on liveness failure but only withholds traffic on readiness failure. `docker-compose.yml` points the `website` container healthcheck at `/health`, keeping container health decoupled from database reachability.
`GET /health` is a liveness check (process is up, no dependency check). `GET /health/ready` runs `SELECT 1` against the database and returns `200` when reachable or `503` otherwise — so an orchestrator restarts on liveness failure but only withholds traffic on readiness failure. A hanging database is reported as not ready within the probe's 2-second deadline, so the route itself never stalls. `docker-compose.yml` points the `website` container healthcheck at `/health`, keeping container health decoupled from database reachability.
## Testing
```sh