refactor(msg-bus): prefer MessageBus as Realtime

This commit is contained in:
Edouard Vanbelle
2026-09-11 00:25:25 +02:00
parent 1d280c161c
commit 7918fff47b
51 changed files with 295 additions and 227 deletions
+67
View File
@@ -9,3 +9,70 @@ Prefix `oxi-`, kebab-case separators. Example: `oxi-view-mode`.
Enforced by `$lib/utils/localStoragePrefs::wipeAppKeys()` which sweeps
every `oxi-*` key on user-account switches — any other prefix leaks the
previous user's state into the new one.
## Logging — `loglevel` with `oxi:*` namespaces
**Never use bare `console.debug/info/warn/error` in `$lib` or route code.**
Route through the shared [`loglevel`](https://github.com/pimterry/loglevel)
logger so users and support can dial verbosity per subsystem from the
browser console without a redeploy.
```ts
import log from 'loglevel';
const bus = log.getLogger('oxi:message-bus');
bus.debug('subscribed', { topic });
bus.warn('reconnect scheduled', { attempt, backoffMs });
bus.error('unexpected frame', { raw });
```
Convention:
- **Namespace = `oxi:<subsystem>`** in kebab-case. One namespace per
subsystem/module boundary — e.g. `oxi:upload` (delta + direct
uploader), `oxi:message-bus` (WS client + `useTopic`). Do not create
finer-grained per-file namespaces; users tune subsystems, not files.
- **Level is user-controlled** via the DevTools helper installed in
`src/hooks.client.ts`:
```js
oxi.setLogLevel('oxi:message-bus', 'debug');
oxi.listLogLevels();
```
Choices persist to `localStorage['loglevel:<namespace>']`. Default is
loglevel's `warn` — production stays quiet unless the user opts in.
- **Add every new namespace to the DevTools comment block** in
`hooks.client.ts` (the `Log levels — namespaces used today: …` line)
so users have a discoverable list.
- **No `console.log` at all** — Stylelint/ESLint don't flag it, but the
codebase convention does. `console.error` is only acceptable in
boot-time paths (`hooks.client.ts`, generator scripts, worker
bootstraps) where the shared logger isn't reachable yet.
- **Workers can't `import log` from a static path** — see
`lib/api/endpoints/deltaUpload.ts`: the worker `postMessage`s a
`{type: 'log', level, msg, extra}` envelope and the main thread relays
it through the shared logger. Mirror this pattern for any new worker.
## Message bus naming
The realtime channel is the **message bus** everywhere — backend port
`MessageBus`, plan doc `docs/plan/message-bus.md`, generated DTOs under
`$lib/generated/message-bus/`, FE store/composables named accordingly.
Only two things keep the older `rt`/`Rt` shorthand, and both for wire-
protocol reasons:
- **JSON-RPC method prefix** — `rt.subscribe`, `rt.event`, `rt.revoked`,
`rt.ping`, `rt.error`. The prefix is opaque wire vocabulary and does
not have to expand to "realtime"; treat it as a short namespace tag
reserved for message-bus methods.
- **Generated type names** — `RtSubscribeParams`, `RtEventBody`, etc.
Modelina keys off the AsyncAPI schema names, which mirror the JSON-RPC
method names.
When adding FE code around the bus, use `message-bus` in file names,
store names, and logger namespaces:
- Store: `$lib/stores/message-bus.svelte.ts`
- Composables: `$lib/composables/useTopic.svelte.ts` (topic-generic — no
bus name in the file)
- Logger namespace: `oxi:message-bus`
- localStorage keys (if any): `oxi-message-bus-*`
+1 -1
View File
@@ -18,7 +18,7 @@
"test:unit": "LANG=C vitest run",
"test:unit:watch": "LANG=C vitest",
"test:unit:coverage": "rm -rf ../tests/e2e/.nyc_output_unit && LANG=C COVERAGE=1 vitest run",
"gen:realtime": "node scripts/gen-realtime-types.mjs"
"gen:message-bus": "node scripts/gen-message-bus-types.mjs"
},
"devDependencies": {
"@asyncapi/modelina": "^5.5.0",
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// Realtime bus — TypeScript DTOs generated from `resources/gen/asyncapi.json`.
// Message bus — TypeScript DTOs generated from `resources/gen/asyncapi.json`.
//
// Sits on the same axis as `resources/gen/openapi.json`: the wire spec
// (authored by `cargo run --features dev_tools --bin generate-asyncapi`)
@@ -7,14 +7,14 @@
// interfaces so `lib/composables/useTopic.ts` and every folder-view
// switch statement is compile-time exhaustive over the `rt.event` variants.
//
// Regenerate: `just asyncapi-ts` (or `npm run gen:realtime`).
// Regenerate: `just asyncapi-ts` (or `npm run gen:message-bus`).
// CI is expected to run the same command and fail if the working tree is
// dirty afterwards — same discipline `just openapi` follows.
//
// Design notes:
// * `modelType: 'interface'` — plain records, not classes-with-getters.
// Matches the FE codebase style (see `lib/api/types.ts`).
// * Output goes to `src/lib/generated/realtime/` — a directory reserved
// * Output goes to `src/lib/generated/message-bus/` — a directory reserved
// for auto-generated files. Never hand-edit anything inside.
// * Every file gets a `AUTO-GENERATED` banner via a preset so a stray
// edit is obvious at review time.
@@ -32,13 +32,13 @@ import { TypeScriptFileGenerator } from '@asyncapi/modelina';
const execFile = promisify(execFileCb);
// Anchor everything on this script's location so `just asyncapi-ts` from
// the repo root and `npm run gen:realtime` from the frontend both work.
// the repo root and `npm run gen:message-bus` from the frontend both work.
const __dirname = dirname(fileURLToPath(import.meta.url));
const frontendRoot = resolve(__dirname, '..');
const repoRoot = resolve(frontendRoot, '..');
const specPath = resolve(repoRoot, 'resources/gen/asyncapi.json');
const outputDir = resolve(frontendRoot, 'src/lib/generated/realtime');
const outputDir = resolve(frontendRoot, 'src/lib/generated/message-bus');
// Load the spec. Failing here means the wire spec hasn't been generated
// yet — hint the operator at the right command.
@@ -47,7 +47,7 @@ try {
spec = JSON.parse(await readFile(specPath, 'utf8'));
} catch (err) {
console.error(
`gen-realtime-types: cannot read ${specPath}: ${err.message}\n` +
`gen-message-bus-types: cannot read ${specPath}: ${err.message}\n` +
`\nDid you run \`just asyncapi\` first? The Rust generator writes\n` +
`resources/gen/asyncapi.json; this script consumes it.`
);
@@ -77,7 +77,7 @@ const generator = new TypeScriptFileGenerator({
const banner =
'// AUTO-GENERATED — do not edit by hand.\n' +
'// Regenerate with `just asyncapi-ts` (which runs\n' +
'// `node frontend/scripts/gen-realtime-types.mjs`).\n' +
'// `node frontend/scripts/gen-message-bus-types.mjs`).\n' +
'// Source of truth: resources/gen/asyncapi.json,\n' +
'// authored by the Rust `generate-asyncapi` binary.\n';
return `${banner}${content}`;
@@ -171,7 +171,7 @@ for (const f of files) {
const anonymous = files.filter((f) => f.endsWith('.ts') && /^AnonymousSchema_/i.test(f));
if (anonymous.length > 0) {
console.error(
`gen-realtime-types: FAIL — Modelina produced ${anonymous.length} ` +
`gen-message-bus-types: FAIL — Modelina produced ${anonymous.length} ` +
`AnonymousSchema_N file(s):`
);
for (const f of anonymous) console.error(` - ${f}`);
@@ -198,7 +198,7 @@ try {
});
} catch (err) {
console.error(
`gen-realtime-types: prettier --write failed: ${err.message}\n` +
`gen-message-bus-types: prettier --write failed: ${err.message}\n` +
`The generated files may still be usable but will fail\n` +
`\`npm run check\` on the prettier step. Fix prettier setup\n` +
`(is @prettier installed in frontend/node_modules?) then\n` +
@@ -208,7 +208,7 @@ try {
}
console.log(
`gen-realtime-types: wrote ${models.length} model(s) to ${outputDir}` +
`gen-message-bus-types: wrote ${models.length} model(s) to ${outputDir}` +
` (rewrote ${rewritten} for verbatimModuleSyntax, 0 AnonymousSchema,` +
` prettier-formatted)`
);