@docker-harpoon/core 0.1.3 → 0.1.5

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 (98) hide show
  1. package/dist/__tests__/bindings.test.js +9 -5
  2. package/dist/__tests__/container.test.js +30 -9
  3. package/dist/__tests__/database.test.js +0 -14
  4. package/dist/__tests__/docker-infra.template.d.ts +2 -0
  5. package/dist/__tests__/docker-infra.template.d.ts.map +1 -0
  6. package/dist/__tests__/docker-infra.template.js +174 -0
  7. package/dist/__tests__/test-setup.d.ts +9 -0
  8. package/dist/__tests__/test-setup.d.ts.map +1 -0
  9. package/dist/__tests__/test-setup.js +27 -0
  10. package/dist/api/index.d.ts +1 -1
  11. package/dist/api/index.d.ts.map +1 -1
  12. package/dist/api/promise.d.ts +13 -3
  13. package/dist/api/promise.d.ts.map +1 -1
  14. package/dist/api/promise.js +33 -18
  15. package/dist/bindings/index.d.ts +2 -2
  16. package/dist/bindings/index.d.ts.map +1 -1
  17. package/dist/bindings/index.js +1 -1
  18. package/dist/bindings/types.d.ts.map +1 -1
  19. package/dist/bindings/types.js +1 -3
  20. package/dist/build-strategies/types.d.ts.map +1 -1
  21. package/dist/config-patchers/index.d.ts.map +1 -1
  22. package/dist/config-patchers/types.d.ts.map +1 -1
  23. package/dist/dockerfile-transformers/core.d.ts.map +1 -1
  24. package/dist/dockerfile-transformers/core.js +2 -5
  25. package/dist/dockerfile-transformers/index.d.ts.map +1 -1
  26. package/dist/dockerfile-transformers/types.d.ts.map +1 -1
  27. package/dist/errors.d.ts +6 -1
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js +3 -3
  30. package/dist/helpers/database.d.ts.map +1 -1
  31. package/dist/helpers/database.js +1 -3
  32. package/dist/index.d.ts +3 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/resources/container.d.ts +25 -2
  36. package/dist/resources/container.d.ts.map +1 -1
  37. package/dist/resources/container.js +301 -77
  38. package/dist/resources/image.d.ts.map +1 -1
  39. package/dist/resources/image.js +14 -35
  40. package/dist/resources/index.d.ts +1 -1
  41. package/dist/resources/index.d.ts.map +1 -1
  42. package/dist/resources/network.d.ts.map +1 -1
  43. package/dist/resources/network.js +23 -9
  44. package/dist/resources/schemas.d.ts +178 -18
  45. package/dist/resources/schemas.d.ts.map +1 -1
  46. package/dist/resources/schemas.js +2 -1
  47. package/dist/services/CircuitBreaker.d.ts +83 -0
  48. package/dist/services/CircuitBreaker.d.ts.map +1 -0
  49. package/dist/services/CircuitBreaker.js +164 -0
  50. package/dist/services/ContainerPool.d.ts +82 -0
  51. package/dist/services/ContainerPool.d.ts.map +1 -0
  52. package/dist/services/ContainerPool.js +186 -0
  53. package/dist/services/DockerBatcher.d.ts +74 -0
  54. package/dist/services/DockerBatcher.d.ts.map +1 -0
  55. package/dist/services/DockerBatcher.js +107 -0
  56. package/dist/services/DockerClient.d.ts +125 -0
  57. package/dist/services/DockerClient.d.ts.map +1 -0
  58. package/dist/services/DockerClient.js +220 -0
  59. package/dist/services/DockerErrors.d.ts +145 -0
  60. package/dist/services/DockerErrors.d.ts.map +1 -0
  61. package/dist/services/DockerErrors.js +224 -0
  62. package/dist/services/DockerRateLimiter.d.ts +80 -0
  63. package/dist/services/DockerRateLimiter.d.ts.map +1 -0
  64. package/dist/services/DockerRateLimiter.js +93 -0
  65. package/dist/services/EventBus.d.ts +126 -0
  66. package/dist/services/EventBus.d.ts.map +1 -0
  67. package/dist/services/EventBus.js +111 -0
  68. package/dist/services/Harpoon.d.ts +151 -0
  69. package/dist/services/Harpoon.d.ts.map +1 -0
  70. package/dist/services/Harpoon.js +148 -0
  71. package/dist/services/HarpoonConfig.d.ts +60 -0
  72. package/dist/services/HarpoonConfig.d.ts.map +1 -0
  73. package/dist/services/HarpoonConfig.js +67 -0
  74. package/dist/services/HarpoonLogger.d.ts +36 -0
  75. package/dist/services/HarpoonLogger.d.ts.map +1 -0
  76. package/dist/services/HarpoonLogger.js +94 -0
  77. package/dist/services/ReadinessCoordinator.d.ts +128 -0
  78. package/dist/services/ReadinessCoordinator.d.ts.map +1 -0
  79. package/dist/services/ReadinessCoordinator.js +170 -0
  80. package/dist/services/ResourceTracker.d.ts +74 -0
  81. package/dist/services/ResourceTracker.d.ts.map +1 -0
  82. package/dist/services/ResourceTracker.js +145 -0
  83. package/dist/services/index.d.ts +29 -0
  84. package/dist/services/index.d.ts.map +1 -0
  85. package/dist/services/index.js +47 -0
  86. package/dist/testing/helpers.d.ts +114 -0
  87. package/dist/testing/helpers.d.ts.map +1 -0
  88. package/dist/testing/helpers.js +140 -0
  89. package/dist/testing/index.d.ts +29 -0
  90. package/dist/testing/index.d.ts.map +1 -0
  91. package/dist/testing/index.js +47 -0
  92. package/dist/testing/mocks.d.ts +66 -0
  93. package/dist/testing/mocks.d.ts.map +1 -0
  94. package/dist/testing/mocks.js +224 -0
  95. package/dist/utils/process.d.ts +24 -0
  96. package/dist/utils/process.d.ts.map +1 -0
  97. package/dist/utils/process.js +49 -0
  98. package/package.json +12 -8
