@the-open-engine/zeroshot 6.26.0 → 6.28.0

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 (49) hide show
  1. package/cli/index.js +211 -2
  2. package/lib/cluster/client.cjs +30 -5
  3. package/lib/cluster/client.d.ts +11 -1
  4. package/lib/cluster/client.mjs +29 -5
  5. package/lib/cluster/connection.cjs +38 -2
  6. package/lib/cluster/connection.d.ts +3 -0
  7. package/lib/cluster/connection.mjs +37 -1
  8. package/lib/cluster/index.cjs +3 -1
  9. package/lib/cluster/index.d.ts +3 -3
  10. package/lib/cluster/index.mjs +2 -2
  11. package/lib/hosted-session/coordinator.cjs +101 -0
  12. package/lib/hosted-session/coordinator.d.ts +9 -0
  13. package/lib/hosted-session/coordinator.mjs +97 -0
  14. package/lib/hosted-session/index.cjs +5 -0
  15. package/lib/hosted-session/index.d.ts +2 -0
  16. package/lib/hosted-session/index.mjs +1 -0
  17. package/lib/hosted-session/types.cjs +2 -0
  18. package/lib/hosted-session/types.d.ts +20 -0
  19. package/lib/hosted-session/types.mjs +1 -0
  20. package/lib/target/credential-lock.d.ts +1 -0
  21. package/lib/target/credential-lock.js +38 -0
  22. package/lib/target/credential-store.d.ts +27 -0
  23. package/lib/target/credential-store.js +113 -0
  24. package/lib/target/device-flow.d.ts +38 -0
  25. package/lib/target/device-flow.js +104 -0
  26. package/lib/target/discovery.d.ts +11 -0
  27. package/lib/target/discovery.js +120 -0
  28. package/lib/target/index.d.ts +6 -0
  29. package/lib/target/index.js +38 -0
  30. package/lib/target/target-registry.d.ts +45 -0
  31. package/lib/target/target-registry.js +132 -0
  32. package/lib/target/target-session.d.ts +39 -0
  33. package/lib/target/target-session.js +162 -0
  34. package/package.json +20 -8
  35. package/scripts/build-cluster.js +21 -7
  36. package/src/cluster/client.ts +45 -5
  37. package/src/cluster/connection.ts +32 -1
  38. package/src/cluster/index.ts +4 -2
  39. package/src/cluster/ws.d.ts +1 -0
  40. package/src/hosted-session/coordinator.ts +110 -0
  41. package/src/hosted-session/index.ts +2 -0
  42. package/src/hosted-session/types.ts +21 -0
  43. package/src/target/credential-lock.ts +35 -0
  44. package/src/target/credential-store.ts +107 -0
  45. package/src/target/device-flow.ts +157 -0
  46. package/src/target/discovery.ts +149 -0
  47. package/src/target/index.ts +55 -0
  48. package/src/target/target-registry.ts +174 -0
  49. package/src/target/target-session.ts +249 -0
