@theruntime/protocol 0.0.0 → 0.0.1
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/LICENSE +21 -0
- package/package.json +14 -4
- package/src/error.ts +54 -0
- package/src/events/index.ts +51 -0
- package/src/guidance/index.ts +3 -0
- package/src/index.ts +46 -0
- package/src/logs/index.ts +46 -0
- package/src/notifications/event.ts +62 -0
- package/src/notifications.ts +5 -0
- package/src/operations/add-component.ts +121 -0
- package/src/operations/add-package.ts +58 -0
- package/src/operations/app-record.ts +29 -0
- package/src/operations/archive-route.ts +48 -0
- package/src/operations/capture-snapshot.ts +46 -0
- package/src/operations/declaration.ts +115 -0
- package/src/operations/declare-route.ts +66 -0
- package/src/operations/delete-app.ts +47 -0
- package/src/operations/delete-record.ts +46 -0
- package/src/operations/describe-route.ts +122 -0
- package/src/operations/emit-event.ts +66 -0
- package/src/operations/get-record.ts +44 -0
- package/src/operations/get-snapshot.ts +64 -0
- package/src/operations/guidance.ts +91 -0
- package/src/operations/heartbeat.ts +27 -0
- package/src/operations/identify.ts +50 -0
- package/src/operations/list-packages.ts +72 -0
- package/src/operations/list-presence.ts +64 -0
- package/src/operations/list-quick-actions.ts +60 -0
- package/src/operations/list-records.ts +55 -0
- package/src/operations/list-route-logs.ts +98 -0
- package/src/operations/list-routes.ts +101 -0
- package/src/operations/list-subscribers.ts +75 -0
- package/src/operations/list-subscriptions.ts +98 -0
- package/src/operations/list-templates.ts +71 -0
- package/src/operations/log-line.ts +222 -0
- package/src/operations/parse-helpers.ts +136 -0
- package/src/operations/query-selection.ts +44 -0
- package/src/operations/read-daemon-log.ts +29 -0
- package/src/operations/read-route-log.ts +39 -0
- package/src/operations/reboot-route.ts +45 -0
- package/src/operations/record-status.ts +18 -0
- package/src/operations/register-route.ts +135 -0
- package/src/operations/save-record.ts +55 -0
- package/src/operations/set-record-status.ts +63 -0
- package/src/operations/set-route-visibility.ts +48 -0
- package/src/operations/set-selection.ts +53 -0
- package/src/operations/subscribe.ts +47 -0
- package/src/operations/unregister-route.ts +36 -0
- package/src/operations/unsubscribe.ts +44 -0
- package/src/operations/visibility.ts +9 -0
- package/src/operations.ts +39 -0
- package/src/payloads/app-ready.ts +24 -0
- package/src/payloads/refusal.ts +44 -0
- package/src/payloads.ts +11 -0
- package/src/quick-actions/index.ts +12 -0
- package/src/readiness.ts +52 -0
- package/src/records/index.ts +46 -0
- package/src/routes/index.ts +104 -0
- package/src/selection/index.ts +16 -0
- package/src/sessions/index.ts +24 -0
- package/src/snapshots/index.ts +20 -0
- package/src/templates/index.ts +14 -0
- package/src/toolchain/index.ts +36 -0
- package/src/version.ts +3 -0
- package/README.md +0 -3
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
|
|
2
|
+
import {
|
|
3
|
+
asRecord,
|
|
4
|
+
optionalPositiveInteger,
|
|
5
|
+
optionalString,
|
|
6
|
+
requireNonNegativeInteger,
|
|
7
|
+
requireOperation,
|
|
8
|
+
requireProtocol,
|
|
9
|
+
requireString,
|
|
10
|
+
} from "./parse-helpers";
|
|
11
|
+
|
|
12
|
+
const NOUN = "a listSubscriptions input";
|
|
13
|
+
|
|
14
|
+
export interface ListSubscriptionsInput {
|
|
15
|
+
readonly operation: "listSubscriptions";
|
|
16
|
+
readonly protocol: ProtocolVersion;
|
|
17
|
+
/** The claimed subject of the read: the session the caller is asking about. The daemon does
|
|
18
|
+
* not verify it. A caller states which session it means, and gets that session's rows. */
|
|
19
|
+
readonly sid: string;
|
|
20
|
+
readonly limit?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseListSubscriptionsInput(value: unknown): ListSubscriptionsInput {
|
|
24
|
+
const record = asRecord(value, NOUN);
|
|
25
|
+
requireOperation(record, "listSubscriptions", NOUN);
|
|
26
|
+
requireProtocol(record, NOUN);
|
|
27
|
+
const sid = requireString(record, "sid", NOUN);
|
|
28
|
+
const limit = optionalPositiveInteger(record, "limit", NOUN);
|
|
29
|
+
return {
|
|
30
|
+
operation: "listSubscriptions",
|
|
31
|
+
protocol: PROTOCOL_VERSION,
|
|
32
|
+
sid,
|
|
33
|
+
...(limit !== undefined ? { limit } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function serializeListSubscriptionsInput(input: ListSubscriptionsInput): string {
|
|
38
|
+
return JSON.stringify(input);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SUMMARY_NOUN = "a subscription summary";
|
|
42
|
+
|
|
43
|
+
export interface SubscriptionSummary {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly dir: string;
|
|
47
|
+
readonly updatedAt: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseSubscriptionSummary(value: unknown): SubscriptionSummary {
|
|
51
|
+
const record = asRecord(value, SUMMARY_NOUN);
|
|
52
|
+
return {
|
|
53
|
+
name: requireString(record, "name", SUMMARY_NOUN),
|
|
54
|
+
url: requireString(record, "url", SUMMARY_NOUN),
|
|
55
|
+
dir: requireString(record, "dir", SUMMARY_NOUN),
|
|
56
|
+
updatedAt: requireString(record, "updatedAt", SUMMARY_NOUN),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const RESULT_NOUN = "a listSubscriptions result";
|
|
61
|
+
|
|
62
|
+
export interface ListSubscriptionsResult {
|
|
63
|
+
readonly sid: string;
|
|
64
|
+
readonly subscriptions: readonly SubscriptionSummary[];
|
|
65
|
+
/** True when the limit cut rows the session holds. What was cut was touched longer ago than
|
|
66
|
+
* what was returned. */
|
|
67
|
+
readonly truncated: boolean;
|
|
68
|
+
/** Rows naming a route the store no longer holds. unregisterRoute keeps a session's rows, so
|
|
69
|
+
* a subscription outlives its route. Such a row carries no url and is counted, not returned. */
|
|
70
|
+
readonly unregistered: number;
|
|
71
|
+
/** The key the daemon's own resolver produced for this connection, when it produced one and it
|
|
72
|
+
* differs from sid. The read still answers for sid. */
|
|
73
|
+
readonly observed?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseListSubscriptionsResult(value: unknown): ListSubscriptionsResult {
|
|
77
|
+
const record = asRecord(value, RESULT_NOUN);
|
|
78
|
+
const sid = requireString(record, "sid", RESULT_NOUN);
|
|
79
|
+
if (!Array.isArray(record.subscriptions)) {
|
|
80
|
+
throw new Error(`${RESULT_NOUN} carries an array of subscriptions`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof record.truncated !== "boolean") {
|
|
83
|
+
throw new Error(`${RESULT_NOUN} says whether rows were cut`);
|
|
84
|
+
}
|
|
85
|
+
const unregistered = requireNonNegativeInteger(record, "unregistered", RESULT_NOUN);
|
|
86
|
+
const observed = optionalString(record, "observed", RESULT_NOUN);
|
|
87
|
+
return {
|
|
88
|
+
sid,
|
|
89
|
+
subscriptions: record.subscriptions.map(parseSubscriptionSummary),
|
|
90
|
+
truncated: record.truncated,
|
|
91
|
+
unregistered,
|
|
92
|
+
...(observed !== undefined ? { observed } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function serializeListSubscriptionsResult(result: ListSubscriptionsResult): string {
|
|
97
|
+
return JSON.stringify(result);
|
|
98
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
|
|
2
|
+
import { asRecord, requireOperation, requireProtocol, requireString } from "./parse-helpers";
|
|
3
|
+
|
|
4
|
+
const NOUN = "a listTemplates input";
|
|
5
|
+
|
|
6
|
+
export interface ListTemplatesInput {
|
|
7
|
+
readonly operation: "listTemplates";
|
|
8
|
+
readonly protocol: ProtocolVersion;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseListTemplatesInput(value: unknown): ListTemplatesInput {
|
|
12
|
+
const record = asRecord(value, NOUN);
|
|
13
|
+
requireOperation(record, "listTemplates", NOUN);
|
|
14
|
+
requireProtocol(record, NOUN);
|
|
15
|
+
return { operation: "listTemplates", protocol: PROTOCOL_VERSION };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function serializeListTemplatesInput(input: ListTemplatesInput): string {
|
|
19
|
+
return JSON.stringify(input);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One template's whole metadata file, field for field. A template directory carries exactly this
|
|
24
|
+
* shape, so a bundle that ships a register later carries it too and needs no second parser.
|
|
25
|
+
* `preview` is a path relative to the template's own directory, or null where none is drawn yet.
|
|
26
|
+
*/
|
|
27
|
+
export interface TemplateSummary {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly title: string;
|
|
30
|
+
readonly description: string;
|
|
31
|
+
readonly purpose: string;
|
|
32
|
+
readonly preview: string | null;
|
|
33
|
+
readonly version: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SUMMARY = "a template summary";
|
|
37
|
+
|
|
38
|
+
export function parseTemplateSummary(value: unknown): TemplateSummary {
|
|
39
|
+
const record = asRecord(value, SUMMARY);
|
|
40
|
+
if (record.preview !== null && typeof record.preview !== "string") {
|
|
41
|
+
throw new Error(`${SUMMARY}'s preview is a path or null`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
id: requireString(record, "id", SUMMARY),
|
|
45
|
+
title: requireString(record, "title", SUMMARY),
|
|
46
|
+
description: requireString(record, "description", SUMMARY),
|
|
47
|
+
purpose: requireString(record, "purpose", SUMMARY),
|
|
48
|
+
preview: record.preview,
|
|
49
|
+
version: requireString(record, "version", SUMMARY),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function serializeTemplateSummary(summary: TemplateSummary): string {
|
|
54
|
+
return JSON.stringify(summary);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ListTemplatesResult {
|
|
58
|
+
readonly templates: readonly TemplateSummary[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseListTemplatesResult(value: unknown): ListTemplatesResult {
|
|
62
|
+
const record = asRecord(value, "a listTemplates result");
|
|
63
|
+
if (!Array.isArray(record.templates)) {
|
|
64
|
+
throw new Error("a listTemplates result carries an array of templates");
|
|
65
|
+
}
|
|
66
|
+
return { templates: record.templates.map(parseTemplateSummary) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function serializeListTemplatesResult(result: ListTemplatesResult): string {
|
|
70
|
+
return JSON.stringify(result);
|
|
71
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/** The shape of one line in a log: what the daemon writes, and what a log read returns. */
|
|
2
|
+
import { asRecord, optionalPositiveInteger, optionalString, requireString } from "./parse-helpers";
|
|
3
|
+
|
|
4
|
+
export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
|
|
5
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every tag a line may carry. A tag is what a reader counts, so renaming one breaks every
|
|
9
|
+
* comparison against a log written before the rename. A tag is added here, never edited.
|
|
10
|
+
*/
|
|
11
|
+
export const LOG_TAGS = [
|
|
12
|
+
"daemon.start",
|
|
13
|
+
"daemon.stop",
|
|
14
|
+
"daemon.stop_error",
|
|
15
|
+
"daemon.stop_deadline",
|
|
16
|
+
"daemon.port_fallback",
|
|
17
|
+
"daemon.sessions_contract",
|
|
18
|
+
"daemon.chrome_assets",
|
|
19
|
+
"daemon.subscriptions_swept",
|
|
20
|
+
"daemon.events_pruned",
|
|
21
|
+
"daemon.processes_unreadable",
|
|
22
|
+
"daemon.data_dir_owned",
|
|
23
|
+
"session.identity_conflict",
|
|
24
|
+
"socket.frame_error",
|
|
25
|
+
"socket.connection_error",
|
|
26
|
+
"mcp.session_close_error",
|
|
27
|
+
"register.theme_unresolved",
|
|
28
|
+
"register.theme_scheme_missing",
|
|
29
|
+
"register.no_tokens",
|
|
30
|
+
"toolchain.unresolved",
|
|
31
|
+
"toolchain.manifest_unparsed",
|
|
32
|
+
"toolchain.package_install",
|
|
33
|
+
"toolchain.shadowed",
|
|
34
|
+
"toolchain.two_copies",
|
|
35
|
+
"toolchain.host_packages_cut",
|
|
36
|
+
"toolchain.host_copy",
|
|
37
|
+
"toolchain.react_unresolved",
|
|
38
|
+
"toolchain.react_major",
|
|
39
|
+
"route.spawn",
|
|
40
|
+
"route.ready",
|
|
41
|
+
"route.painted",
|
|
42
|
+
"route.exit",
|
|
43
|
+
"route.crash_restart",
|
|
44
|
+
"route.crash_refused",
|
|
45
|
+
"route.reap",
|
|
46
|
+
"route.capacity_held",
|
|
47
|
+
"route.cpu_kill",
|
|
48
|
+
"route.entry_dropped",
|
|
49
|
+
"route.orphan_reaped",
|
|
50
|
+
"route.record_unremovable",
|
|
51
|
+
"route.second_tailwind_root",
|
|
52
|
+
"route.no_tailwind_root",
|
|
53
|
+
"route.unresolved_names",
|
|
54
|
+
"route.source_missing",
|
|
55
|
+
"route.stdout",
|
|
56
|
+
"route.stderr",
|
|
57
|
+
"vite.info",
|
|
58
|
+
"vite.warn",
|
|
59
|
+
"vite.error",
|
|
60
|
+
"vite.unresolved_import",
|
|
61
|
+
"vite.optimize_start",
|
|
62
|
+
"vite.optimize_done",
|
|
63
|
+
"vite.optimized_deps",
|
|
64
|
+
"vite.reoptimize",
|
|
65
|
+
"vite.full_reload",
|
|
66
|
+
"vite.hot_update",
|
|
67
|
+
"browser.error",
|
|
68
|
+
"browser.console",
|
|
69
|
+
"proxy.window",
|
|
70
|
+
"proxy.non_2xx",
|
|
71
|
+
"proxy.outdated_dep",
|
|
72
|
+
"proxy.outdated_dep_run",
|
|
73
|
+
"proxy.unreachable",
|
|
74
|
+
"log.dropped",
|
|
75
|
+
"log.swept",
|
|
76
|
+
] as const;
|
|
77
|
+
export type LogTag = (typeof LOG_TAGS)[number];
|
|
78
|
+
|
|
79
|
+
/** What a field beside the five may hold. A number is a number, never a number inside msg. */
|
|
80
|
+
export type LogValue = string | number | boolean;
|
|
81
|
+
|
|
82
|
+
/** A line before the sink stamps its timestamp. */
|
|
83
|
+
export interface LogEntry {
|
|
84
|
+
readonly level: LogLevel;
|
|
85
|
+
readonly tag: LogTag;
|
|
86
|
+
readonly route?: string;
|
|
87
|
+
readonly msg: string;
|
|
88
|
+
readonly [field: string]: LogValue | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface LogLine extends LogEntry {
|
|
92
|
+
readonly t: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const LOG_READ_LIMIT = 200;
|
|
96
|
+
export const LOG_READ_LIMIT_MAX = 1000;
|
|
97
|
+
|
|
98
|
+
export function isLogLevel(value: string): value is LogLevel {
|
|
99
|
+
const levels: readonly string[] = LOG_LEVELS;
|
|
100
|
+
return levels.includes(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isLogTag(value: string): value is LogTag {
|
|
104
|
+
const tags: readonly string[] = LOG_TAGS;
|
|
105
|
+
return tags.includes(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Whether a line at `level` passes a read asking for `minimum` and above. */
|
|
109
|
+
export function meetsLevel(level: LogLevel, minimum: LogLevel): boolean {
|
|
110
|
+
return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(minimum);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const NOUN = "a log line";
|
|
114
|
+
const RESERVED = new Set(["t", "level", "tag", "route", "msg"]);
|
|
115
|
+
|
|
116
|
+
function logValue(value: unknown): LogValue | undefined {
|
|
117
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
118
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function parseLogLine(value: unknown): LogLine {
|
|
123
|
+
const record = asRecord(value, NOUN);
|
|
124
|
+
const t = requireString(record, "t", NOUN);
|
|
125
|
+
const level = requireString(record, "level", NOUN);
|
|
126
|
+
if (!isLogLevel(level)) throw new Error(`${NOUN} carries one of the four levels`);
|
|
127
|
+
const tag = requireString(record, "tag", NOUN);
|
|
128
|
+
if (!isLogTag(tag)) throw new Error(`${NOUN} carries a tag the protocol defines`);
|
|
129
|
+
const route = optionalString(record, "route", NOUN);
|
|
130
|
+
const msg = requireString(record, "msg", NOUN);
|
|
131
|
+
const fields: Record<string, LogValue> = {};
|
|
132
|
+
for (const [field, raw] of Object.entries(record)) {
|
|
133
|
+
if (RESERVED.has(field)) continue;
|
|
134
|
+
const scalar = logValue(raw);
|
|
135
|
+
if (scalar === undefined) throw new Error(`${NOUN}'s ${field} is a flat scalar`);
|
|
136
|
+
fields[field] = scalar;
|
|
137
|
+
}
|
|
138
|
+
return { t, level, tag, ...(route !== undefined ? { route } : {}), msg, ...fields };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function serializeLogLine(line: LogLine): string {
|
|
142
|
+
const { t, level, tag, route, msg, ...fields } = line;
|
|
143
|
+
return JSON.stringify({
|
|
144
|
+
t,
|
|
145
|
+
level,
|
|
146
|
+
tag,
|
|
147
|
+
...(route !== undefined ? { route } : {}),
|
|
148
|
+
msg,
|
|
149
|
+
...fields,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface LogReadResult {
|
|
154
|
+
readonly lines: readonly LogLine[];
|
|
155
|
+
/** True when the limit cut lines the filter had matched. The read is a tail, so what was cut
|
|
156
|
+
* is older than what was returned. */
|
|
157
|
+
readonly truncated: boolean;
|
|
158
|
+
readonly from?: string;
|
|
159
|
+
readonly to?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function parseLogReadResult(value: unknown): LogReadResult {
|
|
163
|
+
const record = asRecord(value, "a log read result");
|
|
164
|
+
if (!Array.isArray(record.lines)) throw new Error("a log read result carries an array of lines");
|
|
165
|
+
if (typeof record.truncated !== "boolean")
|
|
166
|
+
throw new Error("a log read result says whether lines were cut");
|
|
167
|
+
const from = optionalString(record, "from", "a log read result");
|
|
168
|
+
const to = optionalString(record, "to", "a log read result");
|
|
169
|
+
return {
|
|
170
|
+
lines: record.lines.map(parseLogLine),
|
|
171
|
+
truncated: record.truncated,
|
|
172
|
+
...(from !== undefined ? { from } : {}),
|
|
173
|
+
...(to !== undefined ? { to } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function serializeLogReadResult(result: LogReadResult): string {
|
|
178
|
+
return JSON.stringify({
|
|
179
|
+
lines: result.lines.map((line): unknown => JSON.parse(serializeLogLine(line))),
|
|
180
|
+
truncated: result.truncated,
|
|
181
|
+
...(result.from !== undefined ? { from: result.from } : {}),
|
|
182
|
+
...(result.to !== undefined ? { to: result.to } : {}),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The filter every log read shares. `limit` and `level` are resolved to their defaults here. */
|
|
187
|
+
export interface LogReadFilter {
|
|
188
|
+
readonly limit: number;
|
|
189
|
+
readonly level: LogLevel;
|
|
190
|
+
readonly since?: string;
|
|
191
|
+
readonly tag?: LogTag;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function parseLogReadFilter(record: Record<string, unknown>, noun: string): LogReadFilter {
|
|
195
|
+
const limit = optionalPositiveInteger(record, "limit", noun);
|
|
196
|
+
if (limit !== undefined && limit > LOG_READ_LIMIT_MAX)
|
|
197
|
+
throw new Error(`${noun}'s limit is at most ${LOG_READ_LIMIT_MAX}`);
|
|
198
|
+
const since = optionalString(record, "since", noun);
|
|
199
|
+
if (since !== undefined && Number.isNaN(Date.parse(since)))
|
|
200
|
+
throw new Error(`${noun}'s since is an ISO timestamp`);
|
|
201
|
+
const level = optionalString(record, "level", noun);
|
|
202
|
+
if (level !== undefined && !isLogLevel(level))
|
|
203
|
+
throw new Error(`${noun}'s level is one of the four`);
|
|
204
|
+
const tag = optionalString(record, "tag", noun);
|
|
205
|
+
if (tag !== undefined && !isLogTag(tag))
|
|
206
|
+
throw new Error(`${noun}'s tag is one the protocol defines`);
|
|
207
|
+
return {
|
|
208
|
+
limit: limit ?? LOG_READ_LIMIT,
|
|
209
|
+
level: level ?? "info",
|
|
210
|
+
...(since !== undefined ? { since } : {}),
|
|
211
|
+
...(tag !== undefined ? { tag } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function serializeLogReadFilter(filter: LogReadFilter): Record<string, unknown> {
|
|
216
|
+
return {
|
|
217
|
+
...(filter.limit !== LOG_READ_LIMIT ? { limit: filter.limit } : {}),
|
|
218
|
+
...(filter.since !== undefined ? { since: filter.since } : {}),
|
|
219
|
+
...(filter.level !== "info" ? { level: filter.level } : {}),
|
|
220
|
+
...(filter.tag !== undefined ? { tag: filter.tag } : {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "../version";
|
|
2
|
+
|
|
3
|
+
export function asRecord(value: unknown, noun: string): Record<string, unknown> {
|
|
4
|
+
if (!isRecord(value)) throw new Error(`${noun} is an object`);
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
+
return typeof value === "object" && value !== null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function requireOperation(
|
|
13
|
+
record: Record<string, unknown>,
|
|
14
|
+
operation: string,
|
|
15
|
+
noun: string,
|
|
16
|
+
): void {
|
|
17
|
+
if (record.operation !== operation) throw new Error(`${noun} names the ${operation} operation`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function requireProtocol(record: Record<string, unknown>, noun: string): void {
|
|
21
|
+
if (record.protocol !== PROTOCOL_VERSION)
|
|
22
|
+
throw new Error(`${noun} carries protocol ${PROTOCOL_VERSION}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function requireNotification(
|
|
26
|
+
record: Record<string, unknown>,
|
|
27
|
+
notification: string,
|
|
28
|
+
noun: string,
|
|
29
|
+
): void {
|
|
30
|
+
if (record.notification !== notification)
|
|
31
|
+
throw new Error(`${noun} names the ${notification} notification`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requireString(
|
|
35
|
+
record: Record<string, unknown>,
|
|
36
|
+
field: string,
|
|
37
|
+
noun: string,
|
|
38
|
+
): string {
|
|
39
|
+
const value = record[field];
|
|
40
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
41
|
+
throw new Error(`${noun} names a ${field}`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function optionalString(
|
|
46
|
+
record: Record<string, unknown>,
|
|
47
|
+
field: string,
|
|
48
|
+
noun: string,
|
|
49
|
+
max?: number,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
if (!(field in record) || record[field] === undefined) return undefined;
|
|
52
|
+
const value = record[field];
|
|
53
|
+
if (typeof value !== "string") throw new Error(`${noun}'s ${field} is a string`);
|
|
54
|
+
if (max !== undefined && value.length > max)
|
|
55
|
+
throw new Error(`${noun}'s ${field} is at most ${max} characters`);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function optionalBoolean(
|
|
60
|
+
record: Record<string, unknown>,
|
|
61
|
+
field: string,
|
|
62
|
+
noun: string,
|
|
63
|
+
): boolean | undefined {
|
|
64
|
+
if (!(field in record) || record[field] === undefined) return undefined;
|
|
65
|
+
const value = record[field];
|
|
66
|
+
if (typeof value !== "boolean") throw new Error(`${noun}'s ${field} is a boolean`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function optionalStringArray(
|
|
71
|
+
record: Record<string, unknown>,
|
|
72
|
+
field: string,
|
|
73
|
+
noun: string,
|
|
74
|
+
max: number,
|
|
75
|
+
): string[] | undefined {
|
|
76
|
+
if (!(field in record) || record[field] === undefined) return undefined;
|
|
77
|
+
const value = record[field];
|
|
78
|
+
if (
|
|
79
|
+
!Array.isArray(value) ||
|
|
80
|
+
!value.every((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(`${noun}'s ${field} is an array of non-empty strings`);
|
|
83
|
+
}
|
|
84
|
+
if (value.length > max) throw new Error(`${noun}'s ${field} carries at most ${max} entries`);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function requirePositiveInteger(
|
|
89
|
+
record: Record<string, unknown>,
|
|
90
|
+
field: string,
|
|
91
|
+
noun: string,
|
|
92
|
+
): number {
|
|
93
|
+
const value = record[field];
|
|
94
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
95
|
+
throw new Error(`${noun} names a positive integer ${field}`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function optionalPositiveInteger(
|
|
101
|
+
record: Record<string, unknown>,
|
|
102
|
+
field: string,
|
|
103
|
+
noun: string,
|
|
104
|
+
): number | undefined {
|
|
105
|
+
if (!(field in record) || record[field] === undefined) return undefined;
|
|
106
|
+
const value = record[field];
|
|
107
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
108
|
+
throw new Error(`${noun}'s ${field} is a positive integer`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function requireNonNegativeInteger(
|
|
114
|
+
record: Record<string, unknown>,
|
|
115
|
+
field: string,
|
|
116
|
+
noun: string,
|
|
117
|
+
): number {
|
|
118
|
+
const value = record[field];
|
|
119
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
120
|
+
throw new Error(`${noun} names a non-negative integer ${field}`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function optionalNonNegativeInteger(
|
|
126
|
+
record: Record<string, unknown>,
|
|
127
|
+
field: string,
|
|
128
|
+
noun: string,
|
|
129
|
+
): number | undefined {
|
|
130
|
+
if (!(field in record) || record[field] === undefined) return undefined;
|
|
131
|
+
const value = record[field];
|
|
132
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
133
|
+
throw new Error(`${noun}'s ${field} is a non-negative integer`);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
|
|
2
|
+
import { asRecord, requireOperation, requireProtocol, requireString } from "./parse-helpers";
|
|
3
|
+
|
|
4
|
+
const NOUN = "a querySelection input";
|
|
5
|
+
|
|
6
|
+
export interface QuerySelectionInput {
|
|
7
|
+
readonly operation: "querySelection";
|
|
8
|
+
readonly protocol: ProtocolVersion;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseQuerySelectionInput(value: unknown): QuerySelectionInput {
|
|
13
|
+
const record = asRecord(value, NOUN);
|
|
14
|
+
requireOperation(record, "querySelection", NOUN);
|
|
15
|
+
requireProtocol(record, NOUN);
|
|
16
|
+
const name = requireString(record, "name", NOUN);
|
|
17
|
+
return { operation: "querySelection", protocol: PROTOCOL_VERSION, name };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function serializeQuerySelectionInput(input: QuerySelectionInput): string {
|
|
21
|
+
return JSON.stringify(input);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface QuerySelectionResult {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
// Absent when the route's selection has never been set; an empty array when it was set and
|
|
27
|
+
// then explicitly cleared. The two are distinct states, not the same "nothing" spelled two
|
|
28
|
+
// ways: a fresh route was never armed, a cleared one was armed and then let go.
|
|
29
|
+
readonly elements?: readonly unknown[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseQuerySelectionResult(value: unknown): QuerySelectionResult {
|
|
33
|
+
const record = asRecord(value, "a querySelection result");
|
|
34
|
+
const name = requireString(record, "name", "a querySelection result");
|
|
35
|
+
if (!("elements" in record) || record.elements === undefined) return { name };
|
|
36
|
+
if (!Array.isArray(record.elements)) {
|
|
37
|
+
throw new Error("a querySelection result's elements is an array when present");
|
|
38
|
+
}
|
|
39
|
+
return { name, elements: record.elements };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function serializeQuerySelectionResult(result: QuerySelectionResult): string {
|
|
43
|
+
return JSON.stringify(result);
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
|
|
2
|
+
import { parseLogReadFilter, serializeLogReadFilter, type LogReadFilter } from "./log-line";
|
|
3
|
+
import { asRecord, requireOperation, requireProtocol } from "./parse-helpers";
|
|
4
|
+
|
|
5
|
+
const NOUN = "a readDaemonLog input";
|
|
6
|
+
|
|
7
|
+
export interface ReadDaemonLogInput extends LogReadFilter {
|
|
8
|
+
readonly operation: "readDaemonLog";
|
|
9
|
+
readonly protocol: ProtocolVersion;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseReadDaemonLogInput(value: unknown): ReadDaemonLogInput {
|
|
13
|
+
const record = asRecord(value, NOUN);
|
|
14
|
+
requireOperation(record, "readDaemonLog", NOUN);
|
|
15
|
+
requireProtocol(record, NOUN);
|
|
16
|
+
return {
|
|
17
|
+
operation: "readDaemonLog",
|
|
18
|
+
protocol: PROTOCOL_VERSION,
|
|
19
|
+
...parseLogReadFilter(record, NOUN),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function serializeReadDaemonLogInput(input: ReadDaemonLogInput): string {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
operation: input.operation,
|
|
26
|
+
protocol: input.protocol,
|
|
27
|
+
...serializeLogReadFilter(input),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
|
|
2
|
+
import { parseLogReadFilter, serializeLogReadFilter, type LogReadFilter } from "./log-line";
|
|
3
|
+
import { asRecord, requireOperation, requireProtocol, requireString } from "./parse-helpers";
|
|
4
|
+
|
|
5
|
+
const NOUN = "a readRouteLog input";
|
|
6
|
+
|
|
7
|
+
/** The alphabet a route name is normalized into. A name outside it would put a read of one
|
|
8
|
+
* route's log somewhere else in the file system. */
|
|
9
|
+
const ROUTE_LABEL = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
|
|
11
|
+
export interface ReadRouteLogInput extends LogReadFilter {
|
|
12
|
+
readonly operation: "readRouteLog";
|
|
13
|
+
readonly protocol: ProtocolVersion;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseReadRouteLogInput(value: unknown): ReadRouteLogInput {
|
|
18
|
+
const record = asRecord(value, NOUN);
|
|
19
|
+
requireOperation(record, "readRouteLog", NOUN);
|
|
20
|
+
requireProtocol(record, NOUN);
|
|
21
|
+
const name = requireString(record, "name", NOUN);
|
|
22
|
+
if (!ROUTE_LABEL.test(name))
|
|
23
|
+
throw new Error(`${NOUN}'s name is a route label: lowercase letters, digits, and hyphens`);
|
|
24
|
+
return {
|
|
25
|
+
operation: "readRouteLog",
|
|
26
|
+
protocol: PROTOCOL_VERSION,
|
|
27
|
+
name,
|
|
28
|
+
...parseLogReadFilter(record, NOUN),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function serializeReadRouteLogInput(input: ReadRouteLogInput): string {
|
|
33
|
+
return JSON.stringify({
|
|
34
|
+
operation: input.operation,
|
|
35
|
+
protocol: input.protocol,
|
|
36
|
+
name: input.name,
|
|
37
|
+
...serializeLogReadFilter(input),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
|
|
2
|
+
import {
|
|
3
|
+
asRecord,
|
|
4
|
+
optionalBoolean,
|
|
5
|
+
requireOperation,
|
|
6
|
+
requireProtocol,
|
|
7
|
+
requireString,
|
|
8
|
+
} from "./parse-helpers";
|
|
9
|
+
|
|
10
|
+
const NOUN = "a rebootRoute input";
|
|
11
|
+
|
|
12
|
+
export interface RebootRouteInput {
|
|
13
|
+
readonly operation: "rebootRoute";
|
|
14
|
+
readonly protocol: ProtocolVersion;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
// Defaults to true, as in the previous engine's reboot_route tool (`args.forceDirtyCache !== false`).
|
|
17
|
+
readonly forceDirtyCache: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseRebootRouteInput(value: unknown): RebootRouteInput {
|
|
21
|
+
const record = asRecord(value, NOUN);
|
|
22
|
+
requireOperation(record, "rebootRoute", NOUN);
|
|
23
|
+
requireProtocol(record, NOUN);
|
|
24
|
+
const name = requireString(record, "name", NOUN);
|
|
25
|
+
const forceDirtyCache = optionalBoolean(record, "forceDirtyCache", NOUN) ?? true;
|
|
26
|
+
return { operation: "rebootRoute", protocol: PROTOCOL_VERSION, name, forceDirtyCache };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serializeRebootRouteInput(input: RebootRouteInput): string {
|
|
30
|
+
return JSON.stringify(input);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RebootRouteResult {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseRebootRouteResult(value: unknown): RebootRouteResult {
|
|
38
|
+
const record = asRecord(value, "a rebootRoute result");
|
|
39
|
+
const name = requireString(record, "name", "a rebootRoute result");
|
|
40
|
+
return { name };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function serializeRebootRouteResult(result: RebootRouteResult): string {
|
|
44
|
+
return JSON.stringify(result);
|
|
45
|
+
}
|