@quenty/nevermore-cli 4.41.1 → 4.42.1-canary.783.30683193716.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/CHANGELOG.md +28 -0
- package/dist/args/global-args.d.ts +13 -0
- package/dist/args/global-args.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-deploy-command.d.ts +3 -0
- package/dist/commands/batch-command/batch-deploy-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-deploy-command.js +90 -1
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +25 -4
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init.js +1 -1
- package/dist/commands/deploy-command/deploy-init.js.map +1 -1
- package/dist/commands/deploy-command/index.d.ts +3 -0
- package/dist/commands/deploy-command/index.d.ts.map +1 -1
- package/dist/commands/deploy-command/index.js +297 -60
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/commands/deploy-command/select-target.js +1 -1
- package/dist/commands/deploy-command/select-target.js.map +1 -1
- package/dist/commands/deploy-command/version-command.d.ts.map +1 -1
- package/dist/commands/deploy-command/version-command.js +138 -12
- package/dist/commands/deploy-command/version-command.js.map +1 -1
- package/dist/commands/test-command/test-command.d.ts.map +1 -1
- package/dist/commands/test-command/test-command.js +23 -11
- package/dist/commands/test-command/test-command.js.map +1 -1
- package/dist/nevermore.js +17 -0
- package/dist/nevermore.js.map +1 -1
- package/dist/utils/batch/changed-packages-utils.d.ts +1 -1
- package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
- package/dist/utils/batch/changed-packages-utils.js +1 -1
- package/dist/utils/batch/changed-packages-utils.js.map +1 -1
- package/dist/utils/build/base-place-resolver-factory.d.ts +25 -0
- package/dist/utils/build/base-place-resolver-factory.d.ts.map +1 -0
- package/dist/utils/build/base-place-resolver-factory.js +40 -0
- package/dist/utils/build/base-place-resolver-factory.js.map +1 -0
- package/dist/utils/build/build.d.ts +13 -2
- package/dist/utils/build/build.d.ts.map +1 -1
- package/dist/utils/build/build.js.map +1 -1
- package/dist/utils/build/upload.d.ts +1 -1
- package/dist/utils/build/upload.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-metadata.d.ts +1 -1
- package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.d.ts +3 -1
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +45 -6
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +6 -0
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +13 -0
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.d.ts +2 -1
- package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/cloud-job-context.js +2 -2
- package/dist/utils/job-context/cloud-job-context.js.map +1 -1
- package/dist/utils/job-context/local-job-context.d.ts +2 -1
- package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.js +2 -2
- package/dist/utils/job-context/local-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts +20 -2
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +78 -21
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js +89 -12
- package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +13 -5
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +114 -68
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js +57 -5
- package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -1
- package/dist/utils/testing/parsers/traceback-parser.d.ts +31 -0
- package/dist/utils/testing/parsers/traceback-parser.d.ts.map +1 -0
- package/dist/utils/testing/parsers/traceback-parser.js +91 -0
- package/dist/utils/testing/parsers/traceback-parser.js.map +1 -0
- package/dist/utils/testing/parsers/traceback-parser.test.d.ts +2 -0
- package/dist/utils/testing/parsers/traceback-parser.test.d.ts.map +1 -0
- package/dist/utils/testing/parsers/traceback-parser.test.js +45 -0
- package/dist/utils/testing/parsers/traceback-parser.test.js.map +1 -0
- package/dist/utils/testing/runner/combined-project-generator.d.ts +1 -1
- package/dist/utils/testing/runner/combined-project-generator.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +9 -1
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +28 -3
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.test.d.ts +2 -0
- package/dist/utils/testing/runner/test-runner.test.d.ts.map +1 -0
- package/dist/utils/testing/runner/test-runner.test.js +18 -0
- package/dist/utils/testing/runner/test-runner.test.js.map +1 -0
- package/dist/utils/testing/test-log-parser.d.ts +32 -1
- package/dist/utils/testing/test-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/test-log-parser.js +53 -9
- package/dist/utils/testing/test-log-parser.js.map +1 -1
- package/dist/utils/testing/test-log-parser.test.d.ts +2 -0
- package/dist/utils/testing/test-log-parser.test.d.ts.map +1 -0
- package/dist/utils/testing/test-log-parser.test.js +56 -0
- package/dist/utils/testing/test-log-parser.test.js.map +1 -0
- package/dist/utils/watch/github-context.d.ts +50 -0
- package/dist/utils/watch/github-context.d.ts.map +1 -0
- package/dist/utils/watch/github-context.js +125 -0
- package/dist/utils/watch/github-context.js.map +1 -0
- package/dist/utils/watch/github-context.test.d.ts +2 -0
- package/dist/utils/watch/github-context.test.d.ts.map +1 -0
- package/dist/utils/watch/github-context.test.js +112 -0
- package/dist/utils/watch/github-context.test.js.map +1 -0
- package/dist/utils/watch/http-watch-registry.d.ts +40 -0
- package/dist/utils/watch/http-watch-registry.d.ts.map +1 -0
- package/dist/utils/watch/http-watch-registry.js +262 -0
- package/dist/utils/watch/http-watch-registry.js.map +1 -0
- package/dist/utils/watch/http-watch-registry.test.d.ts +2 -0
- package/dist/utils/watch/http-watch-registry.test.d.ts.map +1 -0
- package/dist/utils/watch/http-watch-registry.test.js +211 -0
- package/dist/utils/watch/http-watch-registry.test.js.map +1 -0
- package/dist/utils/watch/local-watch-loop.d.ts +50 -0
- package/dist/utils/watch/local-watch-loop.d.ts.map +1 -0
- package/dist/utils/watch/local-watch-loop.js +103 -0
- package/dist/utils/watch/local-watch-loop.js.map +1 -0
- package/dist/utils/watch/local-watch-loop.test.d.ts +2 -0
- package/dist/utils/watch/local-watch-loop.test.d.ts.map +1 -0
- package/dist/utils/watch/local-watch-loop.test.js +170 -0
- package/dist/utils/watch/local-watch-loop.test.js.map +1 -0
- package/dist/utils/watch/register-notify-watches.d.ts +41 -0
- package/dist/utils/watch/register-notify-watches.d.ts.map +1 -0
- package/dist/utils/watch/register-notify-watches.js +159 -0
- package/dist/utils/watch/register-notify-watches.js.map +1 -0
- package/dist/utils/watch/register-notify-watches.test.d.ts +2 -0
- package/dist/utils/watch/register-notify-watches.test.d.ts.map +1 -0
- package/dist/utils/watch/register-notify-watches.test.js +225 -0
- package/dist/utils/watch/register-notify-watches.test.js.map +1 -0
- package/dist/utils/watch/register-watches.d.ts +68 -0
- package/dist/utils/watch/register-watches.d.ts.map +1 -0
- package/dist/utils/watch/register-watches.js +221 -0
- package/dist/utils/watch/register-watches.js.map +1 -0
- package/dist/utils/watch/register-watches.test.d.ts +2 -0
- package/dist/utils/watch/register-watches.test.d.ts.map +1 -0
- package/dist/utils/watch/register-watches.test.js +576 -0
- package/dist/utils/watch/register-watches.test.js.map +1 -0
- package/dist/utils/watch/stream-watch-loop.d.ts +54 -0
- package/dist/utils/watch/stream-watch-loop.d.ts.map +1 -0
- package/dist/utils/watch/stream-watch-loop.js +282 -0
- package/dist/utils/watch/stream-watch-loop.js.map +1 -0
- package/dist/utils/watch/stream-watch-loop.test.d.ts +2 -0
- package/dist/utils/watch/stream-watch-loop.test.d.ts.map +1 -0
- package/dist/utils/watch/stream-watch-loop.test.js +392 -0
- package/dist/utils/watch/stream-watch-loop.test.js.map +1 -0
- package/dist/utils/watch/websocket-watch-stream.d.ts +25 -0
- package/dist/utils/watch/websocket-watch-stream.d.ts.map +1 -0
- package/dist/utils/watch/websocket-watch-stream.js +306 -0
- package/dist/utils/watch/websocket-watch-stream.js.map +1 -0
- package/dist/utils/watch/websocket-watch-stream.test.d.ts +2 -0
- package/dist/utils/watch/websocket-watch-stream.test.d.ts.map +1 -0
- package/dist/utils/watch/websocket-watch-stream.test.js +475 -0
- package/dist/utils/watch/websocket-watch-stream.test.js.map +1 -0
- package/package.json +7 -4
- package/src/args/global-args.ts +13 -0
- package/src/commands/batch-command/batch-deploy-command.ts +105 -1
- package/src/commands/batch-command/batch-test-command.ts +86 -60
- package/src/commands/deploy-command/deploy-init.ts +1 -1
- package/src/commands/deploy-command/index.ts +327 -32
- package/src/commands/deploy-command/select-target.ts +1 -1
- package/src/commands/deploy-command/version-command.ts +215 -21
- package/src/commands/test-command/test-command.ts +31 -22
- package/src/nevermore.ts +19 -0
- package/src/utils/batch/changed-packages-utils.ts +1 -1
- package/src/utils/build/base-place-resolver-factory.ts +49 -0
- package/src/utils/build/build.ts +13 -2
- package/src/utils/build/upload.ts +1 -1
- package/src/utils/deploy/deploy-metadata.ts +1 -1
- package/src/utils/job-context/base-job-context.ts +68 -6
- package/src/utils/job-context/batch-script-job-context.ts +18 -0
- package/src/utils/job-context/cloud-job-context.ts +7 -2
- package/src/utils/job-context/local-job-context.ts +7 -2
- package/src/utils/open-cloud/open-cloud-client.test.ts +133 -14
- package/src/utils/open-cloud/open-cloud-client.ts +131 -35
- package/src/utils/testing/parsers/batch-log-parser.test.ts +70 -5
- package/src/utils/testing/parsers/batch-log-parser.ts +141 -80
- package/src/utils/testing/parsers/traceback-parser.test.ts +64 -0
- package/src/utils/testing/parsers/traceback-parser.ts +122 -0
- package/src/utils/testing/runner/combined-project-generator.ts +1 -1
- package/src/utils/testing/runner/test-runner.test.ts +29 -0
- package/src/utils/testing/runner/test-runner.ts +36 -4
- package/src/utils/testing/test-log-parser.test.ts +70 -0
- package/src/utils/testing/test-log-parser.ts +80 -10
- package/src/utils/watch/github-context.test.ts +154 -0
- package/src/utils/watch/github-context.ts +184 -0
- package/src/utils/watch/http-watch-registry.test.ts +330 -0
- package/src/utils/watch/http-watch-registry.ts +341 -0
- package/src/utils/watch/local-watch-loop.test.ts +197 -0
- package/src/utils/watch/local-watch-loop.ts +190 -0
- package/src/utils/watch/register-notify-watches.test.ts +282 -0
- package/src/utils/watch/register-notify-watches.ts +226 -0
- package/src/utils/watch/register-watches.test.ts +696 -0
- package/src/utils/watch/register-watches.ts +369 -0
- package/src/utils/watch/stream-watch-loop.test.ts +625 -0
- package/src/utils/watch/stream-watch-loop.ts +411 -0
- package/src/utils/watch/websocket-watch-stream.test.ts +581 -0
- package/src/utils/watch/websocket-watch-stream.ts +403 -0
- package/tsconfig.json +19 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/build/deploy-config.d.ts +0 -71
- package/dist/utils/build/deploy-config.d.ts.map +0 -1
- package/dist/utils/build/deploy-config.js +0 -164
- package/dist/utils/build/deploy-config.js.map +0 -1
- package/dist/utils/build/deploy-config.test.d.ts +0 -2
- package/dist/utils/build/deploy-config.test.d.ts.map +0 -1
- package/dist/utils/build/deploy-config.test.js +0 -126
- package/dist/utils/build/deploy-config.test.js.map +0 -1
- package/src/utils/build/deploy-config.test.ts +0 -149
- package/src/utils/build/deploy-config.ts +0 -259
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import {
|
|
3
|
+
type WatchStream,
|
|
4
|
+
type WatchStreamEvent,
|
|
5
|
+
type WatchStreamNotify,
|
|
6
|
+
type WatchStreamReady,
|
|
7
|
+
type WatchStreamResult,
|
|
8
|
+
type WatchStreamOptions,
|
|
9
|
+
} from '@quenty/nevermore-deploy';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The monitor was released, or its lease lapsed. Retrying cannot help, so this
|
|
13
|
+
* ends the stream rather than backing off against something that is gone.
|
|
14
|
+
*/
|
|
15
|
+
const CLOSE_MONITOR_GONE = 4004;
|
|
16
|
+
|
|
17
|
+
/** Orderly shutdown on the service's side. Reconnect promptly, not as a fault. */
|
|
18
|
+
const CLOSE_GOING_AWAY = 1001;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The service retires a connection roughly hourly so that reconnecting
|
|
22
|
+
* re-verifies the token — a socket must not outlive a revoked credential.
|
|
23
|
+
* Routine by design, so it reconnects promptly and is not reported as a drop.
|
|
24
|
+
*/
|
|
25
|
+
const CLOSE_REAUTHENTICATE = 4005;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handshake refusals worth explaining rather than retrying blindly.
|
|
29
|
+
*
|
|
30
|
+
* A websocket that fails to upgrade reports only "Unexpected server response:
|
|
31
|
+
* <status>", which is the same message for a misconfigured proxy and an expired
|
|
32
|
+
* token — two problems with nothing in common. These are the statuses the API
|
|
33
|
+
* documents, each mapped to the thing that actually has to change.
|
|
34
|
+
*/
|
|
35
|
+
const HANDSHAKE_FAILURES: Record<
|
|
36
|
+
number,
|
|
37
|
+
{ permanent: boolean; describe: () => string }
|
|
38
|
+
> = {
|
|
39
|
+
401: {
|
|
40
|
+
permanent: true,
|
|
41
|
+
describe: () =>
|
|
42
|
+
'The watch service rejected the token on the stream handshake. Set ' +
|
|
43
|
+
'NEVERMORE_WATCH_TOKEN to a valid, unexpired GitHub token.',
|
|
44
|
+
},
|
|
45
|
+
404: {
|
|
46
|
+
permanent: true,
|
|
47
|
+
describe: () =>
|
|
48
|
+
'The watch monitor was not found, or the token cannot write to the ' +
|
|
49
|
+
'repository that owns it. A stream is readable only by a token with ' +
|
|
50
|
+
'write access to that repository.',
|
|
51
|
+
},
|
|
52
|
+
426: {
|
|
53
|
+
permanent: false,
|
|
54
|
+
describe: () =>
|
|
55
|
+
'The watch service answered the stream handshake with 426 Upgrade ' +
|
|
56
|
+
'Required, which means it never saw an Upgrade header — this client ' +
|
|
57
|
+
'sent one, so something in front of the service is stripping it. That ' +
|
|
58
|
+
'is a proxy configuration problem on the service side (nginx needs ' +
|
|
59
|
+
'proxy_http_version 1.1 plus the Upgrade and Connection headers ' +
|
|
60
|
+
'forwarded), not something to fix here.',
|
|
61
|
+
},
|
|
62
|
+
429: {
|
|
63
|
+
permanent: false,
|
|
64
|
+
describe: () =>
|
|
65
|
+
'Too many concurrent watch streams are open for this monitor. Close ' +
|
|
66
|
+
'another `--watch` session, or wait for one to time out.',
|
|
67
|
+
},
|
|
68
|
+
503: {
|
|
69
|
+
permanent: false,
|
|
70
|
+
describe: () =>
|
|
71
|
+
'The watch service is not accepting streams right now. Retrying.',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
76
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Floor under a prompt reconnect. A server that accepts, sends `ready` and
|
|
80
|
+
* immediately closes would otherwise be reconnected to as fast as the loop can
|
|
81
|
+
* spin — around a hundred connections a second, each costing the service a full
|
|
82
|
+
* token verification. That is a stampede aimed at something already failing, so
|
|
83
|
+
* even a "come back now" close waits this long.
|
|
84
|
+
*/
|
|
85
|
+
const RECONNECT_MIN_MS = 250;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* How long a connection must last to count as healthy enough to reset the
|
|
89
|
+
* backoff. Receiving `ready` is not enough by itself: a crash-looping server
|
|
90
|
+
* sends one every time, which would pin the delay at its minimum forever.
|
|
91
|
+
*/
|
|
92
|
+
const HEALTHY_CONNECTION_MS = 10_000;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Answers the service's heartbeat at the application layer as well as the
|
|
96
|
+
* protocol one. A proxy that terminates and re-originates the socket can eat
|
|
97
|
+
* ping/pong control frames while happily forwarding text, which looks exactly
|
|
98
|
+
* like a healthy connection until nothing arrives for an hour.
|
|
99
|
+
*/
|
|
100
|
+
const APP_PING_INTERVAL_MS = 30_000;
|
|
101
|
+
|
|
102
|
+
export interface WebSocketWatchStreamOptions {
|
|
103
|
+
/**
|
|
104
|
+
* The register endpoint this monitor was created against. The stream URL is
|
|
105
|
+
* derived from it so the host is configured in exactly one place, and so no
|
|
106
|
+
* service address is ever written down in this repo.
|
|
107
|
+
*/
|
|
108
|
+
registerUrl: string;
|
|
109
|
+
/** Injected in tests. */
|
|
110
|
+
connect?: (url: string, headers: Record<string, string>) => WebSocket;
|
|
111
|
+
/** Injected in tests; a real session cannot wait ten seconds per assertion. */
|
|
112
|
+
healthyConnectionMs?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class WebSocketWatchStream implements WatchStream {
|
|
116
|
+
private readonly _registerUrl: string;
|
|
117
|
+
private readonly _connect: (
|
|
118
|
+
url: string,
|
|
119
|
+
headers: Record<string, string>
|
|
120
|
+
) => WebSocket;
|
|
121
|
+
private readonly _healthyConnectionMs: number;
|
|
122
|
+
|
|
123
|
+
public constructor(options: WebSocketWatchStreamOptions) {
|
|
124
|
+
this._registerUrl = options.registerUrl;
|
|
125
|
+
this._connect =
|
|
126
|
+
options.connect ?? ((url, headers) => new WebSocket(url, { headers }));
|
|
127
|
+
this._healthyConnectionMs =
|
|
128
|
+
options.healthyConnectionMs ?? HEALTHY_CONNECTION_MS;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async runAsync(
|
|
132
|
+
options: WatchStreamOptions
|
|
133
|
+
): Promise<WatchStreamResult> {
|
|
134
|
+
const url = this._streamUrl(options.monitorId);
|
|
135
|
+
|
|
136
|
+
let reconnects = 0;
|
|
137
|
+
let notifications = 0;
|
|
138
|
+
let attempt = 0;
|
|
139
|
+
|
|
140
|
+
while (!options.signal.aborted) {
|
|
141
|
+
const outcome = await this._holdAsync(url, options, (n) => {
|
|
142
|
+
notifications += n;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (outcome.ending === 'monitor-gone') {
|
|
146
|
+
return {
|
|
147
|
+
ending: 'monitor-gone',
|
|
148
|
+
reason: outcome.reason,
|
|
149
|
+
reconnects,
|
|
150
|
+
notifications,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (outcome.ending === 'rejected') {
|
|
154
|
+
return {
|
|
155
|
+
ending: 'rejected',
|
|
156
|
+
reason: outcome.reason,
|
|
157
|
+
reconnects,
|
|
158
|
+
notifications,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (options.signal.aborted) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// A connection that carried `ready` *and* lasted proves the endpoint is
|
|
166
|
+
// healthy, so the next drop backs off from scratch rather than inheriting
|
|
167
|
+
// a delay from a problem that is over. Both halves matter: a server that
|
|
168
|
+
// sends `ready` and then drops would otherwise never back off at all.
|
|
169
|
+
const healthy =
|
|
170
|
+
outcome.wasReady && outcome.livedMs >= this._healthyConnectionMs;
|
|
171
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
172
|
+
reconnects++;
|
|
173
|
+
// Backoff applies to every close, including the ones that mean "come back
|
|
174
|
+
// now". A healthy connection has already reset `attempt`, so a real
|
|
175
|
+
// shutdown or the hourly retirement reconnects promptly anyway — while a
|
|
176
|
+
// server stuck emitting one of those codes still backs off instead of
|
|
177
|
+
// being hammered at a flat rate forever.
|
|
178
|
+
const delay = Math.max(RECONNECT_MIN_MS, _backoffMs(attempt));
|
|
179
|
+
// A retirement is scheduled maintenance rather than a drop, and saying so
|
|
180
|
+
// hourly would train people to ignore it. But it is only maintenance if
|
|
181
|
+
// the connection actually lasted; arriving on a short-lived one means
|
|
182
|
+
// something is wrong, and silence would hide it completely.
|
|
183
|
+
if (!outcome.routine || !healthy) {
|
|
184
|
+
options.handlers.onReconnect?.(reconnects, outcome.reason);
|
|
185
|
+
}
|
|
186
|
+
await _sleepAsync(delay, options.signal);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { ending: 'aborted', reconnects, notifications };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `.../v1/register/30m/` and `.../v1/monitors/{id}/stream` share a root. */
|
|
193
|
+
private _streamUrl(monitorId: string): string {
|
|
194
|
+
const url = new URL(this._registerUrl);
|
|
195
|
+
const marker = '/v1/register/';
|
|
196
|
+
const index = url.pathname.indexOf(marker);
|
|
197
|
+
if (index < 0) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Cannot derive a watch stream URL from "${this._registerUrl}" — ` +
|
|
200
|
+
`expected its path to contain "${marker}".`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
url.pathname = `${url.pathname.slice(
|
|
204
|
+
0,
|
|
205
|
+
index
|
|
206
|
+
)}/v1/monitors/${encodeURIComponent(monitorId)}/stream`.replace(
|
|
207
|
+
/\/{2,}/g,
|
|
208
|
+
'/'
|
|
209
|
+
);
|
|
210
|
+
url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:';
|
|
211
|
+
return url.toString();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private _holdAsync(
|
|
215
|
+
url: string,
|
|
216
|
+
options: WatchStreamOptions,
|
|
217
|
+
countNotifications: (n: number) => void
|
|
218
|
+
): Promise<_HoldOutcome> {
|
|
219
|
+
return new Promise<_HoldOutcome>((resolve) => {
|
|
220
|
+
const socket = this._connect(url, {
|
|
221
|
+
// Header rather than a query parameter, deliberately: a PAT in a URL
|
|
222
|
+
// lands in every access log between here and the service.
|
|
223
|
+
Authorization: `Bearer ${options.credential}`,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
let ready = false;
|
|
227
|
+
let settled = false;
|
|
228
|
+
const openedAt = Date.now();
|
|
229
|
+
let notifications = 0;
|
|
230
|
+
let pinger: NodeJS.Timeout | undefined;
|
|
231
|
+
|
|
232
|
+
// Handlers are async and messages arrive whenever they arrive, so they
|
|
233
|
+
// are chained onto one tail. Two rebuilds of the same place running
|
|
234
|
+
// concurrently would race each other's uploads.
|
|
235
|
+
//
|
|
236
|
+
// Every link swallows its own rejection. The port does not promise a
|
|
237
|
+
// handler is safe, and a rejected tail would both strand `finish` — the
|
|
238
|
+
// watch hanging forever — and take every later frame with it, since
|
|
239
|
+
// `.then` on a rejected promise never runs.
|
|
240
|
+
let tail: Promise<void> = Promise.resolve();
|
|
241
|
+
const chain = (run: () => Promise<void>): void => {
|
|
242
|
+
tail = tail.then(run).catch((err: unknown) => {
|
|
243
|
+
options.handlers.onHandlerError?.(
|
|
244
|
+
err instanceof Error ? err : new Error(String(err))
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Stamped here rather than at each call site, so a new way of ending a
|
|
250
|
+
// connection cannot forget to report how long it lasted and silently
|
|
251
|
+
// count itself healthy.
|
|
252
|
+
const finish = (outcome: Omit<_HoldOutcome, 'livedMs'>) => {
|
|
253
|
+
if (settled) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
settled = true;
|
|
257
|
+
if (pinger) {
|
|
258
|
+
clearInterval(pinger);
|
|
259
|
+
}
|
|
260
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
261
|
+
countNotifications(notifications);
|
|
262
|
+
const settledOutcome: _HoldOutcome = {
|
|
263
|
+
...outcome,
|
|
264
|
+
livedMs: Date.now() - openedAt,
|
|
265
|
+
};
|
|
266
|
+
// Let queued handlers drain before reporting, so a rebuild that was in
|
|
267
|
+
// flight when the socket dropped still finishes and still records. A
|
|
268
|
+
// rejected tail resolves too: a handler that threw has already been
|
|
269
|
+
// reported, and must not strand the watch.
|
|
270
|
+
void tail.then(
|
|
271
|
+
() => resolve(settledOutcome),
|
|
272
|
+
() => resolve(settledOutcome)
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const onAbort = () => {
|
|
277
|
+
socket.close();
|
|
278
|
+
finish({ ending: 'closed', reason: 'stopped', wasReady: ready });
|
|
279
|
+
};
|
|
280
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
281
|
+
|
|
282
|
+
socket.on('open', () => {
|
|
283
|
+
// 'open' can still fire after an abort during CONNECTING, and an
|
|
284
|
+
// interval installed then would never be cleared.
|
|
285
|
+
if (settled) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
pinger = setInterval(() => {
|
|
289
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
290
|
+
socket.send(JSON.stringify({ type: 'ping' }));
|
|
291
|
+
}
|
|
292
|
+
}, APP_PING_INTERVAL_MS);
|
|
293
|
+
pinger.unref?.();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
socket.on('message', (data: { toString(): string }) => {
|
|
297
|
+
let message: { type?: string } | undefined;
|
|
298
|
+
try {
|
|
299
|
+
message = JSON.parse(data.toString()) as { type?: string };
|
|
300
|
+
} catch {
|
|
301
|
+
// A frame we cannot parse is the service being newer than us, not a
|
|
302
|
+
// reason to drop a working connection.
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
switch (message.type) {
|
|
307
|
+
case 'ready':
|
|
308
|
+
ready = true;
|
|
309
|
+
chain(() =>
|
|
310
|
+
options.handlers.onReadyAsync(message as WatchStreamReady)
|
|
311
|
+
);
|
|
312
|
+
break;
|
|
313
|
+
case 'notify':
|
|
314
|
+
notifications++;
|
|
315
|
+
chain(() =>
|
|
316
|
+
options.handlers.onNotifyAsync(message as WatchStreamNotify)
|
|
317
|
+
);
|
|
318
|
+
break;
|
|
319
|
+
case 'event':
|
|
320
|
+
options.handlers.onEvent?.(message as WatchStreamEvent);
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Fires before 'error' when the upgrade itself was refused, and unlike
|
|
328
|
+
// 'error' it carries the status — which is the only thing that tells a
|
|
329
|
+
// stripped Upgrade header apart from a dead token.
|
|
330
|
+
socket.on('unexpected-response', (_request, response) => {
|
|
331
|
+
const status = response.statusCode ?? 0;
|
|
332
|
+
const known = HANDSHAKE_FAILURES[status];
|
|
333
|
+
// Registering this listener makes teardown ours: `ws` stops doing it.
|
|
334
|
+
response.resume();
|
|
335
|
+
response.destroy();
|
|
336
|
+
finish({
|
|
337
|
+
ending: known?.permanent ? 'rejected' : 'closed',
|
|
338
|
+
reason: known
|
|
339
|
+
? known.describe()
|
|
340
|
+
: `the stream handshake failed with ${status}`,
|
|
341
|
+
wasReady: ready,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
socket.on('error', (err: Error) => {
|
|
346
|
+
// The socket may still be half-open, and nothing else will close it
|
|
347
|
+
// once we have stopped listening.
|
|
348
|
+
socket.terminate();
|
|
349
|
+
finish({ ending: 'closed', reason: err.message, wasReady: ready });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
socket.on('close', (code: number, reason: Buffer) => {
|
|
353
|
+
if (code === CLOSE_MONITOR_GONE) {
|
|
354
|
+
finish({
|
|
355
|
+
ending: 'monitor-gone',
|
|
356
|
+
reason: reason.toString() || 'monitor released or lease expired',
|
|
357
|
+
wasReady: ready,
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
finish({
|
|
362
|
+
ending: 'closed',
|
|
363
|
+
reason: reason.toString() || `closed with ${code}`,
|
|
364
|
+
wasReady: ready,
|
|
365
|
+
immediate: code === CLOSE_GOING_AWAY || code === CLOSE_REAUTHENTICATE,
|
|
366
|
+
routine: code === CLOSE_REAUTHENTICATE,
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface _HoldOutcome {
|
|
374
|
+
ending: 'closed' | 'monitor-gone' | 'rejected';
|
|
375
|
+
reason: string;
|
|
376
|
+
/** Whether this connection got far enough to deliver its `ready` frame. */
|
|
377
|
+
wasReady: boolean;
|
|
378
|
+
/** How long the connection lasted, used to tell healthy from flapping. */
|
|
379
|
+
livedMs: number;
|
|
380
|
+
/** Set for a shutdown or retirement — "come back now" rather than a failure. */
|
|
381
|
+
immediate?: boolean;
|
|
382
|
+
/** Set when the close was scheduled maintenance and not worth reporting. */
|
|
383
|
+
routine?: boolean;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function _backoffMs(attempt: number): number {
|
|
387
|
+
return Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** (attempt - 1));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function _sleepAsync(ms: number, signal: AbortSignal): Promise<void> {
|
|
391
|
+
if (ms <= 0 || signal.aborted) {
|
|
392
|
+
return Promise.resolve();
|
|
393
|
+
}
|
|
394
|
+
return new Promise((resolve) => {
|
|
395
|
+
const timer = setTimeout(finish, ms);
|
|
396
|
+
function finish() {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
signal.removeEventListener('abort', finish);
|
|
399
|
+
resolve();
|
|
400
|
+
}
|
|
401
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
402
|
+
});
|
|
403
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -8,10 +8,23 @@
|
|
|
8
8
|
"declaration": true
|
|
9
9
|
},
|
|
10
10
|
"references": [
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
11
|
+
{
|
|
12
|
+
"path": "../cli-output-helpers"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "../nevermore-template-helpers"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "../nevermore-cli-helpers"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "../studio-bridge"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "../nevermore-deploy"
|
|
25
|
+
}
|
|
15
26
|
],
|
|
16
|
-
"include": [
|
|
17
|
-
|
|
27
|
+
"include": [
|
|
28
|
+
"src/**/*"
|
|
29
|
+
]
|
|
30
|
+
}
|