@sasjs/adapter 4.2.3 → 4.3.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.
@@ -2,9 +2,8 @@ export declare class ErrorResponse {
2
2
  error: ErrorBody;
3
3
  constructor(message: string, details?: any, raw?: any);
4
4
  }
5
- interface ErrorBody {
5
+ export interface ErrorBody {
6
6
  message: string;
7
- details: string;
7
+ details: any;
8
8
  raw: any;
9
9
  }
10
- export {};
package/package.json CHANGED
@@ -84,5 +84,5 @@
84
84
  "https": "1.0.0",
85
85
  "tough-cookie": "4.0.0"
86
86
  },
87
- "version": "4.2.3"
87
+ "version": "4.3.0"
88
88
  }
@@ -6,6 +6,10 @@ import { RequestClient } from './request/RequestClient'
6
6
 
7
7
  const MAX_SESSION_COUNT = 1
8
8
 
9
+ interface ApiErrorResponse {
10
+ response: { status: number | string; data: { message: string } }
11
+ }
12
+
9
13
  export class SessionManager {
10
14
  private loggedErrors: NoSessionStateError[] = []
11
15
 
@@ -17,8 +21,10 @@ export class SessionManager {
17
21
  if (serverUrl) isUrl(serverUrl)
18
22
  }
19
23
 
24
+ // INFO: session pool
20
25
  private sessions: Session[] = []
21
26
  private currentContext: Context | null = null
27
+ private settingContext: boolean = false
22
28
  private _debug: boolean = false
23
29
  private printedSessionState = {
24
30
  printed: false,
@@ -33,71 +39,230 @@ export class SessionManager {
33
39
  this._debug = value
34
40
  }
35
41
 
36
- async getSession(accessToken?: string) {
37
- await this.createSessions(accessToken)
38
- await this.createAndWaitForSession(accessToken)
39
- const session = this.sessions.pop()
42
+ /**
43
+ * Checks if session is valid. Session is considered valid if time since it's creation is less than 'sessionInactiveTimeout' attribute.
44
+ * @param session - session object.
45
+ * @returns - boolean indicating if session is valid.
46
+ */
47
+ private isSessionValid(session: Session): boolean {
48
+ if (!session) return false
49
+
40
50
  const secondsSinceSessionCreation =
41
- (new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
51
+ (new Date().getTime() - new Date(session.creationTimeStamp).getTime()) /
42
52
  1000
43
53
 
44
54
  if (
45
55
  !session!.attributes ||
46
56
  secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
47
57
  ) {
48
- await this.createSessions(accessToken)
49
- const freshSession = this.sessions.pop()
58
+ return false
59
+ } else {
60
+ return true
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Removes session from pool of hot sessions.
66
+ * @param session - session object.
67
+ * @returns - void.
68
+ */
69
+ private removeSessionFromPool(session: Session): void {
70
+ this.sessions = this.sessions.filter((ses) => ses.id !== session.id)
71
+ }
72
+
73
+ /**
74
+ * Filters session pool to keep only valid sessions.
75
+ * @param session - session object.
76
+ * @returns - void.
77
+ */
78
+ private removeExpiredSessions(): void {
79
+ this.sessions = this.sessions.filter((session) =>
80
+ this.isSessionValid(session)
81
+ )
82
+ }
83
+
84
+ /**
85
+ * Throws set of errors as a single error.
86
+ * @param errors - array of errors or string.
87
+ * @param prefix - an optional final error prefix.
88
+ * @returns - never.
89
+ */
90
+ private throwErrors(errors: (Error | string)[], prefix?: string): never {
91
+ throw prefix
92
+ ? prefixMessage(new Error(errors.join('. ')), prefix)
93
+ : new Error(
94
+ errors
95
+ .map((err) =>
96
+ (err as Error).message ? (err as Error).message : err
97
+ )
98
+ .join('. ')
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Returns session.
104
+ * If there is a hot session available, it will be returned immediately and an asynchronous request to create new hot session will be submitted.
105
+ * If there is no available session, 2 session creation requests will be submitted. The session is returned once it is created and ready.
106
+ * @param accessToken - an optional access token.
107
+ * @returns - a promise which resolves with a session.
108
+ */
109
+ async getSession(accessToken?: string) {
110
+ const errors: (Error | string)[] = []
111
+ let isErrorThrown = false
112
+
113
+ const throwIfError = () => {
114
+ if (errors.length && !isErrorThrown) {
115
+ isErrorThrown = true
116
+
117
+ this.throwErrors(errors)
118
+ }
119
+ }
50
120
 
51
- return freshSession
121
+ this.removeExpiredSessions()
122
+
123
+ if (this.sessions.length) {
124
+ const session = this.sessions[0]
125
+
126
+ this.removeSessionFromPool(session)
127
+
128
+ this.createSessions(accessToken).catch((err) => {
129
+ errors.push(err)
130
+ })
131
+
132
+ this.createAndWaitForSession(accessToken).catch((err) => {
133
+ errors.push(err)
134
+ })
135
+
136
+ throwIfError()
137
+
138
+ return session
139
+ } else {
140
+ this.createSessions(accessToken).catch((err) => {
141
+ errors.push(err)
142
+ })
143
+
144
+ await this.createAndWaitForSession(accessToken).catch((err) => {
145
+ errors.push(err)
146
+ })
147
+
148
+ this.removeExpiredSessions()
149
+
150
+ const session = this.sessions.pop()!
151
+
152
+ this.removeSessionFromPool(session)
153
+
154
+ throwIfError()
155
+
156
+ return session
52
157
  }
158
+ }
53
159
 
54
- return session
160
+ /**
161
+ * Returns error message based on the response from SAS API.
162
+ * @param err - an optional access token.
163
+ * @param accessToken - an optional access token.
164
+ * @returns - an error message.
165
+ */
166
+ private getErrorMessage(
167
+ err: ApiErrorResponse,
168
+ url: string,
169
+ method: 'GET' | 'POST' | 'DELETE'
170
+ ) {
171
+ return (
172
+ `${method} request to ${url} failed with status code ${
173
+ err.response.status || 'unknown'
174
+ }. ` + err.response.data.message || ''
175
+ )
55
176
  }
56
177
 
57
- async clearSession(id: string, accessToken?: string) {
178
+ /**
179
+ * Deletes session.
180
+ * @param id - a session id.
181
+ * @param accessToken - an optional access token.
182
+ * @returns - a promise which resolves when session is deleted.
183
+ */
184
+ async clearSession(id: string, accessToken?: string): Promise<void> {
185
+ const url = `/compute/sessions/${id}`
186
+
58
187
  return await this.requestClient
59
- .delete<Session>(`/compute/sessions/${id}`, accessToken)
188
+ .delete<Session>(url, accessToken)
60
189
  .then(() => {
61
190
  this.sessions = this.sessions.filter((s) => s.id !== id)
62
191
  })
63
- .catch((err) => {
64
- throw prefixMessage(err, 'Error while deleting session. ')
192
+ .catch((err: ApiErrorResponse) => {
193
+ throw prefixMessage(
194
+ this.getErrorMessage(err, url, 'DELETE'),
195
+ 'Error while deleting session. '
196
+ )
65
197
  })
66
198
  }
67
199
 
68
- private async createSessions(accessToken?: string) {
69
- if (!this.sessions.length) {
70
- if (!this.currentContext) {
71
- await this.setCurrentContext(accessToken).catch((err) => {
72
- throw err
73
- })
74
- }
200
+ /**
201
+ * Creates sessions in amount equal to MAX_SESSION_COUNT.
202
+ * @param accessToken - an optional access token.
203
+ * @returns - a promise which resolves when required amount of sessions is created.
204
+ */
205
+ private async createSessions(accessToken?: string): Promise<void> {
206
+ const errors: (Error | string)[] = []
75
207
 
208
+ if (!this.sessions.length) {
76
209
  await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
77
- const createdSession = await this.createAndWaitForSession(
78
- accessToken
79
- ).catch((err) => {
80
- throw err
210
+ await this.createAndWaitForSession(accessToken).catch((err) => {
211
+ errors.push(err)
81
212
  })
82
-
83
- this.sessions.push(createdSession)
84
- }).catch((err) => {
85
- throw err
86
213
  })
87
214
  }
215
+
216
+ if (errors.length) {
217
+ this.throwErrors(errors, 'Error while creating session. ')
218
+ }
88
219
  }
89
220
 
90
- private async createAndWaitForSession(accessToken?: string) {
221
+ /**
222
+ * Waits for the current context to be set.
223
+ * @returns - a promise which resolves when current context is set.
224
+ */
225
+ private async waitForCurrentContext(): Promise<void> {
226
+ return new Promise((resolve) => {
227
+ const timer = setInterval(() => {
228
+ if (this.currentContext) {
229
+ this.settingContext = false
230
+
231
+ clearInterval(timer)
232
+
233
+ resolve()
234
+ }
235
+ }, 100)
236
+ })
237
+ }
238
+
239
+ /**
240
+ * Creates and waits for session to be ready.
241
+ * @param accessToken - an optional access token.
242
+ * @returns - a promise which resolves with a session.
243
+ */
244
+ private async createAndWaitForSession(
245
+ accessToken?: string
246
+ ): Promise<Session> {
247
+ if (!this.currentContext) {
248
+ if (!this.settingContext) {
249
+ await this.setCurrentContext(accessToken)
250
+ } else {
251
+ await this.waitForCurrentContext()
252
+ }
253
+ }
254
+
255
+ const url = `${this.serverUrl}/compute/contexts/${
256
+ this.currentContext!.id
257
+ }/sessions`
258
+
91
259
  const { result: createdSession, etag } = await this.requestClient
92
- .post<Session>(
93
- `${this.serverUrl}/compute/contexts/${
94
- this.currentContext!.id
95
- }/sessions`,
96
- {},
97
- accessToken
98
- )
99
- .catch((err) => {
100
- throw err
260
+ .post<Session>(url, {}, accessToken)
261
+ .catch((err: ApiErrorResponse) => {
262
+ throw prefixMessage(
263
+ this.getErrorMessage(err, url, 'POST'),
264
+ `Error while creating session. `
265
+ )
101
266
  })
102
267
 
103
268
  await this.waitForSession(createdSession, etag, accessToken)
@@ -107,14 +272,26 @@ export class SessionManager {
107
272
  return createdSession
108
273
  }
109
274
 
110
- private async setCurrentContext(accessToken?: string) {
275
+ /**
276
+ * Sets current context.
277
+ * @param accessToken - an optional access token.
278
+ * @returns - a promise which resolves when current context is set.
279
+ */
280
+ private async setCurrentContext(accessToken?: string): Promise<void> {
111
281
  if (!this.currentContext) {
282
+ const url = `${this.serverUrl}/compute/contexts?limit=10000`
283
+
284
+ this.settingContext = true
285
+
112
286
  const { result: contexts } = await this.requestClient
113
287
  .get<{
114
288
  items: Context[]
115
- }>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
116
- .catch((err) => {
117
- throw err
289
+ }>(url, accessToken)
290
+ .catch((err: ApiErrorResponse) => {
291
+ throw prefixMessage(
292
+ this.getErrorMessage(err, url, 'GET'),
293
+ `Error while getting list of contexts. `
294
+ )
118
295
  })
119
296
 
120
297
  const contextsList =
@@ -138,18 +315,13 @@ export class SessionManager {
138
315
  }
139
316
  }
140
317
 
141
- private getHeaders(accessToken?: string) {
142
- const headers: any = {
143
- 'Content-Type': 'application/json'
144
- }
145
-
146
- if (accessToken) {
147
- headers.Authorization = `Bearer ${accessToken}`
148
- }
149
-
150
- return headers
151
- }
152
-
318
+ /**
319
+ * Waits for session to be ready.
320
+ * @param session - a session object.
321
+ * @param etag - an etag that can be a string or null.
322
+ * @param accessToken - an optional access token.
323
+ * @returns - a promise which resolves with a session state.
324
+ */
153
325
  private async waitForSession(
154
326
  session: Session,
155
327
  etag: string | null,
@@ -173,13 +345,11 @@ export class SessionManager {
173
345
  this.printedSessionState.printed = true
174
346
  }
175
347
 
348
+ const url = `${this.serverUrl}${stateLink.href}?wait=30`
349
+
176
350
  const { result: state, responseStatus: responseStatus } =
177
- await this.getSessionState(
178
- `${this.serverUrl}${stateLink.href}?wait=30`,
179
- etag!,
180
- accessToken
181
- ).catch((err) => {
182
- throw prefixMessage(err, 'Error while getting session state.')
351
+ await this.getSessionState(url, etag!, accessToken).catch((err) => {
352
+ throw prefixMessage(err, 'Error while waiting for session. ')
183
353
  })
184
354
 
185
355
  sessionState = state.trim()
@@ -216,7 +386,7 @@ export class SessionManager {
216
386
 
217
387
  return sessionState
218
388
  } else {
219
- throw 'Error while getting session state link.'
389
+ throw 'Error while getting session state link. '
220
390
  }
221
391
  } else {
222
392
  this.loggedErrors = []
@@ -225,11 +395,21 @@ export class SessionManager {
225
395
  }
226
396
  }
227
397
 
398
+ /**
399
+ * Gets session state.
400
+ * @param url - a URL to get session state.
401
+ * @param etag - an etag string.
402
+ * @param accessToken - an optional access token.
403
+ * @returns - a promise which resolves with a result string and response status.
404
+ */
228
405
  private async getSessionState(
229
406
  url: string,
230
407
  etag: string,
231
408
  accessToken?: string
232
- ) {
409
+ ): Promise<{
410
+ result: string
411
+ responseStatus: number
412
+ }> {
233
413
  return await this.requestClient
234
414
  .get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
235
415
  .then((res) => ({
@@ -237,20 +417,37 @@ export class SessionManager {
237
417
  responseStatus: res.status
238
418
  }))
239
419
  .catch((err) => {
240
- throw err
420
+ throw prefixMessage(
421
+ this.getErrorMessage(err, url, 'GET'),
422
+ 'Error while getting session state. '
423
+ )
241
424
  })
242
425
  }
243
426
 
244
- async getVariable(sessionId: string, variable: string, accessToken?: string) {
427
+ /**
428
+ * Gets variable.
429
+ * @param sessionId - a session id.
430
+ * @param variable - a variable string.
431
+ * @param accessToken - an optional access token.
432
+ * @returns - a promise which resolves with a result that confirms to SessionVariable interface, etag string and status code.
433
+ */
434
+ async getVariable(
435
+ sessionId: string,
436
+ variable: string,
437
+ accessToken?: string
438
+ ): Promise<{
439
+ result: SessionVariable
440
+ etag: string
441
+ status: number
442
+ }> {
443
+ const url = `${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`
444
+
245
445
  return await this.requestClient
246
- .get<SessionVariable>(
247
- `${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
248
- accessToken
249
- )
446
+ .get<SessionVariable>(url, accessToken)
250
447
  .catch((err) => {
251
448
  throw prefixMessage(
252
- err,
253
- `Error while fetching session variable '${variable}'.`
449
+ this.getErrorMessage(err, url, 'GET'),
450
+ `Error while fetching session variable '${variable}'. `
254
451
  )
255
452
  })
256
453
  }
@@ -21,8 +21,8 @@ export class ErrorResponse {
21
21
  }
22
22
  }
23
23
 
24
- interface ErrorBody {
24
+ export interface ErrorBody {
25
25
  message: string
26
- details: string
26
+ details: any
27
27
  raw: any
28
28
  }
@@ -2,9 +2,8 @@ export declare class ErrorResponse {
2
2
  error: ErrorBody;
3
3
  constructor(message: string, details?: any, raw?: any);
4
4
  }
5
- interface ErrorBody {
5
+ export interface ErrorBody {
6
6
  message: string;
7
- details: string;
7
+ details: any;
8
8
  raw: any;
9
9
  }
10
- export {};