qstd 0.3.56 → 0.3.58
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,1026 @@
|
|
|
1
|
+
# Atomic Modules
|
|
2
|
+
|
|
3
|
+
A compact module structure that keeps concerns clear and greppable. Each module is "atomic": small, focused, and split into predictable files.
|
|
4
|
+
|
|
5
|
+
**Core principle:** Simplicity through consistency. Complexity is managed by predictable patterns, not eliminated by clever abstractions.
|
|
6
|
+
|
|
7
|
+
> **Updating this document:** Blend new guidance into existing sections when possible. Only create new sections when content truly doesn't fit elsewhere. Enhance rather than expand.
|
|
8
|
+
|
|
9
|
+
## Module anatomy
|
|
10
|
+
|
|
11
|
+
**Only `index.ts` is required.** All other files are optional and added as needed:
|
|
12
|
+
|
|
13
|
+
- `index.ts`: **[REQUIRED]** orchestrates and exports the module's public API
|
|
14
|
+
- `types.ts`: **[OPTIONAL]** shared types and interfaces
|
|
15
|
+
- `literals.ts`: **[OPTIONAL]** constants and tunables
|
|
16
|
+
- `fns.ts`: **[OPTIONAL]** pure helper functions/utilities
|
|
17
|
+
- `domain.ts`: **[OPTIONAL]** business/domain logic orchestrator; wraps specialized modules with instrumentation, error handling, and business rules
|
|
18
|
+
- **Specialized modules**: **[OPTIONAL]** `ast.ts`, `parser.ts`, `compiler.ts`, `tokenize.ts`, etc. - focused implementations for complex features
|
|
19
|
+
|
|
20
|
+
**For small modules:** Just put everything in `index.ts` (or `index.tsx` for components). The atomic structure is useful for complex features, not simple utilities.
|
|
21
|
+
|
|
22
|
+
**When to split into specialized modules:** When you have a complex feature (like a parser, compiler, or state machine) with 300+ lines of logic:
|
|
23
|
+
|
|
24
|
+
- Split into focused modules: `ast.ts`, `parse-blocks.ts`, `parse-inline.ts`, `tokenize.ts`, etc.
|
|
25
|
+
- Use `domain.ts` as the orchestrator (adds debugging, error handling, business rules)
|
|
26
|
+
- **Skip `fns.ts`** - it's redundant when you have specialized modules
|
|
27
|
+
- Each specialized module should be self-contained and focused on one responsibility
|
|
28
|
+
|
|
29
|
+
**When to use fns.ts:** Only when you have a moderate amount of logic (< 300 lines) that fits in one file. If your logic is complex enough to need multiple specialized modules, skip `fns.ts` and use `domain.ts` or `index.ts` as the orchestrator.
|
|
30
|
+
|
|
31
|
+
### Deciding between fns.ts and domain.ts
|
|
32
|
+
|
|
33
|
+
When a module grows past ~300 lines, split into `fns.ts` and `domain.ts` based on cognitive load, not just line count. Categorize functions by their nature:
|
|
34
|
+
|
|
35
|
+
**fns.ts — Pure utilities with no business meaning:**
|
|
36
|
+
|
|
37
|
+
- Data transformations (reshape data structures)
|
|
38
|
+
- Formatters (convert to display strings)
|
|
39
|
+
- Simple accessors (extract values from structures)
|
|
40
|
+
- String/template builders
|
|
41
|
+
|
|
42
|
+
**domain.ts — Business logic with semantic meaning:**
|
|
43
|
+
|
|
44
|
+
- Math/algorithms that encode business rules (ELO calculations, scoring formulas)
|
|
45
|
+
- Generators that implement business constraints (matchup generation, judge assignment)
|
|
46
|
+
- State machines and workflow logic
|
|
47
|
+
- Functions that could have different implementations in different business contexts
|
|
48
|
+
|
|
49
|
+
**The litmus test:** If changing a function requires understanding the business domain (not just the data shape), it belongs in `domain.ts`. If it's a generic transformation that could work in any context, it belongs in `fns.ts`.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// fns.ts - Pure utilities, no business knowledge needed
|
|
53
|
+
export function formatEloWithDelta(current: number, baseline: number): string { ... }
|
|
54
|
+
export function buildScoringPrompt(query: string, drafts: Draft[]): string { ... }
|
|
55
|
+
|
|
56
|
+
// domain.ts - Business logic, requires domain understanding
|
|
57
|
+
export function updateElo(ratingA: number, ratingB: number, winner: Winner): [number, number] { ... }
|
|
58
|
+
export function assignJudges(modelIds: string[], matchups: Matchup[]): Assignment[] { ... }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Keep things where they belong
|
|
62
|
+
|
|
63
|
+
**If a file already exists, use it.** Don't define types in `fns.ts` if `types.ts` exists. Don't put constants in `domain.ts` if `literals.ts` exists.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// ❌ BAD: Defining types in fns.ts when types.ts exists
|
|
67
|
+
// fns.ts
|
|
68
|
+
export interface InsertionPoint { ... } // Why is this here?
|
|
69
|
+
export function insertMediaBlock(...) { ... }
|
|
70
|
+
|
|
71
|
+
// ✅ GOOD: Types in types.ts, functions in fns.ts
|
|
72
|
+
// types.ts
|
|
73
|
+
export interface InsertionPoint { ... }
|
|
74
|
+
|
|
75
|
+
// fns.ts
|
|
76
|
+
import * as _t from "./types";
|
|
77
|
+
export function insertMediaBlock(content: _t.ContentBlock[], point: _t.InsertionPoint, ...) { ... }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**The rule:** Once you've split a module into separate files, respect that structure. Each file has a purpose:
|
|
81
|
+
|
|
82
|
+
- Types/interfaces → `types.ts`
|
|
83
|
+
- Constants/config → `literals.ts`
|
|
84
|
+
- Pure functions → `fns.ts`
|
|
85
|
+
- Business logic → `domain.ts`
|
|
86
|
+
|
|
87
|
+
**Exceptions:**
|
|
88
|
+
|
|
89
|
+
- Small modules can keep everything in `index.ts` until they grow
|
|
90
|
+
- Inline function argument types are fine (see Function arguments section)
|
|
91
|
+
|
|
92
|
+
### Component & Hook Exports
|
|
93
|
+
|
|
94
|
+
- **Components:** Export as default function statements.
|
|
95
|
+
```tsx
|
|
96
|
+
export default function Recorder() { ... }
|
|
97
|
+
```
|
|
98
|
+
- **Hooks:** Export as default function statements.
|
|
99
|
+
```ts
|
|
100
|
+
export default function useAudioRecorder() { ... }
|
|
101
|
+
```
|
|
102
|
+
- **Filenames:** Always lowercase kebab-case, matching the export where possible.
|
|
103
|
+
- Import example: `import Recorder from "components/molecules/recorder"`
|
|
104
|
+
|
|
105
|
+
### Internal module imports
|
|
106
|
+
|
|
107
|
+
**Use namespace imports with underscore prefixes** for internal modules:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// Standard module imports (always these prefixes)
|
|
111
|
+
import * as _d from "./domain";
|
|
112
|
+
import * as _l from "./literals";
|
|
113
|
+
import * as _t from "./types";
|
|
114
|
+
import * as _f from "./fns";
|
|
115
|
+
|
|
116
|
+
// Specialized module imports (use descriptive underscore prefix)
|
|
117
|
+
import * as _ast from "./ast";
|
|
118
|
+
import * as _parser from "./parser";
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Why namespace imports with underscores:**
|
|
122
|
+
|
|
123
|
+
- **Greppable**: Easy to find all uses of a module (search for `_ast.`)
|
|
124
|
+
- **Refactor-safe**: Change internal structure without breaking imports
|
|
125
|
+
- **No naming conflicts**: `_ast` never conflicts with other imports
|
|
126
|
+
- **Clear boundaries**: Easy to see what's from which module
|
|
127
|
+
|
|
128
|
+
**Rules:**
|
|
129
|
+
|
|
130
|
+
- ❌ **NEVER destructure imports from internal modules**: `import { parse } from "./parser"`
|
|
131
|
+
- ✅ **ALWAYS use namespace imports**: `import * as _parser from "./parser"`
|
|
132
|
+
- Standard prefixes: `_t` (types), `_l` (literals), `_f` (fns), `_d` (domain)
|
|
133
|
+
|
|
134
|
+
### Re-exports in index.ts
|
|
135
|
+
|
|
136
|
+
**Default to bulk exports.** Use `export * from` unless you need to hide internals:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// ✅ GOOD: Bulk exports (default for most modules)
|
|
140
|
+
export type * from "./types";
|
|
141
|
+
export * from "./literals";
|
|
142
|
+
export * from "./fns";
|
|
143
|
+
|
|
144
|
+
// ✅ GOOD: Selective exports (when some things are private)
|
|
145
|
+
export type { Token, AST } from "./types"; // Only public types
|
|
146
|
+
export { parse, compile } from "./fns"; // Only public functions
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**When to use selective exports:**
|
|
150
|
+
|
|
151
|
+
- Module has internal-only types/functions not meant for consumers
|
|
152
|
+
- You're intentionally limiting the public API surface
|
|
153
|
+
- Performance concerns (tree-shaking complex modules)
|
|
154
|
+
|
|
155
|
+
**When to use bulk exports:**
|
|
156
|
+
|
|
157
|
+
- All types/functions are meant for public consumption
|
|
158
|
+
- You want flexibility to add exports without updating index.ts
|
|
159
|
+
- Early development when the API is still evolving
|
|
160
|
+
|
|
161
|
+
## Path aliases: Always use them
|
|
162
|
+
|
|
163
|
+
**Never use relative paths with `../`.** Always use path aliases configured in tsconfig.json.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// ✅ GOOD: Path aliases
|
|
167
|
+
import * as Entry from "entities/entry";
|
|
168
|
+
import useMediaUpload from "hooks/use-media-upload";
|
|
169
|
+
import * as Recording from "storage/recording";
|
|
170
|
+
|
|
171
|
+
// ❌ AVOID: Relative paths
|
|
172
|
+
import * as Entry from "../../entities/entry";
|
|
173
|
+
import useMediaUpload from "../use-media-upload";
|
|
174
|
+
import * as Recording from "../../../storage/recording";
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Why path aliases?**
|
|
178
|
+
|
|
179
|
+
- **Refactor-safe**: Move files without updating imports
|
|
180
|
+
- **Readable**: Clear where things come from
|
|
181
|
+
- **Consistent**: Same import path from anywhere in the codebase
|
|
182
|
+
- **Shorter**: No counting `../` levels
|
|
183
|
+
|
|
184
|
+
### Two-segment import rule
|
|
185
|
+
|
|
186
|
+
Keep imports to two segments max. Express complexity through dot notation, not deeper paths.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// ✅ GOOD: Two-segment imports
|
|
190
|
+
import * as md from "features/markdown";
|
|
191
|
+
import * as Entry from "entities/entry";
|
|
192
|
+
|
|
193
|
+
// ❌ AVOID: Three+ segment imports
|
|
194
|
+
import * as compilers from "features/markdown/compilers";
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Consuming modules
|
|
198
|
+
|
|
199
|
+
### Entities and features: Namespace imports
|
|
200
|
+
|
|
201
|
+
Always use `* as` namespace imports for entities and feature modules:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// ✅ GOOD: Namespace imports
|
|
205
|
+
import * as Entry from "entities/entry";
|
|
206
|
+
import * as User from "entities/user";
|
|
207
|
+
import * as Recording from "storage/recording";
|
|
208
|
+
|
|
209
|
+
// Usage
|
|
210
|
+
Entry.insertMediaBlock(content, point, galleryId, mediaIds);
|
|
211
|
+
User.getCurrentUser();
|
|
212
|
+
Recording.save(data);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// ❌ AVOID: Destructuring
|
|
217
|
+
import { insertMediaBlock } from "entities/entry";
|
|
218
|
+
import { getCurrentUser } from "entities/user";
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Why?**
|
|
222
|
+
|
|
223
|
+
- **Origin is clear**: `Entry.insertMediaBlock()` vs orphaned `insertMediaBlock()`
|
|
224
|
+
- **No naming conflicts**: Multiple modules can have similar function names
|
|
225
|
+
- **Discoverable**: Type `Entry.` to see all available functions
|
|
226
|
+
|
|
227
|
+
### Hooks: Namespace the return value
|
|
228
|
+
|
|
229
|
+
When consuming hooks, assign to a namespace variable (the "noun" of the hook). **Never destructure.**
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// ✅ GOOD: Namespace variable
|
|
233
|
+
const recorder = useAudioRecorder();
|
|
234
|
+
const uploader = useMediaUpload();
|
|
235
|
+
|
|
236
|
+
// Usage
|
|
237
|
+
if (recorder.isRecording) { ... }
|
|
238
|
+
recorder.start();
|
|
239
|
+
uploader.uploadFiles(files, params);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// ❌ AVOID: Destructuring
|
|
244
|
+
const { isRecording, start } = useAudioRecorder();
|
|
245
|
+
const { uploadFiles, status } = useMediaUpload();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Why?**
|
|
249
|
+
|
|
250
|
+
- **Clarity at call site**: `recorder.start()` is more explicit than `start()`
|
|
251
|
+
- **Avoids naming conflicts**: Multiple hooks with similar methods
|
|
252
|
+
- **Grouping**: Related functionality stays visibly grouped
|
|
253
|
+
- **Discovery**: Typing `recorder.` triggers autocomplete
|
|
254
|
+
|
|
255
|
+
## Writing hooks
|
|
256
|
+
|
|
257
|
+
### Event listener cleanup: Use AbortController
|
|
258
|
+
|
|
259
|
+
**Always use `AbortController` for event listener cleanup in effects.** It's cleaner than manual `removeEventListener` and handles multiple listeners automatically.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// ✅ GOOD: AbortController for clean cleanup
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
const container = containerRef.current;
|
|
265
|
+
if (!container) return;
|
|
266
|
+
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
const signal = controller.signal;
|
|
269
|
+
|
|
270
|
+
const handleScroll = () => { ... };
|
|
271
|
+
const handleResize = () => { ... };
|
|
272
|
+
|
|
273
|
+
container.addEventListener("scroll", handleScroll, { passive: true, signal });
|
|
274
|
+
window.addEventListener("resize", handleResize, { signal });
|
|
275
|
+
|
|
276
|
+
return () => controller.abort();
|
|
277
|
+
}, [deps]);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// ❌ AVOID: Manual removeEventListener
|
|
282
|
+
React.useEffect(() => {
|
|
283
|
+
const container = containerRef.current;
|
|
284
|
+
if (!container) return;
|
|
285
|
+
|
|
286
|
+
const handleScroll = () => { ... };
|
|
287
|
+
const handleResize = () => { ... };
|
|
288
|
+
|
|
289
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
290
|
+
window.addEventListener("resize", handleResize);
|
|
291
|
+
|
|
292
|
+
return () => {
|
|
293
|
+
container.removeEventListener("scroll", handleScroll);
|
|
294
|
+
window.removeEventListener("resize", handleResize);
|
|
295
|
+
};
|
|
296
|
+
}, [deps]);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Why AbortController?**
|
|
300
|
+
|
|
301
|
+
- **Single cleanup call**: `controller.abort()` removes all listeners at once
|
|
302
|
+
- **Less error-prone**: Can't forget to remove a listener or mismatch handler references
|
|
303
|
+
- **Works everywhere**: Same pattern for window, document, elements, and even fetch requests
|
|
304
|
+
- **Composable**: Pass the signal to multiple listeners or async operations
|
|
305
|
+
|
|
306
|
+
**With timeouts:** AbortController handles listeners, but timeouts still need manual cleanup:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
React.useEffect(() => {
|
|
310
|
+
const controller = new AbortController();
|
|
311
|
+
const signal = controller.signal;
|
|
312
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
313
|
+
|
|
314
|
+
window.addEventListener(
|
|
315
|
+
"scroll",
|
|
316
|
+
() => {
|
|
317
|
+
if (timeout) clearTimeout(timeout);
|
|
318
|
+
timeout = setTimeout(handleSnapDecision, 150);
|
|
319
|
+
},
|
|
320
|
+
{ signal }
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return () => {
|
|
324
|
+
controller.abort();
|
|
325
|
+
if (timeout) clearTimeout(timeout);
|
|
326
|
+
};
|
|
327
|
+
}, [deps]);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### React state naming
|
|
331
|
+
|
|
332
|
+
Prefer `store`/`setStore` over `state`/`setState` for local React state:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// ✅ GOOD: Clear naming
|
|
336
|
+
const [store, setStore] = React.useState<_t.Upload>({ ... });
|
|
337
|
+
|
|
338
|
+
// ❌ AVOID: Generic "state"
|
|
339
|
+
const [state, setState] = React.useState<_t.Upload>({ ... });
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Ref comments: Explain the purpose
|
|
343
|
+
|
|
344
|
+
**Always add a comment above each ref explaining its purpose in the app.** Refs often point to invisible DOM elements (sentinels, markers) or serve non-obvious roles. A brief comment saves readers from tracing through the code to understand what each ref is for.
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
// ✅ GOOD: Each ref has a purpose comment
|
|
348
|
+
// Container ref for auto-scroll to follow highlighted word during playback
|
|
349
|
+
const entryRef = React.useRef<HTMLDivElement>(null);
|
|
350
|
+
// Zero-height sentinel at top of entry; when it scrolls out, the header is sticky
|
|
351
|
+
const headerSentinelRef = React.useRef<HTMLDivElement | null>(null);
|
|
352
|
+
// Marker at bottom of content; used to calculate dynamic fade padding for sticky footer
|
|
353
|
+
const contentEndRef = React.useRef<HTMLDivElement | null>(null);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
// ❌ BAD: No context for what these refs do
|
|
358
|
+
const entryRef = React.useRef<HTMLDivElement>(null);
|
|
359
|
+
const headerSentinelRef = React.useRef<HTMLDivElement | null>(null);
|
|
360
|
+
const contentEndRef = React.useRef<HTMLDivElement | null>(null);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Why comment refs?**
|
|
364
|
+
|
|
365
|
+
- **Invisible elements**: Sentinel divs have `height: 0`—comments explain their role
|
|
366
|
+
- **Non-obvious connections**: Refs often connect to IntersectionObservers or scroll handlers defined elsewhere
|
|
367
|
+
- **Quick orientation**: New readers can understand the component's DOM strategy at a glance
|
|
368
|
+
|
|
369
|
+
### Hook return values: Spread state
|
|
370
|
+
|
|
371
|
+
**Spread state properties directly** in the return. Don't wrap in a `state` object—consumers shouldn't need `hook.state.isLoading`.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// ✅ GOOD: Spread state for flat access
|
|
375
|
+
export default function useMediaUpload() {
|
|
376
|
+
const [store, setStore] = React.useState<_t.Upload>({
|
|
377
|
+
status: "idle",
|
|
378
|
+
progress: { loaded: 0, total: 0, percent: 0 },
|
|
379
|
+
uploadedMedia: [],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const uploadFiles = async (...) => { ... };
|
|
383
|
+
const reset = () => { ... };
|
|
384
|
+
|
|
385
|
+
return { ...store, uploadFiles, reset };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Consumer gets flat access:
|
|
389
|
+
const uploader = useMediaUpload();
|
|
390
|
+
uploader.status; // ✅ Direct
|
|
391
|
+
uploader.progress; // ✅ Direct
|
|
392
|
+
uploader.uploadFiles(); // ✅ Direct
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// ❌ AVOID: Nested state object
|
|
397
|
+
return { state, uploadFiles, reset };
|
|
398
|
+
|
|
399
|
+
// Consumer needs extra nesting:
|
|
400
|
+
uploader.state.status; // ❌ Extra layer
|
|
401
|
+
uploader.state.progress; // ❌ Extra layer
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Why spread?**
|
|
405
|
+
|
|
406
|
+
- **Simpler consumer API**: `uploader.status` not `uploader.state.status`
|
|
407
|
+
- **Consistent access pattern**: All properties at same level
|
|
408
|
+
- **Better autocomplete**: Flat object shows all options immediately
|
|
409
|
+
|
|
410
|
+
## Naming conventions
|
|
411
|
+
|
|
412
|
+
### Clarity over brevity
|
|
413
|
+
|
|
414
|
+
Choose names that reveal intent:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// ❌ Too generic
|
|
418
|
+
md.process(data);
|
|
419
|
+
md.handle(thing);
|
|
420
|
+
|
|
421
|
+
// ✅ Clear intent
|
|
422
|
+
md.ast.parse(markdown);
|
|
423
|
+
md.compilers.compile(ast);
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Avoid redundant suffixes
|
|
427
|
+
|
|
428
|
+
**Don't use "State", "Type", "Data", "Info", "Object"** when avoidable:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// ❌ AVOID: Redundant suffixes
|
|
432
|
+
interface PlayerState { ... }
|
|
433
|
+
type RecordingType = "audio" | "video";
|
|
434
|
+
const configObject = { ... };
|
|
435
|
+
|
|
436
|
+
// ✅ PREFER: Clean names
|
|
437
|
+
interface Player { ... }
|
|
438
|
+
type Recording = "audio" | "video";
|
|
439
|
+
const config = { ... };
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**When suffixes ARE appropriate:**
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// ✅ OK: Distinguishing related concepts
|
|
446
|
+
interface PlayerConfig { ... } // Configuration
|
|
447
|
+
interface PlayerControls { ... } // Control methods
|
|
448
|
+
|
|
449
|
+
// ✅ OK: Unit disambiguation
|
|
450
|
+
type DurationMs = number;
|
|
451
|
+
type TokenId = string;
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Document type fields with JSDoc
|
|
455
|
+
|
|
456
|
+
**Add JSDoc to fields when the name is generic but the value has specific meaning.** Especially `id` fields with particular formats, foreign key references, or `string`/`number` fields with constraints.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
// ❌ BAD: What format is id? What do draftA/draftB contain?
|
|
460
|
+
export type PairwiseMatchup = {
|
|
461
|
+
draftA: string;
|
|
462
|
+
draftB: string;
|
|
463
|
+
id: string;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// ✅ GOOD: Hover reveals meaning
|
|
467
|
+
export type PairwiseMatchup = {
|
|
468
|
+
/** Model ID of the first draft (shown as "A" to judges) */
|
|
469
|
+
draftA: string;
|
|
470
|
+
/** Model ID of the second draft (shown as "B" to judges) */
|
|
471
|
+
draftB: string;
|
|
472
|
+
/** Unique matchup identifier, e.g. "claude-3-5-sonnet-vs-gpt-4o" */
|
|
473
|
+
id: string;
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### File naming: Lowercase kebab-case
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
✅ GOOD
|
|
481
|
+
use-audio-player.tsx
|
|
482
|
+
audio-recorder.tsx
|
|
483
|
+
sync-recordings.ts
|
|
484
|
+
|
|
485
|
+
❌ AVOID
|
|
486
|
+
useAudioPlayer.tsx # camelCase
|
|
487
|
+
AudioRecorder.tsx # PascalCase
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Coding style
|
|
491
|
+
|
|
492
|
+
### Never use IIFEs
|
|
493
|
+
|
|
494
|
+
**Never use Immediately Invoked Function Expressions.** They're ugly, hard to read, and always avoidable.
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
// ❌ NEVER: IIFE in useEffect
|
|
498
|
+
React.useEffect(() => {
|
|
499
|
+
void (async () => {
|
|
500
|
+
const data = await fetchData();
|
|
501
|
+
setData(data);
|
|
502
|
+
})();
|
|
503
|
+
}, []);
|
|
504
|
+
|
|
505
|
+
// ✅ GOOD: Promise chain
|
|
506
|
+
React.useEffect(() => {
|
|
507
|
+
fetchData().then(setData).catch(console.error);
|
|
508
|
+
}, []);
|
|
509
|
+
|
|
510
|
+
// ✅ GOOD: Extract to named function if complex
|
|
511
|
+
React.useEffect(() => {
|
|
512
|
+
const loadData = async () => {
|
|
513
|
+
const data = await fetchData();
|
|
514
|
+
const processed = transform(data);
|
|
515
|
+
setData(processed);
|
|
516
|
+
};
|
|
517
|
+
loadData().catch(console.error);
|
|
518
|
+
}, []);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Why IIFEs are banned:**
|
|
522
|
+
|
|
523
|
+
- **Visual noise**: Extra parentheses and invocation clutter the code
|
|
524
|
+
- **Confusing intent**: `void (async () => { ... })()` is cryptic to newcomers
|
|
525
|
+
- **Always avoidable**: Promise chains or extracted functions are clearer
|
|
526
|
+
- **Anti-pattern**: If you need an IIFE, your code structure is wrong
|
|
527
|
+
|
|
528
|
+
**The rule:** If you're reaching for an IIFE, refactor instead. Extract a named function or use promise chains.
|
|
529
|
+
|
|
530
|
+
### Prefer Maps for key-based lookups
|
|
531
|
+
|
|
532
|
+
When aggregating or deduplicating items by a key, use `Map` instead of array methods like `findIndex`. Maps provide O(1) lookups vs O(n) scans.
|
|
533
|
+
|
|
534
|
+
**Initialize Maps with mapped tuples** to leverage TypeScript inference:
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// ❌ AVOID: Manual loop + explicit types
|
|
538
|
+
const claimMap = new Map<string, Claim>();
|
|
539
|
+
for (const claim of existingClaims) {
|
|
540
|
+
claimMap.set(claim.text.toLowerCase(), claim);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ✅ GOOD: Tuple initialization, types inferred
|
|
544
|
+
const claimMap = new Map(
|
|
545
|
+
existingClaims.map((claim) => [claim.text.toLowerCase(), claim])
|
|
546
|
+
);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Why tuple initialization?**
|
|
550
|
+
|
|
551
|
+
- TypeScript infers `Map<string, Claim>` from the tuple shape
|
|
552
|
+
- More concise—one expression instead of loop + set
|
|
553
|
+
- Declarative style fits functional patterns
|
|
554
|
+
|
|
555
|
+
**Full pattern for merging/deduplicating:**
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
export function mergeClaims(existing: Claim[], incoming: Claim[]): Claim[] {
|
|
559
|
+
const claimMap = new Map(existing.map((c) => [c.text.toLowerCase(), c]));
|
|
560
|
+
|
|
561
|
+
for (const claim of incoming) {
|
|
562
|
+
const key = claim.text.toLowerCase();
|
|
563
|
+
const found = claimMap.get(key);
|
|
564
|
+
if (found) {
|
|
565
|
+
claimMap.set(key, {
|
|
566
|
+
...found,
|
|
567
|
+
foundBy: [...found.foundBy, ...claim.foundBy],
|
|
568
|
+
});
|
|
569
|
+
} else {
|
|
570
|
+
claimMap.set(key, claim);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return Array.from(claimMap.values());
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Prefer toSorted over spread + sort
|
|
579
|
+
|
|
580
|
+
For browser-only code, use `toSorted()` instead of `[...array].sort()`. It's cleaner and doesn't require the spread.
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
// ❌ AVOID: Spread then sort
|
|
584
|
+
const shuffled = [...matchups].sort(() => Math.random() - 0.5);
|
|
585
|
+
const ranked = [...drafts].sort((a, b) => b.score - a.score);
|
|
586
|
+
|
|
587
|
+
// ✅ GOOD: toSorted (ES2023, supported in all modern browsers)
|
|
588
|
+
const shuffled = matchups.toSorted(() => Math.random() - 0.5);
|
|
589
|
+
const ranked = drafts.toSorted((a, b) => b.score - a.score);
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Why toSorted?**
|
|
593
|
+
|
|
594
|
+
- Cleaner syntax—no spread ceremony
|
|
595
|
+
- Intent is clear: "give me a sorted copy"
|
|
596
|
+
- Same performance, fewer characters
|
|
597
|
+
|
|
598
|
+
**Note:** `toSorted()` is ES2023. For Node.js < 20 or older browsers, stick with spread + sort.
|
|
599
|
+
|
|
600
|
+
## Component props: Pass the object, not its fields
|
|
601
|
+
|
|
602
|
+
**When multiple props come from the same object, pass the object directly.**
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
// ❌ BAD: Prop explosion - extracting fields from the same object
|
|
606
|
+
<DrawerForMediaPicker
|
|
607
|
+
open={showMediaPicker}
|
|
608
|
+
onClose={handleClose}
|
|
609
|
+
entryId={entry.id}
|
|
610
|
+
entryCreatedAt={entry.createdAt}
|
|
611
|
+
currentContent={entry.content || []}
|
|
612
|
+
currentRevisedContent={entry.revisedContent}
|
|
613
|
+
insertionPoint={insertionPoint}
|
|
614
|
+
/>
|
|
615
|
+
|
|
616
|
+
// ✅ GOOD: Pass the object directly
|
|
617
|
+
<DrawerForMediaPicker
|
|
618
|
+
open={showMediaPicker}
|
|
619
|
+
onClose={handleClose}
|
|
620
|
+
entry={entry}
|
|
621
|
+
insertionPoint={insertionPoint}
|
|
622
|
+
/>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Why this is better:**
|
|
626
|
+
|
|
627
|
+
- **Cleaner call sites** - Less visual noise, easier to read
|
|
628
|
+
- **Flexible** - Need another field later? No prop changes needed
|
|
629
|
+
- **Less maintenance** - One prop instead of many to thread through
|
|
630
|
+
- **Type-safe** - TypeScript still catches misuse inside the component
|
|
631
|
+
|
|
632
|
+
**"But what about re-renders when unrelated fields change?"**
|
|
633
|
+
|
|
634
|
+
With React 19 compiler, this is handled automatically. Even without it, the re-render cost is negligible compared to the cognitive overhead of managing many props. Don't optimize for imaginary performance problems.
|
|
635
|
+
|
|
636
|
+
**The rule:** If you're passing 3+ props from the same object, just pass the object.
|
|
637
|
+
|
|
638
|
+
## Function arguments: The 1-2-3 Rule
|
|
639
|
+
|
|
640
|
+
**Self-documenting arguments eliminate inline comments.**
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// ❌ BAD: Requires comments
|
|
644
|
+
findActiveToken(ttsTokens, audio.currentTime, 0.12); // 0.12 = tolerance
|
|
645
|
+
|
|
646
|
+
// ✅ GOOD: Self-documenting
|
|
647
|
+
findActiveToken(audio.currentTime, {
|
|
648
|
+
tokens: ttsTokens,
|
|
649
|
+
toleranceInSecs: 0.12,
|
|
650
|
+
});
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**The pattern:**
|
|
654
|
+
|
|
655
|
+
1. **First arg** = Primary value (id, time, insertionPoint, etc.)
|
|
656
|
+
2. **Second arg** = Named `props` object (self-documenting)
|
|
657
|
+
3. **Third arg** = Optional config (rarely needed)
|
|
658
|
+
|
|
659
|
+
**Naming:** Always use `props` for the named object argument.
|
|
660
|
+
|
|
661
|
+
**Inline types:** For simple, one-off argument types, define them inline:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// ✅ GOOD: Simple props, inline type
|
|
665
|
+
export function insertMediaBlock(
|
|
666
|
+
insertionPoint: _t.InsertionPoint,
|
|
667
|
+
props: {
|
|
668
|
+
content: _t.ContentBlock[];
|
|
669
|
+
galleryId: string;
|
|
670
|
+
mediaIds: string[];
|
|
671
|
+
}
|
|
672
|
+
): _t.ContentBlock[] { ... }
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**When to extract to a separate type instead:**
|
|
676
|
+
|
|
677
|
+
- Type is reused by multiple functions
|
|
678
|
+
- Complex types (unions, generics, conditional types)
|
|
679
|
+
- Function overloads that share a type
|
|
680
|
+
- Type needs JSDoc documentation
|
|
681
|
+
|
|
682
|
+
**Exceptions:**
|
|
683
|
+
|
|
684
|
+
- Universal patterns: `map(array, fn)`, `filter(array, predicate)`
|
|
685
|
+
- Math operations: `clamp(value, min, max)`
|
|
686
|
+
- Single argument: `parseId(id)`, `normalize(text)`
|
|
687
|
+
- Two clear arguments: `compileNode(type, node)`
|
|
688
|
+
|
|
689
|
+
## Function documentation: Explain the why
|
|
690
|
+
|
|
691
|
+
**Every function should have a JSDoc comment that explains the business purpose—not just what it does, but why the app needs it.**
|
|
692
|
+
|
|
693
|
+
A function name like `contentToText` tells you it converts content to text, but that's obvious from the signature. What's not obvious: _why does the app need to flatten blocks into a string?_
|
|
694
|
+
|
|
695
|
+
````typescript
|
|
696
|
+
// ❌ BAD: Describes what (redundant with signature)
|
|
697
|
+
/**
|
|
698
|
+
* Converts content blocks to text.
|
|
699
|
+
*/
|
|
700
|
+
export const contentToText = (blocks: ContentBlock[]): string => { ... }
|
|
701
|
+
|
|
702
|
+
// ✅ GOOD: Explains why + shows how
|
|
703
|
+
/**
|
|
704
|
+
* Extracts plain text from content blocks for TTS generation.
|
|
705
|
+
*
|
|
706
|
+
* Journal entries are stored as structured blocks (text, diarized-text, media)
|
|
707
|
+
* to support rich editing and inline media. However, TTS services like
|
|
708
|
+
* ElevenLabs require plain text input. This function flattens the block
|
|
709
|
+
* structure into a single string, skipping media blocks since they can't
|
|
710
|
+
* be narrated.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```ts
|
|
714
|
+
* const blocks: ContentBlock[] = [
|
|
715
|
+
* { type: "text", value: "First paragraph." },
|
|
716
|
+
* { type: "media", galleryId: "g1", mediaIds: ["m1"] },
|
|
717
|
+
* { type: "diarized-text", segments: [
|
|
718
|
+
* { speaker: 0, text: "Hello", wordTimings: [] },
|
|
719
|
+
* { speaker: 1, text: "Hi there", wordTimings: [] }
|
|
720
|
+
* ]}
|
|
721
|
+
* ];
|
|
722
|
+
*
|
|
723
|
+
* contentToText(blocks);
|
|
724
|
+
* // Returns: "First paragraph.\n\nHello Hi there"
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
export const contentToText = (blocks: ContentBlock[]): string => { ... }
|
|
728
|
+
````
|
|
729
|
+
|
|
730
|
+
**The pattern:**
|
|
731
|
+
|
|
732
|
+
1. **First line:** One sentence explaining the business purpose (what problem it solves)
|
|
733
|
+
2. **Context paragraph:** Why this function exists—what system constraint or requirement makes it necessary
|
|
734
|
+
3. **@example block:** Show concrete input/output so readers can verify their understanding
|
|
735
|
+
|
|
736
|
+
**Keep comments in sync with code.** When you change a function's logic, update the JSDoc. Stale comments are worse than no comments—they actively mislead.
|
|
737
|
+
|
|
738
|
+
**When to document:**
|
|
739
|
+
|
|
740
|
+
- ✅ Domain/business functions in `fns.ts`, `domain.ts`
|
|
741
|
+
- ✅ Complex transformations or algorithms
|
|
742
|
+
- ✅ Functions with non-obvious behavior or edge cases
|
|
743
|
+
- ⚠️ Simple utilities can have shorter docs (one-liner is fine)
|
|
744
|
+
- ❌ Skip for trivial getters/setters or obvious wrappers
|
|
745
|
+
|
|
746
|
+
**Questions good JSDoc answers:**
|
|
747
|
+
|
|
748
|
+
- Why does this function exist? What problem does it solve?
|
|
749
|
+
- What system or service requires this transformation?
|
|
750
|
+
- What happens to different input types? (shown via example)
|
|
751
|
+
- Are there edge cases or gotchas?
|
|
752
|
+
|
|
753
|
+
## Module organization: Flat > Nested
|
|
754
|
+
|
|
755
|
+
**Prefer flat structure.** Avoid nested sub-modules unless absolutely necessary.
|
|
756
|
+
|
|
757
|
+
```
|
|
758
|
+
✅ GOOD: Flat structure
|
|
759
|
+
tts/
|
|
760
|
+
types.ts
|
|
761
|
+
literals.ts
|
|
762
|
+
fns.ts
|
|
763
|
+
domain.ts
|
|
764
|
+
index.ts
|
|
765
|
+
|
|
766
|
+
⚠️ RARELY needed: Nested only when truly independent
|
|
767
|
+
payments/
|
|
768
|
+
stripe/
|
|
769
|
+
index.ts
|
|
770
|
+
paypal/
|
|
771
|
+
index.ts
|
|
772
|
+
index.ts
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Rule of thumb:** If you're questioning whether to nest, flatten instead.
|
|
776
|
+
|
|
777
|
+
## Contracts: The shared kernel
|
|
778
|
+
|
|
779
|
+
When client and server need identical types, define them once in `packages/contracts`. This is the "shared kernel"—a small, stable set of types that both environments agree on.
|
|
780
|
+
|
|
781
|
+
### Why contracts?
|
|
782
|
+
|
|
783
|
+
In a monorepo with client and server, the same data structures often appear in both:
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
// Without contracts: duplicated types that drift apart
|
|
787
|
+
// packages/client/src/entities/entry/types.ts
|
|
788
|
+
interface WordTiming {
|
|
789
|
+
start: number;
|
|
790
|
+
end: number;
|
|
791
|
+
word: string;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// packages/server/src/entities/entry/types.ts
|
|
795
|
+
interface WordTiming {
|
|
796
|
+
start: number;
|
|
797
|
+
end: number;
|
|
798
|
+
word: string;
|
|
799
|
+
speaker?: number;
|
|
800
|
+
}
|
|
801
|
+
// ^ Oops, server added speaker field. Client breaks when it receives it.
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
Contracts eliminate this drift by defining shared types in one place:
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
// packages/contracts/src/entry/types.ts
|
|
808
|
+
export interface WordTiming {
|
|
809
|
+
speaker?: number;
|
|
810
|
+
start: number;
|
|
811
|
+
end: number;
|
|
812
|
+
word: string;
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### The pattern
|
|
817
|
+
|
|
818
|
+
**Contracts are pure types.** No business logic, no I/O, no environment-specific code. Just the data shapes that cross the client-server boundary.
|
|
819
|
+
|
|
820
|
+
```
|
|
821
|
+
packages/
|
|
822
|
+
contracts/
|
|
823
|
+
src/
|
|
824
|
+
entry/
|
|
825
|
+
types.ts # Shared types: ContentBlock, WordTiming, etc.
|
|
826
|
+
index.ts # export type * from "./types"
|
|
827
|
+
user/
|
|
828
|
+
index.ts # Schema + types for user entity
|
|
829
|
+
index.ts # export * as Entry from "./entry"
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**Local entities import and extend:**
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
// packages/server/src/entities/entry/types.ts
|
|
836
|
+
export type * from "contracts/entry"; // Re-export shared types
|
|
837
|
+
|
|
838
|
+
// Server-specific: DDB record shape
|
|
839
|
+
export interface Record {
|
|
840
|
+
pk: string;
|
|
841
|
+
sk: string;
|
|
842
|
+
audioS3Key: string; // Server stores S3 keys
|
|
843
|
+
content: ContentBlock[];
|
|
844
|
+
// ...
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
// packages/client/src/entities/entry/types.ts
|
|
850
|
+
export type * from "contracts/entry"; // Re-export shared types
|
|
851
|
+
|
|
852
|
+
// Client-specific: UI presentation model
|
|
853
|
+
export interface UnifiedEntry {
|
|
854
|
+
audioUrl?: string; // Client uses presigned URLs
|
|
855
|
+
isLocal: boolean; // Client tracks local state
|
|
856
|
+
content?: ContentBlock[];
|
|
857
|
+
// ...
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### What belongs in contracts?
|
|
862
|
+
|
|
863
|
+
**Include:**
|
|
864
|
+
|
|
865
|
+
- Types that appear identically in both client and server
|
|
866
|
+
- API request/response shapes
|
|
867
|
+
- Enum-like discriminated unions (`ContentBlock`, `SpeakerSegment`)
|
|
868
|
+
- Validation schemas (arktype/zod) when runtime validation is needed
|
|
869
|
+
|
|
870
|
+
**Don't include:**
|
|
871
|
+
|
|
872
|
+
- Environment-specific extensions (DDB keys, presigned URLs)
|
|
873
|
+
- UI-only types (`InsertionPoint`, `UnifiedEntry`)
|
|
874
|
+
- Server-only types (`Record` with `pk`/`sk`)
|
|
875
|
+
|
|
876
|
+
### Consuming contracts
|
|
877
|
+
|
|
878
|
+
**Never import contracts directly in application code.** Always go through local entities:
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
// ✅ GOOD: Import through local entity
|
|
882
|
+
import * as Entry from "entities/entry";
|
|
883
|
+
Entry.ContentBlock; // Shared type (from contracts)
|
|
884
|
+
Entry.Record; // Server-specific type
|
|
885
|
+
|
|
886
|
+
// ❌ AVOID: Bypassing the local entity layer
|
|
887
|
+
import type { ContentBlock } from "contracts/entry";
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
**Why?** The local entity is the API surface. It can add, remove, or alias types without breaking consumers. If you import contracts directly, you're coupled to the shared kernel's internal structure.
|
|
891
|
+
|
|
892
|
+
### Constraints
|
|
893
|
+
|
|
894
|
+
**Contracts must be leaves.** They can't import from server or client packages—only external libraries and other contracts. This prevents circular dependencies.
|
|
895
|
+
|
|
896
|
+
**Keep contracts minimal.** Only share what's truly identical. When in doubt, keep types local and share later when the need is proven.
|
|
897
|
+
|
|
898
|
+
**Deploy together.** When contracts change, both client and server must deploy. In a monorepo this happens naturally, but be aware that independent versioning isn't possible.
|
|
899
|
+
|
|
900
|
+
### Import path setup
|
|
901
|
+
|
|
902
|
+
Configure path aliases in each package's tsconfig.json:
|
|
903
|
+
|
|
904
|
+
```json
|
|
905
|
+
// packages/server/tsconfig.json
|
|
906
|
+
{
|
|
907
|
+
"compilerOptions": {
|
|
908
|
+
"paths": {
|
|
909
|
+
"contracts/*": ["../contracts/src/*"],
|
|
910
|
+
"entities/*": ["src/entities/*"]
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// packages/client/tsconfig.json
|
|
916
|
+
{
|
|
917
|
+
"compilerOptions": {
|
|
918
|
+
"paths": {
|
|
919
|
+
"contracts/*": ["../contracts/src/*"],
|
|
920
|
+
"entities/*": ["./src/entities/*"]
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
### When to create a contract
|
|
927
|
+
|
|
928
|
+
1. **Same type in both environments** - If you're copying a type from server to client (or vice versa), it belongs in contracts.
|
|
929
|
+
2. **API boundary types** - Request/response shapes that define the contract between client and server.
|
|
930
|
+
3. **Shared validation** - When you need the same runtime validation on both sides.
|
|
931
|
+
|
|
932
|
+
**Not every shared concept needs a contract.** Simple primitives (`string`, `number`) or well-known library types don't need to be in contracts. Focus on domain-specific types that carry meaning in your application.
|
|
933
|
+
|
|
934
|
+
## API design principles
|
|
935
|
+
|
|
936
|
+
**Consistency** - Predictable patterns reduce cognitive load
|
|
937
|
+
**Clarity** - Purpose obvious from the path
|
|
938
|
+
**Reduction** - Simplify without dumbing down
|
|
939
|
+
**Accessibility** - Common things easy, complex things possible
|
|
940
|
+
|
|
941
|
+
### Case study: Data over structure
|
|
942
|
+
|
|
943
|
+
**The problem:** We have table, code, list compilers. How do we expose them?
|
|
944
|
+
|
|
945
|
+
**Attempt 1: Deep nesting**
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
// ❌ Four dots, verbose, hard to refactor
|
|
949
|
+
md.compilers.table.compile(node);
|
|
950
|
+
md.compilers.code.compile(node);
|
|
951
|
+
md.compilers.list.compile(node);
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Attempt 2: Flat namespace**
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
// ❌ Naming conflicts, loses organization
|
|
958
|
+
md.compileTable(node);
|
|
959
|
+
md.compileCode(node);
|
|
960
|
+
md.compileList(node);
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**The insight:** We're repeating "compile" with different data. The node type should be a parameter, not a namespace.
|
|
964
|
+
|
|
965
|
+
**Solution: Discriminated dispatch**
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
// ✅ Three dots, type-safe, extensible
|
|
969
|
+
md.compilers.compileNode("table", node);
|
|
970
|
+
md.compilers.compileNode("code", node);
|
|
971
|
+
md.compilers.compileNode("list", node);
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
**Why this works:**
|
|
975
|
+
|
|
976
|
+
- Unified API surface (one function instead of many)
|
|
977
|
+
- Type-safe through string literal unions
|
|
978
|
+
- Data-driven (easy to loop over, test, dynamically dispatch)
|
|
979
|
+
- Stays at three-dot notation
|
|
980
|
+
|
|
981
|
+
### Getting unstuck: The creative process
|
|
982
|
+
|
|
983
|
+
When you hit a constraint that feels wrong:
|
|
984
|
+
|
|
985
|
+
1. **Identify the pattern** — What's being repeated? (Same operation on different data)
|
|
986
|
+
2. **Question the structure** — Am I organizing by _what it does_ or _what it operates on_?
|
|
987
|
+
3. **Try data over structure** — Can the variant become a parameter instead of a namespace?
|
|
988
|
+
4. **Validate the feel** — Does `compileNode("table", node)` read naturally? Does it autocomplete well?
|
|
989
|
+
|
|
990
|
+
**Signals you need to rethink:**
|
|
991
|
+
|
|
992
|
+
- Four+ dot notation feels awkward
|
|
993
|
+
- Many similar functions with slight variations
|
|
994
|
+
- Nested namespaces that mirror each other
|
|
995
|
+
- Comments needed to explain what's what
|
|
996
|
+
|
|
997
|
+
**The unlock:** Constraints breed creativity. The two-segment import rule _forces_ better API design. When you can't nest deeper, you're pushed to find more elegant patterns.
|
|
998
|
+
|
|
999
|
+
## Alternative names for domain.ts
|
|
1000
|
+
|
|
1001
|
+
If `domain` doesn't fit your use case:
|
|
1002
|
+
|
|
1003
|
+
- `services.ts` (\_s): orchestrates flows across helpers/IO
|
|
1004
|
+
- `business.ts` (\_b): explicit business label
|
|
1005
|
+
- `routines.ts` (\_r): stepwise procedures
|
|
1006
|
+
- `operations.ts` (\_o): imperative operations
|
|
1007
|
+
- `engine.ts` (\_e): core driving module
|
|
1008
|
+
|
|
1009
|
+
## Summary
|
|
1010
|
+
|
|
1011
|
+
**Simplicity is not the absence of complexity—it's the management of it.**
|
|
1012
|
+
|
|
1013
|
+
Atomic modules provide:
|
|
1014
|
+
|
|
1015
|
+
- **Predictability** - You always know where to look
|
|
1016
|
+
- **Greppability** - Namespace imports make refactoring safer
|
|
1017
|
+
- **Scalability** - Pattern works for small and large modules
|
|
1018
|
+
- **Elegance** - Consistency creates a sense of rightness
|
|
1019
|
+
|
|
1020
|
+
**Remember:** When you hit complexity, ask:
|
|
1021
|
+
|
|
1022
|
+
- Can I use data instead of structure?
|
|
1023
|
+
- Can I unify similar operations?
|
|
1024
|
+
- Does this feel natural to type and read?
|
|
1025
|
+
|
|
1026
|
+
The answer often reveals a more elegant path.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
-
import type { CompletedPart,
|
|
3
|
-
import type { PresignedPostOptions } from "@aws-sdk/s3-presigned-post";
|
|
2
|
+
import type { CompletedPart, PutObjectCommandInput, UploadPartRequest } from "@aws-sdk/client-s3";
|
|
3
|
+
import type { PresignedPost, PresignedPostOptions } from "@aws-sdk/s3-presigned-post";
|
|
4
|
+
export type { PresignedPost };
|
|
4
5
|
import type { S3Event } from "aws-lambda";
|
|
5
6
|
export type NotificationEvent = S3Event;
|
|
6
7
|
/** Decoded S3 record from an SQS-delivered notification */
|
|
@@ -88,21 +89,21 @@ export type MultiPartProps = {
|
|
|
88
89
|
key: string;
|
|
89
90
|
};
|
|
90
91
|
export type PrepareMultipartUploadProps = {
|
|
91
|
-
|
|
92
|
+
key: string;
|
|
92
93
|
contentType?: string;
|
|
93
|
-
bucketName?: string;
|
|
94
94
|
numOfParts: number;
|
|
95
|
-
|
|
95
|
+
expiresInSecs?: number;
|
|
96
|
+
bucketName?: string;
|
|
96
97
|
};
|
|
97
98
|
export type PrepareMultipartUploadResult = {
|
|
98
|
-
signedUrls: string[];
|
|
99
99
|
uploadId: string;
|
|
100
|
+
signedUrls: string[];
|
|
100
101
|
key: string;
|
|
101
102
|
};
|
|
102
103
|
export type FinalizeMultipartUploadProps = {
|
|
103
|
-
bucketName?: string;
|
|
104
|
-
uploadId: string;
|
|
105
104
|
key: string;
|
|
105
|
+
uploadId: string;
|
|
106
|
+
bucketName?: string;
|
|
106
107
|
};
|
|
107
108
|
export type UploadPartProps = {
|
|
108
109
|
key: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/server/aws/s3/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,EACV,aAAa,EACb,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/server/aws/s3/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qstd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.58",
|
|
4
4
|
"description": "Standard Block component and utilities library with Panda CSS",
|
|
5
5
|
"author": "malin1",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"styled-system",
|
|
38
38
|
"panda.config.ts",
|
|
39
39
|
"README.md",
|
|
40
|
-
"CHANGELOG.md"
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"ATOMIC_MODULES.md"
|
|
41
42
|
],
|
|
42
43
|
"sideEffects": [
|
|
43
44
|
"dist/react/index.css"
|