@devrev/airsync-sdk 2.0.0-beta.6 → 2.0.0-beta.8

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 (89) hide show
  1. package/README.md +8 -12
  2. package/dist/attachments-streaming/attachments-streaming-pool.d.ts.map +1 -1
  3. package/dist/attachments-streaming/attachments-streaming-pool.js +8 -9
  4. package/dist/common/constants.d.ts +0 -7
  5. package/dist/common/constants.d.ts.map +1 -1
  6. package/dist/common/constants.js +1 -28
  7. package/dist/common/helpers.d.ts.map +1 -1
  8. package/dist/common/helpers.js +1 -1
  9. package/dist/http/errors.d.ts +8 -0
  10. package/dist/http/errors.d.ts.map +1 -0
  11. package/dist/http/errors.js +31 -0
  12. package/dist/index.d.ts +18 -13
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +14 -11
  15. package/dist/logger/logger.d.ts.map +1 -1
  16. package/dist/logger/logger.interfaces.d.ts +2 -2
  17. package/dist/logger/logger.interfaces.d.ts.map +1 -1
  18. package/dist/mappers/mappers.d.ts +9 -3
  19. package/dist/mappers/mappers.d.ts.map +1 -1
  20. package/dist/mappers/mappers.interfaces.d.ts +0 -2
  21. package/dist/mappers/mappers.interfaces.d.ts.map +1 -1
  22. package/dist/mappers/mappers.js +33 -13
  23. package/dist/multithreading/adapters/base-adapter.d.ts +16 -35
  24. package/dist/multithreading/adapters/base-adapter.d.ts.map +1 -1
  25. package/dist/multithreading/adapters/base-adapter.js +89 -23
  26. package/dist/multithreading/adapters/extraction-adapter.d.ts +24 -20
  27. package/dist/multithreading/adapters/extraction-adapter.d.ts.map +1 -1
  28. package/dist/multithreading/adapters/extraction-adapter.js +175 -122
  29. package/dist/multithreading/adapters/loading-adapter.d.ts +13 -23
  30. package/dist/multithreading/adapters/loading-adapter.d.ts.map +1 -1
  31. package/dist/multithreading/adapters/loading-adapter.js +176 -301
  32. package/dist/multithreading/emit.d.ts.map +1 -1
  33. package/dist/multithreading/process-task.d.ts.map +1 -1
  34. package/dist/multithreading/process-task.js +24 -6
  35. package/dist/multithreading/spawn/spawn.d.ts +6 -1
  36. package/dist/multithreading/spawn/spawn.d.ts.map +1 -1
  37. package/dist/multithreading/spawn/spawn.helpers.d.ts.map +1 -1
  38. package/dist/multithreading/spawn/spawn.helpers.js +17 -52
  39. package/dist/multithreading/spawn/spawn.js +30 -26
  40. package/dist/multithreading/worker.js +15 -1
  41. package/dist/repo/repo.d.ts +4 -5
  42. package/dist/repo/repo.d.ts.map +1 -1
  43. package/dist/repo/repo.interfaces.d.ts +5 -4
  44. package/dist/repo/repo.interfaces.d.ts.map +1 -1
  45. package/dist/repo/repo.js +13 -15
  46. package/dist/state/base-state.d.ts +10 -5
  47. package/dist/state/base-state.d.ts.map +1 -1
  48. package/dist/state/base-state.js +39 -36
  49. package/dist/state/extraction-state.d.ts.map +1 -1
  50. package/dist/state/extraction-state.js +39 -4
  51. package/dist/state/state.interfaces.d.ts +13 -7
  52. package/dist/state/state.interfaces.d.ts.map +1 -1
  53. package/dist/state/state.interfaces.js +6 -8
  54. package/dist/state/time-value-resolver.d.ts.map +1 -1
  55. package/dist/state/time-value-resolver.js +3 -2
  56. package/dist/testing/mock-event.d.ts.map +1 -1
  57. package/dist/testing/mock-event.js +0 -8
  58. package/dist/testing/mock-server.d.ts +2 -0
  59. package/dist/testing/mock-server.d.ts.map +1 -1
  60. package/dist/testing/mock-server.interfaces.d.ts +6 -0
  61. package/dist/testing/mock-server.interfaces.d.ts.map +1 -1
  62. package/dist/testing/mock-server.js +37 -6
  63. package/dist/testing/test-adapters.d.ts +22 -0
  64. package/dist/testing/test-adapters.d.ts.map +1 -0
  65. package/dist/testing/test-adapters.js +32 -0
  66. package/dist/types/common.d.ts +4 -1
  67. package/dist/types/common.d.ts.map +1 -1
  68. package/dist/types/common.js +1 -1
  69. package/dist/types/errors.d.ts +13 -7
  70. package/dist/types/errors.d.ts.map +1 -1
  71. package/dist/types/errors.js +24 -0
  72. package/dist/types/extraction.d.ts +60 -44
  73. package/dist/types/extraction.d.ts.map +1 -1
  74. package/dist/types/extraction.js +10 -1
  75. package/dist/types/loading.d.ts +11 -30
  76. package/dist/types/loading.d.ts.map +1 -1
  77. package/dist/types/workers.d.ts +27 -23
  78. package/dist/types/workers.d.ts.map +1 -1
  79. package/dist/uploader/uploader.d.ts +1 -2
  80. package/dist/uploader/uploader.d.ts.map +1 -1
  81. package/dist/uploader/uploader.helpers.d.ts +3 -0
  82. package/dist/uploader/uploader.helpers.d.ts.map +1 -1
  83. package/dist/uploader/uploader.helpers.js +16 -0
  84. package/dist/uploader/uploader.interfaces.d.ts +4 -29
  85. package/dist/uploader/uploader.interfaces.d.ts.map +1 -1
  86. package/dist/uploader/uploader.js +4 -23
  87. package/docs/MIGRATION.md +665 -0
  88. package/package.json +24 -10
  89. package/MIGRATION.md +0 -1028
