@docker-harpoon/core 0.1.4 → 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 (87) hide show
  1. package/dist/__tests__/test-setup.d.ts.map +1 -1
  2. package/dist/__tests__/test-setup.js +8 -1
  3. package/dist/api/promise.d.ts +1 -1
  4. package/dist/api/promise.d.ts.map +1 -1
  5. package/dist/api/promise.js +28 -18
  6. package/dist/bindings/index.d.ts +2 -2
  7. package/dist/bindings/index.d.ts.map +1 -1
  8. package/dist/bindings/index.js +1 -1
  9. package/dist/bindings/types.d.ts.map +1 -1
  10. package/dist/bindings/types.js +1 -3
  11. package/dist/build-strategies/types.d.ts.map +1 -1
  12. package/dist/config-patchers/index.d.ts.map +1 -1
  13. package/dist/config-patchers/types.d.ts.map +1 -1
  14. package/dist/dockerfile-transformers/core.d.ts.map +1 -1
  15. package/dist/dockerfile-transformers/core.js +2 -5
  16. package/dist/dockerfile-transformers/index.d.ts.map +1 -1
  17. package/dist/dockerfile-transformers/types.d.ts.map +1 -1
  18. package/dist/errors.d.ts +6 -1
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +3 -3
  21. package/dist/helpers/database.d.ts.map +1 -1
  22. package/dist/helpers/database.js +1 -3
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/resources/container.d.ts +7 -2
  27. package/dist/resources/container.d.ts.map +1 -1
  28. package/dist/resources/container.js +222 -31
  29. package/dist/resources/image.d.ts.map +1 -1
  30. package/dist/resources/image.js +14 -35
  31. package/dist/resources/network.d.ts.map +1 -1
  32. package/dist/resources/network.js +23 -9
  33. package/dist/resources/schemas.d.ts +177 -18
  34. package/dist/resources/schemas.d.ts.map +1 -1
  35. package/dist/resources/schemas.js +1 -1
  36. package/dist/services/CircuitBreaker.d.ts +83 -0
  37. package/dist/services/CircuitBreaker.d.ts.map +1 -0
  38. package/dist/services/CircuitBreaker.js +164 -0
  39. package/dist/services/ContainerPool.d.ts +82 -0
  40. package/dist/services/ContainerPool.d.ts.map +1 -0
  41. package/dist/services/ContainerPool.js +186 -0
  42. package/dist/services/DockerBatcher.d.ts +74 -0
  43. package/dist/services/DockerBatcher.d.ts.map +1 -0
  44. package/dist/services/DockerBatcher.js +107 -0
  45. package/dist/services/DockerClient.d.ts +125 -0
  46. package/dist/services/DockerClient.d.ts.map +1 -0
  47. package/dist/services/DockerClient.js +220 -0
  48. package/dist/services/DockerErrors.d.ts +145 -0
  49. package/dist/services/DockerErrors.d.ts.map +1 -0
  50. package/dist/services/DockerErrors.js +224 -0
  51. package/dist/services/DockerRateLimiter.d.ts +80 -0
  52. package/dist/services/DockerRateLimiter.d.ts.map +1 -0
  53. package/dist/services/DockerRateLimiter.js +93 -0
  54. package/dist/services/EventBus.d.ts +126 -0
  55. package/dist/services/EventBus.d.ts.map +1 -0
  56. package/dist/services/EventBus.js +111 -0
  57. package/dist/services/Harpoon.d.ts +151 -0
  58. package/dist/services/Harpoon.d.ts.map +1 -0
  59. package/dist/services/Harpoon.js +148 -0
  60. package/dist/services/HarpoonConfig.d.ts +60 -0
  61. package/dist/services/HarpoonConfig.d.ts.map +1 -0
  62. package/dist/services/HarpoonConfig.js +67 -0
  63. package/dist/services/HarpoonLogger.d.ts +36 -0
  64. package/dist/services/HarpoonLogger.d.ts.map +1 -0
  65. package/dist/services/HarpoonLogger.js +94 -0
  66. package/dist/services/ReadinessCoordinator.d.ts +128 -0
  67. package/dist/services/ReadinessCoordinator.d.ts.map +1 -0
  68. package/dist/services/ReadinessCoordinator.js +170 -0
  69. package/dist/services/ResourceTracker.d.ts +74 -0
  70. package/dist/services/ResourceTracker.d.ts.map +1 -0
  71. package/dist/services/ResourceTracker.js +145 -0
  72. package/dist/services/index.d.ts +29 -0
  73. package/dist/services/index.d.ts.map +1 -0
  74. package/dist/services/index.js +47 -0
  75. package/dist/testing/helpers.d.ts +114 -0
  76. package/dist/testing/helpers.d.ts.map +1 -0
  77. package/dist/testing/helpers.js +140 -0
  78. package/dist/testing/index.d.ts +29 -0
  79. package/dist/testing/index.d.ts.map +1 -0
  80. package/dist/testing/index.js +47 -0
  81. package/dist/testing/mocks.d.ts +66 -0
  82. package/dist/testing/mocks.d.ts.map +1 -0
  83. package/dist/testing/mocks.js +224 -0
  84. package/dist/utils/process.d.ts +24 -0
  85. package/dist/utils/process.d.ts.map +1 -0
  86. package/dist/utils/process.js +49 -0
  87. package/package.json +12 -8
