@sasjs/adapter 4.2.4 → 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.
- package/README.md +8 -0
- package/SessionManager.d.ts +88 -2
- package/index-dev.js +2 -2
- package/index.js +1 -1
- package/node/SessionManager.d.ts +88 -2
- package/node/main.js +1 -1
- package/package.json +1 -1
- package/screenshots/session-manager-first-request.png +0 -0
- package/screenshots/subsequent-session-request.png +0 -0
- package/src/SessionManager.ts +267 -70
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/src/SessionManager.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>(
|
|
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(
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
}>(
|
|
116
|
-
.catch((err) => {
|
|
117
|
-
throw
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
420
|
+
throw prefixMessage(
|
|
421
|
+
this.getErrorMessage(err, url, 'GET'),
|
|
422
|
+
'Error while getting session state. '
|
|
423
|
+
)
|
|
241
424
|
})
|
|
242
425
|
}
|
|
243
426
|
|
|
244
|
-
|
|
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
|
}
|