terrier-engine 4.14.0 → 4.14.2
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/package.json +1 -1
- package/terrier/api-subscriber.ts +121 -86
- package/terrier/api.ts +25 -0
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import Api, {ApiResponse, noArgListener, Streamer} from "./api"
|
|
|
2
2
|
import {LogEntry} from "./logging"
|
|
3
3
|
import dayjs from "dayjs"
|
|
4
4
|
import duration from "dayjs/plugin/duration"
|
|
5
|
+
import {Logger} from "tuff-core/logging";
|
|
5
6
|
|
|
6
7
|
dayjs.extend(duration)
|
|
7
8
|
|
|
@@ -11,8 +12,14 @@ export type SubscriptionOptions = {
|
|
|
11
12
|
keepAlive?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Options specific to Subscribers that use GET HTTP requests */
|
|
16
|
+
export type GetSubscriberOptions = {
|
|
17
|
+
/** If true, keys in the params object will be snakified before being sent to the server */
|
|
18
|
+
snakifyKeys?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/** Parameters to send with the subscription request */
|
|
15
|
-
export type SubscriptionParams = Record<string,
|
|
22
|
+
export type SubscriptionParams = Record<string, unknown>
|
|
16
23
|
|
|
17
24
|
export type SubscriptionEventHandlers<TResult> = {
|
|
18
25
|
onResult: resultListener<TResult>[]
|
|
@@ -67,10 +74,7 @@ export type CloseEvent = BaseSubscriptionEvent & { _type: '_close' }
|
|
|
67
74
|
* Each subclass is responsible for providing a constructor with whatever dependencies are required, but all subclasses
|
|
68
75
|
* must implement `params` and `options` properties and handle them appropriately.
|
|
69
76
|
*/
|
|
70
|
-
export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams> {
|
|
71
|
-
|
|
72
|
-
public abstract params: TParams
|
|
73
|
-
public abstract options?: SubscriptionOptions
|
|
77
|
+
export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams, TOptions extends SubscriptionOptions = SubscriptionOptions> {
|
|
74
78
|
|
|
75
79
|
protected isSubscribed: boolean = false
|
|
76
80
|
|
|
@@ -78,6 +82,17 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
|
|
|
78
82
|
protected lifecycleHandlers: SubscriptionLifecycleHandlers = { onSubscribe: [], onUnsubscribe: [], onClose: [] }
|
|
79
83
|
protected otherHandlers: Record<string, ((event: unknown) => boolean | void)[]> = {}
|
|
80
84
|
|
|
85
|
+
/**
|
|
86
|
+
* @param params parameters sent with subscription requests to customize server-side behavior
|
|
87
|
+
* @param options options that customize client-side behavior of the subscriber
|
|
88
|
+
* @param logger
|
|
89
|
+
* @protected
|
|
90
|
+
*/
|
|
91
|
+
protected constructor(public params: TParams, public options?: TOptions, public logger?: Logger) {
|
|
92
|
+
this.options ??= {} as TOptions
|
|
93
|
+
this.logger ??= new Logger(this.constructor.name)
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
// Handler registration
|
|
82
97
|
|
|
83
98
|
/** Register a handler for a custom event type */
|
|
@@ -147,9 +162,7 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
|
|
|
147
162
|
|
|
148
163
|
/** Stops the subscription and closes the connection. */
|
|
149
164
|
public unsubscribe(): void {
|
|
150
|
-
if (!this.isSubscribed)
|
|
151
|
-
throw new Error("Can't unsubscribe from a un-started subscription!")
|
|
152
|
-
}
|
|
165
|
+
if (!this.isSubscribed) return
|
|
153
166
|
this.unsubscribeImpl()
|
|
154
167
|
this.isSubscribed = false
|
|
155
168
|
this.notifyLifecycle('onUnsubscribe')
|
|
@@ -170,6 +183,7 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
|
|
|
170
183
|
protected notifyLifecycle(key: keyof SubscriptionLifecycleHandlers): void
|
|
171
184
|
protected notifyLifecycle(key: 'onClose', reason: string | undefined): void
|
|
172
185
|
protected notifyLifecycle(key: keyof SubscriptionLifecycleHandlers, reason: string | undefined = undefined): void {
|
|
186
|
+
this.logNotification(key, reason)
|
|
173
187
|
if (key == 'onClose') {
|
|
174
188
|
this.lifecycleHandlers[key].forEach(handler => handler(reason))
|
|
175
189
|
} else {
|
|
@@ -177,10 +191,47 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
|
|
|
177
191
|
}
|
|
178
192
|
}
|
|
179
193
|
|
|
194
|
+
protected notifyResult(event: ResultEvent<TResult>) {
|
|
195
|
+
this.logNotification('_result', event)
|
|
196
|
+
const shouldContinue = this.eventHandlers.onResult
|
|
197
|
+
.map(handler => handler(event))
|
|
198
|
+
.every(b => b)
|
|
199
|
+
if (!shouldContinue) this.unsubscribe()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
protected notifyError(event: ErrorEvent) {
|
|
203
|
+
this.logNotification('_error', event)
|
|
204
|
+
const shouldContinue = this.eventHandlers.onError
|
|
205
|
+
.map(handler => handler(event))
|
|
206
|
+
.every(b => b)
|
|
207
|
+
if (!shouldContinue) this.unsubscribe()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected notifyLog(event: LogEvent) {
|
|
211
|
+
this.logNotification('_log', event)
|
|
212
|
+
this.eventHandlers.onLog.forEach(handler => handler(event))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
protected notifyOther(key: string, event: any) {
|
|
216
|
+
this.logNotification(key, event)
|
|
217
|
+
const shouldContinue = this.otherHandlers[key]
|
|
218
|
+
.map(handler => handler(event) !== false)
|
|
219
|
+
.every(b => b)
|
|
220
|
+
if (!shouldContinue) this.unsubscribe()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
protected logNotification(key: string, event: any) {
|
|
224
|
+
if (event === undefined) {
|
|
225
|
+
this.logger?.debug('notify', key)
|
|
226
|
+
} else {
|
|
227
|
+
this.logger?.debug('notify', key, event)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
180
231
|
/** Subclasses implement this method to start the subscription */
|
|
181
232
|
protected abstract subscribeImpl(): void
|
|
182
233
|
|
|
183
|
-
/** Subclasses
|
|
234
|
+
/** Subclasses implement this method to end the subscription */
|
|
184
235
|
protected abstract unsubscribeImpl(): void
|
|
185
236
|
}
|
|
186
237
|
|
|
@@ -188,10 +239,12 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
|
|
|
188
239
|
// Polling Subscriber
|
|
189
240
|
////////////////////////////////////////////////////////////////////////////////
|
|
190
241
|
|
|
242
|
+
type PollingResponse<TResult> = (ApiResponse & SubscriptionEvent<TResult>) | { events: SubscriptionEvent<TResult>[] }
|
|
243
|
+
|
|
191
244
|
/**
|
|
192
245
|
* An implementation of ApiSubscriber that uses polling to periodically make a request to the server
|
|
193
246
|
*/
|
|
194
|
-
export class PollingSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams> {
|
|
247
|
+
export class PollingSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams, (SubscriptionOptions & GetSubscriberOptions)> {
|
|
195
248
|
|
|
196
249
|
// used to ensure we only schedule one interval at a time and allows for cancellation.
|
|
197
250
|
private timeoutHandle: NodeJS.Timeout | null = null
|
|
@@ -201,14 +254,16 @@ export class PollingSubscriber<TResult, TParams extends SubscriptionParams> exte
|
|
|
201
254
|
* @param params params to use to make the request.
|
|
202
255
|
* @param interval the interval on which to poll. Either a number of milliseconds or a dayjs Duration.
|
|
203
256
|
* @param options extra subscription options
|
|
257
|
+
* @param logger a specific logger to use
|
|
204
258
|
*/
|
|
205
259
|
constructor(
|
|
206
260
|
public url: string,
|
|
207
|
-
public params: TParams,
|
|
208
261
|
public interval: number | duration.Duration,
|
|
209
|
-
|
|
262
|
+
params: TParams,
|
|
263
|
+
options?: (SubscriptionOptions & GetSubscriberOptions),
|
|
264
|
+
logger?: Logger
|
|
210
265
|
) {
|
|
211
|
-
super()
|
|
266
|
+
super(params, options, logger)
|
|
212
267
|
}
|
|
213
268
|
|
|
214
269
|
subscribeImpl() {
|
|
@@ -228,62 +283,49 @@ export class PollingSubscriber<TResult, TParams extends SubscriptionParams> exte
|
|
|
228
283
|
private makeRequest() {
|
|
229
284
|
this.clearTimeout()
|
|
230
285
|
this.request().then(response => {
|
|
231
|
-
let shouldContinuePolling = true
|
|
232
286
|
if ('events' in response) {
|
|
233
287
|
for (const event of response.events) {
|
|
234
|
-
|
|
288
|
+
this.handleEvent(event)
|
|
235
289
|
}
|
|
236
290
|
} else {
|
|
237
291
|
if (response.status == 'error') response._type = '_error'
|
|
238
|
-
|
|
292
|
+
this.handleEvent(response)
|
|
239
293
|
}
|
|
240
294
|
|
|
241
|
-
|
|
242
|
-
this.startTimeout()
|
|
243
|
-
} else {
|
|
244
|
-
this.unsubscribe()
|
|
245
|
-
}
|
|
295
|
+
this.startTimeout()
|
|
246
296
|
})
|
|
247
297
|
}
|
|
248
298
|
|
|
249
|
-
protected request(): Promise<
|
|
250
|
-
|
|
251
|
-
}
|
|
299
|
+
protected async request(): Promise<PollingResponse<TResult>> {
|
|
300
|
+
const params = Api.objectToQueryParams(this.params, this.options?.snakifyKeys)
|
|
252
301
|
|
|
253
|
-
|
|
254
|
-
|
|
302
|
+
// Add _polling param so that the server-side can adjust behavior based on whether it is being polled or requested normally.
|
|
303
|
+
params._polling = true.toString()
|
|
304
|
+
const result = await Api.get<PollingResponse<TResult>>(this.url, params);
|
|
305
|
+
this.logger?.debug("polling request", params, result)
|
|
306
|
+
return result
|
|
307
|
+
}
|
|
255
308
|
|
|
309
|
+
protected handleEvent(event: SubscriptionEvent<TResult>) {
|
|
256
310
|
switch (event._type) {
|
|
257
311
|
case '_result':
|
|
258
312
|
case undefined:
|
|
259
|
-
|
|
260
|
-
const r = handler(event)
|
|
261
|
-
shouldContinuePolling &&= r
|
|
262
|
-
}
|
|
313
|
+
this.notifyResult(event)
|
|
263
314
|
break
|
|
264
315
|
case '_error':
|
|
265
|
-
|
|
266
|
-
const r = handler(event)
|
|
267
|
-
shouldContinuePolling &&= r
|
|
268
|
-
}
|
|
316
|
+
this.notifyError(event)
|
|
269
317
|
break
|
|
270
318
|
case '_log':
|
|
271
|
-
this.
|
|
319
|
+
this.notifyLog(event)
|
|
272
320
|
break
|
|
273
321
|
case '_close':
|
|
274
322
|
this.notifyLifecycle('onClose')
|
|
275
323
|
this.close()
|
|
276
|
-
shouldContinuePolling = this.options?.keepAlive ?? false
|
|
277
324
|
break
|
|
278
325
|
default:
|
|
279
326
|
const otherEvent = event as any
|
|
280
|
-
|
|
281
|
-
const r = handler(otherEvent) ?? true
|
|
282
|
-
shouldContinuePolling &&= r
|
|
283
|
-
}
|
|
327
|
+
this.notifyOther(otherEvent._type, otherEvent)
|
|
284
328
|
}
|
|
285
|
-
|
|
286
|
-
return shouldContinuePolling
|
|
287
329
|
}
|
|
288
330
|
|
|
289
331
|
protected clearTimeout() {
|
|
@@ -294,8 +336,12 @@ export class PollingSubscriber<TResult, TParams extends SubscriptionParams> exte
|
|
|
294
336
|
}
|
|
295
337
|
|
|
296
338
|
protected startTimeout() {
|
|
339
|
+
if (!this.isSubscribed) return
|
|
297
340
|
if (this.timeoutHandle) return
|
|
298
341
|
const intervalMs = (dayjs.isDuration(this.interval)) ? this.interval.asMilliseconds() : this.interval
|
|
342
|
+
|
|
343
|
+
this.logger?.debug("polling in", intervalMs, "milliseconds")
|
|
344
|
+
|
|
299
345
|
this.timeoutHandle = setTimeout(this.makeRequest.bind(this), intervalMs)
|
|
300
346
|
}
|
|
301
347
|
}
|
|
@@ -312,11 +358,12 @@ export class CableSubscriber<TResult, TParams extends SubscriptionParams> extend
|
|
|
312
358
|
|
|
313
359
|
constructor(
|
|
314
360
|
public channelName: string,
|
|
315
|
-
|
|
316
|
-
|
|
361
|
+
params: TParams,
|
|
362
|
+
options?: SubscriptionOptions,
|
|
363
|
+
logger?: Logger,
|
|
317
364
|
) {
|
|
318
365
|
throw new Error("Method not implemented.")
|
|
319
|
-
super()
|
|
366
|
+
super(params, options, logger)
|
|
320
367
|
}
|
|
321
368
|
|
|
322
369
|
public updateParams(_newParams: TParams): void {
|
|
@@ -334,71 +381,59 @@ export class CableSubscriber<TResult, TParams extends SubscriptionParams> extend
|
|
|
334
381
|
// Event Stream Subscriber
|
|
335
382
|
////////////////////////////////////////////////////////////////////////////////
|
|
336
383
|
|
|
337
|
-
export class StreamingSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams> {
|
|
384
|
+
export class StreamingSubscriber<TResult, TParams extends SubscriptionParams> extends ApiSubscriber<TResult, TParams, (SubscriptionOptions & GetSubscriberOptions)> {
|
|
338
385
|
|
|
339
386
|
private streamer?: Streamer
|
|
340
387
|
|
|
341
388
|
constructor(
|
|
342
389
|
public url: string,
|
|
343
|
-
|
|
344
|
-
|
|
390
|
+
params: TParams,
|
|
391
|
+
options?: (SubscriptionOptions & GetSubscriberOptions),
|
|
392
|
+
logger?: Logger
|
|
345
393
|
) {
|
|
346
|
-
super()
|
|
394
|
+
super(params, options, logger)
|
|
347
395
|
}
|
|
348
396
|
|
|
349
397
|
public on<EventType>(eventType: string, handler: (event: EventType) => boolean | void): this {
|
|
350
|
-
this.streamer
|
|
398
|
+
if (!(eventType in this.otherHandlers) && this.streamer) {
|
|
399
|
+
this.listenOther(eventType, this.streamer)
|
|
400
|
+
}
|
|
351
401
|
return super.on(eventType, handler)
|
|
352
402
|
}
|
|
353
403
|
|
|
354
|
-
public
|
|
355
|
-
this.
|
|
356
|
-
|
|
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)
|
|
404
|
+
public updateParams(newParams: TParams): void {
|
|
405
|
+
this.unsubscribeImpl()
|
|
406
|
+
this.params = newParams
|
|
407
|
+
this.subscribeImpl()
|
|
367
408
|
}
|
|
368
409
|
|
|
369
410
|
protected subscribeImpl(): void {
|
|
370
|
-
const
|
|
411
|
+
const params = Api.objectToQueryParams(this.params, this.options?.snakifyKeys)
|
|
412
|
+
const paramsString = new URLSearchParams(params).toString()
|
|
371
413
|
const streamer = new Streamer(`${this.url}?${paramsString}`, this.options ?? {})
|
|
372
414
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
415
|
+
streamer
|
|
416
|
+
.on<ResultEvent<TResult>>('_result', this.notifyResult.bind(this))
|
|
417
|
+
.on<ErrorEvent>('_error', this.notifyError.bind(this))
|
|
418
|
+
.on<LogEvent>('_log', this.notifyLog.bind(this))
|
|
419
|
+
.onClose(() => {
|
|
420
|
+
this.close()
|
|
421
|
+
this.notifyLifecycle('onClose')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
Object.keys(this.otherHandlers).forEach(key => {
|
|
425
|
+
this.listenOther(key, streamer)
|
|
379
426
|
})
|
|
380
|
-
Object.entries(this.otherHandlers)
|
|
381
|
-
.forEach(([key, handlers]) =>
|
|
382
|
-
handlers.forEach(handler => streamer.on(key, this.wrapHandler(handler)))
|
|
383
|
-
)
|
|
384
427
|
|
|
385
428
|
this.streamer = streamer
|
|
386
429
|
}
|
|
430
|
+
|
|
387
431
|
protected unsubscribeImpl(): void {
|
|
388
432
|
this.streamer?.sse.close()
|
|
389
433
|
this.streamer = undefined
|
|
390
434
|
}
|
|
391
435
|
|
|
392
|
-
|
|
393
|
-
this.
|
|
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
|
-
}
|
|
436
|
+
protected listenOther(key: string, streamer: Streamer) {
|
|
437
|
+
streamer.on(key, (event) => this.notifyOther(key, event))
|
|
403
438
|
}
|
|
404
|
-
}
|
|
439
|
+
}
|
package/terrier/api.ts
CHANGED
|
@@ -2,6 +2,8 @@ import {Logger} from "tuff-core/logging"
|
|
|
2
2
|
import {QueryParams} from "tuff-core/urls"
|
|
3
3
|
import {LogEntry} from "./logging"
|
|
4
4
|
import {ErrorEvent} from "./api-subscriber"
|
|
5
|
+
import Strings from "tuff-core/strings";
|
|
6
|
+
import dayjs from "dayjs";
|
|
5
7
|
|
|
6
8
|
const log = new Logger('Api')
|
|
7
9
|
|
|
@@ -147,6 +149,28 @@ async function post<ResponseType>(url: string, body: Record<string, unknown> | F
|
|
|
147
149
|
return await request<ResponseType & ApiResponse>(url, config)
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
153
|
+
// Query Params
|
|
154
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Transforms the given object to a QueryParams compatible object.
|
|
158
|
+
*/
|
|
159
|
+
function objectToQueryParams(object: Record<string, unknown>, snakifyKeys: boolean = false): Record<string, string> {
|
|
160
|
+
const raw: Record<string, string> = {}
|
|
161
|
+
for (const [key, value] of Object.entries(object)) {
|
|
162
|
+
const paramKey = snakifyKeys ? Strings.splitWords(key).map(w => w.toLowerCase()).join("_") : key
|
|
163
|
+
|
|
164
|
+
if (dayjs.isDayjs(value)) {
|
|
165
|
+
raw[paramKey] = value.format()
|
|
166
|
+
} else if (value == undefined) {
|
|
167
|
+
raw[paramKey] = ""
|
|
168
|
+
} else {
|
|
169
|
+
raw[paramKey] = value?.toString()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return raw
|
|
173
|
+
}
|
|
150
174
|
|
|
151
175
|
////////////////////////////////////////////////////////////////////////////////
|
|
152
176
|
// Event Streams
|
|
@@ -248,5 +272,6 @@ const Api = {
|
|
|
248
272
|
post,
|
|
249
273
|
stream,
|
|
250
274
|
addRequestDecorator,
|
|
275
|
+
objectToQueryParams,
|
|
251
276
|
}
|
|
252
277
|
export default Api
|