@3lineas/grooveskills 1.7.1 → 1.8.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 +3 -2
- package/dist/{chunk-SP3ESDM5.js → chunk-2VONHHPV.js} +80 -17
- package/dist/chunk-2VONHHPV.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/controller-orchestration-queues/SKILL.md +62 -63
- package/skills/controller-orchestration-queues/references/controller-queue-examples.md +66 -42
- package/skills/error-handling-logging/SKILL.md +64 -68
- package/skills/error-handling-logging/templates/app-error.template.md +10 -1
- package/skills/hono/SKILL.md +73 -66
- package/skills/hono/templates/router.template.md +18 -26
- package/skills/react-core/SKILL.md +45 -112
- package/skills/testing-vitest/SKILL.md +68 -61
- package/skills/testing-vitest/references/backend-d1-testing-pyramid.md +69 -0
- package/skills/typescript-clean/SKILL.md +40 -20
- package/skills/valibot-validation/SKILL.md +63 -20
- package/skills/workers-rpc-precision/SKILL.md +129 -0
- package/skills/workers-rpc-precision/references/python-worker-rpc-examples.md +98 -0
- package/dist/chunk-SP3ESDM5.js.map +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Implementation Reference: Backend Testing Pyramid & In-Memory D1 Setup
|
|
2
|
+
|
|
3
|
+
A complete guide for orchestrating Vitest test suites across the 4-tier testing pyramid using in-memory SQLite / Cloudflare D1 emulators.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Global Setup with In-Memory Database (`setup.ts`)
|
|
8
|
+
|
|
9
|
+
Instead of recreating database tables inside every test file, initialize migrations once on an in-memory database:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { beforeAll, afterAll } from "vitest";
|
|
13
|
+
import Database from "better-sqlite3";
|
|
14
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
15
|
+
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
16
|
+
import * as schema from "@/db/schema";
|
|
17
|
+
|
|
18
|
+
const sqliteConnection = new Database(":memory:");
|
|
19
|
+
export const testDb = drizzle(sqliteConnection, { schema });
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
// Apply schema migrations automatically before suite execution
|
|
23
|
+
migrate(testDb, { migrationsFolder: "./drizzle" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
sqliteConnection.close();
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Testing Transactions & Rollback (Tier 3)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { describe, it, expect } from "vitest";
|
|
37
|
+
import { withD1Transaction } from "@/common/utils/d1-transaction";
|
|
38
|
+
import { testDb } from "@/backend/setup";
|
|
39
|
+
import { usersTable } from "@/db/schema";
|
|
40
|
+
|
|
41
|
+
describe("D1 Atomic Batch Transactions", () => {
|
|
42
|
+
it("should rollback all mutations if an error occurs mid-transaction", async () => {
|
|
43
|
+
const executeFailingTransaction = async () => {
|
|
44
|
+
await withD1Transaction(testDb as any, async (transaction) => {
|
|
45
|
+
transaction.stage(
|
|
46
|
+
testDb.insert(usersTable).values({ id: "user_1", email: "first@example.com" }) as any
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
throw new Error("Simulated failure mid-transaction");
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await expect(executeFailingTransaction()).rejects.toThrow("Simulated failure mid-transaction");
|
|
54
|
+
|
|
55
|
+
const users = await testDb.select().from(usersTable);
|
|
56
|
+
expect(users).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Best Practices Checklist
|
|
64
|
+
|
|
65
|
+
- [ ] Run migrations on in-memory SQLite rather than hitting live cloud databases.
|
|
66
|
+
- [ ] Reset mutable database state between tests using transaction rollbacks or truncation.
|
|
67
|
+
- [ ] Validate both happy path and rejection paths on every DTO schema.
|
|
68
|
+
- [ ] Ensure route tests never assert raw implementation details, only HTTP status codes and `ActionResponse<T>` envelopes.
|
|
69
|
+
- [ ] Keep every test file strictly below 200 lines.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: typescript-clean
|
|
3
|
-
description: Advanced TypeScript 5.5+ standards, strict tsconfig rules, discriminated unions, satisfies operator, const type parameters, unknown over any,
|
|
3
|
+
description: Advanced TypeScript 5.5+ standards, strict tsconfig rules, discriminated unions, satisfies operator, const type parameters, unknown over any, atomic type isolation in @/types/, and elimination of unsafe type assertions (as / any). TRIGGER when writing or modifying TypeScript files (.ts, .tsx), type definitions, tsconfig.json, or when handling complex polymorphic domain models.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# TypeScript Clean Code Standards (TS 5.5+)
|
|
7
7
|
|
|
8
|
-
Guidelines for writing safe, expressive,
|
|
8
|
+
Guidelines for writing safe, expressive, strictly typed TypeScript with zero unsafe casts, exhaustive pattern matching, and atomic domain type isolation.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -15,31 +15,57 @@ Guidelines for writing safe, expressive, and strictly typed TypeScript with zero
|
|
|
15
15
|
- `any` disables the compiler and hides runtime bugs. Use `unknown` with narrowers or generics.
|
|
16
16
|
- Non-null assertion operator `!` assumes runtime state without proof. Use optional chaining `?.` or explicit guard clauses.
|
|
17
17
|
|
|
18
|
-
2. **
|
|
18
|
+
2. **Strict Type Isolation (`@/types/{domain}/`)**:
|
|
19
|
+
- In modular domain architectures, **NEVER** write `interface` or `type` definitions inside DTOs, Actions, Routes, or Utility helpers.
|
|
20
|
+
- Every contract has its own dedicated file: `@/types/{domain}/{name}.type.ts`.
|
|
21
|
+
- Organize into domains: `common/`, `context/`, `db/`, `pipeline/`, `services/`, and `{domain}/`.
|
|
22
|
+
|
|
23
|
+
3. **Discriminated Unions for Domain States**:
|
|
19
24
|
- Represent valid combinations of state using tagged unions (`type = "idle" | "loading" | "success" | "error"`).
|
|
20
25
|
- Never use "optional soup" where every field is `x?: string`.
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
4. **Exhaustive Type Checking with `never`**:
|
|
23
28
|
- Every `switch` or conditional on a union must have an `assertNever(value)` exhaustive check in default branch.
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
5. **Favor `satisfies` Over Type Annotations on Literals**:
|
|
26
31
|
- `satisfies` checks that a value adheres to a contract without widening or discarding specific literal types.
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
6. **`const` Type Parameters (TS 5.0+)**:
|
|
29
34
|
- Use `<const T>` in generic helper functions to preserve literal types without requiring `as const` at call site.
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
7. **Custom Type Guards & Inferred Type Predicates (TS 5.5+)**:
|
|
32
37
|
- Use type predicates (`x is TargetType`) or rely on TS 5.5 automatic type narrowing for array filters.
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
8. **Avoid Unsafe `as` Type Assertions**:
|
|
35
40
|
- Never use `value as SomeType` to bypass the type checker. Validate with Valibot schema or custom type guard first.
|
|
36
41
|
|
|
42
|
+
9. **Hard Size Limit of <200 Lines**:
|
|
43
|
+
- Keep type definition files atomic and concise (target: 10-50 lines).
|
|
44
|
+
|
|
37
45
|
---
|
|
38
46
|
|
|
39
|
-
## 2.
|
|
47
|
+
## 2. Directory Structure for Type Isolation
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
src/types/
|
|
51
|
+
├── common/ # Global shared contracts
|
|
52
|
+
│ └── action-response.type.ts # interface ActionResponse<T>
|
|
53
|
+
├── context/ # Infrastructure & runtime execution context
|
|
54
|
+
│ ├── env.type.ts # interface Env (Bindings / Secrets)
|
|
55
|
+
│ └── app-context.type.ts # Context typing (Bindings, Variables)
|
|
56
|
+
├── db/ # Database client and ORM entities
|
|
57
|
+
│ ├── database-client.type.ts # Database client handle
|
|
58
|
+
│ └── {entity}.type.ts # Select / Insert schema types
|
|
59
|
+
└── {domain}/ # Domain-specific DTO & action contracts
|
|
60
|
+
├── create-{entity}-input.type.ts # Inferred from input DTO
|
|
61
|
+
└── {entity}-response.type.ts # Inferred from output DTO
|
|
62
|
+
```
|
|
40
63
|
|
|
41
|
-
|
|
64
|
+
---
|
|
42
65
|
|
|
66
|
+
## 3. Key Patterns
|
|
67
|
+
|
|
68
|
+
### A. Discriminated Unions & Exhaustiveness
|
|
43
69
|
```ts
|
|
44
70
|
export type RequestState<T> =
|
|
45
71
|
| { status: "idle" }
|
|
@@ -68,33 +94,27 @@ export function renderStatus<T>(state: RequestState<T>): string {
|
|
|
68
94
|
```
|
|
69
95
|
|
|
70
96
|
### B. `satisfies` vs Widening Annotations
|
|
71
|
-
|
|
72
97
|
```ts
|
|
73
98
|
type RouteConfig = Record<string, { path: string; isProtected: boolean }>;
|
|
74
99
|
|
|
75
|
-
// ✅ Preserves exact key literals and structure
|
|
76
100
|
export const APP_ROUTES = {
|
|
77
101
|
HOME: { path: "/", isProtected: false },
|
|
78
102
|
DASHBOARD: { path: "/dashboard", isProtected: true },
|
|
79
103
|
} satisfies RouteConfig;
|
|
80
|
-
|
|
81
|
-
// APP_ROUTES.HOME.path is typed as "/" not string!
|
|
82
104
|
```
|
|
83
105
|
|
|
84
106
|
### C. Const Generic Parameters
|
|
85
|
-
|
|
86
107
|
```ts
|
|
87
108
|
export function createEnumSet<const T extends readonly string[]>(values: T): ReadonlySet<T[number]> {
|
|
88
109
|
return new Set(values);
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
// Inferred as ReadonlySet<"admin" | "editor" | "viewer">
|
|
92
112
|
export const ROLES = createEnumSet(["admin", "editor", "viewer"]);
|
|
93
113
|
```
|
|
94
114
|
|
|
95
115
|
---
|
|
96
116
|
|
|
97
|
-
##
|
|
117
|
+
## 4. Strict TSConfig Expectations
|
|
98
118
|
|
|
99
119
|
Always enable:
|
|
100
120
|
- `"strict": true`
|
|
@@ -104,8 +124,8 @@ Always enable:
|
|
|
104
124
|
|
|
105
125
|
---
|
|
106
126
|
|
|
107
|
-
##
|
|
127
|
+
## 5. Sibling Skills
|
|
108
128
|
|
|
109
|
-
- **`best-practices`**: Universal clean code rules and 200-line limit.
|
|
129
|
+
- **`best-practices`**: Universal clean code rules, descriptive naming, and 200-line limit.
|
|
110
130
|
- **`valibot-validation`**: Runtime validation inferred to static TS types.
|
|
111
|
-
- Reference guide:
|
|
131
|
+
- Reference guide: `./references/js-ts-antipatterns.md`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: valibot-validation
|
|
3
|
-
description: Schema validation at system boundaries with Valibot (* as v from "valibot"). Strict type inference (InferInput, InferOutput), input sanitization, pipeline transformations, and courteous user-facing error messages. TRIGGER when defining API request/response schemas, validating forms, parsing external payloads, env configs, or when the project imports 'valibot'.
|
|
3
|
+
description: Schema validation at system boundaries with Valibot (* as v from "valibot"). Strict type inference (InferInput, InferOutput), input sanitization, pipeline transformations, bidirectional DTO isolation, and courteous user-facing error messages. TRIGGER when defining API request/response schemas, validating forms, parsing external payloads, env configs, or when the project imports 'valibot'.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Valibot Schema Validation Standards
|
|
6
|
+
# Valibot Schema Validation & DTO Standards
|
|
7
7
|
|
|
8
|
-
Guidelines for high-performance, modular schema validation at system boundaries using **Valibot** (`* as v from "valibot"`).
|
|
8
|
+
Guidelines for high-performance, modular schema validation at system boundaries and atomic Data Transfer Objects (DTOs) using **Valibot** (`* as v from "valibot"`).
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -14,29 +14,55 @@ Guidelines for high-performance, modular schema validation at system boundaries
|
|
|
14
14
|
1. **Namespace Import `* as v from "valibot"`**:
|
|
15
15
|
- Always import Valibot as namespace `import * as v from "valibot";` to prevent naming collisions and keep schemas consistent.
|
|
16
16
|
|
|
17
|
-
2. **
|
|
18
|
-
-
|
|
17
|
+
2. **Schema & DTO Isolation (1 Schema per File)**:
|
|
18
|
+
- DTO and Schema files (`@/dtos/{domain}/{name}.dto.ts` or `@/schemas/{domain}/`) contain **ONLY** Valibot schemas (`v.object({...})`).
|
|
19
|
+
- **NEVER** declare TypeScript `type` or `interface` inside a DTO/Schema file. Inferred types belong in `@/types/{domain}/`.
|
|
19
20
|
|
|
20
|
-
3. **
|
|
21
|
+
3. **Bidirectional Validation (Input + Output Whitelisting)**:
|
|
22
|
+
- **Input**: Validate request bodies, query params, and route params via validators (`vValidator` / `sValidator`).
|
|
23
|
+
- **Output**: Serialize and whitelist outgoing responses with output schemas (`StoreResponseDto`) to eliminate sensitive fields (`passwordHash`, private tokens) and format values (e.g. ISO date conversions).
|
|
24
|
+
|
|
25
|
+
4. **Pipelines with Polite Error Messages (`v.pipe`)**:
|
|
21
26
|
- Chain schema constraints using `v.pipe(...)`.
|
|
22
27
|
- Provide clear, user-facing error messages on every validation step following the `ux-writing` skill guidelines.
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
- Export inferred types for transformations.
|
|
29
|
+
5. **Explicit Type Inference (`v.InferInput` & `v.InferOutput`)**:
|
|
26
30
|
- Use `v.InferOutput<typeof schema>` for validated downstream data, and `v.InferInput<typeof schema>` for raw form inputs.
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
6. **`v.safeParse` vs `v.parse` Discipline**:
|
|
29
33
|
- Use `v.safeParse` when building user-friendly forms or API endpoints to format issues into polite error responses without throwing.
|
|
30
34
|
- Use `v.parse` for critical startup configurations (like environment variables) where throwing stops catastrophic initialization.
|
|
31
35
|
|
|
36
|
+
7. **Hard Limit of <200 Lines**:
|
|
37
|
+
- Keep atomic DTO and schema definitions concise and focused (target: 20-60 lines).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2. Directory Taxonomy
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
src/
|
|
45
|
+
├── dtos/ (or schemas/)
|
|
46
|
+
│ └── {domain}/
|
|
47
|
+
│ ├── create-{entity}.dto.ts # Request Body Schema
|
|
48
|
+
│ ├── {entity}-param.dto.ts # URL Param Schema
|
|
49
|
+
│ ├── {entity}-query.dto.ts # Query Filter Schema
|
|
50
|
+
│ └── {entity}-response.dto.ts # Whitelist Output Response Schema
|
|
51
|
+
└── types/
|
|
52
|
+
└── {domain}/
|
|
53
|
+
├── create-{entity}-input.type.ts # v.InferInput<typeof CreateEntityDto>
|
|
54
|
+
└── {entity}-response.type.ts # v.InferOutput<typeof EntityResponseDto>
|
|
55
|
+
```
|
|
56
|
+
|
|
32
57
|
---
|
|
33
58
|
|
|
34
|
-
##
|
|
59
|
+
## 3. Bidirectional Schema Example
|
|
35
60
|
|
|
61
|
+
### Input Schema (`src/dtos/users/create-user.dto.ts`):
|
|
36
62
|
```ts
|
|
37
63
|
import * as v from "valibot";
|
|
38
64
|
|
|
39
|
-
export const
|
|
65
|
+
export const CreateUserDto = v.object({
|
|
40
66
|
fullName: v.pipe(
|
|
41
67
|
v.string("El nombre completo es requerido."),
|
|
42
68
|
v.trim(),
|
|
@@ -45,22 +71,38 @@ export const CreateUserSchema = v.object({
|
|
|
45
71
|
email: v.pipe(
|
|
46
72
|
v.string("El correo electrónico es requerido."),
|
|
47
73
|
v.trim(),
|
|
74
|
+
v.toLowerCase(),
|
|
48
75
|
v.email("Por favor ingresa un correo electrónico válido.")
|
|
49
76
|
),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Output Whitelist Schema (`src/dtos/users/user-response.dto.ts`):
|
|
81
|
+
```ts
|
|
82
|
+
import * as v from "valibot";
|
|
83
|
+
|
|
84
|
+
export const UserResponseDto = v.object({
|
|
85
|
+
id: v.string(),
|
|
86
|
+
fullName: v.string(),
|
|
87
|
+
email: v.string(),
|
|
88
|
+
createdAt: v.pipe(
|
|
89
|
+
v.union([v.date(), v.string(), v.number()]),
|
|
90
|
+
v.transform((value) => new Date(value).toISOString())
|
|
54
91
|
),
|
|
55
92
|
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Inferred Type Isolation (`src/types/users/create-user-input.type.ts`):
|
|
96
|
+
```ts
|
|
97
|
+
import type * as v from "valibot";
|
|
98
|
+
import type { CreateUserDto } from "@/dtos/users/create-user.dto";
|
|
56
99
|
|
|
57
|
-
export type CreateUserInput = v.InferInput<typeof
|
|
58
|
-
export type CreateUserOutput = v.InferOutput<typeof CreateUserSchema>;
|
|
100
|
+
export type CreateUserInput = v.InferInput<typeof CreateUserDto>;
|
|
59
101
|
```
|
|
60
102
|
|
|
61
103
|
---
|
|
62
104
|
|
|
63
|
-
##
|
|
105
|
+
## 4. Safe Parsing Helper Pattern
|
|
64
106
|
|
|
65
107
|
```ts
|
|
66
108
|
import * as v from "valibot";
|
|
@@ -95,8 +137,9 @@ export function validateSchema<TSchema extends v.BaseSchema<unknown, unknown, v.
|
|
|
95
137
|
|
|
96
138
|
---
|
|
97
139
|
|
|
98
|
-
##
|
|
140
|
+
## 5. Sibling Skills
|
|
99
141
|
|
|
100
142
|
- **`best-practices`**: Universal clean code and `<200` line limits.
|
|
101
143
|
- **`ux-writing`**: Form validation copy and tone standards.
|
|
102
|
-
- **`hono`**: Hono API route integration with
|
|
144
|
+
- **`hono`**: Hono API route integration with validation hooks and thin route wrappers.
|
|
145
|
+
- **`typescript-clean`**: Isolated type definitions and strict contracts.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workers-rpc-precision
|
|
3
|
+
description: Cloudflare Workers Native RPC (WorkerEntrypoint & Service<T>) and high-precision financial/date calculation standards in Python or TypeScript. TRIGGER when implementing Cloudflare Service Bindings, Workers RPC, decimal financial math (taxes, discounts, totals), billing cycles with timezones, or apps/py-worker.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cloudflare Workers RPC & High-Precision Calculations
|
|
7
|
+
|
|
8
|
+
Architectural standard for zero-overhead, in-memory **Cloudflare Workers RPC** via `WorkerEntrypoint` and `Service<T>`, combined with high-precision financial calculations (`Decimal`) and timezone-aware dates.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Golden Rules (Non-negotiable)
|
|
13
|
+
|
|
14
|
+
1. **Native Workers RPC (No `fetch()` or Fake HTTP URLs)**:
|
|
15
|
+
- **NEVER** use `fetch()`, fake URLs (`http://calculator-internal`), or HTTP headers to communicate between bound workers.
|
|
16
|
+
- Invoke RPC methods directly: `await env.CALCULATOR.calculate_order_totals(payload);`.
|
|
17
|
+
|
|
18
|
+
2. **Clean Boundary Separation**:
|
|
19
|
+
- High-precision math and domain calculations live in the specialized worker (`apps/py-worker` or `apps/calculator-worker`).
|
|
20
|
+
- Business orchestration and API routing live in the primary backend (`apps/backend` or `apps/api`).
|
|
21
|
+
|
|
22
|
+
3. **High-Precision Arithmetic (Zero Floating-Point for Money)**:
|
|
23
|
+
- Never use JavaScript `number` or Python `float` for currency, taxes, discounts, or subtotals.
|
|
24
|
+
- In Python, use `Decimal` from the standard library with explicit quantization (`ROUND_HALF_UP`).
|
|
25
|
+
- Safely extract data from Pyodide JS proxies using `to_py()` or JSON normalization.
|
|
26
|
+
|
|
27
|
+
4. **Timezone-Aware Date Computations**:
|
|
28
|
+
- Compute billing cycles, deadlines, and fiscal periods using explicit timezones (`zoneinfo.ZoneInfo`) and ISO 8601 formatting.
|
|
29
|
+
|
|
30
|
+
5. **End-to-End TypeScript RPC Contract**:
|
|
31
|
+
- Define exact RPC parameter and return types in `@/types/services/{service}.type.ts`.
|
|
32
|
+
- Bind `SERVICE_NAME: Service<YourRpcInterface>` in `@/types/context/env.type.ts`.
|
|
33
|
+
|
|
34
|
+
6. **Caller Action Isolation**:
|
|
35
|
+
- Wrap RPC invocations inside dedicated, atomic Actions (`@/actions/{domain}/{name}.action.ts`).
|
|
36
|
+
|
|
37
|
+
7. **Hard Size Limit: <200 Lines**:
|
|
38
|
+
- Keep entrypoints, pure arithmetic helpers, and domain handlers under 200 lines (target: 20-80 lines).
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. Directory Taxonomy
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
apps/py-worker/ (or specialized worker)
|
|
46
|
+
├── wrangler.jsonc # flags: ["python_workers"] or Service Bindings
|
|
47
|
+
├── requirements.txt # Dependencies
|
|
48
|
+
└── src/
|
|
49
|
+
├── entry.py # WorkerEntrypoint exposing RPC methods (<60 lines)
|
|
50
|
+
├── core/
|
|
51
|
+
│ ├── money.py # Pure Decimal math & rounding (<50 lines)
|
|
52
|
+
│ └── dates.py # Pure timezone & datetime logic (<50 lines)
|
|
53
|
+
└── domains/
|
|
54
|
+
└── finance/
|
|
55
|
+
└── calculate_totals.py # Domain handler (<70 lines)
|
|
56
|
+
|
|
57
|
+
apps/backend/src/
|
|
58
|
+
├── types/services/
|
|
59
|
+
│ └── calculator-service.type.ts # TypeScript RPC interface (<50 lines)
|
|
60
|
+
└── actions/finance/
|
|
61
|
+
└── calculate-totals.action.ts # Action wrapping RPC call (<40 lines)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. End-to-End RPC Blueprint
|
|
67
|
+
|
|
68
|
+
### Python Worker Entrypoint (`src/entry.py`):
|
|
69
|
+
```python
|
|
70
|
+
from workers import WorkerEntrypoint
|
|
71
|
+
from domains.finance.calculate_totals import handle_calculate_totals
|
|
72
|
+
|
|
73
|
+
class CalculatorRpcService(WorkerEntrypoint):
|
|
74
|
+
async def calculate_order_totals(self, data: dict) -> dict:
|
|
75
|
+
return handle_calculate_totals(data)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### TypeScript RPC Contract (`@/types/services/calculator-service.type.ts`):
|
|
79
|
+
```ts
|
|
80
|
+
export interface OrderItemInput {
|
|
81
|
+
id?: string;
|
|
82
|
+
unitPrice: string | number;
|
|
83
|
+
quantity: number;
|
|
84
|
+
discountPct?: string | number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CalculateTotalsPayload {
|
|
88
|
+
items: OrderItemInput[];
|
|
89
|
+
taxRatePct: string | number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CalculateTotalsResult {
|
|
93
|
+
items: Array<{ id?: string; itemTotal: string }>;
|
|
94
|
+
subtotal: string;
|
|
95
|
+
tax: string;
|
|
96
|
+
total: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CalculatorRpcService {
|
|
100
|
+
calculate_order_totals(data: CalculateTotalsPayload): Promise<CalculateTotalsResult>;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Caller Action in TypeScript (`@/actions/finance/calculate-totals.action.ts`):
|
|
105
|
+
```ts
|
|
106
|
+
import type { Service } from "@cloudflare/workers-types";
|
|
107
|
+
import type {
|
|
108
|
+
CalculatorRpcService,
|
|
109
|
+
CalculateTotalsPayload,
|
|
110
|
+
CalculateTotalsResult,
|
|
111
|
+
} from "@/types/services/calculator-service.type";
|
|
112
|
+
|
|
113
|
+
export interface CalculateTotalsActionInput {
|
|
114
|
+
calculator: Service<CalculatorRpcService>;
|
|
115
|
+
payload: CalculateTotalsPayload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function calculateTotalsAction(
|
|
119
|
+
input: CalculateTotalsActionInput
|
|
120
|
+
): Promise<CalculateTotalsResult> {
|
|
121
|
+
return await input.calculator.calculate_order_totals(input.payload);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 4. Deep References
|
|
128
|
+
|
|
129
|
+
- `./references/python-worker-rpc-examples.md`: Complete implementations of `money.py`, Pyodide proxy conversion, and billing cycle handlers.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Implementation Reference: Python Workers RPC & Precision Math
|
|
2
|
+
|
|
3
|
+
Complete helpers and handlers for high-precision arithmetic, JS proxy conversion, and timezone calculations on Cloudflare Workers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Pure Decimal Arithmetic (`src/core/money.py`)
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
11
|
+
|
|
12
|
+
def to_decimal(val) -> Decimal:
|
|
13
|
+
return Decimal(str(val))
|
|
14
|
+
|
|
15
|
+
def round_currency(amount: Decimal, decimals: int = 2) -> Decimal:
|
|
16
|
+
exponent = Decimal("10") ** -decimals
|
|
17
|
+
return amount.quantize(exponent, rounding=ROUND_HALF_UP)
|
|
18
|
+
|
|
19
|
+
def compute_item_total(unit_price: Decimal, quantity: int, discount_pct: Decimal = Decimal(0)) -> Decimal:
|
|
20
|
+
subtotal = unit_price * quantity
|
|
21
|
+
discount = subtotal * (discount_pct / Decimal(100))
|
|
22
|
+
return round_currency(subtotal - discount)
|
|
23
|
+
|
|
24
|
+
def compute_tax(amount: Decimal, tax_rate_pct: Decimal) -> Decimal:
|
|
25
|
+
return round_currency(amount * (tax_rate_pct / Decimal(100)))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Safe Pyodide JS Proxy Handler (`src/domains/finance/calculate_totals.py`)
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import json
|
|
34
|
+
from decimal import Decimal
|
|
35
|
+
from typing import Any
|
|
36
|
+
from core.money import to_decimal, compute_item_total, compute_tax, round_currency
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from js import JSON # type: ignore
|
|
40
|
+
except ImportError:
|
|
41
|
+
JSON = None
|
|
42
|
+
|
|
43
|
+
def handle_calculate_totals(raw_data: Any) -> Any:
|
|
44
|
+
# Safely convert Pyodide JS Proxy or dict
|
|
45
|
+
if hasattr(raw_data, "to_py"):
|
|
46
|
+
data = raw_data.to_py()
|
|
47
|
+
elif isinstance(raw_data, dict):
|
|
48
|
+
data = raw_data
|
|
49
|
+
elif JSON is not None:
|
|
50
|
+
data = json.loads(JSON.stringify(raw_data))
|
|
51
|
+
else:
|
|
52
|
+
data = dict(raw_data)
|
|
53
|
+
|
|
54
|
+
items = data.get("items", [])
|
|
55
|
+
tax_rate = to_decimal(data.get("taxRatePct", 0))
|
|
56
|
+
subtotal = Decimal("0")
|
|
57
|
+
processed_items = []
|
|
58
|
+
|
|
59
|
+
for item in items:
|
|
60
|
+
unit_price = to_decimal(item.get("unitPrice", 0))
|
|
61
|
+
quantity = int(item.get("quantity", 1))
|
|
62
|
+
discount_pct = to_decimal(item.get("discountPct", 0))
|
|
63
|
+
item_total = compute_item_total(unit_price, quantity, discount_pct)
|
|
64
|
+
subtotal += item_total
|
|
65
|
+
processed_items.append({"id": item.get("id"), "itemTotal": str(item_total)})
|
|
66
|
+
|
|
67
|
+
tax = compute_tax(subtotal, tax_rate)
|
|
68
|
+
total = round_currency(subtotal + tax)
|
|
69
|
+
|
|
70
|
+
result = {
|
|
71
|
+
"items": processed_items,
|
|
72
|
+
"subtotal": str(subtotal),
|
|
73
|
+
"tax": str(tax),
|
|
74
|
+
"total": str(total),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if JSON is not None:
|
|
78
|
+
return JSON.parse(json.dumps(result))
|
|
79
|
+
return result
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. Wrangler Service Binding Configuration
|
|
85
|
+
|
|
86
|
+
```jsonc
|
|
87
|
+
// apps/backend/wrangler.jsonc
|
|
88
|
+
{
|
|
89
|
+
"name": "my-backend",
|
|
90
|
+
"services": [
|
|
91
|
+
{
|
|
92
|
+
"binding": "CALCULATOR",
|
|
93
|
+
"service": "my-py-worker",
|
|
94
|
+
"entrypoint": "CalculatorRpcService"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|