@backstage/plugin-events-backend 0.3.13-next.0 → 0.3.13

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/alpha/package.json +1 -1
  4. package/dist/alpha.cjs.js +3 -986
  5. package/dist/alpha.cjs.js.map +1 -1
  6. package/dist/alpha.d.ts +3 -7
  7. package/dist/index.cjs.js +9 -61
  8. package/dist/index.cjs.js.map +1 -1
  9. package/dist/index.d.ts +10 -2
  10. package/dist/schema/openapi.generated.cjs.js +272 -0
  11. package/dist/schema/openapi.generated.cjs.js.map +1 -0
  12. package/dist/service/DefaultEventBroker.cjs.js +32 -0
  13. package/dist/service/DefaultEventBroker.cjs.js.map +1 -0
  14. package/dist/service/EventsBackend.cjs.js +36 -0
  15. package/dist/service/EventsBackend.cjs.js.map +1 -0
  16. package/dist/service/EventsPlugin.cjs.js +98 -0
  17. package/dist/service/EventsPlugin.cjs.js.map +1 -0
  18. package/dist/{cjs/HttpPostIngressEventPublisher-D6pQ6awS.cjs.js → service/http/HttpPostIngressEventPublisher.cjs.js} +3 -18
  19. package/dist/service/http/HttpPostIngressEventPublisher.cjs.js.map +1 -0
  20. package/dist/service/http/validation/RequestValidationContextImpl.cjs.js +20 -0
  21. package/dist/service/http/validation/RequestValidationContextImpl.cjs.js.map +1 -0
  22. package/dist/service/hub/DatabaseEventBusStore.cjs.js +410 -0
  23. package/dist/service/hub/DatabaseEventBusStore.cjs.js.map +1 -0
  24. package/dist/service/hub/MemoryEventBusStore.cjs.js +106 -0
  25. package/dist/service/hub/MemoryEventBusStore.cjs.js.map +1 -0
  26. package/dist/service/hub/createEventBusRouter.cjs.js +130 -0
  27. package/dist/service/hub/createEventBusRouter.cjs.js.map +1 -0
  28. package/package.json +11 -10
  29. package/dist/cjs/HttpPostIngressEventPublisher-D6pQ6awS.cjs.js.map +0 -1
