@anhlp/alp-client 0.0.0-stage → 0.0.1-beta.4
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 +202 -0
- package/NOTICE +7 -0
- package/README.md +28 -2
- package/dist/acp-probe.d.ts +44 -0
- package/dist/acp-probe.js +37 -0
- package/dist/antigravity-probe.d.ts +34 -0
- package/dist/antigravity-probe.js +95 -0
- package/dist/command.d.ts +5 -0
- package/dist/command.js +33 -0
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.js +587 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +533 -0
- package/dist/integration-lock.d.ts +35 -0
- package/dist/integration-lock.js +179 -0
- package/dist/ipc.d.ts +42 -0
- package/dist/ipc.js +69 -0
- package/dist/library-test.d.ts +94 -0
- package/dist/library-test.js +37 -0
- package/dist/mcp-probe.d.ts +24 -0
- package/dist/mcp-probe.js +147 -0
- package/dist/portable.d.ts +19 -0
- package/dist/portable.js +33 -0
- package/dist/prompt.d.ts +32 -0
- package/dist/prompt.js +59 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.js +120 -0
- package/dist/rpc/client.d.ts +82 -0
- package/dist/rpc/client.js +292 -0
- package/dist/rpc/errors.d.ts +42 -0
- package/dist/rpc/errors.js +34 -0
- package/dist/rpc/framing.d.ts +21 -0
- package/dist/rpc/framing.js +57 -0
- package/dist/rpc/handshake.d.ts +19 -0
- package/dist/rpc/handshake.js +8 -0
- package/dist/rpc/reconnect.d.ts +97 -0
- package/dist/rpc/reconnect.js +227 -0
- package/dist/rpc/registry.d.ts +211 -0
- package/dist/rpc/registry.js +145 -0
- package/dist/rpc/timers.d.ts +11 -0
- package/dist/rpc/timers.js +11 -0
- package/dist/service.d.ts +50 -0
- package/dist/service.js +143 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +146 -0
- package/dist/supervise.d.ts +14 -0
- package/dist/supervise.js +87 -0
- package/dist/websocket.d.ts +51 -0
- package/dist/websocket.js +190 -0
- package/package.json +174 -4
- package/src/acp-probe.js +46 -0
- package/src/antigravity-probe.js +106 -0
- package/src/command.js +18 -0
- package/src/doctor.d.ts +13 -0
- package/src/doctor.js +570 -0
- package/src/index.js +596 -0
- package/src/integration-lock.js +179 -0
- package/src/ipc.js +92 -0
- package/src/library-test.js +41 -0
- package/src/mcp-probe.js +118 -0
- package/src/portable.js +28 -0
- package/src/prompt.js +64 -0
- package/src/render.js +136 -0
- package/src/rpc/client.js +296 -0
- package/src/rpc/errors.js +54 -0
- package/src/rpc/framing.js +61 -0
- package/src/rpc/handshake.js +16 -0
- package/src/rpc/reconnect.js +226 -0
- package/src/rpc/registry.js +161 -0
- package/src/rpc/timers.js +16 -0
- package/src/service.js +154 -0
- package/src/setup.js +150 -0
- package/src/supervise.d.ts +14 -0
- package/src/supervise.js +92 -0
- package/src/websocket.js +259 -0
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A select box in the terminal: ↑/↓ (or k/j) move, Enter picks, Ctrl-C or Esc cancels.
|
|
3
|
+
* Choices are { label, value, hint? }; `initial` is the value highlighted first.
|
|
4
|
+
* @template T
|
|
5
|
+
* @param {{ message: string, choices: Array<{ label: string, value: T, hint?: string }>, initial?: T }} question
|
|
6
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream }} [io]
|
|
7
|
+
* @returns {Promise<T>}
|
|
8
|
+
*/
|
|
9
|
+
export function select<T>({ message, choices, initial }: {
|
|
10
|
+
message: string;
|
|
11
|
+
choices: Array<{
|
|
12
|
+
label: string;
|
|
13
|
+
value: T;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}>;
|
|
16
|
+
initial?: T;
|
|
17
|
+
}, { input, output }?: {
|
|
18
|
+
input?: NodeJS.ReadStream;
|
|
19
|
+
output?: NodeJS.WriteStream;
|
|
20
|
+
}): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* A line of text, for a value the list does not offer.
|
|
23
|
+
* @param {string} message
|
|
24
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream }} [io]
|
|
25
|
+
*/
|
|
26
|
+
export function text(message: string, { input, output }?: {
|
|
27
|
+
input?: NodeJS.ReadStream;
|
|
28
|
+
output?: NodeJS.WriteStream;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
export class Cancelled extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// packages/client/src/prompt.js
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
|
+
var dim = (text2) => `\x1B[2m${text2}\x1B[22m`;
|
|
4
|
+
var cyan = (text2) => `\x1B[36m${text2}\x1B[39m`;
|
|
5
|
+
var Cancelled = class extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Setup cancelled.");
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function select({ message, choices, initial }, { input = process.stdin, output = process.stdout } = {}) {
|
|
11
|
+
let index = Math.max(0, choices.findIndex((choice) => choice.value === initial));
|
|
12
|
+
let drawn = 0;
|
|
13
|
+
const draw = () => {
|
|
14
|
+
if (drawn) output.write(`\x1B[${drawn}A\x1B[0J`);
|
|
15
|
+
const lines = [`${cyan("?")} ${message} ${dim("(\u2191/\u2193, Enter)")}`, ...choices.map((choice, at) => at === index ? cyan(`\u276F ${choice.label}`) + (choice.hint ? ` ${dim(choice.hint)}` : "") : ` ${choice.label}${choice.hint ? ` ${dim(choice.hint)}` : ""}`)];
|
|
16
|
+
output.write(lines.join("\n") + "\n");
|
|
17
|
+
drawn = lines.length;
|
|
18
|
+
};
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
emitKeypressEvents(input);
|
|
21
|
+
const raw = input.isRaw;
|
|
22
|
+
input.setRawMode?.(true);
|
|
23
|
+
input.resume();
|
|
24
|
+
output.write("\x1B[?25l");
|
|
25
|
+
const done = (settle, value) => {
|
|
26
|
+
input.off("keypress", onKey);
|
|
27
|
+
input.setRawMode?.(raw ?? false);
|
|
28
|
+
input.pause();
|
|
29
|
+
output.write(`\x1B[${drawn}A\x1B[0J\x1B[?25h`);
|
|
30
|
+
if (settle === resolve) output.write(`${cyan("\u2714")} ${message} ${cyan(choices[index].label)}
|
|
31
|
+
`);
|
|
32
|
+
settle(value);
|
|
33
|
+
};
|
|
34
|
+
const onKey = (text2, key = {}) => {
|
|
35
|
+
if (key.ctrl && key.name === "c" || key.name === "escape") return done(reject, new Cancelled());
|
|
36
|
+
if (key.name === "return" || key.name === "enter") return done(resolve, choices[index].value);
|
|
37
|
+
if (key.name === "up" || key.name === "k") index = (index + choices.length - 1) % choices.length;
|
|
38
|
+
else if (key.name === "down" || key.name === "j") index = (index + 1) % choices.length;
|
|
39
|
+
else return;
|
|
40
|
+
draw();
|
|
41
|
+
};
|
|
42
|
+
input.on("keypress", onKey);
|
|
43
|
+
draw();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function text(message, { input = process.stdin, output = process.stdout } = {}) {
|
|
47
|
+
const { createInterface } = await import("node:readline/promises");
|
|
48
|
+
const rl = createInterface({ input, output });
|
|
49
|
+
try {
|
|
50
|
+
return (await rl.question(`${cyan("?")} ${message} `)).trim();
|
|
51
|
+
} finally {
|
|
52
|
+
rl.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
Cancelled,
|
|
57
|
+
select,
|
|
58
|
+
text
|
|
59
|
+
};
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** `alp ps`: sessions as a tree, one line each. */
|
|
2
|
+
export function renderPs(sessions: any): any[];
|
|
3
|
+
/** `alp log`: one line per run-log entry, with its time. */
|
|
4
|
+
export function renderLog(rootId: any, entries: any): string[];
|
|
5
|
+
export function ago(since: any, now?: number): string;
|
|
6
|
+
export function duration(ms: any): string;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// packages/client/src/render.js
|
|
2
|
+
var TASK_PAST = { create: "created", update: "updated", link: "linked", start: "started", close: "closed", reopen: "reopened", delegate: "delegated", submit: "submitted", release: "released", gate: "gated", clear: "cleared a gate of", orphaned: "reopened (its assignment ended with alpd)" };
|
|
3
|
+
var ago = (since, now = Date.now()) => {
|
|
4
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(since)) / 1e3));
|
|
5
|
+
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s` : `${Math.floor(seconds / 3600)}h${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
6
|
+
};
|
|
7
|
+
var duration = (ms) => ago((/* @__PURE__ */ new Date(0)).toISOString(), ms);
|
|
8
|
+
var tokens = (count) => typeof count !== "number" ? "?" : count >= 1e6 && count % 1e6 < 5e4 ? `${Math.round(count / 1e6)}M` : `${Math.round(count / 1e3)}k`;
|
|
9
|
+
function renderPs(sessions) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
const children = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const session of sessions) {
|
|
13
|
+
const key = session.parentId ?? "";
|
|
14
|
+
children.set(key, [...children.get(key) ?? [], session]);
|
|
15
|
+
}
|
|
16
|
+
const print = (session, depth) => {
|
|
17
|
+
const status = session.parked ? `parked (${session.parked})` : session.status === "running" || session.status === "idle" ? session.activeTurnId ? "running" : session.busy ? "waiting" : "idle" : session.lastError?.code ?? session.status;
|
|
18
|
+
lines.push(`${" ".repeat(depth)}${session.id} ${session.agent} ${session.runtime}:${session.model} ${session.mode} ${status}${depth ? "" : ` ${session.projectRoot}${session.title ? ` "${session.title}"` : ""}`}`);
|
|
19
|
+
for (const child of children.get(session.id) ?? []) print(child, depth + 1);
|
|
20
|
+
};
|
|
21
|
+
for (const root of children.get("") ?? []) print(root, 0);
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
function renderLog(rootId, entries) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const agents = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const time = entry.ts.slice(11, 19);
|
|
29
|
+
let text;
|
|
30
|
+
switch (entry.event) {
|
|
31
|
+
case "assignment.started":
|
|
32
|
+
agents.set(entry.assignmentId, entry.agent);
|
|
33
|
+
text = `${entry.parentAgent} \u2192 ${entry.agent} (${entry.mode}${entry.isolation === "worktree" ? ", worktree" : ""}, ${entry.model}${entry.wait === false ? ", async" : ""}): ${entry.task.split("\n")[0].slice(0, 120)}`;
|
|
34
|
+
break;
|
|
35
|
+
case "assignment.finished":
|
|
36
|
+
text = `${entry.agent} ${entry.status} after ${duration(entry.durationMs ?? 0)}${entry.handoff ? `, handoff ${entry.handoff.outcome}${entry.handoff.verdict ? `, verdict ${entry.handoff.verdict.result.toUpperCase()}` : ""}: ${entry.handoff.summary.split("\n")[0].slice(0, 120)}` : ""}${entry.error ? `: ${entry.error}` : ""}${entry.reconciled ? " (after a restart)" : ""}`;
|
|
37
|
+
break;
|
|
38
|
+
case "mail":
|
|
39
|
+
if (entry.kind === "board") continue;
|
|
40
|
+
text = `\u2709 ${entry.kind} ${entry.from} \u2192 ${entry.to === rootId ? "main" : agents.get(entry.to) ?? entry.to}${entry.body ? `: ${entry.body.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
41
|
+
break;
|
|
42
|
+
case "board.pin":
|
|
43
|
+
text = `${entry.kind === "claim" ? "\u2691" : entry.kind === "decision" ? "\u25C6" : "\u2022"} ${entry.agent} pins ${entry.kind} ${entry.pinId}${entry.task ? ` for ${entry.task}` : ""}${entry.paths ? ` [${entry.paths.join(", ")}]` : ""}: ${entry.body.split("\n")[0].slice(0, 120)}`;
|
|
44
|
+
break;
|
|
45
|
+
case "task":
|
|
46
|
+
text = `\u2610 ${entry.agent} ${TASK_PAST[entry.action] ?? entry.action} ${entry.id} "${entry.title}" \u2192 ${entry.status}${entry.detail ? `: ${entry.detail.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
47
|
+
break;
|
|
48
|
+
case "board.unpin":
|
|
49
|
+
text = `\u2690 ${entry.pinId} by ${entry.agent} ${entry.reason === "session_ended" ? "released when its session ended" : "taken down"}`;
|
|
50
|
+
break;
|
|
51
|
+
case "human.question":
|
|
52
|
+
text = `? ${entry.agent} asks the user [${entry.questionId}]: ${entry.body.split("\n")[0].slice(0, 120)}`;
|
|
53
|
+
break;
|
|
54
|
+
case "verify":
|
|
55
|
+
text = `${entry.skipped ? "\u2013" : entry.passed ? "\u2713" : "\u2717"} verify in the ${entry.where}${entry.taskId ? ` for ${entry.taskId}` : ""}${entry.assignmentId ? ` (${entry.assignmentId})` : ""}: ${entry.skipped ? `skipped: ${entry.skipped}` : entry.detail}`;
|
|
56
|
+
break;
|
|
57
|
+
case "epic.landed":
|
|
58
|
+
text = `\u25C6 ${entry.agent} closed ${entry.id} "${entry.title}": ${entry.tasks} tasks, ${entry.reworked} reworked`;
|
|
59
|
+
break;
|
|
60
|
+
case "notice":
|
|
61
|
+
text = `${entry.level === "error" ? "\u203C" : entry.level === "warning" ? "!" : "\u2139"} ${entry.text}`;
|
|
62
|
+
break;
|
|
63
|
+
case "assignment.parked":
|
|
64
|
+
text = `\u23F8 ${entry.agent} ${entry.assignmentId} parked: ${entry.reason}`;
|
|
65
|
+
break;
|
|
66
|
+
case "assignment.resumed":
|
|
67
|
+
text = `\u25B6 ${entry.agent} ${entry.assignmentId} resumed`;
|
|
68
|
+
break;
|
|
69
|
+
case "instructions":
|
|
70
|
+
text = `# ${entry.agent} instructions ${entry.sha} (ALP.md ${entry.parts?.project}, AGENT.md ${entry.parts?.agent}, ${entry.chars} chars)`;
|
|
71
|
+
break;
|
|
72
|
+
case "context":
|
|
73
|
+
text = entry.level === "soon" ? `\u25D4 ${entry.agent} context ${tokens(entry.tokens)}, compacts at ${tokens(entry.compactAt)}: told it is near` : `\u25D4 ${entry.agent} context ${entry.percent}% full: ${entry.level === "now" ? "told to hand off now" : "told to plan a handoff"}`;
|
|
74
|
+
break;
|
|
75
|
+
case "skill":
|
|
76
|
+
text = Array.isArray(entry.roles) ? `\u2726 ${entry.agent} saved skill ${entry.name} for ${entry.roles.join(", ")}` : `\u2726 ${entry.agent} uses skill ${entry.skill}${entry.via === "skill" ? " (Skill tool)" : ""}`;
|
|
77
|
+
break;
|
|
78
|
+
case "compacted":
|
|
79
|
+
text = entry.failed ? `\u2717 ${entry.agent}'s context could not be compacted${entry.error ? `: ${entry.error}` : ""}` : `\u25D1 ${entry.agent}'s context compacted${entry.preTokens !== void 0 ? ` from ${tokens(entry.preTokens)}` : ""}${entry.postTokens !== void 0 ? ` to ${tokens(entry.postTokens)}` : ""}${entry.trigger === "manual" ? " on request" : ""}`;
|
|
80
|
+
break;
|
|
81
|
+
case "assignment.interrupted":
|
|
82
|
+
text = `\u23F9 ${entry.agent} ${entry.assignmentId} interrupted: ${entry.reason}; the next alpd continues it`;
|
|
83
|
+
break;
|
|
84
|
+
case "assignment.recovered":
|
|
85
|
+
text = `\u21BB ${entry.agent} ${entry.assignmentId} reopened after alpd restarted`;
|
|
86
|
+
break;
|
|
87
|
+
case "session.restarted":
|
|
88
|
+
text = `\u21BB ${entry.agent}'s native process stopped (${entry.error}); restart ${entry.restarts}`;
|
|
89
|
+
break;
|
|
90
|
+
case "session.revived":
|
|
91
|
+
text = `\u25B6 ${entry.agent} running again`;
|
|
92
|
+
break;
|
|
93
|
+
case "session.revive_failed":
|
|
94
|
+
text = `\u2717 ${entry.agent} could not be restarted: ${entry.error}`;
|
|
95
|
+
break;
|
|
96
|
+
case "human.answer":
|
|
97
|
+
text = `\u21B3 ${entry.questionId} ${entry.outcome}${entry.answer !== void 0 ? `: ${entry.answer.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
98
|
+
break;
|
|
99
|
+
case "hook":
|
|
100
|
+
text = entry.skipped ? `\u21AA ${entry.agent}'s ${entry.on} hook ${entry.hook} skipped: the workspace's hooks are not trusted` : `${entry.blocked ? "\u26D4" : entry.error || entry.exitCode !== 0 || entry.timedOut ? "\u2717" : "\u21AA"} ${entry.agent}'s ${entry.on} hook ${entry.hook} ${entry.error ? `could not run: ${entry.error}` : entry.timedOut ? "timed out" : `exit ${entry.exitCode}`}${entry.durationMs !== void 0 ? ` in ${duration(entry.durationMs)}` : ""}${entry.blocked ? ", refused it" : ""}${entry.output ? `: ${entry.output.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
101
|
+
break;
|
|
102
|
+
case "hook.trust":
|
|
103
|
+
text = `${entry.trusted ? "\u2713 the user trusts" : "\u2717 the user did not trust"} the hooks of ${entry.project}${entry.outcome ? ` (${entry.outcome})` : ""}`;
|
|
104
|
+
break;
|
|
105
|
+
case "runtime.slip":
|
|
106
|
+
text = `\u26A0 ALP could not handle ${entry.agent}'s ${entry.method}: ${entry.error}; the session went on`;
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
text = `${entry.event}${entry.branch ? ` ${entry.branch}` : ""}${entry.status ? ` ${entry.status}` : ""}`;
|
|
110
|
+
}
|
|
111
|
+
lines.push(`${time} ${text}`);
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
ago,
|
|
117
|
+
duration,
|
|
118
|
+
renderLog,
|
|
119
|
+
renderPs
|
|
120
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Requests waiting for an answer at once; beyond it a request fails as `overloaded` and is not sent. */
|
|
2
|
+
export const DEFAULT_MAX_PENDING: 4096;
|
|
3
|
+
export class AlpRpcClient {
|
|
4
|
+
/**
|
|
5
|
+
* @param {AlpTransport} transport an open transport; the client owns it from now on
|
|
6
|
+
* @param {AlpRpcClientOptions} [options]
|
|
7
|
+
*/
|
|
8
|
+
constructor(transport: AlpTransport, { maxPending, maxFrame, timeoutMs, onListenerError }?: AlpRpcClientOptions);
|
|
9
|
+
/** True once the connection is gone, for whatever reason. */
|
|
10
|
+
get closed(): boolean;
|
|
11
|
+
/** Requests sent and still waiting for an answer. */
|
|
12
|
+
get pendingCount(): number;
|
|
13
|
+
/**
|
|
14
|
+
* Sends one request and resolves with its result, or rejects with AlpRpcError (the daemon
|
|
15
|
+
* answered with an error) or AlpClientError (the client gave up: see CLIENT_ERROR). Requests are
|
|
16
|
+
* never resent, also not after a reconnect.
|
|
17
|
+
* @template [T=any]
|
|
18
|
+
* @param {string} method
|
|
19
|
+
* @param {unknown} [params]
|
|
20
|
+
* @param {RequestOptions} [options]
|
|
21
|
+
* @returns {Promise<T>}
|
|
22
|
+
*/
|
|
23
|
+
request<T = any>(method: string, params?: unknown, { signal, timeoutMs }?: RequestOptions): Promise<T>;
|
|
24
|
+
/** Every `event` notification: an envelope of a session the connection attached to. Returns a remover. @param {(envelope: any) => void} listener */
|
|
25
|
+
onEvent(listener: (envelope: any) => void): () => boolean;
|
|
26
|
+
/** Every notification from the daemon, `event` included; for methods this client does not know yet. @param {(method: string, params: any) => void} listener */
|
|
27
|
+
onNotification(listener: (method: string, params: any) => void): () => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Answers requests the daemon sends this client (the extension RPC of ALPD §61). The first
|
|
30
|
+
* handler answers; its result, or its error's numeric `code`, `message` and `data`, goes back.
|
|
31
|
+
* @param {(method: string, params: any) => unknown} handler
|
|
32
|
+
*/
|
|
33
|
+
onRequest(handler: (method: string, params: any) => unknown): () => boolean;
|
|
34
|
+
/** Called once when the connection ends, with the error pending requests were rejected with. @param {(error: Error) => void} listener */
|
|
35
|
+
onClose(listener: (error: Error) => void): () => boolean;
|
|
36
|
+
/** Ends the connection; waiting requests reject with reason `closed`. Idempotent. */
|
|
37
|
+
close(): void;
|
|
38
|
+
#private;
|
|
39
|
+
}
|
|
40
|
+
export type TransportState = "open" | "closed";
|
|
41
|
+
/**
|
|
42
|
+
* Why a transport closed: `error` when it failed, `code`/`reason` when the peer said so.
|
|
43
|
+
*/
|
|
44
|
+
export type TransportClose = {
|
|
45
|
+
error?: Error;
|
|
46
|
+
code?: number;
|
|
47
|
+
reason?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Contract (docs/alp/client-sdk.md): `send` delivers or throws, synchronously, and a throw means
|
|
51
|
+
* the frame did not leave; `onMessage` delivers frames in arrival order; `onClose` fires at most
|
|
52
|
+
* once, also after `close()`; nothing is delivered after it; `close` is idempotent.
|
|
53
|
+
*/
|
|
54
|
+
export type AlpTransport = {
|
|
55
|
+
readonly state: TransportState;
|
|
56
|
+
send(frame: string): void;
|
|
57
|
+
onMessage(listener: (frame: string) => void): () => void;
|
|
58
|
+
onClose(listener: (close: TransportClose) => void): () => void;
|
|
59
|
+
close(): void;
|
|
60
|
+
};
|
|
61
|
+
export type AbortSignalLike = {
|
|
62
|
+
readonly aborted: boolean;
|
|
63
|
+
readonly reason?: unknown;
|
|
64
|
+
addEventListener(type: "abort", listener: () => void, options?: {
|
|
65
|
+
once?: boolean;
|
|
66
|
+
}): void;
|
|
67
|
+
removeEventListener(type: "abort", listener: () => void): void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Both end only this client's wait; neither tells the daemon to stop. A timeout of Infinity, or
|
|
71
|
+
* longer than a timer can wait (2^31-1 ms), means none.
|
|
72
|
+
*/
|
|
73
|
+
export type RequestOptions = {
|
|
74
|
+
signal?: AbortSignalLike;
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
};
|
|
77
|
+
export type AlpRpcClientOptions = {
|
|
78
|
+
maxPending?: number;
|
|
79
|
+
maxFrame?: number;
|
|
80
|
+
timeoutMs?: number;
|
|
81
|
+
onListenerError?: (error: unknown) => void;
|
|
82
|
+
};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// packages/client/src/rpc/client.js
|
|
2
|
+
import { AlpRpcError, MAX_FRAME } from "@anhlp/alp-protocol";
|
|
3
|
+
import { AlpClientError } from "./errors.js";
|
|
4
|
+
import { utf8Length } from "./framing.js";
|
|
5
|
+
import { timerDelay } from "./timers.js";
|
|
6
|
+
var CLOSED = "ALP daemon connection closed";
|
|
7
|
+
var DEFAULT_MAX_PENDING = 4096;
|
|
8
|
+
var rethrow = (error) => {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
throw error;
|
|
11
|
+
}, 0);
|
|
12
|
+
};
|
|
13
|
+
function assertTransport(transport) {
|
|
14
|
+
for (const name of ["send", "onMessage", "onClose", "close"]) {
|
|
15
|
+
if (typeof /** @type {any} */
|
|
16
|
+
transport?.[name] !== "function") throw new TypeError(`ALP transport must implement ${name}()`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
var abortError = (signal) => new AlpClientError("ALP request aborted", { reason: "aborted", cause: signal.reason });
|
|
20
|
+
var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
21
|
+
var AlpRpcClient = class {
|
|
22
|
+
/** @type {AlpTransport} */
|
|
23
|
+
#transport;
|
|
24
|
+
#sequence = 0;
|
|
25
|
+
/** @type {Map<number, { resolve(value: unknown): void, reject(error: unknown): void, cleanup(): void }>} */
|
|
26
|
+
#pending = /* @__PURE__ */ new Map();
|
|
27
|
+
/** @type {Set<(envelope: any) => void>} */
|
|
28
|
+
#events = /* @__PURE__ */ new Set();
|
|
29
|
+
/** @type {Set<(method: string, params: any) => void>} */
|
|
30
|
+
#notifications = /* @__PURE__ */ new Set();
|
|
31
|
+
/** @type {Set<(error: Error) => void>} */
|
|
32
|
+
#closes = /* @__PURE__ */ new Set();
|
|
33
|
+
/** @type {Set<(method: string, params: any) => unknown>} */
|
|
34
|
+
#requestHandlers = /* @__PURE__ */ new Set();
|
|
35
|
+
/** @type {Array<() => void>} */
|
|
36
|
+
#detach = [];
|
|
37
|
+
/** @type {AlpClientError | undefined} */
|
|
38
|
+
#closeError;
|
|
39
|
+
#maxPending;
|
|
40
|
+
#maxFrame;
|
|
41
|
+
#timeoutMs;
|
|
42
|
+
#onListenerError;
|
|
43
|
+
/**
|
|
44
|
+
* @param {AlpTransport} transport an open transport; the client owns it from now on
|
|
45
|
+
* @param {AlpRpcClientOptions} [options]
|
|
46
|
+
*/
|
|
47
|
+
constructor(transport, { maxPending = DEFAULT_MAX_PENDING, maxFrame = MAX_FRAME, timeoutMs, onListenerError = rethrow } = {}) {
|
|
48
|
+
assertTransport(transport);
|
|
49
|
+
this.#transport = transport;
|
|
50
|
+
this.#maxPending = maxPending;
|
|
51
|
+
this.#maxFrame = maxFrame;
|
|
52
|
+
timerDelay(timeoutMs);
|
|
53
|
+
this.#timeoutMs = timeoutMs;
|
|
54
|
+
this.#onListenerError = onListenerError;
|
|
55
|
+
this.#detach.push(transport.onMessage((frame) => this.#receive(frame)));
|
|
56
|
+
this.#detach.push(transport.onClose((close) => this.#finish(this.#disconnected(close))));
|
|
57
|
+
if (transport.state === "closed") this.#finish(new AlpClientError(CLOSED, { reason: "disconnected" }));
|
|
58
|
+
}
|
|
59
|
+
/** True once the connection is gone, for whatever reason. */
|
|
60
|
+
get closed() {
|
|
61
|
+
return this.#closeError !== void 0;
|
|
62
|
+
}
|
|
63
|
+
/** Requests sent and still waiting for an answer. */
|
|
64
|
+
get pendingCount() {
|
|
65
|
+
return this.#pending.size;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sends one request and resolves with its result, or rejects with AlpRpcError (the daemon
|
|
69
|
+
* answered with an error) or AlpClientError (the client gave up: see CLIENT_ERROR). Requests are
|
|
70
|
+
* never resent, also not after a reconnect.
|
|
71
|
+
* @template [T=any]
|
|
72
|
+
* @param {string} method
|
|
73
|
+
* @param {unknown} [params]
|
|
74
|
+
* @param {RequestOptions} [options]
|
|
75
|
+
* @returns {Promise<T>}
|
|
76
|
+
*/
|
|
77
|
+
request(method, params = {}, { signal, timeoutMs = this.#timeoutMs } = {}) {
|
|
78
|
+
if (this.#closeError) return Promise.reject(new AlpClientError(CLOSED, { reason: this.#closeError.reason === "closed" ? "closed" : "disconnected", cause: this.#closeError }));
|
|
79
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
80
|
+
if (this.#pending.size >= this.#maxPending) return Promise.reject(new AlpClientError(`Too many ALP requests waiting for an answer (${this.#maxPending})`, { reason: "overloaded" }));
|
|
81
|
+
let delay;
|
|
82
|
+
try {
|
|
83
|
+
delay = timerDelay(timeoutMs);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return Promise.reject(error);
|
|
86
|
+
}
|
|
87
|
+
const id = ++this.#sequence;
|
|
88
|
+
let frame;
|
|
89
|
+
try {
|
|
90
|
+
frame = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return Promise.reject(error);
|
|
93
|
+
}
|
|
94
|
+
if (utf8Length(frame) >= this.#maxFrame) return Promise.reject(new AlpClientError(`ALP request ${method} exceeds the ${this.#maxFrame}-byte frame limit`, { reason: "send_failed" }));
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
let timer;
|
|
97
|
+
const onAbort = () => {
|
|
98
|
+
if (this.#pending.delete(id)) {
|
|
99
|
+
cleanup();
|
|
100
|
+
reject(abortError(
|
|
101
|
+
/** @type {AbortSignalLike} */
|
|
102
|
+
signal
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const cleanup = () => {
|
|
107
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
108
|
+
signal?.removeEventListener("abort", onAbort);
|
|
109
|
+
};
|
|
110
|
+
this.#pending.set(id, { resolve, reject, cleanup });
|
|
111
|
+
if (delay !== void 0) {
|
|
112
|
+
timer = setTimeout(() => {
|
|
113
|
+
if (!this.#pending.delete(id)) return;
|
|
114
|
+
cleanup();
|
|
115
|
+
reject(new AlpClientError(`ALP request ${method} timed out after ${timeoutMs} ms`, { reason: "timeout" }));
|
|
116
|
+
}, delay);
|
|
117
|
+
}
|
|
118
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
119
|
+
try {
|
|
120
|
+
this.#transport.send(frame);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (this.#transport.state === "closed") this.#finish(new AlpClientError(CLOSED, { reason: "disconnected", cause: error }));
|
|
123
|
+
else if (this.#pending.delete(id)) {
|
|
124
|
+
cleanup();
|
|
125
|
+
reject(new AlpClientError(`Could not send ALP request ${method}: ${error instanceof Error ? error.message : String(error)}`, { reason: "send_failed", cause: error }));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Every `event` notification: an envelope of a session the connection attached to. Returns a remover. @param {(envelope: any) => void} listener */
|
|
131
|
+
onEvent(listener) {
|
|
132
|
+
return this.#add(this.#events, listener);
|
|
133
|
+
}
|
|
134
|
+
/** Every notification from the daemon, `event` included; for methods this client does not know yet. @param {(method: string, params: any) => void} listener */
|
|
135
|
+
onNotification(listener) {
|
|
136
|
+
return this.#add(this.#notifications, listener);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Answers requests the daemon sends this client (the extension RPC of ALPD §61). The first
|
|
140
|
+
* handler answers; its result, or its error's numeric `code`, `message` and `data`, goes back.
|
|
141
|
+
* @param {(method: string, params: any) => unknown} handler
|
|
142
|
+
*/
|
|
143
|
+
onRequest(handler) {
|
|
144
|
+
return this.#add(this.#requestHandlers, handler);
|
|
145
|
+
}
|
|
146
|
+
/** Called once when the connection ends, with the error pending requests were rejected with. @param {(error: Error) => void} listener */
|
|
147
|
+
onClose(listener) {
|
|
148
|
+
return this.#add(this.#closes, listener);
|
|
149
|
+
}
|
|
150
|
+
/** Ends the connection; waiting requests reject with reason `closed`. Idempotent. */
|
|
151
|
+
close() {
|
|
152
|
+
if (this.#closeError) return;
|
|
153
|
+
this.#finish(new AlpClientError(CLOSED, { reason: "closed" }));
|
|
154
|
+
}
|
|
155
|
+
/** @template F @param {Set<F>} set @param {F} listener */
|
|
156
|
+
#add(set, listener) {
|
|
157
|
+
if (typeof listener !== "function") throw new TypeError("ALP client listener must be a function");
|
|
158
|
+
if (this.#closeError) return () => false;
|
|
159
|
+
set.add(listener);
|
|
160
|
+
return () => set.delete(listener);
|
|
161
|
+
}
|
|
162
|
+
/** @param {TransportClose} [close] */
|
|
163
|
+
#disconnected(close = {}) {
|
|
164
|
+
if (close.error instanceof AlpClientError) return close.error;
|
|
165
|
+
const data = close.code !== void 0 || close.reason ? { code: close.code, reason: close.reason } : void 0;
|
|
166
|
+
return new AlpClientError(close.error?.message ?? CLOSED, { reason: "disconnected", cause: close.error, data });
|
|
167
|
+
}
|
|
168
|
+
/** @param {string} message */
|
|
169
|
+
#violation(message) {
|
|
170
|
+
this.#finish(new AlpClientError(message, { reason: "protocol" }));
|
|
171
|
+
}
|
|
172
|
+
/** @param {Function} listener @param {unknown[]} args */
|
|
173
|
+
#call(listener, ...args) {
|
|
174
|
+
try {
|
|
175
|
+
listener(...args);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.#onListenerError(error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** @param {string} frame */
|
|
181
|
+
#receive(frame) {
|
|
182
|
+
if (this.#closeError) return;
|
|
183
|
+
if (typeof frame !== "string") return this.#violation("Invalid frame from ALP daemon");
|
|
184
|
+
let message;
|
|
185
|
+
try {
|
|
186
|
+
message = JSON.parse(frame);
|
|
187
|
+
} catch {
|
|
188
|
+
return this.#violation("Invalid frame from ALP daemon");
|
|
189
|
+
}
|
|
190
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return this.#violation("Invalid frame from ALP daemon");
|
|
191
|
+
const hasId = message.id !== void 0 && message.id !== null;
|
|
192
|
+
if (message.method === "event") {
|
|
193
|
+
for (const listener of [...this.#events]) this.#call(listener, message.params);
|
|
194
|
+
for (const listener of [...this.#notifications]) this.#call(listener, message.method, message.params);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (typeof message.method === "string" && hasId && !own(message, "result") && !own(message, "error")) return this.#answer(message.id, message.method, message.params ?? {});
|
|
198
|
+
if (typeof message.method === "string" && !hasId) {
|
|
199
|
+
for (const listener of [...this.#notifications]) this.#call(listener, message.method, message.params);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const waiter = this.#pending.get(message.id);
|
|
203
|
+
if (!waiter) return;
|
|
204
|
+
this.#pending.delete(message.id);
|
|
205
|
+
waiter.cleanup();
|
|
206
|
+
if (own(message, "error") && typeof message.error === "object" && message.error !== null) waiter.reject(new AlpRpcError(message.error));
|
|
207
|
+
else if (own(message, "result") && !own(message, "error")) waiter.resolve(message.result);
|
|
208
|
+
else waiter.reject(new AlpClientError("Malformed response from ALP daemon", { reason: "protocol", data: { id: message.id } }));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Answers a request from alpd. Every request gets exactly one answer: a result that cannot be
|
|
212
|
+
* serialized or does not fit a frame becomes an error, and an error whose data cannot go either
|
|
213
|
+
* goes without it, so alpd's caller never waits for a reply that was silently dropped.
|
|
214
|
+
* @param {unknown} id @param {string} method @param {unknown} params
|
|
215
|
+
*/
|
|
216
|
+
#answer(id, method, params) {
|
|
217
|
+
const frameOf = (body) => {
|
|
218
|
+
try {
|
|
219
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", id, ...body });
|
|
220
|
+
return utf8Length(frame) < this.#maxFrame ? frame : void 0;
|
|
221
|
+
} catch {
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const send = (frame) => {
|
|
226
|
+
if (this.#closeError) return;
|
|
227
|
+
if (frame === void 0) return this.#finish(new AlpClientError("ALP reply exceeds the frame limit", { reason: "protocol" }));
|
|
228
|
+
try {
|
|
229
|
+
this.#transport.send(frame);
|
|
230
|
+
} catch (cause) {
|
|
231
|
+
this.#finish(new AlpClientError("Could not send ALP reply", { reason: "send_failed", cause }));
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const failure = (error) => {
|
|
235
|
+
const code = error && typeof /** @type {any} */
|
|
236
|
+
error.code === "number" ? (
|
|
237
|
+
/** @type {any} */
|
|
238
|
+
error.code
|
|
239
|
+
) : 1e3;
|
|
240
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
241
|
+
const message = text.length > 2e3 ? `${text.slice(0, 2e3)}...` : text;
|
|
242
|
+
const data = error && /** @type {any} */
|
|
243
|
+
error.data !== void 0 ? { data: (
|
|
244
|
+
/** @type {any} */
|
|
245
|
+
error.data
|
|
246
|
+
) } : {};
|
|
247
|
+
return frameOf({ error: { code, message, ...data } }) ?? frameOf({ error: { code, message } }) ?? frameOf({ error: { code: -32603, message: "The reply could not be sent" } });
|
|
248
|
+
};
|
|
249
|
+
const [handler] = this.#requestHandlers;
|
|
250
|
+
if (!handler) return send(failure(Object.assign(new Error(`Unknown method '${method}'`), { code: -32601 })));
|
|
251
|
+
Promise.resolve().then(async () => {
|
|
252
|
+
try {
|
|
253
|
+
const result = await handler(method, params);
|
|
254
|
+
const frame = frameOf({ result: result === void 0 ? {} : result });
|
|
255
|
+
send(frame ?? failure(new Error(`The result of ${method} cannot be serialized or exceeds the ${this.#maxFrame}-byte frame limit`)));
|
|
256
|
+
} catch (error) {
|
|
257
|
+
send(failure(error));
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/** @param {AlpClientError} error */
|
|
262
|
+
#finish(error) {
|
|
263
|
+
if (this.#closeError) return;
|
|
264
|
+
this.#closeError = error;
|
|
265
|
+
for (const detach of this.#detach.splice(0)) {
|
|
266
|
+
try {
|
|
267
|
+
detach();
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
this.#transport.close();
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
const pending = [...this.#pending.values()];
|
|
276
|
+
this.#pending.clear();
|
|
277
|
+
for (const waiter of pending) {
|
|
278
|
+
waiter.cleanup();
|
|
279
|
+
waiter.reject(error);
|
|
280
|
+
}
|
|
281
|
+
const closes = [...this.#closes];
|
|
282
|
+
this.#events.clear();
|
|
283
|
+
this.#notifications.clear();
|
|
284
|
+
this.#requestHandlers.clear();
|
|
285
|
+
this.#closes.clear();
|
|
286
|
+
for (const listener of closes) this.#call(listener, error);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
export {
|
|
290
|
+
AlpRpcClient,
|
|
291
|
+
DEFAULT_MAX_PENDING
|
|
292
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True for an AlpClientError, also one thrown by another copy of this module (a bundle may carry
|
|
3
|
+
* two); `reason` narrows the check.
|
|
4
|
+
* @param {unknown} error
|
|
5
|
+
* @param {string} [reason]
|
|
6
|
+
* @returns {error is AlpClientError}
|
|
7
|
+
*/
|
|
8
|
+
export function isAlpClientError(error: unknown, reason?: string): error is AlpClientError;
|
|
9
|
+
/**
|
|
10
|
+
* Why a request or a connection ended on the client side.
|
|
11
|
+
* - `closed`: this client was closed locally with close().
|
|
12
|
+
* - `disconnected`: the transport closed or failed. A request that was already sent may or may not
|
|
13
|
+
* have run on the daemon; the client never resends it.
|
|
14
|
+
* - `protocol`: the peer sent something that is not ALP JSON-RPC, so the client closed the connection.
|
|
15
|
+
* - `send_failed`: the transport refused the frame; the request never left this client.
|
|
16
|
+
* - `timeout` / `aborted`: the caller stopped waiting. Only the local wait ends; the daemon is not told.
|
|
17
|
+
* - `overloaded`: too many requests are waiting for an answer already; nothing was sent.
|
|
18
|
+
*/
|
|
19
|
+
export const CLIENT_ERROR: Readonly<{
|
|
20
|
+
closed: "closed";
|
|
21
|
+
disconnected: "disconnected";
|
|
22
|
+
protocol: "protocol";
|
|
23
|
+
sendFailed: "send_failed";
|
|
24
|
+
timeout: "timeout";
|
|
25
|
+
aborted: "aborted";
|
|
26
|
+
overloaded: "overloaded";
|
|
27
|
+
}>;
|
|
28
|
+
export class AlpClientError extends Error {
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} message
|
|
31
|
+
* @param {{ reason: 'closed' | 'disconnected' | 'protocol' | 'send_failed' | 'timeout' | 'aborted' | 'overloaded', cause?: unknown, data?: unknown }} details
|
|
32
|
+
*/
|
|
33
|
+
constructor(message: string, { reason, cause, data }: {
|
|
34
|
+
reason: "closed" | "disconnected" | "protocol" | "send_failed" | "timeout" | "aborted" | "overloaded";
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
data?: unknown;
|
|
37
|
+
});
|
|
38
|
+
/** @type {'closed' | 'disconnected' | 'protocol' | 'send_failed' | 'timeout' | 'aborted' | 'overloaded'} */
|
|
39
|
+
reason: "closed" | "disconnected" | "protocol" | "send_failed" | "timeout" | "aborted" | "overloaded";
|
|
40
|
+
/** @type {unknown} */
|
|
41
|
+
data: unknown;
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// packages/client/src/rpc/errors.js
|
|
2
|
+
var BRAND = Symbol.for("@anhlp/alp-client/AlpClientError");
|
|
3
|
+
var CLIENT_ERROR = Object.freeze({
|
|
4
|
+
closed: "closed",
|
|
5
|
+
disconnected: "disconnected",
|
|
6
|
+
protocol: "protocol",
|
|
7
|
+
sendFailed: "send_failed",
|
|
8
|
+
timeout: "timeout",
|
|
9
|
+
aborted: "aborted",
|
|
10
|
+
overloaded: "overloaded"
|
|
11
|
+
});
|
|
12
|
+
var AlpClientError = class extends Error {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @param {{ reason: 'closed' | 'disconnected' | 'protocol' | 'send_failed' | 'timeout' | 'aborted' | 'overloaded', cause?: unknown, data?: unknown }} details
|
|
16
|
+
*/
|
|
17
|
+
constructor(message, { reason, cause, data }) {
|
|
18
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
19
|
+
this.name = "AlpClientError";
|
|
20
|
+
this.reason = reason;
|
|
21
|
+
this.data = data;
|
|
22
|
+
Object.defineProperty(this, BRAND, { value: true });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function isAlpClientError(error, reason) {
|
|
26
|
+
return typeof error === "object" && error !== null && /** @type {any} */
|
|
27
|
+
error[BRAND] === true && (reason === void 0 || /** @type {any} */
|
|
28
|
+
error.reason === reason);
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
AlpClientError,
|
|
32
|
+
CLIENT_ERROR,
|
|
33
|
+
isAlpClientError
|
|
34
|
+
};
|