@herdctl/core 0.0.1 → 0.0.2

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.
Files changed (275) hide show
  1. package/dist/config/__tests__/agent.test.js +31 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +9 -2
  4. package/dist/config/__tests__/merge.test.js.map +1 -1
  5. package/dist/config/__tests__/schema.test.js +350 -1
  6. package/dist/config/__tests__/schema.test.js.map +1 -1
  7. package/dist/config/index.d.ts +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +3 -1
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/schema.d.ts +828 -24
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +118 -6
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
  16. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
  17. package/dist/fleet-manager/__tests__/errors.test.js +1 -49
  18. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
  19. package/dist/fleet-manager/__tests__/integration.test.js +109 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/reload.test.js +1 -1
  22. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  23. package/dist/fleet-manager/config-reload.d.ts +164 -0
  24. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  25. package/dist/fleet-manager/config-reload.js +445 -0
  26. package/dist/fleet-manager/config-reload.js.map +1 -0
  27. package/dist/fleet-manager/context.d.ts +76 -0
  28. package/dist/fleet-manager/context.d.ts.map +1 -0
  29. package/dist/fleet-manager/context.js +11 -0
  30. package/dist/fleet-manager/context.js.map +1 -0
  31. package/dist/fleet-manager/errors.d.ts +0 -25
  32. package/dist/fleet-manager/errors.d.ts.map +1 -1
  33. package/dist/fleet-manager/errors.js +0 -38
  34. package/dist/fleet-manager/errors.js.map +1 -1
  35. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  36. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  37. package/dist/fleet-manager/event-emitters.js +136 -0
  38. package/dist/fleet-manager/event-emitters.js.map +1 -0
  39. package/dist/fleet-manager/event-types.d.ts +0 -15
  40. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  41. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  42. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  43. package/dist/fleet-manager/fleet-manager.js +95 -1720
  44. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  45. package/dist/fleet-manager/index.d.ts +13 -2
  46. package/dist/fleet-manager/index.d.ts.map +1 -1
  47. package/dist/fleet-manager/index.js +19 -6
  48. package/dist/fleet-manager/index.js.map +1 -1
  49. package/dist/fleet-manager/job-control.d.ts +64 -0
  50. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  51. package/dist/fleet-manager/job-control.js +296 -0
  52. package/dist/fleet-manager/job-control.js.map +1 -0
  53. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  54. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  55. package/dist/fleet-manager/log-streaming.js +503 -0
  56. package/dist/fleet-manager/log-streaming.js.map +1 -0
  57. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  58. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  59. package/dist/fleet-manager/schedule-executor.js +209 -0
  60. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  61. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  62. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  63. package/dist/fleet-manager/schedule-management.js +171 -0
  64. package/dist/fleet-manager/schedule-management.js.map +1 -0
  65. package/dist/fleet-manager/status-queries.d.ts +105 -0
  66. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  67. package/dist/fleet-manager/status-queries.js +247 -0
  68. package/dist/fleet-manager/status-queries.js.map +1 -0
  69. package/dist/fleet-manager/types.d.ts +0 -39
  70. package/dist/fleet-manager/types.d.ts.map +1 -1
  71. package/dist/runner/__tests__/job-executor.test.js +206 -1
  72. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  73. package/dist/runner/job-executor.d.ts +9 -0
  74. package/dist/runner/job-executor.d.ts.map +1 -1
  75. package/dist/runner/job-executor.js +78 -4
  76. package/dist/runner/job-executor.js.map +1 -1
  77. package/dist/runner/types.d.ts +2 -0
  78. package/dist/runner/types.d.ts.map +1 -1
  79. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  80. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  81. package/dist/scheduler/__tests__/cron.test.js +867 -0
  82. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  83. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  84. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  85. package/dist/scheduler/cron.d.ts +126 -0
  86. package/dist/scheduler/cron.d.ts.map +1 -0
  87. package/dist/scheduler/cron.js +390 -0
  88. package/dist/scheduler/cron.js.map +1 -0
  89. package/dist/scheduler/errors.d.ts +81 -1
  90. package/dist/scheduler/errors.d.ts.map +1 -1
  91. package/dist/scheduler/errors.js +81 -6
  92. package/dist/scheduler/errors.js.map +1 -1
  93. package/dist/scheduler/index.d.ts +1 -0
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/scheduler/index.js +2 -0
  96. package/dist/scheduler/index.js.map +1 -1
  97. package/dist/scheduler/schedule-runner.d.ts +2 -2
  98. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  99. package/dist/scheduler/schedule-runner.js +20 -8
  100. package/dist/scheduler/schedule-runner.js.map +1 -1
  101. package/dist/scheduler/scheduler.d.ts +4 -4
  102. package/dist/scheduler/scheduler.d.ts.map +1 -1
  103. package/dist/scheduler/scheduler.js +86 -20
  104. package/dist/scheduler/scheduler.js.map +1 -1
  105. package/dist/scheduler/types.d.ts +1 -1
  106. package/dist/scheduler/types.d.ts.map +1 -1
  107. package/dist/state/schemas/job-metadata.d.ts +2 -2
  108. package/package.json +33 -8
  109. package/.turbo/turbo-build.log +0 -4
  110. package/.turbo/turbo-test.log +0 -219
  111. package/.turbo/turbo-typecheck.log +0 -4
  112. package/coverage/base.css +0 -224
  113. package/coverage/block-navigation.js +0 -87
  114. package/coverage/coverage-final.json +0 -51
  115. package/coverage/favicon.png +0 -0
  116. package/coverage/index.html +0 -251
  117. package/coverage/prettify.css +0 -1
  118. package/coverage/prettify.js +0 -2
  119. package/coverage/sort-arrow-sprite.png +0 -0
  120. package/coverage/sorter.js +0 -210
  121. package/coverage/src/config/index.html +0 -191
  122. package/coverage/src/config/index.ts.html +0 -442
  123. package/coverage/src/config/interpolate.ts.html +0 -652
  124. package/coverage/src/config/loader.ts.html +0 -1501
  125. package/coverage/src/config/merge.ts.html +0 -823
  126. package/coverage/src/config/parser.ts.html +0 -1213
  127. package/coverage/src/config/schema.ts.html +0 -1123
  128. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  129. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  130. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  131. package/coverage/src/fleet-manager/index.html +0 -206
  132. package/coverage/src/fleet-manager/index.ts.html +0 -469
  133. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  134. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  135. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  136. package/coverage/src/index.html +0 -116
  137. package/coverage/src/index.ts.html +0 -181
  138. package/coverage/src/runner/errors.ts.html +0 -1006
  139. package/coverage/src/runner/index.html +0 -191
  140. package/coverage/src/runner/index.ts.html +0 -256
  141. package/coverage/src/runner/job-executor.ts.html +0 -1429
  142. package/coverage/src/runner/message-processor.ts.html +0 -1150
  143. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  144. package/coverage/src/runner/types.ts.html +0 -559
  145. package/coverage/src/scheduler/errors.ts.html +0 -388
  146. package/coverage/src/scheduler/index.html +0 -206
  147. package/coverage/src/scheduler/index.ts.html +0 -244
  148. package/coverage/src/scheduler/interval.ts.html +0 -652
  149. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  150. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  151. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  152. package/coverage/src/scheduler/types.ts.html +0 -733
  153. package/coverage/src/state/directory.ts.html +0 -736
  154. package/coverage/src/state/errors.ts.html +0 -376
  155. package/coverage/src/state/fleet-state.ts.html +0 -937
  156. package/coverage/src/state/index.html +0 -221
  157. package/coverage/src/state/index.ts.html +0 -322
  158. package/coverage/src/state/job-metadata.ts.html +0 -1420
  159. package/coverage/src/state/job-output.ts.html +0 -1033
  160. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  161. package/coverage/src/state/schemas/index.html +0 -176
  162. package/coverage/src/state/schemas/index.ts.html +0 -286
  163. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  164. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  165. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  166. package/coverage/src/state/session.ts.html +0 -844
  167. package/coverage/src/state/types.ts.html +0 -262
  168. package/coverage/src/state/utils/atomic.ts.html +0 -748
  169. package/coverage/src/state/utils/index.html +0 -146
  170. package/coverage/src/state/utils/index.ts.html +0 -103
  171. package/coverage/src/state/utils/reads.ts.html +0 -1621
  172. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  173. package/coverage/src/work-sources/adapters/index.html +0 -131
  174. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  175. package/coverage/src/work-sources/errors.ts.html +0 -298
  176. package/coverage/src/work-sources/index.html +0 -176
  177. package/coverage/src/work-sources/index.ts.html +0 -529
  178. package/coverage/src/work-sources/manager.ts.html +0 -1324
  179. package/coverage/src/work-sources/registry.ts.html +0 -619
  180. package/coverage/src/work-sources/types.ts.html +0 -568
  181. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  182. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  183. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  184. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  185. package/src/config/__tests__/agent.test.ts +0 -864
  186. package/src/config/__tests__/interpolate.test.ts +0 -644
  187. package/src/config/__tests__/loader.test.ts +0 -784
  188. package/src/config/__tests__/merge.test.ts +0 -751
  189. package/src/config/__tests__/parser.test.ts +0 -533
  190. package/src/config/__tests__/schema.test.ts +0 -873
  191. package/src/config/index.ts +0 -119
  192. package/src/config/interpolate.ts +0 -189
  193. package/src/config/loader.ts +0 -472
  194. package/src/config/merge.ts +0 -246
  195. package/src/config/parser.ts +0 -376
  196. package/src/config/schema.ts +0 -346
  197. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  198. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  199. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  200. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  201. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  202. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  203. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  204. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  205. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  206. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  207. package/src/fleet-manager/errors.ts +0 -747
  208. package/src/fleet-manager/event-types.ts +0 -378
  209. package/src/fleet-manager/fleet-manager.ts +0 -2315
  210. package/src/fleet-manager/index.ts +0 -128
  211. package/src/fleet-manager/job-manager.ts +0 -663
  212. package/src/fleet-manager/job-queue.ts +0 -798
  213. package/src/fleet-manager/types.ts +0 -839
  214. package/src/index.ts +0 -32
  215. package/src/runner/__tests__/errors.test.ts +0 -382
  216. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  217. package/src/runner/__tests__/message-processor.test.ts +0 -960
  218. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  219. package/src/runner/errors.ts +0 -307
  220. package/src/runner/index.ts +0 -57
  221. package/src/runner/job-executor.ts +0 -448
  222. package/src/runner/message-processor.ts +0 -355
  223. package/src/runner/sdk-adapter.ts +0 -191
  224. package/src/runner/types.ts +0 -158
  225. package/src/scheduler/__tests__/errors.test.ts +0 -159
  226. package/src/scheduler/__tests__/interval.test.ts +0 -515
  227. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  228. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  229. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  230. package/src/scheduler/errors.ts +0 -101
  231. package/src/scheduler/index.ts +0 -53
  232. package/src/scheduler/interval.ts +0 -189
  233. package/src/scheduler/schedule-runner.ts +0 -442
  234. package/src/scheduler/schedule-state.ts +0 -211
  235. package/src/scheduler/scheduler.ts +0 -570
  236. package/src/scheduler/types.ts +0 -216
  237. package/src/state/__tests__/directory.test.ts +0 -595
  238. package/src/state/__tests__/fleet-state.test.ts +0 -868
  239. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  240. package/src/state/__tests__/job-metadata.test.ts +0 -831
  241. package/src/state/__tests__/job-output.test.ts +0 -856
  242. package/src/state/__tests__/session-schema.test.ts +0 -378
  243. package/src/state/__tests__/session.test.ts +0 -604
  244. package/src/state/directory.ts +0 -217
  245. package/src/state/errors.ts +0 -97
  246. package/src/state/fleet-state.ts +0 -284
  247. package/src/state/index.ts +0 -79
  248. package/src/state/job-metadata.ts +0 -445
  249. package/src/state/job-output.ts +0 -316
  250. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  251. package/src/state/schemas/fleet-state.ts +0 -120
  252. package/src/state/schemas/index.ts +0 -67
  253. package/src/state/schemas/job-metadata.ts +0 -181
  254. package/src/state/schemas/job-output.ts +0 -177
  255. package/src/state/schemas/session-info.ts +0 -92
  256. package/src/state/session.ts +0 -253
  257. package/src/state/types.ts +0 -59
  258. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  259. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  260. package/src/state/utils/atomic.ts +0 -221
  261. package/src/state/utils/index.ts +0 -6
  262. package/src/state/utils/reads.ts +0 -512
  263. package/src/work-sources/__tests__/github.test.ts +0 -1800
  264. package/src/work-sources/__tests__/manager.test.ts +0 -529
  265. package/src/work-sources/__tests__/registry.test.ts +0 -477
  266. package/src/work-sources/__tests__/types.test.ts +0 -479
  267. package/src/work-sources/adapters/github.ts +0 -1166
  268. package/src/work-sources/adapters/index.ts +0 -64
  269. package/src/work-sources/errors.ts +0 -71
  270. package/src/work-sources/index.ts +0 -148
  271. package/src/work-sources/manager.ts +0 -413
  272. package/src/work-sources/registry.ts +0 -178
  273. package/src/work-sources/types.ts +0 -161
  274. package/tsconfig.json +0 -9
  275. package/vitest.config.ts +0 -19
