@itookit/dsht 0.5.1 → 0.6.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 (93) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +10 -4
  3. package/README.zh.md +12 -6
  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 +22 -2
  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 +42 -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 +20 -234
  17. package/dist/controller/controller.js +113 -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/loop-prompts-schema.d.ts +16 -2
  23. package/dist/controller/loop-prompts-schema.js +106 -27
  24. package/dist/controller/loop-prompts.d.ts +17 -2
  25. package/dist/controller/loop-prompts.generated.js +2 -1
  26. package/dist/controller/loop-prompts.js +35 -9
  27. package/dist/controller/loop-protocols.d.ts +3 -1
  28. package/dist/controller/loop-protocols.js +8 -3
  29. package/dist/controller/loop-source.d.ts +74 -0
  30. package/dist/controller/loop-source.js +224 -0
  31. package/dist/controller/verifier.d.ts +4 -0
  32. package/dist/cost/controller.d.ts +3 -1
  33. package/dist/cost/controller.js +26 -7
  34. package/dist/cost/index.d.ts +1 -1
  35. package/dist/cost/index.js +1 -1
  36. package/dist/cost/ledger-files.d.ts +20 -0
  37. package/dist/cost/ledger-files.js +115 -15
  38. package/dist/cost/ledger.d.ts +31 -6
  39. package/dist/cost/ledger.js +74 -22
  40. package/dist/cost/pricing.d.ts +39 -0
  41. package/dist/cost/pricing.js +46 -0
  42. package/dist/cost/scanner.js +1 -0
  43. package/dist/cost/types.d.ts +9 -3
  44. package/dist/session/controller.d.ts +23 -35
  45. package/dist/session/controller.js +113 -363
  46. package/dist/session/history-reader.d.ts +32 -0
  47. package/dist/session/history-reader.js +170 -0
  48. package/dist/session/index.d.ts +1 -1
  49. package/dist/session/info.d.ts +3 -38
  50. package/dist/session/info.js +14 -1
  51. package/dist/session/interactions.d.ts +26 -0
  52. package/dist/session/interactions.js +75 -0
  53. package/dist/session/navigator.d.ts +47 -0
  54. package/dist/session/navigator.js +158 -0
  55. package/dist/session/prompt-backfill.d.ts +23 -0
  56. package/dist/session/prompt-backfill.js +88 -0
  57. package/dist/session/state.d.ts +20 -0
  58. package/dist/session/state.js +1 -0
  59. package/dist/session/telemetry.d.ts +15 -6
  60. package/dist/session/telemetry.js +44 -7
  61. package/dist/session/transcript.d.ts +5 -1
  62. package/dist/slash/index.d.ts +1 -1
  63. package/dist/slash/parse.d.ts +2 -126
  64. package/dist/slash/registry.d.ts +1 -1
  65. package/dist/slash/types.d.ts +126 -0
  66. package/dist/slash/types.js +1 -0
  67. package/dist/state.d.ts +5 -17
  68. package/dist/state.js +1 -1
  69. package/dist/storage/files.d.ts +8 -0
  70. package/dist/storage/files.js +18 -1
  71. package/dist/storage/index.d.ts +1 -1
  72. package/dist/storage/index.js +1 -1
  73. package/dist/transport/client.d.ts +4 -3
  74. package/dist/transport/client.js +71 -25
  75. package/dist/ui/app.js +88 -301
  76. package/dist/ui/chat/shell-view.d.ts +2 -0
  77. package/dist/ui/chat/shell-view.js +8 -0
  78. package/dist/ui/chat/use-history-view.d.ts +69 -0
  79. package/dist/ui/chat/use-history-view.js +123 -0
  80. package/dist/ui/dialogs/cost.d.ts +6 -0
  81. package/dist/ui/dialogs/cost.js +5 -1
  82. package/dist/ui/dialogs/loop.d.ts +5 -4
  83. package/dist/ui/dialogs/loop.js +14 -6
  84. package/dist/ui/dialogs/use-panels.d.ts +53 -0
  85. package/dist/ui/dialogs/use-panels.js +51 -0
  86. package/dist/ui/input/use-composer.d.ts +35 -0
  87. package/dist/ui/input/use-composer.js +109 -0
  88. package/dist/ui/input/use-deferred-lines.d.ts +16 -0
  89. package/dist/ui/input/use-deferred-lines.js +54 -0
  90. package/dist/ui/input/use-history-recall.d.ts +20 -0
  91. package/dist/ui/input/use-history-recall.js +47 -0
  92. package/loop.yaml +230 -0
  93. package/package.json +5 -4