@@ -8,24 +8,35 @@ import { Effect, Exit } from 'effect';
8
8
  import { toHarpoonError, NetworkError } from '../errors';
9
9
  // Constants
10
10
  const CLEANUP_TIMEOUT_MS = 5_000; // 5 seconds max for network cleanup
11
+ const makeNetworkLogger = (networkName) => ({
12
+ debug: (message) => {
13
+ if (process.env.HARPOON_LOG_LEVEL === 'debug') {
14
+ console.log(`[Network:${networkName}] ${message}`);
15
+ }
16
+ },
17
+ info: (message) => console.log(`[Network:${networkName}] ${message}`),
18
+ warn: (message) => console.warn(`[Network:${networkName}] ${message}`),
19
+ error: (message) => console.error(`[Network:${networkName}] ${message}`),
20
+ });
11
21
  /**
12
22
  * Safely removes a network, handling all possible states.
13
23
  */
14
24
  const safeRemoveNetwork = async (docker, networkId, networkName) => {
25
+ const log = makeNetworkLogger(networkName);
15
26
  try {
16
27
  const network = docker.getNetwork(networkId);
17
28
  await network.remove();
18
- console.log(`[Cleanup] Network removed: ${networkName}`);
29
+ log.info('Network removed');
19
30
  }
20
31
  catch (e) {
21
32
  if (e.statusCode === 404) {
22
- console.log(`[Cleanup] Network ${networkName} already removed`);
33
+ log.debug('Network already removed');
23
34
  }
24
35
  else if (e.message?.includes('has active endpoints')) {
25
- console.warn(`[Cleanup] Network ${networkName} has active endpoints, skipping removal`);
36
+ log.warn('Network has active endpoints, skipping removal');
26
37
  }
27
38
  else {
28
- console.error(`[Cleanup] Error removing network ${networkName}:`, e.message);
39
+ log.error(`Error removing network: ${e.message}`);
29
40
  }
30
41
  }
31
42
  };
@@ -41,21 +52,22 @@ Effect.tryPromise({
41
52
  filters: { name: [config.name] },
42
53
  });
43
54
  const existing = networks.find((n) => n.Name === config.name);
