@compforge/harness-common 0.0.1
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 +25 -0
- package/dist/client-manager.d.ts +14 -0
- package/dist/client-manager.js +61 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +1 -0
- package/dist/datasource.d.ts +17 -0
- package/dist/datasource.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Harness Common for TypeScript
|
|
2
|
+
|
|
3
|
+
Execution-neutral client lifecycle and datasource contracts, shared by harnesses and toolbox clients.
|
|
4
|
+
No database drivers, Kubernetes adapters, Doctor concepts or business configuration live here.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import { ClientManager, type DataSource } from "@compforge/harness-common";
|
|
8
|
+
|
|
9
|
+
const clients = new ClientManager();
|
|
10
|
+
try {
|
|
11
|
+
const client = await clients.get(source); // source implements DataSource<Client>
|
|
12
|
+
// Borrow the initialized client; the root execution owns cleanup.
|
|
13
|
+
} finally {
|
|
14
|
+
await clients.dispose();
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A DataSource key identifies the complete configuration and access policy. Equal keys share client
|
|
19
|
+
initialization, not query results. Use dataSourceKey to hash configuration without exposing credentials.
|
|
20
|
+
ServiceDataSource associates a consumer-owned Service model with a source without changing its key.
|
|
21
|
+
|
|
22
|
+
Python's corresponding package is sdks/python/common (harness-common). Languages keep idiomatic APIs
|
|
23
|
+
and independently versioned packages; they need not implement every model in lockstep.
|
|
24
|
+
|
|
25
|
+
Development: bun install, make lint, make test. Published artifacts are Node-compatible ESM and declarations.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Client } from "./client.js";
|
|
2
|
+
import type { DataSource } from "./datasource.js";
|
|
3
|
+
/** Consumers borrow clients; only the root owner receives the disposal capability. */
|
|
4
|
+
export interface ClientProvider {
|
|
5
|
+
get<C extends Client>(source: DataSource<C>): Promise<C>;
|
|
6
|
+
}
|
|
7
|
+
/** @spec One root execution shares successful initialization per identity and closes clients at finalize. */
|
|
8
|
+
export declare class ClientManager implements ClientProvider {
|
|
9
|
+
#private;
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
constructor(signal?: AbortSignal);
|
|
12
|
+
get<C extends Client>(source: DataSource<C>): Promise<C>;
|
|
13
|
+
dispose(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** @spec One root execution shares successful initialization per identity and closes clients at finalize. */
|
|
2
|
+
export class ClientManager {
|
|
3
|
+
#controller = new AbortController();
|
|
4
|
+
signal;
|
|
5
|
+
#clients = new Map();
|
|
6
|
+
#ready = [];
|
|
7
|
+
#disposal;
|
|
8
|
+
constructor(signal) {
|
|
9
|
+
this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
|
|
10
|
+
}
|
|
11
|
+
get(source) {
|
|
12
|
+
this.signal.throwIfAborted();
|
|
13
|
+
const existing = this.#clients.get(source.key);
|
|
14
|
+
if (existing)
|
|
15
|
+
return existing;
|
|
16
|
+
// Publish before construction so concurrent callers never dispatch a second factory.
|
|
17
|
+
const pending = Promise.resolve().then(async () => {
|
|
18
|
+
this.signal.throwIfAborted();
|
|
19
|
+
const client = source.createClient(this.signal);
|
|
20
|
+
try {
|
|
21
|
+
await client.initialize();
|
|
22
|
+
this.signal.throwIfAborted();
|
|
23
|
+
this.#ready.push(client);
|
|
24
|
+
return client;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
try {
|
|
28
|
+
await client.dispose();
|
|
29
|
+
}
|
|
30
|
+
catch (cleanup) {
|
|
31
|
+
throw new AggregateError([error, cleanup], "Client initialization and cleanup failed");
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
this.#clients.set(source.key, pending);
|
|
37
|
+
void pending.catch(() => { if (this.#clients.get(source.key) === pending)
|
|
38
|
+
this.#clients.delete(source.key); });
|
|
39
|
+
return pending;
|
|
40
|
+
}
|
|
41
|
+
dispose() {
|
|
42
|
+
return this.#disposal ??= (async () => {
|
|
43
|
+
this.#controller.abort(new Error("Client manager disposed"));
|
|
44
|
+
const settled = await Promise.allSettled(this.#clients.values());
|
|
45
|
+
this.#clients.clear();
|
|
46
|
+
// Dependencies must be acquired before their consumers initialize, so reverse readiness is safe.
|
|
47
|
+
const errors = settled.flatMap(result => result.status === "rejected" && result.reason instanceof AggregateError
|
|
48
|
+
? [result.reason] : []);
|
|
49
|
+
for (const client of this.#ready.splice(0).reverse()) {
|
|
50
|
+
try {
|
|
51
|
+
await client.dispose();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
errors.push(error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (errors.length)
|
|
58
|
+
throw new AggregateError(errors, "Client cleanup failed");
|
|
59
|
+
})();
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Client } from "./client.js";
|
|
2
|
+
/** Identity includes protocol, target, configuration and credentials, never an object address. */
|
|
3
|
+
export interface DataSource<C extends Client> {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
/** Construct only; Client.initialize owns external work. */
|
|
6
|
+
createClient(signal: AbortSignal): C;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* @spec A Service association does not own a client or change the source identity.
|
|
10
|
+
* @why Service models belong to consumers; binding the same source must not fragment client reuse.
|
|
11
|
+
*/
|
|
12
|
+
export interface ServiceDataSource<Service, C extends Client> {
|
|
13
|
+
readonly service: Service;
|
|
14
|
+
readonly source: DataSource<C>;
|
|
15
|
+
}
|
|
16
|
+
/** Stable in-memory identity; connection credentials must not appear in observable cache keys. */
|
|
17
|
+
export declare function dataSourceKey(protocol: string, configuration: unknown): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@compforge/harness-common",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Execution-neutral client lifecycle and datasource contracts for harnesses",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"build": "tsc -p tsconfig.build.json",
|
|
9
|
+
"test": "bun test"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"@types/node": "^22",
|
|
24
|
+
"typescript": "^5.6.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
}
|
|
32
|
+
}
|