@anhlp/alp-client 0.0.0-beta.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.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/acp-probe.d.ts +44 -0
  5. package/dist/acp-probe.js +37 -0
  6. package/dist/antigravity-probe.d.ts +34 -0
  7. package/dist/antigravity-probe.js +95 -0
  8. package/dist/command.d.ts +5 -0
  9. package/dist/command.js +33 -0
  10. package/dist/doctor.d.ts +13 -0
  11. package/dist/doctor.js +587 -0
  12. package/dist/index.d.ts +101 -0
  13. package/dist/index.js +422 -0
  14. package/dist/ipc.d.ts +42 -0
  15. package/dist/ipc.js +69 -0
  16. package/dist/library-test.d.ts +94 -0
  17. package/dist/library-test.js +37 -0
  18. package/dist/mcp-probe.d.ts +24 -0
  19. package/dist/mcp-probe.js +147 -0
  20. package/dist/portable.d.ts +18 -0
  21. package/dist/portable.js +27 -0
  22. package/dist/prompt.d.ts +32 -0
  23. package/dist/prompt.js +59 -0
  24. package/dist/render.d.ts +6 -0
  25. package/dist/render.js +120 -0
  26. package/dist/rpc/client.d.ts +82 -0
  27. package/dist/rpc/client.js +292 -0
  28. package/dist/rpc/errors.d.ts +42 -0
  29. package/dist/rpc/errors.js +34 -0
  30. package/dist/rpc/framing.d.ts +21 -0
  31. package/dist/rpc/framing.js +57 -0
  32. package/dist/rpc/handshake.d.ts +19 -0
  33. package/dist/rpc/handshake.js +8 -0
  34. package/dist/rpc/reconnect.d.ts +97 -0
  35. package/dist/rpc/reconnect.js +227 -0
  36. package/dist/rpc/timers.d.ts +11 -0
  37. package/dist/rpc/timers.js +11 -0
  38. package/dist/service.d.ts +50 -0
  39. package/dist/service.js +143 -0
  40. package/dist/setup.d.ts +2 -0
  41. package/dist/setup.js +146 -0
  42. package/dist/supervise.d.ts +12 -0
  43. package/dist/supervise.js +81 -0
  44. package/dist/websocket.d.ts +51 -0
  45. package/dist/websocket.js +190 -0
  46. package/package.json +168 -0
  47. package/src/acp-probe.js +46 -0
  48. package/src/antigravity-probe.js +106 -0
  49. package/src/command.js +18 -0
  50. package/src/doctor.d.ts +13 -0
  51. package/src/doctor.js +570 -0
  52. package/src/index.js +457 -0
  53. package/src/ipc.js +92 -0
  54. package/src/library-test.js +41 -0
  55. package/src/mcp-probe.js +118 -0
  56. package/src/portable.js +27 -0
  57. package/src/prompt.js +64 -0
  58. package/src/render.js +136 -0
  59. package/src/rpc/client.js +296 -0
  60. package/src/rpc/errors.js +54 -0
  61. package/src/rpc/framing.js +61 -0
  62. package/src/rpc/handshake.js +16 -0
  63. package/src/rpc/reconnect.js +226 -0
  64. package/src/rpc/timers.js +16 -0
  65. package/src/service.js +154 -0
  66. package/src/setup.js +150 -0
  67. package/src/supervise.d.ts +12 -0
  68. package/src/supervise.js +85 -0
  69. package/src/websocket.js +259 -0
