@grimoire-cc/cli 0.4.0 → 0.5.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.
@@ -0,0 +1,477 @@
1
+ # Code Patterns & Idioms
2
+
3
+ ## Table of Contents
4
+
5
+ - [Functional Patterns](#functional-patterns)
6
+ - [Builder Pattern](#builder-pattern)
7
+ - [State Machines](#state-machines)
8
+ - [Immutable Data Patterns](#immutable-data-patterns)
9
+ - [Async Patterns](#async-patterns)
10
+ - [Dependency Injection](#dependency-injection)
11
+ - [Module Organization](#module-organization)
12
+
13
+ ## Functional Patterns
14
+
15
+ ### Pipe and Compose
16
+
17
+ ```typescript
18
+ // Type-safe pipe (left to right)
19
+ function pipe<A, B>(fn1: (a: A) => B): (a: A) => B;
20
+ function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C;
21
+ function pipe<A, B, C, D>(
22
+ fn1: (a: A) => B,
23
+ fn2: (b: B) => C,
24
+ fn3: (c: C) => D,
25
+ ): (a: A) => D;
26
+ function pipe(...fns: Function[]) {
27
+ return (arg: unknown) => fns.reduce((acc, fn) => fn(acc), arg);
28
+ }
29
+
30
+ // Usage
31
+ const processUser = pipe(
32
+ validateInput,
33
+ normalizeEmail,
34
+ createUser,
35
+ );
36
+ ```
37
+
38
+ ### Option / Maybe Pattern
39
+
40
+ ```typescript
41
+ type Option<T> = Some<T> | None;
42
+ interface Some<T> { readonly _tag: "Some"; readonly value: T }
43
+ interface None { readonly _tag: "None" }
44
+
45
+ const some = <T>(value: T): Option<T> => ({ _tag: "Some", value });
46
+ const none: Option<never> = { _tag: "None" };
47
+
48
+ function map<A, B>(opt: Option<A>, fn: (a: A) => B): Option<B> {
49
+ return opt._tag === "Some" ? some(fn(opt.value)) : none;
50
+ }
51
+
52
+ function flatMap<A, B>(opt: Option<A>, fn: (a: A) => Option<B>): Option<B> {
53
+ return opt._tag === "Some" ? fn(opt.value) : none;
54
+ }
55
+
56
+ function getOrElse<T>(opt: Option<T>, fallback: () => T): T {
57
+ return opt._tag === "Some" ? opt.value : fallback();
58
+ }
59
+ ```
60
+
61
+ ### Exhaustive Pattern Matching
62
+
63
+ ```typescript
64
+ type Shape =
65
+ | { kind: "circle"; radius: number }
66
+ | { kind: "rect"; width: number; height: number }
67
+ | { kind: "triangle"; base: number; height: number };
68
+
69
+ function area(shape: Shape): number {
70
+ switch (shape.kind) {
71
+ case "circle":
72
+ return Math.PI * shape.radius ** 2;
73
+ case "rect":
74
+ return shape.width * shape.height;
75
+ case "triangle":
76
+ return (shape.base * shape.height) / 2;
77
+ default: {
78
+ const _exhaustive: never = shape;
79
+ throw new Error(`Unhandled shape: ${_exhaustive}`);
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Currying
86
+
87
+ ```typescript
88
+ function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
89
+ return (a) => (b) => fn(a, b);
90
+ }
91
+
92
+ const multiply = curry((a: number, b: number) => a * b);
93
+ const double = multiply(2);
94
+ double(5); // 10
95
+ ```
96
+
97
+ ## Builder Pattern
98
+
99
+ ### Type-Safe Builder with Required Fields
100
+
101
+ ```typescript
102
+ interface QueryConfig {
103
+ table: string;
104
+ select: string[];
105
+ where?: string;
106
+ limit?: number;
107
+ orderBy?: string;
108
+ }
109
+
110
+ type RequiredFields = "table" | "select";
111
+
112
+ class QueryBuilder<Built extends string = never> {
113
+ private config: Partial<QueryConfig> = {};
114
+
115
+ table(name: string): QueryBuilder<Built | "table"> {
116
+ this.config.table = name;
117
+ return this as QueryBuilder<Built | "table">;
118
+ }
119
+
120
+ select(...fields: string[]): QueryBuilder<Built | "select"> {
121
+ this.config.select = fields;
122
+ return this as QueryBuilder<Built | "select">;
123
+ }
124
+
125
+ where(condition: string): this {
126
+ this.config.where = condition;
127
+ return this;
128
+ }
129
+
130
+ limit(n: number): this {
131
+ this.config.limit = n;
132
+ return this;
133
+ }
134
+
135
+ // Only callable when all required fields are set
136
+ build(this: QueryBuilder<RequiredFields>): QueryConfig {
137
+ return this.config as QueryConfig;
138
+ }
139
+ }
140
+
141
+ // Usage — build() only available after table() and select()
142
+ const query = new QueryBuilder()
143
+ .table("users")
144
+ .select("id", "name")
145
+ .where("active = true")
146
+ .build(); // OK
147
+
148
+ // new QueryBuilder().select("id").build(); // Error — missing table()
149
+ ```
150
+
151
+ ### Fluent Configuration
152
+
153
+ ```typescript
154
+ function createServer() {
155
+ const config = {
156
+ port: 3000,
157
+ host: "localhost",
158
+ cors: false,
159
+ };
160
+
161
+ const builder = {
162
+ port(p: number) { config.port = p; return builder; },
163
+ host(h: string) { config.host = h; return builder; },
164
+ cors(enabled: boolean) { config.cors = enabled; return builder; },
165
+ build() { return Object.freeze(config); },
166
+ };
167
+
168
+ return builder;
169
+ }
170
+
171
+ const server = createServer()
172
+ .port(8080)
173
+ .cors(true)
174
+ .build();
175
+ ```
176
+
177
+ ## State Machines
178
+
179
+ ### Discriminated Union State Machine
180
+
181
+ ```typescript
182
+ type ConnectionState =
183
+ | { status: "disconnected" }
184
+ | { status: "connecting"; attempt: number }
185
+ | { status: "connected"; socket: WebSocket }
186
+ | { status: "error"; error: Error; retryAfter: number };
187
+
188
+ type ConnectionEvent =
189
+ | { type: "CONNECT" }
190
+ | { type: "CONNECTED"; socket: WebSocket }
191
+ | { type: "DISCONNECT" }
192
+ | { type: "ERROR"; error: Error };
193
+
194
+ function transition(
195
+ state: ConnectionState,
196
+ event: ConnectionEvent,
197
+ ): ConnectionState {
198
+ switch (state.status) {
199
+ case "disconnected":
200
+ if (event.type === "CONNECT") {
201
+ return { status: "connecting", attempt: 1 };
202
+ }
203
+ return state;
204
+
205
+ case "connecting":
206
+ if (event.type === "CONNECTED") {
207
+ return { status: "connected", socket: event.socket };
208
+ }
209
+ if (event.type === "ERROR") {
210
+ return {
211
+ status: "error",
212
+ error: event.error,
213
+ retryAfter: state.attempt * 1000,
214
+ };
215
+ }
216
+ return state;
217
+
218
+ case "connected":
219
+ if (event.type === "DISCONNECT") {
220
+ return { status: "disconnected" };
221
+ }
222
+ if (event.type === "ERROR") {
223
+ return { status: "error", error: event.error, retryAfter: 1000 };
224
+ }
225
+ return state;
226
+
227
+ case "error":
228
+ if (event.type === "CONNECT") {
229
+ return { status: "connecting", attempt: 1 };
230
+ }
231
+ return state;
232
+ }
233
+ }
234
+ ```
235
+
236
+ ## Immutable Data Patterns
237
+
238
+ ### Immutable Updates
239
+
240
+ ```typescript
241
+ // Spread for shallow updates
242
+ function updateUser(user: Readonly<User>, patch: Partial<User>): User {
243
+ return { ...user, ...patch };
244
+ }
245
+
246
+ // Nested updates with helper
247
+ function updateNested<T extends object, K extends keyof T>(
248
+ obj: Readonly<T>,
249
+ key: K,
250
+ updater: (value: T[K]) => T[K],
251
+ ): T {
252
+ return { ...obj, [key]: updater(obj[key]) };
253
+ }
254
+
255
+ // Usage
256
+ const updated = updateNested(state, "settings", (s) => ({
257
+ ...s,
258
+ theme: "dark",
259
+ }));
260
+ ```
261
+
262
+ ### Readonly Collections
263
+
264
+ ```typescript
265
+ // Immutable map operations
266
+ function mapSet<K, V>(
267
+ map: ReadonlyMap<K, V>,
268
+ key: K,
269
+ value: V,
270
+ ): ReadonlyMap<K, V> {
271
+ const copy = new Map(map);
272
+ copy.set(key, value);
273
+ return copy;
274
+ }
275
+
276
+ function mapDelete<K, V>(
277
+ map: ReadonlyMap<K, V>,
278
+ key: K,
279
+ ): ReadonlyMap<K, V> {
280
+ const copy = new Map(map);
281
+ copy.delete(key);
282
+ return copy;
283
+ }
284
+ ```
285
+
286
+ ### `as const` for Frozen Data
287
+
288
+ ```typescript
289
+ const PERMISSIONS = {
290
+ admin: ["read", "write", "delete"],
291
+ editor: ["read", "write"],
292
+ viewer: ["read"],
293
+ } as const;
294
+
295
+ // Type is deeply readonly with literal types
296
+ type Role = keyof typeof PERMISSIONS;
297
+ type Permission = (typeof PERMISSIONS)[Role][number];
298
+ // "read" | "write" | "delete"
299
+ ```
300
+
301
+ ## Async Patterns
302
+
303
+ ### Typed Async Utilities
304
+
305
+ ```typescript
306
+ // Promise with timeout
307
+ function withTimeout<T>(
308
+ promise: Promise<T>,
309
+ ms: number,
310
+ ): Promise<T> {
311
+ const timeout = new Promise<never>((_, reject) =>
312
+ setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms),
313
+ );
314
+ return Promise.race([promise, timeout]);
315
+ }
316
+
317
+ // Retry with backoff
318
+ async function retry<T>(
319
+ fn: () => Promise<T>,
320
+ options: { attempts: number; delay: number },
321
+ ): Promise<T> {
322
+ for (let i = 0; i < options.attempts; i++) {
323
+ try {
324
+ return await fn();
325
+ } catch (error) {
326
+ if (i === options.attempts - 1) throw error;
327
+ await new Promise((r) => setTimeout(r, options.delay * 2 ** i));
328
+ }
329
+ }
330
+ throw new Error("Unreachable");
331
+ }
332
+ ```
333
+
334
+ ### AsyncIterable Patterns
335
+
336
+ ```typescript
337
+ // Async generator for paginated APIs
338
+ async function* paginate<T>(
339
+ fetchPage: (cursor?: string) => Promise<{ data: T[]; next?: string }>,
340
+ ): AsyncGenerator<T> {
341
+ let cursor: string | undefined;
342
+ do {
343
+ const page = await fetchPage(cursor);
344
+ yield* page.data;
345
+ cursor = page.next;
346
+ } while (cursor);
347
+ }
348
+
349
+ // Usage
350
+ for await (const user of paginate(fetchUsers)) {
351
+ process(user);
352
+ }
353
+ ```
354
+
355
+ ### Concurrent Task Limiter
356
+
357
+ ```typescript
358
+ async function mapConcurrent<T, R>(
359
+ items: readonly T[],
360
+ fn: (item: T) => Promise<R>,
361
+ concurrency: number,
362
+ ): Promise<R[]> {
363
+ const results: R[] = [];
364
+ const executing = new Set<Promise<void>>();
365
+
366
+ for (const [index, item] of items.entries()) {
367
+ const task = fn(item).then((result) => {
368
+ results[index] = result;
369
+ });
370
+
371
+ const tracked = task.then(() => executing.delete(tracked));
372
+ executing.add(tracked);
373
+
374
+ if (executing.size >= concurrency) {
375
+ await Promise.race(executing);
376
+ }
377
+ }
378
+
379
+ await Promise.all(executing);
380
+ return results;
381
+ }
382
+ ```
383
+
384
+ ## Dependency Injection
385
+
386
+ ### Function-Based DI
387
+
388
+ ```typescript
389
+ // Define dependencies as an interface
390
+ interface Dependencies {
391
+ logger: Logger;
392
+ db: Database;
393
+ cache: Cache;
394
+ }
395
+
396
+ // Functions accept dependencies explicitly
397
+ function createUserService(deps: Dependencies) {
398
+ return {
399
+ async getUser(id: string): Promise<User | null> {
400
+ const cached = await deps.cache.get<User>(`user:${id}`);
401
+ if (cached) return cached;
402
+
403
+ const user = await deps.db.query<User>("SELECT * FROM users WHERE id = $1", [id]);
404
+ if (user) await deps.cache.set(`user:${id}`, user);
405
+ return user;
406
+ },
407
+ };
408
+ }
409
+
410
+ // Wire up at composition root
411
+ const deps: Dependencies = { logger, db, cache };
412
+ const userService = createUserService(deps);
413
+ ```
414
+
415
+ ### Token-Based DI (for larger applications)
416
+
417
+ ```typescript
418
+ // Branded tokens for type safety
419
+ type Token<T> = symbol & { __type: T };
420
+
421
+ function createToken<T>(description: string): Token<T> {
422
+ return Symbol(description) as Token<T>;
423
+ }
424
+
425
+ const TOKENS = {
426
+ Logger: createToken<Logger>("Logger"),
427
+ Database: createToken<Database>("Database"),
428
+ } as const;
429
+
430
+ class Container {
431
+ private bindings = new Map<symbol, unknown>();
432
+
433
+ bind<T>(token: Token<T>, factory: () => T): void {
434
+ this.bindings.set(token, factory);
435
+ }
436
+
437
+ get<T>(token: Token<T>): T {
438
+ const factory = this.bindings.get(token) as (() => T) | undefined;
439
+ if (!factory) throw new Error(`No binding for ${token.toString()}`);
440
+ return factory();
441
+ }
442
+ }
443
+ ```
444
+
445
+ ## Module Organization
446
+
447
+ ### Feature-Based Structure
448
+
449
+ ```
450
+ src/
451
+ ├── features/
452
+ │ ├── users/
453
+ │ │ ├── index.ts # Public API
454
+ │ │ ├── types.ts # Feature types
455
+ │ │ ├── service.ts # Business logic
456
+ │ │ ├── repository.ts # Data access
457
+ │ │ └── validation.ts # Input validation
458
+ │ └── orders/
459
+ │ ├── index.ts
460
+ │ └── ...
461
+ ├── shared/
462
+ │ ├── types.ts # Shared types
463
+ │ ├── errors.ts # Error classes
464
+ │ └── utils.ts # Pure utility functions
465
+ └── index.ts # App entry point
466
+ ```
467
+
468
+ ### Encapsulation via Index Exports
469
+
470
+ ```typescript
471
+ // features/users/index.ts — public API only
472
+ export { createUser, getUser, updateUser } from "./service.js";
473
+ export type { User, CreateUserInput } from "./types.js";
474
+
475
+ // Internal modules (service.ts, repository.ts) are NOT exported
476
+ // Other features import only from the index
477
+ ```