proto: freeze the wire contract at 1.0.0
Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.
Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:
gen_cpp.py -> core/generated/velox_proto.{hpp,cpp}
gen_ts.py -> extension/src/shared/protocol/
gen_openrpc.py -> contracts/openrpc.json
gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp
Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.
59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.
run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.
core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// GENERATED FILE — DO NOT EDIT.
|
||||
//
|
||||
// Source: contracts/schema/**
|
||||
// Generator: contracts/codegen/gen_ts.py
|
||||
// Contract: v1.0.0
|
||||
//
|
||||
// Hand-editing this file is a merge blocker. Fix the schema and regenerate:
|
||||
// python3 contracts/codegen/gen_ts.py
|
||||
// Only lane PROTO commits to contracts/.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
import type {
|
||||
AuthRequiredEvent,
|
||||
GrabberProgressEvent,
|
||||
NotifyEvent,
|
||||
SettingsChangedEvent,
|
||||
SpeedGlobalEvent,
|
||||
TaskAddedEvent,
|
||||
TaskProgressEvent,
|
||||
TaskRemovedEvent,
|
||||
TaskStateEvent,
|
||||
} from './types.js';
|
||||
|
||||
/** Payload for each server-to-client notification, keyed by its wire name. */
|
||||
export interface EventMap {
|
||||
/**
|
||||
* A server asked for credentials. The task sits in retry_wait until the client supplies
|
||||
* them. Credentials travel to the Secret Service, never back through this event and never
|
||||
* into a log.
|
||||
*/
|
||||
"event.auth.required": AuthRequiredEvent;
|
||||
/**
|
||||
* Crawl progress for the Site Grabber wizard. done true means the file list in
|
||||
* grabber.status is final.
|
||||
*/
|
||||
"event.grabber.progress": GrabberProgressEvent;
|
||||
/**
|
||||
* Something the user should see: a completion, a failure, a queue finishing. The client
|
||||
* decides between a toast, a tray balloon and a sound; the daemon does not assume a GUI is
|
||||
* running.
|
||||
*/
|
||||
"event.notify": NotifyEvent;
|
||||
/**
|
||||
* Settings were written by some client. Carries only the key names; a client re-reads what
|
||||
* it cares about. The extension watches for capture.* here and re-fetches capture.getRules
|
||||
* so its rules never lag the daemon's.
|
||||
*/
|
||||
"event.settings.changed": SettingsChangedEvent;
|
||||
/**
|
||||
* Aggregate throughput for the status bar, the tray tooltip and the extension popup.
|
||||
* Emitted at 1 Hz even when nothing is active, so a client can tell 'idle' from
|
||||
* 'disconnected'.
|
||||
*/
|
||||
"event.speed.global": SpeedGlobalEvent;
|
||||
/**
|
||||
* A task entered the list. summary is always present so a client can insert the row
|
||||
* without a follow-up download.get.
|
||||
*/
|
||||
"event.task.added": TaskAddedEvent;
|
||||
/**
|
||||
* Batched byte counters for every active task. Emitted at no more than 4 Hz as one array,
|
||||
* never one notification per task: at twenty active downloads that is four messages a
|
||||
* second instead of eighty. Clients apply a row patch and repaint the touched columns;
|
||||
* rebuilding a model on this event is a bug.
|
||||
*/
|
||||
"event.task.progress": TaskProgressEvent;
|
||||
/** A task left the list. The client deletes the row; there is nothing further to fetch. */
|
||||
"event.task.removed": TaskRemovedEvent;
|
||||
/**
|
||||
* A task changed lifecycle state. Carries the summary so the row can be repainted in full
|
||||
* without a round trip, and error whenever the new state is failed or retry_wait.
|
||||
*/
|
||||
"event.task.state": TaskStateEvent;
|
||||
}
|
||||
|
||||
export type EventName = keyof EventMap;
|
||||
export type EventPayload<E extends EventName> = EventMap[E];
|
||||
|
||||
/**
|
||||
* Discriminated on `method`: narrowing an incoming notification gives the
|
||||
* correctly typed params with no cast at the call site.
|
||||
*/
|
||||
export type ServerNotification = {
|
||||
[E in EventName]: { jsonrpc: '2.0'; method: E; params: EventMap[E] };
|
||||
}[EventName];
|
||||
|
||||
export interface EventMeta {
|
||||
/** Upper bound on emission rate, where the contract sets one. */
|
||||
readonly maxRateHz: number | null;
|
||||
}
|
||||
|
||||
export const EVENTS: { readonly [E in EventName]: EventMeta } = {
|
||||
"event.auth.required": { maxRateHz: null },
|
||||
"event.grabber.progress": { maxRateHz: 4 },
|
||||
"event.notify": { maxRateHz: null },
|
||||
"event.settings.changed": { maxRateHz: null },
|
||||
"event.speed.global": { maxRateHz: 1 },
|
||||
"event.task.added": { maxRateHz: null },
|
||||
"event.task.progress": { maxRateHz: 4 },
|
||||
"event.task.removed": { maxRateHz: null },
|
||||
"event.task.state": { maxRateHz: null },
|
||||
} as const;
|
||||
|
||||
export const EVENT_NAMES = Object.keys(EVENTS) as EventName[];
|
||||
|
||||
export function isEventName(v: unknown): v is EventName {
|
||||
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(EVENTS, v);
|
||||
}
|
||||
Reference in New Issue
Block a user