@@ -0,0 +1,220 @@
1
+ /**
2
+ * DockerClient Service
3
+ *
4
+ * Provides a typed interface to Docker operations using Effect.
5
+ * This service wraps dockerode and converts all operations to Effect-based APIs.
6
+ */
7
+ import { Context, Effect, Layer } from 'effect';
8
+ import Docker from 'dockerode';
9
+ import { existsSync } from 'fs';
10
+ import { homedir, platform } from 'os';
11
+ import { HarpoonConfig } from './HarpoonConfig';
12
+ import { DockerConnectionError, wrapAsDockerError, isTransientError, } from './DockerErrors';
13
+ import { DockerRateLimiter, DockerRateLimiterLive, dockerRetrySchedule } from './DockerRateLimiter';
14
+ /**
15
+ * DockerClient service tag for dependency injection.
16
+ */
17
+ export class DockerClient extends Context.Tag('@harpoon/DockerClient')() {
18
+ }
19
+ /**
20
+ * Detect Docker socket path for the current platform.
21
+ */
22
+ function detectDockerSocket() {
23
+ const candidates = [
24
+ '/var/run/docker.sock',
25
+ `${homedir()}/.docker/run/docker.sock`,
26
+ // Colima on macOS
27
+ `${homedir()}/.colima/default/docker.sock`,
28
+ ];
29
+ for (const path of candidates) {
30
+ if (existsSync(path)) {
31
+ return path;
32
+ }
33
+ }
34
+ // Windows uses named pipes, let dockerode handle it
35
+ if (platform() === 'win32') {
36
+ return '//./pipe/docker_engine';
37
+ }
38
+ // Default fallback
39
+ return '/var/run/docker.sock';
40
+ }
41
+ /**
42
+ * Helper to add retry logic for transient errors.
43
+ */
44
+ const withRetryOnTransient = (effect) => effect.pipe(Effect.retry({
45
+ schedule: dockerRetrySchedule,
46
+ while: (error) => isTransientError(error),
47
+ }));
48
+ /**
49
+ * Live implementation of DockerClient.
50
+ *
51
+ * Creates a connection to Docker and verifies it on startup.
52
+ * Requires HarpoonConfig and DockerRateLimiter to be provided.
53
+ *
54
+ * All Docker API operations are:
55
+ * - Rate limited via semaphore
56
+ * - Retried on transient errors with exponential backoff
57
+ */
58
+ export const DockerClientLive = Layer.scoped(DockerClient, Effect.gen(function* () {
59
+ const config = yield* HarpoonConfig;
60
+ const rateLimiter = yield* DockerRateLimiter;
61
+ // Determine socket path
62
+ const socketPath = config.dockerSocket ?? detectDockerSocket();
63
+ // Create Docker client
64
+ const docker = new Docker({ socketPath });
65
+ // Verify connection on startup (with retry for transient errors)
66
+ yield* Effect.tryPromise({
67
+ try: () => docker.ping(),
68
+ catch: (e) => new DockerConnectionError(`Failed to connect to Docker daemon`, {
69
+ socketPath,
70
+ cause: e,
71
+ }),
72
+ }).pipe(withRetryOnTransient);
73
+ yield* Effect.logDebug('Connected to Docker daemon').pipe(Effect.annotateLogs({ socketPath }));
74
+ // Helper to wrap operations with rate limiting and retry
75
+ const withRateLimitAndRetry = (effect) => rateLimiter.withPermit(withRetryOnTransient(effect));
76
+ // Create the service implementation
77
+ const service = {
78
+ _tag: 'DockerClient',
79
+ client: docker,
80
+ ping: () => rateLimiter.withPermit(Effect.tryPromise({
81
+ try: () => docker.ping(),
82
+ catch: (e) => new DockerConnectionError(`Docker ping failed`, {
83
+ socketPath,
84
+ cause: e,
85
+ }),
86
+ }).pipe(Effect.asVoid)),
87
+ version: () => withRateLimitAndRetry(Effect.tryPromise({
88
+ try: () => docker.version(),
89
+ catch: (e) => wrapAsDockerError('version', e),
90
+ })),
91
+ listContainers: (options) => withRateLimitAndRetry(Effect.tryPromise({
92
+ try: () => docker.listContainers(options),
93
+ catch: (e) => wrapAsDockerError('listContainers', e),
94
+ })),
95
+ getContainer: (id) => docker.getContainer(id),
96
+ createContainer: (options) => withRateLimitAndRetry(Effect.tryPromise({
97
+ try: () => docker.createContainer(options),
98
+ catch: (e) => wrapAsDockerError('createContainer', e),
99
+ })),
100
+ listNetworks: (options) => withRateLimitAndRetry(Effect.tryPromise({
101
+ try: () => docker.listNetworks(options),
102
+ catch: (e) => wrapAsDockerError('listNetworks', e),
103
+ })),
104
+ getNetwork: (id) => docker.getNetwork(id),
105
+ createNetwork: (options) => withRateLimitAndRetry(Effect.tryPromise({
106
+ try: () => docker.createNetwork(options),
107
+ catch: (e) => wrapAsDockerError('createNetwork', e),
108
+ })),
109
+ getImage: (name) => docker.getImage(name),
110
+ listImages: () => withRateLimitAndRetry(Effect.tryPromise({
111
+ try: () => docker.listImages(),
112
+ catch: (e) => wrapAsDockerError('listImages', e),
113
+ })),
114
+ pullImage: (name, options) =>
115
+ // Pull uses multiple permits (2) as it's a heavier operation
116
+ rateLimiter.withPermits(2)(withRetryOnTransient(Effect.tryPromise({
117
+ try: async () => {
118
+ await new Promise((resolve, reject) => {
119
+ docker.pull(name, options ?? {}, (err, stream) => {
120
+ if (err || !stream) {
121
+ reject(err ?? new Error('No stream returned from pull'));
122
+ return;
123
+ }
124
+ // Follow pull progress to completion
125
+ docker.modem.followProgress(stream, (followErr) => {
126
+ if (followErr)
127
+ reject(followErr);
128
+ else
129
+ resolve();
130
+ });
131
+ });
132
+ });
133
+ },
134
+ catch: (e) => wrapAsDockerError('pullImage', e),
135
+ }))),
136
+ };
137
+ return service;
138
+ })).pipe(Layer.provide(DockerRateLimiterLive));
139
+ /**
140
+ * Create a mock container for testing.
141
+ */
142
+ const createMockContainer = (id) => ({
143
+ id,
144
+ start: () => Promise.resolve(),
145
+ stop: () => Promise.resolve(),
146
+ kill: () => Promise.resolve(),
147
+ remove: () => Promise.resolve(),
148
+ inspect: () => Promise.resolve({
149
+ Id: id,
150
+ Name: '/mock-container',
151
+ State: { Running: true },
152
+ NetworkSettings: { Networks: {} },
153
+ }),
154
+ logs: () => Promise.resolve(Buffer.from('')),
155
+ stats: () => Promise.resolve({}),
156
+ exec: () => Promise.resolve({ start: () => Promise.resolve({}) }),
157
+ });
158
+ /**
159
+ * Create a mock network for testing.
160
+ */
161
+ const createMockNetwork = (id) => ({
162
+ id,
163
+ inspect: () => Promise.resolve({ Id: id, Name: 'mock-network' }),
164
+ remove: () => Promise.resolve(),
165
+ connect: () => Promise.resolve(),
166
+ disconnect: () => Promise.resolve(),
167
+ });
168
+ /**
169
+ * Test implementation of DockerClient.
170
+ *
171
+ * Returns mock responses for all operations.
172
+ * Useful for unit tests that don't need real Docker.
173
+ */
174
+ export const DockerClientTest = Layer.succeed(DockerClient, {
175
+ _tag: 'DockerClient',
176
+ client: {},
177
+ ping: () => Effect.void,
178
+ version: () => Effect.succeed({
179
+ Version: '24.0.0',
180
+ ApiVersion: '1.43',
181
+ Os: 'linux',
182
+ Arch: 'amd64',
183
+ }),
184
+ listContainers: () => Effect.succeed([]),
185
+ getContainer: (id) => createMockContainer(id),
186
+ createContainer: (options) => Effect.succeed(createMockContainer(`mock-${options.name}`)),
187
+ listNetworks: () => Effect.succeed([]),
188
+ getNetwork: (id) => createMockNetwork(id),
189
+ createNetwork: (options) => Effect.succeed(createMockNetwork(`mock-${options.Name}`)),
190
+ getImage: (name) => ({
191
+ inspect: () => Promise.resolve({ Id: name }),
192
+ remove: () => Promise.resolve(),
193
+ }),
194
+ listImages: () => Effect.succeed([]),
195
+ pullImage: () => Effect.void,
196
+ });
197
+ /**
198
+ * Create a custom test client with specific mock behaviors.
199
+ */
200
+ export const makeDockerClientTest = (overrides) => Layer.succeed(DockerClient, {
201
+ _tag: 'DockerClient',
202
+ client: {},
203
+ ping: () => Effect.void,
204
+ version: () => Effect.succeed({
205
+ Version: '24.0.0',
206
+ ApiVersion: '1.43',
207
+ }),
208
+ listContainers: () => Effect.succeed([]),
209
+ getContainer: (id) => createMockContainer(id),
210
+ createContainer: (options) => Effect.succeed(createMockContainer(`mock-${options.name}`)),
211
+ listNetworks: () => Effect.succeed([]),
212
+ getNetwork: (id) => createMockNetwork(id),
213
+ createNetwork: (options) => Effect.succeed(createMockNetwork(`mock-${options.Name}`)),
214
+ getImage: (name) => ({
215
+ inspect: () => Promise.resolve({ Id: name }),
216
+ }),
217
+ listImages: () => Effect.succeed([]),
218
+ pullImage: () => Effect.void,
219
+ ...overrides,
220
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Docker-specific Error Types
3
+ *
4
+ * Typed errors for Docker operations that integrate with Effect's error handling.
5
+ */
6
+ declare const DockerConnectionError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
7
+ readonly _tag: "DockerConnectionError";
8
+ } & Readonly<A>;
9
+ /**
10
+ * Base error for Docker connection issues.
11
+ * Thrown when the Docker daemon is unreachable or fails to respond.
12
+ */
13
+ export declare class DockerConnectionError extends DockerConnectionError_base<{
14
+ readonly message: string;
15
+ readonly socketPath?: string;
16
+ readonly cause?: unknown;
17
+ }> {
18
+ constructor(message: string, options?: {
19
+ socketPath?: string;
20
+ cause?: unknown;
21
+ });
22
+ toString(): string;
23
+ }
24
+ declare const DockerApiError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
25
+ readonly _tag: "DockerApiError";
26
+ } & Readonly<A>;
27
+ /**
28
+ * Error for Docker API operation failures.
29
+ * Contains the operation name and status code when available.
30
+ */
31
+ export declare class DockerApiError extends DockerApiError_base<{
32
+ readonly operation: string;
33
+ readonly message: string;
34
+ readonly statusCode?: number;
35
+ readonly cause?: unknown;
36
+ }> {
37
+ constructor(operation: string, message: string, options?: {
38
+ statusCode?: number;
39
+ cause?: unknown;
40
+ });
41
+ toString(): string;
42
+ }
43
+ declare const DockerContainerError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
44
+ readonly _tag: "DockerContainerError";
45
+ } & Readonly<A>;
46
+ /**
47
+ * Error for container-specific operations.
48
+ */
49
+ export declare class DockerContainerError extends DockerContainerError_base<{
50
+ readonly containerId: string;
51
+ readonly containerName?: string;
52
+ readonly operation: string;
53
+ readonly message: string;
54
+ readonly cause?: unknown;
55
+ }> {
56
+ constructor(containerId: string, operation: string, message: string, options?: {
57
+ containerName?: string;
58
+ cause?: unknown;
59
+ });
60
+ toString(): string;
61
+ }
62
+ declare const DockerNetworkError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
63
+ readonly _tag: "DockerNetworkError";
64
+ } & Readonly<A>;
65
+ /**
66
+ * Error for network-specific operations.
67
+ */
68
+ export declare class DockerNetworkError extends DockerNetworkError_base<{
69
+ readonly networkId: string;
70
+ readonly networkName?: string;
71
+ readonly operation: string;
72
+ readonly message: string;
73
+ readonly cause?: unknown;
74
+ }> {
75
+ constructor(networkId: string, operation: string, message: string, options?: {
76
+ networkName?: string;
77
+ cause?: unknown;
78
+ });
79
+ toString(): string;
80
+ }
81
+ declare const DockerImageError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
82
+ readonly _tag: "DockerImageError";
83
+ } & Readonly<A>;
84
+ /**
85
+ * Error for image-specific operations.
86
+ */
87
+ export declare class DockerImageError extends DockerImageError_base<{
88
+ readonly imageTag: string;
89
+ readonly operation: string;
90
+ readonly message: string;
91
+ readonly cause?: unknown;
92
+ }> {
93
+ constructor(imageTag: string, operation: string, message: string, options?: {
94
+ cause?: unknown;
95
+ });
96
+ toString(): string;
97
+ }
98
+ declare const DockerVolumeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
99
+ readonly _tag: "DockerVolumeError";
100
+ } & Readonly<A>;
101
+ /**
102
+ * Error for volume-specific operations.
103
+ */
104
+ export declare class DockerVolumeError extends DockerVolumeError_base<{
105
+ readonly volumeName: string;
106
+ readonly operation: string;
107
+ readonly message: string;
108
+ readonly cause?: unknown;
109
+ }> {
110
+ constructor(volumeName: string, operation: string, message: string, options?: {
111
+ cause?: unknown;
112
+ });
113
+ toString(): string;
114
+ }
115
+ /**
116
+ * Union type of all Docker errors for exhaustive handling.
117
+ */
118
+ export type DockerError = DockerConnectionError | DockerApiError | DockerContainerError | DockerNetworkError | DockerImageError | DockerVolumeError;
119
+ /**
120
+ * Type guard for DockerConnectionError.
121
+ */
122
+ export declare const isDockerConnectionError: (error: unknown) => error is DockerConnectionError;
123
+ /**
124
+ * Type guard for DockerApiError.
125
+ */
126
+ export declare const isDockerApiError: (error: unknown) => error is DockerApiError;
127
+ /**
128
+ * Type guard for any Docker error.
129
+ */
130
+ export declare const isDockerError: (error: unknown) => error is DockerError;
131
+ /**
132
+ * Helper to wrap unknown errors as DockerApiError.
133
+ */
134
+ export declare const wrapAsDockerError: (operation: string, error: unknown) => DockerApiError;
135
+ /**
136
+ * Determine if an error is transient and should be retried.
137
+ *
138
+ * Returns true for:
139
+ * - Connection errors (temporary network issues)
140
+ * - Specific HTTP status codes (429, 5xx)
141
+ * - Error messages matching transient patterns
142
+ */
143
+ export declare const isTransientError: (error: unknown) => boolean;
144
+ export {};
145
+ //# sourceMappingURL=DockerErrors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DockerErrors.d.ts","sourceRoot":"","sources":["../../src/services/DockerErrors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Docker-specific Error Types
3
+ *
4
+ * Typed errors for Docker operations that integrate with Effect's error handling.
5
+ */
6
+ import { Data } from 'effect';
7
+ /**
8
+ * Base error for Docker connection issues.
9
+ * Thrown when the Docker daemon is unreachable or fails to respond.
10
+ */
11
+ export class DockerConnectionError extends Data.TaggedError('DockerConnectionError') {
12
+ constructor(message, options) {
13
+ super({
14
+ message,
15
+ ...(options?.socketPath !== undefined && { socketPath: options.socketPath }),
16
+ ...(options?.cause !== undefined && { cause: options.cause }),
17
+ });
18
+ }
19
+ toString() {
20
+ return `DockerConnectionError: ${this.message}${this.socketPath ? ` (socket: ${this.socketPath})` : ''}`;
21
+ }
22
+ }
23
+ /**
24
+ * Error for Docker API operation failures.
25
+ * Contains the operation name and status code when available.
26
+ */
27
+ export class DockerApiError extends Data.TaggedError('DockerApiError') {
28
+ constructor(operation, message, options) {
29
+ super({
30
+ operation,
31
+ message,
32
+ ...(options?.statusCode !== undefined && { statusCode: options.statusCode }),
33
+ ...(options?.cause !== undefined && { cause: options.cause }),
34
+ });
35
+ }
36
+ toString() {
37
+ return `DockerApiError[${this.operation}]: ${this.message}${this.statusCode ? ` (status: ${this.statusCode})` : ''}`;
38
+ }
39
+ }
40
+ /**
41
+ * Error for container-specific operations.
42
+ */
43
+ export class DockerContainerError extends Data.TaggedError('DockerContainerError') {
44
+ constructor(containerId, operation, message, options) {
45
+ super({
46
+ containerId,
47
+ operation,
48
+ message,
49
+ ...(options?.containerName !== undefined && { containerName: options.containerName }),
50
+ ...(options?.cause !== undefined && { cause: options.cause }),
51
+ });
52
+ }
53
+ toString() {
54
+ const name = this.containerName ?? this.containerId;
55
+ return `DockerContainerError[${name}/${this.operation}]: ${this.message}`;
56
+ }
57
+ }
58
+ /**
59
+ * Error for network-specific operations.
60
+ */
61
+ export class DockerNetworkError extends Data.TaggedError('DockerNetworkError') {
62
+ constructor(networkId, operation, message, options) {
63
+ super({
64
+ networkId,
65
+ operation,
66
+ message,
67
+ ...(options?.networkName !== undefined && { networkName: options.networkName }),
68
+ ...(options?.cause !== undefined && { cause: options.cause }),
69
+ });
70
+ }
71
+ toString() {
72
+ const name = this.networkName ?? this.networkId;
73
+ return `DockerNetworkError[${name}/${this.operation}]: ${this.message}`;
74
+ }
75
+ }
76
+ /**
77
+ * Error for image-specific operations.
78
+ */
79
+ export class DockerImageError extends Data.TaggedError('DockerImageError') {
80
+ constructor(imageTag, operation, message, options) {
81
+ super({
82
+ imageTag,
83
+ operation,
84
+ message,
85
+ cause: options?.cause,
86
+ });
87
+ }
88
+ toString() {
89
+ return `DockerImageError[${this.imageTag}/${this.operation}]: ${this.message}`;
90
+ }
91
+ }
92
+ /**
93
+ * Error for volume-specific operations.
94
+ */
95
+ export class DockerVolumeError extends Data.TaggedError('DockerVolumeError') {
96
+ constructor(volumeName, operation, message, options) {
97
+ super({
98
+ volumeName,
99
+ operation,
100
+ message,
101
+ cause: options?.cause,
102
+ });
103
+ }
104
+ toString() {
105
+ return `DockerVolumeError[${this.volumeName}/${this.operation}]: ${this.message}`;
106
+ }
107
+ }
108
+ /**
109
+ * Type guard for DockerConnectionError.
110
+ */
111
+ export const isDockerConnectionError = (error) => error instanceof DockerConnectionError ||
112
+ (typeof error === 'object' &&
113
+ error !== null &&
114
+ '_tag' in error &&
115
+ error._tag === 'DockerConnectionError');
116
+ /**
117
+ * Type guard for DockerApiError.
118
+ */
119
+ export const isDockerApiError = (error) => error instanceof DockerApiError ||
120
+ (typeof error === 'object' &&
121
+ error !== null &&
122
+ '_tag' in error &&
123
+ error._tag === 'DockerApiError');
124
+ /**
125
+ * Type guard for any Docker error.
126
+ */
127
+ export const isDockerError = (error) => {
128
+ if (typeof error !== 'object' || error === null)
129
+ return false;
130
+ if (!('_tag' in error))
131
+ return false;
132
+ const tag = error._tag;
133
+ return [
134
+ 'DockerConnectionError',
135
+ 'DockerApiError',
136
+ 'DockerContainerError',
137
+ 'DockerNetworkError',
138
+ 'DockerImageError',
139
+ 'DockerVolumeError',
140
+ ].includes(tag);
141
+ };
142
+ /**
143
+ * Helper to wrap unknown errors as DockerApiError.
144
+ */
145
+ export const wrapAsDockerError = (operation, error) => {
146
+ if (isDockerError(error)) {
147
+ return new DockerApiError(operation, String(error), { cause: error });
148
+ }
149
+ if (error instanceof Error) {
150
+ // Extract status code if present (dockerode pattern)
151
+ const statusCode = 'statusCode' in error ? error.statusCode : undefined;
152
+ return new DockerApiError(operation, error.message, {
153
+ ...(statusCode !== undefined && { statusCode }),
154
+ cause: error,
155
+ });
156
+ }
157
+ return new DockerApiError(operation, String(error), { cause: error });
158
+ };
159
+ /**
160
+ * Transient error status codes that should trigger retries.
161
+ *
162
+ * - 408: Request Timeout
163
+ * - 429: Too Many Requests
164
+ * - 500: Internal Server Error
165
+ * - 502: Bad Gateway
166
+ * - 503: Service Unavailable
167
+ * - 504: Gateway Timeout
168
+ */
169
+ const TRANSIENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
170
+ /**
171
+ * Error messages that indicate transient failures.
172
+ */
173
+ const TRANSIENT_ERROR_PATTERNS = [
174
+ /ECONNRESET/i,
175
+ /ECONNREFUSED/i,
176
+ /ETIMEDOUT/i,
177
+ /EPIPE/i,
178
+ /socket hang up/i,
179
+ /network is unreachable/i,
180
+ /connection refused/i,
181
+ ];
182
+ /**
183
+ * Determine if an error is transient and should be retried.
184
+ *
185
+ * Returns true for:
186
+ * - Connection errors (temporary network issues)
187
+ * - Specific HTTP status codes (429, 5xx)
188
+ * - Error messages matching transient patterns
189
+ */
190
+ export const isTransientError = (error) => {
191
+ // Connection errors are always transient
192
+ if (isDockerConnectionError(error)) {
193
+ return true;
194
+ }
195
+ // Check API errors for transient status codes
196
+ if (isDockerApiError(error)) {
197
+ if (error.statusCode && TRANSIENT_STATUS_CODES.has(error.statusCode)) {
198
+ return true;
199
+ }
200
+ // Check error message patterns
201
+ for (const pattern of TRANSIENT_ERROR_PATTERNS) {
202
+ if (pattern.test(error.message)) {
203
+ return true;
204
+ }
205
+ }
206
+ }
207
+ // Check raw errors
208
+ if (error instanceof Error) {
209
+ // Check for status code on raw error
210
+ if ('statusCode' in error) {
211
+ const statusCode = error.statusCode;
212
+ if (statusCode && TRANSIENT_STATUS_CODES.has(statusCode)) {
213
+ return true;
214
+ }
215
+ }
216
+ // Check message patterns
217
+ for (const pattern of TRANSIENT_ERROR_PATTERNS) {
218
+ if (pattern.test(error.message)) {
219
+ return true;
220
+ }
221
+ }
222
+ }
223
+ return false;
224
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * DockerRateLimiter Service
3
+ *
4
+ * Provides semaphore-based rate limiting for Docker API operations.
5
+ * Prevents overwhelming the Docker daemon with too many concurrent requests.
6
+ */
7
+ import { Context, Effect, Layer, Schedule, Duration } from 'effect';
8
+ import { HarpoonConfig } from './HarpoonConfig';
9
+ /**
10
+ * DockerRateLimiter service interface.
11
+ *
12
+ * Provides controlled access to Docker operations through semaphore-based
13
+ * rate limiting.
14
+ */
15
+ export interface DockerRateLimiterService {
16
+ readonly _tag: 'DockerRateLimiter';
17
+ /**
18
+ * Execute an effect with a single permit.
19
+ * Waits for a permit if none are available.
20
+ */
21
+ readonly withPermit: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
22
+ /**
23
+ * Execute an effect with multiple permits.
24
+ * Useful for operations that are more resource-intensive.
25
+ */
26
+ readonly withPermits: (count: number) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
27
+ /**
28
+ * Get the number of currently available permits.
29
+ */
30
+ readonly available: () => Effect.Effect<number>;
31
+ }
32
+ declare const DockerRateLimiter_base: Context.TagClass<DockerRateLimiter, "@harpoon/DockerRateLimiter", DockerRateLimiterService>;
33
+ /**
34
+ * DockerRateLimiter service tag for dependency injection.
35
+ */
36
+ export declare class DockerRateLimiter extends DockerRateLimiter_base {
37
+ }
38
+ /**
39
+ * Exponential backoff schedule for transient Docker API errors.
40
+ *
41
+ * - 5 retries maximum
42
+ * - Base delay: 100ms, exponentially increasing
43
+ * - Max delay: 30 seconds
44
+ * - Includes jitter to prevent thundering herd
45
+ */
46
+ export declare const dockerRetrySchedule: Schedule.Schedule<[Duration.Duration, number], unknown, never>;
47
+ /**
48
+ * Aggressive retry schedule for cleanup operations.
49
+ *
50
+ * - 3 retries maximum
51
+ * - Base delay: 200ms
52
+ * - Max delay: 10 seconds
53
+ * - Quick cleanup attempts with short delays
54
+ */
55
+ export declare const cleanupRetrySchedule: Schedule.Schedule<[Duration.Duration, number], unknown, never>;
56
+ /**
57
+ * Health check schedule with fixed intervals.
58
+ *
59
+ * - 1 second between checks
60
+ * - Used for container readiness polling
61
+ */
62
+ export declare const healthCheckSchedule: Schedule.Schedule<number, unknown, never>;
63
+ /**
64
+ * Live implementation of DockerRateLimiter.
65
+ *
66
+ * Uses HarpoonConfig.parallelLimit to determine semaphore size.
67
+ */
68
+ export declare const DockerRateLimiterLive: Layer.Layer<DockerRateLimiter, never, HarpoonConfig>;
69
+ /**
70
+ * Test implementation with unlimited permits.
71
+ *
72
+ * Useful for unit tests that don't need rate limiting.
73
+ */
74
+ export declare const DockerRateLimiterTest: Layer.Layer<DockerRateLimiter, never, never>;
75
+ /**
76
+ * Create a custom test rate limiter with specific permit count.
77
+ */
78
+ export declare const makeDockerRateLimiterTest: (permits: number) => Layer.Layer<DockerRateLimiter, never, never>;
79
+ export {};
80
+ //# sourceMappingURL=DockerRateLimiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DockerRateLimiter.d.ts","sourceRoot":"","sources":["../../src/services/DockerRateLimiter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAEnC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEzF;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,CACpB,KAAK,EAAE,MAAM,KACV,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEzE;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;CACjD;;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,sBAGpC;CAAG;AAIN;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,gEAI/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,gEAIhC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,2CAAuC,CAAC;AAwBxE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,sDAiBjC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,8CAQhC,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,yBAAyB,mEAanC,CAAC"}