@haathie/pgmb 0.2.8 → 0.2.10
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 +1 -2
- package/lib/abortable-async-iterator.d.ts +14 -0
- package/lib/abortable-async-iterator.js +5 -12
- package/lib/batcher.d.ts +12 -0
- package/lib/batcher.js +1 -5
- package/lib/client.d.ts +77 -0
- package/lib/client.js +34 -41
- package/lib/consts.d.ts +1 -0
- package/lib/consts.js +1 -4
- package/lib/index.d.ts +6 -0
- package/lib/index.js +4 -20
- package/lib/queries.d.ts +498 -0
- package/lib/queries.js +19 -22
- package/lib/query-types.d.ts +17 -0
- package/lib/query-types.js +1 -2
- package/lib/retry-handler.d.ts +11 -0
- package/lib/retry-handler.js +7 -11
- package/lib/sse.d.ts +4 -0
- package/lib/sse.js +16 -52
- package/lib/types.d.ts +234 -0
- package/lib/types.js +1 -2
- package/lib/utils.d.ts +19 -0
- package/lib/utils.js +6 -15
- package/lib/webhook-handler.d.ts +6 -0
- package/lib/webhook-handler.js +7 -13
- package/package.json +3 -9
- package/sql/pgmb-0.2.0-0.2.8.sql +0 -1
- package/src/abortable-async-iterator.ts +0 -98
- package/src/batcher.ts +0 -90
- package/src/client.ts +0 -704
- package/src/consts.ts +0 -1
- package/src/index.ts +0 -6
- package/src/queries.ts +0 -630
- package/src/query-types.ts +0 -21
- package/src/retry-handler.ts +0 -125
- package/src/sse.ts +0 -148
- package/src/types.ts +0 -267
- package/src/utils.ts +0 -71
- package/src/webhook-handler.ts +0 -91
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type QueryResult<T> = {
|
|
2
|
+
rowCount: number;
|
|
3
|
+
rows: T[];
|
|
4
|
+
};
|
|
5
|
+
export interface PgClient {
|
|
6
|
+
query<T = any>(query: string, params?: unknown[]): Promise<QueryResult<T>>;
|
|
7
|
+
exec?(query: string): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface PgReleasableClient extends PgClient {
|
|
10
|
+
release: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface PgPoolLike extends PgClient {
|
|
13
|
+
connect: () => Promise<PgReleasableClient>;
|
|
14
|
+
on(ev: 'remove', handler: (cl: PgReleasableClient) => void): this;
|
|
15
|
+
off(ev: 'remove', handler: (cl: PgReleasableClient) => void): this;
|
|
16
|
+
}
|
|
17
|
+
export type PgClientLike = PgClient | PgPoolLike;
|
package/lib/query-types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IReadNextEventsResult } from './queries.ts';
|
|
2
|
+
import type { PgClientLike } from './query-types.ts';
|
|
3
|
+
import type { IEventData, IEventHandler, IFindEventsFn, IReadEvent, IRetryHandlerOpts } from './types.ts';
|
|
4
|
+
export declare function createRetryHandler<T extends IEventData>({ retriesS }: IRetryHandlerOpts, handler: IEventHandler<T>): IEventHandler<T>;
|
|
5
|
+
export declare function normaliseRetryEventsInReadEventMap<T extends IEventData>(rows: IReadNextEventsResult[], client: PgClientLike, findEvents?: IFindEventsFn): Promise<{
|
|
6
|
+
map: {
|
|
7
|
+
[sid: string]: IReadEvent<T>[];
|
|
8
|
+
};
|
|
9
|
+
retryEvents: number;
|
|
10
|
+
retryItemCount: number;
|
|
11
|
+
}>;
|
package/lib/retry-handler.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const consts_ts_1 = require("./consts.js");
|
|
6
|
-
const queries_ts_1 = require("./queries.js");
|
|
7
|
-
const defaultFindEvents = queries_ts_1.findEvents.run.bind(queries_ts_1.findEvents);
|
|
8
|
-
function createRetryHandler({ retriesS }, handler) {
|
|
1
|
+
import { RETRY_EVENT } from "./consts.js";
|
|
2
|
+
import { findEvents, scheduleEventRetry } from "./queries.js";
|
|
3
|
+
const defaultFindEvents = findEvents.run.bind(findEvents);
|
|
4
|
+
export function createRetryHandler({ retriesS }, handler) {
|
|
9
5
|
return async (ev, ctx) => {
|
|
10
6
|
const { name, client, subscriptionId, logger } = ctx;
|
|
11
7
|
try {
|
|
@@ -18,7 +14,7 @@ function createRetryHandler({ retriesS }, handler) {
|
|
|
18
14
|
if (!nextRetryGapS) {
|
|
19
15
|
return;
|
|
20
16
|
}
|
|
21
|
-
await
|
|
17
|
+
await scheduleEventRetry.run({
|
|
22
18
|
subscriptionId,
|
|
23
19
|
ids: ev.items.map(i => i.id),
|
|
24
20
|
retryNumber: retryNumber + 1,
|
|
@@ -28,7 +24,7 @@ function createRetryHandler({ retriesS }, handler) {
|
|
|
28
24
|
}
|
|
29
25
|
};
|
|
30
26
|
}
|
|
31
|
-
async function normaliseRetryEventsInReadEventMap(rows, client, findEvents = defaultFindEvents) {
|
|
27
|
+
export async function normaliseRetryEventsInReadEventMap(rows, client, findEvents = defaultFindEvents) {
|
|
32
28
|
const map = {};
|
|
33
29
|
const evsToPopulate = [];
|
|
34
30
|
const idsToLoad = [];
|
|
@@ -44,7 +40,7 @@ async function normaliseRetryEventsInReadEventMap(rows, client, findEvents = def
|
|
|
44
40
|
for (const [subscriptionId, items] of subEventList) {
|
|
45
41
|
for (let i = 0; i < items.length; i) {
|
|
46
42
|
const item = items[i];
|
|
47
|
-
if (item.topic !==
|
|
43
|
+
if (item.topic !== RETRY_EVENT) {
|
|
48
44
|
i++;
|
|
49
45
|
continue;
|
|
50
46
|
}
|
package/lib/sse.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { PgmbClient } from './client.ts';
|
|
3
|
+
import type { IEventData, SSERequestHandlerOpts } from './types.ts';
|
|
4
|
+
export declare function createSSERequestHandler<T extends IEventData>(this: PgmbClient<T>, { getSubscriptionOpts, maxReplayEvents, maxReplayIntervalMs, jsonifier }: SSERequestHandlerOpts): (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => Promise<void>;
|
package/lib/sse.js
CHANGED
|
@@ -1,69 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.createSSERequestHandler = createSSERequestHandler;
|
|
37
|
-
const node_assert_1 = __importStar(require("node:assert"));
|
|
38
|
-
const queries_ts_1 = require("./queries.js");
|
|
39
|
-
const utils_ts_1 = require("./utils.js");
|
|
40
|
-
function createSSERequestHandler({ getSubscriptionOpts, maxReplayEvents = 1000, maxReplayIntervalMs = 5 * 60 * 1000, jsonifier = JSON }) {
|
|
1
|
+
import assert, { AssertionError } from 'node:assert';
|
|
2
|
+
import { replayEvents } from "./queries.js";
|
|
3
|
+
import { getCreateDateFromSubscriptionId, getDateFromMessageId } from "./utils.js";
|
|
4
|
+
export function createSSERequestHandler({ getSubscriptionOpts, maxReplayEvents = 1000, maxReplayIntervalMs = 5 * 60 * 1000, jsonifier = JSON }) {
|
|
41
5
|
const replayEnabled = maxReplayEvents > 0;
|
|
42
6
|
return handleSSERequest.bind(this);
|
|
43
7
|
async function handleSSERequest(req, res) {
|
|
44
8
|
let sub;
|
|
45
9
|
let eventsToReplay = [];
|
|
46
10
|
try {
|
|
47
|
-
(
|
|
11
|
+
assert(req.method?.toLowerCase() === 'get', 'SSE only supports GET requests');
|
|
48
12
|
// validate last-event-id header
|
|
49
13
|
const fromEventId = req.headers['last-event-id'];
|
|
50
14
|
if (fromEventId) {
|
|
51
|
-
(
|
|
52
|
-
(
|
|
53
|
-
const fromDt =
|
|
54
|
-
(
|
|
55
|
-
(
|
|
15
|
+
assert(replayEnabled, 'replay disabled on server');
|
|
16
|
+
assert(typeof fromEventId === 'string', 'invalid last-event-id header');
|
|
17
|
+
const fromDt = getDateFromMessageId(fromEventId);
|
|
18
|
+
assert(fromDt, 'invalid last-event-id header value');
|
|
19
|
+
assert(fromDt.getTime() >= (Date.now() - maxReplayIntervalMs), 'last-event-id is too old to replay');
|
|
56
20
|
}
|
|
57
21
|
sub = await this.registerFireAndForgetHandler({
|
|
58
22
|
...await getSubscriptionOpts(req),
|
|
59
23
|
expiryInterval: `${maxReplayIntervalMs * 2} milliseconds`
|
|
60
24
|
});
|
|
61
25
|
if (fromEventId) {
|
|
62
|
-
const fromDt =
|
|
63
|
-
const subDt =
|
|
64
|
-
(
|
|
65
|
-
(
|
|
66
|
-
eventsToReplay = await
|
|
26
|
+
const fromDt = getDateFromMessageId(fromEventId);
|
|
27
|
+
const subDt = getCreateDateFromSubscriptionId(sub.id);
|
|
28
|
+
assert(subDt, 'internal: invalid subscription id format');
|
|
29
|
+
assert(fromDt >= subDt, 'last-event-id is before subscription creation, cannot replay');
|
|
30
|
+
eventsToReplay = await replayEvents.run({
|
|
67
31
|
groupId: this.groupId,
|
|
68
32
|
subscriptionId: sub.id,
|
|
69
33
|
fromEventId: fromEventId,
|
|
@@ -85,7 +49,7 @@ function createSSERequestHandler({ getSubscriptionOpts, maxReplayEvents = 1000,
|
|
|
85
49
|
const message = err instanceof Error ? err.message : String(err);
|
|
86
50
|
// if an assertion failed, we cannot connect with these parameters
|
|
87
51
|
// so use 204 No Content
|
|
88
|
-
const code = err instanceof
|
|
52
|
+
const code = err instanceof AssertionError ? 204 : 500;
|
|
89
53
|
res
|
|
90
54
|
.writeHead(code, message)
|
|
91
55
|
.end();
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { IDatabaseConnection } from '@pgtyped/runtime';
|
|
2
|
+
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
import type { Logger } from 'pino';
|
|
4
|
+
import type { HeaderRecord } from 'undici-types/header.js';
|
|
5
|
+
import type { AbortableAsyncIterator } from './abortable-async-iterator.ts';
|
|
6
|
+
import type { IAssertSubscriptionParams, IFindEventsParams, IFindEventsResult, IReadNextEventsParams, IReadNextEventsResult } from './queries.ts';
|
|
7
|
+
import type { PgClientLike } from './query-types.ts';
|
|
8
|
+
export type ISplitFn<T extends IEventData> = (event: IReadEvent<T>) => IReadEvent<T>[];
|
|
9
|
+
export type SerialisedEvent = {
|
|
10
|
+
body: Buffer | string;
|
|
11
|
+
contentType: string;
|
|
12
|
+
};
|
|
13
|
+
export type WebhookInfo = {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string | URL;
|
|
16
|
+
};
|
|
17
|
+
export type GetWebhookInfoFn = (subscriptionIds: string[]) => Promise<{
|
|
18
|
+
[id: string]: WebhookInfo[];
|
|
19
|
+
}> | {
|
|
20
|
+
[id: string]: WebhookInfo[];
|
|
21
|
+
};
|
|
22
|
+
export type PgmbWebhookOpts<T extends IEventData> = {
|
|
23
|
+
/**
|
|
24
|
+
* Maximum time to wait for webhook request to complete
|
|
25
|
+
* @default 5 seconds
|
|
26
|
+
*/
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
headers?: HeaderRecord;
|
|
29
|
+
/**
|
|
30
|
+
* Configure retry intervals in seconds for failed webhook requests.
|
|
31
|
+
* If null, a failed handler will fail the event processor. Use carefully.
|
|
32
|
+
*/
|
|
33
|
+
retryOpts?: IRetryHandlerOpts | null;
|
|
34
|
+
splitBy?: ISplitFn<T>;
|
|
35
|
+
jsonifier?: JSONifier;
|
|
36
|
+
serialiseEvent?(ev: IReadEvent, logger: Logger): SerialisedEvent;
|
|
37
|
+
};
|
|
38
|
+
export interface IEventData {
|
|
39
|
+
topic: string;
|
|
40
|
+
payload: unknown;
|
|
41
|
+
metadata?: unknown;
|
|
42
|
+
}
|
|
43
|
+
export type IEvent<T extends IEventData> = (T & {
|
|
44
|
+
id: string;
|
|
45
|
+
});
|
|
46
|
+
export type PGMBEventBatcherOpts<T extends IEventData> = {
|
|
47
|
+
/**
|
|
48
|
+
* Whether a particular published message should be logged.
|
|
49
|
+
* By default, all messages are logged -- in case of certain
|
|
50
|
+
* failures, the logs can be used to replay the messages.
|
|
51
|
+
*/
|
|
52
|
+
shouldLog?(msg: T): boolean;
|
|
53
|
+
publish(...msgs: T[]): Promise<{
|
|
54
|
+
id: string;
|
|
55
|
+
}[]>;
|
|
56
|
+
logger?: Logger;
|
|
57
|
+
/**
|
|
58
|
+
* Automatically flush after this interval.
|
|
59
|
+
* Set to undefined or 0 to disable. Will need to
|
|
60
|
+
* manually call `flush()` to publish messages.
|
|
61
|
+
* @default undefined
|
|
62
|
+
*/
|
|
63
|
+
flushIntervalMs?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Max number of messages to send in a batch
|
|
66
|
+
* @default 2500
|
|
67
|
+
*/
|
|
68
|
+
maxBatchSize?: number;
|
|
69
|
+
};
|
|
70
|
+
export type IReadNextEventsFn = (parmas: IReadNextEventsParams, db: IDatabaseConnection) => Promise<IReadNextEventsResult[]>;
|
|
71
|
+
export type IFindEventsFn = (parmas: IFindEventsParams, db: IDatabaseConnection) => Promise<IFindEventsResult[]>;
|
|
72
|
+
export type Pgmb2ClientOpts<T extends IEventData> = {
|
|
73
|
+
client: PgClientLike;
|
|
74
|
+
/**
|
|
75
|
+
* Globally unique identifier for this Pgmb2Client instance. All subs
|
|
76
|
+
* registered with this client will use this groupId.
|
|
77
|
+
*/
|
|
78
|
+
groupId: string;
|
|
79
|
+
logger?: Logger;
|
|
80
|
+
/**
|
|
81
|
+
* How long to sleep between polling for new events from
|
|
82
|
+
* the global events table.
|
|
83
|
+
* Only one global call is required across all clients.
|
|
84
|
+
* Set to 0 to disable polling.
|
|
85
|
+
*
|
|
86
|
+
* @default 1 second
|
|
87
|
+
* */
|
|
88
|
+
pollEventsIntervalMs?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Group level configuration for how often to read new events
|
|
91
|
+
* relevant to the group's subscriptions.
|
|
92
|
+
* @default 1 second
|
|
93
|
+
*/
|
|
94
|
+
readEventsIntervalMs?: number;
|
|
95
|
+
/**
|
|
96
|
+
* How often to mark subscriptions as active,
|
|
97
|
+
* and remove expired ones.
|
|
98
|
+
* @default 1 minute
|
|
99
|
+
*/
|
|
100
|
+
subscriptionMaintenanceMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* How often to maintain the events tables
|
|
103
|
+
* (drop old partitions, create new ones, etc)
|
|
104
|
+
* Set to 0 to disable automatic maintenance.
|
|
105
|
+
*
|
|
106
|
+
* @default 5 minutes
|
|
107
|
+
*/
|
|
108
|
+
tableMaintainanceMs?: number;
|
|
109
|
+
readChunkSize?: number;
|
|
110
|
+
/**
|
|
111
|
+
* As we process in batches, a single handler taking time to finish
|
|
112
|
+
* can lead to buildup of unprocessed checkpoints. To avoid this,
|
|
113
|
+
* we keep moving forward while handlers run in the background, but
|
|
114
|
+
* to avoid an unbounded number of items being backlogged, we limit
|
|
115
|
+
* how much further we can go ahead from the earliest uncompleted checkpoint.
|
|
116
|
+
* @default 10
|
|
117
|
+
*/
|
|
118
|
+
maxActiveCheckpoints?: number;
|
|
119
|
+
webhookHandlerOpts?: Partial<PgmbWebhookOpts<T>>;
|
|
120
|
+
getWebhookInfo?: GetWebhookInfoFn;
|
|
121
|
+
/**
|
|
122
|
+
* Override the default readNextEvents implementation
|
|
123
|
+
*/
|
|
124
|
+
readNextEvents?: IReadNextEventsFn;
|
|
125
|
+
/**
|
|
126
|
+
* Override the default findEvents implementation
|
|
127
|
+
*/
|
|
128
|
+
findEvents?: IFindEventsFn;
|
|
129
|
+
} & Pick<PGMBEventBatcherOpts<IEventData>, 'flushIntervalMs' | 'maxBatchSize' | 'shouldLog'>;
|
|
130
|
+
export type IReadEvent<T extends IEventData = IEventData> = {
|
|
131
|
+
items: IEvent<T>[];
|
|
132
|
+
retry?: IRetryEventPayload;
|
|
133
|
+
};
|
|
134
|
+
export type RegisterSubscriptionParams = Omit<IAssertSubscriptionParams, 'groupId'>;
|
|
135
|
+
export type registerReliableHandlerParams<T extends IEventData = IEventData> = RegisterSubscriptionParams & {
|
|
136
|
+
/**
|
|
137
|
+
* Name for the retry handler, used to ensure retries for a particular
|
|
138
|
+
* handler are not mixed with another handler. This name need only be
|
|
139
|
+
* unique for a particular subscription.
|
|
140
|
+
*/
|
|
141
|
+
name?: string;
|
|
142
|
+
retryOpts?: IRetryHandlerOpts;
|
|
143
|
+
/**
|
|
144
|
+
* If provided, will split an incoming event into multiple events
|
|
145
|
+
* as determined by the function.
|
|
146
|
+
*/
|
|
147
|
+
splitBy?: ISplitFn<T>;
|
|
148
|
+
};
|
|
149
|
+
export type CreateTopicalSubscriptionOpts<T extends IEventData> = {
|
|
150
|
+
/**
|
|
151
|
+
* The topics to subscribe to.
|
|
152
|
+
*/
|
|
153
|
+
topics: T['topic'][];
|
|
154
|
+
/**
|
|
155
|
+
* To scale out processing, you can partition the subscriptions.
|
|
156
|
+
* For example, with `current: 0, total: 3`, only messages
|
|
157
|
+
* where `hashtext(e.id) % 3 == 0` will be received by this subscription.
|
|
158
|
+
* This will result in an approximate even split for all processors, the only
|
|
159
|
+
* caveat being it requires knowing the number of event processors on this
|
|
160
|
+
* subscription beforehand.
|
|
161
|
+
*/
|
|
162
|
+
partition?: {
|
|
163
|
+
current: number;
|
|
164
|
+
total: number;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Add any additional params to filter by.
|
|
168
|
+
* i.e "s.params @> jsonb_build_object(...additionalFilters)"
|
|
169
|
+
* The value should be a valid SQL snippet.
|
|
170
|
+
*/
|
|
171
|
+
additionalFilters?: Record<string, string>;
|
|
172
|
+
/** JSON to populate params */
|
|
173
|
+
additionalParams?: Record<string, any>;
|
|
174
|
+
expiryInterval?: RegisterSubscriptionParams['expiryInterval'];
|
|
175
|
+
};
|
|
176
|
+
export interface IEphemeralListener<T extends IEventData> extends AbortableAsyncIterator<IReadEvent<T>> {
|
|
177
|
+
id: string;
|
|
178
|
+
}
|
|
179
|
+
export type IEventHandlerContext = {
|
|
180
|
+
logger: Logger;
|
|
181
|
+
client: PgClientLike;
|
|
182
|
+
subscriptionId: string;
|
|
183
|
+
/** registered name of the handler */
|
|
184
|
+
name: string;
|
|
185
|
+
extra?: unknown;
|
|
186
|
+
};
|
|
187
|
+
export type IEventHandler<T extends IEventData = IEventData> = (item: IReadEvent<T>, ctx: IEventHandlerContext) => Promise<void>;
|
|
188
|
+
export type IRetryEventPayload = {
|
|
189
|
+
ids: string[];
|
|
190
|
+
handlerName: string;
|
|
191
|
+
retryNumber: number;
|
|
192
|
+
};
|
|
193
|
+
type SSESubscriptionOpts = Pick<RegisterSubscriptionParams, 'conditionsSql' | 'params'>;
|
|
194
|
+
export type SSERequestHandlerOpts = {
|
|
195
|
+
getSubscriptionOpts(req: IncomingMessage): Promise<SSESubscriptionOpts> | SSESubscriptionOpts;
|
|
196
|
+
/**
|
|
197
|
+
* Maximum interval to replay events for an SSE subscription.
|
|
198
|
+
* @default 5 minutes
|
|
199
|
+
*/
|
|
200
|
+
maxReplayIntervalMs?: number;
|
|
201
|
+
/**
|
|
202
|
+
* Max number of events to replay for an SSE subscription.
|
|
203
|
+
* Set to 0 to disable replaying events.
|
|
204
|
+
* @default 1000
|
|
205
|
+
*/
|
|
206
|
+
maxReplayEvents?: number;
|
|
207
|
+
jsonifier?: JSONifier;
|
|
208
|
+
};
|
|
209
|
+
export type IRetryHandlerOpts = {
|
|
210
|
+
retriesS: number[];
|
|
211
|
+
};
|
|
212
|
+
export interface JSONifier {
|
|
213
|
+
stringify(data: unknown): string;
|
|
214
|
+
parse(data: string): unknown;
|
|
215
|
+
}
|
|
216
|
+
export type ITableMutationEventData<T, N extends string> = {
|
|
217
|
+
topic: `${N}.insert`;
|
|
218
|
+
payload: T;
|
|
219
|
+
metadata: {};
|
|
220
|
+
} | {
|
|
221
|
+
topic: `${N}.delete`;
|
|
222
|
+
payload: T;
|
|
223
|
+
metadata: {};
|
|
224
|
+
} | {
|
|
225
|
+
topic: `${N}.update`;
|
|
226
|
+
/**
|
|
227
|
+
* The fields that were updated in the row
|
|
228
|
+
*/
|
|
229
|
+
payload: Partial<T>;
|
|
230
|
+
metadata: {
|
|
231
|
+
old: T;
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
export {};
|
package/lib/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CreateTopicalSubscriptionOpts, IEventData, RegisterSubscriptionParams } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the date from a message ID, same as the PG function
|
|
4
|
+
*/
|
|
5
|
+
export declare function getDateFromMessageId(messageId: string): Date | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Extract the date from a subscription ID
|
|
8
|
+
*/
|
|
9
|
+
export declare function getCreateDateFromSubscriptionId(id: string): Date | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Creates subscription params for a subscription that matches
|
|
12
|
+
* 1 or more topics. Also supports partitioning the subscription
|
|
13
|
+
* such that only a subset of messages are received.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createTopicalSubscriptionParams<T extends IEventData>({ topics, partition, additionalFilters, additionalParams, ...rest }: CreateTopicalSubscriptionOpts<T>): RegisterSubscriptionParams;
|
|
16
|
+
/**
|
|
17
|
+
* Get an environment variable as a number
|
|
18
|
+
*/
|
|
19
|
+
export declare function getEnvNumber(key: string, defaultValue?: number): number;
|
package/lib/utils.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getDateFromMessageId = getDateFromMessageId;
|
|
7
|
-
exports.getCreateDateFromSubscriptionId = getCreateDateFromSubscriptionId;
|
|
8
|
-
exports.createTopicalSubscriptionParams = createTopicalSubscriptionParams;
|
|
9
|
-
exports.getEnvNumber = getEnvNumber;
|
|
10
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
1
|
+
import assert from 'node:assert';
|
|
11
2
|
/**
|
|
12
3
|
* Extract the date from a message ID, same as the PG function
|
|
13
4
|
*/
|
|
14
|
-
function getDateFromMessageId(messageId) {
|
|
5
|
+
export function getDateFromMessageId(messageId) {
|
|
15
6
|
if (!messageId.startsWith('pm')) {
|
|
16
7
|
return undefined;
|
|
17
8
|
}
|
|
@@ -25,7 +16,7 @@ function getDateFromMessageId(messageId) {
|
|
|
25
16
|
/**
|
|
26
17
|
* Extract the date from a subscription ID
|
|
27
18
|
*/
|
|
28
|
-
function getCreateDateFromSubscriptionId(id) {
|
|
19
|
+
export function getCreateDateFromSubscriptionId(id) {
|
|
29
20
|
if (!id.startsWith('su')) {
|
|
30
21
|
return undefined;
|
|
31
22
|
}
|
|
@@ -36,8 +27,8 @@ function getCreateDateFromSubscriptionId(id) {
|
|
|
36
27
|
* 1 or more topics. Also supports partitioning the subscription
|
|
37
28
|
* such that only a subset of messages are received.
|
|
38
29
|
*/
|
|
39
|
-
function createTopicalSubscriptionParams({ topics, partition, additionalFilters = {}, additionalParams = {}, ...rest }) {
|
|
40
|
-
(
|
|
30
|
+
export function createTopicalSubscriptionParams({ topics, partition, additionalFilters = {}, additionalParams = {}, ...rest }) {
|
|
31
|
+
assert(topics.length > 0, 'At least one topic must be provided');
|
|
41
32
|
const filters = { ...additionalFilters };
|
|
42
33
|
filters['topics'] ||= 'ARRAY[e.topic]';
|
|
43
34
|
if (partition) {
|
|
@@ -54,7 +45,7 @@ function createTopicalSubscriptionParams({ topics, partition, additionalFilters
|
|
|
54
45
|
/**
|
|
55
46
|
* Get an environment variable as a number
|
|
56
47
|
*/
|
|
57
|
-
function getEnvNumber(key, defaultValue = 0) {
|
|
48
|
+
export function getEnvNumber(key, defaultValue = 0) {
|
|
58
49
|
const num = +(process.env[key] || defaultValue);
|
|
59
50
|
if (isNaN(num) || !isFinite(num)) {
|
|
60
51
|
return defaultValue;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IEventData, IEventHandler, PgmbWebhookOpts } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Create a handler that sends events to a webhook URL via HTTP POST.
|
|
4
|
+
* @param url Where to send the webhook requests
|
|
5
|
+
*/
|
|
6
|
+
export declare function createWebhookHandler<T extends IEventData>({ timeoutMs, headers, retryOpts, jsonifier, serialiseEvent }: Partial<PgmbWebhookOpts<T>>): IEventHandler;
|
package/lib/webhook-handler.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createWebhookHandler = createWebhookHandler;
|
|
7
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
-
const node_crypto_1 = require("node:crypto");
|
|
9
|
-
const retry_handler_ts_1 = require("./retry-handler.js");
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createRetryHandler } from "./retry-handler.js";
|
|
10
4
|
/**
|
|
11
5
|
* Create a handler that sends events to a webhook URL via HTTP POST.
|
|
12
6
|
* @param url Where to send the webhook requests
|
|
13
7
|
*/
|
|
14
|
-
function createWebhookHandler({ timeoutMs = 5_000, headers, retryOpts = {
|
|
8
|
+
export function createWebhookHandler({ timeoutMs = 5_000, headers, retryOpts = {
|
|
15
9
|
// retry after 5 minutes, then after 30 minutes
|
|
16
10
|
retriesS: [5 * 60, 30 * 60]
|
|
17
11
|
}, jsonifier = JSON, serialiseEvent = createSimpleSerialiser(jsonifier) }) {
|
|
18
12
|
const handler = async (ev, { logger, extra }) => {
|
|
19
|
-
(
|
|
13
|
+
assert(typeof extra === 'object'
|
|
20
14
|
&& extra !== null
|
|
21
15
|
&& 'url' in extra
|
|
22
16
|
&& (typeof extra.url === 'string'
|
|
@@ -51,10 +45,10 @@ function createWebhookHandler({ timeoutMs = 5_000, headers, retryOpts = {
|
|
|
51
45
|
if (!retryOpts) {
|
|
52
46
|
return handler;
|
|
53
47
|
}
|
|
54
|
-
return
|
|
48
|
+
return createRetryHandler(retryOpts, handler);
|
|
55
49
|
}
|
|
56
50
|
function getIdempotencyKeyHeader(ev) {
|
|
57
|
-
const hasher =
|
|
51
|
+
const hasher = createHash('sha256');
|
|
58
52
|
for (const item of ev.items) {
|
|
59
53
|
hasher.update(item.id);
|
|
60
54
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haathie/pgmb",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "PG message broker, with a type-safe typescript client with built-in webhook & SSE support.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
"import": "./src/index.ts",
|
|
12
|
-
"types": "./src/index.ts",
|
|
13
|
-
"require": "./lib/index.js"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "lib/index.js",
|
|
16
11
|
"repository": "https://github.com/haathie/pgmb",
|
|
17
12
|
"scripts": {
|
|
18
13
|
"test": "TZ=UTC NODE_ENV=test node --env-file ./.env.test --test tests/*.test.ts",
|
|
@@ -40,7 +35,6 @@
|
|
|
40
35
|
},
|
|
41
36
|
"files": [
|
|
42
37
|
"lib",
|
|
43
|
-
"src",
|
|
44
38
|
"sql"
|
|
45
39
|
],
|
|
46
40
|
"keywords": [
|
package/sql/pgmb-0.2.0-0.2.8.sql
CHANGED
|
@@ -5,7 +5,6 @@ ALTER TYPE config_type ADD VALUE 'pg_cron_poll_for_events_cron';
|
|
|
5
5
|
ALTER TYPE config_type ADD VALUE 'pg_cron_partition_maintenance_cron';
|
|
6
6
|
|
|
7
7
|
INSERT INTO config(id, value) VALUES
|
|
8
|
-
('poll_chunk_size', '10000'),
|
|
9
8
|
('pg_cron_poll_for_events_cron', '1 second'),
|
|
10
9
|
-- every 30 minutes
|
|
11
10
|
('pg_cron_partition_maintenance_cron', '*/30 * * * *');
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import assert from 'assert'
|
|
2
|
-
|
|
3
|
-
type AAResult<T> = IteratorResult<T>
|
|
4
|
-
|
|
5
|
-
export class AbortableAsyncIterator<T> implements AsyncIterableIterator<T> {
|
|
6
|
-
readonly signal: AbortSignal
|
|
7
|
-
readonly onEnd: () => void
|
|
8
|
-
|
|
9
|
-
ended = false
|
|
10
|
-
|
|
11
|
-
#resolve: (() => void) | undefined
|
|
12
|
-
#reject: ((reason?: unknown) => void) | undefined
|
|
13
|
-
#queue: T[] = []
|
|
14
|
-
#locked = false
|
|
15
|
-
|
|
16
|
-
constructor(signal: AbortSignal, onEnd: () => void = () => {}) {
|
|
17
|
-
this.signal = signal
|
|
18
|
-
this.onEnd = onEnd
|
|
19
|
-
signal.addEventListener('abort', this.#onAbort)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async next(): Promise<AAResult<T>> {
|
|
23
|
-
assert(!this.ended, 'Iterator has already been completed')
|
|
24
|
-
assert(!this.#locked, 'Concurrent calls to next() are not allowed')
|
|
25
|
-
|
|
26
|
-
let nextItem = this.#queue.shift()
|
|
27
|
-
if(nextItem) {
|
|
28
|
-
return { value: nextItem, done: false }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
this.#locked = true
|
|
32
|
-
try {
|
|
33
|
-
await this.#setupNextPromise()
|
|
34
|
-
} finally {
|
|
35
|
-
this.#locked = false
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
nextItem = this.#queue.shift()
|
|
39
|
-
if(nextItem) {
|
|
40
|
-
return { value: nextItem, done: false }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { value: undefined, done: true }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
enqueue(value: T) {
|
|
47
|
-
assert(!this.ended, 'Iterator has already been completed')
|
|
48
|
-
this.#queue.push(value)
|
|
49
|
-
this.#resolve?.()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
throw(reason?: unknown): Promise<AAResult<T>> {
|
|
53
|
-
this.signal.throwIfAborted()
|
|
54
|
-
this.#reject?.(reason)
|
|
55
|
-
this.#end()
|
|
56
|
-
return Promise.resolve({ done: true, value: undefined })
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return(value?: any): Promise<AAResult<T>> {
|
|
60
|
-
this.#resolve?.()
|
|
61
|
-
this.#end()
|
|
62
|
-
return Promise.resolve({ done: true, value })
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
#setupNextPromise() {
|
|
66
|
-
return new Promise<void>((resolve, reject) => {
|
|
67
|
-
this.#resolve = () => {
|
|
68
|
-
resolve()
|
|
69
|
-
this.#cleanupTask()
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.#reject = err => {
|
|
73
|
-
reject(err)
|
|
74
|
-
this.#cleanupTask()
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
#cleanupTask() {
|
|
80
|
-
this.#resolve = undefined
|
|
81
|
-
this.#reject = undefined
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
#onAbort = (reason: any) => {
|
|
85
|
-
this.#reject?.(reason)
|
|
86
|
-
this.#end()
|
|
87
|
-
this.ended = true
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#end() {
|
|
91
|
-
this.signal.removeEventListener('abort', this.#onAbort)
|
|
92
|
-
this.onEnd()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
[Symbol.asyncIterator]() {
|
|
96
|
-
return this
|
|
97
|
-
}
|
|
98
|
-
}
|