@@ -0,0 +1,410 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var errors = require('@backstage/errors');
5
+ var types = require('@backstage/types');
6
+
7
+ const WINDOW_MAX_COUNT_DEFAULT = 1e4;
8
+ const WINDOW_MIN_AGE_DEFAULT = { minutes: 10 };
9
+ const WINDOW_MAX_AGE_DEFAULT = { days: 1 };
10
+ const MAX_BATCH_SIZE = 10;
11
+ const LISTENER_CONNECTION_TIMEOUT_MS = 6e4;
12
+ const KEEPALIVE_INTERVAL_MS = 6e4;
13
+ const TABLE_EVENTS = "event_bus_events";
14
+ const TABLE_SUBSCRIPTIONS = "event_bus_subscriptions";
15
+ const TOPIC_PUBLISH = "event_bus_publish";
16
+ function creatorId(credentials) {
17
+ return `service=${credentials.principal.subject}`;
18
+ }
19
+ const migrationsDir = backendPluginApi.resolvePackagePath(
20
+ "@backstage/plugin-events-backend",
21
+ "migrations"
22
+ );
23
+ class DatabaseEventBusListener {
24
+ #client;
25
+ #logger;
26
+ #listeners = /* @__PURE__ */ new Set();
27
+ #isShuttingDown = false;
28
+ #connPromise;
29
+ #connTimeout;
30
+ #keepaliveInterval;
31
+ constructor(client, logger) {
32
+ this.#client = client;
33
+ this.#logger = logger.child({ type: "DatabaseEventBusListener" });
34
+ }
35
+ async setupListener(topics, signal) {
36
+ if (this.#connTimeout) {
37
+ clearTimeout(this.#connTimeout);
38
+ this.#connTimeout = void 0;
39
+ }
40
+ await this.#ensureConnection();
41
+ const updatePromise = new Promise((resolve, reject) => {
42
+ const listener = {
43
+ topics,
44
+ resolve(result) {
45
+ resolve(result);
46
+ cleanup();
47
+ },
48
+ reject(err) {
49
+ reject(err);
50
+ cleanup();
51
+ }
52
+ };
53
+ this.#listeners.add(listener);
54
+ const onAbort = () => {
55
+ this.#listeners.delete(listener);
56
+ this.#maybeTimeoutConnection();
57
+ reject(signal.reason);
58
+ cleanup();
59
+ };
60
+ function cleanup() {
61
+ signal.removeEventListener("abort", onAbort);
62
+ }
63
+ signal.addEventListener("abort", onAbort);
64
+ });
65
+ updatePromise.catch(() => {
66
+ });
67
+ return { waitForUpdate: () => updatePromise };
68
+ }
69
+ async shutdown() {
70
+ if (this.#isShuttingDown) {
71
+ return;
72
+ }
73
+ this.#isShuttingDown = true;
74
+ const conn = await this.#connPromise?.catch(() => void 0);
75
+ if (conn) {
76
+ this.#destroyConnection(conn);
77
+ }
78
+ }
79
+ #handleNotify(topic) {
80
+ this.#logger.debug(`Listener received notification for topic '${topic}'`);
81
+ for (const l of this.#listeners) {
82
+ if (l.topics.has(topic)) {
83
+ l.resolve({ topic });
84
+ this.#listeners.delete(l);
85
+ }
86
+ }
87
+ this.#maybeTimeoutConnection();
88
+ }
89
+ // We don't try to reconnect on error, instead we notify all listeners and let
90
+ // them try to establish a new connection
91
+ #handleError(error) {
92
+ this.#logger.error(
93
+ `Listener connection failed, notifying all listeners`,
94
+ error
95
+ );
96
+ for (const l of this.#listeners) {
97
+ l.reject(new Error("Listener connection failed"));
98
+ }
99
+ this.#listeners.clear();
100
+ this.#maybeTimeoutConnection();
101
+ }
102
+ #maybeTimeoutConnection() {
103
+ if (this.#listeners.size === 0 && !this.#connTimeout) {
104
+ this.#connTimeout = setTimeout(() => {
105
+ this.#connTimeout = void 0;
106
+ this.#connPromise?.then((conn) => {
107
+ this.#logger.info("Listener connection timed out, destroying");
108
+ this.#connPromise = void 0;
109
+ this.#destroyConnection(conn);
110
+ });
111
+ }, LISTENER_CONNECTION_TIMEOUT_MS);
112
+ }
113
+ }
114
+ #destroyConnection(conn) {
115
+ if (this.#keepaliveInterval) {
116
+ clearInterval(this.#keepaliveInterval);
117
+ this.#keepaliveInterval = void 0;
118
+ }
119
+ this.#client.destroyRawConnection(conn).catch((error) => {
120
+ this.#logger.error(`Listener failed to destroy connection`, error);
121
+ });
122
+ conn.removeAllListeners();
123
+ }
124
+ async #ensureConnection() {
125
+ if (this.#isShuttingDown) {
126
+ throw new Error("Listener is shutting down");
127
+ }
128
+ if (this.#connPromise) {
129
+ await this.#connPromise;
130
+ return;
131
+ }
132
+ this.#connPromise = Promise.resolve().then(async () => {
133
+ const conn = await this.#client.acquireRawConnection();
134
+ try {
135
+ await conn.query(`LISTEN ${TOPIC_PUBLISH}`);
136
+ if (this.#keepaliveInterval) {
137
+ clearInterval(this.#keepaliveInterval);
138
+ }
139
+ this.#keepaliveInterval = setInterval(() => {
140
+ conn.query("select 1").catch((error) => {
141
+ this.#connPromise = void 0;
142
+ this.#destroyConnection(conn);
143
+ this.#handleError(new errors.ForwardedError("Keepalive failed", error));
144
+ });
145
+ }, KEEPALIVE_INTERVAL_MS);
146
+ conn.on("notification", (event) => {
147
+ this.#handleNotify(event.payload);
148
+ });
149
+ conn.on("error", (error) => {
150
+ this.#connPromise = void 0;
151
+ this.#destroyConnection(conn);
152
+ this.#handleError(error);
153
+ });
154
+ conn.on("end", (error) => {
155
+ this.#connPromise = void 0;
156
+ this.#destroyConnection(conn);
157
+ this.#handleError(
158
+ error ?? new Error("Connection ended unexpectedly")
159
+ );
160
+ });
161
+ return conn;
162
+ } catch (error) {
163
+ this.#destroyConnection(conn);
164
+ throw error;
165
+ }
166
+ });
167
+ try {
168
+ await this.#connPromise;
169
+ } catch (error) {
170
+ this.#connPromise = void 0;
171
+ throw error;
172
+ }
173
+ }
174
+ }
175
+ class DatabaseEventBusStore {
176
+ static async create(options) {
177
+ const db = await options.database.getClient();
178
+ if (db.client.config.client !== "pg") {
179
+ throw new Error(
180
+ `DatabaseEventBusStore only supports PostgreSQL, got '${db.client.config.client}'`
181
+ );
182
+ }
183
+ if (!options.database.migrations?.skip) {
184
+ await db.migrate.latest({
185
+ directory: migrationsDir
186
+ });
187
+ }
188
+ const listener = new DatabaseEventBusListener(db.client, options.logger);
189
+ const store = new DatabaseEventBusStore(
190
+ db,
191
+ options.logger,
192
+ listener,
193
+ options.window?.maxCount ?? WINDOW_MAX_COUNT_DEFAULT,
194
+ types.durationToMilliseconds(options.window?.minAge ?? WINDOW_MIN_AGE_DEFAULT),
195
+ types.durationToMilliseconds(options.window?.maxAge ?? WINDOW_MAX_AGE_DEFAULT)
196
+ );
197
+ await options.scheduler.scheduleTask({
198
+ id: "event-bus-cleanup",
199
+ frequency: { seconds: 10 },
200
+ timeout: { minutes: 1 },
201
+ initialDelay: { seconds: 10 },
202
+ fn: () => store.#cleanup()
203
+ });
204
+ options.lifecycle.addShutdownHook(async () => {
205
+ await listener.shutdown();
206
+ });
207
+ return store;
208
+ }
209
+ /** @internal */
210
+ static async forTest({
211
+ db,
212
+ logger,
213
+ minAge = 0,
214
+ maxAge = 1e4
215
+ }) {
216
+ await db.migrate.latest({ directory: migrationsDir });
217
+ const store = new DatabaseEventBusStore(
218
+ db,
219
+ logger,
220
+ new DatabaseEventBusListener(db.client, logger),
221
+ 5,
222
+ minAge,
223
+ maxAge
224
+ );
225
+ return Object.assign(store, { clean: () => store.#cleanup() });
226
+ }
227
+ #db;
228
+ #logger;
229
+ #listener;
230
+ #windowMaxCount;
231
+ #windowMinAge;
232
+ #windowMaxAge;
233
+ constructor(db, logger, listener, windowMaxCount, windowMinAge, windowMaxAge) {
234
+ this.#db = db;
235
+ this.#logger = logger;
236
+ this.#listener = listener;
237
+ this.#windowMaxCount = windowMaxCount;
238
+ this.#windowMinAge = windowMinAge;
239
+ this.#windowMaxAge = windowMaxAge;
240
+ }
241
+ async publish(options) {
242
+ const topic = options.event.topic;
243
+ const notifiedSubscribers = options.notifiedSubscribers ?? [];
244
+ const result = await this.#db.into(
245
+ this.#db.raw("?? (??, ??, ??, ??)", [
246
+ TABLE_EVENTS,
247
+ // These are the rows that we insert, and should match the SELECT below
248
+ "created_by",
249
+ "topic",
250
+ "data_json",
251
+ "notified_subscribers"
252
+ ])
253
+ ).insert(
254
+ (q) => q.select(
255
+ this.#db.raw("?", [creatorId(options.credentials)]),
256
+ this.#db.raw("?", [topic]),
257
+ this.#db.raw("?", [
258
+ JSON.stringify({
259
+ payload: options.event.eventPayload,
260
+ metadata: options.event.metadata
261
+ })
262
+ ]),
263
+ this.#db.raw("?", [notifiedSubscribers])
264
+ ).from(TABLE_SUBSCRIPTIONS).whereNotIn("id", notifiedSubscribers).andWhere(this.#db.raw("? = ANY(topics)", [topic])).having(this.#db.raw("count(*)"), ">", 0)
265
+ // Check if there are any results
266
+ ).returning("id");
267
+ if (result.length === 0) {
268
+ return void 0;
269
+ }
270
+ if (result.length > 1) {
271
+ throw new Error(
272
+ `Failed to insert event, unexpectedly updated ${result.length} rows`
273
+ );
274
+ }
275
+ const [{ id }] = result;
276
+ const notifyResult = await this.#db.select(
277
+ this.#db.raw(`pg_notify(?, ?)`, [TOPIC_PUBLISH, topic])
278
+ );
279
+ if (notifyResult?.length !== 1) {
280
+ this.#logger.warn(
281
+ `Failed to notify subscribers of event with ID '${id}' on topic '${topic}'`
282
+ );
283
+ }
284
+ return { eventId: id };
285
+ }
286
+ async upsertSubscription(id, topics, credentials) {
287
+ const [{ max: maxId }] = await this.#db(TABLE_EVENTS).max("id");
288
+ const result = await this.#db(TABLE_SUBSCRIPTIONS).insert({
289
+ id,
290
+ created_by: creatorId(credentials),
291
+ updated_at: this.#db.fn.now(),
292
+ topics,
293
+ read_until: maxId || 0
294
+ }).onConflict("id").merge(["created_by", "topics", "updated_at"]).returning("*");
295
+ if (result.length !== 1) {
296
+ throw new Error(
297
+ `Failed to upsert subscription, updated ${result.length} rows`
298
+ );
299
+ }
300
+ }
301
+ async readSubscription(id) {
302
+ const { rows: result } = await this.#db.raw(
303
+ `
304
+ WITH subscription AS (
305
+ SELECT topics, read_until
306
+ FROM event_bus_subscriptions
307
+ WHERE id = :id
308
+ FOR UPDATE
309
+ ),
310
+ selected_events AS (
311
+ SELECT event_bus_events.*
312
+ FROM event_bus_events
313
+ INNER JOIN subscription
314
+ ON event_bus_events.topic = ANY(subscription.topics)
315
+ WHERE event_bus_events.id > subscription.read_until
316
+ AND NOT :id = ANY(event_bus_events.notified_subscribers)
317
+ ORDER BY event_bus_events.id ASC LIMIT :limit
318
+ ),
319
+ last_event_id AS (
320
+ SELECT max(id) AS last_event_id
321
+ FROM selected_events
322
+ ),
323
+ events_array AS (
324
+ SELECT json_agg(row_to_json(selected_events)) AS events
325
+ FROM selected_events
326
+ )
327
+ UPDATE event_bus_subscriptions
328
+ SET read_until = COALESCE(last_event_id, (SELECT MAX(id) FROM event_bus_events), 0)
329
+ FROM events_array, last_event_id
330
+ WHERE event_bus_subscriptions.id = :id
331
+ RETURNING events_array.events
332
+ `,
333
+ { id, limit: MAX_BATCH_SIZE }
334
+ );
335
+ if (result.length === 0) {
336
+ throw new errors.NotFoundError(`Subscription with ID '${id}' not found`);
337
+ } else if (result.length > 1) {
338
+ throw new Error(
339
+ `Failed to read subscription, unexpectedly updated ${result.length} rows`
340
+ );
341
+ }
342
+ const rows = result[0].events;
343
+ if (!rows || rows.length === 0) {
344
+ return { events: [] };
345
+ }
346
+ return {
347
+ events: rows.map((row) => {
348
+ const { payload, metadata } = JSON.parse(row.data_json);
349
+ return {
350
+ topic: row.topic,
351
+ eventPayload: payload,
352
+ metadata
353
+ };
354
+ })
355
+ };
356
+ }
357
+ async setupListener(subscriptionId, options) {
358
+ const result = await this.#db(TABLE_SUBSCRIPTIONS).select("topics").where({ id: subscriptionId }).first();
359
+ if (!result) {
360
+ throw new errors.NotFoundError(
361
+ `Subscription with ID '${subscriptionId}' not found`
362
+ );
363
+ }
364
+ options.signal.throwIfAborted();
365
+ return this.#listener.setupListener(
366
+ new Set(result.topics ?? []),
367
+ options.signal
368
+ );
369
+ }
370
+ async #cleanup() {
371
+ try {
372
+ const eventCount = await this.#db(TABLE_EVENTS).delete().orWhere(
373
+ (inner) => inner.whereIn(
374
+ "id",
375
+ this.#db.select("id").from(TABLE_EVENTS).orderBy("id", "desc").offset(this.#windowMaxCount)
376
+ ).andWhere(
377
+ "created_at",
378
+ "<",
379
+ new Date(Date.now() - this.#windowMinAge)
380
+ )
381
+ ).orWhere("created_at", "<", new Date(Date.now() - this.#windowMaxAge));
382
+ if (eventCount > 0) {
383
+ this.#logger.info(
384
+ `Event cleanup resulted in ${eventCount} old events being deleted`
385
+ );
386
+ }
387
+ } catch (error) {
388
+ this.#logger.error("Event cleanup failed", error);
389
+ }
390
+ try {
391
+ const [{ min: minId }] = await this.#db(TABLE_EVENTS).min("id");
392
+ let subscriberCount;
393
+ if (minId === null) {
394
+ subscriberCount = await this.#db(TABLE_SUBSCRIPTIONS).delete();
395
+ } else {
396
+ subscriberCount = await this.#db(TABLE_SUBSCRIPTIONS).delete().where("read_until", "<", minId - 1);
397
+ }
398
+ if (subscriberCount > 0) {
399
+ this.#logger.info(
400
+ `Subscription cleanup resulted in ${subscriberCount} stale subscribers being deleted`
401
+ );
402
+ }
403
+ } catch (error) {
404
+ this.#logger.error("Subscription cleanup failed", error);
405
+ }
406
+ }
407
+ }
408
+
409
+ exports.DatabaseEventBusStore = DatabaseEventBusStore;
410
+ //# sourceMappingURL=DatabaseEventBusStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DatabaseEventBusStore.cjs.js","sources":["../../../src/service/hub/DatabaseEventBusStore.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { EventParams } from '@backstage/plugin-events-node';\nimport { EventBusStore } from './types';\nimport { Knex } from 'knex';\nimport {\n BackstageCredentials,\n BackstageServicePrincipal,\n DatabaseService,\n LifecycleService,\n LoggerService,\n SchedulerService,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\nimport { ForwardedError, NotFoundError } from '@backstage/errors';\nimport { HumanDuration, durationToMilliseconds } from '@backstage/types';\n\nconst WINDOW_MAX_COUNT_DEFAULT = 10_000;\nconst WINDOW_MIN_AGE_DEFAULT = { minutes: 10 };\nconst WINDOW_MAX_AGE_DEFAULT = { days: 1 };\n\nconst MAX_BATCH_SIZE = 10;\nconst LISTENER_CONNECTION_TIMEOUT_MS = 60_000;\nconst KEEPALIVE_INTERVAL_MS = 60_000;\n\nconst TABLE_EVENTS = 'event_bus_events';\nconst TABLE_SUBSCRIPTIONS = 'event_bus_subscriptions';\nconst TOPIC_PUBLISH = 'event_bus_publish';\n\ntype EventsRow = {\n id: string;\n created_by: string;\n created_at: Date;\n topic: string;\n data_json: string;\n notified_subscribers: string[];\n};\n\ntype SubscriptionsRow = {\n id: string;\n created_by: string;\n created_at: Date;\n updated_at: Date;\n read_until: string;\n topics: string[];\n};\n\nfunction creatorId(\n credentials: BackstageCredentials<BackstageServicePrincipal>,\n) {\n return `service=${credentials.principal.subject}`;\n}\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-events-backend',\n 'migrations',\n);\n\ninterface InternalDbClient {\n acquireRawConnection(): Promise<InternalDbConnection>;\n destroyRawConnection(conn: InternalDbConnection): Promise<void>;\n}\n\ninterface InternalDbConnection {\n query(sql: string): Promise<void>;\n end(): Promise<void>;\n on(\n event: 'notification',\n listener: (event: { channel: string; payload: string }) => void,\n ): void;\n on(event: 'error', listener: (error: Error) => void): void;\n on(event: 'end', listener: (error?: Error) => void): void;\n removeAllListeners(): void;\n}\n\n// This internal class manages a single connection to the database that all listeners share\nclass DatabaseEventBusListener {\n readonly #client: InternalDbClient;\n readonly #logger: LoggerService;\n\n readonly #listeners = new Set<{\n topics: Set<string>;\n resolve: (result: { topic: string }) => void;\n reject: (error: Error) => void;\n }>();\n\n #isShuttingDown = false;\n #connPromise?: Promise<InternalDbConnection>;\n #connTimeout?: NodeJS.Timeout;\n #keepaliveInterval?: NodeJS.Timeout;\n\n constructor(client: InternalDbClient, logger: LoggerService) {\n this.#client = client;\n this.#logger = logger.child({ type: 'DatabaseEventBusListener' });\n }\n\n async setupListener(\n topics: Set<string>,\n signal: AbortSignal,\n ): Promise<{ waitForUpdate(): Promise<{ topic: string }> }> {\n if (this.#connTimeout) {\n clearTimeout(this.#connTimeout);\n this.#connTimeout = undefined;\n }\n\n await this.#ensureConnection();\n\n const updatePromise = new Promise<{ topic: string }>((resolve, reject) => {\n const listener = {\n topics,\n resolve(result: { topic: string }) {\n resolve(result);\n cleanup();\n },\n reject(err: Error) {\n reject(err);\n cleanup();\n },\n };\n this.#listeners.add(listener);\n\n const onAbort = () => {\n this.#listeners.delete(listener);\n this.#maybeTimeoutConnection();\n reject(signal.reason);\n cleanup();\n };\n\n function cleanup() {\n signal.removeEventListener('abort', onAbort);\n }\n\n signal.addEventListener('abort', onAbort);\n });\n\n // Ignore unhandled rejections\n updatePromise.catch(() => {});\n\n return { waitForUpdate: () => updatePromise };\n }\n\n async shutdown() {\n if (this.#isShuttingDown) {\n return;\n }\n this.#isShuttingDown = true;\n const conn = await this.#connPromise?.catch(() => undefined);\n if (conn) {\n this.#destroyConnection(conn);\n }\n }\n\n #handleNotify(topic: string) {\n this.#logger.debug(`Listener received notification for topic '${topic}'`);\n for (const l of this.#listeners) {\n if (l.topics.has(topic)) {\n l.resolve({ topic });\n this.#listeners.delete(l);\n }\n }\n this.#maybeTimeoutConnection();\n }\n\n // We don't try to reconnect on error, instead we notify all listeners and let\n // them try to establish a new connection\n #handleError(error: Error) {\n this.#logger.error(\n `Listener connection failed, notifying all listeners`,\n error,\n );\n for (const l of this.#listeners) {\n l.reject(new Error('Listener connection failed'));\n }\n this.#listeners.clear();\n this.#maybeTimeoutConnection();\n }\n\n #maybeTimeoutConnection() {\n // If we don't have any listeners, destroy the connection after a timeout\n if (this.#listeners.size === 0 && !this.#connTimeout) {\n this.#connTimeout = setTimeout(() => {\n this.#connTimeout = undefined;\n this.#connPromise?.then(conn => {\n this.#logger.info('Listener connection timed out, destroying');\n this.#connPromise = undefined;\n this.#destroyConnection(conn);\n });\n }, LISTENER_CONNECTION_TIMEOUT_MS);\n }\n }\n\n #destroyConnection(conn: InternalDbConnection) {\n if (this.#keepaliveInterval) {\n clearInterval(this.#keepaliveInterval);\n this.#keepaliveInterval = undefined;\n }\n this.#client.destroyRawConnection(conn).catch(error => {\n this.#logger.error(`Listener failed to destroy connection`, error);\n });\n conn.removeAllListeners();\n }\n\n async #ensureConnection() {\n if (this.#isShuttingDown) {\n throw new Error('Listener is shutting down');\n }\n if (this.#connPromise) {\n await this.#connPromise;\n return;\n }\n this.#connPromise = Promise.resolve().then(async () => {\n const conn = await this.#client.acquireRawConnection();\n\n try {\n await conn.query(`LISTEN ${TOPIC_PUBLISH}`);\n\n // Set up a keepalive interval to make sure the connection stays alive\n if (this.#keepaliveInterval) {\n clearInterval(this.#keepaliveInterval);\n }\n this.#keepaliveInterval = setInterval(() => {\n conn.query('select 1').catch(error => {\n this.#connPromise = undefined;\n this.#destroyConnection(conn);\n this.#handleError(new ForwardedError('Keepalive failed', error));\n });\n }, KEEPALIVE_INTERVAL_MS);\n\n conn.on('notification', event => {\n this.#handleNotify(event.payload);\n });\n conn.on('error', error => {\n this.#connPromise = undefined;\n this.#destroyConnection(conn);\n this.#handleError(error);\n });\n conn.on('end', error => {\n this.#connPromise = undefined;\n this.#destroyConnection(conn);\n this.#handleError(\n error ?? new Error('Connection ended unexpectedly'),\n );\n });\n return conn;\n } catch (error) {\n this.#destroyConnection(conn);\n throw error;\n }\n });\n try {\n await this.#connPromise;\n } catch (error) {\n this.#connPromise = undefined;\n throw error;\n }\n }\n}\n\nexport class DatabaseEventBusStore implements EventBusStore {\n static async create(options: {\n database: DatabaseService;\n logger: LoggerService;\n scheduler: SchedulerService;\n lifecycle: LifecycleService;\n window?: {\n /** Events within this range will never be deleted */\n minAge?: HumanDuration;\n /** Events outside of this age will always be deleted */\n maxAge?: HumanDuration;\n /** Events outside of this count will be deleted if they are outside the minAge window */\n maxCount?: number;\n };\n }): Promise<DatabaseEventBusStore> {\n const db = await options.database.getClient();\n\n if (db.client.config.client !== 'pg') {\n throw new Error(\n `DatabaseEventBusStore only supports PostgreSQL, got '${db.client.config.client}'`,\n );\n }\n\n if (!options.database.migrations?.skip) {\n await db.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n const listener = new DatabaseEventBusListener(db.client, options.logger);\n\n const store = new DatabaseEventBusStore(\n db,\n options.logger,\n listener,\n options.window?.maxCount ?? WINDOW_MAX_COUNT_DEFAULT,\n durationToMilliseconds(options.window?.minAge ?? WINDOW_MIN_AGE_DEFAULT),\n durationToMilliseconds(options.window?.maxAge ?? WINDOW_MAX_AGE_DEFAULT),\n );\n\n await options.scheduler.scheduleTask({\n id: 'event-bus-cleanup',\n frequency: { seconds: 10 },\n timeout: { minutes: 1 },\n initialDelay: { seconds: 10 },\n fn: () => store.#cleanup(),\n });\n\n options.lifecycle.addShutdownHook(async () => {\n await listener.shutdown();\n });\n\n return store;\n }\n\n /** @internal */\n static async forTest({\n db,\n logger,\n minAge = 0,\n maxAge = 10_000,\n }: {\n db: Knex;\n logger: LoggerService;\n minAge?: number;\n maxAge?: number;\n }) {\n await db.migrate.latest({ directory: migrationsDir });\n\n const store = new DatabaseEventBusStore(\n db,\n logger,\n new DatabaseEventBusListener(db.client, logger),\n 5,\n minAge,\n maxAge,\n );\n\n return Object.assign(store, { clean: () => store.#cleanup() });\n }\n\n readonly #db: Knex;\n readonly #logger: LoggerService;\n readonly #listener: DatabaseEventBusListener;\n readonly #windowMaxCount: number;\n readonly #windowMinAge: number;\n readonly #windowMaxAge: number;\n\n private constructor(\n db: Knex,\n logger: LoggerService,\n listener: DatabaseEventBusListener,\n windowMaxCount: number,\n windowMinAge: number,\n windowMaxAge: number,\n ) {\n this.#db = db;\n this.#logger = logger;\n this.#listener = listener;\n this.#windowMaxCount = windowMaxCount;\n this.#windowMinAge = windowMinAge;\n this.#windowMaxAge = windowMaxAge;\n }\n\n async publish(options: {\n event: EventParams;\n notifiedSubscribers?: string[];\n credentials: BackstageCredentials<BackstageServicePrincipal>;\n }): Promise<{ eventId: string } | undefined> {\n const topic = options.event.topic;\n const notifiedSubscribers = options.notifiedSubscribers ?? [];\n // This query inserts a new event into the database, but only if there are\n // subscribers to the topic that have not already been notified\n const result = await this.#db\n // There's no clean way to create a INSERT INTO .. SELECT with knex, so we end up with quite a lot of .raw(...)\n .into(\n this.#db.raw('?? (??, ??, ??, ??)', [\n TABLE_EVENTS,\n // These are the rows that we insert, and should match the SELECT below\n 'created_by',\n 'topic',\n 'data_json',\n 'notified_subscribers',\n ]),\n )\n .insert<EventsRow>(\n (q: Knex.QueryBuilder) =>\n q\n // We're not reading data to insert from anywhere else, just raw data\n .select(\n this.#db.raw('?', [creatorId(options.credentials)]),\n this.#db.raw('?', [topic]),\n this.#db.raw('?', [\n JSON.stringify({\n payload: options.event.eventPayload,\n metadata: options.event.metadata,\n }),\n ]),\n this.#db.raw('?', [notifiedSubscribers]),\n )\n // The rest of this query is to check whether there are any\n // subscribers that have not been notified yet\n .from(TABLE_SUBSCRIPTIONS)\n .whereNotIn('id', notifiedSubscribers) // Skip notified subscribers\n .andWhere(this.#db.raw('? = ANY(topics)', [topic])) // Match topic\n .having(this.#db.raw('count(*)'), '>', 0), // Check if there are any results\n )\n .returning<{ id: string }[]>('id');\n\n if (result.length === 0) {\n return undefined;\n }\n if (result.length > 1) {\n throw new Error(\n `Failed to insert event, unexpectedly updated ${result.length} rows`,\n );\n }\n\n const [{ id }] = result;\n\n // Notify other event bus instances that an event is available on the topic\n const notifyResult = await this.#db.select(\n this.#db.raw(`pg_notify(?, ?)`, [TOPIC_PUBLISH, topic]),\n );\n if (notifyResult?.length !== 1) {\n this.#logger.warn(\n `Failed to notify subscribers of event with ID '${id}' on topic '${topic}'`,\n );\n }\n\n return { eventId: id };\n }\n\n async upsertSubscription(\n id: string,\n topics: string[],\n credentials: BackstageCredentials<BackstageServicePrincipal>,\n ): Promise<void> {\n const [{ max: maxId }] = await this.#db(TABLE_EVENTS).max('id');\n const result = await this.#db<SubscriptionsRow>(TABLE_SUBSCRIPTIONS)\n .insert({\n id,\n created_by: creatorId(credentials),\n updated_at: this.#db.fn.now(),\n topics,\n read_until: maxId || 0,\n })\n .onConflict('id')\n .merge(['created_by', 'topics', 'updated_at'])\n .returning('*');\n\n if (result.length !== 1) {\n throw new Error(\n `Failed to upsert subscription, updated ${result.length} rows`,\n );\n }\n }\n\n async readSubscription(id: string): Promise<{ events: EventParams[] }> {\n // The below query selects the subscription we're reading from, locks it for\n // an update, reads events for the subscription up to the limit, and then\n // updates the pointer to the last read event.\n //\n // This is written as a plain SQL query to spare us all the horrors of\n // expressing this in knex.\n\n const { rows: result } = await this.#db.raw<{\n rows: [] | [{ events: EventsRow[] }];\n }>(\n `\n WITH subscription AS (\n SELECT topics, read_until\n FROM event_bus_subscriptions\n WHERE id = :id\n FOR UPDATE\n ),\n selected_events AS (\n SELECT event_bus_events.*\n FROM event_bus_events\n INNER JOIN subscription\n ON event_bus_events.topic = ANY(subscription.topics)\n WHERE event_bus_events.id > subscription.read_until\n AND NOT :id = ANY(event_bus_events.notified_subscribers)\n ORDER BY event_bus_events.id ASC LIMIT :limit\n ),\n last_event_id AS (\n SELECT max(id) AS last_event_id\n FROM selected_events\n ),\n events_array AS (\n SELECT json_agg(row_to_json(selected_events)) AS events\n FROM selected_events\n )\n UPDATE event_bus_subscriptions\n SET read_until = COALESCE(last_event_id, (SELECT MAX(id) FROM event_bus_events), 0)\n FROM events_array, last_event_id\n WHERE event_bus_subscriptions.id = :id\n RETURNING events_array.events\n `,\n { id, limit: MAX_BATCH_SIZE },\n );\n\n if (result.length === 0) {\n throw new NotFoundError(`Subscription with ID '${id}' not found`);\n } else if (result.length > 1) {\n throw new Error(\n `Failed to read subscription, unexpectedly updated ${result.length} rows`,\n );\n }\n\n const rows = result[0].events;\n if (!rows || rows.length === 0) {\n return { events: [] };\n }\n\n return {\n events: rows.map(row => {\n const { payload, metadata } = JSON.parse(row.data_json);\n return {\n topic: row.topic,\n eventPayload: payload,\n metadata,\n };\n }),\n };\n }\n\n async setupListener(\n subscriptionId: string,\n options: {\n signal: AbortSignal;\n },\n ): Promise<{ waitForUpdate(): Promise<{ topic: string }> }> {\n const result = await this.#db<SubscriptionsRow>(TABLE_SUBSCRIPTIONS)\n .select('topics')\n .where({ id: subscriptionId })\n .first();\n\n if (!result) {\n throw new NotFoundError(\n `Subscription with ID '${subscriptionId}' not found`,\n );\n }\n\n options.signal.throwIfAborted();\n\n return this.#listener.setupListener(\n new Set(result.topics ?? []),\n options.signal,\n );\n }\n\n async #cleanup() {\n try {\n const eventCount = await this.#db(TABLE_EVENTS)\n .delete()\n // Delete any events that are outside both the min age and size window\n .orWhere(inner =>\n inner\n .whereIn(\n 'id',\n this.#db\n .select('id')\n .from(TABLE_EVENTS)\n .orderBy('id', 'desc')\n .offset(this.#windowMaxCount),\n )\n .andWhere(\n 'created_at',\n '<',\n new Date(Date.now() - this.#windowMinAge),\n ),\n )\n // If events are outside the max age they will always be deleted\n .orWhere('created_at', '<', new Date(Date.now() - this.#windowMaxAge));\n\n if (eventCount > 0) {\n this.#logger.info(\n `Event cleanup resulted in ${eventCount} old events being deleted`,\n );\n }\n } catch (error) {\n this.#logger.error('Event cleanup failed', error);\n }\n\n try {\n // Delete any subscribers that aren't keeping up with current events\n const [{ min: minId }] = await this.#db(TABLE_EVENTS).min('id');\n\n let subscriberCount;\n if (minId === null) {\n // No events left, remove all subscribers. This can happen if no events\n // are published within the max age window.\n subscriberCount = await this.#db(TABLE_SUBSCRIPTIONS).delete();\n } else {\n subscriberCount = await this.#db(TABLE_SUBSCRIPTIONS)\n .delete()\n // Read pointer points to the ID that has been read, so we need an additional offset\n .where('read_until', '<', minId - 1);\n }\n\n if (subscriberCount > 0) {\n this.#logger.info(\n `Subscription cleanup resulted in ${subscriberCount} stale subscribers being deleted`,\n );\n }\n } catch (error) {\n this.#logger.error('Subscription cleanup failed', error);\n }\n }\n}\n"],"names":["resolvePackagePath","ForwardedError","durationToMilliseconds","NotFoundError"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ var errors = require('@backstage/errors');
4
+
5
+ const MAX_BATCH_SIZE = 10;
6
+ const MAX_EVENTS_DEFAULT = 1e3;
7
+ class MemoryEventBusStore {
8
+ #maxEvents;
9
+ #events = new Array();
10
+ #subscribers = /* @__PURE__ */ new Map();
11
+ #listeners = /* @__PURE__ */ new Set();
12
+ constructor(options = {}) {
13
+ this.#maxEvents = options.maxEvents ?? MAX_EVENTS_DEFAULT;
14
+ }
15
+ async publish(options) {
16
+ const topic = options.event.topic;
17
+ const notifiedSubscribers = new Set(options.notifiedSubscribers);
18
+ let hasOtherSubscribers = false;
19
+ for (const sub of this.#subscribers.values()) {
20
+ if (sub.topics.has(topic) && !notifiedSubscribers.has(sub.id)) {
21
+ hasOtherSubscribers = true;
22
+ break;
23
+ }
24
+ }
25
+ if (!hasOtherSubscribers) {
26
+ return void 0;
27
+ }
28
+ const nextSeq = this.#getMaxSeq() + 1;
29
+ this.#events.push({ ...options.event, notifiedSubscribers, seq: nextSeq });
30
+ for (const listener of this.#listeners) {
31
+ if (listener.topics.has(topic)) {
32
+ listener.resolve({ topic });
33
+ this.#listeners.delete(listener);
34
+ }
35
+ }
36
+ if (this.#events.length > this.#maxEvents) {
37
+ this.#events.shift();
38
+ }
39
+ return { eventId: String(nextSeq) };
40
+ }
41
+ #getMaxSeq() {
42
+ return this.#events[this.#events.length - 1]?.seq ?? 0;
43
+ }
44
+ async upsertSubscription(id, topics) {
45
+ const existing = this.#subscribers.get(id);
46
+ if (existing) {
47
+ existing.topics = new Set(topics);
48
+ return;
49
+ }
50
+ const sub = {
51
+ id,
52
+ seq: this.#getMaxSeq(),
53
+ topics: new Set(topics)
54
+ };
55
+ this.#subscribers.set(id, sub);
56
+ }
57
+ async readSubscription(id) {
58
+ const sub = this.#subscribers.get(id);
59
+ if (!sub) {
60
+ throw new errors.NotFoundError(`Subscription not found`);
61
+ }
62
+ const events = this.#events.filter(
63
+ (event) => event.seq > sub.seq && sub.topics.has(event.topic) && !event.notifiedSubscribers.has(id)
64
+ ).slice(0, MAX_BATCH_SIZE);
65
+ sub.seq = events[events.length - 1]?.seq ?? sub.seq;
66
+ return {
67
+ events: events.map(({ topic, eventPayload }) => ({
68
+ topic,
69
+ eventPayload
70
+ }))
71
+ };
72
+ }
73
+ async setupListener(subscriptionId, options) {
74
+ return {
75
+ waitForUpdate: async () => {
76
+ options.signal.throwIfAborted();
77
+ const sub = this.#subscribers.get(subscriptionId);
78
+ if (!sub) {
79
+ throw new errors.NotFoundError(`Subscription not found`);
80
+ }
81
+ return new Promise((resolve, reject) => {
82
+ const listener = {
83
+ topics: sub.topics,
84
+ resolve(result) {
85
+ resolve(result);
86
+ cleanup();
87
+ }
88
+ };
89
+ this.#listeners.add(listener);
90
+ const onAbort = () => {
91
+ this.#listeners.delete(listener);
92
+ reject(options.signal.reason);
93
+ cleanup();
94
+ };
95
+ function cleanup() {
96
+ options.signal.removeEventListener("abort", onAbort);
97
+ }
98
+ options.signal.addEventListener("abort", onAbort);
99
+ });
100
+ }
101
+ };
102
+ }
103
+ }
104
+
105
+ exports.MemoryEventBusStore = MemoryEventBusStore;
106
+ //# sourceMappingURL=MemoryEventBusStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemoryEventBusStore.cjs.js","sources":["../../../src/service/hub/MemoryEventBusStore.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { EventParams } from '@backstage/plugin-events-node';\nimport { EventBusStore } from './types';\nimport { NotFoundError } from '@backstage/errors';\nimport {\n BackstageCredentials,\n BackstageServicePrincipal,\n} from '@backstage/backend-plugin-api';\n\nconst MAX_BATCH_SIZE = 10;\nconst MAX_EVENTS_DEFAULT = 1_000;\n\nexport class MemoryEventBusStore implements EventBusStore {\n #maxEvents: number;\n #events = new Array<\n EventParams & { seq: number; notifiedSubscribers: Set<string> }\n >();\n #subscribers = new Map<\n string,\n { id: string; seq: number; topics: Set<string> }\n >();\n #listeners = new Set<{\n topics: Set<string>;\n resolve(result: { topic: string }): void;\n }>();\n\n constructor(options: { maxEvents?: number } = {}) {\n this.#maxEvents = options.maxEvents ?? MAX_EVENTS_DEFAULT;\n }\n\n async publish(options: {\n event: EventParams;\n notifiedSubscribers: string[];\n credentials: BackstageCredentials<BackstageServicePrincipal>;\n }): Promise<{ eventId: string } | undefined> {\n const topic = options.event.topic;\n const notifiedSubscribers = new Set(options.notifiedSubscribers);\n\n let hasOtherSubscribers = false;\n for (const sub of this.#subscribers.values()) {\n if (sub.topics.has(topic) && !notifiedSubscribers.has(sub.id)) {\n hasOtherSubscribers = true;\n break;\n }\n }\n if (!hasOtherSubscribers) {\n return undefined;\n }\n\n const nextSeq = this.#getMaxSeq() + 1;\n this.#events.push({ ...options.event, notifiedSubscribers, seq: nextSeq });\n\n for (const listener of this.#listeners) {\n if (listener.topics.has(topic)) {\n listener.resolve({ topic });\n this.#listeners.delete(listener);\n }\n }\n\n // Trim old events\n if (this.#events.length > this.#maxEvents) {\n this.#events.shift();\n }\n\n return { eventId: String(nextSeq) };\n }\n\n #getMaxSeq() {\n return this.#events[this.#events.length - 1]?.seq ?? 0;\n }\n\n async upsertSubscription(id: string, topics: string[]): Promise<void> {\n const existing = this.#subscribers.get(id);\n if (existing) {\n existing.topics = new Set(topics);\n return;\n }\n const sub = {\n id: id,\n seq: this.#getMaxSeq(),\n topics: new Set(topics),\n };\n this.#subscribers.set(id, sub);\n }\n\n async readSubscription(id: string): Promise<{ events: EventParams[] }> {\n const sub = this.#subscribers.get(id);\n if (!sub) {\n throw new NotFoundError(`Subscription not found`);\n }\n const events = this.#events\n .filter(\n event =>\n event.seq > sub.seq &&\n sub.topics.has(event.topic) &&\n !event.notifiedSubscribers.has(id),\n )\n .slice(0, MAX_BATCH_SIZE);\n\n sub.seq = events[events.length - 1]?.seq ?? sub.seq;\n\n return {\n events: events.map(({ topic, eventPayload }) => ({\n topic,\n eventPayload,\n })),\n };\n }\n\n async setupListener(\n subscriptionId: string,\n options: {\n signal: AbortSignal;\n },\n ): Promise<{ waitForUpdate(): Promise<{ topic: string }> }> {\n return {\n waitForUpdate: async () => {\n options.signal.throwIfAborted();\n\n const sub = this.#subscribers.get(subscriptionId);\n if (!sub) {\n throw new NotFoundError(`Subscription not found`);\n }\n\n return new Promise<{ topic: string }>((resolve, reject) => {\n const listener = {\n topics: sub.topics,\n resolve(result: { topic: string }) {\n resolve(result);\n cleanup();\n },\n };\n this.#listeners.add(listener);\n\n const onAbort = () => {\n this.#listeners.delete(listener);\n reject(options.signal.reason);\n cleanup();\n };\n\n function cleanup() {\n options.signal.removeEventListener('abort', onAbort);\n }\n\n options.signal.addEventListener('abort', onAbort);\n });\n },\n };\n }\n}\n"],"names":["NotFoundError"],"mappings":"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"}