@fnndsc/porter 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @file Sessions as child processes on this host, one per identity, each in
3
+ * four directories of its own.
4
+ *
5
+ * A session is `chell --daemon <user>@<url> --auth-token-stdin`: the token
6
+ * on stdin, one line, never argv; the config, runtime, cache and tmp
7
+ * directories under the porter's state, keyed by the identity's berth key,
8
+ * because a daemon that shares any of them with another identity's has cost
9
+ * real sessions their working directory. The child's berth lands in its own
10
+ * runtime directory, and that is where the host reads it back.
11
+ *
12
+ * The boot's stdout and stderr are kept, line by line, for as long as the
13
+ * child lives: a greeter that attaches late replays them, one that is there
14
+ * follows them, and the terminal boot and the browser boot are the same
15
+ * text.
16
+ *
17
+ * @module
18
+ */
19
+ import { spawn as childSpawn } from 'node:child_process';
20
+ import { mkdirSync, readFileSync, readdirSync, existsSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { berthKey_compute, berth_pathIn, berthUrl_isAlive } from '@fnndsc/calypso/berth';
23
+ /** How many boot lines a session keeps for late followers. */
24
+ export const BOOT_LINES_KEPT = 2_000;
25
+ /** Default spawner: a real child with piped streams. */
26
+ const spawn_real = (command, args, options) => {
27
+ const child = childSpawn(command, args, { env: options.env, stdio: options.stdio });
28
+ return child;
29
+ };
30
+ /**
31
+ * Splits a stream's chunks into lines, handing over each complete one.
32
+ *
33
+ * @param stream - The stream to read.
34
+ * @param onLine - Told each line without its ending.
35
+ */
36
+ function stream_lines(stream, onLine) {
37
+ if (stream === null)
38
+ return;
39
+ let buffered = '';
40
+ stream.on('data', (chunk) => {
41
+ buffered += chunk.toString();
42
+ let cut = buffered.indexOf('\n');
43
+ while (cut !== -1) {
44
+ onLine(buffered.slice(0, cut).replace(/\r$/, ''));
45
+ buffered = buffered.slice(cut + 1);
46
+ cut = buffered.indexOf('\n');
47
+ }
48
+ });
49
+ stream.on('end', () => {
50
+ if (buffered.length > 0)
51
+ onLine(buffered);
52
+ buffered = '';
53
+ });
54
+ }
55
+ /** Sessions as child processes on this host. */
56
+ export class ProcessHost {
57
+ stateDir;
58
+ chellEntry;
59
+ spawner;
60
+ alive;
61
+ bootTimeoutMs;
62
+ pollMs;
63
+ kill;
64
+ sessions = new Map();
65
+ constructor(options) {
66
+ this.stateDir = options.stateDir;
67
+ this.chellEntry = options.chellEntry;
68
+ this.spawner = options.spawn ?? spawn_real;
69
+ this.alive = options.alive ?? ((url) => berthUrl_isAlive(url));
70
+ this.bootTimeoutMs = options.bootTimeoutMs ?? 120_000;
71
+ this.pollMs = options.pollMs ?? 250;
72
+ this.kill = options.kill ?? ((pid, signal) => { process.kill(pid, signal); });
73
+ }
74
+ /**
75
+ * The four directories an identity's session owns, under the state dir.
76
+ *
77
+ * @param identity - The normalised identity.
78
+ * @returns The directories; not necessarily existing yet.
79
+ */
80
+ dirs_of(identity) {
81
+ const root = join(this.stateDir, berthKey_compute(identity));
82
+ return {
83
+ config: join(root, 'config'),
84
+ runtime: join(root, 'run'),
85
+ cache: join(root, 'cache'),
86
+ tmp: join(root, 'tmp'),
87
+ };
88
+ }
89
+ /** Reads a berth file, if well formed. */
90
+ berthFile_read(path) {
91
+ if (!existsSync(path))
92
+ return null;
93
+ try {
94
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
95
+ if (typeof parsed.url === 'string' && typeof parsed.token === 'string' && typeof parsed.identity === 'string') {
96
+ return { identity: parsed.identity, url: parsed.url, token: parsed.token, ...(typeof parsed.pid === 'number' ? { pid: parsed.pid } : {}) };
97
+ }
98
+ }
99
+ catch {
100
+ // A half-written berth reads as none; the next poll reads it whole.
101
+ }
102
+ return null;
103
+ }
104
+ /** Reads an identity's berth from its own runtime directory, if written. */
105
+ berth_read(identity) {
106
+ return this.berthFile_read(berth_pathIn(this.dirs_of(identity).runtime, identity));
107
+ }
108
+ /** @inheritdoc */
109
+ async sessions_adopt() {
110
+ const sightings = [];
111
+ if (!existsSync(this.stateDir))
112
+ return sightings;
113
+ for (const entry of readdirSync(this.stateDir, { withFileTypes: true })) {
114
+ if (!entry.isDirectory())
115
+ continue;
116
+ const berthDir = join(this.stateDir, entry.name, 'run', 'calypso');
117
+ if (!existsSync(berthDir))
118
+ continue;
119
+ for (const name of readdirSync(berthDir)) {
120
+ if (!name.startsWith('berth-') || !name.endsWith('.json'))
121
+ continue;
122
+ const berth = this.berthFile_read(join(berthDir, name));
123
+ if (berth === null)
124
+ continue;
125
+ const alive = await this.alive(berth.url);
126
+ sightings.push({ identity: berth.identity, berth, alive });
127
+ if (alive && !this.sessions.has(berth.identity)) {
128
+ this.sessions.set(berth.identity, {
129
+ identity: berth.identity,
130
+ child: null,
131
+ adoptedPid: berth.pid ?? null,
132
+ lines: [],
133
+ state: 'ready',
134
+ listeners: new Set(),
135
+ starting: null,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ return sightings;
141
+ }
142
+ /** @inheritdoc */
143
+ async find(identity) {
144
+ const berth = this.berth_read(identity);
145
+ if (berth === null)
146
+ return null;
147
+ return (await this.alive(berth.url)) ? berth : null;
148
+ }
149
+ /** @inheritdoc */
150
+ async spawn(identity, user, cubeUrl, token) {
151
+ const existing = this.sessions.get(identity);
152
+ if (existing?.starting)
153
+ return existing.starting;
154
+ const record = {
155
+ identity,
156
+ child: null,
157
+ adoptedPid: null,
158
+ lines: [],
159
+ state: 'booting',
160
+ listeners: new Set(),
161
+ starting: null,
162
+ };
163
+ this.sessions.set(identity, record);
164
+ record.starting = this.boot_run(record, user, cubeUrl, token).finally(() => { record.starting = null; });
165
+ return record.starting;
166
+ }
167
+ /** @inheritdoc */
168
+ spawn_begin(identity, user, cubeUrl, token) {
169
+ // The failure is already on the boot record for whoever follows it; the
170
+ // promise's own rejection has nobody left to reach.
171
+ void this.spawn(identity, user, cubeUrl, token).catch(() => undefined);
172
+ }
173
+ /** Starts the child and waits for its berth to answer. */
174
+ async boot_run(record, user, cubeUrl, token) {
175
+ const dirs = this.dirs_of(record.identity);
176
+ for (const dir of Object.values(dirs))
177
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
178
+ const env = {
179
+ ...process.env,
180
+ XDG_CONFIG_HOME: dirs.config,
181
+ XDG_RUNTIME_DIR: dirs.runtime,
182
+ XDG_CACHE_HOME: dirs.cache,
183
+ TMPDIR: dirs.tmp,
184
+ // The boot rows are for a greeter that renders them as a terminal
185
+ // would; chalk sees no TTY here and would strip the colour.
186
+ FORCE_COLOR: '3',
187
+ };
188
+ // The greeter draws the brain itself, from the same frames; the boot's
189
+ // own copy on stdout would stand a second, still brain under the rows.
190
+ const child = this.spawner(process.execPath, [this.chellEntry, `${user}@${cubeUrl}`, '--daemon', '--auth-token-stdin', '--no-logo'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
191
+ record.child = child;
192
+ const line_note = (channel, text) => {
193
+ const line = { channel, text };
194
+ record.lines.push(line);
195
+ // A daemon writes for as long as it lives; a follower wants the boot,
196
+ // not the year. The earliest lines go first.
197
+ if (record.lines.length > BOOT_LINES_KEPT)
198
+ record.lines.splice(0, record.lines.length - BOOT_LINES_KEPT);
199
+ for (const listener of record.listeners)
200
+ listener.line(line);
201
+ };
202
+ stream_lines(child.stdout, (text) => line_note('out', text));
203
+ stream_lines(child.stderr, (text) => line_note('err', text));
204
+ child.stdin?.end(`${token}\n`);
205
+ let exited = null;
206
+ child.once('exit', (code) => { exited = code ?? 0; });
207
+ const deadline = Date.now() + this.bootTimeoutMs;
208
+ while (Date.now() < deadline) {
209
+ if (exited !== null) {
210
+ return this.boot_end(record, 'failed', `the session exited during boot (code ${exited})`);
211
+ }
212
+ const berth = this.berth_read(record.identity);
213
+ if (berth !== null && (await this.alive(berth.url))) {
214
+ this.boot_end(record, 'ready');
215
+ return berth;
216
+ }
217
+ await new Promise((resolve) => { setTimeout(resolve, this.pollMs); });
218
+ }
219
+ child.kill('SIGTERM');
220
+ return this.boot_end(record, 'failed', `the session did not answer within ${Math.round(this.bootTimeoutMs / 1000)} s`);
221
+ }
222
+ boot_end(record, state, reason) {
223
+ record.state = state;
224
+ record.reason = reason;
225
+ for (const listener of record.listeners)
226
+ listener.done(state, reason);
227
+ record.listeners.clear();
228
+ if (state === 'failed')
229
+ throw new Error(reason);
230
+ }
231
+ /** @inheritdoc */
232
+ boot_follow(identity, listener) {
233
+ const record = this.sessions.get(identity);
234
+ if (record === undefined)
235
+ return null;
236
+ const report = { lines: [...record.lines], state: record.state, ...(record.reason !== undefined ? { reason: record.reason } : {}) };
237
+ if (record.state === 'booting')
238
+ record.listeners.add(listener);
239
+ return { report, release: () => { record.listeners.delete(listener); } };
240
+ }
241
+ /** @inheritdoc */
242
+ async evict(identity) {
243
+ const record = this.sessions.get(identity);
244
+ this.sessions.delete(identity);
245
+ if (record?.child && record.child.exitCode === null) {
246
+ record.child.kill('SIGTERM');
247
+ return true;
248
+ }
249
+ // A session an earlier porter started is known by its berth, and the
250
+ // berth carries the pid. It is signalled only while the berth answers:
251
+ // a pid whose daemon is gone may be somebody else's by now.
252
+ const berth = this.berth_read(identity);
253
+ const pid = record?.adoptedPid ?? berth?.pid ?? null;
254
+ if (berth !== null && pid !== null && (await this.alive(berth.url))) {
255
+ this.kill(pid, 'SIGTERM');
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ }
261
+ //# sourceMappingURL=processHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processHost.js","sourceRoot":"","sources":["../../src/host/processHost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @file The seam between the door and wherever sessions run.
3
+ *
4
+ * On one host a session is a child process in four directories of its own;
5
+ * on a cluster it is a pod with a volume. The porter never knows which: it
6
+ * asks the host to find an identity's session, to start one with a token,
7
+ * and to end one, and it gets back a berth — an address and an attach
8
+ * token — either way. This is the `BerthResolver` seam of tier 1 walked
9
+ * through: the surface's attach path did not change for identity-keyed
10
+ * daemons, and the door's does not change for pods.
11
+ *
12
+ * @module
13
+ */
14
+ import type { Berth } from '@fnndsc/calypso/berth';
15
+ export type { Berth };
16
+ /** One line of a session's boot, as the host saw it. */
17
+ export interface BootLine {
18
+ /** Which stream it came off. */
19
+ channel: 'out' | 'err';
20
+ /** The line, ANSI and all: the greeter renders it as the terminal would. */
21
+ text: string;
22
+ }
23
+ /** A boot in progress, or finished, as a host reports it. */
24
+ export interface BootReport {
25
+ /** Every line so far, in order. */
26
+ lines: BootLine[];
27
+ /** Whether the session is up (berth answering), still booting, or gone. */
28
+ state: 'booting' | 'ready' | 'failed';
29
+ /** What went wrong, when the state is `failed`. */
30
+ reason?: string;
31
+ }
32
+ /** Who to tell as a boot proceeds. */
33
+ export interface BootListener {
34
+ line(line: BootLine): void;
35
+ done(state: 'ready' | 'failed', reason?: string): void;
36
+ }
37
+ /** One session a host knows of, for a listing. */
38
+ export interface SessionSighting {
39
+ identity: string;
40
+ berth: Berth;
41
+ alive: boolean;
42
+ }
43
+ /** Where sessions run. */
44
+ export interface SessionHost {
45
+ /**
46
+ * Finds every session this host holds a berth for, alive or not, and
47
+ * takes the live ones under its wing — a porter restarted adopts the
48
+ * fleet its predecessor started rather than starting rivals.
49
+ *
50
+ * @returns Every berth found, with whether its daemon answers.
51
+ */
52
+ sessions_adopt(): Promise<SessionSighting[]>;
53
+ /**
54
+ * Finds an identity's live session.
55
+ *
56
+ * @param identity - The normalised `<user>@<url>`.
57
+ * @returns Its berth, or null when no session of theirs answers.
58
+ */
59
+ find(identity: string): Promise<Berth | null>;
60
+ /**
61
+ * Starts an identity's session with a CUBE token, or joins one already
62
+ * starting; resolves once its berth answers.
63
+ *
64
+ * @param identity - The normalised `<user>@<url>`.
65
+ * @param user - The CUBE username.
66
+ * @param cubeUrl - The CUBE API base.
67
+ * @param token - The CUBE token the door minted; the session's only credential.
68
+ * @returns The berth.
69
+ * @throws {Error} When the session does not come up.
70
+ */
71
+ spawn(identity: string, user: string, cubeUrl: string, token: string): Promise<Berth>;
72
+ /**
73
+ * Starts an identity's session and returns at once: the boot is followed
74
+ * through {@link boot_follow}, and its end — ready, or failed with a
75
+ * reason — is recorded there rather than thrown to anyone.
76
+ *
77
+ * @param identity - The normalised `<user>@<url>`.
78
+ * @param user - The CUBE username.
79
+ * @param cubeUrl - The CUBE API base.
80
+ * @param token - The CUBE token the door minted.
81
+ */
82
+ spawn_begin(identity: string, user: string, cubeUrl: string, token: string): void;
83
+ /**
84
+ * Reads what a session's boot has said so far, and follows it.
85
+ *
86
+ * @param identity - The normalised `<user>@<url>`.
87
+ * @param listener - Told each line from now on, and the end.
88
+ * @returns The lines so far and the state now, or null when the host has
89
+ * no boot on record for this identity. The listener is released when
90
+ * the boot ends or when the returned function is called.
91
+ */
92
+ boot_follow(identity: string, listener: BootListener): {
93
+ report: BootReport;
94
+ release: () => void;
95
+ } | null;
96
+ /**
97
+ * Ends an identity's session.
98
+ *
99
+ * @param identity - The normalised `<user>@<url>`.
100
+ * @returns Whether there was one to end.
101
+ */
102
+ evict(identity: string): Promise<boolean>;
103
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sessionHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionHost.js","sourceRoot":"","sources":["../../src/host/sessionHost.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/porter.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file The `porter` entry: the mise display manager.
4
+ *
5
+ * Reads its configuration from the environment, stands a process host on
6
+ * this machine, and listens. `porter --status` lists the sessions a running
7
+ * porter's state directory holds.
8
+ *
9
+ * @module
10
+ */
11
+ import { porterConfig_resolve } from './config.js';
12
+ import { ProcessHost } from './host/processHost.js';
13
+ import { porterApp_build } from './app.js';
14
+ import { berthKey_compute } from '@fnndsc/calypso/berth';
15
+ /**
16
+ * Lists the sessions the state directory holds, without starting anything.
17
+ *
18
+ * @param config - Where the state directory is.
19
+ */
20
+ async function status_print(config) {
21
+ const host = new ProcessHost({ stateDir: config.stateDir, chellEntry: config.chellEntry });
22
+ const sightings = await host.sessions_adopt();
23
+ console.log(`state: ${config.stateDir}`);
24
+ if (sightings.length === 0) {
25
+ console.log('no sessions');
26
+ return;
27
+ }
28
+ for (const sighting of sightings) {
29
+ const state = sighting.alive ? 'answering' : 'gone';
30
+ console.log(`${berthKey_compute(sighting.identity)} ${state.padEnd(9)} ${sighting.berth.url.padEnd(24)} ${sighting.identity}${sighting.berth.pid !== undefined ? ` pid ${sighting.berth.pid}` : ''}`);
31
+ }
32
+ }
33
+ async function porter_start() {
34
+ let config;
35
+ try {
36
+ config = porterConfig_resolve(process.env);
37
+ }
38
+ catch (error) {
39
+ console.error(`[!] ${error instanceof Error ? error.message : String(error)}`);
40
+ process.exit(1);
41
+ }
42
+ if (process.argv.includes('--status')) {
43
+ await status_print(config);
44
+ return;
45
+ }
46
+ const host = new ProcessHost({ stateDir: config.stateDir, chellEntry: config.chellEntry });
47
+ const sweepText = process.env['PORTER_SWEEP_SECONDS'];
48
+ const built = await porterApp_build({
49
+ config,
50
+ host,
51
+ logger: false,
52
+ log: (line) => { console.log(`[+] ${line}`); },
53
+ // Not documented: the sweep's cadence only matters to a test of it.
54
+ ...(sweepText !== undefined && Number(sweepText) > 0 ? { sweepMs: Number(sweepText) * 1000 } : {}),
55
+ });
56
+ await built.app.listen({ host: config.host, port: config.port });
57
+ console.log(`[+] PORTER at http://${config.host}:${config.port}/ for ${config.cubeUrl}`);
58
+ console.log(` state: ${config.stateDir}`);
59
+ console.log(` chell: ${config.chellEntry}`);
60
+ console.log(` idle: sessions end after ${config.idleHours} h with nobody on them; their state directories stay`);
61
+ if (config.secretGenerated) {
62
+ console.log(' secret: made up for this run — set PORTER_SECRET so a restart does not ask every browser again');
63
+ }
64
+ const stop = async () => {
65
+ await built.app.close();
66
+ process.exit(0);
67
+ };
68
+ process.once('SIGINT', () => { void stop(); });
69
+ process.once('SIGTERM', () => { void stop(); });
70
+ }
71
+ void porter_start();
72
+ //# sourceMappingURL=porter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"porter.js","sourceRoot":"","sources":["../src/porter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import type { Berth, SessionHost } from './host/sessionHost.js';
2
+ /** One identity the door has let through. */
3
+ export interface SessionEntry {
4
+ key: string;
5
+ identity: string;
6
+ user: string;
7
+ berth: Berth;
8
+ /** When something last reached the session through the door, epoch ms. */
9
+ activeAt: number;
10
+ /** How many wires — relayed WebSockets — stand open on it right now. */
11
+ wires: number;
12
+ }
13
+ /** The sessions the door knows, by key. */
14
+ export declare class SessionRegistry {
15
+ private readonly byKey;
16
+ private readonly pending;
17
+ private readonly clock;
18
+ constructor(clock?: () => number);
19
+ /**
20
+ * The key an identity is addressed by.
21
+ *
22
+ * @param identity - The normalised identity.
23
+ * @returns Sixteen hex characters, stable for the identity.
24
+ */
25
+ key_of(identity: string): string;
26
+ /** Records an identity and the berth it answers at; a known one keeps its wires. */
27
+ note(identity: string, user: string, berth: Berth): SessionEntry;
28
+ /** Something reached the session through the door just now. */
29
+ activity_note(key: string): void;
30
+ /** A wire opened on the session, or closed. */
31
+ wire_count(key: string, delta: 1 | -1): void;
32
+ /**
33
+ * The sessions nobody has touched for longer than the span: no wire open,
34
+ * nothing through the door since.
35
+ *
36
+ * @param idleMs - The span.
37
+ * @returns The idle entries.
38
+ */
39
+ idle_list(idleMs: number): SessionEntry[];
40
+ /** Records an identity whose session is booting; the berth comes later. */
41
+ pending_note(identity: string, user: string): void;
42
+ /** The identity behind a key, admitted or still booting. */
43
+ identity_of(key: string): string | null;
44
+ /**
45
+ * Moves a booting identity into the registry once its berth answers.
46
+ *
47
+ * @param identity - The identity whose boot ended.
48
+ * @param host - Where to read the berth from.
49
+ * @returns The entry, or null when the boot ended without a berth.
50
+ */
51
+ settle(identity: string, host: Pick<SessionHost, 'find'>): Promise<SessionEntry | null>;
52
+ /** Looks a key up. */
53
+ get(key: string): SessionEntry | null;
54
+ /** Forgets a key. */
55
+ forget(key: string): void;
56
+ /** Every entry, for a status listing. */
57
+ all(): SessionEntry[];
58
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file Who is behind which door.
3
+ *
4
+ * A browser names its session by a short key in the path — `/s/<key>/` —
5
+ * never by the identity, which carries a URL with slashes in it and is the
6
+ * operator's business, not the URL bar's. The key is the berth key calypso
7
+ * already computes for the identity, so a porter restarted finds the same
8
+ * key for the same operator. The registry maps key to identity and to the
9
+ * berth the door last saw.
10
+ *
11
+ * @module
12
+ */
13
+ import { berthKey_compute } from '@fnndsc/calypso/berth';
14
+ /** The sessions the door knows, by key. */
15
+ export class SessionRegistry {
16
+ byKey = new Map();
17
+ pending = new Map();
18
+ clock;
19
+ constructor(clock = () => Date.now()) {
20
+ this.clock = clock;
21
+ }
22
+ /**
23
+ * The key an identity is addressed by.
24
+ *
25
+ * @param identity - The normalised identity.
26
+ * @returns Sixteen hex characters, stable for the identity.
27
+ */
28
+ key_of(identity) {
29
+ return berthKey_compute(identity);
30
+ }
31
+ /** Records an identity and the berth it answers at; a known one keeps its wires. */
32
+ note(identity, user, berth) {
33
+ const key = this.key_of(identity);
34
+ const known = this.byKey.get(key);
35
+ const entry = { key, identity, user, berth, activeAt: this.clock(), wires: known?.wires ?? 0 };
36
+ this.byKey.set(key, entry);
37
+ return entry;
38
+ }
39
+ /** Something reached the session through the door just now. */
40
+ activity_note(key) {
41
+ const entry = this.byKey.get(key);
42
+ if (entry !== undefined)
43
+ entry.activeAt = this.clock();
44
+ }
45
+ /** A wire opened on the session, or closed. */
46
+ wire_count(key, delta) {
47
+ const entry = this.byKey.get(key);
48
+ if (entry === undefined)
49
+ return;
50
+ entry.wires = Math.max(0, entry.wires + delta);
51
+ entry.activeAt = this.clock();
52
+ }
53
+ /**
54
+ * The sessions nobody has touched for longer than the span: no wire open,
55
+ * nothing through the door since.
56
+ *
57
+ * @param idleMs - The span.
58
+ * @returns The idle entries.
59
+ */
60
+ idle_list(idleMs) {
61
+ const now = this.clock();
62
+ return [...this.byKey.values()].filter((entry) => entry.wires === 0 && now - entry.activeAt > idleMs);
63
+ }
64
+ /** Records an identity whose session is booting; the berth comes later. */
65
+ pending_note(identity, user) {
66
+ const key = this.key_of(identity);
67
+ if (this.byKey.has(key))
68
+ return;
69
+ this.pending.set(key, { key, identity, user });
70
+ }
71
+ /** The identity behind a key, admitted or still booting. */
72
+ identity_of(key) {
73
+ return this.byKey.get(key)?.identity ?? this.pending.get(key)?.identity ?? null;
74
+ }
75
+ /**
76
+ * Moves a booting identity into the registry once its berth answers.
77
+ *
78
+ * @param identity - The identity whose boot ended.
79
+ * @param host - Where to read the berth from.
80
+ * @returns The entry, or null when the boot ended without a berth.
81
+ */
82
+ async settle(identity, host) {
83
+ const key = this.key_of(identity);
84
+ const waiting = this.pending.get(key);
85
+ if (waiting === undefined)
86
+ return this.byKey.get(key) ?? null;
87
+ const berth = await host.find(identity);
88
+ if (berth === null)
89
+ return null;
90
+ this.pending.delete(key);
91
+ return this.note(identity, waiting.user, berth);
92
+ }
93
+ /** Looks a key up. */
94
+ get(key) {
95
+ return this.byKey.get(key) ?? null;
96
+ }
97
+ /** Forgets a key. */
98
+ forget(key) {
99
+ this.byKey.delete(key);
100
+ this.pending.delete(key);
101
+ }
102
+ /** Every entry, for a status listing. */
103
+ all() {
104
+ return [...this.byKey.values()];
105
+ }
106
+ }
107
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}