@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.
- package/README.i18n.yaml +2 -2
- package/README.md +10 -4
- package/README.zh.md +12 -6
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +22 -2
- package/dist/cli/startup.js +30 -11
- package/dist/cli/verifier.d.ts +4 -0
- package/dist/cli/verifier.js +28 -5
- package/dist/contracts.d.ts +42 -5
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +14 -3
- package/dist/controller/connection.js +40 -69
- package/dist/controller/controller.d.ts +20 -234
- package/dist/controller/controller.js +113 -811
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/loop-prompts-schema.d.ts +16 -2
- package/dist/controller/loop-prompts-schema.js +106 -27
- package/dist/controller/loop-prompts.d.ts +17 -2
- package/dist/controller/loop-prompts.generated.js +2 -1
- package/dist/controller/loop-prompts.js +35 -9
- package/dist/controller/loop-protocols.d.ts +3 -1
- package/dist/controller/loop-protocols.js +8 -3
- package/dist/controller/loop-source.d.ts +74 -0
- package/dist/controller/loop-source.js +224 -0
- package/dist/controller/verifier.d.ts +4 -0
- package/dist/cost/controller.d.ts +3 -1
- package/dist/cost/controller.js +26 -7
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +115 -15
- package/dist/cost/ledger.d.ts +31 -6
- package/dist/cost/ledger.js +74 -22
- package/dist/cost/pricing.d.ts +39 -0
- package/dist/cost/pricing.js +46 -0
- package/dist/cost/scanner.js +1 -0
- package/dist/cost/types.d.ts +9 -3
- package/dist/session/controller.d.ts +23 -35
- package/dist/session/controller.js +113 -363
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/info.d.ts +3 -38
- package/dist/session/info.js +14 -1
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +15 -6
- package/dist/session/telemetry.js +44 -7
- package/dist/session/transcript.d.ts +5 -1
- package/dist/slash/index.d.ts +1 -1
- package/dist/slash/parse.d.ts +2 -126
- package/dist/slash/registry.d.ts +1 -1
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +5 -17
- package/dist/state.js +1 -1
- package/dist/storage/files.d.ts +8 -0
- package/dist/storage/files.js +18 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/ui/app.js +88 -301
- package/dist/ui/chat/shell-view.d.ts +2 -0
- package/dist/ui/chat/shell-view.js +8 -0
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/dialogs/cost.d.ts +6 -0
- package/dist/ui/dialogs/cost.js +5 -1
- package/dist/ui/dialogs/loop.d.ts +5 -4
- package/dist/ui/dialogs/loop.js +14 -6
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/loop.yaml +230 -0
- package/package.json +5 -4
package/dist/cli/verifier.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/cli/verifier.js
CHANGED
|
@@ -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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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) {
|
package/dist/contracts.d.ts
CHANGED
|
@@ -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 {
|
|
19
|
-
import type {
|
|
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:
|
|
303
|
+
history: HistoryPanel | undefined;
|
|
267
304
|
} | {
|
|
268
305
|
kind: 'search';
|
|
269
|
-
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
|
5
|
-
import { errorText
|
|
6
|
-
import {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 (
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|