terrier-engine 4.13.6 → 4.14.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/data-dive/dives/dive-runs.ts +2 -1
- package/package.json +1 -1
- package/terrier/api-subscriber.ts +404 -0
- package/terrier/api.ts +23 -21
|
@@ -2,7 +2,8 @@ import {ModalPart} from "../../terrier/modals"
|
|
|
2
2
|
import {PartTag} from "tuff-core/parts"
|
|
3
3
|
import {DdDive, DdDiveRun, UnpersistedDdDiveRun} from "../gen/models"
|
|
4
4
|
import Db from "../dd-db"
|
|
5
|
-
import Api
|
|
5
|
+
import Api from "../../terrier/api"
|
|
6
|
+
import {ErrorEvent} from "../../terrier/api-subscriber"
|
|
6
7
|
import {Query} from "../queries/queries"
|
|
7
8
|
import {DivTag, HtmlParentTag} from "tuff-core/html"
|
|
8
9
|
import {IconName} from "../../terrier/theme"
|
package/package.json
CHANGED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import Api, {ApiResponse, noArgListener, Streamer} from "./api"
|
|
2
|
+
import {LogEntry} from "./logging"
|
|
3
|
+
import dayjs from "dayjs"
|
|
4
|
+
import duration from "dayjs/plugin/duration"
|
|
5
|
+
|
|
6
|
+
dayjs.extend(duration)
|
|
7
|
+
|
|
8
|
+
/** Configuration for ApiSubscribers */
|
|
9
|
+
export type SubscriptionOptions = {
|
|
10
|
+
/** If true, retry the subscription after it is cancelled */
|
|
11
|
+
keepAlive?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parameters to send with the subscription request */
|
|
15
|
+
export type SubscriptionParams = Record<string, string>
|
|
16
|
+
|
|
17
|
+
export type SubscriptionEventHandlers<TResult> = {
|
|
18
|
+
onResult: resultListener<TResult>[]
|
|
19
|
+
onError: errorListener[]
|
|
20
|
+
onLog: logListener[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SubscriptionLifecycleHandlers = {
|
|
24
|
+
onSubscribe: noArgListener[]
|
|
25
|
+
onUnsubscribe: noArgListener[]
|
|
26
|
+
onClose: closeListener[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type resultListener<TResult> = (result: ResultEvent<TResult>) => boolean
|
|
30
|
+
type errorListener = (result: ErrorEvent) => boolean
|
|
31
|
+
type logListener = (log: LogEvent) => void
|
|
32
|
+
type closeListener = (reason?: string) => void
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
type BaseSubscriptionEvent = {
|
|
36
|
+
_type: '_result' | '_error' | '_log' | '_close' | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SubscriptionEvent<TResult> = BaseSubscriptionEvent & (ResultEvent<TResult> | ErrorEvent | LogEvent | CloseEvent)
|
|
40
|
+
|
|
41
|
+
/** Type of result events from a subscription */
|
|
42
|
+
export type ResultEvent<TResult> = BaseSubscriptionEvent & { _type: '_result' | undefined } & TResult
|
|
43
|
+
|
|
44
|
+
/** Type of error events from a subscription */
|
|
45
|
+
export type ErrorEvent = BaseSubscriptionEvent & { _type: '_error' } & {
|
|
46
|
+
prefix?: string
|
|
47
|
+
message: string
|
|
48
|
+
backtrace: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Type of log events from a subscription */
|
|
52
|
+
export type LogEvent = BaseSubscriptionEvent & { _type: '_log' } & LogEntry
|
|
53
|
+
|
|
54
|
+
export type CloseEvent = BaseSubscriptionEvent & { _type: '_close' }
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An abstraction over the process of receiving continuous updates from the server.
|
|
58
|
+
* This allows client code to specify a dependency on continuous updates of a particular type without needing
|
|
59
|
+
* to care about how those updates are fulfilled.
|
|
60
|
+
*
|
|
61
|
+
* Behind the scenes, this could be handled by pulling from the client (PollingSubscriber),
|
|
62
|
+
* pushing from the server (CableSubscriber), or a hybrid of the two (StreamingSubscriber).
|
|
63
|
+
*
|
|
64
|
+
* Subclasses are responsible for implementing the process of initiating and finalizing the subscription and
|
|
65
|
+
* passing events to the appropriate handlers.
|
|
66
|
+
*
|
|
67
|
+
* Each subclass is responsible for providing a constructor with whatever dependencies are required, but all subclasses
|
|
68
|
+
* must implement `params` and `options` properties and handle them appropriately.
|
|
69
|
+
*/
|
|
70
|
+
export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams> {
|
|
71
|
+
|
|
72
|
+
public abstract params: TParams
|
|
73
|
+
public abstract options?: SubscriptionOptions
|
|
74
|
+
|
|
75
|
+
protected isSubscribed: boolean = false
|
|
76
|
+
|
|
77
|
+
protected eventHandlers: SubscriptionEventHandlers<TResult> = { onResult: [], onError: [], onLog: [] }
|
|
78
|
+
protected lifecycleHandlers: SubscriptionLifecycleHandlers = { onSubscribe: [], onUnsubscribe: [], onClose: [] }
|
|
79
|
+
protected otherHandlers: Record<string, ((event: unknown) => boolean | void)[]> = {}
|
|
80
|
+
|
|
81
|
+
// Handler registration
|
|
82
|
+
|
|
83
|
+
/** Register a handler for a custom event type */
|
|
84
|
+
public on<EventType>(eventType: string, handler: (event: EventType) => boolean | void): this {
|
|
85
|
+
if (eventType in ['_result', '_error', '_log', '_close']) {
|
|
86
|
+
throw new Error(`${eventType} is a reserved handler type. Please use a different type or use the appropriate handler registration function.`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.otherHandlers[eventType] ??= []
|
|
90
|
+
this.otherHandlers[eventType].push(handler as (event: unknown) => boolean | void)
|
|
91
|
+
return this
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Register a handler for the default result type of this subscriber */
|
|
95
|
+
public onResult(handler: (result: ResultEvent<TResult>) => boolean): this {
|
|
96
|
+
this.eventHandlers.onResult.push(handler)
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Register a handler for error events */
|
|
101
|
+
public onError(handler: (error: ErrorEvent) => boolean): this {
|
|
102
|
+
this.eventHandlers.onError.push(handler)
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Register a handler for log events */
|
|
107
|
+
public onLog(handler: (logEntry: LogEvent) => void): this {
|
|
108
|
+
this.eventHandlers.onLog.push(handler)
|
|
109
|
+
return this
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Register a handler to be notified when this subscriber is subscribed */
|
|
113
|
+
public onSubscribe(handler: () => void): this {
|
|
114
|
+
return this.onLifecycle('onSubscribe', handler)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Register a handler to be notified when this subscriber is manually unsubscribed */
|
|
118
|
+
public onUnsubscribe(handler: () => void): this {
|
|
119
|
+
return this.onLifecycle('onUnsubscribe', handler)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Register a handler to be notified when the publisher source of this subscriber closes the subscription */
|
|
123
|
+
public onClose(handler: () => void): this {
|
|
124
|
+
return this.onLifecycle('onClose', handler)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** utility to register a generic lifecycle handler */
|
|
128
|
+
protected onLifecycle(key: keyof SubscriptionLifecycleHandlers, handler: noArgListener): this
|
|
129
|
+
protected onLifecycle(key: 'onClose', handler: (reason?: string) => void): this
|
|
130
|
+
protected onLifecycle(key: keyof SubscriptionLifecycleHandlers, handler: noArgListener | ((reason?: string) => void)): this {
|
|
131
|
+
this.lifecycleHandlers[key].push(handler)
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Start or stop subscription
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Starts the subscription. Calling this after the subscription has already been started has no effect
|
|
139
|
+
*/
|
|
140
|
+
public subscribe(): this {
|
|
141
|
+
if (this.isSubscribed) return this
|
|
142
|
+
this.subscribeImpl()
|
|
143
|
+
this.isSubscribed = true
|
|
144
|
+
this.notifyLifecycle('onSubscribe')
|
|
145
|
+
return this
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Stops the subscription and closes the connection. */
|
|
149
|
+
public unsubscribe(): void {
|
|
150
|
+
if (!this.isSubscribed) {
|
|
151
|
+
throw new Error("Can't unsubscribe from a un-started subscription!")
|
|
152
|
+
}
|
|
153
|
+
this.unsubscribeImpl()
|
|
154
|
+
this.isSubscribed = false
|
|
155
|
+
this.notifyLifecycle('onUnsubscribe')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Modify subscription params
|
|
159
|
+
|
|
160
|
+
/** Update the params of this subscriber. Depending on the implementation, this may restart the subscription. */
|
|
161
|
+
public abstract updateParams(newParams: TParams): void
|
|
162
|
+
|
|
163
|
+
// Internal
|
|
164
|
+
|
|
165
|
+
protected close() {
|
|
166
|
+
this.isSubscribed = false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Call the handlers for the given lifecycle event */
|
|
170
|
+
protected notifyLifecycle(key: keyof SubscriptionLifecycleHandlers): void
|
|
171
|
+
protected notifyLifecycle(key: 'onClose', reason: string | undefined): void
|
|
172
|
+
protected notifyLifecycle(key: keyof SubscriptionLifecycleHandlers, reason: string | undefined = undefined): void {
|
|
173
|
+
if (key == 'onClose') {
|
|
174
|
+
this.lifecycleHandlers[key].forEach(handler => handler(reason))
|
|
175
|
+
} else {
|
|
176
|
+
this.lifecycleHandlers[key].forEach(handler => handler())
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Subclasses implement this method to start the subscription */
|
|
181
|
+
protected abstract subscribeImpl(): void
|
|
182
|
+
|
|
183
|
+
/** Subclasses imlpement this method to end the subscription */
|
|
184
|
+
protected abstract unsubscribeImpl(): void
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
188
|
+
// Polling Subscriber
|
|
189
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* An implementation of ApiSubscriber that uses polling to periodically make a request to the server
|
|
193
|
+
*/
|
|
194
|
+
export class PollingSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams> {
|
|
195
|
+
|
|
196
|
+
// used to ensure we only schedule one interval at a time and allows for cancellation.
|
|
197
|
+
private timeoutHandle: NodeJS.Timeout | null = null
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param url the url to make a request to.
|
|
201
|
+
* @param params params to use to make the request.
|
|
202
|
+
* @param interval the interval on which to poll. Either a number of milliseconds or a dayjs Duration.
|
|
203
|
+
* @param options extra subscription options
|
|
204
|
+
*/
|
|
205
|
+
constructor(
|
|
206
|
+
public url: string,
|
|
207
|
+
public params: TParams,
|
|
208
|
+
public interval: number | duration.Duration,
|
|
209
|
+
public options: SubscriptionOptions | undefined = undefined
|
|
210
|
+
) {
|
|
211
|
+
super()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
subscribeImpl() {
|
|
215
|
+
if (this.timeoutHandle) return
|
|
216
|
+
this.makeRequest()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
unsubscribeImpl() {
|
|
220
|
+
this.clearTimeout()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
updateParams(newParams: TParams) {
|
|
224
|
+
this.params = newParams
|
|
225
|
+
this.makeRequest()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private makeRequest() {
|
|
229
|
+
this.clearTimeout()
|
|
230
|
+
this.request().then(response => {
|
|
231
|
+
let shouldContinuePolling = true
|
|
232
|
+
if ('events' in response) {
|
|
233
|
+
for (const event of response.events) {
|
|
234
|
+
shouldContinuePolling &&= this.handleEvent(event)
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
if (response.status == 'error') response._type = '_error'
|
|
238
|
+
shouldContinuePolling = this.handleEvent(response)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (shouldContinuePolling) {
|
|
242
|
+
this.startTimeout()
|
|
243
|
+
} else {
|
|
244
|
+
this.unsubscribe()
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
protected request(): Promise<((ApiResponse & SubscriptionEvent<TResult>) | { events: SubscriptionEvent<TResult>[] })> {
|
|
250
|
+
return Api.get<((ApiResponse & SubscriptionEvent<TResult>) | { events: SubscriptionEvent<TResult>[] })>(this.url, this.params)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected handleEvent(event: SubscriptionEvent<TResult>): boolean {
|
|
254
|
+
let shouldContinuePolling = true
|
|
255
|
+
|
|
256
|
+
switch (event._type) {
|
|
257
|
+
case '_result':
|
|
258
|
+
case undefined:
|
|
259
|
+
for (const handler of this.eventHandlers.onResult) {
|
|
260
|
+
const r = handler(event)
|
|
261
|
+
shouldContinuePolling &&= r
|
|
262
|
+
}
|
|
263
|
+
break
|
|
264
|
+
case '_error':
|
|
265
|
+
for (const handler of this.eventHandlers.onError) {
|
|
266
|
+
const r = handler(event)
|
|
267
|
+
shouldContinuePolling &&= r
|
|
268
|
+
}
|
|
269
|
+
break
|
|
270
|
+
case '_log':
|
|
271
|
+
this.eventHandlers.onLog.forEach(handler => handler(event))
|
|
272
|
+
break
|
|
273
|
+
case '_close':
|
|
274
|
+
this.notifyLifecycle('onClose')
|
|
275
|
+
this.close()
|
|
276
|
+
shouldContinuePolling = this.options?.keepAlive ?? false
|
|
277
|
+
break
|
|
278
|
+
default:
|
|
279
|
+
const otherEvent = event as any
|
|
280
|
+
for (const handler of this.otherHandlers[otherEvent._type]) {
|
|
281
|
+
const r = handler(otherEvent) ?? true
|
|
282
|
+
shouldContinuePolling &&= r
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return shouldContinuePolling
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected clearTimeout() {
|
|
290
|
+
if (this.timeoutHandle) {
|
|
291
|
+
clearTimeout(this.timeoutHandle)
|
|
292
|
+
this.timeoutHandle = null
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
protected startTimeout() {
|
|
297
|
+
if (this.timeoutHandle) return
|
|
298
|
+
const intervalMs = (dayjs.isDuration(this.interval)) ? this.interval.asMilliseconds() : this.interval
|
|
299
|
+
this.timeoutHandle = setTimeout(this.makeRequest.bind(this), intervalMs)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
304
|
+
// Action Cable Subscriber
|
|
305
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* An ApiSubscriber that uses ActionCable to get pushed results from the server.
|
|
309
|
+
* Not implemented yet
|
|
310
|
+
*/
|
|
311
|
+
export class CableSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams> {
|
|
312
|
+
|
|
313
|
+
constructor(
|
|
314
|
+
public channelName: string,
|
|
315
|
+
public params: TParams,
|
|
316
|
+
public options: SubscriptionOptions | undefined = undefined
|
|
317
|
+
) {
|
|
318
|
+
throw new Error("Method not implemented.")
|
|
319
|
+
super()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public updateParams(_newParams: TParams): void {
|
|
323
|
+
throw new Error("Method not implemented.")
|
|
324
|
+
}
|
|
325
|
+
protected subscribeImpl(): void {
|
|
326
|
+
throw new Error("Method not implemented.")
|
|
327
|
+
}
|
|
328
|
+
protected unsubscribeImpl(): void {
|
|
329
|
+
throw new Error("Method not implemented.")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
334
|
+
// Event Stream Subscriber
|
|
335
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
336
|
+
|
|
337
|
+
export class StreamingSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams> {
|
|
338
|
+
|
|
339
|
+
private streamer?: Streamer
|
|
340
|
+
|
|
341
|
+
constructor(
|
|
342
|
+
public url: string,
|
|
343
|
+
public params: TParams,
|
|
344
|
+
public options: SubscriptionOptions | undefined = undefined,
|
|
345
|
+
) {
|
|
346
|
+
super()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public on<EventType>(eventType: string, handler: (event: EventType) => boolean | void): this {
|
|
350
|
+
this.streamer?.on(eventType, handler)
|
|
351
|
+
return super.on(eventType, handler)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public onResult(handler: (result: ResultEvent<TResult>) => boolean): this {
|
|
355
|
+
this.streamer?.on<ResultEvent<TResult>>('_result', handler)
|
|
356
|
+
return super.onResult(handler)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public onError(handler: (error: ErrorEvent) => boolean): this {
|
|
360
|
+
this.streamer?.on<ErrorEvent>('_error', handler)
|
|
361
|
+
return super.onError(handler)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public onLog(handler: (logEntry: LogEvent) => void): this {
|
|
365
|
+
this.streamer?.on<LogEvent>('_log', handler)
|
|
366
|
+
return super.onLog(handler)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
protected subscribeImpl(): void {
|
|
370
|
+
const paramsString = new URLSearchParams(this.params).toString()
|
|
371
|
+
const streamer = new Streamer(`${this.url}?${paramsString}`, this.options ?? {})
|
|
372
|
+
|
|
373
|
+
this.eventHandlers.onResult.forEach(handler => streamer.on<ResultEvent<TResult>>('_result', this.wrapHandler(handler)))
|
|
374
|
+
this.eventHandlers.onError.forEach(handler => streamer.on<ErrorEvent>('_error', this.wrapHandler(handler)))
|
|
375
|
+
this.eventHandlers.onLog.forEach(handler => streamer.on<LogEvent>('_log', handler))
|
|
376
|
+
this.lifecycleHandlers.onClose.forEach(handler => streamer.onClose(handler))
|
|
377
|
+
streamer.onClose(() => {
|
|
378
|
+
this.close()
|
|
379
|
+
})
|
|
380
|
+
Object.entries(this.otherHandlers)
|
|
381
|
+
.forEach(([key, handlers]) =>
|
|
382
|
+
handlers.forEach(handler => streamer.on(key, this.wrapHandler(handler)))
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
this.streamer = streamer
|
|
386
|
+
}
|
|
387
|
+
protected unsubscribeImpl(): void {
|
|
388
|
+
this.streamer?.sse.close()
|
|
389
|
+
this.streamer = undefined
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
public updateParams(newParams: TParams): void {
|
|
393
|
+
this.unsubscribeImpl()
|
|
394
|
+
this.params = newParams
|
|
395
|
+
this.subscribeImpl()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private wrapHandler<T>(handler: (param: T) => boolean | void): (param: T) => void {
|
|
399
|
+
return (param: T) => {
|
|
400
|
+
const shouldContinue = handler(param) ?? true
|
|
401
|
+
if (!shouldContinue) this.unsubscribe()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
package/terrier/api.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {Logger} from "tuff-core/logging"
|
|
2
2
|
import {QueryParams} from "tuff-core/urls"
|
|
3
3
|
import {LogEntry} from "./logging"
|
|
4
|
+
import {ErrorEvent} from "./api-subscriber"
|
|
4
5
|
|
|
5
6
|
const log = new Logger('Api')
|
|
6
|
-
log.level = 'debug'
|
|
7
|
-
|
|
8
7
|
|
|
9
8
|
////////////////////////////////////////////////////////////////////////////////
|
|
10
9
|
// Basic Requests
|
|
@@ -79,21 +78,32 @@ async function apiRequest<ResponseType>(url: string, config: RequestInit): Promi
|
|
|
79
78
|
* @param params a set of parameters that will be added to the URL as a query string
|
|
80
79
|
*/
|
|
81
80
|
async function safeGet<ResponseType>(url: string, params: QueryParams | Record<string, string | undefined>): Promise<ResponseType> {
|
|
81
|
+
const response = await get<ApiResponse & ResponseType>(url, params)
|
|
82
|
+
if (response.status == 'error') {
|
|
83
|
+
throw new ApiException(response.message)
|
|
84
|
+
}
|
|
85
|
+
return response
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Performs a GET request for the given datatype.
|
|
90
|
+
* Unlike `safeGet`, this will return the result regardless of the response status.
|
|
91
|
+
* `ResponseType` does not need to include the `status` or `message` fields, this is handled automatically.
|
|
92
|
+
* @param url the base URL for the request
|
|
93
|
+
* @param params a set of parameters that will be added to the URL as a query string
|
|
94
|
+
*/
|
|
95
|
+
async function get<ResponseType>(url: string, params: QueryParams | Record<string, string | undefined>): Promise<ResponseType> {
|
|
82
96
|
if (!params.raw) {
|
|
83
97
|
params = new QueryParams(params as Record<string, string>)
|
|
84
98
|
}
|
|
85
99
|
const fullUrl = (params as QueryParams).serialize(url)
|
|
86
|
-
log.debug(`
|
|
87
|
-
|
|
100
|
+
log.debug(`Getting ${fullUrl}`)
|
|
101
|
+
return await apiRequest<ResponseType>(fullUrl, {
|
|
88
102
|
method: 'GET',
|
|
89
103
|
headers: {
|
|
90
104
|
'Accept': 'application/json'
|
|
91
105
|
}
|
|
92
106
|
})
|
|
93
|
-
if (response.status == 'error') {
|
|
94
|
-
throw new ApiException(response.message)
|
|
95
|
-
}
|
|
96
|
-
return response
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/**
|
|
@@ -142,14 +152,6 @@ async function post<ResponseType>(url: string, body: Record<string, unknown> | F
|
|
|
142
152
|
// Event Streams
|
|
143
153
|
////////////////////////////////////////////////////////////////////////////////
|
|
144
154
|
|
|
145
|
-
/**
|
|
146
|
-
* Type of error events from a streaming response.
|
|
147
|
-
*/
|
|
148
|
-
export type ErrorEvent = {
|
|
149
|
-
prefix?: string
|
|
150
|
-
message: string
|
|
151
|
-
backtrace: string[]
|
|
152
|
-
}
|
|
153
155
|
|
|
154
156
|
/**
|
|
155
157
|
* Configure a `Streamer`.
|
|
@@ -158,7 +160,7 @@ export type StreamOptions = {
|
|
|
158
160
|
keepAlive?: boolean
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
type noArgListener = () => any
|
|
163
|
+
export type noArgListener = () => any
|
|
162
164
|
|
|
163
165
|
type StreamLifecycle = 'close'
|
|
164
166
|
|
|
@@ -172,13 +174,13 @@ export class Streamer {
|
|
|
172
174
|
close: []
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
constructor(readonly url: string, readonly options: StreamOptions) {
|
|
177
|
+
constructor(readonly url: string, readonly options: StreamOptions | undefined = undefined) {
|
|
176
178
|
this.sse = new EventSource(url)
|
|
177
179
|
|
|
178
180
|
// this is a special event sent by the ResponseStreamer on the server
|
|
179
181
|
// to tell us that the request is done
|
|
180
182
|
this.sse.addEventListener('_close', evt => {
|
|
181
|
-
if (!this.options
|
|
183
|
+
if (!this.options?.keepAlive) {
|
|
182
184
|
log.debug(`Closing Streamer at ${url}`, evt)
|
|
183
185
|
this.sse.close()
|
|
184
186
|
for (const listener of this.lifecycleListeners['close']) {
|
|
@@ -220,11 +222,10 @@ export class Streamer {
|
|
|
220
222
|
|
|
221
223
|
onClose(listener: noArgListener) {
|
|
222
224
|
this.lifecycleListeners['close'].push(listener)
|
|
225
|
+
return this
|
|
223
226
|
}
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
228
229
|
/**
|
|
229
230
|
* Creates a streaming response for the given endpoint.
|
|
230
231
|
* @param url
|
|
@@ -242,6 +243,7 @@ function stream(url: string, options: StreamOptions={}): Streamer {
|
|
|
242
243
|
|
|
243
244
|
const Api = {
|
|
244
245
|
safeGet,
|
|
246
|
+
get,
|
|
245
247
|
safePost,
|
|
246
248
|
post,
|
|
247
249
|
stream,
|