@haathie/pgmb 0.2.6 → 0.2.8

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/src/client.ts ADDED
@@ -0,0 +1,704 @@
1
+ import assert from 'assert'
2
+ import { type Logger, pino } from 'pino'
3
+ import { setTimeout } from 'timers/promises'
4
+ import { AbortableAsyncIterator } from './abortable-async-iterator.ts'
5
+ import { PGMBEventBatcher } from './batcher.ts'
6
+ import {
7
+ assertGroup,
8
+ assertSubscription,
9
+ deleteSubscriptions,
10
+ getConfigValue,
11
+ maintainEventsTable,
12
+ markSubscriptionsActive,
13
+ pollForEvents,
14
+ readNextEvents as defaultReadNextEvents,
15
+ releaseGroupLock,
16
+ removeExpiredSubscriptions,
17
+ setGroupCursor,
18
+ writeEvents,
19
+ } from './queries.ts'
20
+ import type { PgClientLike, PgReleasableClient } from './query-types.ts'
21
+ import {
22
+ createRetryHandler,
23
+ normaliseRetryEventsInReadEventMap,
24
+ } from './retry-handler.ts'
25
+ import type {
26
+ GetWebhookInfoFn,
27
+ IEphemeralListener,
28
+ IEventData,
29
+ IEventHandler,
30
+ IFindEventsFn,
31
+ IReadEvent,
32
+ IReadNextEventsFn,
33
+ ISplitFn,
34
+ Pgmb2ClientOpts,
35
+ registerReliableHandlerParams,
36
+ RegisterSubscriptionParams,
37
+ } from './types.ts'
38
+ import { getEnvNumber } from './utils.ts'
39
+ import { createWebhookHandler } from './webhook-handler.ts'
40
+
41
+ type IReliableListener<T extends IEventData> = {
42
+ type: 'reliable'
43
+ handler: IEventHandler<T>
44
+ removeOnEmpty?: boolean
45
+ extra?: unknown
46
+ splitBy?: ISplitFn<T>
47
+ queue: {
48
+ item: IReadEvent<T>
49
+ checkpoint: Checkpoint
50
+ }[]
51
+ };
52
+
53
+ type IFireAndForgetListener<T extends IEventData> = {
54
+ type: 'fire-and-forget'
55
+ stream: IEphemeralListener<T>
56
+ };
57
+
58
+ type IListener<T extends IEventData> =
59
+ | IFireAndForgetListener<T>
60
+ | IReliableListener<T>;
61
+
62
+ type Checkpoint = {
63
+ activeTasks: number
64
+ nextCursor: string
65
+ cancelled?: boolean
66
+ };
67
+
68
+ export type IListenerStore<T extends IEventData> = {
69
+ values: { [id: string]: IListener<T> }
70
+ };
71
+
72
+ export class PgmbClient<
73
+ T extends IEventData = IEventData,
74
+ > extends PGMBEventBatcher<T> {
75
+ readonly client: PgClientLike
76
+ readonly logger: Logger
77
+ readonly groupId: string
78
+ readonly readEventsIntervalMs: number
79
+ readonly pollEventsIntervalMs: number
80
+ readonly readChunkSize: number
81
+ readonly subscriptionMaintenanceMs: number
82
+ readonly tableMaintenanceMs: number
83
+ readonly maxActiveCheckpoints: number
84
+ readonly readNextEvents: IReadNextEventsFn
85
+ readonly findEvents?: IFindEventsFn
86
+
87
+ readonly getWebhookInfo: GetWebhookInfoFn
88
+ readonly webhookHandler: IEventHandler<T>
89
+
90
+ readonly listeners: { [subId: string]: IListenerStore<T> } = {}
91
+
92
+ readonly #webhookHandlerOpts: Partial<{ splitBy?: ISplitFn<T> }>
93
+
94
+ #readClient?: PgReleasableClient
95
+
96
+ #endAc = new AbortController()
97
+
98
+ #readTask?: Promise<void>
99
+ #pollTask?: Promise<void>
100
+ #subMaintainTask?: Promise<void>
101
+ #tableMaintainTask?: Promise<void>
102
+
103
+ #inMemoryCursor: string | null = null
104
+ #activeCheckpoints: Checkpoint[] = []
105
+
106
+ constructor({
107
+ client,
108
+ groupId,
109
+ logger = pino(),
110
+ readEventsIntervalMs = getEnvNumber('PGMB_READ_EVENTS_INTERVAL_MS', 1000),
111
+ readChunkSize = getEnvNumber('PGMB_READ_CHUNK_SIZE', 1000),
112
+ maxActiveCheckpoints = getEnvNumber('PGMB_MAX_ACTIVE_CHECKPOINTS', 10),
113
+ pollEventsIntervalMs = getEnvNumber('PGMB_POLL_EVENTS_INTERVAL_MS', 1000),
114
+ subscriptionMaintenanceMs
115
+ = getEnvNumber('PGMB_SUBSCRIPTION_MAINTENANCE_S', 60) * 1000,
116
+ tableMaintainanceMs
117
+ = getEnvNumber('PGMB_TABLE_MAINTENANCE_M', 15) * 60 * 1000,
118
+ webhookHandlerOpts: {
119
+ splitBy: whSplitBy,
120
+ ...whHandlerOpts
121
+ } = {},
122
+ getWebhookInfo = () => ({}),
123
+ readNextEvents = defaultReadNextEvents.run.bind(defaultReadNextEvents),
124
+ findEvents,
125
+ ...batcherOpts
126
+ }: Pgmb2ClientOpts<T>) {
127
+ super({
128
+ ...batcherOpts,
129
+ logger,
130
+ publish: (...e) => this.publish(e),
131
+ })
132
+ this.client = client
133
+ this.logger = logger
134
+ this.groupId = groupId
135
+ this.readEventsIntervalMs = readEventsIntervalMs
136
+ this.readChunkSize = readChunkSize
137
+ this.pollEventsIntervalMs = pollEventsIntervalMs
138
+ this.subscriptionMaintenanceMs = subscriptionMaintenanceMs
139
+ this.maxActiveCheckpoints = maxActiveCheckpoints
140
+ this.webhookHandler = createWebhookHandler<T>(whHandlerOpts)
141
+ this.#webhookHandlerOpts = { splitBy: whSplitBy }
142
+ this.getWebhookInfo = getWebhookInfo
143
+ this.tableMaintenanceMs = tableMaintainanceMs
144
+ this.readNextEvents = readNextEvents
145
+ this.findEvents = findEvents
146
+ }
147
+
148
+ async init() {
149
+ this.#endAc = new AbortController()
150
+
151
+ if('connect' in this.client) {
152
+ this.client.on('remove', this.#onPoolClientRemoved)
153
+ }
154
+
155
+ const [pgCronRslt] = await getConfigValue
156
+ .run({ key: 'use_pg_cron' }, this.client)
157
+ const isPgCronEnabled = pgCronRslt?.value === 'true'
158
+ if(!isPgCronEnabled) {
159
+ // maintain event table
160
+ await maintainEventsTable.run(undefined, this.client)
161
+ this.logger.debug('maintained events table')
162
+
163
+ if(this.pollEventsIntervalMs) {
164
+ this.#pollTask = this.#startLoop(
165
+ pollForEvents.run.bind(pollForEvents, undefined, this.client),
166
+ this.pollEventsIntervalMs,
167
+ )
168
+ }
169
+
170
+ if(this.tableMaintenanceMs) {
171
+ this.#tableMaintainTask = this.#startLoop(
172
+ maintainEventsTable.run
173
+ .bind(maintainEventsTable, undefined, this.client),
174
+ this.tableMaintenanceMs,
175
+ )
176
+ }
177
+ }
178
+
179
+ await assertGroup.run({ id: this.groupId }, this.client)
180
+ this.logger.debug({ groupId: this.groupId }, 'asserted group exists')
181
+ // clean up expired subscriptions on start
182
+ const [{ deleted }] = await removeExpiredSubscriptions.run(
183
+ { groupId: this.groupId, activeIds: [] },
184
+ this.client,
185
+ )
186
+ this.logger.debug({ deleted }, 'removed expired subscriptions')
187
+
188
+ this.#readTask = this.#startLoop(
189
+ this.readChanges.bind(this),
190
+ this.readEventsIntervalMs,
191
+ )
192
+
193
+ if(this.subscriptionMaintenanceMs) {
194
+ this.#subMaintainTask = this.#startLoop(
195
+ this.#maintainSubscriptions,
196
+ this.subscriptionMaintenanceMs,
197
+ )
198
+ }
199
+
200
+ this.logger.info({ isPgCronEnabled }, 'pgmb client initialised')
201
+ }
202
+
203
+ async end() {
204
+ await super.end()
205
+
206
+ this.#endAc.abort()
207
+
208
+ while(this.#activeCheckpoints.length) {
209
+ await setTimeout(100)
210
+ }
211
+
212
+ for(const id in this.listeners) {
213
+ delete this.listeners[id]
214
+ }
215
+
216
+ await Promise.all([
217
+ this.#readTask,
218
+ this.#pollTask,
219
+ this.#subMaintainTask,
220
+ this.#tableMaintainTask,
221
+ ])
222
+
223
+ await this.#unlockAndReleaseReadClient()
224
+ this.#readTask = undefined
225
+ this.#pollTask = undefined
226
+ this.#subMaintainTask = undefined
227
+ this.#activeCheckpoints = []
228
+ }
229
+
230
+ publish(events: T[], client = this.client) {
231
+ return writeEvents.run(
232
+ {
233
+ topics: events.map((e) => e.topic),
234
+ payloads: events.map((e) => e.payload),
235
+ metadatas: events.map((e) => e.metadata || null),
236
+ },
237
+ client,
238
+ )
239
+ }
240
+
241
+ async assertSubscription(
242
+ opts: RegisterSubscriptionParams,
243
+ client = this.client,
244
+ ) {
245
+ const [rslt] = await assertSubscription.run(
246
+ { ...opts, groupId: this.groupId },
247
+ client,
248
+ )
249
+
250
+ this.logger.debug({ ...opts, ...rslt }, 'asserted subscription')
251
+ return rslt
252
+ }
253
+
254
+ /**
255
+ * Registers a fire-and-forget handler, returning an async iterator
256
+ * that yields events as they arrive. The client does not wait for event
257
+ * processing acknowledgements. Useful for cases where data is eventually
258
+ * consistent, or when event delivery isn't critical
259
+ * (eg. http SSE, websockets).
260
+ */
261
+ async registerFireAndForgetHandler(opts: RegisterSubscriptionParams) {
262
+ const { id: subId } = await this.assertSubscription(opts)
263
+ return this.#listenForEvents(subId)
264
+ }
265
+
266
+ /**
267
+ * Registers a reliable handler for the given subscription params.
268
+ * If the handler throws an error, client will rollback to the last known
269
+ * good cursor, and re-deliver events.
270
+ * To avoid a full redelivery of a batch, a retry strategy can be provided
271
+ * to retry failed events by the handler itself, allowing for delayed retries
272
+ * with backoff, and without disrupting the overall event flow.
273
+ */
274
+ async registerReliableHandler(
275
+ {
276
+ retryOpts,
277
+ name = createListenerId(),
278
+ splitBy,
279
+ ...opts
280
+ }: registerReliableHandlerParams<T>,
281
+ handler: IEventHandler<T>,
282
+ ) {
283
+ const { id: subId } = await this.assertSubscription(opts)
284
+ if(retryOpts) {
285
+ handler = createRetryHandler(retryOpts, handler)
286
+ }
287
+
288
+ const lts = (this.listeners[subId] ||= { values: {} })
289
+ assert(
290
+ !lts.values[name],
291
+ `Handler with id ${name} already registered for subscription ${subId}.` +
292
+ ' Cancel the existing one or use a different id.',
293
+ )
294
+ this.listeners[subId].values[name] = {
295
+ type: 'reliable',
296
+ handler,
297
+ splitBy,
298
+ queue: [],
299
+ }
300
+
301
+ return {
302
+ subscriptionId: subId,
303
+ cancel: () => this.#removeListener(subId, name),
304
+ }
305
+ }
306
+
307
+ async removeSubscription(subId: string) {
308
+ await deleteSubscriptions.run({ ids: [subId] }, this.client)
309
+ this.logger.debug({ subId }, 'deleted subscription')
310
+
311
+ const existingSubs = this.listeners[subId]?.values
312
+ delete this.listeners[subId]
313
+ if(!existingSubs) {
314
+ return
315
+ }
316
+
317
+ await Promise.allSettled(
318
+ Object.values(existingSubs).map(
319
+ (e) => e.type === 'fire-and-forget' &&
320
+ e.stream.throw(new Error('subscription removed')),
321
+ ),
322
+ )
323
+ }
324
+
325
+ #listenForEvents(subId: string): IEphemeralListener<T> {
326
+ const lid = createListenerId()
327
+ const iterator = new AbortableAsyncIterator<IReadEvent<T>>(
328
+ this.#endAc.signal,
329
+ () => this.#removeListener(subId, lid),
330
+ )
331
+
332
+ const stream = iterator as IEphemeralListener<T>
333
+ stream.id = subId
334
+
335
+ this.listeners[subId] ||= { values: {} }
336
+ this.listeners[subId].values[lid] = { type: 'fire-and-forget', stream }
337
+
338
+ return stream
339
+ }
340
+
341
+ #removeListener(subId: string, lid: string) {
342
+ const existingSubs = this.listeners[subId]?.values
343
+ delete existingSubs?.[lid]
344
+ if(existingSubs && Object.keys(existingSubs).length) {
345
+ return
346
+ }
347
+
348
+ delete this.listeners[subId]
349
+ this.logger.debug({ subId }, 'removed last subscriber for sub')
350
+ }
351
+
352
+ async #maintainSubscriptions() {
353
+ const activeIds = Object.keys(this.listeners)
354
+ await markSubscriptionsActive.run({ ids: activeIds }, this.client)
355
+
356
+ this.logger.trace(
357
+ { activeSubscriptions: activeIds.length },
358
+ 'marked subscriptions as active',
359
+ )
360
+
361
+ const [{ deleted }] = await removeExpiredSubscriptions.run(
362
+ { groupId: this.groupId, activeIds },
363
+ this.client,
364
+ )
365
+ this.logger.trace({ deleted }, 'removed expired subscriptions')
366
+ }
367
+
368
+ async readChanges() {
369
+ if(this.#activeCheckpoints.length >= this.maxActiveCheckpoints) {
370
+ return 0
371
+ }
372
+
373
+ const now = Date.now()
374
+ await this.#connectReadClient()
375
+ const rows = await this.readNextEvents(
376
+ {
377
+ groupId: this.groupId,
378
+ cursor: this.#inMemoryCursor,
379
+ chunkSize: this.readChunkSize,
380
+ },
381
+ this.#readClient || this.client,
382
+ )
383
+ .catch(async(err) => {
384
+ if(err instanceof Error && err.message.includes('connection error')) {
385
+ await this.#unlockAndReleaseReadClient()
386
+ }
387
+
388
+ throw err
389
+ })
390
+ if(!rows.length) {
391
+ // if nothing is happening and there are no active checkpoints,
392
+ // we can just let the read client go
393
+ if(!this.#activeCheckpoints.length) {
394
+ await this.#unlockAndReleaseReadClient()
395
+ }
396
+
397
+ return 0
398
+ }
399
+
400
+ const uqSubIds = Array.from(
401
+ new Set(rows.flatMap((r) => r.subscriptionIds)),
402
+ )
403
+ const webhookSubs = await this.getWebhookInfo(uqSubIds)
404
+ let webhookCount = 0
405
+
406
+ for(const sid in webhookSubs) {
407
+ const webhooks = webhookSubs[sid]
408
+ const lts = (this.listeners[sid] ||= { values: {} })
409
+ for(const wh of webhooks) {
410
+ // add reliable listener for each webhook
411
+ lts.values[wh.id] ||= {
412
+ type: 'reliable',
413
+ queue: [],
414
+ extra: wh,
415
+ removeOnEmpty: true,
416
+ handler: this.webhookHandler,
417
+ ...this.#webhookHandlerOpts
418
+ }
419
+
420
+ webhookCount++
421
+ }
422
+ }
423
+
424
+ const { map: subToEventMap, retryEvents, retryItemCount }
425
+ = await normaliseRetryEventsInReadEventMap<T>(
426
+ rows, this.client, this.findEvents
427
+ )
428
+
429
+ const subs = Object.entries(subToEventMap)
430
+ const checkpoint: Checkpoint = {
431
+ activeTasks: 0,
432
+ nextCursor: rows[0].nextCursor,
433
+ }
434
+ for(const [subId, evs] of subs) {
435
+ const listeners = this.listeners[subId]?.values
436
+ if(!listeners) {
437
+ continue
438
+ }
439
+
440
+ for(const ev of evs) {
441
+ for(const lid in listeners) {
442
+ if(ev.retry?.handlerName && lid !== ev.retry.handlerName) {
443
+ continue
444
+ }
445
+
446
+ const lt = listeners[lid]
447
+ if(lt.type === 'fire-and-forget') {
448
+ lt.stream.enqueue(ev)
449
+ continue
450
+ }
451
+
452
+ this.#enqueueEventInReliableListener(subId, lid, ev, checkpoint)
453
+ }
454
+ }
455
+ }
456
+
457
+ this.#activeCheckpoints.push(checkpoint)
458
+ this.#inMemoryCursor = checkpoint.nextCursor
459
+
460
+ this.logger.debug(
461
+ {
462
+ rowsRead: rows.length,
463
+ subscriptions: subs.length,
464
+ durationMs: Date.now() - now,
465
+ checkpoint,
466
+ activeCheckpoints: this.#activeCheckpoints.length,
467
+ webhookCount,
468
+ retryEvents,
469
+ retryItemCount,
470
+ },
471
+ 'read rows',
472
+ )
473
+
474
+ if(!checkpoint.activeTasks && this.#activeCheckpoints.length === 1) {
475
+ await this.#updateCursorFromCompletedCheckpoints()
476
+ }
477
+
478
+ return rows.length
479
+ }
480
+
481
+ /**
482
+ * Runs the reliable listener's handler for each item in its queue,
483
+ * one after the other, till the queue is empty or the client has ended.
484
+ * Any errors are logged, swallowed, and processing continues.
485
+ */
486
+ async #enqueueEventInReliableListener(
487
+ subId: string,
488
+ lid: string,
489
+ item: IReadEvent<T>,
490
+ checkpoint: Checkpoint,
491
+ ) {
492
+ const lt = this.listeners[subId]?.values?.[lid]
493
+ assert(lt?.type === 'reliable', 'invalid listener type: ' + lt.type)
494
+
495
+ const {
496
+ handler, queue, removeOnEmpty, extra,
497
+ splitBy = defaultSplitBy
498
+ } = lt
499
+
500
+ queue.push({ item, checkpoint })
501
+ checkpoint.activeTasks++
502
+ if(queue.length > 1) {
503
+ return
504
+ }
505
+
506
+ while(queue.length) {
507
+ const { item, checkpoint } = queue[0]
508
+ if(checkpoint.cancelled) {
509
+ queue.shift()
510
+ continue
511
+ }
512
+
513
+ const logger = this.logger.child({
514
+ subId,
515
+ items: item.items.map((i) => i.id),
516
+ extra,
517
+ retryNumber: item.retry?.retryNumber,
518
+ })
519
+
520
+ logger.trace(
521
+ {
522
+ cpActiveTasks: checkpoint.activeTasks,
523
+ queue: queue.length,
524
+ },
525
+ 'processing handler queue',
526
+ )
527
+
528
+ try {
529
+ for(const batch of splitBy(item)) {
530
+ await handler(batch, {
531
+ client: this.client,
532
+ logger: this.logger.child({
533
+ subId,
534
+ items: batch.items.map(i => i.id),
535
+ extra,
536
+ retryNumber: item.retry?.retryNumber,
537
+ }),
538
+ subscriptionId: subId,
539
+ extra,
540
+ name: lid,
541
+ })
542
+ }
543
+
544
+ checkpoint.activeTasks--
545
+ assert(
546
+ checkpoint.activeTasks >= 0,
547
+ 'internal: checkpoint.activeTasks < 0',
548
+ )
549
+ if(!checkpoint.activeTasks) {
550
+ await this.#updateCursorFromCompletedCheckpoints()
551
+ }
552
+
553
+ logger.trace(
554
+ {
555
+ cpActiveTasks: checkpoint.activeTasks,
556
+ queue: queue.length,
557
+ },
558
+ 'completed handler task',
559
+ )
560
+ } catch(err) {
561
+ logger.error(
562
+ { err },
563
+ 'error in handler,' +
564
+ 'cancelling all active checkpoints' +
565
+ '. Restarting from last known good cursor.',
566
+ )
567
+ this.#cancelAllActiveCheckpoints()
568
+ } finally {
569
+ queue.shift()
570
+ }
571
+ }
572
+
573
+ if(removeOnEmpty) {
574
+ return this.#removeListener(subId, lid)
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Goes through all checkpoints, and sets the group cursor to the latest
580
+ * completed checkpoint. If a checkpoint has active tasks, stops there.
581
+ * This ensures that we don't accidentally move the cursor forward while
582
+ * there are still pending tasks for earlier checkpoints.
583
+ */
584
+ async #updateCursorFromCompletedCheckpoints() {
585
+ let latestMaxCursor: string | undefined
586
+ while(this.#activeCheckpoints.length) {
587
+ const cp = this.#activeCheckpoints[0]
588
+ if(cp.activeTasks > 0) {
589
+ break
590
+ }
591
+
592
+ latestMaxCursor = cp.nextCursor
593
+ this.#activeCheckpoints.shift()
594
+ }
595
+
596
+ if(!latestMaxCursor) {
597
+ return
598
+ }
599
+
600
+ const releaseLock = !this.#activeCheckpoints.length
601
+ await setGroupCursor.run(
602
+ {
603
+ groupId: this.groupId,
604
+ cursor: latestMaxCursor,
605
+ releaseLock: releaseLock,
606
+ },
607
+ this.#readClient || this.client,
608
+ )
609
+ this.logger.debug(
610
+ {
611
+ cursor: latestMaxCursor,
612
+ activeCheckpoints: this.#activeCheckpoints.length,
613
+ },
614
+ 'set cursor',
615
+ )
616
+
617
+ // if there are no more active checkpoints,
618
+ // clear in-memory cursor, so in case another process takes
619
+ // over, if & when we start reading again, we read from the DB cursor
620
+ if(releaseLock) {
621
+ this.#inMemoryCursor = null
622
+ this.#releaseReadClient()
623
+ }
624
+ }
625
+
626
+ #cancelAllActiveCheckpoints() {
627
+ for(const cp of this.#activeCheckpoints) {
628
+ cp.cancelled = true
629
+ }
630
+
631
+ this.#activeCheckpoints = []
632
+ this.#inMemoryCursor = null
633
+ }
634
+
635
+ async #unlockAndReleaseReadClient() {
636
+ if(!this.#readClient) {
637
+ return
638
+ }
639
+
640
+ try {
641
+ await releaseGroupLock.run({ groupId: this.groupId }, this.#readClient)
642
+ } catch(err) {
643
+ this.logger.error({ err }, 'error releasing read client')
644
+ } finally {
645
+ this.#releaseReadClient()
646
+ }
647
+ }
648
+
649
+ async #connectReadClient() {
650
+ if(!('connect' in this.client)) {
651
+ return false
652
+ }
653
+
654
+ if(this.#readClient) {
655
+ return true
656
+ }
657
+
658
+ this.#readClient = await this.client.connect()
659
+ this.logger.trace('acquired dedicated read client')
660
+ return true
661
+ }
662
+
663
+ #onPoolClientRemoved = async(cl: PgReleasableClient) => {
664
+ if(cl !== this.#readClient) {
665
+ return
666
+ }
667
+
668
+ this.logger.info(
669
+ 'dedicated read client disconnected, may have dup event processing',
670
+ )
671
+ }
672
+
673
+ #releaseReadClient() {
674
+ try {
675
+ this.#readClient?.release()
676
+ } catch{}
677
+
678
+ this.#readClient = undefined
679
+ }
680
+
681
+ async #startLoop(fn: Function, sleepDurationMs: number) {
682
+ const signal = this.#endAc.signal
683
+ while(!signal.aborted) {
684
+ try {
685
+ await setTimeout(sleepDurationMs, undefined, { signal })
686
+ await fn.call(this)
687
+ } catch(err) {
688
+ if(err instanceof Error && err.name === 'AbortError') {
689
+ return
690
+ }
691
+
692
+ this.logger.error({ err, fn: fn.name }, 'error in task')
693
+ }
694
+ }
695
+ }
696
+ }
697
+
698
+ function createListenerId() {
699
+ return Math.random().toString(16).slice(2, 10)
700
+ }
701
+
702
+ function defaultSplitBy<T extends IEventData>(e: IReadEvent<T>) {
703
+ return [e]
704
+ }
package/src/consts.ts ADDED
@@ -0,0 +1 @@
1
+ export const RETRY_EVENT = 'pgmb-retry'
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './client.ts'
2
+ export type * from './types.ts'
3
+ export type * from './query-types.ts'
4
+ export * from './utils.ts'
5
+ export * from './sse.ts'
6
+ export * from './queries.ts'