55
+ const log = makeNetworkLogger(config.name);
44
56
  if (existing) {
45
57
  // Try to remove existing network for clean state
46
58
  try {
47
59
  const net = docker.getNetwork(existing.Id);
48
60
  await net.remove();
49
- console.log(`[Network] Removed existing network: ${config.name}`);
61
+ log.info('Removed existing network');
50
62
  }
51
63
  catch (e) {
52
64
  if (e.message?.includes('has active endpoints')) {
53
65
  // Network in use - try to reuse it
54
- console.warn(`[Network] ${config.name} in use, reusing existing network`);
66
+ log.warn('Network in use, reusing existing network');
55
67
  return { id: existing.Id, name: config.name };
56
68
  }
57
69
  // Other errors - try to proceed with creation anyway
58
- console.warn(`[Network] Could not remove ${config.name}: ${e.message}`);
70
+ log.warn(`Could not remove: ${e.message}`);
59
71
  }
60
72
  }
61
73
  // Create new network
@@ -71,7 +83,7 @@ Effect.tryPromise({
71
83
  (resource, exit) => Effect.gen(function* () {
72
84
  const isInterrupted = Exit.isInterrupted(exit);
73
85
  if (isInterrupted) {
74
- console.log(`[Cleanup] Interrupted - cleaning up network: ${resource.name}`);
86
+ yield* Effect.logDebug('Interrupted - cleaning up network').pipe(Effect.annotateLogs({ service: 'Network', networkId: resource.id }));
75
87
  }
76
88
  yield* Effect.tryPromise({
77
89
  try: () => Promise.race([
@@ -79,7 +91,9 @@ Effect.tryPromise({
79
91
  new Promise((_, reject) => setTimeout(() => reject(new Error('Network cleanup timeout')), CLEANUP_TIMEOUT_MS)),
80
92
  ]),
81
93
  catch: (e) => {
82
- console.error(`[Cleanup] Failed to clean up network ${resource.name}:`, e);
94
+ // Log error but don't fail - cleanup should be best effort
95
+ const log = makeNetworkLogger(resource.name);
96
+ log.error(`Failed to clean up network: ${e}`);
83
97
  return null;
84
98
  },
85
99
  });
@@ -2,7 +2,7 @@
2
2
  * Zod Schemas for Container Resources
3
3
  *
4
4
  * Defines validation schemas for container stats, exec, and log streaming.
5
- * Following Alchemy.run patterns for declarative resource definitions.
5
+ * Following Harpoon patterns for declarative resource definitions.
6
6
  */
7
7
  import { z } from 'zod';
8
8
  /**
@@ -13,7 +13,17 @@ export declare const cpuStatsSchema: z.ZodObject<{
13
13
  system: z.ZodNumber;
14
14
  percent: z.ZodNumber;
15
15
  cores: z.ZodNumber;
16
- }, z.core.$strip>;
16
+ }, "strip", z.ZodTypeAny, {
17
+ usage: number;
18
+ system: number;
19
+ percent: number;
20
+ cores: number;
21
+ }, {
22
+ usage: number;
23
+ system: number;
24
+ percent: number;
25
+ cores: number;
26
+ }>;
17
27
  /**
18
28
  * Memory statistics from Docker container
19
29
  */
@@ -23,7 +33,19 @@ export declare const memoryStatsSchema: z.ZodObject<{
23
33
  percent: z.ZodNumber;
24
34
  usageMB: z.ZodNumber;
25
35
  limitMB: z.ZodNumber;
26
- }, z.core.$strip>;
36
+ }, "strip", z.ZodTypeAny, {
37
+ usage: number;
38
+ limit: number;
39
+ percent: number;
40
+ usageMB: number;
41
+ limitMB: number;
42
+ }, {
43
+ usage: number;
44
+ limit: number;
45
+ percent: number;
46
+ usageMB: number;
47
+ limitMB: number;
48
+ }>;
27
49
  /**
28
50
  * Network I/O statistics from Docker container
29
51
  */
@@ -32,7 +54,17 @@ export declare const networkStatsSchema: z.ZodObject<{
32
54
  txBytes: z.ZodNumber;
33
55
  rxPackets: z.ZodNumber;
34
56
  txPackets: z.ZodNumber;
35
- }, z.core.$strip>;
57
+ }, "strip", z.ZodTypeAny, {
58
+ rxBytes: number;
59
+ txBytes: number;
60
+ rxPackets: number;
61
+ txPackets: number;
62
+ }, {
63
+ rxBytes: number;
64
+ txBytes: number;
65
+ rxPackets: number;
66
+ txPackets: number;
67
+ }>;
36
68
  /**
37
69
  * Complete container statistics
38
70
  */
@@ -42,23 +74,99 @@ export declare const containerStatsSchema: z.ZodObject<{
42
74
  system: z.ZodNumber;
43
75
  percent: z.ZodNumber;
44
76
  cores: z.ZodNumber;
45
- }, z.core.$strip>;
77
+ }, "strip", z.ZodTypeAny, {
78
+ usage: number;
79
+ system: number;
80
+ percent: number;
81
+ cores: number;
82
+ }, {
83
+ usage: number;
84
+ system: number;
85
+ percent: number;
86
+ cores: number;
87
+ }>;
46
88
  memory: z.ZodObject<{
47
89
  usage: z.ZodNumber;
48
90
  limit: z.ZodNumber;
49
91
  percent: z.ZodNumber;
50
92
  usageMB: z.ZodNumber;
51
93
  limitMB: z.ZodNumber;
52
- }, z.core.$strip>;
94
+ }, "strip", z.ZodTypeAny, {
95
+ usage: number;
96
+ limit: number;
97
+ percent: number;
98
+ usageMB: number;
99
+ limitMB: number;
100
+ }, {
101
+ usage: number;
102
+ limit: number;
103
+ percent: number;
104
+ usageMB: number;
105
+ limitMB: number;
106
+ }>;
53
107
  network: z.ZodObject<{
54
108
  rxBytes: z.ZodNumber;
55
109
  txBytes: z.ZodNumber;
56
110
  rxPackets: z.ZodNumber;
57
111
  txPackets: z.ZodNumber;
58
- }, z.core.$strip>;
112
+ }, "strip", z.ZodTypeAny, {
113
+ rxBytes: number;
114
+ txBytes: number;
115
+ rxPackets: number;
116
+ txPackets: number;
117
+ }, {
118
+ rxBytes: number;
119
+ txBytes: number;
120
+ rxPackets: number;
121
+ txPackets: number;
122
+ }>;
59
123
  timestamp: z.ZodString;
60
124
  containerId: z.ZodString;
61
- }, z.core.$strip>;
125
+ }, "strip", z.ZodTypeAny, {
126
+ cpu: {
127
+ usage: number;
128
+ system: number;
129
+ percent: number;
130
+ cores: number;
131
+ };
132
+ memory: {
133
+ usage: number;
134
+ limit: number;
135
+ percent: number;
136
+ usageMB: number;
137
+ limitMB: number;
138
+ };
139
+ network: {
140
+ rxBytes: number;
141
+ txBytes: number;
142
+ rxPackets: number;
143
+ txPackets: number;
144
+ };
145
+ timestamp: string;
146
+ containerId: string;
147
+ }, {
148
+ cpu: {
149
+ usage: number;
150
+ system: number;
151
+ percent: number;
152
+ cores: number;
153
+ };
154
+ memory: {
155
+ usage: number;
156
+ limit: number;
157
+ percent: number;
158
+ usageMB: number;
159
+ limitMB: number;
160
+ };
161
+ network: {
162
+ rxBytes: number;
163
+ txBytes: number;
164
+ rxPackets: number;
165
+ txPackets: number;
166
+ };
167
+ timestamp: string;
168
+ containerId: string;
169
+ }>;
62
170
  export type ContainerStats = z.infer<typeof containerStatsSchema>;
