@anhlp/alp-client 0.0.1-beta.3 → 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/README.md CHANGED
@@ -11,13 +11,18 @@ and setup helpers.
11
11
  - `@anhlp/alp-client` (Node): everything above plus `connect`, `AlpClient`, `connectSocket`,
12
12
  `ensureDaemon`, the private-socket checks and lock helpers, unchanged for existing callers.
13
13
  - `@anhlp/alp-client/ipc` (Node): `createSocketTransport` for a Unix socket or named pipe.
14
+ - `@anhlp/alp-client/integration-lock` (Node): `createIntegrationLock(home, name)`, the single-owner
15
+ lock of one integration service at `<home>/integrations/<name>.lock`, serialized by the exclusive
16
+ guard directory `<name>.lock.reclaim`. Each instance is single-use (`acquire`, then `setReady` and
17
+ `release` while it owns the lock); its recovery rules are in
18
+ [docs/modular-monorepo.md](../../docs/modular-monorepo.md).
14
19
 
15
20
  Timeouts and aborts end only the caller's wait, and nothing is ever resent after a disconnect.
16
21
  See [docs/alp/client-sdk.md](../../docs/alp/client-sdk.md) for the transport contract, the
17
22
  request lifecycle, the old-to-new API table and what still waits on the protocol (Task 06) and
18
23
  the gateway (Task 08).
19
24
 
20
- `startIntegration` discovers an optional integration's descriptor and starts it detached with `--service`, so the entry writes its own log; discovery and the selected `ALP_HOME` are pinned by tests. Restarting is non-destructive: a daemon left running by an older install is reported as older than the current CLI and is never killed in place.
25
+ `startIntegration({ name, entry, home, args })` (or `startIntegration(name)`; `name` defaults to `web`) discovers an optional integration's descriptor and starts it detached with `--service` and then `args`, so the entry writes its own log; discovery and the selected `ALP_HOME` are pinned by tests. `name` is a single lowercase path segment (`assertIntegrationName`). With `entry`, that script runs with the current Node and no descriptor, environment or PATH discovery happens; a missing entry is an error even when a live owner would be reused. `web` is alp-web, the service of the ALP CLI (`alp web`): it is never discovered, so it needs its `entry`, and `"web": { "enabled": false }` in the home's settings refuses it and no other integration. Everything once registered under `web` (`integrations/web.json`, the `alp-web-integration` binary, `ALP_WEB_INTEGRATION`) is the Paseo integration from before its rename; `discoverIntegration('paseo')` reads `ALP_PASEO_INTEGRATION`, then `ALP_WEB_INTEGRATION` for one release, then `ALP_INTEGRATION_ENTRY`. Errors name the integration, the entry and its log under `<home>/logs/integrations`. Either way a live owner of `integrations/<name>.lock` is reused, and the call resolves once that lock is ready and `<home>/<name>.json` names the same pid. Restarting is non-destructive: a daemon left running by an older install is reported as older than the current CLI and is never killed in place.
21
26
 
22
27
  Build: `npm run build --workspace=@anhlp/alp-client`. Test: `npm run test --workspace=@anhlp/alp-client`.
23
28
 
package/dist/index.d.ts CHANGED
@@ -21,6 +21,13 @@ export function findDaemonEntry({ home, env, candidates }?: {
21
21
  env?: NodeJS.ProcessEnv | undefined;
22
22
  candidates?: (string | undefined)[] | undefined;
23
23
  }): Promise<string | undefined>;
24
+ /**
25
+ * Integration names are single lowercase path segments: they name `<home>/integrations/<name>.lock`,
26
+ * its guard and `<home>/<name>.json`. Returns the name, or throws a TypeError.
27
+ * @param {unknown} name
28
+ * @returns {string}
29
+ */
30
+ export function assertIntegrationName(name: unknown): string;
24
31
  /** Wall-clock boot time; a lock from an earlier boot is stale even if its pid is reused. */
25
32
  export function bootTime(): number;
26
33
  export function readLock(home: any): Promise<any>;
@@ -95,8 +102,34 @@ export function ensureDaemon({ home, entry, env, execPath, timeoutMs, lifecycleL
95
102
  timeoutMs?: number | undefined;
96
103
  lifecycleLocked?: boolean | undefined;
97
104
  }): Promise<any>;
