@compforge/harness-common 0.0.1 → 0.3.0
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/README.md +43 -4
- package/dist/client-manager.d.ts +7 -8
- package/dist/client-manager.js +20 -11
- package/dist/client-provider.d.ts +16 -0
- package/dist/client-provider.js +12 -0
- package/dist/component.d.ts +6 -0
- package/dist/component.js +1 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +20 -0
- package/dist/datasource.d.ts +7 -10
- package/dist/datasource.js +1 -12
- package/dist/environment.d.ts +16 -0
- package/dist/environment.js +1 -0
- package/dist/forge.d.ts +4 -0
- package/dist/forge.js +1 -0
- package/dist/host.d.ts +15 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +10 -1
- package/dist/repository.d.ts +6 -0
- package/dist/repository.js +1 -0
- package/dist/service.d.ts +16 -0
- package/dist/service.js +1 -0
- package/dist/workload.d.ts +38 -0
- package/dist/workload.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Harness Common for TypeScript
|
|
2
2
|
|
|
3
|
-
Execution-neutral client lifecycle and datasource contracts, shared by harnesses and toolbox clients.
|
|
3
|
+
Execution-neutral service topology, client lifecycle, environment context and datasource contracts, shared by harnesses and toolbox clients.
|
|
4
4
|
No database drivers, Kubernetes adapters, Doctor concepts or business configuration live here.
|
|
5
5
|
|
|
6
6
|
```typescript
|
|
@@ -15,9 +15,48 @@ try {
|
|
|
15
15
|
}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
A
|
|
19
|
-
initialization, not query results. Use
|
|
20
|
-
ServiceDataSource associates a
|
|
18
|
+
A ClientProvider clientKey identifies the complete configuration and access policy. Equal keys share client
|
|
19
|
+
initialization, not query results. Use clientKey to hash configuration without exposing credentials.
|
|
20
|
+
ServiceDataSource associates a shared Service (or a consumer extension) with a source without changing its key.
|
|
21
|
+
DataSource extends ClientProvider with data-access semantics; accessible concrete environments implement
|
|
22
|
+
ClientProvider directly. Both use the same manager. EnvironmentContext binds an Environment (or a consumer
|
|
23
|
+
extension), a get-only client view and monotonic deadlineMs (performance.now()).
|
|
24
|
+
It needs no fixture and owns neither disposal nor cancellation; forward remainingMs into operations.
|
|
25
|
+
Providers receive a get-only ClientManager view and the root AbortSignal. Acquire dependencies through that
|
|
26
|
+
view during initialize, so concurrent consumers share them and dispose before their dependencies.
|
|
27
|
+
Dependency graphs must be acyclic. A failed initialization permits retry only after successful cleanup;
|
|
28
|
+
failed cleanup poisons that key and is reported again when the root manager is disposed.
|
|
29
|
+
|
|
30
|
+
Service identifies a Component's runtime presence in an Environment. A Component belongs to a Repository
|
|
31
|
+
on a Forge; a Service explicitly declares zero or more Workloads. Consumers extend Service with business
|
|
32
|
+
capabilities, without duplicating these identities. For example:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import type { Service } from "@compforge/harness-common";
|
|
36
|
+
|
|
37
|
+
const service: Service = {
|
|
38
|
+
name: "chat",
|
|
39
|
+
component: {
|
|
40
|
+
name: "api",
|
|
41
|
+
repository: { forge: { name: "github" }, path: "example/app" },
|
|
42
|
+
},
|
|
43
|
+
environment: { name: "test" },
|
|
44
|
+
workloads: [{
|
|
45
|
+
name: "web", platform: "kubernetes",
|
|
46
|
+
location: { kind: "resource", resource_kind: "Deployment", name: "api-server" },
|
|
47
|
+
}],
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Environment carries identity and an optional local/SSH Host declaration, not a connection. HostEnvironment
|
|
52
|
+
describes a target running directly on that host. Concrete accessible environments, such as toolbox's
|
|
53
|
+
KubernetesEnvironment, additionally implement ClientProvider. A Host declaration alone does not add SSH
|
|
54
|
+
support to a client. Service<E> and EnvironmentContext<E> preserve the concrete environment's access type.
|
|
55
|
+
|
|
56
|
+
Workload describes a logical runtime carrier and its resource, Kubernetes Service or labels location.
|
|
57
|
+
WorkloadInstance records an observed Pod incarnation with environment and UID identity.
|
|
58
|
+
These contracts perform no discovery or I/O; callers resolve declarations in their selected environment.
|
|
59
|
+
Container selection affects access routing, not Pod identity. See [the shared model](../../../docs/workload.md).
|
|
21
60
|
|
|
22
61
|
Python's corresponding package is sdks/python/common (harness-common). Languages keep idiomatic APIs
|
|
23
62
|
and independently versioned packages; they need not implement every model in lockstep.
|
package/dist/client-manager.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { Client } from "./client.js";
|
|
2
|
-
import type {
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export declare class ClientManager implements ClientProvider {
|
|
2
|
+
import type { ClientProvider } from "./client-provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* @spec One root execution shares successful initialization per identity and closes clients at finalize.
|
|
5
|
+
* @rule Failed cleanup poisons the identity and remains a root disposal error; it never permits retry.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ClientManager {
|
|
9
8
|
#private;
|
|
10
9
|
readonly signal: AbortSignal;
|
|
11
10
|
constructor(signal?: AbortSignal);
|
|
12
|
-
get<C extends Client>(
|
|
11
|
+
get<C extends Client>(provider: ClientProvider<C>): Promise<C>;
|
|
13
12
|
dispose(): Promise<void>;
|
|
14
13
|
}
|
package/dist/client-manager.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @spec One root execution shares successful initialization per identity and closes clients at finalize.
|
|
3
|
+
* @rule Failed cleanup poisons the identity and remains a root disposal error; it never permits retry.
|
|
4
|
+
*/
|
|
2
5
|
export class ClientManager {
|
|
3
6
|
#controller = new AbortController();
|
|
4
7
|
signal;
|
|
5
8
|
#clients = new Map();
|
|
6
9
|
#ready = [];
|
|
10
|
+
#unclean = new Set();
|
|
11
|
+
#cleanupErrors = [];
|
|
7
12
|
#disposal;
|
|
8
13
|
constructor(signal) {
|
|
9
14
|
this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
|
|
10
15
|
}
|
|
11
|
-
get(
|
|
16
|
+
get(provider) {
|
|
12
17
|
this.signal.throwIfAborted();
|
|
13
|
-
const
|
|
18
|
+
const key = provider.clientKey;
|
|
19
|
+
const existing = this.#clients.get(key);
|
|
14
20
|
if (existing)
|
|
15
21
|
return existing;
|
|
16
|
-
// Publish before construction so concurrent callers never dispatch a second
|
|
22
|
+
// Publish before construction so concurrent callers never dispatch a second provider.
|
|
17
23
|
const pending = Promise.resolve().then(async () => {
|
|
18
24
|
this.signal.throwIfAborted();
|
|
19
|
-
const client =
|
|
25
|
+
const client = provider.createClient(this, this.signal);
|
|
20
26
|
try {
|
|
21
27
|
await client.initialize();
|
|
22
28
|
this.signal.throwIfAborted();
|
|
@@ -28,24 +34,27 @@ export class ClientManager {
|
|
|
28
34
|
await client.dispose();
|
|
29
35
|
}
|
|
30
36
|
catch (cleanup) {
|
|
37
|
+
this.#unclean.add(key);
|
|
38
|
+
this.#cleanupErrors.push(cleanup);
|
|
31
39
|
throw new AggregateError([error, cleanup], "Client initialization and cleanup failed");
|
|
32
40
|
}
|
|
33
41
|
throw error;
|
|
34
42
|
}
|
|
35
43
|
});
|
|
36
|
-
this.#clients.set(
|
|
37
|
-
void pending.catch(() => {
|
|
38
|
-
this.#clients.
|
|
44
|
+
this.#clients.set(key, pending);
|
|
45
|
+
void pending.catch(() => {
|
|
46
|
+
if (!this.#unclean.has(key) && this.#clients.get(key) === pending)
|
|
47
|
+
this.#clients.delete(key);
|
|
48
|
+
});
|
|
39
49
|
return pending;
|
|
40
50
|
}
|
|
41
51
|
dispose() {
|
|
42
52
|
return this.#disposal ??= (async () => {
|
|
43
53
|
this.#controller.abort(new Error("Client manager disposed"));
|
|
44
|
-
|
|
54
|
+
await Promise.allSettled(this.#clients.values());
|
|
45
55
|
this.#clients.clear();
|
|
46
56
|
// Dependencies must be acquired before their consumers initialize, so reverse readiness is safe.
|
|
47
|
-
const errors =
|
|
48
|
-
? [result.reason] : []);
|
|
57
|
+
const errors = [...this.#cleanupErrors];
|
|
49
58
|
for (const client of this.#ready.splice(0).reverse()) {
|
|
50
59
|
try {
|
|
51
60
|
await client.dispose();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Client } from "./client.js";
|
|
2
|
+
import type { ClientManager } from "./client-manager.js";
|
|
3
|
+
/**
|
|
4
|
+
* Keyed construction independent of data or environment semantics.
|
|
5
|
+
* @spec Both environment access and data access share ClientManager ownership.
|
|
6
|
+
* @rule Identity covers implementation, target, credentials and capacity policy.
|
|
7
|
+
*/
|
|
8
|
+
export interface ClientProvider<C extends Client> {
|
|
9
|
+
readonly clientKey: string;
|
|
10
|
+
/** Construct only; initialize borrows dependencies through clients before publishing readiness.
|
|
11
|
+
* Dependencies must be acyclic; borrowers never dispose them. No environment model is required.
|
|
12
|
+
*/
|
|
13
|
+
createClient(clients: Pick<ClientManager, "get">, signal: AbortSignal): C;
|
|
14
|
+
}
|
|
15
|
+
/** Stable configuration identity; credentials never appear in observable keys. */
|
|
16
|
+
export declare function clientKey(protocol: string, configuration: unknown): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Stable configuration identity; credentials never appear in observable keys. */
|
|
3
|
+
export function clientKey(protocol, configuration) {
|
|
4
|
+
const canonical = (value) => {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map(canonical);
|
|
7
|
+
if (value && typeof value === "object")
|
|
8
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)]));
|
|
9
|
+
return value;
|
|
10
|
+
};
|
|
11
|
+
return `${protocol}:${createHash("sha256").update(JSON.stringify(canonical(configuration))).digest("hex")}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClientManager } from "./client-manager.js";
|
|
2
|
+
import type { Environment } from "./environment.js";
|
|
3
|
+
/**
|
|
4
|
+
* Execution-scoped environment access, independent of fixture orchestration.
|
|
5
|
+
* @rule Borrows clients; does not initialize or dispose resources.
|
|
6
|
+
* Deadline uses performance.now() milliseconds, not wall-clock time.
|
|
7
|
+
* Consumers propagate remainingMs; the context itself does not cancel work.
|
|
8
|
+
* Consumers extend the shared Environment identity with their own platform access.
|
|
9
|
+
*/
|
|
10
|
+
export declare class EnvironmentContext<E extends Environment = Environment> {
|
|
11
|
+
readonly environment: E;
|
|
12
|
+
readonly clients: Pick<ClientManager, "get">;
|
|
13
|
+
readonly deadlineMs: number;
|
|
14
|
+
constructor(environment: E, clients: Pick<ClientManager, "get">, deadlineMs: number);
|
|
15
|
+
get remainingMs(): number;
|
|
16
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution-scoped environment access, independent of fixture orchestration.
|
|
3
|
+
* @rule Borrows clients; does not initialize or dispose resources.
|
|
4
|
+
* Deadline uses performance.now() milliseconds, not wall-clock time.
|
|
5
|
+
* Consumers propagate remainingMs; the context itself does not cancel work.
|
|
6
|
+
* Consumers extend the shared Environment identity with their own platform access.
|
|
7
|
+
*/
|
|
8
|
+
export class EnvironmentContext {
|
|
9
|
+
environment;
|
|
10
|
+
clients;
|
|
11
|
+
deadlineMs;
|
|
12
|
+
constructor(environment, clients, deadlineMs) {
|
|
13
|
+
this.environment = environment;
|
|
14
|
+
this.clients = clients;
|
|
15
|
+
this.deadlineMs = deadlineMs;
|
|
16
|
+
}
|
|
17
|
+
get remainingMs() {
|
|
18
|
+
return Math.max(0, this.deadlineMs - performance.now());
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/datasource.d.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import type { Client } from "./client.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
createClient(signal: AbortSignal): C;
|
|
2
|
+
import type { ClientProvider } from "./client-provider.js";
|
|
3
|
+
import type { Service } from "./service.js";
|
|
4
|
+
/** A source of data; accessible environments implement ClientProvider directly. */
|
|
5
|
+
export interface DataSource<C extends Client> extends ClientProvider<C> {
|
|
7
6
|
}
|
|
8
7
|
/**
|
|
9
8
|
* @spec A Service association does not own a client or change the source identity.
|
|
10
|
-
* @why
|
|
9
|
+
* @why Business ownership must not fragment reuse of the same source's client.
|
|
11
10
|
*/
|
|
12
|
-
export interface ServiceDataSource<Service, C extends Client> {
|
|
13
|
-
readonly service:
|
|
11
|
+
export interface ServiceDataSource<S extends Service, C extends Client> {
|
|
12
|
+
readonly service: S;
|
|
14
13
|
readonly source: DataSource<C>;
|
|
15
14
|
}
|
|
16
|
-
/** Stable in-memory identity; connection credentials must not appear in observable cache keys. */
|
|
17
|
-
export declare function dataSourceKey(protocol: string, configuration: unknown): string;
|
package/dist/datasource.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/** Stable in-memory identity; connection credentials must not appear in observable cache keys. */
|
|
3
|
-
export function dataSourceKey(protocol, configuration) {
|
|
4
|
-
const canonical = (value) => {
|
|
5
|
-
if (Array.isArray(value))
|
|
6
|
-
return value.map(canonical);
|
|
7
|
-
if (value && typeof value === "object")
|
|
8
|
-
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)]));
|
|
9
|
-
return value;
|
|
10
|
-
};
|
|
11
|
-
return `${protocol}:${createHash("sha256").update(JSON.stringify(canonical(configuration))).digest("hex")}`;
|
|
12
|
-
}
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Host } from "./host.js";
|
|
2
|
+
/**
|
|
3
|
+
* Named deployment environment, independent of any access mechanism.
|
|
4
|
+
* @rule Accessible environments additionally implement ClientProvider; identity alone grants no access.
|
|
5
|
+
*/
|
|
6
|
+
export interface Environment {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** Omission denotes a generic environment. Concrete platforms declare their kind. */
|
|
9
|
+
readonly kind?: string;
|
|
10
|
+
/** Omission uses the current execution host, without changing environment identity. */
|
|
11
|
+
readonly host?: Host;
|
|
12
|
+
}
|
|
13
|
+
/** Target processes run directly on the optional host, locally or through SSH. */
|
|
14
|
+
export interface HostEnvironment extends Environment {
|
|
15
|
+
readonly kind: "host";
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/forge.d.ts
ADDED
package/dist/forge.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An optional environment access host, not necessarily the target's execution host.
|
|
3
|
+
* Kubernetes access paths belong to this host's filesystem, not the Pod's.
|
|
4
|
+
* SSH addresses use the caller's SSH configuration; credentials do not belong here.
|
|
5
|
+
* This declaration does not imply a consumer supports SSH execution.
|
|
6
|
+
*/
|
|
7
|
+
export type Host = {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
} & ({
|
|
10
|
+
readonly transport?: "local";
|
|
11
|
+
readonly address?: never;
|
|
12
|
+
} | {
|
|
13
|
+
readonly transport: "ssh";
|
|
14
|
+
readonly address: string;
|
|
15
|
+
});
|
package/dist/host.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export * from "./client.js";
|
|
2
|
-
export * from "./client-
|
|
2
|
+
export * from "./client-provider.js";
|
|
3
|
+
export * from "./context.js";
|
|
4
|
+
export { ClientManager } from "./client-manager.js";
|
|
3
5
|
export * from "./datasource.js";
|
|
6
|
+
export * from "./workload.js";
|
|
7
|
+
export * from "./forge.js";
|
|
8
|
+
export * from "./repository.js";
|
|
9
|
+
export * from "./component.js";
|
|
10
|
+
export * from "./host.js";
|
|
11
|
+
export * from "./environment.js";
|
|
12
|
+
export * from "./service.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export * from "./client.js";
|
|
2
|
-
export * from "./client-
|
|
2
|
+
export * from "./client-provider.js";
|
|
3
|
+
export * from "./context.js";
|
|
4
|
+
export { ClientManager } from "./client-manager.js";
|
|
3
5
|
export * from "./datasource.js";
|
|
6
|
+
export * from "./workload.js";
|
|
7
|
+
export * from "./forge.js";
|
|
8
|
+
export * from "./repository.js";
|
|
9
|
+
export * from "./component.js";
|
|
10
|
+
export * from "./host.js";
|
|
11
|
+
export * from "./environment.js";
|
|
12
|
+
export * from "./service.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Component } from "./component.js";
|
|
2
|
+
import type { Environment } from "./environment.js";
|
|
3
|
+
import type { Workload } from "./workload.js";
|
|
4
|
+
/**
|
|
5
|
+
* One Component's logical runtime presence in an Environment.
|
|
6
|
+
* @spec Identity is scoped by name, Component and Environment, not platform resource names.
|
|
7
|
+
* @rule Workload mappings may change without changing Service identity; declarations prove no live instances.
|
|
8
|
+
* Consumers extend this model with their own capabilities and execution policy.
|
|
9
|
+
*/
|
|
10
|
+
export interface Service<E extends Environment = Environment> {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly component: Component;
|
|
13
|
+
readonly environment: E;
|
|
14
|
+
/** Zero or more runtime carriers; an empty array declares no workloads. */
|
|
15
|
+
readonly workloads: readonly Workload[];
|
|
16
|
+
}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Logical carrier, not proof of existence. Location is data, not a discovery executor. */
|
|
2
|
+
export interface Workload {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly platform: "kubernetes";
|
|
5
|
+
/** Omission requires a resolver default; never means all namespaces. */
|
|
6
|
+
readonly namespace?: string;
|
|
7
|
+
readonly location: {
|
|
8
|
+
readonly kind: "resource";
|
|
9
|
+
readonly resource_kind: "Deployment" | "StatefulSet" | "DaemonSet" | "Pod";
|
|
10
|
+
readonly name: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "service";
|
|
13
|
+
readonly name: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "labels";
|
|
16
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
17
|
+
};
|
|
18
|
+
/** Operation default, not workload identity. */
|
|
19
|
+
readonly container?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Observed Pod incarnation. No access credentials belong in instance evidence.
|
|
23
|
+
* @spec Environment and UID separate clusters and same-name Pod replacements.
|
|
24
|
+
* @rule Container participates in access/cache keys, not Pod identity.
|
|
25
|
+
*/
|
|
26
|
+
export interface WorkloadInstance {
|
|
27
|
+
readonly platform: "kubernetes";
|
|
28
|
+
/** Stable caller-owned key distinguishing clusters/contexts, not a display label. */
|
|
29
|
+
readonly environment: string;
|
|
30
|
+
/** Declaration name scoped by owning Service; provenance, not Pod identity. */
|
|
31
|
+
readonly workload: string;
|
|
32
|
+
readonly namespace: string;
|
|
33
|
+
readonly pod: string;
|
|
34
|
+
readonly uid: string;
|
|
35
|
+
readonly container?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Physical identity only; callers retain workload provenance separately. */
|
|
38
|
+
export declare function sameWorkloadInstance(left: WorkloadInstance, right: WorkloadInstance): boolean;
|
package/dist/workload.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Physical identity only; callers retain workload provenance separately. */
|
|
2
|
+
export function sameWorkloadInstance(left, right) {
|
|
3
|
+
return left.platform === right.platform
|
|
4
|
+
&& left.environment === right.environment
|
|
5
|
+
&& left.namespace === right.namespace
|
|
6
|
+
&& left.pod === right.pod
|
|
7
|
+
&& left.uid === right.uid;
|
|
8
|
+
}
|