terrier-engine 4.14.1 → 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.1",
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
 
@@ -73,10 +74,7 @@ export type CloseEvent = BaseSubscriptionEvent & { _type: '_close' }
73
74
  * Each subclass is responsible for providing a constructor with whatever dependencies are required, but all subclasses
74
75
  * must implement `params` and `options` properties and handle them appropriately.
75
76
  */
76
- export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams> {
77
-
78
- public abstract params: TParams
79
- public abstract options?: SubscriptionOptions
77
+ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams, TOptions extends SubscriptionOptions = SubscriptionOptions> {
80
78
 
81
79
  protected isSubscribed: boolean = false
82
80
 
@@ -84,6 +82,17 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
84
82
  protected lifecycleHandlers: SubscriptionLifecycleHandlers = { onSubscribe: [], onUnsubscribe: [], onClose: [] }
85
83
  protected otherHandlers: Record<string, ((event: unknown) => boolean | void)[]> = {}
86
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
+
87
96
  // Handler registration
88
97
 
89
98
  /** Register a handler for a custom event type */
@@ -153,9 +162,7 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
153
162
 
154
163
  /** Stops the subscription and closes the connection. */
155
164
  public unsubscribe(): void {
156
- if (!this.isSubscribed) {
157
- throw new Error("Can't unsubscribe from a un-started subscription!")
158
- }
165
+ if (!this.isSubscribed) return
159
166
  this.unsubscribeImpl()
160
167
  this.isSubscribed = false
161
168
  this.notifyLifecycle('onUnsubscribe')
@@ -176,6 +183,7 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
176
183
  protected notifyLifecycle(key: keyof SubscriptionLifecycleHandlers): void
177
184
  protected notifyLifecycle(key: 'onClose', reason: string | undefined): void
178
185
  protected notifyLifecycle(key: keyof SubscriptionLifecycleHandlers, reason: string | undefined = undefined): void {
186
+ this.logNotification(key, reason)
179
187
  if (key == 'onClose') {
180
188
  this.lifecycleHandlers[key].forEach(handler => handler(reason))
181
189
  } else {
@@ -183,10 +191,47 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
183
191
  }
184
192
  }
185
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
+
186
231
  /** Subclasses implement this method to start the subscription */
187
232
  protected abstract subscribeImpl(): void
188
233
 
189
- /** Subclasses imlpement this method to end the subscription */
234
+ /** Subclasses implement this method to end the subscription */
190
235
  protected abstract unsubscribeImpl(): void
191
236
  }
192
237
 
@@ -194,10 +239,12 @@ export abstract class ApiSubscriber<TResult, TParams extends SubscriptionParams>
194
239
  // Polling Subscriber
195
240
  ////////////////////////////////////////////////////////////////////////////////
196
241
 
242
+ type PollingResponse<TResult> = (ApiResponse & SubscriptionEvent<TResult>) | { events: SubscriptionEvent<TResult>[] }
243
+
197
244
  /**
198
245
  * An implementation of ApiSubscriber that uses polling to periodically make a request to the server
199
246
  */
200
- 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)> {
201
248
 
202
249
  // used to ensure we only schedule one interval at a time and allows for cancellation.
203
250
  private timeoutHandle: NodeJS.Timeout | null = null
@@ -207,14 +254,16 @@ export class PollingSubscriber<TResult, TParams extends SubscriptionParams> exte
207
254
  * @param params params to use to make the request.
208
255
  * @param interval the interval on which to poll. Either a number of milliseconds or a dayjs Duration.
209
256
  * @param options extra subscription options
257
+ * @param logger a specific logger to use
210
258
  */
211
259
  constructor(
212
260
  public url: string,
213
- public params: TParams,
214
261
  public interval: number | duration.Duration,
215
- public options: (SubscriptionOptions & GetSubscriberOptions) | undefined = undefined
262
+ params: TParams,
263
+ options?: (SubscriptionOptions & GetSubscriberOptions),
264
+ logger?: Logger
216
265
  ) {
217
- super()
266
+ super(params, options, logger)
218
267
  }
219
268
 
220
269
  subscribeImpl() {
@@ -234,63 +283,49 @@ export class PollingSubscriber<TResult, TParams extends SubscriptionParams> exte
234
283
  private makeRequest() {
235
284
  this.clearTimeout()
236
285
  this.request().then(response => {
237
- let shouldContinuePolling = true
238
286
  if ('events' in response) {
239
287
  for (const event of response.events) {
240
- shouldContinuePolling &&= this.handleEvent(event)
288
+ this.handleEvent(event)
241
289
  }
242
290
  } else {
243
291
  if (response.status == 'error') response._type = '_error'
244
- shouldContinuePolling = this.handleEvent(response)
292
+ this.handleEvent(response)
245
293
  }
246
294
 
247
- if (shouldContinuePolling) {
248
- this.startTimeout()
249
- } else {
250
- this.unsubscribe()
251
- }
295
+ this.startTimeout()
252
296
  })
253
297
  }
254
298
 
255
- protected request(): Promise<((ApiResponse & SubscriptionEvent<TResult>) | { events: SubscriptionEvent<TResult>[] })> {
299
+ protected async request(): Promise<PollingResponse<TResult>> {
256
300
  const params = Api.objectToQueryParams(this.params, this.options?.snakifyKeys)
257
- return Api.get<((ApiResponse & SubscriptionEvent<TResult>) | { events: SubscriptionEvent<TResult>[] })>(this.url, params)
258
- }
259
301
 
260
- protected handleEvent(event: SubscriptionEvent<TResult>): boolean {
261
- 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
+ }
262
308
 
309
+ protected handleEvent(event: SubscriptionEvent<TResult>) {
263
310
  switch (event._type) {
264
311
  case '_result':
265
312
  case undefined:
266
- for (const handler of this.eventHandlers.onResult) {
267
- const r = handler(event)
268
- shouldContinuePolling &&= r
269
- }
313
+ this.notifyResult(event)
270
314
  break
271
315
  case '_error':
272
- for (const handler of this.eventHandlers.onError) {
273
- const r = handler(event)
274
- shouldContinuePolling &&= r
275
- }
316
+ this.notifyError(event)
276
317
  break
277
318
  case '_log':
278
- this.eventHandlers.onLog.forEach(handler => handler(event))
319
+ this.notifyLog(event)
279
320
  break
280
321
  case '_close':
281
322
  this.notifyLifecycle('onClose')
282
323
  this.close()
283
- shouldContinuePolling = this.options?.keepAlive ?? false
284
324
  break
285
325
  default:
286
326
  const otherEvent = event as any
287
- for (const handler of this.otherHandlers[otherEvent._type]) {
288
- const r = handler(otherEvent) ?? true
289
- shouldContinuePolling &&= r
290
- }
327
+ this.notifyOther(otherEvent._type, otherEvent)
291
328
  }
292
-
293
- return shouldContinuePolling
294
329
  }
295
330
 
296
331
  protected clearTimeout() {
@@ -301,8 +336,12 @@ export class PollingSubscriber<TResult, TParams extends SubscriptionParams> exte
301
336
  }
302
337
 
303
338
  protected startTimeout() {
339
+ if (!this.isSubscribed) return
304
340
  if (this.timeoutHandle) return
305
341
  const intervalMs = (dayjs.isDuration(this.interval)) ? this.interval.asMilliseconds() : this.interval
342
+
343
+ this.logger?.debug("polling in", intervalMs, "milliseconds")
344
+
306
345
  this.timeoutHandle = setTimeout(this.makeRequest.bind(this), intervalMs)
307
346
  }
308
347
  }
@@ -319,11 +358,12 @@ export class CableSubscriber<TResult, TParams extends SubscriptionParams> extend
319
358
 
320
359
  constructor(
321
360
  public channelName: string,
322
- public params: TParams,
323
- public options: SubscriptionOptions | undefined = undefined
361
+ params: TParams,
362
+ options?: SubscriptionOptions,
363
+ logger?: Logger,
324
364
  ) {
325
365
  throw new Error("Method not implemented.")
326
- super()
366
+ super(params, options, logger)
327
367
  }
328
368
 
329
369
  public updateParams(_newParams: TParams): void {
@@ -341,72 +381,59 @@ export class CableSubscriber<TResult, TParams extends SubscriptionParams> extend
341
381
  // Event Stream Subscriber
342
382
  ////////////////////////////////////////////////////////////////////////////////
343
383
 
344
- 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)> {
345
385
 
346
386
  private streamer?: Streamer
347
387
 
348
388
  constructor(
349
389
  public url: string,
350
- public params: TParams,
351
- public options: (SubscriptionOptions & GetSubscriberOptions) | undefined = undefined,
390
+ params: TParams,
391
+ options?: (SubscriptionOptions & GetSubscriberOptions),
392
+ logger?: Logger
352
393
  ) {
353
- super()
394
+ super(params, options, logger)
354
395
  }
355
396
 
356
397
  public on<EventType>(eventType: string, handler: (event: EventType) => boolean | void): this {
357
- this.streamer?.on(eventType, handler)
398
+ if (!(eventType in this.otherHandlers) && this.streamer) {
399
+ this.listenOther(eventType, this.streamer)
400
+ }
358
401
  return super.on(eventType, handler)
359
402
  }
360
403
 
361
- public onResult(handler: (result: ResultEvent<TResult>) => boolean): this {
362
- this.streamer?.on<ResultEvent<TResult>>('_result', handler)
363
- return super.onResult(handler)
364
- }
365
-
366
- public onError(handler: (error: ErrorEvent) => boolean): this {
367
- this.streamer?.on<ErrorEvent>('_error', handler)
368
- return super.onError(handler)
369
- }
370
-
371
- public onLog(handler: (logEntry: LogEvent) => void): this {
372
- this.streamer?.on<LogEvent>('_log', handler)
373
- return super.onLog(handler)
404
+ public updateParams(newParams: TParams): void {
405
+ this.unsubscribeImpl()
406
+ this.params = newParams
407
+ this.subscribeImpl()
374
408
  }
375
409
 
376
410
  protected subscribeImpl(): void {
377
411
  const params = Api.objectToQueryParams(this.params, this.options?.snakifyKeys)
378
- const fullUrl = new URLSearchParams(params).toString()
379
- const streamer = new Streamer(fullUrl, this.options ?? {})
380
-
381
- this.eventHandlers.onResult.forEach(handler => streamer.on<ResultEvent<TResult>>('_result', this.wrapHandler(handler)))
382
- this.eventHandlers.onError.forEach(handler => streamer.on<ErrorEvent>('_error', this.wrapHandler(handler)))
383
- this.eventHandlers.onLog.forEach(handler => streamer.on<LogEvent>('_log', handler))
384
- this.lifecycleHandlers.onClose.forEach(handler => streamer.onClose(handler))
385
- streamer.onClose(() => {
386
- this.close()
412
+ const paramsString = new URLSearchParams(params).toString()
413
+ const streamer = new Streamer(`${this.url}?${paramsString}`, this.options ?? {})
414
+
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)
387
426
  })
388
- Object.entries(this.otherHandlers)
389
- .forEach(([key, handlers]) =>
390
- handlers.forEach(handler => streamer.on(key, this.wrapHandler(handler)))
391
- )
392
427
 
393
428
  this.streamer = streamer
394
429
  }
430
+
395
431
  protected unsubscribeImpl(): void {
396
432
  this.streamer?.sse.close()
397
433
  this.streamer = undefined
398
434
  }
399
435
 
400
- public updateParams(newParams: TParams): void {
401
- this.unsubscribeImpl()
402
- this.params = newParams
403
- this.subscribeImpl()
404
- }
405
-
406
- private wrapHandler<T>(handler: (param: T) => boolean | void): (param: T) => void {
407
- return (param: T) => {
408
- const shouldContinue = handler(param) ?? true
409
- if (!shouldContinue) this.unsubscribe()
410
- }
436
+ protected listenOther(key: string, streamer: Streamer) {
437
+ streamer.on(key, (event) => this.notifyOther(key, event))
411
438
  }
412
- }
439
+ }