@@ -51,10 +51,14 @@ export declare class ProcessVerifier implements VerifierPort {
51
51
  private readonly options;
52
52
  /** This client forks itself, so the harness that judges is this one. */
53
53
  readonly name = "dsht";
54
+ private readonly tasks;
54
55
  constructor(options: ProcessVerifierOptions);
55
56
  /** @param request - Round identity, prompt and verdict path.
56
57
  * @param signal - Cancels the child; the run also stops at its own deadline.
57
58
  * @returns The verdict read back from the file, or a note explaining its absence.
58
59
  */
59
60
  verify(request: VerifierRequest, signal: AbortSignal): Promise<VerifierOutcome>;
61
+ /** Keep host cancellation available until every child has completed its bounded cleanup. */
62
+ settle(): Promise<void>;
63
+ private runVerification;
60
64
  }
@@ -66,6 +66,7 @@ export class ProcessVerifier {
66
66
  options;
67
67
  /** This client forks itself, so the harness that judges is this one. */
68
68
  name = 'dsht';
69
+ tasks = new Set();
69
70
  constructor(options) {
70
71
  this.options = options;
71
72
  }
@@ -74,6 +75,18 @@ export class ProcessVerifier {
74
75
  * @returns The verdict read back from the file, or a note explaining its absence.
75
76
  */
76
77
  async verify(request, signal) {
78
+ const task = this.runVerification(request, signal);
79
+ this.tasks.add(task);
80
+ try {
81
+ return await task;
82
+ }
83
+ finally {
84
+ this.tasks.delete(task);
85
+ }
86
+ }
87
+ /** Keep host cancellation available until every child has completed its bounded cleanup. */
88
+ async settle() { await Promise.allSettled([...this.tasks]); }
89
+ async runVerification(request, signal) {
77
90
  const run = this.options.run ?? runProcess;
78
91
  const file = request.file;
79
92
  // Cancelled before anything happened: no session, no child, no verdict file.
@@ -125,10 +138,18 @@ export class ProcessVerifier {
125
138
  ? { note: 'host cannot cancel', stopped: false }
126
139
  : { note: 'no verifier session to cancel', stopped: true };
127
140
  }
128
- const settled = await Promise.race([
129
- confirmation.then(() => true),
130
- new Promise(resolve => { const wait = setTimeout(() => resolve(false), CANCEL_CONFIRM_MS); wait.unref(); }),
131
- ]);
141
+ let timer;
142
+ let settled;
143
+ try {
144
+ settled = await Promise.race([
145
+ confirmation.then(() => true),
146
+ // This bounded wait is part of verify()/settle(), so it must keep the process alive.
147
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), CANCEL_CONFIRM_MS); }),
148
+ ]);
149
+ }
150
+ finally {
151
+ clearTimeout(timer);
152
+ }
132
153
  if (!settled)
133
154
  return { note: `remote cancel unconfirmed after ${CANCEL_CONFIRM_MS} ms`, stopped: false };
134
155
  return confirmed === true
@@ -197,8 +218,10 @@ export class ProcessVerifier {
197
218
  cwd: this.options.cwd, env: this.options.env, signal: controller.signal, onLine,
198
219
  });
199
220
  // A cancelled review decides nothing, and is not a verifier outage either.
200
- if (cancelled)
221
+ if (cancelled) {
222
+ await cancelNote();
201
223
  return { type: 'cancelled' };
224
+ }
202
225
  // The child stopped because the host wants a human: cancel the turn it left waiting (bounded,
203
226
  // reported), and hand the request to the caller instead of retrying into the same block.
