@3lineas/grooveskills 1.5.0 → 1.7.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/README.md +8 -0
- package/dist/{chunk-PLMYUKGW.js → chunk-E63GTCOE.js} +475 -146
- package/dist/chunk-E63GTCOE.js.map +1 -0
- package/dist/cli.js +23 -3
- package/dist/cli.js.map +1 -1
- package/dist/main.js +1 -1
- package/package.json +3 -2
- package/skills/auth-rbac-policies/SKILL.md +100 -0
- package/skills/auth-rbac-policies/references/auth-adapters-examples.md +99 -0
- package/skills/caching-rate-limiting/SKILL.md +109 -0
- package/skills/caching-rate-limiting/references/cache-rate-limit-examples.md +129 -0
- package/skills/clean-architecture-ddd/SKILL.md +1 -0
- package/skills/controller-orchestration-queues/SKILL.md +109 -0
- package/skills/controller-orchestration-queues/references/controller-queue-examples.md +115 -0
- package/skills/docs-standards/SKILL.md +113 -0
- package/skills/docs-standards/references/adr-template-and-lifecycle.md +111 -0
- package/skills/docs-standards/references/code-comments-and-docstrings.md +147 -0
- package/skills/docs-standards/references/relative-paths-and-diagrams.md +123 -0
- package/skills/env-feature-flags/SKILL.md +94 -0
- package/skills/env-feature-flags/references/env-feature-flags-examples.md +93 -0
- package/skills/external-service-adapters/SKILL.md +148 -0
- package/skills/external-service-adapters/references/email-and-payment-examples.md +198 -0
- package/skills/storage-file-adapters/SKILL.md +125 -0
- package/skills/storage-file-adapters/references/storage-adapters-examples.md +164 -0
- package/skills/webhook-idempotency/SKILL.md +104 -0
- package/skills/webhook-idempotency/references/webhook-idempotency-examples.md +112 -0
- package/dist/chunk-PLMYUKGW.js.map +0 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caching-rate-limiting
|
|
3
|
+
description: Architectural standards for Cache-Aside wrappers (cache.remember pattern) and interchangeable Rate Limiter adapters (Redis, Cloudflare KV, In-Memory). Prevents leaking vendor cache drivers into business logic and guarantees clean sliding-window or token-bucket rate limiting. TRIGGER when optimizing database query performance, adding cache layers, implementing API throttling, DDoS prevention, or per-user rate limiting.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cache-Aside Wrapper & Rate Limiter Adapters
|
|
7
|
+
|
|
8
|
+
Opinionated architectural pattern for performance caching via the **Cache-Aside (`cache.remember`)** wrapper and decoupled **Rate Limiter** adapters.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Core Architectural Concepts
|
|
13
|
+
|
|
14
|
+
1. **Cache-Aside Pattern (`cache.remember`)**:
|
|
15
|
+
- Instead of manual `get`, check null, query DB, and `set`, actions call `cache.remember(key, ttl, fetcher)`.
|
|
16
|
+
- Caching logic stays declarative, resilient, and decoupled from underlying stores.
|
|
17
|
+
2. **Interchangeable Rate Limiting Port**:
|
|
18
|
+
- Throttling (sliding window / token bucket) is encapsulated behind `RateLimiterPort`.
|
|
19
|
+
- HTTP route middleware and actions check permissions without knowing whether Redis, Cloudflare KV, or an In-Memory store enforces limits.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Directory Layout
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
src/
|
|
27
|
+
├── domains/
|
|
28
|
+
│ ├── cache/
|
|
29
|
+
│ │ ├── cache-store.port.ts # Raw key-value cache contract (<40 lines)
|
|
30
|
+
│ │ └── cache.wrapper.ts # Cache-aside remember helper (<60 lines)
|
|
31
|
+
│ └── rate-limit/
|
|
32
|
+
│ └── rate-limiter.port.ts # Rate limiting contract (<40 lines)
|
|
33
|
+
└── infrastructure/
|
|
34
|
+
└── adapters/
|
|
35
|
+
├── cache/
|
|
36
|
+
│ ├── redis-cache.adapter.ts # Redis cache implementation (<70 lines)
|
|
37
|
+
│ ├── cloudflare-kv.adapter.ts # Cloudflare KV cache (<70 lines)
|
|
38
|
+
│ └── memory-cache.adapter.ts # In-memory testing cache (<60 lines)
|
|
39
|
+
└── rate-limit/
|
|
40
|
+
├── redis-rate-limiter.adapter.ts # Redis sliding window (<80 lines)
|
|
41
|
+
└── memory-rate-limiter.adapter.ts # In-memory rate limiter (<60 lines)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Cache-Aside Wrapper (`src/domains/cache/cache.wrapper.ts`)
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import type { CacheStorePort } from "./cache-store.port";
|
|
50
|
+
|
|
51
|
+
export class CacheWrapper {
|
|
52
|
+
constructor(private store: CacheStorePort) {}
|
|
53
|
+
|
|
54
|
+
async remember<T>(key: string, ttlSeconds: number, fetcher: () => Promise<T>): Promise<T> {
|
|
55
|
+
const cached = await this.store.get<T>(key);
|
|
56
|
+
if (cached !== null && cached !== undefined) {
|
|
57
|
+
return cached;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fresh = await fetcher();
|
|
61
|
+
await this.store.set(key, fresh, ttlSeconds);
|
|
62
|
+
return fresh;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async forget(key: string): Promise<void> {
|
|
66
|
+
await this.store.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 4. Rate Limiter Port & Enforcement
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/domains/rate-limit/rate-limiter.port.ts
|
|
77
|
+
export interface RateLimitResult {
|
|
78
|
+
success: boolean;
|
|
79
|
+
limit: number;
|
|
80
|
+
remaining: number;
|
|
81
|
+
resetSeconds: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RateLimiterPort {
|
|
85
|
+
limit(identifier: string): Promise<RateLimitResult>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Rate Limiter Middleware Example
|
|
90
|
+
```ts
|
|
91
|
+
export async function rateLimitMiddleware(c: Context, limiter: RateLimiterPort) {
|
|
92
|
+
const ip = c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for") ?? "anonymous";
|
|
93
|
+
const check = await limiter.limit(`ip:${ip}`);
|
|
94
|
+
|
|
95
|
+
c.header("X-RateLimit-Limit", String(check.limit));
|
|
96
|
+
c.header("X-RateLimit-Remaining", String(check.remaining));
|
|
97
|
+
|
|
98
|
+
if (!check.success) {
|
|
99
|
+
throw new AppError({ message: "Has excedido el límite de solicitudes. Intenta más tarde.", statusCode: 429 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. Detailed Adapters Reference
|
|
107
|
+
|
|
108
|
+
For real implementations of Redis cache, Cloudflare KV, and Redis sliding-window rate limiters, consult:
|
|
109
|
+
- `references/cache-rate-limit-examples.md`
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Implementation Reference: Cache & Rate Limiting Adapters
|
|
2
|
+
|
|
3
|
+
Production implementations of Redis cache, Cloudflare KV, and sliding-window rate limiters.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Redis Cache Adapter (`ioredis`)
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/infrastructure/adapters/cache/redis-cache.adapter.ts
|
|
11
|
+
import type { Redis } from "ioredis";
|
|
12
|
+
import type { CacheStorePort } from "@/domains/cache/cache-store.port";
|
|
13
|
+
|
|
14
|
+
export class RedisCacheAdapter implements CacheStorePort {
|
|
15
|
+
constructor(private redis: Redis) {}
|
|
16
|
+
|
|
17
|
+
async get<T>(key: string): Promise<T | null> {
|
|
18
|
+
const raw = await this.redis.get(key);
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(raw) as T;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
|
|
28
|
+
await this.redis.set(key, JSON.stringify(value), "EX", ttlSeconds);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async delete(key: string): Promise<void> {
|
|
32
|
+
await this.redis.del(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Cloudflare Workers KV Cache Adapter
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// src/infrastructure/adapters/cache/cloudflare-kv.adapter.ts
|
|
43
|
+
import type { CacheStorePort } from "@/domains/cache/cache-store.port";
|
|
44
|
+
|
|
45
|
+
export class CloudflareKvAdapter implements CacheStorePort {
|
|
46
|
+
constructor(private kv: KVNamespace) {}
|
|
47
|
+
|
|
48
|
+
async get<T>(key: string): Promise<T | null> {
|
|
49
|
+
return this.kv.get<T>(key, "json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
|
|
53
|
+
await this.kv.put(key, JSON.stringify(value), { expirationTtl: Math.max(ttlSeconds, 60) });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async delete(key: string): Promise<void> {
|
|
57
|
+
await this.kv.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. Redis Sliding Window Rate Limiter Adapter
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/infrastructure/adapters/rate-limit/redis-rate-limiter.adapter.ts
|
|
68
|
+
import type { Redis } from "ioredis";
|
|
69
|
+
import type { RateLimiterPort, RateLimitResult } from "@/domains/rate-limit/rate-limiter.port";
|
|
70
|
+
|
|
71
|
+
export class RedisSlidingWindowRateLimiter implements RateLimiterPort {
|
|
72
|
+
constructor(
|
|
73
|
+
private redis: Redis,
|
|
74
|
+
private maxRequests: number = 60,
|
|
75
|
+
private windowSeconds: number = 60
|
|
76
|
+
) {}
|
|
77
|
+
|
|
78
|
+
async limit(identifier: string): Promise<RateLimitResult> {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const windowStart = now - this.windowSeconds * 1000;
|
|
81
|
+
const key = `ratelimit:${identifier}`;
|
|
82
|
+
|
|
83
|
+
const multi = this.redis.multi();
|
|
84
|
+
multi.zremrangebyscore(key, 0, windowStart);
|
|
85
|
+
multi.zadd(key, now, `${now}-${Math.random()}`);
|
|
86
|
+
multi.zcard(key);
|
|
87
|
+
multi.expire(key, this.windowSeconds);
|
|
88
|
+
|
|
89
|
+
const results = await multi.exec();
|
|
90
|
+
const count = (results?.[2]?.[1] as number) ?? 1;
|
|
91
|
+
|
|
92
|
+
const remaining = Math.max(0, this.maxRequests - count);
|
|
93
|
+
const success = count <= this.maxRequests;
|
|
94
|
+
|
|
95
|
+
return { success, limit: this.maxRequests, remaining, resetSeconds: this.windowSeconds };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 4. In-Memory Testing Cache Adapter
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// src/infrastructure/adapters/cache/memory-cache.adapter.ts
|
|
106
|
+
import type { CacheStorePort } from "@/domains/cache/cache-store.port";
|
|
107
|
+
|
|
108
|
+
export class MemoryCacheAdapter implements CacheStorePort {
|
|
109
|
+
private store = new Map<string, { value: unknown; expiresAt: number }>();
|
|
110
|
+
|
|
111
|
+
async get<T>(key: string): Promise<T | null> {
|
|
112
|
+
const item = this.store.get(key);
|
|
113
|
+
if (!item) return null;
|
|
114
|
+
if (Date.now() > item.expiresAt) {
|
|
115
|
+
this.store.delete(key);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return item.value as T;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
|
|
122
|
+
this.store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async delete(key: string): Promise<void> {
|
|
126
|
+
this.store.delete(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
@@ -101,3 +101,4 @@ export async function chargeSubscriptionAction(
|
|
|
101
101
|
- **`best-practices`**: Universal clean code rules and guard clauses.
|
|
102
102
|
- **`error-handling-logging`**: Structured `AppError` and `ActionResponse`.
|
|
103
103
|
- **`valibot-validation`**: Domain input/output validation.
|
|
104
|
+
- **`external-service-adapters`**: Standardized domain ports, vendor SDK wrappers, and adapters for third-party services.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: controller-orchestration-queues
|
|
3
|
+
description: Strict orchestration standards for Controllers, atomic Actions, database Transactions, and background Queue Adapters (Cloudflare Queues, BullMQ, AWS SQS, In-Memory). Mandates that Actions do only ONE atomic thing, while Controllers sequence multiple actions, manage transactions, or dispatch async jobs. Enforces Queue Workers as slim controllers. TRIGGER when orchestrating multi-step workflows, handling DB transactions across actions, dispatching background tasks, or designing queue consumers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Controller Action Orchestration, Transactions & Queue Adapters
|
|
7
|
+
|
|
8
|
+
Architectural standard establishing strict separation of concerns between **Atomic Actions** and **Orchestration Controllers / Queue Workers**.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. The Core Architectural Law
|
|
13
|
+
|
|
14
|
+
1. **Actions Do Exactly ONE Atomic Thing**:
|
|
15
|
+
- Pure single responsibility. Example: `createOrderAction`, `chargeCardAction`, `sendOrderEmailAction`.
|
|
16
|
+
- Actions NEVER orchestrate other sibling actions or dispatch unrelated background jobs directly.
|
|
17
|
+
2. **Controllers are Orchestrators**:
|
|
18
|
+
- HTTP route handlers and RPC controllers validate inputs, sequence actions, wrap transactional DB boundaries (`withTransaction`), and dispatch background jobs via `QueuePort`.
|
|
19
|
+
3. **Queue Workers are Slim Controllers**:
|
|
20
|
+
- A queue worker message handler behaves like an HTTP controller: parses payload and delegates to atomic actions.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Directory Layout
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
src/
|
|
28
|
+
├── actions/
|
|
29
|
+
│ ├── orders/
|
|
30
|
+
│ │ ├── create-order.action.ts # Atomic insert order (<70 lines)
|
|
31
|
+
│ │ └── update-order-status.action.ts # Atomic update status (<60 lines)
|
|
32
|
+
│ └── billing/
|
|
33
|
+
│ └── charge-card.action.ts # Atomic payment charge (<70 lines)
|
|
34
|
+
├── controllers/
|
|
35
|
+
│ └── orders/
|
|
36
|
+
│ └── checkout.controller.ts # Orchestrates actions + Queue (<90 lines)
|
|
37
|
+
├── domains/
|
|
38
|
+
│ └── queue/
|
|
39
|
+
│ ├── queue.port.ts # Domain interface for dispatching (<40 lines)
|
|
40
|
+
│ └── queue-message.types.ts # Strongly typed queue messages (<50 lines)
|
|
41
|
+
└── infrastructure/
|
|
42
|
+
├── adapters/
|
|
43
|
+
│ └── queue/
|
|
44
|
+
│ ├── cloudflare-queues.adapter.ts # Cloudflare Queues adapter (<80 lines)
|
|
45
|
+
│ ├── bullmq-queues.adapter.ts # Redis / BullMQ adapter (<80 lines)
|
|
46
|
+
│ ├── sqs-queues.adapter.ts # AWS SQS adapter (<80 lines)
|
|
47
|
+
│ └── memory-queues.adapter.ts # In-memory testing queue (<60 lines)
|
|
48
|
+
└── workers/
|
|
49
|
+
└── order-events.worker.ts # Slim worker invoking actions (<80 lines)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. Domain Queue Contract (`src/domains/queue/queue.port.ts`)
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export interface QueueJob<TData = unknown> {
|
|
58
|
+
id?: string;
|
|
59
|
+
name: string;
|
|
60
|
+
data: TData;
|
|
61
|
+
delaySeconds?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface QueuePort {
|
|
65
|
+
dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }>;
|
|
66
|
+
dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }>;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 4. Orchestration Controller Pattern
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// src/controllers/orders/checkout.controller.ts
|
|
76
|
+
import type { Context } from "hono";
|
|
77
|
+
import type { QueuePort } from "@/domains/queue/queue.port";
|
|
78
|
+
import { createOrderAction } from "@/actions/orders/create-order.action";
|
|
79
|
+
import { chargeCardAction } from "@/actions/billing/charge-card.action";
|
|
80
|
+
import { db, withTransaction } from "@/infrastructure/db/connection";
|
|
81
|
+
|
|
82
|
+
export async function checkoutController(c: Context, queue: QueuePort) {
|
|
83
|
+
const payload = c.get("validatedJson");
|
|
84
|
+
const actor = c.get("actor");
|
|
85
|
+
|
|
86
|
+
// Step 1: Charge card (external side-effect, kept outside DB transaction)
|
|
87
|
+
const charge = await chargeCardAction(payload.paymentMethodId, payload.amount);
|
|
88
|
+
|
|
89
|
+
// Step 2: Atomic DB Transaction for local entity creation
|
|
90
|
+
const order = await withTransaction(db, async (tx) => {
|
|
91
|
+
return createOrderAction(tx, { actorId: actor.id, amount: payload.amount, chargeId: charge.id });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Step 3: Dispatch async post-processing via Queue
|
|
95
|
+
await queue.dispatch({
|
|
96
|
+
name: "orders.post-checkout",
|
|
97
|
+
data: { orderId: order.id, customerEmail: actor.email },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return c.json({ orderId: order.id, status: "confirmed" }, 201);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. Detailed Adapters & Workers Reference
|
|
107
|
+
|
|
108
|
+
For real implementations of Cloudflare Queues, BullMQ, AWS SQS, and In-Memory Queue adapters, consult:
|
|
109
|
+
- `references/controller-queue-examples.md`
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Implementation Reference: Controller Orchestration & Queue Adapters
|
|
2
|
+
|
|
3
|
+
Real-world queue adapters (Cloudflare Queues, BullMQ, AWS SQS, In-Memory) and Slim Worker controllers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Cloudflare Queues Adapter
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/infrastructure/adapters/queue/cloudflare-queues.adapter.ts
|
|
11
|
+
import type { QueuePort, QueueJob } from "@/domains/queue/queue.port";
|
|
12
|
+
import { AppError } from "@/errors/app-error";
|
|
13
|
+
|
|
14
|
+
export class CloudflareQueueAdapter implements QueuePort {
|
|
15
|
+
constructor(private queue: Queue) {}
|
|
16
|
+
|
|
17
|
+
async dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }> {
|
|
18
|
+
try {
|
|
19
|
+
await this.queue.send(job, { delaySeconds: job.delaySeconds });
|
|
20
|
+
return { jobId: job.id ?? crypto.randomUUID() };
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new AppError({ message: "Error al encolar mensaje en Cloudflare Queues.", statusCode: 502, cause: err });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }> {
|
|
27
|
+
await this.queue.sendBatch(jobs.map((j) => ({ body: j, delaySeconds: j.delaySeconds })));
|
|
28
|
+
return { count: jobs.length };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. BullMQ (Redis) Adapter
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// src/infrastructure/adapters/queue/bullmq-queues.adapter.ts
|
|
39
|
+
import { Queue as BullQueue } from "bullmq";
|
|
40
|
+
import type { QueuePort, QueueJob } from "@/domains/queue/queue.port";
|
|
41
|
+
import { AppError } from "@/errors/app-error";
|
|
42
|
+
|
|
43
|
+
export class BullMQAdapter implements QueuePort {
|
|
44
|
+
constructor(private bullQueue: BullQueue) {}
|
|
45
|
+
|
|
46
|
+
async dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }> {
|
|
47
|
+
try {
|
|
48
|
+
const bJob = await this.bullQueue.add(job.name, job.data, {
|
|
49
|
+
jobId: job.id,
|
|
50
|
+
delay: job.delaySeconds ? job.delaySeconds * 1000 : undefined,
|
|
51
|
+
});
|
|
52
|
+
return { jobId: String(bJob.id) };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new AppError({ message: "Error al enviar trabajo a BullMQ.", statusCode: 502, cause: err });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }> {
|
|
59
|
+
await this.bullQueue.addBulk(
|
|
60
|
+
jobs.map((j) => ({
|
|
61
|
+
name: j.name,
|
|
62
|
+
data: j.data,
|
|
63
|
+
opts: { jobId: j.id, delay: j.delaySeconds ? j.delaySeconds * 1000 : undefined },
|
|
64
|
+
}))
|
|
65
|
+
);
|
|
66
|
+
return { count: jobs.length };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 3. In-Memory Queue Adapter (Testing Fake)
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/infrastructure/adapters/queue/memory-queues.adapter.ts
|
|
77
|
+
import type { QueuePort, QueueJob } from "@/domains/queue/queue.port";
|
|
78
|
+
|
|
79
|
+
export class MemoryQueueAdapter implements QueuePort {
|
|
80
|
+
public jobs: QueueJob[] = [];
|
|
81
|
+
|
|
82
|
+
async dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }> {
|
|
83
|
+
const id = job.id ?? `job_${this.jobs.length + 1}`;
|
|
84
|
+
this.jobs.push({ ...job, id });
|
|
85
|
+
return { jobId: id };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }> {
|
|
89
|
+
for (const job of jobs) {
|
|
90
|
+
await this.dispatch(job);
|
|
91
|
+
}
|
|
92
|
+
return { count: jobs.length };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 4. Slim Queue Worker Controller Pattern
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// src/infrastructure/workers/order-events.worker.ts
|
|
103
|
+
import { sendOrderEmailAction } from "@/actions/email/send-order-email.action";
|
|
104
|
+
import { syncCrmAction } from "@/actions/crm/sync-crm.action";
|
|
105
|
+
import { logger } from "@/infrastructure/logger/app-logger";
|
|
106
|
+
|
|
107
|
+
export async function handleOrderQueueMessage(messageBody: { name: string; data: { orderId: string; customerEmail: string } }) {
|
|
108
|
+
logger.info("Procesando evento de orden", { jobName: messageBody.name, orderId: messageBody.data.orderId });
|
|
109
|
+
|
|
110
|
+
if (messageBody.name === "orders.post-checkout") {
|
|
111
|
+
await sendOrderEmailAction(messageBody.data.customerEmail, messageBody.data.orderId);
|
|
112
|
+
await syncCrmAction(messageBody.data.orderId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docs-standards
|
|
3
|
+
description: Comprehensive documentation standards: English documentation rule, strict relative POSIX links, Mermaid diagram syntax rules, ADR lifecycle, clean Markdown formatting, and intent-focused code docstrings. TRIGGER when writing, structuring, or updating technical docs, README files, architectural guides, ADRs, Mermaid diagrams, or in-code docstrings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Technical Documentation Standards
|
|
7
|
+
|
|
8
|
+
Authoritative standard for technical documentation, architectural specifications, markdown files, and in-code docstrings across projects.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Core Principles
|
|
13
|
+
|
|
14
|
+
1. **All Technical Documentation in English**:
|
|
15
|
+
- Internal technical guides, README files, ADRs, architecture specs, code comments, and docstrings must be written exclusively in English.
|
|
16
|
+
- User-facing application microcopy remains governed by project localization rules (e.g. `ux-writing`), but technical documentation is English-only.
|
|
17
|
+
|
|
18
|
+
2. **Strict Relative POSIX Paths**:
|
|
19
|
+
- When referencing other files, specs, schemas, or tests within documentation, ALWAYS use relative POSIX paths (`./path/to/file.md` or `../other/file.md`).
|
|
20
|
+
- Never use absolute filesystem paths (`/Users/...`, `C:\...`) or IDE-specific virtual URI protocols.
|
|
21
|
+
- Always verify that relative links resolve correctly from the containing document's directory.
|
|
22
|
+
|
|
23
|
+
3. **Document the "Why", Not the "What"**:
|
|
24
|
+
- Never describe literal syntax (e.g., `// loop through items` or `// sets age to 20`).
|
|
25
|
+
- Document business constraints, invariant assumptions, edge case rationale, architectural trade-offs, and failure expectations.
|
|
26
|
+
- Delete dead code, commented-out blocks, and speculative notes immediately.
|
|
27
|
+
|
|
28
|
+
4. **Hard Limit of <200 Lines Per File**:
|
|
29
|
+
- Split broad specifications or long documents into concise primary manifests (`SKILL.md`, `README.md`) and focused modular topic files under `./references/` or `./docs/`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Documentation Architecture & Directory Layout
|
|
34
|
+
|
|
35
|
+
Organize complex documentation sets into modular topic documents:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
docs/
|
|
39
|
+
├── architecture/
|
|
40
|
+
│ ├── overview.md # High-level system diagram and component boundaries (<150 lines)
|
|
41
|
+
│ └── adr/
|
|
42
|
+
│ ├── 0001-init-stack.md # Individual decision record (<100 lines)
|
|
43
|
+
│ └── 0002-event-bus.md # Modular decision record (<100 lines)
|
|
44
|
+
├── guides/
|
|
45
|
+
│ ├── local-setup.md # Prerequisites and bootstrap guide (<120 lines)
|
|
46
|
+
│ └── testing-strategy.md # Test matrices, fixtures, and CI workflows (<140 lines)
|
|
47
|
+
└── references/
|
|
48
|
+
└── api-contracts.md # Data transfer payloads and external specs (<180 lines)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Link files using explicit relative paths:
|
|
52
|
+
- From `docs/architecture/overview.md` to an ADR: `[ADR 0001](./adr/0001-init-stack.md)`
|
|
53
|
+
- From `docs/architecture/adr/0002-event-bus.md` to architecture overview: `[Overview](../overview.md)`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. Mermaid Diagrams Best Practices
|
|
58
|
+
|
|
59
|
+
Diagrams clarify systems only when syntactically resilient. Always enforce:
|
|
60
|
+
|
|
61
|
+
1. **Direction**: Use explicit layout directions (`flowchart TD` or `flowchart LR`).
|
|
62
|
+
2. **Safe Quoting**: Always wrap node text and edge labels containing punctuation, brackets, parentheses, or spaces inside double quotes:
|
|
63
|
+
- `id1["Service A (v2.1)"] -->|"POST /events"| id2["Worker Engine"]`
|
|
64
|
+
3. **Escaping**: Never leave unescaped curly braces `{}` or square brackets `[]` raw inside node bodies.
|
|
65
|
+
4. **Subgraphs**: Assign explicit identifier tokens to subgraphs:
|
|
66
|
+
```mermaid
|
|
67
|
+
flowchart LR
|
|
68
|
+
subgraph CoreDomain["Domain Layer"]
|
|
69
|
+
Action["ProcessPaymentAction"]
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
*Deep reference*: See [./references/relative-paths-and-diagrams.md](./references/relative-paths-and-diagrams.md) for full syntax and escaping guidelines.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 4. Architecture Decision Records (ADR)
|
|
78
|
+
|
|
79
|
+
Document critical architectural forks before or during implementation:
|
|
80
|
+
|
|
81
|
+
- **State Lifecycle**: `Proposed` -> `Accepted` -> `Superseded by ADR-XXXX` or `Deprecated`.
|
|
82
|
+
- **Structure**: Title, Status, Context, Decision, Consequences (Positive, Negative, Neutral).
|
|
83
|
+
- **Append-only log**: Maintain an index table or individual timestamped files in `./docs/architecture/adr/`.
|
|
84
|
+
|
|
85
|
+
*Deep reference*: See [./references/adr-template-and-lifecycle.md](./references/adr-template-and-lifecycle.md) for templates and lifecycle rules.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 5. In-Code Comments and Docstrings
|
|
90
|
+
|
|
91
|
+
Apply standard formatters and focus on architectural context:
|
|
92
|
+
|
|
93
|
+
- **TypeScript / JavaScript**: Use JSDoc / TSDoc (`/** ... */`) above exported symbols.
|
|
94
|
+
- **Python**: Use PEP 257 docstrings (`"""..."""`) on functions, modules, and classes.
|
|
95
|
+
- **Rust**: Use `///` outer doc comments for items and `//!` for module-level docs.
|
|
96
|
+
- **No obvious commentary**: If the function signature `getUserById(id: string)` already communicates the mechanics, only document non-obvious errors, side-effects, or performance implications.
|
|
97
|
+
|
|
98
|
+
*Deep reference*: See [./references/code-comments-and-docstrings.md](./references/code-comments-and-docstrings.md) for concrete language examples and anti-patterns.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 6. Clean Markdown Formatting & Alerts
|
|
103
|
+
|
|
104
|
+
- Use standard GitHub markdown alert callouts:
|
|
105
|
+
```markdown
|
|
106
|
+
> [!NOTE]
|
|
107
|
+
> Informational notes highlighting context.
|
|
108
|
+
|
|
109
|
+
> [!WARNING]
|
|
110
|
+
> Critical operational caveats, breaking changes, or deprecations.
|
|
111
|
+
```
|
|
112
|
+
- Use consistent heading hierarchies (`#` -> `##` -> `###`). Never skip heading levels.
|
|
113
|
+
- Format tables with clear header alignment (`| :--- | :--- | :---: |`).
|