eoas 3.1.2-beta2 → 3.1.2-beta4
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/dist/commands/publish.d.ts +1 -0
- package/dist/commands/publish.js +30 -14
- package/dist/commands/republish.js +137 -47
- package/dist/lib/fetch.d.ts +4 -0
- package/dist/lib/fetch.js +80 -4
- package/dist/lib/rateLimiter.d.ts +7 -0
- package/dist/lib/rateLimiter.js +34 -0
- package/dist/lib/serverUpdates.d.ts +35 -10
- package/dist/lib/serverUpdates.js +31 -3
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ export default class Publish extends Command {
|
|
|
15
15
|
message: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
16
16
|
dumpSourcemap: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
17
17
|
'rollout-percentage': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
|
+
'upload-rate': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
19
|
};
|
|
19
20
|
private sanitizeFlags;
|
|
20
21
|
run(): Promise<void>;
|
package/dist/commands/publish.js
CHANGED
|
@@ -18,6 +18,7 @@ const ora_1 = require("../lib/ora");
|
|
|
18
18
|
const package_1 = require("../lib/package");
|
|
19
19
|
const packageRunner_1 = require("../lib/packageRunner");
|
|
20
20
|
const prompts_1 = require("../lib/prompts");
|
|
21
|
+
const rateLimiter_1 = require("../lib/rateLimiter");
|
|
21
22
|
const repo_1 = require("../lib/repo");
|
|
22
23
|
const runtimeVersion_1 = require("../lib/runtimeVersion");
|
|
23
24
|
const vcs_1 = require("../lib/vcs");
|
|
@@ -79,8 +80,17 @@ class Publish extends core_1.Command {
|
|
|
79
80
|
max: 99,
|
|
80
81
|
description: 'Publish this update as a progressive rollout served to N% of devices (1-99). The remaining devices keep receiving the previous update of each branch/runtime version. With --platform all, the rollout applies independently to each platform. Progression (increase, end, revert) is managed from the dashboard.',
|
|
81
82
|
}),
|
|
83
|
+
'upload-rate': core_1.Flags.string({
|
|
84
|
+
description: 'Maximum number of asset uploads started per second. Accepts decimals (e.g. 1.5). Lower this if your storage provider rate-limits uploads.',
|
|
85
|
+
default: '10',
|
|
86
|
+
}),
|
|
82
87
|
};
|
|
83
88
|
sanitizeFlags(flags) {
|
|
89
|
+
const uploadRate = Number(flags['upload-rate']);
|
|
90
|
+
if (!Number.isFinite(uploadRate) || uploadRate <= 0) {
|
|
91
|
+
log_1.default.error('--upload-rate must be a positive number');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
84
94
|
return {
|
|
85
95
|
disableRepositoryCheck: flags.disableRepositoryCheck,
|
|
86
96
|
platform: flags.platform,
|
|
@@ -93,6 +103,7 @@ class Publish extends core_1.Command {
|
|
|
93
103
|
message: flags.message,
|
|
94
104
|
dumpSourcemap: flags.dumpSourcemap,
|
|
95
105
|
rolloutPercentage: flags['rollout-percentage'],
|
|
106
|
+
uploadRate,
|
|
96
107
|
};
|
|
97
108
|
}
|
|
98
109
|
async run() {
|
|
@@ -102,11 +113,14 @@ class Publish extends core_1.Command {
|
|
|
102
113
|
process.exit(1);
|
|
103
114
|
}
|
|
104
115
|
const { flags } = await this.parse(Publish);
|
|
105
|
-
const { platform, nonInteractive, branch, customServerUrl, outputDir, packageRunner, providedDeprecatedChannel, disableRepositoryCheck, message, dumpSourcemap, rolloutPercentage, } = this.sanitizeFlags(flags);
|
|
116
|
+
const { platform, nonInteractive, branch, customServerUrl, outputDir, packageRunner, providedDeprecatedChannel, disableRepositoryCheck, message, dumpSourcemap, rolloutPercentage, uploadRate, } = this.sanitizeFlags(flags);
|
|
106
117
|
if (!branch) {
|
|
107
118
|
log_1.default.error('Branch name is required');
|
|
108
119
|
process.exit(1);
|
|
109
120
|
}
|
|
121
|
+
// Bucket capacity is ~1s of budget so a low --upload-rate also caps the
|
|
122
|
+
// initial burst, not just the steady rate.
|
|
123
|
+
const publishAssetsRateLimiter = new rateLimiter_1.RateLimiter('publish-eoas-assets', Math.max(1, Math.ceil(uploadRate)), uploadRate);
|
|
110
124
|
const projectDir = process.cwd();
|
|
111
125
|
const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
|
|
112
126
|
if (!hasExpo) {
|
|
@@ -289,13 +303,18 @@ class Publish extends core_1.Command {
|
|
|
289
303
|
manifest: files,
|
|
290
304
|
})))).flat();
|
|
291
305
|
await Promise.all(resolvedUploads.map(async ({ item: itm, absolutePath, manifestEntry }) => {
|
|
306
|
+
await publishAssetsRateLimiter.take();
|
|
292
307
|
const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${serverUrl}/${appId}/uploadLocalFile`);
|
|
293
308
|
if (isLocalBucketFileUpload) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
309
|
+
// A stream-backed body is consumed by the first attempt, so each
|
|
310
|
+
// retry rebuilds the multipart body from a buffer.
|
|
311
|
+
const fileBuffer = await fs_extra_1.default.readFile(absolutePath);
|
|
312
|
+
const response = await (0, fetch_1.fetchWithRetriesRebuildingBody)(itm.requestUploadUrl, () => {
|
|
313
|
+
const formData = new form_data_1.default();
|
|
314
|
+
formData.append(itm.fileName, fileBuffer, {
|
|
315
|
+
filename: path_1.default.basename(absolutePath),
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
299
318
|
method: 'PUT',
|
|
300
319
|
headers: {
|
|
301
320
|
...formData.getHeaders(),
|
|
@@ -305,14 +324,11 @@ class Publish extends core_1.Command {
|
|
|
305
324
|
// The URL was validated as a string; following a redirect would
|
|
306
325
|
// send these bytes to an origin nothing ever checked.
|
|
307
326
|
redirect: 'error',
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
file.close();
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
log_1.default.error('Failed to upload file', await response.text());
|
|
331
|
+
throw new Error('Failed to upload file');
|
|
316
332
|
}
|
|
317
333
|
return;
|
|
318
334
|
}
|
|
@@ -11,6 +11,7 @@ const package_1 = require("../lib/package");
|
|
|
11
11
|
const prompts_1 = require("../lib/prompts");
|
|
12
12
|
const serverUpdates_1 = require("../lib/serverUpdates");
|
|
13
13
|
const vcs_1 = require("../lib/vcs");
|
|
14
|
+
const REPUBLISH_PAGE_SIZE = 20;
|
|
14
15
|
class Publish extends core_1.Command {
|
|
15
16
|
static args = {};
|
|
16
17
|
static description = 'Republish a previous update to a branch';
|
|
@@ -93,31 +94,27 @@ class Publish extends core_1.Command {
|
|
|
93
94
|
})),
|
|
94
95
|
});
|
|
95
96
|
log_1.default.log(`Selected runtime version: ${selectedRuntimeVersion.runtimeVersion}`);
|
|
96
|
-
let
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (updates.length === 0) {
|
|
113
|
-
log_1.default.error(`No republishable updates found for runtime version ${selectedRuntimeVersion.runtimeVersion}.`);
|
|
114
|
-
process.exit(1);
|
|
97
|
+
let firstGroupsPage = null;
|
|
98
|
+
if (platform === 'all') {
|
|
99
|
+
try {
|
|
100
|
+
firstGroupsPage = await (0, serverUpdates_1.fetchPublishGroups)({
|
|
101
|
+
baseUrl,
|
|
102
|
+
appId,
|
|
103
|
+
branch,
|
|
104
|
+
runtimeVersion: selectedRuntimeVersion.runtimeVersion,
|
|
105
|
+
credentials,
|
|
106
|
+
limit: REPUBLISH_PAGE_SIZE,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
log_1.default.error(e instanceof Error ? e.message : e);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
115
113
|
}
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
// did not already narrow the run to one platform.
|
|
114
|
+
// Publish groups are a control-plane read model. Servers without that
|
|
115
|
+
// capability return no group page and keep the single-update flow.
|
|
119
116
|
let mode = 'single';
|
|
120
|
-
if (
|
|
117
|
+
if (firstGroupsPage && firstGroupsPage.items.length > 0) {
|
|
121
118
|
const selectedMode = await (0, prompts_1.promptAsync)({
|
|
122
119
|
type: 'select',
|
|
123
120
|
name: 'mode',
|
|
@@ -138,16 +135,58 @@ class Publish extends core_1.Command {
|
|
|
138
135
|
mode = selectedMode.mode;
|
|
139
136
|
}
|
|
140
137
|
if (mode === 'group') {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
if (!firstGroupsPage) {
|
|
139
|
+
log_1.default.error('Publish group listing is not available');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const groups = [...firstGroupsPage.items];
|
|
143
|
+
let nextGroupCursor = firstGroupsPage.nextCursor;
|
|
144
|
+
let group;
|
|
145
|
+
let initialChoiceIndex = 0;
|
|
146
|
+
while (!group) {
|
|
147
|
+
const choices = groups.map(candidate => ({
|
|
148
|
+
...(0, serverUpdates_1.describePublishGroup)(candidate),
|
|
149
|
+
value: { kind: 'group', group: candidate },
|
|
150
|
+
}));
|
|
151
|
+
if (nextGroupCursor) {
|
|
152
|
+
choices.push({ title: 'Load more publishes', value: { kind: 'loadMore' } });
|
|
153
|
+
}
|
|
154
|
+
const selectedGroup = await (0, prompts_1.promptAsync)({
|
|
155
|
+
type: 'select',
|
|
156
|
+
name: 'group',
|
|
157
|
+
message: 'Select a publish to republish',
|
|
158
|
+
choices,
|
|
159
|
+
initial: initialChoiceIndex,
|
|
160
|
+
});
|
|
161
|
+
const selection = selectedGroup.group;
|
|
162
|
+
if (selection.kind === 'loadMore') {
|
|
163
|
+
const previousGroupCount = groups.length;
|
|
164
|
+
try {
|
|
165
|
+
const page = await (0, serverUpdates_1.fetchPublishGroups)({
|
|
166
|
+
baseUrl,
|
|
167
|
+
appId,
|
|
168
|
+
branch,
|
|
169
|
+
runtimeVersion: selectedRuntimeVersion.runtimeVersion,
|
|
170
|
+
credentials,
|
|
171
|
+
cursor: nextGroupCursor ?? undefined,
|
|
172
|
+
limit: REPUBLISH_PAGE_SIZE,
|
|
173
|
+
});
|
|
174
|
+
if (!page) {
|
|
175
|
+
throw new Error('Publish group listing is no longer available');
|
|
176
|
+
}
|
|
177
|
+
groups.push(...page.items);
|
|
178
|
+
nextGroupCursor = page.nextCursor;
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
log_1.default.error(e instanceof Error ? e.message : e);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
initialChoiceIndex = Math.min(previousGroupCount, Math.max(0, groups.length - 1));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
group = selection.group;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
151
190
|
const republishUrl = new URL(`${baseUrl}/${appId}/republish/${branch}`);
|
|
152
191
|
republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
|
|
153
192
|
republishUrl.searchParams.set('publishGroup', group.publishGroup);
|
|
@@ -171,27 +210,78 @@ class Publish extends core_1.Command {
|
|
|
171
210
|
: `✅ Republished ${group.platforms.join(' + ')}`);
|
|
172
211
|
return;
|
|
173
212
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
213
|
+
const updates = [];
|
|
214
|
+
let nextCursor;
|
|
215
|
+
const loadNextPage = async () => {
|
|
216
|
+
const previousCount = updates.length;
|
|
217
|
+
do {
|
|
218
|
+
const page = await (0, serverUpdates_1.fetchUpdates)({
|
|
219
|
+
baseUrl,
|
|
220
|
+
appId,
|
|
221
|
+
branch,
|
|
222
|
+
runtimeVersion: selectedRuntimeVersion.runtimeVersion,
|
|
223
|
+
credentials,
|
|
224
|
+
cursor: nextCursor ?? undefined,
|
|
225
|
+
limit: REPUBLISH_PAGE_SIZE,
|
|
226
|
+
});
|
|
227
|
+
// Rollbacks have no files to republish. Apply a requested platform
|
|
228
|
+
// before presenting pages, but do not fetch ahead merely to fill 20.
|
|
229
|
+
updates.push(...page.items.filter(update => update.updateUUID !== 'Rollback to embedded' &&
|
|
230
|
+
(platform === 'all' || update.platform === platform)));
|
|
231
|
+
nextCursor = page.nextCursor;
|
|
232
|
+
} while (updates.length === previousCount && nextCursor);
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
await loadNextPage();
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
log_1.default.error(e instanceof Error ? e.message : e);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
if (updates.length === 0 && !nextCursor) {
|
|
176
242
|
log_1.default.error(`No republishable updates found for runtime version ${selectedRuntimeVersion.runtimeVersion} on platform ${platform}.`);
|
|
177
243
|
process.exit(1);
|
|
178
244
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
choices: platformUpdates.map(update => ({
|
|
245
|
+
let selectedUpdate;
|
|
246
|
+
let initialChoiceIndex = 0;
|
|
247
|
+
while (!selectedUpdate) {
|
|
248
|
+
const choices = updates.map(update => ({
|
|
184
249
|
title: update.updateUUID,
|
|
185
|
-
value: update,
|
|
250
|
+
value: { kind: 'update', update },
|
|
186
251
|
description: `Created at: ${update.createdAt}, Platform: ${update.platform}, Commit hash: ${update.commitHash}`,
|
|
187
|
-
}))
|
|
188
|
-
|
|
189
|
-
|
|
252
|
+
}));
|
|
253
|
+
if (nextCursor) {
|
|
254
|
+
choices.push({ title: 'Load more updates', value: { kind: 'loadMore' } });
|
|
255
|
+
}
|
|
256
|
+
const answer = await (0, prompts_1.promptAsync)({
|
|
257
|
+
type: 'select',
|
|
258
|
+
name: 'update',
|
|
259
|
+
message: 'Select an update to republish',
|
|
260
|
+
choices,
|
|
261
|
+
initial: initialChoiceIndex,
|
|
262
|
+
});
|
|
263
|
+
const selection = answer.update;
|
|
264
|
+
if (selection.kind === 'loadMore') {
|
|
265
|
+
const firstNewUpdateIndex = updates.length;
|
|
266
|
+
try {
|
|
267
|
+
await loadNextPage();
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
log_1.default.error(e instanceof Error ? e.message : e);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
initialChoiceIndex = Math.min(firstNewUpdateIndex, Math.max(0, updates.length - 1));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
selectedUpdate = selection.update;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
log_1.default.log(`Re-publishing update: ${selectedUpdate.updateUUID}`);
|
|
190
280
|
const republishUrl = new URL(`${baseUrl}/${appId}/republish/${branch}`);
|
|
191
|
-
republishUrl.searchParams.set('platform',
|
|
281
|
+
republishUrl.searchParams.set('platform', selectedUpdate.platform);
|
|
192
282
|
republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
|
|
193
|
-
republishUrl.searchParams.set('updateId',
|
|
194
|
-
republishUrl.searchParams.set('commitHash',
|
|
283
|
+
republishUrl.searchParams.set('updateId', selectedUpdate.updateId);
|
|
284
|
+
republishUrl.searchParams.set('commitHash', selectedUpdate.commitHash);
|
|
195
285
|
const republishSpinner = (0, ora_1.ora)('🔄 Republishing update...').start();
|
|
196
286
|
const republishResponse = await (0, fetch_1.fetchWithRetries)(republishUrl.toString(), {
|
|
197
287
|
method: 'POST',
|
package/dist/lib/fetch.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import { RequestInit, Response } from 'node-fetch';
|
|
2
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
3
|
+
export declare function retryAfterMs(header: string | null): number | null;
|
|
4
|
+
export declare function redactUrl(url: string): string;
|
|
2
5
|
export declare function fetchWithRetries(url: string, options: RequestInit): Promise<Response>;
|
|
6
|
+
export declare function fetchWithRetriesRebuildingBody(url: string, makeOptions: () => RequestInit): Promise<Response>;
|
package/dist/lib/fetch.js
CHANGED
|
@@ -1,27 +1,103 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fetchWithRetries = void 0;
|
|
3
|
+
exports.fetchWithRetriesRebuildingBody = exports.fetchWithRetries = exports.redactUrl = exports.retryAfterMs = exports.isRetryableStatus = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fetch_retry_1 = tslib_1.__importDefault(require("fetch-retry"));
|
|
6
6
|
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
|
|
7
7
|
const log_1 = tslib_1.__importDefault(require("./log"));
|
|
8
8
|
const fetch = (0, fetch_retry_1.default)(node_fetch_1.default);
|
|
9
|
+
function isRetryableStatus(status) {
|
|
10
|
+
return status === 429 || status >= 500;
|
|
11
|
+
}
|
|
12
|
+
exports.isRetryableStatus = isRetryableStatus;
|
|
13
|
+
// Retry-After is either delay-seconds or an HTTP-date.
|
|
14
|
+
function retryAfterMs(header) {
|
|
15
|
+
if (!header) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const seconds = Number(header);
|
|
19
|
+
if (Number.isFinite(seconds)) {
|
|
20
|
+
return seconds * 1000;
|
|
21
|
+
}
|
|
22
|
+
const dateMs = Date.parse(header);
|
|
23
|
+
if (!Number.isNaN(dateMs)) {
|
|
24
|
+
return dateMs - Date.now();
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
exports.retryAfterMs = retryAfterMs;
|
|
29
|
+
const MAX_RETRY_DELAY_MS = 60000;
|
|
30
|
+
const MAX_RETRIED_ATTEMPT = 3;
|
|
31
|
+
// Upload URLs carry their credentials in the query string (S3 presign, Azure
|
|
32
|
+
// SAS, the local bucket's upload token), so logs may only show origin and path.
|
|
33
|
+
function redactUrl(url) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
return parsed.origin + parsed.pathname;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return '<unparseable url>';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.redactUrl = redactUrl;
|
|
43
|
+
function retryDelayForResponse(attempt, response) {
|
|
44
|
+
const serverDelay = retryAfterMs(response.headers.get('retry-after'));
|
|
45
|
+
const backoff = Math.pow(2, attempt) * 2000;
|
|
46
|
+
return Math.min(Math.max(serverDelay ?? 0, backoff), MAX_RETRY_DELAY_MS);
|
|
47
|
+
}
|
|
9
48
|
async function fetchWithRetries(url, options) {
|
|
10
49
|
return await fetch(url, {
|
|
11
50
|
...options,
|
|
12
|
-
retryDelay(attempt) {
|
|
51
|
+
retryDelay(attempt, _error, response) {
|
|
52
|
+
if (response) {
|
|
53
|
+
return retryDelayForResponse(attempt, response);
|
|
54
|
+
}
|
|
13
55
|
return Math.pow(2, attempt) * 500;
|
|
14
56
|
},
|
|
15
|
-
retryOn: (attempt, error) => {
|
|
16
|
-
if (attempt >
|
|
57
|
+
retryOn: (attempt, error, response) => {
|
|
58
|
+
if (attempt > MAX_RETRIED_ATTEMPT) {
|
|
17
59
|
return false;
|
|
18
60
|
}
|
|
19
61
|
if (error) {
|
|
20
62
|
log_1.default.warn(`Retry ${attempt} after network error:`, error.message);
|
|
21
63
|
return true;
|
|
22
64
|
}
|
|
65
|
+
if (response && isRetryableStatus(response.status)) {
|
|
66
|
+
log_1.default.warn(`Retry ${attempt} after HTTP ${response.status} from ${redactUrl(url)}`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
23
69
|
return false;
|
|
24
70
|
},
|
|
25
71
|
});
|
|
26
72
|
}
|
|
27
73
|
exports.fetchWithRetries = fetchWithRetries;
|
|
74
|
+
function sleep(ms) {
|
|
75
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
76
|
+
}
|
|
77
|
+
// fetch-retry re-sends the same RequestInit on every attempt, which silently
|
|
78
|
+
// replays an already-consumed stream body as empty. Callers with a one-shot
|
|
79
|
+
// body (multipart uploads) use this variant: makeOptions rebuilds the body
|
|
80
|
+
// for each attempt. Same retry policy as fetchWithRetries.
|
|
81
|
+
async function fetchWithRetriesRebuildingBody(url, makeOptions) {
|
|
82
|
+
for (let attempt = 0;; attempt++) {
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await (0, node_fetch_1.default)(url, makeOptions());
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (attempt > MAX_RETRIED_ATTEMPT) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
log_1.default.warn(`Retry ${attempt} after network error:`, error.message);
|
|
92
|
+
await sleep(Math.pow(2, attempt) * 500);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (attempt <= MAX_RETRIED_ATTEMPT && isRetryableStatus(response.status)) {
|
|
96
|
+
log_1.default.warn(`Retry ${attempt} after HTTP ${response.status} from ${redactUrl(url)}`);
|
|
97
|
+
await sleep(retryDelayForResponse(attempt, response));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
return response;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.fetchWithRetriesRebuildingBody = fetchWithRetriesRebuildingBody;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RateLimiter = void 0;
|
|
4
|
+
const kv = {};
|
|
5
|
+
class RateLimiter {
|
|
6
|
+
key;
|
|
7
|
+
capacity;
|
|
8
|
+
refillRate;
|
|
9
|
+
constructor(key, capacity, refillRate) {
|
|
10
|
+
this.key = key;
|
|
11
|
+
this.capacity = capacity;
|
|
12
|
+
this.refillRate = refillRate;
|
|
13
|
+
}
|
|
14
|
+
async take() {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const bucket = kv[this.key];
|
|
17
|
+
let tokens = bucket ? bucket.tokens : this.capacity;
|
|
18
|
+
const lastRefill = bucket ? bucket.lastRefill : now;
|
|
19
|
+
const elapsed = (now - lastRefill) / 1000;
|
|
20
|
+
tokens = Math.min(this.capacity, tokens + elapsed * this.refillRate);
|
|
21
|
+
if (tokens < 1) {
|
|
22
|
+
const waitMs = ((1 - tokens) / this.refillRate) * 1000;
|
|
23
|
+
await new Promise(res => setTimeout(res, waitMs));
|
|
24
|
+
await this.take();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
tokens -= 1;
|
|
28
|
+
kv[this.key] = {
|
|
29
|
+
tokens,
|
|
30
|
+
lastRefill: now,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.RateLimiter = RateLimiter;
|
|
@@ -14,27 +14,52 @@ export interface ServerUpdateItem {
|
|
|
14
14
|
message?: string;
|
|
15
15
|
publishGroup?: string;
|
|
16
16
|
}
|
|
17
|
+
export interface ServerUpdatesPage {
|
|
18
|
+
items: ServerUpdateItem[];
|
|
19
|
+
nextCursor: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface PublishGroupUpdateItem {
|
|
22
|
+
updateId: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
platform: string;
|
|
25
|
+
commitHash: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PublishGroupSummary {
|
|
28
|
+
publishGroup: string;
|
|
29
|
+
platforms: string[];
|
|
30
|
+
commitHash: string;
|
|
31
|
+
message?: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
updates: PublishGroupUpdateItem[];
|
|
34
|
+
}
|
|
35
|
+
export interface ServerPublishGroupsPage {
|
|
36
|
+
items: PublishGroupSummary[];
|
|
37
|
+
nextCursor: string | null;
|
|
38
|
+
}
|
|
17
39
|
export declare function fetchRuntimeVersions({ baseUrl, appId, branch, credentials, }: {
|
|
18
40
|
baseUrl: string;
|
|
19
41
|
appId: string;
|
|
20
42
|
branch: string;
|
|
21
43
|
credentials: Credentials;
|
|
22
44
|
}): Promise<RuntimeVersionInfo[]>;
|
|
23
|
-
export declare function fetchUpdates({ baseUrl, appId, branch, runtimeVersion, credentials, }: {
|
|
45
|
+
export declare function fetchUpdates({ baseUrl, appId, branch, runtimeVersion, credentials, cursor, limit, }: {
|
|
24
46
|
baseUrl: string;
|
|
25
47
|
appId: string;
|
|
26
48
|
branch: string;
|
|
27
49
|
runtimeVersion: string;
|
|
28
50
|
credentials: Credentials;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
cursor?: string;
|
|
52
|
+
limit?: number;
|
|
53
|
+
}): Promise<ServerUpdatesPage>;
|
|
54
|
+
export declare function fetchPublishGroups({ baseUrl, appId, branch, runtimeVersion, credentials, cursor, limit, }: {
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
appId: string;
|
|
57
|
+
branch: string;
|
|
58
|
+
runtimeVersion: string;
|
|
59
|
+
credentials: Credentials;
|
|
60
|
+
cursor?: string;
|
|
61
|
+
limit?: number;
|
|
62
|
+
}): Promise<ServerPublishGroupsPage | null>;
|
|
38
63
|
export declare function groupPublishedUpdates(updates: ServerUpdateItem[]): {
|
|
39
64
|
groups: PublishGroupSummary[];
|
|
40
65
|
ungrouped: ServerUpdateItem[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.describePublishGroup = exports.groupPublishedUpdates = exports.fetchUpdates = exports.fetchRuntimeVersions = void 0;
|
|
3
|
+
exports.describePublishGroup = exports.groupPublishedUpdates = exports.fetchPublishGroups = exports.fetchUpdates = exports.fetchRuntimeVersions = void 0;
|
|
4
4
|
const auth_1 = require("./auth");
|
|
5
5
|
const fetch_1 = require("./fetch");
|
|
6
6
|
async function fetchRuntimeVersions({ baseUrl, appId, branch, credentials, }) {
|
|
@@ -16,8 +16,13 @@ async function fetchRuntimeVersions({ baseUrl, appId, branch, credentials, }) {
|
|
|
16
16
|
return (await response.json());
|
|
17
17
|
}
|
|
18
18
|
exports.fetchRuntimeVersions = fetchRuntimeVersions;
|
|
19
|
-
async function fetchUpdates({ baseUrl, appId, branch, runtimeVersion, credentials, }) {
|
|
20
|
-
const
|
|
19
|
+
async function fetchUpdates({ baseUrl, appId, branch, runtimeVersion, credentials, cursor, limit = 20, }) {
|
|
20
|
+
const url = new URL(`${baseUrl}/api/apps/${encodeURIComponent(appId)}/branch/${encodeURIComponent(branch)}/runtimeVersion/${encodeURIComponent(runtimeVersion)}/updates`);
|
|
21
|
+
url.searchParams.set('limit', String(limit));
|
|
22
|
+
if (cursor) {
|
|
23
|
+
url.searchParams.set('cursor', cursor);
|
|
24
|
+
}
|
|
25
|
+
const response = await (0, fetch_1.fetchWithRetries)(url.toString(), {
|
|
21
26
|
headers: {
|
|
22
27
|
...(0, auth_1.getAuthHeaders)(credentials),
|
|
23
28
|
'use-cli-auth': 'true',
|
|
@@ -29,6 +34,29 @@ async function fetchUpdates({ baseUrl, appId, branch, runtimeVersion, credential
|
|
|
29
34
|
return (await response.json());
|
|
30
35
|
}
|
|
31
36
|
exports.fetchUpdates = fetchUpdates;
|
|
37
|
+
// Publish groups only exist in control-plane mode. A 404 marks group mode as
|
|
38
|
+
// unavailable without requiring a separate capability negotiation request.
|
|
39
|
+
async function fetchPublishGroups({ baseUrl, appId, branch, runtimeVersion, credentials, cursor, limit = 20, }) {
|
|
40
|
+
const url = new URL(`${baseUrl}/api/apps/${encodeURIComponent(appId)}/branch/${encodeURIComponent(branch)}/runtimeVersion/${encodeURIComponent(runtimeVersion)}/publish-groups`);
|
|
41
|
+
url.searchParams.set('limit', String(limit));
|
|
42
|
+
if (cursor) {
|
|
43
|
+
url.searchParams.set('cursor', cursor);
|
|
44
|
+
}
|
|
45
|
+
const response = await (0, fetch_1.fetchWithRetries)(url.toString(), {
|
|
46
|
+
headers: {
|
|
47
|
+
...(0, auth_1.getAuthHeaders)(credentials),
|
|
48
|
+
'use-cli-auth': 'true',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (response.status === 404) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`Failed to fetch publish groups: ${await response.text()}`);
|
|
56
|
+
}
|
|
57
|
+
return (await response.json());
|
|
58
|
+
}
|
|
59
|
+
exports.fetchPublishGroups = fetchPublishGroups;
|
|
32
60
|
// groupPublishedUpdates splits a listing into publish groups (newest first)
|
|
33
61
|
// and the leftover ungrouped updates (older CLIs, stateless servers). Filter
|
|
34
62
|
// out rollback markers before calling if they should not be offered.
|