agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
152 lines • 5.92 kB
JavaScript
/**
* clientSet — which clients the pool holds, and in which of its two sets.
*
* Pattern: two sets and their indexes, mutated only together.
* Role: § 5.3 of the design. The OPEN set holds every client that counts
* toward `maxClients` — opening, busy, idle, deferred, or an abandoned
* open that has not settled. The ENDING set holds every client from the
* decision to end it until that ending's work has settled, and counts
* toward `maxEnding`. A client leaves the open set only into the ending
* set — a lost handle too: its ending is its local close — or, when its
* open failed and left nothing to end, by being dropped. So the pool
* holds at most `maxClients + maxEnding` client objects.
*
* Each key has at most one CURRENT client — the one new leases go to. A client
* named by `terminate`, or whose session expired, stops being current at once
* while it stays in the open set, counted, until it can end.
*
* What the pool remembers of a key after its client left — the incarnation
* count and why the last one left — is bounded (`KEY_MEMORY`), least recently
* used forgotten first: after that a key's count starts again at 1. The record
* reads it; no binding depends on it.
*/
/** How many keys' incarnation counts the pool remembers. */
export const KEY_MEMORY = 4096;
export class ClientSet {
open = new Set();
ending = new Set();
current = new Map();
perPrincipal = new Map();
memory = new Map();
/** Clients in the open set. */
get openCount() {
return this.open.size;
}
/** Clients in the ending set. */
get endingCount() {
return this.ending.size;
}
/** Every client in the open set. */
openClients() {
return [...this.open];
}
/** Clients of the open set that count for `principal`. */
principalCount(principal) {
return this.perPrincipal.get(principal) ?? 0;
}
/** The key's current client — opening or open, taking new leases — if it has one. */
currentFor(key) {
return this.current.get(key);
}
/** The next incarnation number for `key`, and why its previous client left. */
nextIncarnation(key) {
const remembered = this.remember(key);
remembered.incarnations += 1;
const previous = remembered.previous;
remembered.previous = undefined;
return { generation: remembered.incarnations, ...(previous !== undefined && { previous }) };
}
/** A new client joins the open set as its key's current client. */
admit(client) {
this.open.add(client);
this.current.set(client.key.key, client);
this.perPrincipal.set(client.key.principal, this.principalCount(client.key.principal) + 1);
}
/**
* The client stops taking new leases: its key has no current client until the
* next open, and that open's record says `why`. A client that is no longer
* its key's current one has already left it, so nothing is recorded again —
* a later client of the key keeps its own reason.
*/
retire(client, why) {
if (this.current.get(client.key.key) !== client)
return;
this.current.delete(client.key.key);
this.remember(client.key.key).previous = why;
}
/** The client moves from the open set into the ending set. */
moveToEnding(client, why) {
if (why !== undefined)
this.retire(client, why);
else if (this.current.get(client.key.key) === client)
this.current.delete(client.key.key);
this.leaveOpen(client);
this.ending.add(client);
}
/** The client's ending settled: it leaves the ending set. */
ended(client) {
this.ending.delete(client);
}
/** The client leaves the open set without ending — a failed open, a lost handle. */
drop(client, why) {
if (why !== undefined)
this.retire(client, why);
else if (this.current.get(client.key.key) === client)
this.current.delete(client.key.key);
this.leaveOpen(client);
}
/**
* The least recently used idle client of the open set — among those `pick`
* accepts — or `undefined` when every one is busy.
*/
leastRecentlyUsedIdle(pick) {
let oldest;
for (const client of this.open) {
if (client.busy || client.state !== 'open' || !pick(client))
continue;
if (oldest === undefined || client.usedAt < oldest.usedAt)
oldest = client;
}
return oldest;
}
leaveOpen(client) {
if (!this.open.delete(client))
return;
const left = this.principalCount(client.key.principal) - 1;
if (left > 0)
this.perPrincipal.set(client.key.principal, left);
else
this.perPrincipal.delete(client.key.principal);
}
remember(key) {
let remembered = this.memory.get(key);
if (remembered !== undefined)
this.memory.delete(key);
else
remembered = { incarnations: 0, previous: undefined };
this.memory.set(key, remembered);
while (this.memory.size > KEY_MEMORY) {
const oldest = this.memory.keys().next().value;
if (oldest === undefined)
break;
this.memory.delete(oldest);
}
return remembered;
}
}
/** The record's `previous` for an end reason, when it names one. */
export function previousOf(reason) {
switch (reason) {
case 'idle':
case 'evicted':
case 'expired':
case 'handle-lost':
return reason;
case 'terminated':
case 'closed':
return 'terminated';
case 'abandoned-open':
return undefined;
}
}
//# sourceMappingURL=clientSet.js.map