@itookit/dsht 0.5.1 → 0.5.2

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 (66) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -2
  3. package/README.zh.md +6 -4
  4. package/dist/catalog/controller.d.ts +26 -6
  5. package/dist/catalog/controller.js +73 -45
  6. package/dist/catalog/index.d.ts +1 -0
  7. package/dist/cli/dsht.js +4 -1
  8. package/dist/cli/startup.js +30 -11
  9. package/dist/cli/verifier.d.ts +4 -0
  10. package/dist/cli/verifier.js +28 -5
  11. package/dist/contracts.d.ts +20 -5
  12. package/dist/controller/connection-streams.d.ts +22 -0
  13. package/dist/controller/connection-streams.js +105 -0
  14. package/dist/controller/connection.d.ts +14 -3
  15. package/dist/controller/connection.js +40 -69
  16. package/dist/controller/controller.d.ts +17 -233
  17. package/dist/controller/controller.js +108 -811
  18. package/dist/controller/foreground.d.ts +44 -0
  19. package/dist/controller/foreground.js +79 -0
  20. package/dist/controller/loop-coordinator.d.ts +48 -0
  21. package/dist/controller/loop-coordinator.js +647 -0
  22. package/dist/controller/verifier.d.ts +4 -0
  23. package/dist/cost/controller.d.ts +1 -1
  24. package/dist/cost/controller.js +12 -5
  25. package/dist/cost/scanner.js +1 -0
  26. package/dist/session/controller.d.ts +23 -35
  27. package/dist/session/controller.js +113 -363
  28. package/dist/session/history-reader.d.ts +32 -0
  29. package/dist/session/history-reader.js +170 -0
  30. package/dist/session/index.d.ts +1 -1
  31. package/dist/session/info.d.ts +3 -38
  32. package/dist/session/info.js +14 -1
  33. package/dist/session/interactions.d.ts +26 -0
  34. package/dist/session/interactions.js +75 -0
  35. package/dist/session/navigator.d.ts +47 -0
  36. package/dist/session/navigator.js +158 -0
  37. package/dist/session/prompt-backfill.d.ts +23 -0
  38. package/dist/session/prompt-backfill.js +88 -0
  39. package/dist/session/state.d.ts +20 -0
  40. package/dist/session/state.js +1 -0
  41. package/dist/session/telemetry.d.ts +15 -6
  42. package/dist/session/telemetry.js +44 -7
  43. package/dist/session/transcript.d.ts +5 -1
  44. package/dist/slash/index.d.ts +1 -1
  45. package/dist/slash/parse.d.ts +2 -126
  46. package/dist/slash/registry.d.ts +1 -1
  47. package/dist/slash/types.d.ts +126 -0
  48. package/dist/slash/types.js +1 -0
  49. package/dist/state.d.ts +5 -17
  50. package/dist/state.js +1 -1
  51. package/dist/transport/client.d.ts +4 -3
  52. package/dist/transport/client.js +71 -25
  53. package/dist/ui/app.js +86 -301
  54. package/dist/ui/chat/shell-view.d.ts +2 -0
  55. package/dist/ui/chat/shell-view.js +8 -0
  56. package/dist/ui/chat/use-history-view.d.ts +69 -0
  57. package/dist/ui/chat/use-history-view.js +123 -0
  58. package/dist/ui/dialogs/use-panels.d.ts +53 -0
  59. package/dist/ui/dialogs/use-panels.js +51 -0
  60. package/dist/ui/input/use-composer.d.ts +35 -0
  61. package/dist/ui/input/use-composer.js +109 -0
  62. package/dist/ui/input/use-deferred-lines.d.ts +16 -0
  63. package/dist/ui/input/use-deferred-lines.js +54 -0
  64. package/dist/ui/input/use-history-recall.d.ts +20 -0
  65. package/dist/ui/input/use-history-recall.js +47 -0
  66. package/package.json +1 -1
@@ -22,7 +22,7 @@ export class RemoteError extends Error {
22
22
  this.details = error.details;
23
23
  }
