@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.
- package/dist/__tests__/test-setup.d.ts.map +1 -1
- package/dist/__tests__/test-setup.js +8 -1
- package/dist/api/promise.d.ts +1 -1
- package/dist/api/promise.d.ts.map +1 -1
- package/dist/api/promise.js +28 -18
- package/dist/bindings/index.d.ts +2 -2
- package/dist/bindings/index.d.ts.map +1 -1
- package/dist/bindings/index.js +1 -1
- package/dist/bindings/types.d.ts.map +1 -1
- package/dist/bindings/types.js +1 -3
- package/dist/build-strategies/types.d.ts.map +1 -1
- package/dist/config-patchers/index.d.ts.map +1 -1
- package/dist/config-patchers/types.d.ts.map +1 -1
- package/dist/dockerfile-transformers/core.d.ts.map +1 -1
- package/dist/dockerfile-transformers/core.js +2 -5
- package/dist/dockerfile-transformers/index.d.ts.map +1 -1
- package/dist/dockerfile-transformers/types.d.ts.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -3
- package/dist/helpers/database.d.ts.map +1 -1
- package/dist/helpers/database.js +1 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/resources/container.d.ts +7 -2
- package/dist/resources/container.d.ts.map +1 -1
- package/dist/resources/container.js +222 -31
- package/dist/resources/image.d.ts.map +1 -1
- package/dist/resources/image.js +14 -35
- package/dist/resources/network.d.ts.map +1 -1
- package/dist/resources/network.js +23 -9
- package/dist/resources/schemas.d.ts +177 -18
- package/dist/resources/schemas.d.ts.map +1 -1
- package/dist/resources/schemas.js +1 -1
- package/dist/services/CircuitBreaker.d.ts +83 -0
- package/dist/services/CircuitBreaker.d.ts.map +1 -0
- package/dist/services/CircuitBreaker.js +164 -0
- package/dist/services/ContainerPool.d.ts +82 -0
- package/dist/services/ContainerPool.d.ts.map +1 -0
- package/dist/services/ContainerPool.js +186 -0
- package/dist/services/DockerBatcher.d.ts +74 -0
- package/dist/services/DockerBatcher.d.ts.map +1 -0
- package/dist/services/DockerBatcher.js +107 -0
- package/dist/services/DockerClient.d.ts +125 -0
- package/dist/services/DockerClient.d.ts.map +1 -0
- package/dist/services/DockerClient.js +220 -0
- package/dist/services/DockerErrors.d.ts +145 -0
- package/dist/services/DockerErrors.d.ts.map +1 -0
- package/dist/services/DockerErrors.js +224 -0
- package/dist/services/DockerRateLimiter.d.ts +80 -0
- package/dist/services/DockerRateLimiter.d.ts.map +1 -0
- package/dist/services/DockerRateLimiter.js +93 -0
- package/dist/services/EventBus.d.ts +126 -0
- package/dist/services/EventBus.d.ts.map +1 -0
- package/dist/services/EventBus.js +111 -0
- package/dist/services/Harpoon.d.ts +151 -0
- package/dist/services/Harpoon.d.ts.map +1 -0
- package/dist/services/Harpoon.js +148 -0
- package/dist/services/HarpoonConfig.d.ts +60 -0
- package/dist/services/HarpoonConfig.d.ts.map +1 -0
- package/dist/services/HarpoonConfig.js +67 -0
- package/dist/services/HarpoonLogger.d.ts +36 -0
- package/dist/services/HarpoonLogger.d.ts.map +1 -0
- package/dist/services/HarpoonLogger.js +94 -0
- package/dist/services/ReadinessCoordinator.d.ts +128 -0
- package/dist/services/ReadinessCoordinator.d.ts.map +1 -0
- package/dist/services/ReadinessCoordinator.js +170 -0
- package/dist/services/ResourceTracker.d.ts +74 -0
- package/dist/services/ResourceTracker.d.ts.map +1 -0
- package/dist/services/ResourceTracker.js +145 -0
- package/dist/services/index.d.ts +29 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +47 -0
- package/dist/testing/helpers.d.ts +114 -0
- package/dist/testing/helpers.d.ts.map +1 -0
- package/dist/testing/helpers.js +140 -0
- package/dist/testing/index.d.ts +29 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +47 -0
- package/dist/testing/mocks.d.ts +66 -0
- package/dist/testing/mocks.d.ts.map +1 -0
- package/dist/testing/mocks.js +224 -0
- package/dist/utils/process.d.ts +24 -0
- package/dist/utils/process.d.ts.map +1 -0
- package/dist/utils/process.js +49 -0
- package/package.json +12 -8
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
/**
|
|
@@ -96,18 +228,39 @@ export declare const logOptionsSchema: z.ZodObject<{
|
|
|
96
228
|
tail: z.ZodOptional<z.ZodNumber>;
|
|
97
229
|
timestamps: z.ZodOptional<z.ZodBoolean>;
|
|
98
230
|
follow: z.ZodOptional<z.ZodBoolean>;
|
|
99
|
-
}, z.
|
|
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
|
+
}>;
|
|
100
248
|
/**
|
|
101
249
|
* A single log line from a container
|
|
102
250
|
*/
|
|
103
251
|
export declare const logLineSchema: z.ZodObject<{
|
|
104
|
-
stream: z.ZodEnum<
|
|
105
|
-
stderr: "stderr";
|
|
106
|
-
stdout: "stdout";
|
|
107
|
-
}>;
|
|
252
|
+
stream: z.ZodEnum<["stdout", "stderr"]>;
|
|
108
253
|
timestamp: z.ZodOptional<z.ZodString>;
|
|
109
254
|
message: z.ZodString;
|
|
110
|
-
}, z.
|
|
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
|
+
}>;
|
|
111
264
|
export type LogOptions = z.infer<typeof logOptionsSchema>;
|
|
112
265
|
export type LogLine = z.infer<typeof logLineSchema>;
|
|
113
266
|
/**
|
|
@@ -116,6 +269,12 @@ export type LogLine = z.infer<typeof logLineSchema>;
|
|
|
116
269
|
export declare const statsOptionsSchema: z.ZodObject<{
|
|
117
270
|
stream: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
118
271
|
oneShot: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
119
|
-
}, z.
|
|
272
|
+
}, "strip", z.ZodTypeAny, {
|
|
273
|
+
stream: boolean;
|
|
274
|
+
oneShot: boolean;
|
|
275
|
+
}, {
|
|
276
|
+
stream?: boolean | undefined;
|
|
277
|
+
oneShot?: boolean | undefined;
|
|
278
|
+
}>;
|
|
120
279
|
export type StatsOptions = z.infer<typeof statsOptionsSchema>;
|
|
121
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
|
|
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
|
|
5
|
+
* Following Harpoon patterns for declarative resource definitions.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
// ============ Container Stats Schema ============
|
|
@@ -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));
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContainerPool Service
|
|
3
|
+
*
|
|
4
|
+
* Provides a pool of pre-warmed containers for faster test execution.
|
|
5
|
+
* Uses Effect's Pool for efficient resource management.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect, Layer, Scope, Duration } from 'effect';
|
|
8
|
+
import { DockerClient } from './DockerClient';
|
|
9
|
+
import { HarpoonConfig } from './HarpoonConfig';
|
|
10
|
+
import { type ContainerConfig, type ContainerResource } from '../resources/container';
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for the container pool.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContainerPoolConfig {
|
|
15
|
+
/** Base configuration for pooled containers */
|
|
16
|
+
readonly containerConfig: ContainerConfig;
|
|
17
|
+
/** Minimum number of containers to keep in the pool */
|
|
18
|
+
readonly minSize: number;
|
|
19
|
+
/** Maximum number of containers in the pool */
|
|
20
|
+
readonly maxSize: number;
|
|
21
|
+
/** Time to wait for a container before timing out */
|
|
22
|
+
readonly acquireTimeout: Duration.Duration;
|
|
23
|
+
/** How long an idle container stays in the pool */
|
|
24
|
+
readonly idleTimeout: Duration.Duration;
|
|
25
|
+
/** Prefix for container names */
|
|
26
|
+
readonly namePrefix: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pooled container with additional metadata.
|
|
30
|
+
*/
|
|
31
|
+
export interface PooledContainer {
|
|
32
|
+
readonly resource: ContainerResource;
|
|
33
|
+
readonly acquiredAt: Date;
|
|
34
|
+
readonly poolId: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* ContainerPool service interface.
|
|
38
|
+
*/
|
|
39
|
+
export interface ContainerPoolService {
|
|
40
|
+
readonly _tag: 'ContainerPool';
|
|
41
|
+
/**
|
|
42
|
+
* Acquire a container from the pool.
|
|
43
|
+
* Returns a scoped effect that releases the container when done.
|
|
44
|
+
*/
|
|
45
|
+
readonly acquire: () => Effect.Effect<PooledContainer, Error, Scope.Scope>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the current size of the pool (available + in-use).
|
|
48
|
+
*/
|
|
49
|
+
readonly size: () => Effect.Effect<number>;
|
|
50
|
+
/**
|
|
51
|
+
* Get the number of available containers in the pool.
|
|
52
|
+
*/
|
|
53
|
+
readonly available: () => Effect.Effect<number>;
|
|
54
|
+
/**
|
|
55
|
+
* Get the number of containers currently in use.
|
|
56
|
+
*/
|
|
57
|
+
readonly inUse: () => Effect.Effect<number>;
|
|
58
|
+
/**
|
|
59
|
+
* Pre-warm the pool by creating containers up to minSize.
|
|
60
|
+
*/
|
|
61
|
+
readonly warmup: () => Effect.Effect<void, Error>;
|
|
62
|
+
}
|
|
63
|
+
declare const ContainerPool_base: Context.TagClass<ContainerPool, "@harpoon/ContainerPool", ContainerPoolService>;
|
|
64
|
+
/**
|
|
65
|
+
* ContainerPool service tag for dependency injection.
|
|
66
|
+
*/
|
|
67
|
+
export declare class ContainerPool extends ContainerPool_base {
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a ContainerPool with specific configuration.
|
|
71
|
+
*/
|
|
72
|
+
export declare const makeContainerPool: (config?: Partial<ContainerPoolConfig>) => Effect.Effect<ContainerPoolService, Error, DockerClient | HarpoonConfig | Scope.Scope>;
|
|
73
|
+
/**
|
|
74
|
+
* Live implementation layer.
|
|
75
|
+
*/
|
|
76
|
+
export declare const ContainerPoolLive: (config?: Partial<ContainerPoolConfig>) => Layer.Layer<ContainerPool, Error, DockerClient | HarpoonConfig>;
|
|
77
|
+
/**
|
|
78
|
+
* Test implementation that creates containers on-demand.
|
|
79
|
+
*/
|
|
80
|
+
export declare const ContainerPoolTest: Layer.Layer<ContainerPool, never, never>;
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=ContainerPool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContainerPool.d.ts","sourceRoot":"","sources":["../../src/services/ContainerPool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAQ,KAAK,EAAE,QAAQ,EAAO,MAAM,QAAQ,CAAC;AAG5E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAa,KAAK,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAIjG;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAE1C,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,qDAAqD;IACrD,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAE3C,mDAAmD;IACnD,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAExC,iCAAiC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAID;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAE/B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAE3E;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAE3C;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEhD;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAE5C;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;CACnD;;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,kBAGhC;CAAG;AAiBN;;GAEG;AACH,eAAO,MAAM,iBAAiB,mIAyI1B,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,iBAAiB,4GAG0B,CAAC;AAEzD;;GAEG;AACH,eAAO,MAAM,iBAAiB,0CA2E7B,CAAC"}
|