@@ -1,512 +0,0 @@
1
- /**
2
- * Safe concurrent read utilities
3
- *
4
- * Provides read operations that handle concurrent access safely.
5
- * Read operations don't require locks - multiple readers can access
6
- * files simultaneously. These utilities handle edge cases like:
7
- * - Files being written to during read (retry on partial read)
8
- * - Empty or truncated YAML files
9
- * - Incomplete last lines in JSONL files
10
- */
11
-
12
- import { readFile } from "node:fs/promises";
13
- import { parse as parseYaml, YAMLParseError } from "yaml";
14
-
15
- /**
16
- * Error thrown when a safe read operation fails
17
- */
18
- export class SafeReadError extends Error {
19
- public readonly path: string;
20
- public readonly code?: string;
21
-
22
- constructor(message: string, path: string, cause?: Error) {
23
- super(message);
24
- this.name = "SafeReadError";
25
- this.path = path;
26
- this.cause = cause;
27
- this.code = (cause as NodeJS.ErrnoException | undefined)?.code;
28
- }
29
- }
30
-
31
- /**
32
- * Options for safe read operations
33
- */
34
- export interface SafeReadOptions {
35
- /**
36
- * Maximum number of retry attempts for transient failures.
37
- * Default: 3
38
- */
39
- maxRetries?: number;
40
-
41
- /**
42
- * Base delay in milliseconds between retries.
43
- * Uses exponential backoff: delay = baseDelayMs * 2^attempt
44
- * Default: 10
45
- */
46
- baseDelayMs?: number;
47
-
48
- /**
49
- * Injectable read function for testing
50
- * @internal
51
- */
52
- readFn?: (path: string, encoding: BufferEncoding) => Promise<string>;
53
- }
54
-
55
- /**
56
- * Options for safeReadJsonl
57
- */
58
- export interface SafeReadJsonlOptions extends SafeReadOptions {
59
- /**
60
- * Whether to skip invalid JSON lines instead of failing.
61
- * Default: false - only skips truly incomplete last line
62
- */
63
- skipInvalidLines?: boolean;
64
- }
65
-
66
- /**
67
- * Result type for safe YAML read operations
68
- */
69
- export type SafeReadYamlResult<T> =
70
- | { success: true; data: T }
71
- | { success: false; error: SafeReadError };
72
-
73
- /**
74
- * Result type for safe JSONL read operations
75
- */
76
- export type SafeReadJsonlResult<T> =
77
- | { success: true; data: T[]; skippedLines: number }
78
- | { success: false; error: SafeReadError };
79
-
80
- /**
81
- * Check if an error is likely a transient read error that should be retried.
82
- * This happens when reading a file while it's being written atomically.
83
- *
84
- * We treat YAML parse errors as potentially transient because:
85
- * 1. If a read occurs during an atomic write, the file might be empty or partially written
86
- * 2. The retry gives time for the atomic rename to complete
87
- * 3. Non-transient errors (like truly malformed YAML) will fail consistently
88
- */
89
- function isTransientReadError(error: unknown): boolean {
90
- if (!(error instanceof Error)) return false;
91
-
92
- // All YAML parse errors are potentially transient - they could indicate
93
- // a partial read during an atomic write operation
94
- if (error instanceof YAMLParseError) {
95
- return true;
96
- }
97
-
98
- // JSON parse errors for incomplete content
99
- if (error instanceof SyntaxError) {
100
- const msg = error.message.toLowerCase();
101
- if (
102
- msg.includes("unexpected end") ||
103
- msg.includes("unexpected token") ||
104
- msg.includes("unterminated")
105
- ) {
106
- return true;
107
- }
108
- }
109
-
110
- return false;
111
- }
112
-
113
- /**
114
- * Wait for a delay using exponential backoff
115
- */
116
- async function backoffDelay(attempt: number, baseDelayMs: number): Promise<void> {
117
- const delay = baseDelayMs * Math.pow(2, attempt);
118
- await new Promise((resolve) => setTimeout(resolve, delay));
119
- }
120
-
121
- /**
122
- * Read and parse a YAML file safely with retry logic.
123
- *
124
- * This function handles:
125
- * - Files being written to during read (retries on parse failure)
126
- * - Empty files (returns null/undefined based on YAML spec)
127
- * - Truncated files (retries then fails gracefully)
128
- *
129
- * Read operations don't require locks - multiple concurrent reads are safe.
130
- * The retry logic handles the case where a read occurs during an atomic write.
131
- *
132
- * @param filePath - Path to the YAML file
133
- * @param options - Read options including retry configuration
134
- * @returns Promise resolving to SafeReadYamlResult with success/failure
135
- *
136
- * @example
137
- * ```typescript
138
- * const result = await safeReadYaml<MyConfig>('/path/to/config.yaml');
139
- * if (result.success) {
140
- * console.log(result.data);
141
- * } else {
142
- * console.error(result.error.message);
143
- * }
144
- * ```
145
- */
146
- export async function safeReadYaml<T = unknown>(
147
- filePath: string,
148
- options: SafeReadOptions = {}
149
- ): Promise<SafeReadYamlResult<T>> {
150
- const {
151
- maxRetries = 3,
152
- baseDelayMs = 10,
153
- readFn = (path, encoding) => readFile(path, encoding),
154
- } = options;
155
-
156
- let lastError: Error | undefined;
157
-
158
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
159
- try {
160
- const content = await readFn(filePath, "utf-8");
161
-
162
- // Handle empty file - YAML spec says empty doc is null
163
- if (content.trim() === "") {
164
- return { success: true, data: null as T };
165
- }
166
-
167
- const parsed = parseYaml(content) as T;
168
- return { success: true, data: parsed };
169
- } catch (error) {
170
- lastError = error as Error;
171
-
172
- // File not found or permission errors are not transient
173
- const code = (error as NodeJS.ErrnoException).code;
174
- if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
175
- return {
176
- success: false,
177
- error: new SafeReadError(
178
- `Failed to read YAML file ${filePath}: ${(error as Error).message}`,
179
- filePath,
180
- error as Error
181
- ),
182
- };
183
- }
184
-
185
- // Check if this is a transient error worth retrying
186
- if (isTransientReadError(error) && attempt < maxRetries) {
187
- await backoffDelay(attempt, baseDelayMs);
188
- continue;
189
- }
190
-
191
- // Non-transient error or retries exhausted
192
- return {
193
- success: false,
194
- error: new SafeReadError(
195
- `Failed to parse YAML file ${filePath}: ${(error as Error).message}`,
196
- filePath,
197
- error as Error
198
- ),
199
- };
200
- }
201
- }
202
-
203
- // Should not reach here, but handle it just in case
204
- return {
205
- success: false,
206
- error: new SafeReadError(
207
- `Failed to read YAML file ${filePath} after ${maxRetries + 1} attempts`,
208
- filePath,
209
- lastError
210
- ),
211
- };
212
- }
213
-
214
- /**
215
- * Read and parse a JSONL file safely, handling incomplete last lines.
216
- *
217
- * This function handles:
218
- * - Incomplete last line (truncates to last valid line)
219
- * - Empty files (returns empty array)
220
- * - Files being written to during read (retries on failure)
221
- *
222
- * JSONL (JSON Lines) format has one JSON object per line. When reading
223
- * a file that's being appended to, the last line may be incomplete.
224
- * This function safely truncates to the last complete line.
225
- *
226
- * Read operations don't require locks - multiple concurrent reads are safe.
227
- *
228
- * @param filePath - Path to the JSONL file
229
- * @param options - Read options including retry configuration
230
- * @returns Promise resolving to SafeReadJsonlResult with array of parsed objects
231
- *
232
- * @example
233
- * ```typescript
234
- * const result = await safeReadJsonl<LogEntry>('/path/to/events.jsonl');
235
- * if (result.success) {
236
- * console.log(`Read ${result.data.length} entries, skipped ${result.skippedLines}`);
237
- * }
238
- * ```
239
- */
240
- export async function safeReadJsonl<T = unknown>(
241
- filePath: string,
242
- options: SafeReadJsonlOptions = {}
243
- ): Promise<SafeReadJsonlResult<T>> {
244
- const {
245
- maxRetries = 3,
246
- baseDelayMs = 10,
247
- skipInvalidLines = false,
248
- readFn = (path, encoding) => readFile(path, encoding),
249
- } = options;
250
-
251
- let lastError: Error | undefined;
252
-
253
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
254
- try {
255
- const content = await readFn(filePath, "utf-8");
256
-
257
- // Handle empty file
258
- if (content.trim() === "") {
259
- return { success: true, data: [], skippedLines: 0 };
260
- }
261
-
262
- const result = parseJsonlContent<T>(content, skipInvalidLines);
263
- return { success: true, ...result };
264
- } catch (error) {
265
- lastError = error as Error;
266
-
267
- // File not found or permission errors are not transient
268
- const code = (error as NodeJS.ErrnoException).code;
269
- if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
270
- return {
271
- success: false,
272
- error: new SafeReadError(
273
- `Failed to read JSONL file ${filePath}: ${(error as Error).message}`,
274
- filePath,
275
- error as Error
276
- ),
277
- };
278
- }
279
-
280
- // Retry on transient errors
281
- if (attempt < maxRetries) {
282
- await backoffDelay(attempt, baseDelayMs);
283
- continue;
284
- }
285
- }
286
- }
287
-
288
- return {
289
- success: false,
290
- error: new SafeReadError(
291
- `Failed to read JSONL file ${filePath} after ${maxRetries + 1} attempts`,
292
- filePath,
293
- lastError
294
- ),
295
- };
296
- }
297
-
298
- /**
299
- * Parse JSONL content, handling incomplete last line.
300
- *
301
- * @internal
302
- */
303
- function parseJsonlContent<T>(
304
- content: string,
305
- skipInvalidLines: boolean
306
- ): { data: T[]; skippedLines: number } {
307
- const lines = content.split("\n");
308
- const result: T[] = [];
309
- let skippedLines = 0;
310
-
311
- for (let i = 0; i < lines.length; i++) {
312
- const line = lines[i].trim();
313
-
314
- // Skip empty lines
315
- if (line === "") {
316
- continue;
317
- }
318
-
319
- try {
320
- const parsed = JSON.parse(line) as T;
321
- result.push(parsed);
322
- } catch (error) {
323
- // Last line may be incomplete - always skip it silently
324
- if (i === lines.length - 1 || i === lines.length - 2) {
325
- // Could be the actual last line or second-to-last if file ends with \n
326
- skippedLines++;
327
- continue;
328
- }
329
-
330
- // For middle lines, either skip or fail based on option
331
- if (skipInvalidLines) {
332
- skippedLines++;
333
- continue;
334
- }
335
-
336
- // Re-throw for non-last-line errors when not skipping
337
- throw new SafeReadError(
338
- `Invalid JSON on line ${i + 1}: ${(error as Error).message}`,
339
- "",
340
- error as Error
341
- );
342
- }
343
- }
344
-
345
- return { data: result, skippedLines };
346
- }
347
-
348
- /**
349
- * Read a YAML file with retry logic, throwing on failure.
350
- *
351
- * This is a convenience wrapper around safeReadYaml that throws
352
- * instead of returning a result object.
353
- *
354
- * @param filePath - Path to the YAML file
355
- * @param options - Read options
356
- * @returns Promise resolving to parsed YAML content
357
- * @throws SafeReadError on failure
358
- */
359
- export async function readYaml<T = unknown>(
360
- filePath: string,
361
- options: SafeReadOptions = {}
362
- ): Promise<T> {
363
- const result = await safeReadYaml<T>(filePath, options);
364
- if (!result.success) {
365
- throw result.error;
366
- }
367
- return result.data;
368
- }
369
-
370
- /**
371
- * Read a JSONL file, handling incomplete last line, throwing on failure.
372
- *
373
- * This is a convenience wrapper around safeReadJsonl that throws
374
- * instead of returning a result object.
375
- *
376
- * @param filePath - Path to the JSONL file
377
- * @param options - Read options
378
- * @returns Promise resolving to array of parsed objects
379
- * @throws SafeReadError on failure
380
- */
381
- export async function readJsonl<T = unknown>(
382
- filePath: string,
383
- options: SafeReadJsonlOptions = {}
384
- ): Promise<T[]> {
385
- const result = await safeReadJsonl<T>(filePath, options);
386
- if (!result.success) {
387
- throw result.error;
388
- }
389
- return result.data;
390
- }
391
-
392
- /**
393
- * Result type for safe JSON read operations
394
- */
395
- export type SafeReadJsonResult<T> =
396
- | { success: true; data: T }
397
- | { success: false; error: SafeReadError };
398
-
399
- /**
400
- * Read and parse a JSON file safely with retry logic.
401
- *
402
- * This function handles:
403
- * - Files being written to during read (retries on parse failure)
404
- * - Empty files (returns null)
405
- * - Truncated files (retries then fails gracefully)
406
- *
407
- * Read operations don't require locks - multiple concurrent reads are safe.
408
- * The retry logic handles the case where a read occurs during an atomic write.
409
- *
410
- * @param filePath - Path to the JSON file
411
- * @param options - Read options including retry configuration
412
- * @returns Promise resolving to SafeReadJsonResult with success/failure
413
- *
414
- * @example
415
- * ```typescript
416
- * const result = await safeReadJson<MyConfig>('/path/to/config.json');
417
- * if (result.success) {
418
- * console.log(result.data);
419
- * } else {
420
- * console.error(result.error.message);
421
- * }
422
- * ```
423
- */
424
- export async function safeReadJson<T = unknown>(
425
- filePath: string,
426
- options: SafeReadOptions = {}
427
- ): Promise<SafeReadJsonResult<T>> {
428
- const {
429
- maxRetries = 3,
430
- baseDelayMs = 10,
431
- readFn = (path, encoding) => readFile(path, encoding),
432
- } = options;
433
-
434
- let lastError: Error | undefined;
435
-
436
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
437
- try {
438
- const content = await readFn(filePath, "utf-8");
439
-
440
- // Handle empty file
441
- if (content.trim() === "") {
442
- return { success: true, data: null as T };
443
- }
444
-
445
- const parsed = JSON.parse(content) as T;
446
- return { success: true, data: parsed };
447
- } catch (error) {
448
- lastError = error as Error;
449
-
450
- // File not found or permission errors are not transient
451
- const code = (error as NodeJS.ErrnoException).code;
452
- if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
453
- return {
454
- success: false,
455
- error: new SafeReadError(
456
- `Failed to read JSON file ${filePath}: ${(error as Error).message}`,
457
- filePath,
458
- error as Error
459
- ),
460
- };
461
- }
462
-
463
- // Check if this is a transient error worth retrying
464
- if (isTransientReadError(error) && attempt < maxRetries) {
465
- await backoffDelay(attempt, baseDelayMs);
466
- continue;
467
- }
468
-
469
- // Non-transient error or retries exhausted
470
- return {
471
- success: false,
472
- error: new SafeReadError(
473
- `Failed to parse JSON file ${filePath}: ${(error as Error).message}`,
474
- filePath,
475
- error as Error
476
- ),
477
- };
478
- }
479
- }
480
-
481
- // Should not reach here, but handle it just in case
482
- return {
483
- success: false,
484
- error: new SafeReadError(
485
- `Failed to read JSON file ${filePath} after ${maxRetries + 1} attempts`,
486
- filePath,
487
- lastError
488
- ),
489
- };
490
- }
491
-
492
- /**
493
- * Read a JSON file with retry logic, throwing on failure.
494
- *
495
- * This is a convenience wrapper around safeReadJson that throws
496
- * instead of returning a result object.
497
- *
498
- * @param filePath - Path to the JSON file
499
- * @param options - Read options
500
- * @returns Promise resolving to parsed JSON content
501
- * @throws SafeReadError on failure
502
- */
503
- export async function readJson<T = unknown>(
504
- filePath: string,
505
- options: SafeReadOptions = {}
506
- ): Promise<T> {
507
- const result = await safeReadJson<T>(filePath, options);
508
- if (!result.success) {
509
- throw result.error;
510
- }
511
- return result.data;
512
- }