@quenty/nevermore-cli 4.42.0 → 4.42.1-canary.783.30683606415.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 +22 -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 +7 -2
- 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 +301 -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 +17 -7
- 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 +74 -20
- 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/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 +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- 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 +8 -2
- package/src/commands/deploy-command/deploy-init.ts +1 -1
- package/src/commands/deploy-command/index.ts +331 -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 +24 -18
- 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 +127 -34
- package/src/utils/testing/runner/combined-project-generator.ts +1 -1
- package/src/utils/testing/runner/test-runner.ts +1 -1
- 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,411 @@
|
|
|
1
|
+
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
2
|
+
import {
|
|
3
|
+
type PlaceVersionSource,
|
|
4
|
+
type WatchStream,
|
|
5
|
+
type WatchStreamNotify,
|
|
6
|
+
type WatchStreamReady,
|
|
7
|
+
type WatchStreamStatus,
|
|
8
|
+
} from '@quenty/nevermore-deploy';
|
|
9
|
+
import { type LocalWatchEntry } from './local-watch-loop.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether the service has actually failed to read this source, as opposed to
|
|
13
|
+
* simply not having polled it yet.
|
|
14
|
+
*
|
|
15
|
+
* One failure is a blip — an asset-delivery hiccup, a rate limit — and the next
|
|
16
|
+
* poll usually clears it. Requiring the service's own consecutive counter to
|
|
17
|
+
* have moved keeps a transient error from abandoning a working stream.
|
|
18
|
+
*/
|
|
19
|
+
function _isUnreadable(status: WatchStreamStatus): boolean {
|
|
20
|
+
return (
|
|
21
|
+
status.sourceError != null && (status.sourceConsecutiveFailures ?? 0) > 1
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A locally-watched place, paired with the watch name it was registered under. */
|
|
26
|
+
export interface StreamWatchEntry extends LocalWatchEntry {
|
|
27
|
+
/** `Watch.name` in the registered monitor; how the service refers to it. */
|
|
28
|
+
watchName: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StreamWatchLoopOptions {
|
|
32
|
+
entries: StreamWatchEntry[];
|
|
33
|
+
stream: WatchStream;
|
|
34
|
+
/**
|
|
35
|
+
* Where the version to build from comes from.
|
|
36
|
+
*
|
|
37
|
+
* The service and this CLI do not share a version vocabulary — see the note
|
|
38
|
+
* on `_settleAsync` — so the stream says *that* a place moved and this says
|
|
39
|
+
* *what* it moved to.
|
|
40
|
+
*/
|
|
41
|
+
source: PlaceVersionSource;
|
|
42
|
+
monitorId: string;
|
|
43
|
+
/** Bearer token; the same one registration used. */
|
|
44
|
+
credential: string;
|
|
45
|
+
/** Rebuild and redeploy. Errors are reported and the stream stays up. */
|
|
46
|
+
redeployAsync: (entry: StreamWatchEntry, version: number) => Promise<void>;
|
|
47
|
+
/** Lets a caller stop the loop; SIGINT is wired to one by default. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StreamWatchLoopResult {
|
|
52
|
+
redeploys: number;
|
|
53
|
+
failures: number;
|
|
54
|
+
reconnects: number;
|
|
55
|
+
/** True when the monitor went away rather than the user stopping. */
|
|
56
|
+
monitorGone: boolean;
|
|
57
|
+
/** True when the service refused the handshake for good. */
|
|
58
|
+
rejected: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* True when the service cannot observe any of these places, so holding the
|
|
61
|
+
* stream would wait forever. The caller should fall back to polling.
|
|
62
|
+
*/
|
|
63
|
+
unobservable: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Watch by being told, and rebuild here.
|
|
68
|
+
*
|
|
69
|
+
* The service polls the base place and pushes a `notify` when it moves, so this
|
|
70
|
+
* side holds a socket instead of a poll timer — no Open Cloud quota spent per
|
|
71
|
+
* developer, and the rebuild starts when the change happens rather than up to
|
|
72
|
+
* an interval later.
|
|
73
|
+
*
|
|
74
|
+
* There is no session length. A held connection renews the monitor's lease, so
|
|
75
|
+
* the lease passed to `--watch` is a floor for how long the monitor outlives a
|
|
76
|
+
* dropped link, not a clock on the terminal: this runs until Ctrl-C.
|
|
77
|
+
*/
|
|
78
|
+
export async function runStreamWatchLoopAsync(
|
|
79
|
+
options: StreamWatchLoopOptions
|
|
80
|
+
): Promise<StreamWatchLoopResult> {
|
|
81
|
+
const { entries, stream, source, redeployAsync } = options;
|
|
82
|
+
|
|
83
|
+
// Always our own controller, chained to the caller's when there is one: this
|
|
84
|
+
// loop has to be able to stop the stream itself (an unobservable monitor),
|
|
85
|
+
// which it could not do if the only signal belonged to someone else.
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const signal = controller.signal;
|
|
88
|
+
const onSigint = () => controller.abort();
|
|
89
|
+
if (options.signal) {
|
|
90
|
+
if (options.signal.aborted) {
|
|
91
|
+
controller.abort();
|
|
92
|
+
} else {
|
|
93
|
+
options.signal.addEventListener('abort', onSigint, { once: true });
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
process.on('SIGINT', onSigint);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const byWatchName = new Map(entries.map((e) => [e.watchName, e]));
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What each place has actually been built from here, as an Open Cloud place
|
|
103
|
+
* version. Seeded from the lock and advanced only by a rebuild that
|
|
104
|
+
* succeeded: the service moves its own baseline whether or not anyone was
|
|
105
|
+
* listening, so this map is the only durable record of what exists.
|
|
106
|
+
*/
|
|
107
|
+
const built = new Map<string, number | undefined>(
|
|
108
|
+
entries.map((e) => [e.watchName, e.baseline])
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The last service-side token each watch was settled at. Purely an
|
|
113
|
+
* optimization — a reconnect that reports the same token needs no Open Cloud
|
|
114
|
+
* call — and deliberately not advanced when a rebuild fails, so the next
|
|
115
|
+
* `ready` retries it.
|
|
116
|
+
*/
|
|
117
|
+
const settledAt = new Map<string, string>();
|
|
118
|
+
|
|
119
|
+
let redeploys = 0;
|
|
120
|
+
let failures = 0;
|
|
121
|
+
let lastDropReason: string | undefined;
|
|
122
|
+
let unobservable = false;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Watches the service has told us it cannot read, from either direction:
|
|
126
|
+
* the `ready` snapshot on connect, or a `poll-failed` event as it happens.
|
|
127
|
+
*
|
|
128
|
+
* Both are needed. A connection opened before the service's first failed
|
|
129
|
+
* poll gets a clean snapshot, and the next one is an hour away — so without
|
|
130
|
+
* the live tail a private base place would look fine for an hour before the
|
|
131
|
+
* watch admitted it was doing nothing.
|
|
132
|
+
*/
|
|
133
|
+
const unreadable = new Set<string>();
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Give up the stream once nothing on it can ever fire, so the caller can
|
|
137
|
+
* poll instead — this machine's credentials reach places the service's
|
|
138
|
+
* credential-free driver cannot.
|
|
139
|
+
*/
|
|
140
|
+
const abandonIfNothingLeftToWatch = (): boolean => {
|
|
141
|
+
// Counted against every entry this run owns, not against what the last
|
|
142
|
+
// frame happened to mention: a watch the service has stopped reporting
|
|
143
|
+
// altogether can no more produce a notification than one it cannot read.
|
|
144
|
+
if (unreadable.size < byWatchName.size) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
unobservable = true;
|
|
148
|
+
controller.abort();
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
OutputHelper.info(
|
|
153
|
+
`Watching ${entries.length} base place${
|
|
154
|
+
entries.length === 1 ? '' : 's'
|
|
155
|
+
} over the watch service. Ctrl-C to stop.`
|
|
156
|
+
);
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
OutputHelper.info(
|
|
159
|
+
` ${entry.label} — base place ${entry.placeId} (${entry.versionType}${
|
|
160
|
+
entry.baseline == null ? '' : ` v${entry.baseline}`
|
|
161
|
+
})`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Bring one watch up to date, given the service's opaque change token.
|
|
167
|
+
*
|
|
168
|
+
* The token is all the service can offer: for a Roblox place it is the
|
|
169
|
+
* asset-delivery content hash, which answers "has this moved" and nothing
|
|
170
|
+
* else. The lock file speaks Open Cloud place versions instead, and the two
|
|
171
|
+
* are not comparable — so a change is confirmed here by asking Open Cloud
|
|
172
|
+
* what the place is actually at, which is the same number the build records.
|
|
173
|
+
*/
|
|
174
|
+
const settleAsync = async (
|
|
175
|
+
entry: StreamWatchEntry,
|
|
176
|
+
token: string | null
|
|
177
|
+
): Promise<void> => {
|
|
178
|
+
if (token != null && settledAt.get(entry.watchName) === token) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let latest: number;
|
|
183
|
+
try {
|
|
184
|
+
latest = await source.resolveLatestPlaceVersionAsync(
|
|
185
|
+
entry.universeId,
|
|
186
|
+
entry.placeId,
|
|
187
|
+
entry.versionType
|
|
188
|
+
);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// Never fatal. A watch is meant to be left running for hours, and the
|
|
191
|
+
// next frame will ask again.
|
|
192
|
+
OutputHelper.warn(
|
|
193
|
+
`Could not check ${entry.label}: ` +
|
|
194
|
+
(err instanceof Error ? err.message : String(err))
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const previous = built.get(entry.watchName);
|
|
200
|
+
const settle = () => {
|
|
201
|
+
if (token != null) {
|
|
202
|
+
settledAt.set(entry.watchName, token);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (previous === undefined) {
|
|
207
|
+
// Never built here and nothing locked: adopt what is there rather than
|
|
208
|
+
// reading "unknown -> something" as a change.
|
|
209
|
+
built.set(entry.watchName, latest);
|
|
210
|
+
settle();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (latest === previous) {
|
|
214
|
+
settle();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
OutputHelper.info(
|
|
219
|
+
`${entry.label}: base place moved v${previous} → v${latest}, rebuilding...`
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
await redeployAsync(entry, latest);
|
|
223
|
+
redeploys++;
|
|
224
|
+
built.set(entry.watchName, latest);
|
|
225
|
+
settle();
|
|
226
|
+
OutputHelper.info(`${entry.label}: rebuilt from v${latest}.`);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// Left unsettled on purpose: the next `ready` sees it as still stale and
|
|
229
|
+
// tries again, which is what a rebuild that failed halfway deserves.
|
|
230
|
+
failures++;
|
|
231
|
+
OutputHelper.error(
|
|
232
|
+
`${entry.label}: rebuild failed — ` +
|
|
233
|
+
(err instanceof Error ? err.message : String(err))
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const result = await stream.runAsync({
|
|
240
|
+
monitorId: options.monitorId,
|
|
241
|
+
credential: options.credential,
|
|
242
|
+
signal,
|
|
243
|
+
handlers: {
|
|
244
|
+
// Every connection starts here, including reconnects. Nothing is
|
|
245
|
+
// replayed while a client is away, so this is the whole catch-up.
|
|
246
|
+
onReadyAsync: async (ready: WatchStreamReady) => {
|
|
247
|
+
const ours = ready.watches.filter((s) => byWatchName.has(s.name));
|
|
248
|
+
|
|
249
|
+
// A source the service cannot read never fires, and the socket says
|
|
250
|
+
// nothing about it — the failure is visible only here. Waiting
|
|
251
|
+
// silently forever is the worst of the options, so a monitor that can
|
|
252
|
+
// see none of these places gives up the stream and lets the caller
|
|
253
|
+
// poll instead, using this machine's credentials, which can reach
|
|
254
|
+
// places the service cannot.
|
|
255
|
+
//
|
|
256
|
+
// Keyed on `sourceError`, not on `state`. `state` tracks dispatching,
|
|
257
|
+
// and a notify that reaches nobody is a success — so a watch whose
|
|
258
|
+
// place is private sits at "pending" forever and never reaches
|
|
259
|
+
// "failed". `sourceError` is the service's answer to exactly this.
|
|
260
|
+
const failedToRead = ours.filter((s) => _isUnreadable(s));
|
|
261
|
+
|
|
262
|
+
// An empty intersection is the same silent wait by another route: the
|
|
263
|
+
// monitor exists but holds none of our watches, so nothing can ever
|
|
264
|
+
// arrive for us. Reachable when another checkout re-registered this
|
|
265
|
+
// monitor name with different place names.
|
|
266
|
+
// A watch the monitor does not hold at all is as dead as one it
|
|
267
|
+
// cannot read — no frame can ever name it. Folding the missing into
|
|
268
|
+
// the same set means a partial snapshot falls back too, instead of
|
|
269
|
+
// holding the socket for the ones that remain and silently never
|
|
270
|
+
// rebuilding the rest.
|
|
271
|
+
const missing = [...byWatchName.keys()].filter(
|
|
272
|
+
(name) => !ready.watches.some((s) => s.name === name)
|
|
273
|
+
);
|
|
274
|
+
for (const name of missing) {
|
|
275
|
+
if (unreadable.has(name)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
unreadable.add(name);
|
|
279
|
+
OutputHelper.warn(
|
|
280
|
+
`The watch monitor no longer holds ${
|
|
281
|
+
byWatchName.get(name)!.label
|
|
282
|
+
} — another checkout has probably re-registered it.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (abandonIfNothingLeftToWatch()) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const status of failedToRead) {
|
|
290
|
+
if (unreadable.has(status.name)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
unreadable.add(status.name);
|
|
294
|
+
OutputHelper.warn(
|
|
295
|
+
`The watch service cannot read ${
|
|
296
|
+
byWatchName.get(status.name)!.label
|
|
297
|
+
}` + (status.sourceError ? `: ${status.sourceError}` : '.')
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (abandonIfNothingLeftToWatch()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const status of ours) {
|
|
305
|
+
if (!unreadable.has(status.name)) {
|
|
306
|
+
await settleAsync(
|
|
307
|
+
byWatchName.get(status.name)!,
|
|
308
|
+
status.currentVersion
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
onNotifyAsync: async (notify: WatchStreamNotify) => {
|
|
315
|
+
const entry = byWatchName.get(notify.watchName);
|
|
316
|
+
if (!entry) {
|
|
317
|
+
OutputHelper.verbose(
|
|
318
|
+
`Ignoring a notification for "${notify.watchName}", which this ` +
|
|
319
|
+
'run does not watch.'
|
|
320
|
+
);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
await settleAsync(entry, notify.version);
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
onEvent: (message) => {
|
|
327
|
+
OutputHelper.verbose(
|
|
328
|
+
` [${message.event.type}] ${message.event.message}`
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// The service only logs this once it has failed several polls in a
|
|
332
|
+
// row, so it already means "not a blip". There is no matching
|
|
333
|
+
// recovered event, so this is one-way — but the cost of being wrong
|
|
334
|
+
// is polling something that could have been streamed, which still
|
|
335
|
+
// works.
|
|
336
|
+
const name = message.event.watchName;
|
|
337
|
+
if (
|
|
338
|
+
message.event.type !== 'poll-failed' ||
|
|
339
|
+
name == null ||
|
|
340
|
+
!byWatchName.has(name) ||
|
|
341
|
+
unreadable.has(name)
|
|
342
|
+
) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
unreadable.add(name);
|
|
346
|
+
OutputHelper.warn(
|
|
347
|
+
`The watch service cannot read ${byWatchName.get(name)!.label}: ${
|
|
348
|
+
message.event.message
|
|
349
|
+
}`
|
|
350
|
+
);
|
|
351
|
+
abandonIfNothingLeftToWatch();
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// The stream swallows a handler's throw so it cannot strand the watch
|
|
355
|
+
// or kill the process; unreported it would be invisible. Everything
|
|
356
|
+
// above already catches its own failures, so reaching here is a bug.
|
|
357
|
+
onHandlerError: (err) => {
|
|
358
|
+
OutputHelper.error(`Watch handler failed: ${err.message}`);
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
// A misconfiguration retries on a fixed reason forever, so the same
|
|
362
|
+
// sentence would be printed until someone gave up reading it. Said once
|
|
363
|
+
// per distinct cause, counted after that.
|
|
364
|
+
onReconnect: (attempt, reason) => {
|
|
365
|
+
if (reason === lastDropReason) {
|
|
366
|
+
OutputHelper.verbose(
|
|
367
|
+
`Watch stream still unavailable (attempt ${attempt}).`
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
lastDropReason = reason;
|
|
372
|
+
OutputHelper.warn(`Watch stream dropped: ${reason}`);
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (result.ending === 'monitor-gone') {
|
|
378
|
+
OutputHelper.warn(
|
|
379
|
+
'The watch monitor was released or its lease lapsed, so nothing is ' +
|
|
380
|
+
'being watched any more. Re-run with --watch to register again.'
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (result.ending === 'rejected') {
|
|
384
|
+
OutputHelper.error(
|
|
385
|
+
`Could not hold the watch stream: ${
|
|
386
|
+
result.reason ?? 'the service refused the handshake'
|
|
387
|
+
}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (!unobservable) {
|
|
391
|
+
OutputHelper.info(
|
|
392
|
+
`Stopped watching. ${redeploys} rebuild${redeploys === 1 ? '' : 's'}.`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
redeploys,
|
|
398
|
+
failures,
|
|
399
|
+
reconnects: result.reconnects,
|
|
400
|
+
monitorGone: result.ending === 'monitor-gone',
|
|
401
|
+
rejected: result.ending === 'rejected',
|
|
402
|
+
unobservable,
|
|
403
|
+
};
|
|
404
|
+
} finally {
|
|
405
|
+
if (options.signal) {
|
|
406
|
+
options.signal.removeEventListener('abort', onSigint);
|
|
407
|
+
} else {
|
|
408
|
+
process.off('SIGINT', onSigint);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|