@3minapi/cli 0.0.0-stage → 0.22.1
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/LICENSE +21 -0
- package/README.md +88 -2
- package/dist/bin.js +54 -0
- package/dist/cli.js +241 -0
- package/dist/commands/auth.js +573 -0
- package/dist/commands/common.js +48 -0
- package/dist/commands/dashboard.js +298 -0
- package/dist/commands/delete.js +167 -0
- package/dist/commands/doctor.js +326 -0
- package/dist/commands/guide.js +84 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/init.js +248 -0
- package/dist/commands/logs.js +154 -0
- package/dist/commands/new.js +109 -0
- package/dist/commands/paths.js +64 -0
- package/dist/commands/reload.js +32 -0
- package/dist/commands/results.js +445 -0
- package/dist/commands/start.js +443 -0
- package/dist/commands/stop.js +68 -0
- package/dist/commands/tasks.js +93 -0
- package/dist/commands/worker.js +178 -0
- package/dist/core/api.js +132 -0
- package/dist/core/config.js +188 -0
- package/dist/core/control-db.js +462 -0
- package/dist/core/credentials.js +75 -0
- package/dist/core/domain/constants.js +121 -0
- package/dist/core/domain/contract.js +102 -0
- package/dist/core/domain/errors.js +57 -0
- package/dist/core/domain/index.js +3 -0
- package/dist/core/domain/types.js +1 -0
- package/dist/core/error-report.js +103 -0
- package/dist/core/host/detach.js +49 -0
- package/dist/core/host/live-connection.js +270 -0
- package/dist/core/host/live-receiver.js +264 -0
- package/dist/core/host/module.js +1 -0
- package/dist/core/host/poller.js +113 -0
- package/dist/core/host/pull-step.js +54 -0
- package/dist/core/host/receiver.js +1 -0
- package/dist/core/host/reload.js +34 -0
- package/dist/core/host/runner-lock.js +245 -0
- package/dist/core/host/session.js +84 -0
- package/dist/core/host/usage.js +20 -0
- package/dist/core/log-read.js +59 -0
- package/dist/core/logger.js +206 -0
- package/dist/core/names.js +27 -0
- package/dist/core/network.js +10 -0
- package/dist/core/open-browser.js +26 -0
- package/dist/core/output.js +68 -0
- package/dist/core/paths.js +26 -0
- package/dist/core/project.js +85 -0
- package/dist/core/prompt.js +140 -0
- package/dist/core/redact.js +45 -0
- package/dist/core/secure-fs.js +50 -0
- package/dist/core/text-file.js +12 -0
- package/dist/core/text.js +27 -0
- package/dist/core/time.js +84 -0
- package/dist/core/update-notice.js +142 -0
- package/dist/core/version.js +16 -0
- package/dist/core/warnings.js +10 -0
- package/dist/help.js +144 -0
- package/dist/modules/dashboard/assets.js +411 -0
- package/dist/modules/dashboard/data.js +565 -0
- package/dist/modules/dashboard/guard.js +75 -0
- package/dist/modules/dashboard/lock.js +93 -0
- package/dist/modules/dashboard/server.js +295 -0
- package/dist/modules/dashboard/views/html.js +114 -0
- package/dist/modules/dashboard/views/layout.js +59 -0
- package/dist/modules/dashboard/views/logs.js +122 -0
- package/dist/modules/dashboard/views/overview.js +316 -0
- package/dist/modules/dashboard/views/runs.js +486 -0
- package/dist/modules/dashboard/views/workers.js +157 -0
- package/dist/modules/workers/builtin/host-identity.js +59 -0
- package/dist/modules/workers/builtin/results-worker.js +37 -0
- package/dist/modules/workers/local-server.js +244 -0
- package/dist/modules/workers/module.js +98 -0
- package/dist/modules/workers/reload-result.js +16 -0
- package/dist/modules/workers/repeat-scheduler.js +165 -0
- package/dist/modules/workers/results/missing-paths.js +66 -0
- package/dist/modules/workers/results/query.js +175 -0
- package/dist/modules/workers/results/reader-input.js +134 -0
- package/dist/modules/workers/results/reader.js +430 -0
- package/dist/modules/workers/results/results-child.js +64 -0
- package/dist/modules/workers/results/shape.js +212 -0
- package/dist/modules/workers/results/store.js +336 -0
- package/dist/modules/workers/results/substitute.js +30 -0
- package/dist/modules/workers/runtime/env.js +43 -0
- package/dist/modules/workers/runtime/error-chain.js +109 -0
- package/dist/modules/workers/runtime/executor.js +289 -0
- package/dist/modules/workers/runtime/inspect.js +53 -0
- package/dist/modules/workers/runtime/protocol.js +1 -0
- package/dist/modules/workers/runtime/resolve.js +30 -0
- package/dist/modules/workers/runtime/schema.js +85 -0
- package/dist/modules/workers/runtime/thread.js +117 -0
- package/dist/modules/workers/start.js +265 -0
- package/dist/modules/workers/sync.js +241 -0
- package/dist/modules/workers/task-runner.js +892 -0
- package/dist/modules/workers/template.js +100 -0
- package/dist/modules/workers/worker-set.js +122 -0
- package/dist/modules/workers/workers.js +66 -0
- package/package.json +48 -5
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { LIVE_RETRY_MS, POLL_INTERVAL_MAX_MS, POLL_MIN_INTERVAL_MS, StopError, isTrustedLiveUrl } from "../domain/index.js";
|
|
2
|
+
import { stripControl } from "../text.js";
|
|
3
|
+
import { formatInterval, sleep } from "../time.js";
|
|
4
|
+
import { LiveConnection } from "./live-connection.js";
|
|
5
|
+
import { endReceive, pullStep } from "./pull-step.js";
|
|
6
|
+
const HOST_CHECK_DEDUPE_MS = 30_000;
|
|
7
|
+
export class LiveReceiver {
|
|
8
|
+
opts;
|
|
9
|
+
conn;
|
|
10
|
+
now;
|
|
11
|
+
minIntervalMs;
|
|
12
|
+
retryMs;
|
|
13
|
+
halted = new AbortController();
|
|
14
|
+
stopError = null;
|
|
15
|
+
lastQuota;
|
|
16
|
+
resyncState = 'idle';
|
|
17
|
+
napWake = null;
|
|
18
|
+
signalled = false;
|
|
19
|
+
signalSeq = 0;
|
|
20
|
+
more = false;
|
|
21
|
+
slotPending = false;
|
|
22
|
+
triggers = new Set();
|
|
23
|
+
notBefore = 0;
|
|
24
|
+
hostCheck = null;
|
|
25
|
+
drops = [];
|
|
26
|
+
url;
|
|
27
|
+
hostCheckedAt = Number.NEGATIVE_INFINITY;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.opts = opts;
|
|
30
|
+
this.now = opts.now ?? Date.now;
|
|
31
|
+
this.minIntervalMs = opts.minIntervalMs ?? POLL_MIN_INTERVAL_MS;
|
|
32
|
+
this.retryMs = opts.retryMs ?? LIVE_RETRY_MS;
|
|
33
|
+
this.lastQuota = opts.quota ?? null;
|
|
34
|
+
this.url = opts.url;
|
|
35
|
+
this.conn = new LiveConnection({
|
|
36
|
+
url: opts.url,
|
|
37
|
+
token: opts.token,
|
|
38
|
+
logger: opts.logger,
|
|
39
|
+
onOpen: () => this.pullSoon('catch_up'),
|
|
40
|
+
onSignal: (topic) => this.onSignal(topic),
|
|
41
|
+
onCheckHost: () => this.checkHost('close'),
|
|
42
|
+
onRefreshUrl: () => this.checkHost('refresh'),
|
|
43
|
+
onState: opts.onState,
|
|
44
|
+
timing: opts.timing,
|
|
45
|
+
now: opts.now,
|
|
46
|
+
random: opts.random
|
|
47
|
+
});
|
|
48
|
+
opts.module.onSlotFree(() => {
|
|
49
|
+
if (!this.slotPending)
|
|
50
|
+
return;
|
|
51
|
+
this.slotPending = false;
|
|
52
|
+
this.more = true;
|
|
53
|
+
this.triggers.add('slot_free');
|
|
54
|
+
this.wake();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
get liveState() {
|
|
58
|
+
return this.conn.liveState;
|
|
59
|
+
}
|
|
60
|
+
stop(err) {
|
|
61
|
+
this.stopError ??= err;
|
|
62
|
+
this.halted.abort();
|
|
63
|
+
this.conn.close();
|
|
64
|
+
this.wake();
|
|
65
|
+
}
|
|
66
|
+
requestResync() {
|
|
67
|
+
if (this.resyncState !== 'idle')
|
|
68
|
+
return;
|
|
69
|
+
this.resyncState = 'pending';
|
|
70
|
+
this.wake();
|
|
71
|
+
}
|
|
72
|
+
requestDrop(names) {
|
|
73
|
+
this.drops.push(...names);
|
|
74
|
+
this.wake();
|
|
75
|
+
}
|
|
76
|
+
get resyncIdle() {
|
|
77
|
+
return this.resyncState === 'idle';
|
|
78
|
+
}
|
|
79
|
+
setUrl(url) {
|
|
80
|
+
if (url === this.url)
|
|
81
|
+
return;
|
|
82
|
+
if (!isTrustedLiveUrl(url, this.opts.apiUrl)) {
|
|
83
|
+
this.opts.logger.warn('ignored a live url that does not match the API host');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.url = url;
|
|
87
|
+
this.conn.setUrl(url);
|
|
88
|
+
this.opts.onLiveUrl?.(url);
|
|
89
|
+
}
|
|
90
|
+
async run() {
|
|
91
|
+
const { api, logger, module, signal } = this.opts;
|
|
92
|
+
const sleepSignal = AbortSignal.any([signal, this.halted.signal]);
|
|
93
|
+
const flushTimer = setInterval(() => void module.flushPending(), this.retryMs);
|
|
94
|
+
flushTimer.unref();
|
|
95
|
+
let noActive = false;
|
|
96
|
+
this.conn.start();
|
|
97
|
+
try {
|
|
98
|
+
while (!signal.aborted && this.stopError === null) {
|
|
99
|
+
if (this.drops.length > 0) {
|
|
100
|
+
module.dropLocal(this.drops.splice(0));
|
|
101
|
+
if (module.activeCount === 0) {
|
|
102
|
+
noActive = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (this.resyncState === 'pending') {
|
|
107
|
+
this.resyncState = 'running';
|
|
108
|
+
try {
|
|
109
|
+
await module.resync();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (!(err instanceof StopError))
|
|
113
|
+
throw err;
|
|
114
|
+
this.stop(err);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.resyncState = 'idle';
|
|
119
|
+
}
|
|
120
|
+
if (module.activeCount === 0) {
|
|
121
|
+
noActive = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let wait = POLL_INTERVAL_MAX_MS;
|
|
127
|
+
if ((this.signalled || this.more) && this.conn.connected) {
|
|
128
|
+
const left = this.notBefore - this.now();
|
|
129
|
+
if (left > 0) {
|
|
130
|
+
wait = left;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const full = module.pullMax() === 0;
|
|
134
|
+
if (full && !this.signalled) {
|
|
135
|
+
this.more = false;
|
|
136
|
+
this.slotPending = true;
|
|
137
|
+
this.triggers.clear();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const seq = this.signalSeq;
|
|
141
|
+
const was = { signalled: this.signalled, more: this.more };
|
|
142
|
+
const trigger = [...this.triggers];
|
|
143
|
+
this.signalled = false;
|
|
144
|
+
this.more = false;
|
|
145
|
+
this.triggers.clear();
|
|
146
|
+
const out = await pullStep({
|
|
147
|
+
api,
|
|
148
|
+
logger,
|
|
149
|
+
module,
|
|
150
|
+
isStopped: () => this.stopError !== null,
|
|
151
|
+
onPull: this.opts.onPull,
|
|
152
|
+
onError: this.opts.onError,
|
|
153
|
+
retryHint: `in ${formatInterval(this.retryMs)}`,
|
|
154
|
+
quietTooSoon: true,
|
|
155
|
+
trigger
|
|
156
|
+
});
|
|
157
|
+
if (out.kind === 'skipped')
|
|
158
|
+
break;
|
|
159
|
+
if (out.kind === 'stopped') {
|
|
160
|
+
this.stop(out.err);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (out.kind === 'pulled') {
|
|
164
|
+
this.lastQuota = out.res.quota;
|
|
165
|
+
this.notBefore =
|
|
166
|
+
this.now() +
|
|
167
|
+
Math.min(Math.max(this.minIntervalMs, out.res.poll.min_interval_ms), POLL_INTERVAL_MAX_MS);
|
|
168
|
+
if (out.noActive) {
|
|
169
|
+
noActive = true;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (full || out.res.has_more) {
|
|
173
|
+
if (module.pullMax() > 0) {
|
|
174
|
+
this.more = true;
|
|
175
|
+
this.triggers.add(full ? 'slot_free' : 'has_more');
|
|
176
|
+
}
|
|
177
|
+
else
|
|
178
|
+
this.slotPending = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.signalled ||= was.signalled;
|
|
183
|
+
this.more ||= was.more;
|
|
184
|
+
for (const t of trigger)
|
|
185
|
+
this.triggers.add(t);
|
|
186
|
+
this.triggers.add('retry');
|
|
187
|
+
this.notBefore =
|
|
188
|
+
this.now() +
|
|
189
|
+
(out.kind === 'too_soon'
|
|
190
|
+
? Math.min(out.retryAfterMs, POLL_INTERVAL_MAX_MS)
|
|
191
|
+
: this.retryMs);
|
|
192
|
+
}
|
|
193
|
+
if (this.signalSeq !== seq)
|
|
194
|
+
this.signalled = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.napWake = new AbortController();
|
|
199
|
+
await sleep(wait, AbortSignal.any([sleepSignal, this.napWake.signal]));
|
|
200
|
+
this.napWake = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
clearInterval(flushTimer);
|
|
205
|
+
this.conn.close();
|
|
206
|
+
}
|
|
207
|
+
await this.hostCheck;
|
|
208
|
+
if (this.stopError === null)
|
|
209
|
+
this.resyncState = 'idle';
|
|
210
|
+
return endReceive(module, this.stopError, this.lastQuota, noActive);
|
|
211
|
+
}
|
|
212
|
+
onSignal(topic) {
|
|
213
|
+
if (topic === 'host') {
|
|
214
|
+
void this.checkHost('signal');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (this.opts.module.topics.includes(topic)) {
|
|
218
|
+
this.pullSoon('signal');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.opts.logger.debug('ignored an unknown live topic', {
|
|
222
|
+
topic: stripControl(topic).slice(0, 32)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
pullSoon(trigger) {
|
|
226
|
+
this.triggers.add(trigger);
|
|
227
|
+
this.signalled = true;
|
|
228
|
+
this.signalSeq++;
|
|
229
|
+
this.wake();
|
|
230
|
+
}
|
|
231
|
+
wake() {
|
|
232
|
+
this.napWake?.abort();
|
|
233
|
+
}
|
|
234
|
+
checkHost(why) {
|
|
235
|
+
if (this.hostCheck)
|
|
236
|
+
return this.hostCheck;
|
|
237
|
+
if (this.stopError !== null)
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
if (why === 'close' && this.now() - this.hostCheckedAt < HOST_CHECK_DEDUPE_MS) {
|
|
240
|
+
return Promise.resolve();
|
|
241
|
+
}
|
|
242
|
+
this.hostCheck = (async () => {
|
|
243
|
+
try {
|
|
244
|
+
const res = await this.opts.api.getHost();
|
|
245
|
+
this.hostCheckedAt = this.now();
|
|
246
|
+
this.lastQuota = res.quota;
|
|
247
|
+
if (res.live)
|
|
248
|
+
this.setUrl(res.live.url);
|
|
249
|
+
this.opts.logger.info('host checked', { why });
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
if (err instanceof StopError) {
|
|
253
|
+
this.stop(err);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.opts.logger.warn('host check failed', { why, message: err.message });
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
this.hostCheck = null;
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
return this.hostCheck;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { POLL_INTERVAL_MAX_MS, StopError } from "../domain/index.js";
|
|
2
|
+
import { sleep } from "../time.js";
|
|
3
|
+
import { endReceive, pullStep } from "./pull-step.js";
|
|
4
|
+
const INTERVAL_TRIGGER = ['interval'];
|
|
5
|
+
const RETRY_TRIGGER = ['retry'];
|
|
6
|
+
export class Poller {
|
|
7
|
+
opts;
|
|
8
|
+
stopError = null;
|
|
9
|
+
wake = new AbortController();
|
|
10
|
+
lastQuota = null;
|
|
11
|
+
resyncState = 'idle';
|
|
12
|
+
napWake = null;
|
|
13
|
+
drops = [];
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.opts = opts;
|
|
16
|
+
this.lastQuota = opts.quota ?? null;
|
|
17
|
+
}
|
|
18
|
+
stop(err) {
|
|
19
|
+
this.stopError ??= err;
|
|
20
|
+
this.wake.abort();
|
|
21
|
+
}
|
|
22
|
+
requestResync() {
|
|
23
|
+
if (this.resyncState !== 'idle')
|
|
24
|
+
return;
|
|
25
|
+
this.resyncState = 'pending';
|
|
26
|
+
this.napWake?.abort();
|
|
27
|
+
}
|
|
28
|
+
requestDrop(names) {
|
|
29
|
+
this.drops.push(...names);
|
|
30
|
+
this.napWake?.abort();
|
|
31
|
+
}
|
|
32
|
+
get resyncIdle() {
|
|
33
|
+
return this.resyncState === 'idle';
|
|
34
|
+
}
|
|
35
|
+
async run() {
|
|
36
|
+
const { api, logger, signal, module } = this.opts;
|
|
37
|
+
const sleepSignal = AbortSignal.any([signal, this.wake.signal]);
|
|
38
|
+
let noActive = false;
|
|
39
|
+
let retrying = false;
|
|
40
|
+
while (!signal.aborted && this.stopError === null) {
|
|
41
|
+
let wait = this.opts.intervalMs;
|
|
42
|
+
const out = await pullStep({
|
|
43
|
+
api,
|
|
44
|
+
logger,
|
|
45
|
+
module,
|
|
46
|
+
isStopped: () => this.stopError !== null,
|
|
47
|
+
onPull: this.opts.onPull,
|
|
48
|
+
onError: this.opts.onError,
|
|
49
|
+
retryHint: 'at the next interval',
|
|
50
|
+
trigger: retrying ? RETRY_TRIGGER : INTERVAL_TRIGGER
|
|
51
|
+
});
|
|
52
|
+
retrying = out.kind === 'too_soon' || out.kind === 'failed';
|
|
53
|
+
if (out.kind === 'skipped')
|
|
54
|
+
break;
|
|
55
|
+
if (out.kind === 'stopped') {
|
|
56
|
+
this.stop(out.err);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (out.kind === 'pulled') {
|
|
60
|
+
this.lastQuota = out.res.quota;
|
|
61
|
+
wait = Math.min(Math.max(this.opts.intervalMs, out.res.poll.min_interval_ms), POLL_INTERVAL_MAX_MS);
|
|
62
|
+
if (out.noActive) {
|
|
63
|
+
noActive = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (out.kind === 'too_soon') {
|
|
68
|
+
wait = Math.min(out.retryAfterMs, POLL_INTERVAL_MAX_MS);
|
|
69
|
+
}
|
|
70
|
+
if (await this.nap(wait, sleepSignal)) {
|
|
71
|
+
noActive = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (this.stopError === null)
|
|
76
|
+
this.resyncState = 'idle';
|
|
77
|
+
return endReceive(module, this.stopError, this.lastQuota, noActive);
|
|
78
|
+
}
|
|
79
|
+
async nap(wait, sleepSignal) {
|
|
80
|
+
const until = Date.now() + wait;
|
|
81
|
+
while (!this.opts.signal.aborted && this.stopError === null) {
|
|
82
|
+
if (this.drops.length > 0) {
|
|
83
|
+
this.opts.module.dropLocal(this.drops.splice(0));
|
|
84
|
+
if (this.opts.module.activeCount === 0)
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (this.resyncState === 'pending') {
|
|
88
|
+
this.resyncState = 'running';
|
|
89
|
+
try {
|
|
90
|
+
await this.opts.module.resync();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (!(err instanceof StopError))
|
|
94
|
+
throw err;
|
|
95
|
+
this.stop(err);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.resyncState = 'idle';
|
|
100
|
+
}
|
|
101
|
+
if (this.opts.module.activeCount === 0)
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const left = until - Date.now();
|
|
105
|
+
if (left <= 0)
|
|
106
|
+
return false;
|
|
107
|
+
this.napWake = new AbortController();
|
|
108
|
+
await sleep(left, AbortSignal.any([sleepSignal, this.napWake.signal]));
|
|
109
|
+
this.napWake = null;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ApiError } from "../api.js";
|
|
2
|
+
import { StopError } from "../domain/index.js";
|
|
3
|
+
export async function pullStep(opts) {
|
|
4
|
+
const { api, logger, module } = opts;
|
|
5
|
+
try {
|
|
6
|
+
await module.beforePull();
|
|
7
|
+
if (opts.isStopped())
|
|
8
|
+
return { kind: 'skipped' };
|
|
9
|
+
const res = await api.pullTasks(module.pullMax());
|
|
10
|
+
logger.debug('pulled', {
|
|
11
|
+
trigger: opts.trigger.join('+'),
|
|
12
|
+
tasks: res.tasks.length,
|
|
13
|
+
cancelled: res.cancelled.length,
|
|
14
|
+
has_more: res.has_more,
|
|
15
|
+
remaining: res.quota.remaining
|
|
16
|
+
});
|
|
17
|
+
opts.onPull?.(res);
|
|
18
|
+
module.onPull(res);
|
|
19
|
+
return { kind: 'pulled', res, noActive: module.activeCount === 0 };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err instanceof StopError)
|
|
23
|
+
return { kind: 'stopped', err };
|
|
24
|
+
if (!(err instanceof ApiError))
|
|
25
|
+
throw err;
|
|
26
|
+
const tooSoon = err.code === 'poll_too_soon' && err.retryAfterMs !== null;
|
|
27
|
+
if (err.code === 'invalid_request') {
|
|
28
|
+
logger.error('the server rejected the pull request (a CLI bug)', {
|
|
29
|
+
trigger: opts.trigger.join('+'),
|
|
30
|
+
message: err.message
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else if (!tooSoon) {
|
|
34
|
+
logger.warn(`pull failed; trying again ${opts.retryHint}`, {
|
|
35
|
+
trigger: opts.trigger.join('+'),
|
|
36
|
+
status: err.status,
|
|
37
|
+
message: err.message
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!(tooSoon && opts.quietTooSoon))
|
|
41
|
+
opts.onError?.(err);
|
|
42
|
+
return tooSoon
|
|
43
|
+
? { kind: 'too_soon', retryAfterMs: err.retryAfterMs }
|
|
44
|
+
: { kind: 'failed', err };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function endReceive(module, stopError, lastQuota, noActive) {
|
|
48
|
+
if (stopError !== null) {
|
|
49
|
+
await module.halt(stopError.reason);
|
|
50
|
+
return new StopError(stopError.reason, stopError.message, stopError.resetAt ?? lastQuota?.reset_at);
|
|
51
|
+
}
|
|
52
|
+
await module.shutdown();
|
|
53
|
+
return noActive ? module.ended() : null;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CliError, ExitCode, RELOAD_WAIT_MS, StopError } from "../domain/index.js";
|
|
2
|
+
import { readReload, readRunner, requestReload } from "./runner-lock.js";
|
|
3
|
+
import { stopHint } from "./session.js";
|
|
4
|
+
import { sleep } from "../time.js";
|
|
5
|
+
const POLL_MS = 200;
|
|
6
|
+
export async function reloadRunner(ctx, info) {
|
|
7
|
+
if (!info.reload) {
|
|
8
|
+
throw new CliError(`the running start (pid ${info.pid}) is from an older 3minapi and cannot reload. Restart it: '3minapi stop' then '3minapi start'.`);
|
|
9
|
+
}
|
|
10
|
+
const requestedAt = requestReload(ctx.dirs.data, info);
|
|
11
|
+
const deadline = requestedAt + RELOAD_WAIT_MS;
|
|
12
|
+
for (;;) {
|
|
13
|
+
const record = readReload(ctx.dirs.data, info);
|
|
14
|
+
if (record?.state === 'done' && record.startedAt >= requestedAt && record.result) {
|
|
15
|
+
return record.result;
|
|
16
|
+
}
|
|
17
|
+
const runner = readRunner(ctx.dirs.data);
|
|
18
|
+
if (runner.state !== 'running' || runner.info.nonce !== info.nonce) {
|
|
19
|
+
throw new CliError("the running start ended before the reload finished. See '3minapi status' and '3minapi logs'.");
|
|
20
|
+
}
|
|
21
|
+
if (Date.now() >= deadline) {
|
|
22
|
+
throw new CliError(`no reload result after ${RELOAD_WAIT_MS / 1000}s — the start may still be loading worker files. See '3minapi logs'.`);
|
|
23
|
+
}
|
|
24
|
+
await sleep(POLL_MS, ctx.signal);
|
|
25
|
+
if (ctx.signal.aborted)
|
|
26
|
+
throw new CliError('cancelled — the running start may still reload.', ExitCode.Interrupted);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function failReload(result) {
|
|
30
|
+
if (result.stopped) {
|
|
31
|
+
throw new StopError(result.stopped, `${result.error}\nThe running start stopped all calls. ${stopHint(result.stopped)}`);
|
|
32
|
+
}
|
|
33
|
+
throw new CliError(`reload failed: ${result.error} The running start keeps its current workers.`);
|
|
34
|
+
}
|