@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.
Files changed (207) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/args/global-args.d.ts +13 -0
  3. package/dist/args/global-args.d.ts.map +1 -1
  4. package/dist/commands/batch-command/batch-deploy-command.d.ts +3 -0
  5. package/dist/commands/batch-command/batch-deploy-command.d.ts.map +1 -1
  6. package/dist/commands/batch-command/batch-deploy-command.js +90 -1
  7. package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
  8. package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
  9. package/dist/commands/batch-command/batch-test-command.js +25 -4
  10. package/dist/commands/batch-command/batch-test-command.js.map +1 -1
  11. package/dist/commands/deploy-command/deploy-init.js +1 -1
  12. package/dist/commands/deploy-command/deploy-init.js.map +1 -1
  13. package/dist/commands/deploy-command/index.d.ts +3 -0
  14. package/dist/commands/deploy-command/index.d.ts.map +1 -1
  15. package/dist/commands/deploy-command/index.js +297 -60
  16. package/dist/commands/deploy-command/index.js.map +1 -1
  17. package/dist/commands/deploy-command/select-target.js +1 -1
  18. package/dist/commands/deploy-command/select-target.js.map +1 -1
  19. package/dist/commands/deploy-command/version-command.d.ts.map +1 -1
  20. package/dist/commands/deploy-command/version-command.js +138 -12
  21. package/dist/commands/deploy-command/version-command.js.map +1 -1
  22. package/dist/commands/test-command/test-command.d.ts.map +1 -1
  23. package/dist/commands/test-command/test-command.js +23 -11
  24. package/dist/commands/test-command/test-command.js.map +1 -1
  25. package/dist/nevermore.js +17 -0
  26. package/dist/nevermore.js.map +1 -1
  27. package/dist/utils/batch/changed-packages-utils.d.ts +1 -1
  28. package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
  29. package/dist/utils/batch/changed-packages-utils.js +1 -1
  30. package/dist/utils/batch/changed-packages-utils.js.map +1 -1
  31. package/dist/utils/build/base-place-resolver-factory.d.ts +25 -0
  32. package/dist/utils/build/base-place-resolver-factory.d.ts.map +1 -0
  33. package/dist/utils/build/base-place-resolver-factory.js +40 -0
  34. package/dist/utils/build/base-place-resolver-factory.js.map +1 -0
  35. package/dist/utils/build/build.d.ts +13 -2
  36. package/dist/utils/build/build.d.ts.map +1 -1
  37. package/dist/utils/build/build.js.map +1 -1
  38. package/dist/utils/build/upload.d.ts +1 -1
  39. package/dist/utils/build/upload.d.ts.map +1 -1
  40. package/dist/utils/deploy/deploy-metadata.d.ts +1 -1
  41. package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
  42. package/dist/utils/job-context/base-job-context.d.ts +3 -1
  43. package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
  44. package/dist/utils/job-context/base-job-context.js +45 -6
  45. package/dist/utils/job-context/base-job-context.js.map +1 -1
  46. package/dist/utils/job-context/batch-script-job-context.d.ts +6 -0
  47. package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
  48. package/dist/utils/job-context/batch-script-job-context.js +13 -0
  49. package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
  50. package/dist/utils/job-context/cloud-job-context.d.ts +2 -1
  51. package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
  52. package/dist/utils/job-context/cloud-job-context.js +2 -2
  53. package/dist/utils/job-context/cloud-job-context.js.map +1 -1
  54. package/dist/utils/job-context/local-job-context.d.ts +2 -1
  55. package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
  56. package/dist/utils/job-context/local-job-context.js +2 -2
  57. package/dist/utils/job-context/local-job-context.js.map +1 -1
  58. package/dist/utils/open-cloud/open-cloud-client.d.ts +20 -2
  59. package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
  60. package/dist/utils/open-cloud/open-cloud-client.js +78 -21
  61. package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
  62. package/dist/utils/open-cloud/open-cloud-client.test.js +89 -12
  63. package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
  64. package/dist/utils/testing/parsers/batch-log-parser.d.ts +13 -5
  65. package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
  66. package/dist/utils/testing/parsers/batch-log-parser.js +114 -68
  67. package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
  68. package/dist/utils/testing/parsers/batch-log-parser.test.js +57 -5
  69. package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -1
  70. package/dist/utils/testing/parsers/traceback-parser.d.ts +31 -0
  71. package/dist/utils/testing/parsers/traceback-parser.d.ts.map +1 -0
  72. package/dist/utils/testing/parsers/traceback-parser.js +91 -0
  73. package/dist/utils/testing/parsers/traceback-parser.js.map +1 -0
  74. package/dist/utils/testing/parsers/traceback-parser.test.d.ts +2 -0
  75. package/dist/utils/testing/parsers/traceback-parser.test.d.ts.map +1 -0
  76. package/dist/utils/testing/parsers/traceback-parser.test.js +45 -0
  77. package/dist/utils/testing/parsers/traceback-parser.test.js.map +1 -0
  78. package/dist/utils/testing/runner/combined-project-generator.d.ts +1 -1
  79. package/dist/utils/testing/runner/combined-project-generator.d.ts.map +1 -1
  80. package/dist/utils/testing/runner/test-runner.d.ts +9 -1
  81. package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
  82. package/dist/utils/testing/runner/test-runner.js +28 -3
  83. package/dist/utils/testing/runner/test-runner.js.map +1 -1
  84. package/dist/utils/testing/runner/test-runner.test.d.ts +2 -0
  85. package/dist/utils/testing/runner/test-runner.test.d.ts.map +1 -0
  86. package/dist/utils/testing/runner/test-runner.test.js +18 -0
  87. package/dist/utils/testing/runner/test-runner.test.js.map +1 -0
  88. package/dist/utils/testing/test-log-parser.d.ts +32 -1
  89. package/dist/utils/testing/test-log-parser.d.ts.map +1 -1
  90. package/dist/utils/testing/test-log-parser.js +53 -9
  91. package/dist/utils/testing/test-log-parser.js.map +1 -1
  92. package/dist/utils/testing/test-log-parser.test.d.ts +2 -0
  93. package/dist/utils/testing/test-log-parser.test.d.ts.map +1 -0
  94. package/dist/utils/testing/test-log-parser.test.js +56 -0
  95. package/dist/utils/testing/test-log-parser.test.js.map +1 -0
  96. package/dist/utils/watch/github-context.d.ts +50 -0
  97. package/dist/utils/watch/github-context.d.ts.map +1 -0
  98. package/dist/utils/watch/github-context.js +125 -0
  99. package/dist/utils/watch/github-context.js.map +1 -0
  100. package/dist/utils/watch/github-context.test.d.ts +2 -0
  101. package/dist/utils/watch/github-context.test.d.ts.map +1 -0
  102. package/dist/utils/watch/github-context.test.js +112 -0
  103. package/dist/utils/watch/github-context.test.js.map +1 -0
  104. package/dist/utils/watch/http-watch-registry.d.ts +40 -0
  105. package/dist/utils/watch/http-watch-registry.d.ts.map +1 -0
  106. package/dist/utils/watch/http-watch-registry.js +262 -0
  107. package/dist/utils/watch/http-watch-registry.js.map +1 -0
  108. package/dist/utils/watch/http-watch-registry.test.d.ts +2 -0
  109. package/dist/utils/watch/http-watch-registry.test.d.ts.map +1 -0
  110. package/dist/utils/watch/http-watch-registry.test.js +211 -0
  111. package/dist/utils/watch/http-watch-registry.test.js.map +1 -0
  112. package/dist/utils/watch/local-watch-loop.d.ts +50 -0
  113. package/dist/utils/watch/local-watch-loop.d.ts.map +1 -0
  114. package/dist/utils/watch/local-watch-loop.js +103 -0
  115. package/dist/utils/watch/local-watch-loop.js.map +1 -0
  116. package/dist/utils/watch/local-watch-loop.test.d.ts +2 -0
  117. package/dist/utils/watch/local-watch-loop.test.d.ts.map +1 -0
  118. package/dist/utils/watch/local-watch-loop.test.js +170 -0
  119. package/dist/utils/watch/local-watch-loop.test.js.map +1 -0
  120. package/dist/utils/watch/register-notify-watches.d.ts +41 -0
  121. package/dist/utils/watch/register-notify-watches.d.ts.map +1 -0
  122. package/dist/utils/watch/register-notify-watches.js +159 -0
  123. package/dist/utils/watch/register-notify-watches.js.map +1 -0
  124. package/dist/utils/watch/register-notify-watches.test.d.ts +2 -0
  125. package/dist/utils/watch/register-notify-watches.test.d.ts.map +1 -0
  126. package/dist/utils/watch/register-notify-watches.test.js +225 -0
  127. package/dist/utils/watch/register-notify-watches.test.js.map +1 -0
  128. package/dist/utils/watch/register-watches.d.ts +68 -0
  129. package/dist/utils/watch/register-watches.d.ts.map +1 -0
  130. package/dist/utils/watch/register-watches.js +221 -0
  131. package/dist/utils/watch/register-watches.js.map +1 -0
  132. package/dist/utils/watch/register-watches.test.d.ts +2 -0
  133. package/dist/utils/watch/register-watches.test.d.ts.map +1 -0
  134. package/dist/utils/watch/register-watches.test.js +576 -0
  135. package/dist/utils/watch/register-watches.test.js.map +1 -0
  136. package/dist/utils/watch/stream-watch-loop.d.ts +54 -0
  137. package/dist/utils/watch/stream-watch-loop.d.ts.map +1 -0
  138. package/dist/utils/watch/stream-watch-loop.js +282 -0
  139. package/dist/utils/watch/stream-watch-loop.js.map +1 -0
  140. package/dist/utils/watch/stream-watch-loop.test.d.ts +2 -0
  141. package/dist/utils/watch/stream-watch-loop.test.d.ts.map +1 -0
  142. package/dist/utils/watch/stream-watch-loop.test.js +392 -0
  143. package/dist/utils/watch/stream-watch-loop.test.js.map +1 -0
  144. package/dist/utils/watch/websocket-watch-stream.d.ts +25 -0
  145. package/dist/utils/watch/websocket-watch-stream.d.ts.map +1 -0
  146. package/dist/utils/watch/websocket-watch-stream.js +306 -0
  147. package/dist/utils/watch/websocket-watch-stream.js.map +1 -0
  148. package/dist/utils/watch/websocket-watch-stream.test.d.ts +2 -0
  149. package/dist/utils/watch/websocket-watch-stream.test.d.ts.map +1 -0
  150. package/dist/utils/watch/websocket-watch-stream.test.js +475 -0
  151. package/dist/utils/watch/websocket-watch-stream.test.js.map +1 -0
  152. package/package.json +7 -4
  153. package/src/args/global-args.ts +13 -0
  154. package/src/commands/batch-command/batch-deploy-command.ts +105 -1
  155. package/src/commands/batch-command/batch-test-command.ts +86 -60
  156. package/src/commands/deploy-command/deploy-init.ts +1 -1
  157. package/src/commands/deploy-command/index.ts +327 -32
  158. package/src/commands/deploy-command/select-target.ts +1 -1
  159. package/src/commands/deploy-command/version-command.ts +215 -21
  160. package/src/commands/test-command/test-command.ts +31 -22
  161. package/src/nevermore.ts +19 -0
  162. package/src/utils/batch/changed-packages-utils.ts +1 -1
  163. package/src/utils/build/base-place-resolver-factory.ts +49 -0
  164. package/src/utils/build/build.ts +13 -2
  165. package/src/utils/build/upload.ts +1 -1
  166. package/src/utils/deploy/deploy-metadata.ts +1 -1
  167. package/src/utils/job-context/base-job-context.ts +68 -6
  168. package/src/utils/job-context/batch-script-job-context.ts +18 -0
  169. package/src/utils/job-context/cloud-job-context.ts +7 -2
  170. package/src/utils/job-context/local-job-context.ts +7 -2
  171. package/src/utils/open-cloud/open-cloud-client.test.ts +133 -14
  172. package/src/utils/open-cloud/open-cloud-client.ts +131 -35
  173. package/src/utils/testing/parsers/batch-log-parser.test.ts +70 -5
  174. package/src/utils/testing/parsers/batch-log-parser.ts +141 -80
  175. package/src/utils/testing/parsers/traceback-parser.test.ts +64 -0
  176. package/src/utils/testing/parsers/traceback-parser.ts +122 -0
  177. package/src/utils/testing/runner/combined-project-generator.ts +1 -1
  178. package/src/utils/testing/runner/test-runner.test.ts +29 -0
  179. package/src/utils/testing/runner/test-runner.ts +36 -4
  180. package/src/utils/testing/test-log-parser.test.ts +70 -0
  181. package/src/utils/testing/test-log-parser.ts +80 -10
  182. package/src/utils/watch/github-context.test.ts +154 -0
  183. package/src/utils/watch/github-context.ts +184 -0
  184. package/src/utils/watch/http-watch-registry.test.ts +330 -0
  185. package/src/utils/watch/http-watch-registry.ts +341 -0
  186. package/src/utils/watch/local-watch-loop.test.ts +197 -0
  187. package/src/utils/watch/local-watch-loop.ts +190 -0
  188. package/src/utils/watch/register-notify-watches.test.ts +282 -0
  189. package/src/utils/watch/register-notify-watches.ts +226 -0
  190. package/src/utils/watch/register-watches.test.ts +696 -0
  191. package/src/utils/watch/register-watches.ts +369 -0
  192. package/src/utils/watch/stream-watch-loop.test.ts +625 -0
  193. package/src/utils/watch/stream-watch-loop.ts +411 -0
  194. package/src/utils/watch/websocket-watch-stream.test.ts +581 -0
  195. package/src/utils/watch/websocket-watch-stream.ts +403 -0
  196. package/tsconfig.json +19 -6
  197. package/tsconfig.tsbuildinfo +1 -1
  198. package/dist/utils/build/deploy-config.d.ts +0 -71
  199. package/dist/utils/build/deploy-config.d.ts.map +0 -1
  200. package/dist/utils/build/deploy-config.js +0 -164
  201. package/dist/utils/build/deploy-config.js.map +0 -1
  202. package/dist/utils/build/deploy-config.test.d.ts +0 -2
  203. package/dist/utils/build/deploy-config.test.d.ts.map +0 -1
  204. package/dist/utils/build/deploy-config.test.js +0 -126
  205. package/dist/utils/build/deploy-config.test.js.map +0 -1
  206. package/src/utils/build/deploy-config.test.ts +0 -149
  207. 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
+ }