hw-weapp-compiler 1.0.23 → 1.0.25
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/README.md +4 -0
- package/build/utils/upload.js +322 -56
- package/package.json +3 -2
- package/test/upload-async-client.js +72 -0
- package/test/upload.js +92 -0
package/README.md
CHANGED
package/build/utils/upload.js
CHANGED
|
@@ -16,18 +16,236 @@ const { DIST_DIR } = require('../config/constants');
|
|
|
16
16
|
|
|
17
17
|
const { obsConfig, ossConfig } = getConfig();
|
|
18
18
|
|
|
19
|
+
const UPLOAD_CONCURRENCY = 10;
|
|
20
|
+
const DEFAULT_OBS_TIMEOUT_SECONDS = 15;
|
|
21
|
+
const DEFAULT_OBS_MAX_RETRY_COUNT = 1;
|
|
22
|
+
const OBS_PENDING_WARNING_MS = 15 * 1000;
|
|
23
|
+
const OBS_TIMEOUT_BUFFER_MS = 5 * 1000;
|
|
24
|
+
|
|
19
25
|
let obsClient;
|
|
26
|
+
let obsClientReady;
|
|
20
27
|
let ossClient;
|
|
21
28
|
let progress;
|
|
22
29
|
let uploadQueue = {};
|
|
30
|
+
let uploadRunner;
|
|
31
|
+
|
|
32
|
+
function getRemoteKey(config, file) {
|
|
33
|
+
return compatiblePath(path.join(config.dir, path.relative(DIST_DIR, file)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getDisplayFile(file) {
|
|
37
|
+
return compatiblePath(path.relative(process.cwd(), file));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getObsStatus(value) {
|
|
41
|
+
return value && value.CommonMsg ? value.CommonMsg.Status : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getInvalidObsResponseError(operation) {
|
|
45
|
+
const error = new Error(`OBS ${operation} returned no HTTP status`);
|
|
46
|
+
error.code = 'OBS_INVALID_RESPONSE';
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatObsError(error) {
|
|
51
|
+
if (!error) {
|
|
52
|
+
return 'unknown error';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const commonMsg = error.CommonMsg || {};
|
|
56
|
+
const details = {
|
|
57
|
+
status: commonMsg.Status || error.statusCode,
|
|
58
|
+
code: commonMsg.Code || error.code,
|
|
59
|
+
message: commonMsg.Message || error.message,
|
|
60
|
+
requestId: commonMsg.RequestId || error.requestId,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = Object.entries(details)
|
|
64
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
65
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
66
|
+
.join(', ');
|
|
67
|
+
|
|
68
|
+
return result || String(error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toNonNegativeNumber(value, fallback) {
|
|
72
|
+
const number = Number(value);
|
|
73
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toPositiveNumber(value, fallback) {
|
|
77
|
+
const number = Number(value);
|
|
78
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getObsRuntimeOptions() {
|
|
82
|
+
const timeoutSeconds = toPositiveNumber(
|
|
83
|
+
obsConfig.timeout,
|
|
84
|
+
DEFAULT_OBS_TIMEOUT_SECONDS,
|
|
85
|
+
);
|
|
86
|
+
const maxRetryCount = toNonNegativeNumber(
|
|
87
|
+
obsConfig.max_retry_count,
|
|
88
|
+
DEFAULT_OBS_MAX_RETRY_COUNT,
|
|
89
|
+
);
|
|
90
|
+
const defaultOperationTimeoutMs =
|
|
91
|
+
timeoutSeconds * (maxRetryCount + 1) * 1000 + OBS_TIMEOUT_BUFFER_MS;
|
|
92
|
+
const operationTimeoutMs = obsConfig.operation_timeout
|
|
93
|
+
? toPositiveNumber(
|
|
94
|
+
obsConfig.operation_timeout,
|
|
95
|
+
defaultOperationTimeoutMs / 1000,
|
|
96
|
+
) * 1000
|
|
97
|
+
: defaultOperationTimeoutMs;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
timeoutSeconds,
|
|
101
|
+
maxRetryCount,
|
|
102
|
+
operationTimeoutMs,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createObsRequestLogger(operation, file, key) {
|
|
107
|
+
const startedAt = Date.now();
|
|
108
|
+
const { timeoutSeconds, maxRetryCount, operationTimeoutMs } = getObsRuntimeOptions();
|
|
109
|
+
const displayFile = getDisplayFile(file);
|
|
110
|
+
|
|
111
|
+
console.log(`[OBS] ${operation} started: key=${key}, file=${displayFile}`);
|
|
112
|
+
|
|
113
|
+
const pendingTimer = setTimeout(() => {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[OBS] ${operation} is still pending after ${OBS_PENDING_WARNING_MS / 1000}s: key=${key}, ` +
|
|
116
|
+
`server=${obsConfig.server}, bucket=${obsConfig.bucket}, ` +
|
|
117
|
+
`sdkTimeout=${timeoutSeconds}s, maxRetryCount=${maxRetryCount}, ` +
|
|
118
|
+
`operationTimeout=${operationTimeoutMs / 1000}s`,
|
|
119
|
+
);
|
|
120
|
+
}, OBS_PENDING_WARNING_MS);
|
|
121
|
+
|
|
122
|
+
// 诊断定时器不应单独阻止构建进程退出。
|
|
123
|
+
if (pendingTimer.unref) {
|
|
124
|
+
pendingTimer.unref();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
finish(outcome, detail) {
|
|
129
|
+
clearTimeout(pendingTimer);
|
|
130
|
+
const elapsed = Date.now() - startedAt;
|
|
131
|
+
const suffix = detail ? `, ${detail}` : '';
|
|
132
|
+
const message = `[OBS] ${operation} ${outcome} in ${elapsed}ms: key=${key}${suffix}`;
|
|
133
|
+
|
|
134
|
+
if (outcome === 'failed') {
|
|
135
|
+
console.error(message);
|
|
136
|
+
} else if (outcome === 'miss') {
|
|
137
|
+
console.warn(message);
|
|
138
|
+
} else {
|
|
139
|
+
console.log(message);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runObsRequest(operation, file, key, request) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const requestLogger = createObsRequestLogger(operation, file, key);
|
|
148
|
+
const { operationTimeoutMs } = getObsRuntimeOptions();
|
|
149
|
+
let settled = false;
|
|
150
|
+
let operationTimer;
|
|
151
|
+
let activeRequest;
|
|
152
|
+
|
|
153
|
+
const finish = (error, result, outcome) => {
|
|
154
|
+
if (settled) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
settled = true;
|
|
159
|
+
clearTimeout(operationTimer);
|
|
160
|
+
activeRequest = null;
|
|
161
|
+
requestLogger.finish(
|
|
162
|
+
outcome || (error ? 'failed' : 'completed'),
|
|
163
|
+
formatObsError(error || result),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (error) {
|
|
167
|
+
reject(error);
|
|
168
|
+
} else {
|
|
169
|
+
resolve(result);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const armOperationTimeout = () => {
|
|
174
|
+
if (settled) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
clearTimeout(operationTimer);
|
|
178
|
+
operationTimer = setTimeout(() => {
|
|
179
|
+
const requestToAbort = activeRequest;
|
|
180
|
+
const error = new Error(
|
|
181
|
+
`OBS ${operation} timed out after ${operationTimeoutMs / 1000}s without progress`,
|
|
182
|
+
);
|
|
183
|
+
error.code = 'OBS_OPERATION_TIMEOUT';
|
|
184
|
+
finish(error);
|
|
185
|
+
if (requestToAbort && requestToAbort.destroy) {
|
|
186
|
+
requestToAbort.destroy();
|
|
187
|
+
}
|
|
188
|
+
}, operationTimeoutMs);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const registerRequest = (clientRequest) => {
|
|
192
|
+
activeRequest = clientRequest;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
armOperationTimeout();
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
request(finish, armOperationTimeout, registerRequest);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
finish(error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function waitForObsClientReady(client) {
|
|
206
|
+
if (client.util && client.util.signatureContext && client.util.server) {
|
|
207
|
+
return Promise.resolve(client);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { operationTimeoutMs } = getObsRuntimeOptions();
|
|
211
|
+
const startedAt = Date.now();
|
|
212
|
+
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const checkReady = () => {
|
|
215
|
+
if (client.util && client.util.signatureContext && client.util.server) {
|
|
216
|
+
resolve(client);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (Date.now() - startedAt >= operationTimeoutMs) {
|
|
221
|
+
const error = new Error(
|
|
222
|
+
`OBS client initialization timed out after ${operationTimeoutMs / 1000}s`,
|
|
223
|
+
);
|
|
224
|
+
error.code = 'OBS_CLIENT_INIT_TIMEOUT';
|
|
225
|
+
reject(error);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
setImmediate(checkReady);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
setImmediate(checkReady);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
23
235
|
|
|
24
236
|
function getObsClient() {
|
|
25
237
|
if (!obsClient) {
|
|
238
|
+
const { timeoutSeconds, maxRetryCount } = getObsRuntimeOptions();
|
|
26
239
|
obsClient = new OBSClient({
|
|
27
240
|
...obsConfig,
|
|
241
|
+
timeout: timeoutSeconds,
|
|
242
|
+
max_retry_count: maxRetryCount,
|
|
28
243
|
});
|
|
244
|
+
// esdk-obs-nodejs 3.26.8 将 initFactory 改成了 async,但构造函数没有等待它。
|
|
245
|
+
// 立即请求会在 signatureContext 尚未赋值时破坏 SDK 的 bucket 协商锁。
|
|
246
|
+
obsClientReady = waitForObsClientReady(obsClient);
|
|
29
247
|
}
|
|
30
|
-
return
|
|
248
|
+
return obsClientReady;
|
|
31
249
|
}
|
|
32
250
|
|
|
33
251
|
function getOssClient() {
|
|
@@ -41,49 +259,62 @@ function getOssClient() {
|
|
|
41
259
|
|
|
42
260
|
function doOssUpload(file) {
|
|
43
261
|
return getOssClient().put(
|
|
44
|
-
|
|
262
|
+
getRemoteKey(ossConfig, file),
|
|
45
263
|
file,
|
|
46
264
|
);
|
|
47
265
|
}
|
|
48
266
|
|
|
49
267
|
function getOssStat(file) {
|
|
50
|
-
return getOssClient().head(
|
|
268
|
+
return getOssClient().head(getRemoteKey(ossConfig, file));
|
|
51
269
|
}
|
|
52
270
|
|
|
53
|
-
function doObsUpload(file) {
|
|
54
|
-
|
|
55
|
-
|
|
271
|
+
async function doObsUpload(file) {
|
|
272
|
+
const key = getRemoteKey(obsConfig, file);
|
|
273
|
+
const client = await getObsClient();
|
|
274
|
+
return runObsRequest('upload', file, key, (finish, reportProgress, registerRequest) => {
|
|
275
|
+
client.putObject(
|
|
56
276
|
{
|
|
57
277
|
Bucket: obsConfig.bucket,
|
|
58
|
-
Key:
|
|
278
|
+
Key: key,
|
|
59
279
|
SourceFile: file,
|
|
280
|
+
ProgressCallback: reportProgress,
|
|
281
|
+
RequestHook: registerRequest,
|
|
60
282
|
},
|
|
61
283
|
(err, result) => {
|
|
284
|
+
const status = getObsStatus(result);
|
|
62
285
|
if (err) {
|
|
63
|
-
|
|
64
|
-
} else if (
|
|
65
|
-
|
|
286
|
+
finish(err);
|
|
287
|
+
} else if (status === undefined) {
|
|
288
|
+
finish(getInvalidObsResponseError('upload'));
|
|
289
|
+
} else if (status >= 300) {
|
|
290
|
+
finish(result);
|
|
66
291
|
} else {
|
|
67
|
-
|
|
292
|
+
finish(null, result);
|
|
68
293
|
}
|
|
69
294
|
},
|
|
70
295
|
);
|
|
71
296
|
});
|
|
72
297
|
}
|
|
73
298
|
async function getObsStat(file) {
|
|
74
|
-
|
|
75
|
-
|
|
299
|
+
const key = getRemoteKey(obsConfig, file);
|
|
300
|
+
const client = await getObsClient();
|
|
301
|
+
return runObsRequest('metadata check', file, key, (finish, _reportProgress, registerRequest) => {
|
|
302
|
+
client.getObjectMetadata(
|
|
76
303
|
{
|
|
77
304
|
Bucket: obsConfig.bucket,
|
|
78
|
-
Key:
|
|
305
|
+
Key: key,
|
|
306
|
+
RequestHook: registerRequest,
|
|
79
307
|
},
|
|
80
308
|
(err, result) => {
|
|
309
|
+
const status = getObsStatus(result);
|
|
81
310
|
if (err) {
|
|
82
|
-
|
|
83
|
-
} else if (
|
|
84
|
-
|
|
311
|
+
finish(err);
|
|
312
|
+
} else if (status === undefined) {
|
|
313
|
+
finish(getInvalidObsResponseError('metadata check'));
|
|
314
|
+
} else if (status < 300) {
|
|
315
|
+
finish(null, result);
|
|
85
316
|
} else {
|
|
86
|
-
|
|
317
|
+
finish(result, null, 'miss');
|
|
87
318
|
}
|
|
88
319
|
},
|
|
89
320
|
);
|
|
@@ -111,8 +342,7 @@ function doUpload(file) {
|
|
|
111
342
|
}
|
|
112
343
|
|
|
113
344
|
function updateProgress() {
|
|
114
|
-
const completed =
|
|
115
|
-
Object.entries(uploadQueue).filter((item) => item[1] === 'completed').length + 1;
|
|
345
|
+
const completed = Object.values(uploadQueue).filter((status) => status === 'completed').length;
|
|
116
346
|
const total = Object.keys(uploadQueue).length;
|
|
117
347
|
|
|
118
348
|
if (!progress) {
|
|
@@ -121,49 +351,72 @@ function updateProgress() {
|
|
|
121
351
|
width: 40,
|
|
122
352
|
clear: true,
|
|
123
353
|
});
|
|
354
|
+
} else {
|
|
355
|
+
// watch 构建期间可能继续追加资源,进度条总数需要同步扩容。
|
|
356
|
+
progress.total = total;
|
|
124
357
|
}
|
|
125
358
|
|
|
126
359
|
progress.tick();
|
|
127
360
|
|
|
128
|
-
if (completed === total) {
|
|
129
|
-
progress.tick({
|
|
130
|
-
current: total,
|
|
131
|
-
});
|
|
132
|
-
progress = null;
|
|
133
|
-
uploadQueue = {};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
361
|
return `[${completed}/${total}]`;
|
|
137
362
|
}
|
|
138
363
|
|
|
139
|
-
async function
|
|
140
|
-
|
|
141
|
-
.filter((item) => item[1] === false)
|
|
142
|
-
.splice(0, 10);
|
|
143
|
-
|
|
144
|
-
if (files && files.length) {
|
|
145
|
-
await Promise.all(
|
|
146
|
-
files.map(async (file) => {
|
|
147
|
-
uploadQueue[file[0]] = 'uploading';
|
|
148
|
-
try {
|
|
149
|
-
await getStat(file[0]);
|
|
150
|
-
setStorage(file[0], true);
|
|
151
|
-
updateProgress();
|
|
152
|
-
} catch (error) {
|
|
153
|
-
try {
|
|
154
|
-
await doUpload(file[0]);
|
|
155
|
-
setStorage(file[0], true);
|
|
156
|
-
} catch (uploadError) {
|
|
157
|
-
console.error(`Failed to upload ${file[0]}:`, uploadError);
|
|
158
|
-
}
|
|
159
|
-
updateProgress();
|
|
160
|
-
}
|
|
161
|
-
uploadQueue[file[0]] = 'completed';
|
|
162
|
-
}),
|
|
163
|
-
);
|
|
364
|
+
async function uploadFile(file) {
|
|
365
|
+
uploadQueue[file] = 'uploading';
|
|
164
366
|
|
|
165
|
-
|
|
367
|
+
try {
|
|
368
|
+
await getStat(file);
|
|
369
|
+
setStorage(file, true);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
try {
|
|
372
|
+
await doUpload(file);
|
|
373
|
+
setStorage(file, true);
|
|
374
|
+
} catch (uploadError) {
|
|
375
|
+
console.error(
|
|
376
|
+
`[assets] upload failed: file=${getDisplayFile(file)}, ${formatObsError(uploadError)}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
uploadQueue[file] = 'completed';
|
|
381
|
+
updateProgress();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function checkUpload() {
|
|
386
|
+
// done hook 可能在上一次上传尚未结束时再次触发。复用同一个 runner,避免每次构建
|
|
387
|
+
// 都额外启动 10 个请求,最终耗尽 OBS/HTTP 连接池。
|
|
388
|
+
if (uploadRunner) {
|
|
389
|
+
return uploadRunner;
|
|
166
390
|
}
|
|
391
|
+
|
|
392
|
+
uploadRunner = (async () => {
|
|
393
|
+
while (true) {
|
|
394
|
+
const files = Object.entries(uploadQueue)
|
|
395
|
+
.filter((item) => item[1] === 'pending')
|
|
396
|
+
.slice(0, UPLOAD_CONCURRENCY)
|
|
397
|
+
.map((item) => item[0]);
|
|
398
|
+
|
|
399
|
+
if (!files.length) {
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await Promise.all(files.map(uploadFile));
|
|
404
|
+
}
|
|
405
|
+
})().finally(() => {
|
|
406
|
+
uploadRunner = null;
|
|
407
|
+
|
|
408
|
+
// 文件状态已经写完后再清空,避免旧实现最后一个文件把已清空的队列重新写回。
|
|
409
|
+
if (!Object.values(uploadQueue).includes('pending')) {
|
|
410
|
+
progress = null;
|
|
411
|
+
uploadQueue = {};
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 兜住 runner 完成与 watch 新增资源恰好交错的情况。
|
|
416
|
+
checkUpload();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return uploadRunner;
|
|
167
420
|
}
|
|
168
421
|
|
|
169
422
|
function addToUploadQueue(assets) {
|
|
@@ -172,13 +425,26 @@ function addToUploadQueue(assets) {
|
|
|
172
425
|
return;
|
|
173
426
|
}
|
|
174
427
|
|
|
428
|
+
let cachedCount = 0;
|
|
429
|
+
let pendingCount = 0;
|
|
430
|
+
|
|
175
431
|
assets.forEach((asset) => {
|
|
176
432
|
const file = path.resolve(DIST_DIR, asset);
|
|
433
|
+
if (getStorage(file)) {
|
|
434
|
+
cachedCount += 1;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
177
437
|
if (uploadQueue[file] === undefined) {
|
|
178
|
-
uploadQueue[file] =
|
|
438
|
+
uploadQueue[file] = 'pending';
|
|
439
|
+
pendingCount += 1;
|
|
179
440
|
}
|
|
180
441
|
});
|
|
181
|
-
|
|
442
|
+
|
|
443
|
+
console.log(
|
|
444
|
+
`[assets] upload queue: provider=${ossConfig ? 'OSS' : 'OBS'}, total=${assets.length}, ` +
|
|
445
|
+
`cached=${cachedCount}, pending=${pendingCount}`,
|
|
446
|
+
);
|
|
447
|
+
return pendingCount ? checkUpload() : Promise.resolve();
|
|
182
448
|
}
|
|
183
449
|
|
|
184
450
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hw-weapp-compiler",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "基于 webpack5 的小程序构建工具,兼容 Node.js 14~24",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "node ./index.js build",
|
|
54
54
|
"dev": "nodemon --inspect --trace-deprecation --watch build ./index.js dev",
|
|
55
|
-
"simulation": "node ./index.js build -s"
|
|
55
|
+
"simulation": "node ./index.js build -s",
|
|
56
|
+
"test": "node ./test/upload.js && node ./test/upload-async-client.js"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const Module = require('module');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
class AsyncOBSClient {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.util = {
|
|
11
|
+
server: null,
|
|
12
|
+
signatureContext: null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// 模拟 esdk-obs-nodejs 3.26.8:initFactory 在第一次 await 后才完成赋值。
|
|
16
|
+
Promise.resolve().then(() => {
|
|
17
|
+
this.util.server = config.server;
|
|
18
|
+
this.util.signatureContext = { signature: 'obs' };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getObjectMetadata(_params, callback) {
|
|
23
|
+
assert(this.util.signatureContext, 'request started before OBS client initialization');
|
|
24
|
+
callback(null, { CommonMsg: { Status: 200 } });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function run() {
|
|
29
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hw-upload-async-client-test-'));
|
|
30
|
+
const originalCwd = process.cwd();
|
|
31
|
+
const originalLoad = Module._load;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
fs.mkdirSync(path.join(projectDir, 'dist/assets'), { recursive: true });
|
|
35
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/ready.png'), 'ready');
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(projectDir, '.weapp.js'),
|
|
38
|
+
`module.exports = { obsConfig: ${JSON.stringify({
|
|
39
|
+
access_key_id: 'test-ak',
|
|
40
|
+
secret_access_key: 'test-sk',
|
|
41
|
+
server: 'http://obs.example.com',
|
|
42
|
+
bucket: 'test-bucket',
|
|
43
|
+
dir: 'test-dir',
|
|
44
|
+
operation_timeout: 1,
|
|
45
|
+
})} };`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
Module._load = function load(request, parent, isMain) {
|
|
49
|
+
if (request === 'esdk-obs-nodejs') {
|
|
50
|
+
return AsyncOBSClient;
|
|
51
|
+
}
|
|
52
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
process.chdir(projectDir);
|
|
56
|
+
const uploadModule = path.resolve(originalCwd, 'build/utils/upload.js');
|
|
57
|
+
const { addToUploadQueue } = require(uploadModule);
|
|
58
|
+
await addToUploadQueue(['assets/ready.png']);
|
|
59
|
+
|
|
60
|
+
// 等待 storage.js 的防抖写入结束后再清理测试目录。
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
62
|
+
} finally {
|
|
63
|
+
Module._load = originalLoad;
|
|
64
|
+
process.chdir(originalCwd);
|
|
65
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
run().catch((error) => {
|
|
70
|
+
console.error(error);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
});
|
package/test/upload.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
async function run() {
|
|
8
|
+
const requests = [];
|
|
9
|
+
const sockets = new Set();
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
requests.push(`${req.method} ${req.url}`);
|
|
12
|
+
|
|
13
|
+
if (req.url.includes('stuck.png')) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (req.method === 'HEAD') {
|
|
18
|
+
res.statusCode = 404;
|
|
19
|
+
res.end();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
req.resume();
|
|
24
|
+
req.on('end', () => {
|
|
25
|
+
res.statusCode = 200;
|
|
26
|
+
res.setHeader('etag', 'test-etag');
|
|
27
|
+
res.end();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
server.on('connection', (socket) => {
|
|
31
|
+
sockets.add(socket);
|
|
32
|
+
socket.on('close', () => sockets.delete(socket));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
36
|
+
|
|
37
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hw-upload-test-'));
|
|
38
|
+
const originalCwd = process.cwd();
|
|
39
|
+
const endpoint = `http://127.0.0.1:${server.address().port}`;
|
|
40
|
+
const uploadModule = path.resolve(originalCwd, 'build/utils/upload.js');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(path.join(projectDir, 'dist/assets'), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/ok.png'), 'ok');
|
|
45
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/stuck.png'), 'stuck');
|
|
46
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/later.png'), 'later');
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(projectDir, '.weapp.js'),
|
|
49
|
+
`module.exports = { obsConfig: ${JSON.stringify({
|
|
50
|
+
access_key_id: 'test-ak',
|
|
51
|
+
secret_access_key: 'test-sk',
|
|
52
|
+
server: endpoint,
|
|
53
|
+
bucket: 'test-bucket',
|
|
54
|
+
dir: 'test-dir',
|
|
55
|
+
timeout: 10,
|
|
56
|
+
max_retry_count: 0,
|
|
57
|
+
operation_timeout: 0.1,
|
|
58
|
+
})} };`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
process.chdir(projectDir);
|
|
62
|
+
const { addToUploadQueue } = require(uploadModule);
|
|
63
|
+
|
|
64
|
+
await addToUploadQueue(['assets/ok.png']);
|
|
65
|
+
assert(requests.some((request) => request.startsWith('HEAD ')));
|
|
66
|
+
assert(requests.some((request) => request.startsWith('PUT ')));
|
|
67
|
+
|
|
68
|
+
const startedAt = Date.now();
|
|
69
|
+
const stuckRun = addToUploadQueue(['assets/stuck.png']);
|
|
70
|
+
const sameRun = addToUploadQueue(['assets/later.png']);
|
|
71
|
+
assert.strictEqual(stuckRun, sameRun, 'watch rebuild should reuse the active queue runner');
|
|
72
|
+
await sameRun;
|
|
73
|
+
|
|
74
|
+
const elapsed = Date.now() - startedAt;
|
|
75
|
+
assert(elapsed < 2000, `timeout fallback took ${elapsed}ms`);
|
|
76
|
+
assert(requests.filter((request) => request.includes('stuck.png')).length >= 2);
|
|
77
|
+
assert(requests.some((request) => request.includes('later.png')));
|
|
78
|
+
|
|
79
|
+
// 等待 storage.js 的防抖写入结束后再清理测试目录。
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
81
|
+
} finally {
|
|
82
|
+
process.chdir(originalCwd);
|
|
83
|
+
sockets.forEach((socket) => socket.destroy());
|
|
84
|
+
await new Promise((resolve) => server.close(resolve));
|
|
85
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
run().catch((error) => {
|
|
90
|
+
console.error(error);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
});
|