@@ -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
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Newline-delimited JSON framing for stream transports (a Unix socket, a named pipe, a TCP
3
+ * stream). Portable: strings in, strings out; the stream owner decodes bytes to text first.
4
+ * Message transports such as a WebSocket carry whole frames and need none of this.
5
+ */
6
+ /** UTF-8 length of `text` without Buffer or TextEncoder; an unpaired surrogate counts as U+FFFD (3 bytes). @param {string} text */
7
+ export function utf8Length(text: string): number;
8
+ /**
9
+ * Splits decoded text chunks into frames. `push` returns the complete frames in order, blank
10
+ * lines skipped, and throws once a frame (complete or still arriving) exceeds `maxFrame` bytes;
11
+ * the decoder is unusable after that. Each character is measured once, so a frame that arrives
12
+ * in many chunks costs linear time.
13
+ * @param {{ maxFrame: number }} options
14
+ * @returns {{ push(chunk: string): string[], readonly buffered: number }}
15
+ */
16
+ export function createLineDecoder({ maxFrame }: {
17
+ maxFrame: number;
18
+ }): {
19
+ push(chunk: string): string[];
20
+ readonly buffered: number;
21
+ };
@@ -0,0 +1,57 @@
1
+ // packages/client/src/rpc/framing.js
2
+ function utf8Length(text) {
3
+ let bytes = 0;
4
+ for (let index = 0; index < text.length; index++) {
5
+ const code = text.charCodeAt(index);
6
+ if (code < 128) bytes += 1;
7
+ else if (code < 2048) bytes += 2;
8
+ else if (code >= 55296 && code <= 56319 && index + 1 < text.length && (text.charCodeAt(index + 1) & 64512) === 56320) {
9
+ bytes += 4;
10
+ index++;
11
+ } else bytes += 3;
12
+ }
13
+ return bytes;
14
+ }
15
+ function createLineDecoder({ maxFrame }) {
16
+ let parts = [];
17
+ let bytes = 0;
18
+ let failed = false;
19
+ const overflow = () => {
20
+ failed = true;
21
+ parts = [];
22
+ bytes = 0;
23
+ return new RangeError(`Frame exceeds ${maxFrame} bytes`);
24
+ };
25
+ return {
26
+ push(chunk) {
27
+ if (failed) throw new RangeError("Line decoder already failed");
28
+ const frames = [];
29
+ let start = 0;
30
+ for (let index = chunk.indexOf("\n"); index >= 0; index = chunk.indexOf("\n", start)) {
31
+ const piece = chunk.slice(start, index);
32
+ bytes += utf8Length(piece);
33
+ if (bytes > maxFrame) throw overflow();
34
+ parts.push(piece);
35
+ const line = parts.length === 1 ? parts[0] : parts.join("");
36
+ parts = [];
37
+ bytes = 0;
38
+ start = index + 1;
39
+ if (line.trim()) frames.push(line);
40
+ }
41
+ if (start < chunk.length) {
42
+ const rest = chunk.slice(start);
43
+ bytes += utf8Length(rest);
44
+ if (bytes > maxFrame) throw overflow();
45
+ parts.push(rest);
46
+ }
47
+ return frames;
48
+ },
49
+ get buffered() {
50
+ return bytes;
51
+ }
52
+ };
53
+ }
54
+ export {
55
+ createLineDecoder,
56
+ utf8Length
57
+ };
@@ -0,0 +1,19 @@
1
+ /** @typedef {{ name: string, version: string }} ClientInfo */
2
+ /**
3
+ * The version handshake every ALP connection starts with (plans/reference/ALPD.md §3): alpd
4
+ * answers nothing else before it. Shared by the Node `connect` and portable clients, so a change
5
+ * to the handshake (Task 06) lands in one place.
6
+ * @param {{ request(method: string, params?: unknown, options?: import('./client.js').RequestOptions): Promise<any> }} client
7
+ * @param {ClientInfo} [info]
8
+ * @param {import('./client.js').RequestOptions} [options]
9
+ * @returns {Promise<{ protocolVersion: number } & Record<string, unknown>>}
10
+ */
11
+ export function hello(client: {
12
+ request(method: string, params?: unknown, options?: import("./client.js").RequestOptions): Promise<any>;
13
+ }, info?: ClientInfo, options?: import("./client.js").RequestOptions): Promise<{
14
+ protocolVersion: number;
15
+ } & Record<string, unknown>>;
16
+ export type ClientInfo = {
17
+ name: string;
18
+ version: string;
19
+ };
@@ -0,0 +1,8 @@
1
+ // packages/client/src/rpc/handshake.js
2
+ import { PROTOCOL_VERSION } from "@anhlp/alp-protocol";
3
+ function hello(client, info = { name: "alp", version: "0" }, options) {
4
+ return client.request("daemon.hello", { protocolVersion: PROTOCOL_VERSION, client: info }, options);
5
+ }
6
+ export {
7
+ hello
8
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Keeps an ALP connection up: connects, runs the caller's `onConnect` (handshake, reattach and
3
+ * replay as the protocol defines them), and after a disconnect connects again with exponential
4
+ * backoff until stopped. A connection counts as up only once it has stayed open `stableMs`; one
5
+ * that drops sooner (a gateway closing right after the handshake) counts as a failed attempt, so
6
+ * backoff and `maxAttempts` still apply. It never resends a request: requests in flight when the connection drops
7
+ * reject as `disconnected`, and requests made while it is down reject at once. Whether and how to
8
+ * retry belongs to the caller, who knows which methods are safe to repeat.
9
+ */
10
+ /**
11
+ * @typedef {import('./client.js').AlpTransport} AlpTransport
12
+ * @typedef {import('./client.js').AlpRpcClientOptions} AlpRpcClientOptions
13
+ * @typedef {import('./client.js').RequestOptions} RequestOptions
14
+ * @typedef {import('./client.js').AbortSignalLike} AbortSignalLike
15
+ * @typedef {'connecting' | 'open' | 'waiting' | 'stopped'} ReconnectState
16
+ * @typedef {{ initialMs?: number, maxMs?: number, factor?: number, jitter?: number }} Backoff
17
+ * @typedef {{
18
+ * connect(context: { attempt: number, signal: AbortSignalLike }): Promise<AlpTransport>,
19
+ * onConnect?(client: AlpRpcClient, context: { attempt: number, reconnect: boolean, signal: AbortSignalLike }): unknown,
20
+ * onDisconnect?(error: Error, context: { reconnect: boolean }): void,
21
+ * backoff?: Backoff,
22
+ * maxAttempts?: number,
23
+ * stableMs?: number,
24
+ * shouldRetry?(error: Error): boolean,
25
+ * client?: AlpRpcClientOptions,
26
+ * random?: () => number,
27
+ * }} ReconnectOptions
28
+ * @typedef {{
29
+ * readonly state: ReconnectState,
30
+ * readonly client: AlpRpcClient | undefined,
31
+ * readonly attempt: number,
32
+ * ready(options?: { signal?: AbortSignalLike }): Promise<AlpRpcClient>,
33
+ * request<T = any>(method: string, params?: unknown, options?: RequestOptions): Promise<T>,
34
+ * onEvent(listener: (envelope: any) => void): () => boolean,
35
+ * onNotification(listener: (method: string, params: any) => void): () => boolean,
36
+ * onState(listener: (state: ReconnectState, error?: Error) => void): () => boolean,
37
+ * stop(): void,
38
+ * }} ReconnectingClient
39
+ */
40
+ /** The delay before attempt `attempt` (1-based), with ±jitter. @param {number} attempt @param {Backoff} backoff @param {() => number} random */
41
+ export function backoffDelay(attempt: number, { initialMs, maxMs, factor, jitter }?: Backoff, random?: () => number): number;
42
+ /**
43
+ * Whether a failed connect or onConnect is worth another attempt: everything but alpd refusing the
44
+ * handshake (ERROR.protocol, an unsupported protocol version), which retrying cannot fix.
45
+ * @param {Error} error
46
+ */
47
+ export function retryable(error: Error): boolean;
48
+ /**
49
+ * @param {ReconnectOptions} options
50
+ * @returns {ReconnectingClient}
51
+ */
52
+ export function createReconnectingClient({ connect, onConnect, onDisconnect, backoff, maxAttempts, stableMs, shouldRetry, client: clientOptions, random }: ReconnectOptions): ReconnectingClient;
53
+ export type AlpTransport = import("./client.js").AlpTransport;
54
+ export type AlpRpcClientOptions = import("./client.js").AlpRpcClientOptions;
55
+ export type RequestOptions = import("./client.js").RequestOptions;
56
+ export type AbortSignalLike = import("./client.js").AbortSignalLike;
57
+ export type ReconnectState = "connecting" | "open" | "waiting" | "stopped";
58
+ export type Backoff = {
59
+ initialMs?: number;
60
+ maxMs?: number;
61
+ factor?: number;
62
+ jitter?: number;
63
+ };
64
+ export type ReconnectOptions = {
65
+ connect(context: {
66
+ attempt: number;
67
+ signal: AbortSignalLike;
68
+ }): Promise<AlpTransport>;
69
+ onConnect?(client: AlpRpcClient, context: {
70
+ attempt: number;
71
+ reconnect: boolean;
72
+ signal: AbortSignalLike;
73
+ }): unknown;
74
+ onDisconnect?(error: Error, context: {
75
+ reconnect: boolean;
76
+ }): void;
77
+ backoff?: Backoff;
78
+ maxAttempts?: number;
79
+ stableMs?: number;
80
+ shouldRetry?(error: Error): boolean;
81
+ client?: AlpRpcClientOptions;
82
+ random?: () => number;
83
+ };
84
+ export type ReconnectingClient = {
85
+ readonly state: ReconnectState;
86
+ readonly client: AlpRpcClient | undefined;
87
+ readonly attempt: number;
88
+ ready(options?: {
89
+ signal?: AbortSignalLike;
90
+ }): Promise<AlpRpcClient>;
91
+ request<T = any>(method: string, params?: unknown, options?: RequestOptions): Promise<T>;
92
+ onEvent(listener: (envelope: any) => void): () => boolean;
93
+ onNotification(listener: (method: string, params: any) => void): () => boolean;
94
+ onState(listener: (state: ReconnectState, error?: Error) => void): () => boolean;
95
+ stop(): void;
96
+ };
97
+ import { AlpRpcClient } from './client.js';