Skip to content

Commit 08e6b69

Browse files
New IDM installation flow, attachments streaming bugfix, retry all 5xx requests (#32)
1 parent 4a9cf31 commit 08e6b69

File tree

12 files changed

+129
-51
lines changed

12 files changed

+129
-51
lines changed

package-lock.json

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devrev/ts-adaas",
3-
"version": "1.2.6",
3+
"version": "1.3.0",
44
"description": "DevRev ADaaS (AirDrop-as-a-Service) Typescript SDK.",
55
"type": "commonjs",
66
"main": "./dist/index.js",
@@ -37,7 +37,7 @@
3737
"typescript": "^5.3.3"
3838
},
3939
"dependencies": {
40-
"@devrev/typescript-sdk": "^1.1.27",
40+
"@devrev/typescript-sdk": "^1.1.54",
4141
"axios": "^1.7.9",
4242
"axios-retry": "^4.5.0",
4343
"form-data": "^4.0.1",

src/common/install-initial-domain-mapping.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { axios, axiosClient } from '../http/axios-client';
2-
import { FunctionInput } from '@devrev/typescript-sdk/dist/snap-ins';
2+
import { AirdropEvent } from '../types/extraction';
33

44
import { InitialDomainMapping } from '../types/common';
55
import { serializeAxiosError } from '../logger/logger';
66

77
export async function installInitialDomainMapping(
8-
event: FunctionInput,
8+
event: AirdropEvent,
99
initialDomainMappingJson: InitialDomainMapping
1010
) {
1111
const devrevEndpoint = event.execution_metadata.devrev_endpoint;

src/http/axios-client.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
/**
2+
* Axios client setup with retry capabilities using axios-retry.
3+
*
4+
* This module exports an Axios client instance (`axiosClient`) that is configured to automatically retry
5+
* failed requests under certain conditions.
6+
*
7+
* Retry Conditions:
8+
* 1. Network errors (where no response is received).
9+
* 2. Idempotent requests (defaults include GET, HEAD, OPTIONS, PUT).
10+
* 3. All 5xx server errors.
11+
*
12+
* Retry Strategy:
13+
* - A maximum of 5 retries are attempted.
14+
* - Exponential backoff delay is applied between retries, increasing with each retry attempt.
15+
*
16+
* Additional Features:
17+
* - When the maximum number of retry attempts is reached, sensitive headers (like authorization)
18+
* are removed from error logs for security reasons.
19+
*
20+
* Exported:
21+
* - `axios`: Original axios instance for additional customizations or direct use.
22+
* - `axiosClient`: Configured axios instance with retry logic.
23+
*/
24+
125
import axios, { AxiosError } from 'axios';
226
import axiosRetry from 'axios-retry';
327

@@ -17,8 +41,9 @@ axiosRetry(axiosClient, {
1741
},
1842
retryCondition: (error: AxiosError) => {
1943
return (
20-
axiosRetry.isNetworkOrIdempotentRequestError(error) &&
21-
error.response?.status !== 429
44+
(axiosRetry.isNetworkOrIdempotentRequestError(error) &&
45+
error.response?.status !== 429) ||
46+
(error.response?.status ?? 0) >= 500
2247
);
2348
},
2449
onMaxRetryTimesExceeded(error: AxiosError) {

src/state/state.interfaces.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { AirdropEvent } from '../types/extraction';
22
import { FileToLoad } from '../types/loading';
33
import { WorkerAdapterOptions } from '../types/workers';
4+
import { InitialDomainMapping } from '../types/common';
45

56
export interface SdkState {
67
lastSyncStarted?: string;
78
lastSuccessfulSyncStarted?: string;
89
toDevRev?: ToDevRev;
910
fromDevRev?: FromDevRev;
11+
snapInVersionId?: string;
1012
}
1113

1214
/**
13-
* AdapterState is an interface that defines the structure of the adapter state that is used by the external extractor. It extends the connector state with additional fields: lastSyncStarted, lastSuccessfulSyncStarted, and attachmentsMetadata.
15+
* AdapterState is an interface that defines the structure of the adapter state that is used by the external extractor.
16+
* It extends the connector state with additional fields: lastSyncStarted, lastSuccessfulSyncStarted, snapInVersionId and attachmentsMetadata.
1417
*/
1518
export type AdapterState<ConnectorState> = ConnectorState & SdkState;
1619

@@ -28,5 +31,6 @@ export interface FromDevRev {
2831
export interface StateInterface<ConnectorState> {
2932
event: AirdropEvent;
3033
initialState: ConnectorState;
34+
initialDomainMapping?: InitialDomainMapping;
3135
options?: WorkerAdapterOptions;
3236
}

src/state/state.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,54 @@ import { AirdropEvent, EventType, SyncMode } from '../types/extraction';
44
import { STATELESS_EVENT_TYPES } from '../common/constants';
55
import { serializeAxiosError, getPrintableState } from '../logger/logger';
66
import { ErrorRecord } from '../types/common';
7+
import { installInitialDomainMapping } from '../common/install-initial-domain-mapping';
78

89
import { AdapterState, SdkState, StateInterface } from './state.interfaces';
910
import { getSyncDirection } from '../common/helpers';
1011

1112
export async function createAdapterState<ConnectorState>({
1213
event,
1314
initialState,
15+
initialDomainMapping,
1416
options,
1517
}: StateInterface<ConnectorState>): Promise<State<ConnectorState>> {
1618
const newInitialState = structuredClone(initialState);
1719
const as = new State<ConnectorState>({
1820
event,
1921
initialState: newInitialState,
22+
initialDomainMapping,
2023
options,
2124
});
2225

2326
if (!STATELESS_EVENT_TYPES.includes(event.payload.event_type)) {
2427
await as.fetchState(newInitialState);
2528

29+
const snapInVersionId = event.context.snap_in_version_id;
30+
31+
const hasSnapInVersionInState = 'snapInVersionId' in as.state;
32+
33+
const shouldUpdateIDM =
34+
!hasSnapInVersionInState || as.state.snapInVersionId !== snapInVersionId;
35+
36+
if (shouldUpdateIDM) {
37+
console.log(
38+
`Snap-in version in state (${as.state?.snapInVersionId}) differs from the version in event context (${snapInVersionId}) - initial domain mapping needs to be updated.`
39+
);
40+
if (initialDomainMapping) {
41+
await installInitialDomainMapping(event, initialDomainMapping);
42+
as.state.snapInVersionId = snapInVersionId;
43+
console.log('Successfully installed new initial domain mapping.');
44+
} else {
45+
console.warn(
46+
'No initial domain mapping was passed to spawn function. Skipping initial domain mapping installation.'
47+
);
48+
}
49+
} else {
50+
console.log(
51+
`Snap-in version in state matches the version in event context (${snapInVersionId}). Skipping initial domain mapping installation.`
52+
);
53+
}
54+
2655
if (
2756
event.payload.event_type === EventType.ExtractionDataStart &&
2857
!as.state.lastSyncStarted
@@ -47,13 +76,15 @@ export class State<ConnectorState> {
4776
this.initialSdkState =
4877
getSyncDirection({ event }) === SyncMode.LOADING
4978
? {
79+
snapInVersionId: '',
5080
fromDevRev: {
5181
filesToLoad: [],
5282
},
5383
}
5484
: {
5585
lastSyncStarted: '',
5686
lastSuccessfulSyncStarted: '',
87+
snapInVersionId: '',
5788
toDevRev: {
5889
attachmentsMetadata: {
5990
artifactIds: [],
@@ -132,19 +163,15 @@ export class State<ConnectorState> {
132163
);
133164

134165
try {
135-
const response = await axiosClient.post(
136-
this.workerUrl + '.get',
137-
{},
138-
{
139-
headers: {
140-
Authorization: this.devrevToken,
141-
},
142-
params: {
143-
sync_unit: this.event.payload.event_context.sync_unit_id,
144-
request_id: this.event.payload.event_context.uuid,
145-
},
146-
}
147-
);
166+
const response = await axiosClient.get(this.workerUrl + '.get', {
167+
headers: {
168+
Authorization: this.devrevToken,
169+
},
170+
params: {
171+
sync_unit: this.event.payload.event_context.sync_unit_id,
172+
request_id: this.event.payload.event_context.uuid,
173+
},
174+
});
148175

149176
this.state = JSON.parse(response.data.state);
150177

src/tests/test-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function createEvent({
2121
service_account_token: 'test_token',
2222
},
2323
snap_in_version_id: 'test_snap_in_version_id',
24+
snap_in_id: 'test_snap_in_id',
2425
...contextOverrides,
2526
},
2627
payload: {

src/types/extraction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export interface AirdropEvent {
221221
service_account_token: string;
222222
};
223223
snap_in_version_id: string;
224+
snap_in_id: string;
224225
};
225226
payload: AirdropMessage;
226227
execution_metadata: {

src/types/workers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ExtractorEventType, AirdropEvent } from './extraction';
77

88
import { LoaderEventType } from './loading';
99

10+
import { InitialDomainMapping } from './common';
11+
1012
/**
1113
* WorkerAdapterInterface is an interface for WorkerAdapter class.
1214
* @interface WorkerAdapterInterface
@@ -59,13 +61,15 @@ export interface SpawnInterface {
5961
* @param {AirdropEvent} event - The event object received from the platform
6062
* @param {object=} initialState - The initial state of the adapter
6163
* @param {string} workerPath - The path to the worker file
64+
* @param {string} initialDomainMapping - The initial domain mapping
6265
* @param {WorkerAdapterOptions} options - The options to create a new instance of Spawn class
6366
*/
6467
export interface SpawnFactoryInterface<ConnectorState> {
6568
event: AirdropEvent;
6669
initialState: ConnectorState;
6770
workerPath?: string;
6871
options?: WorkerAdapterOptions;
72+
initialDomainMapping?: InitialDomainMapping;
6973
}
7074

7175
/**
@@ -149,6 +153,7 @@ export interface WorkerData<ConnectorState> {
149153
event: AirdropEvent;
150154
initialState: ConnectorState;
151155
workerPath: string;
156+
initialDomainMapping?: InitialDomainMapping;
152157
options?: WorkerAdapterOptions;
153158
}
154159

src/workers/process-task.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ export function processTask<ConnectorState>({
1717
void (async () => {
1818
const event = workerData.event;
1919
const initialState = workerData.initialState as ConnectorState;
20+
const initialDomainMapping = workerData.initialDomainMapping;
2021
const options = workerData.options;
2122
console = new Logger({ event, options });
2223

2324
const adapterState = await createAdapterState<ConnectorState>({
2425
event,
2526
initialState,
27+
initialDomainMapping,
2628
options,
2729
});
2830

src/workers/spawn.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export async function spawn<ConnectorState>({
8686
event,
8787
initialState,
8888
workerPath,
89+
initialDomainMapping,
8990
options,
9091
}: SpawnFactoryInterface<ConnectorState>): Promise<void> {
9192
const logger = new Logger({ event, options });
@@ -115,6 +116,7 @@ export async function spawn<ConnectorState>({
115116
event,
116117
initialState,
117118
workerPath: script,
119+
initialDomainMapping,
118120
options,
119121
});
120122

0 commit comments

Comments
 (0)