T-2: W1.4 (TypeScript): a TypeScript part says the revision view it holds and waits for, and view-waits past five minutes
The forge module reads the catalog half from TypeScript (W12.7) and must wait and say so as the Go readers do (design 47, A part with no view says it waits): views' Follower gains waitFor and holds, raiseWaits and clearWaits, the status fields view, waits-for, waiting-since and waits-raised, WAITS_BOUND_MS (five minutes) and viewWaitsFinding, by the Go side's rules (go/views follow.go, findings.go). Rows in the first-view test of test/sdk.test.ts. Builds: design 47 G §196, §200 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -123,7 +123,8 @@ caller the runtime proved in the call's `_meta`, never an argument, and `callers
|
||||
holder's list per verb (`parseVerbCallers`, `configuredVerbCallers`: `*` any proved caller, `operator` any person);
|
||||
`views.Follower` keeps a first view of an owner's state from its pages and its events, asking again only on a gap,
|
||||
and `views.statusTools` serves its read and ask-again tools (`<module>_first_views`, `<module>_first_view_again`)
|
||||
over the reader's state `first-views`. Checked by `test/stdio.test.ts` and `test/sdk.test.ts`.
|
||||
over the reader's state `first-views`. `waitFor(view)` and `holds(view)` say the revision view a part waits for and
|
||||
holds; past five minutes `raiseWaits` is told and `viewWaitsFinding` says the part's `view-waits` finding. Checked by `test/stdio.test.ts` and `test/sdk.test.ts`.
|
||||
|
||||
### `seats` and `channel` — publishing on a seat, the channel's benches (Go)
|
||||
A seat may name its caller or its kind (novox/hq ADR 0259): `stdio.SeatPublish` submits an accept or says
|
||||
|
||||
+106
-1
@@ -22,6 +22,15 @@ export const STATUS_STATE = "first-views";
|
||||
/** How long a reader asks before it raises its condition naming the owner and the tool (ADR 0615 §6), in ms. */
|
||||
export const BOUND_MS = 2 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* How long a part waits for a view before it writes its finding view-waits (design 47 *A part with no view says it
|
||||
* waits*), in ms: the Go side's `views.WaitsBound`, so an owner down for a restart raises nothing.
|
||||
*/
|
||||
export const WAITS_BOUND_MS = 5 * 60 * 1000;
|
||||
|
||||
/** The kind of a part's finding of a view it has waited for past WAITS_BOUND_MS: the Go side's `views.ViewWaitsKind`. */
|
||||
export const VIEW_WAITS_KIND = "view-waits";
|
||||
|
||||
/** The key of a reader's status of its first view of `owner`'s `state`. */
|
||||
export function statusKey(owner: string, state: string): string {
|
||||
return `${owner}.${state}`;
|
||||
@@ -79,6 +88,15 @@ export interface Status {
|
||||
behind?: string;
|
||||
why?: string;
|
||||
error?: string;
|
||||
/**
|
||||
* The revision of the revision view the part holds from this first view, as its seat's client names it (design 47 *A
|
||||
* part with no view says it waits*); `waits-for` the one it waits for and acts on nothing new until it holds, since
|
||||
* `waiting-since`; `waits-raised` says its view-waits finding is written (past WAITS_BOUND_MS).
|
||||
*/
|
||||
view?: string;
|
||||
"waits-for"?: string;
|
||||
"waiting-since"?: string;
|
||||
"waits-raised"?: boolean;
|
||||
}
|
||||
|
||||
/** How a part makes a follower: the owner and the tool it asks (for the status and the log), and the hooks. */
|
||||
@@ -91,6 +109,9 @@ export interface FollowerOptions {
|
||||
/** The reader's condition of this first view: raised past the bound, cleared by the answer. */
|
||||
readonly raise?: (s: Status) => void | Promise<void>;
|
||||
readonly clear?: (s: Status) => void | Promise<void>;
|
||||
/** The part's view-waits finding: written past WAITS_BOUND_MS of a waitFor, cleared when it holds the view. */
|
||||
readonly raiseWaits?: (s: Status) => void | Promise<void>;
|
||||
readonly clearWaits?: (s: Status) => void | Promise<void>;
|
||||
/** Told of each key the picture changes, after the change is held. */
|
||||
readonly changed?: (key: string, value: unknown, deleted: boolean, at: string) => void | Promise<void>;
|
||||
/** Told of the status each time an ask starts, is answered or fails, and of each event taken: where a reader keeps it. */
|
||||
@@ -103,6 +124,8 @@ export interface FollowerOptions {
|
||||
readonly wait?: number;
|
||||
/** Waits ms between two asks; a test hands one that does not sleep. */
|
||||
readonly pause?: (ms: number) => Promise<void>;
|
||||
/** Waits out WAITS_BOUND_MS of a waitFor; a test hands one it ends itself. Absent: a timer that holds no process open. */
|
||||
readonly waitsBound?: (ms: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const ms = (t: string | undefined): number => (t ? Date.parse(t) || 0 : 0);
|
||||
@@ -194,6 +217,53 @@ export class Follower {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Say the part waits for `view`, a revision of the revision view its seat's client names, and acts on nothing new
|
||||
* until it holds it (holds): the status says what and since when, and if it still waits for it past WAITS_BOUND_MS,
|
||||
* raiseWaits is told (the part writes view-waits, viewWaitsFinding). Waiting for the view it waits for already changes
|
||||
* nothing, not even since when. Resolves once the status is said; the bound runs on by itself.
|
||||
*/
|
||||
async waitFor(view: string): Promise<void> {
|
||||
if (!view || this.st["waits-for"] === view) return;
|
||||
this.st["waits-for"] = view;
|
||||
this.st["waiting-since"] = this.now();
|
||||
this.st["waits-raised"] = false;
|
||||
this.log(`views: ${this.o.tool} waits for ${this.o.owner}'s view ${view}, from ${this.st["waiting-since"]}`);
|
||||
await this.say();
|
||||
void this.boundWaits(view);
|
||||
}
|
||||
|
||||
private async boundWaits(view: string): Promise<void> {
|
||||
const bound = this.o.waitsBound ?? ((wait: number) => new Promise<void>((resolve) => setTimeout(resolve, wait).unref?.()));
|
||||
await bound(WAITS_BOUND_MS);
|
||||
if (this.closed || this.st["waits-for"] !== view || this.st["waits-raised"]) return;
|
||||
this.st["waits-raised"] = true;
|
||||
this.log(`views: ${this.o.tool} has waited for ${this.o.owner}'s view ${view} since ${this.st["waiting-since"]}`);
|
||||
try {
|
||||
await this.o.raiseWaits?.(this.status());
|
||||
} catch (err) {
|
||||
this.log(`views: the view-waits of ${this.o.tool} is not written: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
await this.say();
|
||||
}
|
||||
|
||||
/** Say the part holds `view` now: it waits no more, and a raised view-waits is cleared (clearWaits). */
|
||||
async holds(view: string): Promise<void> {
|
||||
const raised = !!this.st["waits-raised"];
|
||||
this.st.view = view;
|
||||
delete this.st["waits-for"];
|
||||
delete this.st["waiting-since"];
|
||||
delete this.st["waits-raised"];
|
||||
if (raised) {
|
||||
try {
|
||||
await this.o.clearWaits?.(this.status());
|
||||
} catch (err) {
|
||||
this.log(`views: the view-waits of ${this.o.tool} is not cleared: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
await this.say();
|
||||
}
|
||||
|
||||
/** Ask the first view until it is answered (ADR 0615 §6); resolves once answered, or once the follower is closed. */
|
||||
start(): Promise<void> {
|
||||
return this.askUntil("start", false);
|
||||
@@ -378,6 +448,40 @@ export class Follower {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What a views part says is wrong with a state it follows, in plain fields, as the Go side's `views.Finding`: the writer
|
||||
* the part gives encodes it as the condition keeper reads it, which is the seat mesh-conditions' contract, not this
|
||||
* package's (novox/hq ADR 0634 §4).
|
||||
*/
|
||||
export interface Finding {
|
||||
readonly observer: string;
|
||||
readonly kind: string;
|
||||
readonly id: string;
|
||||
readonly summary: string;
|
||||
readonly headline: string;
|
||||
readonly explanation: string;
|
||||
readonly at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A part's finding of the view it has waited for since `s["waiting-since"]` (design 47), as the Go side's
|
||||
* `views.ViewWaitsFinding`: it names the part, the owner and the revision, and clears when the part holds it.
|
||||
*/
|
||||
export function viewWaitsFinding(part: string, s: Status, at: Date): Finding {
|
||||
const since = s["waiting-since"] ? new Date(s["waiting-since"]).toISOString().replace(/\.\d{3}Z$/, "Z") : "";
|
||||
return {
|
||||
observer: part,
|
||||
kind: VIEW_WAITS_KIND,
|
||||
id: `${part}.${s.owner}.${s.state}`,
|
||||
summary: `${part} waits for ${s.owner}'s view ${s["waits-for"] ?? ""} since ${since} and acts on nothing new until it holds it; it holds ${s.view || "none"}`,
|
||||
headline: `${part} waits for a view of ${s.owner}`,
|
||||
explanation:
|
||||
`${part} keeps running what it runs and takes up nothing new until ${s.owner}'s view ${s["waits-for"] ?? ""} reaches it. Look at ${s.owner}: is it running, and has it ` +
|
||||
`said that revision? ${part}'s first views tool says what it holds and waits for.`,
|
||||
at: at.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** A Follower's `said` that keeps its status at statusKey in the reader's state STATUS_STATE. */
|
||||
export function keepStatus(st: State<Status>): (s: Status) => Promise<void> {
|
||||
return async (s) => {
|
||||
@@ -413,7 +517,8 @@ export function statusTools(
|
||||
`${reader}'s first views of the owners' states it follows (novox/hq ADR 0615): per owner and state, the tool it asks, the ` +
|
||||
"revision its picture holds now (events taken in included), the highest revision it heard and why it is behind it, if it " +
|
||||
"is, when it asked and was answered, how many asks (by hand among them) and whether its condition (unanswered past two " +
|
||||
"minutes) is raised.",
|
||||
"minutes) is raised; and the revision of the revision view it holds, the one it waits for and acts on nothing new until it " +
|
||||
"holds, since when, and whether its view-waits finding (past five minutes) is written.",
|
||||
input: { type: "object", properties: {} },
|
||||
run: async () => {
|
||||
const out: Status[] = [];
|
||||
|
||||
+44
-1
@@ -664,7 +664,7 @@ test("the provider loop is named for what it is, and its old name stays an alias
|
||||
});
|
||||
|
||||
import { allows, refusal, parseCallers, parseVerbCallers, parseConfiguredVerbCallers, moduleOf, UNPROVEN } from "../dist/callers/index.js";
|
||||
import { Follower, statusTools, keepStatus, statusKey, type Answer, type Status } from "../dist/views/index.js";
|
||||
import { Follower, statusTools, keepStatus, statusKey, viewWaitsFinding, type Answer, type Status } from "../dist/views/index.js";
|
||||
|
||||
test("callers and verb callers judge a proved caller by the Go side's rules (ADR 0459 §2, ADR 0627 §2)", () => {
|
||||
const rows: [string, string[], string, boolean][] = [
|
||||
@@ -784,4 +784,47 @@ test("a first view follows its owner's events, asks again on a gap and by hand,
|
||||
// Another created is the state made again: a gap too.
|
||||
f.close();
|
||||
assert.equal((await f.apply({ key: "a", revision: 1, created: "2026-11-09T11:00:00.000Z", value: 9 })).gap, true);
|
||||
|
||||
// Design 47 *A part with no view says it waits*: the status says the view held, the one waited for and since when;
|
||||
// past its bound the part's view-waits is raised once, and holding the view clears it.
|
||||
const ends: (() => void)[] = [];
|
||||
const bounds: number[] = [];
|
||||
const wRaised: Status[] = [];
|
||||
const wCleared: Status[] = [];
|
||||
const settle = async () => { for (let i = 0; i < 20; i++) await Promise.resolve(); };
|
||||
const w = new Follower({
|
||||
owner: "o", tool: "o.revision", state: "catalog", ask, now: () => new Date(created), said: keepStatus(st),
|
||||
raiseWaits: (s) => void wRaised.push(s), clearWaits: (s) => void wCleared.push(s),
|
||||
waitsBound: (ms) => (bounds.push(ms), new Promise<void>((resolve) => ends.push(resolve))),
|
||||
});
|
||||
await w.holds("trunk.11");
|
||||
await w.waitFor("trunk.12");
|
||||
await w.waitFor("trunk.12"); // the same view again: nothing new, no second bound
|
||||
assert.deepEqual(bounds, [5 * 60 * 1000], "one five-minute bound");
|
||||
s = w.status();
|
||||
assert.equal(s.view, "trunk.11");
|
||||
assert.equal(s["waits-for"], "trunk.12");
|
||||
assert.equal(s["waiting-since"], created);
|
||||
assert.equal(s["waits-raised"], false);
|
||||
assert.equal(kept.get(statusKey("o", "catalog"))!["waits-for"], "trunk.12", "the read tool's state says it");
|
||||
ends.shift()!();
|
||||
await settle();
|
||||
assert.equal(wRaised.length, 1, "past its bound view-waits is raised");
|
||||
assert.equal(wRaised[0]["waits-for"], "trunk.12");
|
||||
const fd = viewWaitsFinding("gitea", w.status(), new Date(created));
|
||||
assert.equal(fd.kind, "view-waits");
|
||||
assert.equal(fd.id, "gitea.o.catalog");
|
||||
assert.match(fd.summary, /^gitea waits for o's view trunk\.12 since 2026-11-09T10:00:00Z and acts on nothing new until it holds it; it holds trunk\.11$/);
|
||||
await w.holds("trunk.12");
|
||||
s = w.status();
|
||||
assert.equal(s.view, "trunk.12");
|
||||
assert.equal(s["waits-for"], undefined);
|
||||
assert.equal(s["waits-raised"], undefined);
|
||||
assert.equal(wCleared.length, 1, "holding the view clears it");
|
||||
await w.waitFor("trunk.13");
|
||||
await w.holds("trunk.13");
|
||||
ends.shift()!(); // the bound of a view now held raises nothing
|
||||
await settle();
|
||||
assert.equal(wRaised.length, 1, "a view held before its bound raises nothing");
|
||||
assert.equal(wCleared.length, 1, "nothing raised, nothing cleared");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user