@allurereport/service 3.14.0 → 3.14.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/dist/model.d.ts CHANGED
@@ -13,8 +13,20 @@ export type AllureServiceApiClientConfig = UploadReportConfig & {
13
13
  export type UploadReportPayload = {
14
14
  reportUuid: string;
15
15
  pluginId?: string;
16
- files: Record<string, string>;
17
- onProgress?: () => void;
16
+ files: Record<string, string> | Record<string, string>[];
17
+ onProgress?: (uploadedFilesDelta?: number) => void;
18
+ };
19
+ export type UploadReportFilePayload = {
20
+ filename: string;
21
+ file?: Buffer;
22
+ filepath?: string;
23
+ signal?: AbortSignal;
24
+ };
25
+ export type UploadReportFilesPayload = {
26
+ reportUuid: string;
27
+ pluginId?: string;
28
+ files: UploadReportFilePayload[];
29
+ signal?: AbortSignal;
18
30
  };
19
31
  export type UploadReportResult = {
20
32
  indexHref?: string;
@@ -40,19 +52,15 @@ export interface AllureServiceApiClient {
40
52
  reportUuid: string;
41
53
  pluginId?: string;
42
54
  }): Promise<unknown>;
43
- addReportAsset(payload: {
44
- filename: string;
45
- file?: Buffer;
46
- filepath?: string;
55
+ addReportAsset(payload: UploadReportFilePayload): Promise<unknown>;
56
+ addReportAssets?(payload: {
57
+ files: UploadReportFilePayload[];
47
58
  signal?: AbortSignal;
48
59
  }): Promise<unknown>;
49
- addReportFile(payload: {
60
+ addReportFile(payload: UploadReportFilePayload & {
50
61
  reportUuid: string;
51
62
  pluginId?: string;
52
- filename: string;
53
- file?: Buffer;
54
- filepath?: string;
55
- signal?: AbortSignal;
56
63
  }): Promise<string>;
64
+ addReportFiles?(payload: UploadReportFilesPayload): Promise<Record<string, string>>;
57
65
  uploadReport(payload: UploadReportPayload): Promise<UploadReportResult>;
58
66
  }
package/dist/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type HistoryDataPoint } from "@allurereport/core-api";
2
- import { type AllureServiceApiClient, type AllureServiceApiClientConfig, type UploadReportPayload } from "./model.js";
2
+ import { type AllureServiceApiClient, type AllureServiceApiClientConfig, type UploadReportFilePayload, type UploadReportFilesPayload, type UploadReportPayload } from "./model.js";
3
3
  export declare class AllureServiceClient implements AllureServiceApiClient {
4
4
  #private;
5
5
  readonly config: AllureServiceApiClientConfig;
@@ -23,19 +23,17 @@ export declare class AllureServiceClient implements AllureServiceApiClient {
23
23
  reportUuid: string;
24
24
  pluginId?: string;
25
25
  }): Promise<unknown>;
26
- addReportAsset(payload: {
27
- filename: string;
28
- file?: Buffer;
29
- filepath?: string;
26
+ addReportAssets(payload: {
27
+ files: UploadReportFilePayload[];
30
28
  signal?: AbortSignal;
31
29
  }): Promise<unknown>;
32
- addReportFile(payload: {
30
+ addReportAsset(payload: UploadReportFilePayload): Promise<unknown>;
31
+ addReportFiles(payload: UploadReportFilesPayload): Promise<{
32
+ [k: string]: string;
33
+ }>;
34
+ addReportFile(payload: UploadReportFilePayload & {
33
35
  reportUuid: string;
34
36
  pluginId?: string;
35
- filename: string;
36
- file?: Buffer;
37
- filepath?: string;
38
- signal?: AbortSignal;
39
37
  }): Promise<string>;
40
38
  uploadReport(payload: UploadReportPayload): Promise<import("./model.js").UploadReportResult>;
41
39
  }
package/dist/service.js CHANGED
@@ -19,6 +19,16 @@ const UPLOAD_CONTENT_TYPE = "application/octet-stream";
19
19
  const createUploadBlob = (content) => new Blob([content], { type: UPLOAD_CONTENT_TYPE });
20
20
  const createReportUrl = (baseUrl, reportUuid) => `${baseUrl}/${reportUuid}`;
21
21
  const createReportFileUrl = (baseUrl, reportUuid, reportFilename) => `${baseUrl}/${joinPosix(reportUuid, reportFilename)}`;
22
+ const readUploadContent = async (payload) => {
23
+ const { file, filepath, signal } = payload;
24
+ if (file) {
25
+ return file;
26
+ }
27
+ if (!filepath) {
28
+ throw new Error("File or filepath is required");
29
+ }
30
+ return signal ? readFile(filepath, { signal }) : readFile(filepath);
31
+ };
22
32
  export class AllureServiceClient {
23
33
  constructor(config) {
24
34
  this.config = config;
@@ -79,18 +89,20 @@ export class AllureServiceClient {
79
89
  },
80
90
  });
81
91
  }
82
- async addReportAsset(payload) {
83
- const { filename, file, filepath, signal } = payload;
84
- if (!file && !filepath) {
85
- throw new Error("File or filepath is required");
86
- }
87
- let content = file;
88
- if (!content) {
89
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
92
+ async addReportAssets(payload) {
93
+ const { files, signal } = payload;
94
+ if (files.length === 0) {
95
+ return undefined;
90
96
  }
97
+ const entries = await Promise.all(files.map(async ({ filename, ...filePayload }) => ({
98
+ filename,
99
+ content: await readUploadContent({ filename, ...filePayload, signal: signal ?? filePayload.signal }),
100
+ })));
91
101
  const form = new FormData();
92
- form.set("filename", filename);
93
- form.set("file", createUploadBlob(content), filename);
102
+ for (const { filename, content } of entries) {
103
+ form.append("filename", filename);
104
+ form.append("file", createUploadBlob(content), filename);
105
+ }
94
106
  return __classPrivateFieldGet(this, _AllureServiceClient_client, "f").post("/api/assets/upload", {
95
107
  body: form,
96
108
  headers: {
@@ -99,19 +111,27 @@ export class AllureServiceClient {
99
111
  ...(signal ? { signal } : {}),
100
112
  });
101
113
  }
102
- async addReportFile(payload) {
103
- const { reportUuid, filename, file, filepath, pluginId, signal } = payload;
104
- const reportFilename = pluginId ? joinPosix(pluginId, filename) : filename;
105
- if (!file && !filepath) {
106
- throw new Error("File or filepath is required");
107
- }
108
- let content = file;
109
- if (!content) {
110
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
114
+ async addReportAsset(payload) {
115
+ return this.addReportAssets({ files: [payload], ...(payload.signal ? { signal: payload.signal } : {}) });
116
+ }
117
+ async addReportFiles(payload) {
118
+ const { reportUuid, pluginId, files, signal } = payload;
119
+ if (files.length === 0) {
120
+ return {};
111
121
  }
122
+ const entries = await Promise.all(files.map(async ({ filename, ...filePayload }) => {
123
+ const reportFilename = pluginId ? joinPosix(pluginId, filename) : filename;
124
+ return {
125
+ filename,
126
+ reportFilename,
127
+ content: await readUploadContent({ filename, ...filePayload, signal: signal ?? filePayload.signal }),
128
+ };
129
+ }));
112
130
  const form = new FormData();
113
- form.set("filename", reportFilename);
114
- form.set("file", createUploadBlob(content), reportFilename);
131
+ for (const { reportFilename, content } of entries) {
132
+ form.append("filename", reportFilename);
133
+ form.append("file", createUploadBlob(content), reportFilename);
134
+ }
115
135
  await __classPrivateFieldGet(this, _AllureServiceClient_client, "f").post(`/api/reports/${reportUuid}/upload`, {
116
136
  body: form,
117
137
  headers: {
@@ -119,7 +139,19 @@ export class AllureServiceClient {
119
139
  },
120
140
  ...(signal ? { signal } : {}),
121
141
  });
122
- return createReportFileUrl(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), reportUuid, reportFilename);
142
+ return Object.fromEntries(entries.map(({ filename, reportFilename }) => [
143
+ filename,
144
+ createReportFileUrl(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), reportUuid, reportFilename),
145
+ ]));
146
+ }
147
+ async addReportFile(payload) {
148
+ const result = await this.addReportFiles({
149
+ reportUuid: payload.reportUuid,
150
+ pluginId: payload.pluginId,
151
+ files: [payload],
152
+ ...(payload.signal ? { signal: payload.signal } : {}),
153
+ });
154
+ return result[payload.filename];
123
155
  }
124
156
  async uploadReport(payload) {
125
157
  return uploadReport({
@@ -128,7 +160,9 @@ export class AllureServiceClient {
128
160
  uploadMaxAttempts: this.config.uploadMaxAttempts,
129
161
  uploadMaxSimultaneousFailures: this.config.uploadMaxSimultaneousFailures,
130
162
  addReportAsset: this.addReportAsset.bind(this),
163
+ addReportAssets: this.addReportAssets.bind(this),
131
164
  addReportFile: this.addReportFile.bind(this),
165
+ addReportFiles: this.addReportFiles.bind(this),
132
166
  });
133
167
  }
134
168
  }
@@ -1,5 +1,5 @@
1
1
  import { type AxiosRequestConfig } from "axios";
2
- import type { UploadReportConfig, UploadReportPayload, UploadReportResult } from "../model.js";
2
+ import type { UploadReportConfig, UploadReportFilePayload, UploadReportFilesPayload, UploadReportPayload, UploadReportResult } from "../model.js";
3
3
  export declare class KnownError extends Error {
4
4
  status?: number;
5
5
  constructor(message: string, status?: number);
@@ -40,16 +40,14 @@ export declare const createServiceHttpClient: (serviceUrl: string, params?: {
40
40
  };
41
41
  export type HttpClient = ReturnType<typeof createServiceHttpClient>;
42
42
  export declare const uploadReport: (payload: UploadReportPayload & UploadReportConfig & {
43
- addReportAsset: (payload: {
44
- filename: string;
45
- filepath: string;
43
+ addReportAsset: (payload: UploadReportFilePayload) => Promise<unknown>;
44
+ addReportAssets?: (payload: {
45
+ files: UploadReportFilePayload[];
46
46
  signal?: AbortSignal;
47
47
  }) => Promise<unknown>;
48
- addReportFile: (payload: {
48
+ addReportFile: (payload: UploadReportFilePayload & {
49
49
  reportUuid: string;
50
50
  pluginId?: string;
51
- filename: string;
52
- filepath: string;
53
- signal?: AbortSignal;
54
51
  }) => Promise<string>;
52
+ addReportFiles?: (payload: UploadReportFilesPayload) => Promise<Record<string, string>>;
55
53
  }) => Promise<UploadReportResult>;
@@ -24,6 +24,30 @@ const stringifyErrorObject = (value) => {
24
24
  return entries.length > 0 ? `{ ${entries.join(", ")} }` : undefined;
25
25
  }
26
26
  };
27
+ const MAX_UPLOAD_BATCH_FILES = 32;
28
+ const createUploadFileBatches = (files) => {
29
+ const entries = Object.entries(files);
30
+ const batches = [];
31
+ let batch = {};
32
+ let batchFiles = 0;
33
+ const flushBatch = () => {
34
+ if (batchFiles === 0) {
35
+ return;
36
+ }
37
+ batches.push(batch);
38
+ batch = {};
39
+ batchFiles = 0;
40
+ };
41
+ for (const [filename, filepath] of entries) {
42
+ if (batchFiles >= MAX_UPLOAD_BATCH_FILES) {
43
+ flushBatch();
44
+ }
45
+ batch[filename] = filepath;
46
+ batchFiles += 1;
47
+ }
48
+ flushBatch();
49
+ return batches;
50
+ };
27
51
  export const formatResponseErrorData = (data) => {
28
52
  if (data === undefined || data === null || data === "") {
29
53
  return undefined;
@@ -116,21 +140,21 @@ export const createServiceHttpClient = (serviceUrl, params) => {
116
140
  delete: sendRequest("delete"),
117
141
  };
118
142
  };
119
- const uploadWithRetry = async (filename, uploadAbortController, failedUploads, maxAttempts, maxSimultaneousFailures, uploadFn) => {
143
+ const uploadWithRetry = async (batchId, uploadAbortController, failedUploads, maxAttempts, maxSimultaneousFailures, uploadFn) => {
120
144
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
121
145
  if (uploadAbortController.signal.aborted) {
122
146
  return false;
123
147
  }
124
148
  try {
125
149
  await uploadFn();
126
- failedUploads.delete(filename);
150
+ failedUploads.delete(batchId);
127
151
  return true;
128
152
  }
129
153
  catch (error) {
130
154
  if (uploadAbortController.signal.aborted) {
131
155
  return false;
132
156
  }
133
- failedUploads.add(filename);
157
+ failedUploads.add(batchId);
134
158
  if (failedUploads.size > maxSimultaneousFailures || attempt >= maxAttempts) {
135
159
  throw error;
136
160
  }
@@ -140,8 +164,8 @@ const uploadWithRetry = async (filename, uploadAbortController, failedUploads, m
140
164
  };
141
165
  export const uploadReport = async (payload) => {
142
166
  const { reportUuid, pluginId, files, onProgress, addReportAsset, addReportFile, uploadConcurrency, uploadMaxAttempts, uploadMaxSimultaneousFailures, } = payload;
143
- const fileEntries = Object.entries(files);
144
- if (fileEntries.length === 0) {
167
+ const fileBatches = Array.isArray(files) ? files.flatMap(createUploadFileBatches) : createUploadFileBatches(files);
168
+ if (fileBatches.length === 0) {
145
169
  return {
146
170
  hrefs: {},
147
171
  };
@@ -150,46 +174,119 @@ export const uploadReport = async (payload) => {
150
174
  const failedUploads = new Set();
151
175
  const hrefs = {};
152
176
  let indexHref;
153
- let nextFileIndex = 0;
154
- const uploadNext = async () => {
155
- while (!uploadAbortController.signal.aborted) {
156
- const fileIndex = nextFileIndex++;
157
- if (fileIndex >= fileEntries.length) {
158
- return;
177
+ let nextBatchIndex = 0;
178
+ const uploadBatches = [];
179
+ const canUploadReportFilesInBatch = !!payload.addReportFiles;
180
+ const canUploadAssetsInBatch = !!payload.addReportAssets;
181
+ for (const [batchIndex, batchFiles] of fileBatches.entries()) {
182
+ const reportDataFiles = [];
183
+ const assetFiles = [];
184
+ for (const [filename, filepath] of Object.entries(batchFiles)) {
185
+ const filePayload = { filename, filepath, signal: uploadAbortController.signal };
186
+ if (isReportDataFile(filename)) {
187
+ reportDataFiles.push(filePayload);
159
188
  }
160
- const [filename, filepath] = fileEntries[fileIndex];
161
- let fileUrl;
162
- const uploaded = await uploadWithRetry(filename, uploadAbortController, failedUploads, uploadMaxAttempts, uploadMaxSimultaneousFailures, async () => {
163
- if (isReportDataFile(filename)) {
164
- fileUrl = await addReportFile({
165
- reportUuid,
166
- pluginId,
167
- filename,
168
- filepath,
169
- signal: uploadAbortController.signal,
189
+ else {
190
+ assetFiles.push(filePayload);
191
+ }
192
+ }
193
+ if (reportDataFiles.length > 0) {
194
+ if (canUploadReportFilesInBatch) {
195
+ uploadBatches.push({
196
+ batchId: `batch-${batchIndex}-report`,
197
+ kind: "report",
198
+ files: reportDataFiles,
199
+ });
200
+ }
201
+ else {
202
+ reportDataFiles.forEach((filePayload, fileIndex) => {
203
+ uploadBatches.push({
204
+ batchId: `batch-${batchIndex}-report-${fileIndex}`,
205
+ kind: "report",
206
+ files: [filePayload],
207
+ });
208
+ });
209
+ }
210
+ }
211
+ if (assetFiles.length > 0) {
212
+ if (canUploadAssetsInBatch) {
213
+ uploadBatches.push({
214
+ batchId: `batch-${batchIndex}-asset`,
215
+ kind: "asset",
216
+ files: assetFiles,
217
+ });
218
+ }
219
+ else {
220
+ assetFiles.forEach((filePayload, fileIndex) => {
221
+ uploadBatches.push({
222
+ batchId: `batch-${batchIndex}-asset-${fileIndex}`,
223
+ kind: "asset",
224
+ files: [filePayload],
170
225
  });
226
+ });
227
+ }
228
+ }
229
+ }
230
+ const uploadFileBatch = async ({ kind, files: batchFiles }) => {
231
+ if (batchFiles.length === 0 || uploadAbortController.signal.aborted) {
232
+ return;
233
+ }
234
+ if (kind === "report") {
235
+ if (payload.addReportFiles) {
236
+ const batchResult = await payload.addReportFiles({
237
+ reportUuid,
238
+ pluginId,
239
+ files: batchFiles,
240
+ signal: uploadAbortController.signal,
241
+ });
242
+ for (const [filename, fileUrl] of Object.entries(batchResult)) {
243
+ hrefs[filename] = fileUrl;
244
+ if (filename === "index.html") {
245
+ indexHref = fileUrl;
246
+ }
171
247
  }
172
- else {
173
- await addReportAsset({
174
- filename,
175
- filepath,
176
- signal: uploadAbortController.signal,
248
+ }
249
+ else {
250
+ for (const filePayload of batchFiles) {
251
+ const fileUrl = await payload.addReportFile({
252
+ reportUuid,
253
+ pluginId,
254
+ ...filePayload,
177
255
  });
256
+ hrefs[filePayload.filename] = fileUrl;
257
+ if (filePayload.filename === "index.html") {
258
+ indexHref = fileUrl;
259
+ }
178
260
  }
261
+ }
262
+ }
263
+ else if (payload.addReportAssets) {
264
+ await payload.addReportAssets({
265
+ files: batchFiles,
266
+ signal: uploadAbortController.signal,
179
267
  });
180
- if (!uploaded || uploadAbortController.signal.aborted) {
268
+ }
269
+ else {
270
+ for (const filePayload of batchFiles) {
271
+ await payload.addReportAsset(filePayload);
272
+ }
273
+ }
274
+ onProgress?.(batchFiles.length);
275
+ };
276
+ const uploadNext = async () => {
277
+ while (!uploadAbortController.signal.aborted) {
278
+ const batchIndex = nextBatchIndex++;
279
+ if (batchIndex >= uploadBatches.length) {
181
280
  return;
182
281
  }
183
- if (fileUrl) {
184
- hrefs[filename] = fileUrl;
185
- if (filename === "index.html") {
186
- indexHref = fileUrl;
187
- }
282
+ const batch = uploadBatches[batchIndex];
283
+ const uploaded = await uploadWithRetry(batch.batchId, uploadAbortController, failedUploads, uploadMaxAttempts, uploadMaxSimultaneousFailures, async () => uploadFileBatch(batch));
284
+ if (!uploaded || uploadAbortController.signal.aborted) {
285
+ return;
188
286
  }
189
- onProgress?.();
190
287
  }
191
288
  };
192
- const uploadTasks = Array.from({ length: Math.min(uploadConcurrency, fileEntries.length) }, () => uploadNext());
289
+ const uploadTasks = Array.from({ length: Math.min(uploadConcurrency, uploadBatches.length) }, () => uploadNext());
193
290
  try {
194
291
  await Promise.all(uploadTasks);
195
292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/service",
3
- "version": "3.14.0",
3
+ "version": "3.14.1",
4
4
  "description": "Allure Service API",
5
5
  "keywords": [
6
6
  "allure",
@@ -30,8 +30,8 @@
30
30
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
31
31
  },
32
32
  "dependencies": {
33
- "@allurereport/core-api": "3.14.0",
34
- "@allurereport/plugin-api": "3.14.0",
33
+ "@allurereport/core-api": "3.14.1",
34
+ "@allurereport/plugin-api": "3.14.1",
35
35
  "axios": "^1.18.1",
36
36
  "open": "^11.0.0"
37
37
  },