@cosmicdrift/kumiko-framework 0.367.0 → 0.369.0
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/AGENTS.md +1 -7
- package/README.md +1 -1
- package/agents/examples/access-control.md +2 -2
- package/agents/examples/anonymous-access-multitenant.md +2 -2
- package/agents/examples/anonymous-access.md +3 -3
- package/agents/examples/apex-landing.md +1 -1
- package/agents/examples/apex-surface-auth.md +6 -7
- package/agents/examples/api-worker-split.md +7 -8
- package/agents/examples/auth-claims.md +2 -1
- package/agents/examples/auth-foundation-providers.md +3 -3
- package/agents/examples/auth-mfa.md +3 -4
- package/agents/examples/basic-entity.md +3 -4
- package/agents/examples/chat-channels.md +4 -4
- package/agents/examples/compliance-profiles-demo.md +2 -3
- package/agents/examples/cross-feature-events.md +4 -4
- package/agents/examples/crypto-shredding-hr.md +14 -9
- package/agents/examples/currencies-global.md +3 -4
- package/agents/examples/currencies-per-tenant.md +3 -4
- package/agents/examples/custom-fields-basic.md +2 -3
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/dashboard-stats.md +2 -3
- package/agents/examples/delivery-notifications.md +5 -6
- package/agents/examples/e2e-generator.md +1 -1
- package/agents/examples/embedded-entity-form.md +2 -3
- package/agents/examples/embedded.md +2 -3
- package/agents/examples/encrypted-tenant-config.md +5 -6
- package/agents/examples/error-contract.md +3 -4
- package/agents/examples/event-sourcing.md +19 -23
- package/agents/examples/feature-toggles.md +4 -4
- package/agents/examples/field-access.md +3 -4
- package/agents/examples/files-post-processing.md +12 -13
- package/agents/examples/golden-path.md +208 -0
- package/agents/examples/idempotency.md +2 -2
- package/agents/examples/inbound-mail-triage.md +7 -7
- package/agents/examples/lane-routing.md +3 -3
- package/agents/examples/ledger-banking.md +2 -3
- package/agents/examples/ledger-invoicing.md +2 -3
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +3 -3
- package/agents/examples/managed-config.md +3 -4
- package/agents/examples/managed-pages.md +2 -2
- package/agents/examples/notes-history-basic.md +4 -5
- package/agents/examples/ownership.md +3 -5
- package/agents/examples/pdf-audit-export.md +2 -3
- package/agents/examples/pipeline-basics.md +2 -2
- package/agents/examples/public-share-token.md +3 -3
- package/agents/examples/rate-limiting.md +2 -1
- package/agents/examples/realtime-sse.md +2 -3
- package/agents/examples/record-detail-layout.md +1 -1
- package/agents/examples/reference-data.md +3 -3
- package/agents/examples/relations.md +2 -2
- package/agents/examples/screens-nav.md +1 -1
- package/agents/examples/search.md +2 -2
- package/agents/examples/secrets-demo.md +8 -8
- package/agents/examples/security-baseline.md +2 -2
- package/agents/examples/session-revocation.md +7 -8
- package/agents/examples/state-machine.md +181 -165
- package/agents/examples/tags-basic.md +2 -3
- package/agents/examples/tenant-isolation.md +2 -3
- package/agents/examples/tenant-settings.md +2 -2
- package/agents/examples/tier-admin.md +3 -4
- package/agents/examples/timezones.md +3 -3
- package/agents/examples/user-data-rights.md +12 -8
- package/agents/examples/user-profile.md +3 -3
- package/agents/examples/webhook-step.md +2 -3
- package/agents/examples/wizard-form.md +4 -5
- package/agents/examples/workflow-engine.md +7 -3
- package/agents/examples/writeform-section.md +1 -1
- package/agents/examples.md +1 -0
- package/agents/features.md +4 -4
- package/dist/consumer-cli.js +2 -3
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/owner-transition-rekey.js +2 -0
- package/dist/crypto/pg-kms-adapter.js +1 -1
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +30 -15
- package/dist/db/bun-db-deprecated.d.ts +10 -0
- package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
- package/dist/db/config-seed.js +1 -1
- package/dist/db/connection.d.ts +23 -5
- package/dist/db/connection.js +10 -5
- package/dist/db/dialect.js +1 -1
- package/dist/db/entity-field-encryption.d.ts +5 -0
- package/dist/db/entity-field-encryption.js +29 -0
- package/dist/db/entity-table-meta.js +6 -5
- package/dist/db/event-store-executor-read.js +26 -5
- package/dist/db/index.d.ts +4 -4
- package/dist/db/index.js +1 -1
- package/dist/db/migrate-runner.js +1 -1
- package/dist/db/queries/backfill-pii.js +16 -9
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/event-store.d.ts +2 -0
- package/dist/db/queries/event-store.js +26 -0
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/query.d.ts +105 -1
- package/dist/db/query.js +1450 -3
- package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
- package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
- package/dist/db/tenant-db.d.ts +11 -0
- package/dist/db/tenant-db.js +51 -12
- package/dist/derivatives/derivatives-context.d.ts +1 -1
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/gdpr-storage.js +3 -3
- package/dist/engine/boot-validator/pii-retention.js +3 -3
- package/dist/engine/boot-validator/record-owned.js +3 -3
- package/dist/engine/boot-validator/screens.js +37 -1
- package/dist/engine/bridge-stub.d.ts +4 -0
- package/dist/engine/bridge-stub.js +62 -0
- package/dist/engine/build-config-feature-schema.js +197 -65
- package/dist/engine/event-helpers.d.ts +2 -1
- package/dist/engine/event-helpers.js +1 -0
- package/dist/engine/extension-selector-plugins.d.ts +3 -0
- package/dist/engine/extension-selector-plugins.js +14 -0
- package/dist/engine/feature-config-events-jobs.d.ts +2 -0
- package/dist/engine/feature-config-events-jobs.js +3 -2
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/es-ops/operations-schema.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/archive.js +1 -1
- package/dist/event-store/errors.d.ts +2 -1
- package/dist/event-store/errors.js +6 -2
- package/dist/event-store/event-store.js +17 -9
- package/dist/event-store/events-schema.js +1 -1
- package/dist/event-store/rebuild-dead-letter.js +3 -3
- package/dist/event-store/snapshot.js +1 -1
- package/dist/event-store/upcaster-dead-letter.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +1 -1
- package/dist/files/file-routes.js +1 -1
- package/dist/i18n/settings-hub-keys.js +1 -2
- package/dist/internal/stack-support.d.ts +12 -0
- package/dist/internal/stack-support.js +12 -0
- package/dist/internal/testing.d.ts +4 -0
- package/dist/internal/testing.js +5 -0
- package/dist/jobs/job-runner.js +23 -11
- package/dist/migrations/pending-rebuilds.js +1 -1
- package/dist/migrations/pii-event-backfill.js +2 -2
- package/dist/pipeline/dispatch-shared.js +43 -23
- package/dist/pipeline/dispatch-write.js +6 -2
- package/dist/pipeline/dispatcher-utils.js +6 -2
- package/dist/pipeline/event-consumer-state.js +1 -1
- package/dist/pipeline/projection-rebuild.d.ts +1 -0
- package/dist/pipeline/projection-rebuild.js +1 -1
- package/dist/pipeline/projection-state.js +1 -1
- package/dist/pipeline/system-hooks.js +1 -1
- package/dist/schema-cli.js +3 -3
- package/dist/search/purge-subject.js +20 -10
- package/dist/seeding/index.d.ts +1 -0
- package/dist/seeding/index.js +1 -0
- package/dist/stack/index.d.ts +10 -9
- package/dist/stack/index.js +11 -19
- package/dist/testing/e2e-generator.d.ts +5 -49
- package/dist/testing/e2e-generator.js +5 -382
- package/dist/testing/handler-context.d.ts +5 -4
- package/dist/testing/handler-context.js +5 -69
- package/dist/testing/index.d.ts +10 -26
- package/dist/testing/index.js +10 -34
- package/dist/testing/real-providers.d.ts +5 -3
- package/dist/testing/real-providers.js +5 -21
- package/dist/upgrade-cli.js +6 -4
- package/package.json +37 -9
- package/src/changes.json +64 -0
- package/src/scripts/codemod/README.md +43 -0
- package/src/scripts/codemod/codemod-support.ts +276 -0
- package/src/scripts/codemod/db-factory.ts +810 -0
- package/src/scripts/codemod/feature-factory.ts +429 -0
- package/src/scripts/codemod/s3-imports.ts +274 -0
- package/src/scripts/codemod/tenant-db-options.ts +454 -0
- package/src/scripts/codemod/testing-move-targets.ts +161 -0
- package/src/scripts/codemod/testing-move.ts +520 -0
- package/dist/bun-db/connection.d.ts +0 -17
- package/dist/bun-db/connection.js +0 -40
- package/dist/bun-db/index.d.ts +0 -4
- package/dist/bun-db/query.d.ts +0 -106
- package/dist/bun-db/query.js +0 -1457
- package/dist/db/api.d.ts +0 -19
- package/dist/db/api.js +0 -23
- package/dist/db/bun-provider.d.ts +0 -2
- package/dist/db/bun-provider.js +0 -28
- package/dist/db/postgres-provider.d.ts +0 -2
- package/dist/db/postgres-provider.js +0 -21
- package/dist/db/query-api.d.ts +0 -2
- package/dist/db/query-api.js +0 -6
- package/dist/stack/db.d.ts +0 -22
- package/dist/stack/db.js +0 -67
- package/dist/stack/drain-event-consumers.d.ts +0 -4
- package/dist/stack/drain-event-consumers.js +0 -51
- package/dist/stack/drain-jobs.d.ts +0 -22
- package/dist/stack/drain-jobs.js +0 -141
- package/dist/stack/event-collector.d.ts +0 -9
- package/dist/stack/event-collector.js +0 -12
- package/dist/stack/push-entity-projection-tables.d.ts +0 -24
- package/dist/stack/push-entity-projection-tables.js +0 -74
- package/dist/stack/redis.d.ts +0 -17
- package/dist/stack/redis.js +0 -52
- package/dist/stack/request-helper.d.ts +0 -35
- package/dist/stack/request-helper.js +0 -144
- package/dist/stack/test-stack.d.ts +0 -192
- package/dist/stack/test-stack.js +0 -429
- package/dist/testing/access-assertions.d.ts +0 -2
- package/dist/testing/access-assertions.js +0 -13
- package/dist/testing/assertions.d.ts +0 -10
- package/dist/testing/assertions.js +0 -22
- package/dist/testing/boot-validator-fixture.d.ts +0 -3
- package/dist/testing/boot-validator-fixture.js +0 -101
- package/dist/testing/closed-connection-error.d.ts +0 -7
- package/dist/testing/closed-connection-error.js +0 -46
- package/dist/testing/db-cleanup.d.ts +0 -9
- package/dist/testing/db-cleanup.js +0 -31
- package/dist/testing/expect-error.d.ts +0 -2
- package/dist/testing/expect-error.js +0 -22
- package/dist/testing/file-provider-contract.d.ts +0 -2
- package/dist/testing/file-provider-contract.js +0 -102
- package/dist/testing/http-cookies.d.ts +0 -7
- package/dist/testing/http-cookies.js +0 -47
- package/dist/testing/late-bound.d.ts +0 -9
- package/dist/testing/late-bound.js +0 -31
- package/dist/testing/manual-clock.d.ts +0 -14
- package/dist/testing/manual-clock.js +0 -12
- package/dist/testing/multipart-helper.d.ts +0 -22
- package/dist/testing/multipart-helper.js +0 -77
- package/dist/testing/mutable-master-key-provider.d.ts +0 -7
- package/dist/testing/mutable-master-key-provider.js +0 -35
- package/dist/testing/observability-recorder.d.ts +0 -9
- package/dist/testing/observability-recorder.js +0 -29
- package/dist/testing/seed.d.ts +0 -9
- package/dist/testing/seed.js +0 -21
- package/dist/testing/shared-entities.d.ts +0 -286
- package/dist/testing/shared-entities.js +0 -56
- package/dist/testing/utils.d.ts +0 -1
- package/dist/testing/utils.js +0 -1
- package/dist/testing/wait-for.d.ts +0 -20
- package/dist/testing/wait-for.js +0 -47
- package/dist/testing/without-ambient-temporal.d.ts +0 -5
- package/dist/testing/without-ambient-temporal.js +0 -17
- package/dist/testing/zip-fixture.d.ts +0 -1
- package/dist/testing/zip-fixture.js +0 -33
- /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
- /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
package/dist/db/api.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
|
|
2
|
-
export type DbConnectionOptions = {
|
|
3
|
-
readonly maxConnections?: number;
|
|
4
|
-
readonly idleTimeoutSeconds?: number;
|
|
5
|
-
readonly connectTimeoutSeconds?: number;
|
|
6
|
-
readonly closeTimeoutSeconds?: number;
|
|
7
|
-
};
|
|
8
|
-
export declare const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
|
|
9
|
-
export type DbPoolHandle = {
|
|
10
|
-
/** Provider Connection — Calls gehen über asRawClient() oder direkt. */
|
|
11
|
-
readonly db: any;
|
|
12
|
-
/** Raw driver client (postgres-js or Bun.SQL) */
|
|
13
|
-
readonly client: any;
|
|
14
|
-
/** LISTEN-capable client; Bun.SQL serves it from its own dedicated connection */
|
|
15
|
-
readonly listenClient?: PgListenClient;
|
|
16
|
-
/** Pool schliessen */
|
|
17
|
-
close: () => Promise<void>;
|
|
18
|
-
};
|
|
19
|
-
export declare function createConnection(url: string, options?: DbConnectionOptions): Promise<DbPoolHandle>;
|
package/dist/db/api.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// DB-API: Unified Types und Connection-Factory.
|
|
2
|
-
// Provider-agnostic code verwendet ausschliesslich diese Types.
|
|
3
|
-
// Provider-Implementierungen: postgres-provider.ts, bun-provider.ts.
|
|
4
|
-
//
|
|
5
|
-
// `asRawClient(db)` aus bun-db/query.ts normalisiert beide Provider
|
|
6
|
-
// zu { unsafe, begin } — business code ist provider-neutral.
|
|
7
|
-
//
|
|
8
|
-
// Default: postgres-js. Set DB_PROVIDER=bun für Bun.SQL (experimentell).
|
|
9
|
-
// A DB outage can leave an in-flight query referenced on a dead connection,
|
|
10
|
-
// so a plain end() (no timeout = wait for queries to finish) hangs forever —
|
|
11
|
-
// close() must always bound how long shutdown waits.
|
|
12
|
-
export const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
|
|
13
|
-
let _provider;
|
|
14
|
-
export async function createConnection(url, options = {}) {
|
|
15
|
-
const p = process.env["DB_PROVIDER"];
|
|
16
|
-
if (p === "bun" || p === "bun-sql") {
|
|
17
|
-
const { createBunConnection } = await import("./bun-provider.js");
|
|
18
|
-
return createBunConnection(url, options);
|
|
19
|
-
}
|
|
20
|
-
// postgres-js: sync, kein async import nötig
|
|
21
|
-
const { createPgConnection } = await import("./postgres-provider.js");
|
|
22
|
-
return createPgConnection(url, options);
|
|
23
|
-
}
|
package/dist/db/bun-provider.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Bun.SQL provider, opt-in via DB_PROVIDER=bun; postgres-js stays the default.
|
|
2
|
-
// Known bug (Bun <= 1.4.3, oven-sh/bun#29484): a cached plan is not re-prepared
|
|
3
|
-
// after DDL, so e.g. `SELECT *` after ADD COLUMN fails on that connection with
|
|
4
|
-
// 0A000 "cached plan must not change result type". prepare:false is no fix,
|
|
5
|
-
// it changes how jsonb params are bound.
|
|
6
|
-
//
|
|
7
|
-
// LISTEN runs on Bun.SQL's own dedicated connection, which is exempt from the
|
|
8
|
-
// pool's idleTimeout and maxLifetime and re-LISTENs after a drop.
|
|
9
|
-
import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS, } from "./api.js";
|
|
10
|
-
export function createBunConnection(url, options = {}) {
|
|
11
|
-
const bunOpts = {};
|
|
12
|
-
if (options.maxConnections !== undefined)
|
|
13
|
-
bunOpts.max = options.maxConnections;
|
|
14
|
-
if (options.idleTimeoutSeconds !== undefined)
|
|
15
|
-
bunOpts.idleTimeout = options.idleTimeoutSeconds;
|
|
16
|
-
if (options.connectTimeoutSeconds !== undefined)
|
|
17
|
-
bunOpts.connectionTimeout = options.connectTimeoutSeconds;
|
|
18
|
-
const db = new Bun.SQL(url, bunOpts);
|
|
19
|
-
const closeTimeout = options.closeTimeoutSeconds ?? DEFAULT_DB_CLOSE_TIMEOUT_SECONDS;
|
|
20
|
-
return {
|
|
21
|
-
db,
|
|
22
|
-
client: db,
|
|
23
|
-
listenClient: db,
|
|
24
|
-
close: async () => {
|
|
25
|
-
await db.end({ timeout: closeTimeout });
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// PostgreSQL Provider: postgres-js.
|
|
2
|
-
// Standard-Provider — stabil, keine bekannten Protocol-Level-Bugs.
|
|
3
|
-
// `asRawClient(db)` wrappt .unsafe und .begin transparent.
|
|
4
|
-
import postgres from "postgres";
|
|
5
|
-
import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS, } from "./api.js";
|
|
6
|
-
export function createPgConnection(url, options = {}) {
|
|
7
|
-
const pgOptions = {};
|
|
8
|
-
if (options.maxConnections !== undefined)
|
|
9
|
-
pgOptions.max = options.maxConnections;
|
|
10
|
-
if (options.idleTimeoutSeconds !== undefined)
|
|
11
|
-
pgOptions.idle_timeout = options.idleTimeoutSeconds;
|
|
12
|
-
if (options.connectTimeoutSeconds !== undefined) {
|
|
13
|
-
pgOptions.connect_timeout = options.connectTimeoutSeconds;
|
|
14
|
-
}
|
|
15
|
-
const client = postgres(url, pgOptions);
|
|
16
|
-
return {
|
|
17
|
-
db: client,
|
|
18
|
-
client,
|
|
19
|
-
close: () => client.end({ timeout: options.closeTimeoutSeconds ?? DEFAULT_DB_CLOSE_TIMEOUT_SECONDS }),
|
|
20
|
-
};
|
|
21
|
-
}
|
package/dist/db/query-api.d.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export type { JsonTextMatch, SelectOptions, WhereObject, WhereOperator, WhereValue, } from "../db/query.js";
|
|
2
|
-
export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
|
package/dist/db/query-api.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
// Legacy re-export shim — query-api.ts hat früher drizzle gewrapped.
|
|
2
|
-
// Heute liegen die Helpers in src/bun-db/. Wir re-exportieren von dort
|
|
3
|
-
// damit existing imports `@cosmicdrift/kumiko-framework/db` weiterhin
|
|
4
|
-
// funktionieren während wir die Callers schrittweise auf den direkten
|
|
5
|
-
// bun-db-Import migrieren.
|
|
6
|
-
export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
|
package/dist/stack/db.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { DbConnection, PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
|
|
2
|
-
declare function requireEnv(name: string): string;
|
|
3
|
-
export type TestDb = {
|
|
4
|
-
db: DbConnection;
|
|
5
|
-
outsideTransactionDb: DbConnection;
|
|
6
|
-
client: PgListenClient;
|
|
7
|
-
dbName: string;
|
|
8
|
-
cleanup: () => Promise<void>;
|
|
9
|
-
};
|
|
10
|
-
export type CreateTestDbOptions = {
|
|
11
|
-
readonly baseUrl?: string;
|
|
12
|
-
readonly dbName?: string;
|
|
13
|
-
readonly persistent?: boolean;
|
|
14
|
-
readonly poolMax?: number;
|
|
15
|
-
readonly outsideTransactionPoolMax?: number;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
|
|
19
|
-
* Für Bun.SQL: DB_PROVIDER=bun setzen (experimentell — siehe db/bun-provider.ts).
|
|
20
|
-
*/
|
|
21
|
-
export declare function createTestDb(arg?: string | CreateTestDbOptions): Promise<TestDb>;
|
|
22
|
-
export { requireEnv };
|
package/dist/stack/db.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// Test-DB Factory: CREATE DATABASE → connect → createEventsTable.
|
|
2
|
-
// Provider-agnostic via createConnection (DB_PROVIDER env).
|
|
3
|
-
// postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
|
|
4
|
-
import { createConnection } from "../db/api.js";
|
|
5
|
-
import { DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX } from "../db/connection.js";
|
|
6
|
-
import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack.js";
|
|
7
|
-
import { ensureTemporalPolyfill } from "../time/polyfill.js";
|
|
8
|
-
import { generateId } from "../utils/index.js";
|
|
9
|
-
function requireEnv(name) {
|
|
10
|
-
const value = process.env[name];
|
|
11
|
-
if (!value) {
|
|
12
|
-
throw new Error(`Missing required env var: ${name}. Copy .env.example to .env and fill in values. Tests also need the test services (Postgres, Redis) running, e.g. "docker compose up -d".`);
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
|
|
18
|
-
* Für Bun.SQL: DB_PROVIDER=bun setzen (experimentell — siehe db/bun-provider.ts).
|
|
19
|
-
*/
|
|
20
|
-
export async function createTestDb(arg) {
|
|
21
|
-
await ensureTemporalPolyfill();
|
|
22
|
-
const opts = typeof arg === "string" ? { baseUrl: arg } : (arg ?? {});
|
|
23
|
-
const url = opts.baseUrl ?? requireEnv("TEST_DATABASE_URL");
|
|
24
|
-
const dbName = opts.dbName ?? `kumiko_test_${generateId().slice(-8)}`;
|
|
25
|
-
const adminUrl = url.replace(/\/[^/]+$/, "/postgres");
|
|
26
|
-
const admin = await createConnection(adminUrl, { maxConnections: 1 });
|
|
27
|
-
try {
|
|
28
|
-
if (opts.persistent) {
|
|
29
|
-
if (!(await databaseExists(admin.db, dbName))) {
|
|
30
|
-
await createDatabase(admin.db, dbName);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
await createDatabase(admin.db, dbName);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
await admin.close();
|
|
39
|
-
}
|
|
40
|
-
const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
|
|
41
|
-
const conn = await createConnection(testUrl, opts.poolMax !== undefined ? { maxConnections: opts.poolMax } : {});
|
|
42
|
-
const outsideConn = await createConnection(testUrl, {
|
|
43
|
-
maxConnections: opts.outsideTransactionPoolMax ?? DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX,
|
|
44
|
-
});
|
|
45
|
-
const { createEventsTable } = await import("../event-store/index.js");
|
|
46
|
-
await createEventsTable(conn.db);
|
|
47
|
-
return {
|
|
48
|
-
db: conn.db,
|
|
49
|
-
outsideTransactionDb: outsideConn.db,
|
|
50
|
-
client: conn.client,
|
|
51
|
-
dbName,
|
|
52
|
-
cleanup: async () => {
|
|
53
|
-
await conn.close();
|
|
54
|
-
await outsideConn.close();
|
|
55
|
-
if (!opts.persistent) {
|
|
56
|
-
const admin2 = await createConnection(adminUrl, { maxConnections: 1 });
|
|
57
|
-
try {
|
|
58
|
-
await dropDatabaseIfExists(admin2.db, dbName);
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
await admin2.close();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
export { requireEnv };
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
2
|
-
import { getConsumerState } from "../pipeline/index.js";
|
|
3
|
-
const DEFAULT_MAX_PASSES = 25;
|
|
4
|
-
// Drives eventDispatcher.runOnce() until every named consumer has caught up
|
|
5
|
-
// to the events high-water mark captured at call time. No "no progress ⇒
|
|
6
|
-
// throw" shortcut: a pass can legitimately advance zero cursors even though
|
|
7
|
-
// work remains — runOnce's FOR UPDATE SKIP LOCKED lets a concurrent pass
|
|
8
|
-
// (the test stack's own 50ms poll timer) hold a consumer's row, and a
|
|
9
|
-
// consumer coming off a thrown-pass backoff sits idle until its retryAtMs
|
|
10
|
-
// elapses. A bounded pass budget tolerates both without pretending to detect
|
|
11
|
-
// "stuck" from a single empty pass. The target is a snapshot: events a
|
|
12
|
-
// consumer's own handler writes afterward (cascades) are not waited on.
|
|
13
|
-
export async function drainEventConsumers(stack, consumerNames, options) {
|
|
14
|
-
const { db, eventDispatcher } = stack;
|
|
15
|
-
if (!eventDispatcher) {
|
|
16
|
-
throw new Error("drainEventConsumers: stack.eventDispatcher is undefined — no system consumer or " +
|
|
17
|
-
"multiStreamProjection is wired for this stack.");
|
|
18
|
-
}
|
|
19
|
-
const maxPasses = options?.maxPasses ?? DEFAULT_MAX_PASSES;
|
|
20
|
-
if (!Number.isInteger(maxPasses) || maxPasses < 1) {
|
|
21
|
-
throw new Error("drainEventConsumers: maxPasses must be a positive integer");
|
|
22
|
-
}
|
|
23
|
-
const target = await getEventsHighWaterMark(db);
|
|
24
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
25
|
-
await eventDispatcher.runOnce();
|
|
26
|
-
const states = await loadConsumerStates(db, consumerNames);
|
|
27
|
-
// skip: every named consumer has already caught up to the target snapshot — the
|
|
28
|
-
// remaining budgeted passes are unnecessary, not a swallowed error.
|
|
29
|
-
if (states.every((state) => state.lastProcessedEventId >= target))
|
|
30
|
-
return;
|
|
31
|
-
if (pass === maxPasses - 1) {
|
|
32
|
-
throw budgetExhaustedError(maxPasses, target, states);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function loadConsumerStates(db, consumerNames) {
|
|
37
|
-
return Promise.all(consumerNames.map(async (name) => {
|
|
38
|
-
const state = await getConsumerState(db, name);
|
|
39
|
-
if (!state) {
|
|
40
|
-
throw new Error(`drainEventConsumers: consumer "${name}" not registered or per-instance`);
|
|
41
|
-
}
|
|
42
|
-
return state;
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
function budgetExhaustedError(maxPasses, target, states) {
|
|
46
|
-
const unfinished = states.filter((state) => state.lastProcessedEventId < target);
|
|
47
|
-
const lines = unfinished.map((state) => ` ${state.name}: lastProcessedEventId=${state.lastProcessedEventId} target=${target} ` +
|
|
48
|
-
`status=${state.status} attempts=${state.attempts} lastError=${state.lastError ?? "null"}`);
|
|
49
|
-
return new Error(`drainEventConsumers: budget of ${maxPasses} passes exhausted, consumers still behind target:\n` +
|
|
50
|
-
lines.join("\n"));
|
|
51
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { DbConnection } from "../db/index.js";
|
|
2
|
-
import type { JobRunner } from "../jobs/index.js";
|
|
3
|
-
import { type EventDispatcher } from "../pipeline/index.js";
|
|
4
|
-
export type JobFailureTracker = {
|
|
5
|
-
readonly onJobComplete: (jobName: string, jobId: string) => void;
|
|
6
|
-
readonly onJobFailed: (jobName: string, jobId: string, error: string) => void;
|
|
7
|
-
waitForActivity(): Promise<void>;
|
|
8
|
-
readonly failures: ReadonlyMap<string, {
|
|
9
|
-
readonly jobName: string;
|
|
10
|
-
readonly error: string;
|
|
11
|
-
}>;
|
|
12
|
-
clear(): void;
|
|
13
|
-
};
|
|
14
|
-
export declare function createJobFailureTracker(): JobFailureTracker;
|
|
15
|
-
export type DrainJobsStack = {
|
|
16
|
-
readonly db: DbConnection;
|
|
17
|
-
readonly eventDispatcher?: EventDispatcher;
|
|
18
|
-
readonly jobRunner?: JobRunner;
|
|
19
|
-
};
|
|
20
|
-
export declare function drainJobs(stack: DrainJobsStack, tracker: JobFailureTracker, options?: {
|
|
21
|
-
readonly timeoutMs?: number;
|
|
22
|
-
}): Promise<void>;
|
package/dist/stack/drain-jobs.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
2
|
-
import { getConsumerState } from "../pipeline/index.js";
|
|
3
|
-
// Gap between idle-checks while waiting for a job hook to fire. Short enough
|
|
4
|
-
// that a test never feels it, long enough that consecutive idle passes don't
|
|
5
|
-
// hammer Postgres/Redis with pointless polling between hook-driven wakeups.
|
|
6
|
-
const DRAIN_IDLE_POLL_MS = 25;
|
|
7
|
-
// Two consecutive idle passes (not one) before declaring the stack drained —
|
|
8
|
-
// a single "pending===0 and consumers caught up" pass can be a race: a job
|
|
9
|
-
// hook fires and enqueues a follow-up between the countPendingJobs() read
|
|
10
|
-
// and the consumer check of the SAME pass. A DRAIN_IDLE_POLL_MS wait sits
|
|
11
|
-
// between every pass, including the first and second idle one, so an
|
|
12
|
-
// enqueue that lands just after the first idle read still gets picked up
|
|
13
|
-
// by the second pass instead of racing straight past it.
|
|
14
|
-
const REQUIRED_IDLE_PASSES = 2;
|
|
15
|
-
// Below the 15s per-test budget so a stuck drain reports what is open rather
|
|
16
|
-
// than surfacing as a bare test timeout.
|
|
17
|
-
const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
|
|
18
|
-
function sleep(ms) {
|
|
19
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
-
}
|
|
21
|
-
export function createJobFailureTracker() {
|
|
22
|
-
const failures = new Map();
|
|
23
|
-
let wake;
|
|
24
|
-
let armed;
|
|
25
|
-
function fire() {
|
|
26
|
-
wake?.();
|
|
27
|
-
armed = undefined;
|
|
28
|
-
wake = undefined;
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
onJobComplete(_jobName, jobId) {
|
|
32
|
-
// A retry that eventually succeeds must not leave a stale failure
|
|
33
|
-
// behind from its earlier attempt(s).
|
|
34
|
-
failures.delete(jobId);
|
|
35
|
-
fire();
|
|
36
|
-
},
|
|
37
|
-
onJobFailed(jobName, jobId, error) {
|
|
38
|
-
failures.set(jobId, { jobName, error });
|
|
39
|
-
fire();
|
|
40
|
-
},
|
|
41
|
-
waitForActivity() {
|
|
42
|
-
armed ??= new Promise((resolve) => {
|
|
43
|
-
wake = resolve;
|
|
44
|
-
});
|
|
45
|
-
return armed;
|
|
46
|
-
},
|
|
47
|
-
failures,
|
|
48
|
-
clear() {
|
|
49
|
-
failures.clear();
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
// Diagnostic only: lets the deadline error name what was still open.
|
|
54
|
-
async function describeLaggingConsumers(db, consumerNames) {
|
|
55
|
-
// skip: no shared consumers wired — nothing to catch up on.
|
|
56
|
-
if (consumerNames.length === 0)
|
|
57
|
-
return [];
|
|
58
|
-
const target = await getEventsHighWaterMark(db);
|
|
59
|
-
const lagging = [];
|
|
60
|
-
for (const name of consumerNames) {
|
|
61
|
-
const state = await getConsumerState(db, name);
|
|
62
|
-
if (state && state.lastProcessedEventId < target) {
|
|
63
|
-
lagging.push(`${name} (status=${state.status}, lastProcessedEventId=${state.lastProcessedEventId}, target=${target})`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return lagging;
|
|
67
|
-
}
|
|
68
|
-
async function drainTimeoutError(db, consumerNames, timeoutMs, pending) {
|
|
69
|
-
const lagging = await describeLaggingConsumers(db, consumerNames);
|
|
70
|
-
return new Error(`drainJobs: not drained after ${timeoutMs}ms — pending jobs: ${pending}, ` +
|
|
71
|
-
`consumers behind: ${lagging.length === 0 ? "none" : lagging.join("; ")}`);
|
|
72
|
-
}
|
|
73
|
-
async function sharedConsumersCaughtUp(db, consumerNames) {
|
|
74
|
-
// skip: no shared consumers wired — nothing to catch up on.
|
|
75
|
-
if (consumerNames.length === 0)
|
|
76
|
-
return true;
|
|
77
|
-
const target = await getEventsHighWaterMark(db);
|
|
78
|
-
let allCaughtUp = true;
|
|
79
|
-
for (const name of consumerNames) {
|
|
80
|
-
const state = await getConsumerState(db, name);
|
|
81
|
-
if (!state) {
|
|
82
|
-
throw new Error(`drainJobs: consumer "${name}" not registered or per-instance`);
|
|
83
|
-
}
|
|
84
|
-
if (state.status === "dead") {
|
|
85
|
-
throw new Error(`drainJobs: consumer "${state.name}" is dead — lastError: ${state.lastError ?? "unknown"}`);
|
|
86
|
-
}
|
|
87
|
-
if (state.lastProcessedEventId < target)
|
|
88
|
-
allCaughtUp = false;
|
|
89
|
-
}
|
|
90
|
-
return allCaughtUp;
|
|
91
|
-
}
|
|
92
|
-
// Fails a stuck drain with a diagnosis instead of a generic test-runner timeout.
|
|
93
|
-
// It only bounds the loop; it never replaces waiting for real work.
|
|
94
|
-
export async function drainJobs(stack, tracker, options) {
|
|
95
|
-
const { db, eventDispatcher, jobRunner } = stack;
|
|
96
|
-
const timeoutMs = options?.timeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
97
|
-
const deadline = Date.now() + timeoutMs;
|
|
98
|
-
if (!jobRunner) {
|
|
99
|
-
throw new Error("drainJobs: stack.jobRunner is undefined — pass `jobs: {...}` to setupTestStack and " +
|
|
100
|
-
"register at least one r.job(...) so a runner is built.");
|
|
101
|
-
}
|
|
102
|
-
const sharedConsumerNames = (eventDispatcher?.consumers ?? [])
|
|
103
|
-
.filter((consumer) => (consumer.delivery ?? "shared") === "shared")
|
|
104
|
-
.map((consumer) => consumer.name);
|
|
105
|
-
try {
|
|
106
|
-
try {
|
|
107
|
-
let consecutiveIdlePasses = 0;
|
|
108
|
-
while (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
|
|
109
|
-
await eventDispatcher?.runOnce();
|
|
110
|
-
const pending = await jobRunner.countPendingJobs();
|
|
111
|
-
const caughtUp = await sharedConsumersCaughtUp(db, sharedConsumerNames);
|
|
112
|
-
consecutiveIdlePasses = pending === 0 && caughtUp ? consecutiveIdlePasses + 1 : 0;
|
|
113
|
-
if (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
|
|
114
|
-
if (Date.now() > deadline) {
|
|
115
|
-
throw await drainTimeoutError(db, sharedConsumerNames, timeoutMs, pending);
|
|
116
|
-
}
|
|
117
|
-
await Promise.race([tracker.waitForActivity(), sleep(DRAIN_IDLE_POLL_MS)]);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
// A failed job often kills its consumer, so the drain error is only the follow-up symptom.
|
|
123
|
-
if (tracker.failures.size === 0)
|
|
124
|
-
throw err;
|
|
125
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
-
throw new Error(`${message}; ${describeJobFailures(tracker)}`, { cause: err });
|
|
127
|
-
}
|
|
128
|
-
if (tracker.failures.size > 0) {
|
|
129
|
-
throw new Error(`drainJobs: ${describeJobFailures(tracker)}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
tracker.clear();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function describeJobFailures(tracker) {
|
|
137
|
-
const details = [...tracker.failures.values()]
|
|
138
|
-
.map((failure) => `${failure.jobName}: ${failure.error}`)
|
|
139
|
-
.join("; ");
|
|
140
|
-
return `job(s) failed: ${details}`;
|
|
141
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { SseEvent } from "../api/sse-broker.js";
|
|
2
|
-
import type { SaveContext } from "../engine/types/index.js";
|
|
3
|
-
export type EventCollector = {
|
|
4
|
-
readonly sse: SseEvent[];
|
|
5
|
-
readonly postSave: SaveContext[];
|
|
6
|
-
/** Clears all collected events — call in beforeEach for per-test isolation */
|
|
7
|
-
reset(): void;
|
|
8
|
-
};
|
|
9
|
-
export declare function createEventCollector(): EventCollector;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { DbConnection } from "../db/connection.js";
|
|
2
|
-
import type { Registry } from "../engine/types/index.js";
|
|
3
|
-
type DbHolder = {
|
|
4
|
-
readonly db: DbConnection;
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Push all implicit-projection tables — one per `r.entity()` — that the
|
|
8
|
-
* registry knows about. setupTestStack already handles explicit
|
|
9
|
-
* projections, MSPs, and `r.storeTable()` declarations in its own loop;
|
|
10
|
-
* implicit projections are the missing piece for a fresh boot.
|
|
11
|
-
*
|
|
12
|
-
* Idempotent via `tableExists` so a persistent dev DB
|
|
13
|
-
* (`KUMIKO_DEV_DB_NAME`) reuses existing tables on reboot. One batched
|
|
14
|
-
* push at the end so drizzle-kit's `generateMigration` runs once over
|
|
15
|
-
* the whole missing set.
|
|
16
|
-
*
|
|
17
|
-
* Lives next to `setupTestStack` because both are stack-bootstrap
|
|
18
|
-
* helpers that legitimately speak the `unsafe*`-DDL layer; the
|
|
19
|
-
* Table-DDL Guard's stack/** allowlist is the single shared exemption
|
|
20
|
-
* site. Apps still declare data via `r.entity()` / `r.storeTable()` and
|
|
21
|
-
* never call this directly.
|
|
22
|
-
*/
|
|
23
|
-
export declare function pushEntityProjectionTables(stack: DbHolder, registry: Registry): Promise<void>;
|
|
24
|
-
export {};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { columnNamesOf, tableExists } from "../db/schema-inspection.js";
|
|
2
|
-
import { schemaTableName } from "../db/table-introspection.js";
|
|
3
|
-
import { addMissingColumns, addMissingIndexes, tableToMeta, unsafePushTables, } from "./table-helpers.js";
|
|
4
|
-
// biome-ignore lint/suspicious/noConsole: stack-internal status logging
|
|
5
|
-
const logInfo = (msg) => console.log(msg);
|
|
6
|
-
/**
|
|
7
|
-
* Push all implicit-projection tables — one per `r.entity()` — that the
|
|
8
|
-
* registry knows about. setupTestStack already handles explicit
|
|
9
|
-
* projections, MSPs, and `r.storeTable()` declarations in its own loop;
|
|
10
|
-
* implicit projections are the missing piece for a fresh boot.
|
|
11
|
-
*
|
|
12
|
-
* Idempotent via `tableExists` so a persistent dev DB
|
|
13
|
-
* (`KUMIKO_DEV_DB_NAME`) reuses existing tables on reboot. One batched
|
|
14
|
-
* push at the end so drizzle-kit's `generateMigration` runs once over
|
|
15
|
-
* the whole missing set.
|
|
16
|
-
*
|
|
17
|
-
* Lives next to `setupTestStack` because both are stack-bootstrap
|
|
18
|
-
* helpers that legitimately speak the `unsafe*`-DDL layer; the
|
|
19
|
-
* Table-DDL Guard's stack/** allowlist is the single shared exemption
|
|
20
|
-
* site. Apps still declare data via `r.entity()` / `r.storeTable()` and
|
|
21
|
-
* never call this directly.
|
|
22
|
-
*/
|
|
23
|
-
export async function pushEntityProjectionTables(stack, registry) {
|
|
24
|
-
const seen = new Set();
|
|
25
|
-
const missing = {};
|
|
26
|
-
for (const [projName, proj] of registry.getAllProjections()) {
|
|
27
|
-
if (!proj.isImplicit)
|
|
28
|
-
continue;
|
|
29
|
-
if (seen.has(proj.table))
|
|
30
|
-
continue;
|
|
31
|
-
seen.add(proj.table);
|
|
32
|
-
const physical = schemaTableName(proj.table) ?? tableToMeta(proj.table).tableName;
|
|
33
|
-
if (!(await tableExists(stack.db, `public.${physical}`))) {
|
|
34
|
-
missing[projName] = proj.table;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
await syncExistingProjectionTable(stack, physical, proj.table);
|
|
38
|
-
}
|
|
39
|
-
if (Object.keys(missing).length > 0) {
|
|
40
|
-
await unsafePushTables(stack.db, missing);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function isUnsafeToAutoAdd(col) {
|
|
44
|
-
return col.notNull && !col.primaryKey && col.defaultSql === undefined;
|
|
45
|
-
}
|
|
46
|
-
// Backfills columns and indexes a persistent dev DB predates; a required column with no
|
|
47
|
-
// default can't be added safely on a table that may hold rows, so that
|
|
48
|
-
// stays a boot error instead.
|
|
49
|
-
async function syncExistingProjectionTable(stack, physical, table) {
|
|
50
|
-
const meta = tableToMeta(table);
|
|
51
|
-
const liveColumns = await columnNamesOf(stack.db, physical);
|
|
52
|
-
const missingColumns = meta.columns.filter((c) => !liveColumns.has(c.name));
|
|
53
|
-
if (missingColumns.length === 0) {
|
|
54
|
-
logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
|
|
55
|
-
await addMissingIndexes(stack.db, physical, meta.indexes);
|
|
56
|
-
// skip: table is already in sync, only indexes needed backfilling
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const unsafeColumns = missingColumns.filter(isUnsafeToAutoAdd);
|
|
60
|
-
if (unsafeColumns.length > 0) {
|
|
61
|
-
const devDbName = process.env["KUMIKO_DEV_DB_NAME"] ?? "<KUMIKO_DEV_DB_NAME>";
|
|
62
|
-
throw new Error(`[kumiko-stack] table "${physical}" is missing required column(s) ` +
|
|
63
|
-
`${unsafeColumns.map((c) => `"${c.name}"`).join(", ")} with no default. ` +
|
|
64
|
-
"Auto-sync only backfills nullable columns or columns with a default on an " +
|
|
65
|
-
"existing dev DB — a required column with no default can't be added safely " +
|
|
66
|
-
`once the table may hold rows. Drop the persistent dev database "${devDbName}" ` +
|
|
67
|
-
`(e.g. \`dropdb ${devDbName}\`, adjusted for your local Postgres host/port/user) ` +
|
|
68
|
-
"so the next boot recreates it from scratch.");
|
|
69
|
-
}
|
|
70
|
-
logInfo(`[kumiko-stack] table ${physical} is missing column(s) ` +
|
|
71
|
-
`${missingColumns.map((c) => `"${c.name}"`).join(", ")} — adding`);
|
|
72
|
-
await addMissingColumns(stack.db, physical, missingColumns);
|
|
73
|
-
await addMissingIndexes(stack.db, physical, meta.indexes);
|
|
74
|
-
}
|
package/dist/stack/redis.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type TestRedis = {
|
|
2
|
-
redis: import("ioredis").Redis;
|
|
3
|
-
redisUrl: string;
|
|
4
|
-
/** The per-test `keyPrefix` on `redis`, e.g. `kt:1a2b3c4d:`. */
|
|
5
|
-
keyPrefix: string;
|
|
6
|
-
/** Delete every key this test created (prefix-scoped). Replaces the old
|
|
7
|
-
* `redis.flushdb()` — that wiped other parallel tests' BullMQ state. */
|
|
8
|
-
flushNamespace: () => Promise<void>;
|
|
9
|
-
cleanup: () => Promise<void>;
|
|
10
|
-
};
|
|
11
|
-
export declare function queueNamePrefixForTestRedis(keyPrefix: string): string;
|
|
12
|
-
export type CreateTestRedisOptions = {
|
|
13
|
-
/** Reuse another stack's keyPrefix on a fresh connection. cleanup() then only
|
|
14
|
-
* disconnects — the owning stack flushes the namespace and its queues. */
|
|
15
|
-
readonly borrowKeyPrefix?: string;
|
|
16
|
-
};
|
|
17
|
-
export declare function createTestRedis(opts?: CreateTestRedisOptions): Promise<TestRedis>;
|
package/dist/stack/redis.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { generateId } from "../utils/index.js";
|
|
2
|
-
import { requireEnv } from "./db.js";
|
|
3
|
-
// BullMQ queues live on the raw redisUrl, outside the test Redis keyPrefix, so
|
|
4
|
-
// parallel stacks sharing the prod default queue name would consume each
|
|
5
|
-
// other's jobs. BullMQ rejects `:` in queue names.
|
|
6
|
-
export function queueNamePrefixForTestRedis(keyPrefix) {
|
|
7
|
-
return keyPrefix.split(":").filter(Boolean).join("-");
|
|
8
|
-
}
|
|
9
|
-
export async function createTestRedis(opts = {}) {
|
|
10
|
-
const Redis = (await import("ioredis")).Redis;
|
|
11
|
-
const redisUrl = requireEnv("REDIS_URL");
|
|
12
|
-
// Every test gets a per-file key prefix on a shared DB (no DB-pool-of-15
|
|
13
|
-
// round-robin). Collisions at birthday-paradox rates are gone — the
|
|
14
|
-
// prefix space is unbounded. See Track B.3 in docs/plans/tests-refactor.
|
|
15
|
-
const prefix = opts.borrowKeyPrefix ?? `kt:${generateId().slice(-8)}:`;
|
|
16
|
-
const redis = new Redis(redisUrl, { keyPrefix: prefix });
|
|
17
|
-
async function deleteKeysMatching(pattern) {
|
|
18
|
-
// Open a prefix-less client for the scan — ioredis' keyPrefix is applied
|
|
19
|
-
// per-command but SCAN's returned keys are full names, so managing the
|
|
20
|
-
// del set with the prefix already on the connection is error-prone.
|
|
21
|
-
const raw = new Redis(redisUrl);
|
|
22
|
-
try {
|
|
23
|
-
const stream = raw.scanStream({ match: pattern, count: 500 });
|
|
24
|
-
const keys = [];
|
|
25
|
-
for await (const batch of stream)
|
|
26
|
-
keys.push(...batch);
|
|
27
|
-
if (keys.length > 0)
|
|
28
|
-
await raw.del(...keys);
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
raw.disconnect();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const flushNamespace = () => deleteKeysMatching(`${prefix}*`);
|
|
35
|
-
return {
|
|
36
|
-
redis,
|
|
37
|
-
redisUrl,
|
|
38
|
-
keyPrefix: prefix,
|
|
39
|
-
flushNamespace,
|
|
40
|
-
cleanup: async () => {
|
|
41
|
-
if (opts.borrowKeyPrefix !== undefined) {
|
|
42
|
-
redis.disconnect();
|
|
43
|
-
// skip: the owning stack flushes the shared namespace and its queues
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
await flushNamespace();
|
|
47
|
-
// The derived per-stack JobRunner queues sit outside the keyPrefix.
|
|
48
|
-
await deleteKeysMatching(`bull:${queueNamePrefixForTestRedis(prefix)}-*`);
|
|
49
|
-
redis.disconnect();
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|