@devrev/ts-adaas 1.20.2-beta.4 → 1.20.2-beta.6
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/common/constants.d.ts.map +1 -1
- package/dist/common/constants.js +1 -0
- package/dist/common/event-type-translation.d.ts.map +1 -1
- package/dist/common/event-type-translation.js +1 -0
- package/dist/common/event-type-translation.test.js +1 -0
- package/dist/http/axios-client.d.ts +6 -2
- package/dist/http/axios-client.d.ts.map +1 -1
- package/dist/http/axios-client.js +7 -4
- package/dist/mock-server/mock-server.d.ts +4 -0
- package/dist/mock-server/mock-server.d.ts.map +1 -1
- package/dist/mock-server/mock-server.interfaces.d.ts +9 -0
- package/dist/mock-server/mock-server.interfaces.d.ts.map +1 -1
- package/dist/mock-server/mock-server.js +39 -6
- package/dist/multithreading/process-task.d.ts.map +1 -1
- package/dist/multithreading/process-task.js +21 -6
- package/dist/multithreading/process-task.test.js +28 -1
- package/dist/multithreading/spawn/spawn.d.ts.map +1 -1
- package/dist/multithreading/spawn/spawn.helpers.d.ts.map +1 -1
- package/dist/multithreading/spawn/spawn.helpers.js +9 -0
- package/dist/multithreading/spawn/spawn.helpers.test.js +34 -0
- package/dist/multithreading/spawn/spawn.js +1 -0
- package/dist/multithreading/worker-adapter/worker-adapter.d.ts +13 -0
- package/dist/multithreading/worker-adapter/worker-adapter.d.ts.map +1 -1
- package/dist/multithreading/worker-adapter/worker-adapter.emit.test.js +51 -5
- package/dist/multithreading/worker-adapter/worker-adapter.js +131 -79
- package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.d.ts +2 -0
- package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.d.ts.map +1 -0
- package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.js +135 -0
- package/dist/repo/repo.d.ts +1 -2
- package/dist/repo/repo.d.ts.map +1 -1
- package/dist/repo/repo.js +6 -12
- package/dist/repo/repo.test.js +32 -1
- package/dist/state/state.d.ts.map +1 -1
- package/dist/state/state.js +9 -5
- package/dist/state/state.time-value-resolution.test.js +31 -2
- package/dist/types/extraction.d.ts +6 -3
- package/dist/types/extraction.d.ts.map +1 -1
- package/dist/types/extraction.js +3 -0
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ const workers_1 = require("../../types/workers");
|
|
|
22
22
|
const uploader_1 = require("../../uploader/uploader");
|
|
23
23
|
const event_type_translation_1 = require("../../common/event-type-translation");
|
|
24
24
|
const helpers_1 = require("../../common/helpers");
|
|
25
|
+
const spawn_helpers_1 = require("../spawn/spawn.helpers");
|
|
25
26
|
function createWorkerAdapter({ event, adapterState, options, }) {
|
|
26
27
|
return new WorkerAdapter({
|
|
27
28
|
event,
|
|
@@ -159,93 +160,49 @@ class WorkerAdapter {
|
|
|
159
160
|
*/
|
|
160
161
|
async emit(newEventType, data) {
|
|
161
162
|
return (0, logger_context_1.runWithSdkLogContext)(async () => {
|
|
162
|
-
var _a
|
|
163
|
+
var _a;
|
|
163
164
|
newEventType = (0, event_type_translation_1.translateOutgoingEventType)(newEventType);
|
|
164
165
|
if (this.hasWorkerEmitted) {
|
|
165
166
|
console.warn(`Trying to emit event with event type: ${newEventType}. Ignoring emit request because it has already been emitted.`);
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
-
// TODO: Remove in v2.0.0
|
|
170
|
-
if (newEventType === extraction_1.ExtractorEventType.ExternalSyncUnitExtractionDone &&
|
|
171
|
-
(data === null || data === void 0 ? void 0 : data.external_sync_units) &&
|
|
172
|
-
data.external_sync_units.length > 0) {
|
|
173
|
-
console.log(`Uploading ${data.external_sync_units.length} external sync units via repo before emitting event.`);
|
|
174
|
-
this.initializeRepos([
|
|
175
|
-
{
|
|
176
|
-
itemType: constants_1.AirSyncDefaultItemTypes.EXTERNAL_SYNC_UNITS,
|
|
177
|
-
overridenOptions: {
|
|
178
|
-
batchSize: 25000,
|
|
179
|
-
skipConfirmation: true,
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
]);
|
|
183
|
-
await ((_a = this.getRepo(constants_1.AirSyncDefaultItemTypes.EXTERNAL_SYNC_UNITS)) === null || _a === void 0 ? void 0 : _a.push(data.external_sync_units));
|
|
184
|
-
// Remove inline external_sync_units from data to avoid SQS size issues
|
|
185
|
-
delete data.external_sync_units;
|
|
186
|
-
}
|
|
187
|
-
// Upload all repos before emitting the event
|
|
188
|
-
console.log(`Uploading all repos before emitting event with event type: ${newEventType}.`);
|
|
169
|
+
let eventPayload;
|
|
189
170
|
try {
|
|
190
|
-
|
|
171
|
+
// If the event is ExternalSyncUnitExtractionDone, upload external sync units via a Repo before emitting.
|
|
172
|
+
// TODO: Remove in v2.0.0
|
|
173
|
+
if (newEventType === extraction_1.ExtractorEventType.ExternalSyncUnitExtractionDone &&
|
|
174
|
+
(data === null || data === void 0 ? void 0 : data.external_sync_units) &&
|
|
175
|
+
data.external_sync_units.length > 0) {
|
|
176
|
+
console.log(`Uploading ${data.external_sync_units.length} external sync units via repo before emitting event.`);
|
|
177
|
+
this.initializeRepos([
|
|
178
|
+
{
|
|
179
|
+
itemType: constants_1.AirSyncDefaultItemTypes.EXTERNAL_SYNC_UNITS,
|
|
180
|
+
overridenOptions: {
|
|
181
|
+
batchSize: 25000,
|
|
182
|
+
skipConfirmation: true,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
await ((_a = this.getRepo(constants_1.AirSyncDefaultItemTypes.EXTERNAL_SYNC_UNITS)) === null || _a === void 0 ? void 0 : _a.push(data.external_sync_units));
|
|
187
|
+
// Remove inline external_sync_units from data to avoid SQS size issues
|
|
188
|
+
delete data.external_sync_units;
|
|
189
|
+
}
|
|
190
|
+
await this.beforeEmit(newEventType);
|
|
191
|
+
eventPayload = this.buildEmitPayload(newEventType, data);
|
|
191
192
|
}
|
|
192
193
|
catch (error) {
|
|
193
|
-
console.error('Error while
|
|
194
|
-
|
|
195
|
-
this.hasWorkerEmitted = true;
|
|
194
|
+
console.error('Error while preparing event for emission.', (0, logger_1.serializeError)(error));
|
|
195
|
+
await this.emitError(error);
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
|
-
// If the extraction is done, we want to save the timestamp of the last successful sync
|
|
199
|
-
if (newEventType === extraction_1.ExtractorEventType.AttachmentExtractionDone) {
|
|
200
|
-
console.log(`Overwriting lastSuccessfulSyncStarted with lastSyncStarted (${this.state.lastSyncStarted}).`);
|
|
201
|
-
this.state.lastSuccessfulSyncStarted = this.state.lastSyncStarted;
|
|
202
|
-
this.state.lastSyncStarted = '';
|
|
203
|
-
// Clear pending extraction boundaries now that the cycle is complete
|
|
204
|
-
this.state.pendingWorkersOldest = '';
|
|
205
|
-
this.state.pendingWorkersNewest = '';
|
|
206
|
-
// Update workersOldest and workersNewest boundaries from resolved extraction timestamps.
|
|
207
|
-
// Expand boundaries: workersOldest gets the earliest timestamp, workersNewest gets the latest.
|
|
208
|
-
const extractionStart = this.event.payload.event_context.extract_from;
|
|
209
|
-
const extractionEnd = this.event.payload.event_context.extract_to;
|
|
210
|
-
if (extractionStart &&
|
|
211
|
-
(!this.state.workersOldest ||
|
|
212
|
-
extractionStart < this.state.workersOldest)) {
|
|
213
|
-
console.log(`Updating workersOldest from '${this.state.workersOldest}' to '${extractionStart}'.`);
|
|
214
|
-
this.state.workersOldest = extractionStart;
|
|
215
|
-
}
|
|
216
|
-
if (extractionEnd &&
|
|
217
|
-
(!this.state.workersNewest ||
|
|
218
|
-
extractionEnd > this.state.workersNewest)) {
|
|
219
|
-
console.log(`Updating workersNewest from '${this.state.workersNewest}' to '${extractionEnd}'.`);
|
|
220
|
-
this.state.workersNewest = extractionEnd;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// We want to save the state every time we emit an event, except for the start and delete events
|
|
224
|
-
if (!constants_1.STATELESS_EVENT_TYPES.includes(this.event.payload.event_type)) {
|
|
225
|
-
console.log(`Saving state before emitting event with event type: ${newEventType}.`);
|
|
226
|
-
try {
|
|
227
|
-
await this.adapterState.postState(this.state);
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
console.error('Error while posting state', error);
|
|
231
|
-
node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(workers_1.WorkerMessageSubject.WorkerMessageExit);
|
|
232
|
-
this.hasWorkerEmitted = true;
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
198
|
try {
|
|
237
|
-
// Always prune error messages to make them shorter before emit
|
|
238
|
-
if ((_b = data === null || data === void 0 ? void 0 : data.error) === null || _b === void 0 ? void 0 : _b.message) {
|
|
239
|
-
data.error.message = (0, helpers_1.truncateMessage)(data.error.message);
|
|
240
|
-
}
|
|
241
199
|
const isExtractionEvent = Object.values(extraction_1.ExtractorEventType).includes(newEventType);
|
|
242
|
-
const isLoaderEvent = Object.values(loading_1.LoaderEventType).includes(newEventType);
|
|
243
200
|
const progressData = {};
|
|
244
201
|
if (isExtractionEvent &&
|
|
245
|
-
(newEventType
|
|
246
|
-
newEventType
|
|
247
|
-
newEventType
|
|
248
|
-
newEventType
|
|
202
|
+
(newEventType === extraction_1.ExtractorEventType.DataExtractionDone ||
|
|
203
|
+
newEventType === extraction_1.ExtractorEventType.DataExtractionProgress ||
|
|
204
|
+
newEventType === extraction_1.ExtractorEventType.AttachmentExtractionDone ||
|
|
205
|
+
newEventType === extraction_1.ExtractorEventType.AttachmentExtractionProgress)) {
|
|
249
206
|
const repo = this.lastExtractedItemType
|
|
250
207
|
? this.repos.find((r) => r.itemType === this.lastExtractedItemType)
|
|
251
208
|
: undefined;
|
|
@@ -260,9 +217,7 @@ class WorkerAdapter {
|
|
|
260
217
|
await (0, control_protocol_1.emit)({
|
|
261
218
|
eventType: newEventType,
|
|
262
219
|
event: this.event,
|
|
263
|
-
data:
|
|
264
|
-
? { reports: this.reports, processed_files: this.processedFiles }
|
|
265
|
-
: {})),
|
|
220
|
+
data: eventPayload,
|
|
266
221
|
worker_metadata: Object.assign({}, progressData),
|
|
267
222
|
});
|
|
268
223
|
const message = {
|
|
@@ -280,14 +235,111 @@ class WorkerAdapter {
|
|
|
280
235
|
}
|
|
281
236
|
});
|
|
282
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Emits a phase-specific error after task or pre-emit execution fails. This
|
|
240
|
+
* path avoids flushing repos again, so artifacts uploaded before the failure
|
|
241
|
+
* are still reported without retrying the failed batch.
|
|
242
|
+
*/
|
|
243
|
+
async emitError(error) {
|
|
244
|
+
return (0, logger_context_1.runWithSdkLogContext)(async () => {
|
|
245
|
+
if (this.hasWorkerEmitted) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Include artifacts uploaded before a failure so they are not orphaned
|
|
249
|
+
// and can be associated with the emitted phase error.
|
|
250
|
+
for (const repo of this.repos) {
|
|
251
|
+
this.artifacts = [...this.artifacts, ...repo.uploadedArtifacts];
|
|
252
|
+
}
|
|
253
|
+
const { eventType } = (0, spawn_helpers_1.getTimeoutErrorEventType)(this.event.payload.event_type);
|
|
254
|
+
try {
|
|
255
|
+
await (0, control_protocol_1.emit)({
|
|
256
|
+
eventType,
|
|
257
|
+
event: this.event,
|
|
258
|
+
data: this.buildEmitPayload(eventType, {
|
|
259
|
+
error: { message: (0, logger_1.serializeError)(error) },
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
const message = {
|
|
263
|
+
subject: workers_1.WorkerMessageSubject.WorkerMessageEmitted,
|
|
264
|
+
payload: { eventType },
|
|
265
|
+
};
|
|
266
|
+
this.artifacts = [];
|
|
267
|
+
node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(message);
|
|
268
|
+
this.hasWorkerEmitted = true;
|
|
269
|
+
}
|
|
270
|
+
catch (emitError) {
|
|
271
|
+
console.error(`Error while emitting error event with event type: ${eventType}.`, (0, logger_1.serializeError)(emitError));
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
283
275
|
async uploadAllRepos() {
|
|
284
276
|
for (const repo of this.repos) {
|
|
285
|
-
|
|
277
|
+
await repo.upload();
|
|
286
278
|
this.artifacts.push(...repo.uploadedArtifacts);
|
|
287
|
-
|
|
288
|
-
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Pre-emit hook: uploads all repos and updates extraction boundaries/state.
|
|
283
|
+
* Throws if uploading repos or persisting state fails. The caller emits a
|
|
284
|
+
* compensating error event with whatever artifacts did upload.
|
|
285
|
+
*/
|
|
286
|
+
async beforeEmit(eventType) {
|
|
287
|
+
// Upload all repos before emitting the event.
|
|
288
|
+
console.log(`Uploading all repos before emitting event with event type: ${eventType}.`);
|
|
289
|
+
await this.uploadAllRepos();
|
|
290
|
+
// If the extraction is done, save the timestamp of the last successful sync.
|
|
291
|
+
if (eventType === extraction_1.ExtractorEventType.AttachmentExtractionDone) {
|
|
292
|
+
console.log(`Overwriting lastSuccessfulSyncStarted with lastSyncStarted (${this.state.lastSyncStarted}).`);
|
|
293
|
+
this.state.lastSuccessfulSyncStarted = this.state.lastSyncStarted;
|
|
294
|
+
this.state.lastSyncStarted = '';
|
|
295
|
+
// Clear pending extraction boundaries now that the cycle is complete.
|
|
296
|
+
this.state.pendingWorkersOldest = '';
|
|
297
|
+
this.state.pendingWorkersNewest = '';
|
|
298
|
+
// Update workersOldest and workersNewest boundaries from resolved extraction timestamps.
|
|
299
|
+
const extractionStart = this.event.payload.event_context.extract_from;
|
|
300
|
+
const extractionEnd = this.event.payload.event_context.extract_to;
|
|
301
|
+
if (extractionStart &&
|
|
302
|
+
(!this.state.workersOldest ||
|
|
303
|
+
extractionStart < this.state.workersOldest)) {
|
|
304
|
+
console.log(`Updating workersOldest from '${this.state.workersOldest}' to '${extractionStart}'.`);
|
|
305
|
+
this.state.workersOldest = extractionStart;
|
|
306
|
+
}
|
|
307
|
+
if (extractionEnd &&
|
|
308
|
+
(!this.state.workersNewest || extractionEnd > this.state.workersNewest)) {
|
|
309
|
+
console.log(`Updating workersNewest from '${this.state.workersNewest}' to '${extractionEnd}'.`);
|
|
310
|
+
this.state.workersNewest = extractionEnd;
|
|
289
311
|
}
|
|
290
312
|
}
|
|
313
|
+
if (constants_1.STATELESS_EVENT_TYPES.includes(this.event.payload.event_type)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
let stateSizeKb = 0;
|
|
317
|
+
try {
|
|
318
|
+
stateSizeKb =
|
|
319
|
+
Buffer.byteLength(JSON.stringify(this.state), 'utf8') / 1024;
|
|
320
|
+
}
|
|
321
|
+
catch (_a) {
|
|
322
|
+
stateSizeKb = 0;
|
|
323
|
+
}
|
|
324
|
+
console.log(`Saving ${stateSizeKb.toFixed(2)} KB state before emitting event with event type: ${eventType}. Current state`, (0, logger_1.getPrintableState)(this.state));
|
|
325
|
+
try {
|
|
326
|
+
await this.adapterState.postState(this.state);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error('Error while posting state.', (0, logger_1.serializeError)(error));
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
buildEmitPayload(eventType, data) {
|
|
334
|
+
var _a;
|
|
335
|
+
if ((_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) {
|
|
336
|
+
data.error.message = (0, helpers_1.truncateMessage)(data.error.message);
|
|
337
|
+
}
|
|
338
|
+
const isExtractionEvent = Object.values(extraction_1.ExtractorEventType).includes(eventType);
|
|
339
|
+
const isLoaderEvent = Object.values(loading_1.LoaderEventType).includes(eventType);
|
|
340
|
+
return Object.assign(Object.assign(Object.assign({}, data), (isExtractionEvent ? { artifacts: this.artifacts } : {})), (isLoaderEvent
|
|
341
|
+
? { reports: this.reports, processed_files: this.processedFiles }
|
|
342
|
+
: {}));
|
|
291
343
|
}
|
|
292
344
|
async loadItemTypes({ itemTypesToLoad, }) {
|
|
293
345
|
return (0, logger_context_1.runWithSdkLogContext)(async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-adapter.upload-failure.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.upload-failure.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const state_1 = require("../../state/state");
|
|
4
|
+
const jest_setup_1 = require("../../tests/jest.setup");
|
|
5
|
+
const test_helpers_1 = require("../../tests/test-helpers");
|
|
6
|
+
const test_utils_1 = require("../../common/test-utils");
|
|
7
|
+
const types_1 = require("../../types");
|
|
8
|
+
const worker_adapter_1 = require("./worker-adapter");
|
|
9
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
10
|
+
jest.mock('../../common/control-protocol', () => ({
|
|
11
|
+
emit: jest.fn().mockResolvedValue({}),
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('../../mappers/mappers');
|
|
14
|
+
jest.mock('node:worker_threads', () => ({
|
|
15
|
+
parentPort: { postMessage: jest.fn() },
|
|
16
|
+
}));
|
|
17
|
+
jest.mock('../../attachments-streaming/attachments-streaming-pool', () => ({
|
|
18
|
+
AttachmentsStreamingPool: jest.fn().mockImplementation(() => ({
|
|
19
|
+
streamAll: jest.fn().mockResolvedValue(undefined),
|
|
20
|
+
})),
|
|
21
|
+
}));
|
|
22
|
+
const UPLOAD_URL_PATH = '/internal/airdrop.artifacts.upload-url';
|
|
23
|
+
function makeAdapter() {
|
|
24
|
+
const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
|
|
25
|
+
payload: { event_type: types_1.EventType.StartExtractingData },
|
|
26
|
+
});
|
|
27
|
+
const initialState = {
|
|
28
|
+
attachments: { completed: false },
|
|
29
|
+
lastSyncStarted: '',
|
|
30
|
+
lastSuccessfulSyncStarted: '',
|
|
31
|
+
snapInVersionId: '',
|
|
32
|
+
toDevRev: {
|
|
33
|
+
attachmentsMetadata: {
|
|
34
|
+
artifactIds: [],
|
|
35
|
+
lastProcessed: 0,
|
|
36
|
+
lastProcessedAttachmentsIdsList: [],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const adapterState = new state_1.State({ event, initialState });
|
|
41
|
+
const adapter = new worker_adapter_1.WorkerAdapter({ event, adapterState });
|
|
42
|
+
const workerThreads = require('node:worker_threads');
|
|
43
|
+
const mockPostMessage = jest.fn();
|
|
44
|
+
if (workerThreads.parentPort) {
|
|
45
|
+
jest
|
|
46
|
+
.spyOn(workerThreads.parentPort, 'postMessage')
|
|
47
|
+
.mockImplementation(mockPostMessage);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
workerThreads.parentPort = { postMessage: mockPostMessage };
|
|
51
|
+
}
|
|
52
|
+
return { adapter, mockPostMessage };
|
|
53
|
+
}
|
|
54
|
+
describe(`${worker_adapter_1.WorkerAdapter.name} upload failure (near-integration)`, () => {
|
|
55
|
+
let adapter;
|
|
56
|
+
let mockPostMessage;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
jest_setup_1.mockServer.resetRoutes();
|
|
60
|
+
({ adapter, mockPostMessage } = makeAdapter());
|
|
61
|
+
adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
|
|
62
|
+
});
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
it('should throw from repo.push when upload-url returns 400 during batch upload', async () => {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
jest_setup_1.mockServer.setRoute({
|
|
69
|
+
path: UPLOAD_URL_PATH,
|
|
70
|
+
method: 'GET',
|
|
71
|
+
status: 400,
|
|
72
|
+
});
|
|
73
|
+
adapter.initializeRepos([
|
|
74
|
+
{
|
|
75
|
+
itemType: 'tasks',
|
|
76
|
+
overridenOptions: { batchSize: 10 },
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
await expect((_a = adapter.getRepo('tasks')) === null || _a === void 0 ? void 0 : _a.push((0, test_helpers_1.createItems)(20))).rejects.toThrow('artifact upload URL');
|
|
80
|
+
expect((_b = adapter.getRepo('tasks')) === null || _b === void 0 ? void 0 : _b.getItems().length).toBe(20);
|
|
81
|
+
});
|
|
82
|
+
it('should emit DataExtractionError when uploadAllRepos fails', async () => {
|
|
83
|
+
var _a;
|
|
84
|
+
const { emit: mockEmit } = require('../../common/control-protocol');
|
|
85
|
+
jest_setup_1.mockServer.setRoute({
|
|
86
|
+
path: UPLOAD_URL_PATH,
|
|
87
|
+
method: 'GET',
|
|
88
|
+
status: 400,
|
|
89
|
+
});
|
|
90
|
+
adapter.initializeRepos([{ itemType: 'tasks' }]);
|
|
91
|
+
await ((_a = adapter.getRepo('tasks')) === null || _a === void 0 ? void 0 : _a.push((0, test_helpers_1.createItems)(5)));
|
|
92
|
+
await adapter.emit(types_1.ExtractorEventType.DataExtractionDone);
|
|
93
|
+
expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
94
|
+
eventType: types_1.ExtractorEventType.DataExtractionError,
|
|
95
|
+
data: expect.objectContaining({
|
|
96
|
+
error: expect.objectContaining({
|
|
97
|
+
message: expect.stringContaining('artifact upload URL'),
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
}));
|
|
101
|
+
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
102
|
+
subject: 'emit',
|
|
103
|
+
payload: { eventType: types_1.ExtractorEventType.DataExtractionError },
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
it('should include partial artifacts when push batch succeeded before uploadAllRepos fails', async () => {
|
|
107
|
+
var _a;
|
|
108
|
+
const { emit: mockEmit } = require('../../common/control-protocol');
|
|
109
|
+
jest_setup_1.mockServer.setRoute({
|
|
110
|
+
path: UPLOAD_URL_PATH,
|
|
111
|
+
method: 'GET',
|
|
112
|
+
status: 200,
|
|
113
|
+
succeedThenFail: {
|
|
114
|
+
successCount: 1,
|
|
115
|
+
errorStatus: 400,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
adapter.initializeRepos([
|
|
119
|
+
{
|
|
120
|
+
itemType: 'tasks',
|
|
121
|
+
overridenOptions: { batchSize: 10 },
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
await ((_a = adapter.getRepo('tasks')) === null || _a === void 0 ? void 0 : _a.push((0, test_helpers_1.createItems)(15)));
|
|
125
|
+
await adapter.emit(types_1.ExtractorEventType.DataExtractionDone);
|
|
126
|
+
expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
127
|
+
eventType: types_1.ExtractorEventType.DataExtractionError,
|
|
128
|
+
data: expect.objectContaining({
|
|
129
|
+
artifacts: expect.arrayContaining([
|
|
130
|
+
expect.objectContaining({ item_count: 10, item_type: 'tasks' }),
|
|
131
|
+
]),
|
|
132
|
+
}),
|
|
133
|
+
}));
|
|
134
|
+
});
|
|
135
|
+
});
|
package/dist/repo/repo.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Item } from '../repo/repo.interfaces';
|
|
2
|
-
import { ErrorRecord } from '../types/common';
|
|
3
2
|
import { Artifact } from '../uploader/uploader.interfaces';
|
|
4
3
|
import { NormalizedAttachment, NormalizedItem, RepoFactoryInterface } from './repo.interfaces';
|
|
5
4
|
export declare class Repo {
|
|
@@ -22,7 +21,7 @@ export declare class Repo {
|
|
|
22
21
|
};
|
|
23
22
|
constructor({ event, itemType, normalize, onUpload, options, }: RepoFactoryInterface);
|
|
24
23
|
getItems(): (NormalizedItem | NormalizedAttachment | Item)[];
|
|
25
|
-
upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void
|
|
24
|
+
upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void>;
|
|
26
25
|
push(items: Item[]): Promise<boolean>;
|
|
27
26
|
}
|
|
28
27
|
//# sourceMappingURL=repo.d.ts.map
|
package/dist/repo/repo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/repo/repo.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/repo/repo.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAI3D,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAG3B,qBAAa,IAAI;IACf,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAmD;IAChE,OAAO,CAAC,SAAS,CAAC,CAAwD;IAC1E,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,OAAO,CAAC,CAAuB;IAChC,iBAAiB,EAAE,QAAQ,EAAE,CAAC;IAC9B,UAAU,EAAE;QACjB,YAAY,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACnD,YAAY,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACpD,CAGC;gBAEU,EACV,KAAK,EACL,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,OAAO,GACR,EAAE,oBAAoB;IAUvB,QAAQ,IAAI,CAAC,cAAc,GAAG,oBAAoB,GAAG,IAAI,CAAC,EAAE;IAItD,MAAM,CACV,KAAK,CAAC,EAAE,CAAC,cAAc,GAAG,oBAAoB,GAAG,IAAI,CAAC,EAAE,GACvD,OAAO,CAAC,IAAI,CAAC;IA4DV,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAkC5C"}
|
package/dist/repo/repo.js
CHANGED
|
@@ -23,6 +23,7 @@ class Repo {
|
|
|
23
23
|
return this.items;
|
|
24
24
|
}
|
|
25
25
|
async upload(batch) {
|
|
26
|
+
var _a;
|
|
26
27
|
const itemsToUpload = batch || this.items;
|
|
27
28
|
if (itemsToUpload.length > 0) {
|
|
28
29
|
for (const item of itemsToUpload) {
|
|
@@ -44,8 +45,8 @@ class Repo {
|
|
|
44
45
|
console.log(`Uploading ${itemsToUpload.length} items of type ${this.itemType}. `);
|
|
45
46
|
const { artifact, error } = await this.uploader.upload(this.itemType, itemsToUpload);
|
|
46
47
|
if (error || !artifact) {
|
|
47
|
-
console.error(
|
|
48
|
-
|
|
48
|
+
console.error(`Error while uploading batch of ${itemsToUpload.length} items of type ${this.itemType}.`, error);
|
|
49
|
+
throw new Error((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : `Upload failed for item type "${this.itemType}" without artifact.`);
|
|
49
50
|
}
|
|
50
51
|
this.onUpload(artifact);
|
|
51
52
|
this.uploadedArtifacts.push(artifact);
|
|
@@ -80,16 +81,9 @@ class Repo {
|
|
|
80
81
|
// Upload in batches while the number of items exceeds the batch size
|
|
81
82
|
const batchSize = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.batchSize) || constants_1.ARTIFACT_BATCH_SIZE;
|
|
82
83
|
while (this.items.length >= batchSize) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Upload the batch
|
|
87
|
-
await this.upload(batch);
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
console.error('Error while uploading batch', error);
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
84
|
+
const batch = this.items.slice(0, batchSize);
|
|
85
|
+
await this.upload(batch);
|
|
86
|
+
this.items.splice(0, batchSize);
|
|
93
87
|
}
|
|
94
88
|
return true;
|
|
95
89
|
}
|
package/dist/repo/repo.test.js
CHANGED
|
@@ -313,7 +313,7 @@ describe(repo_1.Repo.name, () => {
|
|
|
313
313
|
error: new Error('fail'),
|
|
314
314
|
artifact: null,
|
|
315
315
|
});
|
|
316
|
-
await repo.upload([itemWithDate('1', '2022-01-01T00:00:00.000Z')]);
|
|
316
|
+
await expect(repo.upload([itemWithDate('1', '2022-01-01T00:00:00.000Z')])).rejects.toThrow('fail');
|
|
317
317
|
expect(repo.dateRanges.creationDate.oldest).toBe(ts('2022-01-01T00:00:00.000Z'));
|
|
318
318
|
expect(repo.dateRanges.creationDate.newest).toBe(ts('2022-01-01T00:00:00.000Z'));
|
|
319
319
|
});
|
|
@@ -343,4 +343,35 @@ describe(repo_1.Repo.name, () => {
|
|
|
343
343
|
expect(repo.dateRanges.modifiedDate.newest).toBe(ts('2023-01-01T00:00:00.000Z'));
|
|
344
344
|
});
|
|
345
345
|
});
|
|
346
|
+
it('should throw when upload fails', async () => {
|
|
347
|
+
mockUploadFn.mockResolvedValueOnce({
|
|
348
|
+
error: { message: 'upload failed' },
|
|
349
|
+
});
|
|
350
|
+
await expect(repo.upload((0, test_helpers_1.createItems)(1))).rejects.toThrow('upload failed');
|
|
351
|
+
});
|
|
352
|
+
it('should retain items in repo when batch upload fails during push', async () => {
|
|
353
|
+
repo = new repo_1.Repo({
|
|
354
|
+
event: (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
|
|
355
|
+
payload: { event_type: types_1.EventType.ExtractionDataStart },
|
|
356
|
+
}),
|
|
357
|
+
itemType: 'test_item_type',
|
|
358
|
+
normalize,
|
|
359
|
+
onUpload: jest.fn(),
|
|
360
|
+
options: { batchSize: 10 },
|
|
361
|
+
});
|
|
362
|
+
const items = (0, test_helpers_1.createItems)(20);
|
|
363
|
+
mockUploadFn
|
|
364
|
+
.mockResolvedValueOnce({
|
|
365
|
+
artifact: {
|
|
366
|
+
id: 'artifact-1',
|
|
367
|
+
item_type: 'test_item_type',
|
|
368
|
+
item_count: 10,
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
.mockResolvedValueOnce({
|
|
372
|
+
error: { message: 'second batch failed' },
|
|
373
|
+
});
|
|
374
|
+
await expect(repo.push(items)).rejects.toThrow('second batch failed');
|
|
375
|
+
expect(repo.getItems().length).toBe(10);
|
|
376
|
+
});
|
|
346
377
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state/state.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,YAAY,EAIZ,cAAc,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,wBAAsB,kBAAkB,CAAC,cAAc,EAAE,EACvD,KAAK,EACL,YAAY,EACZ,oBAAoB,EACpB,OAAO,GACR,EAAE,cAAc,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state/state.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,YAAY,EAIZ,cAAc,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,wBAAsB,kBAAkB,CAAC,cAAc,EAAE,EACvD,KAAK,EACL,YAAY,EACZ,oBAAoB,EACpB,OAAO,GACR,EAAE,cAAc,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAqLjE;AAED,qBAAa,KAAK,CAAC,cAAc;IAC/B,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,eAAe,CAAW;IAClC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,cAAc,CAAC,cAAc,CAAC;IAenE,IAAI,KAAK,IAAI,YAAY,CAAC,cAAc,CAAC,CAExC;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,cAAc,CAAC,EAE5C;IAED,IAAI,eAAe,IAAI,eAAe,CAErC;IAED;;;;OAIG;IACG,IAAI,CAAC,YAAY,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDvD;;;OAGG;IACG,SAAS,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC,cAAc,CAAC;IAqDpD;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAqBjE"}
|
package/dist/state/state.js
CHANGED
|
@@ -67,7 +67,7 @@ async function createAdapterState({ event, initialState, initialDomainMapping, o
|
|
|
67
67
|
const eventContext = event.payload.event_context;
|
|
68
68
|
const overrideExtractFrom = (value) => {
|
|
69
69
|
if (eventContext.extract_from !== undefined) {
|
|
70
|
-
eventContext.
|
|
70
|
+
eventContext.original_extract_from = eventContext.extract_from;
|
|
71
71
|
}
|
|
72
72
|
eventContext.extract_from = value;
|
|
73
73
|
};
|
|
@@ -126,10 +126,14 @@ async function createAdapterState({ event, initialState, initialDomainMapping, o
|
|
|
126
126
|
}
|
|
127
127
|
else {
|
|
128
128
|
// Non-StartExtractingMetadata events: reuse pending values from state
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
const pendingExtractFrom = as.state.pendingWorkersOldest || as.state.lastSuccessfulSyncStarted;
|
|
130
|
+
if (pendingExtractFrom &&
|
|
131
|
+
(eventContext.extract_from === undefined ||
|
|
132
|
+
eventContext.extract_from !== pendingExtractFrom)) {
|
|
133
|
+
overrideExtractFrom(pendingExtractFrom);
|
|
134
|
+
console.log(`Using ${as.state.pendingWorkersOldest
|
|
135
|
+
? 'pendingWorkersOldest'
|
|
136
|
+
: 'lastSuccessfulSyncStarted'} as extract_from: ${pendingExtractFrom}.`);
|
|
133
137
|
}
|
|
134
138
|
else {
|
|
135
139
|
console.log('pendingWorkersOldest is not set in state. extract_from will not be populated for this invocation.');
|
|
@@ -102,7 +102,7 @@ describe('State — TimeValue resolution', () => {
|
|
|
102
102
|
// Assert: the successful sync boundary takes precedence and is retained
|
|
103
103
|
// for later phases.
|
|
104
104
|
expect(event.payload.event_context.extract_from).toBe('2025-05-01T00:00:00.000Z');
|
|
105
|
-
expect(event.payload.event_context.
|
|
105
|
+
expect(event.payload.event_context.original_extract_from).toBe(platformExtractFrom);
|
|
106
106
|
expect(state.state.pendingWorkersOldest).toBe('2025-05-01T00:00:00.000Z');
|
|
107
107
|
});
|
|
108
108
|
it('should preserve the platform extract_from when resolving a CPv2 start time', async () => {
|
|
@@ -134,7 +134,7 @@ describe('State — TimeValue resolution', () => {
|
|
|
134
134
|
initialDomainMapping: {},
|
|
135
135
|
});
|
|
136
136
|
expect(event.payload.event_context.extract_from).toBe(resolvedExtractFrom);
|
|
137
|
-
expect(event.payload.event_context.
|
|
137
|
+
expect(event.payload.event_context.original_extract_from).toBe(platformExtractFrom);
|
|
138
138
|
});
|
|
139
139
|
it('should use the platform extract_from when lastSuccessfulSyncStarted is not set', async () => {
|
|
140
140
|
// Arrange: CPv1.1 sends a concrete extract_from instead of a TimeValue.
|
|
@@ -228,6 +228,35 @@ describe('State — TimeValue resolution', () => {
|
|
|
228
228
|
expect(event.payload.event_context.extract_from).toBe(lastSuccessfulSyncStarted);
|
|
229
229
|
expect(state.state.pendingWorkersOldest).toBe(lastSuccessfulSyncStarted);
|
|
230
230
|
});
|
|
231
|
+
it('should override platform extract_from on StartExtractingData and preserve its original value', async () => {
|
|
232
|
+
const platformExtractFrom = '2026-08-24T13:26:15.388Z';
|
|
233
|
+
const lastSuccessfulSyncStarted = '2026-08-25T13:26:15.388Z';
|
|
234
|
+
const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
|
|
235
|
+
context: {
|
|
236
|
+
snap_in_version_id: 'test_snap_in_version_id',
|
|
237
|
+
},
|
|
238
|
+
payload: {
|
|
239
|
+
event_type: extraction_1.EventType.StartExtractingData,
|
|
240
|
+
event_context: {
|
|
241
|
+
extract_from: platformExtractFrom,
|
|
242
|
+
extraction_start_time: {},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
fetchStateSpy.mockResolvedValue({
|
|
247
|
+
state: JSON.stringify({
|
|
248
|
+
snapInVersionId: 'test_snap_in_version_id',
|
|
249
|
+
lastSuccessfulSyncStarted,
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
await (0, state_1.createAdapterState)({
|
|
253
|
+
event,
|
|
254
|
+
initialState: {},
|
|
255
|
+
initialDomainMapping: {},
|
|
256
|
+
});
|
|
257
|
+
expect(event.payload.event_context.extract_from).toBe(lastSuccessfulSyncStarted);
|
|
258
|
+
expect(event.payload.event_context.original_extract_from).toBe(platformExtractFrom);
|
|
259
|
+
});
|
|
231
260
|
it('should skip resolution when extraction_end_time has no type', async () => {
|
|
232
261
|
// Arrange: platform sends extraction_end_time without a type field
|
|
233
262
|
const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
|
|
@@ -56,7 +56,8 @@ export declare enum EventType {
|
|
|
56
56
|
StartDeletingExtractorState = "START_DELETING_EXTRACTOR_STATE",
|
|
57
57
|
StartExtractingAttachments = "START_EXTRACTING_ATTACHMENTS",
|
|
58
58
|
ContinueExtractingAttachments = "CONTINUE_EXTRACTING_ATTACHMENTS",
|
|
59
|
-
StartDeletingExtractorAttachmentsState = "START_DELETING_EXTRACTOR_ATTACHMENTS_STATE"
|
|
59
|
+
StartDeletingExtractorAttachmentsState = "START_DELETING_EXTRACTOR_ATTACHMENTS_STATE",
|
|
60
|
+
ContinueExtractingMetadata = "CONTINUE_EXTRACTING_METADATA"
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* ExtractorEventType is an enum that defines the different types of events that can be sent from the external extractor to ADaaS.
|
|
@@ -143,7 +144,9 @@ export declare enum ExtractorEventType {
|
|
|
143
144
|
AttachmentExtractionDone = "ATTACHMENT_EXTRACTION_DONE",
|
|
144
145
|
AttachmentExtractionError = "ATTACHMENT_EXTRACTION_ERROR",
|
|
145
146
|
ExtractorAttachmentsStateDeletionDone = "EXTRACTOR_ATTACHMENTS_STATE_DELETION_DONE",
|
|
146
|
-
ExtractorAttachmentsStateDeletionError = "EXTRACTOR_ATTACHMENTS_STATE_DELETION_ERROR"
|
|
147
|
+
ExtractorAttachmentsStateDeletionError = "EXTRACTOR_ATTACHMENTS_STATE_DELETION_ERROR",
|
|
148
|
+
MetadataExtractionProgress = "METADATA_EXTRACTION_PROGRESS",
|
|
149
|
+
MetadataExtractionDelayed = "METADATA_EXTRACTION_DELAYED"
|
|
147
150
|
}
|
|
148
151
|
/**
|
|
149
152
|
* @deprecated
|
|
@@ -303,7 +306,7 @@ export interface EventContext {
|
|
|
303
306
|
* Original extraction start timestamp supplied by the platform before the SDK
|
|
304
307
|
* overrides extract_from.
|
|
305
308
|
*/
|
|
306
|
-
|
|
309
|
+
original_extract_from?: string;
|
|
307
310
|
import_slug: string;
|
|
308
311
|
initial_sync_scope?: InitialSyncScope;
|
|
309
312
|
mode: string;
|