@devrev/airsync-sdk 2.0.0-beta.9 → 2.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAOtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAWzD,OAAO,EAEL,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B,qBAAa,QAAQ;IACnB,OAAO,CAAC,kBAAkB,CAAC,CAAU;IACrC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,gBAAgB,CAAU;gBAEtB,EAAE,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,wBAAwB;IAW1E,mFAAmF;IAC7E,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,GAChC,OAAO,CAAC,cAAc,CAAC;IAwEpB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IA2B5C,mFAAmF;IAC7E,cAAc,CAClB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAmBzC,kFAAkF;IAC5E,cAAc,CAClB,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAqCnC,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QACvD,QAAQ,CAAC,EAAE,aAAa,CAAC;QACzB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;IAgCF,oEAAoE;IAC9D,4BAA4B,CAAC,EACjC,QAAQ,GACT,EAAE;QACD,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QACV,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;QACrC,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7B,CAAC;YAuDY,sBAAsB;YAsBtB,gBAAgB;IAc9B,yEAAyE;IACnE,yBAAyB,CAAC,EAC9B,UAAU,EACV,SAAiB,GAClB,EAAE;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC;CA6B/C"}
1
+ {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAUtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAWzD,OAAO,EAEL,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,cAAc,EACd,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B,qBAAa,QAAQ;IACnB,OAAO,CAAC,kBAAkB,CAAC,CAAU;IACrC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,gBAAgB,CAAU;gBAEtB,EAAE,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,wBAAwB;IAW1E,mFAAmF;IAC7E,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,GAChC,OAAO,CAAC,cAAc,CAAC;IAwEpB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IA2B5C,mFAAmF;IAC7E,cAAc,CAClB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAmBzC,kFAAkF;IAC5E,cAAc,CAClB,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAqCzC;;;;OAIG;IACG,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAA0B,GACjC,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IAkB3C,+DAA+D;IACzD,0BAA0B,CAC9B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,SAA0B,GACjC,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IAkB3C,+DAA+D;IACzD,uBAAuB,CAC3B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAoBzC;;;;OAIG;IACG,uBAAuB,CAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,cAAc,CAAC;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IA6HnD,oDAAoD;YACtC,mBAAmB;IAsD3B,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QACvD,QAAQ,CAAC,EAAE,aAAa,CAAC;QACzB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;IAgCF,oEAAoE;IAC9D,4BAA4B,CAAC,EACjC,QAAQ,GACT,EAAE;QACD,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QACV,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;QACrC,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7B,CAAC;YAuDY,sBAAsB;YAsBtB,gBAAgB;IAc9B,yEAAyE;IACnE,yBAAyB,CAAC,EAC9B,UAAU,EACV,SAAiB,GAClB,EAAE;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC;CA6B/C"}
@@ -40,6 +40,12 @@ export interface ArtifactToUpload {
40
40
  value: string;
41
41
  }[];
42
42
  }
