@backstage/plugin-events-backend 0.6.7-next.1 → 0.6.7-next.2
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @backstage/plugin-events-backend
|
|
2
2
|
|
|
3
|
+
## 0.6.7-next.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 02fd3bf: Improve database event cleanup under large backlogs and speed up reading events for subscriptions with many retained events.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @backstage/backend-openapi-utils@0.7.3-next.2
|
|
10
|
+
|
|
3
11
|
## 0.6.7-next.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -56,6 +56,16 @@ in combination with suitable event subscribers.
|
|
|
56
56
|
|
|
57
57
|
However, it is not limited to these use cases.
|
|
58
58
|
|
|
59
|
+
## Database event retention
|
|
60
|
+
|
|
61
|
+
The database-backed event bus uses PostgreSQL. Events are retained for at least
|
|
62
|
+
10 minutes. Beyond that window, cleanup aims to keep the newest 10,000 events;
|
|
63
|
+
events older than one day are eligible for deletion regardless of count.
|
|
64
|
+
Cleanup runs every minute in batches, so bursts and backlogs can temporarily
|
|
65
|
+
leave more than 10,000 events. Subscriptions that have fallen behind the
|
|
66
|
+
retained events are also removed; consumers should be ready to recreate their
|
|
67
|
+
subscriptions if they have been inactive for too long.
|
|
68
|
+
|
|
59
69
|
## Use Cases
|
|
60
70
|
|
|
61
71
|
### Request Validator
|
|
@@ -8,6 +8,9 @@ const WINDOW_MAX_COUNT_DEFAULT = 1e4;
|
|
|
8
8
|
const WINDOW_MIN_AGE_DEFAULT = { minutes: 10 };
|
|
9
9
|
const WINDOW_MAX_AGE_DEFAULT = { days: 1 };
|
|
10
10
|
const MAX_BATCH_SIZE = 10;
|
|
11
|
+
const CLEANUP_BATCH_SIZE = 1e3;
|
|
12
|
+
const CLEANUP_STATEMENT_TIMEOUT_MS = 2e4;
|
|
13
|
+
const CLEANUP_RUN_DURATION_MS = 5e4;
|
|
11
14
|
const LISTENER_CONNECTION_TIMEOUT_MS = 6e4;
|
|
12
15
|
const KEEPALIVE_INTERVAL_MS = 6e4;
|
|
13
16
|
const TABLE_EVENTS = "event_bus_events";
|
|
@@ -196,10 +199,11 @@ class DatabaseEventBusStore {
|
|
|
196
199
|
);
|
|
197
200
|
await options.scheduler.scheduleTask({
|
|
198
201
|
id: "event-bus-cleanup",
|
|
199
|
-
|
|
202
|
+
scope: "global",
|
|
203
|
+
frequency: { minutes: 1 },
|
|
200
204
|
timeout: { minutes: 1 },
|
|
201
205
|
initialDelay: { seconds: 10 },
|
|
202
|
-
fn: () => store.#cleanup()
|
|
206
|
+
fn: (signal) => store.#cleanup(signal)
|
|
203
207
|
});
|
|
204
208
|
options.lifecycle.addShutdownHook(async () => {
|
|
205
209
|
await listener.shutdown();
|
|
@@ -222,7 +226,9 @@ class DatabaseEventBusStore {
|
|
|
222
226
|
minAge,
|
|
223
227
|
maxAge
|
|
224
228
|
);
|
|
225
|
-
return Object.assign(store, {
|
|
229
|
+
return Object.assign(store, {
|
|
230
|
+
clean: (signal) => store.#cleanup(signal)
|
|
231
|
+
});
|
|
226
232
|
}
|
|
227
233
|
#db;
|
|
228
234
|
#logger;
|
|
@@ -308,20 +314,30 @@ class DatabaseEventBusStore {
|
|
|
308
314
|
FOR UPDATE
|
|
309
315
|
),
|
|
310
316
|
selected_events AS (
|
|
311
|
-
SELECT
|
|
312
|
-
FROM
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
SELECT event.*
|
|
318
|
+
FROM subscription
|
|
319
|
+
CROSS JOIN LATERAL (
|
|
320
|
+
SELECT DISTINCT topic
|
|
321
|
+
FROM unnest(subscription.topics) AS subscribed(topic)
|
|
322
|
+
) AS subscribed_topics
|
|
323
|
+
CROSS JOIN LATERAL (
|
|
324
|
+
SELECT event_bus_events.*
|
|
325
|
+
FROM event_bus_events
|
|
326
|
+
WHERE event_bus_events.topic = subscribed_topics.topic
|
|
327
|
+
AND event_bus_events.id > subscription.read_until
|
|
328
|
+
AND NOT :id = ANY(event_bus_events.notified_subscribers)
|
|
329
|
+
ORDER BY event_bus_events.id ASC
|
|
330
|
+
LIMIT :limit
|
|
331
|
+
) AS event
|
|
332
|
+
ORDER BY event.id ASC
|
|
333
|
+
LIMIT :limit
|
|
318
334
|
),
|
|
319
335
|
last_event_id AS (
|
|
320
336
|
SELECT max(id) AS last_event_id
|
|
321
337
|
FROM selected_events
|
|
322
338
|
),
|
|
323
339
|
events_array AS (
|
|
324
|
-
SELECT json_agg(row_to_json(selected_events)) AS events
|
|
340
|
+
SELECT json_agg(row_to_json(selected_events) ORDER BY id) AS events
|
|
325
341
|
FROM selected_events
|
|
326
342
|
)
|
|
327
343
|
UPDATE event_bus_subscriptions
|
|
@@ -367,41 +383,134 @@ class DatabaseEventBusStore {
|
|
|
367
383
|
options.signal
|
|
368
384
|
);
|
|
369
385
|
}
|
|
370
|
-
async #cleanup() {
|
|
371
|
-
|
|
372
|
-
|
|
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);
|
|
386
|
+
async #cleanup(signal) {
|
|
387
|
+
if (signal?.aborted) {
|
|
388
|
+
return;
|
|
389
389
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
390
|
+
const stopAt = Date.now() + CLEANUP_RUN_DURATION_MS;
|
|
391
|
+
let eventCount = 0;
|
|
392
|
+
let subscriberCount = 0;
|
|
393
|
+
const setStatementTimeout = async (trx) => {
|
|
394
|
+
const remainingMs = stopAt - Date.now();
|
|
395
|
+
if (remainingMs <= 0) {
|
|
396
|
+
return false;
|
|
397
397
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
398
|
+
await trx.raw(
|
|
399
|
+
`SET LOCAL statement_timeout = '${Math.min(
|
|
400
|
+
CLEANUP_STATEMENT_TIMEOUT_MS,
|
|
401
|
+
remainingMs
|
|
402
|
+
)}ms'`
|
|
403
|
+
);
|
|
404
|
+
return true;
|
|
405
|
+
};
|
|
406
|
+
const deleteEventBatch = (createdBefore, maxId) => {
|
|
407
|
+
const bindings = [createdBefore];
|
|
408
|
+
if (maxId !== void 0) {
|
|
409
|
+
bindings.push(maxId);
|
|
410
|
+
}
|
|
411
|
+
bindings.push(CLEANUP_BATCH_SIZE);
|
|
412
|
+
return this.#db.transaction(async (trx) => {
|
|
413
|
+
if (!await setStatementTimeout(trx)) {
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
const result = await trx.raw(
|
|
417
|
+
`WITH candidates AS (
|
|
418
|
+
SELECT id FROM event_bus_events
|
|
419
|
+
WHERE created_at < ? ${maxId === void 0 ? "" : "AND id <= ?"}
|
|
420
|
+
ORDER BY created_at, id
|
|
421
|
+
LIMIT ? FOR UPDATE SKIP LOCKED
|
|
422
|
+
)
|
|
423
|
+
DELETE FROM event_bus_events
|
|
424
|
+
WHERE id = ANY (ARRAY(SELECT id FROM candidates))`,
|
|
425
|
+
bindings
|
|
401
426
|
);
|
|
427
|
+
return result.rowCount;
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
const deleteSubscriberBatch = () => this.#db.transaction(async (trx) => {
|
|
431
|
+
if (!await setStatementTimeout(trx)) {
|
|
432
|
+
return 0;
|
|
402
433
|
}
|
|
403
|
-
|
|
404
|
-
|
|
434
|
+
const [{ min: minId }] = await trx(TABLE_EVENTS).min("id");
|
|
435
|
+
if (!await setStatementTimeout(trx)) {
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
const where = minId === null ? "updated_at < ?" : "read_until < CAST(? AS bigint) - 1";
|
|
439
|
+
const binding = minId === null ? new Date(Date.now() - this.#windowMaxAge) : minId;
|
|
440
|
+
const result = await trx.raw(
|
|
441
|
+
`WITH candidates AS (
|
|
442
|
+
SELECT id FROM event_bus_subscriptions
|
|
443
|
+
WHERE ${where}
|
|
444
|
+
ORDER BY id
|
|
445
|
+
LIMIT ? FOR UPDATE SKIP LOCKED
|
|
446
|
+
)
|
|
447
|
+
DELETE FROM event_bus_subscriptions
|
|
448
|
+
WHERE id = ANY (ARRAY(SELECT id FROM candidates))`,
|
|
449
|
+
[binding, CLEANUP_BATCH_SIZE]
|
|
450
|
+
);
|
|
451
|
+
return result.rowCount;
|
|
452
|
+
});
|
|
453
|
+
let eventCleanupPhase = "maxAge";
|
|
454
|
+
let countCutoff;
|
|
455
|
+
let failed = false;
|
|
456
|
+
while (!failed && !signal?.aborted && Date.now() < stopAt) {
|
|
457
|
+
let subscriberDeleted = 0;
|
|
458
|
+
try {
|
|
459
|
+
if (eventCleanupPhase === "maxAge") {
|
|
460
|
+
const deleted = await deleteEventBatch(
|
|
461
|
+
new Date(Date.now() - this.#windowMaxAge)
|
|
462
|
+
);
|
|
463
|
+
eventCount += deleted;
|
|
464
|
+
if (deleted < CLEANUP_BATCH_SIZE) {
|
|
465
|
+
eventCleanupPhase = "maxCount";
|
|
466
|
+
}
|
|
467
|
+
} else if (eventCleanupPhase === "maxCount") {
|
|
468
|
+
if (countCutoff === void 0) {
|
|
469
|
+
const cutoff = await this.#db.transaction(async (trx) => {
|
|
470
|
+
if (!await setStatementTimeout(trx)) {
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
return trx(TABLE_EVENTS).select("id").orderBy("id", "desc").offset(this.#windowMaxCount).first();
|
|
474
|
+
});
|
|
475
|
+
countCutoff = cutoff?.id;
|
|
476
|
+
}
|
|
477
|
+
if (countCutoff === void 0) {
|
|
478
|
+
eventCleanupPhase = "complete";
|
|
479
|
+
} else {
|
|
480
|
+
const deleted = await deleteEventBatch(
|
|
481
|
+
new Date(Date.now() - this.#windowMinAge),
|
|
482
|
+
countCutoff
|
|
483
|
+
);
|
|
484
|
+
eventCount += deleted;
|
|
485
|
+
if (deleted < CLEANUP_BATCH_SIZE) {
|
|
486
|
+
eventCleanupPhase = "complete";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
this.#logger.error("Event cleanup failed", error);
|
|
492
|
+
failed = true;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
subscriberDeleted = await deleteSubscriberBatch();
|
|
496
|
+
subscriberCount += subscriberDeleted;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
this.#logger.error("Subscription cleanup failed", error);
|
|
499
|
+
failed = true;
|
|
500
|
+
}
|
|
501
|
+
if (eventCleanupPhase === "complete" && subscriberDeleted < CLEANUP_BATCH_SIZE) {
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (eventCount > 0) {
|
|
506
|
+
this.#logger.info(
|
|
507
|
+
`Event cleanup resulted in ${eventCount} old events being deleted`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (subscriberCount > 0) {
|
|
511
|
+
this.#logger.info(
|
|
512
|
+
`Subscription cleanup resulted in ${subscriberCount} stale subscribers being deleted`
|
|
513
|
+
);
|
|
405
514
|
}
|
|
406
515
|
}
|
|
407
516
|
}
|
|
@@ -1 +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)\n .where('updated_at', '<', new Date(Date.now() - this.#windowMaxAge))\n .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":";;;;;;AA8BA,MAAM,wBAAA,GAA2B,GAAA;AACjC,MAAM,sBAAA,GAAyB,EAAE,OAAA,EAAS,EAAA,EAAG;AAC7C,MAAM,sBAAA,GAAyB,EAAE,IAAA,EAAM,CAAA,EAAE;AAEzC,MAAM,cAAA,GAAiB,EAAA;AACvB,MAAM,8BAAA,GAAiC,GAAA;AACvC,MAAM,qBAAA,GAAwB,GAAA;AAE9B,MAAM,YAAA,GAAe,kBAAA;AACrB,MAAM,mBAAA,GAAsB,yBAAA;AAC5B,MAAM,aAAA,GAAgB,mBAAA;AAoBtB,SAAS,UACP,WAAA,EACA;AACA,EAAA,OAAO,CAAA,QAAA,EAAW,WAAA,CAAY,SAAA,CAAU,OAAO,CAAA,CAAA;AACjD;AAEA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,EACpB,kCAAA;AAAA,EACA;AACF,CAAA;AAoBA,MAAM,wBAAA,CAAyB;AAAA,EACpB,OAAA;AAAA,EACA,OAAA;AAAA,EAEA,UAAA,uBAAiB,GAAA,EAIvB;AAAA,EAEH,eAAA,GAAkB,KAAA;AAAA,EAClB,YAAA;AAAA,EACA,YAAA;AAAA,EACA,kBAAA;AAAA,EAEA,WAAA,CAAY,QAA0B,MAAA,EAAuB;AAC3D,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAA,CAAK,UAAU,MAAA,CAAO,KAAA,CAAM,EAAE,IAAA,EAAM,4BAA4B,CAAA;AAAA,EAClE;AAAA,EAEA,MAAM,aAAA,CACJ,MAAA,EACA,MAAA,EAC0D;AAC1D,IAAA,IAAI,KAAK,YAAA,EAAc;AACrB,MAAA,YAAA,CAAa,KAAK,YAAY,CAAA;AAC9B,MAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AAAA,IACtB;AAEA,IAAA,MAAM,KAAK,iBAAA,EAAkB;AAE7B,IAAA,MAAM,aAAA,GAAgB,IAAI,OAAA,CAA2B,CAAC,SAAS,MAAA,KAAW;AACxE,MAAA,MAAM,QAAA,GAAW;AAAA,QACf,MAAA;AAAA,QACA,QAAQ,MAAA,EAA2B;AACjC,UAAA,OAAA,CAAQ,MAAM,CAAA;AACd,UAAA,OAAA,EAAQ;AAAA,QACV,CAAA;AAAA,QACA,OAAO,GAAA,EAAY;AACjB,UAAA,MAAA,CAAO,GAAG,CAAA;AACV,UAAA,OAAA,EAAQ;AAAA,QACV;AAAA,OACF;AACA,MAAA,IAAA,CAAK,UAAA,CAAW,IAAI,QAAQ,CAAA;AAE5B,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,IAAA,CAAK,UAAA,CAAW,OAAO,QAAQ,CAAA;AAC/B,QAAA,IAAA,CAAK,uBAAA,EAAwB;AAC7B,QAAA,MAAA,CAAO,OAAO,MAAM,CAAA;AACpB,QAAA,OAAA,EAAQ;AAAA,MACV,CAAA;AAEA,MAAA,SAAS,OAAA,GAAU;AACjB,QAAA,MAAA,CAAO,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAAA,MAC7C;AAEA,MAAA,MAAA,CAAO,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAAA,IAC1C,CAAC,CAAA;AAGD,IAAA,aAAA,CAAc,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAE5B,IAAA,OAAO,EAAE,aAAA,EAAe,MAAM,aAAA,EAAc;AAAA,EAC9C;AAAA,EAEA,MAAM,QAAA,GAAW;AACf,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,YAAA,EAAc,KAAA,CAAM,MAAM,MAAS,CAAA;AAC3D,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,cAAc,KAAA,EAAe;AAC3B,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,CAAA,0CAAA,EAA6C,KAAK,CAAA,CAAA,CAAG,CAAA;AACxE,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,UAAA,EAAY;AAC/B,MAAA,IAAI,CAAA,CAAE,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA,EAAG;AACvB,QAAA,CAAA,CAAE,OAAA,CAAQ,EAAE,KAAA,EAAO,CAAA;AACnB,QAAA,IAAA,CAAK,UAAA,CAAW,OAAO,CAAC,CAAA;AAAA,MAC1B;AAAA,IACF;AACA,IAAA,IAAA,CAAK,uBAAA,EAAwB;AAAA,EAC/B;AAAA;AAAA;AAAA,EAIA,aAAa,KAAA,EAAc;AACzB,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA;AAAA,MACX,CAAA,mDAAA,CAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,UAAA,EAAY;AAC/B,MAAA,CAAA,CAAE,MAAA,CAAO,IAAI,KAAA,CAAM,4BAA4B,CAAC,CAAA;AAAA,IAClD;AACA,IAAA,IAAA,CAAK,WAAW,KAAA,EAAM;AACtB,IAAA,IAAA,CAAK,uBAAA,EAAwB;AAAA,EAC/B;AAAA,EAEA,uBAAA,GAA0B;AAExB,IAAA,IAAI,KAAK,UAAA,CAAW,IAAA,KAAS,CAAA,IAAK,CAAC,KAAK,YAAA,EAAc;AACpD,MAAA,IAAA,CAAK,YAAA,GAAe,WAAW,MAAM;AACnC,QAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AACpB,QAAA,IAAA,CAAK,YAAA,EAAc,KAAK,CAAA,IAAA,KAAQ;AAC9B,UAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,2CAA2C,CAAA;AAC7D,UAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AACpB,UAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAAA,QAC9B,CAAC,CAAA;AAAA,MACH,GAAG,8BAA8B,CAAA;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,mBAAmB,IAAA,EAA4B;AAC7C,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,MAAA,aAAA,CAAc,KAAK,kBAAkB,CAAA;AACrC,MAAA,IAAA,CAAK,kBAAA,GAAqB,MAAA;AAAA,IAC5B;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,oBAAA,CAAqB,IAAI,CAAA,CAAE,MAAM,CAAA,KAAA,KAAS;AACrD,MAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,CAAA,qCAAA,CAAA,EAAyC,KAAK,CAAA;AAAA,IACnE,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,kBAAA,EAAmB;AAAA,EAC1B;AAAA,EAEA,MAAM,iBAAA,GAAoB;AACxB,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,IAC7C;AACA,IAAA,IAAI,KAAK,YAAA,EAAc;AACrB,MAAA,MAAM,IAAA,CAAK,YAAA;AACX,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,YAAA,GAAe,OAAA,CAAQ,OAAA,EAAQ,CAAE,KAAK,YAAY;AACrD,MAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAAQ,oBAAA,EAAqB;AAErD,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA,OAAA,EAAU,aAAa,CAAA,CAAE,CAAA;AAG1C,QAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,UAAA,aAAA,CAAc,KAAK,kBAAkB,CAAA;AAAA,QACvC;AACA,QAAA,IAAA,CAAK,kBAAA,GAAqB,YAAY,MAAM;AAC1C,UAAA,IAAA,CAAK,KAAA,CAAM,UAAU,CAAA,CAAE,KAAA,CAAM,CAAA,KAAA,KAAS;AACpC,YAAA,IAAA,CAAK,YAAA,GAAe,KAAA,CAAA;AACpB,YAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,YAAA,IAAA,CAAK,YAAA,CAAa,IAAIC,qBAAA,CAAe,kBAAA,EAAoB,KAAK,CAAC,CAAA;AAAA,UACjE,CAAC,CAAA;AAAA,QACH,GAAG,qBAAqB,CAAA;AAExB,QAAA,IAAA,CAAK,EAAA,CAAG,gBAAgB,CAAA,KAAA,KAAS;AAC/B,UAAA,IAAA,CAAK,aAAA,CAAc,MAAM,OAAO,CAAA;AAAA,QAClC,CAAC,CAAA;AACD,QAAA,IAAA,CAAK,EAAA,CAAG,SAAS,CAAA,KAAA,KAAS;AACxB,UAAA,IAAA,CAAK,YAAA,GAAe,KAAA,CAAA;AACpB,UAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,UAAA,IAAA,CAAK,aAAa,KAAK,CAAA;AAAA,QACzB,CAAC,CAAA;AACD,QAAA,IAAA,CAAK,EAAA,CAAG,OAAO,CAAA,KAAA,KAAS;AACtB,UAAA,IAAA,CAAK,YAAA,GAAe,KAAA,CAAA;AACpB,UAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,UAAA,IAAA,CAAK,YAAA;AAAA,YACH,KAAA,IAAS,IAAI,KAAA,CAAM,+BAA+B;AAAA,WACpD;AAAA,QACF,CAAC,CAAA;AACD,QAAA,OAAO,IAAA;AAAA,MACT,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF,CAAC,CAAA;AACD,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,YAAA;AAAA,IACb,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AACpB,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF;AAEO,MAAM,qBAAA,CAA+C;AAAA,EAC1D,aAAa,OAAO,OAAA,EAae;AACjC,IAAA,MAAM,EAAA,GAAK,MAAM,OAAA,CAAQ,QAAA,CAAS,SAAA,EAAU;AAE5C,IAAA,IAAI,EAAA,CAAG,MAAA,CAAO,MAAA,CAAO,MAAA,KAAW,IAAA,EAAM;AACpC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,qDAAA,EAAwD,EAAA,CAAG,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,CAAA;AAAA,OACjF;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,UAAA,EAAY,IAAA,EAAM;AACtC,MAAA,MAAM,EAAA,CAAG,QAAQ,MAAA,CAAO;AAAA,QACtB,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,WAAW,IAAI,wBAAA,CAAyB,EAAA,CAAG,MAAA,EAAQ,QAAQ,MAAM,CAAA;AAEvE,IAAA,MAAM,QAAQ,IAAI,qBAAA;AAAA,MAChB,EAAA;AAAA,MACA,OAAA,CAAQ,MAAA;AAAA,MACR,QAAA;AAAA,MACA,OAAA,CAAQ,QAAQ,QAAA,IAAY,wBAAA;AAAA,MAC5BC,4BAAA,CAAuB,OAAA,CAAQ,MAAA,EAAQ,MAAA,IAAU,sBAAsB,CAAA;AAAA,MACvEA,4BAAA,CAAuB,OAAA,CAAQ,MAAA,EAAQ,MAAA,IAAU,sBAAsB;AAAA,KACzE;AAEA,IAAA,MAAM,OAAA,CAAQ,UAAU,YAAA,CAAa;AAAA,MACnC,EAAA,EAAI,mBAAA;AAAA,MACJ,SAAA,EAAW,EAAE,OAAA,EAAS,EAAA,EAAG;AAAA,MACzB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,MACtB,YAAA,EAAc,EAAE,OAAA,EAAS,EAAA,EAAG;AAAA,MAC5B,EAAA,EAAI,MAAM,KAAA,CAAM,QAAA;AAAS,KAC1B,CAAA;AAED,IAAA,OAAA,CAAQ,SAAA,CAAU,gBAAgB,YAAY;AAC5C,MAAA,MAAM,SAAS,QAAA,EAAS;AAAA,IAC1B,CAAC,CAAA;AAED,IAAA,OAAO,KAAA;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,OAAA,CAAQ;AAAA,IACnB,EAAA;AAAA,IACA,MAAA;AAAA,IACA,MAAA,GAAS,CAAA;AAAA,IACT,MAAA,GAAS;AAAA,GACX,EAKG;AACD,IAAA,MAAM,GAAG,OAAA,CAAQ,MAAA,CAAO,EAAE,SAAA,EAAW,eAAe,CAAA;AAEpD,IAAA,MAAM,QAAQ,IAAI,qBAAA;AAAA,MAChB,EAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAI,wBAAA,CAAyB,EAAA,CAAG,MAAA,EAAQ,MAAM,CAAA;AAAA,MAC9C,CAAA;AAAA,MACA,MAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO,MAAA,CAAO,OAAO,KAAA,EAAO,EAAE,OAAO,MAAM,KAAA,CAAM,QAAA,EAAS,EAAG,CAAA;AAAA,EAC/D;AAAA,EAES,GAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA,eAAA;AAAA,EACA,aAAA;AAAA,EACA,aAAA;AAAA,EAED,YACN,EAAA,EACA,MAAA,EACA,QAAA,EACA,cAAA,EACA,cACA,YAAA,EACA;AACA,IAAA,IAAA,CAAK,GAAA,GAAM,EAAA;AACX,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,eAAA,GAAkB,cAAA;AACvB,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AACrB,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AAAA,EACvB;AAAA,EAEA,MAAM,QAAQ,OAAA,EAI+B;AAC3C,IAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,CAAM,KAAA;AAC5B,IAAA,MAAM,mBAAA,GAAsB,OAAA,CAAQ,mBAAA,IAAuB,EAAC;AAG5D,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,GAAA,CAEvB,IAAA;AAAA,MACC,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,qBAAA,EAAuB;AAAA,QAClC,YAAA;AAAA;AAAA,QAEA,YAAA;AAAA,QACA,OAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACD;AAAA,KACH,CACC,MAAA;AAAA,MACC,CAAC,MACC,CAAA,CAEG,MAAA;AAAA,QACC,IAAA,CAAK,IAAI,GAAA,CAAI,GAAA,EAAK,CAAC,SAAA,CAAU,OAAA,CAAQ,WAAW,CAAC,CAAC,CAAA;AAAA,QAClD,KAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,CAAC,KAAK,CAAC,CAAA;AAAA,QACzB,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK;AAAA,UAChB,KAAK,SAAA,CAAU;AAAA,YACb,OAAA,EAAS,QAAQ,KAAA,CAAM,YAAA;AAAA,YACvB,QAAA,EAAU,QAAQ,KAAA,CAAM;AAAA,WACzB;AAAA,SACF,CAAA;AAAA,QACD,KAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,CAAC,mBAAmB,CAAC;AAAA,OACzC,CAGC,IAAA,CAAK,mBAAmB,CAAA,CACxB,UAAA,CAAW,MAAM,mBAAmB,CAAA,CACpC,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAA,CACjD,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,UAAU,CAAA,EAAG,GAAA,EAAK,CAAC;AAAA;AAAA,KAC9C,CACC,UAA4B,IAAI,CAAA;AAEnC,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,6CAAA,EAAgD,OAAO,MAAM,CAAA,KAAA;AAAA,OAC/D;AAAA,IACF;AAEA,IAAA,MAAM,CAAC,EAAE,EAAA,EAAI,CAAA,GAAI,MAAA;AAGjB,IAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,GAAA,CAAI,MAAA;AAAA,MAClC,KAAK,GAAA,CAAI,GAAA,CAAI,mBAAmB,CAAC,aAAA,EAAe,KAAK,CAAC;AAAA,KACxD;AACA,IAAA,IAAI,YAAA,EAAc,WAAW,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,QACX,CAAA,+CAAA,EAAkD,EAAE,CAAA,YAAA,EAAe,KAAK,CAAA,CAAA;AAAA,OAC1E;AAAA,IACF;AAEA,IAAA,OAAO,EAAE,SAAS,EAAA,EAAG;AAAA,EACvB;AAAA,EAEA,MAAM,kBAAA,CACJ,EAAA,EACA,MAAA,EACA,WAAA,EACe;AACf,IAAA,MAAM,CAAC,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA,GAAI,MAAM,IAAA,CAAK,GAAA,CAAI,YAAY,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAC9D,IAAA,MAAM,SAAS,MAAM,IAAA,CAAK,GAAA,CAAsB,mBAAmB,EAChE,MAAA,CAAO;AAAA,MACN,EAAA;AAAA,MACA,UAAA,EAAY,UAAU,WAAW,CAAA;AAAA,MACjC,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,GAAA,EAAI;AAAA,MAC5B,MAAA;AAAA,MACA,YAAY,KAAA,IAAS;AAAA,KACtB,CAAA,CACA,UAAA,CAAW,IAAI,CAAA,CACf,KAAA,CAAM,CAAC,YAAA,EAAc,QAAA,EAAU,YAAY,CAAC,CAAA,CAC5C,UAAU,GAAG,CAAA;AAEhB,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,uCAAA,EAA0C,OAAO,MAAM,CAAA,KAAA;AAAA,OACzD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,EAAA,EAAgD;AAQrE,IAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,MAAM,KAAK,GAAA,CAAI,GAAA;AAAA,MAGtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAAA,MA8BA,EAAE,EAAA,EAAI,KAAA,EAAO,cAAA;AAAe,KAC9B;AAEA,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,MAAA,MAAM,IAAIC,oBAAA,CAAc,CAAA,sBAAA,EAAyB,EAAE,CAAA,WAAA,CAAa,CAAA;AAAA,IAClE,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,kDAAA,EAAqD,OAAO,MAAM,CAAA,KAAA;AAAA,OACpE;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAC,CAAA,CAAE,MAAA;AACvB,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG;AAC9B,MAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAE;AAAA,IACtB;AAEA,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,KAAO;AACtB,QAAA,MAAM,EAAE,OAAA,EAAS,QAAA,KAAa,IAAA,CAAK,KAAA,CAAM,IAAI,SAAS,CAAA;AACtD,QAAA,OAAO;AAAA,UACL,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,YAAA,EAAc,OAAA;AAAA,UACd;AAAA,SACF;AAAA,MACF,CAAC;AAAA,KACH;AAAA,EACF;AAAA,EAEA,MAAM,aAAA,CACJ,cAAA,EACA,OAAA,EAG0D;AAC1D,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,GAAA,CAAsB,mBAAmB,CAAA,CAChE,MAAA,CAAO,QAAQ,CAAA,CACf,MAAM,EAAE,EAAA,EAAI,cAAA,EAAgB,EAC5B,KAAA,EAAM;AAET,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,MAAM,IAAIA,oBAAA;AAAA,QACR,yBAAyB,cAAc,CAAA,WAAA;AAAA,OACzC;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,OAAO,cAAA,EAAe;AAE9B,IAAA,OAAO,KAAK,SAAA,CAAU,aAAA;AAAA,MACpB,IAAI,GAAA,CAAI,MAAA,CAAO,MAAA,IAAU,EAAE,CAAA;AAAA,MAC3B,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,GAAW;AACf,IAAA,IAAI;AACF,MAAA,MAAM,aAAa,MAAM,IAAA,CAAK,IAAI,YAAY,CAAA,CAC3C,QAAO,CAEP,OAAA;AAAA,QAAQ,WACP,KAAA,CACG,OAAA;AAAA,UACC,IAAA;AAAA,UACA,IAAA,CAAK,GAAA,CACF,MAAA,CAAO,IAAI,EACX,IAAA,CAAK,YAAY,CAAA,CACjB,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,MAAA,CAAO,KAAK,eAAe;AAAA,SAChC,CACC,QAAA;AAAA,UACC,YAAA;AAAA,UACA,GAAA;AAAA,UACA,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,EAAI,GAAI,KAAK,aAAa;AAAA;AAC1C,OACJ,CAEC,OAAA,CAAQ,YAAA,EAAc,GAAA,EAAK,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA,CAAK,aAAa,CAAC,CAAA;AAEvE,MAAA,IAAI,aAAa,CAAA,EAAG;AAClB,QAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,UACX,6BAA6B,UAAU,CAAA,yBAAA;AAAA,SACzC;AAAA,MACF;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,sBAAA,EAAwB,KAAK,CAAA;AAAA,IAClD;AAEA,IAAA,IAAI;AAEF,MAAA,MAAM,CAAC,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA,GAAI,MAAM,IAAA,CAAK,GAAA,CAAI,YAAY,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAE9D,MAAA,IAAI,eAAA;AACJ,MAAA,IAAI,UAAU,IAAA,EAAM;AAGlB,QAAA,eAAA,GAAkB,MAAM,IAAA,CAAK,GAAA,CAAI,mBAAmB,CAAA,CACjD,MAAM,YAAA,EAAc,GAAA,EAAK,IAAI,IAAA,CAAK,KAAK,GAAA,EAAI,GAAI,KAAK,aAAa,CAAC,EAClE,MAAA,EAAO;AAAA,MACZ,CAAA,MAAO;AACL,QAAA,eAAA,GAAkB,MAAM,IAAA,CAAK,GAAA,CAAI,mBAAmB,CAAA,CACjD,MAAA,EAAO,CAEP,KAAA,CAAM,YAAA,EAAc,GAAA,EAAK,KAAA,GAAQ,CAAC,CAAA;AAAA,MACvC;AAEA,MAAA,IAAI,kBAAkB,CAAA,EAAG;AACvB,QAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,UACX,oCAAoC,eAAe,CAAA,gCAAA;AAAA,SACrD;AAAA,MACF;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,6BAAA,EAA+B,KAAK,CAAA;AAAA,IACzD;AAAA,EACF;AACF;;"}
|
|
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 CLEANUP_BATCH_SIZE = 1_000;\nconst CLEANUP_STATEMENT_TIMEOUT_MS = 20_000;\nconst CLEANUP_RUN_DURATION_MS = 50_000;\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 scope: 'global',\n frequency: { minutes: 1 },\n timeout: { minutes: 1 },\n initialDelay: { seconds: 10 },\n fn: signal => store.#cleanup(signal),\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, {\n clean: (signal?: AbortSignal) => store.#cleanup(signal),\n });\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.*\n FROM subscription\n CROSS JOIN LATERAL (\n SELECT DISTINCT topic\n FROM unnest(subscription.topics) AS subscribed(topic)\n ) AS subscribed_topics\n CROSS JOIN LATERAL (\n SELECT event_bus_events.*\n FROM event_bus_events\n WHERE event_bus_events.topic = subscribed_topics.topic\n AND 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\n LIMIT :limit\n ) AS event\n ORDER BY event.id ASC\n 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) ORDER BY id) 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(signal?: AbortSignal) {\n if (signal?.aborted) {\n return;\n }\n\n const stopAt = Date.now() + CLEANUP_RUN_DURATION_MS;\n let eventCount = 0;\n let subscriberCount = 0;\n\n const setStatementTimeout = async (trx: Knex.Transaction) => {\n const remainingMs = stopAt - Date.now();\n if (remainingMs <= 0) {\n return false;\n }\n\n await trx.raw(\n `SET LOCAL statement_timeout = '${Math.min(\n CLEANUP_STATEMENT_TIMEOUT_MS,\n remainingMs,\n )}ms'`,\n );\n return true;\n };\n\n const deleteEventBatch = (createdBefore: Date, maxId?: string) => {\n const bindings: (Date | string | number)[] = [createdBefore];\n if (maxId !== undefined) {\n bindings.push(maxId);\n }\n bindings.push(CLEANUP_BATCH_SIZE);\n\n return this.#db.transaction(async trx => {\n if (!(await setStatementTimeout(trx))) {\n return 0;\n }\n const result = await trx.raw<{ rowCount: number }>(\n `WITH candidates AS (\n SELECT id FROM event_bus_events\n WHERE created_at < ? ${maxId === undefined ? '' : 'AND id <= ?'}\n ORDER BY created_at, id\n LIMIT ? FOR UPDATE SKIP LOCKED\n )\n DELETE FROM event_bus_events\n WHERE id = ANY (ARRAY(SELECT id FROM candidates))`,\n bindings,\n );\n return result.rowCount;\n });\n };\n\n const deleteSubscriberBatch = () =>\n this.#db.transaction<number>(async trx => {\n if (!(await setStatementTimeout(trx))) {\n return 0;\n }\n const [{ min: minId }] = await trx(TABLE_EVENTS).min('id');\n\n if (!(await setStatementTimeout(trx))) {\n return 0;\n }\n\n const where =\n minId === null\n ? 'updated_at < ?'\n : 'read_until < CAST(? AS bigint) - 1';\n const binding =\n minId === null ? new Date(Date.now() - this.#windowMaxAge) : minId;\n const result = await trx.raw<{ rowCount: number }>(\n `WITH candidates AS (\n SELECT id FROM event_bus_subscriptions\n WHERE ${where}\n ORDER BY id\n LIMIT ? FOR UPDATE SKIP LOCKED\n )\n DELETE FROM event_bus_subscriptions\n WHERE id = ANY (ARRAY(SELECT id FROM candidates))`,\n [binding, CLEANUP_BATCH_SIZE],\n );\n return result.rowCount;\n });\n\n // Expire events past maxAge first, then trim older events past maxCount.\n let eventCleanupPhase: 'maxAge' | 'maxCount' | 'complete' = 'maxAge';\n let countCutoff: string | undefined;\n let failed = false;\n\n // Finish one bounded event batch and one bounded subscriber batch before\n // checking cancellation or the run budget, so neither can starve the other.\n while (!failed && !signal?.aborted && Date.now() < stopAt) {\n let subscriberDeleted = 0;\n\n try {\n if (eventCleanupPhase === 'maxAge') {\n const deleted = await deleteEventBatch(\n new Date(Date.now() - this.#windowMaxAge),\n );\n eventCount += deleted;\n if (deleted < CLEANUP_BATCH_SIZE) {\n eventCleanupPhase = 'maxCount';\n }\n } else if (eventCleanupPhase === 'maxCount') {\n if (countCutoff === undefined) {\n const cutoff = await this.#db.transaction(async trx => {\n if (!(await setStatementTimeout(trx))) {\n return undefined;\n }\n return trx<EventsRow>(TABLE_EVENTS)\n .select('id')\n .orderBy('id', 'desc')\n .offset(this.#windowMaxCount)\n .first();\n });\n countCutoff = cutoff?.id;\n }\n\n if (countCutoff === undefined) {\n eventCleanupPhase = 'complete';\n } else {\n const deleted = await deleteEventBatch(\n new Date(Date.now() - this.#windowMinAge),\n countCutoff,\n );\n eventCount += deleted;\n if (deleted < CLEANUP_BATCH_SIZE) {\n eventCleanupPhase = 'complete';\n }\n }\n }\n } catch (error) {\n this.#logger.error('Event cleanup failed', error);\n failed = true;\n }\n\n try {\n subscriberDeleted = await deleteSubscriberBatch();\n subscriberCount += subscriberDeleted;\n } catch (error) {\n this.#logger.error('Subscription cleanup failed', error);\n failed = true;\n }\n\n if (\n eventCleanupPhase === 'complete' &&\n subscriberDeleted < CLEANUP_BATCH_SIZE\n ) {\n break;\n }\n }\n\n if (eventCount > 0) {\n this.#logger.info(\n `Event cleanup resulted in ${eventCount} old events being deleted`,\n );\n }\n if (subscriberCount > 0) {\n this.#logger.info(\n `Subscription cleanup resulted in ${subscriberCount} stale subscribers being deleted`,\n );\n }\n }\n}\n"],"names":["resolvePackagePath","ForwardedError","durationToMilliseconds","NotFoundError"],"mappings":";;;;;;AA8BA,MAAM,wBAAA,GAA2B,GAAA;AACjC,MAAM,sBAAA,GAAyB,EAAE,OAAA,EAAS,EAAA,EAAG;AAC7C,MAAM,sBAAA,GAAyB,EAAE,IAAA,EAAM,CAAA,EAAE;AAEzC,MAAM,cAAA,GAAiB,EAAA;AACvB,MAAM,kBAAA,GAAqB,GAAA;AAC3B,MAAM,4BAAA,GAA+B,GAAA;AACrC,MAAM,uBAAA,GAA0B,GAAA;AAChC,MAAM,8BAAA,GAAiC,GAAA;AACvC,MAAM,qBAAA,GAAwB,GAAA;AAE9B,MAAM,YAAA,GAAe,kBAAA;AACrB,MAAM,mBAAA,GAAsB,yBAAA;AAC5B,MAAM,aAAA,GAAgB,mBAAA;AAoBtB,SAAS,UACP,WAAA,EACA;AACA,EAAA,OAAO,CAAA,QAAA,EAAW,WAAA,CAAY,SAAA,CAAU,OAAO,CAAA,CAAA;AACjD;AAEA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,EACpB,kCAAA;AAAA,EACA;AACF,CAAA;AAoBA,MAAM,wBAAA,CAAyB;AAAA,EACpB,OAAA;AAAA,EACA,OAAA;AAAA,EAEA,UAAA,uBAAiB,GAAA,EAIvB;AAAA,EAEH,eAAA,GAAkB,KAAA;AAAA,EAClB,YAAA;AAAA,EACA,YAAA;AAAA,EACA,kBAAA;AAAA,EAEA,WAAA,CAAY,QAA0B,MAAA,EAAuB;AAC3D,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAA,CAAK,UAAU,MAAA,CAAO,KAAA,CAAM,EAAE,IAAA,EAAM,4BAA4B,CAAA;AAAA,EAClE;AAAA,EAEA,MAAM,aAAA,CACJ,MAAA,EACA,MAAA,EAC0D;AAC1D,IAAA,IAAI,KAAK,YAAA,EAAc;AACrB,MAAA,YAAA,CAAa,KAAK,YAAY,CAAA;AAC9B,MAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AAAA,IACtB;AAEA,IAAA,MAAM,KAAK,iBAAA,EAAkB;AAE7B,IAAA,MAAM,aAAA,GAAgB,IAAI,OAAA,CAA2B,CAAC,SAAS,MAAA,KAAW;AACxE,MAAA,MAAM,QAAA,GAAW;AAAA,QACf,MAAA;AAAA,QACA,QAAQ,MAAA,EAA2B;AACjC,UAAA,OAAA,CAAQ,MAAM,CAAA;AACd,UAAA,OAAA,EAAQ;AAAA,QACV,CAAA;AAAA,QACA,OAAO,GAAA,EAAY;AACjB,UAAA,MAAA,CAAO,GAAG,CAAA;AACV,UAAA,OAAA,EAAQ;AAAA,QACV;AAAA,OACF;AACA,MAAA,IAAA,CAAK,UAAA,CAAW,IAAI,QAAQ,CAAA;AAE5B,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,IAAA,CAAK,UAAA,CAAW,OAAO,QAAQ,CAAA;AAC/B,QAAA,IAAA,CAAK,uBAAA,EAAwB;AAC7B,QAAA,MAAA,CAAO,OAAO,MAAM,CAAA;AACpB,QAAA,OAAA,EAAQ;AAAA,MACV,CAAA;AAEA,MAAA,SAAS,OAAA,GAAU;AACjB,QAAA,MAAA,CAAO,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAAA,MAC7C;AAEA,MAAA,MAAA,CAAO,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAAA,IAC1C,CAAC,CAAA;AAGD,IAAA,aAAA,CAAc,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAE5B,IAAA,OAAO,EAAE,aAAA,EAAe,MAAM,aAAA,EAAc;AAAA,EAC9C;AAAA,EAEA,MAAM,QAAA,GAAW;AACf,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,YAAA,EAAc,KAAA,CAAM,MAAM,MAAS,CAAA;AAC3D,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,cAAc,KAAA,EAAe;AAC3B,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,CAAA,0CAAA,EAA6C,KAAK,CAAA,CAAA,CAAG,CAAA;AACxE,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,UAAA,EAAY;AAC/B,MAAA,IAAI,CAAA,CAAE,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA,EAAG;AACvB,QAAA,CAAA,CAAE,OAAA,CAAQ,EAAE,KAAA,EAAO,CAAA;AACnB,QAAA,IAAA,CAAK,UAAA,CAAW,OAAO,CAAC,CAAA;AAAA,MAC1B;AAAA,IACF;AACA,IAAA,IAAA,CAAK,uBAAA,EAAwB;AAAA,EAC/B;AAAA;AAAA;AAAA,EAIA,aAAa,KAAA,EAAc;AACzB,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA;AAAA,MACX,CAAA,mDAAA,CAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,UAAA,EAAY;AAC/B,MAAA,CAAA,CAAE,MAAA,CAAO,IAAI,KAAA,CAAM,4BAA4B,CAAC,CAAA;AAAA,IAClD;AACA,IAAA,IAAA,CAAK,WAAW,KAAA,EAAM;AACtB,IAAA,IAAA,CAAK,uBAAA,EAAwB;AAAA,EAC/B;AAAA,EAEA,uBAAA,GAA0B;AAExB,IAAA,IAAI,KAAK,UAAA,CAAW,IAAA,KAAS,CAAA,IAAK,CAAC,KAAK,YAAA,EAAc;AACpD,MAAA,IAAA,CAAK,YAAA,GAAe,WAAW,MAAM;AACnC,QAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AACpB,QAAA,IAAA,CAAK,YAAA,EAAc,KAAK,CAAA,IAAA,KAAQ;AAC9B,UAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,2CAA2C,CAAA;AAC7D,UAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AACpB,UAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAAA,QAC9B,CAAC,CAAA;AAAA,MACH,GAAG,8BAA8B,CAAA;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,mBAAmB,IAAA,EAA4B;AAC7C,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,MAAA,aAAA,CAAc,KAAK,kBAAkB,CAAA;AACrC,MAAA,IAAA,CAAK,kBAAA,GAAqB,MAAA;AAAA,IAC5B;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,oBAAA,CAAqB,IAAI,CAAA,CAAE,MAAM,CAAA,KAAA,KAAS;AACrD,MAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,CAAA,qCAAA,CAAA,EAAyC,KAAK,CAAA;AAAA,IACnE,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,kBAAA,EAAmB;AAAA,EAC1B;AAAA,EAEA,MAAM,iBAAA,GAAoB;AACxB,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,IAC7C;AACA,IAAA,IAAI,KAAK,YAAA,EAAc;AACrB,MAAA,MAAM,IAAA,CAAK,YAAA;AACX,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,YAAA,GAAe,OAAA,CAAQ,OAAA,EAAQ,CAAE,KAAK,YAAY;AACrD,MAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAAQ,oBAAA,EAAqB;AAErD,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA,OAAA,EAAU,aAAa,CAAA,CAAE,CAAA;AAG1C,QAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,UAAA,aAAA,CAAc,KAAK,kBAAkB,CAAA;AAAA,QACvC;AACA,QAAA,IAAA,CAAK,kBAAA,GAAqB,YAAY,MAAM;AAC1C,UAAA,IAAA,CAAK,KAAA,CAAM,UAAU,CAAA,CAAE,KAAA,CAAM,CAAA,KAAA,KAAS;AACpC,YAAA,IAAA,CAAK,YAAA,GAAe,KAAA,CAAA;AACpB,YAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,YAAA,IAAA,CAAK,YAAA,CAAa,IAAIC,qBAAA,CAAe,kBAAA,EAAoB,KAAK,CAAC,CAAA;AAAA,UACjE,CAAC,CAAA;AAAA,QACH,GAAG,qBAAqB,CAAA;AAExB,QAAA,IAAA,CAAK,EAAA,CAAG,gBAAgB,CAAA,KAAA,KAAS;AAC/B,UAAA,IAAA,CAAK,aAAA,CAAc,MAAM,OAAO,CAAA;AAAA,QAClC,CAAC,CAAA;AACD,QAAA,IAAA,CAAK,EAAA,CAAG,SAAS,CAAA,KAAA,KAAS;AACxB,UAAA,IAAA,CAAK,YAAA,GAAe,KAAA,CAAA;AACpB,UAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,UAAA,IAAA,CAAK,aAAa,KAAK,CAAA;AAAA,QACzB,CAAC,CAAA;AACD,QAAA,IAAA,CAAK,EAAA,CAAG,OAAO,CAAA,KAAA,KAAS;AACtB,UAAA,IAAA,CAAK,YAAA,GAAe,KAAA,CAAA;AACpB,UAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,UAAA,IAAA,CAAK,YAAA;AAAA,YACH,KAAA,IAAS,IAAI,KAAA,CAAM,+BAA+B;AAAA,WACpD;AAAA,QACF,CAAC,CAAA;AACD,QAAA,OAAO,IAAA;AAAA,MACT,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF,CAAC,CAAA;AACD,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,YAAA;AAAA,IACb,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AACpB,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF;AAEO,MAAM,qBAAA,CAA+C;AAAA,EAC1D,aAAa,OAAO,OAAA,EAae;AACjC,IAAA,MAAM,EAAA,GAAK,MAAM,OAAA,CAAQ,QAAA,CAAS,SAAA,EAAU;AAE5C,IAAA,IAAI,EAAA,CAAG,MAAA,CAAO,MAAA,CAAO,MAAA,KAAW,IAAA,EAAM;AACpC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,qDAAA,EAAwD,EAAA,CAAG,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,CAAA;AAAA,OACjF;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,UAAA,EAAY,IAAA,EAAM;AACtC,MAAA,MAAM,EAAA,CAAG,QAAQ,MAAA,CAAO;AAAA,QACtB,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,WAAW,IAAI,wBAAA,CAAyB,EAAA,CAAG,MAAA,EAAQ,QAAQ,MAAM,CAAA;AAEvE,IAAA,MAAM,QAAQ,IAAI,qBAAA;AAAA,MAChB,EAAA;AAAA,MACA,OAAA,CAAQ,MAAA;AAAA,MACR,QAAA;AAAA,MACA,OAAA,CAAQ,QAAQ,QAAA,IAAY,wBAAA;AAAA,MAC5BC,4BAAA,CAAuB,OAAA,CAAQ,MAAA,EAAQ,MAAA,IAAU,sBAAsB,CAAA;AAAA,MACvEA,4BAAA,CAAuB,OAAA,CAAQ,MAAA,EAAQ,MAAA,IAAU,sBAAsB;AAAA,KACzE;AAEA,IAAA,MAAM,OAAA,CAAQ,UAAU,YAAA,CAAa;AAAA,MACnC,EAAA,EAAI,mBAAA;AAAA,MACJ,KAAA,EAAO,QAAA;AAAA,MACP,SAAA,EAAW,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,MACxB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,MACtB,YAAA,EAAc,EAAE,OAAA,EAAS,EAAA,EAAG;AAAA,MAC5B,EAAA,EAAI,CAAA,MAAA,KAAU,KAAA,CAAM,QAAA,CAAS,MAAM;AAAA,KACpC,CAAA;AAED,IAAA,OAAA,CAAQ,SAAA,CAAU,gBAAgB,YAAY;AAC5C,MAAA,MAAM,SAAS,QAAA,EAAS;AAAA,IAC1B,CAAC,CAAA;AAED,IAAA,OAAO,KAAA;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,OAAA,CAAQ;AAAA,IACnB,EAAA;AAAA,IACA,MAAA;AAAA,IACA,MAAA,GAAS,CAAA;AAAA,IACT,MAAA,GAAS;AAAA,GACX,EAKG;AACD,IAAA,MAAM,GAAG,OAAA,CAAQ,MAAA,CAAO,EAAE,SAAA,EAAW,eAAe,CAAA;AAEpD,IAAA,MAAM,QAAQ,IAAI,qBAAA;AAAA,MAChB,EAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAI,wBAAA,CAAyB,EAAA,CAAG,MAAA,EAAQ,MAAM,CAAA;AAAA,MAC9C,CAAA;AAAA,MACA,MAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO,MAAA,CAAO,OAAO,KAAA,EAAO;AAAA,MAC1B,KAAA,EAAO,CAAC,MAAA,KAAyB,KAAA,CAAM,SAAS,MAAM;AAAA,KACvD,CAAA;AAAA,EACH;AAAA,EAES,GAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA,eAAA;AAAA,EACA,aAAA;AAAA,EACA,aAAA;AAAA,EAED,YACN,EAAA,EACA,MAAA,EACA,QAAA,EACA,cAAA,EACA,cACA,YAAA,EACA;AACA,IAAA,IAAA,CAAK,GAAA,GAAM,EAAA;AACX,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,eAAA,GAAkB,cAAA;AACvB,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AACrB,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AAAA,EACvB;AAAA,EAEA,MAAM,QAAQ,OAAA,EAI+B;AAC3C,IAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,CAAM,KAAA;AAC5B,IAAA,MAAM,mBAAA,GAAsB,OAAA,CAAQ,mBAAA,IAAuB,EAAC;AAG5D,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,GAAA,CAEvB,IAAA;AAAA,MACC,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,qBAAA,EAAuB;AAAA,QAClC,YAAA;AAAA;AAAA,QAEA,YAAA;AAAA,QACA,OAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACD;AAAA,KACH,CACC,MAAA;AAAA,MACC,CAAC,MACC,CAAA,CAEG,MAAA;AAAA,QACC,IAAA,CAAK,IAAI,GAAA,CAAI,GAAA,EAAK,CAAC,SAAA,CAAU,OAAA,CAAQ,WAAW,CAAC,CAAC,CAAA;AAAA,QAClD,KAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,CAAC,KAAK,CAAC,CAAA;AAAA,QACzB,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK;AAAA,UAChB,KAAK,SAAA,CAAU;AAAA,YACb,OAAA,EAAS,QAAQ,KAAA,CAAM,YAAA;AAAA,YACvB,QAAA,EAAU,QAAQ,KAAA,CAAM;AAAA,WACzB;AAAA,SACF,CAAA;AAAA,QACD,KAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,CAAC,mBAAmB,CAAC;AAAA,OACzC,CAGC,IAAA,CAAK,mBAAmB,CAAA,CACxB,UAAA,CAAW,MAAM,mBAAmB,CAAA,CACpC,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAA,CACjD,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,UAAU,CAAA,EAAG,GAAA,EAAK,CAAC;AAAA;AAAA,KAC9C,CACC,UAA4B,IAAI,CAAA;AAEnC,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,6CAAA,EAAgD,OAAO,MAAM,CAAA,KAAA;AAAA,OAC/D;AAAA,IACF;AAEA,IAAA,MAAM,CAAC,EAAE,EAAA,EAAI,CAAA,GAAI,MAAA;AAGjB,IAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,GAAA,CAAI,MAAA;AAAA,MAClC,KAAK,GAAA,CAAI,GAAA,CAAI,mBAAmB,CAAC,aAAA,EAAe,KAAK,CAAC;AAAA,KACxD;AACA,IAAA,IAAI,YAAA,EAAc,WAAW,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,QACX,CAAA,+CAAA,EAAkD,EAAE,CAAA,YAAA,EAAe,KAAK,CAAA,CAAA;AAAA,OAC1E;AAAA,IACF;AAEA,IAAA,OAAO,EAAE,SAAS,EAAA,EAAG;AAAA,EACvB;AAAA,EAEA,MAAM,kBAAA,CACJ,EAAA,EACA,MAAA,EACA,WAAA,EACe;AACf,IAAA,MAAM,CAAC,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA,GAAI,MAAM,IAAA,CAAK,GAAA,CAAI,YAAY,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAC9D,IAAA,MAAM,SAAS,MAAM,IAAA,CAAK,GAAA,CAAsB,mBAAmB,EAChE,MAAA,CAAO;AAAA,MACN,EAAA;AAAA,MACA,UAAA,EAAY,UAAU,WAAW,CAAA;AAAA,MACjC,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,GAAA,EAAI;AAAA,MAC5B,MAAA;AAAA,MACA,YAAY,KAAA,IAAS;AAAA,KACtB,CAAA,CACA,UAAA,CAAW,IAAI,CAAA,CACf,KAAA,CAAM,CAAC,YAAA,EAAc,QAAA,EAAU,YAAY,CAAC,CAAA,CAC5C,UAAU,GAAG,CAAA;AAEhB,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,uCAAA,EAA0C,OAAO,MAAM,CAAA,KAAA;AAAA,OACzD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,EAAA,EAAgD;AAQrE,IAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,MAAM,KAAK,GAAA,CAAI,GAAA;AAAA,MAGtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAAA,MAwCA,EAAE,EAAA,EAAI,KAAA,EAAO,cAAA;AAAe,KAC9B;AAEA,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,MAAA,MAAM,IAAIC,oBAAA,CAAc,CAAA,sBAAA,EAAyB,EAAE,CAAA,WAAA,CAAa,CAAA;AAAA,IAClE,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,kDAAA,EAAqD,OAAO,MAAM,CAAA,KAAA;AAAA,OACpE;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAC,CAAA,CAAE,MAAA;AACvB,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG;AAC9B,MAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAE;AAAA,IACtB;AAEA,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,KAAO;AACtB,QAAA,MAAM,EAAE,OAAA,EAAS,QAAA,KAAa,IAAA,CAAK,KAAA,CAAM,IAAI,SAAS,CAAA;AACtD,QAAA,OAAO;AAAA,UACL,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,YAAA,EAAc,OAAA;AAAA,UACd;AAAA,SACF;AAAA,MACF,CAAC;AAAA,KACH;AAAA,EACF;AAAA,EAEA,MAAM,aAAA,CACJ,cAAA,EACA,OAAA,EAG0D;AAC1D,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,GAAA,CAAsB,mBAAmB,CAAA,CAChE,MAAA,CAAO,QAAQ,CAAA,CACf,MAAM,EAAE,EAAA,EAAI,cAAA,EAAgB,EAC5B,KAAA,EAAM;AAET,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,MAAM,IAAIA,oBAAA;AAAA,QACR,yBAAyB,cAAc,CAAA,WAAA;AAAA,OACzC;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,OAAO,cAAA,EAAe;AAE9B,IAAA,OAAO,KAAK,SAAA,CAAU,aAAA;AAAA,MACpB,IAAI,GAAA,CAAI,MAAA,CAAO,MAAA,IAAU,EAAE,CAAA;AAAA,MAC3B,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,MAAA,EAAsB;AACnC,IAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,EAAI,GAAI,uBAAA;AAC5B,IAAA,IAAI,UAAA,GAAa,CAAA;AACjB,IAAA,IAAI,eAAA,GAAkB,CAAA;AAEtB,IAAA,MAAM,mBAAA,GAAsB,OAAO,GAAA,KAA0B;AAC3D,MAAA,MAAM,WAAA,GAAc,MAAA,GAAS,IAAA,CAAK,GAAA,EAAI;AACtC,MAAA,IAAI,eAAe,CAAA,EAAG;AACpB,QAAA,OAAO,KAAA;AAAA,MACT;AAEA,MAAA,MAAM,GAAA,CAAI,GAAA;AAAA,QACR,kCAAkC,IAAA,CAAK,GAAA;AAAA,UACrC,4BAAA;AAAA,UACA;AAAA,SACD,CAAA,GAAA;AAAA,OACH;AACA,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAEA,IAAA,MAAM,gBAAA,GAAmB,CAAC,aAAA,EAAqB,KAAA,KAAmB;AAChE,MAAA,MAAM,QAAA,GAAuC,CAAC,aAAa,CAAA;AAC3D,MAAA,IAAI,UAAU,MAAA,EAAW;AACvB,QAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AAAA,MACrB;AACA,MAAA,QAAA,CAAS,KAAK,kBAAkB,CAAA;AAEhC,MAAA,OAAO,IAAA,CAAK,GAAA,CAAI,WAAA,CAAY,OAAM,GAAA,KAAO;AACvC,QAAA,IAAI,CAAE,MAAM,mBAAA,CAAoB,GAAG,CAAA,EAAI;AACrC,UAAA,OAAO,CAAA;AAAA,QACT;AACA,QAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,GAAA;AAAA,UACvB,CAAA;AAAA;AAAA,mCAAA,EAE2B,KAAA,KAAU,MAAA,GAAY,EAAA,GAAK,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,6DAAA,CAAA;AAAA,UAMnE;AAAA,SACF;AACA,QAAA,OAAO,MAAA,CAAO,QAAA;AAAA,MAChB,CAAC,CAAA;AAAA,IACH,CAAA;AAEA,IAAA,MAAM,wBAAwB,MAC5B,IAAA,CAAK,GAAA,CAAI,WAAA,CAAoB,OAAM,GAAA,KAAO;AACxC,MAAA,IAAI,CAAE,MAAM,mBAAA,CAAoB,GAAG,CAAA,EAAI;AACrC,QAAA,OAAO,CAAA;AAAA,MACT;AACA,MAAA,MAAM,CAAC,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA,GAAI,MAAM,GAAA,CAAI,YAAY,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAEzD,MAAA,IAAI,CAAE,MAAM,mBAAA,CAAoB,GAAG,CAAA,EAAI;AACrC,QAAA,OAAO,CAAA;AAAA,MACT;AAEA,MAAA,MAAM,KAAA,GACJ,KAAA,KAAU,IAAA,GACN,gBAAA,GACA,oCAAA;AACN,MAAA,MAAM,OAAA,GACJ,KAAA,KAAU,IAAA,GAAO,IAAI,IAAA,CAAK,KAAK,GAAA,EAAI,GAAI,IAAA,CAAK,aAAa,CAAA,GAAI,KAAA;AAC/D,MAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,GAAA;AAAA,QACvB,CAAA;AAAA;AAAA,kBAAA,EAEU,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,2DAAA,CAAA;AAAA,QAMf,CAAC,SAAS,kBAAkB;AAAA,OAC9B;AACA,MAAA,OAAO,MAAA,CAAO,QAAA;AAAA,IAChB,CAAC,CAAA;AAGH,IAAA,IAAI,iBAAA,GAAwD,QAAA;AAC5D,IAAA,IAAI,WAAA;AACJ,IAAA,IAAI,MAAA,GAAS,KAAA;AAIb,IAAA,OAAO,CAAC,UAAU,CAAC,MAAA,EAAQ,WAAW,IAAA,CAAK,GAAA,KAAQ,MAAA,EAAQ;AACzD,MAAA,IAAI,iBAAA,GAAoB,CAAA;AAExB,MAAA,IAAI;AACF,QAAA,IAAI,sBAAsB,QAAA,EAAU;AAClC,UAAA,MAAM,UAAU,MAAM,gBAAA;AAAA,YACpB,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,EAAI,GAAI,KAAK,aAAa;AAAA,WAC1C;AACA,UAAA,UAAA,IAAc,OAAA;AACd,UAAA,IAAI,UAAU,kBAAA,EAAoB;AAChC,YAAA,iBAAA,GAAoB,UAAA;AAAA,UACtB;AAAA,QACF,CAAA,MAAA,IAAW,sBAAsB,UAAA,EAAY;AAC3C,UAAA,IAAI,gBAAgB,KAAA,CAAA,EAAW;AAC7B,YAAA,MAAM,SAAS,MAAM,IAAA,CAAK,GAAA,CAAI,WAAA,CAAY,OAAM,GAAA,KAAO;AACrD,cAAA,IAAI,CAAE,MAAM,mBAAA,CAAoB,GAAG,CAAA,EAAI;AACrC,gBAAA,OAAO,KAAA,CAAA;AAAA,cACT;AACA,cAAA,OAAO,GAAA,CAAe,YAAY,CAAA,CAC/B,MAAA,CAAO,IAAI,CAAA,CACX,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,MAAA,CAAO,IAAA,CAAK,eAAe,EAC3B,KAAA,EAAM;AAAA,YACX,CAAC,CAAA;AACD,YAAA,WAAA,GAAc,MAAA,EAAQ,EAAA;AAAA,UACxB;AAEA,UAAA,IAAI,gBAAgB,KAAA,CAAA,EAAW;AAC7B,YAAA,iBAAA,GAAoB,UAAA;AAAA,UACtB,CAAA,MAAO;AACL,YAAA,MAAM,UAAU,MAAM,gBAAA;AAAA,cACpB,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,EAAI,GAAI,KAAK,aAAa,CAAA;AAAA,cACxC;AAAA,aACF;AACA,YAAA,UAAA,IAAc,OAAA;AACd,YAAA,IAAI,UAAU,kBAAA,EAAoB;AAChC,cAAA,iBAAA,GAAoB,UAAA;AAAA,YACtB;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,sBAAA,EAAwB,KAAK,CAAA;AAChD,QAAA,MAAA,GAAS,IAAA;AAAA,MACX;AAEA,MAAA,IAAI;AACF,QAAA,iBAAA,GAAoB,MAAM,qBAAA,EAAsB;AAChD,QAAA,eAAA,IAAmB,iBAAA;AAAA,MACrB,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,6BAAA,EAA+B,KAAK,CAAA;AACvD,QAAA,MAAA,GAAS,IAAA;AAAA,MACX;AAEA,MAAA,IACE,iBAAA,KAAsB,UAAA,IACtB,iBAAA,GAAoB,kBAAA,EACpB;AACA,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAI,aAAa,CAAA,EAAG;AAClB,MAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,QACX,6BAA6B,UAAU,CAAA,yBAAA;AAAA,OACzC;AAAA,IACF;AACA,IAAA,IAAI,kBAAkB,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,QACX,oCAAoC,eAAe,CAAA,gCAAA;AAAA,OACrD;AAAA,IACF;AAAA,EACF;AACF;;"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// @ts-check
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The event bus currently creates its tables only for PostgreSQL. Build these
|
|
21
|
+
* indexes concurrently so upgrading a busy event bus does not block writes.
|
|
22
|
+
* The old topic index is replaced by (topic, id), which also serves topic-only
|
|
23
|
+
* lookups while letting subscription reads start at their cursor.
|
|
24
|
+
*
|
|
25
|
+
* This migration runs outside a transaction because PostgreSQL forbids
|
|
26
|
+
* CREATE/DROP INDEX CONCURRENTLY inside one. An interrupted concurrent build
|
|
27
|
+
* can leave an invalid index, so retries repair it before continuing.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {import('knex').Knex} knex
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @param {string[]} columns
|
|
34
|
+
*/
|
|
35
|
+
async function ensureIndex(knex, name, columns) {
|
|
36
|
+
const { rows } = await knex.raw(
|
|
37
|
+
`SELECT i.indisvalid, i.indisunique,
|
|
38
|
+
pg_get_indexdef(i.indexrelid, 1, true) AS first_column,
|
|
39
|
+
pg_get_indexdef(i.indexrelid, 2, true) AS second_column
|
|
40
|
+
FROM pg_class index_table
|
|
41
|
+
JOIN pg_namespace namespace ON namespace.oid = index_table.relnamespace
|
|
42
|
+
JOIN pg_index i ON i.indexrelid = index_table.oid
|
|
43
|
+
JOIN pg_class event_table ON event_table.oid = i.indrelid
|
|
44
|
+
WHERE index_table.relname = ?
|
|
45
|
+
AND namespace.nspname = current_schema()
|
|
46
|
+
AND event_table.relname = 'event_bus_events'`,
|
|
47
|
+
[name],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (rows.length > 0) {
|
|
51
|
+
const index = rows[0];
|
|
52
|
+
if (index.indisvalid) {
|
|
53
|
+
const actualColumns = [index.first_column, index.second_column].filter(
|
|
54
|
+
Boolean,
|
|
55
|
+
);
|
|
56
|
+
if (index.indisunique || actualColumns.join(',') !== columns.join(',')) {
|
|
57
|
+
throw new Error(`Existing index ${name} has an unexpected definition`);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await knex.raw(`DROP INDEX CONCURRENTLY IF EXISTS ${name}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await knex.raw(
|
|
66
|
+
`CREATE INDEX CONCURRENTLY ${name} ON event_bus_events (${columns.join(
|
|
67
|
+
', ',
|
|
68
|
+
)})`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {import('knex').Knex} knex */
|
|
73
|
+
exports.up = async function up(knex) {
|
|
74
|
+
if (knex.client.config.client !== 'pg') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await ensureIndex(knex, 'event_bus_events_created_at_id_idx', [
|
|
79
|
+
'created_at',
|
|
80
|
+
'id',
|
|
81
|
+
]);
|
|
82
|
+
await ensureIndex(knex, 'event_bus_events_topic_id_idx', ['topic', 'id']);
|
|
83
|
+
await knex.raw(
|
|
84
|
+
'DROP INDEX CONCURRENTLY IF EXISTS event_bus_events_topic_idx',
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** @param {import('knex').Knex} knex */
|
|
89
|
+
exports.down = async function down(knex) {
|
|
90
|
+
if (knex.client.config.client !== 'pg') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await ensureIndex(knex, 'event_bus_events_topic_idx', ['topic']);
|
|
95
|
+
await knex.raw(
|
|
96
|
+
'DROP INDEX CONCURRENTLY IF EXISTS event_bus_events_topic_id_idx',
|
|
97
|
+
);
|
|
98
|
+
await knex.raw(
|
|
99
|
+
'DROP INDEX CONCURRENTLY IF EXISTS event_bus_events_created_at_id_idx',
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
exports.config = { transaction: false };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-events-backend",
|
|
3
|
-
"version": "0.6.7-next.
|
|
3
|
+
"version": "0.6.7-next.2",
|
|
4
4
|
"backstage": {
|
|
5
5
|
"role": "backend-plugin",
|
|
6
6
|
"pluginId": "events",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"test": "backstage-cli package test"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@backstage/backend-openapi-utils": "0.7.3-next.
|
|
70
|
+
"@backstage/backend-openapi-utils": "0.7.3-next.2",
|
|
71
71
|
"@backstage/backend-plugin-api": "1.11.0-next.1",
|
|
72
72
|
"@backstage/config": "1.3.10-next.0",
|
|
73
73
|
"@backstage/errors": "1.3.2-next.0",
|
|
@@ -81,11 +81,11 @@
|
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@backstage/backend-app-api": "1.8.0-next.1",
|
|
84
|
-
"@backstage/backend-defaults": "0.18.1-next.
|
|
84
|
+
"@backstage/backend-defaults": "0.18.1-next.2",
|
|
85
85
|
"@backstage/backend-test-utils": "1.11.8-next.1",
|
|
86
86
|
"@backstage/cli": "0.36.7-next.0",
|
|
87
87
|
"@backstage/plugin-events-backend-test-utils": "0.1.60-next.0",
|
|
88
|
-
"@backstage/repo-tools": "0.19.2-next.
|
|
88
|
+
"@backstage/repo-tools": "0.19.2-next.2",
|
|
89
89
|
"@types/content-type": "^1.1.8",
|
|
90
90
|
"supertest": "^7.0.0"
|
|
91
91
|
},
|