63
171
  export type CpuStats = z.infer<typeof cpuStatsSchema>;
64
172
  export type MemoryStats = z.infer<typeof memoryStatsSchema>;
@@ -67,13 +175,27 @@ export type NetworkStats = z.infer<typeof networkStatsSchema>;
67
175
  * Options for executing a command in a container
68
176
  */
69
177
  export declare const execOptionsSchema: z.ZodObject<{
70
- cmd: z.ZodArray<z.ZodString>;
178
+ cmd: z.ZodArray<z.ZodString, "many">;
71
179
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
72
180
  workingDir: z.ZodOptional<z.ZodString>;
73
181
  user: z.ZodOptional<z.ZodString>;
74
182
  privileged: z.ZodOptional<z.ZodBoolean>;
75
183
  tty: z.ZodOptional<z.ZodBoolean>;
76
- }, z.core.$strip>;
184
+ }, "strip", z.ZodTypeAny, {
185
+ cmd: string[];
186
+ env?: Record<string, string> | undefined;
187
+ workingDir?: string | undefined;
188
+ user?: string | undefined;
189
+ privileged?: boolean | undefined;
190
+ tty?: boolean | undefined;
191
+ }, {
192
+ cmd: string[];
193
+ env?: Record<string, string> | undefined;
194
+ workingDir?: string | undefined;
195
+ user?: string | undefined;
196
+ privileged?: boolean | undefined;
197
+ tty?: boolean | undefined;
198
+ }>;
77
199
  /**
78
200
  * Result of executing a command in a container
79
201
  */
