oh-my-customcode 0.49.0 → 0.50.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,278 @@
1
+ // Source: https://github.com/tmdgusya/engineering-disciplines (MIT License)
2
+ // Complete implementation of condition-based waiting utilities
3
+
4
+ import { access, readFile, stat } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+
7
+ // ============================================================
8
+ // Core waitUntil implementation
9
+ // ============================================================
10
+
11
+ export interface WaitOptions {
12
+ /** Maximum wait time in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ /** Polling interval in milliseconds. Default: 100 */
15
+ interval?: number;
16
+ /** Error message to show on timeout */
17
+ message?: string;
18
+ }
19
+
20
+ /**
21
+ * Waits until the condition returns true, polling at the specified interval.
22
+ * Throws a timeout error if the condition is not met within the timeout period.
23
+ *
24
+ * @example
25
+ * // Wait for a file to exist
26
+ * await waitUntil(
27
+ * () => fileExists('/path/to/file'),
28
+ * { timeout: 5000, message: 'File was not created' }
29
+ * );
30
+ */
31
+ export async function waitUntil(
32
+ condition: () => boolean | Promise<boolean>,
33
+ options: WaitOptions = {}
34
+ ): Promise<void> {
35
+ const { timeout = 5000, interval = 100, message = 'Condition was not met' } = options;
36
+ const deadline = Date.now() + timeout;
37
+
38
+ while (Date.now() < deadline) {
39
+ const result = await condition();
40
+ if (result) return;
41
+ await sleep(interval);
42
+ }
43
+
44
+ throw new Error(`waitUntil timeout after ${timeout}ms: ${message}`);
45
+ }
46
+
47
+ /**
48
+ * Waits until the condition returns a truthy value (not undefined/null/false),
49
+ * then returns that value.
50
+ *
51
+ * @example
52
+ * const record = await waitFor(
53
+ * () => db.find({ id: 'expected-id' }),
54
+ * { timeout: 3000, message: 'Record was not created' }
55
+ * );
56
+ */
57
+ export async function waitFor<T>(
58
+ condition: () => T | Promise<T>,
59
+ options: WaitOptions = {}
60
+ ): Promise<T> {
61
+ const { timeout = 5000, interval = 100, message = 'Value was not available' } = options;
62
+ const deadline = Date.now() + timeout;
63
+
64
+ while (Date.now() < deadline) {
65
+ const result = await condition();
66
+ if (result) return result;
67
+ await sleep(interval);
68
+ }
69
+
70
+ throw new Error(`waitFor timeout after ${timeout}ms: ${message}`);
71
+ }
72
+
73
+ // ============================================================
74
+ // Common condition helpers
75
+ // ============================================================
76
+
77
+ /**
78
+ * Returns true if the file exists and is accessible.
79
+ */
80
+ export async function fileExists(filePath: string): Promise<boolean> {
81
+ try {
82
+ await access(filePath);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Returns true if the file exists and has content (size > 0).
91
+ */
92
+ export async function fileHasContent(filePath: string): Promise<boolean> {
93
+ try {
94
+ const stats = await stat(filePath);
95
+ return stats.size > 0;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Returns true if the file exists and contains the expected text.
103
+ */
104
+ export async function fileContains(filePath: string, text: string): Promise<boolean> {
105
+ try {
106
+ const content = await readFile(filePath, 'utf-8');
107
+ return content.includes(text);
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Returns true if the HTTP endpoint responds with a successful status code.
115
+ */
116
+ export async function httpEndpointReady(url: string): Promise<boolean> {
117
+ try {
118
+ const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
119
+ return response.ok;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Returns true if all files in the list exist.
127
+ */
128
+ export async function allFilesExist(filePaths: string[]): Promise<boolean> {
129
+ const results = await Promise.all(filePaths.map(fileExists));
130
+ return results.every(Boolean);
131
+ }
132
+
133
+ // ============================================================
134
+ // Convenience waiters
135
+ // ============================================================
136
+
137
+ /**
138
+ * Waits for a file to exist.
139
+ *
140
+ * @example
141
+ * await waitForFile('/path/to/output.json', { timeout: 5000 });
142
+ */
143
+ export async function waitForFile(filePath: string, options: WaitOptions = {}): Promise<void> {
144
+ await waitUntil(() => fileExists(filePath), {
145
+ timeout: 5000,
146
+ message: `File not created: ${filePath}`,
147
+ ...options,
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Waits for a file to contain specific text.
153
+ *
154
+ * @example
155
+ * await waitForFileContent('/path/to/log.txt', 'Server started', { timeout: 10000 });
156
+ */
157
+ export async function waitForFileContent(
158
+ filePath: string,
159
+ text: string,
160
+ options: WaitOptions = {}
161
+ ): Promise<void> {
162
+ await waitUntil(() => fileContains(filePath, text), {
163
+ timeout: 5000,
164
+ message: `File ${filePath} did not contain: ${text}`,
165
+ ...options,
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Waits for an HTTP endpoint to respond with a successful status.
171
+ *
172
+ * @example
173
+ * await waitForServer('http://localhost:3000/health', { timeout: 15000 });
174
+ */
175
+ export async function waitForServer(url: string, options: WaitOptions = {}): Promise<void> {
176
+ await waitUntil(() => httpEndpointReady(url), {
177
+ timeout: 15000,
178
+ interval: 300,
179
+ message: `Server at ${url} did not become ready`,
180
+ ...options,
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Waits for a directory to contain at least minCount files.
186
+ *
187
+ * @example
188
+ * await waitForDirectoryCount('/output/dir', 3, { timeout: 5000 });
189
+ */
190
+ export async function waitForDirectoryCount(
191
+ dirPath: string,
192
+ minCount: number,
193
+ options: WaitOptions = {}
194
+ ): Promise<void> {
195
+ const { readdir } = await import('node:fs/promises');
196
+ await waitUntil(
197
+ async () => {
198
+ try {
199
+ const entries = await readdir(dirPath);
200
+ return entries.length >= minCount;
201
+ } catch {
202
+ return false;
203
+ }
204
+ },
205
+ {
206
+ timeout: 5000,
207
+ message: `Directory ${dirPath} did not reach ${minCount} files`,
208
+ ...options,
209
+ }
210
+ );
211
+ }
212
+
213
+ // ============================================================
214
+ // Utilities
215
+ // ============================================================
216
+
217
+ /**
218
+ * Sleep for the specified number of milliseconds.
219
+ */
220
+ export function sleep(ms: number): Promise<void> {
221
+ return new Promise((resolve) => setTimeout(resolve, ms));
222
+ }
223
+
224
+ // ============================================================
225
+ // Usage examples (not for import, documentation only)
226
+ // ============================================================
227
+
228
+ /*
229
+ // Example 1: Wait for build output file
230
+ await waitForFile(join(outputDir, 'bundle.js'), { timeout: 30000 });
231
+
232
+ // Example 2: Wait for server to start
233
+ await waitForServer('http://localhost:8080/health', { timeout: 20000 });
234
+
235
+ // Example 3: Wait for database record with custom condition
236
+ const user = await waitFor(
237
+ async () => {
238
+ const u = await db.users.findUnique({ where: { email: 'test@example.com' } });
239
+ return u?.emailVerified ? u : null;
240
+ },
241
+ { timeout: 5000, message: 'User email was not verified' }
242
+ );
243
+
244
+ // Example 4: Wait for process output
245
+ const logs: string[] = [];
246
+ const proc = spawn('npm', ['start']);
247
+ proc.stdout.on('data', (chunk) => logs.push(chunk.toString()));
248
+
249
+ await waitUntil(
250
+ () => logs.some((line) => line.includes('Listening on port')),
251
+ { timeout: 15000, interval: 200, message: 'Server did not log startup message' }
252
+ );
253
+
254
+ // Example 5: Wait for all output files to be generated
255
+ const expectedFiles = ['report.json', 'summary.txt', 'data.csv'].map((f) =>
256
+ join(outputDir, f)
257
+ );
258
+ await waitUntil(() => allFilesExist(expectedFiles), {
259
+ timeout: 10000,
260
+ message: `Not all output files were generated in ${outputDir}`,
261
+ });
262
+
263
+ // Example 6: Test that verifies count reaches expected value
264
+ it('should process all items', async () => {
265
+ await triggerBatchProcessing(items);
266
+
267
+ await waitUntil(
268
+ async () => {
269
+ const processed = await db.items.count({ where: { status: 'done' } });
270
+ return processed >= items.length;
271
+ },
272
+ { timeout: 5000, message: `Expected ${items.length} items to be processed` }
273
+ );
274
+
275
+ const processed = await db.items.count({ where: { status: 'done' } });
276
+ expect(processed).toBe(items.length);
277
+ });
278
+ */
@@ -0,0 +1,240 @@
1
+ # Condition-Based Waiting
2
+
3
+ <!-- Source: https://github.com/tmdgusya/engineering-disciplines (MIT License) -->
4
+
5
+ ## Overview
6
+
7
+ Arbitrary delays (`sleep`, `setTimeout`, `await new Promise(r => setTimeout(r, 1000))`) are a debugging anti-pattern. They hide timing issues, make tests slow, and still fail intermittently.
8
+
9
+ **Core principle:** Replace arbitrary delays with condition-based polling that waits for the actual state you need.
10
+
11
+ ## When to Use
12
+
13
+ **Use when:**
14
+ - Tests use `await sleep(1000)` or similar arbitrary waits
15
+ - Code has `setTimeout` for "giving time" to async operations
16
+ - Tests are flaky because timing depends on system speed
17
+ - You need to wait for: file to exist, process to start, server to be ready, database record to appear
18
+
19
+ ## The Problem with Arbitrary Delays
20
+
21
+ ```typescript
22
+ // ❌ Arbitrary delay - fragile and slow
23
+ await fs.writeFile(path, content);
24
+ await sleep(500); // "Give it time to write"
25
+ const result = await fs.readFile(path);
26
+
27
+ // Problems:
28
+ // 1. 500ms may not be enough on slow systems
29
+ // 2. 500ms is always wasted on fast systems
30
+ // 3. The actual condition (file readable) is never verified
31
+ ```
32
+
33
+ ## The Solution: Condition-Based Waiting
34
+
35
+ ```typescript
36
+ // ✓ Condition-based - fast and reliable
37
+ await fs.writeFile(path, content);
38
+ await waitUntil(() => fs.access(path).then(() => true).catch(() => false));
39
+ const result = await fs.readFile(path);
40
+
41
+ // Benefits:
42
+ // 1. Proceeds as soon as condition is met (fast on fast systems)
43
+ // 2. Has a timeout for safety (catches real failures)
44
+ // 3. The actual condition is explicit and verified
45
+ ```
46
+
47
+ ## Core Implementation
48
+
49
+ ```typescript
50
+ interface WaitOptions {
51
+ timeout?: number; // Max wait time in ms (default: 5000)
52
+ interval?: number; // Poll interval in ms (default: 100)
53
+ message?: string; // Error message on timeout
54
+ }
55
+
56
+ async function waitUntil(
57
+ condition: () => boolean | Promise<boolean>,
58
+ options: WaitOptions = {}
59
+ ): Promise<void> {
60
+ const { timeout = 5000, interval = 100, message = 'Condition not met' } = options;
61
+ const deadline = Date.now() + timeout;
62
+
63
+ while (Date.now() < deadline) {
64
+ const result = await condition();
65
+ if (result) return;
66
+ await new Promise(resolve => setTimeout(resolve, interval));
67
+ }
68
+
69
+ throw new Error(`waitUntil timeout after ${timeout}ms: ${message}`);
70
+ }
71
+ ```
72
+
73
+ See `condition-based-waiting-example.ts` for the complete implementation with all utilities.
74
+
75
+ ## Common Patterns
76
+
77
+ ### Wait for File to Exist
78
+
79
+ ```typescript
80
+ // ❌ Arbitrary delay
81
+ await triggerFileCreation();
82
+ await sleep(1000);
83
+ const content = await fs.readFile(outputPath, 'utf-8');
84
+
85
+ // ✓ Condition-based
86
+ await triggerFileCreation();
87
+ await waitUntil(
88
+ () => fs.access(outputPath).then(() => true).catch(() => false),
89
+ { timeout: 5000, message: `File not created: ${outputPath}` }
90
+ );
91
+ const content = await fs.readFile(outputPath, 'utf-8');
92
+ ```
93
+
94
+ ### Wait for Process to Start
95
+
96
+ ```typescript
97
+ // ❌ Arbitrary delay
98
+ startServer();
99
+ await sleep(2000); // "Give server time to start"
100
+ const response = await fetch('http://localhost:3000/health');
101
+
102
+ // ✓ Condition-based
103
+ startServer();
104
+ await waitUntil(
105
+ async () => {
106
+ try {
107
+ const res = await fetch('http://localhost:3000/health');
108
+ return res.ok;
109
+ } catch {
110
+ return false;
111
+ }
112
+ },
113
+ { timeout: 10000, message: 'Server did not start within 10s' }
114
+ );
115
+ const response = await fetch('http://localhost:3000/health');
116
+ ```
117
+
118
+ ### Wait for Database Record
119
+
120
+ ```typescript
121
+ // ❌ Arbitrary delay
122
+ await triggerAsyncOperation();
123
+ await sleep(500);
124
+ const record = await db.find({ id: expectedId });
125
+
126
+ // ✓ Condition-based
127
+ await triggerAsyncOperation();
128
+ await waitUntil(
129
+ async () => {
130
+ const record = await db.find({ id: expectedId });
131
+ return record !== null;
132
+ },
133
+ { timeout: 3000, message: `Record ${expectedId} not created` }
134
+ );
135
+ const record = await db.find({ id: expectedId });
136
+ ```
137
+
138
+ ### Wait for Log Output
139
+
140
+ ```typescript
141
+ // ❌ Arbitrary delay
142
+ process.spawn('my-command');
143
+ await sleep(1000);
144
+ expect(logOutput).toContain('Server started');
145
+
146
+ // ✓ Condition-based
147
+ const logOutput: string[] = [];
148
+ const proc = process.spawn('my-command');
149
+ proc.stdout.on('data', (chunk) => logOutput.push(chunk.toString()));
150
+
151
+ await waitUntil(
152
+ () => logOutput.some(line => line.includes('Server started')),
153
+ { timeout: 10000, message: 'Server start message not seen in logs' }
154
+ );
155
+ ```
156
+
157
+ ### Wait for Count to Reach Expected Value
158
+
159
+ ```typescript
160
+ // ❌ Arbitrary delay
161
+ await triggerBatchOperation();
162
+ await sleep(2000);
163
+ const items = await db.findAll();
164
+ expect(items.length).toBe(10);
165
+
166
+ // ✓ Condition-based
167
+ await triggerBatchOperation();
168
+ await waitUntil(
169
+ async () => {
170
+ const items = await db.findAll();
171
+ return items.length >= 10;
172
+ },
173
+ { timeout: 5000, message: 'Batch did not complete: expected 10 items' }
174
+ );
175
+ const items = await db.findAll();
176
+ expect(items.length).toBe(10);
177
+ ```
178
+
179
+ ## Choosing Timeout and Interval Values
180
+
181
+ | Scenario | Timeout | Interval | Rationale |
182
+ |----------|---------|----------|-----------|
183
+ | File write | 2s | 50ms | Fast local I/O |
184
+ | Process start | 10s | 200ms | Process startup varies |
185
+ | HTTP server ready | 15s | 300ms | Server may need to compile |
186
+ | Database record | 3s | 100ms | DB writes are fast |
187
+ | CI environment | 2-3x local | same | CI is often slower |
188
+
189
+ **Rule of thumb:**
190
+ - Interval: 10-20% of expected wait time, minimum 50ms
191
+ - Timeout: 3-5x the typical wait time
192
+ - Add a buffer for CI: multiply timeout by 2-3x
193
+
194
+ ## Testing the Waiters Themselves
195
+
196
+ ```typescript
197
+ // Test that waitUntil resolves when condition becomes true
198
+ it('resolves when condition becomes true', async () => {
199
+ let ready = false;
200
+ setTimeout(() => { ready = true; }, 100);
201
+
202
+ await expect(
203
+ waitUntil(() => ready, { timeout: 1000 })
204
+ ).resolves.toBeUndefined();
205
+ });
206
+
207
+ // Test that waitUntil rejects on timeout
208
+ it('rejects with timeout error when condition never met', async () => {
209
+ await expect(
210
+ waitUntil(() => false, { timeout: 100, message: 'test condition' })
211
+ ).rejects.toThrow('waitUntil timeout after 100ms: test condition');
212
+ });
213
+ ```
214
+
215
+ ## Migration Guide
216
+
217
+ When refactoring existing `sleep` calls:
218
+
219
+ 1. **Identify what the sleep is "waiting for"** — read surrounding code
220
+ 2. **Find the observable state change** — what becomes true when the operation completes?
221
+ 3. **Replace with `waitUntil(condition)`** — poll for that state
222
+ 4. **Set appropriate timeout** — 3-5x the typical wait time
223
+ 5. **Add descriptive message** — what should have happened?
224
+
225
+ ```typescript
226
+ // Before
227
+ await startProcess();
228
+ await sleep(2000);
229
+ checkResult();
230
+
231
+ // After - step 1: what is sleep waiting for? Process to be ready.
232
+ // After - step 2: observable state? Process responds to health check.
233
+ // After - step 3-5:
234
+ await startProcess();
235
+ await waitUntil(
236
+ () => isProcessReady(),
237
+ { timeout: 10000, message: 'Process did not become ready' }
238
+ );
239
+ checkResult();
240
+ ```