@devrev/ts-adaas 1.20.2-beta.3 → 1.20.2-beta.5

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.
Files changed (36) hide show
  1. package/dist/common/constants.d.ts.map +1 -1
  2. package/dist/common/constants.js +0 -1
  3. package/dist/common/event-type-translation.d.ts.map +1 -1
  4. package/dist/common/event-type-translation.js +0 -1
  5. package/dist/common/event-type-translation.test.js +0 -1
  6. package/dist/mock-server/mock-server.d.ts +0 -4
  7. package/dist/mock-server/mock-server.d.ts.map +1 -1
  8. package/dist/mock-server/mock-server.interfaces.d.ts +0 -9
  9. package/dist/mock-server/mock-server.interfaces.d.ts.map +1 -1
  10. package/dist/mock-server/mock-server.js +6 -39
  11. package/dist/multithreading/process-task.d.ts.map +1 -1
  12. package/dist/multithreading/process-task.js +6 -21
  13. package/dist/multithreading/process-task.test.js +1 -28
  14. package/dist/multithreading/spawn/spawn.d.ts.map +1 -1
  15. package/dist/multithreading/spawn/spawn.helpers.d.ts.map +1 -1
  16. package/dist/multithreading/spawn/spawn.helpers.js +0 -9
  17. package/dist/multithreading/spawn/spawn.helpers.test.js +0 -34
  18. package/dist/multithreading/spawn/spawn.js +0 -1
  19. package/dist/multithreading/worker-adapter/worker-adapter.d.ts +0 -13
  20. package/dist/multithreading/worker-adapter/worker-adapter.d.ts.map +1 -1
  21. package/dist/multithreading/worker-adapter/worker-adapter.emit.test.js +5 -51
  22. package/dist/multithreading/worker-adapter/worker-adapter.js +79 -131
  23. package/dist/repo/repo.d.ts +2 -1
  24. package/dist/repo/repo.d.ts.map +1 -1
  25. package/dist/repo/repo.js +12 -6
  26. package/dist/repo/repo.test.js +1 -32
  27. package/dist/state/state.d.ts.map +1 -1
  28. package/dist/state/state.js +34 -4
  29. package/dist/state/state.time-value-resolution.test.js +151 -0
  30. package/dist/types/extraction.d.ts +9 -7
  31. package/dist/types/extraction.d.ts.map +1 -1
  32. package/dist/types/extraction.js +0 -7
  33. package/package.json +1 -1
  34. package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.d.ts +0 -2
  35. package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.d.ts.map +0 -1
  36. package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.js +0 -135
