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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.14.0",
7
+ "version": "4.14.2",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -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, 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 imlpement this method to end the subscription */
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
- public options: SubscriptionOptions | undefined = undefined
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
- shouldContinuePolling &&= this.handleEvent(event)
288
+ this.handleEvent(event)
235
289
  }
236
290
  } else {
237
291
  if (response.status == 'error') response._type = '_error'
238
- shouldContinuePolling = this.handleEvent(response)
292
+ this.handleEvent(response)
239
293
  }
240
294
 
241
- if (shouldContinuePolling) {
242
- this.startTimeout()
243
- } else {
244
- this.unsubscribe()
245
- }
295
+ this.startTimeout()
246
296
  })
247
297
  }
248
298
 
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
- }
299
+ protected async request(): Promise<PollingResponse<TResult>> {
300
+ const params = Api.objectToQueryParams(this.params, this.options?.snakifyKeys)
252
301
 
253
- protected handleEvent(event: SubscriptionEvent<TResult>): boolean {
254
- let shouldContinuePolling = true
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
- for (const handler of this.eventHandlers.onResult) {
260
- const r = handler(event)
261
- shouldContinuePolling &&= r
262
- }
313
+ this.notifyResult(event)
263
314
  break
264
315
  case '_error':
265
- for (const handler of this.eventHandlers.onError) {
266
- const r = handler(event)
267
- shouldContinuePolling &&= r
268
- }
316
+ this.notifyError(event)
269
317
  break
270
318
  case '_log':
271
- this.eventHandlers.onLog.forEach(handler => handler(event))
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
- for (const handler of this.otherHandlers[otherEvent._type]) {
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
- public params: TParams,
316
- public options: SubscriptionOptions | undefined = undefined
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
- public params: TParams,
344
- public options: SubscriptionOptions | undefined = undefined,
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?.on(eventType, handler)
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 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)
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 paramsString = new URLSearchParams(this.params).toString()
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
- 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()
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
- 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
- }
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