@bjorntech/key-ferry 0.1.12 → 0.1.14
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 +20 -0
- package/dist/broker-registry.d.ts +32 -0
- package/dist/broker-registry.d.ts.map +1 -1
- package/dist/broker-registry.js +376 -42
- package/dist/broker-registry.js.map +1 -1
- package/dist/contracts.d.ts +13 -3
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -1
- package/dist/options.d.ts +1 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +17 -0
- package/dist/options.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +4 -4
- package/dist/plugin.js.map +1 -1
- package/dist/rpc.d.ts +62 -12
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +41 -27
- package/dist/rpc.js.map +1 -1
- package/dist/scoped-controller.d.ts +5 -3
- package/dist/scoped-controller.d.ts.map +1 -1
- package/dist/scoped-controller.js +110 -44
- package/dist/scoped-controller.js.map +1 -1
- package/dist/scopes/controller-helpers.d.ts.map +1 -1
- package/dist/scopes/controller-helpers.js +15 -6
- package/dist/scopes/controller-helpers.js.map +1 -1
- package/dist/services/process.d.ts +3 -0
- package/dist/services/process.d.ts.map +1 -1
- package/dist/services/process.js +22 -6
- package/dist/services/process.js.map +1 -1
- package/dist/varlock/diagnostics.d.ts +7 -0
- package/dist/varlock/diagnostics.d.ts.map +1 -0
- package/dist/varlock/diagnostics.js +35 -0
- package/dist/varlock/diagnostics.js.map +1 -0
- package/dist/varlock/manager.d.ts.map +1 -1
- package/dist/varlock/manager.js +265 -72
- package/dist/varlock/manager.js.map +1 -1
- package/dist/varlock/ownership.d.ts +60 -0
- package/dist/varlock/ownership.d.ts.map +1 -0
- package/dist/varlock/ownership.js +372 -0
- package/dist/varlock/ownership.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,11 @@ Direct provider/API transport protection is not included.
|
|
|
36
36
|
- Varlock CLI 1.23.0 installed separately as `varlock`
|
|
37
37
|
- An installed Varlock CLI and a configured Varlock environment/schema with the
|
|
38
38
|
secrets and proxy rules your MCP servers need
|
|
39
|
+
- A private, writable runtime directory for ownership records. By default this
|
|
40
|
+
is `tmpdir()/key-ferry-<uid>`; `adapter.ownershipDirectory` may be used to
|
|
41
|
+
inject another private, writable directory. If it cannot be secured, startup
|
|
42
|
+
fails closed with the `ownership_unavailable` diagnostic (also shown by
|
|
43
|
+
`key-ferry.doctor`).
|
|
39
44
|
- Configure secret sources and authentication following the
|
|
40
45
|
[Varlock documentation](https://varlock.dev/)
|
|
41
46
|
|
|
@@ -194,6 +199,21 @@ filters requested keys. Mark each remote upstream host with `@proxy(domain="..."
|
|
|
194
199
|
Key Ferry does not install Varlock, create its schema, or configure provider
|
|
195
200
|
authentication. Never commit a real secret.
|
|
196
201
|
|
|
202
|
+
Broker processes remain available for `idleTimeoutMs` milliseconds after the
|
|
203
|
+
last lease is released (default: `600000`, or ten minutes). This reuses the
|
|
204
|
+
authenticated Varlock proxy when returning to a project. Set
|
|
205
|
+
`idleTimeoutMs: 0` for immediate teardown; idle brokers are always stopped on
|
|
206
|
+
host shutdown or when their schema/configuration identity changes. This is a
|
|
207
|
+
security/performance trade-off: resolved secrets remain in the proxy process's
|
|
208
|
+
memory for up to the idle timeout after leaving a project. On startup Key Ferry
|
|
209
|
+
also reaps orphaned, identity-checked proxy records left by an earlier process.
|
|
210
|
+
If stopping a proxy fails during shutdown, Key Ferry keeps its ownership record and
|
|
211
|
+
defers cleanup to the next startup, whose reaper retries the same session UUID.
|
|
212
|
+
On SIGINT or SIGTERM it performs bounded cleanup (up to three seconds), removes
|
|
213
|
+
its signal listener, and re-raises the signal when it is the only listener so
|
|
214
|
+
the host's normal termination semantics are preserved; host-owned signal
|
|
215
|
+
listeners remain in control.
|
|
216
|
+
|
|
197
217
|
## TUI commands
|
|
198
218
|
|
|
199
219
|
| Palette command | Slash command | RPC |
|
|
@@ -10,20 +10,52 @@ export interface BrokerLease extends ProjectKeyFerryManagerService {
|
|
|
10
10
|
}
|
|
11
11
|
export interface BrokerRegistryOptions {
|
|
12
12
|
readonly canonicalize?: (directory: string) => Effect.Effect<string, unknown>;
|
|
13
|
+
/** Supplies the schema fingerprint; the default retains the real filesystem implementation. */
|
|
14
|
+
readonly schemaHash?: (directory: string) => Effect.Effect<string, unknown>;
|
|
13
15
|
readonly maxEntries?: number;
|
|
16
|
+
readonly now?: () => number;
|
|
17
|
+
readonly idleTimeoutMs?: number;
|
|
18
|
+
readonly logger?: (event: BrokerLogEvent) => void;
|
|
19
|
+
readonly hostShutdownHooks?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface BrokerLogEvent {
|
|
22
|
+
readonly event: "start" | "ready" | "release" | "idle-scheduled" | "reused" | "disposed" | "stop-failed" | "dispose-failed" | "stale-reaped" | "stale-skipped";
|
|
23
|
+
readonly scope: "global" | "project";
|
|
24
|
+
readonly directory: string;
|
|
25
|
+
readonly keyHash: string;
|
|
26
|
+
readonly pid?: number;
|
|
27
|
+
readonly proxySessionId?: string;
|
|
14
28
|
}
|
|
15
29
|
type BrokerResult = {
|
|
16
30
|
readonly status: SafeStatus;
|
|
17
31
|
readonly environment: ResolvedEnvironment;
|
|
18
32
|
};
|
|
33
|
+
export declare const RETRY_BASE_DELAY_MS = 5000;
|
|
34
|
+
export declare const RETRY_MAX_DELAY_MS = 300000;
|
|
35
|
+
export declare const retryDelayMs: (failureCount: number) => number;
|
|
19
36
|
/** A module-local broker; it deliberately does not communicate across processes. */
|
|
20
37
|
export declare function makeBrokerRegistry(configuration?: BrokerRegistryOptions): {
|
|
21
38
|
acquire: <R>(factory: ProjectKeyFerryManagerFactory<R>, options: ProjectKeyFerryManagerOptions) => Effect.Effect<BrokerLease, any, never>;
|
|
22
39
|
retry: (factory: ProjectKeyFerryManagerFactory<any>, options: ProjectKeyFerryManagerOptions) => Effect.Effect<BrokerResult, any, never>;
|
|
40
|
+
retryIfDue: (factory: ProjectKeyFerryManagerFactory<any>, options: ProjectKeyFerryManagerOptions) => Effect.Effect<BrokerResult | undefined, any, never>;
|
|
41
|
+
shutdown: Effect.Effect<void, never, never>;
|
|
23
42
|
};
|
|
43
|
+
export interface HostSignalProcess {
|
|
44
|
+
readonly pid: number;
|
|
45
|
+
readonly on: (event: any, listener: (...args: any[]) => void) => unknown;
|
|
46
|
+
readonly removeListener: (event: any, listener: (...args: any[]) => void) => unknown;
|
|
47
|
+
readonly listeners: (event: any) => Function[];
|
|
48
|
+
readonly kill: (pid: number, signal: "SIGINT" | "SIGTERM") => unknown;
|
|
49
|
+
}
|
|
50
|
+
export declare function removeHostShutdownHooks(): void;
|
|
51
|
+
export declare function installHostShutdownHooks(shutdown: Effect.Effect<void>, signalProcess?: HostSignalProcess, options?: {
|
|
52
|
+
readonly cleanupTimeoutMs?: number;
|
|
53
|
+
}): (() => void) | undefined;
|
|
24
54
|
export declare const brokerRegistry: {
|
|
25
55
|
acquire: <R>(factory: ProjectKeyFerryManagerFactory<R>, options: ProjectKeyFerryManagerOptions) => Effect.Effect<BrokerLease, any, never>;
|
|
26
56
|
retry: (factory: ProjectKeyFerryManagerFactory<any>, options: ProjectKeyFerryManagerOptions) => Effect.Effect<BrokerResult, any, never>;
|
|
57
|
+
retryIfDue: (factory: ProjectKeyFerryManagerFactory<any>, options: ProjectKeyFerryManagerOptions) => Effect.Effect<BrokerResult | undefined, any, never>;
|
|
58
|
+
shutdown: Effect.Effect<void, never, never>;
|
|
27
59
|
};
|
|
28
60
|
export type BrokerRegistry = ReturnType<typeof makeBrokerRegistry>;
|
|
29
61
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broker-registry.d.ts","sourceRoot":"","sources":["../src/broker-registry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"broker-registry.d.ts","sourceRoot":"","sources":["../src/broker-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/broker-registry.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer, PubSub, Scope, Semaphore, Stream, } from "effect";
|
|
3
5
|
import { ProjectKeyFerryManager, VarlockError, } from "./contracts.js";
|
|
6
|
+
import { setOwnershipLifecycleLogger } from "./varlock/ownership.js";
|
|
7
|
+
export const RETRY_BASE_DELAY_MS = 5_000;
|
|
8
|
+
export const RETRY_MAX_DELAY_MS = 300_000;
|
|
9
|
+
export const retryDelayMs = (failureCount) => Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** Math.max(0, failureCount - 1));
|
|
10
|
+
function retryPending(entry) {
|
|
11
|
+
return entry.attempt && entry.attemptPending ? { pending: entry.attempt } : undefined;
|
|
12
|
+
}
|
|
4
13
|
const adapterKeys = new Set([
|
|
5
14
|
"command",
|
|
6
15
|
"environmentKeys",
|
|
@@ -9,6 +18,7 @@ const adapterKeys = new Set([
|
|
|
9
18
|
"reloadTimeoutMs",
|
|
10
19
|
"pollIntervalMs",
|
|
11
20
|
"requireStrictEgress",
|
|
21
|
+
"idleTimeoutMs",
|
|
12
22
|
]);
|
|
13
23
|
function safeError(error, code) {
|
|
14
24
|
if (error instanceof VarlockError)
|
|
@@ -51,19 +61,70 @@ function validateOptions(options) {
|
|
|
51
61
|
validateAdapterShape(adapter);
|
|
52
62
|
return adapter;
|
|
53
63
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const schemaHashCache = new Map();
|
|
65
|
+
async function contentHash(directory) {
|
|
66
|
+
const hash = createHash("sha256");
|
|
67
|
+
try {
|
|
68
|
+
const names = (await readdir(directory))
|
|
69
|
+
.filter((name) => name === ".env.schema" || name.startsWith(".env.schema."))
|
|
70
|
+
.sort();
|
|
71
|
+
const metadata = await Promise.all(names.map(async (name) => {
|
|
72
|
+
const info = await stat(join(directory, name));
|
|
73
|
+
return `${name}:${info.mtimeMs}:${info.size}`;
|
|
74
|
+
}));
|
|
75
|
+
const signature = metadata.join("\0");
|
|
76
|
+
const cached = schemaHashCache.get(directory);
|
|
77
|
+
if (cached?.signature === signature)
|
|
78
|
+
return cached.hash;
|
|
79
|
+
for (const name of names) {
|
|
80
|
+
hash.update(name);
|
|
81
|
+
hash.update(await readFile(join(directory, name)));
|
|
82
|
+
}
|
|
83
|
+
const result = hash.digest("hex");
|
|
84
|
+
schemaHashCache.set(directory, { signature, hash: result });
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// The manager performs the authoritative directory validation. Keep the
|
|
89
|
+
// registry usable with injected/test canonicalizers and distinguish an
|
|
90
|
+
// unreadable schema from a real schema without exposing its contents.
|
|
91
|
+
hash.update("schema-unavailable");
|
|
92
|
+
}
|
|
93
|
+
return hash.digest("hex");
|
|
94
|
+
}
|
|
95
|
+
function keyFor(directory, options, schemaHash) {
|
|
96
|
+
return Effect.gen(function* () {
|
|
97
|
+
const adapter = validateOptions(options);
|
|
98
|
+
const keys = [
|
|
99
|
+
...new Set([
|
|
100
|
+
...adapter.environmentKeys,
|
|
101
|
+
...Object.keys(globalThis.process.env).filter((key) => /^OP_/.test(key)),
|
|
102
|
+
]),
|
|
103
|
+
].sort();
|
|
104
|
+
const valuesHash = createHash("sha256");
|
|
105
|
+
for (const key of keys)
|
|
106
|
+
valuesHash.update(`${key}\0${globalThis.process.env[key] ?? ""}\0`);
|
|
107
|
+
const context = {
|
|
108
|
+
projectDirectory: directory,
|
|
109
|
+
owner: options.owner,
|
|
110
|
+
sessionID: options.sessionID,
|
|
111
|
+
command: adapter.command,
|
|
112
|
+
environmentKeys: [...new Set(adapter.environmentKeys)].sort(),
|
|
113
|
+
startupTimeoutMs: adapter.startupTimeoutMs,
|
|
114
|
+
commandTimeoutMs: adapter.commandTimeoutMs,
|
|
115
|
+
reloadTimeoutMs: adapter.reloadTimeoutMs,
|
|
116
|
+
pollIntervalMs: adapter.pollIntervalMs,
|
|
117
|
+
requireStrictEgress: adapter.requireStrictEgress,
|
|
118
|
+
idleTimeoutMs: adapter.idleTimeoutMs,
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
contextKey: stable(context),
|
|
122
|
+
key: stable({
|
|
123
|
+
...context,
|
|
124
|
+
environmentValuesHash: valuesHash.digest("hex"),
|
|
125
|
+
schemaHash: yield* schemaHash(directory),
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
67
128
|
});
|
|
68
129
|
}
|
|
69
130
|
function readinessChanged(entry, revision, ready) {
|
|
@@ -96,31 +157,107 @@ function awaitCurrentReady(entry) {
|
|
|
96
157
|
export function makeBrokerRegistry(configuration = {}) {
|
|
97
158
|
const entries = new Set();
|
|
98
159
|
const factories = new WeakMap();
|
|
160
|
+
const factoryMaps = new Set();
|
|
99
161
|
const lock = Effect.runSync(Semaphore.make(1));
|
|
100
162
|
const canonicalize = configuration.canonicalize ??
|
|
101
163
|
((directory) => Effect.tryPromise(() => realpath(directory)));
|
|
164
|
+
const schemaHash = configuration.schemaHash ??
|
|
165
|
+
((directory) => Effect.tryPromise(() => contentHash(directory)));
|
|
102
166
|
const maxEntries = configuration.maxEntries ?? 128;
|
|
167
|
+
const now = configuration.now ?? (() => Date.now());
|
|
168
|
+
// Direct registry consumers historically received immediate teardown. The
|
|
169
|
+
// validated plugin adapter always supplies the public ten-minute default;
|
|
170
|
+
// retaining this fallback keeps injected/test registries backwards compatible.
|
|
171
|
+
const idleTimeoutMs = configuration.idleTimeoutMs ?? 0;
|
|
172
|
+
const log = (entry, event) => {
|
|
173
|
+
configuration.logger?.({
|
|
174
|
+
event,
|
|
175
|
+
scope: entry.options.owner === "key-ferry" ? "project" : "global",
|
|
176
|
+
directory: entry.options.projectDirectory,
|
|
177
|
+
keyHash: createHash("sha256").update(entry.key).digest("hex").slice(0, 12),
|
|
178
|
+
...(entry.status?.pid === undefined ? {} : { pid: entry.status.pid }),
|
|
179
|
+
...(entry.status
|
|
180
|
+
?.proxySessionId === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: {
|
|
183
|
+
proxySessionId: entry.status
|
|
184
|
+
.proxySessionId,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
};
|
|
103
188
|
const publish = (entry, status) => {
|
|
104
189
|
entry.status = status;
|
|
105
190
|
return PubSub.publish(entry.events, status);
|
|
106
191
|
};
|
|
192
|
+
const recordFailure = (entry, failure) => {
|
|
193
|
+
entry.failure = failure;
|
|
194
|
+
entry.failureCount += 1;
|
|
195
|
+
entry.lastFailureAt = new Date(now()).toISOString();
|
|
196
|
+
entry.nextRetryAt = new Date(now() + retryDelayMs(entry.failureCount)).toISOString();
|
|
197
|
+
return {
|
|
198
|
+
lastFailureAt: entry.lastFailureAt,
|
|
199
|
+
nextRetryAt: entry.nextRetryAt,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
const clearFailure = (entry) => {
|
|
203
|
+
entry.failure = undefined;
|
|
204
|
+
entry.failureCount = 0;
|
|
205
|
+
entry.lastFailureAt = undefined;
|
|
206
|
+
entry.nextRetryAt = undefined;
|
|
207
|
+
entry.retryBatch = undefined;
|
|
208
|
+
};
|
|
209
|
+
const capacityFailure = () => Effect.die(new Error("broker registry capacity reached"));
|
|
210
|
+
const disposeStaleEntries = (factoryMap, contextKey) => Effect.gen(function* () {
|
|
211
|
+
for (const [oldKey, oldEntry] of factoryMap) {
|
|
212
|
+
if (oldEntry.contextKey !== contextKey || oldEntry.leases.size > 0)
|
|
213
|
+
continue;
|
|
214
|
+
yield* disposeEntry(oldEntry);
|
|
215
|
+
entries.delete(oldEntry);
|
|
216
|
+
factoryMap.delete(oldKey);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const disposeEntry = (entry) => Effect.gen(function* () {
|
|
220
|
+
log(entry, "disposed");
|
|
221
|
+
if (entry.idleFiber) {
|
|
222
|
+
yield* Fiber.interrupt(entry.idleFiber);
|
|
223
|
+
entry.idleFiber = undefined;
|
|
224
|
+
}
|
|
225
|
+
if (entry.startup)
|
|
226
|
+
yield* Fiber.interrupt(entry.startup);
|
|
227
|
+
if (entry.manager) {
|
|
228
|
+
yield* entry.manager.dispose.pipe(Effect.catch(() => Effect.sync(() => {
|
|
229
|
+
// The manager may have left the ownership record in place. Drop
|
|
230
|
+
// the in-process entry, but make the startup reaper handoff
|
|
231
|
+
// explicit so cleanup is still observable without error text.
|
|
232
|
+
log(entry, "dispose-failed");
|
|
233
|
+
})));
|
|
234
|
+
}
|
|
235
|
+
yield* Scope.close(entry.scope, Exit.void).pipe(Effect.ignore);
|
|
236
|
+
entry.manager = undefined;
|
|
237
|
+
entry.disposed = true;
|
|
238
|
+
entry.idleUntil = undefined;
|
|
239
|
+
}).pipe(Effect.catch(() => Effect.void));
|
|
107
240
|
const start = (entry) => {
|
|
108
241
|
const operation = Effect.gen(function* () {
|
|
109
242
|
const context = yield* Layer.build(entry.factory.layer(entry.options)).pipe(Effect.provideService(Scope.Scope, entry.scope));
|
|
110
243
|
const manager = Context.get(context, ProjectKeyFerryManager);
|
|
111
244
|
entry.manager = manager;
|
|
245
|
+
entry.status = yield* manager.status;
|
|
246
|
+
log(entry, "start");
|
|
112
247
|
const result = yield* manager.ensureReady;
|
|
248
|
+
log(entry, "ready");
|
|
113
249
|
yield* publish(entry, result.status);
|
|
114
250
|
return result;
|
|
115
251
|
});
|
|
116
252
|
return operation.pipe(Effect.catch((error) => {
|
|
117
253
|
const failure = safeError(error, "start_failed");
|
|
118
|
-
|
|
254
|
+
const metadata = recordFailure(entry, failure);
|
|
119
255
|
return publish(entry, {
|
|
120
256
|
phase: "failed",
|
|
121
257
|
projectDirectory: entry.options.projectDirectory,
|
|
122
258
|
generation: entry.status?.generation ?? 0,
|
|
123
259
|
error: failure,
|
|
260
|
+
...metadata,
|
|
124
261
|
}).pipe(Effect.flatMap(() => Effect.fail(failure)));
|
|
125
262
|
}));
|
|
126
263
|
};
|
|
@@ -133,7 +270,9 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
133
270
|
}
|
|
134
271
|
return yield* awaitCurrentReady(entry);
|
|
135
272
|
});
|
|
136
|
-
const completeStartup = (entry, target) => Fiber.join(entry.startup).pipe(Effect.flatMap((result) =>
|
|
273
|
+
const completeStartup = (entry, target) => Fiber.join(entry.startup).pipe(Effect.flatMap((result) => Effect.sync(() => clearFailure(entry)).pipe(Effect.andThen(Deferred.succeed(target, result)))), Effect.catch((error) => Effect.sync(() => {
|
|
274
|
+
entry.attemptPending = false;
|
|
275
|
+
}).pipe(Effect.andThen(Deferred.fail(target, safeError(error, "start_failed"))))), Effect.tap(() => Effect.sync(() => {
|
|
137
276
|
entry.attemptPending = false;
|
|
138
277
|
})), Effect.forkDetach);
|
|
139
278
|
const refreshShared = (entry) => Effect.gen(function* () {
|
|
@@ -159,13 +298,14 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
159
298
|
entry.refresh = undefined;
|
|
160
299
|
}).pipe(Effect.catch((error) => {
|
|
161
300
|
const failure = safeError(error, "refresh_failed");
|
|
162
|
-
|
|
301
|
+
const metadata = recordFailure(entry, failure);
|
|
163
302
|
entry.refresh = undefined;
|
|
164
303
|
return publish(entry, {
|
|
165
304
|
phase: "failed",
|
|
166
305
|
projectDirectory: entry.options.projectDirectory,
|
|
167
306
|
generation: entry.status?.generation ?? 0,
|
|
168
307
|
error: failure,
|
|
308
|
+
...metadata,
|
|
169
309
|
}).pipe(Effect.flatMap(() => Deferred.fail(next, failure)));
|
|
170
310
|
}), Effect.forkIn(entry.scope));
|
|
171
311
|
return next;
|
|
@@ -173,14 +313,16 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
173
313
|
return yield* Deferred.await(deferred);
|
|
174
314
|
});
|
|
175
315
|
const getOrCreateEntry = (factory, options, directory) => Effect.gen(function* () {
|
|
176
|
-
const
|
|
316
|
+
const identity = yield* keyFor(directory, options, schemaHash).pipe(Effect.orDie);
|
|
317
|
+
const key = identity.key;
|
|
177
318
|
const factoryMap = factories.get(factory) ?? new Map();
|
|
178
319
|
factories.set(factory, factoryMap);
|
|
320
|
+
factoryMaps.add(factoryMap);
|
|
179
321
|
const existing = factoryMap.get(key);
|
|
180
322
|
if (existing)
|
|
181
323
|
return existing;
|
|
182
324
|
if (entries.size >= maxEntries)
|
|
183
|
-
return yield*
|
|
325
|
+
return yield* capacityFailure();
|
|
184
326
|
const entry = {
|
|
185
327
|
key,
|
|
186
328
|
options: {
|
|
@@ -194,7 +336,10 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
194
336
|
leases: new Set(),
|
|
195
337
|
readinessRevision: 0,
|
|
196
338
|
disposed: false,
|
|
339
|
+
failureCount: 0,
|
|
340
|
+
contextKey: identity.contextKey,
|
|
197
341
|
};
|
|
342
|
+
yield* disposeStaleEntries(factoryMap, identity.contextKey);
|
|
198
343
|
factoryMap.set(key, entry);
|
|
199
344
|
entries.add(entry);
|
|
200
345
|
return entry;
|
|
@@ -202,9 +347,7 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
202
347
|
const disposeLeaseEntry = (entry, factoryMap, close) => Effect.gen(function* () {
|
|
203
348
|
if (entry.startup)
|
|
204
349
|
yield* Fiber.interrupt(entry.startup);
|
|
205
|
-
|
|
206
|
-
yield* entry.manager.dispose;
|
|
207
|
-
yield* Scope.close(entry.scope, Exit.void);
|
|
350
|
+
yield* disposeEntry(entry);
|
|
208
351
|
}).pipe(Effect.catch(() => Effect.void), Effect.flatMap(() => lock.withPermit(Effect.gen(function* () {
|
|
209
352
|
if (entry.closing !== close)
|
|
210
353
|
return;
|
|
@@ -217,13 +360,71 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
217
360
|
entry.closing = undefined;
|
|
218
361
|
yield* Deferred.succeed(close, undefined);
|
|
219
362
|
}))));
|
|
363
|
+
const scheduleIdle = (entry, factoryMap, directory, timeout) => Effect.gen(function* () {
|
|
364
|
+
entry.idleUntil = new Date(now() + timeout).toISOString();
|
|
365
|
+
log(entry, "idle-scheduled");
|
|
366
|
+
yield* publish(entry, {
|
|
367
|
+
...(entry.status ?? { phase: "idle", projectDirectory: directory, generation: 0 }),
|
|
368
|
+
phase: "idle",
|
|
369
|
+
idleUntil: entry.idleUntil,
|
|
370
|
+
});
|
|
371
|
+
entry.idleFiber = yield* Effect.sleep(Duration.millis(timeout)).pipe(Effect.andThen(Effect.gen(function* () {
|
|
372
|
+
if (entry.leases.size === 0 && !entry.disposed) {
|
|
373
|
+
entry.disposed = true;
|
|
374
|
+
const deferred = yield* Deferred.make();
|
|
375
|
+
entry.closing = deferred;
|
|
376
|
+
yield* disposeLeaseEntry(entry, factoryMap, deferred).pipe(Effect.forkDetach);
|
|
377
|
+
}
|
|
378
|
+
})), Effect.forkIn(entry.scope));
|
|
379
|
+
});
|
|
380
|
+
const closeImmediately = (entry, factoryMap, required) => Effect.gen(function* () {
|
|
381
|
+
if (!required)
|
|
382
|
+
return undefined;
|
|
383
|
+
entry.disposed = true;
|
|
384
|
+
const deferred = yield* Deferred.make();
|
|
385
|
+
entry.closing = deferred;
|
|
386
|
+
yield* disposeLeaseEntry(entry, factoryMap, deferred).pipe(Effect.forkDetach);
|
|
387
|
+
return deferred;
|
|
388
|
+
});
|
|
389
|
+
const releaseLease = (entry, factoryMap, leaseID, directory, isReleased, markReleased) => lock.withPermit(Effect.gen(function* () {
|
|
390
|
+
if (isReleased())
|
|
391
|
+
return undefined;
|
|
392
|
+
markReleased();
|
|
393
|
+
log(entry, "release");
|
|
394
|
+
entry.leases.delete(leaseID);
|
|
395
|
+
return yield* releaseAfterDelete(entry, factoryMap, directory);
|
|
396
|
+
}));
|
|
397
|
+
const releaseAfterDelete = (entry, factoryMap, directory) => entry.leases.size !== 0 || entry.disposed
|
|
398
|
+
? Effect.succeed(undefined)
|
|
399
|
+
: releaseLastLease(entry, factoryMap, directory);
|
|
400
|
+
const releaseLastLease = (entry, factoryMap, directory) => Effect.gen(function* () {
|
|
401
|
+
const timeout = Number(entry.options.adapter.idleTimeoutMs ?? idleTimeoutMs);
|
|
402
|
+
const immediate = yield* closeImmediately(entry, factoryMap, entry.failure !== undefined || timeout <= 0);
|
|
403
|
+
if (immediate)
|
|
404
|
+
return immediate;
|
|
405
|
+
yield* scheduleIdle(entry, factoryMap, directory, timeout);
|
|
406
|
+
return undefined;
|
|
407
|
+
});
|
|
408
|
+
const reuseIdle = (entry) => Effect.gen(function* () {
|
|
409
|
+
if (!entry.idleFiber)
|
|
410
|
+
return;
|
|
411
|
+
yield* Fiber.interrupt(entry.idleFiber);
|
|
412
|
+
entry.idleFiber = undefined;
|
|
413
|
+
entry.idleUntil = undefined;
|
|
414
|
+
entry.disposed = false;
|
|
415
|
+
if (entry.status?.phase === "idle")
|
|
416
|
+
yield* publish(entry, { ...entry.status, phase: "ready", idleUntil: undefined });
|
|
417
|
+
log(entry, "reused");
|
|
418
|
+
});
|
|
220
419
|
const acquireAttempt = (factory, options) => lock.withPermit(Effect.gen(function* () {
|
|
221
420
|
const directory = yield* canonicalize(options.projectDirectory).pipe(Effect.mapError(() => new TypeError("project directory does not exist")));
|
|
222
421
|
const factoryMap = factories.get(factory) ?? new Map();
|
|
223
422
|
factories.set(factory, factoryMap);
|
|
423
|
+
factoryMaps.add(factoryMap);
|
|
224
424
|
const entry = yield* getOrCreateEntry(factory, options, directory);
|
|
225
425
|
if (entry.closing)
|
|
226
426
|
return { waiting: entry.closing };
|
|
427
|
+
yield* reuseIdle(entry);
|
|
227
428
|
const leaseID = Symbol();
|
|
228
429
|
entry.leases.add(leaseID);
|
|
229
430
|
if (!entry.startup && !entry.failure) {
|
|
@@ -239,19 +440,7 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
239
440
|
return yield* operation(entry.manager);
|
|
240
441
|
});
|
|
241
442
|
const release = Effect.gen(function* () {
|
|
242
|
-
const close = yield*
|
|
243
|
-
if (released)
|
|
244
|
-
return undefined;
|
|
245
|
-
released = true;
|
|
246
|
-
entry.leases.delete(leaseID);
|
|
247
|
-
if (entry.leases.size !== 0 || entry.disposed)
|
|
248
|
-
return undefined;
|
|
249
|
-
entry.disposed = true;
|
|
250
|
-
const deferred = yield* Deferred.make();
|
|
251
|
-
entry.closing = deferred;
|
|
252
|
-
yield* disposeLeaseEntry(entry, factoryMap, deferred).pipe(Effect.forkDetach);
|
|
253
|
-
return deferred;
|
|
254
|
-
}));
|
|
443
|
+
const close = yield* releaseLease(entry, factoryMap, leaseID, directory, () => released, () => (released = true));
|
|
255
444
|
if (close)
|
|
256
445
|
yield* Deferred.await(close);
|
|
257
446
|
});
|
|
@@ -296,10 +485,30 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
296
485
|
const retryAttempt = (factory, options) => Effect.gen(function* () {
|
|
297
486
|
const directory = yield* canonicalize(options.projectDirectory);
|
|
298
487
|
const target = yield* lock.withPermit(Effect.gen(function* () {
|
|
299
|
-
const entry = factories
|
|
488
|
+
const entry = factories
|
|
489
|
+
.get(factory)
|
|
490
|
+
?.get((yield* keyFor(directory, options, schemaHash)).key);
|
|
491
|
+
if (entry?.retryBatch)
|
|
492
|
+
return entry.retryBatch;
|
|
493
|
+
if (entry) {
|
|
494
|
+
const initial = yield* retryEntry(entry);
|
|
495
|
+
if ("waiting" in initial || "pending" in initial)
|
|
496
|
+
return initial;
|
|
497
|
+
const batch = yield* Deferred.make();
|
|
498
|
+
entry.retryBatch = batch;
|
|
499
|
+
yield* Deferred.await(initial).pipe(Effect.matchEffect({
|
|
500
|
+
onSuccess: (result) => Deferred.succeed(batch, result),
|
|
501
|
+
onFailure: (error) => Deferred.fail(batch, error),
|
|
502
|
+
}),
|
|
503
|
+
// Keep the completed batch as the result of this explicit
|
|
504
|
+
// retry. A background retry clears it when its deadline is due
|
|
505
|
+
// and atomically establishes the next batch.
|
|
506
|
+
Effect.andThen(Effect.void), Effect.forkDetach);
|
|
507
|
+
return batch;
|
|
508
|
+
}
|
|
300
509
|
return yield* retryEntry(entry);
|
|
301
510
|
}));
|
|
302
|
-
if ("waiting" in target)
|
|
511
|
+
if ("waiting" in target || "pending" in target)
|
|
303
512
|
return target;
|
|
304
513
|
return yield* Deferred.await(target);
|
|
305
514
|
});
|
|
@@ -309,6 +518,12 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
309
518
|
yield* Deferred.await(result.waiting);
|
|
310
519
|
return yield* retry(factory, options);
|
|
311
520
|
}
|
|
521
|
+
if ("pending" in result) {
|
|
522
|
+
// This caller observed an attempt already in flight. Its result is the
|
|
523
|
+
// result of this retry operation; do not turn a shared failure into a
|
|
524
|
+
// fresh attempt when the pending attempt completes.
|
|
525
|
+
return yield* Deferred.await(result.pending);
|
|
526
|
+
}
|
|
312
527
|
return result;
|
|
313
528
|
});
|
|
314
529
|
const retryEntry = (entry) => {
|
|
@@ -320,8 +535,9 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
320
535
|
}));
|
|
321
536
|
if (entry.closing)
|
|
322
537
|
return Effect.succeed({ waiting: entry.closing });
|
|
323
|
-
|
|
324
|
-
|
|
538
|
+
const pending = retryPending(entry);
|
|
539
|
+
if (pending)
|
|
540
|
+
return Effect.succeed(pending);
|
|
325
541
|
if (!entry.failure)
|
|
326
542
|
return Effect.fail(new VarlockError({
|
|
327
543
|
code: "not_ready",
|
|
@@ -329,6 +545,10 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
329
545
|
retryable: false,
|
|
330
546
|
}));
|
|
331
547
|
return Effect.gen(function* () {
|
|
548
|
+
// The previous attempt is complete (attemptPending was checked above),
|
|
549
|
+
// so closing its scope cannot race a startup. This runs manager and
|
|
550
|
+
// certificate-directory finalizers before allocating the replacement.
|
|
551
|
+
yield* Scope.close(entry.scope, Exit.void).pipe(Effect.ignore);
|
|
332
552
|
entry.failure = undefined;
|
|
333
553
|
entry.disposed = false;
|
|
334
554
|
entry.scope = yield* Scope.make();
|
|
@@ -341,7 +561,121 @@ export function makeBrokerRegistry(configuration = {}) {
|
|
|
341
561
|
return entry.ready;
|
|
342
562
|
});
|
|
343
563
|
};
|
|
344
|
-
|
|
564
|
+
const retryIfDue = (factory, options) => Effect.gen(function* () {
|
|
565
|
+
const directory = yield* canonicalize(options.projectDirectory);
|
|
566
|
+
const entry = factories
|
|
567
|
+
.get(factory)
|
|
568
|
+
?.get((yield* keyFor(directory, options, schemaHash)).key);
|
|
569
|
+
if (!entry?.failure || (entry.nextRetryAt && now() < Date.parse(entry.nextRetryAt)))
|
|
570
|
+
return undefined;
|
|
571
|
+
entry.retryBatch = undefined;
|
|
572
|
+
return yield* retry(factory, options).pipe(Effect.catch((error) => error instanceof VarlockError && error.code === "not_ready"
|
|
573
|
+
? Effect.succeed(undefined)
|
|
574
|
+
: Effect.fail(error)));
|
|
575
|
+
});
|
|
576
|
+
let unregisterOwnershipLogger = () => undefined;
|
|
577
|
+
const shutdown = Effect.uninterruptible(Effect.gen(function* () {
|
|
578
|
+
removeHooksWhenConfigured(configuration.hostShutdownHooks);
|
|
579
|
+
const current = [...entries];
|
|
580
|
+
yield* Effect.forEach(current, (entry) => disposeEntry(entry), { concurrency: "unbounded" });
|
|
581
|
+
for (const map of factoryMaps)
|
|
582
|
+
map.clear();
|
|
583
|
+
entries.clear();
|
|
584
|
+
unregisterOwnershipLogger();
|
|
585
|
+
}));
|
|
586
|
+
installHooksWhenConfigured(configuration.hostShutdownHooks, shutdown);
|
|
587
|
+
unregisterOwnershipLogger = setOwnershipLifecycleLogger((event) => configuration.logger?.({
|
|
588
|
+
...event,
|
|
589
|
+
scope: "project",
|
|
590
|
+
directory: "",
|
|
591
|
+
keyHash: event.keyHash,
|
|
592
|
+
}));
|
|
593
|
+
return { acquire, retry, retryIfDue, shutdown };
|
|
594
|
+
}
|
|
595
|
+
let hostHooks;
|
|
596
|
+
let hostShutdown;
|
|
597
|
+
function removeHooksWhenConfigured(enabled) {
|
|
598
|
+
if (enabled)
|
|
599
|
+
removeHostShutdownHooks();
|
|
600
|
+
}
|
|
601
|
+
function installHooksWhenConfigured(enabled, shutdown) {
|
|
602
|
+
if (enabled)
|
|
603
|
+
installHostShutdownHooks(shutdown);
|
|
604
|
+
}
|
|
605
|
+
function signalListener(name, onSigInt, onSigTerm) {
|
|
606
|
+
return name === "SIGINT" ? onSigInt : onSigTerm;
|
|
607
|
+
}
|
|
608
|
+
function reraiseSignal(signalProcess, name, listener) {
|
|
609
|
+
signalProcess.removeListener(name, listener);
|
|
610
|
+
signalProcess.kill(signalProcess.pid, name);
|
|
611
|
+
}
|
|
612
|
+
function finishSignalCleanup(signalProcess, name, listener, active) {
|
|
613
|
+
if (!active && signalProcess.listeners(name).length > 0)
|
|
614
|
+
return;
|
|
615
|
+
const others = signalProcess.listeners(name).filter((item) => item !== listener);
|
|
616
|
+
signalProcess.removeListener(name, listener);
|
|
617
|
+
if (others.length === 0)
|
|
618
|
+
signalProcess.kill(signalProcess.pid, name);
|
|
619
|
+
}
|
|
620
|
+
export function removeHostShutdownHooks() {
|
|
621
|
+
hostHooks?.();
|
|
622
|
+
hostHooks = undefined;
|
|
623
|
+
hostShutdown = undefined;
|
|
624
|
+
}
|
|
625
|
+
export function installHostShutdownHooks(shutdown, signalProcess = process, options = {}) {
|
|
626
|
+
if (hostHooks)
|
|
627
|
+
return;
|
|
628
|
+
hostShutdown = shutdown;
|
|
629
|
+
let active = true;
|
|
630
|
+
const beforeExit = () => {
|
|
631
|
+
if (active)
|
|
632
|
+
void Effect.runPromise(hostShutdown).catch(() => undefined);
|
|
633
|
+
};
|
|
634
|
+
let cleaning = false;
|
|
635
|
+
// Node and Bun suppress their default signal termination while a signal
|
|
636
|
+
// listener is installed. Cleanup is bounded; `exit` cannot await work and
|
|
637
|
+
// is covered by the startup ownership reaper instead.
|
|
638
|
+
const boundedTimer = (resolve) => {
|
|
639
|
+
const timer = setTimeout(resolve, options.cleanupTimeoutMs ?? 3_000);
|
|
640
|
+
timer.unref?.();
|
|
641
|
+
return timer;
|
|
642
|
+
};
|
|
643
|
+
const signal = (name) => {
|
|
644
|
+
if (!active)
|
|
645
|
+
return;
|
|
646
|
+
const listener = signalListener(name, onSigInt, onSigTerm);
|
|
647
|
+
if (cleaning) {
|
|
648
|
+
reraiseSignal(signalProcess, name, listener);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
cleaning = true;
|
|
652
|
+
let timer;
|
|
653
|
+
const timeout = new Promise((resolve) => {
|
|
654
|
+
timer = boundedTimer(resolve);
|
|
655
|
+
});
|
|
656
|
+
void Promise.race([Effect.runPromise(hostShutdown).catch(() => undefined), timeout]).finally(() => {
|
|
657
|
+
if (timer !== undefined)
|
|
658
|
+
clearTimeout(timer);
|
|
659
|
+
// shutdown() removes the hooks at its start, so do not let that normal
|
|
660
|
+
// cleanup path suppress the signal re-raise requested by this handler.
|
|
661
|
+
finishSignalCleanup(signalProcess, name, listener, active);
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
const onSigInt = () => signal("SIGINT");
|
|
665
|
+
const onSigTerm = () => signal("SIGTERM");
|
|
666
|
+
signalProcess.on("beforeExit", beforeExit);
|
|
667
|
+
signalProcess.on("SIGINT", onSigInt);
|
|
668
|
+
signalProcess.on("SIGTERM", onSigTerm);
|
|
669
|
+
hostHooks = () => {
|
|
670
|
+
active = false;
|
|
671
|
+
signalProcess.removeListener("beforeExit", beforeExit);
|
|
672
|
+
signalProcess.removeListener("SIGINT", onSigInt);
|
|
673
|
+
signalProcess.removeListener("SIGTERM", onSigTerm);
|
|
674
|
+
};
|
|
675
|
+
return hostHooks;
|
|
345
676
|
}
|
|
346
|
-
export const brokerRegistry = makeBrokerRegistry(
|
|
677
|
+
export const brokerRegistry = makeBrokerRegistry({
|
|
678
|
+
idleTimeoutMs: 600_000,
|
|
679
|
+
hostShutdownHooks: true,
|
|
680
|
+
});
|
|
347
681
|
//# sourceMappingURL=broker-registry.js.map
|