@sasjs/adapter 4.9.2 → 4.10.1

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,13 +1,30 @@
1
1
  import { Link } from './Link';
2
+ import { SessionManager } from '../SessionManager';
3
+ export declare enum SessionState {
4
+ Completed = "completed",
5
+ Running = "running",
6
+ Pending = "pending",
7
+ Idle = "idle",
8
+ Unavailable = "unavailable",
9
+ NoState = "",
10
+ Failed = "failed",
11
+ Error = "error"
12
+ }
2
13
  export interface Session {
3
14
  id: string;
4
- state: string;
15
+ state: SessionState;
16
+ stateUrl: string;
5
17
  links: Link[];
6
18
  attributes: {
7
19
  sessionInactiveTimeout: number;
8
20
  };
9
21
  creationTimeStamp: string;
22
+ etag: string;
10
23
  }
11
24
  export interface SessionVariable {
12
25
  value: string;
13
26
  }
27
+ export interface JobSessionManager {
28
+ session: Session;
29
+ sessionManager: SessionManager;
30
+ }
package/package.json CHANGED
@@ -84,5 +84,5 @@
84
84
  "https": "1.0.0",
85
85
  "tough-cookie": "4.1.3"
86
86
  },
87
- "version": "4.9.2"
87
+ "version": "4.10.1"
88
88
  }
@@ -1,4 +1,4 @@
1
- import { Session, Context, SessionVariable } from './types'
1
+ import { Session, Context, SessionVariable, SessionState } from './types'
2
2
  import { NoSessionStateError } from './types/errors'
3
3
  import { asyncForEach, isUrl } from './utils'
4
4
  import { prefixMessage } from '@sasjs/utils/error'