package/cli/index.js CHANGED
@@ -3958,7 +3958,7 @@ for (const providerName of VALID_PROVIDERS) {
3958
3958
 
3959
3959
  // Settings management
3960
3960
  const settingsCmd = program.command('settings').description('Manage zeroshot settings');
3961
- const INTERNAL_SETTINGS_KEYS = new Set(['lastUpdateCheckClaim']);
3961
+ const INTERNAL_SETTINGS_KEYS = new Set(['lastUpdateCheckClaim', '_targets']);
3962
3962
 
3963
3963
  function visibleSettingKeys() {
3964
3964
  return Object.keys(DEFAULT_SETTINGS).filter((key) => !INTERNAL_SETTINGS_KEYS.has(key));
@@ -4339,8 +4339,12 @@ function parseSettingValue(value) {
4339
4339
 
4340
4340
  function resetGlobalSettings() {
4341
4341
  mutateSettings((settings) => {
4342
+ const preserved = {};
4343
+ for (const key of INTERNAL_SETTINGS_KEYS) {
4344
+ if (key in settings) preserved[key] = settings[key];
4345
+ }
4342
4346
  for (const key of Object.keys(settings)) delete settings[key];
4343
- Object.assign(settings, JSON.parse(JSON.stringify({ ...DEFAULT_SETTINGS })));
4347
+ Object.assign(settings, JSON.parse(JSON.stringify({ ...DEFAULT_SETTINGS })), preserved);
4344
4348
  });
4345
4349
  }
4346
4350
 
@@ -4486,6 +4490,211 @@ settingsCmd.action(() => {
4486
4490
  formatSettingsList(settings, true);
4487
4491
  });
4488
4492
 
4493
+ // Target management commands
4494
+ // Target modules are compiled during install/package preparation for the Node 18+ CLI.
4495
+ const importTarget = (mod) => import(`../lib/target/${mod}.js`);
4496
+ const targetCmd = program.command('target').description('Manage named remote targets');
4497
+
4498
+ targetCmd
4499
+ .command('add <name>')
4500
+ .description('Register a named remote target')
4501
+ .requiredOption('--url <url>', 'Service URL for the target')
4502
+ .action(async (name, options) => {
4503
+ try {
4504
+ const { addTarget } = await importTarget('target-registry');
4505
+ const settingsPort = {
4506
+ load: () => loadSettings(),
4507
+ mutate: (fn) => mutateSettings(fn),
4508
+ };
4509
+ const record = addTarget(name, options.url, settingsPort);
4510
+ console.log(chalk.green(`✓ Target "${name}" added (${record.url})`));
4511
+ } catch (error) {
4512
+ console.error(chalk.red(error.message));
4513
+ process.exit(1);
4514
+ }
4515
+ });
4516
+
4517
+ targetCmd
4518
+ .command('login <name>')
4519
+ .description('Authenticate with a remote target via device login')
4520
+ .action(async (name) => {
4521
+ try {
4522
+ const { getTarget } = await importTarget('target-registry');
4523
+ const { targetLogin } = await importTarget('target-session');
4524
+ const { KeyringCredentialStore } = await importTarget('credential-store');
4525
+ const { acquireTargetLock } = await importTarget('credential-lock');
4526
+ const { discoverTargetSessionEndpoints } = await importTarget('discovery');
4527
+
4528
+ const settingsPort = {
4529
+ load: () => loadSettings(),
4530
+ mutate: (fn) => mutateSettings(fn),
4531
+ };
4532
+
4533
+ const target = getTarget(name, settingsPort);
4534
+ if (!target) {
4535
+ console.error(chalk.red(`Target "${name}" not found.`));
4536
+ process.exit(1);
4537
+ }
4538
+
4539
+ const http = { fetch: (url, init) => fetch(url, init) };
4540
+ const discoveryEndpoints = await discoverTargetSessionEndpoints(target.url, http);
4541
+
4542
+ const credentialStore = await KeyringCredentialStore.create();
4543
+ const openPkg = await import('open');
4544
+ const browserOpen = openPkg.default || openPkg;
4545
+
4546
+ const result = await targetLogin(
4547
+ name,
4548
+ target,
4549
+ credentialStore,
4550
+ () => acquireTargetLock(target.id),
4551
+ settingsPort,
4552
+ {
4553
+ http,
4554
+ clock: { now: () => Date.now() },
4555
+ browserOpener: {
4556
+ open: async (url) => {
4557
+ await browserOpen(url);
4558
+ },
4559
+ },
4560
+ stderr: process.stderr,
4561
+ discoveryEndpoints,
4562
+ }
4563
+ );
4564
+
4565
+ console.log(
4566
+ chalk.green(`✓ Logged in to "${name}" (organization: ${result.organization.name})`)
4567
+ );
4568
+ } catch (error) {
4569
+ console.error(chalk.red(error.message));
4570
+ process.exit(1);
4571
+ }
4572
+ });
4573
+
4574
+ targetCmd
4575
+ .command('list')
4576
+ .description('List registered remote targets')
4577
+ .option('--json', 'Output as JSON')
4578
+ .action(async (options) => {
4579
+ try {
4580
+ const { listTargets } = await importTarget('target-registry');
4581
+ const settingsPort = {
4582
+ load: () => loadSettings(),
4583
+ mutate: (fn) => mutateSettings(fn),
4584
+ };
4585
+
4586
+ const targets = listTargets(settingsPort);
4587
+
4588
+ if (options.json) {
4589
+ const output = targets.map(({ name, record }) => ({
4590
+ name,
4591
+ id: record.id,
4592
+ url: record.url,
4593
+ organization: record.organization ?? null,
4594
+ loggedIn: false,
4595
+ createdAt: record.createdAt,
4596
+ }));
4597
+
4598
+ // Try to check keyring presence for each target
4599
+ try {
4600
+ const { KeyringCredentialStore } = await importTarget('credential-store');
4601
+ const { targetServiceKey, TARGET_ACCOUNT } = await importTarget('credential-store');
4602
+ const store = await KeyringCredentialStore.create();
4603
+ for (const item of output) {
4604
+ const matchingTarget = targets.find((t) => t.name === item.name);
4605
+ if (matchingTarget) {
4606
+ const cred = await store.get(
4607
+ targetServiceKey(matchingTarget.record.id),
4608
+ TARGET_ACCOUNT
4609
+ );
4610
+ item.loggedIn = cred !== null;
4611
+ }
4612
+ }
4613
+ } catch {
4614
+ // Keyring unavailable, all show as not logged in
4615
+ }
4616
+
4617
+ console.log(JSON.stringify(output, null, 2));
4618
+ return;
4619
+ }
4620
+
4621
+ if (targets.length === 0) {
4622
+ console.log(
4623
+ chalk.dim(
4624
+ 'No targets registered. Use `zeroshot target add <name> --url <url>` to add one.'
4625
+ )
4626
+ );
4627
+ return;
4628
+ }
4629
+
4630
+ for (const { name, record } of targets) {
4631
+ const org = record.organization ? ` (org: ${record.organization.name})` : '';
4632
+ console.log(` ${chalk.bold(name)} ${record.url}${org}`);
4633
+ }
4634
+ } catch (error) {
4635
+ console.error(chalk.red(error.message));
4636
+ process.exit(1);
4637
+ }
4638
+ });
4639
+
4640
+ targetCmd
4641
+ .command('remove <name>')
4642
+ .description('Remove a named remote target')
4643
+ .option('--force', 'Remove even if remote revocation fails')
4644
+ .action(async (name, options) => {
4645
+ try {
4646
+ const { getTarget, removeTarget } = await importTarget('target-registry');
4647
+ const { revokeAndCleanup } = await importTarget('target-session');
4648
+ const { acquireTargetLock } = await importTarget('credential-lock');
4649
+ const { discoverTargetSessionEndpoints } = await importTarget('discovery');
4650
+
4651
+ const settingsPort = {
4652
+ load: () => loadSettings(),
4653
+ mutate: (fn) => mutateSettings(fn),
4654
+ };
4655
+
4656
+ const target = getTarget(name, settingsPort);
4657
+ if (!target) {
4658
+ console.error(chalk.red(`Target "${name}" not found.`));
4659
+ process.exit(1);
4660
+ }
4661
+
4662
+ // Try to revoke and cleanup keyring
4663
+ try {
4664
+ const { KeyringCredentialStore } = await importTarget('credential-store');
4665
+ const credentialStore = await KeyringCredentialStore.create();
4666
+ const http = { fetch: (url, init) => fetch(url, init) };
4667
+ const discoveryEndpoints = await discoverTargetSessionEndpoints(target.url, http);
4668
+ await revokeAndCleanup(
4669
+ target,
4670
+ credentialStore,
4671
+ () => acquireTargetLock(target.id),
4672
+ {
4673
+ http,
4674
+ discoveryEndpoints,
4675
+ },
4676
+ !!options.force
4677
+ );
4678
+ } catch (error) {
4679
+ if (!options.force) {
4680
+ console.error(chalk.red(error.message));
4681
+ process.exit(1);
4682
+ }
4683
+ // Force mode: continue with removal
4684
+ }
4685
+
4686
+ removeTarget(name, settingsPort);
4687
+ console.log(chalk.green(`✓ Target "${name}" removed`));
4688
+ } catch (error) {
4689
+ console.error(chalk.red(error.message));
4690
+ process.exit(1);
4691
+ }
4692
+ });
4693
+
4694
+ targetCmd.action(() => {
4695
+ targetCmd.help();
4696
+ });
4697
+
4489
4698
  // Providers management
4490
4699
  const providersCmd = program.command('providers').description('Manage AI providers');
4491
4700
  providersCmd.action(async () => {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClusterClient = void 0;
4
4
  exports.connect = connect;
5
+ exports.connectInitialized = connectInitialized;
5
6
  const protocol_js_1 = require("./generated/protocol.cjs");
6
7
  const connection_js_1 = require("./connection.cjs");
7
8
  const socket_js_1 = require("./socket.cjs");
@@ -79,12 +80,15 @@ class ClusterClient {
79
80
  }
80
81
  }
81
82
  exports.ClusterClient = ClusterClient;
82
- async function defaultWebSocketFactory(url, protocols) {
83
+ async function defaultWebSocketFactory(url, protocols, options) {
83
84
  const globalWebSocket = globalThis.WebSocket;
84
- if (globalWebSocket)
85
+ if (globalWebSocket) {
86
+ if (options?.headers && Object.keys(options.headers).length > 0) {
87
+ throw new errors_js_1.ClusterConfigError('WebSocket upgrade headers require the ws library; the browser WebSocket API cannot carry request headers', 'HEADERS_UNSUPPORTED');
88
+ }
85
89
  return new globalWebSocket(url, protocols);
90
+ }
86
91
  try {
87
- // Dynamic loading keeps the optional Node runtime off the browser/global-WebSocket path.
88
92
  const imported = await Promise.resolve().then(() => require('ws'));
89
93
  const candidate = imported !== null && typeof imported === 'object' && 'default' in imported
90
94
  ? imported.default
@@ -93,7 +97,7 @@ async function defaultWebSocketFactory(url, protocols) {
93
97
  throw new TypeError("The installed 'ws' module does not export a WebSocket constructor");
94
98
  }
95
99
  const Constructor = candidate;
96
- return new Constructor(url, protocols);
100
+ return options?.headers ? new Constructor(url, protocols, { headers: options.headers }) : new Constructor(url, protocols);
97
101
  }
98
102
  catch (cause) {
99
103
  throw new errors_js_1.ClusterConfigError("No WebSocket runtime is available; install 'ws' or pass webSocketFactory", 'WEBSOCKET_UNAVAILABLE', { cause });
@@ -128,7 +132,7 @@ async function connect(url, options = {}) {
128
132
  const factory = options.webSocketFactory ?? defaultWebSocketFactory;
129
133
  let socket;
130
134
  try {
131
- socket = await factory(url, options.protocols);
135
+ socket = await factory(url, options.protocols, options.headers ? { headers: options.headers } : undefined);
132
136
  await waitForOpen(socket, options.signal);
133
137
  const connection = new connection_js_1.Connection(socket);
134
138
  await new ClusterClient(connection).initialize(options.initialize, options.signal === undefined ? {} : { signal: options.signal });
@@ -144,3 +148,24 @@ async function connect(url, options = {}) {
144
148
  throw error;
145
149
  }
146
150
  }
151
+ async function connectInitialized(url, options = {}) {
152
+ const factory = options.webSocketFactory ?? defaultWebSocketFactory;
153
+ let socket;
154
+ try {
155
+ socket = await factory(url, options.protocols, options.headers ? { headers: options.headers } : undefined);
156
+ await waitForOpen(socket, options.signal);
157
+ const connection = new connection_js_1.Connection(socket);
158
+ const client = new ClusterClient(connection);
159
+ const initializeResult = await client.initialize(options.initialize, options.signal === undefined ? {} : { signal: options.signal });
160
+ return { connection, client, initializeResult };
161
+ }
162
+ catch (error) {
163
+ if (socket) {
164
+ try {
165
+ await socket.close();
166
+ }
167
+ catch { /* preserve the construction error */ }
168
+ }
169
+ throw error;
170
+ }
171
+ }
@@ -3,11 +3,15 @@ import { Connection } from './connection.js';
3
3
  import type { CallOptions } from './connection.js';
4
4
  import type { WebSocketLike } from './socket.js';
5
5
  import { AgentAttachSubscriptionStream, LogsSubscriptionStream, WatchSubscriptionStream } from './subscriptions.js';
6
+ export interface WebSocketFactoryOptions {
7
+ readonly headers?: Readonly<Record<string, string>>;
8
+ }
6
9
  export interface ConnectOptions {
7
10
  readonly protocols?: string | readonly string[];
8
- readonly webSocketFactory?: (url: string, protocols?: string | readonly string[]) => WebSocketLike | Promise<WebSocketLike>;
11
+ readonly webSocketFactory?: (url: string, protocols?: string | readonly string[], options?: WebSocketFactoryOptions) => WebSocketLike | Promise<WebSocketLike>;
9
12
  readonly signal?: AbortSignal;
10
13
  readonly initialize?: InitializeParams;
14
+ readonly headers?: Readonly<Record<string, string>>;
11
15
  }
12
16
  export interface WatchSubscription {
13
17
  readonly result: WatchResult;
@@ -24,6 +28,11 @@ export interface AgentAttachSubscription {
24
28
  export interface CoherentWatchSubscription extends WatchSubscription {
25
29
  readonly snapshot: GetResult;
26
30
  }
31
+ export interface ConnectInitializedResult {
32
+ readonly connection: Connection;
33
+ readonly client: ClusterClient;
34
+ readonly initializeResult: InitializeResult;
35
+ }
27
36
  export declare class ClusterClient {
28
37
  readonly connection: Connection;
29
38
  constructor(connection: Connection);
@@ -42,3 +51,4 @@ export declare class ClusterClient {
42
51
  agentAttach(params: AgentAttachParams, options?: CallOptions): Promise<AgentAttachSubscription>;
43
52
  }
44
53
  export declare function connect(url: string, options?: ConnectOptions): Promise<Connection>;
54
+ export declare function connectInitialized(url: string, options?: ConnectOptions): Promise<ConnectInitializedResult>;
@@ -74,12 +74,15 @@ export class ClusterClient {
74
74
  };
75
75
  }
76
76
  }
77
- async function defaultWebSocketFactory(url, protocols) {
77
+ async function defaultWebSocketFactory(url, protocols, options) {
78
78
  const globalWebSocket = globalThis.WebSocket;
79
- if (globalWebSocket)
79
+ if (globalWebSocket) {
80
+ if (options?.headers && Object.keys(options.headers).length > 0) {
81
+ throw new ClusterConfigError('WebSocket upgrade headers require the ws library; the browser WebSocket API cannot carry request headers', 'HEADERS_UNSUPPORTED');
82
+ }
80
83
  return new globalWebSocket(url, protocols);
84
+ }
81
85
  try {
82
- // Dynamic loading keeps the optional Node runtime off the browser/global-WebSocket path.
83
86
  const imported = await import('ws');
84
87
  const candidate = imported !== null && typeof imported === 'object' && 'default' in imported
85
88
  ? imported.default
@@ -88,7 +91,7 @@ async function defaultWebSocketFactory(url, protocols) {
88
91
  throw new TypeError("The installed 'ws' module does not export a WebSocket constructor");
89
92
  }
90
93
  const Constructor = candidate;
91
- return new Constructor(url, protocols);
94
+ return options?.headers ? new Constructor(url, protocols, { headers: options.headers }) : new Constructor(url, protocols);
92
95
  }
93
96
  catch (cause) {
94
97
  throw new ClusterConfigError("No WebSocket runtime is available; install 'ws' or pass webSocketFactory", 'WEBSOCKET_UNAVAILABLE', { cause });
@@ -123,7 +126,7 @@ export async function connect(url, options = {}) {
123
126
  const factory = options.webSocketFactory ?? defaultWebSocketFactory;
124
127
  let socket;
125
128
  try {
126
- socket = await factory(url, options.protocols);
129
+ socket = await factory(url, options.protocols, options.headers ? { headers: options.headers } : undefined);
127
130
  await waitForOpen(socket, options.signal);
128
131
  const connection = new Connection(socket);
129
132
  await new ClusterClient(connection).initialize(options.initialize, options.signal === undefined ? {} : { signal: options.signal });
@@ -139,3 +142,24 @@ export async function connect(url, options = {}) {
139
142
  throw error;
140
143
  }
141
144
  }
145
+ export async function connectInitialized(url, options = {}) {
146
+ const factory = options.webSocketFactory ?? defaultWebSocketFactory;
147
+ let socket;
148
+ try {
149
+ socket = await factory(url, options.protocols, options.headers ? { headers: options.headers } : undefined);
150
+ await waitForOpen(socket, options.signal);
151
+ const connection = new Connection(socket);
152
+ const client = new ClusterClient(connection);
153
+ const initializeResult = await client.initialize(options.initialize, options.signal === undefined ? {} : { signal: options.signal });
154
+ return { connection, client, initializeResult };
155
+ }
156
+ catch (error) {
157
+ if (socket) {
158
+ try {
159
+ await socket.close();
160
+ }
161
+ catch { /* preserve the construction error */ }
162
+ }
163
+ throw error;
164
+ }
165
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Connection = exports.PROTOCOL_DIAGNOSTIC_CAPACITY = exports.CONNECTION_TRANSITIONS = void 0;
3
+ exports.Connection = exports.CLOSE_REASON_MAX_BYTES = exports.PROTOCOL_DIAGNOSTIC_CAPACITY = exports.CONNECTION_TRANSITIONS = void 0;
4
4
  const protocol_js_1 = require("./generated/protocol.cjs");
5
5
  const errors_js_1 = require("./errors.cjs");
6
6
  const frames_js_1 = require("./frames.cjs");
@@ -19,6 +19,21 @@ exports.CONNECTION_TRANSITIONS = Object.freeze({
19
19
  CLOSED: Object.freeze([]),
20
20
  });
21
21
  exports.PROTOCOL_DIAGNOSTIC_CAPACITY = 128;
22
+ exports.CLOSE_REASON_MAX_BYTES = 123;
23
+ const CLOSE_REASON_ENCODER = new TextEncoder();
24
+ function boundedCloseReason(reason) {
25
+ const retained = [];
26
+ const scratch = new Uint8Array(4);
27
+ let bytes = 0;
28
+ for (const codePoint of reason) {
29
+ const { written } = CLOSE_REASON_ENCODER.encodeInto(codePoint, scratch);
30
+ if (bytes + written > exports.CLOSE_REASON_MAX_BYTES)
31
+ break;
32
+ retained.push(codePoint);
33
+ bytes += written;
34
+ }
35
+ return retained.join('');
36
+ }
22
37
  function deferred() {
23
38
  let resolve;
24
39
  let reject;
@@ -37,6 +52,8 @@ class Connection {
37
52
  #removeSocketListeners = [];
38
53
  #ownedSubscriptions = new WeakSet();
39
54
  #closePromise;
55
+ #closeCode;
56
+ #closeReason;
40
57
  closeDiagnostics = [];
41
58
  protocolDiagnostics = [];
42
59
  #socket;
@@ -44,11 +61,13 @@ class Connection {
44
61
  if (socket.readyState !== 1)
45
62
  throw new errors_js_1.ClusterStateError('Connection requires an already-open WebSocket', 'SOCKET_NOT_OPEN');
46
63
  this.#socket = socket;
47
- this.#removeSocketListeners.push((0, socket_js_1.addSocketListener)(socket, 'message', (event) => this.#onMessage(event)), (0, socket_js_1.addSocketListener)(socket, 'error', () => { void this.#startClose(false); }), (0, socket_js_1.addSocketListener)(socket, 'close', () => { void this.#startClose(false); }));
64
+ this.#removeSocketListeners.push((0, socket_js_1.addSocketListener)(socket, 'message', (event) => this.#onMessage(event)), (0, socket_js_1.addSocketListener)(socket, 'error', () => { void this.#startClose(false); }), (0, socket_js_1.addSocketListener)(socket, 'close', (...args) => { this.#captureCloseState(args); void this.#startClose(false); }));
48
65
  }
49
66
  get state() { return this.#state; }
50
67
  get pendingSize() { return this.#pending.size; }
51
68
  get subscriptionCount() { return this.#subscriptions.size; }
69
+ get closeCode() { return this.#closeCode; }
70
+ get closeReason() { return this.#closeReason; }
52
71
  call(method, params, options = {}) {
53
72
  if (!protocol_js_1.UNARY_METHODS.includes(method)) {
54
73
  throw new errors_js_1.ClusterConfigError(`${method} is a subscription method`, 'INVALID_METHOD');
@@ -322,6 +341,23 @@ class Connection {
322
341
  this.protocolDiagnostics.shift();
323
342
  this.protocolDiagnostics.push(new errors_js_1.ClusterProtocolError(message, 'INVALID_PEER_FRAME', cause === undefined ? undefined : { cause }));
324
343
  }
344
+ #captureCloseState(args) {
345
+ if (args.length === 0)
346
+ return;
347
+ const first = args[0];
348
+ if (typeof first === 'number') {
349
+ this.#closeCode = first;
350
+ const raw = args.length > 1 ? String(args[1]) : undefined;
351
+ this.#closeReason = raw === undefined ? undefined : boundedCloseReason(raw);
352
+ }
353
+ else if (first !== null && typeof first === 'object') {
354
+ const event = first;
355
+ if (typeof event.code === 'number')
356
+ this.#closeCode = event.code;
357
+ if (typeof event.reason === 'string')
358
+ this.#closeReason = boundedCloseReason(event.reason);
359
+ }
360
+ }
325
361
  #startClose(sendCancels) {
326
362
  if (this.#closePromise)
327
363
  return this.#closePromise;
@@ -6,6 +6,7 @@ import type { WebSocketLike } from './socket.js';
6
6
  export type ConnectionState = 'OPEN' | 'CLOSING' | 'CLOSED';
7
7
  export declare const CONNECTION_TRANSITIONS: Readonly<Record<ConnectionState, readonly ConnectionState[]>>;
8
8
  export declare const PROTOCOL_DIAGNOSTIC_CAPACITY = 128;
9
+ export declare const CLOSE_REASON_MAX_BYTES = 123;
9
10
  export interface CallOptions {
10
11
  readonly signal?: AbortSignal;
11
12
  readonly requestTimeoutMs?: number;
@@ -32,6 +33,8 @@ export declare class Connection {
32
33
  get state(): ConnectionState;
33
34
  get pendingSize(): number;
34
35
  get subscriptionCount(): number;
36
+ get closeCode(): number | undefined;
37
+ get closeReason(): string | undefined;
35
38
  call<M extends UnaryClusterMethod>(method: M, params: ClusterMethodParams[M], options?: CallOptions): Promise<ClusterMethodResults[M]>;
36
39
  cancelSubscription(registration: SubscriptionRegistration): Promise<void>;
37
40
  openSubscription<M extends SubscriptionMethod>(method: M, params: ClusterMethodParams[M], options?: CallOptions): Promise<EstablishedSubscription<ClusterMethodResults[M]>>;
@@ -16,6 +16,21 @@ export const CONNECTION_TRANSITIONS = Object.freeze({
16
16
  CLOSED: Object.freeze([]),
17
17
  });
18
18
  export const PROTOCOL_DIAGNOSTIC_CAPACITY = 128;
19
+ export const CLOSE_REASON_MAX_BYTES = 123;
20
+ const CLOSE_REASON_ENCODER = new TextEncoder();
21
+ function boundedCloseReason(reason) {
22
+ const retained = [];
23
+ const scratch = new Uint8Array(4);
24
+ let bytes = 0;
25
+ for (const codePoint of reason) {
26
+ const { written } = CLOSE_REASON_ENCODER.encodeInto(codePoint, scratch);
27
+ if (bytes + written > CLOSE_REASON_MAX_BYTES)
28
+ break;
29
+ retained.push(codePoint);
30
+ bytes += written;
31
+ }
32
+ return retained.join('');
33
+ }
19
34
  function deferred() {
20
35
  let resolve;
21
36
  let reject;
@@ -34,6 +49,8 @@ export class Connection {
34
49
  #removeSocketListeners = [];
35
50
  #ownedSubscriptions = new WeakSet();
36
51
  #closePromise;
52
+ #closeCode;
53
+ #closeReason;
37
54
  closeDiagnostics = [];
38
55
  protocolDiagnostics = [];
39
56
  #socket;
@@ -41,11 +58,13 @@ export class Connection {
41
58
  if (socket.readyState !== 1)
42
59
  throw new ClusterStateError('Connection requires an already-open WebSocket', 'SOCKET_NOT_OPEN');
43
60
  this.#socket = socket;
44
- this.#removeSocketListeners.push(addSocketListener(socket, 'message', (event) => this.#onMessage(event)), addSocketListener(socket, 'error', () => { void this.#startClose(false); }), addSocketListener(socket, 'close', () => { void this.#startClose(false); }));
61
+ this.#removeSocketListeners.push(addSocketListener(socket, 'message', (event) => this.#onMessage(event)), addSocketListener(socket, 'error', () => { void this.#startClose(false); }), addSocketListener(socket, 'close', (...args) => { this.#captureCloseState(args); void this.#startClose(false); }));
45
62
  }
46
63
  get state() { return this.#state; }
47
64
  get pendingSize() { return this.#pending.size; }
48
65
  get subscriptionCount() { return this.#subscriptions.size; }
66
+ get closeCode() { return this.#closeCode; }
67
+ get closeReason() { return this.#closeReason; }
49
68
  call(method, params, options = {}) {
50
69
  if (!UNARY_METHODS.includes(method)) {
51
70
  throw new ClusterConfigError(`${method} is a subscription method`, 'INVALID_METHOD');
@@ -319,6 +338,23 @@ export class Connection {
319
338
  this.protocolDiagnostics.shift();
320
339
  this.protocolDiagnostics.push(new ClusterProtocolError(message, 'INVALID_PEER_FRAME', cause === undefined ? undefined : { cause }));
321
340
  }
341
+ #captureCloseState(args) {
342
+ if (args.length === 0)
343
+ return;
344
+ const first = args[0];
345
+ if (typeof first === 'number') {
346
+ this.#closeCode = first;
347
+ const raw = args.length > 1 ? String(args[1]) : undefined;
348
+ this.#closeReason = raw === undefined ? undefined : boundedCloseReason(raw);
349
+ }
350
+ else if (first !== null && typeof first === 'object') {
351
+ const event = first;
352
+ if (typeof event.code === 'number')
353
+ this.#closeCode = event.code;
354
+ if (typeof event.reason === 'string')
355
+ this.#closeReason = boundedCloseReason(event.reason);
356
+ }
357
+ }
322
358
  #startClose(sendCancels) {
323
359
  if (this.#closePromise)
324
360
  return this.#closePromise;
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.connect = exports.ClusterClient = exports.WatchSubscriptionStream = exports.LogsSubscriptionStream = exports.AgentAttachSubscriptionStream = exports.Connection = exports.PROTOCOL_DIAGNOSTIC_CAPACITY = exports.CONNECTION_TRANSITIONS = exports.assertGraphSpec = exports.assertGraphProfileSupported = exports.assertGraphProfile = exports.ClusterTransportError = exports.ClusterTimeoutError = exports.ClusterStateError = exports.ClusterRpcError = exports.ClusterRequestError = exports.ClusterProtocolError = exports.ClusterInternalError = exports.ClusterError = exports.ClusterConfigError = exports.SUBSCRIPTION_QUEUE_MAX_BYTES = void 0;
17
+ exports.connectInitialized = exports.connect = exports.ClusterClient = exports.WatchSubscriptionStream = exports.LogsSubscriptionStream = exports.AgentAttachSubscriptionStream = exports.Connection = exports.PROTOCOL_DIAGNOSTIC_CAPACITY = exports.CONNECTION_TRANSITIONS = exports.CLOSE_REASON_MAX_BYTES = exports.assertGraphSpec = exports.assertGraphProfileSupported = exports.assertGraphProfile = exports.ClusterTransportError = exports.ClusterTimeoutError = exports.ClusterStateError = exports.ClusterRpcError = exports.ClusterRequestError = exports.ClusterProtocolError = exports.ClusterInternalError = exports.ClusterError = exports.ClusterConfigError = exports.SUBSCRIPTION_QUEUE_MAX_BYTES = void 0;
18
18
  __exportStar(require("./generated/protocol.cjs"), exports);
19
19
  var queue_js_1 = require("./queue.cjs");
20
20
  Object.defineProperty(exports, "SUBSCRIPTION_QUEUE_MAX_BYTES", { enumerable: true, get: function () { return queue_js_1.SUBSCRIPTION_QUEUE_MAX_BYTES; } });
@@ -35,6 +35,7 @@ Object.defineProperty(exports, "assertGraphSpec", { enumerable: true, get: funct
35
35
  __exportStar(require("./payload-value.cjs"), exports);
36
36
  __exportStar(require("./json-source.cjs"), exports);
37
37
  var connection_js_1 = require("./connection.cjs");
38
+ Object.defineProperty(exports, "CLOSE_REASON_MAX_BYTES", { enumerable: true, get: function () { return connection_js_1.CLOSE_REASON_MAX_BYTES; } });
38
39
  Object.defineProperty(exports, "CONNECTION_TRANSITIONS", { enumerable: true, get: function () { return connection_js_1.CONNECTION_TRANSITIONS; } });
39
40
  Object.defineProperty(exports, "PROTOCOL_DIAGNOSTIC_CAPACITY", { enumerable: true, get: function () { return connection_js_1.PROTOCOL_DIAGNOSTIC_CAPACITY; } });
40
41
  Object.defineProperty(exports, "Connection", { enumerable: true, get: function () { return connection_js_1.Connection; } });
@@ -45,3 +46,4 @@ Object.defineProperty(exports, "WatchSubscriptionStream", { enumerable: true, ge
45
46
  var client_js_1 = require("./client.cjs");
46
47
  Object.defineProperty(exports, "ClusterClient", { enumerable: true, get: function () { return client_js_1.ClusterClient; } });
47
48
  Object.defineProperty(exports, "connect", { enumerable: true, get: function () { return client_js_1.connect; } });
49
+ Object.defineProperty(exports, "connectInitialized", { enumerable: true, get: function () { return client_js_1.connectInitialized; } });
@@ -4,10 +4,10 @@ export { ClusterConfigError, ClusterError, ClusterInternalError, ClusterProtocol
4
4
  export { assertGraphProfile, assertGraphProfileSupported, assertGraphSpec } from './validators.js';
5
5
  export * from './payload-value.js';
6
6
  export * from './json-source.js';
7
- export { CONNECTION_TRANSITIONS, PROTOCOL_DIAGNOSTIC_CAPACITY, Connection } from './connection.js';
7
+ export { CLOSE_REASON_MAX_BYTES, CONNECTION_TRANSITIONS, PROTOCOL_DIAGNOSTIC_CAPACITY, Connection } from './connection.js';
8
8
  export type { CallOptions, ConnectionState, } from './connection.js';
9
9
  export type { WebSocketLike } from './socket.js';
10
10
  export { AgentAttachSubscriptionStream, LogsSubscriptionStream, WatchSubscriptionStream, } from './subscriptions.js';
11
11
  export type { Subscription, SubscriptionClosedItem, SubscriptionItem, WatchSubscriptionItem, WatchSubscriptionClosedItem, } from './subscriptions.js';
12
- export { ClusterClient, connect } from './client.js';
13
- export type { AgentAttachSubscription, CoherentWatchSubscription, ConnectOptions, LogsSubscription, WatchSubscription, } from './client.js';
12
+ export { ClusterClient, connect, connectInitialized } from './client.js';
13
+ export type { AgentAttachSubscription, CoherentWatchSubscription, ConnectInitializedResult, ConnectOptions, LogsSubscription, WatchSubscription, WebSocketFactoryOptions, } from './client.js';
@@ -4,6 +4,6 @@ export { ClusterConfigError, ClusterError, ClusterInternalError, ClusterProtocol
4
4
  export { assertGraphProfile, assertGraphProfileSupported, assertGraphSpec } from './validators.mjs';
5
5
  export * from './payload-value.mjs';
6
6
  export * from './json-source.mjs';
7
- export { CONNECTION_TRANSITIONS, PROTOCOL_DIAGNOSTIC_CAPACITY, Connection } from './connection.mjs';
7
+ export { CLOSE_REASON_MAX_BYTES, CONNECTION_TRANSITIONS, PROTOCOL_DIAGNOSTIC_CAPACITY, Connection } from './connection.mjs';
8
8
  export { AgentAttachSubscriptionStream, LogsSubscriptionStream, WatchSubscriptionStream, } from './subscriptions.mjs';
9
- export { ClusterClient, connect } from './client.mjs';
9
+ export { ClusterClient, connect, connectInitialized } from './client.mjs';