@haathie/pgmb 0.2.6 → 0.2.7

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