@devrev/ts-adaas 1.7.2-beta.0 → 1.9.0-beta.0
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/attachments-streaming/attachments-streaming-pool.d.ts +12 -0
- package/dist/attachments-streaming/attachments-streaming-pool.interfaces.d.ts +8 -0
- package/dist/attachments-streaming/attachments-streaming-pool.interfaces.js +2 -0
- package/dist/attachments-streaming/attachments-streaming-pool.js +75 -0
- package/dist/attachments-streaming/attachments-streaming-pool.test.d.ts +1 -0
- package/dist/attachments-streaming/attachments-streaming-pool.test.js +273 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/workers/worker-adapter.d.ts +0 -21
- package/dist/workers/worker-adapter.js +34 -133
- package/dist/workers/worker-adapter.test.js +26 -328
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProcessAttachmentReturnType } from '../types';
|
|
2
|
+
import { AttachmentsStreamingPoolParams } from './attachments-streaming-pool.interfaces';
|
|
3
|
+
export declare class AttachmentsStreamingPool<ConnectorState> {
|
|
4
|
+
private adapter;
|
|
5
|
+
private attachments;
|
|
6
|
+
private batchSize;
|
|
7
|
+
private delay;
|
|
8
|
+
private stream;
|
|
9
|
+
constructor({ adapter, attachments, batchSize, stream, }: AttachmentsStreamingPoolParams<ConnectorState>);
|
|
10
|
+
streamAll(): Promise<ProcessAttachmentReturnType>;
|
|
11
|
+
startPoolStreaming(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NormalizedAttachment, ExternalSystemAttachmentStreamingFunction } from '../types';
|
|
2
|
+
import { WorkerAdapter } from '../workers/worker-adapter';
|
|
3
|
+
export interface AttachmentsStreamingPoolParams<ConnectorState> {
|
|
4
|
+
adapter: WorkerAdapter<ConnectorState>;
|
|
5
|
+
attachments: NormalizedAttachment[];
|
|
6
|
+
batchSize?: number;
|
|
7
|
+
stream: ExternalSystemAttachmentStreamingFunction;
|
|
8
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AttachmentsStreamingPool = void 0;
|
|
4
|
+
class AttachmentsStreamingPool {
|
|
5
|
+
constructor({ adapter, attachments, batchSize = 10, stream, }) {
|
|
6
|
+
this.adapter = adapter;
|
|
7
|
+
this.attachments = [...attachments]; // Create a copy we can mutate
|
|
8
|
+
this.batchSize = batchSize;
|
|
9
|
+
this.delay = undefined;
|
|
10
|
+
this.stream = stream;
|
|
11
|
+
}
|
|
12
|
+
async streamAll() {
|
|
13
|
+
console.log(`Starting download of ${this.attachments.length} attachments, streaming ${this.batchSize} at once.`);
|
|
14
|
+
if (!this.adapter.state.toDevRev) {
|
|
15
|
+
const error = new Error('toDevRev state is not initialized');
|
|
16
|
+
console.error(error);
|
|
17
|
+
return { error };
|
|
18
|
+
}
|
|
19
|
+
// Get the list of successfully processed attachments in previous (possibly incomplete) batch extraction.
|
|
20
|
+
// If no such list exists, create an empty one.
|
|
21
|
+
if (!this.adapter.state.toDevRev.attachmentsMetadata
|
|
22
|
+
.lastProcessedAttachmentsIdsList) {
|
|
23
|
+
this.adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList =
|
|
24
|
+
[];
|
|
25
|
+
}
|
|
26
|
+
// Start initial batch of promises up to batchSize limit
|
|
27
|
+
const initialBatchSize = Math.min(this.batchSize, this.attachments.length);
|
|
28
|
+
const initialPromises = [];
|
|
29
|
+
for (let i = 0; i < initialBatchSize; i++) {
|
|
30
|
+
initialPromises.push(this.startPoolStreaming());
|
|
31
|
+
}
|
|
32
|
+
// Wait for all promises to complete
|
|
33
|
+
await Promise.all(initialPromises);
|
|
34
|
+
if (this.delay) {
|
|
35
|
+
return { delay: this.delay };
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
async startPoolStreaming() {
|
|
40
|
+
var _a, _b, _c, _d;
|
|
41
|
+
// Process attachments until the attachments array is empty
|
|
42
|
+
while (this.attachments.length > 0) {
|
|
43
|
+
if (this.delay) {
|
|
44
|
+
break; // Exit if we have a delay
|
|
45
|
+
}
|
|
46
|
+
// Check if we can process next attachment
|
|
47
|
+
const attachment = this.attachments.shift();
|
|
48
|
+
if (!attachment) {
|
|
49
|
+
break; // Exit if no more attachments
|
|
50
|
+
}
|
|
51
|
+
if (this.adapter.state.toDevRev &&
|
|
52
|
+
((_a = this.adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList) === null || _a === void 0 ? void 0 : _a.includes(attachment.id))) {
|
|
53
|
+
console.log(`Attachment with ID ${attachment.id} has already been processed. Skipping.`);
|
|
54
|
+
continue; // Skip if the attachment ID is already processed
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const response = await this.adapter.processAttachment(attachment, this.stream);
|
|
58
|
+
// Check if rate limit was hit
|
|
59
|
+
if (response === null || response === void 0 ? void 0 : response.delay) {
|
|
60
|
+
this.delay = response.delay; // Set the delay for rate limiting
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// No rate limiting, process normally
|
|
64
|
+
if ((_c = (_b = this.adapter.state.toDevRev) === null || _b === void 0 ? void 0 : _b.attachmentsMetadata) === null || _c === void 0 ? void 0 : _c.lastProcessedAttachmentsIdsList) {
|
|
65
|
+
(_d = this.adapter.state.toDevRev) === null || _d === void 0 ? void 0 : _d.attachmentsMetadata.lastProcessedAttachmentsIdsList.push(attachment.id);
|
|
66
|
+
console.log(`Successfully processed attachment: ${attachment.id}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn(`Skipping attachment with ID ${attachment.id} due to error: ${error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.AttachmentsStreamingPool = AttachmentsStreamingPool;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const attachments_streaming_pool_1 = require("./attachments-streaming-pool");
|
|
4
|
+
describe('AttachmentsStreamingPool', () => {
|
|
5
|
+
let mockAdapter;
|
|
6
|
+
let mockStream;
|
|
7
|
+
let mockAttachments;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Create mock adapter
|
|
10
|
+
mockAdapter = {
|
|
11
|
+
state: {
|
|
12
|
+
attachments: { completed: false },
|
|
13
|
+
toDevRev: {
|
|
14
|
+
attachmentsMetadata: {
|
|
15
|
+
lastProcessedAttachmentsIdsList: []
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
processAttachment: jest.fn().mockResolvedValue({})
|
|
20
|
+
};
|
|
21
|
+
// Create mock stream function
|
|
22
|
+
mockStream = jest.fn().mockResolvedValue({ success: true });
|
|
23
|
+
// Create mock attachments
|
|
24
|
+
mockAttachments = [
|
|
25
|
+
{
|
|
26
|
+
id: 'attachment-1',
|
|
27
|
+
url: 'https://example.com/file1.pdf',
|
|
28
|
+
file_name: 'file1.pdf',
|
|
29
|
+
parent_id: 'parent-1'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'attachment-2',
|
|
33
|
+
url: 'https://example.com/file2.jpg',
|
|
34
|
+
file_name: 'file2.jpg',
|
|
35
|
+
parent_id: 'parent-2'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'attachment-3',
|
|
39
|
+
url: 'https://example.com/file3.doc',
|
|
40
|
+
file_name: 'file3.doc',
|
|
41
|
+
parent_id: 'parent-3'
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
// Mock console methods
|
|
45
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
46
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
47
|
+
jest.spyOn(console, 'warn').mockImplementation();
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
jest.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
describe('constructor', () => {
|
|
54
|
+
it('should initialize with default values', () => {
|
|
55
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
56
|
+
adapter: mockAdapter,
|
|
57
|
+
attachments: mockAttachments,
|
|
58
|
+
stream: mockStream
|
|
59
|
+
});
|
|
60
|
+
expect(pool).toBeDefined();
|
|
61
|
+
expect(pool['adapter']).toBe(mockAdapter);
|
|
62
|
+
expect(pool['attachments']).toEqual(mockAttachments);
|
|
63
|
+
expect(pool['batchSize']).toBe(10);
|
|
64
|
+
expect(pool['stream']).toBe(mockStream);
|
|
65
|
+
});
|
|
66
|
+
it('should initialize with custom batch size', () => {
|
|
67
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
68
|
+
adapter: mockAdapter,
|
|
69
|
+
attachments: mockAttachments,
|
|
70
|
+
batchSize: 5,
|
|
71
|
+
stream: mockStream
|
|
72
|
+
});
|
|
73
|
+
expect(pool['batchSize']).toBe(5);
|
|
74
|
+
});
|
|
75
|
+
it('should create a copy of attachments array', () => {
|
|
76
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
77
|
+
adapter: mockAdapter,
|
|
78
|
+
attachments: mockAttachments,
|
|
79
|
+
stream: mockStream
|
|
80
|
+
});
|
|
81
|
+
expect(pool['attachments']).toEqual(mockAttachments);
|
|
82
|
+
expect(pool['attachments']).not.toBe(mockAttachments); // Different reference
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('streamAll', () => {
|
|
86
|
+
it('should initialize lastProcessedAttachmentsIdsList if it does not exist', async () => {
|
|
87
|
+
mockAdapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList = undefined;
|
|
88
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
89
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
90
|
+
adapter: mockAdapter,
|
|
91
|
+
attachments: mockAttachments,
|
|
92
|
+
stream: mockStream
|
|
93
|
+
});
|
|
94
|
+
// Mock startPoolStreaming to avoid actual processing
|
|
95
|
+
jest.spyOn(pool, 'startPoolStreaming').mockResolvedValue(undefined);
|
|
96
|
+
await pool.streamAll();
|
|
97
|
+
expect(mockAdapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
it('should process all attachments successfully', async () => {
|
|
100
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
101
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
102
|
+
adapter: mockAdapter,
|
|
103
|
+
attachments: mockAttachments,
|
|
104
|
+
stream: mockStream
|
|
105
|
+
});
|
|
106
|
+
const result = await pool.streamAll();
|
|
107
|
+
expect(result).toEqual({});
|
|
108
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(3);
|
|
109
|
+
});
|
|
110
|
+
it('should handle empty attachments array', async () => {
|
|
111
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
112
|
+
adapter: mockAdapter,
|
|
113
|
+
attachments: [],
|
|
114
|
+
stream: mockStream
|
|
115
|
+
});
|
|
116
|
+
const result = await pool.streamAll();
|
|
117
|
+
expect(result).toEqual({});
|
|
118
|
+
expect(mockAdapter.processAttachment).not.toHaveBeenCalled();
|
|
119
|
+
expect(console.log).toHaveBeenCalledWith('Starting download of 0 attachments, streaming 10 at once.');
|
|
120
|
+
});
|
|
121
|
+
it('should return delay when rate limit is hit', async () => {
|
|
122
|
+
const delayResponse = { delay: 5000 };
|
|
123
|
+
mockAdapter.processAttachment.mockResolvedValue(delayResponse);
|
|
124
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
125
|
+
adapter: mockAdapter,
|
|
126
|
+
attachments: mockAttachments,
|
|
127
|
+
stream: mockStream
|
|
128
|
+
});
|
|
129
|
+
const result = await pool.streamAll();
|
|
130
|
+
expect(result).toEqual({ delay: 5000 });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('startPoolStreaming', () => {
|
|
134
|
+
it('should skip already processed attachments', async () => {
|
|
135
|
+
mockAdapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList = ['attachment-1'];
|
|
136
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
137
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
138
|
+
adapter: mockAdapter,
|
|
139
|
+
attachments: mockAttachments,
|
|
140
|
+
stream: mockStream
|
|
141
|
+
});
|
|
142
|
+
await pool.streamAll();
|
|
143
|
+
expect(console.log).toHaveBeenCalledWith('Attachment with ID attachment-1 has already been processed. Skipping.');
|
|
144
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(2); // Only 2 out of 3
|
|
145
|
+
});
|
|
146
|
+
it('should add successfully processed attachment IDs to the list', async () => {
|
|
147
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
148
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
149
|
+
adapter: mockAdapter,
|
|
150
|
+
attachments: mockAttachments,
|
|
151
|
+
stream: mockStream
|
|
152
|
+
});
|
|
153
|
+
await pool.streamAll();
|
|
154
|
+
expect(mockAdapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList).toEqual([
|
|
155
|
+
'attachment-1',
|
|
156
|
+
'attachment-2',
|
|
157
|
+
'attachment-3'
|
|
158
|
+
]);
|
|
159
|
+
expect(console.log).toHaveBeenCalledWith('Successfully processed attachment: attachment-1');
|
|
160
|
+
expect(console.log).toHaveBeenCalledWith('Successfully processed attachment: attachment-2');
|
|
161
|
+
expect(console.log).toHaveBeenCalledWith('Successfully processed attachment: attachment-3');
|
|
162
|
+
});
|
|
163
|
+
it('should handle processing errors gracefully', async () => {
|
|
164
|
+
const error = new Error('Processing failed');
|
|
165
|
+
mockAdapter.processAttachment
|
|
166
|
+
.mockResolvedValueOnce({}) // First attachment succeeds
|
|
167
|
+
.mockRejectedValueOnce(error) // Second attachment fails
|
|
168
|
+
.mockResolvedValueOnce({}); // Third attachment succeeds
|
|
169
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
170
|
+
adapter: mockAdapter,
|
|
171
|
+
attachments: mockAttachments,
|
|
172
|
+
stream: mockStream
|
|
173
|
+
});
|
|
174
|
+
await pool.streamAll();
|
|
175
|
+
expect(console.warn).toHaveBeenCalledWith('Skipping attachment with ID attachment-2 due to error: Error: Processing failed');
|
|
176
|
+
expect(mockAdapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList).toEqual([
|
|
177
|
+
'attachment-1',
|
|
178
|
+
'attachment-3'
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
it('should stop processing when rate limit delay is encountered', async () => {
|
|
182
|
+
mockAdapter.processAttachment
|
|
183
|
+
.mockResolvedValueOnce({}) // First attachment succeeds
|
|
184
|
+
.mockResolvedValueOnce({ delay: 5000 }) // Second attachment triggers rate limit
|
|
185
|
+
.mockResolvedValueOnce({}); // Third attachment succeeds
|
|
186
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
187
|
+
adapter: mockAdapter,
|
|
188
|
+
attachments: mockAttachments,
|
|
189
|
+
stream: mockStream
|
|
190
|
+
});
|
|
191
|
+
await pool.streamAll();
|
|
192
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(3);
|
|
193
|
+
expect(mockAdapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList).toEqual([
|
|
194
|
+
'attachment-1',
|
|
195
|
+
'attachment-3'
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
it('should pass correct parameters to processAttachment', async () => {
|
|
199
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
200
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
201
|
+
adapter: mockAdapter,
|
|
202
|
+
attachments: [mockAttachments[0]],
|
|
203
|
+
stream: mockStream
|
|
204
|
+
});
|
|
205
|
+
await pool.streamAll();
|
|
206
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0], mockStream);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('edge cases', () => {
|
|
210
|
+
it('should handle single attachment', async () => {
|
|
211
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
212
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
213
|
+
adapter: mockAdapter,
|
|
214
|
+
attachments: [mockAttachments[0]],
|
|
215
|
+
stream: mockStream
|
|
216
|
+
});
|
|
217
|
+
const result = await pool.streamAll();
|
|
218
|
+
expect(result).toEqual({});
|
|
219
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
it('should handle batch size larger than attachments array', async () => {
|
|
222
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
223
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
224
|
+
adapter: mockAdapter,
|
|
225
|
+
attachments: mockAttachments,
|
|
226
|
+
batchSize: 100,
|
|
227
|
+
stream: mockStream
|
|
228
|
+
});
|
|
229
|
+
await pool.streamAll();
|
|
230
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(3);
|
|
231
|
+
});
|
|
232
|
+
it('should handle batch size of 1', async () => {
|
|
233
|
+
mockAdapter.processAttachment.mockResolvedValue({});
|
|
234
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
235
|
+
adapter: mockAdapter,
|
|
236
|
+
attachments: mockAttachments,
|
|
237
|
+
batchSize: 1,
|
|
238
|
+
stream: mockStream
|
|
239
|
+
});
|
|
240
|
+
await pool.streamAll();
|
|
241
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(3);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('concurrency behavior', () => {
|
|
245
|
+
it('should process attachments concurrently within batch size', async () => {
|
|
246
|
+
let processCallCount = 0;
|
|
247
|
+
const processPromises = [];
|
|
248
|
+
mockAdapter.processAttachment.mockImplementation(() => {
|
|
249
|
+
const promise = new Promise((resolve) => {
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
processCallCount++;
|
|
252
|
+
resolve({});
|
|
253
|
+
}, 100);
|
|
254
|
+
});
|
|
255
|
+
processPromises.push(promise);
|
|
256
|
+
return promise;
|
|
257
|
+
});
|
|
258
|
+
const pool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
259
|
+
adapter: mockAdapter,
|
|
260
|
+
attachments: mockAttachments,
|
|
261
|
+
batchSize: 2,
|
|
262
|
+
stream: mockStream
|
|
263
|
+
});
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
await pool.streamAll();
|
|
266
|
+
const endTime = Date.now();
|
|
267
|
+
expect(mockAdapter.processAttachment).toHaveBeenCalledTimes(3);
|
|
268
|
+
expect(processCallCount).toBe(3);
|
|
269
|
+
// Should complete in roughly 200ms (2 batches of 100ms each) rather than 300ms (sequential)
|
|
270
|
+
expect(endTime - startTime).toBeLessThan(250);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ErrorLevel, ErrorRecord, LogRecord, AdapterUpdateParams, InitialDomainMapping, } from './common';
|
|
2
|
-
export { EventType, ExtractorEventType, ExtractionMode, ExternalSyncUnit, EventContextIn, EventContextOut, ConnectionData, EventData, DomainObjectState, AirdropEvent, AirdropMessage, ExtractorEvent, SyncMode, ExternalSystemAttachmentStreamingParams, ExternalSystemAttachmentStreamingResponse, ExternalSystemAttachmentStreamingFunction, ExternalProcessAttachmentFunction, ExternalSystemAttachmentReducerFunction, ExternalSystemAttachmentIteratorFunction, } from './extraction';
|
|
2
|
+
export { EventType, ExtractorEventType, ExtractionMode, ExternalSyncUnit, EventContextIn, EventContextOut, ConnectionData, EventData, DomainObjectState, AirdropEvent, AirdropMessage, ExtractorEvent, SyncMode, ExternalSystemAttachmentStreamingParams, ExternalSystemAttachmentStreamingResponse, ExternalSystemAttachmentStreamingFunction, ExternalProcessAttachmentFunction, ExternalSystemAttachmentReducerFunction, ExternalSystemAttachmentIteratorFunction, ProcessAttachmentReturnType, } from './extraction';
|
|
3
3
|
export { LoaderEventType, ExternalSystemItem, ExternalSystemItemLoadingResponse, ExternalSystemItemLoadingParams, ExternalSystemAttachment, } from './loading';
|
|
4
4
|
export { NormalizedItem, NormalizedAttachment, RepoInterface, } from '../repo/repo.interfaces';
|
|
5
5
|
export { AdapterState } from '../state/state.interfaces';
|
|
@@ -68,27 +68,6 @@ export declare class WorkerAdapter<ConnectorState> {
|
|
|
68
68
|
item: ExternalSystemAttachment;
|
|
69
69
|
create: ExternalSystemLoadingFunction<ExternalSystemAttachment>;
|
|
70
70
|
}): Promise<LoadItemResponse>;
|
|
71
|
-
/**
|
|
72
|
-
* Transforms an array of attachments into array of batches of the specified size.
|
|
73
|
-
*
|
|
74
|
-
* @param {Object} parameters - The parameters object
|
|
75
|
-
* @param {NormalizedAttachment[]} parameters.attachments - Array of attachments to be processed
|
|
76
|
-
* @param {number} [parameters.batchSize=1] - The size of each batch (defaults to 1)
|
|
77
|
-
* @param {ConnectorState} parameters.adapter - The adapter instance
|
|
78
|
-
* @returns {NormalizedAttachment[][]} An array of attachment batches
|
|
79
|
-
*/
|
|
80
|
-
private defaultAttachmentsReducer;
|
|
81
|
-
/**
|
|
82
|
-
* This iterator function processes attachments batch by batch, saves progress to state, and handles rate limiting.
|
|
83
|
-
*
|
|
84
|
-
* @param {Object} parameters - The parameters object
|
|
85
|
-
* @param {NormalizedAttachment[][]} parameters.reducedAttachments - Array of attachment batches to process
|
|
86
|
-
* @param {Object} parameters.adapter - The connector adapter that contains state and processing methods
|
|
87
|
-
* @param {Object} parameters.stream - Stream object for logging or progress reporting
|
|
88
|
-
* @returns {Promise<{delay?: number} | void>} Returns an object with delay information if rate-limited, otherwise void
|
|
89
|
-
* @throws Will not throw exceptions but will log warnings for processing failures
|
|
90
|
-
*/
|
|
91
|
-
private defaultAttachmentsIterator;
|
|
92
71
|
/**
|
|
93
72
|
* Streams the attachments to the DevRev platform.
|
|
94
73
|
* The attachments are streamed to the platform and the artifact information is returned.
|
|
@@ -15,7 +15,7 @@ const mappers_1 = require("../mappers/mappers");
|
|
|
15
15
|
const uploader_1 = require("../uploader/uploader");
|
|
16
16
|
const logger_1 = require("../logger/logger");
|
|
17
17
|
const mappers_interface_1 = require("../mappers/mappers.interface");
|
|
18
|
-
const
|
|
18
|
+
const attachments_streaming_pool_1 = require("../attachments-streaming/attachments-streaming-pool");
|
|
19
19
|
function createWorkerAdapter({ event, adapterState, options, }) {
|
|
20
20
|
return new WorkerAdapter({
|
|
21
21
|
event,
|
|
@@ -40,108 +40,6 @@ function createWorkerAdapter({ event, adapterState, options, }) {
|
|
|
40
40
|
class WorkerAdapter {
|
|
41
41
|
constructor({ event, adapterState, options, }) {
|
|
42
42
|
this.repos = [];
|
|
43
|
-
/**
|
|
44
|
-
* Transforms an array of attachments into array of batches of the specified size.
|
|
45
|
-
*
|
|
46
|
-
* @param {Object} parameters - The parameters object
|
|
47
|
-
* @param {NormalizedAttachment[]} parameters.attachments - Array of attachments to be processed
|
|
48
|
-
* @param {number} [parameters.batchSize=1] - The size of each batch (defaults to 1)
|
|
49
|
-
* @param {ConnectorState} parameters.adapter - The adapter instance
|
|
50
|
-
* @returns {NormalizedAttachment[][]} An array of attachment batches
|
|
51
|
-
*/
|
|
52
|
-
this.defaultAttachmentsReducer = ({ attachments, batchSize = 1 }) => {
|
|
53
|
-
// Transform the attachments array into smaller batches
|
|
54
|
-
const batches = attachments.reduce((result, item, index) => {
|
|
55
|
-
// Determine the index of the current batch
|
|
56
|
-
const batchIndex = Math.floor(index / batchSize);
|
|
57
|
-
// Initialize a new batch if it doesn't already exist
|
|
58
|
-
if (!result[batchIndex]) {
|
|
59
|
-
result[batchIndex] = [];
|
|
60
|
-
}
|
|
61
|
-
// Append the current item to the current batch
|
|
62
|
-
result[batchIndex].push(item);
|
|
63
|
-
return result;
|
|
64
|
-
}, []);
|
|
65
|
-
// Return the array of batches
|
|
66
|
-
return batches;
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* This iterator function processes attachments batch by batch, saves progress to state, and handles rate limiting.
|
|
70
|
-
*
|
|
71
|
-
* @param {Object} parameters - The parameters object
|
|
72
|
-
* @param {NormalizedAttachment[][]} parameters.reducedAttachments - Array of attachment batches to process
|
|
73
|
-
* @param {Object} parameters.adapter - The connector adapter that contains state and processing methods
|
|
74
|
-
* @param {Object} parameters.stream - Stream object for logging or progress reporting
|
|
75
|
-
* @returns {Promise<{delay?: number} | void>} Returns an object with delay information if rate-limited, otherwise void
|
|
76
|
-
* @throws Will not throw exceptions but will log warnings for processing failures
|
|
77
|
-
*/
|
|
78
|
-
this.defaultAttachmentsIterator = async ({ reducedAttachments, adapter, stream }) => {
|
|
79
|
-
if (!adapter.state.toDevRev) {
|
|
80
|
-
const error = new Error(`toDevRev state is not defined.`);
|
|
81
|
-
console.error(error.message);
|
|
82
|
-
return { error };
|
|
83
|
-
}
|
|
84
|
-
// Get index of the last processed batch of this artifact
|
|
85
|
-
const lastProcessedBatchIndex = adapter.state.toDevRev.attachmentsMetadata.lastProcessed || 0;
|
|
86
|
-
// Get the list of successfully processed attachments in previous (possibly incomplete) batch extraction.
|
|
87
|
-
// If no such list exists, create an empty one.
|
|
88
|
-
if (!adapter.state.toDevRev.attachmentsMetadata
|
|
89
|
-
.lastProcessedAttachmentsIdsList) {
|
|
90
|
-
adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList =
|
|
91
|
-
[];
|
|
92
|
-
}
|
|
93
|
-
// Loop through the batches of attachments
|
|
94
|
-
for (let i = lastProcessedBatchIndex; i < reducedAttachments.length; i++) {
|
|
95
|
-
// Check if we hit timeout
|
|
96
|
-
if (adapter.isTimeout) {
|
|
97
|
-
await (0, helpers_2.sleep)(constants_1.DEFAULT_SLEEP_DELAY_MS);
|
|
98
|
-
}
|
|
99
|
-
const attachmentsBatch = reducedAttachments[i];
|
|
100
|
-
// Create a list of promises for parallel processing
|
|
101
|
-
const promises = [];
|
|
102
|
-
for (const attachment of attachmentsBatch) {
|
|
103
|
-
if (adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList.includes(attachment.id)) {
|
|
104
|
-
console.log(`Attachment with ID ${attachment.id} has already been processed. Skipping.`);
|
|
105
|
-
continue; // Skip if the attachment ID is already processed
|
|
106
|
-
}
|
|
107
|
-
const promise = adapter
|
|
108
|
-
.processAttachment(attachment, stream)
|
|
109
|
-
.then((response) => {
|
|
110
|
-
var _a, _b, _c;
|
|
111
|
-
// Check if rate limit was hit
|
|
112
|
-
if (response === null || response === void 0 ? void 0 : response.delay) {
|
|
113
|
-
// Store this promise result to be checked later
|
|
114
|
-
return { delay: response.delay };
|
|
115
|
-
}
|
|
116
|
-
// No rate limiting, process normally
|
|
117
|
-
if ((_b = (_a = adapter.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata) === null || _b === void 0 ? void 0 : _b.lastProcessedAttachmentsIdsList) {
|
|
118
|
-
(_c = adapter.state.toDevRev) === null || _c === void 0 ? void 0 : _c.attachmentsMetadata.lastProcessedAttachmentsIdsList.push(attachment.id);
|
|
119
|
-
}
|
|
120
|
-
return null; // Return null for successful processing
|
|
121
|
-
})
|
|
122
|
-
.catch((error) => {
|
|
123
|
-
console.warn(`Skipping attachment with ID ${attachment.id} due to error: ${error}`);
|
|
124
|
-
return null; // Return null for errors too
|
|
125
|
-
});
|
|
126
|
-
promises.push(promise);
|
|
127
|
-
}
|
|
128
|
-
// Wait for all promises to settle and check for rate limiting
|
|
129
|
-
const results = await Promise.all(promises);
|
|
130
|
-
// Check if any of the results indicate rate limiting
|
|
131
|
-
const rateLimit = results.find((result) => result === null || result === void 0 ? void 0 : result.delay);
|
|
132
|
-
if (rateLimit) {
|
|
133
|
-
// Return the delay information to the caller
|
|
134
|
-
return { delay: rateLimit.delay };
|
|
135
|
-
}
|
|
136
|
-
if (adapter.state.toDevRev) {
|
|
137
|
-
// Update the last processed batch index
|
|
138
|
-
adapter.state.toDevRev.attachmentsMetadata.lastProcessed = i + 1;
|
|
139
|
-
// Reset successfullyProcessedAttachments list
|
|
140
|
-
adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList.length = 0;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return {};
|
|
144
|
-
};
|
|
145
43
|
this.event = event;
|
|
146
44
|
this.options = options;
|
|
147
45
|
this.adapterState = adapterState;
|
|
@@ -700,18 +598,18 @@ class WorkerAdapter {
|
|
|
700
598
|
},
|
|
701
599
|
];
|
|
702
600
|
this.initializeRepos(repos);
|
|
601
|
+
const attachmentsMetadata = (_a = this.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata;
|
|
703
602
|
// If there are no attachments metadata artifact IDs in state, finish here
|
|
704
|
-
if (!((_b =
|
|
705
|
-
this.state.toDevRev.attachmentsMetadata.artifactIds.length === 0) {
|
|
603
|
+
if (!((_b = attachmentsMetadata === null || attachmentsMetadata === void 0 ? void 0 : attachmentsMetadata.artifactIds) === null || _b === void 0 ? void 0 : _b.length)) {
|
|
706
604
|
console.log(`No attachments metadata artifact IDs found in state.`);
|
|
707
605
|
return;
|
|
708
606
|
}
|
|
709
607
|
else {
|
|
710
|
-
console.log(`Found ${
|
|
608
|
+
console.log(`Found ${attachmentsMetadata.artifactIds.length} attachments metadata artifact IDs in state.`);
|
|
711
609
|
}
|
|
712
610
|
// Loop through the attachments metadata artifact IDs
|
|
713
|
-
while (
|
|
714
|
-
const attachmentsMetadataArtifactId =
|
|
611
|
+
while (attachmentsMetadata.artifactIds.length > 0) {
|
|
612
|
+
const attachmentsMetadataArtifactId = attachmentsMetadata.artifactIds[0];
|
|
715
613
|
console.log(`Started processing attachments for attachments metadata artifact ID: ${attachmentsMetadataArtifactId}.`);
|
|
716
614
|
const { attachments, error } = await this.uploader.getAttachmentsFromArtifactId({
|
|
717
615
|
artifact: attachmentsMetadataArtifactId,
|
|
@@ -723,43 +621,46 @@ class WorkerAdapter {
|
|
|
723
621
|
if (!attachments || attachments.length === 0) {
|
|
724
622
|
console.warn(`No attachments found for artifact ID: ${attachmentsMetadataArtifactId}.`);
|
|
725
623
|
// Remove empty artifact and reset lastProcessed
|
|
726
|
-
|
|
727
|
-
|
|
624
|
+
attachmentsMetadata.artifactIds.shift();
|
|
625
|
+
attachmentsMetadata.lastProcessed = 0;
|
|
728
626
|
continue;
|
|
729
627
|
}
|
|
730
628
|
console.log(`Found ${attachments.length} attachments for artifact ID: ${attachmentsMetadataArtifactId}.`);
|
|
731
|
-
|
|
732
|
-
let reducer;
|
|
733
|
-
// Use the iterator to process each batch, streaming all attachments inside one batch in parallel.
|
|
734
|
-
let iterator;
|
|
629
|
+
let response;
|
|
735
630
|
if (processors) {
|
|
736
631
|
console.log(`Using custom processors for attachments.`);
|
|
737
|
-
reducer = processors.reducer;
|
|
738
|
-
iterator = processors.iterator;
|
|
632
|
+
const reducer = processors.reducer;
|
|
633
|
+
const iterator = processors.iterator;
|
|
634
|
+
const reducedAttachments = reducer({
|
|
635
|
+
attachments,
|
|
636
|
+
adapter: this,
|
|
637
|
+
batchSize,
|
|
638
|
+
});
|
|
639
|
+
response = await iterator({
|
|
640
|
+
reducedAttachments,
|
|
641
|
+
adapter: this,
|
|
642
|
+
stream,
|
|
643
|
+
});
|
|
739
644
|
}
|
|
740
645
|
else {
|
|
741
|
-
console.log(`Using
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
646
|
+
console.log(`Using attachments streaming pool for attachments streaming.`);
|
|
647
|
+
const attachmentsPool = new attachments_streaming_pool_1.AttachmentsStreamingPool({
|
|
648
|
+
adapter: this,
|
|
649
|
+
attachments,
|
|
650
|
+
batchSize,
|
|
651
|
+
stream,
|
|
652
|
+
});
|
|
653
|
+
response = await attachmentsPool.streamAll();
|
|
746
654
|
}
|
|
747
|
-
const reducedAttachments = reducer({
|
|
748
|
-
attachments,
|
|
749
|
-
adapter: this,
|
|
750
|
-
batchSize,
|
|
751
|
-
});
|
|
752
|
-
const response = await iterator({
|
|
753
|
-
reducedAttachments,
|
|
754
|
-
adapter: this,
|
|
755
|
-
stream,
|
|
756
|
-
});
|
|
757
655
|
if ((response === null || response === void 0 ? void 0 : response.delay) || (response === null || response === void 0 ? void 0 : response.error)) {
|
|
758
656
|
return response;
|
|
759
657
|
}
|
|
760
658
|
console.log(`Finished processing all attachments for artifact ID: ${attachmentsMetadataArtifactId}.`);
|
|
761
|
-
|
|
762
|
-
|
|
659
|
+
attachmentsMetadata.artifactIds.shift();
|
|
660
|
+
attachmentsMetadata.lastProcessed = 0;
|
|
661
|
+
if (attachmentsMetadata.lastProcessedAttachmentsIdsList) {
|
|
662
|
+
attachmentsMetadata.lastProcessedAttachmentsIdsList.length = 0;
|
|
663
|
+
}
|
|
763
664
|
}
|
|
764
665
|
return;
|
|
765
666
|
}
|
|
@@ -4,6 +4,7 @@ const worker_adapter_1 = require("./worker-adapter");
|
|
|
4
4
|
const state_1 = require("../state/state");
|
|
5
5
|
const test_helpers_1 = require("../tests/test-helpers");
|
|
6
6
|
const types_1 = require("../types");
|
|
7
|
+
const attachments_streaming_pool_1 = require("../attachments-streaming/attachments-streaming-pool");
|
|
7
8
|
// Mock dependencies
|
|
8
9
|
jest.mock('../common/control-protocol', () => ({
|
|
9
10
|
emit: jest.fn().mockResolvedValue({}),
|
|
@@ -19,6 +20,15 @@ jest.mock('node:worker_threads', () => ({
|
|
|
19
20
|
postMessage: jest.fn(),
|
|
20
21
|
},
|
|
21
22
|
}));
|
|
23
|
+
jest.mock('../attachments-streaming/attachments-streaming-pool', () => {
|
|
24
|
+
return {
|
|
25
|
+
AttachmentsStreamingPool: jest.fn().mockImplementation(() => {
|
|
26
|
+
return {
|
|
27
|
+
streamAll: jest.fn().mockResolvedValue(undefined),
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
22
32
|
describe('WorkerAdapter', () => {
|
|
23
33
|
let adapter;
|
|
24
34
|
let mockEvent;
|
|
@@ -51,269 +61,6 @@ describe('WorkerAdapter', () => {
|
|
|
51
61
|
adapterState: mockAdapterState,
|
|
52
62
|
});
|
|
53
63
|
});
|
|
54
|
-
describe('defaultAttachmentsReducer', () => {
|
|
55
|
-
it('should correctly batch attachments based on the batchSize', () => {
|
|
56
|
-
// Arrange
|
|
57
|
-
const attachments = [
|
|
58
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
59
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
60
|
-
{ url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
|
|
61
|
-
{ url: 'http://example.com/file4.pdf', id: 'attachment4', file_name: 'file4.pdf', parent_id: 'parent4' },
|
|
62
|
-
{ url: 'http://example.com/file5.pdf', id: 'attachment5', file_name: 'file5.pdf', parent_id: 'parent5' },
|
|
63
|
-
];
|
|
64
|
-
// Act - call the private method using function call notation
|
|
65
|
-
const result = adapter['defaultAttachmentsReducer']({
|
|
66
|
-
attachments,
|
|
67
|
-
adapter,
|
|
68
|
-
batchSize: 2,
|
|
69
|
-
});
|
|
70
|
-
// Assert
|
|
71
|
-
expect(result).toHaveLength(3);
|
|
72
|
-
expect(result[0]).toHaveLength(2);
|
|
73
|
-
expect(result[1]).toHaveLength(2);
|
|
74
|
-
expect(result[2]).toHaveLength(1);
|
|
75
|
-
expect(result[0][0].id).toBe('attachment1');
|
|
76
|
-
expect(result[0][1].id).toBe('attachment2');
|
|
77
|
-
expect(result[1][0].id).toBe('attachment3');
|
|
78
|
-
expect(result[1][1].id).toBe('attachment4');
|
|
79
|
-
expect(result[2][0].id).toBe('attachment5');
|
|
80
|
-
});
|
|
81
|
-
it('should return a single batch when batchSize equals the number of attachments', () => {
|
|
82
|
-
// Arrange
|
|
83
|
-
const attachments = [
|
|
84
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
85
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
86
|
-
];
|
|
87
|
-
// Act
|
|
88
|
-
const result = adapter['defaultAttachmentsReducer']({
|
|
89
|
-
attachments,
|
|
90
|
-
adapter,
|
|
91
|
-
batchSize: 2,
|
|
92
|
-
});
|
|
93
|
-
// Assert
|
|
94
|
-
expect(result).toHaveLength(1);
|
|
95
|
-
expect(result[0]).toHaveLength(2);
|
|
96
|
-
expect(result[0][0].id).toBe('attachment1');
|
|
97
|
-
expect(result[0][1].id).toBe('attachment2');
|
|
98
|
-
});
|
|
99
|
-
it('should return a single batch when batchSize is bigger than the number of attachments', () => {
|
|
100
|
-
// Arrange
|
|
101
|
-
const attachments = [
|
|
102
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
103
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
104
|
-
];
|
|
105
|
-
// Act
|
|
106
|
-
const result = adapter['defaultAttachmentsReducer']({
|
|
107
|
-
attachments,
|
|
108
|
-
adapter,
|
|
109
|
-
batchSize: 10,
|
|
110
|
-
});
|
|
111
|
-
// Assert
|
|
112
|
-
expect(result).toHaveLength(1);
|
|
113
|
-
expect(result[0]).toHaveLength(2);
|
|
114
|
-
expect(result[0][0].id).toBe('attachment1');
|
|
115
|
-
expect(result[0][1].id).toBe('attachment2');
|
|
116
|
-
});
|
|
117
|
-
it('should handle empty attachments array', () => {
|
|
118
|
-
// Arrange
|
|
119
|
-
const attachments = [];
|
|
120
|
-
// Act
|
|
121
|
-
const result = adapter['defaultAttachmentsReducer']({
|
|
122
|
-
attachments,
|
|
123
|
-
adapter,
|
|
124
|
-
batchSize: 2,
|
|
125
|
-
});
|
|
126
|
-
// Assert
|
|
127
|
-
expect(result).toHaveLength(0);
|
|
128
|
-
});
|
|
129
|
-
it('should default to batchSize of 1 if not provided', () => {
|
|
130
|
-
// Arrange
|
|
131
|
-
const attachments = [
|
|
132
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
133
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
134
|
-
];
|
|
135
|
-
// Act
|
|
136
|
-
const result = adapter['defaultAttachmentsReducer']({
|
|
137
|
-
attachments,
|
|
138
|
-
adapter
|
|
139
|
-
});
|
|
140
|
-
// Assert
|
|
141
|
-
expect(result).toHaveLength(2);
|
|
142
|
-
expect(result[0]).toHaveLength(1);
|
|
143
|
-
expect(result[1]).toHaveLength(1);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
describe('defaultAttachmentsIterator', () => {
|
|
147
|
-
it('should process all batches of attachments', async () => {
|
|
148
|
-
var _a, _b;
|
|
149
|
-
// Arrange
|
|
150
|
-
const mockAttachments = [
|
|
151
|
-
[
|
|
152
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
153
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
154
|
-
],
|
|
155
|
-
[
|
|
156
|
-
{ url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
|
|
157
|
-
],
|
|
158
|
-
];
|
|
159
|
-
const mockStream = jest.fn();
|
|
160
|
-
// Mock the processAttachment method
|
|
161
|
-
adapter.processAttachment = jest.fn().mockResolvedValue(null);
|
|
162
|
-
// Act
|
|
163
|
-
const result = await adapter['defaultAttachmentsIterator']({
|
|
164
|
-
reducedAttachments: mockAttachments,
|
|
165
|
-
adapter: adapter,
|
|
166
|
-
stream: mockStream,
|
|
167
|
-
});
|
|
168
|
-
// Assert
|
|
169
|
-
expect(adapter.processAttachment).toHaveBeenCalledTimes(3);
|
|
170
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
|
|
171
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][1], mockStream);
|
|
172
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[1][0], mockStream);
|
|
173
|
-
// Verify the state was updated correctly
|
|
174
|
-
expect((_a = adapter.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata.lastProcessed).toBe(2);
|
|
175
|
-
expect((_b = adapter.state.toDevRev) === null || _b === void 0 ? void 0 : _b.attachmentsMetadata.lastProcessedAttachmentsIdsList).toEqual([]);
|
|
176
|
-
expect(result).toEqual({});
|
|
177
|
-
});
|
|
178
|
-
it('should handle rate limiting during processing', async () => {
|
|
179
|
-
var _a;
|
|
180
|
-
// Arrange
|
|
181
|
-
const mockAttachments = [
|
|
182
|
-
[
|
|
183
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
184
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
185
|
-
],
|
|
186
|
-
[
|
|
187
|
-
{ url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
|
|
188
|
-
],
|
|
189
|
-
];
|
|
190
|
-
const mockStream = jest.fn();
|
|
191
|
-
// Mock the processAttachment method to simulate rate limiting on the second attachment
|
|
192
|
-
adapter.processAttachment = jest.fn()
|
|
193
|
-
.mockResolvedValueOnce(null) // First attachment processes successfully
|
|
194
|
-
.mockResolvedValueOnce({ delay: 30 }); // Second attachment hits rate limit
|
|
195
|
-
// Set up adapter state
|
|
196
|
-
adapter.state.toDevRev = {
|
|
197
|
-
attachmentsMetadata: {
|
|
198
|
-
lastProcessed: 0,
|
|
199
|
-
artifactIds: [],
|
|
200
|
-
lastProcessedAttachmentsIdsList: [],
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
// Act
|
|
204
|
-
const result = await adapter['defaultAttachmentsIterator']({
|
|
205
|
-
reducedAttachments: mockAttachments,
|
|
206
|
-
adapter: adapter,
|
|
207
|
-
stream: mockStream,
|
|
208
|
-
});
|
|
209
|
-
// Assert
|
|
210
|
-
expect(adapter.processAttachment).toHaveBeenCalledTimes(2);
|
|
211
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
|
|
212
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][1], mockStream);
|
|
213
|
-
// Verify the delay was returned
|
|
214
|
-
expect(result).toEqual({ delay: 30 });
|
|
215
|
-
// And lastProcessed wasn't updated yet
|
|
216
|
-
expect((_a = adapter.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata.lastProcessed).toBe(0);
|
|
217
|
-
});
|
|
218
|
-
it('should skip already processed attachments', async () => {
|
|
219
|
-
// Arrange
|
|
220
|
-
const mockAttachments = [
|
|
221
|
-
[
|
|
222
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
223
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
224
|
-
],
|
|
225
|
-
];
|
|
226
|
-
const mockStream = jest.fn();
|
|
227
|
-
// Mock the processAttachment method
|
|
228
|
-
adapter.processAttachment = jest.fn().mockResolvedValue(null);
|
|
229
|
-
// Set up adapter state to indicate attachment1 was already processed
|
|
230
|
-
adapter.state.toDevRev = {
|
|
231
|
-
attachmentsMetadata: {
|
|
232
|
-
lastProcessed: 0,
|
|
233
|
-
artifactIds: [],
|
|
234
|
-
lastProcessedAttachmentsIdsList: ['attachment1'],
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
// Act
|
|
238
|
-
await adapter['defaultAttachmentsIterator']({
|
|
239
|
-
reducedAttachments: mockAttachments,
|
|
240
|
-
adapter: adapter,
|
|
241
|
-
stream: mockStream,
|
|
242
|
-
});
|
|
243
|
-
// Assert
|
|
244
|
-
expect(adapter.processAttachment).toHaveBeenCalledTimes(1);
|
|
245
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][1], mockStream);
|
|
246
|
-
expect(adapter.processAttachment).not.toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
|
|
247
|
-
});
|
|
248
|
-
it('should continue from last processed batch', async () => {
|
|
249
|
-
// Arrange
|
|
250
|
-
const mockAttachments = [
|
|
251
|
-
[
|
|
252
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
253
|
-
],
|
|
254
|
-
[
|
|
255
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
256
|
-
],
|
|
257
|
-
];
|
|
258
|
-
const mockStream = jest.fn();
|
|
259
|
-
// Mock the processAttachment method
|
|
260
|
-
adapter.processAttachment = jest.fn().mockResolvedValue(null);
|
|
261
|
-
// Set up adapter state to indicate we already processed the first batch
|
|
262
|
-
adapter.state.toDevRev = {
|
|
263
|
-
attachmentsMetadata: {
|
|
264
|
-
lastProcessed: 1, // Skip first batch (index 0)
|
|
265
|
-
artifactIds: [],
|
|
266
|
-
lastProcessedAttachmentsIdsList: [],
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
// Act
|
|
270
|
-
await adapter['defaultAttachmentsIterator']({
|
|
271
|
-
reducedAttachments: mockAttachments,
|
|
272
|
-
adapter: adapter,
|
|
273
|
-
stream: mockStream,
|
|
274
|
-
});
|
|
275
|
-
// Assert
|
|
276
|
-
expect(adapter.processAttachment).toHaveBeenCalledTimes(1);
|
|
277
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[1][0], mockStream);
|
|
278
|
-
expect(adapter.processAttachment).not.toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
|
|
279
|
-
});
|
|
280
|
-
it('should handle errors during processing and continue', async () => {
|
|
281
|
-
// Arrange
|
|
282
|
-
const mockAttachments = [
|
|
283
|
-
[
|
|
284
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
285
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
286
|
-
],
|
|
287
|
-
];
|
|
288
|
-
const mockStream = jest.fn();
|
|
289
|
-
// Mock processAttachment to throw an error for the first attachment
|
|
290
|
-
adapter.processAttachment = jest.fn()
|
|
291
|
-
.mockRejectedValueOnce(new Error('Processing error'))
|
|
292
|
-
.mockResolvedValueOnce(null);
|
|
293
|
-
// Mock console.warn to avoid test output noise
|
|
294
|
-
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
295
|
-
// Set up adapter state
|
|
296
|
-
adapter.state.toDevRev = {
|
|
297
|
-
attachmentsMetadata: {
|
|
298
|
-
lastProcessed: 0,
|
|
299
|
-
artifactIds: [],
|
|
300
|
-
lastProcessedAttachmentsIdsList: [],
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
// Act
|
|
304
|
-
const result = await adapter['defaultAttachmentsIterator']({
|
|
305
|
-
reducedAttachments: mockAttachments,
|
|
306
|
-
adapter: adapter,
|
|
307
|
-
stream: mockStream,
|
|
308
|
-
});
|
|
309
|
-
// Assert - both attachments should have been processed
|
|
310
|
-
expect(adapter.processAttachment).toHaveBeenCalledTimes(2);
|
|
311
|
-
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
312
|
-
expect(result).toEqual({});
|
|
313
|
-
// Restore console.warn
|
|
314
|
-
consoleWarnSpy.mockRestore();
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
64
|
describe('streamAttachments', () => {
|
|
318
65
|
it('should process all artifact batches successfully', async () => {
|
|
319
66
|
// Arrange
|
|
@@ -341,11 +88,6 @@ describe('WorkerAdapter', () => {
|
|
|
341
88
|
});
|
|
342
89
|
// Mock the initializeRepos method
|
|
343
90
|
adapter.initializeRepos = jest.fn();
|
|
344
|
-
// Mock the defaultAttachmentsReducer and defaultAttachmentsIterator
|
|
345
|
-
const mockReducedAttachments = [['batch1']];
|
|
346
|
-
adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue(mockReducedAttachments);
|
|
347
|
-
adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({});
|
|
348
|
-
// Act
|
|
349
91
|
const result = await adapter.streamAttachments({
|
|
350
92
|
stream: mockStream,
|
|
351
93
|
});
|
|
@@ -353,9 +95,8 @@ describe('WorkerAdapter', () => {
|
|
|
353
95
|
expect(adapter.initializeRepos).toHaveBeenCalledWith([
|
|
354
96
|
{ itemType: 'ssor_attachment' },
|
|
355
97
|
]);
|
|
98
|
+
expect(adapter.initializeRepos).toHaveBeenCalledTimes(1);
|
|
356
99
|
expect(adapter['uploader'].getAttachmentsFromArtifactId).toHaveBeenCalledTimes(2);
|
|
357
|
-
expect(adapter['defaultAttachmentsReducer']).toHaveBeenCalledTimes(2);
|
|
358
|
-
expect(adapter['defaultAttachmentsIterator']).toHaveBeenCalledTimes(2);
|
|
359
100
|
// Verify state was updated correctly
|
|
360
101
|
expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toEqual([]);
|
|
361
102
|
expect(adapter.state.toDevRev.attachmentsMetadata.lastProcessed).toBe(0);
|
|
@@ -379,11 +120,7 @@ describe('WorkerAdapter', () => {
|
|
|
379
120
|
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
380
121
|
],
|
|
381
122
|
});
|
|
382
|
-
// Mock the required methods
|
|
383
123
|
adapter.initializeRepos = jest.fn();
|
|
384
|
-
const mockReducedAttachments = [['batch1']];
|
|
385
|
-
adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue(mockReducedAttachments);
|
|
386
|
-
adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({});
|
|
387
124
|
// Act
|
|
388
125
|
const result = await adapter.streamAttachments({
|
|
389
126
|
stream: mockStream,
|
|
@@ -391,12 +128,6 @@ describe('WorkerAdapter', () => {
|
|
|
391
128
|
});
|
|
392
129
|
// Assert
|
|
393
130
|
expect(consoleWarnSpy).toHaveBeenCalledWith('The specified batch size (0) is invalid. Using 1 instead.');
|
|
394
|
-
// Verify that the reducer was called with batchSize 50 (not 100)
|
|
395
|
-
expect(adapter['defaultAttachmentsReducer']).toHaveBeenCalledWith({
|
|
396
|
-
attachments: expect.any(Array),
|
|
397
|
-
adapter: adapter,
|
|
398
|
-
batchSize: 1,
|
|
399
|
-
});
|
|
400
131
|
expect(result).toBeUndefined();
|
|
401
132
|
// Restore console.warn
|
|
402
133
|
consoleWarnSpy.mockRestore();
|
|
@@ -421,9 +152,6 @@ describe('WorkerAdapter', () => {
|
|
|
421
152
|
});
|
|
422
153
|
// Mock the required methods
|
|
423
154
|
adapter.initializeRepos = jest.fn();
|
|
424
|
-
const mockReducedAttachments = [['batch1']];
|
|
425
|
-
adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue(mockReducedAttachments);
|
|
426
|
-
adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({});
|
|
427
155
|
// Act
|
|
428
156
|
const result = await adapter.streamAttachments({
|
|
429
157
|
stream: mockStream,
|
|
@@ -431,12 +159,6 @@ describe('WorkerAdapter', () => {
|
|
|
431
159
|
});
|
|
432
160
|
// Assert
|
|
433
161
|
expect(consoleWarnSpy).toHaveBeenCalledWith('The specified batch size (100) is too large. Using 50 instead.');
|
|
434
|
-
// Verify that the reducer was called with batchSize 50 (not 100)
|
|
435
|
-
expect(adapter['defaultAttachmentsReducer']).toHaveBeenCalledWith({
|
|
436
|
-
attachments: expect.any(Array),
|
|
437
|
-
adapter: adapter,
|
|
438
|
-
batchSize: 50, // Should be capped at 50
|
|
439
|
-
});
|
|
440
162
|
expect(result).toBeUndefined();
|
|
441
163
|
// Restore console.warn
|
|
442
164
|
consoleWarnSpy.mockRestore();
|
|
@@ -568,6 +290,12 @@ describe('WorkerAdapter', () => {
|
|
|
568
290
|
it('should handle rate limiting from iterator', async () => {
|
|
569
291
|
// Arrange
|
|
570
292
|
const mockStream = jest.fn();
|
|
293
|
+
attachments_streaming_pool_1.AttachmentsStreamingPool.mockImplementationOnce(() => {
|
|
294
|
+
return {
|
|
295
|
+
// Return an object with a `streamAll` method that resolves to your desired value.
|
|
296
|
+
streamAll: jest.fn().mockResolvedValue({ delay: 30 }),
|
|
297
|
+
};
|
|
298
|
+
});
|
|
571
299
|
// Set up adapter state with artifact IDs
|
|
572
300
|
adapter.state.toDevRev = {
|
|
573
301
|
attachmentsMetadata: {
|
|
@@ -582,10 +310,6 @@ describe('WorkerAdapter', () => {
|
|
|
582
310
|
});
|
|
583
311
|
// Mock methods
|
|
584
312
|
adapter.initializeRepos = jest.fn();
|
|
585
|
-
adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue([]);
|
|
586
|
-
adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({
|
|
587
|
-
delay: 30,
|
|
588
|
-
});
|
|
589
313
|
// Act
|
|
590
314
|
const result = await adapter.streamAttachments({
|
|
591
315
|
stream: mockStream,
|
|
@@ -600,6 +324,14 @@ describe('WorkerAdapter', () => {
|
|
|
600
324
|
it('should handle error from iterator', async () => {
|
|
601
325
|
// Arrange
|
|
602
326
|
const mockStream = jest.fn();
|
|
327
|
+
attachments_streaming_pool_1.AttachmentsStreamingPool.mockImplementationOnce(() => {
|
|
328
|
+
return {
|
|
329
|
+
// Return an object with a `streamAll` method that resolves to your desired value.
|
|
330
|
+
streamAll: jest.fn().mockResolvedValue({
|
|
331
|
+
error: "Mock error",
|
|
332
|
+
}),
|
|
333
|
+
};
|
|
334
|
+
});
|
|
603
335
|
// Set up adapter state with artifact IDs
|
|
604
336
|
adapter.state.toDevRev = {
|
|
605
337
|
attachmentsMetadata: {
|
|
@@ -614,51 +346,17 @@ describe('WorkerAdapter', () => {
|
|
|
614
346
|
});
|
|
615
347
|
// Mock methods
|
|
616
348
|
adapter.initializeRepos = jest.fn();
|
|
617
|
-
adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue([]);
|
|
618
|
-
const mockError = new Error('Iterator error');
|
|
619
|
-
adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({
|
|
620
|
-
error: mockError,
|
|
621
|
-
});
|
|
622
349
|
// Act
|
|
623
350
|
const result = await adapter.streamAttachments({
|
|
624
351
|
stream: mockStream,
|
|
625
352
|
});
|
|
626
353
|
// Assert
|
|
627
354
|
expect(result).toEqual({
|
|
628
|
-
error:
|
|
355
|
+
error: "Mock error",
|
|
629
356
|
});
|
|
630
357
|
// The artifactIds array should remain unchanged
|
|
631
358
|
expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toEqual(['artifact1']);
|
|
632
359
|
});
|
|
633
|
-
it('should continue processing from last processed attachment for the current artifact', async () => {
|
|
634
|
-
const mockStream = jest.fn();
|
|
635
|
-
adapter.state.toDevRev = {
|
|
636
|
-
attachmentsMetadata: {
|
|
637
|
-
artifactIds: ['artifact1'],
|
|
638
|
-
lastProcessed: 0,
|
|
639
|
-
lastProcessedAttachmentsIdsList: ['attachment1', 'attachment2'],
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
|
|
643
|
-
attachments: [
|
|
644
|
-
{ url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
|
|
645
|
-
{ url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
|
|
646
|
-
{ url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
|
|
647
|
-
],
|
|
648
|
-
});
|
|
649
|
-
adapter.processAttachment = jest.fn().mockResolvedValue(null);
|
|
650
|
-
await adapter.streamAttachments({
|
|
651
|
-
stream: mockStream,
|
|
652
|
-
batchSize: 3,
|
|
653
|
-
});
|
|
654
|
-
expect(adapter.processAttachment).toHaveBeenCalledTimes(1);
|
|
655
|
-
expect(adapter.processAttachment).toHaveBeenCalledWith({
|
|
656
|
-
url: 'http://example.com/file3.pdf',
|
|
657
|
-
id: 'attachment3',
|
|
658
|
-
file_name: 'file3.pdf',
|
|
659
|
-
parent_id: 'parent3'
|
|
660
|
-
}, mockStream);
|
|
661
|
-
});
|
|
662
360
|
it('should reset lastProcessed and attachment IDs list after processing all artifacts', async () => {
|
|
663
361
|
const mockStream = jest.fn();
|
|
664
362
|
adapter.state.toDevRev = {
|
package/package.json
CHANGED