A module's state through the runtime, in TypeScript and Go (novox/hq ADR 0201)

state(name) / stdio.State(name): get, put, delete, keys and watch over the
runtime's mesh/state verbs. A watch's handler is registered under the id it
is asked with, so its current values — which arrive before the watch is
answered — and its changes reach it and no other watch.
This commit is contained in:
jochen
2026-10-04 02:47:31 +02:00
parent 0b018562e3
commit 096c5562a4
12 changed files with 488 additions and 9 deletions
+89
View File
@@ -0,0 +1,89 @@
// State — the current value of something a module keeps on the bus, which every machine sees,
// including one that joins later (novox/hq ADR 0201). Not history: that is an event. Never a secret,
// sealed or not: a bucket is kept, and a machine joining a year later reads it whole.
//
// A module declares the state it keeps (`state` in its manifest) and the state of others it reads
// (`reads`, as `<module>.<name>`), and names each here the way it named it there. Every instance of
// the owner may write; a reader reads and watches. The runtime that launched the bundle does the work
// and refuses, with the reason, what the module did not declare.
/** One key's current value. */
export interface StateEntry<T = unknown> {
readonly key: string;
readonly value: T;
/** Increases with every write to the bucket; a later value has a larger one. */
readonly revision: number;
}
/** One change a watch hands over. */
export interface StateChange<T = unknown> {
/** The state, as the module named it. */
readonly state: string;
readonly key: string;
readonly op: "put" | "delete";
/** The new value; absent for a delete. */
readonly value?: T;
readonly revision: number;
/** True for a value that was there when the watch began, false for a change since. */
readonly current: boolean;
}
/** One state a module keeps or reads. */
export interface State<T = unknown> {
/** A key's current value, or null when it has none. */
get(key: string): Promise<StateEntry<T> | null>;
/** Write a key — the owner's instances only. Resolves to the revision written. */
put(key: string, value: T): Promise<number>;
/** Remove a key — the owner's instances only. Removing one that is not there is not an error. */
delete(key: string): Promise<void>;
/** Every key with a current value, sorted. */
keys(): Promise<string[]>;
/**
* Hand `handler` every current value — none that is deleted — and then every change, in order.
* Resolves once the current values have all been handled, so code after it has the whole state.
* `key` narrows it: a key, with `*` for one name and `**` for the rest. The watch lasts as long as
* the process; a bundle that starts again watches again as its code runs again.
*/
watch(handler: (change: StateChange<T>) => Promise<void> | void, opts?: { key?: string }): Promise<void>;
}
/** What the state surface needs of the channel to the runtime; the stdio session provides it. */
export interface StateChannel {
ask(method: string, params: unknown): Promise<unknown>;
/** Ask for a watch, with `deliver` registered for its changes before the request is sent. */
watch(params: unknown, deliver: (change: StateChange<unknown>) => Promise<void>): Promise<void>;
}
let channel: StateChannel | undefined;
/** Bind the channel. The stdio session calls this when the runtime launched the process. */
export function useStateChannel(c: StateChannel): void {
channel = c;
}
function bound(): StateChannel {
if (!channel) {
throw new Error(
"no runtime to keep state through — a module's state is reached through the runtime that launched " +
"its bundle (novox/hq ADR 0201)",
);
}
return channel;
}
/** A state this module keeps (by its local name) or reads (`<module>.<name>`). */
export function state<T = unknown>(name: string): State<T> {
return {
get: async (key) => (await bound().ask("mesh/state.get", { state: name, key })) as StateEntry<T> | null,
put: async (key, value) =>
((await bound().ask("mesh/state.put", { state: name, key, value })) as { revision: number }).revision,
delete: async (key) => {
await bound().ask("mesh/state.delete", { state: name, key });
},
keys: async () => (await bound().ask("mesh/state.keys", { state: name })) as string[],
watch: async (handler, opts = {}) =>
bound().watch({ state: name, key: opts.key ?? "" }, async (change) => {
await handler(change as StateChange<T>);
}),
};
}