@@ -0,0 +1,665 @@
1
+ # Migrating connectors from v1 (`@devrev/ts-adaas`) to v2 (`@devrev/airsync-sdk`)
2
+
3
+ This is the complete list of connector-facing changes from `@devrev/ts-adaas`
4
+ 1.x to `@devrev/airsync-sdk` 2.x, written so an AI agent can apply it on its
5
+ own:
6
+
7
+ > Read `MIGRATION.md` from `@devrev/airsync-sdk` and migrate this connector.
8
+
9
+ It ships in the package at `node_modules/@devrev/airsync-sdk/docs/MIGRATION.md`
10
+ (read it from disk; the `exports` map only blocks `import`/`require`). Before
11
+ v2 is installed, use
12
+ `https://raw.githubusercontent.com/devrev/adaas-sdk/v2/docs/MIGRATION.md`.
13
+
14
+ **The wire protocol is unchanged.** Event payloads, routes, headers, the
15
+ artifact flow and every event-type and `ERROR_CODE=…` string are identical to
16
+ v1. Only the TypeScript API changed, all at once, with no compatibility shims.
17
+
18
+ ---
19
+
20
+ ## Runbook
21
+
22
+ **Rules.** Work autonomously; when a rewrite is ambiguous, pick the option
23
+ that preserves runtime behavior and note it in `MIGRATION_REVIEW.md`. Migrate
24
+ source **and** tests. Never change a runtime string value. Don't add comments
25
+ about the migration; the `// v1` / `// v2` labels here are reading aids. Leave
26
+ no compile errors or orphaned imports. A section with no hits is a no-op.
27
+ Existing comments and commented-out code that describe v1 behavior (`emit`,
28
+ `{ id }` / `{ error }` returns, `.data.sync_mapper_record`) are rewritten to
29
+ v2; templates are copied into new connectors, so stale comments spread.
30
+
31
+ 1. **Locate the package.** Find the `package.json` that depends on
32
+ `@devrev/ts-adaas` (generated connectors keep it in `code/`). Run everything
33
+ from there. If it already depends on `@devrev/airsync-sdk` only, report
34
+ "already on v2" and stop.
35
+ 2. **Start from green latest v1.** If behind the latest stable 1.x, upgrade and
36
+ apply the release notes of every release in between that mentions a
37
+ migration (the last ones were 1.16.0–1.18.0), then get tsc/lint/build/test
38
+ green on v1. Record failures that remain as **Pre-existing**.
39
+ 3. **Install v2.**
40
+ ```bash
41
+ npm uninstall @devrev/ts-adaas
42
+ npm install @devrev/airsync-sdk@beta # @latest after GA
43
+ ```
44
+ If the installed `docs/MIGRATION.md` differs from this copy, switch to it.
45
+ 4. **Apply §1–§14 in order** across source and tests. After §1, search by
46
+ symbol (`\.emit\(`, `WorkerAdapter`, `sync_mapper_record`, …), not by the old
47
+ package name; test files that import nothing from the SDK still need
48
+ changes.
49
+ 5. **Verify**: `npm install && npx tsc --noEmit && npm run lint && npm run build && npm test`.
50
+ If tsconfig excludes tests, the test run is their type check. Fix and rerun,
51
+ at most 4 attempts; the migration must add no new failures compared to
52
+ step 2.
53
+ 6. **Sweep.** This must return nothing except legitimate hits (an
54
+ `EventEmitter.emit`, the connector's own `axiosClient` from §9):
55
+ ```bash
56
+ grep -rnE "@devrev/ts-adaas|airsync-sdk/dist|\bprocessTask\b|WorkerAdapter|\.emit\(|hasWorkerEmitted|Airdrop(Event|Message)|axiosClient|(format|serialize)AxiosError|workerPath:|overridenOptions|skipConfirmation|isLocalDevelopment|--local|@devrev/typescript-sdk|(Extractor|Loader)EventType|ExtractionCommonError|ERROR_CODE=|sdkState|onUpload|uploadedArtifacts|currentEventDataLength|EXTRACTION_(DATA|METADATA|ATTACHMENTS|EXTERNAL_SYNC_UNITS)_" \
57
+ --include='*.ts' --include='*.js' --include='*.json' --include='*.yml' --include='*.yaml' --include='*.md' --include='*.mdc' . \
58
+ | grep -vE 'node_modules|/dist/|/coverage/|package-lock.json|MIGRATION_REVIEW.md'
59
+ ```
60
+ The sweep covers `test/` and other folders that tsc, lint and jest often
61
+ exclude. A hit there is real: `@devrev/typescript-sdk` must be in
62
+ `package.json` (§1), and a `local` argv must become the env var (§3).
63
+ Also grep `getByExternalId|getByTargetId` and check each call against §10:
64
+ existence checks there compile but silently invert.
65
+ Then rewrite any connector docs (`README`, `AGENTS.md`, `CLAUDE.md`, `docs/`)
66
+ that describe v1 behavior.
67
+ 7. **Write `MIGRATION_REVIEW.md`** at the repo root: one line per section that
68
+ changed code with `high` / `medium` / `low confidence`, a sentence on what
69
+ to verify for medium/low, and the final check results with an
70
+ **Unresolved** list. Use medium/low for anything judged: emits bubbled out of
71
+ helpers (§4), paths that emitted nothing (§4), `onTimeout` bodies kept (§4),
72
+ delay values that look like milliseconds or headers (§4), the cursor decision
73
+ (§8), ESU repo rewrites (§7), mapper error handling (§10), and a v1 baseline
74
+ upgrade.
75
+ ```markdown
76
+ # v2 Migration Review
77
+ Migrated `@devrev/ts-adaas` 1.20.2 → `@devrev/airsync-sdk` 2.0.0.
78
+ - §4 emit→return — high confidence
79
+ - §8 state / cursor — low confidence: repointed `lastSuccessfulSyncStarted`
80
+ to `extract_from`; confirm the connector didn't own this cursor.
81
+ ## Verification
82
+ tsc ✓ · lint ✓ · build ✓ · test ✓ (48 passed)
83
+ ```
84
+ 8. **Report** the start and end versions, which sections fired, the check
85
+ results and how many review entries need a second look.
86
+
87
+ ---
88
+
89
+ ## 1. Package, imports and runtime
90
+
91
+ - Replace `@devrev/ts-adaas` with `@devrev/airsync-sdk` **everywhere the
92
+ string appears**: imports, `jest.mock` / `jest.requireActual` arguments and
93
+ `moduleNameMapper`.
94
+ - **Import only from the root.** `exports` exposes nothing else, so every
95
+ `@devrev/airsync-sdk/dist/**` import fails with
96
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`. Everything public is on the root, including
97
+ `Mappers`, `Item`, `ItemTypeToLoad`, `InitialSyncScope`, the mapper
98
+ `*Params` / `*Response` types and `SyncMapperRecord*`. Deep imports with no
99
+ root equivalent are listed in §11. Never import the SDK's internal HTTP client
100
+ (`dist/http/axios-client-internal`): use the connector's own client (§9).
101
+ - **Node.js ≥ 20.** Bump the connector's `engines`, CI image and `.nvmrc` if
102
+ they pin older. Dependabot / CI configs that name `@devrev/ts-adaas` switch to
103
+ `@devrev/airsync-sdk`.
104
+ - `ts-node` and `@devrev/typescript-sdk` are no longer SDK dependencies. A
105
+ connector that runs `.ts` workers directly needs `ts-node` in its own
106
+ `devDependencies`. A connector that uses `@devrev/typescript-sdk` must list
107
+ it. The template's local test server (`code/test/runner.ts`, and the copy
108
+ `airsync start-worker` writes to `code/.snapin-test/`) imports
109
+ `@devrev/typescript-sdk/dist/snap-ins`, so template connectors add it to
110
+ `devDependencies`. Most connectors exclude `test/` from tsc, lint and jest,
111
+ so the gates pass and the local worker then fails with
112
+ `Cannot find module '@devrev/typescript-sdk/dist/snap-ins'`.
113
+ `InputData` (the type of `event.input_data`) is now exported from the
114
+ root.
115
+
116
+ ## 2. Event types
117
+
118
+ | v1 | v2 |
119
+ |----|----|
120
+ | `AirdropEvent` | `AirSyncEvent` |
121
+ | `AirdropMessage` | `AirSyncMessage` |
122
+
123
+ The shapes are unchanged. `AirSyncEvent.context` now also declares `user_id`,
124
+ `dev_oid`, `source_id` and `service_account_id` (on `event.context`, not on
125
+ `payload.event_context`). Delete a hand-rolled context type that added these
126
+ (e.g. `interface CustomAirdropEvent extends AirdropEvent { context: CustomContext }`):
127
+ a narrower `context` no longer satisfies `AirSyncEvent`. Keep only the fields
128
+ the SDK type lacks.
129
+
130
+ Deprecated `EventContext` aliases are removed. Use the current names:
131
+
132
+ | Removed | Use |
133
+ |---------|-----|
134
+ | `dev_org` | `dev_oid` |
135
+ | `dev_user`, `dev_user_id` | `dev_uid` |
136
+ | `external_sync_unit` | `external_sync_unit_id` |
137
+ | `external_system` | `external_system_id` |
138
+ | `sync_run`, `sync_run_id` | `run_id` |
139
+ | `uuid` | `request_id_adaas` |
140
+
141
+ `reset_extraction` and `reset_extract_from` remain (the SDK never read them);
142
+ leave connector logic that uses them alone.
143
+
144
+ ## 3. `spawn`
145
+
146
+ | v1 | v2 |
147
+ |----|----|
148
+ | `workerPath` | removed; use `baseWorkerPath` |
149
+ | `baseWorkerPath?` | **required**, the directory containing `workers/` (usually `__dirname`) |
150
+ | `options.workerPathOverrides` | top-level `workerPathOverrides` |
151
+ | `options: WorkerAdapterOptions` | `options?: SpawnOptions` (`{ timeout?, batchSize? }`) |
152
+ | `options.isLocalDevelopment`, `local` / `--local` argv | removed; set `AIRSYNC_LOCAL_DEVELOPMENT=1` |
153
+
154
+ ```ts
155
+ // v1
156
+ spawn({
157
+ event,
158
+ initialState,
159
+ workerPath: __dirname + '/workers/data-extraction',
160
+ options: { isLocalDevelopment: true, workerPathOverrides: { [EventType.StartExtractingData]: '/workers/custom' } },
161
+ });
162
+ // v2
163
+ spawn({
164
+ event,
165
+ initialState,
166
+ baseWorkerPath: __dirname,
167
+ workerPathOverrides: { [EventType.StartExtractingData]: '/workers/custom' },
168
+ });
169
+ ```
170
+
171
+ - If `workerPath` came from a dispatcher helper (e.g.
172
+ `getWorkerPerLoadingPhase(event)`), delete the helper and its now-unused
173
+ imports.
174
+ - **Fixture and test events** must use the modern event-type strings; v1
175
+ translated legacy ones and v2 doesn't. Replace `EXTRACTION_EXTERNAL_SYNC_UNITS_START`,
176
+ `EXTRACTION_METADATA_START`, `EXTRACTION_DATA_START` / `_CONTINUE` / `_DELETE`
177
+ and `EXTRACTION_ATTACHMENTS_START` / `_CONTINUE` / `_DELETE` with
178
+ `START_EXTRACTING_EXTERNAL_SYNC_UNITS`, `START_EXTRACTING_METADATA`,
179
+ `START_EXTRACTING_DATA` / `CONTINUE_EXTRACTING_DATA` /
180
+ `START_DELETING_EXTRACTOR_STATE` and `START_EXTRACTING_ATTACHMENTS` /
181
+ `CONTINUE_EXTRACTING_ATTACHMENTS` / `START_DELETING_EXTRACTOR_ATTACHMENTS_STATE`.
182
+ - **Local mode** comes only from the `AIRSYNC_LOCAL_DEVELOPMENT=1` environment
183
+ variable, and is ignored on Lambda. v1 also enabled it from a bare `local`
184
+ argument, which the template test server receives (`main.ts local --port …`,
185
+ also used by `airsync start-worker`). v2 ignores both `local` and `--local`
186
+ without an error: logs come out as production JSON and no copies land in
187
+ `extracted_files/`. A local runner that did `process.argv.push('--local')`
188
+ or relied on the `local` argument sets
189
+ `process.env.AIRSYNC_LOCAL_DEVELOPMENT = '1'` before calling `spawn` instead,
190
+ or documents running the worker with `AIRSYNC_LOCAL_DEVELOPMENT=1`.
191
+
192
+ ## 4. Workers return a `TaskResult` instead of emitting
193
+
194
+ `adapter.emit` is gone (a compile error). A task reports how the phase ended by
195
+ returning a result; the SDK picks the platform event and emits it exactly once.
196
+
197
+ ```ts
198
+ export type TaskResult =
199
+ | { status: 'success' }
200
+ | { status: 'progress' }
201
+ | { status: 'delay'; delaySeconds: number }
202
+ | { status: 'error'; error: ErrorRecord }; // { message: string; code?: AirSyncErrorCode }
203
+ ```
204
+
205
+ Replace `processTask` with `processExtractionTask` or `processLoadingTask`,
206
+ depending on the worker (extraction workers use repos or `streamAttachments`;
207
+ loading workers use `loadItemTypes`, `loadAttachments` or `mappers`). Their
208
+ generic stays the connector state: `processTask<State>` →
209
+ `processExtractionTask<State>`.
210
+
211
+ ```ts
212
+ // v1
213
+ processTask({
214
+ task: async ({ adapter }) => {
215
+ // ...
216
+ await adapter.emit(ExtractorEventType.DataExtractionDone);
217
+ },
218
+ onTimeout: async ({ adapter }) => {
219
+ await adapter.postState();
220
+ await adapter.emit(ExtractorEventType.DataExtractionProgress);
221
+ },
222
+ });
223
+ // v2
224
+ processExtractionTask({
225
+ task: async ({ adapter }) => {
226
+ // ...
227
+ return { status: 'success' };
228
+ },
229
+ });
230
+ ```
231
+
232
+ | v1 | v2 |
233
+ |----|----|
234
+ | `emit(XxxDone)` | `return { status: 'success' }` |
235
+ | `emit(XxxProgress, { progress })` | `return { status: 'progress' }` |
236
+ | `emit(XxxDelayed, { delay })` | `return { status: 'delay', delaySeconds: delay }` — keep the same number; the platform always read it as seconds. If it is clearly milliseconds or a raw `Retry-After` header, keep the behavior (`Number(header)`) and flag it in the review |
237
+ | `emit(XxxDelayed)` with no `delay` | `delaySeconds` is required; use the connector's existing default retry constant, else `0` (what v1 effectively sent), and flag it |
238
+ | `emit(XxxError, { error })` | `return { status: 'error', error }` — `error` is required; fall back to `{ message: '<phase> failed' }` |
239
+ | `emit(XxxDone, { error })` (finished with warnings) | `return { status: 'success' }` and log the error. Returning `error` would fail the phase |
240
+ | `emit(ExternalSyncUnitExtractionDone, { external_sync_units })` | push to a repo, then `success` (§7) |
241
+ | `emit(MetadataExtractionDone, { pre_extraction_item_counts })` | `adapter.preExtractionItemCounts = counts`, then `success` |
242
+ | a path that **emitted nothing** (a `catch` that only logs, a branch that falls through) | `{ status: 'error', error: { message } }`, or rethrow. In v1 the SDK reported these as a phase error ("Worker exited without emitting"); never turn them into `success` or `progress` |
243
+ | any `emit(XxxDone)` after `loadItemTypes` / `loadAttachments` / `streamAttachments`, guarded (`!adapter.hasWorkerEmitted && !adapter.isTimeout`) or not | `return adapter.loadItemTypes(...)` (or the other call). Never discard their result and return `success`: v1 emitted their `delay` / `error` itself. Drop the guard and any module-level "already emitted" flags |
244
+
245
+ In non-resumable phases (external sync units, state deletion), `progress` and
246
+ `delay` become `*Error`.
247
+
248
+ **Emits inside helpers.** Only the task's return value reaches the SDK, so a
249
+ helper that emitted must hand its result back up. Either return
250
+ `TaskResult | null` up the call chain (`null` = keep going), or record it and
251
+ return it once the loop ends:
252
+
253
+ ```ts
254
+ private result: TaskResult | undefined;
255
+ private onPermissionDenied() {
256
+ this.result = { status: 'error', error: { message: '...' } };
257
+ }
258
+ // at the end of the task: a recorded result, else continue only on timeout
259
+ return this.result ?? (adapter.isTimeout
260
+ ? { status: 'progress' }
261
+ : { status: 'error', error: { message: 'Extraction ended without a result' } });
262
+ ```
263
+
264
+ If a shared `handleError` emitted from many `catch` blocks that rethrow, v1
265
+ kept only the first emit. Record the first result (`firstError ??= …`) and
266
+ return it from the task's outermost `catch`.
267
+
268
+ If a helper (often the HTTP client's 429 handler) **emitted and then threw a
269
+ sentinel error** that call sites rethrow, keep the throw, drop the emit, and map
270
+ it in the task's outermost `catch`:
271
+ `if (e instanceof RateLimitDelayedError) return { status: 'delay', delaySeconds: e.retryAfterSeconds };`.
272
+ Don't let it reach a generic error branch, or every rate limit fails the sync.
273
+ In external sync units (non-resumable) the SDK still sends `delay` as an
274
+ error; that matches nothing better, so keep it and flag it in the review.
275
+
276
+ Tests of helpers that now record a result read it back
277
+ (`expect(extractor['result']).toEqual(…)`) or, for `TaskResult | null`
278
+ helpers, assert on the return value.
279
+
280
+ **Timeouts.**
281
+
282
+ - In the **task body**, `if (adapter.isTimeout) return;` becomes
283
+ `return { status: 'progress' };`. The same applies to a `return;` in a
284
+ `catch` that handles a timeout (`if (adapter.isTimeout || e instanceof
285
+ MyTimeout) return;`). Early `return`s inside `Promise<void>` helpers stay as
286
+ they are, as long as the task checks `isTimeout` after calling them.
287
+ - After the soft timeout, a returned `success` or `progress` is replaced by
288
+ the `onTimeout` result. A returned `error` or `delay` is always emitted as is.
289
+ - `onTimeout` is optional and defaults to `progress` (or a timeout error in
290
+ external sync units and state deletion; metadata is resumable). v1 ran
291
+ `onTimeout` only when the task finished **without emitting**; v2 also runs it
292
+ when the task returns `success` after the timeout. So:
293
+ - **Delete** it when it only emitted the usual event or computed a
294
+ `progress` number, when the v1 task emitted on every path, or when the task
295
+ only returns `loadItemTypes` / `loadAttachments` / `streamAttachments`
296
+ (they handled the timeout themselves).
297
+ - **Keep** it, returning a `TaskResult`, only when it could actually run in
298
+ v1 (the task had a path that didn't emit) **and** it did real work or set a
299
+ custom error. Make its body safe to run after a completed task. A custom
300
+ "Lambda timeout" `onTimeout` on a task that emits on every path is
301
+ deleted.
302
+ - Don't call `process.exit()` or `postState()` in `onTimeout`; the SDK saves
303
+ state and exits.
304
+
305
+ **Throws.** A task that throws emits the phase's `*Error` with the message,
306
+ along with the artifacts already uploaded. `repo.push()` and `repo.upload()`
307
+ resolve `void` and throw if an upload fails. **Any use of `push()`'s value is
308
+ now wrong**, and `Boolean(await repo.push(x))` or `if (!(await …push(x)))`
309
+ compiles but is always falsy. Make wrappers `await repo.push(x)` and return
310
+ `true` (or `void`). Keep a top-level `try/catch` that maps an error to `delay`
311
+ (e.g. a thrown 429); otherwise it's only needed for a custom message.
312
+
313
+ `ProcessTaskInterface<T>` and `TaskAdapterInterface<T>` now take the
314
+ **adapter** type: `ProcessTaskInterface<ExtractionAdapter<State>>`, not
315
+ `<State>`.
316
+
317
+ ## 5. Adapters
318
+
319
+ The `WorkerAdapter` class is gone. Annotate with the phase's adapter:
320
+
321
+ ```ts
322
+ async function extractList(adapter: ExtractionAdapter<State>) { ... }
323
+ ```
324
+
325
+ | Members | On |
326
+ |---------|----|
327
+ | `initializeRepos`, `getRepo`, `streamAttachments`, `processAttachment`, `shouldExtract`, `artifacts`, `preExtractionItemCounts` | `ExtractionAdapter` |
328
+ | `loadItemTypes`, `loadAttachments`, `mappers`, `reports`, `processedFiles` | `LoadingAdapter` |
329
+ | `event`, `state`, `postState`, `uploadArtifact`, `downloadArtifact`, `isTimeout`, `extractionScope` | both |
330
+
331
+ - The adapters and `Repo` are **type-only**: `new ExtractionAdapter(...)` and
332
+ `class X extends LoadingAdapter<S>` don't compile; `interface X extends …`
333
+ does. SDK internals (`emit`, `sdkState`, `options`, constructors, …) aren't
334
+ public. `isTimeout` and `artifacts` are read-only.
335
+ - Code that used `adapter.mappers` during extraction creates its own
336
+ `new Mappers({ event: adapter.event })` once in the task (or a class
337
+ constructor) and passes it down to helpers as a `mappers` argument typed
338
+ `Pick<Mappers, 'getByExternalId'>`; tests then pass a plain double.
339
+ - **Integration tests** that built an adapter by hand use the root factories:
340
+
341
+ ```ts
342
+ // v1
343
+ const adapter = new WorkerAdapter({ event, adapterState: new State({ event, initialState }) });
344
+ adapterState.state.fromDevRev = { filesToLoad };
345
+ // v2
346
+ const adapter = createTestExtractionAdapter({ event, initialState });
347
+ const loader = createTestLoadingAdapter({ event, initialState, filesToLoad });
348
+ ```
349
+
350
+ Platform calls still go to the event's URLs. Build the event with
351
+ `createMockEvent(mockServer.baseUrl, …)` and serve the URLs with
352
+ `MockServer`. Artifact downloads need two routes: `GET
353
+ /internal/airdrop.artifacts.download-url` returning `{ download_url }`, and
354
+ that URL serving the gzipped JSONL. Assert on the returned `TaskResult`,
355
+ `adapter.reports` and `adapter.processedFiles`, not on `emit` or SDK state;
356
+ to test a resume, build a second adapter with `filesToLoad` at the cursor you
357
+ expect. To simulate a timeout, use a plain mock object, not a real adapter.
358
+ Test repos are plain objects (`{ itemType, push: jest.fn() }`).
359
+ - `adapter.postState()` takes no argument and **throws** on failure (v1 exited
360
+ the process). State is saved on every emit, so keep a call only to save
361
+ state before an external write that must not be repeated. v1
362
+ `postState(next)` becomes `adapter.state = next; await adapter.postState();`.
363
+
364
+ ## 6. Loading, attachments and callback results
365
+
366
+ `loadItemTypes`, `loadAttachments` and `streamAttachments` return a
367
+ `TaskResult`. Return it directly:
368
+
369
+ ```ts
370
+ processLoadingTask({ task: async ({ adapter }) => adapter.loadItemTypes({ itemTypesToLoad }) });
371
+ processExtractionTask({ task: async ({ adapter }) => adapter.streamAttachments({ stream }) });
372
+ ```
373
+
374
+ The result has no `reports` / `processed_files`. To add a report, push it to
375
+ `adapter.reports` before returning. Tests assert on `adapter.reports`.
376
+
377
+ **Connector callbacks** return a `status` union, like `TaskResult`:
378
+
379
+ | Callback | v1 | v2 |
380
+ |----------|----|----|
381
+ | attachment `stream` | `{ httpStream }` / `{ delay }` / `{ error: { message, statusCode? } }` | `{ status: 'success', httpStream }` / `{ status: 'delay', delaySeconds }` / `{ status: 'error', error: { message } }` |
382
+ | loader `create` / `update` | `{ id, modifiedDate? }` / `{ delay }` / `{ error: 'msg' \| { message } }` / neither (counted as failed) | `{ status: 'success', id, modifiedDate? }` / `{ status: 'delay', delaySeconds }` / `{ status: 'error', error: { message } }` |
383
+ | `adapter.processAttachment`, custom `iterator` | `{ delay? , error? } \| undefined` | `{ status: 'success' }` / `{ status: 'delay', delaySeconds }` / `{ status: 'error', error }` |
384
+
385
+ ```ts
386
+ // custom iterator, v2
387
+ const response = await adapter.processAttachment(attachment, stream);
388
+ if (response.status === 'delay') return response;
389
+ if (response.status === 'error') console.warn(response.error.message);
390
+ // ...
391
+ return { status: 'success' }; // was a bare `return;`
392
+ ```
393
+
394
+ - Loader `error` results still count as failed and loading continues. A v1
395
+ result with **both** `delay` and `error` becomes `delay` (v1 honored the delay).
396
+ - `success` needs a real `id`: a v1 `{ id }` whose value may be `undefined`
397
+ (from an untyped response) was counted as failed, so guard it:
398
+ `if (!id) return { status: 'error', error: { message } }`. Extra fields such
399
+ as `skip` are dropped; every `success` creates or updates a mapper record.
400
+ - Shared helpers typed as these responses change too. Callers that read
401
+ `result.delay` or interpolate `${result.error}` (now an object) switch on
402
+ `result.status`.
403
+ - An error `code` returned from the stream callback is only logged; the pool
404
+ skips that attachment.
405
+ - `httpStream` is typed `HttpStreamResponse` (`{ data, headers }`); an axios
406
+ response still satisfies it. Types derived by indexing the old interface
407
+ (`ExternalSystemAttachmentStreamingResponse['httpStream']`) use the root
408
+ `HttpStreamResponse` instead.
409
+ - `statusCode` on stream errors is gone; put it in the message if you need it.
410
+
411
+ **`processAttachment` works in any extraction phase.** SSOR artifacts created
412
+ outside the attachments phase are held by the SDK and reported with
413
+ `ATTACHMENT_EXTRACTION_DONE`. **Delete any workaround** for this, along with
414
+ its state fields and tests. Workarounds typically override the
415
+ `ssor_attachment` repo's `onUpload`, splice `uploadedArtifacts`, adjust
416
+ `currentEventDataLength`, or stash artifact refs in state.
417
+
418
+ ## 7. External sync units go through a repo
419
+
420
+ `EventData.external_sync_units` is removed. Push them to a repo:
421
+
422
+ ```ts
423
+ adapter.initializeRepos([
424
+ // matches the batching v1 used for the inline path
425
+ { itemType: AirSyncDefaultItemTypes.EXTERNAL_SYNC_UNITS, overriddenOptions: { batchSize: 25000 } },
426
+ ]);
427
+ await adapter.getRepo(AirSyncDefaultItemTypes.EXTERNAL_SYNC_UNITS)?.push(externalSyncUnits);
428
+ return { status: 'success' };
429
+ ```
430
+
431
+ If the connector already pushed ESUs to a repo, keep its options as they are
432
+ (apart from the §14 rename and removing `skipConfirmation`) and only convert
433
+ the emit.
434
+
435
+ ## 8. State: connector state only
436
+
437
+ - `adapter.state` is the connector's own state (the `initialState` shape);
438
+ reads and writes work as before. SDK bookkeeping (`toDevRev`, `fromDevRev`,
439
+ `workers*`, `pendingWorkers*`, `lastSyncStarted`,
440
+ `lastSuccessfulSyncStarted`, `snapInVersionId`) is internal. The
441
+ `AdapterState<S>` alias is removed; use your own `State` type.
442
+ - **Persisted state** becomes `{ stateVersion: 2, connectorState, sdkState }`.
443
+ A v1 blob is migrated automatically on first read: known SDK keys move to
444
+ `sdkState` and everything else becomes connector state. **Rolling back to v1
445
+ after a v2 worker has run is not supported**; drain or reset syncs first. A
446
+ v1 state with a top-level key named `connectorState` or `sdkState` is
447
+ misread; rename it before upgrading.
448
+ - **Sync window**: read
449
+ `adapter.event.payload.event_context.extract_from` / `extract_to`. The SDK
450
+ resolves them from the platform's time values, or from the last successful
451
+ sync on platforms that don't send them.
452
+ - Remove SDK fields (`toDevRev`, `fromDevRev`, …) from the connector's own
453
+ `State` interface, `getInitialState` and fixtures.
454
+
455
+ **`lastSyncStarted` / `lastSuccessfulSyncStarted`: rename or repoint.**
456
+ Getting this wrong compiles but silently breaks incremental sync.
457
+
458
+ - **Not in the connector's `State` interface** (it came from the SDK): delete
459
+ writes, and repoint reads to `extract_from`.
460
+ - **Declared and used as the connector's own cursor** (derived from data, or
461
+ read alongside `extract_from`): **rename** it (e.g. →
462
+ `lastSuccessfulWindowStart`) everywhere, including fixtures. Otherwise the v1
463
+ auto-migration moves it into SDK state and the connector loses it.
464
+ - **Declared but only mirrors the SDK**: its only writes are "now" when data
465
+ extraction starts and a copy on finish. Treat it as not declared, **even if
466
+ it is read next to `extract_from`**. Renaming would leave it empty after the
467
+ upgrade, because the v1 value moves into SDK state.
468
+
469
+ `extract_from` equals the last successful sync start only when the platform
470
+ sends no time window. If a repointed read feeds logic beyond "skip items older
471
+ than this" (e.g. distinguishing first syncs), flag it as low confidence; there
472
+ is no public "last successful sync" value. Fixtures move from
473
+ `adapter.state.lastSuccessfulSyncStarted = X` to
474
+ `event.payload.event_context.extract_from = X`.
475
+
476
+ ## 9. HTTP: bring your own axios client
477
+
478
+ `axios`, `axiosClient`, `formatAxiosError`, `serializeAxiosError` and
479
+ `HTTPResponse` are no longer exported. Add `axios` (and `axios-retry` for a
480
+ retrying client) to the connector's dependencies.
481
+
482
+ - **`axios`** (v1 re-exported plain axios, without retries) →
483
+ `import axios from 'axios'`. Calls stay as they are; don't route them through
484
+ a retrying client.
485
+ - **`axiosClient`** → the connector's own client, in its own module (e.g.
486
+ `src/http/axios-client.ts`), so tests can mock that module:
487
+
488
+ ```ts
489
+ import axios from 'axios';
490
+ import axiosRetry from 'axios-retry';
491
+
492
+ export const axiosClient = axios.create();
493
+ axiosRetry(axiosClient, {
494
+ retries: 5,
495
+ retryDelay: (retryCount, error) => axiosRetry.exponentialDelay(retryCount, error, 1000),
496
+ retryCondition: (error) =>
497
+ (axiosRetry.isNetworkOrIdempotentRequestError(error) && error.response?.status !== 429) ||
498
+ (error.response?.status ?? 0) >= 500,
499
+ onMaxRetryTimesExceeded: (error) => {
500
+ delete error.config?.headers?.authorization;
501
+ delete error.config?.headers?.Authorization;
502
+ delete error.request?._header;
503
+ },
504
+ });
505
+ ```
506
+
507
+ A deep import of the SDK's internal client (`dist/http/axios-client-internal`)
508
+ relied on more: a 30 s timeout, retries of 429s that send `Retry-After` (waiting
509
+ that long) and of `ECONNABORTED`. Reproduce what the connector depends on with
510
+ `axios.create({ timeout: 30_000 })`, `shouldResetTimeout: true`, and a
511
+ `retryCondition` / `retryDelay` that honor `Retry-After`.
512
+
513
+ For logging, use the root `serializeError(error)`, which returns a string.
514
+ Where `serializeAxiosError(e)` was used as an object (spread, or `.response`),
515
+ build the fields you need from the axios error yourself, leaving out headers.
516
+
517
+ ## 10. Mappers
518
+
519
+ | Method | v1 | v2 |
520
+ |--------|----|----|
521
+ | `getByTargetId`, `getByExternalId` | `AxiosResponse<T>`; rejects with 404 when missing | `T \| null`; `null` when no record exists |
522
+ | `create`, `update` | `AxiosResponse<T>` | `T` |
523
+ | any other failure | axios error | `AirSyncHttpError` (`message`, `status?`), exported from the root |
524
+
525
+ ```ts
526
+ // v1
527
+ try {
528
+ const res = await mappers.getByTargetId({ sync_unit, target });
529
+ return res.data.sync_mapper_record.external_ids[0];
530
+ } catch (e) {
531
+ if (axios.isAxiosError(e) && e.response?.status === 404) return undefined;
532
+ throw e;
533
+ }
534
+ // v2
535
+ const record = await mappers.getByTargetId({ sync_unit, target });
536
+ return record?.sync_mapper_record.external_ids[0];
537
+ ```
538
+
539
+ - Drop `.data`; loosely typed code would otherwise read `undefined`.
540
+ - **Existence checks invert silently.** `try { await mappers.getByExternalId(…); return true; } catch { return false; }`
541
+ now always returns `true`, because a missing record resolves `null`. Rewrite
542
+ the `try` body as `return (await mappers.getByExternalId(…)) !== null;` and
543
+ **keep the `catch`** if v1 had one, so other failures still mean what they
544
+ meant before. Find these with
545
+ `grep -n "getByExternalId\|getByTargetId"`, not by searching for 404.
546
+ - Hand-written structural types (`{ getByTargetId(): Promise<{ data?: … }> }`)
547
+ keep `.data` compiling. Replace them with `Pick<Mappers, 'getByTargetId'>`.
548
+ - Replace 404 `catch` branches with a `null` check.
549
+ - Replace `isAxiosError` checks on mapper calls with
550
+ `e instanceof AirSyncHttpError` / `e.status`. Checks on the connector's own
551
+ HTTP calls stay.
552
+ - Test doubles: `mockResolvedValue({ data: { sync_mapper_record } })` →
553
+ `mockResolvedValue({ sync_mapper_record })`; a 404 `mockRejectedValue(...)`
554
+ or a "not found" `mockResolvedValue({ data: {} })` → `mockResolvedValue(null)`.
555
+ Find them with `grep sync_mapper_record`.
556
+
557
+ ## 11. Removed symbols
558
+
559
+ | Removed | Replacement |
560
+ |---------|-------------|
561
+ | `ExtractorEventType`, `LoaderEventType` | none; return a `TaskResult` (§4), and in tests assert on it. Compare against the raw wire string if really needed |
562
+ | `ExtractionCommonError` | `AirSyncErrorCode` on `error.code` (§12) |
563
+ | `WorkerAdapterOptions` | `SpawnOptions` (spawn), `RepoOptions` (`overriddenOptions`) |
564
+ | `WorkerAdapterInterface`, `SpawnInterface`, `StreamResponse`, `StreamAttachmentsResponse` | none; drop the import |
565
+ | `ArtifactsPrepareResponse` | none; if the connector uses it as its own type, declare it locally (`{ url: string; id: string; form_data: { key: string; value: string }[] }`) |
566
+ | worker IPC types (`WorkerEvent`, `WorkerMessage*`, `WorkerData`, `GetWorkerPathInterface`) | none; assert on the returned `TaskResult` |
567
+ | `State`, `createAdapterState`, `ToDevRev`, `AdapterState` | `createTest*Adapter` (§5); drop SDK state types (§8) |
568
+ | `Uploader` (deep import of `dist/uploader/uploader`, used to store and read back the connector's own artifacts) | `adapter.uploadArtifact(itemType, data)` → `{ id }` and `adapter.downloadArtifact<T>(id)` → `T[]`; both throw instead of returning `{ error }` |
569
+ | `Adapter`, `createAdapter`, `DemoExtractor`, `HTTPClient`, `defaultResponse`, deprecated `Uploader` | the v2 worker contract, your own axios client, repos |
570
+ | `ExtractionMode` | `SyncMode` |
571
+ | `EventContextIn`, `EventContextOut` | `EventContext` |
572
+ | `DomainObjectState`, `ErrorLevel`, `LogRecord`, `AdapterUpdateParams` | none |
573
+ | `translate*EventType` helpers | none; the platform sends modern strings |
574
+ | `EventType.UnknownEventType` | the string `'UNKNOWN_EVENT_TYPE'` |
575
+ | `adapter.hasWorkerEmitted`, `adapter.emit` spies | none; return / assert on the `TaskResult` (§4, §13) |
576
+ | `EventData.progress`, `EventData.external_sync_units` | none (§4, §7) |
577
+
578
+ Deprecated `EventType` members are removed. They had **different** string
579
+ values from their replacements:
580
+
581
+ | Removed | Use |
582
+ |---------|-----|
583
+ | `ExtractionExternalSyncUnitsStart` | `StartExtractingExternalSyncUnits` |
584
+ | `ExtractionMetadataStart` | `StartExtractingMetadata` |
585
+ | `ExtractionDataStart` / `ExtractionDataContinue` | `StartExtractingData` / `ContinueExtractingData` |
586
+ | `ExtractionDataDelete` | `StartDeletingExtractorState` |
587
+ | `ExtractionAttachmentsStart` / `ExtractionAttachmentsContinue` | `StartExtractingAttachments` / `ContinueExtractingAttachments` |
588
+ | `ExtractionAttachmentsDelete` | `StartDeletingExtractorAttachmentsState` |
589
+
590
+ This also applies to `workerPathOverrides` keys. `EventType` can gain members
591
+ in minor versions, so give `switch` statements over it a `default`.
592
+
593
+ ## 12. Error codes
594
+
595
+ `ExtractionCommonError` is now `AirSyncErrorCode`. Its values are bare codes,
596
+ and you set them on `error.code`:
597
+
598
+ ```ts
599
+ // v1
600
+ return { status: 'error', error: { message: ExtractionCommonError.EXTERNAL_SYNC_UNIT_DELETED } };
601
+ // v2
602
+ return {
603
+ status: 'error',
604
+ error: { message: `Project ${id} was deleted.`, code: AirSyncErrorCode.EXTERNAL_SYNC_UNIT_DELETED },
605
+ };
606
+ ```
607
+
608
+ The platform only recognizes a code when the message is exactly
609
+ `ERROR_CODE=<code>`, so the SDK sends just that and logs your `message`. Write
610
+ a useful `message`. Raw `'ERROR_CODE=…'` strings and connector helpers that
611
+ return them (`HISTORICAL_DONE`, `authErrorCode(e)`) still work at runtime but
612
+ are migrated too: make the helper return `AirSyncErrorCode | undefined`, set it
613
+ as `code`, and update tests that pin the literal. The wire string doesn't
614
+ change.
615
+
616
+ ## 13. Tests
617
+
618
+ - Mock factories: `processTask` → `processExtractionTask` /
619
+ `processLoadingTask`. Drop the `WorkerAdapter`, `axios`, `axiosClient` and
620
+ `emit: jest.fn()` keys; if nothing is left, or the file under test never
621
+ imports the SDK, delete the `jest.mock` entirely. Keep `jest.requireActual`
622
+ only for enums that still exist (`EventType`, `SyncMode`, `AirSyncErrorCode`).
623
+ - HTTP mocks: a test that mocked the SDK's `axiosClient` mocks the connector's
624
+ client module instead (`jest.mock('../http/axios-client', () => ({ axiosClient: { get: jest.fn(), post: jest.fn() } }))`).
625
+ Don't automock `axios` when the source calls `axios.create()`, because the
626
+ client becomes `undefined`. For plain `axios` use
627
+ `jest.mock('axios', () => ({ __esModule: true, default: { get: jest.fn(), isAxiosError: jest.fn() }, AxiosError: jest.requireActual('axios').AxiosError }))`.
628
+ Patching a client's `defaults.adapter` now affects only connector traffic.
629
+ - ESU tests assert on the repo: give the mock adapter
630
+ `initializeRepos: jest.fn()` and `getRepo: () => ({ push: mockPush })`, then
631
+ check `mockPush.mock.calls[0][0]`.
632
+ - Replace emit assertions with assertions on the returned result. This applies
633
+ to helpers you call directly too.
634
+ ```ts
635
+ const result = await mockProcessTask.mock.calls[0][0].task({ adapter });
636
+ expect(result).toEqual({ status: 'success' });
637
+ ```
638
+ - Casts like `as unknown as WorkerAdapter<S>` become `ExtractionAdapter<S>` /
639
+ `LoadingAdapter<S>`.
640
+ - Update doubles and fixtures to the v2 shapes: `delaySeconds`, callback
641
+ `status` unions (§6), mapper `null` / unwrapped bodies (§10), `code` on
642
+ errors (§12), renamed state fields and the removed SDK fields (§8).
643
+ - Source that now calls `new Mappers(...)` needs a `Mappers` mock in the SDK
644
+ module factory.
645
+ - For a deleted `onTimeout`, drop the tests of its body and assert it's
646
+ `undefined`. Tests of a deleted SSOR workaround go with it.
647
+
648
+ ## 14. Smaller changes
649
+
650
+ - `RepoInterface.overridenOptions` → `overriddenOptions`.
651
+ - `skipConfirmation` is removed; the SDK handles the ESU repo itself.
652
+ - `NormalizedAttachment.grand_parent_id` is `string`; use `String(id)`.
653
+
654
+ ---
655
+
656
+ ## Rare cases
657
+
658
+ - **Attachment syncs started on SDK < 1.15.2 and still in flight** re-upload
659
+ their already-processed attachments once. Drain them before upgrading if
660
+ that matters.
661
+ - **`is_sdk_log`** is no longer on log lines; update dashboards that filter on
662
+ it.
663
+ - **Connectors migrated against a beta (≤ `2.0.0-beta.6`)** must re-apply
664
+ §3–§14 against GA, because most of the API above changed after those betas.
665
+ State they wrote is still read.