98
- export function discoverIntegration(name?: string, options?: {}): Promise<string | undefined>;
99
- export function startIntegration(nameOrOptions?: string): Promise<any>;
105
+ /**
106
+ * The entry of integration `name`: an environment override, a descriptor under the home or a
107
+ * distribution root, or an `alp-<name>-integration` binary. Discovery never returns an entry for
108
+ * `web`: that name belongs to alp-web, which ships with the CLI and is started with its explicit
109
+ * entry. Everything once registered under `web` (the descriptor `integrations/web.json`, the
110
+ * `alp-web-integration` binary, `ALP_WEB_INTEGRATION`) is the Paseo integration from before its
111
+ * rename to `paseo`, which must never run under the web lock. `ALP_WEB_INTEGRATION` is read for
112
+ * `paseo` only, after `ALP_PASEO_INTEGRATION`, for one release (like the alp-web-integration alias).
113
+ * @param {string} [name]
114
+ * @param {{ home?: string }} [options]
115
+ */
116
+ export function discoverIntegration(name?: string, options?: {
117
+ home?: string;
118
+ }): Promise<string | undefined>;
119
+ /**
120
+ * Starts integration `name` (default `web`) detached with `--service` (then `args`) unless its lock
121
+ * already names a live owner, then waits until its ready lock and `<home>/<name>.json` name the same
122
+ * pid. `entry` names the script to run with this Node and skips descriptor, environment and PATH
123
+ * discovery; `web` (alp-web) is never discovered, so it needs its entry. `"web": { "enabled": false }`
124
+ * in the home's settings.json refuses `web` only.
125
+ * @param {string | { name?: string, entry?: string, home?: string, args?: string[] }} [nameOrOptions]
126
+ */
127
+ export function startIntegration(nameOrOptions?: string | {
128
+ name?: string;
129
+ entry?: string;
130
+ home?: string;
131
+ args?: string[];
132
+ }): Promise<any>;
100
133
  export * from "./portable.js";
101
134
  export { createSocketTransport } from "./ipc.js";
102
135
  /**
@@ -110,6 +143,8 @@ export class AlpClient extends AlpRpcClient {
110
143
  * @param {import('./rpc/client.js').AlpRpcClientOptions & import('./ipc.js').SocketTransportOptions} [options]
111
144
  */
112
145
  constructor(socket: import("node:net").Socket, { maxFrame, maxBufferedBytes, ...options }?: import("./rpc/client.js").AlpRpcClientOptions & import("./ipc.js").SocketTransportOptions);
146
+ /** Host features captured by connect's handshake. @type {readonly string[]} */
147
+ capabilities: readonly string[];
113
148
  }
114
149
  import { AlpRpcClient } from './rpc/client.js';
115
150
  export { PROTOCOL_VERSION, MAX_FRAME, AlpRpcError, encodeFrame } from "@anhlp/alp-protocol";
package/dist/index.js CHANGED
@@ -98,6 +98,10 @@ async function findDaemonEntry({ home = alpHome(), env = process.env, candidates
98
98
  }
99
99
  return void 0;
100
100
  }
101
+ function assertIntegrationName(name) {
102
+ if (typeof name !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(name)) throw new TypeError(`Invalid integration name: ${JSON.stringify(name)}`);
103
+ return name;
104
+ }
101
105
  function bootTime() {
102
106
  return Math.round((Date.now() - os.uptime() * 1e3) / 1e3);
103
107
  }
@@ -144,6 +148,8 @@ async function releaseDaemonLock(home) {
144
148
  });
145
149
  }