204
227
  if (human !== undefined) {
@@ -12,11 +12,26 @@ export type { LivePhase, Message } from './session/transcript.ts';
12
12
  export type { FileReference } from './references.ts';
13
13
  export type { HistorySearch, RemovalTarget } from './session/types.ts';
14
14
  export type { QueuedInput } from './session/telemetry.ts';
15
- export type { ModelState, PanelState } from './session/info.ts';
16
15
  export type { CostTotal, Coverage } from './cost/index.ts';
17
16
  export type { ShellBlock } from './shell/index.ts';
18
- import type { ModelState, PanelState } from './session/info.ts';
19
- import type { RemovalTarget } from './session/types.ts';
17
+ import type { HistorySearch, RemovalTarget } from './session/types.ts';
18
+ import type { ObjectValue } from './json.ts';
19
+ /** Data delivered to dialogs; visibility and keyboard ownership belong to the UI. */
20
+ export interface ModelState {
21
+ catalog: ObjectValue;
22
+ provider?: string;
23
+ model?: ObjectValue;
24
+ }
25
+ export interface HistoryPanel {
26
+ query: string;
27
+ contentSearch: boolean;
28
+ matches?: HistorySearch;
29
+ }
30
+ export interface SearchPanel {
31
+ query: string;
32
+ items: ObjectValue[];
33
+ hasMore: boolean;
34
+ }
20
35
  /** One user-saved shortcut prompt; the client owns the list, no session or host does. */
21
36
  export interface SavedPrompt {
22
37
  id: string;
@@ -24,6 +39,26 @@ export interface SavedPrompt {
24
39
  }
25
40
  /** One panel-like surface the reader can see; the application names it, the UI renders it. */
26
41
  export type PanelName = 'help' | 'cost' | 'status' | 'queue' | 'prompts' | 'thoughts' | 'history' | 'search' | 'model' | 'removal' | 'loop';
42
+ /** Where the loop records one client runs came from.
43
+ *
44
+ * The shipped file travels with the package and a user file may layer over it, so a record list is no
45
+ * longer one file's content: this says which files were read and which records the user's own file
46
+ * replaced or added. The warnings are the client's own notes — a shipped file it could not read, or a
47
+ * record the user overrides that the package has since changed — and the UI shows them rather than
48
+ * acting on them.
49
+ */
50
+ export interface LoopSourceInfo {
51
+ /** Shipped `loop.yaml` that was read; absent when the compiled-in records were used instead. */
52
+ builtin?: string;
53
+ /** User file layered over the shipped records; absent when there is none. */
54
+ file?: string;
55
+ /** Shipped records the user's file replaced, in that file's order. */
56
+ overridden: readonly string[];
57
+ /** Records the user's file added. */
58
+ added: readonly string[];
59
+ /** Notes to show the operator, in the order they were discovered. */
60
+ warnings: readonly string[];
61
+ }
27
62
  /** One `loop.yaml` record as the record list offers it.
28
63
  *
29
64
  * The name is what `/loop` runs; the rest is what a chooser shows about it, plus the defaults a run
@@ -49,6 +84,8 @@ export interface LoopRecord {
49
84
  * document under review, a target, a threshold) is retargeted without editing `loop.yaml`.
50
85
  */
51
86
  vars: Readonly<Record<string, string>>;
87
+ /** Set when this record came from the user's own file rather than from the shipped one. */
88
+ fromFile?: true;
52
89
  }
53
90
  /** The four numbers one loop run uses.
54
91
  *
@@ -263,10 +300,10 @@ export type ViewEffect =
263
300
  /** Payload that opens its own panel. */
264
301
  | {
265
302
  kind: 'history';
266
- history: PanelState['history'];
303
+ history: HistoryPanel | undefined;
267
304
  } | {
268
305
  kind: 'search';
269
- search: PanelState['search'];
306
+ search: SearchPanel | undefined;
270
307
  } | {
271
308
  kind: 'model';
272
309
  model: ModelState;
@@ -0,0 +1,22 @@
1
+ /** Event/control subscriptions and their startup deadlines belong to one connection generation. */
2
+ import { Client } from '../transport/client.ts';
3
+ import { type HostEvent } from '../transport/events.ts';
4
+ export interface StreamHost {
5
+ identified(clientId: string): void;
6
+ event(event: HostEvent): boolean;
7
+ changed(): void;
8
+ degraded(message: string): void;
9
+ fail(error: Error): void;
10
+ }
11
+ export declare class ConnectionStreams {
12
+ private readonly client;
13
+ private readonly host;
14
+ private readonly subscriptions;
15
+ private readonly abort;
16
+ private readonly signal;
17
+ constructor(client: Client, host: StreamHost, signal: AbortSignal);
18
+ start(): Promise<void>;
19
+ close(): void;
20
+ /** Resolve the first usable frame while retaining the stream until this generation ends. */
21
+ private follow;
22
+ }
@@ -0,0 +1,105 @@
1
+ /** Event/control subscriptions and their startup deadlines belong to one connection generation. */
2
+ import { RemoteError } from "../transport/client.js";
3
+ import { controlFrame, hostEvent } from "../transport/events.js";
4
+ import { errorText, object, string } from "../transport/wire.js";
5
+ export class ConnectionStreams {
6
+ client;
7
+ host;
8
+ subscriptions = new Set();
9
+ abort = new AbortController();
10
+ signal;
11
+ constructor(client, host, signal) {
12
+ this.client = client;
13
+ this.host = host;
14
+ this.signal = AbortSignal.any([signal, this.abort.signal]);
15
+ }
16
+ async start() {
17
+ let clientId = '';
18
+ await this.follow('$events', 'Host ready timed out', value => {
19
+ const frame = object(value);
20
+ if (frame.type === 'ready') {
21
+ clientId = string(frame.clientId);
22
+ this.host.identified(clientId);
23
+ return true;
24
+ }
25
+ const event = hostEvent(frame);
26
+ if (event && !this.host.event(event) && 'eventId' in event) {
27
+ // Unknown waterfalls still need a result so the host's event chain can continue.
28
+ void this.client.call('$events/result', { clientId, eventId: event.eventId, outcome: { kind: 'next' } })
29
+ .catch(error => this.host.fail(new Error(errorText(error))));
30
+ }
31
+ return false;
32
+ });
33
+ await this.follow('session/control', 'Session control baseline timed out', value => {
34
+ try {
35
+ this.host.event({ kind: 'control', frame: controlFrame(value) });
36
+ this.host.changed();
37
+ }
38
+ catch (error) {
39
+ // Missing live metrics must not blind a running conversation.
40
+ this.host.degraded(`Live metrics degraded: ${errorText(error)}`);
41
+ }
42
+ return true;
43
+ }, error => {
44
+ if (!(error instanceof RemoteError) || error.code !== 'gateway/method-unavailable')
45
+ return false;
46
+ this.host.degraded('Live metrics unavailable on this host');
47
+ return true;
48
+ });
49
+ }
50
+ close() {
51
+ this.abort.abort();
52
+ for (const subscription of this.subscriptions)
53
+ subscription.cancel();
54
+ this.subscriptions.clear();
55
+ }
56
+ /** Resolve the first usable frame while retaining the stream until this generation ends. */
57
+ follow(endpoint, timeout, item, unavailable) {
58
+ this.signal.throwIfAborted();
59
+ return new Promise((resolve, reject) => {
60
+ let settled = false;
61
+ const finish = (error) => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ clearTimeout(timer);
66
+ this.signal.removeEventListener('abort', cancelled);
67
+ if (error === undefined)
68
+ resolve();
69
+ else
70
+ reject(error);
71
+ };
72
+ const cancelled = () => finish(this.signal.reason);
73
+ const timer = setTimeout(() => finish(new Error(timeout)), this.client.timeoutMs);
74
+ this.signal.addEventListener('abort', cancelled, { once: true });
75
+ try {
76
+ const subscription = this.client.subscribe(endpoint, {}, {
77
+ item: value => {
78
+ if (this.signal.aborted)
79
+ return;
80
+ if (item(value))
81
+ finish();
82
+ },
83
+ end: error => {
84
+ if (this.signal.aborted)
85
+ return;
86
+ if (unavailable?.(error)) {
87
+ finish();
88
+ return;
89
+ }
90
+ const reason = error ?? new Error(`${endpoint} stream ended`);
91
+ finish(reason);
92
+ this.host.fail(reason);
93
+ },
94
+ });
95
+ if (this.signal.aborted)
96
+ subscription.cancel();
97
+ else
98
+ this.subscriptions.add(subscription);
99
+ }
100
+ catch (error) {
101
+ finish(error);
102
+ }
103
+ });
104
+ }
105
+ }
@@ -1,9 +1,20 @@
1
1
  import { Client } from '../transport/client.ts';
2
2
  import { type Json } from '../transport/wire.ts';
3
- import { type HostEvent } from '../transport/events.ts';
3
+ import type { HostEvent } from '../transport/events.ts';
4
4
  import type { HostAccess } from '../transport/host.ts';
5
5
  import type { ConnectionView } from '../session/connection-view.ts';
6
- import type { ControllerStore } from '../state.ts';
6
+ /** Connection publication cannot replace session, navigation, catalog or shell state. */
7
+ export interface ConnectionStore {
8
+ readonly state: {
9
+ readonly online: boolean;
10
+ };
11
+ update(patch: Partial<{
12
+ online: boolean;
13
+ status: string;
14
+ lastFailure: string;
15
+ controlError: string | undefined;
16
+ }>): void;
17
+ }
7
18
  /** Connection inputs resolved by the CLI or a library consumer. */
8
19
  export interface ConnectionOptions {
9
20
  base: string;
@@ -40,7 +51,7 @@ export declare class ConnectionController implements HostAccess, ConnectionView
40
51
  private readonly abort;
41
52
  private runTask;
42
53
  private generationFailed;
43
- constructor(store: ControllerStore, options: ConnectionOptions, listener: ConnectionListener);
54
+ constructor(store: ConnectionStore, options: ConnectionOptions, listener: ConnectionListener);
44
55
  /** Start one retry loop, with a fresh snapshot generation after every disconnect. */
45
56
  start(): void;
46
57
  /** Cancel retries and HTTP, close the socket, and wait for the loop to settle. */
@@ -1,9 +1,9 @@
1
1
  /** Owns the physical connection, its generations, and the host event subscriptions. */
2
2
  import { setTimeout as delay } from 'node:timers/promises';
3
3
  import { AuthenticationRequired } from "../transport/auth.js";
4
- import { HttpError, RemoteError } from "../transport/client.js";
5
- import { errorText, object, string } from "../transport/wire.js";
6
- import { controlFrame, hostEvent } from "../transport/events.js";
4
+ import { HttpError } from "../transport/client.js";
5
+ import { errorText } from "../transport/wire.js";
6
+ import { ConnectionStreams } from "./connection-streams.js";
7
7
  /** Owns the physical connection: reconnects, subscriptions and generation lifecycle.
8
8
  *
9
9
  * Host running state, projections and interactions belong to the session domain; this class only
@@ -56,90 +56,61 @@ export class ConnectionController {
56
56
  let attempt = 0;
57
57
  while (!this.abort.signal.aborted) {
58
58
  this.listener.begin();
59
+ if (this.abort.signal.aborted)
60
+ break;
59
61
  const client = this.options.makeClient();
60
62
  this.current = client;
63
+ const generation = new AbortController();
64
+ const signal = AbortSignal.any([this.abort.signal, generation.signal]);
65
+ let disconnect;
66
+ const disconnected = new Promise(resolve => { disconnect = resolve; });
67
+ const fail = (error) => {
68
+ if (generation.signal.aborted)
69
+ return;
70
+ generation.abort(error);
71
+ disconnect(error);
72
+ // End HTTP as well as streams, including requests the ready callback is awaiting.
73
+ void client.close();
74
+ };
75
+ this.generationFailed = fail;
76
+ const streams = new ConnectionStreams(client, {
77
+ identified: id => { this.clientId = id; }, event: event => this.listener.event(event),
78
+ changed: () => this.store.update({}), degraded: controlError => this.store.update({ controlError }), fail,
79
+ }, signal);
61
80
  try {
81
+ signal.throwIfAborted();
62
82
  await this.options.authenticate(client);
83
+ signal.throwIfAborted();
63
84
  await client.connect();
64
- let fail;
65
- const disconnected = new Promise(resolve => { fail = resolve; });
66
- this.generationFailed = fail;
67
- const ready = new Promise((resolve, reject) => {
68
- const timer = setTimeout(() => reject(new Error('Host ready timed out')), client.timeoutMs);
69
- client.subscribe('$events', {}, {
70
- item: value => {
71
- const frame = object(value);
72
- if (frame.type === 'ready') {
73
- this.clientId = string(frame.clientId);
74
- clearTimeout(timer);
75
- resolve();
76
- return;
77
- }
78
- const event = hostEvent(frame);
79
- if (!event)
80
- return;
81
- if (this.listener.event(event))
82
- return;
83
- // An unanswered waterfall would block the host's event chain, so it is always settled.
84
- if ('eventId' in event) {
85
- void client.call('$events/result', { clientId: this.clientId,
86
- eventId: event.eventId, outcome: { kind: 'next' } }).catch(error => fail(new Error(errorText(error))));
87
- }
88
- },
89
- end: error => { clearTimeout(timer); const reason = error ?? new Error('Event stream ended'); reject(reason); fail(reason); },
90
- });
91
- });
92
- await ready;
93
- await new Promise((resolve, reject) => {
94
- const timer = setTimeout(() => reject(new Error('Session control baseline timed out')), client.timeoutMs);
95
- client.subscribe('session/control', {}, {
96
- item: value => {
97
- try {
98
- this.listener.event({ kind: 'control', frame: controlFrame(value) });
99
- this.store.update({});
100
- clearTimeout(timer);
101
- resolve();
102
- }
103
- catch (error) {
104
- // Live metrics are not worth the connection: a frame this client cannot decode is
105
- // reported and skipped, because failing the generation would blind a running turn.
106
- clearTimeout(timer);
107
- this.store.update({ controlError: `Live metrics degraded: ${errorText(error)}` });
108
- resolve();
109
- }
110
- },
111
- end: error => {
112
- clearTimeout(timer);
113
- if (error instanceof RemoteError && error.code === 'gateway/method-unavailable') {
114
- this.store.update({ controlError: 'Live metrics unavailable on this host' });
115
- resolve();
116
- }
117
- else {
118
- const reason = error ?? new Error('Session control stream ended');
119
- reject(reason);
120
- fail(reason);
121
- }
122
- },
123
- });
124
- });
85
+ signal.throwIfAborted();
86
+ await streams.start();
87
+ signal.throwIfAborted();
125
88
  await this.listener.ready();
89
+ signal.throwIfAborted();
126
90
  attempt = 0;
127
91
  const error = await disconnected;
128
92
  if (!this.abort.signal.aborted)
129
93
  throw error;
130
94
  }
131
95
  catch (error) {
132
- if (error instanceof AuthenticationRequired || error instanceof HttpError && [401, 403].includes(error.status)) {
133
- this.store.update({ lastFailure: `${errorText(error)}. Set DSH_TOKEN and restart to log in.`, status: 'Login required' });
96
+ if (this.abort.signal.aborted)
97
+ break;
98
+ const reason = generation.signal.aborted ? generation.signal.reason : error;
99
+ if (reason instanceof AuthenticationRequired || reason instanceof HttpError && [401, 403].includes(reason.status)) {
100
+ this.store.update({ lastFailure: `${errorText(reason)}. Set DSH_TOKEN and restart to log in.`, status: 'Login required' });
134
101
  return;
135
102
  }
136
- if (!this.abort.signal.aborted)
137
- this.store.update({ lastFailure: errorText(error), status: 'Reconnecting…' });
103
+ this.store.update({ lastFailure: errorText(reason), status: 'Reconnecting…' });
138
104
  }
139
105
  finally {
140
106
  this.generationFailed = undefined;
141
- this.store.update({ online: false, pending: [] });
107
+ generation.abort();
108
+ streams.close();
109
+ this.clientId = '';
110
+ this.store.update({ online: false });
142
111
  await client.close();
112
+ if (this.current === client)
113
+ this.current = undefined;
143
114
  await this.listener.ended();
144
115
  }
145
116
  if (!this.abort.signal.aborted) {