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:
@@ -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>);
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user