146
150
  var AlpClient = class extends AlpRpcClient {
151
+ /** Host features captured by connect's handshake. @type {readonly string[]} */
152
+ capabilities = [];
147
153
  /**
148
154
  * @param {import('node:net').Socket} socket a connected or connecting socket; the client owns it
149
155
  * @param {import('./rpc/client.js').AlpRpcClientOptions & import('./ipc.js').SocketTransportOptions} [options]
@@ -172,7 +178,8 @@ async function connectSocket(socketPath, options) {
172
178
  async function connect(socketPath, client = { name: "alp", version: "0" }, options = {}) {
173
179
  const connection = new AlpClient(await openSocket(socketPath), options);
174
180
  try {
175
- await hello(connection, client);
181
+ const greeting = await hello(connection, client);
182
+ connection.capabilities = Object.freeze(Array.isArray(greeting.capabilities) ? greeting.capabilities.filter((value) => typeof value === "string") : []);
176
183
  } catch (error) {
177
184
  connection.close();
178
185
  throw error;
@@ -346,7 +353,7 @@ async function integrationCandidateFromHome(home, name) {
346
353
  return await isFile(legacy) ? legacy : void 0;
347
354
  }
348
355
  function integrationBinaryName(name) {
349
- return name === "web" ? "alp-web-integration" : `alp-${name}-integration`;
356
+ return `alp-${name}-integration`;
350
357
  }
351
358
  async function distributionRoots() {
352
359
  const roots = [];
@@ -366,10 +373,12 @@ async function distributionRoots() {
366
373
  return [...new Set(roots)];
367
374
  }
368
375
  async function discoverIntegration(name = "web", options = {}) {
376
+ assertIntegrationName(name);
377
+ if (name === "web") return void 0;
369
378
  const envCandidates = [
370
- process.env.ALP_WEB_INTEGRATION,
371
- process.env.ALP_INTEGRATION_ENTRY,
372
- name !== "web" ? process.env[`ALP_${name.toUpperCase()}_INTEGRATION`] : void 0
379
+ process.env[`ALP_${name.toUpperCase()}_INTEGRATION`],
380
+ name === "paseo" ? process.env.ALP_WEB_INTEGRATION : void 0,
381
+ process.env.ALP_INTEGRATION_ENTRY
373
382
  ].filter(Boolean);
374
383
  for (const candidate of envCandidates) if (await isFile(candidate)) return candidate;
375
384
  const home = options?.home ? path.resolve(options.home) : alpHome();
@@ -398,21 +407,46 @@ function integrationAlive(lock) {
398
407
  if (typeof lock.bootTime === "number" && Math.abs(lock.bootTime - bootTime()) > 30) return false;
399
408
  return true;
400
409
  }
410
+ function integrationText(name, home) {
411
+ const logs = path.join(home, "logs", "integrations");
412
+ if (name === "web") return {
413
+ what: "alp web",
414
+ log: path.join(logs, "alp-web.log"),
415
+ missing: "alp web runs the alp-web service of the ALP CLI, which is started with its entry (packages/cli/dist/alp-web.js); run `alp web`"
416
+ };
417
+ if (name === "paseo") return {
418
+ what: "The Paseo integration",
419
+ log: path.join(logs, "paseo.log"),
420
+ missing: "The Paseo integration is an optional integration package. Install it (for example `npm install -g @anhlp/alp-paseo@beta`), then run the command again."
421
+ };
422
+ return {
423
+ what: `ALP integration ${name}`,
424
+ log: path.join(logs, `${name}.log`),
425
+ missing: `ALP integration ${name} is not installed: no ALP_${name.toUpperCase()}_INTEGRATION, descriptor <home>/integrations/${name}.json or alp-${name}-integration binary was found.`
426
+ };
427
+ }
401
428
  async function startIntegration(nameOrOptions = "web") {
402
429
  const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
403
- const name = options.name ?? "web";
430
+ const name = assertIntegrationName(options.name ?? "web");
431
+ if (options.entry !== void 0 && (typeof options.entry !== "string" || !options.entry)) throw new TypeError("startIntegration entry must be a non-empty path");
432
+ const args = options.args ?? [];
433
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) throw new TypeError("startIntegration args must be a list of strings");
404
434
  const fs = await import("node:fs/promises");
405
435
  const pathMod = await import("node:path");
406
436
  const home = options.home ? pathMod.resolve(options.home) : alpHome();
437
+ const text = integrationText(name, home);
438
+ const explicit = options.entry !== void 0 ? pathMod.resolve(options.entry) : void 0;
439
+ if (explicit !== void 0 && !await isFile(explicit)) throw new Error(`${text.what}: integration entry not found: ${explicit}`);
407
440
  const lockPath = pathMod.join(home, "integrations", `${name}.lock`);
408
441
  const webPath = pathMod.join(home, `${name}.json`);
409
- let settings = {};
410
- try {
411
- settings = JSON.parse(await fs.readFile(pathMod.join(home, "settings.json"), "utf8"));
412
- } catch {
413
- }
414
- if (settings?.web?.enabled === false) {
415
- throw new Error('ALP web app is disabled by settings ("web.enabled": false).');
442
+ const started = explicit ? ` (entry ${explicit})` : "";
443
+ if (name === "web") {
444
+ let settings = {};
445
+ try {
446
+ settings = JSON.parse(await fs.readFile(pathMod.join(home, "settings.json"), "utf8"));
447
+ } catch {
448
+ }
449
+ if (settings?.web?.enabled === false) throw new Error('alp web is disabled by settings ("web.enabled": false).');
416
450
  }
417
451
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
418
452
  const readLock2 = async () => {
@@ -440,20 +474,18 @@ async function startIntegration(nameOrOptions = "web") {
440
474
  await sleep(100);
441
475
  }
442
476
  const lock = await readLock2();
443
- if (lock && !lock.ready) throw new Error("ALP web integration started but did not become ready; see ~/.alp/logs/integrations/paseo.log");
444
- if (!lock) throw new Error("ALP web app is provided by an optional integration package. Install it (for example `npm install -g @anhlp/alp-paseo@beta`) and run `alp web` again.");
445
- throw new Error("ALP web integration did not publish a matching web.json lock");
477
+ if (lock && !lock.ready) throw new Error(`${text.what}${started} started but did not become ready; see ${text.log}`);
478
+ if (!lock) throw new Error(explicit ? `${text.what}${started} did not start; see ${text.log}` : text.missing);
479
+ throw new Error(`${text.what}${started} did not publish a ${name}.json matching its lock; see ${text.log}`);
446
480
  }
447
481
  const existing = await readLock2();
448
482
  if (existing && integrationAlive(existing)) {
449
483
  return waitForReady();
450
484
  }
451
- const entry = await discoverIntegration(name, { home });
452
- if (!entry) {
453
- throw new Error("ALP web app is provided by an optional integration package. Install it (for example `npm install -g @anhlp/alp-paseo@beta`) and run `alp web` again.");
454
- }
485
+ const entry = explicit ?? await discoverIntegration(name, { home });
486
+ if (!entry) throw new Error(text.missing);
455
487
  const { spawn: spawn2 } = await import("node:child_process");
456
- const child = spawn2(process.execPath, [entry, "--service"], { stdio: "ignore", detached: true, env: { ...process.env, ALP_HOME: home } });
488
+ const child = spawn2(process.execPath, [entry, "--service", ...args], { stdio: "ignore", detached: true, env: { ...process.env, ALP_HOME: home } });
457
489
  let spawnError = null;
458
490
  child.on("error", (error) => {
459
491
  spawnError = error;
@@ -466,7 +498,7 @@ async function startIntegration(nameOrOptions = "web") {
466
498
  if (spawnError) throw spawnError;
467
499
  await sleep(150);
468
500
  if (spawnError) throw spawnError;
469
- if (exited) throw new Error("ALP web integration failed to start; see ~/.alp/logs/integrations/paseo.log");
501
+ if (exited) throw new Error(`${text.what}${explicit ? started : ` (${entry})`} failed to start; see ${text.log}`);
470
502
  return waitForReady();
471
503
  }
472
504
  export {
@@ -477,6 +509,7 @@ export {
477
509
  PROTOCOL_VERSION,
478
510
  acquireDaemonLock,
479
511
  alpHome,
512
+ assertIntegrationName,
480
513
  assertPrivateDirectory,
481
514
  assertPrivateSocket,
482
515
  bootTime,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @typedef {{ pid: number, bootTime: number, startedAt: string, ready: boolean, lockVersion?: number, owner?: string }} IntegrationLockRecord
3
+ * @typedef {{
4
+ * acquire(): Promise<{ acquired: boolean, lock: IntegrationLockRecord }>,
5
+ * setReady(ready: boolean): Promise<boolean>,
6
+ * release(): Promise<boolean>,
7
+ * }} IntegrationLock
8
+ */
9
+ /**
10
+ * The single-owner lock of one integration service, at `<home>/integrations/<name>.lock`, with its
11
+ * mutation guard `<name>.lock.reclaim`. Each instance is single-use: acquire once, then setReady and
12
+ * release while it owns the lock.
13
+ *
14
+ * Single-host, same PID namespace, private local filesystem only. Never reap the mutation guard.
15
+ * @param {string} home the ALP_HOME the integration runs under
16
+ * @param {string} name the integration name, for example `web`
17
+ * @returns {IntegrationLock}
18
+ */
19
+ export function createIntegrationLock(home: string, name: string): IntegrationLock;
20
+ export type IntegrationLockRecord = {
21
+ pid: number;
22
+ bootTime: number;
23
+ startedAt: string;
24
+ ready: boolean;
25
+ lockVersion?: number;
26
+ owner?: string;
27
+ };
28
+ export type IntegrationLock = {
29
+ acquire(): Promise<{
30
+ acquired: boolean;
31
+ lock: IntegrationLockRecord;
32
+ }>;
33
+ setReady(ready: boolean): Promise<boolean>;
34
+ release(): Promise<boolean>;
35
+ };
@@ -0,0 +1,179 @@
1
+ // packages/client/src/integration-lock.js
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, open, rename, rmdir, unlink } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { assertIntegrationName, bootTime } from "./index.js";
6
+ function createIntegrationLock(home, name) {
7
+ const file = path.join(home, "integrations", `${assertIntegrationName(name)}.lock`);
8
+ const guard = `${file}.reclaim`;
9
+ let phase = "new";
10
+ let owned;
11
+ const recovery = (reason) => new Error(`${reason}. Stop ALL integration processes using this ALP_HOME before manual lock-only recovery of ${file} and ${guard}; see docs/modular-monorepo.md`);
12
+ async function guarded(run) {
13
+ await mkdir(path.dirname(file), { recursive: true, mode: 448 });
14
+ const deadline = Date.now() + 2e4;
15
+ for (; ; ) {
16
+ try {
17
+ await mkdir(guard, { mode: 448 });
18
+ break;
19
+ } catch (error) {
20
+ if (
21
+ /** @type {any} */
22
+ error?.code !== "EEXIST"
23
+ ) throw error;
24
+ if (Date.now() >= deadline) throw recovery("Integration mutation guard remains busy or abandoned");
25
+ await new Promise((resolve) => setTimeout(resolve, 25));
26
+ }
27
+ }
28
+ const token = path.join(guard, `owner-${process.pid}-${randomUUID()}`);
29
+ try {
30
+ const handle = await open(token, "wx", 384);
31
+ await handle.close();
32
+ } catch (error) {
33
+ throw recovery(`Integration guard initialization failed: ${String(error)}`);
34
+ }
35
+ try {
36
+ return await run();
37
+ } finally {
38
+ try {
39
+ await unlink(token);
40
+ } catch (error) {
41
+ throw recovery(
42
+ /** @type {any} */
43
+ error?.code === "ENOENT" ? "Integration guard ownership was lost" : `Integration guard token cleanup failed: ${String(error)}`
44
+ );
45
+ }
46
+ try {
47
+ await rmdir(guard);
48
+ } catch (error) {
49
+ throw recovery(`Integration guard cleanup failed: ${String(error)}`);
50
+ }
51
+ }
52
+ }
53
+ async function snapshot() {
54
+ let handle;
55
+ try {
56
+ handle = await open(file, "r");
57
+ } catch (error) {
58
+ if (
59
+ /** @type {any} */
60
+ error?.code === "ENOENT"
61
+ ) return void 0;
62
+ throw error;
63
+ }
64
+ try {
65
+ const info = await handle.stat();
66
+ const raw = await handle.readFile("utf8");
67
+ let value;
68
+ try {
69
+ value = JSON.parse(raw);
70
+ } catch {
71
+ value = void 0;
72
+ }
73
+ return { value, info };
74
+ } finally {
75
+ await handle.close();
76
+ }
77
+ }
78
+ function complete(value) {
79
+ return value && Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.bootTime === "number" && Number.isFinite(value.bootTime) && value.bootTime > 0 && typeof value.startedAt === "string" && Number.isFinite(Date.parse(value.startedAt)) && typeof value.ready === "boolean" && (value.lockVersion === void 0 && value.owner === void 0 || value.lockVersion === 1 && typeof value.owner === "string" && /^[a-f0-9-]{36}$/.test(value.owner));
80
+ }
81
+ function dead(value) {
82
+ try {
83
+ process.kill(value.pid, 0);
84
+ return false;
85
+ } catch (error) {
86
+ if (
87
+ /** @type {any} */
88
+ error?.code !== "ESRCH"
89
+ ) return false;
90
+ if (Math.abs(value.bootTime - bootTime()) > 30) throw recovery("Integration owner boot context is uncertain");
91
+ return true;
92
+ }
93
+ }
94
+ function matches(current) {
95
+ return !!owned && current?.value?.owner === owned.owner && current.info.dev === owned.dev && current.info.ino === owned.ino;
96
+ }
97
+ return {
98
+ async acquire() {
99
+ if (phase !== "new") throw new Error("Integration lock instance has already been used");
100
+ phase = "acquiring";
101
+ try {
102
+ return await guarded(async () => {
103
+ const current = await snapshot();
104
+ if (current) {
105
+ if (!complete(current.value)) throw recovery("Integration lock is malformed, incomplete or unsupported");
106
+ if (!dead(current.value)) {
107
+ phase = "retired";
108
+ return { acquired: false, lock: current.value };
109
+ }
110
+ await unlink(file);
111
+ }
112
+ const value = { pid: process.pid, bootTime: bootTime(), startedAt: (/* @__PURE__ */ new Date()).toISOString(), ready: false, lockVersion: 1, owner: randomUUID() };
113
+ const handle = await open(file, "wx", 384);
114
+ let info;
115
+ try {
116
+ await handle.writeFile(JSON.stringify(value));
117
+ info = await handle.stat();
118
+ } finally {
119
+ await handle.close();
120
+ }
121
+ owned = { owner: value.owner, dev: info.dev, ino: info.ino };
122
+ phase = "owned";
123
+ return { acquired: true, lock: value };
124
+ });
125
+ } catch (error) {
126
+ phase = "retired";
127
+ owned = void 0;
128
+ throw error;
129
+ }
130
+ },
131
+ async setReady(ready) {
132
+ if (phase !== "owned") return false;
133
+ return guarded(async () => {
134
+ if (phase !== "owned") return false;
135
+ const current = await snapshot();
136
+ if (!current || !matches(current)) {
137
+ phase = "retired";
138
+ owned = void 0;
139
+ return false;
140
+ }
141
+ const value = { ...current.value, ready };
142
+ const tmp = `${file}.${randomUUID()}.tmp`;
143
+ const handle = await open(tmp, "wx", 384);
144
+ let info;
145
+ try {
146
+ try {
147
+ await handle.writeFile(JSON.stringify(value));
148
+ info = await handle.stat();
149
+ } finally {
150
+ await handle.close();
151
+ }
152
+ await rename(tmp, file);
153
+ owned = { owner: value.owner, dev: info.dev, ino: info.ino };
154
+ return true;
155
+ } finally {
156
+ await unlink(tmp).catch((error) => {
157
+ if (error?.code !== "ENOENT") throw error;
158
+ });
159
+ }
160
+ });
161
+ },
162
+ async release() {
163
+ if (phase !== "owned") return false;
164
+ return guarded(async () => {
165
+ if (phase !== "owned") return false;
166
+ const current = await snapshot();
167
+ const ours = matches(current);
168
+ phase = "retired";
169
+ owned = void 0;
170
+ if (!ours) return false;
171
+ await unlink(file);
172
+ return true;
173
+ });
174
+ }
175
+ };
176
+ }
177
+ export {
178
+ createIntegrationLock
179
+ };
@@ -16,3 +16,4 @@ export { AlpClientError, CLIENT_ERROR, isAlpClientError } from "./rpc/errors.js"
16
16
  export { createLineDecoder, utf8Length } from "./rpc/framing.js";