43
+ /** Multipart upload state returned by the artifacts upload endpoints. */
44
+ export interface MultipartUpload {
45
+ id: string;
46
+ url: string;
47
+ cursor: string;
48
+ }
43
49
  export interface UploadResponse {
44
50
  artifact?: Artifact;
45
51
  error?: ErrorRecord;
@@ -1 +1 @@
1
- {"version":3,"file":"uploader.interfaces.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IACxB;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzC,oBAAY,iBAAiB;IAC3B,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,kBAAkB,yBAAyB;IAC3C,kBAAkB,yBAAyB;CAC5C;AAED,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;IAChD,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;CACjD;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,QAAQ,EACN,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,kBAAkB,GACpC,iBAAiB,CAAC,kBAAkB,CACvC,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;KACf,EAAE,CAAC;CACL;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE;QACF,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
1
+ {"version":3,"file":"uploader.interfaces.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IACxB;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzC,oBAAY,iBAAiB;IAC3B,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,kBAAkB,yBAAyB;IAC3C,kBAAkB,yBAAyB;CAC5C;AAED,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;IAChD,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;CACjD;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,QAAQ,EACN,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,kBAAkB,GACpC,iBAAiB,CAAC,kBAAkB,CACvC,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;KACf,EAAE,CAAC;CACL;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE;QACF,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Uploader = void 0;
7
+ const http_1 = __importDefault(require("http"));
8
+ const https_1 = __importDefault(require("https"));
7
9
  const form_data_1 = __importDefault(require("form-data"));
8
10
  const js_jsonl_1 = require("js-jsonl");
9
11
  const constants_1 = require("../common/constants");
@@ -136,6 +138,201 @@ class Uploader {
136
138
  return { error };
137
139
  }
138
140
  }
141
+ /**
142
+ * Creates an S3 multipart upload for an artifact whose size is not known.
143
+ * The returned URL is for part 1; subsequent URLs are obtained with
144
+ * getNextMultipartUploadPart.
145
+ */
146
+ async createMultipartUpload(filename, fileType, partSize = constants_1.S3_MULTIPART_CHUNK_SIZE) {
147
+ const url = `${this.devrevApiEndpoint}/internal/artifacts.upload.create`;
148
+ try {
149
+ const response = await client_1.axiosClient.post(url, {
150
+ file_name: (0, uploader_helpers_1.truncateFilename)(filename),
151
+ file_type: fileType,
152
+ part_size: partSize,
153
+ }, { headers: Object.assign({}, this.defaultHeaders) });
154
+ return { response: response.data };
155
+ }
156
+ catch (error) {
157
+ return { error };
158
+ }
159
+ }
160
+ /** Gets a presigned URL for the next multipart upload part. */
161
+ async getNextMultipartUploadPart(artifactId, cursor, partSize = constants_1.S3_MULTIPART_CHUNK_SIZE) {
162
+ const url = `${this.devrevApiEndpoint}/internal/artifacts.upload.next`;
163
+ try {
164
+ const response = await client_1.axiosClient.post(url, {
165
+ id: artifactId,
166
+ cursor,
167
+ part_size: partSize,
168
+ }, { headers: Object.assign({}, this.defaultHeaders) });
169
+ return { response: response.data };
170
+ }
171
+ catch (error) {
172
+ return { error };
173
+ }
174
+ }
175
+ /** Finalizes and publishes an S3 multipart artifact upload. */
176
+ async completeMultipartUpload(artifactId, cursor) {
177
+ const url = `${this.devrevApiEndpoint}/internal/artifacts.upload.complete`;
178
+ try {
179
+ const response = await client_1.axiosClient.post(url, {
180
+ id: artifactId,
181
+ cursor,
182
+ }, { headers: Object.assign({}, this.defaultHeaders) });
183
+ if (response.status >= 200 && response.status < 300) {
184
+ return { response };
185
+ }
186
+ return { error: response };
187
+ }
188
+ catch (error) {
189
+ return { error };
190
+ }
191
+ }
192
+ /**
193
+ * Streams an attachment of unknown size through an S3 multipart upload.
194
+ * At most one 10 MiB part is buffered at a time. The final part may be
195
+ * smaller than S3's 5 MiB minimum.
196
+ */
197
+ async streamArtifactMultipart(filename, fileType, fileStream) {
198
+ try {
199
+ const iterator = fileStream.data[Symbol.asyncIterator]();
200
+ let pendingChunk;
201
+ // Read a complete part before requesting its presigned URL. The gateway
202
+ // signs Content-Length using part_size, so the requested part size must
203
+ // be the exact byte length that will be sent (including the final part).
204
+ const readPart = async () => {
205
+ const chunks = [];
206
+ let length = 0;
207
+ while (length < constants_1.S3_MULTIPART_CHUNK_SIZE) {
208
+ let chunk = pendingChunk;
209
+ pendingChunk = undefined;
210
+ if (!chunk) {
211
+ const next = await iterator.next();
212
+ if (next.done) {
213
+ break;
214
+ }
215
+ chunk = Buffer.isBuffer(next.value)
216
+ ? next.value
217
+ : Buffer.from(next.value);
218
+ }
219
+ if (chunk.length === 0) {
220
+ continue;
221
+ }
222
+ const capacity = constants_1.S3_MULTIPART_CHUNK_SIZE - length;
223
+ if (chunk.length <= capacity) {
224
+ chunks.push(chunk);
225
+ length += chunk.length;
226
+ }
227
+ else {
228
+ chunks.push(chunk.subarray(0, capacity));
229
+ pendingChunk = chunk.subarray(capacity);
230
+ length += capacity;
231
+ }
232
+ }
233
+ return length > 0 ? Buffer.concat(chunks, length) : undefined;
234
+ };
235
+ let part = await readPart();
236
+ if (!part) {
237
+ throw new Error('Cannot complete a multipart upload with an empty file.');
238
+ }
239
+ const { response: createdUpload, error: createError } = await this.createMultipartUpload(filename, fileType, part.length);
240
+ if (createError || !createdUpload) {
241
+ return {
242
+ error: createError !== null && createError !== void 0 ? createError : new Error('Multipart upload creation returned no response.'),
243
+ };
244
+ }
245
+ const artifactId = createdUpload.id;
246
+ let cursor = createdUpload.cursor;
247
+ let uploadUrl = createdUpload.url;
248
+ if (!artifactId || !cursor || !uploadUrl) {
249
+ return {
250
+ error: new Error('Multipart upload creation returned incomplete upload information.'),
251
+ };
252
+ }
253
+ while (part) {
254
+ const { response, error } = await this.uploadMultipartPart(uploadUrl, part, part.length);
255
+ if (error || !response) {
256
+ return {
257
+ error: error !== null && error !== void 0 ? error : new Error('Multipart part upload returned no response.'),
258
+ };
259
+ }
260
+ const nextPart = part.length === constants_1.S3_MULTIPART_CHUNK_SIZE
261
+ ? await readPart()
262
+ : undefined;
263
+ if (!nextPart) {
264
+ break;
265
+ }
266
+ const { response: nextUpload, error: nextError } = await this.getNextMultipartUploadPart(artifactId, cursor, nextPart.length);
267
+ if (nextError || !(nextUpload === null || nextUpload === void 0 ? void 0 : nextUpload.url) || !nextUpload.cursor) {
268
+ return {
269
+ error: nextError !== null && nextError !== void 0 ? nextError : new Error('Could not obtain the next multipart upload URL.'),
270
+ };
271
+ }
272
+ part = nextPart;
273
+ uploadUrl = nextUpload.url;
274
+ cursor = nextUpload.cursor;
275
+ }
276
+ const { error: completeError } = await this.completeMultipartUpload(artifactId, cursor);
277
+ if (completeError) {
278
+ return { error: completeError };
279
+ }
280
+ return { response: { artifact_id: artifactId } };
281
+ }
282
+ catch (error) {
283
+ return { error };
284
+ }
285
+ finally {
286
+ (0, uploader_helpers_1.destroyHttpStream)(fileStream);
287
+ }
288
+ }
289
+ /** Uploads one raw part to its presigned S3 URL. */
290
+ async uploadMultipartPart(uploadUrl, part, contentLength) {
291
+ try {
292
+ // Use Node's native request instead of Axios for the presigned request.
293
+ // Axios may normalize, remove, or recalculate Content-Length; that is
294
+ // not allowed when the presigned URL includes this header in its
295
+ // signature.
296
+ const parsedUrl = new URL(uploadUrl);
297
+ const transport = parsedUrl.protocol === 'https:' ? https_1.default : http_1.default;
298
+ const response = await new Promise((resolve, reject) => {
299
+ const request = transport.request(parsedUrl, {
300
+ method: 'PUT',
301
+ headers: {
302
+ 'Content-Length': String(contentLength),
303
+ },
304
+ }, (incomingResponse) => {
305
+ const chunks = [];
306
+ incomingResponse.on('data', (chunk) => chunks.push(chunk));
307
+ incomingResponse.on('end', () => {
308
+ var _a, _b;
309
+ const data = Buffer.concat(chunks);
310
+ const status = (_a = incomingResponse.statusCode) !== null && _a !== void 0 ? _a : 0;
311
+ const axiosResponse = {
312
+ data,
313
+ status,
314
+ statusText: (_b = incomingResponse.statusMessage) !== null && _b !== void 0 ? _b : '',
315
+ headers: incomingResponse.headers,
316
+ config: {},
317
+ request,
318
+ };
319
+ if (status >= 200 && status < 400) {
320
+ resolve(axiosResponse);
321
+ }
322
+ else {
323
+ reject(new Error(`Multipart part upload returned HTTP ${status}.`));
324
+ }
325
+ });
326
+ });
327
+ request.once('error', reject);
328
+ request.end(part);
329
+ });
330
+ return { response };
331
+ }
332
+ catch (error) {
333
+ return { error };
334
+ }
335
+ }
139
336
  async confirmArtifactUpload(artifactId) {
140
337
  const url = `${this.devrevApiEndpoint}/internal/airdrop.artifacts.confirm-upload`;
141
338
  try {
package/docs/MIGRATION.md CHANGED
@@ -9,11 +9,14 @@ own:
9
9
  It ships in the package at `node_modules/@devrev/airsync-sdk/docs/MIGRATION.md`
10
10
  (read it from disk; the `exports` map only blocks `import`/`require`). Before
11
11
  v2 is installed, use
12
- `https://raw.githubusercontent.com/devrev/adaas-sdk/v2/docs/MIGRATION.md`.
12
+ `https://raw.githubusercontent.com/devrev/adaas-sdk/v2/docs/MIGRATION.md`
13
+ (use `main` after GA). The installed guide always takes precedence.
13
14
 
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.
15
+ **The modern wire protocol is unchanged.** Event names, routes, headers,
16
+ artifact formats and `ERROR_CODE=…` strings remain compatible with platforms
17
+ that send modern events. Legacy `EXTRACTION_*` event names are intentionally
18
+ unsupported. The public TypeScript API changes all at once, without deprecated
19
+ aliases; runtime differences are described below.
17
20
 
18
21
  ---
19
22
 
@@ -21,7 +24,8 @@ v1. Only the TypeScript API changed, all at once, with no compatibility shims.
21
24
 
22
25
  **Rules.** Work autonomously; when a rewrite is ambiguous, pick the option
23
26
  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
27
+ source **and** tests. Preserve runtime string values except the documented
28
+ legacy event-string replacements in §3 and §11. Don't add comments
25
29
  about the migration; the `// v1` / `// v2` labels here are reading aids. Leave
26
30
  no compile errors or orphaned imports. A section with no hits is a no-op.
27
31
  Existing comments and commented-out code that describe v1 behavior (`emit`,
@@ -30,40 +34,58 @@ v2; templates are copied into new connectors, so stale comments spread.
30
34
 
31
35
  1. **Locate the package.** Find the `package.json` that depends on
32
36
  `@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.
37
+ from there. If it already depends on `@devrev/airsync-sdk`, inspect its
38
+ installed version and migration review. Early betas still need the
39
+ changes under **Rare cases** below; upgrade to the selected v2 version
40
+ and verify source, tests and local runners against this guide. Stop only
41
+ when the current-contract migration and verification are complete.
35
42
  2. **Start from green latest v1.** If behind the latest stable 1.x, upgrade and
36
43
  apply the release notes of every release in between that mentions a
37
44
  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**.
45
+ green on v1. If the upgrade alone breaks tsc with axios type mismatches,
46
+ bump the connector's `axios` to the version that SDK release depends on.
47
+ Record failures that remain as **Pre-existing**.
39
48
  3. **Install v2.**
40
49
  ```bash
41
50
  npm uninstall @devrev/ts-adaas
42
51
  npm install @devrev/airsync-sdk@beta # @latest after GA
43
52
  ```
44
- If the installed `docs/MIGRATION.md` differs from this copy, switch to it.
53
+ Rename or remove `overrides` / `resolutions` entries keyed on
54
+ `@devrev/ts-adaas`; `npm uninstall` leaves them behind. If the installed
55
+ `docs/MIGRATION.md` differs from this copy, switch to it.
45
56
  4. **Apply §1–§14 in order** across source and tests. After §1, search by
46
57
  symbol (`\.emit\(`, `WorkerAdapter`, `sync_mapper_record`, …), not by the old
47
58
  package name; test files that import nothing from the SDK still need
48
59
  changes.
49
60
  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,
61
+ If tsconfig excludes folders the connector still runs (`test/`, a local
62
+ runner), type-check them too: use the repo's `tsconfig.test.json` if it has
63
+ one, otherwise a temporary tsconfig that extends `tsconfig.json` and
64
+ overrides `"include": ["src", "test"]`, `"exclude": ["node_modules", "dist"]`,
65
+ `"rootDir": "."` and `"noEmit": true` (the base `exclude` and `rootDir` still
66
+ apply otherwise), then `npx tsc -p` it. Fix and rerun,
51
67
  at most 4 attempts; the migration must add no new failures compared to
52
68
  step 2.
53
69
  6. **Sweep.** This must return nothing except legitimate hits (an
54
- `EventEmitter.emit`, the connector's own `axiosClient` from §9):
70
+ `EventEmitter.emit`, the connector's own `axiosClient` from §9,
71
+ `@devrev/typescript-sdk` in `package.json` from §1, `git config --local`,
72
+ legacy event names in fields the SDK doesn't read, such as
73
+ `event_type_adaas`):
55
74
  ```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' . \
75
+ grep -rnE "@devrev/ts-adaas|airsync-sdk/dist|\bprocessTask\b|\bWorkerAdapter\b|\.emit\(|hasWorkerEmitted|\bAirdrop(Event|Message)\b|axiosClient|(format|serialize)AxiosError|workerPath:|overridenOptions|skipConfirmation|isLocalDevelopment|--local|-- local\b|@devrev/typescript-sdk|(Extractor|Loader)EventType|ExtractionCommonError|ERROR_CODE=|reset_(extraction|extract_from)|sdkState|onUpload|uploadedArtifacts|currentEventDataLength|EXTRACTION_(DATA|METADATA|ATTACHMENTS|EXTERNAL_SYNC_UNITS)_" \
76
+ --include='*.ts' --include='*.js' --include='*.json' --include='*.yml' --include='*.yaml' --include='*.md' --include='*.mdc' --include='*.sh' . \
58
77
  | grep -vE 'node_modules|/dist/|/coverage/|package-lock.json|MIGRATION_REVIEW.md'
59
78
  ```
60
79
  The sweep covers `test/` and other folders that tsc, lint and jest often
61
80
  exclude. A hit there is real: `@devrev/typescript-sdk` must be in
62
81
  `package.json` (§1), and a `local` argv must become the env var (§3).
82
+ Also check `package.json` scripts and shell scripts for a bare `local`
83
+ argument (`npm run test:server -- local`, `main.ts local`).
63
84
  Also grep `getByExternalId|getByTargetId` and check each call against §10:
64
85
  existence checks there compile but silently invert.
65
86
  Then rewrite any connector docs (`README`, `AGENTS.md`, `CLAUDE.md`, `docs/`)
66
- that describe v1 behavior.
87
+ that describe v1 behavior, including text that paraphrases DevRev docs with
88
+ legacy event names.
67
89
  7. **Write `MIGRATION_REVIEW.md`** at the repo root: one line per section that
68
90
  changed code with `high` / `medium` / `low confidence`, a sentence on what
69
91
  to verify for medium/low, and the final check results with an
@@ -71,7 +93,7 @@ v2; templates are copied into new connectors, so stale comments spread.
71
93
  helpers (§4), paths that emitted nothing (§4), `onTimeout` bodies kept (§4),
72
94
  delay values that look like milliseconds or headers (§4), the cursor decision
73
95
  (§8), ESU repo rewrites (§7), mapper error handling (§10), and a v1 baseline
74
- upgrade.
96
+ upgrade. Record the exact installed versions.
75
97
  ```markdown
76
98
  # v2 Migration Review
77
99
  Migrated `@devrev/ts-adaas` 1.20.2 → `@devrev/airsync-sdk` 2.0.0.
@@ -99,7 +121,7 @@ v2; templates are copied into new connectors, so stale comments spread.
99
121
  root equivalent are listed in §11. Never import the SDK's internal HTTP client
100
122
  (`dist/http/axios-client-internal`): use the connector's own client (§9).
101
123
  - **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
124
+ they pin older. `@types/node` can stay as it is. Dependabot / CI configs that name `@devrev/ts-adaas` switch to
103
125
  `@devrev/airsync-sdk`.
104
126
  - `ts-node` and `@devrev/typescript-sdk` are no longer SDK dependencies. A
105
127
  connector that runs `.ts` workers directly needs `ts-node` in its own
@@ -107,7 +129,7 @@ v2; templates are copied into new connectors, so stale comments spread.
107
129
  it. The template's local test server (`code/test/runner.ts`, and the copy
108
130
  `airsync start-worker` writes to `code/.snapin-test/`) imports
109
131
  `@devrev/typescript-sdk/dist/snap-ins`, so template connectors add it to
110
- `devDependencies`. Most connectors exclude `test/` from tsc, lint and jest,
132
+ `devDependencies` (`1.1.78`, the version the template pins). Most connectors exclude `test/` from tsc, lint and jest,
111
133
  so the gates pass and the local worker then fails with
112
134
  `Cannot find module '@devrev/typescript-sdk/dist/snap-ins'`.
113
135
  `InputData` (the type of `event.input_data`) is now exported from the
@@ -125,7 +147,10 @@ The shapes are unchanged. `AirSyncEvent.context` now also declares `user_id`,
125
147
  `payload.event_context`). Delete a hand-rolled context type that added these
126
148
  (e.g. `interface CustomAirdropEvent extends AirdropEvent { context: CustomContext }`):
127
149
  a narrower `context` no longer satisfies `AirSyncEvent`. Keep only the fields
128
- the SDK type lacks.
150
+ the SDK type lacks. These fields are required, so typed mock events must set
151
+ them. `execution_metadata` is typed `{ devrev_endpoint }`; a custom type that
152
+ widened it with `@devrev/typescript-sdk` fields goes too, along with mock values
153
+ for fields nothing reads.
129
154
 
130
155
  Deprecated `EventContext` aliases are removed. Use the current names:
131
156
 
@@ -138,8 +163,18 @@ Deprecated `EventContext` aliases are removed. Use the current names:
138
163
  | `sync_run`, `sync_run_id` | `run_id` |
139
164
  | `uuid` | `request_id_adaas` |
140
165
 
141
- `reset_extraction` and `reset_extract_from` remain (the SDK never read them);
142
- leave connector logic that uses them alone.
166
+ Rename these keys in fixture and test `event_context` objects too. When an
167
+ object already has the new key (or has two old keys for one new one, like
168
+ `dev_user` and `dev_user_id`), keep the existing new-name value, otherwise the
169
+ first old one, and drop the rest. Untyped
170
+ JSON still compiles, but the SDK reads only the current names (e.g. state
171
+ requests carry `request_id_adaas`).
172
+
173
+ `reset_extraction` and `reset_extract_from` are removed. Delete these keys
174
+ from fixtures and remove connector branches that read them. The SDK never
175
+ interpreted either flag. Configure backfills through the platform's
176
+ `extraction_start_time` / `extraction_end_time`; connectors read the
177
+ SDK-resolved `extract_from` / `extract_to` for the sync window (§8).
143
178
 
144
179
  ## 3. `spawn`
145
180
 
@@ -233,14 +268,14 @@ processExtractionTask({
233
268
  |----|----|
234
269
  | `emit(XxxDone)` | `return { status: 'success' }` |
235
270
  | `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 |
271
+ | `emit(XxxDelayed, { delay })` | `return { status: 'delay', delaySeconds: delay }` — keep the same number; the platform always read it as seconds. A raw `Retry-After` header stays `Number(header)`. If it is clearly milliseconds (a `*_MS` constant, a value in the thousands, "ms" in the message), keep the number, flag it and list it under **Unresolved**: v1 waited that many seconds too, and fixing it changes behavior |
237
272
  | `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
273
  | `emit(XxxError, { error })` | `return { status: 'error', error }` — `error` is required; fall back to `{ message: '<phase> failed' }` |
239
274
  | `emit(XxxDone, { error })` (finished with warnings) | `return { status: 'success' }` and log the error. Returning `error` would fail the phase |
240
275
  | `emit(ExternalSyncUnitExtractionDone, { external_sync_units })` | push to a repo, then `success` (§7) |
241
276
  | `emit(MetadataExtractionDone, { pre_extraction_item_counts })` | `adapter.preExtractionItemCounts = counts`, then `success` |
242
277
  | 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 |
278
+ | 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. If v1 did more work after the call and then emitted `Done` with an `error`, return the call's result and log that error |
244
279
 
245
280
  In non-resumable phases (external sync units, state deletion), `progress` and
246
281
  `delay` become `*Error`.
@@ -265,6 +300,21 @@ If a shared `handleError` emitted from many `catch` blocks that rethrow, v1
265
300
  kept only the first emit. Record the first result (`firstError ??= …`) and
266
301
  return it from the task's outermost `catch`.
267
302
 
303
+ If a helper **emitted and then kept going** (no throw), the caller often
304
+ emitted again later, e.g. `DataExtractionDone` after the loop, and v1 dropped
305
+ every emit after the first. Record the first result the same way, stop the
306
+ work that followed it (return out of the loop or extractor), and return it
307
+ from the task. Unawaited emits inside loops count too. When the emitting helper
308
+ is deep in the call stack, throw an error class that carries the `TaskResult`
309
+ instead (`class TaskResultError extends Error { constructor(readonly result: TaskResult) { super(); } }`),
310
+ stop the extractor, and return `e.result` from the task's outermost `catch`.
311
+ A free-function handler that must keep only the first result stores it per
312
+ adapter (`WeakMap<adapter, TaskResult>`) and the task reads it back.
313
+
314
+ A helper that returned `{ delay?, error? }` and emitted only when one was set
315
+ needs an explicit result when neither is set: `success` if the caller carried
316
+ on, otherwise `error`.
317
+
268
318
  If a helper (often the HTTP client's 429 handler) **emitted and then threw a
269
319
  sentinel error** that call sites rethrow, keep the throw, drop the emit, and map
270
320
  it in the task's outermost `catch`:
@@ -301,7 +351,12 @@ helpers, assert on the return value.
301
351
  v1 (the task had a path that didn't emit) **and** it did real work or set a
302
352
  custom error. Make its body safe to run after a completed task. A custom
303
353
  "Lambda timeout" `onTimeout` on a task that emits on every path is
304
- deleted.
354
+ deleted; v1 never ran it.
355
+ - `onTimeout` runs only after the task has returned, in v1 and v2, so
356
+ cleanup there (cancelling a rate limiter, aborting requests) never
357
+ interrupted a stuck task. Check the public `adapter.isTimeout` between
358
+ batches and perform any needed cancellation inside the task.
359
+ `timeoutSignal` is internal and unavailable to connector code.
305
360
  - Don't call `process.exit()` or `postState()` in `onTimeout`; the SDK saves
306
361
  state and exits.
307
362
 
@@ -312,6 +367,16 @@ now wrong**, and `Boolean(await repo.push(x))` or `if (!(await …push(x)))`
312
367
  compiles but is always falsy. Make wrappers `await repo.push(x)` and return
313
368
  `true` (or `void`). Keep a top-level `try/catch` that maps an error to `delay`
314
369
  (e.g. a thrown 429); otherwise it's only needed for a custom message.
370
+ Find every unawaited `repo.push()` / `repo.upload()`, including calls in
371
+ helpers and loops, and await it before advancing the cursor or returning.
372
+ An unhandled upload rejection can bypass the task's error handling.
373
+ `loadItemTypes` and `loadAttachments` don't throw: a `create` / `update` that
374
+ throws becomes their `error` result, so a 429 must be returned as
375
+ `{ status: 'delay', delaySeconds }` from the callback (§6). A `catch` around
376
+ them that checks `isAxiosError(e) && status === 429` is dead; delete it.
377
+ Failures fetching loader stats or reading/writing sync mapper records fail
378
+ the phase and retain the current cursor. A connector callback returning
379
+ `status: 'error'` still counts that item as failed and continues.
315
380
 
316
381
  `ProcessTaskInterface<T>` and `TaskAdapterInterface<T>` now take the
317
382
  **adapter** type: `ProcessTaskInterface<ExtractionAdapter<State>>`, not
@@ -332,7 +397,9 @@ async function extractList(adapter: ExtractionAdapter<State>) { ... }
332
397
  | `event`, `state`, `mappers`, `postState`, `uploadArtifact`, `downloadArtifact`, `isTimeout`, `extractionScope` | both |
333
398
 
334
399
  - The adapters and `Repo` are **type-only**: `new ExtractionAdapter(...)` and
335
- `class X extends LoadingAdapter<S>` don't compile; `interface X extends …`
400
+ `class X extends LoadingAdapter<S>` or `extends Repo` (test doubles that
401
+ subclassed `Repo` through `dist/`) don't compile; use a plain object with
402
+ the methods the code calls. `interface X extends …`
336
403
  does. SDK internals (`emit`, `sdkState`, `options`, constructors, …) aren't
337
404
  public. `isTimeout` and `artifacts` are read-only.
338
405
  - `adapter.mappers` works in extraction as in v1; keep those calls and only
@@ -400,7 +467,9 @@ return { status: 'success' }; // was a bare `return;`
400
467
  as `skip` are dropped; every `success` creates or updates a mapper record.
401
468
  - Shared helpers typed as these responses change too. Callers that read
402
469
  `result.delay` or interpolate `${result.error}` (now an object) switch on
403
- `result.status`.
470
+ `result.status`. A helper that only builds delays and errors returns the
471
+ narrowed type (`Extract<…, { status: 'delay' | 'error' }>`), so callers and
472
+ tests can read `delaySeconds` and `error` without a check.
404
473
  - An error `code` returned from the stream callback is only logged; the pool
405
474
  skips that attachment.
406
475
  - `httpStream` is typed `HttpStreamResponse` (`{ data, headers }`); an axios
@@ -416,6 +485,14 @@ its state fields and tests. Workarounds typically override the
416
485
  `ssor_attachment` repo's `onUpload`, splice `uploadedArtifacts`, adjust
417
486
  `currentEventDataLength`, or stash artifact refs in state.
418
487
 
488
+ - A worker whose only job was re-injecting those refs returns
489
+ `{ status: 'success' }` (or its remaining work); keep its phase wired.
490
+ - A connector's own `{ itemType: 'ssor_attachment' }` repo entry can stay.
491
+ - Tests that mocked the deep `Uploader` use adapter doubles instead:
492
+ `{ uploadArtifact: jest.fn(), downloadArtifact: jest.fn() }`.
493
+ - If the workaround stashed refs in connector state, syncs between data done and
494
+ attachments done lose them after the upgrade; drain them first.
495
+
419
496
  ## 7. External sync units go through a repo
420
497
 
421
498
  `EventData.external_sync_units` is removed. Push them to a repo:
@@ -443,9 +520,14 @@ the emit.
443
520
  - **Persisted state** becomes `{ stateVersion: 2, connectorState, sdkState }`.
444
521
  A v1 blob is migrated automatically on first read: known SDK keys move to
445
522
  `sdkState` and everything else becomes connector state. **Rolling back to v1
446
- after a v2 worker has run is not supported**; drain or reset syncs first. A
523
+ after a v2 worker has persisted state is not supported**. A drained sync
524
+ still has a v2 envelope: recovery needs a verified saved v1 state together
525
+ with the v1 connector, or an explicit reset/reinitialization and reimport.
526
+ Upgrade between runs, after v1 workers have finished. A
447
527
  v1 state with a top-level key named `connectorState` or `sdkState` is
448
- misread; rename it before upgrading.
528
+ misread. Rename it in a staged v1 deployment and migrate its persisted
529
+ value before upgrading, or explicitly reset/reimport. A top-level
530
+ connector-owned `stateVersion` in a flat v1 blob is preserved.
449
531
  - **Sync window**: read
450
532
  `adapter.event.payload.event_context.extract_from` / `extract_to`. The SDK
451
533
  resolves them from the platform's time values, or from the last successful
@@ -456,17 +538,34 @@ the emit.
456
538
  **`lastSyncStarted` / `lastSuccessfulSyncStarted`: rename or repoint.**
457
539
  Getting this wrong compiles but silently breaks incremental sync.
458
540
 
459
- - **Not in the connector's `State` interface** (it came from the SDK): delete
460
- writes, and repoint reads to `extract_from`.
541
+ These rules cover **top-level** state keys only; the v1 migration moves only
542
+ those. A cursor nested in the connector's own object (`state.extractor.lastSuccessfulSyncStarted`)
543
+ is connector-owned: leave it alone.
544
+
545
+ - **Not in the connector's `State` interface**, or declared but never written
546
+ by the connector (it came from the SDK): delete writes, and repoint reads to
547
+ `extract_from`.
461
548
  - **Declared and used as the connector's own cursor** (derived from data, or
462
549
  read alongside `extract_from`): **rename** it (e.g. →
463
- `lastSuccessfulWindowStart`) everywhere, including fixtures. Otherwise the v1
464
- auto-migration moves it into SDK state and the connector loses it.
550
+ `lastSuccessfulWindowStart`) everywhere, including fixtures. Before
551
+ deploying v2, use a staged v1 deployment to copy the persisted value into
552
+ the new connector key, retaining the legacy key for v1 SDK bookkeeping.
553
+ Alternatively, explicitly reset/reimport. A source-only rename does not
554
+ migrate persisted data: the v1 auto-migration moves the old key into SDK
555
+ state and leaves the new connector key unset.
465
556
  - **Declared but only mirrors the SDK**: its only writes are "now" when data
466
557
  extraction starts and a copy on finish. Treat it as not declared, **even if
467
558
  it is read next to `extract_from`**. Renaming would leave it empty after the
468
559
  upgrade, because the v1 value moves into SDK state.
469
560
 
561
+ After repointing, branches that chose between the two
562
+ (`reset_extract_from ? extract_from : lastSuccessfulSyncStarted`) read the same
563
+ value; collapse them and drop the `reset_extract_from` read. If the connector
564
+ already used `extract_from` for something else (e.g. a skip filter) and the
565
+ cursor for a second window (e.g. "re-emit and bump the floor"), repointing
566
+ merges the two, so code below the window may become dead. Flag that low
567
+ confidence.
568
+
470
569
  `extract_from` equals the last successful sync start only when the platform
471
570
  sends no time window. If a repointed read feeds logic beyond "skip items older
472
571
  than this" (e.g. distinguishing first syncs), flag it as low confidence; there
@@ -491,7 +590,7 @@ import axios from 'axios';
491
590
  import axiosRetry from 'axios-retry';
492
591
 
493
592
  export const axiosClient = axios.create();
494
- axiosRetry(axiosClient, {
593
+ axiosRetry(axiosClient as Parameters<typeof axiosRetry>[0], {
495
594
  retries: 5,
496
595
  retryDelay: (retryCount, error) => axiosRetry.exponentialDelay(retryCount, error, 1000),
497
596
  retryCondition: (error) =>
@@ -505,6 +604,9 @@ axiosRetry(axiosClient, {
505
604
  });
506
605
  ```
507
606
 
607
+ The cast is needed under ts-jest, which can resolve axios's `.d.ts` and
608
+ `.d.cts` to different types.
609
+
508
610
  A deep import of the SDK's internal client (`dist/http/axios-client-internal`)
509
611
  relied on more: a 30 s timeout, retries of 429s that send `Retry-After` (waiting
510
612
  that long) and of `ECONNABORTED`. Reproduce what the connector depends on with
@@ -550,6 +652,10 @@ return record?.sync_mapper_record.external_ids[0];
550
652
  - Hand-written structural types (`{ getByTargetId(): Promise<{ data?: … }> }`)
551
653
  keep `.data` compiling. Replace them with `Pick<Mappers, 'getByTargetId'>`.
552
654
  - Replace 404 `catch` branches with a `null` check.
655
+ - A **generic `catch`** around a lookup (no 404 test) also handled "not
656
+ found": it reported an item error, logged a warning or tried the next ID.
657
+ With `null` that path no longer runs, so add a `null` branch that does what
658
+ the `catch` did, and keep the `catch` for other failures.
553
659
  - Replace `isAxiosError` checks on mapper calls with
554
660
  `e instanceof AirSyncHttpError` / `e.status`. Checks on the connector's own
555
661
  HTTP calls stay.
@@ -596,8 +702,10 @@ in minor versions, so give `switch` statements over it a `default`.
596
702
 
597
703
  ## 12. Error codes
598
704
 
599
- `ExtractionCommonError` is now `AirSyncErrorCode`. Its values are bare codes,
600
- and you set them on `error.code`:
705
+ `ExtractionCommonError` is now `AirSyncErrorCode`. These remain the platform's
706
+ known common error codes (`CommonError`), mapped by the snap-in manager in
707
+ `internal/common/errors_snapin.go`. Their values are bare codes, and you set
708
+ them on `error.code`:
601
709
 
602
710
  ```ts
603
711
  // v1
@@ -609,9 +717,11 @@ return {
609
717
  };
610
718
  ```
611
719
 
612
- The platform only recognizes a code when the message is exactly
613
- `ERROR_CODE=<code>`, so the SDK sends just that and logs your `message`. Write
614
- a useful `message`. Raw `'ERROR_CODE=…'` strings and connector helpers that
720
+ `error.code` is an SDK input field. Before sending the callback, the SDK
721
+ converts it into `event_data.error.message = 'ERROR_CODE=<code>'` and omits
722
+ the `code` property. The backend continues reading the common error from
723
+ `error.message`; your human-readable `message` goes to the logs. Write a
724
+ useful `message`. Raw `'ERROR_CODE=…'` strings and connector helpers that
615
725
  return them (`HISTORICAL_DONE`, `authErrorCode(e)`) still work at runtime but
616
726
  are migrated too: make the helper return `AirSyncErrorCode | undefined`, set it
617
727
  as `code`, and update tests that pin the literal. The wire string doesn't
@@ -645,7 +755,11 @@ change.
645
755
  `status` unions (§6), mapper `null` / unwrapped bodies (§10), `code` on
646
756
  errors (§12), renamed state fields and the removed SDK fields (§8).
647
757
  - For a deleted `onTimeout`, drop the tests of its body and assert it's
648
- `undefined`. Tests of a deleted SSOR workaround go with it.
758
+ `undefined`. A factory like `processExtractionTask: (_config) => jest.fn()`
759
+ discards the config; use `processExtractionTask: jest.fn()` and read
760
+ `mocked.mock.calls[0][0].onTimeout`. Capture it in a module-level `const`
761
+ right after importing the worker: `jest.clearAllMocks()` in a `beforeEach`
762
+ wipes the calls. Tests of a deleted SSOR workaround go with it.
649
763
 
650
764
  ## 14. Smaller changes
651
765