@@ -82,7 +204,17 @@ export declare const execResultSchema: z.ZodObject<{
82
204
  stdout: z.ZodString;
83
205
  stderr: z.ZodString;
84
206
  durationMs: z.ZodNumber;
85
- }, z.core.$strip>;
207
+ }, "strip", z.ZodTypeAny, {
208
+ exitCode: number;
209
+ stdout: string;
210
+ stderr: string;
211
+ durationMs: number;
212
+ }, {
213
+ exitCode: number;
214
+ stdout: string;
215
+ stderr: string;
216
+ durationMs: number;
217
+ }>;
86
218
  export type ExecOptions = z.infer<typeof execOptionsSchema>;
87
219
  export type ExecResult = z.infer<typeof execResultSchema>;
88
220
  /**
@@ -95,18 +227,40 @@ export declare const logOptionsSchema: z.ZodObject<{
95
227
  until: z.ZodOptional<z.ZodNumber>;
96
228
  tail: z.ZodOptional<z.ZodNumber>;
97
229
  timestamps: z.ZodOptional<z.ZodBoolean>;
98
- }, z.core.$strip>;
230
+ follow: z.ZodOptional<z.ZodBoolean>;
231
+ }, "strip", z.ZodTypeAny, {
232
+ stdout?: boolean | undefined;
233
+ stderr?: boolean | undefined;
234
+ since?: number | undefined;
235
+ until?: number | undefined;
236
+ tail?: number | undefined;
237
+ timestamps?: boolean | undefined;
238
+ follow?: boolean | undefined;
239
+ }, {
240
+ stdout?: boolean | undefined;
241
+ stderr?: boolean | undefined;
242
+ since?: number | undefined;
243
+ until?: number | undefined;
244
+ tail?: number | undefined;
245
+ timestamps?: boolean | undefined;
246
+ follow?: boolean | undefined;
247
+ }>;
99
248
  /**
100
249
  * A single log line from a container
101
250
  */
102
251
  export declare const logLineSchema: z.ZodObject<{
103
- stream: z.ZodEnum<{
104
- stderr: "stderr";
105
- stdout: "stdout";
106
- }>;
252
+ stream: z.ZodEnum<["stdout", "stderr"]>;
107
253
  timestamp: z.ZodOptional<z.ZodString>;
108
254
  message: z.ZodString;
109
- }, z.core.$strip>;
255
+ }, "strip", z.ZodTypeAny, {
256
+ stream: "stderr" | "stdout";
257
+ timestamp?: string | undefined;
258
+ message: string;
259
+ }, {
260
+ stream: "stderr" | "stdout";
261
+ timestamp?: string | undefined;
262
+ message: string;
263
+ }>;
110
264
  export type LogOptions = z.infer<typeof logOptionsSchema>;
111
265
  export type LogLine = z.infer<typeof logLineSchema>;
112
266
  /**
@@ -115,6 +269,12 @@ export type LogLine = z.infer<typeof logLineSchema>;
115
269
  export declare const statsOptionsSchema: z.ZodObject<{
116
270
  stream: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
117
271
  oneShot: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
118
- }, z.core.$strip>;
272
+ }, "strip", z.ZodTypeAny, {
273
+ stream: boolean;
274
+ oneShot: boolean;
275
+ }, {
276
+ stream?: boolean | undefined;
277
+ oneShot?: boolean | undefined;
278
+ }>;
119
279
  export type StatsOptions = z.infer<typeof statsOptionsSchema>;
120
280
  //# sourceMappingURL=schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/resources/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;iBAKzB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;iBAM5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;iBAK7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;iBAM/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;iBAO5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;iBAK3B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;iBAO3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;iBAIxB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/resources/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAKzB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;EAM5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAK7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAM/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;EAO5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;EAK3B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;EAQ3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;EAIxB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;EAG7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -2,7 +2,7 @@
2
2
  * Zod Schemas for Container Resources
3
3
  *
4
4
  * Defines validation schemas for container stats, exec, and log streaming.
5
- * Following Alchemy.run patterns for declarative resource definitions.
5
+ * Following Harpoon patterns for declarative resource definitions.
6
6
  */