17
17
  export { MAX_TIMER_MS, timerDelay } from "./rpc/timers.js";
18
18
  export { backoffDelay, createReconnectingClient, retryable } from "./rpc/reconnect.js";
19
+ export { attachments, base64Encode, conversation, sha256Hex, workspaceRegistry } from "./rpc/registry.js";
package/dist/portable.js CHANGED
@@ -6,6 +6,7 @@ import { createLineDecoder, utf8Length } from "./rpc/framing.js";
6
6
  import { MAX_TIMER_MS, timerDelay } from "./rpc/timers.js";
7
7
  import { hello } from "./rpc/handshake.js";
8
8
  import { backoffDelay, createReconnectingClient, retryable } from "./rpc/reconnect.js";
9
+ import { attachments, base64Encode, conversation, sha256Hex, workspaceRegistry } from "./rpc/registry.js";
9
10
  export {
10
11
  AlpClientError,
11
12
  AlpRpcClient,
@@ -16,12 +17,17 @@ export {
16
17
  MAX_FRAME,
17
18
  MAX_TIMER_MS,
18
19
  PROTOCOL_VERSION,
20
+ attachments,
19
21
  backoffDelay,
22
+ base64Encode,
23
+ conversation,
20
24
  createLineDecoder,
21
25
  createReconnectingClient,
22
26
  hello,
23
27
  isAlpClientError,
24
28
  retryable,
29
+ sha256Hex,
25
30
  timerDelay,
26
- utf8Length
31
+ utf8Length,
32
+ workspaceRegistry
27
33
  };