@lark-apaas/miaoda-cli 0.1.33-alpha.59a1bf2 → 0.1.33-alpha.dc7b6a0
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.
|
@@ -11,6 +11,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
11
11
|
const index_1 = require("../../../config/migrate-configs/index");
|
|
12
12
|
const index_2 = require("../../../services/app/init/index");
|
|
13
13
|
const migrate_rule_1 = require("../../../utils/migrate-rule");
|
|
14
|
+
const file_ops_1 = require("../../../utils/file-ops");
|
|
14
15
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
15
16
|
const error_1 = require("../../../utils/error");
|
|
16
17
|
const output_1 = require("../../../utils/output");
|
|
@@ -103,24 +104,31 @@ async function handleAppMigrate(opts) {
|
|
|
103
104
|
// 沙箱环境下先停掉 dev server(预览)—— 保活由 code-server 管,`pkill dev.js` 会被立刻
|
|
104
105
|
// 重新拉起,拿不到"停住"的状态。dev server 活着时 vite 持续往 node_modules/.vite/deps 写
|
|
105
106
|
// 文件,下面 rm node_modules 走到顶层 rmdir 时目录又非空,直接 ENOTEMPTY 失败(历史故障)。
|
|
107
|
+
// stop 返回成功即代表 dev 进程已确认退出(服务端等到 exit 事件才返回),不需要再 pkill。
|
|
106
108
|
// - 只在沙箱内做(本地用户自己管 dev 进程,不该被 CLI 动)
|
|
107
109
|
// - 软失败:接口不存在(老版本 code-server 返 404)/ 连不上都不阻断,降级回老路径
|
|
108
110
|
// (带重试的 rm + 结尾 pkill 让保活拉起)
|
|
109
|
-
// - ⚠️
|
|
111
|
+
// - ⚠️ 恢复条件用 mayBeSuspended 而不是 ok:服务端在抛错前就置了暂停位,stop 失败
|
|
112
|
+
// (500 / 超时)时暂停位可能已生效,不恢复就等于永久关掉保活。只有 404 能确定无副作用。
|
|
110
113
|
let previewStopped = false;
|
|
114
|
+
let previewMayBeSuspended = false;
|
|
111
115
|
let previewStopError;
|
|
112
116
|
if ((0, env_1.isSandboxEnv)()) {
|
|
113
117
|
(0, logger_1.log)('migrate', '停止 dev server(预览)以便安全重装依赖...');
|
|
114
118
|
const stopped = await (0, preview_control_1.stopPreview)({ logPrefix: 'migrate' });
|
|
115
119
|
previewStopped = stopped.ok;
|
|
120
|
+
previewMayBeSuspended = stopped.mayBeSuspended;
|
|
116
121
|
if (!stopped.ok) {
|
|
117
122
|
previewStopError = stopped.error;
|
|
118
|
-
|
|
123
|
+
const tail = previewMayBeSuspended ? '结束时仍会尝试恢复' : '降级为不停 dev 直接重装';
|
|
124
|
+
(0, logger_1.log)('migrate', `⚠ 停止预览失败 (continuing, ${tail}): ${stopped.error ?? ''}`);
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
const followLatest = config.followLatestPackages ?? [];
|
|
122
128
|
let installError;
|
|
123
129
|
let nodeModulesCleanError;
|
|
130
|
+
let nodeModulesPurgeStrategy;
|
|
131
|
+
let nodeModulesTrashLeftover;
|
|
124
132
|
let previewRestarted = false;
|
|
125
133
|
let previewRestartError;
|
|
126
134
|
try {
|
|
@@ -128,24 +136,24 @@ async function handleAppMigrate(opts) {
|
|
|
128
136
|
// 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
|
|
129
137
|
// 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
|
|
130
138
|
// (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
|
|
131
|
-
// -
|
|
132
|
-
//
|
|
133
|
-
//
|
|
139
|
+
// - 用 purgeDirectory(rename 主档)而不是 fs.rmSync:dev server 没停成功时(老
|
|
140
|
+
// code-server 没有 preview/stop 路由)vite 还在往 node_modules/.vite/deps 写,rm 走到
|
|
141
|
+
// 顶层 rmdir 必然 ENOTEMPTY,而 `maxRetries` 不 re-walk、救不了(沙箱实测数据见
|
|
142
|
+
// purgeDirectory 注释)。同分区 rename 是原子的,一次就把旧树摘掉。
|
|
143
|
+
// - 软失败:摘不掉也继续跑 npm install 让 npm 自己 reconcile —— 半迁移态
|
|
134
144
|
// (package-lock 已删 + 新 deps 没装)比"清理不彻底"糟得多。
|
|
135
145
|
const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
|
|
136
146
|
if (node_fs_1.default.existsSync(nodeModulesPath)) {
|
|
137
147
|
(0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
148
|
+
const purged = (0, file_ops_1.purgeDirectory)(nodeModulesPath);
|
|
149
|
+
nodeModulesPurgeStrategy = purged.strategy;
|
|
150
|
+
nodeModulesTrashLeftover = purged.trashLeftover;
|
|
151
|
+
if (!purged.purged) {
|
|
152
|
+
nodeModulesCleanError = purged.error;
|
|
153
|
+
(0, logger_1.log)('migrate', `⚠ 清理 node_modules 失败 (continuing): ${nodeModulesCleanError ?? ''}`);
|
|
145
154
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(0, logger_1.log)('migrate', `⚠ 清理 node_modules 失败 (continuing): ${nodeModulesCleanError}`);
|
|
155
|
+
else if (purged.trashLeftover !== undefined) {
|
|
156
|
+
(0, logger_1.log)('migrate', `⚠ node_modules 已移走但残留目录未删净: ${purged.trashLeftover}(不影响 install,需手动清)`);
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
// 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
|
|
@@ -200,12 +208,14 @@ async function handleAppMigrate(opts) {
|
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
finally {
|
|
203
|
-
// 恢复 dev server ——
|
|
211
|
+
// 恢复 dev server —— 只要暂停位可能已生效就必须调,且必须在 finally 里:
|
|
204
212
|
// rule 应用 / install / 清 cache 任何一步抛异常都不能把保活留在关闭状态,否则用户沙箱
|
|
205
213
|
// 的预览再也起不来(比迁移失败严重得多)。
|
|
206
214
|
// 无条件恢复(不看 installError):dev 进程已经被停了,留着不起来只会更糟;新 scripts/dev.js
|
|
207
215
|
// + 新依赖也正是靠这次 start 生效。
|
|
208
|
-
|
|
216
|
+
// 用 previewMayBeSuspended 而非 previewStopped:stop 超时/500 时服务端暂停位已经置上了。
|
|
217
|
+
// 多余的 start 无害(服务端未暂停且进程在跑时 no-op)。
|
|
218
|
+
if (previewMayBeSuspended) {
|
|
209
219
|
(0, logger_1.log)('migrate', '恢复 dev server(预览)...');
|
|
210
220
|
const started = await (0, preview_control_1.startPreview)({ logPrefix: 'migrate' });
|
|
211
221
|
previewRestarted = started.ok;
|
|
@@ -215,15 +225,17 @@ async function handleAppMigrate(opts) {
|
|
|
215
225
|
}
|
|
216
226
|
}
|
|
217
227
|
}
|
|
218
|
-
// 降级路径:preview
|
|
219
|
-
//
|
|
228
|
+
// 降级路径:preview 接口不存在(老版本 code-server 返 404,确定没进暂停态),dev 进程还是
|
|
229
|
+
// 老的,按历史行为 pkill 掉 dev.js 主进程,靠平台 supervisor 自动重新 exec dev.sh → dev.js,
|
|
220
230
|
// 让新 deps + 新 scripts 生效。
|
|
221
231
|
// - 只在沙箱内做(isSandboxEnv;本地环境用户进程混杂,主动 pkill 风险大)
|
|
232
|
+
// - previewMayBeSuspended 时不做 —— 新 code-server 上暂停位管着重建,pkill 无意义;
|
|
233
|
+
// 进程该由上面的 startPreview 拉起
|
|
222
234
|
// - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
|
|
223
235
|
// 留着旧进程让用户先处理 installError
|
|
224
236
|
// - 软失败:pkill 无匹配进程退出 1,catch 吞掉
|
|
225
237
|
let devRestarted = previewRestarted;
|
|
226
|
-
if (!
|
|
238
|
+
if (!previewMayBeSuspended && installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
227
239
|
(0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
|
|
228
240
|
try {
|
|
229
241
|
(0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
|
|
@@ -248,27 +260,34 @@ async function handleAppMigrate(opts) {
|
|
|
248
260
|
followLatestPackages: followLatest,
|
|
249
261
|
installError,
|
|
250
262
|
nodeModulesCleanError,
|
|
263
|
+
nodeModulesPurgeStrategy,
|
|
264
|
+
nodeModulesTrashLeftover,
|
|
251
265
|
devRestarted,
|
|
252
266
|
previewStopped,
|
|
253
267
|
previewStopError,
|
|
268
|
+
previewMayBeSuspended,
|
|
254
269
|
previewRestarted,
|
|
255
270
|
previewRestartError,
|
|
256
|
-
nextActions:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
nextActions: [
|
|
272
|
+
'git status / git diff 评估改动并 commit',
|
|
273
|
+
...((0, env_1.isSandboxEnv)()
|
|
274
|
+
? []
|
|
275
|
+
: [
|
|
276
|
+
'重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev(killOrphansByPort 会自动踢掉旧进程)',
|
|
277
|
+
]),
|
|
278
|
+
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
279
|
+
// 预览恢复失败时补一条人工兜底动作 —— 暂停位还挂着,预览不会自己起来。
|
|
280
|
+
// 兜底自愈:暂停位只存 code-server 内存,重启 code-server 也能恢复。
|
|
281
|
+
...(previewRestartError === undefined
|
|
282
|
+
? []
|
|
283
|
+
: [
|
|
284
|
+
`⚠ dev server(预览)未恢复成功,需手动 curl -X POST ${(0, preview_control_1.resolveCodeServerBaseUrl)()}/api/v1/preview/start 恢复保活(暂停状态只存内存,重启 code-server 亦可自愈)`,
|
|
285
|
+
]),
|
|
286
|
+
// node_modules 已 rename 摘掉但 trash 没删净:不影响 install,但会出现在 git status 里
|
|
287
|
+
...(nodeModulesTrashLeftover === undefined
|
|
288
|
+
? []
|
|
289
|
+
: [`⚠ 残留目录未清: rm -rf ${nodeModulesTrashLeftover}`]),
|
|
290
|
+
],
|
|
272
291
|
},
|
|
273
292
|
});
|
|
274
293
|
}
|
package/dist/utils/file-ops.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.removeLineFromContent = removeLineFromContent;
|
|
|
8
8
|
exports.unlockDirForWrite = unlockDirForWrite;
|
|
9
9
|
exports.lockDirReadonly = lockDirReadonly;
|
|
10
10
|
exports.forceRemove = forceRemove;
|
|
11
|
+
exports.purgeDirectory = purgeDirectory;
|
|
11
12
|
const node_child_process_1 = require("node:child_process");
|
|
12
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -124,3 +125,85 @@ function forceRemove(target) {
|
|
|
124
125
|
}
|
|
125
126
|
node_fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
126
127
|
}
|
|
128
|
+
/** {@link purgeDirectory} 移走目录时用的临时名前缀(相对父目录)。 */
|
|
129
|
+
const PURGE_TRASH_PREFIX = '.miaoda-purge-';
|
|
130
|
+
/**
|
|
131
|
+
* 删除一个**可能正在被其它进程写入**的目录(典型场景:dev server 活着时清 node_modules)。
|
|
132
|
+
*
|
|
133
|
+
* 主档是 `rename` 而不是 `rm`:同分区 rename 是原子的,不受目录内容变化影响,一次就把整棵
|
|
134
|
+
* 旧树从原路径摘掉;随后再删被移走的 trash(此时原路径已经腾空,删失败也不阻断调用方)。
|
|
135
|
+
*
|
|
136
|
+
* ⚠️ 不要改回 `fs.rmSync(..., { maxRetries })`。在沙箱内实测(Node v22)过四种组合:
|
|
137
|
+
*
|
|
138
|
+
* ```text
|
|
139
|
+
* 持续写入 + maxRetries:0 → ENOTEMPTY(241ms)
|
|
140
|
+
* 持续写入 + maxRetries:5/300ms → ENOTEMPTY(4836ms,白等 5 轮)
|
|
141
|
+
* 0.5s 短写入 + maxRetries:5/300ms → ENOTEMPTY(4747ms) ← 关键反例
|
|
142
|
+
* 0.5s 短写入 + 外层重试整个 rmSync → 成功(717ms)
|
|
143
|
+
* 持续写入 + shell rm -rf → 失败
|
|
144
|
+
* 持续写入 + rename → 成功(122ms)
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* 原因:`maxRetries` 只重试失败的那一次 `rmdir`,**不会重新 walk 目录**。删除过程中新写进来的
|
|
148
|
+
* 文件永远不会被 unlink,于是 rmdir 一直 ENOTEMPTY,重试纯属白等。只有重新走一遍(外层重试)
|
|
149
|
+
* 或干脆 rename 摘掉才有用。
|
|
150
|
+
*
|
|
151
|
+
* rename 失败(跨设备 EXDEV、父目录不可写 EPERM 等)时退回“外层重试整个 rmSync”。
|
|
152
|
+
*
|
|
153
|
+
* 注意:rename 之后原路径可能被仍在运行的写入方立刻重建(vite 会 `mkdir -p` 回
|
|
154
|
+
* `node_modules/.vite/deps`)。这不算失败 —— 旧依赖树已经不在原路径了,后续 `npm install`
|
|
155
|
+
* 能在干净状态上物化新依赖集合。
|
|
156
|
+
*/
|
|
157
|
+
function purgeDirectory(target, opts = {}) {
|
|
158
|
+
if (!node_fs_1.default.existsSync(target))
|
|
159
|
+
return { purged: true, strategy: 'absent' };
|
|
160
|
+
const parent = node_path_1.default.dirname(target);
|
|
161
|
+
const trash = node_path_1.default.join(parent, `${PURGE_TRASH_PREFIX}${node_path_1.default.basename(target)}-${String(process.pid)}`);
|
|
162
|
+
try {
|
|
163
|
+
node_fs_1.default.renameSync(target, trash);
|
|
164
|
+
}
|
|
165
|
+
catch (renameErr) {
|
|
166
|
+
(0, logger_1.debug)(`file-ops: rename ${target} failed: ${renameErr.message}`);
|
|
167
|
+
return rmSyncWithOuterRetry(target, opts);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
node_fs_1.default.rmSync(trash, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
catch (rmErr) {
|
|
173
|
+
(0, logger_1.debug)(`file-ops: rm trash ${trash} failed: ${rmErr.message}`);
|
|
174
|
+
return { purged: true, strategy: 'rename', trashLeftover: trash };
|
|
175
|
+
}
|
|
176
|
+
return { purged: true, strategy: 'rename' };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 重试整个 `fs.rmSync`(每次重新 walk 目录),而不是依赖 `maxRetries`(它不 re-walk,见
|
|
180
|
+
* {@link purgeDirectory} 的实测数据)。
|
|
181
|
+
*/
|
|
182
|
+
function rmSyncWithOuterRetry(target, opts) {
|
|
183
|
+
const attempts = opts.attempts ?? 4;
|
|
184
|
+
const delayMs = opts.retryDelayMs ?? 300;
|
|
185
|
+
let lastErr;
|
|
186
|
+
for (let i = 1; i <= attempts; i++) {
|
|
187
|
+
try {
|
|
188
|
+
node_fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
189
|
+
return { purged: true, strategy: 'rmSync' };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
lastErr = err;
|
|
193
|
+
if (i < attempts)
|
|
194
|
+
sleepSync(delayMs);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
purged: false,
|
|
199
|
+
strategy: 'rmSync',
|
|
200
|
+
error: lastErr instanceof Error ? lastErr.message : String(lastErr),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** 同步等待。这条链路整体是同步的(migrate 的清理阶段),不引入 async 传染。 */
|
|
204
|
+
function sleepSync(ms) {
|
|
205
|
+
const end = Date.now() + ms;
|
|
206
|
+
while (Date.now() < end) {
|
|
207
|
+
// busy wait —— 只在 rename 失败的兜底路径上跑,最多 3 次 300ms
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -20,15 +20,31 @@ const logger_1 = require("../utils/logger");
|
|
|
20
20
|
* vite 会持续往 `node_modules/.vite/deps` 写文件,`fs.rmSync` 自底向上删到顶层 `rmdir` 时目录
|
|
21
21
|
* 又变非空,直接 `ENOTEMPTY` 失败。先 stop 再删才是干净的做法。
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* 服务端语义(ide-vscode `src/vs/platform/force/node/preview/previewService.ts`):
|
|
24
|
+
* - stop 先置暂停位(`_suspended`),再等在途创建落地、shutdown 并等进程真正退出。
|
|
25
|
+
* **返回成功即代表 dev 进程已死,调用方不需要自己 pkill。**
|
|
26
|
+
* - 等退出有 10s 超时,超时按失败处理(HTTP 500)。所以本地超时必须 > 10s,否则会抢在
|
|
27
|
+
* 服务端给出结论前 abort(见 STOP_TIMEOUT_MS)。
|
|
28
|
+
* - ⚠️ 暂停位在 `_doStop()` 第一行就置上,**在抛错之前**。所以 stop 失败 ≠ 没进暂停态:
|
|
29
|
+
* 除了 404(路由不存在,必然无副作用)以外的任何失败,都必须当成"可能已暂停"去恢复,
|
|
30
|
+
* 否则保活被永久关掉。见 {@link PreviewControlResult.mayBeSuspended}。
|
|
31
|
+
* - 暂停位只存内存,不持久化:code-server 重启会丢状态并重新拉起预览(兜底自愈路径)。
|
|
32
|
+
* - start 在已运行时是 no-op;设计态沙箱(miaoda_design)本就不启动预览,start 也是 no-op。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ 调用契约:**只要 stop 可能已经生效就必须保证 start 被调到**(调用方放 `finally`,并按
|
|
35
|
+
* `mayBeSuspended` 判断)。漏掉 start 会让用户沙箱的预览再也起不来 —— 比迁移失败严重得多。
|
|
25
36
|
*
|
|
26
37
|
* 全部 API 都是软失败语义:不抛异常,失败信息放在返回值的 `error` 上,由调用方决定降级路径
|
|
27
38
|
* (老版本 code-server 没有这两个接口,会返回 404)。
|
|
28
39
|
*/
|
|
29
40
|
const DEFAULT_CODE_SERVER_BASE_URL = 'http://localhost:8080/code-server';
|
|
30
41
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
31
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* stop 的本地超时。必须大于服务端等进程退出的 10s(STOP_TIMEOUT)—— 否则我们会先 abort,
|
|
44
|
+
* 拿到一个"网络超时",而服务端那边照样置着暂停位并继续走完流程。
|
|
45
|
+
*/
|
|
46
|
+
const STOP_TIMEOUT_MS = 20_000;
|
|
47
|
+
/** start 是恢复动作,值得多试几次;stop 失败由调用方分类处理,不在这里重试 */
|
|
32
48
|
const START_ATTEMPTS = 3;
|
|
33
49
|
const START_RETRY_DELAY_MS = 500;
|
|
34
50
|
/** code-server 本地基址,去掉尾部斜杠。`MIAODA_CODE_SERVER_BASE_URL` > 默认 localhost:8080。 */
|
|
@@ -37,18 +53,32 @@ function resolveCodeServerBaseUrl() {
|
|
|
37
53
|
const base = fromEnv !== undefined && fromEnv !== '' ? fromEnv : DEFAULT_CODE_SERVER_BASE_URL;
|
|
38
54
|
return base.replace(/\/+$/, '');
|
|
39
55
|
}
|
|
40
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* 停止预览(dev 进程 + 保活)。返回 `ok` 时进程已确认退出,无需再 pkill。
|
|
58
|
+
*
|
|
59
|
+
* 软失败:不抛异常。失败时务必看 `mayBeSuspended` —— 为 true 就必须调 {@link startPreview} 恢复。
|
|
60
|
+
*/
|
|
41
61
|
async function stopPreview(opts = {}) {
|
|
42
|
-
|
|
62
|
+
const res = await postPreview('stop', 1, {
|
|
63
|
+
...opts,
|
|
64
|
+
timeoutMs: opts.timeoutMs ?? STOP_TIMEOUT_MS,
|
|
65
|
+
});
|
|
66
|
+
// 404 = 路由不存在(老 code-server),确定没有副作用;其余失败保守认为暂停位可能已置上
|
|
67
|
+
return { ...res, mayBeSuspended: res.ok || res.status !== 404 };
|
|
43
68
|
}
|
|
44
|
-
/**
|
|
69
|
+
/** 启动预览(解除暂停 + 拉起进程)。软失败:不抛异常;内部带重试,因为这是恢复动作。 */
|
|
45
70
|
async function startPreview(opts = {}) {
|
|
46
71
|
return postPreview('start', START_ATTEMPTS, opts);
|
|
47
72
|
}
|
|
48
73
|
async function postPreview(action, attempts, opts) {
|
|
49
74
|
const url = `${resolveCodeServerBaseUrl()}/api/v1/preview/${action}`;
|
|
50
75
|
const logPrefix = opts.logPrefix ?? 'preview';
|
|
51
|
-
let last = {
|
|
76
|
+
let last = {
|
|
77
|
+
ok: false,
|
|
78
|
+
url,
|
|
79
|
+
error: 'no attempt made',
|
|
80
|
+
mayBeSuspended: false,
|
|
81
|
+
};
|
|
52
82
|
for (let i = 1; i <= attempts; i++) {
|
|
53
83
|
last = await postOnce(url, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
54
84
|
if (last.ok)
|
|
@@ -77,15 +107,17 @@ async function postOnce(url, timeoutMs) {
|
|
|
77
107
|
url,
|
|
78
108
|
status: res.status,
|
|
79
109
|
error: `POST ${url} → ${String(res.status)} ${res.statusText}${detail}`,
|
|
110
|
+
mayBeSuspended: false,
|
|
80
111
|
};
|
|
81
112
|
}
|
|
82
|
-
return { ok: true, url, status: res.status };
|
|
113
|
+
return { ok: true, url, status: res.status, mayBeSuspended: false };
|
|
83
114
|
}
|
|
84
115
|
catch (err) {
|
|
85
116
|
return {
|
|
86
117
|
ok: false,
|
|
87
118
|
url,
|
|
88
119
|
error: `POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
+
mayBeSuspended: false,
|
|
89
121
|
};
|
|
90
122
|
}
|
|
91
123
|
}
|