@pattern-stack/codegen 0.4.1 → 0.4.3
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 +6 -0
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -0
- package/dist/runtime/subsystems/bridge/bridge.module.js +38 -21
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +1 -0
- package/dist/runtime/subsystems/bridge/index.js +29 -12
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +31 -14
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -0
- package/dist/runtime/subsystems/jobs/index.js +27 -10
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +9 -4
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.js +6 -2
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +27 -10
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -4
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/src/cli/index.js +29 -2
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +2 -1
- package/runtime/analytics/index.ts +31 -0
- package/runtime/analytics/metrics.ts +85 -0
- package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
- package/runtime/analytics/packs/index.ts +5 -0
- package/runtime/analytics/packs/monetary-measures.ts +20 -0
- package/runtime/analytics/specs.ts +54 -0
- package/runtime/analytics/types.ts +105 -0
- package/runtime/base-classes/activity-entity-repository.ts +50 -0
- package/runtime/base-classes/activity-entity-service.ts +48 -0
- package/runtime/base-classes/base-read-use-cases.ts +88 -0
- package/runtime/base-classes/base-repository.ts +289 -0
- package/runtime/base-classes/base-service.ts +183 -0
- package/runtime/base-classes/index.ts +38 -0
- package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
- package/runtime/base-classes/knowledge-entity-service.ts +14 -0
- package/runtime/base-classes/lifecycle-events.ts +152 -0
- package/runtime/base-classes/metadata-entity-repository.ts +80 -0
- package/runtime/base-classes/metadata-entity-service.ts +48 -0
- package/runtime/base-classes/synced-entity-repository.ts +57 -0
- package/runtime/base-classes/synced-entity-service.ts +50 -0
- package/runtime/base-classes/with-analytics.ts +22 -0
- package/runtime/constants/tokens.ts +29 -0
- package/runtime/eav-helpers.ts +74 -0
- package/runtime/pipes/zod-validation.pipe.ts +64 -0
- package/runtime/shared/openapi/error-response.dto.ts +24 -0
- package/runtime/shared/openapi/errors.ts +39 -0
- package/runtime/shared/openapi/index.ts +20 -0
- package/runtime/shared/openapi/registry.tokens.ts +13 -0
- package/runtime/shared/openapi/registry.ts +151 -0
- package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
- package/runtime/subsystems/analytics/analytics.module.ts +64 -0
- package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
- package/runtime/subsystems/analytics/cube-backend.ts +75 -0
- package/runtime/subsystems/analytics/index.ts +15 -0
- package/runtime/subsystems/analytics/noop-backend.ts +27 -0
- package/runtime/subsystems/auth/auth.module.ts +91 -0
- package/runtime/subsystems/auth/auth.tokens.ts +27 -0
- package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
- package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
- package/runtime/subsystems/auth/index.ts +77 -0
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
- package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
- package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
- package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
- package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
- package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
- package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
- package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
- package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
- package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
- package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
- package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
- package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
- package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
- package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
- package/runtime/subsystems/bridge/bridge.module.ts +160 -0
- package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
- package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
- package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
- package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
- package/runtime/subsystems/bridge/generated/registry.ts +6 -0
- package/runtime/subsystems/bridge/index.ts +84 -0
- package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
- package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
- package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
- package/runtime/subsystems/cache/cache.module.ts +115 -0
- package/runtime/subsystems/cache/cache.protocol.ts +45 -0
- package/runtime/subsystems/cache/cache.schema.ts +27 -0
- package/runtime/subsystems/cache/cache.tokens.ts +17 -0
- package/runtime/subsystems/cache/index.ts +22 -0
- package/runtime/subsystems/events/domain-events.schema.ts +77 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
- package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
- package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
- package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
- package/runtime/subsystems/events/events-errors.ts +30 -0
- package/runtime/subsystems/events/events.module.ts +230 -0
- package/runtime/subsystems/events/events.tokens.ts +62 -0
- package/runtime/subsystems/events/generated/bus.ts +103 -0
- package/runtime/subsystems/events/generated/index.ts +7 -0
- package/runtime/subsystems/events/generated/registry.ts +84 -0
- package/runtime/subsystems/events/generated/schemas.ts +59 -0
- package/runtime/subsystems/events/generated/types.ts +94 -0
- package/runtime/subsystems/events/index.ts +21 -0
- package/runtime/subsystems/index.ts +63 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
- package/runtime/subsystems/jobs/index.ts +120 -0
- package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
- package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
- package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
- package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +860 -0
- package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
- package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
- package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
- package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +312 -0
- package/runtime/subsystems/jobs/job-worker.ts +624 -0
- package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
- package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
- package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
- package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
- package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
- package/runtime/subsystems/storage/index.ts +18 -0
- package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
- package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
- package/runtime/subsystems/storage/storage.module.ts +60 -0
- package/runtime/subsystems/storage/storage.protocol.ts +78 -0
- package/runtime/subsystems/storage/storage.tokens.ts +9 -0
- package/runtime/subsystems/storage/storage.utils.ts +20 -0
- package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
- package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
- package/runtime/subsystems/sync/index.ts +98 -0
- package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
- package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
- package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
- package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
- package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
- package/runtime/subsystems/sync/sync-errors.ts +54 -0
- package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
- package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
- package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
- package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
- package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
- package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
- package/runtime/subsystems/sync/sync.module.ts +156 -0
- package/runtime/subsystems/sync/sync.tokens.ts +57 -0
- package/runtime/types/drizzle.ts +23 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection tokens for the bridge subsystem (ADR-023 Phase 2, BRIDGE-2).
|
|
3
|
+
*
|
|
4
|
+
* String constants (not Symbols) so they match by value across import
|
|
5
|
+
* boundaries — same convention as `EVENT_BUS` / `EVENTS_MULTI_TENANT` in the
|
|
6
|
+
* events subsystem (per EVT-6 §Implementation Notes). The jobs subsystem
|
|
7
|
+
* uses Symbols for its analogous tokens; we keep the bridge file internally
|
|
8
|
+
* consistent with the events convention because the bridge is conceptually
|
|
9
|
+
* downstream of (and imports from) the events module.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Token for the `IJobBridge` repo backend (memory in BRIDGE-3, Drizzle in
|
|
14
|
+
* BRIDGE-4). Consumed by `BridgeDeliveryHandler` (BRIDGE-5), the outbox
|
|
15
|
+
* drain (BRIDGE-4 modification), and `EventFlowService` (BRIDGE-7).
|
|
16
|
+
*/
|
|
17
|
+
export const BRIDGE_DELIVERY_REPO = 'BRIDGE_DELIVERY_REPO' as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Token for the `IEventFlow` facade implementation (BRIDGE-7). Use cases
|
|
21
|
+
* inject this in preference to `EVENT_BUS` / `TYPED_EVENT_BUS` — calling
|
|
22
|
+
* `eventFlow.publish(...)` / `eventFlow.publishAndStart(...)` is the
|
|
23
|
+
* sanctioned authoring surface (ADR-023 §Decision 7).
|
|
24
|
+
*/
|
|
25
|
+
export const EVENT_FLOW = 'EVENT_FLOW' as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Token for the resolved multi-tenancy flag, provided by
|
|
29
|
+
* `BridgeModule.forRoot({ multiTenant })` in BRIDGE-8. Consumed by
|
|
30
|
+
* `EventFlowService.publishAndStart` (entry), `BridgeDeliveryHandler.handle`
|
|
31
|
+
* (entry), and `DrizzleBridgeDeliveryRepo.insertDelivery` (pre-write) — the
|
|
32
|
+
* three enforcement sites called out in ADR-023 §Multi-tenancy.
|
|
33
|
+
*/
|
|
34
|
+
export const BRIDGE_MULTI_TENANT = 'BRIDGE_MULTI_TENANT' as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Token for the resolved `BridgeModuleOptions` object. Provided by
|
|
38
|
+
* `BridgeModule.forRoot(...)` / `forRootAsync(...)` in BRIDGE-8.
|
|
39
|
+
* Mirrors `EVENTS_MODULE_OPTIONS` and `JOBS_DOMAIN_OPTIONS` shape — backends
|
|
40
|
+
* inject this when they need to observe additional module configuration
|
|
41
|
+
* (e.g. pool overrides) without each adding a dedicated token.
|
|
42
|
+
*/
|
|
43
|
+
export const BRIDGE_MODULE_OPTIONS = 'BRIDGE_MODULE_OPTIONS' as const;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Token for the codegen-emitted `bridgeRegistry` — the
|
|
47
|
+
* `Record<EventTypeName, BridgeTriggerEntry[]>` map that drives
|
|
48
|
+
* outbox-drain trigger lookup (BRIDGE-4) and `EventFlowService` Case B
|
|
49
|
+
* dedup (BRIDGE-7). Provider registration lands in BRIDGE-8; the token is
|
|
50
|
+
* declared here so generated code (BRIDGE-6) can import it without
|
|
51
|
+
* depending on the still-being-formalised module.
|
|
52
|
+
*/
|
|
53
|
+
export const BRIDGE_REGISTRY = 'BRIDGE_REGISTRY' as const;
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Token for the `IBridgeOutboxDrainHook` implementation (BRIDGE-4).
|
|
58
|
+
* Injected `@Optional()` into `DrizzleEventBus` — when the bridge
|
|
59
|
+
* subsystem is not installed the token is undefined and the events
|
|
60
|
+
* outbox drain skips the bridge block entirely (preserves EVT-4
|
|
61
|
+
* baseline behaviour).
|
|
62
|
+
*
|
|
63
|
+
* Resolution order: `BridgeModule.forRoot()` provides this token in
|
|
64
|
+
* BRIDGE-8 alongside the rest of the bridge subsystem. `EventsModule`
|
|
65
|
+
* itself never provides it; the events subsystem stays unaware of the
|
|
66
|
+
* bridge unless the consumer wires it.
|
|
67
|
+
*/
|
|
68
|
+
export const BRIDGE_OUTBOX_DRAIN_HOOK = 'BRIDGE_OUTBOX_DRAIN_HOOK' as const;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventFlowService — `IEventFlow` facade implementation (BRIDGE-7,
|
|
3
|
+
* ADR-023 §Decision 7 + §`publishAndStart` + existing `triggers:` collision).
|
|
4
|
+
*
|
|
5
|
+
* Two verbs:
|
|
6
|
+
*
|
|
7
|
+
* - `publish(event, tx?)` — thin delegate to `IEventBus.publish(event, tx)`.
|
|
8
|
+
* Subscribers + bridge triggers fire as normal. Caller owns transaction.
|
|
9
|
+
*
|
|
10
|
+
* - `publishAndStart(event, jobType, input, opts?)` — the load-bearing
|
|
11
|
+
* verb. Opens a transaction and performs THREE writes inside it:
|
|
12
|
+
* 1. Outbox insert via `eventBus.publish(event, tx)`.
|
|
13
|
+
* 2. Eager `orchestrator.start(jobType, input, opts, tx)`.
|
|
14
|
+
* 3. **Case B only** — for every `bridgeRegistry[event.type]` entry
|
|
15
|
+
* whose `jobType` matches the argument, pre-write a
|
|
16
|
+
* `bridge_delivery(status='delivered', wrapper_run_id=null,
|
|
17
|
+
* user_run_id=<eagerRunId>)` row via `bridgeRepo.insertDelivery(
|
|
18
|
+
* row, tx)`. The `UNIQUE (event_id, trigger_id)` constraint then
|
|
19
|
+
* dedups the drain's later attempt for that trigger; sibling
|
|
20
|
+
* triggers (different trigger_id) still spawn normally.
|
|
21
|
+
*
|
|
22
|
+
* Returns `{ runId }` from the eager start. All three writes share
|
|
23
|
+
* one tx — a crash anywhere rolls back all of them; the drain
|
|
24
|
+
* re-claims the event on the next cycle and the bridge UNIQUE makes
|
|
25
|
+
* the retry idempotent.
|
|
26
|
+
*
|
|
27
|
+
* **Pre-write ALL matching triggerIds** (lead decision 2026-04-22): the
|
|
28
|
+
* facade uses `filter()` not `find()`. If a project has two triggers in
|
|
29
|
+
* the registry for the same `(event, jobType)` pair (rare; codegen-time
|
|
30
|
+
* `DuplicateTriggerError` from BRIDGE-7's BRIDGE-6 follow-up patch
|
|
31
|
+
* prevents new occurrences), each gets its own pre-write — otherwise
|
|
32
|
+
* the un-pre-written sibling would spawn a wrapper that re-runs the user
|
|
33
|
+
* job, producing a double-spawn.
|
|
34
|
+
*
|
|
35
|
+
* **Multi-tenancy gate** at `publishAndStart` entry: when
|
|
36
|
+
* `BRIDGE_MULTI_TENANT=true` and `opts?.tenantId === undefined`, throw
|
|
37
|
+
* `MissingTenantIdError('EventFlowService.publishAndStart')`. Site (a)
|
|
38
|
+
* of the three ADR-023 §Multi-tenancy enforcement sites (BRIDGE-5
|
|
39
|
+
* handler is (b); BRIDGE-4 drizzle repo is (c)).
|
|
40
|
+
*/
|
|
41
|
+
import { Inject, Injectable, Optional } from '@nestjs/common';
|
|
42
|
+
|
|
43
|
+
import { DRIZZLE } from '../../constants/tokens';
|
|
44
|
+
import type { DrizzleClient } from '../../types/drizzle';
|
|
45
|
+
|
|
46
|
+
import { EVENT_BUS } from '../events/events.tokens';
|
|
47
|
+
import type {
|
|
48
|
+
DomainEvent,
|
|
49
|
+
DrizzleTransaction,
|
|
50
|
+
IEventBus,
|
|
51
|
+
} from '../events/event-bus.protocol';
|
|
52
|
+
import type {
|
|
53
|
+
EventOfType,
|
|
54
|
+
EventTypeName,
|
|
55
|
+
} from '../events/generated/types';
|
|
56
|
+
|
|
57
|
+
import { JOB_ORCHESTRATOR } from '../jobs/jobs-domain.tokens';
|
|
58
|
+
import type { IJobOrchestrator } from '../jobs/job-orchestrator.protocol';
|
|
59
|
+
|
|
60
|
+
import {
|
|
61
|
+
BRIDGE_DELIVERY_REPO,
|
|
62
|
+
BRIDGE_MULTI_TENANT,
|
|
63
|
+
BRIDGE_REGISTRY,
|
|
64
|
+
} from './bridge.tokens';
|
|
65
|
+
import type {
|
|
66
|
+
BridgeRegistry,
|
|
67
|
+
BridgeTriggerEntry,
|
|
68
|
+
IEventFlow,
|
|
69
|
+
IJobBridge,
|
|
70
|
+
PublishAndStartOptions,
|
|
71
|
+
PublishAndStartResult,
|
|
72
|
+
} from './bridge.protocol';
|
|
73
|
+
import { assertTenantId } from './assert-tenant-id';
|
|
74
|
+
|
|
75
|
+
@Injectable()
|
|
76
|
+
export class EventFlowService implements IEventFlow {
|
|
77
|
+
constructor(
|
|
78
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
79
|
+
@Inject(EVENT_BUS) private readonly eventBus: IEventBus,
|
|
80
|
+
@Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,
|
|
81
|
+
@Inject(BRIDGE_DELIVERY_REPO) private readonly bridgeRepo: IJobBridge,
|
|
82
|
+
@Optional()
|
|
83
|
+
@Inject(BRIDGE_REGISTRY)
|
|
84
|
+
private readonly registry: BridgeRegistry = {},
|
|
85
|
+
@Optional()
|
|
86
|
+
@Inject(BRIDGE_MULTI_TENANT)
|
|
87
|
+
private readonly multiTenant: boolean = false,
|
|
88
|
+
) {}
|
|
89
|
+
|
|
90
|
+
async publish<T extends EventTypeName>(
|
|
91
|
+
event: EventOfType<T>,
|
|
92
|
+
tx?: DrizzleTransaction,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
// Thin delegate. Subscribers + bridge triggers fire as normal via the
|
|
95
|
+
// outbox drain (BRIDGE-4) and `IEventBus.subscribe` (Tier 1).
|
|
96
|
+
await this.eventBus.publish(event as DomainEvent, tx);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async publishAndStart<T extends EventTypeName>(
|
|
100
|
+
event: EventOfType<T>,
|
|
101
|
+
jobType: string,
|
|
102
|
+
input: unknown,
|
|
103
|
+
opts: PublishAndStartOptions = {},
|
|
104
|
+
): Promise<PublishAndStartResult> {
|
|
105
|
+
// Multi-tenancy gate — throw before any DB write so failures surface
|
|
106
|
+
// at the call site, not from inside an aborted tx. Site (a) of the
|
|
107
|
+
// three ADR-023 §Multi-tenancy enforcement sites; shared helper from
|
|
108
|
+
// BRIDGE-8 keeps all three sites in lock-step.
|
|
109
|
+
assertTenantId(
|
|
110
|
+
'EventFlowService.publishAndStart',
|
|
111
|
+
this.multiTenant,
|
|
112
|
+
opts.tenantId,
|
|
113
|
+
);
|
|
114
|
+
// Resolve null → null (cross-tenant work) once, so the same value
|
|
115
|
+
// flows to both the eager start AND the bridge_delivery row.
|
|
116
|
+
const tenantId: string | null = opts.tenantId ?? null;
|
|
117
|
+
|
|
118
|
+
// Identify Case B — every registry entry whose jobType matches.
|
|
119
|
+
// Lead decision 2026-04-22: pre-write ALL matches (filter, not find)
|
|
120
|
+
// so duplicate triggers in the registry don't double-spawn.
|
|
121
|
+
const matchingTriggers = this.matchingTriggers(event.type as EventTypeName, jobType);
|
|
122
|
+
|
|
123
|
+
return this.db.transaction(async (tx) => {
|
|
124
|
+
// 1. Outbox insert.
|
|
125
|
+
await this.eventBus.publish(event as DomainEvent, tx);
|
|
126
|
+
|
|
127
|
+
// 2. Eager start. Threads tx through (BRIDGE-7 protocol extension
|
|
128
|
+
// on IJobOrchestrator.start, JOB-3 backend uses `tx ?? this.db`).
|
|
129
|
+
const run = await this.orchestrator.start(
|
|
130
|
+
jobType,
|
|
131
|
+
input,
|
|
132
|
+
{
|
|
133
|
+
parentRunId: opts.parentRunId,
|
|
134
|
+
tenantId,
|
|
135
|
+
triggerSource: 'event',
|
|
136
|
+
triggerRef: event.id,
|
|
137
|
+
},
|
|
138
|
+
tx,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// 3. Case B pre-writes — one per matching trigger.
|
|
142
|
+
const now = new Date();
|
|
143
|
+
for (const trigger of matchingTriggers) {
|
|
144
|
+
await this.bridgeRepo.insertDelivery(
|
|
145
|
+
{
|
|
146
|
+
eventId: event.id,
|
|
147
|
+
triggerId: trigger.triggerId,
|
|
148
|
+
wrapperRunId: null, // facade never writes a wrapper
|
|
149
|
+
userRunId: run.id,
|
|
150
|
+
status: 'delivered',
|
|
151
|
+
tenantId,
|
|
152
|
+
attemptedAt: now,
|
|
153
|
+
deliveredAt: now,
|
|
154
|
+
},
|
|
155
|
+
tx,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { runId: run.id };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Linear scan of the per-event-type trigger list for entries whose
|
|
165
|
+
* `jobType` matches. Typical N is 1–5; the table is not big enough to
|
|
166
|
+
* warrant a secondary index. Returns an empty array for Case A.
|
|
167
|
+
*/
|
|
168
|
+
private matchingTriggers(
|
|
169
|
+
eventType: EventTypeName,
|
|
170
|
+
jobType: string,
|
|
171
|
+
): BridgeTriggerEntry[] {
|
|
172
|
+
const triggers = this.registry[eventType] ?? [];
|
|
173
|
+
return triggers.filter((t) => t.jobType === jobType) as BridgeTriggerEntry[];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge subsystem — public API (ADR-023 Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* The bridge is the formalized seam between events (ADR-024) and jobs
|
|
5
|
+
* (ADR-022). It is owned by neither subsystem; it imports from both.
|
|
6
|
+
*
|
|
7
|
+
* BRIDGE-1 added the schema. BRIDGE-2 adds the protocols, DI tokens, and
|
|
8
|
+
* the typed `MissingTenantIdError`. Backends (memory in BRIDGE-3, Drizzle
|
|
9
|
+
* in BRIDGE-4), the framework `BridgeDeliveryHandler` (BRIDGE-5), the
|
|
10
|
+
* codegen-emitted `bridgeRegistry` (BRIDGE-6), the `EventFlowService`
|
|
11
|
+
* facade (BRIDGE-7), the `BridgeModule.forRoot()` wiring (BRIDGE-8), and
|
|
12
|
+
* the CLI / scaffold / docs (BRIDGE-9) follow.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Schema (BRIDGE-1)
|
|
16
|
+
export {
|
|
17
|
+
bridgeDelivery,
|
|
18
|
+
bridgeDeliveryStatusEnum,
|
|
19
|
+
} from './bridge-delivery.schema';
|
|
20
|
+
export type { BridgeDeliveryRecord } from './bridge-delivery.schema';
|
|
21
|
+
|
|
22
|
+
// Protocols (BRIDGE-2 + BRIDGE-5 registry + BRIDGE-4 drain hook)
|
|
23
|
+
export type {
|
|
24
|
+
IJobBridge,
|
|
25
|
+
IEventFlow,
|
|
26
|
+
IBridgeOutboxDrainHook,
|
|
27
|
+
BridgeDeliveryInsert,
|
|
28
|
+
BridgeOutboxDrainResult,
|
|
29
|
+
BridgeRegistry,
|
|
30
|
+
BridgeTriggerEntry,
|
|
31
|
+
PublishAndStartOptions,
|
|
32
|
+
PublishAndStartResult,
|
|
33
|
+
} from './bridge.protocol';
|
|
34
|
+
|
|
35
|
+
// DI tokens (BRIDGE-2 + BRIDGE-4 drain hook)
|
|
36
|
+
export {
|
|
37
|
+
BRIDGE_DELIVERY_REPO,
|
|
38
|
+
EVENT_FLOW,
|
|
39
|
+
BRIDGE_MULTI_TENANT,
|
|
40
|
+
BRIDGE_MODULE_OPTIONS,
|
|
41
|
+
BRIDGE_REGISTRY,
|
|
42
|
+
BRIDGE_OUTBOX_DRAIN_HOOK,
|
|
43
|
+
} from './bridge.tokens';
|
|
44
|
+
|
|
45
|
+
// Errors (BRIDGE-2 + BRIDGE-3 + BRIDGE-8)
|
|
46
|
+
export {
|
|
47
|
+
MissingTenantIdError,
|
|
48
|
+
UniqueConstraintError,
|
|
49
|
+
BridgeReservedPoolsNotPolledError,
|
|
50
|
+
} from './bridge-errors';
|
|
51
|
+
|
|
52
|
+
// Multi-tenancy helper (BRIDGE-8)
|
|
53
|
+
export { assertTenantId } from './assert-tenant-id';
|
|
54
|
+
|
|
55
|
+
// Reserved pools constant (BRIDGE-8) — consumers spread into
|
|
56
|
+
// `JobWorkerModule.forRoot({ pools })`.
|
|
57
|
+
export {
|
|
58
|
+
BRIDGE_RESERVED_POOLS,
|
|
59
|
+
type BridgeReservedPool,
|
|
60
|
+
} from './reserved-pools';
|
|
61
|
+
|
|
62
|
+
// Memory backend (BRIDGE-3)
|
|
63
|
+
export { MemoryBridgeDeliveryRepo } from './bridge-delivery.memory-backend';
|
|
64
|
+
|
|
65
|
+
// Framework handler (BRIDGE-5)
|
|
66
|
+
export {
|
|
67
|
+
BridgeDeliveryHandler,
|
|
68
|
+
BRIDGE_DELIVERY_JOB_TYPE,
|
|
69
|
+
BridgeDeliveryJobType,
|
|
70
|
+
type BridgeDeliveryInput,
|
|
71
|
+
} from './bridge-delivery-handler';
|
|
72
|
+
|
|
73
|
+
// Drizzle backend + outbox-drain hook (BRIDGE-4)
|
|
74
|
+
export { DrizzleBridgeDeliveryRepo } from './bridge-delivery.drizzle-backend';
|
|
75
|
+
export { BridgeOutboxDrainHook } from './bridge-outbox-drain-hook';
|
|
76
|
+
|
|
77
|
+
// EventFlow facade (BRIDGE-7)
|
|
78
|
+
export { EventFlowService } from './event-flow.service';
|
|
79
|
+
|
|
80
|
+
// Module wiring (BRIDGE-8)
|
|
81
|
+
export {
|
|
82
|
+
BridgeModule,
|
|
83
|
+
type BridgeModuleOptions,
|
|
84
|
+
} from './bridge.module';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BRIDGE_RESERVED_POOLS` — the three reserved bridge pools that workers
|
|
3
|
+
* must claim from for bridge fanout to function (BRIDGE-8, ADR-022 +
|
|
4
|
+
* ADR-023).
|
|
5
|
+
*
|
|
6
|
+
* Consumers spread this into their `JobWorkerModule.forRoot({ pools })`
|
|
7
|
+
* configuration to ensure bridge wrappers are picked up:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { BRIDGE_RESERVED_POOLS } from '@/runtime/subsystems/bridge';
|
|
11
|
+
*
|
|
12
|
+
* JobWorkerModule.forRoot({
|
|
13
|
+
* mode: 'embedded',
|
|
14
|
+
* pools: ['interactive', 'batch', ...BRIDGE_RESERVED_POOLS],
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Cross-link: `BridgeModule.onModuleInit` (BRIDGE-8) compares this list
|
|
19
|
+
* against the worker module's active pools and throws
|
|
20
|
+
* `BridgeReservedPoolsNotPolledError` when any are missing — this turns
|
|
21
|
+
* the silent footgun ("wrappers sit pending forever") into a fail-fast
|
|
22
|
+
* at boot.
|
|
23
|
+
*
|
|
24
|
+
* Lives in its own file (re-exported from the barrel) to keep the
|
|
25
|
+
* `BridgeModule` import graph acyclic — `bridge.module.ts` imports from
|
|
26
|
+
* here, and the barrel re-exports both. Consumers only ever import from
|
|
27
|
+
* the barrel.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const BRIDGE_RESERVED_POOLS = [
|
|
31
|
+
'events_inbound',
|
|
32
|
+
'events_change',
|
|
33
|
+
'events_outbound',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export type BridgeReservedPool = (typeof BRIDGE_RESERVED_POOLS)[number];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrizzleCacheService — Postgres-backed ICacheService via Drizzle ORM.
|
|
3
|
+
*
|
|
4
|
+
* Storage: `cache_entries` table with key (text pk), value (jsonb), expiresAt (timestamp).
|
|
5
|
+
* TTL enforcement: reads filter by `expiresAt > now() OR expiresAt IS NULL`.
|
|
6
|
+
* Prefix invalidation: `DELETE WHERE key LIKE 'escaped_prefix%'`.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* - OnModuleInit: starts periodic cleanup of expired entries.
|
|
10
|
+
* Uses the Jobs subsystem if available (optional injection); falls back to setInterval.
|
|
11
|
+
* - OnModuleDestroy: clears the setInterval timer if used.
|
|
12
|
+
*
|
|
13
|
+
* Error behavior per ADR-008:
|
|
14
|
+
* - get() / has() return null/false on any error (never throw for reads).
|
|
15
|
+
* - set() / delete() / invalidateByPrefix() throw on failure.
|
|
16
|
+
*/
|
|
17
|
+
import { Injectable, Inject, Optional, type OnModuleInit, type OnModuleDestroy } from '@nestjs/common';
|
|
18
|
+
import { gt, or, like, sql, eq } from 'drizzle-orm';
|
|
19
|
+
import type { DrizzleClient } from '../../types/drizzle';
|
|
20
|
+
import type { ICacheService } from './cache.protocol';
|
|
21
|
+
import { cacheEntries } from './cache.schema';
|
|
22
|
+
import { DRIZZLE } from '../../constants/tokens';
|
|
23
|
+
import { CACHE_DEFAULT_TTL } from './cache.tokens';
|
|
24
|
+
|
|
25
|
+
// Re-export for backward compatibility
|
|
26
|
+
export { CACHE_DEFAULT_TTL } from './cache.tokens';
|
|
27
|
+
|
|
28
|
+
/** Cleanup interval in milliseconds when jobs subsystem is unavailable. */
|
|
29
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
30
|
+
|
|
31
|
+
@Injectable()
|
|
32
|
+
export class DrizzleCacheService implements ICacheService, OnModuleInit, OnModuleDestroy {
|
|
33
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
34
|
+
/** In-flight getOrSet promises — keyed by cache key to deduplicate stampedes. */
|
|
35
|
+
private readonly inflight = new Map<string, Promise<unknown>>();
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
39
|
+
@Optional() @Inject(CACHE_DEFAULT_TTL) private readonly defaultTtl: number | null = null,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
async onModuleInit(): Promise<void> {
|
|
43
|
+
this.cleanupTimer = setInterval(() => {
|
|
44
|
+
void this.deleteExpired();
|
|
45
|
+
}, CLEANUP_INTERVAL_MS);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async onModuleDestroy(): Promise<void> {
|
|
49
|
+
if (this.cleanupTimer !== null) {
|
|
50
|
+
clearInterval(this.cleanupTimer);
|
|
51
|
+
this.cleanupTimer = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
56
|
+
try {
|
|
57
|
+
const rows = await this.db
|
|
58
|
+
.select()
|
|
59
|
+
.from(cacheEntries)
|
|
60
|
+
.where(
|
|
61
|
+
sql`${cacheEntries.key} = ${key} AND (${cacheEntries.expiresAt} IS NULL OR ${cacheEntries.expiresAt} > now())`,
|
|
62
|
+
)
|
|
63
|
+
.limit(1);
|
|
64
|
+
|
|
65
|
+
if (rows.length === 0) return null;
|
|
66
|
+
return rows[0]!.value as T;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
|
73
|
+
const effectiveTtl = ttlSeconds ?? this.defaultTtl ?? null;
|
|
74
|
+
const expiresAt =
|
|
75
|
+
effectiveTtl !== null
|
|
76
|
+
? new Date(Date.now() + effectiveTtl * 1000)
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
const jsonValue = value as Parameters<typeof cacheEntries.value.mapFromDriverValue>[0];
|
|
80
|
+
await this.db
|
|
81
|
+
.insert(cacheEntries)
|
|
82
|
+
.values({ key, value: jsonValue, expiresAt })
|
|
83
|
+
.onConflictDoUpdate({
|
|
84
|
+
target: cacheEntries.key,
|
|
85
|
+
set: { value: jsonValue, expiresAt },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete(key: string): Promise<void> {
|
|
90
|
+
await this.db.delete(cacheEntries).where(eq(cacheEntries.key, key));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async invalidateByPrefix(prefix: string): Promise<number> {
|
|
94
|
+
// Escape LIKE wildcards to prevent prefix characters from matching unintended entries
|
|
95
|
+
const escaped = prefix.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
96
|
+
const result = await this.db
|
|
97
|
+
.delete(cacheEntries)
|
|
98
|
+
.where(like(cacheEntries.key, `${escaped}%`))
|
|
99
|
+
.returning({ key: cacheEntries.key });
|
|
100
|
+
return result.length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async has(key: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
const result = await this.get(key);
|
|
106
|
+
return result !== null;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getOrSet<T = unknown>(
|
|
113
|
+
key: string,
|
|
114
|
+
factory: () => Promise<T>,
|
|
115
|
+
ttlSeconds?: number,
|
|
116
|
+
): Promise<T> {
|
|
117
|
+
// Fast path: cache hit
|
|
118
|
+
const cached = await this.get<T>(key);
|
|
119
|
+
if (cached !== null) return cached;
|
|
120
|
+
|
|
121
|
+
// Stampede protection: if another call is already computing this key, reuse its promise
|
|
122
|
+
const existing = this.inflight.get(key) as Promise<T> | undefined;
|
|
123
|
+
if (existing !== undefined) return existing;
|
|
124
|
+
|
|
125
|
+
const promise = factory().then(async (value) => {
|
|
126
|
+
await this.set(key, value, ttlSeconds);
|
|
127
|
+
return value;
|
|
128
|
+
}).finally(() => {
|
|
129
|
+
this.inflight.delete(key);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.inflight.set(key, promise as Promise<unknown>);
|
|
133
|
+
return promise;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Remove all expired entries. Called by the cleanup timer. */
|
|
137
|
+
private async deleteExpired(): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
await this.db
|
|
140
|
+
.delete(cacheEntries)
|
|
141
|
+
.where(
|
|
142
|
+
or(
|
|
143
|
+
gt(sql`now()`, cacheEntries.expiresAt),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
} catch {
|
|
147
|
+
// Cleanup failures are non-fatal — stale rows are filtered at read time
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryCacheService — Map-backed ICacheService for tests and development.
|
|
3
|
+
*
|
|
4
|
+
* TTL is enforced via setTimeout — expired entries are deleted from the Map
|
|
5
|
+
* when the timer fires. get() / has() also check the expiry time defensively
|
|
6
|
+
* in case the timer fires late.
|
|
7
|
+
*
|
|
8
|
+
* No lifecycle hooks required — all state is in-process.
|
|
9
|
+
*
|
|
10
|
+
* Error behavior:
|
|
11
|
+
* - get() / has() never throw; they return null/false.
|
|
12
|
+
* - set() / delete() / invalidateByPrefix() throw on failure (consistent with protocol).
|
|
13
|
+
*/
|
|
14
|
+
import { Injectable, Inject, Optional } from '@nestjs/common';
|
|
15
|
+
import type { ICacheService } from './cache.protocol';
|
|
16
|
+
import { CACHE_DEFAULT_TTL } from './cache.tokens';
|
|
17
|
+
|
|
18
|
+
interface CacheRecord {
|
|
19
|
+
value: unknown;
|
|
20
|
+
expiresAt: number | null; // epoch ms, or null for no expiry
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Injectable()
|
|
24
|
+
export class MemoryCacheService implements ICacheService {
|
|
25
|
+
private readonly store = new Map<string, CacheRecord>();
|
|
26
|
+
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
27
|
+
/** In-flight getOrSet promises — keyed by cache key to deduplicate stampedes. */
|
|
28
|
+
private readonly inflight = new Map<string, Promise<unknown>>();
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
@Optional() @Inject(CACHE_DEFAULT_TTL) private readonly defaultTtl: number | null = null,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
35
|
+
const record = this.store.get(key);
|
|
36
|
+
if (!record) return null;
|
|
37
|
+
if (record.expiresAt !== null && record.expiresAt <= Date.now()) {
|
|
38
|
+
this.evict(key);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return record.value as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
|
45
|
+
const effectiveTtl = ttlSeconds ?? this.defaultTtl ?? null;
|
|
46
|
+
|
|
47
|
+
// Clear any existing timer for this key
|
|
48
|
+
this.clearTimer(key);
|
|
49
|
+
|
|
50
|
+
const expiresAt = effectiveTtl !== null ? Date.now() + effectiveTtl * 1000 : null;
|
|
51
|
+
this.store.set(key, { value, expiresAt });
|
|
52
|
+
|
|
53
|
+
if (effectiveTtl !== null) {
|
|
54
|
+
const timer = setTimeout(() => this.evict(key), effectiveTtl * 1000);
|
|
55
|
+
this.timers.set(key, timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async delete(key: string): Promise<void> {
|
|
60
|
+
this.evict(key);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async invalidateByPrefix(prefix: string): Promise<number> {
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (const key of this.store.keys()) {
|
|
66
|
+
if (key.startsWith(prefix)) {
|
|
67
|
+
this.evict(key);
|
|
68
|
+
count++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return count;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async has(key: string): Promise<boolean> {
|
|
75
|
+
const value = await this.get(key);
|
|
76
|
+
return value !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getOrSet<T = unknown>(
|
|
80
|
+
key: string,
|
|
81
|
+
factory: () => Promise<T>,
|
|
82
|
+
ttlSeconds?: number,
|
|
83
|
+
): Promise<T> {
|
|
84
|
+
// Fast path: cache hit
|
|
85
|
+
const cached = await this.get<T>(key);
|
|
86
|
+
if (cached !== null) return cached;
|
|
87
|
+
|
|
88
|
+
// Stampede protection: if another call is already computing this key, reuse its promise
|
|
89
|
+
const existing = this.inflight.get(key) as Promise<T> | undefined;
|
|
90
|
+
if (existing !== undefined) return existing;
|
|
91
|
+
|
|
92
|
+
const promise = factory().then(async (value) => {
|
|
93
|
+
await this.set(key, value, ttlSeconds);
|
|
94
|
+
return value;
|
|
95
|
+
}).finally(() => {
|
|
96
|
+
this.inflight.delete(key);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.inflight.set(key, promise as Promise<unknown>);
|
|
100
|
+
return promise;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Remove a key from store and cancel its expiry timer. */
|
|
104
|
+
private evict(key: string): void {
|
|
105
|
+
this.store.delete(key);
|
|
106
|
+
this.clearTimer(key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private clearTimer(key: string): void {
|
|
110
|
+
const timer = this.timers.get(key);
|
|
111
|
+
if (timer !== undefined) {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
this.timers.delete(key);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|