@@ -12,6 +12,7 @@ interface ApiErrorResponse {
12
12
 
13
13
  export class SessionManager {
14
14
  private loggedErrors: NoSessionStateError[] = []
15
+ private sessionStateLinkError = 'Error while getting session state link. '
15
16
 
16
17
  constructor(
17
18
  private serverUrl: string,
@@ -28,7 +29,7 @@ export class SessionManager {
28
29
  private _debug: boolean = false
29
30
  private printedSessionState = {
30
31
  printed: false,
31
- state: ''
32
+ state: SessionState.NoState
32
33
  }
33
34
 
34
35
  public get debug() {
@@ -265,6 +266,18 @@ export class SessionManager {
265
266
  )
266
267
  })
267
268
 
269
+ // Add response etag to Session object.
270
+ createdSession.etag = etag
271
+
272
+ // Get session state link.
273
+ const stateLink = createdSession.links.find((link) => link.rel === 'state')
274
+
275
+ // Throw error if session state link is not present.
276
+ if (!stateLink) throw this.sessionStateLinkError
277
+
278
+ // Add session state link to Session object.
279
+ createdSession.stateUrl = stateLink.href
280
+
268
281
  await this.waitForSession(createdSession, etag, accessToken)
269
282
 
270
283
  this.sessions.push(createdSession)
@@ -327,32 +340,30 @@ export class SessionManager {
327
340
  etag: string | null,
328
341
  accessToken?: string
329
342
  ): Promise<string> {
343
+ let { state: sessionState } = session
344
+ const { stateUrl } = session
330
345
  const logger = process.logger || console
331
346
 
332
- let sessionState = session.state
333
-
334
- const stateLink = session.links.find((l: any) => l.rel === 'state')
335
-
336
347
  if (
337
- sessionState === 'pending' ||
338
- sessionState === 'running' ||
339
- sessionState === ''
348
+ sessionState === SessionState.Pending ||
349
+ sessionState === SessionState.Running ||
350
+ sessionState === SessionState.NoState
340
351
  ) {
341
- if (stateLink) {
352
+ if (stateUrl) {
342
353
  if (this.debug && !this.printedSessionState.printed) {
343
- logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
354
+ logger.info(`Polling: ${this.serverUrl + stateUrl}`)
344
355
 
345
356
  this.printedSessionState.printed = true
346
357
  }
347
358
 
348
- const url = `${this.serverUrl}${stateLink.href}?wait=30`
359
+ const url = `${this.serverUrl}${stateUrl}?wait=30`
349
360
 
350
361
  const { result: state, responseStatus: responseStatus } =
351
362
  await this.getSessionState(url, etag!, accessToken).catch((err) => {
352
363
  throw prefixMessage(err, 'Error while waiting for session. ')
353
364
  })
354
365
 
355
- sessionState = state.trim()
366
+ sessionState = state.trim() as SessionState
356
367
 
357
368
  if (this.debug && this.printedSessionState.state !== sessionState) {
358
369
  logger.info(`Current session state is '${sessionState}'`)
@@ -364,7 +375,7 @@ export class SessionManager {
364
375
  if (!sessionState) {
365
376
  const stateError = new NoSessionStateError(
366
377
  responseStatus,
367
- this.serverUrl + stateLink.href,
378
+ this.serverUrl + stateUrl,
368
379
  session.links.find((l: any) => l.rel === 'log')?.href as string
369
380
  )
370
381
 
@@ -386,7 +397,7 @@ export class SessionManager {
386
397
 
387
398
  return sessionState
388
399
  } else {
389
- throw 'Error while getting session state link. '
400
+ throw this.sessionStateLinkError
390
401
  }
391
402
  } else {
392
403
  this.loggedErrors = []
@@ -413,7 +424,7 @@ export class SessionManager {
413
424
  return await this.requestClient
414
425
  .get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
415
426
  .then((res) => ({
416
- result: res.result as string,
427
+ result: res.result as SessionState,
417
428
  responseStatus: res.status
418
429
  }))
419
430
  .catch((err) => {
@@ -170,16 +170,21 @@ export async function executeOnComputeApi(
170
170
  postedJob,
171
171
  debug,
172
172
  authConfig,
173
- pollOptions
173
+ pollOptions,
174
+ {
175
+ session,
176
+ sessionManager
177
+ }
174
178
  ).catch(async (err) => {
175
179
  const error = err?.response?.data
176
180
  const result = /err=[0-9]*,/.exec(error)
177
-
178
181
  const errorCode = '5113'
182
+
179
183
  if (result?.[0]?.slice(4, -1) === errorCode) {
184
+ const logCount = 1000000
180
185
  const sessionLogUrl =
181
186
  postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
182
- const logCount = 1000000
187
+
183
188
  err.log = await fetchLogByChunks(
184
189
  requestClient,
185
190
  access_token!,
@@ -187,6 +192,7 @@ export async function executeOnComputeApi(
187
192
  logCount
188
193
  )
189
194
  }
195
+
190
196
  throw prefixMessage(err, 'Error while polling job status. ')
191
197
  })
192
198
 
@@ -205,12 +211,12 @@ export async function executeOnComputeApi(
205
211
 
206
212
  let jobResult
207
213
  let log = ''
208
-
209
214
  const logLink = currentJob.links.find((l) => l.rel === 'log')
210
215
 
211
216
  if (debug && logLink) {
212
217
  const logUrl = `${logLink.href}/content`
213
218
  const logCount = currentJob.logStatistics?.lineCount ?? 1000000
219
+
214
220
  log = await fetchLogByChunks(
215
221
  requestClient,
216
222
  access_token!,
@@ -223,9 +229,7 @@ export async function executeOnComputeApi(
223
229
  throw new ComputeJobExecutionError(currentJob, log)
224
230
  }
225
231
 
226
- if (!expectWebout) {
227
- return { job: currentJob, log }
228
- }
232
+ if (!expectWebout) return { job: currentJob, log }
229
233
 
230
234
  const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
231
235
 
@@ -236,6 +240,7 @@ export async function executeOnComputeApi(
236
240
  if (logLink) {
237
241
  const logUrl = `${logLink.href}/content`
238
242
  const logCount = currentJob.logStatistics?.lineCount ?? 1000000
243
+
239
244
  log = await fetchLogByChunks(
240
245
  requestClient,
241
246
  access_token!,
@@ -3,7 +3,7 @@ import { Job, PollOptions, PollStrategy } from '../..'
3
3
  import { getTokens } from '../../auth/getTokens'
4
4
  import { RequestClient } from '../../request/RequestClient'
5
5
  import { JobStatePollError } from '../../types/errors'
6
- import { Link, WriteStream } from '../../types'
6
+ import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
7
7
  import { delay, isNode } from '../../utils'
8
8
 
9
9
  export enum JobState {
@@ -37,6 +37,7 @@ export enum JobState {
37
37
  * { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
38
38
  * { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
39
39
  * ]
40
+ * @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state.
40
41
  * @returns - a promise which resolves with a job state
41
42
  */
42
43
  export async function pollJobState(
@@ -44,7 +45,8 @@ export async function pollJobState(
44
45
  postedJob: Job,
45
46
  debug: boolean,
46
47
  authConfig?: AuthConfig,
47
- pollOptions?: PollOptions
48
+ pollOptions?: PollOptions,
49
+ jobSessionManager?: JobSessionManager
48
50
  ): Promise<JobState> {
49
51
  const logger = process.logger || console
50
52
 
@@ -127,7 +129,8 @@ export async function pollJobState(
127
129
  pollOptions,
128
130
  authConfig,
129
131
  streamLog,
130
- logFileStream
132
+ logFileStream,
133
+ jobSessionManager
131
134
  )
132
135
 
133
136
  currentState = result.state
@@ -158,7 +161,8 @@ export async function pollJobState(
158
161
  defaultPollOptions,
159
162
  authConfig,
160
163
  streamLog,
161
- logFileStream
164
+ logFileStream,
165
+ jobSessionManager
162
166
  )
163
167
 
164
168
  currentState = result.state
@@ -208,7 +212,21 @@ const needsRetry = (state: string) =>
208
212
  state === JobState.Pending ||
209
213
  state === JobState.Unavailable
210
214
 
211
- const doPoll = async (
215
+ /**
216
+ * Polls job state.
217
+ * @param requestClient - the pre-configured HTTP request client.
218
+ * @param postedJob - the relative or absolute path to the job.
219
+ * @param currentState - current job state.
220
+ * @param debug - sets the _debug flag in the job arguments.
221
+ * @param pollCount - current poll count.
222
+ * @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath.
223
+ * @param authConfig - an access token, refresh token, client and secret for an authorized user.
224
+ * @param streamLog - indicates if job log should be streamed.
225
+ * @param logStream - job log stream.
226
+ * @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state. Session state is considered healthy if it is equal to 'running' or 'idle'.
227
+ * @returns - a promise which resolves with a job state
228
+ */
229
+ export const doPoll = async (
212
230
  requestClient: RequestClient,
213
231
  postedJob: Job,
214
232
  currentState: JobState,
@@ -217,7 +235,8 @@ const doPoll = async (
217
235
  pollOptions: PollOptions,
218
236
  authConfig?: AuthConfig,
219
237
  streamLog?: boolean,
220
- logStream?: WriteStream
238
+ logStream?: WriteStream,
239
+ jobSessionManager?: JobSessionManager
221
240
  ): Promise<{ state: JobState; pollCount: number }> => {
222
241
  const { maxPollCount, pollInterval } = pollOptions
223
242
  const logger = process.logger || console
@@ -229,6 +248,40 @@ const doPoll = async (
229
248
  let startLogLine = 0
230
249
 
231
250
  while (needsRetry(state) && pollCount <= maxPollCount) {
251
+ // Check parent session state on every 10th job state poll.
252
+ if (jobSessionManager && pollCount && pollCount % 10 === 0 && authConfig) {
253
+ const { session, sessionManager } = jobSessionManager
254
+ const { stateUrl, etag, id: sessionId } = session
255
+ const { access_token } = authConfig
256
+ const { id: jobId } = postedJob
257
+
258
+ // Get session state.
259
+ const { result: sessionState, responseStatus } = await sessionManager[
260
+ 'getSessionState'
261
+ ](stateUrl, etag, access_token).catch((err) => {
262
+ // Handle error while getting session state.
263
+ throw new JobStatePollError(jobId, err)
264
+ })
265
+
266
+ // Checks if session state is equal to 'running' or 'idle'.
267
+ const isSessionStatesHealthy = (state: string) =>
268
+ [SessionState.Running, SessionState.Idle].includes(
269
+ state as SessionState
270
+ )
271
+
272
+ // Clear parent session and throw an error if session state is not
273
+ // 'running', 'idle' or response status is not 200.
274
+ if (!isSessionStatesHealthy(sessionState) || responseStatus !== 200) {
275
+ sessionManager.clearSession(sessionId, access_token)
276
+
277
+ const sessionError = isSessionStatesHealthy(sessionState)
278
+ ? `Session response status is not 200. Session response status is ${responseStatus}.`
279
+ : `Session state of the job is not 'running' or 'idle'. Session state is '${sessionState}'`
280
+
281
+ throw new JobStatePollError(jobId, new Error(sessionError))
282
+ }
283
+ }
284
+
232
285
  state = await getJobState(
233
286
  requestClient,
234
287
  postedJob,
@@ -1,14 +1,16 @@
1
1
  import { AuthConfig } from '@sasjs/utils/types'
2
- import { Job, Session } from '../../../types'
2
+ import { Job, Session, SessionState } from '../../../types'
3
3
 
4
4
  export const mockSession: Session = {
5
5
  id: 's35510n',
6
- state: 'idle',
6
+ state: SessionState.Idle,
7
+ stateUrl: '',
7
8
  links: [],
8
9
  attributes: {
9
10
  sessionInactiveTimeout: 1
10
11
  },
11
- creationTimeStamp: new Date().valueOf().toString()
12
+ creationTimeStamp: new Date().valueOf().toString(),
13
+ etag: 'etag-string'
12
14
  }
13
15
 
14
16
  export const mockJob: Job = {
@@ -1,15 +1,34 @@
1
1
  import { Link } from './Link'
2
+ import { SessionManager } from '../SessionManager'
3
+
4
+ export enum SessionState {
5
+ Completed = 'completed',
6
+ Running = 'running',
7
+ Pending = 'pending',
8
+ Idle = 'idle',
9
+ Unavailable = 'unavailable',
10
+ NoState = '',
11
+ Failed = 'failed',
12
+ Error = 'error'
13
+ }
2
14
 
3
15
  export interface Session {
4
16
  id: string
5
- state: string
17
+ state: SessionState
18
+ stateUrl: string
6
19
  links: Link[]
7
20
  attributes: {
8
21
  sessionInactiveTimeout: number
9
22
  }
10
23
  creationTimeStamp: string
24
+ etag: string
11
25
  }
12
26
 
13
27
  export interface SessionVariable {
14
28
  value: string
15
29
  }
30
+
31
+ export interface JobSessionManager {
32
+ session: Session
33
+ sessionManager: SessionManager
34
+ }
@@ -1,13 +1,30 @@
1
1
  import { Link } from './Link';
2
+ import { SessionManager } from '../SessionManager';
3
+ export declare enum SessionState {
4
+ Completed = "completed",
5
+ Running = "running",
6
+ Pending = "pending",
7
+ Idle = "idle",
8
+ Unavailable = "unavailable",
9
+ NoState = "",
10
+ Failed = "failed",
11
+ Error = "error"
12
+ }
2
13
  export interface Session {
3
14
  id: string;
4
- state: string;
15
+ state: SessionState;
16
+ stateUrl: string;
5
17
  links: Link[];
6
18
  attributes: {
7
19
  sessionInactiveTimeout: number;
8
20
  };
9
21
  creationTimeStamp: string;
22
+ etag: string;
10
23
  }
11
24
  export interface SessionVariable {
12
25
  value: string;
13
26
  }
27
+ export interface JobSessionManager {
28
+ session: Session;
29
+ sessionManager: SessionManager;
30
+ }