@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.
- package/dist/__tests__/bindings.test.js +9 -5
- package/dist/__tests__/container.test.js +30 -9
- package/dist/__tests__/database.test.js +0 -14
- package/dist/__tests__/docker-infra.template.d.ts +2 -0
- package/dist/__tests__/docker-infra.template.d.ts.map +1 -0
- package/dist/__tests__/docker-infra.template.js +174 -0
- package/dist/__tests__/test-setup.d.ts +9 -0
- package/dist/__tests__/test-setup.d.ts.map +1 -0
- package/dist/__tests__/test-setup.js +27 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/promise.d.ts +13 -3
- package/dist/api/promise.d.ts.map +1 -1
- package/dist/api/promise.js +33 -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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/resources/container.d.ts +25 -2
- package/dist/resources/container.d.ts.map +1 -1
- package/dist/resources/container.js +301 -77
- package/dist/resources/image.d.ts.map +1 -1
- package/dist/resources/image.js +14 -35
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/network.d.ts.map +1 -1
- package/dist/resources/network.js +23 -9
- package/dist/resources/schemas.d.ts +178 -18
- package/dist/resources/schemas.d.ts.map +1 -1
- package/dist/resources/schemas.js +2 -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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HarpoonConfig Service
|
|
3
|
+
*
|
|
4
|
+
* Provides typed configuration for all Harpoon services using Effect's Config module.
|
|
5
|
+
* Configuration is loaded from environment variables with sensible defaults.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Layer, Duration } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* Configuration interface for Harpoon services.
|
|
10
|
+
*/
|
|
11
|
+
export interface HarpoonConfigService {
|
|
12
|
+
readonly _tag: 'HarpoonConfig';
|
|
13
|
+
/** Docker socket path (auto-detected if not specified) */
|
|
14
|
+
readonly dockerSocket: string | undefined;
|
|
15
|
+
/** Default timeout for operations */
|
|
16
|
+
readonly defaultTimeout: Duration.Duration;
|
|
17
|
+
/** Timeout for cleanup operations */
|
|
18
|
+
readonly cleanupTimeout: Duration.Duration;
|
|
19
|
+
/** Log level for Harpoon operations */
|
|
20
|
+
readonly logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
21
|
+
/** Maximum parallel operations (for Effect.all concurrency) */
|
|
22
|
+
readonly parallelLimit: number;
|
|
23
|
+
/** Number of retry attempts for failed operations */
|
|
24
|
+
readonly retryAttempts: number;
|
|
25
|
+
/** Delay between retry attempts */
|
|
26
|
+
readonly retryDelay: Duration.Duration;
|
|
27
|
+
/** Docker startup timeout (for auto-start on macOS/Windows) */
|
|
28
|
+
readonly dockerStartupTimeout: Duration.Duration;
|
|
29
|
+
}
|
|
30
|
+
declare const HarpoonConfig_base: Context.TagClass<HarpoonConfig, "@harpoon/HarpoonConfig", HarpoonConfigService>;
|
|
31
|
+
/**
|
|
32
|
+
* HarpoonConfig service tag for dependency injection.
|
|
33
|
+
*/
|
|
34
|
+
export declare class HarpoonConfig extends HarpoonConfig_base {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Live implementation that loads configuration from environment variables.
|
|
38
|
+
*
|
|
39
|
+
* Environment variables:
|
|
40
|
+
* - DOCKER_SOCKET: Docker socket path
|
|
41
|
+
* - HARPOON_TIMEOUT: Default timeout in milliseconds (default: 30000)
|
|
42
|
+
* - HARPOON_CLEANUP_TIMEOUT: Cleanup timeout in milliseconds (default: 5000)
|
|
43
|
+
* - HARPOON_LOG_LEVEL: Log level (default: 'info')
|
|
44
|
+
* - HARPOON_PARALLEL_LIMIT: Max parallel operations (default: 4)
|
|
45
|
+
* - HARPOON_RETRY_ATTEMPTS: Retry attempts (default: 3)
|
|
46
|
+
* - HARPOON_RETRY_DELAY: Retry delay in milliseconds (default: 1000)
|
|
47
|
+
* - HARPOON_DOCKER_STARTUP_TIMEOUT: Docker startup timeout in milliseconds (default: 60000)
|
|
48
|
+
*/
|
|
49
|
+
export declare const HarpoonConfigLive: Layer.Layer<HarpoonConfig, import("effect/ConfigError").ConfigError, never>;
|
|
50
|
+
/**
|
|
51
|
+
* Create a test configuration layer with custom overrides.
|
|
52
|
+
* Uses fast timeouts suitable for testing.
|
|
53
|
+
*/
|
|
54
|
+
export declare const makeHarpoonConfigTest: (overrides?: Partial<Omit<HarpoonConfigService, "_tag">>) => Layer.Layer<HarpoonConfig, never, never>;
|
|
55
|
+
/**
|
|
56
|
+
* Default test configuration layer.
|
|
57
|
+
*/
|
|
58
|
+
export declare const HarpoonConfigTest: Layer.Layer<HarpoonConfig, never, never>;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=HarpoonConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HarpoonConfig.d.ts","sourceRoot":"","sources":["../../src/services/HarpoonConfig.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAkB,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAE/B,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAE1C,qCAAqC;IACrC,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAE3C,qCAAqC;IACrC,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAE3C,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAEvD,+DAA+D;IAC/D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,qDAAqD;IACrD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,mCAAmC;IACnC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAEvC,+DAA+D;IAC/D,QAAQ,CAAC,oBAAoB,EAAE,QAAQ,CAAC,QAAQ,CAAC;CAClD;;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,kBAGhC;CAAG;AAEN;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,6EA6C7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,uGAc9B,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,iBAAiB,0CAA0B,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HarpoonConfig Service
|
|
3
|
+
*
|
|
4
|
+
* Provides typed configuration for all Harpoon services using Effect's Config module.
|
|
5
|
+
* Configuration is loaded from environment variables with sensible defaults.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Config, Effect, Layer, Duration } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* HarpoonConfig service tag for dependency injection.
|
|
10
|
+
*/
|
|
11
|
+
export class HarpoonConfig extends Context.Tag('@harpoon/HarpoonConfig')() {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Live implementation that loads configuration from environment variables.
|
|
15
|
+
*
|
|
16
|
+
* Environment variables:
|
|
17
|
+
* - DOCKER_SOCKET: Docker socket path
|
|
18
|
+
* - HARPOON_TIMEOUT: Default timeout in milliseconds (default: 30000)
|
|
19
|
+
* - HARPOON_CLEANUP_TIMEOUT: Cleanup timeout in milliseconds (default: 5000)
|
|
20
|
+
* - HARPOON_LOG_LEVEL: Log level (default: 'info')
|
|
21
|
+
* - HARPOON_PARALLEL_LIMIT: Max parallel operations (default: 4)
|
|
22
|
+
* - HARPOON_RETRY_ATTEMPTS: Retry attempts (default: 3)
|
|
23
|
+
* - HARPOON_RETRY_DELAY: Retry delay in milliseconds (default: 1000)
|
|
24
|
+
* - HARPOON_DOCKER_STARTUP_TIMEOUT: Docker startup timeout in milliseconds (default: 60000)
|
|
25
|
+
*/
|
|
26
|
+
export const HarpoonConfigLive = Layer.effect(HarpoonConfig, Effect.gen(function* () {
|
|
27
|
+
// Load all configuration with defaults
|
|
28
|
+
const dockerSocket = yield* Config.string('DOCKER_SOCKET').pipe(Config.option);
|
|
29
|
+
const defaultTimeout = yield* Config.integer('HARPOON_TIMEOUT').pipe(Config.withDefault(30000));
|
|
30
|
+
const cleanupTimeout = yield* Config.integer('HARPOON_CLEANUP_TIMEOUT').pipe(Config.withDefault(5000));
|
|
31
|
+
const logLevel = yield* Config.literal('debug', 'info', 'warn', 'error')('HARPOON_LOG_LEVEL').pipe(Config.withDefault('info'));
|
|
32
|
+
const parallelLimit = yield* Config.integer('HARPOON_PARALLEL_LIMIT').pipe(Config.withDefault(4));
|
|
33
|
+
const retryAttempts = yield* Config.integer('HARPOON_RETRY_ATTEMPTS').pipe(Config.withDefault(3));
|
|
34
|
+
const retryDelay = yield* Config.integer('HARPOON_RETRY_DELAY').pipe(Config.withDefault(1000));
|
|
35
|
+
const dockerStartupTimeout = yield* Config.integer('HARPOON_DOCKER_STARTUP_TIMEOUT').pipe(Config.withDefault(60000));
|
|
36
|
+
return {
|
|
37
|
+
_tag: 'HarpoonConfig',
|
|
38
|
+
dockerSocket: dockerSocket._tag === 'Some' ? dockerSocket.value : undefined,
|
|
39
|
+
defaultTimeout: Duration.millis(defaultTimeout),
|
|
40
|
+
cleanupTimeout: Duration.millis(cleanupTimeout),
|
|
41
|
+
logLevel,
|
|
42
|
+
parallelLimit,
|
|
43
|
+
retryAttempts,
|
|
44
|
+
retryDelay: Duration.millis(retryDelay),
|
|
45
|
+
dockerStartupTimeout: Duration.millis(dockerStartupTimeout),
|
|
46
|
+
};
|
|
47
|
+
}));
|
|
48
|
+
/**
|
|
49
|
+
* Create a test configuration layer with custom overrides.
|
|
50
|
+
* Uses fast timeouts suitable for testing.
|
|
51
|
+
*/
|
|
52
|
+
export const makeHarpoonConfigTest = (overrides = {}) => Layer.succeed(HarpoonConfig, {
|
|
53
|
+
_tag: 'HarpoonConfig',
|
|
54
|
+
dockerSocket: undefined,
|
|
55
|
+
defaultTimeout: Duration.millis(5000),
|
|
56
|
+
cleanupTimeout: Duration.millis(1000),
|
|
57
|
+
logLevel: 'debug',
|
|
58
|
+
parallelLimit: 2,
|
|
59
|
+
retryAttempts: 1,
|
|
60
|
+
retryDelay: Duration.millis(100),
|
|
61
|
+
dockerStartupTimeout: Duration.millis(5000),
|
|
62
|
+
...overrides,
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Default test configuration layer.
|
|
66
|
+
*/
|
|
67
|
+
export const HarpoonConfigTest = makeHarpoonConfigTest();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HarpoonLogger Service
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging for all Harpoon operations using Effect's Logger.
|
|
5
|
+
* Logs include timestamps, log levels, and structured annotations.
|
|
6
|
+
*/
|
|
7
|
+
import { Effect, Layer } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* Live logger layer with pretty-printed format.
|
|
10
|
+
*/
|
|
11
|
+
export declare const HarpoonLoggerLive: Layer.Layer<never, never, never>;
|
|
12
|
+
/**
|
|
13
|
+
* JSON logger layer for structured logging.
|
|
14
|
+
*/
|
|
15
|
+
export declare const HarpoonLoggerJson: Layer.Layer<never, never, never>;
|
|
16
|
+
/**
|
|
17
|
+
* Test logger layer that suppresses all output.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HarpoonLoggerTest: Layer.Layer<never, never, never>;
|
|
20
|
+
/**
|
|
21
|
+
* Minimum log level layer - only show warnings and above.
|
|
22
|
+
*/
|
|
23
|
+
export declare const HarpoonLoggerMinWarn: Layer.Layer<never, never, never>;
|
|
24
|
+
/**
|
|
25
|
+
* Debug log level layer - show all logs including debug.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HarpoonLoggerDebug: Layer.Layer<never, never, never>;
|
|
28
|
+
/**
|
|
29
|
+
* Helper to create a span for tracing operations.
|
|
30
|
+
*/
|
|
31
|
+
export declare const withSpan: <A, E, R>(name: string, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
32
|
+
/**
|
|
33
|
+
* Helper to add annotations to logs within a scope.
|
|
34
|
+
*/
|
|
35
|
+
export declare const withLogAnnotations: <A, E, R>(annotations: Record<string, unknown>, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
36
|
+
//# sourceMappingURL=HarpoonLogger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HarpoonLogger.d.ts","sourceRoot":"","sources":["../../src/services/HarpoonLogger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,MAAM,EAAE,KAAK,EAAQ,MAAM,QAAQ,CAAC;AAqE/D;;GAEG;AACH,eAAO,MAAM,iBAAiB,kCAAyD,CAAC;AAExF;;GAEG;AACH,eAAO,MAAM,iBAAiB,kCAA6D,CAAC;AAE5F;;GAEG;AACH,eAAO,MAAM,iBAAiB,kCAAoD,CAAC;AAEnF;;GAEG;AACH,eAAO,MAAM,oBAAoB,kCAGhC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,kCAG9B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,yEAG0B,CAAC;AAE3D;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,iGAG2B,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HarpoonLogger Service
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging for all Harpoon operations using Effect's Logger.
|
|
5
|
+
* Logs include timestamps, log levels, and structured annotations.
|
|
6
|
+
*/
|
|
7
|
+
import { Logger, LogLevel, Effect, Layer, List } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* Custom log format for Harpoon.
|
|
10
|
+
*
|
|
11
|
+
* Format: [ISO_TIMESTAMP] LEVEL [Harpoon] message | key=value key=value
|
|
12
|
+
*
|
|
13
|
+
* Example:
|
|
14
|
+
* [2024-01-15T10:30:00.000Z] INFO [Harpoon] Container created | containerId="abc123" name="my-app"
|
|
15
|
+
*/
|
|
16
|
+
const harpoonLogFormat = Logger.make(({ logLevel, message, annotations, date, spans, cause }) => {
|
|
17
|
+
const timestamp = date.toISOString();
|
|
18
|
+
const level = logLevel.label.toUpperCase().padEnd(5);
|
|
19
|
+
// Format annotations as key=value pairs
|
|
20
|
+
const annotationEntries = Object.entries(annotations);
|
|
21
|
+
const annotationStr = annotationEntries.length > 0
|
|
22
|
+
? ' | ' + annotationEntries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')
|
|
23
|
+
: '';
|
|
24
|
+
// Format spans if present (spans is a List<LogSpan>)
|
|
25
|
+
const spansArray = List.toArray(spans);
|
|
26
|
+
const spanStr = spansArray.length > 0 ? ` [${spansArray.map((s) => s.label).join(' > ')}]` : '';
|
|
27
|
+
// Build the log message
|
|
28
|
+
const formatted = `[${timestamp}] ${level} [Harpoon]${spanStr} ${message}${annotationStr}`;
|
|
29
|
+
// Route to appropriate console method based on level
|
|
30
|
+
if (logLevel._tag === 'Error' || logLevel._tag === 'Fatal') {
|
|
31
|
+
console.error(formatted);
|
|
32
|
+
if (cause) {
|
|
33
|
+
console.error('Cause:', cause);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (logLevel._tag === 'Warning') {
|
|
37
|
+
console.warn(formatted);
|
|
38
|
+
}
|
|
39
|
+
else if (logLevel._tag === 'Debug' || logLevel._tag === 'Trace') {
|
|
40
|
+
console.debug(formatted);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(formatted);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* JSON log format for structured logging (useful for log aggregation systems).
|
|
48
|
+
*/
|
|
49
|
+
const harpoonJsonLogFormat = Logger.make(({ logLevel, message, annotations, date, spans, cause }) => {
|
|
50
|
+
const spansArray = List.toArray(spans);
|
|
51
|
+
const logEntry = {
|
|
52
|
+
timestamp: date.toISOString(),
|
|
53
|
+
level: logLevel.label,
|
|
54
|
+
service: 'harpoon',
|
|
55
|
+
message: String(message),
|
|
56
|
+
...(Object.keys(annotations).length > 0 && { annotations }),
|
|
57
|
+
...(spansArray.length > 0 && { spans: spansArray.map((s) => s.label) }),
|
|
58
|
+
...(cause && { cause: String(cause) }),
|
|
59
|
+
};
|
|
60
|
+
if (logLevel._tag === 'Error' || logLevel._tag === 'Fatal') {
|
|
61
|
+
console.error(JSON.stringify(logEntry));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(JSON.stringify(logEntry));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Live logger layer with pretty-printed format.
|
|
69
|
+
*/
|
|
70
|
+
export const HarpoonLoggerLive = Logger.replace(Logger.defaultLogger, harpoonLogFormat);
|
|
71
|
+
/**
|
|
72
|
+
* JSON logger layer for structured logging.
|
|
73
|
+
*/
|
|
74
|
+
export const HarpoonLoggerJson = Logger.replace(Logger.defaultLogger, harpoonJsonLogFormat);
|
|
75
|
+
/**
|
|
76
|
+
* Test logger layer that suppresses all output.
|
|
77
|
+
*/
|
|
78
|
+
export const HarpoonLoggerTest = Logger.replace(Logger.defaultLogger, Logger.none);
|
|
79
|
+
/**
|
|
80
|
+
* Minimum log level layer - only show warnings and above.
|
|
81
|
+
*/
|
|
82
|
+
export const HarpoonLoggerMinWarn = Layer.merge(HarpoonLoggerLive, Logger.minimumLogLevel(LogLevel.Warning));
|
|
83
|
+
/**
|
|
84
|
+
* Debug log level layer - show all logs including debug.
|
|
85
|
+
*/
|
|
86
|
+
export const HarpoonLoggerDebug = Layer.merge(HarpoonLoggerLive, Logger.minimumLogLevel(LogLevel.Debug));
|
|
87
|
+
/**
|
|
88
|
+
* Helper to create a span for tracing operations.
|
|
89
|
+
*/
|
|
90
|
+
export const withSpan = (name, effect) => Effect.withSpan(name)(effect);
|
|
91
|
+
/**
|
|
92
|
+
* Helper to add annotations to logs within a scope.
|
|
93
|
+
*/
|
|
94
|
+
export const withLogAnnotations = (annotations, effect) => Effect.annotateLogs(annotations)(effect);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReadinessCoordinator Service
|
|
3
|
+
*
|
|
4
|
+
* Coordinates container readiness checks with support for multiple health check types.
|
|
5
|
+
* Uses Effect's Deferred for efficient waiting and Ref for state management.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect, Layer, Duration } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* Log-based health check - waits for a pattern in container logs.
|
|
10
|
+
*/
|
|
11
|
+
export interface LogHealthCheck {
|
|
12
|
+
readonly type: 'log';
|
|
13
|
+
/** Regex pattern or string to match in logs */
|
|
14
|
+
readonly pattern: string | RegExp;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* HTTP health check - polls an HTTP endpoint.
|
|
18
|
+
*/
|
|
19
|
+
export interface HttpHealthCheck {
|
|
20
|
+
readonly type: 'http';
|
|
21
|
+
/** URL to check (e.g., 'http://localhost:3000/health') */
|
|
22
|
+
readonly url: string;
|
|
23
|
+
/** Expected status code (default: 200) */
|
|
24
|
+
readonly expectedStatus?: number;
|
|
25
|
+
/** HTTP method (default: GET) */
|
|
26
|
+
readonly method?: 'GET' | 'HEAD';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* TCP health check - checks if a port is accepting connections.
|
|
30
|
+
*/
|
|
31
|
+
export interface TcpHealthCheck {
|
|
32
|
+
readonly type: 'tcp';
|
|
33
|
+
/** Host to connect to */
|
|
34
|
+
readonly host: string;
|
|
35
|
+
/** Port to check */
|
|
36
|
+
readonly port: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Exec health check - runs a command inside the container.
|
|
40
|
+
*/
|
|
41
|
+
export interface ExecHealthCheck {
|
|
42
|
+
readonly type: 'exec';
|
|
43
|
+
/** Command to execute */
|
|
44
|
+
readonly cmd: string[];
|
|
45
|
+
/** Expected exit code (default: 0) */
|
|
46
|
+
readonly expectedExitCode?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Custom health check - user-provided check function.
|
|
50
|
+
*/
|
|
51
|
+
export interface CustomHealthCheck {
|
|
52
|
+
readonly type: 'custom';
|
|
53
|
+
/** Custom check function that returns true when ready */
|
|
54
|
+
readonly check: () => Effect.Effect<boolean, Error>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Union type of all health check types.
|
|
58
|
+
*/
|
|
59
|
+
export type HealthCheck = LogHealthCheck | HttpHealthCheck | TcpHealthCheck | ExecHealthCheck | CustomHealthCheck;
|
|
60
|
+
/**
|
|
61
|
+
* Options for waiting for container readiness.
|
|
62
|
+
*/
|
|
63
|
+
export interface WaitOptions {
|
|
64
|
+
/** Maximum time to wait (default: 60 seconds) */
|
|
65
|
+
readonly timeout?: Duration.Duration;
|
|
66
|
+
/** Interval between checks (default: 1 second) */
|
|
67
|
+
readonly interval?: Duration.Duration;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Error thrown when container readiness check fails.
|
|
71
|
+
*/
|
|
72
|
+
export declare class ReadinessError extends Error {
|
|
73
|
+
readonly _tag = "ReadinessError";
|
|
74
|
+
readonly containerId: string;
|
|
75
|
+
readonly healthCheckType: string;
|
|
76
|
+
constructor(containerId: string, healthCheckType: string, message: string, cause?: Error);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* ReadinessCoordinator service interface.
|
|
80
|
+
*/
|
|
81
|
+
export interface ReadinessCoordinatorService {
|
|
82
|
+
readonly _tag: 'ReadinessCoordinator';
|
|
83
|
+
/**
|
|
84
|
+
* Wait for a container to become ready.
|
|
85
|
+
* Uses the specified health check to determine readiness.
|
|
86
|
+
*/
|
|
87
|
+
readonly waitForReady: (containerId: string, healthCheck: HealthCheck, checkFn: () => Effect.Effect<boolean, Error>, options?: WaitOptions) => Effect.Effect<void, ReadinessError>;
|
|
88
|
+
/**
|
|
89
|
+
* Check if a container is currently marked as ready.
|
|
90
|
+
*/
|
|
91
|
+
readonly isReady: (containerId: string) => Effect.Effect<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Mark a container as ready (for external updates).
|
|
94
|
+
*/
|
|
95
|
+
readonly markReady: (containerId: string) => Effect.Effect<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Mark a container as not ready.
|
|
98
|
+
*/
|
|
99
|
+
readonly markNotReady: (containerId: string) => Effect.Effect<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Remove tracking for a container.
|
|
102
|
+
*/
|
|
103
|
+
readonly untrack: (containerId: string) => Effect.Effect<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Get all tracked containers and their readiness status.
|
|
106
|
+
*/
|
|
107
|
+
readonly getAll: () => Effect.Effect<ReadonlyArray<{
|
|
108
|
+
containerId: string;
|
|
109
|
+
ready: boolean;
|
|
110
|
+
timestamp: Date;
|
|
111
|
+
}>>;
|
|
112
|
+
}
|
|
113
|
+
declare const ReadinessCoordinator_base: Context.TagClass<ReadinessCoordinator, "@harpoon/ReadinessCoordinator", ReadinessCoordinatorService>;
|
|
114
|
+
/**
|
|
115
|
+
* ReadinessCoordinator service tag for dependency injection.
|
|
116
|
+
*/
|
|
117
|
+
export declare class ReadinessCoordinator extends ReadinessCoordinator_base {
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Live implementation using Effect Ref and Deferred.
|
|
121
|
+
*/
|
|
122
|
+
export declare const ReadinessCoordinatorLive: Layer.Layer<ReadinessCoordinator, never, never>;
|
|
123
|
+
/**
|
|
124
|
+
* Test implementation that can be pre-configured with ready containers.
|
|
125
|
+
*/
|
|
126
|
+
export declare const ReadinessCoordinatorTest: Layer.Layer<ReadinessCoordinator, never, never>;
|
|
127
|
+
export {};
|
|
128
|
+
//# sourceMappingURL=ReadinessCoordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReadinessCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/ReadinessCoordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReadinessCoordinator Service
|
|
3
|
+
*
|
|
4
|
+
* Coordinates container readiness checks with support for multiple health check types.
|
|
5
|
+
* Uses Effect's Deferred for efficient waiting and Ref for state management.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect, Layer, Ref, Duration, Schedule } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when container readiness check fails.
|
|
10
|
+
*/
|
|
11
|
+
export class ReadinessError extends Error {
|
|
12
|
+
_tag = 'ReadinessError';
|
|
13
|
+
containerId;
|
|
14
|
+
healthCheckType;
|
|
15
|
+
constructor(containerId, healthCheckType, message, cause) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.containerId = containerId;
|
|
18
|
+
this.healthCheckType = healthCheckType;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
this.name = 'ReadinessError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* ReadinessCoordinator service tag for dependency injection.
|
|
25
|
+
*/
|
|
26
|
+
export class ReadinessCoordinator extends Context.Tag('@harpoon/ReadinessCoordinator')() {
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Default wait options.
|
|
30
|
+
*/
|
|
31
|
+
const defaultWaitOptions = {
|
|
32
|
+
timeout: Duration.seconds(60),
|
|
33
|
+
interval: Duration.seconds(1),
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Live implementation using Effect Ref and Deferred.
|
|
37
|
+
*/
|
|
38
|
+
export const ReadinessCoordinatorLive = Layer.scoped(ReadinessCoordinator, Effect.gen(function* () {
|
|
39
|
+
// State: Map of containerId -> readiness state
|
|
40
|
+
const stateRef = yield* Ref.make(new Map());
|
|
41
|
+
// Pending waiters: Map of containerId -> Deferred for coordination
|
|
42
|
+
const waitersRef = yield* Ref.make(new Map());
|
|
43
|
+
yield* Effect.logDebug('ReadinessCoordinator initialized');
|
|
44
|
+
const service = {
|
|
45
|
+
_tag: 'ReadinessCoordinator',
|
|
46
|
+
waitForReady: (containerId, healthCheck, checkFn, options) => Effect.gen(function* () {
|
|
47
|
+
const opts = { ...defaultWaitOptions, ...options };
|
|
48
|
+
yield* Effect.logDebug('Waiting for container readiness').pipe(Effect.annotateLogs({
|
|
49
|
+
containerId,
|
|
50
|
+
healthCheckType: healthCheck.type,
|
|
51
|
+
timeoutMs: Duration.toMillis(opts.timeout),
|
|
52
|
+
}));
|
|
53
|
+
// Check if already ready
|
|
54
|
+
const currentState = yield* Ref.get(stateRef);
|
|
55
|
+
const existing = currentState.get(containerId);
|
|
56
|
+
if (existing?.ready) {
|
|
57
|
+
yield* Effect.logDebug('Container already ready').pipe(Effect.annotateLogs({ containerId }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Create a schedule that runs at the specified interval up to the timeout
|
|
61
|
+
const schedule = Schedule.spaced(opts.interval).pipe(Schedule.upTo(opts.timeout));
|
|
62
|
+
// Perform the health check with retry
|
|
63
|
+
const check = Effect.gen(function* () {
|
|
64
|
+
const result = yield* checkFn().pipe(Effect.catchAll((e) => Effect.succeed(false).pipe(Effect.tap(() => Effect.logDebug('Health check error').pipe(Effect.annotateLogs({ error: String(e) }))))));
|
|
65
|
+
if (!result) {
|
|
66
|
+
return yield* Effect.fail(new ReadinessError(containerId, healthCheck.type, 'Health check not yet passing'));
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
yield* check.pipe(Effect.retry({
|
|
71
|
+
schedule,
|
|
72
|
+
}), Effect.catchAll(() => Effect.fail(new ReadinessError(containerId, healthCheck.type, `Container ${containerId} did not become ready within ${Duration.toMillis(opts.timeout)}ms`))));
|
|
73
|
+
// Mark as ready
|
|
74
|
+
yield* Ref.update(stateRef, (map) => {
|
|
75
|
+
const newMap = new Map(map);
|
|
76
|
+
newMap.set(containerId, {
|
|
77
|
+
ready: true,
|
|
78
|
+
timestamp: new Date(),
|
|
79
|
+
healthCheckType: healthCheck.type,
|
|
80
|
+
});
|
|
81
|
+
return newMap;
|
|
82
|
+
});
|
|
83
|
+
yield* Effect.logInfo('Container is ready').pipe(Effect.annotateLogs({ containerId, healthCheckType: healthCheck.type }));
|
|
84
|
+
}),
|
|
85
|
+
isReady: (containerId) => Ref.get(stateRef).pipe(Effect.map((map) => map.get(containerId)?.ready ?? false)),
|
|
86
|
+
markReady: (containerId) => Ref.update(stateRef, (map) => {
|
|
87
|
+
const newMap = new Map(map);
|
|
88
|
+
newMap.set(containerId, {
|
|
89
|
+
ready: true,
|
|
90
|
+
timestamp: new Date(),
|
|
91
|
+
healthCheckType: 'manual',
|
|
92
|
+
});
|
|
93
|
+
return newMap;
|
|
94
|
+
}).pipe(Effect.tap(() => Effect.logDebug('Container marked as ready').pipe(Effect.annotateLogs({ containerId })))),
|
|
95
|
+
markNotReady: (containerId) => Ref.update(stateRef, (map) => {
|
|
96
|
+
const newMap = new Map(map);
|
|
97
|
+
const existing = map.get(containerId);
|
|
98
|
+
if (existing) {
|
|
99
|
+
newMap.set(containerId, { ...existing, ready: false });
|
|
100
|
+
}
|
|
101
|
+
return newMap;
|
|
102
|
+
}).pipe(Effect.tap(() => Effect.logDebug('Container marked as not ready').pipe(Effect.annotateLogs({ containerId })))),
|
|
103
|
+
untrack: (containerId) => Effect.all([
|
|
104
|
+
Ref.update(stateRef, (map) => {
|
|
105
|
+
const newMap = new Map(map);
|
|
106
|
+
newMap.delete(containerId);
|
|
107
|
+
return newMap;
|
|
108
|
+
}),
|
|
109
|
+
Ref.update(waitersRef, (map) => {
|
|
110
|
+
const newMap = new Map(map);
|
|
111
|
+
newMap.delete(containerId);
|
|
112
|
+
return newMap;
|
|
113
|
+
}),
|
|
114
|
+
]).pipe(Effect.asVoid),
|
|
115
|
+
getAll: () => Ref.get(stateRef).pipe(Effect.map((map) => [...map.entries()].map(([containerId, state]) => ({
|
|
116
|
+
containerId,
|
|
117
|
+
ready: state.ready,
|
|
118
|
+
timestamp: state.timestamp,
|
|
119
|
+
})))),
|
|
120
|
+
};
|
|
121
|
+
return service;
|
|
122
|
+
}));
|
|
123
|
+
/**
|
|
124
|
+
* Test implementation that can be pre-configured with ready containers.
|
|
125
|
+
*/
|
|
126
|
+
export const ReadinessCoordinatorTest = Layer.scoped(ReadinessCoordinator, Effect.gen(function* () {
|
|
127
|
+
const stateRef = yield* Ref.make(new Map());
|
|
128
|
+
return {
|
|
129
|
+
_tag: 'ReadinessCoordinator',
|
|
130
|
+
waitForReady: (containerId, healthCheck, checkFn, _options) => checkFn().pipe(Effect.catchAll((e) => Effect.fail(new ReadinessError(containerId, healthCheck.type, String(e), e))), Effect.flatMap((ready) => ready
|
|
131
|
+
? Ref.update(stateRef, (map) => {
|
|
132
|
+
const newMap = new Map(map);
|
|
133
|
+
newMap.set(containerId, {
|
|
134
|
+
ready: true,
|
|
135
|
+
timestamp: new Date(),
|
|
136
|
+
healthCheckType: healthCheck.type,
|
|
137
|
+
});
|
|
138
|
+
return newMap;
|
|
139
|
+
})
|
|
140
|
+
: Effect.fail(new ReadinessError(containerId, healthCheck.type, 'Not ready in test')))),
|
|
141
|
+
isReady: (containerId) => Ref.get(stateRef).pipe(Effect.map((map) => map.get(containerId)?.ready ?? false)),
|
|
142
|
+
markReady: (containerId) => Ref.update(stateRef, (map) => {
|
|
143
|
+
const newMap = new Map(map);
|
|
144
|
+
newMap.set(containerId, {
|
|
145
|
+
ready: true,
|
|
146
|
+
timestamp: new Date(),
|
|
147
|
+
healthCheckType: 'manual',
|
|
148
|
+
});
|
|
149
|
+
return newMap;
|
|
150
|
+
}),
|
|
151
|
+
markNotReady: (containerId) => Ref.update(stateRef, (map) => {
|
|
152
|
+
const newMap = new Map(map);
|
|
153
|
+
const existing = map.get(containerId);
|
|
154
|
+
if (existing) {
|
|
155
|
+
newMap.set(containerId, { ...existing, ready: false });
|
|
156
|
+
}
|
|
157
|
+
return newMap;
|
|
158
|
+
}),
|
|
159
|
+
untrack: (containerId) => Ref.update(stateRef, (map) => {
|
|
160
|
+
const newMap = new Map(map);
|
|
161
|
+
newMap.delete(containerId);
|
|
162
|
+
return newMap;
|
|
163
|
+
}),
|
|
164
|
+
getAll: () => Ref.get(stateRef).pipe(Effect.map((map) => [...map.entries()].map(([containerId, state]) => ({
|
|
165
|
+
containerId,
|
|
166
|
+
ready: state.ready,
|
|
167
|
+
timestamp: state.timestamp,
|
|
168
|
+
})))),
|
|
169
|
+
};
|
|
170
|
+
}));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceTracker Service
|
|
3
|
+
*
|
|
4
|
+
* Tracks all created resources (containers, networks, etc.) for lifecycle management.
|
|
5
|
+
* Uses Effect's Ref for thread-safe state management.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect, Layer, Option } from 'effect';
|
|
8
|
+
import { HarpoonConfig } from './HarpoonConfig';
|
|
9
|
+
/**
|
|
10
|
+
* Types of resources that can be tracked.
|
|
11
|
+
*/
|
|
12
|
+
export type ResourceType = 'container' | 'network' | 'database' | 'image';
|
|
13
|
+
/**
|
|
14
|
+
* A tracked resource with cleanup function.
|
|
15
|
+
*/
|
|
16
|
+
export interface TrackedResource {
|
|
17
|
+
/** Unique identifier for this resource */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Type of resource */
|
|
20
|
+
readonly type: ResourceType;
|
|
21
|
+
/** Human-readable name */
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** When the resource was created */
|
|
24
|
+
readonly createdAt: Date;
|
|
25
|
+
/** Cleanup function to destroy the resource */
|
|
26
|
+
readonly cleanup: () => Effect.Effect<void, Error>;
|
|
27
|
+
/** Priority for cleanup ordering (higher = cleanup first) */
|
|
28
|
+
readonly priority: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* ResourceTracker service interface.
|
|
32
|
+
*/
|
|
33
|
+
export interface ResourceTrackerService {
|
|
34
|
+
readonly _tag: 'ResourceTracker';
|
|
35
|
+
/** Register a new resource for tracking */
|
|
36
|
+
readonly register: (resource: TrackedResource) => Effect.Effect<void>;
|
|
37
|
+
/** Unregister a resource by ID */
|
|
38
|
+
readonly unregister: (id: string) => Effect.Effect<void>;
|
|
39
|
+
/** Get a tracked resource by ID */
|
|
40
|
+
readonly get: (id: string) => Effect.Effect<Option.Option<TrackedResource>>;
|
|
41
|
+
/** Get all tracked resources */
|
|
42
|
+
readonly getAll: () => Effect.Effect<readonly TrackedResource[]>;
|
|
43
|
+
/** Get resources by type */
|
|
44
|
+
readonly getByType: (type: ResourceType) => Effect.Effect<readonly TrackedResource[]>;
|
|
45
|
+
/** Get the count of tracked resources */
|
|
46
|
+
readonly count: () => Effect.Effect<number>;
|
|
47
|
+
/** Destroy all tracked resources in priority order */
|
|
48
|
+
readonly destroyAll: (options?: {
|
|
49
|
+
concurrency?: number;
|
|
50
|
+
}) => Effect.Effect<void, Error>;
|
|
51
|
+
/** Destroy resources by type */
|
|
52
|
+
readonly destroyByType: (type: ResourceType, options?: {
|
|
53
|
+
concurrency?: number;
|
|
54
|
+
}) => Effect.Effect<void, Error>;
|
|
55
|
+
/** Clear all tracking without cleanup (use with caution) */
|
|
56
|
+
readonly clear: () => Effect.Effect<void>;
|
|
57
|
+
}
|
|
58
|
+
declare const ResourceTracker_base: Context.TagClass<ResourceTracker, "@harpoon/ResourceTracker", ResourceTrackerService>;
|
|
59
|
+
/**
|
|
60
|
+
* ResourceTracker service tag for dependency injection.
|
|
61
|
+
*/
|
|
62
|
+
export declare class ResourceTracker extends ResourceTracker_base {
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Live implementation using Effect Ref for state management.
|
|
66
|
+
* Enhanced with retry schedules and per-resource timeouts.
|
|
67
|
+
*/
|
|
68
|
+
export declare const ResourceTrackerLive: Layer.Layer<ResourceTracker, never, HarpoonConfig>;
|
|
69
|
+
/**
|
|
70
|
+
* Test implementation that tracks resources in memory but doesn't actually clean up.
|
|
71
|
+
*/
|
|
72
|
+
export declare const ResourceTrackerTest: Layer.Layer<ResourceTracker, never, never>;
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=ResourceTracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceTracker.d.ts","sourceRoot":"","sources":["../../src/services/ResourceTracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAO,MAAM,EAAY,MAAM,QAAQ,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,uBAAuB;IACvB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAE5B,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,oCAAoC;IACpC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IAEzB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEnD,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAEjC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEtE,kCAAkC;IAClC,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEzD,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC;IAE5E,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC;IAEjE,4BAA4B;IAC5B,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC;IAEtF,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAE5C,sDAAsD;IACtD,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAExF,gCAAgC;IAChC,QAAQ,CAAC,aAAa,EAAE,CACtB,IAAI,EAAE,YAAY,EAClB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,KAC/B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEhC,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAC3C;;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,oBAGlC;CAAG;AAqCN;;;GAGG;AACH,eAAO,MAAM,mBAAmB,oDAoI/B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,4CAkD/B,CAAC"}
|