@@ -22,7 +22,6 @@ 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");
26
25
  function createWorkerAdapter({ event, adapterState, options, }) {
27
26
  return new WorkerAdapter({
28
27
  event,
@@ -160,49 +159,93 @@ class WorkerAdapter {
160
159
  */
161
160
  async emit(newEventType, data) {
162
161
  return (0, logger_context_1.runWithSdkLogContext)(async () => {
163
- var _a;
162
+ var _a, _b;
164
163
  newEventType = (0, event_type_translation_1.translateOutgoingEventType)(newEventType);
165
164
  if (this.hasWorkerEmitted) {
166
165
  console.warn(`Trying to emit event with event type: ${newEventType}. Ignoring emit request because it has already been emitted.`);
167
166
  return;
168
167
  }
169
- let eventPayload;
170
- try {
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
- },
168
+ // If the event is ExternalSyncUnitExtractionDone, upload external sync units via a Repo before emitting
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,
184
180
  },
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);
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}.`);
189
+ try {
190
+ await this.uploadAllRepos();
192
191
  }
193
192
  catch (error) {
194
- console.error('Error while preparing event for emission.', (0, logger_1.serializeError)(error));
195
- await this.emitError(error);
193
+ console.error('Error while uploading repos', error);
194
+ 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);
195
+ this.hasWorkerEmitted = true;
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
+ }
198
236
  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
+ }
199
241
  const isExtractionEvent = Object.values(extraction_1.ExtractorEventType).includes(newEventType);
242
+ const isLoaderEvent = Object.values(loading_1.LoaderEventType).includes(newEventType);
200
243
  const progressData = {};
201
244
  if (isExtractionEvent &&
202
- (newEventType === extraction_1.ExtractorEventType.DataExtractionDone ||
203
- newEventType === extraction_1.ExtractorEventType.DataExtractionProgress ||
204
- newEventType === extraction_1.ExtractorEventType.AttachmentExtractionDone ||
205
- newEventType === extraction_1.ExtractorEventType.AttachmentExtractionProgress)) {
245
+ (newEventType == extraction_1.ExtractorEventType.DataExtractionDone ||
246
+ newEventType == extraction_1.ExtractorEventType.DataExtractionProgress ||
247
+ newEventType == extraction_1.ExtractorEventType.AttachmentExtractionDone ||
248
+ newEventType == extraction_1.ExtractorEventType.AttachmentExtractionProgress)) {
206
249
  const repo = this.lastExtractedItemType
207
250
  ? this.repos.find((r) => r.itemType === this.lastExtractedItemType)
208
251
  : undefined;
@@ -217,7 +260,9 @@ class WorkerAdapter {
217
260
  await (0, control_protocol_1.emit)({
218
261
  eventType: newEventType,
219
262
  event: this.event,
220
- data: eventPayload,
263
+ data: Object.assign(Object.assign(Object.assign({}, data), (isExtractionEvent ? { artifacts: this.artifacts } : {})), (isLoaderEvent
264
+ ? { reports: this.reports, processed_files: this.processedFiles }
265
+ : {})),
221
266
  worker_metadata: Object.assign({}, progressData),
222
267
  });
223
268
  const message = {
@@ -235,111 +280,14 @@ class WorkerAdapter {
235
280
  }
236
281
  });
237
282
  }
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
- }
275
283
  async uploadAllRepos() {
276
284
  for (const repo of this.repos) {
277
- await repo.upload();
285
+ const error = await repo.upload();
278
286
  this.artifacts.push(...repo.uploadedArtifacts);
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;
287
+ if (error) {
288
+ throw error;
311
289
  }
312
290
  }
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
- : {}));
343
291
  }
344
292
  async loadItemTypes({ itemTypesToLoad, }) {
345
293
  return (0, logger_context_1.runWithSdkLogContext)(async () => {
@@ -1,4 +1,5 @@
1
1
  import { Item } from '../repo/repo.interfaces';
2
+ import { ErrorRecord } from '../types/common';
2
3
  import { Artifact } from '../uploader/uploader.interfaces';
3
4
  import { NormalizedAttachment, NormalizedItem, RepoFactoryInterface } from './repo.interfaces';
4
5
  export declare class Repo {
@@ -21,7 +22,7 @@ export declare class Repo {
21
22
  };
22
23
  constructor({ event, itemType, normalize, onUpload, options, }: RepoFactoryInterface);
23
24
  getItems(): (NormalizedItem | NormalizedAttachment | Item)[];
24
- upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void>;
25
+ upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void | ErrorRecord>;
25
26
  push(items: Item[]): Promise<boolean>;
26
27
  }
27
28
  //# sourceMappingURL=repo.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/repo/repo.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,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,GAAG,WAAW,CAAC;IAsDxB,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAyC5C"}
package/dist/repo/repo.js CHANGED
@@ -23,7 +23,6 @@ class Repo {
23
23
  return this.items;
24
24
  }
25
25
  async upload(batch) {
26
- var _a;
27
26
  const itemsToUpload = batch || this.items;
28
27
  if (itemsToUpload.length > 0) {
29
28
  for (const item of itemsToUpload) {
@@ -45,8 +44,8 @@ class Repo {
45
44
  console.log(`Uploading ${itemsToUpload.length} items of type ${this.itemType}. `);
46
45
  const { artifact, error } = await this.uploader.upload(this.itemType, itemsToUpload);
47
46
  if (error || !artifact) {
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.`);
47
+ console.error('Error while uploading batch', error);
48
+ return error;
50
49
  }
51
50
  this.onUpload(artifact);
52
51
  this.uploadedArtifacts.push(artifact);
@@ -81,9 +80,16 @@ class Repo {
81
80
  // Upload in batches while the number of items exceeds the batch size
82
81
  const batchSize = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.batchSize) || constants_1.ARTIFACT_BATCH_SIZE;
83
82
  while (this.items.length >= batchSize) {
84
- const batch = this.items.slice(0, batchSize);
85
- await this.upload(batch);
86
- this.items.splice(0, batchSize);
83
+ // Slice out a batch of batchSize items to upload
84
+ const batch = this.items.splice(0, batchSize);
85
+ try {
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
+ }
87
93
  }
88
94
  return true;
89
95
  }
@@ -313,7 +313,7 @@ describe(repo_1.Repo.name, () => {
313
313
  error: new Error('fail'),
314
314
  artifact: null,
315
315
  });
316
- await expect(repo.upload([itemWithDate('1', '2022-01-01T00:00:00.000Z')])).rejects.toThrow('fail');
316
+ await repo.upload([itemWithDate('1', '2022-01-01T00:00:00.000Z')]);
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,35 +343,4 @@ 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
- });
377
346
  });