24
24
  }
25
- /** A single authenticated host connection; close before reconnecting or exiting. */
25
+ /** One client lifetime. Peer disconnects allow reconnect; close permanently ends the lifetime. */
26
26
  export class Client {
27
27
  timeoutMs;
28
28
  base;
@@ -31,6 +31,7 @@ export class Client {
31
31
  socket;
32
32
  listeners = new Map();
33
33
  lifetime = new AbortController();
34
+ closeTask;
34
35
  constructor(base, timeoutMs = 15_000) {
35
36
  this.timeoutMs = timeoutMs;
36
37
  this.base = new URL(base);
@@ -117,13 +118,17 @@ export class Client {
117
118
  }
118
119
  /** Connect the physical mux. A disconnected instance may reconnect with its cookie. */
119
120
  async connect() {
121
+ this.lifetime.signal.throwIfAborted();
120
122
  if (this.socket)
121
123
  throw new Error('Mux is already connected');
122
124
  const url = new URL('/api/remote.mux', this.base);
123
125
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
124
126
  const socket = new WebSocket(url, { headers: { cookie: this.cookie }, handshakeTimeout: this.timeoutMs });
125
127
  this.socket = socket;
128
+ const closed = new Promise(resolve => socket.once('close', () => resolve()));
126
129
  socket.on('message', (raw, binary) => {
130
+ if (this.socket !== socket || this.lifetime.signal.aborted)
131
+ return;
127
132
  try {
128
133
  if (binary)
129
134
  throw new Error('Unexpected binary mux frame');
@@ -154,22 +159,40 @@ export class Client {
154
159
  socket.terminate();
155
160
  }
156
161
  });
157
- socket.on('error', error => this.fail(error));
162
+ socket.on('error', error => { if (this.socket === socket)
163
+ this.fail(error); });
158
164
  socket.on('close', () => {
159
- if (this.socket === socket)
160
- this.socket = undefined;
165
+ if (this.socket !== socket)
166
+ return;
167
+ this.socket = undefined;
161
168
  this.fail(new Error('Connection closed'));
162
169
  });
163
- await once(socket, 'open');
170
+ try {
171
+ await once(socket, 'open', { signal: this.lifetime.signal });
172
+ this.lifetime.signal.throwIfAborted();
173
+ }
174
+ catch (error) {
175
+ if (socket.readyState !== WebSocket.CLOSED)
176
+ socket.terminate();
177
+ await closed;
178
+ throw error;
179
+ }
164
180
  }
165
181
  /** Subscribe on the existing mux; each subscription has a fresh stream identity. */
166
182
  subscribe(endpoint, args, listener) {
183
+ this.lifetime.signal.throwIfAborted();
167
184
  const socket = this.socket;
168
185
  if (socket?.readyState !== WebSocket.OPEN)
169
186
  throw new Error('Mux is not connected');
170
187
  const streamId = randomUUID();
171
188
  this.listeners.set(streamId, listener);
172
- socket.send(JSON.stringify({ type: 'open', streamId, endpoint, payload: { args } }));
189
+ try {
190
+ socket.send(JSON.stringify({ type: 'open', streamId, endpoint, payload: { args } }));
191
+ }
192
+ catch (error) {
193
+ this.listeners.delete(streamId);
194
+ throw error;
195
+ }
173
196
  return { cancel: () => {
174
197
  if (!this.listeners.delete(streamId))
175
198
  return;
@@ -180,57 +203,80 @@ export class Client {
180
203
  /** Archived session IDs from the latest authoritative workspace baseline. */
181
204
  archivedSessionIds = new Set();
182
205
  /** List workspaces by consuming and cancelling the authoritative opening baseline. */
183
- async listWorkspaces() {
206
+ async listWorkspaces(signal) {
207
+ signal?.throwIfAborted();
184
208
  return new Promise((resolve, reject) => {
185
209
  let sub;
186
- const timer = setTimeout(() => { sub.cancel(); reject(new Error('Workspace baseline timed out')); }, this.timeoutMs);
210
+ let settled = false;
211
+ const finish = (error, items) => {
212
+ if (settled)
213
+ return;
214
+ settled = true;
215
+ clearTimeout(timer);
216
+ signal?.removeEventListener('abort', cancel);
217
+ sub?.cancel();
218
+ if (error !== undefined)
219
+ reject(error);
220
+ else
221
+ resolve(items);
222
+ };
223
+ const cancel = () => finish(signal.reason);
224
+ const timer = setTimeout(() => finish(new Error('Workspace baseline timed out')), this.timeoutMs);
225
+ signal?.addEventListener('abort', cancel, { once: true });
187
226
  try {
188
227
  sub = this.subscribe('workspace/follow', {}, {
189
228
  item: value => {
190
- clearTimeout(timer);
191
- sub.cancel();
229
+ if (settled)
230
+ return;
192
231
  try {
193
232
  const frame = object(value);
194
233
  if (frame.type !== 'baseline')
195
234
  throw new Error('Workspace stream omitted its baseline');
196
235
  this.archivedSessionIds = new Set(array(object(frame.value).archivedSessionIds).map(string));
197
- resolve(array(object(frame.value).items).map(object));
236
+ finish(undefined, array(object(frame.value).items).map(object));
198
237
  }
199
238
  catch (error) {
200
- reject(error);
239
+ finish(error);
201
240
  }
202
241
  },
203
- end: error => { clearTimeout(timer); reject(error ?? new Error('Workspace stream ended before baseline')); },
242
+ end: error => finish(error ?? new Error('Workspace stream ended before baseline')),
204
243
  });
244
+ if (settled)
245
+ sub.cancel();
205
246
  }
206
247
  catch (error) {
207
- clearTimeout(timer);
208
- reject(error);
248
+ finish(error);
209
249
  }
210
250
  });
211
251
  }
212
252
  /** List visible sessions, optionally filtering by the workspace's accounted IDs. */
213
- async listSessions(workspaceId) {
214
- const sessions = array(object(await this.call('session/list', { _request: {} })).items).map(object);
253
+ async listSessions(workspaceId, signal) {
254
+ const sessions = array(object(await this.call('session/list', { _request: {} }, signal)).items).map(object);
215
255
  if (!workspaceId)
216
256
  return sessions;
217
- const workspace = (await this.listWorkspaces()).find(item => item.workspaceId === workspaceId);
257
+ const workspace = (await this.listWorkspaces(signal)).find(item => item.workspaceId === workspaceId);
218
258
  if (!workspace)
219
259
  throw new Error(`Workspace not found: ${workspaceId}`);
220
260
  const ids = new Set(array(workspace.sessionIds).map(string));
221
261
  return sessions.filter(item => ids.has(string(item.sessionId)));
222
262
  }
223
263
  /** Close all streams, abort in-flight HTTP, and await the physical socket's closure. */
224
- async close() {
225
- this.lifetime.abort();
264
+ close() {
265
+ if (this.closeTask)
266
+ return this.closeTask;
226
267
  const socket = this.socket;
227
268
  this.socket = undefined;
269
+ // Reserve before publishing cancellation: termination callbacks may themselves call close.
270
+ this.closeTask = Promise.resolve().then(async () => {
271
+ if (socket && socket.readyState !== WebSocket.CLOSED) {
272
+ const closed = new Promise(resolve => socket.once('close', () => resolve()));
273
+ socket.terminate();
274
+ await closed;
275
+ }
276
+ });
277
+ this.lifetime.abort();
228
278
  this.fail(new Error('Client closed'));
229
- if (socket && socket.readyState !== WebSocket.CLOSED) {
230
- const closed = new Promise(resolve => socket.once('close', () => resolve()));
231
- socket.terminate();
232
- await closed;
233
- }
279
+ return this.closeTask;
234
280
  }
235
281
  signal(timeoutMs = this.timeoutMs) {
236
282
  return timeoutMs === null ? this.lifetime.signal : AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(timeoutMs)]);