toolroll 0.9.47 → 0.9.49
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.
- package/CHANGELOG.md +21 -0
- package/dist/browser/workspace.js +16 -16
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/contracts/cli.d.ts +87 -0
- package/dist/contracts/cli.js +2 -0
- package/dist/contracts/health.d.ts +84 -0
- package/dist/contracts/health.js +11 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/flow-live.d.ts +5 -13
- package/dist/flow-live.js +11 -110
- package/dist/health-cli.d.ts +3 -0
- package/dist/health-cli.js +79 -0
- package/dist/live-bus.d.ts +89 -0
- package/dist/live-bus.js +321 -0
- package/dist/mcp-http.d.ts +1 -0
- package/dist/mcp-http.js +6 -4
- package/dist/mcp-oauth.d.ts +3 -0
- package/dist/mcp-oauth.js +8 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +53 -1
- package/dist/operate.js +37 -24
- package/dist/public-access.d.ts +3 -1
- package/dist/public-access.js +5 -1
- package/dist/read-executor-worker.d.ts +1 -0
- package/dist/read-executor-worker.js +26 -0
- package/dist/read-executor.d.ts +49 -0
- package/dist/read-executor.js +119 -0
- package/dist/request-budget.d.ts +31 -3
- package/dist/request-budget.js +50 -1
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +213 -100
- package/dist/server-telemetry.d.ts +86 -0
- package/dist/server-telemetry.js +263 -0
- package/dist/session-http.d.ts +5 -0
- package/dist/session-http.js +13 -0
- package/dist/session-server.d.ts +5 -0
- package/dist/session-server.js +2 -0
- package/dist/sqlite-telemetry.d.ts +9 -0
- package/dist/sqlite-telemetry.js +98 -0
- package/dist/store.d.ts +6 -0
- package/dist/store.js +25 -3
- package/dist/surface.js +2 -1
- package/dist/task-live.d.ts +5 -14
- package/dist/task-live.js +13 -110
- package/dist/team-cli.js +28 -6
- package/dist/team-contract.d.ts +7 -0
- package/dist/team-contract.js +7 -0
- package/dist/team-http.d.ts +5 -0
- package/dist/team-http.js +19 -3
- package/dist/team-leads.d.ts +5 -1
- package/dist/team-leads.js +42 -4
- package/dist/team-runtime.d.ts +1 -1
- package/dist/team-runtime.js +3 -0
- package/dist/teams.d.ts +3 -0
- package/dist/teams.js +15 -0
- package/dist/work-index.js +12 -1
- package/dist/workspace-revision.d.ts +2 -0
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type ParseResult = {
|
|
|
40
40
|
error: string;
|
|
41
41
|
};
|
|
42
42
|
type Write = (line: string) => void;
|
|
43
|
-
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
|
43
|
+
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
|
44
44
|
export declare function parseArgs(argv: readonly string[]): ParseResult;
|
|
45
45
|
/** The commands that operate the queue rather than report on the world. */
|
|
46
46
|
/** Every verb the CLI routes OUTSIDE runOperate. Exported (with
|
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { COMMAND_GUIDE, SURFACE_NOTES, SURFACE_SCHEMA_VERSION } from "./surface.
|
|
|
28
28
|
import { runSessionCommand, SESSION_CLI_ACTIONS } from "./session-cli.js";
|
|
29
29
|
import { maybeRunTeamCommand, TEAM_CLI_ACTIONS } from "./team-cli.js";
|
|
30
30
|
import { maybeRunRemoteCommand } from "./remote-exec.js";
|
|
31
|
+
import { runHealthCommand } from "./health-cli.js";
|
|
31
32
|
import { discover, inspectAll } from "./discover.js";
|
|
32
33
|
import { readPulls } from "./pulls.js";
|
|
33
34
|
import { detectGraphs, chooseBackend, setupOptions, LABELS as GRAPH_LABELS, } from "./graph.js";
|
|
@@ -67,6 +68,7 @@ Usage
|
|
|
67
68
|
toolroll up app + builder for every saved project — the normal start
|
|
68
69
|
toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)
|
|
69
70
|
toolroll status running, queued, ready results, release check and plan windows
|
|
71
|
+
toolroll health recent server latency, write waits and live streams
|
|
70
72
|
toolroll integrations which integrations work, and what to do about the ones that don't (--json)
|
|
71
73
|
toolroll session native coding sessions through the running service
|
|
72
74
|
toolroll connect save a private connection to your central service
|
|
@@ -383,6 +385,8 @@ export async function main(argv, write = line => console.log(line), mainOptions
|
|
|
383
385
|
}
|
|
384
386
|
async function dispatch(argv, write, mainOptions) {
|
|
385
387
|
// A saved API token sends commands marked remote to the central server, before anything local opens.
|
|
388
|
+
if (argv[0] === 'health')
|
|
389
|
+
return runHealthCommand(argv.slice(1), write, mainOptions.team ?? mainOptions.remote);
|
|
386
390
|
const central = await maybeRunRemoteCommand(argv, mainOptions.remote ?? mainOptions.team);
|
|
387
391
|
if (typeof central === "number")
|
|
388
392
|
return central;
|
package/dist/contracts/cli.d.ts
CHANGED
|
@@ -304,6 +304,93 @@ export declare const COMMAND_OUTPUTS: {
|
|
|
304
304
|
reason: z.ZodString;
|
|
305
305
|
message: z.ZodOptional<z.ZodString>;
|
|
306
306
|
}, z.core.$loose>], "ok">;
|
|
307
|
+
readonly health: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
308
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
309
|
+
ok: z.ZodLiteral<true>;
|
|
310
|
+
command: z.ZodLiteral<"health">;
|
|
311
|
+
health: z.ZodObject<{
|
|
312
|
+
version: z.ZodLiteral<1>;
|
|
313
|
+
windowSeconds: z.ZodLiteral<300>;
|
|
314
|
+
uptimeSeconds: z.ZodNumber;
|
|
315
|
+
requests: z.ZodObject<{
|
|
316
|
+
count: z.ZodNumber;
|
|
317
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
318
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
319
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
320
|
+
totalMs: z.ZodNumber;
|
|
321
|
+
}, z.core.$loose>;
|
|
322
|
+
routes: z.ZodRecord<z.ZodEnum<{
|
|
323
|
+
assets: "assets";
|
|
324
|
+
auth: "auth";
|
|
325
|
+
chat: "chat";
|
|
326
|
+
"chat-stream": "chat-stream";
|
|
327
|
+
cli: "cli";
|
|
328
|
+
console: "console";
|
|
329
|
+
decisions: "decisions";
|
|
330
|
+
"flow-stream": "flow-stream";
|
|
331
|
+
flows: "flows";
|
|
332
|
+
health: "health";
|
|
333
|
+
mcp: "mcp";
|
|
334
|
+
metrics: "metrics";
|
|
335
|
+
other: "other";
|
|
336
|
+
projects: "projects";
|
|
337
|
+
results: "results";
|
|
338
|
+
sessions: "sessions";
|
|
339
|
+
settings: "settings";
|
|
340
|
+
tasks: "tasks";
|
|
341
|
+
team: "team";
|
|
342
|
+
"team-stream": "team-stream";
|
|
343
|
+
}> & z.core.$partial, z.ZodObject<{
|
|
344
|
+
count: z.ZodNumber;
|
|
345
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
346
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
347
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
348
|
+
totalMs: z.ZodNumber;
|
|
349
|
+
}, z.core.$loose>>;
|
|
350
|
+
eventLoop: z.ZodObject<{
|
|
351
|
+
count: z.ZodNumber;
|
|
352
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
353
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
354
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
355
|
+
totalMs: z.ZodNumber;
|
|
356
|
+
}, z.core.$loose>;
|
|
357
|
+
writes: z.ZodObject<{
|
|
358
|
+
wait: z.ZodObject<{
|
|
359
|
+
count: z.ZodNumber;
|
|
360
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
361
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
362
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
363
|
+
totalMs: z.ZodNumber;
|
|
364
|
+
}, z.core.$loose>;
|
|
365
|
+
hold: z.ZodObject<{
|
|
366
|
+
count: z.ZodNumber;
|
|
367
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
368
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
369
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
370
|
+
totalMs: z.ZodNumber;
|
|
371
|
+
}, z.core.$loose>;
|
|
372
|
+
statement: z.ZodObject<{
|
|
373
|
+
count: z.ZodNumber;
|
|
374
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
375
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
376
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
377
|
+
totalMs: z.ZodNumber;
|
|
378
|
+
}, z.core.$loose>;
|
|
379
|
+
}, z.core.$loose>;
|
|
380
|
+
streams: z.ZodObject<{
|
|
381
|
+
open: z.ZodNumber;
|
|
382
|
+
queuedBytes: z.ZodNumber;
|
|
383
|
+
maxQueuedBytes: z.ZodNumber;
|
|
384
|
+
}, z.core.$loose>;
|
|
385
|
+
budgetRefusals: z.ZodNumber;
|
|
386
|
+
}, z.core.$loose>;
|
|
387
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
388
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
389
|
+
ok: z.ZodLiteral<false>;
|
|
390
|
+
command: z.ZodLiteral<"health">;
|
|
391
|
+
reason: z.ZodString;
|
|
392
|
+
message: z.ZodOptional<z.ZodString>;
|
|
393
|
+
}, z.core.$loose>], "ok">;
|
|
307
394
|
readonly integrations: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
308
395
|
envelopeVersion: z.ZodLiteral<1>;
|
|
309
396
|
ok: z.ZodLiteral<true>;
|
package/dist/contracts/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import { ENVELOPE_VERSION, envelopeJson } from "../envelope.js";
|
|
13
13
|
import { parseContract } from "./contract.js";
|
|
14
14
|
import { LISTED_TASK_STATES } from "./lead-tools.js";
|
|
15
|
+
import { healthSnapshotSchema } from './health.js';
|
|
15
16
|
const str = z.string();
|
|
16
17
|
const int = z.number().int();
|
|
17
18
|
const list = z.array(z.unknown());
|
|
@@ -167,6 +168,7 @@ export const COMMAND_OUTPUTS = {
|
|
|
167
168
|
contract: contractOutput,
|
|
168
169
|
scan: scanOutput,
|
|
169
170
|
status: statusOutput,
|
|
171
|
+
health: answers('health', { health: healthSnapshotSchema }),
|
|
170
172
|
integrations: integrationsOutput,
|
|
171
173
|
ready: z.union([
|
|
172
174
|
answers("ready", { count: int, tasks: rows({ id: str, title: str, state: str }) }),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const latencySummarySchema: z.ZodObject<{
|
|
3
|
+
count: z.ZodNumber;
|
|
4
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
5
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
6
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
7
|
+
totalMs: z.ZodNumber;
|
|
8
|
+
}, z.core.$loose>;
|
|
9
|
+
export declare const healthSnapshotSchema: z.ZodObject<{
|
|
10
|
+
version: z.ZodLiteral<1>;
|
|
11
|
+
windowSeconds: z.ZodLiteral<300>;
|
|
12
|
+
uptimeSeconds: z.ZodNumber;
|
|
13
|
+
requests: z.ZodObject<{
|
|
14
|
+
count: z.ZodNumber;
|
|
15
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
16
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
17
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
18
|
+
totalMs: z.ZodNumber;
|
|
19
|
+
}, z.core.$loose>;
|
|
20
|
+
routes: z.ZodRecord<z.ZodEnum<{
|
|
21
|
+
assets: "assets";
|
|
22
|
+
auth: "auth";
|
|
23
|
+
chat: "chat";
|
|
24
|
+
"chat-stream": "chat-stream";
|
|
25
|
+
cli: "cli";
|
|
26
|
+
console: "console";
|
|
27
|
+
decisions: "decisions";
|
|
28
|
+
"flow-stream": "flow-stream";
|
|
29
|
+
flows: "flows";
|
|
30
|
+
health: "health";
|
|
31
|
+
mcp: "mcp";
|
|
32
|
+
metrics: "metrics";
|
|
33
|
+
other: "other";
|
|
34
|
+
projects: "projects";
|
|
35
|
+
results: "results";
|
|
36
|
+
sessions: "sessions";
|
|
37
|
+
settings: "settings";
|
|
38
|
+
tasks: "tasks";
|
|
39
|
+
team: "team";
|
|
40
|
+
"team-stream": "team-stream";
|
|
41
|
+
}> & z.core.$partial, z.ZodObject<{
|
|
42
|
+
count: z.ZodNumber;
|
|
43
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
44
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
45
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
46
|
+
totalMs: z.ZodNumber;
|
|
47
|
+
}, z.core.$loose>>;
|
|
48
|
+
eventLoop: z.ZodObject<{
|
|
49
|
+
count: z.ZodNumber;
|
|
50
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
51
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
52
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
53
|
+
totalMs: z.ZodNumber;
|
|
54
|
+
}, z.core.$loose>;
|
|
55
|
+
writes: z.ZodObject<{
|
|
56
|
+
wait: z.ZodObject<{
|
|
57
|
+
count: z.ZodNumber;
|
|
58
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
59
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
60
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
61
|
+
totalMs: z.ZodNumber;
|
|
62
|
+
}, z.core.$loose>;
|
|
63
|
+
hold: z.ZodObject<{
|
|
64
|
+
count: z.ZodNumber;
|
|
65
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
66
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
67
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
68
|
+
totalMs: z.ZodNumber;
|
|
69
|
+
}, z.core.$loose>;
|
|
70
|
+
statement: z.ZodObject<{
|
|
71
|
+
count: z.ZodNumber;
|
|
72
|
+
p50Ms: z.ZodNullable<z.ZodNumber>;
|
|
73
|
+
p99Ms: z.ZodNullable<z.ZodNumber>;
|
|
74
|
+
maxMs: z.ZodNullable<z.ZodNumber>;
|
|
75
|
+
totalMs: z.ZodNumber;
|
|
76
|
+
}, z.core.$loose>;
|
|
77
|
+
}, z.core.$loose>;
|
|
78
|
+
streams: z.ZodObject<{
|
|
79
|
+
open: z.ZodNumber;
|
|
80
|
+
queuedBytes: z.ZodNumber;
|
|
81
|
+
maxQueuedBytes: z.ZodNumber;
|
|
82
|
+
}, z.core.$loose>;
|
|
83
|
+
budgetRefusals: z.ZodNumber;
|
|
84
|
+
}, z.core.$loose>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ROUTE_FAMILIES } from '../server-telemetry.js';
|
|
3
|
+
const count = z.number().int().nonnegative();
|
|
4
|
+
const milliseconds = z.number().nonnegative();
|
|
5
|
+
export const latencySummarySchema = z.looseObject({ count, p50Ms: milliseconds.nullable(), p99Ms: milliseconds.nullable(), maxMs: milliseconds.nullable(), totalMs: milliseconds });
|
|
6
|
+
export const healthSnapshotSchema = z.looseObject({
|
|
7
|
+
version: z.literal(1), windowSeconds: z.literal(300), uptimeSeconds: z.number().nonnegative(),
|
|
8
|
+
requests: latencySummarySchema, routes: z.partialRecord(z.enum(ROUTE_FAMILIES), latencySummarySchema), eventLoop: latencySummarySchema,
|
|
9
|
+
writes: z.looseObject({ wait: latencySummarySchema, hold: latencySummarySchema, statement: latencySummarySchema }),
|
|
10
|
+
streams: z.looseObject({ open: count, queuedBytes: count, maxQueuedBytes: count }), budgetRefusals: count,
|
|
11
|
+
});
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -94,13 +94,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
95
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
96
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
+
error?: never;
|
|
97
98
|
active: boolean;
|
|
98
99
|
phase: string;
|
|
99
100
|
detail: string;
|
|
100
101
|
running: boolean;
|
|
101
102
|
canResume: boolean;
|
|
102
103
|
canCancel: boolean;
|
|
103
|
-
error?: never;
|
|
104
104
|
automaticRecovery?: never;
|
|
105
105
|
recoveryAttempts?: never;
|
|
106
106
|
recoveryRepairs?: never;
|
package/dist/flow-live.d.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LiveRoomOptions, type LiveRooms, type LiveViewer } from "./live-bus.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
3
|
export type FlowPresence = {
|
|
4
4
|
name: string;
|
|
5
5
|
cards: number[];
|
|
6
6
|
editing: boolean;
|
|
7
7
|
};
|
|
8
|
-
export type FlowViewer = {
|
|
9
|
-
name: string;
|
|
8
|
+
export type FlowViewer = LiveViewer & {
|
|
10
9
|
card: number | null;
|
|
11
10
|
editing: boolean;
|
|
12
|
-
response: ServerResponse;
|
|
13
|
-
valid: () => boolean;
|
|
14
11
|
};
|
|
15
12
|
/** What changes when anything on the canvas changes: metadata only, kept short and opaque. */
|
|
16
13
|
export declare function flowFingerprint(store: Store, flow: number, now?: Date): string | null;
|
|
17
|
-
export type FlowRooms =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** Every stream, ended (the server is closing). */
|
|
21
|
-
close: () => void;
|
|
22
|
-
size: () => number;
|
|
23
|
-
};
|
|
24
|
-
export declare function createFlowRooms(fingerprint: (flow: number) => string | null, tickMs?: number): FlowRooms;
|
|
14
|
+
export type FlowRooms = LiveRooms<number, FlowViewer>;
|
|
15
|
+
/** Rooms keyed by flow. With a bus, a write reaches every open page at once; without one, only the safety net. */
|
|
16
|
+
export declare function createFlowRooms(fingerprint: (flow: number) => string | null, options?: LiveRoomOptions): FlowRooms;
|
package/dist/flow-live.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* The live canvas (v88): a flow open in several browsers stays the same in
|
|
3
3
|
* all of them, and each shows who else has it open.
|
|
4
4
|
*
|
|
5
|
-
* - A change nudge.
|
|
6
|
-
* checks the flow's fingerprint (its revision, cards, moves, comments,
|
|
7
|
-
* watchers, step runs, linked task progress and triggers); when it
|
|
5
|
+
* - A change nudge. When the live bus says the workspace was written
|
|
6
|
+
* (live-bus.ts), one small read checks the flow's fingerprint (its revision, cards, moves, comments,
|
|
7
|
+
* watchers, step runs, linked task progress and triggers); when it changed, every open page
|
|
8
8
|
* hears "change" and reads the flow again the usual way. The nudge
|
|
9
9
|
* carries no card data: what a page shows is always what its own read
|
|
10
10
|
* returned, under its own checks.
|
|
@@ -13,13 +13,11 @@
|
|
|
13
13
|
* name. It lives in memory only and goes when the page closes or hides.
|
|
14
14
|
*
|
|
15
15
|
* Streams are refresh hints only: nothing here writes, and a page that
|
|
16
|
-
* can't keep one open falls back to reading every few seconds.
|
|
16
|
+
* can't keep one open falls back to reading every few seconds. With nothing
|
|
17
|
+
* written, an open flow costs no reads; a 30-second check heals a miss.
|
|
17
18
|
*/
|
|
18
19
|
import { createHash } from "node:crypto";
|
|
19
|
-
|
|
20
|
-
const CHECK_EVERY = 15;
|
|
21
|
-
const HEARTBEAT_EVERY = 15;
|
|
22
|
-
const PRESENCE_SETTLE_MS = 150;
|
|
20
|
+
import { createLiveRooms } from "./live-bus.js";
|
|
23
21
|
// Statements belong to their database, without keeping closed Stores alive.
|
|
24
22
|
const fingerprintStatements = new WeakMap();
|
|
25
23
|
function fingerprintStatement(store) {
|
|
@@ -94,17 +92,12 @@ export function flowFingerprint(store, flow, now = new Date()) {
|
|
|
94
92
|
const row = fingerprintStatement(store).get({ $flow: flow, $now: now.toISOString() });
|
|
95
93
|
return row === undefined ? null : createHash("sha256").update(Object.values(row).map(String).join("|")).digest("hex").slice(0, 16);
|
|
96
94
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const write = (viewer, event, data) => {
|
|
100
|
-
if (viewer.response.writableEnded || viewer.response.destroyed)
|
|
101
|
-
return;
|
|
102
|
-
viewer.response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
103
|
-
};
|
|
95
|
+
/** Rooms keyed by flow. With a bus, a write reaches every open page at once; without one, only the safety net. */
|
|
96
|
+
export function createFlowRooms(fingerprint, options = {}) {
|
|
104
97
|
/** Everyone on the flow but you, one line per person however many pages they have open. */
|
|
105
|
-
const others = (
|
|
98
|
+
const others = (viewers, me) => {
|
|
106
99
|
const people = new Map();
|
|
107
|
-
for (const one of
|
|
100
|
+
for (const one of viewers) {
|
|
108
101
|
if (one.name === me)
|
|
109
102
|
continue;
|
|
110
103
|
const seen = people.get(one.name) ?? { name: one.name, cards: [], editing: false };
|
|
@@ -115,97 +108,5 @@ export function createFlowRooms(fingerprint, tickMs = TICK_MS) {
|
|
|
115
108
|
}
|
|
116
109
|
return [...people.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
117
110
|
};
|
|
118
|
-
|
|
119
|
-
// A page that moves from one card to the next leaves and joins at once: say it once, after it settles.
|
|
120
|
-
if (room.settling !== null)
|
|
121
|
-
clearTimeout(room.settling);
|
|
122
|
-
room.settling = setTimeout(() => {
|
|
123
|
-
room.settling = null;
|
|
124
|
-
for (const viewer of room.viewers)
|
|
125
|
-
write(viewer, "here", { people: others(room, viewer.name) });
|
|
126
|
-
}, PRESENCE_SETTLE_MS);
|
|
127
|
-
room.settling.unref();
|
|
128
|
-
};
|
|
129
|
-
const leave = (flow, viewer) => {
|
|
130
|
-
const room = rooms.get(flow);
|
|
131
|
-
if (room === undefined || !room.viewers.delete(viewer))
|
|
132
|
-
return;
|
|
133
|
-
if (!viewer.response.writableEnded)
|
|
134
|
-
viewer.response.end();
|
|
135
|
-
if (room.viewers.size > 0) {
|
|
136
|
-
announce(room);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (room.timer !== null)
|
|
140
|
-
clearInterval(room.timer);
|
|
141
|
-
if (room.settling !== null)
|
|
142
|
-
clearTimeout(room.settling);
|
|
143
|
-
rooms.delete(flow);
|
|
144
|
-
};
|
|
145
|
-
const tick = (flow, room) => {
|
|
146
|
-
room.ticks += 1;
|
|
147
|
-
if (room.ticks % CHECK_EVERY === 0) {
|
|
148
|
-
// A signed-out or narrowed account stops hearing about the flow.
|
|
149
|
-
for (const viewer of [...room.viewers])
|
|
150
|
-
if (!viewer.valid()) {
|
|
151
|
-
write(viewer, "gone", {});
|
|
152
|
-
leave(flow, viewer);
|
|
153
|
-
}
|
|
154
|
-
if (!rooms.has(flow))
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
let now;
|
|
158
|
-
try {
|
|
159
|
-
now = fingerprint(flow);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return; /* a busy database: the next tick reads again */
|
|
163
|
-
}
|
|
164
|
-
if (now === null) {
|
|
165
|
-
for (const viewer of [...room.viewers]) {
|
|
166
|
-
write(viewer, "gone", {});
|
|
167
|
-
leave(flow, viewer);
|
|
168
|
-
}
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (now !== room.last) {
|
|
172
|
-
room.last = now;
|
|
173
|
-
for (const viewer of room.viewers)
|
|
174
|
-
write(viewer, "change", { at: now });
|
|
175
|
-
}
|
|
176
|
-
else if (room.ticks % HEARTBEAT_EVERY === 0) {
|
|
177
|
-
for (const viewer of room.viewers)
|
|
178
|
-
if (!viewer.response.writableEnded)
|
|
179
|
-
viewer.response.write(": keep-alive\n\n");
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
return {
|
|
183
|
-
join(flow, viewer) {
|
|
184
|
-
let room = rooms.get(flow);
|
|
185
|
-
if (room === undefined) {
|
|
186
|
-
let last = null;
|
|
187
|
-
try {
|
|
188
|
-
last = fingerprint(flow);
|
|
189
|
-
}
|
|
190
|
-
catch { /* the first tick reads it */ }
|
|
191
|
-
room = { viewers: new Set(), last, timer: null, ticks: 0, settling: null };
|
|
192
|
-
const created = room;
|
|
193
|
-
room.timer = setInterval(() => tick(flow, created), tickMs);
|
|
194
|
-
room.timer.unref();
|
|
195
|
-
rooms.set(flow, room);
|
|
196
|
-
}
|
|
197
|
-
room.viewers.add(viewer);
|
|
198
|
-
write(viewer, "change", { at: room.last });
|
|
199
|
-
write(viewer, "here", { people: others(room, viewer.name) });
|
|
200
|
-
announce(room);
|
|
201
|
-
const gone = () => leave(flow, viewer);
|
|
202
|
-
viewer.response.once("close", gone);
|
|
203
|
-
},
|
|
204
|
-
close() {
|
|
205
|
-
for (const [flow, room] of [...rooms])
|
|
206
|
-
for (const viewer of [...room.viewers])
|
|
207
|
-
leave(flow, viewer);
|
|
208
|
-
},
|
|
209
|
-
size: () => [...rooms.values()].reduce((sum, room) => sum + room.viewers.size, 0),
|
|
210
|
-
};
|
|
111
|
+
return createLiveRooms(fingerprint, others, options);
|
|
211
112
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type TeamCliOptions } from './team-cli.js';
|
|
2
|
+
/** Read the running server, never a fresh CLI process's empty counters or the local database. */
|
|
3
|
+
export declare function runHealthCommand(argv: readonly string[], write: (line: string) => void, options?: TeamCliOptions): Promise<number>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { bearerOf, centralProfile } from './team-cli.js';
|
|
2
|
+
import { envelopeJson } from './envelope.js';
|
|
3
|
+
import { healthSnapshotSchema } from './contracts/health.js';
|
|
4
|
+
/** Read the running server, never a fresh CLI process's empty counters or the local database. */
|
|
5
|
+
export async function runHealthCommand(argv, write, options = {}) {
|
|
6
|
+
const json = argv.includes('--json');
|
|
7
|
+
const fail = (message, reason = 'unavailable', code = 1) => {
|
|
8
|
+
write(json ? envelopeJson({ command: 'health', ok: false, reason, message }) : message);
|
|
9
|
+
return code;
|
|
10
|
+
};
|
|
11
|
+
if (argv.includes('--help')) {
|
|
12
|
+
write('toolroll health [--profile <name>] [--json]\nRecent latency, event-loop delay, write waits and live streams from the connected server. An instance operator must sign in with toolroll connect first.');
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
let profileName;
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
if (argv[i] === '--json')
|
|
18
|
+
continue;
|
|
19
|
+
if (argv[i] === '--profile' && profileName === undefined && argv[i + 1] && !argv[i + 1].startsWith('-')) {
|
|
20
|
+
profileName = argv[++i];
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
return fail('Use toolroll health [--profile <name>] [--json].', 'usage', 2);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const profile = centralProfile(options, profileName);
|
|
27
|
+
if (!profile)
|
|
28
|
+
return fail('Connect to the server first with toolroll connect, then run toolroll health.', 'not-connected', 2);
|
|
29
|
+
const timeout = AbortSignal.timeout(30_000);
|
|
30
|
+
const response = await (options.fetch ?? fetch)(`${profile.origin}/health`, {
|
|
31
|
+
redirect: 'manual', credentials: 'omit', signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
|
|
32
|
+
headers: { authorization: bearerOf(profile), accept: json ? 'application/json' : 'text/plain' },
|
|
33
|
+
});
|
|
34
|
+
if (response.status !== 200) {
|
|
35
|
+
await response.body?.cancel();
|
|
36
|
+
return fail(response.status === 401 || response.status === 403 || response.status === 302
|
|
37
|
+
? 'Sign in as an instance operator to read server health.'
|
|
38
|
+
: response.status === 404 ? 'This server does not provide performance health yet. Update the server.' : 'Server health is unavailable. Try again shortly.');
|
|
39
|
+
}
|
|
40
|
+
// A health response is bounded by the fixed route vocabulary. Refuse unrelated HTML and oversized replies.
|
|
41
|
+
const reader = response.body?.getReader();
|
|
42
|
+
if (!reader)
|
|
43
|
+
return fail('The server returned no health measurements.');
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let size = 0;
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const part = await reader.read();
|
|
49
|
+
if (part.done)
|
|
50
|
+
break;
|
|
51
|
+
size += part.value.byteLength;
|
|
52
|
+
if (size > 128 * 1024) {
|
|
53
|
+
await reader.cancel();
|
|
54
|
+
return fail('The server returned an unexpected health response.');
|
|
55
|
+
}
|
|
56
|
+
chunks.push(part.value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
reader.releaseLock();
|
|
61
|
+
}
|
|
62
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
|
|
63
|
+
if (json) {
|
|
64
|
+
const parsed = healthSnapshotSchema.safeParse(JSON.parse(text));
|
|
65
|
+
if (!parsed.success)
|
|
66
|
+
return fail('The server returned an unexpected health response.');
|
|
67
|
+
write(envelopeJson({ command: 'health', ok: true, health: parsed.data }));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (!response.headers.get('content-type')?.startsWith('text/plain') || !text.startsWith('Server health'))
|
|
71
|
+
return fail('The server returned an unexpected health response.');
|
|
72
|
+
write(text.trimEnd());
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return fail('Could not read server health. Check the saved connection and server availability.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live views push on write. Nothing here polls once a second.
|
|
3
|
+
*
|
|
4
|
+
* - The bus. The database's own revision triggers (workspace-revision.ts)
|
|
5
|
+
* move one counter on every meaningful write. After each in-process
|
|
6
|
+
* transact() COMMIT, and whenever another process (the worker, the CLI)
|
|
7
|
+
* writes the database's write-ahead log, the revision is read once; when
|
|
8
|
+
* it moved, every subscriber hears `{ revision }`. A rolled-back or quiet
|
|
9
|
+
* write moves nothing and says nothing. A page left open with nothing
|
|
10
|
+
* being written costs no database work at all.
|
|
11
|
+
* - The rooms. Each open task or flow is a room keyed by what it shows.
|
|
12
|
+
* When the bus speaks, a room rechecks every viewer's access (a signed-out
|
|
13
|
+
* or narrowed account is dropped), takes its fingerprint once, and only if
|
|
14
|
+
* that moved tells its pages `change` with the fingerprint and revision —
|
|
15
|
+
* an invalidation, never content. The page reads itself again under its
|
|
16
|
+
* own checks, so redaction stays the page's.
|
|
17
|
+
* - The safety net. Every room also checks itself about every 30 s, so a
|
|
18
|
+
* missed signal heals, and sends a keep-alive between.
|
|
19
|
+
* - Back-pressure. Each page's stream is bounded: a page that falls behind
|
|
20
|
+
* gets one `reload` and ends so its EventSource can reconnect.
|
|
21
|
+
*
|
|
22
|
+
* Transport seam: a future WebSocket (live cursors) subscribes to the same
|
|
23
|
+
* LiveBus beside these SSE rooms; nothing in the Store or the triggers changes.
|
|
24
|
+
*/
|
|
25
|
+
import { type FSWatcher } from "node:fs";
|
|
26
|
+
import type { ServerResponse } from "node:http";
|
|
27
|
+
import type { Store } from "./store.js";
|
|
28
|
+
export type LiveChange = {
|
|
29
|
+
revision: string;
|
|
30
|
+
};
|
|
31
|
+
export type LiveBus = {
|
|
32
|
+
subscribe: (listener: (change: LiveChange) => void) => () => void;
|
|
33
|
+
publish: (change: LiveChange) => void;
|
|
34
|
+
/** The last revision heard, if any. */
|
|
35
|
+
revision: () => string | null;
|
|
36
|
+
listeners: () => number;
|
|
37
|
+
};
|
|
38
|
+
export declare function createLiveBus(): LiveBus;
|
|
39
|
+
export type WorkspaceFollower = {
|
|
40
|
+
close: () => void; /** Read the revision now (tests, and a commit signal). */
|
|
41
|
+
check: () => void;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Publish the workspace revision when it moves: after this process commits,
|
|
45
|
+
* and when the database's write-ahead log changes under another process.
|
|
46
|
+
* Signals in one turn of the event loop coalesce into one read.
|
|
47
|
+
*/
|
|
48
|
+
export declare function followWorkspace(store: Store, current: () => string, bus: LiveBus, options?: {
|
|
49
|
+
file?: string | null;
|
|
50
|
+
watchFile?: (path: string, listener: () => void) => FSWatcher | null;
|
|
51
|
+
}): WorkspaceFollower;
|
|
52
|
+
/** About every 30 s a room checks itself; a keep-alive goes between. */
|
|
53
|
+
export declare const SAFETY_NET_MS = 30000;
|
|
54
|
+
export declare const KEEP_ALIVE_MS = 15000;
|
|
55
|
+
/** What a page's stream may hold unsent before it is "behind". */
|
|
56
|
+
export declare const STREAM_LIMIT_BYTES: number;
|
|
57
|
+
/** One page's stream, bounded. Overflow is terminal, even below Node's drain threshold. */
|
|
58
|
+
export declare class LiveStream {
|
|
59
|
+
readonly response: ServerResponse;
|
|
60
|
+
private readonly limit;
|
|
61
|
+
private readonly onOverflow;
|
|
62
|
+
private ended;
|
|
63
|
+
constructor(response: ServerResponse, limit?: number, onOverflow?: () => void);
|
|
64
|
+
get open(): boolean;
|
|
65
|
+
private overflow;
|
|
66
|
+
private write;
|
|
67
|
+
send(event: string, data: unknown): void;
|
|
68
|
+
keepAlive(): void;
|
|
69
|
+
}
|
|
70
|
+
export type LiveViewer = {
|
|
71
|
+
name: string;
|
|
72
|
+
response: ServerResponse;
|
|
73
|
+
valid: () => boolean;
|
|
74
|
+
};
|
|
75
|
+
export type LiveRooms<K, V extends LiveViewer> = {
|
|
76
|
+
/** Keep this page's stream on the room until it closes; the first events say where it stands and who's here. */
|
|
77
|
+
join: (key: K, viewer: V) => void;
|
|
78
|
+
/** Every stream, ended (the server is closing). */
|
|
79
|
+
close: () => void;
|
|
80
|
+
size: () => number;
|
|
81
|
+
};
|
|
82
|
+
export type LiveRoomOptions = {
|
|
83
|
+
bus?: LiveBus | null;
|
|
84
|
+
safetyNetMs?: number;
|
|
85
|
+
keepAliveMs?: number;
|
|
86
|
+
limitBytes?: number;
|
|
87
|
+
};
|
|
88
|
+
/** Rooms of open pages that hear the bus. `presence` says who else is here, as each page should see it. */
|
|
89
|
+
export declare function createLiveRooms<K, V extends LiveViewer>(fingerprint: (key: K) => string | null, presence: (viewers: readonly V[], me: string) => unknown, options?: LiveRoomOptions): LiveRooms<K, V>;
|