@haathie/pgmb 0.2.0 → 0.2.1
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/{readme.md → README.md} +13 -0
- package/lib/client.d.ts +6 -4
- package/lib/client.js +65 -49
- package/lib/types.d.ts +22 -6
- package/lib/webhook-handler.d.ts +2 -2
- package/package.json +2 -2
package/{readme.md → README.md}
RENAMED
|
@@ -124,6 +124,19 @@ await pgmb.registerReliableHandler(
|
|
|
124
124
|
retryOpts: {
|
|
125
125
|
// will retry after 1 minute, then after 5 minutes
|
|
126
126
|
retriesS: [60, 5 * 60]
|
|
127
|
+
},
|
|
128
|
+
// optionally provide a splitBy function to split
|
|
129
|
+
// event to be processed differently based on some attribute.
|
|
130
|
+
splitBy(ev) {
|
|
131
|
+
return Object.values(
|
|
132
|
+
// group events by their topic
|
|
133
|
+
ev.items.reduce((acc, item) => {
|
|
134
|
+
const key = item.topic
|
|
135
|
+
acc[key] ||= { items: [] }
|
|
136
|
+
acc[key].items.push(item)
|
|
137
|
+
return acc
|
|
138
|
+
}, {})
|
|
139
|
+
)
|
|
127
140
|
}
|
|
128
141
|
},
|
|
129
142
|
async({ items }, { logger }) => {
|
package/lib/client.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type Logger } from 'pino';
|
|
2
2
|
import { PGMBEventBatcher } from './batcher.ts';
|
|
3
3
|
import type { PgClientLike } from './query-types.ts';
|
|
4
|
-
import type { GetWebhookInfoFn, IEphemeralListener, IEventData, IEventHandler, IReadEvent, Pgmb2ClientOpts, registerReliableHandlerParams, RegisterSubscriptionParams } from './types.ts';
|
|
4
|
+
import type { GetWebhookInfoFn, IEphemeralListener, IEventData, IEventHandler, IReadEvent, IReadNextEventsFn, ISplitFn, Pgmb2ClientOpts, registerReliableHandlerParams, RegisterSubscriptionParams } from './types.ts';
|
|
5
5
|
type IReliableListener<T extends IEventData> = {
|
|
6
6
|
type: 'reliable';
|
|
7
7
|
handler: IEventHandler<T>;
|
|
8
8
|
removeOnEmpty?: boolean;
|
|
9
9
|
extra?: unknown;
|
|
10
|
+
splitBy?: ISplitFn<T>;
|
|
10
11
|
queue: {
|
|
11
12
|
item: IReadEvent<T>;
|
|
12
13
|
checkpoint: Checkpoint;
|
|
@@ -37,12 +38,13 @@ export declare class PgmbClient<T extends IEventData = IEventData> extends PGMBE
|
|
|
37
38
|
readonly subscriptionMaintenanceMs: number;
|
|
38
39
|
readonly tableMaintenanceMs: number;
|
|
39
40
|
readonly maxActiveCheckpoints: number;
|
|
41
|
+
readonly readNextEvents: IReadNextEventsFn;
|
|
40
42
|
readonly getWebhookInfo: GetWebhookInfoFn;
|
|
41
|
-
readonly webhookHandler: IEventHandler
|
|
43
|
+
readonly webhookHandler: IEventHandler<T>;
|
|
42
44
|
readonly listeners: {
|
|
43
45
|
[subId: string]: IListenerStore<T>;
|
|
44
46
|
};
|
|
45
|
-
constructor({ client, groupId, logger, sleepDurationMs, readChunkSize, maxActiveCheckpoints, poll, subscriptionMaintenanceMs, webhookHandlerOpts, getWebhookInfo, tableMaintainanceMs, ...batcherOpts }: Pgmb2ClientOpts);
|
|
47
|
+
constructor({ client, groupId, logger, sleepDurationMs, readChunkSize, maxActiveCheckpoints, poll, subscriptionMaintenanceMs, webhookHandlerOpts: { splitBy: whSplitBy, ...whHandlerOpts }, getWebhookInfo, tableMaintainanceMs, readNextEvents, ...batcherOpts }: Pgmb2ClientOpts<T>);
|
|
46
48
|
init(): Promise<void>;
|
|
47
49
|
end(): Promise<void>;
|
|
48
50
|
publish(events: T[], client?: PgClientLike): Promise<import("./queries.ts").IWriteEventsResult[]>;
|
|
@@ -63,7 +65,7 @@ export declare class PgmbClient<T extends IEventData = IEventData> extends PGMBE
|
|
|
63
65
|
* to retry failed events by the handler itself, allowing for delayed retries
|
|
64
66
|
* with backoff, and without disrupting the overall event flow.
|
|
65
67
|
*/
|
|
66
|
-
registerReliableHandler({ retryOpts, name, ...opts }: registerReliableHandlerParams
|
|
68
|
+
registerReliableHandler({ retryOpts, name, splitBy, ...opts }: registerReliableHandlerParams<T>, handler: IEventHandler<T>): Promise<{
|
|
67
69
|
subscriptionId: string;
|
|
68
70
|
cancel: () => void;
|
|
69
71
|
}>;
|
package/lib/client.js
CHANGED
|
@@ -21,9 +21,11 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
21
21
|
subscriptionMaintenanceMs;
|
|
22
22
|
tableMaintenanceMs;
|
|
23
23
|
maxActiveCheckpoints;
|
|
24
|
+
readNextEvents;
|
|
24
25
|
getWebhookInfo;
|
|
25
26
|
webhookHandler;
|
|
26
27
|
listeners = {};
|
|
28
|
+
#webhookHandlerOpts;
|
|
27
29
|
#readClient;
|
|
28
30
|
#endAc = new AbortController();
|
|
29
31
|
#shouldPoll;
|
|
@@ -33,11 +35,11 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
33
35
|
#tableMaintainTask;
|
|
34
36
|
#inMemoryCursor = null;
|
|
35
37
|
#activeCheckpoints = [];
|
|
36
|
-
constructor({ client, groupId, logger = (0, pino_1.pino)(), sleepDurationMs = 750, readChunkSize = 1000, maxActiveCheckpoints = 10, poll, subscriptionMaintenanceMs = 60 * 1000, webhookHandlerOpts = {}, getWebhookInfo = () => ({}), tableMaintainanceMs = 5 * 60 * 1000, ...batcherOpts }) {
|
|
38
|
+
constructor({ client, groupId, logger = (0, pino_1.pino)(), sleepDurationMs = 750, readChunkSize = 1000, maxActiveCheckpoints = 10, poll, subscriptionMaintenanceMs = 60 * 1000, webhookHandlerOpts: { splitBy: whSplitBy, ...whHandlerOpts } = {}, getWebhookInfo = () => ({}), tableMaintainanceMs = 5 * 60 * 1000, readNextEvents = queries_ts_1.readNextEvents.run.bind(queries_ts_1.readNextEvents), ...batcherOpts }) {
|
|
37
39
|
super({
|
|
38
40
|
...batcherOpts,
|
|
39
41
|
logger,
|
|
40
|
-
publish: (...e) => this.publish(e)
|
|
42
|
+
publish: (...e) => this.publish(e),
|
|
41
43
|
});
|
|
42
44
|
this.client = client;
|
|
43
45
|
this.logger = logger;
|
|
@@ -47,9 +49,11 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
47
49
|
this.#shouldPoll = !!poll;
|
|
48
50
|
this.subscriptionMaintenanceMs = subscriptionMaintenanceMs;
|
|
49
51
|
this.maxActiveCheckpoints = maxActiveCheckpoints;
|
|
50
|
-
this.webhookHandler = (0, webhook_handler_ts_1.createWebhookHandler)(
|
|
52
|
+
this.webhookHandler = (0, webhook_handler_ts_1.createWebhookHandler)(whHandlerOpts);
|
|
53
|
+
this.#webhookHandlerOpts = { splitBy: whSplitBy };
|
|
51
54
|
this.getWebhookInfo = getWebhookInfo;
|
|
52
55
|
this.tableMaintenanceMs = tableMaintainanceMs;
|
|
56
|
+
this.readNextEvents = readNextEvents;
|
|
53
57
|
}
|
|
54
58
|
async init() {
|
|
55
59
|
this.#endAc = new AbortController();
|
|
@@ -62,11 +66,9 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
62
66
|
await queries_ts_1.assertGroup.run({ id: this.groupId }, this.client);
|
|
63
67
|
this.logger.debug({ groupId: this.groupId }, 'asserted group exists');
|
|
64
68
|
// clean up expired subscriptions on start
|
|
65
|
-
const [{ deleted }] = await queries_ts_1.removeExpiredSubscriptions
|
|
66
|
-
.run({ groupId: this.groupId, activeIds: [] }, this.client);
|
|
69
|
+
const [{ deleted }] = await queries_ts_1.removeExpiredSubscriptions.run({ groupId: this.groupId, activeIds: [] }, this.client);
|
|
67
70
|
this.logger.debug({ deleted }, 'removed expired subscriptions');
|
|
68
|
-
this.#readTask
|
|
69
|
-
= this.#startLoop(this.readChanges.bind(this), this.sleepDurationMs);
|
|
71
|
+
this.#readTask = this.#startLoop(this.readChanges.bind(this), this.sleepDurationMs);
|
|
70
72
|
if (this.#shouldPoll) {
|
|
71
73
|
this.#pollTask = this.#startLoop(queries_ts_1.pollForEvents.run.bind(queries_ts_1.pollForEvents, undefined, this.client), this.sleepDurationMs);
|
|
72
74
|
}
|
|
@@ -74,8 +76,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
74
76
|
this.#subMaintainTask = this.#startLoop(this.#maintainSubscriptions, this.subscriptionMaintenanceMs);
|
|
75
77
|
}
|
|
76
78
|
if (this.tableMaintenanceMs) {
|
|
77
|
-
this.#tableMaintainTask = this.#startLoop(queries_ts_1.maintainEventsTable.run
|
|
78
|
-
.bind(queries_ts_1.maintainEventsTable, undefined, this.client), this.tableMaintenanceMs);
|
|
79
|
+
this.#tableMaintainTask = this.#startLoop(queries_ts_1.maintainEventsTable.run.bind(queries_ts_1.maintainEventsTable, undefined, this.client), this.tableMaintenanceMs);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
async end() {
|
|
@@ -91,7 +92,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
91
92
|
this.#readTask,
|
|
92
93
|
this.#pollTask,
|
|
93
94
|
this.#subMaintainTask,
|
|
94
|
-
this.#tableMaintainTask
|
|
95
|
+
this.#tableMaintainTask,
|
|
95
96
|
]);
|
|
96
97
|
await this.#unlockAndReleaseReadClient();
|
|
97
98
|
this.#readTask = undefined;
|
|
@@ -101,14 +102,13 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
101
102
|
}
|
|
102
103
|
publish(events, client = this.client) {
|
|
103
104
|
return queries_ts_1.writeEvents.run({
|
|
104
|
-
topics: events.map(e => e.topic),
|
|
105
|
-
payloads: events.map(e => e.payload),
|
|
106
|
-
metadatas: events.map(e => e.metadata || null),
|
|
105
|
+
topics: events.map((e) => e.topic),
|
|
106
|
+
payloads: events.map((e) => e.payload),
|
|
107
|
+
metadatas: events.map((e) => e.metadata || null),
|
|
107
108
|
}, client);
|
|
108
109
|
}
|
|
109
110
|
async assertSubscription(opts, client = this.client) {
|
|
110
|
-
const [rslt] = await queries_ts_1.assertSubscription
|
|
111
|
-
.run({ ...opts, groupId: this.groupId }, client);
|
|
111
|
+
const [rslt] = await queries_ts_1.assertSubscription.run({ ...opts, groupId: this.groupId }, client);
|
|
112
112
|
this.logger.debug({ ...opts, ...rslt }, 'asserted subscription');
|
|
113
113
|
return rslt;
|
|
114
114
|
}
|
|
@@ -131,18 +131,23 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
131
131
|
* to retry failed events by the handler itself, allowing for delayed retries
|
|
132
132
|
* with backoff, and without disrupting the overall event flow.
|
|
133
133
|
*/
|
|
134
|
-
async registerReliableHandler({ retryOpts, name = createListenerId(), ...opts }, handler) {
|
|
134
|
+
async registerReliableHandler({ retryOpts, name = createListenerId(), splitBy, ...opts }, handler) {
|
|
135
135
|
const { id: subId } = await this.assertSubscription(opts);
|
|
136
136
|
if (retryOpts) {
|
|
137
137
|
handler = (0, retry_handler_ts_1.createRetryHandler)(retryOpts, handler);
|
|
138
138
|
}
|
|
139
139
|
const lts = (this.listeners[subId] ||= { values: {} });
|
|
140
|
-
(0, assert_1.default)(!lts.values[name], `Handler with id ${name} already registered for subscription ${subId}.`
|
|
141
|
-
|
|
142
|
-
this.listeners[subId].values[name] = {
|
|
140
|
+
(0, assert_1.default)(!lts.values[name], `Handler with id ${name} already registered for subscription ${subId}.` +
|
|
141
|
+
' Cancel the existing one or use a different id.');
|
|
142
|
+
this.listeners[subId].values[name] = {
|
|
143
|
+
type: 'reliable',
|
|
144
|
+
handler,
|
|
145
|
+
splitBy,
|
|
146
|
+
queue: [],
|
|
147
|
+
};
|
|
143
148
|
return {
|
|
144
149
|
subscriptionId: subId,
|
|
145
|
-
cancel: () => this.#removeListener(subId, name)
|
|
150
|
+
cancel: () => this.#removeListener(subId, name),
|
|
146
151
|
};
|
|
147
152
|
}
|
|
148
153
|
async removeSubscription(subId) {
|
|
@@ -153,8 +158,8 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
153
158
|
if (!existingSubs) {
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
156
|
-
await Promise.allSettled(Object.values(existingSubs).map(e =>
|
|
157
|
-
|
|
161
|
+
await Promise.allSettled(Object.values(existingSubs).map((e) => e.type === 'fire-and-forget' &&
|
|
162
|
+
e.stream.throw(new Error('subscription removed'))));
|
|
158
163
|
}
|
|
159
164
|
#listenForEvents(subId) {
|
|
160
165
|
const lid = createListenerId();
|
|
@@ -178,8 +183,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
178
183
|
const activeIds = Object.keys(this.listeners);
|
|
179
184
|
await queries_ts_1.markSubscriptionsActive.run({ ids: activeIds }, this.client);
|
|
180
185
|
this.logger.trace({ activeSubscriptions: activeIds.length }, 'marked subscriptions as active');
|
|
181
|
-
const [{ deleted }] = await queries_ts_1.removeExpiredSubscriptions
|
|
182
|
-
.run({ groupId: this.groupId, activeIds }, this.client);
|
|
186
|
+
const [{ deleted }] = await queries_ts_1.removeExpiredSubscriptions.run({ groupId: this.groupId, activeIds }, this.client);
|
|
183
187
|
this.logger.trace({ deleted }, 'removed expired subscriptions');
|
|
184
188
|
}
|
|
185
189
|
async readChanges() {
|
|
@@ -188,10 +192,10 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
188
192
|
}
|
|
189
193
|
const now = Date.now();
|
|
190
194
|
await this.#connectReadClient();
|
|
191
|
-
const rows = await
|
|
195
|
+
const rows = await this.readNextEvents({
|
|
192
196
|
groupId: this.groupId,
|
|
193
197
|
cursor: this.#inMemoryCursor,
|
|
194
|
-
chunkSize: this.readChunkSize
|
|
198
|
+
chunkSize: this.readChunkSize,
|
|
195
199
|
}, this.#readClient || this.client)
|
|
196
200
|
.catch(async (err) => {
|
|
197
201
|
if (err instanceof Error && err.message.includes('connection error')) {
|
|
@@ -207,7 +211,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
207
211
|
}
|
|
208
212
|
return 0;
|
|
209
213
|
}
|
|
210
|
-
const uqSubIds = Array.from(new Set(rows.flatMap(r => r.subscriptionIds)));
|
|
214
|
+
const uqSubIds = Array.from(new Set(rows.flatMap((r) => r.subscriptionIds)));
|
|
211
215
|
const webhookSubs = await this.getWebhookInfo(uqSubIds);
|
|
212
216
|
let webhookCount = 0;
|
|
213
217
|
for (const sid in webhookSubs) {
|
|
@@ -220,14 +224,18 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
220
224
|
queue: [],
|
|
221
225
|
extra: wh,
|
|
222
226
|
removeOnEmpty: true,
|
|
223
|
-
handler: this.webhookHandler
|
|
227
|
+
handler: this.webhookHandler,
|
|
228
|
+
...this.#webhookHandlerOpts
|
|
224
229
|
};
|
|
225
230
|
webhookCount++;
|
|
226
231
|
}
|
|
227
232
|
}
|
|
228
|
-
const { map: subToEventMap, retryEvents, retryItemCount } = await (0, retry_handler_ts_1.normaliseRetryEventsInReadEventMap)(rows, this.client);
|
|
233
|
+
const { map: subToEventMap, retryEvents, retryItemCount, } = await (0, retry_handler_ts_1.normaliseRetryEventsInReadEventMap)(rows, this.client);
|
|
229
234
|
const subs = Object.entries(subToEventMap);
|
|
230
|
-
const checkpoint = {
|
|
235
|
+
const checkpoint = {
|
|
236
|
+
activeTasks: 0,
|
|
237
|
+
nextCursor: rows[0].nextCursor,
|
|
238
|
+
};
|
|
231
239
|
for (const [subId, evs] of subs) {
|
|
232
240
|
const listeners = this.listeners[subId]?.values;
|
|
233
241
|
if (!listeners) {
|
|
@@ -257,7 +265,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
257
265
|
activeCheckpoints: this.#activeCheckpoints.length,
|
|
258
266
|
webhookCount,
|
|
259
267
|
retryEvents,
|
|
260
|
-
retryItemCount
|
|
268
|
+
retryItemCount,
|
|
261
269
|
}, 'read rows');
|
|
262
270
|
if (!checkpoint.activeTasks && this.#activeCheckpoints.length === 1) {
|
|
263
271
|
await this.#updateCursorFromCompletedCheckpoints();
|
|
@@ -272,7 +280,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
272
280
|
async #enqueueEventInReliableListener(subId, lid, item, checkpoint) {
|
|
273
281
|
const lt = this.listeners[subId]?.values?.[lid];
|
|
274
282
|
(0, assert_1.default)(lt?.type === 'reliable', 'invalid listener type: ' + lt.type);
|
|
275
|
-
const { handler, queue, removeOnEmpty, extra } = lt;
|
|
283
|
+
const { handler, queue, removeOnEmpty, extra, splitBy = defaultSplitBy } = lt;
|
|
276
284
|
queue.push({ item, checkpoint });
|
|
277
285
|
checkpoint.activeTasks++;
|
|
278
286
|
if (queue.length > 1) {
|
|
@@ -286,7 +294,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
286
294
|
}
|
|
287
295
|
const logger = this.logger.child({
|
|
288
296
|
subId,
|
|
289
|
-
items: item.items.map(i => i.id),
|
|
297
|
+
items: item.items.map((i) => i.id),
|
|
290
298
|
extra,
|
|
291
299
|
retryNumber: item.retry?.retryNumber,
|
|
292
300
|
});
|
|
@@ -295,13 +303,20 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
295
303
|
queue: queue.length,
|
|
296
304
|
}, 'processing handler queue');
|
|
297
305
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
306
|
+
for (const batch of splitBy(item)) {
|
|
307
|
+
await handler(batch, {
|
|
308
|
+
client: this.client,
|
|
309
|
+
logger: this.logger.child({
|
|
310
|
+
subId,
|
|
311
|
+
items: batch.items.map(i => i.id),
|
|
312
|
+
extra,
|
|
313
|
+
retryNumber: item.retry?.retryNumber,
|
|
314
|
+
}),
|
|
315
|
+
subscriptionId: subId,
|
|
316
|
+
extra,
|
|
317
|
+
name: lid,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
305
320
|
checkpoint.activeTasks--;
|
|
306
321
|
(0, assert_1.default)(checkpoint.activeTasks >= 0, 'internal: checkpoint.activeTasks < 0');
|
|
307
322
|
if (!checkpoint.activeTasks) {
|
|
@@ -313,9 +328,9 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
313
328
|
}, 'completed handler task');
|
|
314
329
|
}
|
|
315
330
|
catch (err) {
|
|
316
|
-
logger.error({ err }, 'error in handler,'
|
|
317
|
-
|
|
318
|
-
|
|
331
|
+
logger.error({ err }, 'error in handler,' +
|
|
332
|
+
'cancelling all active checkpoints' +
|
|
333
|
+
'. Restarting from last known good cursor.');
|
|
319
334
|
this.#cancelAllActiveCheckpoints();
|
|
320
335
|
}
|
|
321
336
|
finally {
|
|
@@ -349,11 +364,11 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
349
364
|
await queries_ts_1.setGroupCursor.run({
|
|
350
365
|
groupId: this.groupId,
|
|
351
366
|
cursor: latestMaxCursor,
|
|
352
|
-
releaseLock: releaseLock
|
|
367
|
+
releaseLock: releaseLock,
|
|
353
368
|
}, this.#readClient || this.client);
|
|
354
369
|
this.logger.debug({
|
|
355
370
|
cursor: latestMaxCursor,
|
|
356
|
-
activeCheckpoints: this.#activeCheckpoints.length
|
|
371
|
+
activeCheckpoints: this.#activeCheckpoints.length,
|
|
357
372
|
}, 'set cursor');
|
|
358
373
|
// if there are no more active checkpoints,
|
|
359
374
|
// clear in-memory cursor, so in case another process takes
|
|
@@ -375,8 +390,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
375
390
|
return;
|
|
376
391
|
}
|
|
377
392
|
try {
|
|
378
|
-
await queries_ts_1.releaseGroupLock
|
|
379
|
-
.run({ groupId: this.groupId }, this.#readClient);
|
|
393
|
+
await queries_ts_1.releaseGroupLock.run({ groupId: this.groupId }, this.#readClient);
|
|
380
394
|
}
|
|
381
395
|
catch (err) {
|
|
382
396
|
this.logger.error({ err }, 'error releasing read client');
|
|
@@ -400,8 +414,7 @@ class PgmbClient extends batcher_ts_1.PGMBEventBatcher {
|
|
|
400
414
|
if (cl !== this.#readClient) {
|
|
401
415
|
return;
|
|
402
416
|
}
|
|
403
|
-
this.logger
|
|
404
|
-
.info('dedicated read client disconnected, may have dup event processing');
|
|
417
|
+
this.logger.info('dedicated read client disconnected, may have dup event processing');
|
|
405
418
|
};
|
|
406
419
|
#releaseReadClient() {
|
|
407
420
|
try {
|
|
@@ -430,3 +443,6 @@ exports.PgmbClient = PgmbClient;
|
|
|
430
443
|
function createListenerId() {
|
|
431
444
|
return Math.random().toString(16).slice(2, 10);
|
|
432
445
|
}
|
|
446
|
+
function defaultSplitBy(e) {
|
|
447
|
+
return [e];
|
|
448
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { IDatabaseConnection } from '@pgtyped/runtime';
|
|
1
2
|
import type { IncomingMessage } from 'node:http';
|
|
2
3
|
import type { Logger } from 'pino';
|
|
3
4
|
import type { HeaderRecord } from 'undici-types/header.js';
|
|
4
5
|
import type { AbortableAsyncIterator } from './abortable-async-iterator.ts';
|
|
5
|
-
import type { IAssertSubscriptionParams } from './queries.ts';
|
|
6
|
+
import type { IAssertSubscriptionParams, IReadNextEventsParams, IReadNextEventsResult } from './queries.ts';
|
|
6
7
|
import type { PgClientLike } from './query-types.ts';
|
|
8
|
+
export type ISplitFn<T extends IEventData> = (event: IReadEvent<T>) => IReadEvent<T>[];
|
|
7
9
|
export type SerialisedEvent = {
|
|
8
10
|
body: Buffer | string;
|
|
9
11
|
contentType: string;
|
|
@@ -17,7 +19,7 @@ export type GetWebhookInfoFn = (subscriptionIds: string[]) => Promise<{
|
|
|
17
19
|
}> | {
|
|
18
20
|
[id: string]: WebhookInfo[];
|
|
19
21
|
};
|
|
20
|
-
export type PgmbWebhookOpts = {
|
|
22
|
+
export type PgmbWebhookOpts<T extends IEventData> = {
|
|
21
23
|
/**
|
|
22
24
|
* Maximum time to wait for webhook request to complete
|
|
23
25
|
* @default 5 seconds
|
|
@@ -29,6 +31,7 @@ export type PgmbWebhookOpts = {
|
|
|
29
31
|
* If null, a failed handler will fail the event processor. Use carefully.
|
|
30
32
|
*/
|
|
31
33
|
retryOpts?: IRetryHandlerOpts | null;
|
|
34
|
+
splitBy?: ISplitFn<T>;
|
|
32
35
|
jsonifier?: JSONifier;
|
|
33
36
|
serialiseEvent?(ev: IReadEvent): SerialisedEvent;
|
|
34
37
|
};
|
|
@@ -64,7 +67,8 @@ export type PGMBEventBatcherOpts<T extends IEventData> = {
|
|
|
64
67
|
*/
|
|
65
68
|
maxBatchSize?: number;
|
|
66
69
|
};
|
|
67
|
-
export type
|
|
70
|
+
export type IReadNextEventsFn = (parmas: IReadNextEventsParams, db: IDatabaseConnection) => Promise<IReadNextEventsResult[]>;
|
|
71
|
+
export type Pgmb2ClientOpts<T extends IEventData> = {
|
|
68
72
|
client: PgClientLike;
|
|
69
73
|
/**
|
|
70
74
|
* Globally unique identifier for this Pgmb2Client instance. All subs
|
|
@@ -100,22 +104,31 @@ export type Pgmb2ClientOpts = {
|
|
|
100
104
|
* @default true
|
|
101
105
|
*/
|
|
102
106
|
poll?: boolean;
|
|
103
|
-
webhookHandlerOpts?: Partial<PgmbWebhookOpts
|
|
107
|
+
webhookHandlerOpts?: Partial<PgmbWebhookOpts<T>>;
|
|
104
108
|
getWebhookInfo?: GetWebhookInfoFn;
|
|
109
|
+
/**
|
|
110
|
+
* Override the default readNextEvents implementation
|
|
111
|
+
*/
|
|
112
|
+
readNextEvents?: IReadNextEventsFn;
|
|
105
113
|
} & Pick<PGMBEventBatcherOpts<IEventData>, 'flushIntervalMs' | 'maxBatchSize' | 'shouldLog'>;
|
|
106
114
|
export type IReadEvent<T extends IEventData = IEventData> = {
|
|
107
115
|
items: IEvent<T>[];
|
|
108
116
|
retry?: IRetryEventPayload;
|
|
109
117
|
};
|
|
110
118
|
export type RegisterSubscriptionParams = Omit<IAssertSubscriptionParams, 'groupId'>;
|
|
111
|
-
export type registerReliableHandlerParams = RegisterSubscriptionParams & {
|
|
119
|
+
export type registerReliableHandlerParams<T extends IEventData = IEventData> = RegisterSubscriptionParams & {
|
|
112
120
|
/**
|
|
113
121
|
* Name for the retry handler, used to ensure retries for a particular
|
|
114
122
|
* handler are not mixed with another handler. This name need only be
|
|
115
123
|
* unique for a particular subscription.
|
|
116
|
-
|
|
124
|
+
*/
|
|
117
125
|
name?: string;
|
|
118
126
|
retryOpts?: IRetryHandlerOpts;
|
|
127
|
+
/**
|
|
128
|
+
* If provided, will split an incoming event into multiple events
|
|
129
|
+
* as determined by the function.
|
|
130
|
+
*/
|
|
131
|
+
splitBy?: ISplitFn<T>;
|
|
119
132
|
};
|
|
120
133
|
export type CreateTopicalSubscriptionOpts<T extends IEventData> = {
|
|
121
134
|
/**
|
|
@@ -126,6 +139,9 @@ export type CreateTopicalSubscriptionOpts<T extends IEventData> = {
|
|
|
126
139
|
* To scale out processing, you can partition the subscriptions.
|
|
127
140
|
* For example, with `current: 0, total: 3`, only messages
|
|
128
141
|
* where `hashtext(e.id) % 3 == 0` will be received by this subscription.
|
|
142
|
+
* This will result in an approximate even split for all processors, the only
|
|
143
|
+
* caveat being it requires knowing the number of event processors on this
|
|
144
|
+
* subscription beforehand.
|
|
129
145
|
*/
|
|
130
146
|
partition?: {
|
|
131
147
|
current: number;
|
package/lib/webhook-handler.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { IEventHandler, PgmbWebhookOpts } from './types.ts';
|
|
1
|
+
import type { IEventData, IEventHandler, PgmbWebhookOpts } from './types.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Create a handler that sends events to a webhook URL via HTTP POST.
|
|
4
4
|
* @param url Where to send the webhook requests
|
|
5
5
|
*/
|
|
6
|
-
export declare function createWebhookHandler({ timeoutMs, headers, retryOpts, jsonifier, serialiseEvent }: Partial<PgmbWebhookOpts
|
|
6
|
+
export declare function createWebhookHandler<T extends IEventData>({ timeoutMs, headers, retryOpts, jsonifier, serialiseEvent }: Partial<PgmbWebhookOpts<T>>): IEventHandler;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haathie/pgmb",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "PG message broker, with a type-safe typescript client with built-in webhook & SSE support.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org",
|