@pattern-stack/codegen 0.2.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/CHANGELOG.md +67 -0
- package/README.md +214 -0
- package/dist/runtime/analytics/index.d.ts +6 -0
- package/dist/runtime/analytics/index.js +49 -0
- package/dist/runtime/analytics/index.js.map +1 -0
- package/dist/runtime/analytics/metrics.d.ts +75 -0
- package/dist/runtime/analytics/metrics.js +1 -0
- package/dist/runtime/analytics/metrics.js.map +1 -0
- package/dist/runtime/analytics/packs/crm-entity-measures.d.ts +21 -0
- package/dist/runtime/analytics/packs/crm-entity-measures.js +1 -0
- package/dist/runtime/analytics/packs/crm-entity-measures.js.map +1 -0
- package/dist/runtime/analytics/packs/index.d.ts +3 -0
- package/dist/runtime/analytics/packs/index.js +1 -0
- package/dist/runtime/analytics/packs/index.js.map +1 -0
- package/dist/runtime/analytics/packs/monetary-measures.d.ts +21 -0
- package/dist/runtime/analytics/packs/monetary-measures.js +1 -0
- package/dist/runtime/analytics/packs/monetary-measures.js.map +1 -0
- package/dist/runtime/analytics/specs.d.ts +49 -0
- package/dist/runtime/analytics/specs.js +1 -0
- package/dist/runtime/analytics/specs.js.map +1 -0
- package/dist/runtime/analytics/types.d.ts +85 -0
- package/dist/runtime/analytics/types.js +49 -0
- package/dist/runtime/analytics/types.js.map +1 -0
- package/dist/runtime/base-classes/activity-entity-repository.d.ts +26 -0
- package/dist/runtime/base-classes/activity-entity-repository.js +195 -0
- package/dist/runtime/base-classes/activity-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/activity-entity-service.d.ts +39 -0
- package/dist/runtime/base-classes/activity-entity-service.js +214 -0
- package/dist/runtime/base-classes/activity-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/base-read-use-cases.d.ts +68 -0
- package/dist/runtime/base-classes/base-read-use-cases.js +32 -0
- package/dist/runtime/base-classes/base-read-use-cases.js.map +1 -0
- package/dist/runtime/base-classes/base-repository.d.ts +99 -0
- package/dist/runtime/base-classes/base-repository.js +160 -0
- package/dist/runtime/base-classes/base-repository.js.map +1 -0
- package/dist/runtime/base-classes/base-service.d.ts +98 -0
- package/dist/runtime/base-classes/base-service.js +186 -0
- package/dist/runtime/base-classes/base-service.js.map +1 -0
- package/dist/runtime/base-classes/index.d.ts +18 -0
- package/dist/runtime/base-classes/index.js +617 -0
- package/dist/runtime/base-classes/index.js.map +1 -0
- package/dist/runtime/base-classes/knowledge-entity-repository.d.ts +17 -0
- package/dist/runtime/base-classes/knowledge-entity-repository.js +166 -0
- package/dist/runtime/base-classes/knowledge-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/knowledge-entity-service.d.ts +15 -0
- package/dist/runtime/base-classes/knowledge-entity-service.js +192 -0
- package/dist/runtime/base-classes/knowledge-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/lifecycle-events.d.ts +49 -0
- package/dist/runtime/base-classes/lifecycle-events.js +76 -0
- package/dist/runtime/base-classes/lifecycle-events.js.map +1 -0
- package/dist/runtime/base-classes/metadata-entity-repository.d.ts +27 -0
- package/dist/runtime/base-classes/metadata-entity-repository.js +212 -0
- package/dist/runtime/base-classes/metadata-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/metadata-entity-service.d.ts +39 -0
- package/dist/runtime/base-classes/metadata-entity-service.js +214 -0
- package/dist/runtime/base-classes/metadata-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/synced-entity-repository.d.ts +32 -0
- package/dist/runtime/base-classes/synced-entity-repository.js +203 -0
- package/dist/runtime/base-classes/synced-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/synced-entity-service.d.ts +41 -0
- package/dist/runtime/base-classes/synced-entity-service.js +215 -0
- package/dist/runtime/base-classes/synced-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/with-analytics.d.ts +18 -0
- package/dist/runtime/base-classes/with-analytics.js +11 -0
- package/dist/runtime/base-classes/with-analytics.js.map +1 -0
- package/dist/runtime/constants/tokens.d.ts +29 -0
- package/dist/runtime/constants/tokens.js +8 -0
- package/dist/runtime/constants/tokens.js.map +1 -0
- package/dist/runtime/subsystems/analytics/analytics-query.protocol.d.ts +30 -0
- package/dist/runtime/subsystems/analytics/analytics-query.protocol.js +1 -0
- package/dist/runtime/subsystems/analytics/analytics-query.protocol.js.map +1 -0
- package/dist/runtime/subsystems/analytics/analytics.module.d.ts +34 -0
- package/dist/runtime/subsystems/analytics/analytics.module.js +117 -0
- package/dist/runtime/subsystems/analytics/analytics.module.js.map +1 -0
- package/dist/runtime/subsystems/analytics/analytics.tokens.d.ts +24 -0
- package/dist/runtime/subsystems/analytics/analytics.tokens.js +10 -0
- package/dist/runtime/subsystems/analytics/analytics.tokens.js.map +1 -0
- package/dist/runtime/subsystems/analytics/cube-backend.d.ts +28 -0
- package/dist/runtime/subsystems/analytics/cube-backend.js +71 -0
- package/dist/runtime/subsystems/analytics/cube-backend.js.map +1 -0
- package/dist/runtime/subsystems/analytics/index.d.ts +6 -0
- package/dist/runtime/subsystems/analytics/index.js +122 -0
- package/dist/runtime/subsystems/analytics/index.js.map +1 -0
- package/dist/runtime/subsystems/analytics/noop-backend.d.ts +7 -0
- package/dist/runtime/subsystems/analytics/noop-backend.js +25 -0
- package/dist/runtime/subsystems/analytics/noop-backend.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.d.ts +43 -0
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +133 -0
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.memory-backend.d.ts +21 -0
- package/dist/runtime/subsystems/cache/cache.memory-backend.js +100 -0
- package/dist/runtime/subsystems/cache/cache.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.module.d.ts +37 -0
- package/dist/runtime/subsystems/cache/cache.module.js +272 -0
- package/dist/runtime/subsystems/cache/cache.module.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.protocol.d.ts +42 -0
- package/dist/runtime/subsystems/cache/cache.protocol.js +1 -0
- package/dist/runtime/subsystems/cache/cache.protocol.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.schema.d.ts +64 -0
- package/dist/runtime/subsystems/cache/cache.schema.js +18 -0
- package/dist/runtime/subsystems/cache/cache.schema.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.tokens.d.ts +18 -0
- package/dist/runtime/subsystems/cache/cache.tokens.js +8 -0
- package/dist/runtime/subsystems/cache/cache.tokens.js.map +1 -0
- package/dist/runtime/subsystems/cache/index.d.ts +11 -0
- package/dist/runtime/subsystems/cache/index.js +277 -0
- package/dist/runtime/subsystems/cache/index.js.map +1 -0
- package/dist/runtime/subsystems/events/domain-events.schema.d.ts +187 -0
- package/dist/runtime/subsystems/events/domain-events.schema.js +32 -0
- package/dist/runtime/subsystems/events/domain-events.schema.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +38 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +199 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +18 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +71 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.protocol.d.ts +52 -0
- package/dist/runtime/subsystems/events/event-bus.protocol.js +1 -0
- package/dist/runtime/subsystems/events/event-bus.protocol.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.redis-backend.d.ts +95 -0
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js +229 -0
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -0
- package/dist/runtime/subsystems/events/events.module.d.ts +46 -0
- package/dist/runtime/subsystems/events/events.module.js +531 -0
- package/dist/runtime/subsystems/events/events.module.js.map +1 -0
- package/dist/runtime/subsystems/events/events.tokens.d.ts +19 -0
- package/dist/runtime/subsystems/events/events.tokens.js +8 -0
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -0
- package/dist/runtime/subsystems/events/index.d.ts +12 -0
- package/dist/runtime/subsystems/events/index.js +536 -0
- package/dist/runtime/subsystems/events/index.js.map +1 -0
- package/dist/runtime/subsystems/index.d.ts +24 -0
- package/dist/runtime/subsystems/index.js +1643 -0
- package/dist/runtime/subsystems/index.js.map +1 -0
- package/dist/runtime/subsystems/jobs/index.d.ts +14 -0
- package/dist/runtime/subsystems/jobs/index.js +680 -0
- package/dist/runtime/subsystems/jobs/index.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.d.ts +54 -0
- package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.js +186 -0
- package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.d.ts +38 -0
- package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.js +228 -0
- package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.memory-backend.d.ts +12 -0
- package/dist/runtime/subsystems/jobs/job-queue.memory-backend.js +44 -0
- package/dist/runtime/subsystems/jobs/job-queue.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.protocol.d.ts +48 -0
- package/dist/runtime/subsystems/jobs/job-queue.protocol.js +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.protocol.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.redis-backend.d.ts +46 -0
- package/dist/runtime/subsystems/jobs/job-queue.redis-backend.js +187 -0
- package/dist/runtime/subsystems/jobs/job-queue.redis-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.schema.d.ts +237 -0
- package/dist/runtime/subsystems/jobs/job-queue.schema.js +44 -0
- package/dist/runtime/subsystems/jobs/job-queue.schema.js.map +1 -0
- package/dist/runtime/subsystems/jobs/jobs.module.d.ts +18 -0
- package/dist/runtime/subsystems/jobs/jobs.module.js +676 -0
- package/dist/runtime/subsystems/jobs/jobs.module.js.map +1 -0
- package/dist/runtime/subsystems/jobs/jobs.tokens.d.ts +13 -0
- package/dist/runtime/subsystems/jobs/jobs.tokens.js +8 -0
- package/dist/runtime/subsystems/jobs/jobs.tokens.js.map +1 -0
- package/dist/runtime/subsystems/storage/index.d.ts +6 -0
- package/dist/runtime/subsystems/storage/index.js +204 -0
- package/dist/runtime/subsystems/storage/index.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.local-backend.d.ts +18 -0
- package/dist/runtime/subsystems/storage/storage.local-backend.js +108 -0
- package/dist/runtime/subsystems/storage/storage.local-backend.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.memory-backend.d.ts +28 -0
- package/dist/runtime/subsystems/storage/storage.memory-backend.js +72 -0
- package/dist/runtime/subsystems/storage/storage.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.module.d.ts +40 -0
- package/dist/runtime/subsystems/storage/storage.module.js +201 -0
- package/dist/runtime/subsystems/storage/storage.module.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.protocol.d.ts +69 -0
- package/dist/runtime/subsystems/storage/storage.protocol.js +1 -0
- package/dist/runtime/subsystems/storage/storage.protocol.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.tokens.d.ts +11 -0
- package/dist/runtime/subsystems/storage/storage.tokens.js +6 -0
- package/dist/runtime/subsystems/storage/storage.tokens.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.utils.d.ts +9 -0
- package/dist/runtime/subsystems/storage/storage.utils.js +18 -0
- package/dist/runtime/subsystems/storage/storage.utils.js.map +1 -0
- package/dist/runtime/types/drizzle.d.ts +17 -0
- package/dist/runtime/types/drizzle.js +1 -0
- package/dist/runtime/types/drizzle.js.map +1 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/index.js +7365 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/index.d.ts +2384 -0
- package/dist/src/index.js +2198 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +114 -0
- package/templates/broadcast/new/backend-interface.ejs.t +47 -0
- package/templates/broadcast/new/bridge-listener.ejs.t +67 -0
- package/templates/broadcast/new/channel.ejs.t +77 -0
- package/templates/broadcast/new/index.ejs.t +21 -0
- package/templates/broadcast/new/memory-backend.ejs.t +87 -0
- package/templates/broadcast/new/module.ejs.t +57 -0
- package/templates/broadcast/new/prompt.js +268 -0
- package/templates/broadcast/new/websocket-backend.ejs.t +259 -0
- package/templates/entity/new/backend/application/commands/create.ejs.t +55 -0
- package/templates/entity/new/backend/application/commands/delete.ejs.t +45 -0
- package/templates/entity/new/backend/application/commands/grouped-index.ejs.t +149 -0
- package/templates/entity/new/backend/application/commands/index.ejs.t +15 -0
- package/templates/entity/new/backend/application/commands/update.ejs.t +58 -0
- package/templates/entity/new/backend/application/queries/declarative-queries.ejs.t +36 -0
- package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +42 -0
- package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +81 -0
- package/templates/entity/new/backend/application/queries/index.ejs.t +14 -0
- package/templates/entity/new/backend/application/queries/list.ejs.t +36 -0
- package/templates/entity/new/backend/application/schemas/_inject-index.ejs.t +7 -0
- package/templates/entity/new/backend/application/schemas/dto.ejs.t +45 -0
- package/templates/entity/new/backend/database/_inject-index.ejs.t +7 -0
- package/templates/entity/new/backend/database/electric-migration.ejs.t +21 -0
- package/templates/entity/new/backend/database/repository.ejs.t +450 -0
- package/templates/entity/new/backend/database/schema.ejs.t +248 -0
- package/templates/entity/new/backend/domain/_inject-index.ejs.t +12 -0
- package/templates/entity/new/backend/domain/entity.ejs.t +108 -0
- package/templates/entity/new/backend/domain/grouped-index.ejs.t +163 -0
- package/templates/entity/new/backend/domain/index.ejs.t +15 -0
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +71 -0
- package/templates/entity/new/backend/modules/core/_ensure-anchor-tokens.ejs.t +10 -0
- package/templates/entity/new/backend/modules/core/_inject-token.ejs.t +7 -0
- package/templates/entity/new/backend/modules/core/module.ejs.t +67 -0
- package/templates/entity/new/backend/modules/trpc/module.ejs.t +67 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +201 -0
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +37 -0
- package/templates/entity/new/clean-lite-ps/dto/create.ejs.t +17 -0
- package/templates/entity/new/clean-lite-ps/dto/output.ejs.t +25 -0
- package/templates/entity/new/clean-lite-ps/dto/update.ejs.t +11 -0
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +52 -0
- package/templates/entity/new/clean-lite-ps/index.ejs.t +20 -0
- package/templates/entity/new/clean-lite-ps/module.ejs.t +43 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +617 -0
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +62 -0
- package/templates/entity/new/clean-lite-ps/service.ejs.t +34 -0
- package/templates/entity/new/clean-lite-ps/use-cases/declarative-queries.ejs.t +34 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +16 -0
- package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +16 -0
- package/templates/entity/new/frontend/_inject-entities-entry.ejs.t +7 -0
- package/templates/entity/new/frontend/_inject-entities-import.ejs.t +7 -0
- package/templates/entity/new/frontend/collections/_ensure-anchor-collections.ejs.t +10 -0
- package/templates/entity/new/frontend/collections/_inject-index.ejs.t +9 -0
- package/templates/entity/new/frontend/collections/_inject-schema-import.ejs.t +9 -0
- package/templates/entity/new/frontend/collections/collection.ejs.t +61 -0
- package/templates/entity/new/frontend/collections/collections-base.ejs.t +24 -0
- package/templates/entity/new/frontend/entity/collection.ejs.t +172 -0
- package/templates/entity/new/frontend/entity/combined.ejs.t +474 -0
- package/templates/entity/new/frontend/entity/fields.ejs.t +104 -0
- package/templates/entity/new/frontend/entity/hooks.ejs.t +73 -0
- package/templates/entity/new/frontend/entity/index.ejs.t +21 -0
- package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +84 -0
- package/templates/entity/new/frontend/entity/mutations.ejs.t +38 -0
- package/templates/entity/new/frontend/entity/types.ejs.t +59 -0
- package/templates/entity/new/frontend/generated/_inject-index-export.ejs.t +7 -0
- package/templates/entity/new/frontend/generated/_inject-index-import.ejs.t +7 -0
- package/templates/entity/new/frontend/generated/_inject-index-registry.ejs.t +7 -0
- package/templates/entity/new/frontend/store/_inject-collection-import.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-collections.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-entity.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-import.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-lookups.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-resolve.ejs.t +10 -0
- package/templates/entity/new/frontend/store/hooks.ejs.t +72 -0
- package/templates/entity/new/frontend/unified-entity.ejs.t +28 -0
- package/templates/entity/new/prompt.js +1421 -0
- package/templates/relationship/new/controller.ejs.t +36 -0
- package/templates/relationship/new/dto/create.ejs.t +41 -0
- package/templates/relationship/new/dto/output.ejs.t +44 -0
- package/templates/relationship/new/dto/update.ejs.t +10 -0
- package/templates/relationship/new/entity.ejs.t +98 -0
- package/templates/relationship/new/index.ejs.t +19 -0
- package/templates/relationship/new/module.ejs.t +35 -0
- package/templates/relationship/new/prompt.js +682 -0
- package/templates/relationship/new/repository.ejs.t +54 -0
- package/templates/relationship/new/service.ejs.t +31 -0
- package/templates/relationship/new/use-cases/declarative-queries.ejs.t +34 -0
- package/templates/relationship/new/use-cases/find-by-id.ejs.t +16 -0
- package/templates/relationship/new/use-cases/list.ejs.t +16 -0
|
@@ -0,0 +1,1643 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
12
|
+
|
|
13
|
+
// runtime/subsystems/events/events.tokens.ts
|
|
14
|
+
var EVENT_BUS = "EVENT_BUS";
|
|
15
|
+
var REDIS_URL = /* @__PURE__ */ Symbol("REDIS_URL");
|
|
16
|
+
|
|
17
|
+
// runtime/subsystems/events/events.module.ts
|
|
18
|
+
import { Module } from "@nestjs/common";
|
|
19
|
+
|
|
20
|
+
// runtime/subsystems/events/event-bus.drizzle-backend.ts
|
|
21
|
+
import { Injectable, Inject, Logger } from "@nestjs/common";
|
|
22
|
+
import { eq, and, sql } from "drizzle-orm";
|
|
23
|
+
|
|
24
|
+
// runtime/subsystems/events/domain-events.schema.ts
|
|
25
|
+
import {
|
|
26
|
+
jsonb,
|
|
27
|
+
pgTable,
|
|
28
|
+
text,
|
|
29
|
+
timestamp,
|
|
30
|
+
uuid
|
|
31
|
+
} from "drizzle-orm/pg-core";
|
|
32
|
+
var domainEvents = pgTable(
|
|
33
|
+
"domain_events",
|
|
34
|
+
{
|
|
35
|
+
id: uuid("id").primaryKey(),
|
|
36
|
+
type: text("type").notNull(),
|
|
37
|
+
aggregateId: text("aggregate_id").notNull(),
|
|
38
|
+
aggregateType: text("aggregate_type").notNull(),
|
|
39
|
+
payload: jsonb("payload").notNull().$type(),
|
|
40
|
+
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
|
|
41
|
+
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
42
|
+
/** Lifecycle status: pending | processed | failed */
|
|
43
|
+
status: text("status").notNull().default("pending"),
|
|
44
|
+
/** Error message from the last failed dispatch attempt. */
|
|
45
|
+
error: text("error"),
|
|
46
|
+
metadata: jsonb("metadata").$type()
|
|
47
|
+
}
|
|
48
|
+
// Indexes: add via migration when deploying
|
|
49
|
+
// - (status, occurred_at) for polling
|
|
50
|
+
// - (aggregate_id, aggregate_type) for replay
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// runtime/constants/tokens.ts
|
|
54
|
+
var DRIZZLE = "DRIZZLE";
|
|
55
|
+
|
|
56
|
+
// runtime/subsystems/events/event-bus.drizzle-backend.ts
|
|
57
|
+
var POLL_INTERVAL_MS = 1e3;
|
|
58
|
+
var POLL_BATCH_SIZE = 50;
|
|
59
|
+
var MAX_RETRIES = 3;
|
|
60
|
+
var DrizzleEventBus = class {
|
|
61
|
+
constructor(db) {
|
|
62
|
+
this.db = db;
|
|
63
|
+
}
|
|
64
|
+
db;
|
|
65
|
+
logger = new Logger(DrizzleEventBus.name);
|
|
66
|
+
polling = false;
|
|
67
|
+
pollTimer = null;
|
|
68
|
+
handlers = /* @__PURE__ */ new Map();
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Lifecycle
|
|
71
|
+
// ============================================================================
|
|
72
|
+
async onModuleInit() {
|
|
73
|
+
this.polling = true;
|
|
74
|
+
this.schedulePoll();
|
|
75
|
+
}
|
|
76
|
+
async onModuleDestroy() {
|
|
77
|
+
this.polling = false;
|
|
78
|
+
if (this.pollTimer) {
|
|
79
|
+
clearTimeout(this.pollTimer);
|
|
80
|
+
this.pollTimer = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// IEventBus
|
|
85
|
+
// ============================================================================
|
|
86
|
+
async publish(event, tx) {
|
|
87
|
+
const client = tx ?? this.db;
|
|
88
|
+
await client.insert(domainEvents).values({
|
|
89
|
+
id: event.id,
|
|
90
|
+
type: event.type,
|
|
91
|
+
aggregateId: event.aggregateId,
|
|
92
|
+
aggregateType: event.aggregateType,
|
|
93
|
+
payload: event.payload,
|
|
94
|
+
occurredAt: event.occurredAt,
|
|
95
|
+
processedAt: null,
|
|
96
|
+
status: "pending",
|
|
97
|
+
metadata: event.metadata
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async publishMany(events, tx) {
|
|
101
|
+
if (events.length === 0) return;
|
|
102
|
+
const client = tx ?? this.db;
|
|
103
|
+
await client.insert(domainEvents).values(
|
|
104
|
+
events.map((e) => ({
|
|
105
|
+
id: e.id,
|
|
106
|
+
type: e.type,
|
|
107
|
+
aggregateId: e.aggregateId,
|
|
108
|
+
aggregateType: e.aggregateType,
|
|
109
|
+
payload: e.payload,
|
|
110
|
+
occurredAt: e.occurredAt,
|
|
111
|
+
processedAt: null,
|
|
112
|
+
status: "pending",
|
|
113
|
+
metadata: e.metadata
|
|
114
|
+
}))
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
subscribe(eventType, handler) {
|
|
118
|
+
if (!this.handlers.has(eventType)) {
|
|
119
|
+
this.handlers.set(eventType, /* @__PURE__ */ new Set());
|
|
120
|
+
}
|
|
121
|
+
const set = this.handlers.get(eventType);
|
|
122
|
+
const h = handler;
|
|
123
|
+
set.add(h);
|
|
124
|
+
return () => {
|
|
125
|
+
set.delete(h);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Polling
|
|
130
|
+
// ============================================================================
|
|
131
|
+
schedulePoll() {
|
|
132
|
+
if (!this.polling) return;
|
|
133
|
+
this.pollTimer = setTimeout(async () => {
|
|
134
|
+
try {
|
|
135
|
+
await this.processBatch();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.logger.error(`Poll cycle error: ${err}`);
|
|
138
|
+
} finally {
|
|
139
|
+
this.schedulePoll();
|
|
140
|
+
}
|
|
141
|
+
}, POLL_INTERVAL_MS);
|
|
142
|
+
}
|
|
143
|
+
async processBatch() {
|
|
144
|
+
const rows = await this.db.transaction(async (tx) => {
|
|
145
|
+
return tx.execute(
|
|
146
|
+
sql`SELECT * FROM domain_events WHERE status = 'pending' ORDER BY occurred_at ASC LIMIT ${POLL_BATCH_SIZE} FOR UPDATE SKIP LOCKED`
|
|
147
|
+
);
|
|
148
|
+
}).then((result) => result.rows ?? result);
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
const event = {
|
|
151
|
+
id: row["id"],
|
|
152
|
+
type: row["type"],
|
|
153
|
+
aggregateId: row["aggregate_id"],
|
|
154
|
+
aggregateType: row["aggregate_type"],
|
|
155
|
+
payload: row["payload"],
|
|
156
|
+
occurredAt: new Date(row["occurred_at"]),
|
|
157
|
+
metadata: row["metadata"]
|
|
158
|
+
};
|
|
159
|
+
let attempt = 0;
|
|
160
|
+
let lastError;
|
|
161
|
+
while (attempt < MAX_RETRIES) {
|
|
162
|
+
try {
|
|
163
|
+
await this.dispatch(event);
|
|
164
|
+
await this.db.update(domainEvents).set({ status: "processed", processedAt: /* @__PURE__ */ new Date() }).where(eq(domainEvents.id, event.id));
|
|
165
|
+
lastError = void 0;
|
|
166
|
+
break;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
lastError = err;
|
|
169
|
+
attempt++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (lastError !== void 0) {
|
|
173
|
+
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
|
174
|
+
await this.db.update(domainEvents).set({ status: "failed", error: errorMessage }).where(and(eq(domainEvents.id, event.id), eq(domainEvents.status, "pending")));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async dispatch(event) {
|
|
179
|
+
const set = this.handlers.get(event.type);
|
|
180
|
+
if (!set) return;
|
|
181
|
+
let firstError;
|
|
182
|
+
for (const handler of set) {
|
|
183
|
+
try {
|
|
184
|
+
await handler(event);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this.logger.error(
|
|
187
|
+
`Handler error for event type "${event.type}" (id: ${event.id}): ${err}`
|
|
188
|
+
);
|
|
189
|
+
if (firstError === void 0) {
|
|
190
|
+
firstError = err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (firstError !== void 0) {
|
|
195
|
+
throw firstError;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
DrizzleEventBus = __decorateClass([
|
|
200
|
+
Injectable(),
|
|
201
|
+
__decorateParam(0, Inject(DRIZZLE))
|
|
202
|
+
], DrizzleEventBus);
|
|
203
|
+
|
|
204
|
+
// runtime/subsystems/events/event-bus.memory-backend.ts
|
|
205
|
+
import { Injectable as Injectable2, Logger as Logger2 } from "@nestjs/common";
|
|
206
|
+
var MemoryEventBus = class {
|
|
207
|
+
logger = new Logger2(MemoryEventBus.name);
|
|
208
|
+
/** All events published since construction (or last clear). */
|
|
209
|
+
publishedEvents = [];
|
|
210
|
+
handlers = /* @__PURE__ */ new Map();
|
|
211
|
+
async publish(event) {
|
|
212
|
+
this.publishedEvents.push(event);
|
|
213
|
+
await this.dispatch(event);
|
|
214
|
+
}
|
|
215
|
+
async publishMany(events) {
|
|
216
|
+
for (const event of events) {
|
|
217
|
+
await this.publish(event);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
subscribe(eventType, handler) {
|
|
221
|
+
if (!this.handlers.has(eventType)) {
|
|
222
|
+
this.handlers.set(eventType, /* @__PURE__ */ new Set());
|
|
223
|
+
}
|
|
224
|
+
const set = this.handlers.get(eventType);
|
|
225
|
+
const h = handler;
|
|
226
|
+
set.add(h);
|
|
227
|
+
return () => {
|
|
228
|
+
set.delete(h);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Remove all published events and subscriptions. Useful in beforeEach. */
|
|
232
|
+
clear() {
|
|
233
|
+
this.publishedEvents.length = 0;
|
|
234
|
+
this.handlers.clear();
|
|
235
|
+
}
|
|
236
|
+
async dispatch(event) {
|
|
237
|
+
const set = this.handlers.get(event.type);
|
|
238
|
+
if (!set) return;
|
|
239
|
+
let firstError;
|
|
240
|
+
for (const handler of set) {
|
|
241
|
+
try {
|
|
242
|
+
await handler(event);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
this.logger.error(
|
|
245
|
+
`Handler error for event type "${event.type}" (id: ${event.id}): ${err}`
|
|
246
|
+
);
|
|
247
|
+
if (firstError === void 0) {
|
|
248
|
+
firstError = err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (firstError !== void 0) {
|
|
253
|
+
throw firstError;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
MemoryEventBus = __decorateClass([
|
|
258
|
+
Injectable2()
|
|
259
|
+
], MemoryEventBus);
|
|
260
|
+
|
|
261
|
+
// runtime/subsystems/events/event-bus.redis-backend.ts
|
|
262
|
+
import { Injectable as Injectable3, Inject as Inject2, Logger as Logger3 } from "@nestjs/common";
|
|
263
|
+
var CHANNEL_PREFIX = "events:";
|
|
264
|
+
async function createRedisClient(url) {
|
|
265
|
+
let Redis;
|
|
266
|
+
try {
|
|
267
|
+
const mod = await import("ioredis");
|
|
268
|
+
Redis = mod.default ?? mod;
|
|
269
|
+
} catch {
|
|
270
|
+
throw new Error(
|
|
271
|
+
'RedisEventBus requires the "ioredis" package. Install it with: npm install ioredis'
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return new Redis(url);
|
|
275
|
+
}
|
|
276
|
+
var RedisEventBus = class {
|
|
277
|
+
constructor(redisUrl) {
|
|
278
|
+
this.redisUrl = redisUrl;
|
|
279
|
+
}
|
|
280
|
+
redisUrl;
|
|
281
|
+
logger = new Logger3(RedisEventBus.name);
|
|
282
|
+
publisher = null;
|
|
283
|
+
subscriber = null;
|
|
284
|
+
connected = false;
|
|
285
|
+
/**
|
|
286
|
+
* In-process subscriber registry. Handlers registered here are called when
|
|
287
|
+
* a message arrives on the subscriber client — keeping fan-out within the
|
|
288
|
+
* same process without an extra round-trip through Redis.
|
|
289
|
+
*/
|
|
290
|
+
handlers = /* @__PURE__ */ new Map();
|
|
291
|
+
/**
|
|
292
|
+
* Track which event types have active Redis subscriptions.
|
|
293
|
+
* Used to avoid subscribing multiple times to the same type channel.
|
|
294
|
+
*/
|
|
295
|
+
subscribedTypes = /* @__PURE__ */ new Set();
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Lifecycle
|
|
298
|
+
// ============================================================================
|
|
299
|
+
async onModuleInit() {
|
|
300
|
+
this.publisher = await createRedisClient(this.redisUrl);
|
|
301
|
+
this.subscriber = await createRedisClient(this.redisUrl);
|
|
302
|
+
this.publisher.on(
|
|
303
|
+
"error",
|
|
304
|
+
(err) => this.logger.error(`Redis publisher error: ${err.message}`, err.stack)
|
|
305
|
+
);
|
|
306
|
+
this.subscriber.on(
|
|
307
|
+
"error",
|
|
308
|
+
(err) => this.logger.error(`Redis subscriber error: ${err.message}`, err.stack)
|
|
309
|
+
);
|
|
310
|
+
this.subscriber.on("message", (channel, message) => {
|
|
311
|
+
void this.handleMessage(channel, message);
|
|
312
|
+
});
|
|
313
|
+
this.connected = true;
|
|
314
|
+
this.logger.log(`RedisEventBus connected to ${this.redisUrl}`);
|
|
315
|
+
}
|
|
316
|
+
async onModuleDestroy() {
|
|
317
|
+
this.connected = false;
|
|
318
|
+
if (this.subscriber) {
|
|
319
|
+
await this.subscriber.unsubscribe();
|
|
320
|
+
this.subscriber.disconnect();
|
|
321
|
+
this.subscriber = null;
|
|
322
|
+
}
|
|
323
|
+
if (this.publisher) {
|
|
324
|
+
this.publisher.disconnect();
|
|
325
|
+
this.publisher = null;
|
|
326
|
+
}
|
|
327
|
+
this.subscribedTypes.clear();
|
|
328
|
+
this.logger.log("RedisEventBus disconnected");
|
|
329
|
+
}
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// IEventBus
|
|
332
|
+
// ============================================================================
|
|
333
|
+
/**
|
|
334
|
+
* Publish a single event.
|
|
335
|
+
*
|
|
336
|
+
* `tx` is accepted but ignored — see module-level JSDoc for details.
|
|
337
|
+
*/
|
|
338
|
+
async publish(event, tx) {
|
|
339
|
+
void tx;
|
|
340
|
+
this.assertConnected();
|
|
341
|
+
const payload = this.serialize(event);
|
|
342
|
+
const channel = `${CHANNEL_PREFIX}${event.type}`;
|
|
343
|
+
await this.publisher.publish(channel, payload);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Publish multiple events using a pipeline so all PUBLISH commands are sent
|
|
347
|
+
* in a single round-trip.
|
|
348
|
+
*
|
|
349
|
+
* `tx` is accepted but ignored — see module-level JSDoc for details.
|
|
350
|
+
*/
|
|
351
|
+
async publishMany(events, tx) {
|
|
352
|
+
void tx;
|
|
353
|
+
if (events.length === 0) return;
|
|
354
|
+
this.assertConnected();
|
|
355
|
+
const pipeline = this.publisher.pipeline();
|
|
356
|
+
for (const event of events) {
|
|
357
|
+
const payload = this.serialize(event);
|
|
358
|
+
const channel = `${CHANNEL_PREFIX}${event.type}`;
|
|
359
|
+
pipeline.publish(channel, payload);
|
|
360
|
+
}
|
|
361
|
+
await pipeline.exec();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Register a handler for a specific event type.
|
|
365
|
+
* Returns an unsubscribe function — call it to remove the handler.
|
|
366
|
+
*
|
|
367
|
+
* On first handler for a type, subscribes to the per-type Redis channel.
|
|
368
|
+
* On removal of the last handler for a type, unsubscribes from the channel.
|
|
369
|
+
*/
|
|
370
|
+
subscribe(eventType, handler) {
|
|
371
|
+
if (!this.handlers.has(eventType)) {
|
|
372
|
+
this.handlers.set(eventType, /* @__PURE__ */ new Set());
|
|
373
|
+
void this.subscribeToType(eventType);
|
|
374
|
+
}
|
|
375
|
+
const set = this.handlers.get(eventType);
|
|
376
|
+
const h = handler;
|
|
377
|
+
set.add(h);
|
|
378
|
+
return () => {
|
|
379
|
+
set.delete(h);
|
|
380
|
+
if (set.size === 0) {
|
|
381
|
+
this.handlers.delete(eventType);
|
|
382
|
+
void this.unsubscribeFromType(eventType);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// ============================================================================
|
|
387
|
+
// Internal helpers
|
|
388
|
+
// ============================================================================
|
|
389
|
+
assertConnected() {
|
|
390
|
+
if (!this.connected || !this.publisher) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
"RedisEventBus is not connected. Ensure the module has been initialised before publishing."
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
serialize(event) {
|
|
397
|
+
return JSON.stringify({
|
|
398
|
+
...event,
|
|
399
|
+
occurredAt: event.occurredAt.toISOString()
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
deserialize(raw) {
|
|
403
|
+
const parsed = JSON.parse(raw);
|
|
404
|
+
return {
|
|
405
|
+
...parsed,
|
|
406
|
+
occurredAt: new Date(parsed.occurredAt)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async handleMessage(channel, message) {
|
|
410
|
+
let event;
|
|
411
|
+
try {
|
|
412
|
+
event = this.deserialize(message);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
this.logger.warn(`Failed to deserialize event on channel "${channel}": ${err}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
await this.dispatch(event);
|
|
418
|
+
}
|
|
419
|
+
async dispatch(event) {
|
|
420
|
+
const set = this.handlers.get(event.type);
|
|
421
|
+
if (!set) return;
|
|
422
|
+
for (const handler of set) {
|
|
423
|
+
try {
|
|
424
|
+
await handler(event);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
this.logger.error(
|
|
427
|
+
`Handler error for event type "${event.type}" (id: ${event.id}): ${err}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Subscribe to a per-type Redis channel.
|
|
434
|
+
* Called lazily when the first handler is registered for a type.
|
|
435
|
+
*/
|
|
436
|
+
async subscribeToType(eventType) {
|
|
437
|
+
if (this.subscribedTypes.has(eventType)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const channel = `${CHANNEL_PREFIX}${eventType}`;
|
|
441
|
+
try {
|
|
442
|
+
await this.subscriber.subscribe(channel);
|
|
443
|
+
this.subscribedTypes.add(eventType);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
this.logger.error(`Failed to subscribe to channel "${channel}": ${err}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Unsubscribe from a per-type Redis channel.
|
|
450
|
+
* Called when the last handler for a type is removed.
|
|
451
|
+
*/
|
|
452
|
+
async unsubscribeFromType(eventType) {
|
|
453
|
+
if (!this.subscribedTypes.has(eventType)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const channel = `${CHANNEL_PREFIX}${eventType}`;
|
|
457
|
+
try {
|
|
458
|
+
await this.subscriber.unsubscribe(channel);
|
|
459
|
+
this.subscribedTypes.delete(eventType);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
this.logger.error(`Failed to unsubscribe from channel "${channel}": ${err}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
RedisEventBus = __decorateClass([
|
|
466
|
+
Injectable3(),
|
|
467
|
+
__decorateParam(0, Inject2(REDIS_URL))
|
|
468
|
+
], RedisEventBus);
|
|
469
|
+
|
|
470
|
+
// runtime/subsystems/events/events.module.ts
|
|
471
|
+
var EventsModule = class {
|
|
472
|
+
static forRootAsync(asyncOptions) {
|
|
473
|
+
return {
|
|
474
|
+
module: EventsModule,
|
|
475
|
+
global: true,
|
|
476
|
+
imports: asyncOptions.imports ?? [],
|
|
477
|
+
providers: [
|
|
478
|
+
{
|
|
479
|
+
provide: "EVENTS_MODULE_OPTIONS",
|
|
480
|
+
useFactory: asyncOptions.useFactory,
|
|
481
|
+
inject: asyncOptions.inject ?? []
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
provide: EVENT_BUS,
|
|
485
|
+
useFactory: (options) => {
|
|
486
|
+
const mod = EventsModule.forRoot(options);
|
|
487
|
+
const provider = mod.providers?.find(
|
|
488
|
+
(p) => typeof p === "object" && p !== null && "provide" in p && p.provide === EVENT_BUS
|
|
489
|
+
);
|
|
490
|
+
if (provider && typeof provider === "object" && "useClass" in provider) {
|
|
491
|
+
return new provider.useClass();
|
|
492
|
+
}
|
|
493
|
+
throw new Error("EventsModule.forRootAsync: failed to resolve provider");
|
|
494
|
+
},
|
|
495
|
+
inject: ["EVENTS_MODULE_OPTIONS"]
|
|
496
|
+
}
|
|
497
|
+
],
|
|
498
|
+
exports: [EVENT_BUS]
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
static forRoot(options = { backend: "drizzle" }) {
|
|
502
|
+
if (options.backend === "redis") {
|
|
503
|
+
const resolvedUrl = options.redisUrl ?? process.env["REDIS_URL"] ?? "redis://localhost:6379";
|
|
504
|
+
return {
|
|
505
|
+
module: EventsModule,
|
|
506
|
+
global: true,
|
|
507
|
+
providers: [
|
|
508
|
+
{ provide: REDIS_URL, useValue: resolvedUrl },
|
|
509
|
+
{ provide: EVENT_BUS, useClass: RedisEventBus },
|
|
510
|
+
// Register concrete class so NestJS can resolve lifecycle hooks
|
|
511
|
+
RedisEventBus
|
|
512
|
+
],
|
|
513
|
+
exports: [EVENT_BUS]
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const provider = options.backend === "drizzle" ? { provide: EVENT_BUS, useClass: DrizzleEventBus } : { provide: EVENT_BUS, useClass: MemoryEventBus };
|
|
517
|
+
return {
|
|
518
|
+
module: EventsModule,
|
|
519
|
+
global: true,
|
|
520
|
+
providers: [provider],
|
|
521
|
+
exports: [EVENT_BUS]
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
EventsModule = __decorateClass([
|
|
526
|
+
Module({})
|
|
527
|
+
], EventsModule);
|
|
528
|
+
|
|
529
|
+
// runtime/subsystems/jobs/jobs.tokens.ts
|
|
530
|
+
var JOB_QUEUE = /* @__PURE__ */ Symbol("JOB_QUEUE");
|
|
531
|
+
var REDIS_URL2 = /* @__PURE__ */ Symbol("REDIS_URL");
|
|
532
|
+
|
|
533
|
+
// runtime/subsystems/jobs/jobs.module.ts
|
|
534
|
+
import { Module as Module2 } from "@nestjs/common";
|
|
535
|
+
|
|
536
|
+
// runtime/subsystems/jobs/job-queue.drizzle-backend.ts
|
|
537
|
+
import { Injectable as Injectable4, Inject as Inject3, Logger as Logger4 } from "@nestjs/common";
|
|
538
|
+
import { randomUUID } from "crypto";
|
|
539
|
+
import { eq as eq2, and as and2, lte, sql as sql2, lt } from "drizzle-orm";
|
|
540
|
+
|
|
541
|
+
// runtime/subsystems/jobs/job-queue.schema.ts
|
|
542
|
+
import {
|
|
543
|
+
pgTable as pgTable2,
|
|
544
|
+
uuid as uuid2,
|
|
545
|
+
text as text2,
|
|
546
|
+
jsonb as jsonb2,
|
|
547
|
+
integer,
|
|
548
|
+
timestamp as timestamp2
|
|
549
|
+
} from "drizzle-orm/pg-core";
|
|
550
|
+
var jobQueue = pgTable2(
|
|
551
|
+
"job_queue",
|
|
552
|
+
{
|
|
553
|
+
id: uuid2("id").primaryKey().defaultRandom(),
|
|
554
|
+
/** Job type — matches the type registered via process(). */
|
|
555
|
+
type: text2("type").notNull(),
|
|
556
|
+
/** Arbitrary JSON payload passed to the handler. */
|
|
557
|
+
payload: jsonb2("payload").notNull().default({}),
|
|
558
|
+
/** Current job lifecycle status. */
|
|
559
|
+
status: text2("status").notNull().default("pending").$type(),
|
|
560
|
+
/** Earliest time the job may be claimed. */
|
|
561
|
+
runAt: timestamp2("run_at").notNull().defaultNow(),
|
|
562
|
+
/** Higher priority jobs are claimed first (ORDER BY priority DESC). */
|
|
563
|
+
priority: integer("priority").notNull().default(0),
|
|
564
|
+
/** Number of processing attempts made so far. */
|
|
565
|
+
attempts: integer("attempts").notNull().default(0),
|
|
566
|
+
/** Maximum number of retries before status → failed. */
|
|
567
|
+
maxRetries: integer("max_retries").notNull().default(3),
|
|
568
|
+
/** Base backoff in ms (doubles on each retry). */
|
|
569
|
+
backoffMs: integer("backoff_ms").notNull().default(1e3),
|
|
570
|
+
/** Error message from the last failed attempt. */
|
|
571
|
+
lastError: text2("last_error"),
|
|
572
|
+
createdAt: timestamp2("created_at").notNull().defaultNow(),
|
|
573
|
+
completedAt: timestamp2("completed_at"),
|
|
574
|
+
/** When the job was last claimed by a worker (used for stale job recovery). */
|
|
575
|
+
claimedAt: timestamp2("claimed_at")
|
|
576
|
+
}
|
|
577
|
+
// Indexes: add via migration when deploying
|
|
578
|
+
// - (status, run_at) for claim query
|
|
579
|
+
// - (type, status) for routing
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// runtime/subsystems/jobs/job-queue.drizzle-backend.ts
|
|
583
|
+
var POLL_INTERVAL_MS2 = 1e3;
|
|
584
|
+
var STALE_RECOVERY_INTERVAL_MS = 6e4;
|
|
585
|
+
var STALE_THRESHOLD_MS = 5 * 6e4;
|
|
586
|
+
var DrizzleJobQueue = class {
|
|
587
|
+
constructor(db) {
|
|
588
|
+
this.db = db;
|
|
589
|
+
}
|
|
590
|
+
db;
|
|
591
|
+
logger = new Logger4(DrizzleJobQueue.name);
|
|
592
|
+
polling = false;
|
|
593
|
+
pollTimer = null;
|
|
594
|
+
staleTimer = null;
|
|
595
|
+
handlers = /* @__PURE__ */ new Map();
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// Lifecycle
|
|
598
|
+
// ============================================================================
|
|
599
|
+
async onModuleInit() {
|
|
600
|
+
this.polling = true;
|
|
601
|
+
this.startPolling();
|
|
602
|
+
this.staleTimer = setInterval(() => {
|
|
603
|
+
void this.recoverStaleJobs();
|
|
604
|
+
}, STALE_RECOVERY_INTERVAL_MS);
|
|
605
|
+
}
|
|
606
|
+
async onModuleDestroy() {
|
|
607
|
+
this.polling = false;
|
|
608
|
+
if (this.pollTimer !== null) {
|
|
609
|
+
clearTimeout(this.pollTimer);
|
|
610
|
+
this.pollTimer = null;
|
|
611
|
+
}
|
|
612
|
+
if (this.staleTimer !== null) {
|
|
613
|
+
clearInterval(this.staleTimer);
|
|
614
|
+
this.staleTimer = null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// ============================================================================
|
|
618
|
+
// Protocol implementation
|
|
619
|
+
// ============================================================================
|
|
620
|
+
async enqueue(type, payload, options) {
|
|
621
|
+
const id = randomUUID();
|
|
622
|
+
const delay = options?.delay ?? 0;
|
|
623
|
+
const runAt = new Date(Date.now() + delay);
|
|
624
|
+
await this.db.insert(jobQueue).values({
|
|
625
|
+
id,
|
|
626
|
+
type,
|
|
627
|
+
payload,
|
|
628
|
+
status: "pending",
|
|
629
|
+
runAt,
|
|
630
|
+
priority: options?.priority ?? 0,
|
|
631
|
+
attempts: 0,
|
|
632
|
+
maxRetries: options?.retries ?? 3,
|
|
633
|
+
backoffMs: options?.backoff ?? 1e3
|
|
634
|
+
});
|
|
635
|
+
return id;
|
|
636
|
+
}
|
|
637
|
+
process(type, handler, payloadSchema) {
|
|
638
|
+
this.handlers.set(type, {
|
|
639
|
+
handler,
|
|
640
|
+
schema: payloadSchema
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
async schedule(type, cron, payload) {
|
|
644
|
+
const id = randomUUID();
|
|
645
|
+
await this.db.insert(jobQueue).values({
|
|
646
|
+
id,
|
|
647
|
+
type,
|
|
648
|
+
payload: { ...payload, __cron: cron },
|
|
649
|
+
status: "pending",
|
|
650
|
+
runAt: /* @__PURE__ */ new Date(),
|
|
651
|
+
priority: 0,
|
|
652
|
+
attempts: 0,
|
|
653
|
+
maxRetries: 0,
|
|
654
|
+
backoffMs: 0
|
|
655
|
+
});
|
|
656
|
+
return id;
|
|
657
|
+
}
|
|
658
|
+
async cancel(jobId) {
|
|
659
|
+
await this.db.update(jobQueue).set({ status: "expired" }).where(and2(eq2(jobQueue.id, jobId), eq2(jobQueue.status, "pending")));
|
|
660
|
+
}
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// Polling loop
|
|
663
|
+
// ============================================================================
|
|
664
|
+
startPolling() {
|
|
665
|
+
const tick = async () => {
|
|
666
|
+
if (!this.polling) return;
|
|
667
|
+
try {
|
|
668
|
+
await this.claimAndProcess();
|
|
669
|
+
} catch (err) {
|
|
670
|
+
this.logger.error(`Poll cycle error: ${err}`);
|
|
671
|
+
} finally {
|
|
672
|
+
if (this.polling) {
|
|
673
|
+
this.pollTimer = setTimeout(tick, POLL_INTERVAL_MS2);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
this.pollTimer = setTimeout(tick, 0);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Claim one pending job using UPDATE...RETURNING with an advisory lock.
|
|
681
|
+
* The advisory lock (pg_try_advisory_xact_lock) prevents concurrent workers
|
|
682
|
+
* from claiming the same job when multiple instances are polling.
|
|
683
|
+
* Jobs are claimed in priority DESC, run_at ASC order.
|
|
684
|
+
*/
|
|
685
|
+
async claimAndProcess() {
|
|
686
|
+
const rows = await this.db.update(jobQueue).set({ status: "active", attempts: sql2`${jobQueue.attempts} + 1`, claimedAt: /* @__PURE__ */ new Date() }).where(
|
|
687
|
+
and2(
|
|
688
|
+
eq2(jobQueue.status, "pending"),
|
|
689
|
+
lte(jobQueue.runAt, /* @__PURE__ */ new Date())
|
|
690
|
+
)
|
|
691
|
+
).returning();
|
|
692
|
+
rows.sort((a, b) => {
|
|
693
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
694
|
+
return a.runAt.getTime() - b.runAt.getTime();
|
|
695
|
+
});
|
|
696
|
+
const job = rows[0];
|
|
697
|
+
if (!job) return;
|
|
698
|
+
const entry = this.handlers.get(job.type);
|
|
699
|
+
if (!entry) {
|
|
700
|
+
await this.db.update(jobQueue).set({ status: "failed", lastError: `No handler registered for job type: ${job.type}` }).where(eq2(jobQueue.id, job.id));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const payload = entry.schema ? entry.schema.parse(job.payload) : job.payload;
|
|
705
|
+
await entry.handler(payload);
|
|
706
|
+
await this.db.update(jobQueue).set({ status: "completed", completedAt: /* @__PURE__ */ new Date() }).where(eq2(jobQueue.id, job.id));
|
|
707
|
+
} catch (err) {
|
|
708
|
+
await this.handleFailure(job, err);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async handleFailure(job, err) {
|
|
712
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
713
|
+
const exhausted = job.attempts >= job.maxRetries;
|
|
714
|
+
if (exhausted) {
|
|
715
|
+
await this.db.update(jobQueue).set({ status: "failed", lastError: errorMessage }).where(eq2(jobQueue.id, job.id));
|
|
716
|
+
} else {
|
|
717
|
+
const backoffDelay = job.backoffMs * Math.pow(2, job.attempts - 1);
|
|
718
|
+
const retryAt = new Date(Date.now() + backoffDelay);
|
|
719
|
+
await this.db.update(jobQueue).set({ status: "pending", runAt: retryAt, lastError: errorMessage }).where(eq2(jobQueue.id, job.id));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Reset stale active jobs back to pending.
|
|
724
|
+
* A job is considered stale if it has been in 'active' state for more than
|
|
725
|
+
* STALE_THRESHOLD_MS milliseconds (i.e. the worker crashed without completing).
|
|
726
|
+
*/
|
|
727
|
+
async recoverStaleJobs() {
|
|
728
|
+
const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MS);
|
|
729
|
+
try {
|
|
730
|
+
await this.db.update(jobQueue).set({ status: "pending", claimedAt: null }).where(
|
|
731
|
+
and2(
|
|
732
|
+
eq2(jobQueue.status, "active"),
|
|
733
|
+
lt(jobQueue.claimedAt, staleThreshold)
|
|
734
|
+
)
|
|
735
|
+
);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
this.logger.error(`Stale job recovery error: ${err}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
DrizzleJobQueue = __decorateClass([
|
|
742
|
+
Injectable4(),
|
|
743
|
+
__decorateParam(0, Inject3(DRIZZLE))
|
|
744
|
+
], DrizzleJobQueue);
|
|
745
|
+
|
|
746
|
+
// runtime/subsystems/jobs/job-queue.memory-backend.ts
|
|
747
|
+
import { Injectable as Injectable5 } from "@nestjs/common";
|
|
748
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
749
|
+
var MemoryJobQueue = class {
|
|
750
|
+
handlers = /* @__PURE__ */ new Map();
|
|
751
|
+
async enqueue(type, payload, _options) {
|
|
752
|
+
const id = randomUUID2();
|
|
753
|
+
const entry = this.handlers.get(type);
|
|
754
|
+
if (entry) {
|
|
755
|
+
const validated = entry.schema ? entry.schema.parse(payload) : payload;
|
|
756
|
+
await entry.handler(validated);
|
|
757
|
+
}
|
|
758
|
+
return id;
|
|
759
|
+
}
|
|
760
|
+
process(type, handler, payloadSchema) {
|
|
761
|
+
this.handlers.set(type, {
|
|
762
|
+
handler,
|
|
763
|
+
schema: payloadSchema
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
async schedule(_type, _cron, _payload) {
|
|
767
|
+
return randomUUID2();
|
|
768
|
+
}
|
|
769
|
+
async cancel(_jobId) {
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
MemoryJobQueue = __decorateClass([
|
|
773
|
+
Injectable5()
|
|
774
|
+
], MemoryJobQueue);
|
|
775
|
+
|
|
776
|
+
// runtime/subsystems/jobs/job-queue.redis-backend.ts
|
|
777
|
+
import { Injectable as Injectable6, Inject as Inject4, Logger as Logger5 } from "@nestjs/common";
|
|
778
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
779
|
+
var KEY_PREFIX = "jobs:";
|
|
780
|
+
var SCHEDULE_KEY = "jobs:__schedules";
|
|
781
|
+
async function createRedisClient2(url) {
|
|
782
|
+
let Redis;
|
|
783
|
+
try {
|
|
784
|
+
const mod = await import("ioredis");
|
|
785
|
+
Redis = mod.default ?? mod;
|
|
786
|
+
} catch {
|
|
787
|
+
throw new Error(
|
|
788
|
+
"RedisJobQueue requires ioredis. Install it: bun add ioredis"
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
return new Redis(url);
|
|
792
|
+
}
|
|
793
|
+
var RedisJobQueue = class {
|
|
794
|
+
constructor(redisUrl) {
|
|
795
|
+
this.redisUrl = redisUrl;
|
|
796
|
+
}
|
|
797
|
+
redisUrl;
|
|
798
|
+
logger = new Logger5(RedisJobQueue.name);
|
|
799
|
+
client;
|
|
800
|
+
running = false;
|
|
801
|
+
handlers = /* @__PURE__ */ new Map();
|
|
802
|
+
consumers = [];
|
|
803
|
+
async onModuleInit() {
|
|
804
|
+
this.client = await createRedisClient2(this.redisUrl);
|
|
805
|
+
this.running = true;
|
|
806
|
+
for (const [type] of this.handlers) {
|
|
807
|
+
await this.startConsumer(type);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async onModuleDestroy() {
|
|
811
|
+
this.running = false;
|
|
812
|
+
for (const consumer of this.consumers) {
|
|
813
|
+
try {
|
|
814
|
+
await consumer.client.disconnect();
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.consumers.length = 0;
|
|
819
|
+
if (this.client) {
|
|
820
|
+
await this.client.quit();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// ============================================================================
|
|
824
|
+
// Protocol implementation
|
|
825
|
+
// ============================================================================
|
|
826
|
+
async enqueue(type, payload, options) {
|
|
827
|
+
const id = randomUUID3();
|
|
828
|
+
const job = {
|
|
829
|
+
id,
|
|
830
|
+
type,
|
|
831
|
+
payload,
|
|
832
|
+
options: options ?? {},
|
|
833
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
834
|
+
attempts: 0
|
|
835
|
+
};
|
|
836
|
+
const key = KEY_PREFIX + type;
|
|
837
|
+
if (options?.delay && options.delay > 0) {
|
|
838
|
+
setTimeout(async () => {
|
|
839
|
+
try {
|
|
840
|
+
await this.client.rpush(key, JSON.stringify(job));
|
|
841
|
+
} catch (err) {
|
|
842
|
+
this.logger.error(`Failed to enqueue delayed job ${id}: ${err}`);
|
|
843
|
+
}
|
|
844
|
+
}, options.delay);
|
|
845
|
+
} else {
|
|
846
|
+
await this.client.rpush(key, JSON.stringify(job));
|
|
847
|
+
}
|
|
848
|
+
return id;
|
|
849
|
+
}
|
|
850
|
+
process(type, handler, payloadSchema) {
|
|
851
|
+
this.handlers.set(type, {
|
|
852
|
+
handler,
|
|
853
|
+
schema: payloadSchema
|
|
854
|
+
});
|
|
855
|
+
if (this.running) {
|
|
856
|
+
this.startConsumer(type).catch((err) => {
|
|
857
|
+
this.logger.error(`Failed to start consumer for ${type}: ${err}`);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async schedule(type, cron, payload) {
|
|
862
|
+
const id = randomUUID3();
|
|
863
|
+
const schedule = { id, type, cron, payload, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
864
|
+
await this.client.hset(SCHEDULE_KEY, id, JSON.stringify(schedule));
|
|
865
|
+
return id;
|
|
866
|
+
}
|
|
867
|
+
async cancel(jobId) {
|
|
868
|
+
const keys = await this.client.keys(KEY_PREFIX + "*");
|
|
869
|
+
for (const key of keys) {
|
|
870
|
+
if (key === SCHEDULE_KEY) continue;
|
|
871
|
+
const items = await this.client.lrange(key, 0, -1);
|
|
872
|
+
for (const item of items) {
|
|
873
|
+
try {
|
|
874
|
+
const job = JSON.parse(item);
|
|
875
|
+
if (job.id === jobId) {
|
|
876
|
+
await this.client.lrem(key, 1, item);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
await this.client.hdel(SCHEDULE_KEY, jobId);
|
|
884
|
+
}
|
|
885
|
+
// ============================================================================
|
|
886
|
+
// Consumer loop
|
|
887
|
+
// ============================================================================
|
|
888
|
+
async startConsumer(type) {
|
|
889
|
+
const consumerClient = await createRedisClient2(this.redisUrl);
|
|
890
|
+
this.consumers.push({ type, client: consumerClient });
|
|
891
|
+
const key = KEY_PREFIX + type;
|
|
892
|
+
const entry = this.handlers.get(type);
|
|
893
|
+
if (!entry) return;
|
|
894
|
+
const loop = async () => {
|
|
895
|
+
while (this.running) {
|
|
896
|
+
try {
|
|
897
|
+
const result = await consumerClient.blpop(key, 5);
|
|
898
|
+
if (!result) continue;
|
|
899
|
+
const [, raw] = result;
|
|
900
|
+
const job = JSON.parse(raw);
|
|
901
|
+
const payload = entry.schema ? entry.schema.parse(job.payload) : job.payload;
|
|
902
|
+
try {
|
|
903
|
+
await entry.handler(payload);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
const maxRetries = job.options.retries ?? 3;
|
|
906
|
+
job.attempts += 1;
|
|
907
|
+
if (job.attempts < maxRetries) {
|
|
908
|
+
const backoff = (job.options.backoff ?? 1e3) * Math.pow(2, job.attempts - 1);
|
|
909
|
+
this.logger.warn(
|
|
910
|
+
`Job ${job.id} failed (attempt ${job.attempts}/${maxRetries}), retrying in ${backoff}ms`
|
|
911
|
+
);
|
|
912
|
+
setTimeout(async () => {
|
|
913
|
+
try {
|
|
914
|
+
await this.client.rpush(key, JSON.stringify(job));
|
|
915
|
+
} catch (e) {
|
|
916
|
+
this.logger.error(`Failed to re-enqueue job ${job.id}: ${e}`);
|
|
917
|
+
}
|
|
918
|
+
}, backoff);
|
|
919
|
+
} else {
|
|
920
|
+
this.logger.error(
|
|
921
|
+
`Job ${job.id} exhausted ${maxRetries} retries: ${err instanceof Error ? err.message : err}`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
if (this.running) {
|
|
927
|
+
this.logger.error(`Consumer error for ${type}: ${err}`);
|
|
928
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
loop().catch((err) => {
|
|
934
|
+
this.logger.error(`Consumer loop for ${type} terminated: ${err}`);
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
RedisJobQueue = __decorateClass([
|
|
939
|
+
Injectable6(),
|
|
940
|
+
__decorateParam(0, Inject4(REDIS_URL2))
|
|
941
|
+
], RedisJobQueue);
|
|
942
|
+
|
|
943
|
+
// runtime/subsystems/jobs/job-queue.bullmq-backend.ts
|
|
944
|
+
import { Injectable as Injectable7, Inject as Inject5, Logger as Logger6 } from "@nestjs/common";
|
|
945
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
946
|
+
var QueueClass;
|
|
947
|
+
var WorkerClass;
|
|
948
|
+
async function loadBullMQ() {
|
|
949
|
+
try {
|
|
950
|
+
const mod = await import("bullmq");
|
|
951
|
+
QueueClass = mod.Queue;
|
|
952
|
+
WorkerClass = mod.Worker;
|
|
953
|
+
} catch {
|
|
954
|
+
throw new Error(
|
|
955
|
+
"BullMQJobQueue requires bullmq. Install it: bun add bullmq"
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
var DEFAULT_QUEUE_NAME = "codegen-jobs";
|
|
960
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
961
|
+
var BullMQJobQueue = class {
|
|
962
|
+
constructor(redisUrl) {
|
|
963
|
+
this.redisUrl = redisUrl;
|
|
964
|
+
}
|
|
965
|
+
redisUrl;
|
|
966
|
+
logger = new Logger6(BullMQJobQueue.name);
|
|
967
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
968
|
+
queue;
|
|
969
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
970
|
+
workers = [];
|
|
971
|
+
handlers = /* @__PURE__ */ new Map();
|
|
972
|
+
initialized = false;
|
|
973
|
+
async onModuleInit() {
|
|
974
|
+
await loadBullMQ();
|
|
975
|
+
const connection = this.parseRedisUrl(this.redisUrl);
|
|
976
|
+
this.queue = new QueueClass(DEFAULT_QUEUE_NAME, { connection });
|
|
977
|
+
this.initialized = true;
|
|
978
|
+
for (const [type] of this.handlers) {
|
|
979
|
+
this.createWorker(type, connection);
|
|
980
|
+
}
|
|
981
|
+
this.logger.log(`BullMQ queue "${DEFAULT_QUEUE_NAME}" initialized`);
|
|
982
|
+
}
|
|
983
|
+
async onModuleDestroy() {
|
|
984
|
+
const closePromises = this.workers.map((w) => w.close());
|
|
985
|
+
await Promise.allSettled(closePromises);
|
|
986
|
+
this.workers.length = 0;
|
|
987
|
+
if (this.queue) {
|
|
988
|
+
await this.queue.close();
|
|
989
|
+
}
|
|
990
|
+
this.logger.log("BullMQ queue shut down");
|
|
991
|
+
}
|
|
992
|
+
// ============================================================================
|
|
993
|
+
// Protocol implementation
|
|
994
|
+
// ============================================================================
|
|
995
|
+
async enqueue(type, payload, options) {
|
|
996
|
+
if (!this.queue) {
|
|
997
|
+
throw new Error("BullMQJobQueue not initialized \u2014 call onModuleInit first");
|
|
998
|
+
}
|
|
999
|
+
const jobId = randomUUID4();
|
|
1000
|
+
const bullOpts = {
|
|
1001
|
+
jobId,
|
|
1002
|
+
removeOnComplete: true,
|
|
1003
|
+
removeOnFail: 100
|
|
1004
|
+
// keep last 100 failed jobs for debugging
|
|
1005
|
+
};
|
|
1006
|
+
if (options?.delay && options.delay > 0) {
|
|
1007
|
+
bullOpts.delay = options.delay;
|
|
1008
|
+
}
|
|
1009
|
+
if (options?.priority !== void 0) {
|
|
1010
|
+
bullOpts.priority = options.priority;
|
|
1011
|
+
}
|
|
1012
|
+
if (options?.retries !== void 0) {
|
|
1013
|
+
bullOpts.attempts = options.retries + 1;
|
|
1014
|
+
}
|
|
1015
|
+
if (options?.backoff !== void 0) {
|
|
1016
|
+
bullOpts.backoff = {
|
|
1017
|
+
type: "exponential",
|
|
1018
|
+
delay: options.backoff
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
await this.queue.add(type, payload, bullOpts);
|
|
1022
|
+
return jobId;
|
|
1023
|
+
}
|
|
1024
|
+
process(type, handler, payloadSchema) {
|
|
1025
|
+
this.handlers.set(type, {
|
|
1026
|
+
handler,
|
|
1027
|
+
schema: payloadSchema
|
|
1028
|
+
});
|
|
1029
|
+
if (this.initialized) {
|
|
1030
|
+
const connection = this.parseRedisUrl(this.redisUrl);
|
|
1031
|
+
this.createWorker(type, connection);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async schedule(type, cron, payload) {
|
|
1035
|
+
if (!this.queue) {
|
|
1036
|
+
throw new Error("BullMQJobQueue not initialized \u2014 call onModuleInit first");
|
|
1037
|
+
}
|
|
1038
|
+
const jobId = randomUUID4();
|
|
1039
|
+
await this.queue.add(type, payload ?? {}, {
|
|
1040
|
+
jobId,
|
|
1041
|
+
repeat: { pattern: cron },
|
|
1042
|
+
removeOnComplete: true
|
|
1043
|
+
});
|
|
1044
|
+
return jobId;
|
|
1045
|
+
}
|
|
1046
|
+
async cancel(jobId) {
|
|
1047
|
+
if (!this.queue) return;
|
|
1048
|
+
try {
|
|
1049
|
+
const job = await this.queue.getJob(jobId);
|
|
1050
|
+
if (job) {
|
|
1051
|
+
await job.remove();
|
|
1052
|
+
}
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
this.logger.warn(`Failed to cancel job ${jobId}: ${err}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// ============================================================================
|
|
1058
|
+
// Worker management
|
|
1059
|
+
// ============================================================================
|
|
1060
|
+
createWorker(type, connection) {
|
|
1061
|
+
const entry = this.handlers.get(type);
|
|
1062
|
+
if (!entry) return;
|
|
1063
|
+
const worker = new WorkerClass(
|
|
1064
|
+
DEFAULT_QUEUE_NAME,
|
|
1065
|
+
async (job) => {
|
|
1066
|
+
if (job.name !== type) return;
|
|
1067
|
+
const payload = entry.schema ? entry.schema.parse(job.data) : job.data;
|
|
1068
|
+
await entry.handler(payload);
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
connection,
|
|
1072
|
+
concurrency: DEFAULT_CONCURRENCY
|
|
1073
|
+
// Only pick up jobs matching this handler's type
|
|
1074
|
+
// BullMQ doesn't natively filter by name in the worker, so we
|
|
1075
|
+
// check job.name inside the processor. For high-throughput systems
|
|
1076
|
+
// with many job types, consider separate queues per type.
|
|
1077
|
+
}
|
|
1078
|
+
);
|
|
1079
|
+
worker.on("failed", (job, err) => {
|
|
1080
|
+
this.logger.error(`Job ${job?.id} (${type}) failed: ${err.message}`);
|
|
1081
|
+
});
|
|
1082
|
+
worker.on("error", (err) => {
|
|
1083
|
+
this.logger.error(`Worker error for ${type}: ${err.message}`);
|
|
1084
|
+
});
|
|
1085
|
+
this.workers.push(worker);
|
|
1086
|
+
}
|
|
1087
|
+
// ============================================================================
|
|
1088
|
+
// Helpers
|
|
1089
|
+
// ============================================================================
|
|
1090
|
+
parseRedisUrl(url) {
|
|
1091
|
+
try {
|
|
1092
|
+
const parsed = new URL(url);
|
|
1093
|
+
return {
|
|
1094
|
+
host: parsed.hostname,
|
|
1095
|
+
port: parseInt(parsed.port || "6379", 10),
|
|
1096
|
+
password: parsed.password || void 0,
|
|
1097
|
+
db: parsed.pathname ? parseInt(parsed.pathname.slice(1) || "0", 10) : 0
|
|
1098
|
+
};
|
|
1099
|
+
} catch {
|
|
1100
|
+
return { host: "localhost", port: 6379 };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
BullMQJobQueue = __decorateClass([
|
|
1105
|
+
Injectable7(),
|
|
1106
|
+
__decorateParam(0, Inject5(REDIS_URL2))
|
|
1107
|
+
], BullMQJobQueue);
|
|
1108
|
+
|
|
1109
|
+
// runtime/subsystems/jobs/jobs.module.ts
|
|
1110
|
+
var DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
1111
|
+
var JobsModule = class {
|
|
1112
|
+
static forRootAsync(asyncOptions) {
|
|
1113
|
+
return {
|
|
1114
|
+
module: JobsModule,
|
|
1115
|
+
global: true,
|
|
1116
|
+
imports: asyncOptions.imports ?? [],
|
|
1117
|
+
providers: [
|
|
1118
|
+
{
|
|
1119
|
+
provide: "JOBS_MODULE_OPTIONS",
|
|
1120
|
+
useFactory: asyncOptions.useFactory,
|
|
1121
|
+
inject: asyncOptions.inject ?? []
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
provide: JOB_QUEUE,
|
|
1125
|
+
useFactory: (options) => {
|
|
1126
|
+
const mod = JobsModule.forRoot(options);
|
|
1127
|
+
const provider = mod.providers?.find(
|
|
1128
|
+
(p) => typeof p === "object" && p !== null && "provide" in p && p.provide === JOB_QUEUE
|
|
1129
|
+
);
|
|
1130
|
+
if (provider && typeof provider === "object" && "useClass" in provider) {
|
|
1131
|
+
return new provider.useClass();
|
|
1132
|
+
}
|
|
1133
|
+
throw new Error("JobsModule.forRootAsync: failed to resolve provider");
|
|
1134
|
+
},
|
|
1135
|
+
inject: ["JOBS_MODULE_OPTIONS"]
|
|
1136
|
+
}
|
|
1137
|
+
],
|
|
1138
|
+
exports: [JOB_QUEUE]
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
static forRoot(options = { backend: "drizzle" }) {
|
|
1142
|
+
switch (options.backend) {
|
|
1143
|
+
case "redis":
|
|
1144
|
+
return {
|
|
1145
|
+
module: JobsModule,
|
|
1146
|
+
global: true,
|
|
1147
|
+
providers: [
|
|
1148
|
+
{ provide: REDIS_URL2, useValue: options.redisUrl ?? DEFAULT_REDIS_URL },
|
|
1149
|
+
{ provide: JOB_QUEUE, useClass: RedisJobQueue }
|
|
1150
|
+
],
|
|
1151
|
+
exports: [JOB_QUEUE]
|
|
1152
|
+
};
|
|
1153
|
+
case "bullmq":
|
|
1154
|
+
return {
|
|
1155
|
+
module: JobsModule,
|
|
1156
|
+
global: true,
|
|
1157
|
+
providers: [
|
|
1158
|
+
{ provide: REDIS_URL2, useValue: options.redisUrl ?? DEFAULT_REDIS_URL },
|
|
1159
|
+
{ provide: JOB_QUEUE, useClass: BullMQJobQueue }
|
|
1160
|
+
],
|
|
1161
|
+
exports: [JOB_QUEUE]
|
|
1162
|
+
};
|
|
1163
|
+
case "memory":
|
|
1164
|
+
return {
|
|
1165
|
+
module: JobsModule,
|
|
1166
|
+
global: true,
|
|
1167
|
+
providers: [{ provide: JOB_QUEUE, useClass: MemoryJobQueue }],
|
|
1168
|
+
exports: [JOB_QUEUE]
|
|
1169
|
+
};
|
|
1170
|
+
case "drizzle":
|
|
1171
|
+
default:
|
|
1172
|
+
return {
|
|
1173
|
+
module: JobsModule,
|
|
1174
|
+
global: true,
|
|
1175
|
+
providers: [{ provide: JOB_QUEUE, useClass: DrizzleJobQueue }],
|
|
1176
|
+
exports: [JOB_QUEUE]
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
JobsModule = __decorateClass([
|
|
1182
|
+
Module2({})
|
|
1183
|
+
], JobsModule);
|
|
1184
|
+
|
|
1185
|
+
// runtime/subsystems/cache/cache.tokens.ts
|
|
1186
|
+
var CACHE = /* @__PURE__ */ Symbol("CACHE");
|
|
1187
|
+
var CACHE_DEFAULT_TTL = /* @__PURE__ */ Symbol("CACHE_DEFAULT_TTL");
|
|
1188
|
+
|
|
1189
|
+
// runtime/subsystems/cache/cache.module.ts
|
|
1190
|
+
import { Module as Module3 } from "@nestjs/common";
|
|
1191
|
+
|
|
1192
|
+
// runtime/subsystems/cache/cache.drizzle-backend.ts
|
|
1193
|
+
import { Injectable as Injectable8, Inject as Inject6, Optional } from "@nestjs/common";
|
|
1194
|
+
import { gt, or, like, sql as sql3, eq as eq3 } from "drizzle-orm";
|
|
1195
|
+
|
|
1196
|
+
// runtime/subsystems/cache/cache.schema.ts
|
|
1197
|
+
import { pgTable as pgTable3, text as text3, jsonb as jsonb3, timestamp as timestamp3 } from "drizzle-orm/pg-core";
|
|
1198
|
+
var cacheEntries = pgTable3(
|
|
1199
|
+
"cache_entries",
|
|
1200
|
+
{
|
|
1201
|
+
/** Cache key — primary key, text (not uuid) to support arbitrary key namespacing. */
|
|
1202
|
+
key: text3("key").primaryKey(),
|
|
1203
|
+
/** Cached value serialised as JSONB. */
|
|
1204
|
+
value: jsonb3("value").notNull(),
|
|
1205
|
+
/** NULL means the entry never expires. */
|
|
1206
|
+
expiresAt: timestamp3("expires_at", { withTimezone: true })
|
|
1207
|
+
}
|
|
1208
|
+
// Index: add (expires_at) via migration for cleanup queries
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
// runtime/subsystems/cache/cache.drizzle-backend.ts
|
|
1212
|
+
var CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1213
|
+
var DrizzleCacheService = class {
|
|
1214
|
+
constructor(db, defaultTtl = null) {
|
|
1215
|
+
this.db = db;
|
|
1216
|
+
this.defaultTtl = defaultTtl;
|
|
1217
|
+
}
|
|
1218
|
+
db;
|
|
1219
|
+
defaultTtl;
|
|
1220
|
+
cleanupTimer = null;
|
|
1221
|
+
/** In-flight getOrSet promises — keyed by cache key to deduplicate stampedes. */
|
|
1222
|
+
inflight = /* @__PURE__ */ new Map();
|
|
1223
|
+
async onModuleInit() {
|
|
1224
|
+
this.cleanupTimer = setInterval(() => {
|
|
1225
|
+
void this.deleteExpired();
|
|
1226
|
+
}, CLEANUP_INTERVAL_MS);
|
|
1227
|
+
}
|
|
1228
|
+
async onModuleDestroy() {
|
|
1229
|
+
if (this.cleanupTimer !== null) {
|
|
1230
|
+
clearInterval(this.cleanupTimer);
|
|
1231
|
+
this.cleanupTimer = null;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
async get(key) {
|
|
1235
|
+
try {
|
|
1236
|
+
const rows = await this.db.select().from(cacheEntries).where(
|
|
1237
|
+
sql3`${cacheEntries.key} = ${key} AND (${cacheEntries.expiresAt} IS NULL OR ${cacheEntries.expiresAt} > now())`
|
|
1238
|
+
).limit(1);
|
|
1239
|
+
if (rows.length === 0) return null;
|
|
1240
|
+
return rows[0].value;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async set(key, value, ttlSeconds) {
|
|
1246
|
+
const effectiveTtl = ttlSeconds ?? this.defaultTtl ?? null;
|
|
1247
|
+
const expiresAt = effectiveTtl !== null ? new Date(Date.now() + effectiveTtl * 1e3) : null;
|
|
1248
|
+
const jsonValue = value;
|
|
1249
|
+
await this.db.insert(cacheEntries).values({ key, value: jsonValue, expiresAt }).onConflictDoUpdate({
|
|
1250
|
+
target: cacheEntries.key,
|
|
1251
|
+
set: { value: jsonValue, expiresAt }
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
async delete(key) {
|
|
1255
|
+
await this.db.delete(cacheEntries).where(eq3(cacheEntries.key, key));
|
|
1256
|
+
}
|
|
1257
|
+
async invalidateByPrefix(prefix) {
|
|
1258
|
+
const escaped = prefix.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1259
|
+
const result = await this.db.delete(cacheEntries).where(like(cacheEntries.key, `${escaped}%`)).returning({ key: cacheEntries.key });
|
|
1260
|
+
return result.length;
|
|
1261
|
+
}
|
|
1262
|
+
async has(key) {
|
|
1263
|
+
try {
|
|
1264
|
+
const result = await this.get(key);
|
|
1265
|
+
return result !== null;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async getOrSet(key, factory, ttlSeconds) {
|
|
1271
|
+
const cached = await this.get(key);
|
|
1272
|
+
if (cached !== null) return cached;
|
|
1273
|
+
const existing = this.inflight.get(key);
|
|
1274
|
+
if (existing !== void 0) return existing;
|
|
1275
|
+
const promise = factory().then(async (value) => {
|
|
1276
|
+
await this.set(key, value, ttlSeconds);
|
|
1277
|
+
return value;
|
|
1278
|
+
}).finally(() => {
|
|
1279
|
+
this.inflight.delete(key);
|
|
1280
|
+
});
|
|
1281
|
+
this.inflight.set(key, promise);
|
|
1282
|
+
return promise;
|
|
1283
|
+
}
|
|
1284
|
+
/** Remove all expired entries. Called by the cleanup timer. */
|
|
1285
|
+
async deleteExpired() {
|
|
1286
|
+
try {
|
|
1287
|
+
await this.db.delete(cacheEntries).where(
|
|
1288
|
+
or(
|
|
1289
|
+
gt(sql3`now()`, cacheEntries.expiresAt)
|
|
1290
|
+
)
|
|
1291
|
+
);
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
DrizzleCacheService = __decorateClass([
|
|
1297
|
+
Injectable8(),
|
|
1298
|
+
__decorateParam(0, Inject6(DRIZZLE)),
|
|
1299
|
+
__decorateParam(1, Optional()),
|
|
1300
|
+
__decorateParam(1, Inject6(CACHE_DEFAULT_TTL))
|
|
1301
|
+
], DrizzleCacheService);
|
|
1302
|
+
|
|
1303
|
+
// runtime/subsystems/cache/cache.memory-backend.ts
|
|
1304
|
+
import { Injectable as Injectable9, Inject as Inject7, Optional as Optional2 } from "@nestjs/common";
|
|
1305
|
+
var MemoryCacheService = class {
|
|
1306
|
+
constructor(defaultTtl = null) {
|
|
1307
|
+
this.defaultTtl = defaultTtl;
|
|
1308
|
+
}
|
|
1309
|
+
defaultTtl;
|
|
1310
|
+
store = /* @__PURE__ */ new Map();
|
|
1311
|
+
timers = /* @__PURE__ */ new Map();
|
|
1312
|
+
/** In-flight getOrSet promises — keyed by cache key to deduplicate stampedes. */
|
|
1313
|
+
inflight = /* @__PURE__ */ new Map();
|
|
1314
|
+
async get(key) {
|
|
1315
|
+
const record = this.store.get(key);
|
|
1316
|
+
if (!record) return null;
|
|
1317
|
+
if (record.expiresAt !== null && record.expiresAt <= Date.now()) {
|
|
1318
|
+
this.evict(key);
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
return record.value;
|
|
1322
|
+
}
|
|
1323
|
+
async set(key, value, ttlSeconds) {
|
|
1324
|
+
const effectiveTtl = ttlSeconds ?? this.defaultTtl ?? null;
|
|
1325
|
+
this.clearTimer(key);
|
|
1326
|
+
const expiresAt = effectiveTtl !== null ? Date.now() + effectiveTtl * 1e3 : null;
|
|
1327
|
+
this.store.set(key, { value, expiresAt });
|
|
1328
|
+
if (effectiveTtl !== null) {
|
|
1329
|
+
const timer = setTimeout(() => this.evict(key), effectiveTtl * 1e3);
|
|
1330
|
+
this.timers.set(key, timer);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async delete(key) {
|
|
1334
|
+
this.evict(key);
|
|
1335
|
+
}
|
|
1336
|
+
async invalidateByPrefix(prefix) {
|
|
1337
|
+
let count = 0;
|
|
1338
|
+
for (const key of this.store.keys()) {
|
|
1339
|
+
if (key.startsWith(prefix)) {
|
|
1340
|
+
this.evict(key);
|
|
1341
|
+
count++;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return count;
|
|
1345
|
+
}
|
|
1346
|
+
async has(key) {
|
|
1347
|
+
const value = await this.get(key);
|
|
1348
|
+
return value !== null;
|
|
1349
|
+
}
|
|
1350
|
+
async getOrSet(key, factory, ttlSeconds) {
|
|
1351
|
+
const cached = await this.get(key);
|
|
1352
|
+
if (cached !== null) return cached;
|
|
1353
|
+
const existing = this.inflight.get(key);
|
|
1354
|
+
if (existing !== void 0) return existing;
|
|
1355
|
+
const promise = factory().then(async (value) => {
|
|
1356
|
+
await this.set(key, value, ttlSeconds);
|
|
1357
|
+
return value;
|
|
1358
|
+
}).finally(() => {
|
|
1359
|
+
this.inflight.delete(key);
|
|
1360
|
+
});
|
|
1361
|
+
this.inflight.set(key, promise);
|
|
1362
|
+
return promise;
|
|
1363
|
+
}
|
|
1364
|
+
/** Remove a key from store and cancel its expiry timer. */
|
|
1365
|
+
evict(key) {
|
|
1366
|
+
this.store.delete(key);
|
|
1367
|
+
this.clearTimer(key);
|
|
1368
|
+
}
|
|
1369
|
+
clearTimer(key) {
|
|
1370
|
+
const timer = this.timers.get(key);
|
|
1371
|
+
if (timer !== void 0) {
|
|
1372
|
+
clearTimeout(timer);
|
|
1373
|
+
this.timers.delete(key);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
MemoryCacheService = __decorateClass([
|
|
1378
|
+
Injectable9(),
|
|
1379
|
+
__decorateParam(0, Optional2()),
|
|
1380
|
+
__decorateParam(0, Inject7(CACHE_DEFAULT_TTL))
|
|
1381
|
+
], MemoryCacheService);
|
|
1382
|
+
|
|
1383
|
+
// runtime/subsystems/cache/cache.module.ts
|
|
1384
|
+
var CacheModule = class {
|
|
1385
|
+
static forRootAsync(asyncOptions) {
|
|
1386
|
+
return {
|
|
1387
|
+
module: CacheModule,
|
|
1388
|
+
global: true,
|
|
1389
|
+
imports: asyncOptions.imports ?? [],
|
|
1390
|
+
providers: [
|
|
1391
|
+
{
|
|
1392
|
+
provide: "CACHE_MODULE_OPTIONS",
|
|
1393
|
+
useFactory: asyncOptions.useFactory,
|
|
1394
|
+
inject: asyncOptions.inject ?? []
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
provide: CACHE,
|
|
1398
|
+
useFactory: (options) => {
|
|
1399
|
+
if (options.backend === "drizzle") {
|
|
1400
|
+
return new DrizzleCacheService(
|
|
1401
|
+
null,
|
|
1402
|
+
options.defaultTtl ?? null
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
return new MemoryCacheService(options.defaultTtl ?? null);
|
|
1406
|
+
},
|
|
1407
|
+
inject: ["CACHE_MODULE_OPTIONS"]
|
|
1408
|
+
},
|
|
1409
|
+
{ provide: DrizzleCacheService, useExisting: CACHE },
|
|
1410
|
+
{ provide: MemoryCacheService, useExisting: CACHE }
|
|
1411
|
+
],
|
|
1412
|
+
exports: [CACHE]
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
static forRoot(options = { backend: "drizzle" }) {
|
|
1416
|
+
const ConcreteClass = options.backend === "drizzle" ? DrizzleCacheService : MemoryCacheService;
|
|
1417
|
+
const providers = options.defaultTtl !== void 0 ? [
|
|
1418
|
+
// Register the concrete class as the canonical instance
|
|
1419
|
+
ConcreteClass,
|
|
1420
|
+
{ provide: CACHE_DEFAULT_TTL, useValue: options.defaultTtl },
|
|
1421
|
+
// CACHE token points to the same instance — no duplicate
|
|
1422
|
+
{ provide: CACHE, useExisting: ConcreteClass }
|
|
1423
|
+
] : [
|
|
1424
|
+
ConcreteClass,
|
|
1425
|
+
{ provide: CACHE, useExisting: ConcreteClass }
|
|
1426
|
+
];
|
|
1427
|
+
return {
|
|
1428
|
+
module: CacheModule,
|
|
1429
|
+
global: true,
|
|
1430
|
+
providers,
|
|
1431
|
+
exports: [CACHE]
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
CacheModule = __decorateClass([
|
|
1436
|
+
Module3({})
|
|
1437
|
+
], CacheModule);
|
|
1438
|
+
|
|
1439
|
+
// runtime/subsystems/storage/storage.local-backend.ts
|
|
1440
|
+
import {
|
|
1441
|
+
createReadStream,
|
|
1442
|
+
existsSync,
|
|
1443
|
+
mkdirSync,
|
|
1444
|
+
readdirSync,
|
|
1445
|
+
readFileSync,
|
|
1446
|
+
unlinkSync,
|
|
1447
|
+
writeFileSync
|
|
1448
|
+
} from "fs";
|
|
1449
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
1450
|
+
import { Readable } from "stream";
|
|
1451
|
+
|
|
1452
|
+
// runtime/subsystems/storage/storage.utils.ts
|
|
1453
|
+
async function toBuffer(data) {
|
|
1454
|
+
if (Buffer.isBuffer(data)) {
|
|
1455
|
+
return data;
|
|
1456
|
+
}
|
|
1457
|
+
const reader = data.getReader();
|
|
1458
|
+
const chunks = [];
|
|
1459
|
+
while (true) {
|
|
1460
|
+
const { done, value } = await reader.read();
|
|
1461
|
+
if (done) break;
|
|
1462
|
+
if (value) chunks.push(value);
|
|
1463
|
+
}
|
|
1464
|
+
return Buffer.concat(chunks);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// runtime/subsystems/storage/storage.local-backend.ts
|
|
1468
|
+
var LocalStorageBackend = class {
|
|
1469
|
+
basePath;
|
|
1470
|
+
constructor(basePath = "./storage") {
|
|
1471
|
+
this.basePath = resolve(basePath);
|
|
1472
|
+
}
|
|
1473
|
+
async upload(key, data, contentType) {
|
|
1474
|
+
const filePath = this.resolvePath(key);
|
|
1475
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
1476
|
+
const buffer = await toBuffer(data);
|
|
1477
|
+
writeFileSync(filePath, buffer);
|
|
1478
|
+
return key;
|
|
1479
|
+
}
|
|
1480
|
+
async download(key) {
|
|
1481
|
+
const filePath = this.resolvePath(key);
|
|
1482
|
+
if (!existsSync(filePath)) {
|
|
1483
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1484
|
+
}
|
|
1485
|
+
return readFileSync(filePath);
|
|
1486
|
+
}
|
|
1487
|
+
async delete(key) {
|
|
1488
|
+
const filePath = this.resolvePath(key);
|
|
1489
|
+
if (!existsSync(filePath)) {
|
|
1490
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1491
|
+
}
|
|
1492
|
+
unlinkSync(filePath);
|
|
1493
|
+
}
|
|
1494
|
+
async getUrl(key, _expiresInSeconds) {
|
|
1495
|
+
const filePath = this.resolvePath(key);
|
|
1496
|
+
if (!existsSync(filePath)) {
|
|
1497
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1498
|
+
}
|
|
1499
|
+
return `file://${filePath}`;
|
|
1500
|
+
}
|
|
1501
|
+
async exists(key) {
|
|
1502
|
+
try {
|
|
1503
|
+
return existsSync(this.resolvePath(key));
|
|
1504
|
+
} catch {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async list(prefix) {
|
|
1509
|
+
const keys = this.listRecursive(this.basePath);
|
|
1510
|
+
if (prefix === void 0) return keys;
|
|
1511
|
+
return keys.filter((k) => k.startsWith(prefix));
|
|
1512
|
+
}
|
|
1513
|
+
async downloadStream(key) {
|
|
1514
|
+
const filePath = this.resolvePath(key);
|
|
1515
|
+
if (!existsSync(filePath)) {
|
|
1516
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1517
|
+
}
|
|
1518
|
+
const nodeStream = createReadStream(filePath);
|
|
1519
|
+
return Readable.toWeb(nodeStream);
|
|
1520
|
+
}
|
|
1521
|
+
resolvePath(key) {
|
|
1522
|
+
const resolved = resolve(this.basePath, key);
|
|
1523
|
+
if (!resolved.startsWith(this.basePath + sep)) {
|
|
1524
|
+
throw new Error(`Invalid storage key (path traversal attempt): ${key}`);
|
|
1525
|
+
}
|
|
1526
|
+
return resolved;
|
|
1527
|
+
}
|
|
1528
|
+
/** Recursively list all files under dir, returning keys relative to basePath. */
|
|
1529
|
+
listRecursive(dir) {
|
|
1530
|
+
if (!existsSync(dir)) return [];
|
|
1531
|
+
const keys = [];
|
|
1532
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1533
|
+
const full = join(dir, entry.name);
|
|
1534
|
+
if (entry.isDirectory()) {
|
|
1535
|
+
keys.push(...this.listRecursive(full));
|
|
1536
|
+
} else {
|
|
1537
|
+
keys.push(relative(this.basePath, full));
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return keys;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
// runtime/subsystems/storage/storage.memory-backend.ts
|
|
1545
|
+
var MemoryStorageBackend = class {
|
|
1546
|
+
store = /* @__PURE__ */ new Map();
|
|
1547
|
+
async upload(key, data, contentType) {
|
|
1548
|
+
const buffer = await toBuffer(data);
|
|
1549
|
+
this.store.set(key, { data: buffer, contentType });
|
|
1550
|
+
return key;
|
|
1551
|
+
}
|
|
1552
|
+
async download(key) {
|
|
1553
|
+
const entry = this.store.get(key);
|
|
1554
|
+
if (!entry) {
|
|
1555
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1556
|
+
}
|
|
1557
|
+
return entry.data;
|
|
1558
|
+
}
|
|
1559
|
+
async delete(key) {
|
|
1560
|
+
if (!this.store.has(key)) {
|
|
1561
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1562
|
+
}
|
|
1563
|
+
this.store.delete(key);
|
|
1564
|
+
}
|
|
1565
|
+
async getUrl(key, _expiresInSeconds) {
|
|
1566
|
+
if (!this.store.has(key)) {
|
|
1567
|
+
throw new Error(`Storage: file not found: ${key}`);
|
|
1568
|
+
}
|
|
1569
|
+
return `memory://${key}`;
|
|
1570
|
+
}
|
|
1571
|
+
async exists(key) {
|
|
1572
|
+
return this.store.has(key);
|
|
1573
|
+
}
|
|
1574
|
+
async list(prefix) {
|
|
1575
|
+
const keys = Array.from(this.store.keys());
|
|
1576
|
+
if (prefix === void 0) return keys;
|
|
1577
|
+
return keys.filter((k) => k.startsWith(prefix));
|
|
1578
|
+
}
|
|
1579
|
+
async downloadStream(key) {
|
|
1580
|
+
const buffer = await this.download(key);
|
|
1581
|
+
return new ReadableStream({
|
|
1582
|
+
start(controller) {
|
|
1583
|
+
controller.enqueue(new Uint8Array(buffer));
|
|
1584
|
+
controller.close();
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
/** Clear all stored files. Useful for test teardown. */
|
|
1589
|
+
clear() {
|
|
1590
|
+
this.store.clear();
|
|
1591
|
+
}
|
|
1592
|
+
/** Return number of stored files. Useful for test assertions. */
|
|
1593
|
+
size() {
|
|
1594
|
+
return this.store.size;
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
// runtime/subsystems/storage/storage.module.ts
|
|
1599
|
+
import { Module as Module4 } from "@nestjs/common";
|
|
1600
|
+
|
|
1601
|
+
// runtime/subsystems/storage/storage.tokens.ts
|
|
1602
|
+
var STORAGE = /* @__PURE__ */ Symbol("STORAGE");
|
|
1603
|
+
|
|
1604
|
+
// runtime/subsystems/storage/storage.module.ts
|
|
1605
|
+
var StorageModule = class {
|
|
1606
|
+
static forRoot(options = { backend: "local" }) {
|
|
1607
|
+
const provider = options.backend === "local" ? {
|
|
1608
|
+
provide: STORAGE,
|
|
1609
|
+
useFactory: () => new LocalStorageBackend(options.basePath ?? "./storage")
|
|
1610
|
+
} : {
|
|
1611
|
+
provide: STORAGE,
|
|
1612
|
+
useClass: MemoryStorageBackend
|
|
1613
|
+
};
|
|
1614
|
+
return {
|
|
1615
|
+
module: StorageModule,
|
|
1616
|
+
global: true,
|
|
1617
|
+
providers: [provider],
|
|
1618
|
+
exports: [STORAGE]
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
StorageModule = __decorateClass([
|
|
1623
|
+
Module4({})
|
|
1624
|
+
], StorageModule);
|
|
1625
|
+
export {
|
|
1626
|
+
CACHE,
|
|
1627
|
+
CacheModule,
|
|
1628
|
+
DrizzleCacheService,
|
|
1629
|
+
DrizzleEventBus,
|
|
1630
|
+
DrizzleJobQueue,
|
|
1631
|
+
EVENT_BUS,
|
|
1632
|
+
EventsModule,
|
|
1633
|
+
JOB_QUEUE,
|
|
1634
|
+
JobsModule,
|
|
1635
|
+
LocalStorageBackend,
|
|
1636
|
+
MemoryCacheService,
|
|
1637
|
+
MemoryEventBus,
|
|
1638
|
+
MemoryJobQueue,
|
|
1639
|
+
MemoryStorageBackend,
|
|
1640
|
+
STORAGE,
|
|
1641
|
+
StorageModule
|
|
1642
|
+
};
|
|
1643
|
+
//# sourceMappingURL=index.js.map
|