7
7
  import { z } from 'zod';
8
8
  // ============ Container Stats Schema ============
@@ -76,6 +76,7 @@ export const logOptionsSchema = z.object({
76
76
  until: z.number().optional().describe('Unix timestamp to end at'),
77
77
  tail: z.number().int().min(0).optional().describe('Number of lines to tail'),
78
78
  timestamps: z.boolean().optional().describe('Include timestamps (default: false)'),
79
+ follow: z.boolean().optional().describe('Follow log output (default: false)'),
79
80
  });
80
81
  /**
81
82
  * A single log line from a container
@@ -0,0 +1,83 @@
1
+ /**
2
+ * CircuitBreaker Service
3
+ *
4
+ * Implements the circuit breaker pattern to prevent cascading failures
5
+ * when the Docker daemon becomes unresponsive or overloaded.
6
+ *
7
+ * States:
8
+ * - Closed: Normal operation, requests pass through
9
+ * - Open: Circuit tripped, requests fail fast
10
+ * - HalfOpen: Testing if the service has recovered
11
+ */
12
+ import { Context, Effect, Layer, Duration } from 'effect';
13
+ /**
14
+ * Circuit breaker states.
15
+ */
16
+ export type CircuitState = 'Closed' | 'Open' | 'HalfOpen';
17
+ /**
18
+ * Circuit breaker configuration.
19
+ */
20
+ export interface CircuitBreakerConfig {
21
+ /** Number of failures before opening the circuit */
22
+ readonly maxFailures: number;
23
+ /** Time to wait before attempting to close the circuit */
24
+ readonly resetTimeout: Duration.Duration;
25
+ /** Optional name for logging */
26
+ readonly name?: string;
27
+ }
28
+ /**
29
+ * Error thrown when the circuit is open.
30
+ */
31
+ export declare class CircuitOpenError extends Error {
32
+ readonly _tag = "CircuitOpenError";
33
+ readonly circuitName: string;
34
+ readonly resetTimeout: Duration.Duration;
35
+ constructor(circuitName: string, resetTimeout: Duration.Duration);
36
+ }
37
+ /**
38
+ * CircuitBreaker service interface.
39
+ */
40
+ export interface CircuitBreakerService {
41
+ readonly _tag: 'CircuitBreaker';
42
+ /**
43
+ * Execute an effect protected by the circuit breaker.
44
+ * If the circuit is open, fails fast with CircuitOpenError.
45
+ */
46
+ readonly protect: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | CircuitOpenError, R>;
47
+ /**
48
+ * Get the current circuit state.
49
+ */
50
+ readonly getState: () => Effect.Effect<CircuitState>;
51
+ /**
52
+ * Manually reset the circuit to closed state.
53
+ */
54
+ readonly reset: () => Effect.Effect<void>;
55
+ /**
56
+ * Get current failure count.
57
+ */
58
+ readonly getFailures: () => Effect.Effect<number>;
59
+ }
60
+ declare const CircuitBreaker_base: Context.TagClass<CircuitBreaker, "@harpoon/CircuitBreaker", CircuitBreakerService>;
61
+ /**
62
+ * CircuitBreaker service tag for dependency injection.
63
+ */
64
+ export declare class CircuitBreaker extends CircuitBreaker_base {
65
+ }
66
+ /**
67
+ * Factory function to create a circuit breaker with specific configuration.
68
+ */
69
+ export declare const makeCircuitBreaker: (config: CircuitBreakerConfig) => Effect.Effect<CircuitBreakerService, never, never>;
70
+ /**
71
+ * Live implementation with default configuration.
72
+ */
73
+ export declare const CircuitBreakerLive: Layer.Layer<CircuitBreaker, never, never>;
74
+ /**
75
+ * Test implementation that never opens (all requests pass through).
76
+ */
77
+ export declare const CircuitBreakerTest: Layer.Layer<CircuitBreaker, never, never>;
78
+ /**
79
+ * Create a custom circuit breaker layer with specific configuration.
80
+ */
81
+ export declare const makeCircuitBreakerLayer: (config: CircuitBreakerConfig) => Layer.Layer<CircuitBreaker, never, never>;
82
+ export {};
83
+ //# sourceMappingURL=CircuitBreaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CircuitBreaker.d.ts","sourceRoot":"","sources":["../../src/services/CircuitBreaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAO,QAAQ,EAAS,MAAM,QAAQ,CAAC;AAEtE;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAEzC,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAEzC,YAAY,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,QAAQ,EAQ/D;CACF;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAE/C;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAErD;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAE1C;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;CACnD;;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,mBAGjC;CAAG;AAEN;;GAEG;AACH,eAAO,MAAM,kBAAkB,sFA+I3B,CAAC;AAWL;;GAEG;AACH,eAAO,MAAM,kBAAkB,2CAAkE,CAAC;AAElG;;GAEG;AACH,eAAO,MAAM,kBAAkB,2CAO7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,uBAAuB,6EAEsD,CAAC"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * CircuitBreaker Service
3
+ *
4
+ * Implements the circuit breaker pattern to prevent cascading failures
5
+ * when the Docker daemon becomes unresponsive or overloaded.
6
+ *
7
+ * States:
8
+ * - Closed: Normal operation, requests pass through
9
+ * - Open: Circuit tripped, requests fail fast
10
+ * - HalfOpen: Testing if the service has recovered
11
+ */
12
+ import { Context, Effect, Layer, Ref, Duration, Clock } from 'effect';
13
+ /**
14
+ * Error thrown when the circuit is open.
15
+ */
16
+ export class CircuitOpenError extends Error {
17
+ _tag = 'CircuitOpenError';
18
+ circuitName;
19
+ resetTimeout;
20
+ constructor(circuitName, resetTimeout) {
21
+ super(`Circuit breaker "${circuitName}" is open. ` +
22
+ `Retry after ${Duration.toMillis(resetTimeout)}ms`);
23
+ this.circuitName = circuitName;
24
+ this.resetTimeout = resetTimeout;
25
+ this.name = 'CircuitOpenError';
26
+ }
27
+ }
28
+ /**
29
+ * CircuitBreaker service tag for dependency injection.
30
+ */
31
+ export class CircuitBreaker extends Context.Tag('@harpoon/CircuitBreaker')() {
32
+ }
33
+ /**
34
+ * Factory function to create a circuit breaker with specific configuration.
35
+ */
36
+ export const makeCircuitBreaker = (config) => Effect.gen(function* () {
37
+ const name = config.name ?? 'default';
38
+ const resetTimeoutMs = Duration.toMillis(config.resetTimeout);
39
+ // Initialize state
40
+ const stateRef = yield* Ref.make({
41
+ state: 'Closed',
42
+ failures: 0,
43
+ lastFailure: null,
44
+ successesSinceHalfOpen: 0,
45
+ });
46
+ // Helper to check if we should transition from Open to HalfOpen
47
+ const checkReset = Effect.gen(function* () {
48
+ const current = yield* Ref.get(stateRef);
49
+ if (current.state !== 'Open')
50
+ return;
51
+ const now = yield* Clock.currentTimeMillis;
52
+ const timeSinceFailure = current.lastFailure ? now - current.lastFailure : Infinity;
53
+ if (timeSinceFailure >= resetTimeoutMs) {
54
+ yield* Ref.update(stateRef, (s) => ({
55
+ ...s,
56
+ state: 'HalfOpen',
57
+ successesSinceHalfOpen: 0,
58
+ }));
59
+ yield* Effect.logDebug('Circuit breaker transitioning to HalfOpen').pipe(Effect.annotateLogs({ circuit: name }));
60
+ }
61
+ });
62
+ // Record a failure
63
+ const recordFailure = Effect.gen(function* () {
64
+ const now = yield* Clock.currentTimeMillis;
65
+ const current = yield* Ref.get(stateRef);
66
+ if (current.state === 'HalfOpen') {
67
+ // Any failure in HalfOpen reopens the circuit
68
+ yield* Ref.set(stateRef, {
69
+ state: 'Open',
70
+ failures: current.failures + 1,
71
+ lastFailure: now,
72
+ successesSinceHalfOpen: 0,
73
+ });
74
+ yield* Effect.logWarning('Circuit breaker reopened from HalfOpen').pipe(Effect.annotateLogs({ circuit: name }));
75
+ }
76
+ else {
77
+ const newFailures = current.failures + 1;
78
+ const shouldOpen = newFailures >= config.maxFailures;
79
+ yield* Ref.set(stateRef, {
80
+ state: shouldOpen ? 'Open' : 'Closed',
81
+ failures: newFailures,
82
+ lastFailure: now,
83
+ successesSinceHalfOpen: 0,
84
+ });
85
+ if (shouldOpen) {
86
+ yield* Effect.logWarning('Circuit breaker opened').pipe(Effect.annotateLogs({
87
+ circuit: name,
88
+ failures: newFailures,
89
+ maxFailures: config.maxFailures,
90
+ }));
91
+ }
92
+ }
93
+ });
94
+ // Record a success
95
+ const recordSuccess = Effect.gen(function* () {
96
+ const current = yield* Ref.get(stateRef);
97
+ if (current.state === 'HalfOpen') {
98
+ // Success in HalfOpen mode - close the circuit
99
+ yield* Ref.set(stateRef, {
100
+ state: 'Closed',
101
+ failures: 0,
102
+ lastFailure: null,
103
+ successesSinceHalfOpen: 0,
104
+ });
105
+ yield* Effect.logInfo('Circuit breaker closed after successful probe').pipe(Effect.annotateLogs({ circuit: name }));
106
+ }
107
+ else if (current.state === 'Closed' && current.failures > 0) {
108
+ // Reset failure count on success in closed state
109
+ yield* Ref.update(stateRef, (s) => ({
110
+ ...s,
111
+ failures: 0,
112
+ }));
113
+ }
114
+ });
115
+ const service = {
116
+ _tag: 'CircuitBreaker',
117
+ protect: (effect) => Effect.gen(function* () {
118
+ // Check if we should transition from Open to HalfOpen
119
+ yield* checkReset;
120
+ const current = yield* Ref.get(stateRef);
121
+ if (current.state === 'Open') {
122
+ return yield* Effect.fail(new CircuitOpenError(name, config.resetTimeout));
123
+ }
124
+ // Execute the effect
125
+ const result = yield* effect.pipe(Effect.tap(() => recordSuccess), Effect.tapError(() => recordFailure));
126
+ return result;
127
+ }),
128
+ getState: () => Ref.get(stateRef).pipe(Effect.map((s) => s.state)),
129
+ reset: () => Ref.set(stateRef, {
130
+ state: 'Closed',
131
+ failures: 0,
132
+ lastFailure: null,
133
+ successesSinceHalfOpen: 0,
134
+ }).pipe(Effect.tap(() => Effect.logInfo('Circuit breaker manually reset').pipe(Effect.annotateLogs({ circuit: name })))),
135
+ getFailures: () => Ref.get(stateRef).pipe(Effect.map((s) => s.failures)),
136
+ };
137
+ return service;
138
+ });
139
+ /**
140
+ * Default circuit breaker configuration.
141
+ */
142
+ const defaultConfig = {
143
+ maxFailures: 5,
144
+ resetTimeout: Duration.seconds(30),
145
+ name: 'docker',
146
+ };
147
+ /**
148
+ * Live implementation with default configuration.
149
+ */
150
+ export const CircuitBreakerLive = Layer.effect(CircuitBreaker, makeCircuitBreaker(defaultConfig));
151
+ /**
152
+ * Test implementation that never opens (all requests pass through).
153
+ */
154
+ export const CircuitBreakerTest = Layer.succeed(CircuitBreaker, {
155
+ _tag: 'CircuitBreaker',
156
+ protect: (effect) => effect,
157
+ getState: () => Effect.succeed('Closed'),
158
+ reset: () => Effect.void,
159
+ getFailures: () => Effect.succeed(0),
160
+ });
161
+ /**
162
+ * Create a custom circuit breaker layer with specific configuration.
163
+ */
164
+ export const makeCircuitBreakerLayer = (config) => Layer.effect(CircuitBreaker, makeCircuitBreaker(config));