@@ -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,CA6IjE;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"}
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"}
@@ -65,6 +65,12 @@ async function createAdapterState({ event, initialState, initialDomainMapping, o
65
65
  // On StartExtractingMetadata: resolve fresh from TimeValue objects and store in pending state (always overwrite).
66
66
  // On all other events: reuse the pending values cached during StartExtractingMetadata.
67
67
  const eventContext = event.payload.event_context;
68
+ const overrideExtractFrom = (value) => {
69
+ if (eventContext.extract_from !== undefined) {
70
+ eventContext.platform_extract_from = eventContext.extract_from;
71
+ }
72
+ eventContext.extract_from = value;
73
+ };
68
74
  if (event.payload.event_type === extraction_1.EventType.StartExtractingMetadata) {
69
75
  const timeFields = [
70
76
  {
@@ -83,7 +89,12 @@ async function createAdapterState({ event, initialState, initialDomainMapping, o
83
89
  if (timeValue && timeValue.type) {
84
90
  try {
85
91
  const resolved = (0, time_value_resolver_1.resolveTimeValue)(timeValue, as.state);
86
- eventContext[target] = resolved;
92
+ if (target === 'extract_from') {
93
+ overrideExtractFrom(resolved);
94
+ }
95
+ else {
96
+ eventContext[target] = resolved;
97
+ }
87
98
  as.state[pending] = resolved;
88
99
  console.log(`Resolved ${target} to ${resolved}. Stored in ${pending}.`);
89
100
  }
@@ -97,13 +108,32 @@ async function createAdapterState({ event, initialState, initialDomainMapping, o
97
108
  process.exit(1);
98
109
  }
99
110
  }
111
+ else if (target === 'extract_from' &&
112
+ as.state.lastSuccessfulSyncStarted) {
113
+ overrideExtractFrom(as.state.lastSuccessfulSyncStarted);
114
+ as.state.pendingWorkersOldest = as.state.lastSuccessfulSyncStarted;
115
+ console.log(`Using lastSuccessfulSyncStarted as extract_from: ${as.state.lastSuccessfulSyncStarted}.`);
116
+ }
117
+ else if (target === 'extract_from' &&
118
+ !as.state.lastSuccessfulSyncStarted &&
119
+ eventContext.extract_from !== undefined) {
120
+ // CPv1.1 supplies the resolved boundary directly. Keep it pending so
121
+ // later extraction phases expose the same event_context value.
122
+ as.state.pendingWorkersOldest = eventContext.extract_from;
123
+ console.log(`Using platform-provided extract_from: ${eventContext.extract_from}. Stored in pendingWorkersOldest.`);
124
+ }
100
125
  }
101
126
  }
102
127
  else {
103
128
  // Non-StartExtractingMetadata events: reuse pending values from state
104
- if (as.state.pendingWorkersOldest) {
105
- eventContext.extract_from = as.state.pendingWorkersOldest;
106
- console.log(`Reusing pendingWorkersOldest as extract_from: ${as.state.pendingWorkersOldest}.`);
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}.`);
107
137
  }
108
138
  else {
109
139
  console.log('pendingWorkersOldest is not set in state. extract_from will not be populated for this invocation.');
@@ -73,6 +73,98 @@ describe('State — TimeValue resolution', () => {
73
73
  });
74
74
  });
75
75
  describe('Backwards compatibility - missing TimeValue type', () => {
76
+ it('should prefer lastSuccessfulSyncStarted over the platform extract_from when no CPv2 start time is provided', async () => {
77
+ // Arrange: CPv1.1 sends a concrete extract_from instead of a TimeValue.
78
+ const platformExtractFrom = '2025-04-01T00:00:00.000Z';
79
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
80
+ context: {
81
+ snap_in_version_id: 'test_snap_in_version_id',
82
+ },
83
+ payload: {
84
+ event_type: extraction_1.EventType.StartExtractingMetadata,
85
+ event_context: {
86
+ extract_from: platformExtractFrom,
87
+ },
88
+ },
89
+ });
90
+ fetchStateSpy.mockResolvedValue({
91
+ state: JSON.stringify({
92
+ snapInVersionId: 'test_snap_in_version_id',
93
+ lastSuccessfulSyncStarted: '2025-05-01T00:00:00.000Z',
94
+ }),
95
+ });
96
+ // Act
97
+ const state = await (0, state_1.createAdapterState)({
98
+ event,
99
+ initialState: {},
100
+ initialDomainMapping: {},
101
+ });
102
+ // Assert: the successful sync boundary takes precedence and is retained
103
+ // for later phases.
104
+ expect(event.payload.event_context.extract_from).toBe('2025-05-01T00:00:00.000Z');
105
+ expect(event.payload.event_context.platform_extract_from).toBe(platformExtractFrom);
106
+ expect(state.state.pendingWorkersOldest).toBe('2025-05-01T00:00:00.000Z');
107
+ });
108
+ it('should preserve the platform extract_from when resolving a CPv2 start time', async () => {
109
+ const platformExtractFrom = '2025-04-01T00:00:00.000Z';
110
+ const resolvedExtractFrom = '2025-06-01T00:00:00.000Z';
111
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
112
+ context: {
113
+ snap_in_version_id: 'test_snap_in_version_id',
114
+ },
115
+ payload: {
116
+ event_type: extraction_1.EventType.StartExtractingMetadata,
117
+ event_context: {
118
+ extraction_start_time: {
119
+ type: extraction_1.TimeValueType.ABSOLUTE_TIME,
120
+ value: resolvedExtractFrom,
121
+ },
122
+ extract_from: platformExtractFrom,
123
+ },
124
+ },
125
+ });
126
+ fetchStateSpy.mockResolvedValue({
127
+ state: JSON.stringify({
128
+ snapInVersionId: 'test_snap_in_version_id',
129
+ }),
130
+ });
131
+ await (0, state_1.createAdapterState)({
132
+ event,
133
+ initialState: {},
134
+ initialDomainMapping: {},
135
+ });
136
+ expect(event.payload.event_context.extract_from).toBe(resolvedExtractFrom);
137
+ expect(event.payload.event_context.platform_extract_from).toBe(platformExtractFrom);
138
+ });
139
+ it('should use the platform extract_from when lastSuccessfulSyncStarted is not set', async () => {
140
+ // Arrange: CPv1.1 sends a concrete extract_from instead of a TimeValue.
141
+ const platformExtractFrom = '2025-04-01T00:00:00.000Z';
142
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
143
+ context: {
144
+ snap_in_version_id: 'test_snap_in_version_id',
145
+ },
146
+ payload: {
147
+ event_type: extraction_1.EventType.StartExtractingMetadata,
148
+ event_context: {
149
+ extract_from: platformExtractFrom,
150
+ },
151
+ },
152
+ });
153
+ fetchStateSpy.mockResolvedValue({
154
+ state: JSON.stringify({
155
+ snapInVersionId: 'test_snap_in_version_id',
156
+ }),
157
+ });
158
+ // Act
159
+ const state = await (0, state_1.createAdapterState)({
160
+ event,
161
+ initialState: {},
162
+ initialDomainMapping: {},
163
+ });
164
+ // Assert: the platform value is used and retained for later phases.
165
+ expect(event.payload.event_context.extract_from).toBe(platformExtractFrom);
166
+ expect(state.state.pendingWorkersOldest).toBe(platformExtractFrom);
167
+ });
76
168
  it('should skip resolution when extraction_start_time has no type', async () => {
77
169
  // Arrange: platform sends extraction_start_time without a type field (old platform version)
78
170
  const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
@@ -106,6 +198,65 @@ describe('State — TimeValue resolution', () => {
106
198
  expect(event.payload.event_context.extract_to).toBe('2025-06-01T00:00:00.000Z');
107
199
  expect(state.state.pendingWorkersNewest).toBe('2025-06-01T00:00:00.000Z');
108
200
  });
201
+ it('should use lastSuccessfulSyncStarted for extract_from when extraction_start_time has no type', async () => {
202
+ // Arrange: platform sends an empty extraction_start_time on a subsequent sync
203
+ const lastSuccessfulSyncStarted = '2025-05-01T00:00:00.000Z';
204
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
205
+ context: {
206
+ snap_in_version_id: 'test_snap_in_version_id',
207
+ },
208
+ payload: {
209
+ event_type: extraction_1.EventType.StartExtractingMetadata,
210
+ event_context: {
211
+ extraction_start_time: {},
212
+ },
213
+ },
214
+ });
215
+ fetchStateSpy.mockResolvedValue({
216
+ state: JSON.stringify({
217
+ snapInVersionId: 'test_snap_in_version_id',
218
+ lastSuccessfulSyncStarted,
219
+ }),
220
+ });
221
+ // Act
222
+ const state = await (0, state_1.createAdapterState)({
223
+ event,
224
+ initialState: {},
225
+ initialDomainMapping: {},
226
+ });
227
+ // Assert: the fallback is also stored for the following data phase
228
+ expect(event.payload.event_context.extract_from).toBe(lastSuccessfulSyncStarted);
229
+ expect(state.state.pendingWorkersOldest).toBe(lastSuccessfulSyncStarted);
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.platform_extract_from).toBe(platformExtractFrom);
259
+ });
109
260
  it('should skip resolution when extraction_end_time has no type', async () => {
110
261
  // Arrange: platform sends extraction_end_time without a type field
111
262
  const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
@@ -56,8 +56,7 @@ 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",
60
- ContinueExtractingMetadata = "CONTINUE_EXTRACTING_METADATA"
59
+ StartDeletingExtractorAttachmentsState = "START_DELETING_EXTRACTOR_ATTACHMENTS_STATE"
61
60
  }
62
61
  /**
63
62
  * ExtractorEventType is an enum that defines the different types of events that can be sent from the external extractor to ADaaS.
@@ -144,9 +143,7 @@ export declare enum ExtractorEventType {
144
143
  AttachmentExtractionDone = "ATTACHMENT_EXTRACTION_DONE",
145
144
  AttachmentExtractionError = "ATTACHMENT_EXTRACTION_ERROR",
146
145
  ExtractorAttachmentsStateDeletionDone = "EXTRACTOR_ATTACHMENTS_STATE_DELETION_DONE",
147
- ExtractorAttachmentsStateDeletionError = "EXTRACTOR_ATTACHMENTS_STATE_DELETION_ERROR",
148
- MetadataExtractionProgress = "METADATA_EXTRACTION_PROGRESS",
149
- MetadataExtractionDelayed = "METADATA_EXTRACTION_DELAYED"
146
+ ExtractorAttachmentsStateDeletionError = "EXTRACTOR_ATTACHMENTS_STATE_DELETION_ERROR"
150
147
  }
151
148
  /**
152
149
  * @deprecated
@@ -297,11 +294,16 @@ export interface EventContext {
297
294
  external_system_name: string;
298
295
  external_system_type: string;
299
296
  /**
300
- * Resolved start timestamp of extraction (ISO 8601 format).
301
- * Automatically computed by the SDK from extraction_start_time and worker state.
297
+ * Extraction start timestamp (ISO 8601 format), supplied directly by CPv1.1
298
+ * or resolved by the SDK from extraction_start_time and worker state for CPv2.
302
299
  * This is the field developers should read to know when to start extracting from.
303
300
  */
304
301
  extract_from?: string;
302
+ /**
303
+ * Original extraction start timestamp supplied by the platform before the SDK
304
+ * overrides extract_from.
305
+ */
306
+ platform_extract_from?: string;
305
307
  import_slug: string;
306
308
  initial_sync_scope?: InitialSyncScope;
307
309
  mode: string;