Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- packages/agent-core-v2/src/_base/contribution/registry.ts +55 -0
- packages/agent-core-v2/src/_base/di/cascadeEngine.ts +798 -0
- packages/agent-core-v2/src/_base/di/collection.ts +302 -0
- packages/agent-core-v2/src/_base/di/dependencyGraph.ts +262 -0
- packages/agent-core-v2/src/_base/di/descriptors.ts +14 -0
- packages/agent-core-v2/src/_base/di/errors.ts +30 -0
- packages/agent-core-v2/src/_base/di/fiber.ts +714 -0
- packages/agent-core-v2/src/_base/di/graph.ts +89 -0
- packages/agent-core-v2/src/_base/di/instantiation.ts +212 -0
- packages/agent-core-v2/src/_base/di/instantiationService.ts +1006 -0
- packages/agent-core-v2/src/_base/di/lifecycle.ts +709 -0
- packages/agent-core-v2/src/_base/di/scope.ts +300 -0
- packages/agent-core-v2/src/_base/di/scopeUnits.ts +110 -0
- packages/agent-core-v2/src/_base/di/service.ts +138 -0
- packages/agent-core-v2/src/_base/di/serviceCollection.ts +144 -0
- packages/agent-core-v2/src/_base/di/test.ts +53 -0
- packages/agent-core-v2/src/_base/di/testInstantiationService.ts +348 -0
- packages/agent-core-v2/src/_base/di/util/linkedList.ts +84 -0
- packages/agent-core-v2/src/_base/errors/codes.ts +70 -0
- packages/agent-core-v2/src/_base/errors/errorMessage.ts +23 -0
- packages/agent-core-v2/src/_base/errors/errors.ts +70 -0
- packages/agent-core-v2/src/_base/errors/serialize.ts +97 -0
- packages/agent-core-v2/src/_base/errors/unexpectedError.ts +31 -0
- packages/agent-core-v2/src/_base/execEnv/bufferedReadable.ts +52 -0
- packages/agent-core-v2/src/_base/execEnv/decodeText.ts +185 -0
- packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts +322 -0
- packages/agent-core-v2/src/_base/execEnv/globPattern.ts +46 -0
- packages/agent-core-v2/src/_base/execEnv/loginShellPath.ts +80 -0
- packages/agent-core-v2/src/_base/execEnv/shellPathBridge.ts +150 -0
- packages/agent-core-v2/src/_base/lifecycle/disposer.ts +37 -0
- packages/agent-core-v2/src/_base/lifecycle/errors.ts +12 -0
- packages/agent-core-v2/src/_base/lifecycle/index.ts +4 -0
- packages/agent-core-v2/src/_base/lifecycle/keyedResource.ts +150 -0
- packages/agent-core-v2/src/_base/lifecycle/ledger.ts +317 -0
- packages/agent-core-v2/src/_base/lifecycle/lifecycleMachine.ts +235 -0
- packages/agent-core-v2/src/_base/log/fileLog.ts +285 -0
- packages/agent-core-v2/src/_base/log/formatter.ts +196 -0
- packages/agent-core-v2/src/_base/log/log.ts +59 -0
- packages/agent-core-v2/src/_base/log/logConfig.ts +72 -0
- packages/agent-core-v2/src/_base/log/logService.ts +181 -0
- packages/agent-core-v2/src/_base/state/stateRegistry.ts +163 -0
- packages/agent-core-v2/src/_base/text/encoding.ts +113 -0
- packages/agent-core-v2/src/_base/text/frontmatter.ts +43 -0
- packages/agent-core-v2/src/_base/text/line-endings.ts +67 -0
- packages/agent-core-v2/src/_base/utils/abort.ts +93 -0
- packages/agent-core-v2/src/_base/utils/canonical-args.ts +24 -0
- packages/agent-core-v2/src/_base/utils/env.ts +10 -0
- packages/agent-core-v2/src/_base/utils/fileMeta.ts +113 -0
- packages/agent-core-v2/src/_base/utils/fs.ts +168 -0
- packages/agent-core-v2/src/_base/utils/hero-slug.ts +263 -0
packages/agent-core-v2/src/_base/contribution/registry.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Disposable, type IDisposable } from '../di/lifecycle';
|
| 2 |
+
import { Emitter, type Event } from '../event';
|
| 3 |
+
|
| 4 |
+
export interface ContributionRegistration<T> {
|
| 5 |
+
readonly sourceId: string;
|
| 6 |
+
readonly priority: number;
|
| 7 |
+
readonly contribution: T;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export interface RegisterContributionOptions {
|
| 11 |
+
readonly priority?: number;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export class ContributionRegistry<T> extends Disposable {
|
| 15 |
+
private readonly registrations = new Map<string, ContributionRegistration<T>>();
|
| 16 |
+
private readonly onDidChangeEmitter = this._register(new Emitter<string>());
|
| 17 |
+
readonly onDidChange: Event<string> = this.onDidChangeEmitter.event;
|
| 18 |
+
|
| 19 |
+
register(
|
| 20 |
+
sourceId: string,
|
| 21 |
+
contribution: T,
|
| 22 |
+
options?: RegisterContributionOptions,
|
| 23 |
+
): IDisposable {
|
| 24 |
+
const registration: ContributionRegistration<T> = {
|
| 25 |
+
sourceId,
|
| 26 |
+
priority: options?.priority ?? 0,
|
| 27 |
+
contribution,
|
| 28 |
+
};
|
| 29 |
+
this.registrations.set(sourceId, registration);
|
| 30 |
+
this.onDidChangeEmitter.fire(sourceId);
|
| 31 |
+
let active = true;
|
| 32 |
+
return {
|
| 33 |
+
dispose: () => {
|
| 34 |
+
if (!active) return;
|
| 35 |
+
active = false;
|
| 36 |
+
if (this.registrations.get(sourceId) !== registration) return;
|
| 37 |
+
this.registrations.delete(sourceId);
|
| 38 |
+
this.onDidChangeEmitter.fire(sourceId);
|
| 39 |
+
},
|
| 40 |
+
};
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
unregister(sourceId: string): void {
|
| 44 |
+
if (!this.registrations.delete(sourceId)) return;
|
| 45 |
+
this.onDidChangeEmitter.fire(sourceId);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
entries(): readonly ContributionRegistration<T>[] {
|
| 49 |
+
return [...this.registrations.values()];
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
get(sourceId: string): ContributionRegistration<T> | undefined {
|
| 53 |
+
return this.registrations.get(sourceId);
|
| 54 |
+
}
|
| 55 |
+
}
|
packages/agent-core-v2/src/_base/di/cascadeEngine.ts
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { onUnexpectedError } from '../errors/unexpectedError';
|
| 2 |
+
import { Emitter, type Event } from '../event';
|
| 3 |
+
import { isPromiseLike } from '../lifecycle/disposer';
|
| 4 |
+
import type { SyncDescriptor } from './descriptors';
|
| 5 |
+
import {
|
| 6 |
+
PairIndex,
|
| 7 |
+
type DependencyGraph,
|
| 8 |
+
type ScopedToken,
|
| 9 |
+
} from './dependencyGraph';
|
| 10 |
+
import { CascadeConflictError } from './errors';
|
| 11 |
+
import type { ServiceIdentifier } from './instantiation';
|
| 12 |
+
|
| 13 |
+
export type UnitState = 'Pending' | 'Activating' | 'Active' | 'Unloading' | 'Failed';
|
| 14 |
+
|
| 15 |
+
export type CascadeAction = 'provide' | 'unprovide' | 'update';
|
| 16 |
+
|
| 17 |
+
export type UnitActivation = 'eager' | 'ondemand';
|
| 18 |
+
|
| 19 |
+
export interface CascadeChange {
|
| 20 |
+
readonly action: CascadeAction;
|
| 21 |
+
readonly token: ServiceIdentifier<any>;
|
| 22 |
+
readonly descriptor?: SyncDescriptor<unknown>;
|
| 23 |
+
readonly instance?: unknown;
|
| 24 |
+
readonly activation?: UnitActivation;
|
| 25 |
+
readonly config?: unknown;
|
| 26 |
+
readonly reason: string;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface CascadeHistoryEntry {
|
| 30 |
+
readonly seq: number;
|
| 31 |
+
readonly reason: string;
|
| 32 |
+
readonly changes: ReadonlyArray<{ token: string; action: CascadeAction }>;
|
| 33 |
+
readonly affected: readonly string[];
|
| 34 |
+
readonly tornDown: readonly string[];
|
| 35 |
+
readonly rebuilt: readonly string[];
|
| 36 |
+
readonly failed: readonly string[];
|
| 37 |
+
readonly abortWaited: boolean;
|
| 38 |
+
readonly abortTimedOut: boolean;
|
| 39 |
+
readonly durationMs: number;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export interface UnitSnapshot {
|
| 43 |
+
readonly token: string;
|
| 44 |
+
readonly state: UnitState;
|
| 45 |
+
readonly error?: string;
|
| 46 |
+
readonly everActive: boolean;
|
| 47 |
+
readonly inFlight: boolean;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export interface UnitStateChange {
|
| 51 |
+
readonly token: string;
|
| 52 |
+
readonly state: UnitState;
|
| 53 |
+
readonly error?: string;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export interface CascadeEngineOptions {
|
| 57 |
+
onWillCascade?: (
|
| 58 |
+
affected: readonly ScopedToken[],
|
| 59 |
+
reason: string,
|
| 60 |
+
) => void | Promise<void>;
|
| 61 |
+
readonly abortWaitMs?: number;
|
| 62 |
+
readonly resolveTimeoutMs?: number;
|
| 63 |
+
readonly historyCapacity?: number;
|
| 64 |
+
readonly now?: () => number;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface CascadeHost {
|
| 68 |
+
isRegistered(token: ServiceIdentifier<any>): boolean;
|
| 69 |
+
ownerScopeOf(token: ServiceIdentifier<any>): object | undefined;
|
| 70 |
+
isMaterialized(token: ServiceIdentifier<any>): boolean;
|
| 71 |
+
materialize(token: ServiceIdentifier<any>): unknown;
|
| 72 |
+
retire(token: ServiceIdentifier<any>): void | Promise<void>;
|
| 73 |
+
applyProvide(
|
| 74 |
+
token: ServiceIdentifier<any>,
|
| 75 |
+
descriptor: SyncDescriptor<unknown>,
|
| 76 |
+
config: unknown,
|
| 77 |
+
): number;
|
| 78 |
+
applyProvideInstance(
|
| 79 |
+
token: ServiceIdentifier<any>,
|
| 80 |
+
instance: unknown,
|
| 81 |
+
config: unknown,
|
| 82 |
+
): number;
|
| 83 |
+
applyUnprovide(token: ServiceIdentifier<any>): void;
|
| 84 |
+
recipeOf(token: ServiceIdentifier<any>): SyncDescriptor<unknown> | undefined;
|
| 85 |
+
dependenciesOf(
|
| 86 |
+
recipe: SyncDescriptor<unknown>,
|
| 87 |
+
): Array<ServiceIdentifier<any>>;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export interface CascadeScopeHandle {
|
| 91 |
+
readonly cascade: CascadeEngine;
|
| 92 |
+
readonly cascadeDisposed: boolean;
|
| 93 |
+
readonly cascadeDepth: number;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
interface UnitRecord {
|
| 97 |
+
state: UnitState;
|
| 98 |
+
error?: unknown;
|
| 99 |
+
activation: UnitActivation;
|
| 100 |
+
everActive: boolean;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
interface QueuedRequest {
|
| 104 |
+
readonly engine: CascadeEngine;
|
| 105 |
+
readonly change: CascadeChange;
|
| 106 |
+
readonly resolve: () => void;
|
| 107 |
+
readonly reject: (error: unknown) => void;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
const DEFAULT_ABORT_WAIT_MS = 5000;
|
| 111 |
+
const DEFAULT_RESOLVE_TIMEOUT_MS = 30000;
|
| 112 |
+
const DEFAULT_HISTORY_CAPACITY = 200;
|
| 113 |
+
|
| 114 |
+
export class CascadeTree {
|
| 115 |
+
readonly graph: DependencyGraph;
|
| 116 |
+
readonly queue: QueuedRequest[] = [];
|
| 117 |
+
readonly engines = new Set<CascadeEngine>();
|
| 118 |
+
running = false;
|
| 119 |
+
orchestrator: object | undefined;
|
| 120 |
+
private readonly _inFlight = new PairIndex<true>();
|
| 121 |
+
private _settleWaiters: Array<() => void> = [];
|
| 122 |
+
private readonly _scopeSeq = new Map<object, number>();
|
| 123 |
+
private _nextScopeSeq = 0;
|
| 124 |
+
private readonly _droppedScopes = new WeakSet<object>();
|
| 125 |
+
private readonly _onDidAddEngine = new Emitter<CascadeEngine>();
|
| 126 |
+
readonly onDidAddEngine: Event<CascadeEngine> = this._onDidAddEngine.event;
|
| 127 |
+
private readonly _onDidRemoveEngine = new Emitter<CascadeEngine>();
|
| 128 |
+
readonly onDidRemoveEngine: Event<CascadeEngine> = this._onDidRemoveEngine.event;
|
| 129 |
+
|
| 130 |
+
constructor(graph: DependencyGraph) {
|
| 131 |
+
this.graph = graph;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
addEngine(engine: CascadeEngine): void {
|
| 135 |
+
this.engines.add(engine);
|
| 136 |
+
this._onDidAddEngine.fire(engine);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
removeEngine(engine: CascadeEngine): void {
|
| 140 |
+
if (this.engines.delete(engine)) {
|
| 141 |
+
this._onDidRemoveEngine.fire(engine);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
inFlightSet(ref: ScopedToken, on: boolean): void {
|
| 146 |
+
if (on) {
|
| 147 |
+
this._inFlight.set(ref.scope, ref.token, true);
|
| 148 |
+
} else {
|
| 149 |
+
this._inFlight.delete(ref.scope, ref.token);
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
inFlightHas(ref: ScopedToken): boolean {
|
| 154 |
+
return this._inFlight.get(ref.scope, ref.token) !== undefined;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
inFlightClear(refs: Iterable<ScopedToken>): void {
|
| 158 |
+
for (const ref of refs) {
|
| 159 |
+
this._inFlight.delete(ref.scope, ref.token);
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
seqOf(scope: object): number {
|
| 164 |
+
let seq = this._scopeSeq.get(scope);
|
| 165 |
+
if (seq === undefined) {
|
| 166 |
+
if (this._droppedScopes.has(scope)) {
|
| 167 |
+
return -1;
|
| 168 |
+
}
|
| 169 |
+
seq = this._nextScopeSeq++;
|
| 170 |
+
this._scopeSeq.set(scope, seq);
|
| 171 |
+
}
|
| 172 |
+
return seq;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
dropScope(scope: object): void {
|
| 176 |
+
this._droppedScopes.add(scope);
|
| 177 |
+
this._scopeSeq.delete(scope);
|
| 178 |
+
this._inFlight.deleteScope(scope);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
addSettleWaiter(waiter: () => void): void {
|
| 182 |
+
this._settleWaiters.push(waiter);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
fireSettleWaiters(): void {
|
| 186 |
+
const waiters = this._settleWaiters.splice(0);
|
| 187 |
+
for (const waiter of waiters) {
|
| 188 |
+
waiter();
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
export class CascadeEngine {
|
| 194 |
+
private readonly _units = new Map<
|
| 195 |
+
ServiceIdentifier<any>,
|
| 196 |
+
UnitRecord
|
| 197 |
+
>();
|
| 198 |
+
private readonly _pendingIndex = new Map<
|
| 199 |
+
ServiceIdentifier<any>,
|
| 200 |
+
Set<ServiceIdentifier<any>>
|
| 201 |
+
>();
|
| 202 |
+
private readonly _history: CascadeHistoryEntry[] = [];
|
| 203 |
+
private _historySeq = 0;
|
| 204 |
+
private _disposed = false;
|
| 205 |
+
private _activationSuspended = 0;
|
| 206 |
+
private readonly _onDidChangeUnitState = new Emitter<UnitStateChange>();
|
| 207 |
+
readonly onDidChangeUnitState: Event<UnitStateChange> = this._onDidChangeUnitState.event;
|
| 208 |
+
private readonly _onDidCascade = new Emitter<CascadeHistoryEntry>();
|
| 209 |
+
readonly onDidCascade: Event<CascadeHistoryEntry> = this._onDidCascade.event;
|
| 210 |
+
|
| 211 |
+
constructor(
|
| 212 |
+
private readonly _host: CascadeHost,
|
| 213 |
+
private readonly _scope: CascadeScopeHandle,
|
| 214 |
+
private readonly _tree: CascadeTree,
|
| 215 |
+
private _options: CascadeEngineOptions = {},
|
| 216 |
+
) {
|
| 217 |
+
this._tree.addEngine(this);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
configure(options: CascadeEngineOptions): void {
|
| 221 |
+
this._options = { ...this._options, ...options };
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
stateOf(token: ServiceIdentifier<any>): UnitState | undefined {
|
| 225 |
+
return this._units.get(token)?.state;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
activationOf(token: ServiceIdentifier<any>): UnitActivation | undefined {
|
| 229 |
+
return this._units.get(token)?.activation;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
materializable(token: ServiceIdentifier<any>): boolean {
|
| 233 |
+
return this._host.recipeOf(token) !== undefined;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
failureOf(token: ServiceIdentifier<any>): unknown {
|
| 237 |
+
const unit = this._units.get(token);
|
| 238 |
+
return unit?.state === 'Failed' ? unit.error : undefined;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
isInFlight(token: ServiceIdentifier<any>): boolean {
|
| 242 |
+
const owner = this._host.ownerScopeOf(token) ?? this._scope;
|
| 243 |
+
return this._tree.inFlightHas({ scope: owner, token });
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
history(): readonly CascadeHistoryEntry[] {
|
| 247 |
+
return this._history;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
pendingSnapshot(): ReadonlyMap<string, readonly string[]> {
|
| 251 |
+
const snapshot = new Map<string, readonly string[]>();
|
| 252 |
+
for (const [token, unit] of this._units) {
|
| 253 |
+
if (unit.state !== 'Pending') continue;
|
| 254 |
+
snapshot.set(
|
| 255 |
+
token.toString(),
|
| 256 |
+
this._missingDeps(token).map((dep) => dep.toString()),
|
| 257 |
+
);
|
| 258 |
+
}
|
| 259 |
+
return snapshot;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
unitsSnapshot(): UnitSnapshot[] {
|
| 263 |
+
const snapshot: UnitSnapshot[] = [];
|
| 264 |
+
for (const [token, unit] of this._units) {
|
| 265 |
+
snapshot.push({
|
| 266 |
+
token: token.toString(),
|
| 267 |
+
state: unit.state,
|
| 268 |
+
error: unit.error === undefined ? undefined : serializeError(unit.error),
|
| 269 |
+
everActive: unit.everActive,
|
| 270 |
+
inFlight: this.isInFlight(token),
|
| 271 |
+
});
|
| 272 |
+
}
|
| 273 |
+
return snapshot;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
submit(change: CascadeChange): Promise<void> {
|
| 277 |
+
if (this._disposed) {
|
| 278 |
+
return Promise.resolve();
|
| 279 |
+
}
|
| 280 |
+
return new Promise<void>((resolve, reject) => {
|
| 281 |
+
this._tree.queue.push({ engine: this, change, resolve, reject });
|
| 282 |
+
this._pump();
|
| 283 |
+
});
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
submitAll(changes: readonly CascadeChange[]): Promise<void> {
|
| 287 |
+
if (this._disposed || changes.length === 0) {
|
| 288 |
+
return Promise.resolve();
|
| 289 |
+
}
|
| 290 |
+
return new Promise<void>((resolve, reject) => {
|
| 291 |
+
for (const change of changes) {
|
| 292 |
+
this._tree.queue.push({ engine: this, change, resolve, reject });
|
| 293 |
+
}
|
| 294 |
+
this._pump();
|
| 295 |
+
});
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
update(token: ServiceIdentifier<any>, reason?: string): Promise<void> {
|
| 299 |
+
return this.submit({
|
| 300 |
+
action: 'update',
|
| 301 |
+
token,
|
| 302 |
+
reason: reason ?? `update ${String(token)}`,
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
whenIdle(): Promise<void> {
|
| 307 |
+
if (!this._tree.running && this._tree.queue.length === 0) {
|
| 308 |
+
return Promise.resolve();
|
| 309 |
+
}
|
| 310 |
+
return new Promise<void>((resolve) => {
|
| 311 |
+
this._tree.addSettleWaiter(() => {
|
| 312 |
+
void this.whenIdle().then(resolve);
|
| 313 |
+
});
|
| 314 |
+
});
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
suspendActivation(): void {
|
| 318 |
+
this._activationSuspended++;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
resumeActivation(): void {
|
| 322 |
+
if (this._activationSuspended === 0) {
|
| 323 |
+
return;
|
| 324 |
+
}
|
| 325 |
+
this._activationSuspended--;
|
| 326 |
+
if (this._activationSuspended > 0 || this._disposed) {
|
| 327 |
+
return;
|
| 328 |
+
}
|
| 329 |
+
const rebuilt: string[] = [];
|
| 330 |
+
const failed: string[] = [];
|
| 331 |
+
this._recheckTreeFixpoint(rebuilt, failed);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
private _recheckTreeFixpoint(rebuilt: string[], failed: string[]): void {
|
| 335 |
+
const enginesInOrder = [...this._tree.engines]
|
| 336 |
+
.filter((engine) => !engine._disposed)
|
| 337 |
+
.toSorted((a, b) => a._scope.cascadeDepth - b._scope.cascadeDepth);
|
| 338 |
+
for (;;) {
|
| 339 |
+
let progress = false;
|
| 340 |
+
for (const engine of enginesInOrder) {
|
| 341 |
+
const before = rebuilt.length + failed.length;
|
| 342 |
+
engine._recheckForCascade(rebuilt, failed);
|
| 343 |
+
if (rebuilt.length + failed.length > before) {
|
| 344 |
+
progress = true;
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
if (!progress) {
|
| 348 |
+
break;
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
resolveWhenAvailable<T>(
|
| 354 |
+
token: ServiceIdentifier<any>,
|
| 355 |
+
timeoutMs?: number,
|
| 356 |
+
): Promise<T> {
|
| 357 |
+
if (!this.isInFlight(token)) {
|
| 358 |
+
try {
|
| 359 |
+
return Promise.resolve(this._host.materialize(token) as T);
|
| 360 |
+
} catch (error) {
|
| 361 |
+
return Promise.reject(error);
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
return new Promise<T>((resolve, reject) => {
|
| 365 |
+
const timer = setTimeout(() => {
|
| 366 |
+
reject(
|
| 367 |
+
new CascadeConflictError(
|
| 368 |
+
String(token),
|
| 369 |
+
'timed out waiting for the in-flight cascade to settle',
|
| 370 |
+
),
|
| 371 |
+
);
|
| 372 |
+
}, timeoutMs ?? this._options.resolveTimeoutMs ?? DEFAULT_RESOLVE_TIMEOUT_MS);
|
| 373 |
+
this._tree.addSettleWaiter(() => {
|
| 374 |
+
clearTimeout(timer);
|
| 375 |
+
try {
|
| 376 |
+
resolve(this._host.materialize(token) as T);
|
| 377 |
+
} catch (error) {
|
| 378 |
+
reject(error);
|
| 379 |
+
}
|
| 380 |
+
});
|
| 381 |
+
});
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
observedMaterialization(token: ServiceIdentifier<any>): void {
|
| 385 |
+
const unit = this._units.get(token);
|
| 386 |
+
if (unit !== undefined && unit.state === 'Pending') {
|
| 387 |
+
unit.everActive = true;
|
| 388 |
+
this._setUnitState(token, unit, 'Active', undefined);
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
dispose(): void {
|
| 393 |
+
this._disposed = true;
|
| 394 |
+
this._tree.removeEngine(this);
|
| 395 |
+
this._tree.dropScope(this._scope);
|
| 396 |
+
this._units.clear();
|
| 397 |
+
this._pendingIndex.clear();
|
| 398 |
+
const remaining: QueuedRequest[] = [];
|
| 399 |
+
for (const request of this._tree.queue) {
|
| 400 |
+
if (request.engine === this) {
|
| 401 |
+
request.resolve();
|
| 402 |
+
} else {
|
| 403 |
+
remaining.push(request);
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
this._tree.queue.length = 0;
|
| 407 |
+
this._tree.queue.push(...remaining);
|
| 408 |
+
this._onDidChangeUnitState.dispose();
|
| 409 |
+
this._onDidCascade.dispose();
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
_teardownForCascade(
|
| 413 |
+
token: ServiceIdentifier<any>,
|
| 414 |
+
tornDown: string[],
|
| 415 |
+
parkAsPending: boolean,
|
| 416 |
+
): void | Promise<void> {
|
| 417 |
+
if (this._disposed || !this._host.isMaterialized(token)) {
|
| 418 |
+
return undefined;
|
| 419 |
+
}
|
| 420 |
+
this._setUnitState(token, this._unitFor(token), 'Unloading', undefined);
|
| 421 |
+
const out = this._host.retire(token);
|
| 422 |
+
tornDown.push(this._label({ scope: this._scope, token }));
|
| 423 |
+
if (parkAsPending) {
|
| 424 |
+
this._markPending(token, undefined, true);
|
| 425 |
+
}
|
| 426 |
+
return out;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
_recheckForCascade(rebuilt: string[], failed: string[]): void {
|
| 430 |
+
if (this._disposed || this._activationSuspended > 0) {
|
| 431 |
+
return;
|
| 432 |
+
}
|
| 433 |
+
this._recheckPending(rebuilt, failed);
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
_applyChangeForCascade(change: CascadeChange): void {
|
| 437 |
+
if (this._disposed) {
|
| 438 |
+
return;
|
| 439 |
+
}
|
| 440 |
+
switch (change.action) {
|
| 441 |
+
case 'provide':
|
| 442 |
+
if (change.descriptor !== undefined) {
|
| 443 |
+
this._host.applyProvide(change.token, change.descriptor, change.config);
|
| 444 |
+
this._markPending(change.token, change.activation ?? 'eager', false);
|
| 445 |
+
} else {
|
| 446 |
+
this._host.applyProvideInstance(change.token, change.instance, change.config);
|
| 447 |
+
const unit = this._unitFor(change.token);
|
| 448 |
+
unit.everActive = true;
|
| 449 |
+
this._setUnitState(change.token, unit, 'Active', undefined);
|
| 450 |
+
}
|
| 451 |
+
break;
|
| 452 |
+
case 'unprovide':
|
| 453 |
+
this._host.applyUnprovide(change.token);
|
| 454 |
+
this._units.delete(change.token);
|
| 455 |
+
break;
|
| 456 |
+
case 'update':
|
| 457 |
+
this._markPending(change.token, undefined, true);
|
| 458 |
+
break;
|
| 459 |
+
}
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
private _pump(): void {
|
| 463 |
+
if (this._tree.running) {
|
| 464 |
+
return;
|
| 465 |
+
}
|
| 466 |
+
const batch = this._tree.queue.splice(0);
|
| 467 |
+
if (batch.length === 0) {
|
| 468 |
+
return;
|
| 469 |
+
}
|
| 470 |
+
this._tree.running = true;
|
| 471 |
+
const finish = (error?: unknown): void => {
|
| 472 |
+
this._tree.running = false;
|
| 473 |
+
for (const request of batch) {
|
| 474 |
+
if (error === undefined) {
|
| 475 |
+
request.resolve();
|
| 476 |
+
} else {
|
| 477 |
+
request.reject(error);
|
| 478 |
+
}
|
| 479 |
+
}
|
| 480 |
+
this._tree.fireSettleWaiters();
|
| 481 |
+
this._pump();
|
| 482 |
+
};
|
| 483 |
+
const orchestrator = batch[0]!.engine;
|
| 484 |
+
try {
|
| 485 |
+
const out = orchestrator._transact(batch);
|
| 486 |
+
if (isPromiseLike(out)) {
|
| 487 |
+
Promise.resolve(out).then(
|
| 488 |
+
() => { finish(); },
|
| 489 |
+
(error: unknown) => { finish(error); },
|
| 490 |
+
);
|
| 491 |
+
} else {
|
| 492 |
+
finish();
|
| 493 |
+
}
|
| 494 |
+
} catch (error) {
|
| 495 |
+
finish(error);
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
private _transact(batch: QueuedRequest[]): void | Promise<void> {
|
| 500 |
+
const changes = mergeBatch(batch);
|
| 501 |
+
const started = this._options.now?.() ?? Date.now();
|
| 502 |
+
const reason = changes.map(({ change }) => change.reason).join('; ');
|
| 503 |
+
const affected = this._tree.graph.affectedSet(
|
| 504 |
+
changes.map(({ engine, change }) => ({ scope: engine._scope, token: change.token })),
|
| 505 |
+
);
|
| 506 |
+
for (const ref of affected) {
|
| 507 |
+
this._tree.inFlightSet(ref, true);
|
| 508 |
+
}
|
| 509 |
+
this._tree.orchestrator = this._scope;
|
| 510 |
+
const complete = (abort: { waited: boolean; timedOut: boolean }): void | Promise<void> => {
|
| 511 |
+
const clear = (): void => {
|
| 512 |
+
this._tree.inFlightClear(affected);
|
| 513 |
+
this._tree.orchestrator = undefined;
|
| 514 |
+
};
|
| 515 |
+
let out: void | Promise<void>;
|
| 516 |
+
try {
|
| 517 |
+
out = this._applyTransaction(changes, affected, reason, started, abort);
|
| 518 |
+
} catch (error) {
|
| 519 |
+
clear();
|
| 520 |
+
throw error;
|
| 521 |
+
}
|
| 522 |
+
if (isPromiseLike(out)) {
|
| 523 |
+
return Promise.resolve(out).then(clear, (error: unknown) => {
|
| 524 |
+
clear();
|
| 525 |
+
throw error;
|
| 526 |
+
});
|
| 527 |
+
}
|
| 528 |
+
clear();
|
| 529 |
+
return undefined;
|
| 530 |
+
};
|
| 531 |
+
const wait = this._waitForAbort(affected, reason);
|
| 532 |
+
if (isPromiseLike(wait)) {
|
| 533 |
+
return Promise.resolve(wait).then(complete);
|
| 534 |
+
}
|
| 535 |
+
return complete(wait);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
private _waitForAbort(
|
| 539 |
+
affected: readonly ScopedToken[],
|
| 540 |
+
reason: string,
|
| 541 |
+
): { waited: boolean; timedOut: boolean } | Promise<{ waited: boolean; timedOut: boolean }> {
|
| 542 |
+
const hook = this._options.onWillCascade;
|
| 543 |
+
if (hook === undefined) {
|
| 544 |
+
return { waited: false, timedOut: false };
|
| 545 |
+
}
|
| 546 |
+
let out: void | Promise<void>;
|
| 547 |
+
try {
|
| 548 |
+
out = hook(affected, reason);
|
| 549 |
+
} catch (error) {
|
| 550 |
+
onUnexpectedError(error);
|
| 551 |
+
return { waited: false, timedOut: false };
|
| 552 |
+
}
|
| 553 |
+
if (!isPromiseLike(out)) {
|
| 554 |
+
return { waited: false, timedOut: false };
|
| 555 |
+
}
|
| 556 |
+
const waitMs = this._options.abortWaitMs ?? DEFAULT_ABORT_WAIT_MS;
|
| 557 |
+
return Promise.race([
|
| 558 |
+
Promise.resolve(out).then(
|
| 559 |
+
() => ({ waited: true, timedOut: false }),
|
| 560 |
+
(error: unknown) => {
|
| 561 |
+
onUnexpectedError(error);
|
| 562 |
+
return { waited: true, timedOut: false };
|
| 563 |
+
},
|
| 564 |
+
),
|
| 565 |
+
new Promise<{ waited: boolean; timedOut: boolean }>((resolve) => {
|
| 566 |
+
setTimeout(() => { resolve({ waited: true, timedOut: true }); }, waitMs);
|
| 567 |
+
}),
|
| 568 |
+
]);
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
private _applyTransaction(
|
| 572 |
+
changes: QueuedRequest[],
|
| 573 |
+
affected: readonly ScopedToken[],
|
| 574 |
+
reason: string,
|
| 575 |
+
started: number,
|
| 576 |
+
abort: { waited: boolean; timedOut: boolean },
|
| 577 |
+
): void | Promise<void> {
|
| 578 |
+
const tornDown: string[] = [];
|
| 579 |
+
const rebuilt: string[] = [];
|
| 580 |
+
const failed: string[] = [];
|
| 581 |
+
|
| 582 |
+
const teardownOrder = this._tree.graph.reverseTopoOrder(affected);
|
| 583 |
+
let index = 0;
|
| 584 |
+
const step = (): void | Promise<void> => {
|
| 585 |
+
while (index < teardownOrder.length) {
|
| 586 |
+
const ref = teardownOrder[index]!;
|
| 587 |
+
index += 1;
|
| 588 |
+
const owner = engineOf(ref);
|
| 589 |
+
if (owner === undefined || owner._disposed) {
|
| 590 |
+
continue;
|
| 591 |
+
}
|
| 592 |
+
const removed = changes.some(
|
| 593 |
+
({ engine, change }) =>
|
| 594 |
+
engine === owner && change.token === ref.token && change.action === 'unprovide',
|
| 595 |
+
);
|
| 596 |
+
const out = owner._teardownForCascade(ref.token, tornDown, !removed);
|
| 597 |
+
if (isPromiseLike(out)) {
|
| 598 |
+
return Promise.resolve(out).then(step);
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
return undefined;
|
| 602 |
+
};
|
| 603 |
+
|
| 604 |
+
const after = (): void => {
|
| 605 |
+
for (const { engine, change } of changes) {
|
| 606 |
+
engine._applyChangeForCascade(change);
|
| 607 |
+
}
|
| 608 |
+
this._recheckTreeFixpoint(rebuilt, failed);
|
| 609 |
+
this._pushHistory({
|
| 610 |
+
seq: ++this._historySeq,
|
| 611 |
+
reason,
|
| 612 |
+
changes: changes.map(({ engine, change }) => ({
|
| 613 |
+
token: this._label({ scope: engine._scope, token: change.token }),
|
| 614 |
+
action: change.action,
|
| 615 |
+
})),
|
| 616 |
+
affected: affected.map((ref) => this._label(ref)),
|
| 617 |
+
tornDown,
|
| 618 |
+
rebuilt,
|
| 619 |
+
failed,
|
| 620 |
+
abortWaited: abort.waited,
|
| 621 |
+
abortTimedOut: abort.timedOut,
|
| 622 |
+
durationMs: (this._options.now?.() ?? Date.now()) - started,
|
| 623 |
+
});
|
| 624 |
+
};
|
| 625 |
+
|
| 626 |
+
const drained = step();
|
| 627 |
+
if (isPromiseLike(drained)) {
|
| 628 |
+
return Promise.resolve(drained).then(after);
|
| 629 |
+
}
|
| 630 |
+
after();
|
| 631 |
+
return undefined;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
private _unitFor(token: ServiceIdentifier<any>): UnitRecord {
|
| 635 |
+
let unit = this._units.get(token);
|
| 636 |
+
if (unit === undefined) {
|
| 637 |
+
unit = { state: 'Pending', activation: 'eager', everActive: false };
|
| 638 |
+
this._units.set(token, unit);
|
| 639 |
+
this._onDidChangeUnitState.fire({ token: token.toString(), state: 'Pending' });
|
| 640 |
+
}
|
| 641 |
+
return unit;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
private _setUnitState(
|
| 645 |
+
token: ServiceIdentifier<any>,
|
| 646 |
+
unit: UnitRecord,
|
| 647 |
+
state: UnitState,
|
| 648 |
+
error: unknown,
|
| 649 |
+
): void {
|
| 650 |
+
const changed = unit.state !== state;
|
| 651 |
+
unit.state = state;
|
| 652 |
+
unit.error = error;
|
| 653 |
+
if (changed) {
|
| 654 |
+
this._onDidChangeUnitState.fire({
|
| 655 |
+
token: token.toString(),
|
| 656 |
+
state,
|
| 657 |
+
error: error === undefined ? undefined : serializeError(error),
|
| 658 |
+
});
|
| 659 |
+
}
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
private _markPending(
|
| 663 |
+
token: ServiceIdentifier<any>,
|
| 664 |
+
activation?: UnitActivation,
|
| 665 |
+
everActive?: boolean,
|
| 666 |
+
): void {
|
| 667 |
+
const unit = this._unitFor(token);
|
| 668 |
+
this._setUnitState(token, unit, 'Pending', undefined);
|
| 669 |
+
if (activation !== undefined) {
|
| 670 |
+
unit.activation = activation;
|
| 671 |
+
}
|
| 672 |
+
if (everActive !== undefined) {
|
| 673 |
+
unit.everActive = everActive;
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
private _recheckPending(rebuilt: string[], failed: string[]): void {
|
| 678 |
+
for (;;) {
|
| 679 |
+
this._pendingIndex.clear();
|
| 680 |
+
const satisfied: ServiceIdentifier<any>[] = [];
|
| 681 |
+
for (const [token, unit] of this._units) {
|
| 682 |
+
if (unit.state !== 'Pending') continue;
|
| 683 |
+
const missing = this._missingDeps(token);
|
| 684 |
+
if (missing.length === 0) {
|
| 685 |
+
if (unit.everActive || unit.activation === 'eager') {
|
| 686 |
+
satisfied.push(token);
|
| 687 |
+
}
|
| 688 |
+
} else {
|
| 689 |
+
for (const dep of missing) {
|
| 690 |
+
let waiters = this._pendingIndex.get(dep);
|
| 691 |
+
if (waiters === undefined) {
|
| 692 |
+
waiters = new Set();
|
| 693 |
+
this._pendingIndex.set(dep, waiters);
|
| 694 |
+
}
|
| 695 |
+
waiters.add(token);
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
}
|
| 699 |
+
if (satisfied.length === 0) {
|
| 700 |
+
return;
|
| 701 |
+
}
|
| 702 |
+
const ordered = this._tree.graph.topoOrder(
|
| 703 |
+
satisfied.map((token) => ({ scope: this._scope, token })),
|
| 704 |
+
);
|
| 705 |
+
for (const ref of ordered) {
|
| 706 |
+
this._activate(ref.token, rebuilt, failed);
|
| 707 |
+
}
|
| 708 |
+
for (const [token, unit] of this._units) {
|
| 709 |
+
if (
|
| 710 |
+
(unit.state === 'Pending' || unit.state === 'Activating') &&
|
| 711 |
+
this._host.isMaterialized(token)
|
| 712 |
+
) {
|
| 713 |
+
unit.everActive = true;
|
| 714 |
+
this._setUnitState(token, unit, 'Active', undefined);
|
| 715 |
+
}
|
| 716 |
+
}
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
private _activate(token: ServiceIdentifier<any>, rebuilt: string[], failed: string[]): void {
|
| 721 |
+
const unit = this._unitFor(token);
|
| 722 |
+
this._setUnitState(token, unit, 'Activating', undefined);
|
| 723 |
+
try {
|
| 724 |
+
this._host.materialize(token);
|
| 725 |
+
unit.everActive = true;
|
| 726 |
+
this._setUnitState(token, unit, 'Active', undefined);
|
| 727 |
+
rebuilt.push(this._label({ scope: this._scope, token }));
|
| 728 |
+
} catch (error) {
|
| 729 |
+
this._setUnitState(token, unit, 'Failed', error);
|
| 730 |
+
failed.push(this._label({ scope: this._scope, token }));
|
| 731 |
+
}
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
private _missingDeps(token: ServiceIdentifier<any>): Array<ServiceIdentifier<any>> {
|
| 735 |
+
const recipe = this._host.recipeOf(token);
|
| 736 |
+
if (recipe === undefined) {
|
| 737 |
+
return [token];
|
| 738 |
+
}
|
| 739 |
+
return this._host
|
| 740 |
+
.dependenciesOf(recipe)
|
| 741 |
+
.filter((dep) => !this._isAvailable(dep));
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
private _isAvailable(dep: ServiceIdentifier<any>): boolean {
|
| 745 |
+
if (!this._host.isRegistered(dep)) {
|
| 746 |
+
return false;
|
| 747 |
+
}
|
| 748 |
+
const owner = this._host.ownerScopeOf(dep);
|
| 749 |
+
const engine = owner === undefined ? undefined : engineOf({ scope: owner, token: dep });
|
| 750 |
+
const state = engine?.stateOf(dep);
|
| 751 |
+
if (state === undefined || state === 'Active') {
|
| 752 |
+
return true;
|
| 753 |
+
}
|
| 754 |
+
return (
|
| 755 |
+
state === 'Pending' &&
|
| 756 |
+
engine !== undefined &&
|
| 757 |
+
engine.activationOf(dep) === 'ondemand' &&
|
| 758 |
+
engine.materializable(dep)
|
| 759 |
+
);
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
private _label(ref: ScopedToken): string {
|
| 763 |
+
if (ref.scope === this._tree.orchestrator) {
|
| 764 |
+
return ref.token.toString();
|
| 765 |
+
}
|
| 766 |
+
return `#${this._tree.seqOf(ref.scope)}:${ref.token.toString()}`;
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
_mergeKey(): string {
|
| 770 |
+
return String(this._tree.seqOf(this._scope));
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
private _pushHistory(entry: CascadeHistoryEntry): void {
|
| 774 |
+
this._history.push(entry);
|
| 775 |
+
const capacity = this._options.historyCapacity ?? DEFAULT_HISTORY_CAPACITY;
|
| 776 |
+
if (this._history.length > capacity) {
|
| 777 |
+
this._history.splice(0, this._history.length - capacity);
|
| 778 |
+
}
|
| 779 |
+
this._onDidCascade.fire(entry);
|
| 780 |
+
}
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
function serializeError(error: unknown): string {
|
| 784 |
+
return error instanceof Error ? error.message : String(error);
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
function engineOf(ref: ScopedToken): CascadeEngine | undefined {
|
| 788 |
+
return (ref.scope as Partial<CascadeScopeHandle>).cascade;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
function mergeBatch(batch: QueuedRequest[]): QueuedRequest[] {
|
| 792 |
+
const byKey = new Map<string, QueuedRequest>();
|
| 793 |
+
for (const request of batch) {
|
| 794 |
+
const key = `${request.engine._mergeKey()}:${request.change.token.toString()}`;
|
| 795 |
+
byKey.set(key, request);
|
| 796 |
+
}
|
| 797 |
+
return [...byKey.values()];
|
| 798 |
+
}
|
packages/agent-core-v2/src/_base/di/collection.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Emitter, type Event } from '../event';
|
| 2 |
+
import type { Ledger } from '../lifecycle/ledger';
|
| 3 |
+
import { storeCustomDependency, type ServiceIdentifier } from './instantiation';
|
| 4 |
+
|
| 5 |
+
export interface CollectionToken<T> {
|
| 6 |
+
(target: any, key: string | symbol | undefined, index: number): void;
|
| 7 |
+
|
| 8 |
+
readonly name: string;
|
| 9 |
+
|
| 10 |
+
readonly __t?: T;
|
| 11 |
+
|
| 12 |
+
toString(): string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface DefinitionToken<T> extends CollectionToken<T> {
|
| 16 |
+
readonly __definition?: T;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface DefinitionRecord<T> {
|
| 20 |
+
readonly definition: T;
|
| 21 |
+
readonly owner: string;
|
| 22 |
+
readonly generation: number;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface DefinitionChange<T> {
|
| 26 |
+
readonly current: DefinitionRecord<T> | undefined;
|
| 27 |
+
readonly previous: DefinitionRecord<T> | undefined;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export interface DefinitionView<T> {
|
| 31 |
+
readonly current: DefinitionRecord<T> | undefined;
|
| 32 |
+
readonly onDidChangeDefinition: Event<DefinitionChange<T>>;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const _collectionTokens = new Map<string, CollectionToken<unknown>>();
|
| 36 |
+
const _collectionTokenSet = new WeakSet<object>();
|
| 37 |
+
const _definitionTokenSet = new WeakSet<object>();
|
| 38 |
+
const _collectionValidators = new WeakMap<
|
| 39 |
+
object,
|
| 40 |
+
(value: unknown, existing: readonly unknown[]) => void
|
| 41 |
+
>();
|
| 42 |
+
|
| 43 |
+
export function collection<T>(
|
| 44 |
+
name: string,
|
| 45 |
+
options: { readonly validate?: (value: T, existing: readonly T[]) => void } = {},
|
| 46 |
+
): CollectionToken<T> {
|
| 47 |
+
const existing = _collectionTokens.get(name);
|
| 48 |
+
if (existing !== undefined) {
|
| 49 |
+
return existing as CollectionToken<T>;
|
| 50 |
+
}
|
| 51 |
+
const token = function collectionDecorator(
|
| 52 |
+
target: any,
|
| 53 |
+
_key: string | symbol | undefined,
|
| 54 |
+
index: number,
|
| 55 |
+
): void {
|
| 56 |
+
if (arguments.length !== 3) {
|
| 57 |
+
throw new Error('@CollectionToken-decorator can only be used to decorate a parameter');
|
| 58 |
+
}
|
| 59 |
+
storeCustomDependency(token as unknown as ServiceIdentifier<any>, 'collection', target, index);
|
| 60 |
+
} as unknown as CollectionToken<T>;
|
| 61 |
+
Object.defineProperty(token, 'toString', {
|
| 62 |
+
value: () => `collection:${name}`,
|
| 63 |
+
enumerable: false,
|
| 64 |
+
});
|
| 65 |
+
Object.defineProperty(token, 'name', { value: name, enumerable: false, configurable: true });
|
| 66 |
+
_collectionTokens.set(name, token as CollectionToken<unknown>);
|
| 67 |
+
_collectionTokenSet.add(token);
|
| 68 |
+
if (options.validate !== undefined) {
|
| 69 |
+
_collectionValidators.set(
|
| 70 |
+
token,
|
| 71 |
+
options.validate as (value: unknown, existing: readonly unknown[]) => void,
|
| 72 |
+
);
|
| 73 |
+
}
|
| 74 |
+
return token;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
export function definition<T>(name: string): DefinitionToken<T> {
|
| 78 |
+
const token = collection<T>(name, {
|
| 79 |
+
validate: (_value, existing) => {
|
| 80 |
+
if (existing.length > 0) throw new Error(`Definition ${name} already has an active provider`);
|
| 81 |
+
},
|
| 82 |
+
}) as DefinitionToken<T>;
|
| 83 |
+
_definitionTokenSet.add(token);
|
| 84 |
+
return token;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
export function isCollectionToken(thing: unknown): thing is CollectionToken<unknown> {
|
| 88 |
+
return typeof thing === 'function' && _collectionTokenSet.has(thing);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export function isDefinitionToken(thing: unknown): thing is DefinitionToken<unknown> {
|
| 92 |
+
return typeof thing === 'function' && _definitionTokenSet.has(thing);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
export interface CollectionRecord<T> {
|
| 96 |
+
readonly value: T;
|
| 97 |
+
readonly providerName: string;
|
| 98 |
+
readonly scopePath: string;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
export interface CollectionChange<T> {
|
| 102 |
+
readonly added: readonly T[];
|
| 103 |
+
readonly removed: readonly T[];
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
export interface CollectionView<T> {
|
| 107 |
+
readonly items: readonly T[];
|
| 108 |
+
readonly records: readonly CollectionRecord<T>[];
|
| 109 |
+
readonly onDidChange: Event<CollectionChange<T>>;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
interface StoredRecord {
|
| 113 |
+
readonly id: number;
|
| 114 |
+
readonly value: unknown;
|
| 115 |
+
readonly providerName: string;
|
| 116 |
+
readonly scopePath: string;
|
| 117 |
+
readonly provider: object;
|
| 118 |
+
readonly providerBook: Ledger;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export type { StoredRecord };
|
| 122 |
+
|
| 123 |
+
export class CollectionStore {
|
| 124 |
+
private readonly _records = new Map<
|
| 125 |
+
CollectionToken<unknown>,
|
| 126 |
+
Map<number, StoredRecord>
|
| 127 |
+
>();
|
| 128 |
+
private readonly _views = new Set<CollectionViewImpl<unknown>>();
|
| 129 |
+
private _nextId = 0;
|
| 130 |
+
|
| 131 |
+
constructor(private readonly _parentOf: (container: object) => object | undefined) {}
|
| 132 |
+
|
| 133 |
+
addRecord<T>(
|
| 134 |
+
token: CollectionToken<T>,
|
| 135 |
+
provider: object,
|
| 136 |
+
providerName: string,
|
| 137 |
+
scopePath: string,
|
| 138 |
+
providerBook: Ledger,
|
| 139 |
+
value: T,
|
| 140 |
+
): () => void {
|
| 141 |
+
let records = this._records.get(token as CollectionToken<unknown>);
|
| 142 |
+
if (records === undefined) {
|
| 143 |
+
records = new Map();
|
| 144 |
+
this._records.set(token as CollectionToken<unknown>, records);
|
| 145 |
+
}
|
| 146 |
+
_collectionValidators.get(token)?.(
|
| 147 |
+
value,
|
| 148 |
+
[...records.values()].map((entry) => entry.value),
|
| 149 |
+
);
|
| 150 |
+
const record: StoredRecord = {
|
| 151 |
+
id: ++this._nextId,
|
| 152 |
+
value,
|
| 153 |
+
providerName,
|
| 154 |
+
scopePath,
|
| 155 |
+
provider,
|
| 156 |
+
providerBook,
|
| 157 |
+
};
|
| 158 |
+
records.set(record.id, record);
|
| 159 |
+
const fire = (view: CollectionViewImpl<unknown>, kind: 'added' | 'removed'): void => {
|
| 160 |
+
if (view.consumer === provider || this._isRelated(view.consumer, provider)) {
|
| 161 |
+
view._fireDelta(kind, [record]);
|
| 162 |
+
}
|
| 163 |
+
};
|
| 164 |
+
for (const view of this._views) {
|
| 165 |
+
if (view.token === (token as unknown as CollectionToken<unknown>)) {
|
| 166 |
+
fire(view, 'added');
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
return () => {
|
| 170 |
+
if (!records.delete(record.id)) {
|
| 171 |
+
return;
|
| 172 |
+
}
|
| 173 |
+
for (const view of this._views) {
|
| 174 |
+
if (view.token === (token as unknown as CollectionToken<unknown>)) {
|
| 175 |
+
fire(view, 'removed');
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
};
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
createView<T>(token: CollectionToken<T>, consumer: object): CollectionViewImpl<T> {
|
| 182 |
+
const view = new CollectionViewImpl<T>(this, token, consumer);
|
| 183 |
+
this._views.add(view as unknown as CollectionViewImpl<unknown>);
|
| 184 |
+
return view;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
dropView(view: CollectionViewImpl<unknown>): void {
|
| 188 |
+
this._views.delete(view);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
recordsFor<T>(token: CollectionToken<T>, consumer: object): CollectionRecord<T>[] {
|
| 192 |
+
const records = this._records.get(token as CollectionToken<unknown>);
|
| 193 |
+
if (records === undefined) {
|
| 194 |
+
return [];
|
| 195 |
+
}
|
| 196 |
+
const out: CollectionRecord<T>[] = [];
|
| 197 |
+
for (const record of records.values()) {
|
| 198 |
+
if (record.provider === consumer || this._isRelated(consumer, record.provider)) {
|
| 199 |
+
out.push({
|
| 200 |
+
value: record.value as T,
|
| 201 |
+
providerName: record.providerName,
|
| 202 |
+
scopePath: record.scopePath,
|
| 203 |
+
});
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
return out;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
storedRecordsFor(
|
| 210 |
+
token: CollectionToken<unknown>,
|
| 211 |
+
consumer: object,
|
| 212 |
+
): readonly StoredRecord[] {
|
| 213 |
+
const records = this._records.get(token);
|
| 214 |
+
if (records === undefined) {
|
| 215 |
+
return [];
|
| 216 |
+
}
|
| 217 |
+
const out: StoredRecord[] = [];
|
| 218 |
+
for (const record of records.values()) {
|
| 219 |
+
if (record.provider === consumer || this._isRelated(consumer, record.provider)) {
|
| 220 |
+
out.push(record);
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
return out;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
definitionFor<T>(token: CollectionToken<T>, consumer: object): DefinitionRecord<T> | undefined {
|
| 227 |
+
const record = this.storedRecordsFor(
|
| 228 |
+
token as CollectionToken<unknown>,
|
| 229 |
+
consumer,
|
| 230 |
+
)[0];
|
| 231 |
+
if (record === undefined) return undefined;
|
| 232 |
+
return {
|
| 233 |
+
definition: record.value as T,
|
| 234 |
+
owner: `${record.providerName}@${record.scopePath}`,
|
| 235 |
+
generation: record.id,
|
| 236 |
+
};
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
private _isRelated(consumer: object, provider: object): boolean {
|
| 240 |
+
for (let c: object | undefined = consumer; c !== undefined; c = this._parentOf(c)) {
|
| 241 |
+
if (c === provider) return true;
|
| 242 |
+
}
|
| 243 |
+
for (let p: object | undefined = provider; p !== undefined; p = this._parentOf(p)) {
|
| 244 |
+
if (p === consumer) return true;
|
| 245 |
+
}
|
| 246 |
+
return false;
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
export class CollectionViewImpl<T> implements CollectionView<T>, DefinitionView<T> {
|
| 251 |
+
private readonly _onDidChange = new Emitter<CollectionChange<T>>();
|
| 252 |
+
private readonly _onDidChangeDefinition = new Emitter<DefinitionChange<T>>();
|
| 253 |
+
readonly onDidChange: Event<CollectionChange<T>> = this._onDidChange.event;
|
| 254 |
+
readonly onDidChangeDefinition: Event<DefinitionChange<T>> =
|
| 255 |
+
this._onDidChangeDefinition.event;
|
| 256 |
+
|
| 257 |
+
constructor(
|
| 258 |
+
private readonly _store: CollectionStore,
|
| 259 |
+
readonly token: CollectionToken<T>,
|
| 260 |
+
readonly consumer: object,
|
| 261 |
+
) {}
|
| 262 |
+
|
| 263 |
+
get records(): CollectionRecord<T>[] {
|
| 264 |
+
return this._store.recordsFor(this.token, this.consumer);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
get items(): readonly T[] {
|
| 268 |
+
return this.records.map((record) => record.value);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
get current(): DefinitionRecord<T> | undefined {
|
| 272 |
+
return this._store.definitionFor(this.token, this.consumer);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
_fireDelta(kind: 'added' | 'removed', records: readonly StoredRecord[]): void {
|
| 276 |
+
const previous = kind === 'removed' ? this.definitionRecord(records[0]) : undefined;
|
| 277 |
+
const values = records.map((record) => record.value as T);
|
| 278 |
+
this._onDidChange.fire(
|
| 279 |
+
kind === 'added'
|
| 280 |
+
? { added: values, removed: [] }
|
| 281 |
+
: { added: [], removed: values },
|
| 282 |
+
);
|
| 283 |
+
if (isDefinitionToken(this.token)) {
|
| 284 |
+
this._onDidChangeDefinition.fire({ current: this.current, previous });
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
dispose(): void {
|
| 289 |
+
this._store.dropView(this as unknown as CollectionViewImpl<unknown>);
|
| 290 |
+
this._onDidChange.dispose();
|
| 291 |
+
this._onDidChangeDefinition.dispose();
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
private definitionRecord(record: StoredRecord | undefined): DefinitionRecord<T> | undefined {
|
| 295 |
+
if (record === undefined) return undefined;
|
| 296 |
+
return {
|
| 297 |
+
definition: record.value as T,
|
| 298 |
+
owner: `${record.providerName}@${record.scopePath}`,
|
| 299 |
+
generation: record.id,
|
| 300 |
+
};
|
| 301 |
+
}
|
| 302 |
+
}
|
packages/agent-core-v2/src/_base/di/dependencyGraph.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ServiceIdentifier } from './instantiation';
|
| 2 |
+
|
| 3 |
+
export interface ScopedToken {
|
| 4 |
+
readonly scope: object;
|
| 5 |
+
readonly token: ServiceIdentifier<any>;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export type DependencyEdgeKind = 'instance' | 'collection';
|
| 9 |
+
|
| 10 |
+
export interface DependencyEdge {
|
| 11 |
+
readonly consumer: ScopedToken;
|
| 12 |
+
readonly dependency: ScopedToken;
|
| 13 |
+
readonly kind: DependencyEdgeKind;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export class PairIndex<V> {
|
| 17 |
+
private readonly _map = new Map<object, Map<
|
| 18 |
+
ServiceIdentifier<any>,
|
| 19 |
+
V
|
| 20 |
+
>>();
|
| 21 |
+
|
| 22 |
+
get(scope: object, token: ServiceIdentifier<unknown>): V | undefined {
|
| 23 |
+
return this._map.get(scope)?.get(token);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
set(scope: object, token: ServiceIdentifier<unknown>, value: V): void {
|
| 27 |
+
let inner = this._map.get(scope);
|
| 28 |
+
if (inner === undefined) {
|
| 29 |
+
inner = new Map();
|
| 30 |
+
this._map.set(scope, inner);
|
| 31 |
+
}
|
| 32 |
+
inner.set(token, value);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
delete(scope: object, token: ServiceIdentifier<unknown>): void {
|
| 36 |
+
const inner = this._map.get(scope);
|
| 37 |
+
if (inner === undefined) return;
|
| 38 |
+
inner.delete(token);
|
| 39 |
+
if (inner.size === 0) {
|
| 40 |
+
this._map.delete(scope);
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
deleteScope(scope: object): void {
|
| 45 |
+
this._map.delete(scope);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
get size(): number {
|
| 49 |
+
return this._map.size;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
entries(): Array<[ScopedToken, V]> {
|
| 53 |
+
const out: Array<[ScopedToken, V]> = [];
|
| 54 |
+
for (const [scope, inner] of this._map) {
|
| 55 |
+
for (const [token, value] of inner) {
|
| 56 |
+
out.push([{ scope, token }, value]);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
return out;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
clear(): void {
|
| 63 |
+
this._map.clear();
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export class DependencyGraph {
|
| 68 |
+
private readonly _refByInstance = new Map<object, ScopedToken>();
|
| 69 |
+
private readonly _instanceByRef = new PairIndex<object>();
|
| 70 |
+
private readonly _out = new Map<object, PairIndex<DependencyEdgeKind>>();
|
| 71 |
+
private readonly _in = new PairIndex<Map<object, DependencyEdgeKind>>();
|
| 72 |
+
|
| 73 |
+
addInstance(
|
| 74 |
+
instance: object,
|
| 75 |
+
scope: object,
|
| 76 |
+
token: ServiceIdentifier<any>,
|
| 77 |
+
): void {
|
| 78 |
+
const ref: ScopedToken = { scope, token };
|
| 79 |
+
this._refByInstance.set(instance, ref);
|
| 80 |
+
this._instanceByRef.set(scope, token, instance);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
removeInstance(instance: object): void {
|
| 84 |
+
const ref = this._refByInstance.get(instance);
|
| 85 |
+
if (ref !== undefined) {
|
| 86 |
+
this._refByInstance.delete(instance);
|
| 87 |
+
if (this._instanceByRef.get(ref.scope, ref.token) === instance) {
|
| 88 |
+
this._instanceByRef.delete(ref.scope, ref.token);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
const out = this._out.get(instance);
|
| 92 |
+
if (out !== undefined) {
|
| 93 |
+
for (const [dependency] of out.entries()) {
|
| 94 |
+
const inbound = this._in.get(dependency.scope, dependency.token);
|
| 95 |
+
if (inbound !== undefined) {
|
| 96 |
+
inbound.delete(instance);
|
| 97 |
+
if (inbound.size === 0) {
|
| 98 |
+
this._in.delete(dependency.scope, dependency.token);
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
this._out.delete(instance);
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
removeScope(scope: object): void {
|
| 107 |
+
for (const [instance, ref] of this._refByInstance) {
|
| 108 |
+
if (ref.scope === scope) {
|
| 109 |
+
this.removeInstance(instance);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
this._instanceByRef.deleteScope(scope);
|
| 113 |
+
this._in.deleteScope(scope);
|
| 114 |
+
for (const [instance, out] of this._out) {
|
| 115 |
+
out.deleteScope(scope);
|
| 116 |
+
if (out.size === 0) {
|
| 117 |
+
this._out.delete(instance);
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
addEdge(
|
| 123 |
+
consumerInstance: object,
|
| 124 |
+
dependency: ScopedToken,
|
| 125 |
+
kind: DependencyEdgeKind = 'instance',
|
| 126 |
+
): void {
|
| 127 |
+
let out = this._out.get(consumerInstance);
|
| 128 |
+
if (out === undefined) {
|
| 129 |
+
out = new PairIndex();
|
| 130 |
+
this._out.set(consumerInstance, out);
|
| 131 |
+
}
|
| 132 |
+
out.set(dependency.scope, dependency.token, kind);
|
| 133 |
+
|
| 134 |
+
let inbound = this._in.get(dependency.scope, dependency.token);
|
| 135 |
+
if (inbound === undefined) {
|
| 136 |
+
inbound = new Map();
|
| 137 |
+
this._in.set(dependency.scope, dependency.token, inbound);
|
| 138 |
+
}
|
| 139 |
+
inbound.set(consumerInstance, kind);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
affectedSet(changed: Iterable<ScopedToken>): ScopedToken[] {
|
| 143 |
+
const seen = new PairIndex<true>();
|
| 144 |
+
const queue: ScopedToken[] = [];
|
| 145 |
+
const push = (ref: ScopedToken): void => {
|
| 146 |
+
if (seen.get(ref.scope, ref.token) !== undefined) return;
|
| 147 |
+
seen.set(ref.scope, ref.token, true);
|
| 148 |
+
queue.push(ref);
|
| 149 |
+
};
|
| 150 |
+
for (const ref of changed) {
|
| 151 |
+
push(ref);
|
| 152 |
+
}
|
| 153 |
+
const affected: ScopedToken[] = [];
|
| 154 |
+
while (queue.length > 0) {
|
| 155 |
+
const ref = queue.pop()!;
|
| 156 |
+
affected.push(ref);
|
| 157 |
+
const inbound = this._in.get(ref.scope, ref.token);
|
| 158 |
+
if (inbound === undefined) continue;
|
| 159 |
+
for (const [consumerInstance, kind] of inbound) {
|
| 160 |
+
if (kind !== 'instance') continue;
|
| 161 |
+
const consumer = this._refByInstance.get(consumerInstance);
|
| 162 |
+
if (consumer === undefined) continue;
|
| 163 |
+
push(consumer);
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
return affected;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
topoOrder(tokens: Iterable<ScopedToken>): ScopedToken[] {
|
| 170 |
+
const subset = new PairIndex<ScopedToken>();
|
| 171 |
+
for (const ref of tokens) {
|
| 172 |
+
subset.set(ref.scope, ref.token, ref);
|
| 173 |
+
}
|
| 174 |
+
const ordered: ScopedToken[] = [];
|
| 175 |
+
const done = new PairIndex<true>();
|
| 176 |
+
const visit = (ref: ScopedToken): void => {
|
| 177 |
+
if (done.get(ref.scope, ref.token) !== undefined) return;
|
| 178 |
+
done.set(ref.scope, ref.token, true);
|
| 179 |
+
for (const dependency of this._dependenciesOf(ref, subset)) {
|
| 180 |
+
visit(dependency);
|
| 181 |
+
}
|
| 182 |
+
ordered.push(ref);
|
| 183 |
+
};
|
| 184 |
+
for (const [, ref] of subset.entries()) {
|
| 185 |
+
visit(ref);
|
| 186 |
+
}
|
| 187 |
+
return ordered;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
reverseTopoOrder(tokens: Iterable<ScopedToken>): ScopedToken[] {
|
| 191 |
+
return this.topoOrder(tokens).toReversed();
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
findCycle(label: (ref: ScopedToken) => string): string[] | null {
|
| 195 |
+
const state = new PairIndex<'visiting' | 'done'>();
|
| 196 |
+
const path: ScopedToken[] = [];
|
| 197 |
+
const visit = (ref: ScopedToken): string[] | null => {
|
| 198 |
+
state.set(ref.scope, ref.token, 'visiting');
|
| 199 |
+
path.push(ref);
|
| 200 |
+
for (const dependency of this._dependenciesOf(ref, undefined)) {
|
| 201 |
+
const mark = state.get(dependency.scope, dependency.token);
|
| 202 |
+
if (mark === 'done') continue;
|
| 203 |
+
if (mark === 'visiting') {
|
| 204 |
+
const start = path.findIndex(
|
| 205 |
+
(entry) =>
|
| 206 |
+
entry.scope === dependency.scope && entry.token === dependency.token,
|
| 207 |
+
);
|
| 208 |
+
return [...path.slice(start), dependency].map(label);
|
| 209 |
+
}
|
| 210 |
+
const cycle = visit(dependency);
|
| 211 |
+
if (cycle !== null) return cycle;
|
| 212 |
+
}
|
| 213 |
+
path.pop();
|
| 214 |
+
state.set(ref.scope, ref.token, 'done');
|
| 215 |
+
return null;
|
| 216 |
+
};
|
| 217 |
+
for (const [ref] of this._instanceByRef.entries()) {
|
| 218 |
+
if (state.get(ref.scope, ref.token) !== undefined) continue;
|
| 219 |
+
const cycle = visit(ref);
|
| 220 |
+
if (cycle !== null) return cycle;
|
| 221 |
+
}
|
| 222 |
+
return null;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
edges(): DependencyEdge[] {
|
| 226 |
+
const edges: DependencyEdge[] = [];
|
| 227 |
+
for (const [consumerInstance, out] of this._out) {
|
| 228 |
+
const consumer = this._refByInstance.get(consumerInstance);
|
| 229 |
+
if (consumer === undefined) continue;
|
| 230 |
+
for (const [dependency, kind] of out.entries()) {
|
| 231 |
+
edges.push({ consumer, dependency, kind });
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
return edges;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
clear(): void {
|
| 238 |
+
this._refByInstance.clear();
|
| 239 |
+
this._instanceByRef.clear();
|
| 240 |
+
this._out.clear();
|
| 241 |
+
this._in.clear();
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
private _dependenciesOf(
|
| 245 |
+
ref: ScopedToken,
|
| 246 |
+
subset: PairIndex<ScopedToken> | undefined,
|
| 247 |
+
): ScopedToken[] {
|
| 248 |
+
const instance = this._instanceByRef.get(ref.scope, ref.token);
|
| 249 |
+
if (instance === undefined) return [];
|
| 250 |
+
const out = this._out.get(instance);
|
| 251 |
+
if (out === undefined) return [];
|
| 252 |
+
const result: ScopedToken[] = [];
|
| 253 |
+
for (const [dependency, kind] of out.entries()) {
|
| 254 |
+
if (kind !== 'instance') continue;
|
| 255 |
+
if (subset !== undefined && subset.get(dependency.scope, dependency.token) === undefined) {
|
| 256 |
+
continue;
|
| 257 |
+
}
|
| 258 |
+
result.push(dependency);
|
| 259 |
+
}
|
| 260 |
+
return result;
|
| 261 |
+
}
|
| 262 |
+
}
|
packages/agent-core-v2/src/_base/di/descriptors.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export class SyncDescriptor<T> {
|
| 2 |
+
public readonly ctor: any;
|
| 3 |
+
|
| 4 |
+
constructor(
|
| 5 |
+
ctor: new (...args: any[]) => T,
|
| 6 |
+
public readonly staticArguments: ReadonlyArray<any> = [],
|
| 7 |
+
) {
|
| 8 |
+
this.ctor = ctor;
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface SyncDescriptor0<T> {
|
| 13 |
+
readonly ctor: new () => T;
|
| 14 |
+
}
|
packages/agent-core-v2/src/_base/di/errors.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Graph } from './graph';
|
| 2 |
+
|
| 3 |
+
export class CyclicDependencyError extends Error {
|
| 4 |
+
readonly path: ReadonlyArray<string>;
|
| 5 |
+
|
| 6 |
+
constructor(pathOrGraph: ReadonlyArray<string> | Graph<any>) {
|
| 7 |
+
if (Array.isArray(pathOrGraph)) {
|
| 8 |
+
const path = pathOrGraph as ReadonlyArray<string>;
|
| 9 |
+
super(`Cyclic DI dependency detected: ${path.join(' → ')}`);
|
| 10 |
+
this.path = path;
|
| 11 |
+
} else {
|
| 12 |
+
const graph = pathOrGraph as Graph<any>;
|
| 13 |
+
const cycle = graph.findCycleSlow();
|
| 14 |
+
const detail = cycle ?? `UNABLE to detect cycle, dumping graph:\n${graph.toString()}`;
|
| 15 |
+
super(`cyclic dependency between services: ${detail}`);
|
| 16 |
+
this.path = cycle ? cycle.split(' -> ') : [];
|
| 17 |
+
}
|
| 18 |
+
this.name = 'CyclicDependencyError';
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export class CascadeConflictError extends Error {
|
| 23 |
+
constructor(
|
| 24 |
+
readonly token: string,
|
| 25 |
+
readonly detail: string,
|
| 26 |
+
) {
|
| 27 |
+
super(`Cascade conflict resolving '${token}': ${detail}`);
|
| 28 |
+
this.name = 'CascadeConflictError';
|
| 29 |
+
}
|
| 30 |
+
}
|
packages/agent-core-v2/src/_base/di/fiber.ts
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { IDisposable } from './lifecycle';
|
| 2 |
+
import type { Emitter } from '../event';
|
| 3 |
+
import { isPromiseLike, type EffectBody } from '../lifecycle/disposer';
|
| 4 |
+
import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
| 5 |
+
import {
|
| 6 |
+
collection,
|
| 7 |
+
isCollectionToken,
|
| 8 |
+
type CollectionToken,
|
| 9 |
+
type CollectionView,
|
| 10 |
+
} from './collection';
|
| 11 |
+
import { SyncDescriptor } from './descriptors';
|
| 12 |
+
import {
|
| 13 |
+
isServiceIdentifier,
|
| 14 |
+
ScopeActivation,
|
| 15 |
+
_util,
|
| 16 |
+
type LiveRef,
|
| 17 |
+
type ServiceIdentifier,
|
| 18 |
+
} from './instantiation';
|
| 19 |
+
|
| 20 |
+
export enum FiberState {
|
| 21 |
+
Pending = 0,
|
| 22 |
+
Activating = 1,
|
| 23 |
+
Active = 2,
|
| 24 |
+
Unloading = 3,
|
| 25 |
+
Failed = 4,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export interface ConfigSchema {
|
| 29 |
+
readonly '~standard': {
|
| 30 |
+
readonly validate: (value: unknown) => unknown;
|
| 31 |
+
};
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface RecipeStatics {
|
| 35 |
+
readonly name?: string;
|
| 36 |
+
readonly inject?: readonly ServiceIdentifier<any>[];
|
| 37 |
+
readonly Config?: ConfigSchema;
|
| 38 |
+
readonly meta?: Record<string, unknown>;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export type ServiceClassRecipe =
|
| 42 |
+
(new (...args: any[]) => unknown) & RecipeStatics;
|
| 43 |
+
|
| 44 |
+
export type ServiceFunctionRecipe = ((
|
| 45 |
+
fiber: Fiber,
|
| 46 |
+
config?: any,
|
| 47 |
+
) => any) &
|
| 48 |
+
RecipeStatics;
|
| 49 |
+
|
| 50 |
+
export type ServiceObjectRecipe = {
|
| 51 |
+
apply(
|
| 52 |
+
fiber: Fiber,
|
| 53 |
+
config?: any,
|
| 54 |
+
): any;
|
| 55 |
+
} & RecipeStatics;
|
| 56 |
+
|
| 57 |
+
export type ServiceRecipe =
|
| 58 |
+
| ServiceClassRecipe
|
| 59 |
+
| ServiceFunctionRecipe
|
| 60 |
+
| ServiceObjectRecipe;
|
| 61 |
+
|
| 62 |
+
export interface FiberProvideOptions {
|
| 63 |
+
readonly config?: unknown;
|
| 64 |
+
readonly activation?: ScopeActivation;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface Fiber {
|
| 68 |
+
readonly name: string;
|
| 69 |
+
readonly state: FiberState;
|
| 70 |
+
readonly config: unknown;
|
| 71 |
+
|
| 72 |
+
provide<T>(
|
| 73 |
+
id: ServiceIdentifier<T>,
|
| 74 |
+
recipe: ServiceClassRecipe,
|
| 75 |
+
opts?: FiberProvideOptions,
|
| 76 |
+
): FiberHandle<T>;
|
| 77 |
+
provide<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T>;
|
| 78 |
+
provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
|
| 79 |
+
provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
|
| 80 |
+
|
| 81 |
+
effect(body: EffectBody, label?: string): FiberHandle;
|
| 82 |
+
|
| 83 |
+
on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle;
|
| 84 |
+
|
| 85 |
+
get<T>(id: ServiceIdentifier<T>): T;
|
| 86 |
+
ref<T>(id: ServiceIdentifier<T>): LiveRef<T>;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export interface FiberHandle<T = unknown> extends PromiseLike<FiberHandle<T>> {
|
| 90 |
+
readonly name: string;
|
| 91 |
+
readonly state: FiberState;
|
| 92 |
+
readonly uid: number;
|
| 93 |
+
update(config?: unknown): Promise<void>;
|
| 94 |
+
dispose(): Promise<void>;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export class FiberProtocolError extends Error {
|
| 98 |
+
constructor(detail: string) {
|
| 99 |
+
super(detail);
|
| 100 |
+
this.name = 'FiberProtocolError';
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
export class ServiceRecipeError extends Error {
|
| 105 |
+
constructor(detail: string) {
|
| 106 |
+
super(detail);
|
| 107 |
+
this.name = 'ServiceRecipeError';
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
export interface ConstructionFrame {
|
| 112 |
+
readonly ctor: new (...args: any[]) => any;
|
| 113 |
+
readonly config: unknown;
|
| 114 |
+
readonly token: ServiceIdentifier<any> | undefined;
|
| 115 |
+
readonly host: FiberHost;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
const _constructionStack: ConstructionFrame[] = [];
|
| 119 |
+
|
| 120 |
+
export function pushConstructionFrame(frame: ConstructionFrame): void {
|
| 121 |
+
_constructionStack.push(frame);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
export function popConstructionFrame(): void {
|
| 125 |
+
_constructionStack.pop();
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
export function currentConstruction(): ConstructionFrame | undefined {
|
| 129 |
+
return _constructionStack.at(-1);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export const SERVICE_MARK = Symbol('serviceUnit');
|
| 133 |
+
|
| 134 |
+
export function isServiceRecipe(ctor: any): ctor is ServiceClassRecipe {
|
| 135 |
+
return typeof ctor === 'function' && ctor.prototype?.[SERVICE_MARK] === true;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export function isClassRecipe(recipe: unknown): recipe is ServiceClassRecipe {
|
| 139 |
+
return (
|
| 140 |
+
typeof recipe === 'function' &&
|
| 141 |
+
Object.prototype.hasOwnProperty.call(recipe, 'prototype')
|
| 142 |
+
);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
export type BufferedOp = (runtime: Fiber) => void;
|
| 146 |
+
|
| 147 |
+
export interface UnitInternals {
|
| 148 |
+
readonly unitBook: Ledger;
|
| 149 |
+
takeUnitBuffer(): BufferedOp[] | null;
|
| 150 |
+
setUnitRuntime(runtime: Fiber): void;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
export interface FiberHost {
|
| 154 |
+
mintUid(): number;
|
| 155 |
+
provideToken<T>(
|
| 156 |
+
id: ServiceIdentifier<T>,
|
| 157 |
+
descriptor: SyncDescriptor<T>,
|
| 158 |
+
options: {
|
| 159 |
+
readonly activation: 'eager' | 'ondemand';
|
| 160 |
+
readonly config?: unknown;
|
| 161 |
+
},
|
| 162 |
+
): TokenProvideCore;
|
| 163 |
+
provideTokenInstance<T>(id: ServiceIdentifier<T>, instance: T): TokenProvideCore;
|
| 164 |
+
tokenState(id: ServiceIdentifier<any>): string | undefined;
|
| 165 |
+
updateToken(id: ServiceIdentifier<any>, config: unknown, hasConfig: boolean): Promise<void>;
|
| 166 |
+
resolveTokenWhenAvailable<T>(id: ServiceIdentifier<T>): Promise<T>;
|
| 167 |
+
resolveInstance<T>(id: ServiceIdentifier<T>): T;
|
| 168 |
+
materializedInstance<T>(id: ServiceIdentifier<T>): T | undefined;
|
| 169 |
+
liveRef<T>(id: ServiceIdentifier<T>): LiveRef<T>;
|
| 170 |
+
recordInstanceEdge(node: object | undefined, id: ServiceIdentifier<any>): void;
|
| 171 |
+
collectionView<T>(token: CollectionToken<T>): CollectionView<T>;
|
| 172 |
+
addCollectionRecord<T>(
|
| 173 |
+
token: CollectionToken<T>,
|
| 174 |
+
providerName: string,
|
| 175 |
+
providerBook: Ledger,
|
| 176 |
+
value: T,
|
| 177 |
+
): () => void;
|
| 178 |
+
constructService<T>(ctor: new (...args: any[]) => T, config: unknown): T;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
export interface TokenProvideCore {
|
| 182 |
+
uid(): number;
|
| 183 |
+
dispose(): Promise<void>;
|
| 184 |
+
release(): void;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
export type FiberEventResolver = (
|
| 188 |
+
host: FiberHost,
|
| 189 |
+
event: string,
|
| 190 |
+
handler: (e: any) => void,
|
| 191 |
+
) => IDisposable;
|
| 192 |
+
|
| 193 |
+
let _eventResolver: FiberEventResolver | undefined;
|
| 194 |
+
|
| 195 |
+
export function setFiberEventResolver(resolver: FiberEventResolver | undefined): void {
|
| 196 |
+
_eventResolver = resolver;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
export function bindServiceUnit(instance: UnitInternals & IDisposable, frame: ConstructionFrame): void {
|
| 200 |
+
const buffer = instance.takeUnitBuffer();
|
| 201 |
+
if (buffer === null) {
|
| 202 |
+
return;
|
| 203 |
+
}
|
| 204 |
+
const ctor = (instance as any).constructor as ServiceClassRecipe;
|
| 205 |
+
const runtime = new FiberRuntime(
|
| 206 |
+
frame.host,
|
| 207 |
+
instance.unitBook,
|
| 208 |
+
recipeName(ctor),
|
| 209 |
+
frame.config,
|
| 210 |
+
frame.token,
|
| 211 |
+
new Set(_util.getInstanceDependencies(ctor as unknown as _util.DI_TARGET_OBJ).map((d) => d.id)),
|
| 212 |
+
instance,
|
| 213 |
+
);
|
| 214 |
+
instance.setUnitRuntime(runtime);
|
| 215 |
+
try {
|
| 216 |
+
for (const op of buffer) {
|
| 217 |
+
op(runtime);
|
| 218 |
+
}
|
| 219 |
+
} catch (error) {
|
| 220 |
+
instance.dispose();
|
| 221 |
+
throw error;
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
function recipeName(recipe: RecipeStatics & { name?: string }): string {
|
| 226 |
+
return recipe.name ?? 'anonymous';
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function validateConfig(schema: ConfigSchema | undefined, config: unknown, name: string): unknown {
|
| 230 |
+
if (schema === undefined) {
|
| 231 |
+
return config;
|
| 232 |
+
}
|
| 233 |
+
const out = schema['~standard'].validate(config);
|
| 234 |
+
if (isPromiseLike(out)) {
|
| 235 |
+
throw new ServiceRecipeError(`config schema of unit '${name}' must validate synchronously`);
|
| 236 |
+
}
|
| 237 |
+
const result = out as {
|
| 238 |
+
readonly value?: unknown;
|
| 239 |
+
readonly issues?: ReadonlyArray<{ readonly message: string }>;
|
| 240 |
+
};
|
| 241 |
+
if (result.issues !== undefined && result.issues.length > 0) {
|
| 242 |
+
throw new ServiceRecipeError(
|
| 243 |
+
`invalid config for unit '${name}': ${result.issues.map((issue) => issue.message).join('; ')}`,
|
| 244 |
+
);
|
| 245 |
+
}
|
| 246 |
+
return 'value' in result ? result.value : config;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
function mapUnitState(state: string | undefined): FiberState {
|
| 250 |
+
if (state === undefined) {
|
| 251 |
+
return FiberState.Active;
|
| 252 |
+
}
|
| 253 |
+
switch (state) {
|
| 254 |
+
case 'Pending':
|
| 255 |
+
return FiberState.Pending;
|
| 256 |
+
case 'Activating':
|
| 257 |
+
return FiberState.Activating;
|
| 258 |
+
case 'Unloading':
|
| 259 |
+
return FiberState.Unloading;
|
| 260 |
+
case 'Failed':
|
| 261 |
+
return FiberState.Failed;
|
| 262 |
+
default:
|
| 263 |
+
return FiberState.Active;
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
export class FiberRuntime implements Fiber {
|
| 268 |
+
constructor(
|
| 269 |
+
private readonly _host: FiberHost,
|
| 270 |
+
private readonly _book: Ledger,
|
| 271 |
+
readonly name: string,
|
| 272 |
+
readonly config: unknown,
|
| 273 |
+
private readonly _token: ServiceIdentifier<any> | undefined,
|
| 274 |
+
private readonly _declared: ReadonlySet<ServiceIdentifier<any>>,
|
| 275 |
+
private readonly _edgeNode: object | undefined,
|
| 276 |
+
) {}
|
| 277 |
+
|
| 278 |
+
get state(): FiberState {
|
| 279 |
+
if (this._token !== undefined) {
|
| 280 |
+
return mapUnitState(this._host.tokenState(this._token));
|
| 281 |
+
}
|
| 282 |
+
return FiberState.Active;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
provide<T>(
|
| 286 |
+
id: ServiceIdentifier<T>,
|
| 287 |
+
recipe: ServiceClassRecipe,
|
| 288 |
+
opts?: FiberProvideOptions,
|
| 289 |
+
): FiberHandle<T>;
|
| 290 |
+
provide<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T>;
|
| 291 |
+
provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
|
| 292 |
+
provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
|
| 293 |
+
provide(
|
| 294 |
+
first: ServiceIdentifier<any> | ServiceRecipe | CollectionToken<any>,
|
| 295 |
+
second?: any,
|
| 296 |
+
third?: FiberProvideOptions,
|
| 297 |
+
): FiberHandle {
|
| 298 |
+
if (isCollectionToken(first)) {
|
| 299 |
+
return this._provideRecord(first, second);
|
| 300 |
+
}
|
| 301 |
+
if (isServiceIdentifier(first)) {
|
| 302 |
+
if (isClassRecipe(second)) {
|
| 303 |
+
return this._provideToken(first, second, third);
|
| 304 |
+
}
|
| 305 |
+
if (
|
| 306 |
+
typeof second === 'function' ||
|
| 307 |
+
(second !== null && typeof second === 'object' && typeof second.apply === 'function')
|
| 308 |
+
) {
|
| 309 |
+
throw new ServiceRecipeError(
|
| 310 |
+
`token-bound provide of '${String(first)}' requires a class recipe or an instance (function/object recipes are only valid for the anonymous form)`,
|
| 311 |
+
);
|
| 312 |
+
}
|
| 313 |
+
return this._provideTokenInstance(first, second);
|
| 314 |
+
}
|
| 315 |
+
return this._provideAnonymous(first as ServiceRecipe, second as FiberProvideOptions | undefined);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
effect(body: EffectBody, label?: string): FiberHandle {
|
| 319 |
+
const entry = this._book.effect(body, label ?? `effect:${this.name}`);
|
| 320 |
+
return new BasicFiberHandle({
|
| 321 |
+
name: label ?? `effect:${this.name}`,
|
| 322 |
+
uid: this._host.mintUid(),
|
| 323 |
+
state: () => (entry.disposed ? FiberState.Unloading : FiberState.Active),
|
| 324 |
+
update: async () => {
|
| 325 |
+
await entry.dispose();
|
| 326 |
+
if (this._book.isActive) {
|
| 327 |
+
this._book.effect(body, label ?? `effect:${this.name}`);
|
| 328 |
+
}
|
| 329 |
+
},
|
| 330 |
+
dispose: async () => {
|
| 331 |
+
await entry.dispose();
|
| 332 |
+
},
|
| 333 |
+
whenActive: () => Promise.resolve(),
|
| 334 |
+
});
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle {
|
| 338 |
+
let subscription: IDisposable;
|
| 339 |
+
if (typeof event === 'string') {
|
| 340 |
+
if (_eventResolver === undefined) {
|
| 341 |
+
throw new FiberProtocolError(
|
| 342 |
+
`no event resolver registered — cannot subscribe to '${event}'`,
|
| 343 |
+
);
|
| 344 |
+
}
|
| 345 |
+
subscription = _eventResolver(this._host, event, handler);
|
| 346 |
+
} else if (typeof event?.event === 'function') {
|
| 347 |
+
subscription = event.event(handler);
|
| 348 |
+
} else {
|
| 349 |
+
throw new FiberProtocolError(`unsupported event source for unit '${this.name}'`);
|
| 350 |
+
}
|
| 351 |
+
const label = typeof event === 'string' ? `on:${event}` : `on:${event.constructor?.name ?? 'emitter'}`;
|
| 352 |
+
const entry = this._book.register(() => {
|
| 353 |
+
subscription.dispose();
|
| 354 |
+
}, label);
|
| 355 |
+
return new BasicFiberHandle({
|
| 356 |
+
name: label,
|
| 357 |
+
uid: this._host.mintUid(),
|
| 358 |
+
state: () => (entry.disposed ? FiberState.Unloading : FiberState.Active),
|
| 359 |
+
update: async () => {},
|
| 360 |
+
dispose: async () => {
|
| 361 |
+
await entry.dispose();
|
| 362 |
+
},
|
| 363 |
+
whenActive: () => Promise.resolve(),
|
| 364 |
+
});
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
get<T>(id: ServiceIdentifier<T>): T {
|
| 368 |
+
if (!this._declared.has(id)) {
|
| 369 |
+
throw new FiberProtocolError(
|
| 370 |
+
`unit '${this.name}' resolves undeclared dependency '${String(id)}' — declare it as a constructor parameter (class recipe) or in the inject static (function recipe)`,
|
| 371 |
+
);
|
| 372 |
+
}
|
| 373 |
+
this._host.recordInstanceEdge(this._edgeNode, id);
|
| 374 |
+
return this._host.resolveInstance(id);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
ref<T>(id: ServiceIdentifier<T>): LiveRef<T> {
|
| 378 |
+
return this._host.liveRef(id);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
private _provideToken<T>(
|
| 382 |
+
id: ServiceIdentifier<T>,
|
| 383 |
+
recipe: ServiceClassRecipe,
|
| 384 |
+
opts: FiberProvideOptions | undefined,
|
| 385 |
+
): FiberHandle<T> {
|
| 386 |
+
if (!isClassRecipe(recipe)) {
|
| 387 |
+
throw new ServiceRecipeError(
|
| 388 |
+
`token-bound provide of '${String(id)}' requires a class recipe (function/object recipes are only valid for the anonymous form)`,
|
| 389 |
+
);
|
| 390 |
+
}
|
| 391 |
+
const name = recipe.name ?? String(id);
|
| 392 |
+
const config = validateConfig(recipe.Config, opts?.config, name);
|
| 393 |
+
const core = this._host.provideToken(
|
| 394 |
+
id,
|
| 395 |
+
new SyncDescriptor<T>(recipe as new (...args: any[]) => T),
|
| 396 |
+
{
|
| 397 |
+
activation: opts?.activation === ScopeActivation.OnDemand ? 'ondemand' : 'eager',
|
| 398 |
+
config,
|
| 399 |
+
},
|
| 400 |
+
);
|
| 401 |
+
const entry = this._book.register(() => core.dispose(), `provide:${String(id)}`);
|
| 402 |
+
return new BasicFiberHandle<T>({
|
| 403 |
+
name,
|
| 404 |
+
uid: undefined,
|
| 405 |
+
uidOf: () => core.uid(),
|
| 406 |
+
state: () => mapUnitState(this._host.tokenState(id)),
|
| 407 |
+
update: (next) => {
|
| 408 |
+
const hasConfig = next !== undefined;
|
| 409 |
+
return this._host.updateToken(
|
| 410 |
+
id,
|
| 411 |
+
hasConfig ? validateConfig(recipe.Config, next, name) : undefined,
|
| 412 |
+
hasConfig,
|
| 413 |
+
);
|
| 414 |
+
},
|
| 415 |
+
dispose: async () => {
|
| 416 |
+
await entry.dispose();
|
| 417 |
+
},
|
| 418 |
+
whenActive: () => this._host.resolveTokenWhenAvailable(id).then(() => undefined),
|
| 419 |
+
});
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
private _provideTokenInstance<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T> {
|
| 423 |
+
const core = this._host.provideTokenInstance(id, instance);
|
| 424 |
+
const entry = this._book.register(() => core.dispose(), `provide:${String(id)}`);
|
| 425 |
+
return new BasicFiberHandle<T>({
|
| 426 |
+
name: String(id),
|
| 427 |
+
uid: undefined,
|
| 428 |
+
uidOf: () => core.uid(),
|
| 429 |
+
state: () => mapUnitState(this._host.tokenState(id)),
|
| 430 |
+
update: () => this._host.updateToken(id, undefined, false),
|
| 431 |
+
dispose: async () => {
|
| 432 |
+
await entry.dispose();
|
| 433 |
+
},
|
| 434 |
+
whenActive: () => this._host.resolveTokenWhenAvailable(id).then(() => undefined),
|
| 435 |
+
});
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
private _provideAnonymous(recipe: ServiceRecipe, opts: FiberProvideOptions | undefined): FiberHandle {
|
| 439 |
+
if (isClassRecipe(recipe)) {
|
| 440 |
+
return this._provideAnonymousClass(recipe, opts);
|
| 441 |
+
}
|
| 442 |
+
return this._provideFunction(recipe, opts);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
private _provideAnonymousClass(recipe: ServiceClassRecipe, opts: FiberProvideOptions | undefined): FiberHandle {
|
| 446 |
+
const name = recipeName(recipe);
|
| 447 |
+
let config = validateConfig(recipe.Config, opts?.config, name);
|
| 448 |
+
let state = FiberState.Activating;
|
| 449 |
+
let failure: unknown;
|
| 450 |
+
let instance: unknown;
|
| 451 |
+
let entry: LedgerEntry | undefined;
|
| 452 |
+
const construct = (nextConfig: unknown): void => {
|
| 453 |
+
state = FiberState.Activating;
|
| 454 |
+
instance = this._host.constructService(recipe, nextConfig);
|
| 455 |
+
entry = this._book.register(() => {
|
| 456 |
+
state = FiberState.Unloading;
|
| 457 |
+
(instance as Partial<IDisposable>).dispose?.();
|
| 458 |
+
}, `provide:${name}`);
|
| 459 |
+
state = FiberState.Active;
|
| 460 |
+
};
|
| 461 |
+
try {
|
| 462 |
+
construct(config);
|
| 463 |
+
} catch (error) {
|
| 464 |
+
state = FiberState.Failed;
|
| 465 |
+
failure = error;
|
| 466 |
+
throw error;
|
| 467 |
+
}
|
| 468 |
+
for (const dependency of _util.getInstanceDependencies(recipe as unknown as _util.DI_TARGET_OBJ)) {
|
| 469 |
+
this._host.recordInstanceEdge(this._edgeNode, dependency.id);
|
| 470 |
+
}
|
| 471 |
+
return new BasicFiberHandle({
|
| 472 |
+
name,
|
| 473 |
+
uid: this._host.mintUid(),
|
| 474 |
+
state: () => state,
|
| 475 |
+
update: async (next) => {
|
| 476 |
+
await entry?.dispose();
|
| 477 |
+
entry = undefined;
|
| 478 |
+
try {
|
| 479 |
+
if (next !== undefined) {
|
| 480 |
+
config = validateConfig(recipe.Config, next, name);
|
| 481 |
+
}
|
| 482 |
+
construct(config);
|
| 483 |
+
} catch (error) {
|
| 484 |
+
state = FiberState.Failed;
|
| 485 |
+
failure = error;
|
| 486 |
+
throw error;
|
| 487 |
+
}
|
| 488 |
+
},
|
| 489 |
+
dispose: async () => {
|
| 490 |
+
await entry?.dispose();
|
| 491 |
+
entry = undefined;
|
| 492 |
+
},
|
| 493 |
+
whenActive: () => (failure !== undefined ? Promise.reject(failure) : Promise.resolve()),
|
| 494 |
+
});
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
private _provideFunction(recipe: ServiceFunctionRecipe | ServiceObjectRecipe, opts: FiberProvideOptions | undefined): FiberHandle {
|
| 498 |
+
const name = recipeName(recipe);
|
| 499 |
+
const config = validateConfig(recipe.Config, opts?.config, name);
|
| 500 |
+
const book = new Ledger(`unit:${name}`);
|
| 501 |
+
const anchor = this._book.register((reason) => book.teardown(reason), `provide:${name}`);
|
| 502 |
+
const facade = new FiberRuntime(
|
| 503 |
+
this._host,
|
| 504 |
+
book,
|
| 505 |
+
name,
|
| 506 |
+
config,
|
| 507 |
+
undefined,
|
| 508 |
+
new Set(recipe.inject ?? []),
|
| 509 |
+
this._edgeNode,
|
| 510 |
+
);
|
| 511 |
+
const run = (): void => {
|
| 512 |
+
let out: unknown;
|
| 513 |
+
if (typeof recipe === 'function') {
|
| 514 |
+
out = recipe(facade, config);
|
| 515 |
+
} else {
|
| 516 |
+
out = recipe.apply(facade, config);
|
| 517 |
+
}
|
| 518 |
+
book.effect((() => out) as EffectBody, `effect:${name}`);
|
| 519 |
+
};
|
| 520 |
+
try {
|
| 521 |
+
run();
|
| 522 |
+
} catch (error) {
|
| 523 |
+
void book.teardown('unload');
|
| 524 |
+
anchor.release();
|
| 525 |
+
throw error;
|
| 526 |
+
}
|
| 527 |
+
return new BasicFiberHandle({
|
| 528 |
+
name,
|
| 529 |
+
uid: this._host.mintUid(),
|
| 530 |
+
state: () => (anchor.disposed ? FiberState.Unloading : FiberState.Active),
|
| 531 |
+
update: async () => {
|
| 532 |
+
await book.clear('unload');
|
| 533 |
+
try {
|
| 534 |
+
run();
|
| 535 |
+
} catch (error) {
|
| 536 |
+
void book.teardown('unload');
|
| 537 |
+
anchor.release();
|
| 538 |
+
throw error;
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
dispose: async () => {
|
| 542 |
+
await anchor.dispose();
|
| 543 |
+
},
|
| 544 |
+
whenActive: () => Promise.resolve(),
|
| 545 |
+
});
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
private _provideRecord<T>(token: CollectionToken<T>, value: T): FiberHandle {
|
| 549 |
+
const remove = this._host.addCollectionRecord(token, this.name, this._book, value);
|
| 550 |
+
const entry = this._book.register(() => {
|
| 551 |
+
remove();
|
| 552 |
+
}, `provide:${token.name}`);
|
| 553 |
+
return new BasicFiberHandle({
|
| 554 |
+
name: `${this.name}→${token.name}`,
|
| 555 |
+
uid: this._host.mintUid(),
|
| 556 |
+
state: () => (entry.disposed ? FiberState.Unloading : FiberState.Active),
|
| 557 |
+
update: async () => {},
|
| 558 |
+
dispose: async () => {
|
| 559 |
+
await entry.dispose();
|
| 560 |
+
},
|
| 561 |
+
whenActive: () => Promise.resolve(),
|
| 562 |
+
});
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
interface BasicHandleParts {
|
| 567 |
+
readonly name: string;
|
| 568 |
+
readonly uid?: number;
|
| 569 |
+
readonly uidOf?: () => number;
|
| 570 |
+
readonly state: () => FiberState;
|
| 571 |
+
readonly update: (config: unknown) => Promise<void>;
|
| 572 |
+
readonly dispose: () => Promise<void>;
|
| 573 |
+
readonly whenActive: () => Promise<void>;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
class BasicFiberHandle<T> implements FiberHandle<T> {
|
| 577 |
+
constructor(private readonly _parts: BasicHandleParts) {}
|
| 578 |
+
|
| 579 |
+
get name(): string {
|
| 580 |
+
return this._parts.name;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
get state(): FiberState {
|
| 584 |
+
return this._parts.state();
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
get uid(): number {
|
| 588 |
+
if (this._parts.uidOf !== undefined) {
|
| 589 |
+
return this._parts.uidOf();
|
| 590 |
+
}
|
| 591 |
+
return this._parts.uid!;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
update(config?: unknown): Promise<void> {
|
| 595 |
+
return this._parts.update(config);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
dispose(): Promise<void> {
|
| 599 |
+
return this._parts.dispose();
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
// eslint-disable-next-line eslint-plugin-unicorn(no-thenable)
|
| 603 |
+
then<TResult1 = FiberHandle<T>, TResult2 = never>(
|
| 604 |
+
onfulfilled?: ((value: FiberHandle<T>) => TResult1 | PromiseLike<TResult1>) | null,
|
| 605 |
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
| 606 |
+
): PromiseLike<TResult1 | TResult2> {
|
| 607 |
+
return thenSettle(this._parts.whenActive().then(() => settledView(this)), onfulfilled, onrejected);
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
interface SettledHandleView {
|
| 612 |
+
readonly name: string;
|
| 613 |
+
readonly state: FiberState;
|
| 614 |
+
readonly uid: number;
|
| 615 |
+
update(config?: unknown): Promise<void>;
|
| 616 |
+
dispose(): Promise<void>;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
function settledView<T>(handle: FiberHandle<T>): SettledHandleView {
|
| 620 |
+
return {
|
| 621 |
+
get name() {
|
| 622 |
+
return handle.name;
|
| 623 |
+
},
|
| 624 |
+
get state() {
|
| 625 |
+
return handle.state;
|
| 626 |
+
},
|
| 627 |
+
get uid() {
|
| 628 |
+
return handle.uid;
|
| 629 |
+
},
|
| 630 |
+
update: (config?: unknown) => handle.update(config),
|
| 631 |
+
dispose: () => handle.dispose(),
|
| 632 |
+
};
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
function thenSettle<T, TResult1, TResult2>(
|
| 636 |
+
settled: Promise<SettledHandleView>,
|
| 637 |
+
onfulfilled: ((value: FiberHandle<T>) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
| 638 |
+
onrejected: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined,
|
| 639 |
+
): PromiseLike<TResult1 | TResult2> {
|
| 640 |
+
return settled.then(
|
| 641 |
+
onfulfilled as unknown as (value: SettledHandleView) => TResult1 | PromiseLike<TResult1>,
|
| 642 |
+
onrejected,
|
| 643 |
+
);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
export class PendingFiberHandle<T> implements FiberHandle<T> {
|
| 647 |
+
private _real: FiberHandle<T> | undefined;
|
| 648 |
+
private _disposed = false;
|
| 649 |
+
|
| 650 |
+
constructor(private readonly _pendingName: string) {}
|
| 651 |
+
|
| 652 |
+
attach(real: FiberHandle<T>): void {
|
| 653 |
+
if (this._disposed) {
|
| 654 |
+
void real.dispose();
|
| 655 |
+
return;
|
| 656 |
+
}
|
| 657 |
+
this._real = real;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
get name(): string {
|
| 661 |
+
return this._real?.name ?? this._pendingName;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
get state(): FiberState {
|
| 665 |
+
return this._real?.state ?? FiberState.Activating;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
get uid(): number {
|
| 669 |
+
if (this._real === undefined) {
|
| 670 |
+
throw new FiberProtocolError(
|
| 671 |
+
`handle '${this._pendingName}' has not been flushed yet (construction protocol)`,
|
| 672 |
+
);
|
| 673 |
+
}
|
| 674 |
+
return this._real.uid;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
update(config?: unknown): Promise<void> {
|
| 678 |
+
if (this._real === undefined) {
|
| 679 |
+
return Promise.reject(
|
| 680 |
+
new FiberProtocolError(`handle '${this._pendingName}' has not been flushed yet`),
|
| 681 |
+
);
|
| 682 |
+
}
|
| 683 |
+
return this._real.update(config);
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
async dispose(): Promise<void> {
|
| 687 |
+
this._disposed = true;
|
| 688 |
+
await this._real?.dispose();
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
// eslint-disable-next-line eslint-plugin-unicorn(no-thenable) — FiberHandle is a thenable by design
|
| 692 |
+
then<TResult1 = FiberHandle<T>, TResult2 = never>(
|
| 693 |
+
onfulfilled?: ((value: FiberHandle<T>) => TResult1 | PromiseLike<TResult1>) | null,
|
| 694 |
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
| 695 |
+
): PromiseLike<TResult1 | TResult2> {
|
| 696 |
+
if (this._real !== undefined) {
|
| 697 |
+
return this._real.then(onfulfilled, onrejected);
|
| 698 |
+
}
|
| 699 |
+
return thenSettle(Promise.resolve(settledView(this)), onfulfilled, onrejected);
|
| 700 |
+
}
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
const _scopeUnitsTokens = new Map<string, CollectionToken<ServiceRecipe>>();
|
| 704 |
+
|
| 705 |
+
export function ScopeUnits(kind: string): CollectionToken<ServiceRecipe> {
|
| 706 |
+
let token = _scopeUnitsTokens.get(kind);
|
| 707 |
+
if (token === undefined) {
|
| 708 |
+
token = collection<ServiceRecipe>(`scope-units:${kind}`);
|
| 709 |
+
_scopeUnitsTokens.set(kind, token);
|
| 710 |
+
}
|
| 711 |
+
return token;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
export type { EffectBody };
|
packages/agent-core-v2/src/_base/di/graph.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export class Node<T> {
|
| 2 |
+
readonly incoming = new Map<string, Node<T>>();
|
| 3 |
+
readonly outgoing = new Map<string, Node<T>>();
|
| 4 |
+
|
| 5 |
+
constructor(
|
| 6 |
+
readonly key: string,
|
| 7 |
+
readonly data: T
|
| 8 |
+
) { }
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export class Graph<T> {
|
| 12 |
+
private readonly _nodes = new Map<string, Node<T>>();
|
| 13 |
+
|
| 14 |
+
constructor(private readonly _hashFn: (element: T) => string) { }
|
| 15 |
+
|
| 16 |
+
roots(): Node<T>[] {
|
| 17 |
+
const ret: Node<T>[] = [];
|
| 18 |
+
for (const node of this._nodes.values()) {
|
| 19 |
+
if (node.outgoing.size === 0) {
|
| 20 |
+
ret.push(node);
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
return ret;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
insertEdge(from: T, to: T): void {
|
| 27 |
+
const fromNode = this.lookupOrInsertNode(from);
|
| 28 |
+
const toNode = this.lookupOrInsertNode(to);
|
| 29 |
+
fromNode.outgoing.set(toNode.key, toNode);
|
| 30 |
+
toNode.incoming.set(fromNode.key, fromNode);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
removeNode(data: T): void {
|
| 34 |
+
const key = this._hashFn(data);
|
| 35 |
+
this._nodes.delete(key);
|
| 36 |
+
for (const node of this._nodes.values()) {
|
| 37 |
+
node.outgoing.delete(key);
|
| 38 |
+
node.incoming.delete(key);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
lookupOrInsertNode(data: T): Node<T> {
|
| 43 |
+
const key = this._hashFn(data);
|
| 44 |
+
let node = this._nodes.get(key);
|
| 45 |
+
if (!node) {
|
| 46 |
+
node = new Node(key, data);
|
| 47 |
+
this._nodes.set(key, node);
|
| 48 |
+
}
|
| 49 |
+
return node;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
isEmpty(): boolean {
|
| 53 |
+
return this._nodes.size === 0;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
toString(): string {
|
| 57 |
+
const data: string[] = [];
|
| 58 |
+
for (const [key, value] of this._nodes) {
|
| 59 |
+
data.push(`${key}\n\t(-> incoming)[${[...value.incoming.keys()].join(', ')}]\n\t(outgoing ->)[${[...value.outgoing.keys()].join(',')}]\n`);
|
| 60 |
+
}
|
| 61 |
+
return data.join('\n');
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
findCycleSlow() {
|
| 65 |
+
for (const [id, node] of this._nodes) {
|
| 66 |
+
const seen = new Set<string>([id]);
|
| 67 |
+
const res = this._findCycle(node, seen);
|
| 68 |
+
if (res) {
|
| 69 |
+
return res;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
return undefined;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
|
| 76 |
+
for (const [id, outgoing] of node.outgoing) {
|
| 77 |
+
if (seen.has(id)) {
|
| 78 |
+
return [...seen, id].join(' -> ');
|
| 79 |
+
}
|
| 80 |
+
seen.add(id);
|
| 81 |
+
const value = this._findCycle(outgoing, seen);
|
| 82 |
+
if (value) {
|
| 83 |
+
return value;
|
| 84 |
+
}
|
| 85 |
+
seen.delete(id);
|
| 86 |
+
}
|
| 87 |
+
return undefined;
|
| 88 |
+
}
|
| 89 |
+
}
|
packages/agent-core-v2/src/_base/di/instantiation.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { SyncDescriptor, SyncDescriptor0 } from './descriptors';
|
| 2 |
+
import type { CascadeEngine } from './cascadeEngine';
|
| 3 |
+
import type { Event } from '../event';
|
| 4 |
+
import type { DisposableStore, IDisposable } from './lifecycle';
|
| 5 |
+
import type { ServiceCollection } from './serviceCollection';
|
| 6 |
+
|
| 7 |
+
export type DependencyKind = 'instance' | 'collection' | 'ref';
|
| 8 |
+
|
| 9 |
+
export namespace _util {
|
| 10 |
+
export const serviceIds = new Map<string, ServiceIdentifier<any>>();
|
| 11 |
+
export const DI_TARGET = '$di$target';
|
| 12 |
+
export const DI_DEPENDENCIES = '$di$dependencies';
|
| 13 |
+
|
| 14 |
+
export interface ServiceDependency {
|
| 15 |
+
readonly id: ServiceIdentifier<any>;
|
| 16 |
+
readonly index: number;
|
| 17 |
+
readonly kind: DependencyKind;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function getServiceDependencies(
|
| 21 |
+
ctor: DI_TARGET_OBJ,
|
| 22 |
+
): ServiceDependency[] {
|
| 23 |
+
return ctor[DI_DEPENDENCIES] || [];
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export function getInstanceDependencies(
|
| 27 |
+
ctor: DI_TARGET_OBJ,
|
| 28 |
+
): ServiceDependency[] {
|
| 29 |
+
return getServiceDependencies(ctor).filter(
|
| 30 |
+
(dependency) => dependency.kind === 'instance',
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface DI_TARGET_OBJ extends Function {
|
| 35 |
+
[DI_TARGET]: Function;
|
| 36 |
+
[DI_DEPENDENCIES]: { id: ServiceIdentifier<any>; index: number; kind: DependencyKind }[];
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export type BrandedService = { _serviceBrand: undefined };
|
| 41 |
+
|
| 42 |
+
export interface IConstructorSignature<T, Args extends any[] = []> {
|
| 43 |
+
new <Services extends BrandedService[]>(...args: [...Args, ...Services]): T;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export type GetLeadingNonServiceArgs<TArgs extends any[]> =
|
| 47 |
+
TArgs extends [] ? []
|
| 48 |
+
: TArgs extends [...infer TFirst, BrandedService] ? GetLeadingNonServiceArgs<TFirst>
|
| 49 |
+
: TArgs;
|
| 50 |
+
|
| 51 |
+
export interface ServiceIdentifier<T> {
|
| 52 |
+
(target: any, key: string | symbol | undefined, index: number): void;
|
| 53 |
+
|
| 54 |
+
readonly type: T;
|
| 55 |
+
|
| 56 |
+
toString(): string;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function storeServiceDependency(
|
| 60 |
+
id: ServiceIdentifier<any>,
|
| 61 |
+
target: Function,
|
| 62 |
+
index: number,
|
| 63 |
+
kind: DependencyKind = 'instance',
|
| 64 |
+
): void {
|
| 65 |
+
const t = target as _util.DI_TARGET_OBJ;
|
| 66 |
+
if (t[_util.DI_TARGET] === target) {
|
| 67 |
+
t[_util.DI_DEPENDENCIES].push({ id, index, kind });
|
| 68 |
+
} else {
|
| 69 |
+
t[_util.DI_DEPENDENCIES] = [{ id, index, kind }];
|
| 70 |
+
t[_util.DI_TARGET] = target;
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
export function storeCustomDependency(
|
| 75 |
+
id: ServiceIdentifier<any>,
|
| 76 |
+
kind: DependencyKind,
|
| 77 |
+
target: any,
|
| 78 |
+
index: number,
|
| 79 |
+
): void {
|
| 80 |
+
storeServiceDependency(id, target, index, kind);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
export function createDecorator<T>(name: string): ServiceIdentifier<T> {
|
| 84 |
+
const existing = _util.serviceIds.get(name);
|
| 85 |
+
if (existing) {
|
| 86 |
+
return existing as ServiceIdentifier<T>;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const id = function serviceDecorator(
|
| 90 |
+
target: any,
|
| 91 |
+
_key: string | symbol | undefined,
|
| 92 |
+
index: number,
|
| 93 |
+
): void {
|
| 94 |
+
if (arguments.length !== 3) {
|
| 95 |
+
throw new Error(
|
| 96 |
+
'@IServiceName-decorator can only be used to decorate a parameter',
|
| 97 |
+
);
|
| 98 |
+
}
|
| 99 |
+
storeServiceDependency(id, target, index);
|
| 100 |
+
} as unknown as ServiceIdentifier<T>;
|
| 101 |
+
|
| 102 |
+
Object.defineProperty(id, 'toString', {
|
| 103 |
+
value: function toString(): string {
|
| 104 |
+
return name;
|
| 105 |
+
},
|
| 106 |
+
enumerable: false,
|
| 107 |
+
writable: false,
|
| 108 |
+
configurable: false,
|
| 109 |
+
});
|
| 110 |
+
Object.defineProperty(id, SERVICE_IDENTIFIER_MARK, { value: true, enumerable: false });
|
| 111 |
+
|
| 112 |
+
_util.serviceIds.set(name, id);
|
| 113 |
+
return id;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
const SERVICE_IDENTIFIER_MARK = Symbol('serviceIdentifier');
|
| 117 |
+
|
| 118 |
+
export function isServiceIdentifier(thing: unknown): thing is ServiceIdentifier<unknown> {
|
| 119 |
+
return (
|
| 120 |
+
typeof thing === 'function' &&
|
| 121 |
+
(thing as unknown as Record<symbol, unknown>)[SERVICE_IDENTIFIER_MARK] === true
|
| 122 |
+
);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export function refineServiceDecorator<T1, T extends T1>(
|
| 126 |
+
serviceIdentifier: ServiceIdentifier<T1>,
|
| 127 |
+
): ServiceIdentifier<T> {
|
| 128 |
+
return serviceIdentifier as ServiceIdentifier<T>;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
export enum ScopeActivation {
|
| 132 |
+
OnScopeCreated = 0,
|
| 133 |
+
OnDemand = 1,
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
export interface LiveRef<T> {
|
| 137 |
+
readonly current: T | undefined;
|
| 138 |
+
readonly onDidChange: Event<void>;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
export function ref<T>(
|
| 142 |
+
id: ServiceIdentifier<T>,
|
| 143 |
+
): (target: object, key: string | symbol | undefined, index: number) => void {
|
| 144 |
+
return function refDecorator(target: any, _key: string | symbol | undefined, index: number): void {
|
| 145 |
+
if (arguments.length !== 3) {
|
| 146 |
+
throw new Error('@ref-decorator can only be used to decorate a parameter');
|
| 147 |
+
}
|
| 148 |
+
storeServiceDependency(id, target, index, 'ref');
|
| 149 |
+
};
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
export interface ServicesAccessor {
|
| 153 |
+
get<T>(id: ServiceIdentifier<T>): T;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
export interface ProvideOptions {
|
| 157 |
+
readonly activation?: 'eager' | 'ondemand';
|
| 158 |
+
readonly config?: unknown;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
export interface ProvideAllEntry<T = unknown> {
|
| 162 |
+
readonly id: ServiceIdentifier<T>;
|
| 163 |
+
readonly descriptor: SyncDescriptor<T>;
|
| 164 |
+
readonly options?: ProvideOptions;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
export interface ProvideHandle extends IDisposable {
|
| 168 |
+
readonly uid: number;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
export interface IInstantiationService {
|
| 172 |
+
readonly _serviceBrand: undefined;
|
| 173 |
+
|
| 174 |
+
readonly cascade: CascadeEngine;
|
| 175 |
+
|
| 176 |
+
invokeFunction<R, TS extends any[] = []>(
|
| 177 |
+
fn: (accessor: ServicesAccessor, ...args: TS) => R,
|
| 178 |
+
...args: TS
|
| 179 |
+
): R;
|
| 180 |
+
createInstance<T>(descriptor: SyncDescriptor0<T>): T;
|
| 181 |
+
createInstance<
|
| 182 |
+
Ctor extends new (
|
| 183 |
+
...args: any[]
|
| 184 |
+
) => unknown,
|
| 185 |
+
R extends InstanceType<Ctor>,
|
| 186 |
+
>(
|
| 187 |
+
ctor: Ctor,
|
| 188 |
+
...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>
|
| 189 |
+
): R;
|
| 190 |
+
createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService;
|
| 191 |
+
provide<T>(
|
| 192 |
+
id: ServiceIdentifier<T>,
|
| 193 |
+
instanceOrDescriptor: T | SyncDescriptor<T>,
|
| 194 |
+
options?: ProvideOptions,
|
| 195 |
+
): ProvideHandle;
|
| 196 |
+
provideAll(entries: ReadonlyArray<ProvideAllEntry>): void;
|
| 197 |
+
unprovide<T>(id: ServiceIdentifier<T>): void;
|
| 198 |
+
dispose(): void;
|
| 199 |
+
disposeAsync(): Promise<void>;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
export const IInstantiationService: ServiceIdentifier<IInstantiationService> =
|
| 203 |
+
createDecorator<IInstantiationService>('instantiationService');
|
| 204 |
+
|
| 205 |
+
export interface ServiceCollectionLike {
|
| 206 |
+
set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: any): unknown;
|
| 207 |
+
get<T>(id: ServiceIdentifier<T>): any;
|
| 208 |
+
has(id: ServiceIdentifier<any>): boolean;
|
| 209 |
+
forEach(
|
| 210 |
+
callback: (id: ServiceIdentifier<any>, value: any) => void,
|
| 211 |
+
): void;
|
| 212 |
+
}
|
packages/agent-core-v2/src/_base/di/instantiationService.ts
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SyncDescriptor } from './descriptors';
|
| 2 |
+
import { CascadeEngine, CascadeTree, type CascadeChange, type CascadeHost } from './cascadeEngine';
|
| 3 |
+
import {
|
| 4 |
+
CollectionStore,
|
| 5 |
+
type CollectionToken,
|
| 6 |
+
type CollectionView,
|
| 7 |
+
type CollectionViewImpl,
|
| 8 |
+
} from './collection';
|
| 9 |
+
import { DependencyGraph } from './dependencyGraph';
|
| 10 |
+
import { CascadeConflictError, CyclicDependencyError } from './errors';
|
| 11 |
+
import {
|
| 12 |
+
bindServiceUnit,
|
| 13 |
+
isServiceRecipe,
|
| 14 |
+
pushConstructionFrame,
|
| 15 |
+
popConstructionFrame,
|
| 16 |
+
type ConstructionFrame,
|
| 17 |
+
type FiberHost,
|
| 18 |
+
type TokenProvideCore,
|
| 19 |
+
type UnitInternals,
|
| 20 |
+
} from './fiber';
|
| 21 |
+
import { Graph } from './graph';
|
| 22 |
+
import {
|
| 23 |
+
IInstantiationService as IInstantiationServiceDecorator,
|
| 24 |
+
_util,
|
| 25 |
+
type IInstantiationService,
|
| 26 |
+
type LiveRef,
|
| 27 |
+
type ProvideAllEntry,
|
| 28 |
+
type ProvideHandle,
|
| 29 |
+
type ProvideOptions,
|
| 30 |
+
type ServiceIdentifier,
|
| 31 |
+
type ServicesAccessor,
|
| 32 |
+
} from './instantiation';
|
| 33 |
+
import { isDisposable, type DisposableStore, type IDisposable } from './lifecycle';
|
| 34 |
+
import { onUnexpectedError } from '../errors/unexpectedError';
|
| 35 |
+
import { Emitter } from '../event';
|
| 36 |
+
import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
| 37 |
+
import type { Disposer } from '../lifecycle/disposer';
|
| 38 |
+
import { ServiceCollection } from './serviceCollection';
|
| 39 |
+
|
| 40 |
+
const enum TraceType {
|
| 41 |
+
None = 0,
|
| 42 |
+
Creation = 1,
|
| 43 |
+
Invocation = 2,
|
| 44 |
+
Branch = 3,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export class Trace {
|
| 48 |
+
static readonly all = new Set<string>();
|
| 49 |
+
|
| 50 |
+
private static readonly _None = new class extends Trace {
|
| 51 |
+
constructor() { super(TraceType.None, null); }
|
| 52 |
+
override stop() { }
|
| 53 |
+
override branch() { return this; }
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
static traceInvocation(_enableTracing: boolean, fn: any): Trace {
|
| 57 |
+
return !_enableTracing
|
| 58 |
+
? Trace._None
|
| 59 |
+
: new Trace(
|
| 60 |
+
TraceType.Invocation,
|
| 61 |
+
fn.name ?? new Error('Trace invocation').stack!.split('\n').slice(3, 4).join('\n'),
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
static traceCreation(_enableTracing: boolean, ctor: any): Trace {
|
| 66 |
+
return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
private static _totals: number = 0;
|
| 70 |
+
private readonly _start: number = Date.now();
|
| 71 |
+
private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];
|
| 72 |
+
|
| 73 |
+
private constructor(
|
| 74 |
+
readonly type: TraceType,
|
| 75 |
+
readonly name: string | null
|
| 76 |
+
) { }
|
| 77 |
+
|
| 78 |
+
branch(id: ServiceIdentifier<any>, first: boolean): Trace {
|
| 79 |
+
const child = new Trace(TraceType.Branch, id.toString());
|
| 80 |
+
this._dep.push([id, first, child]);
|
| 81 |
+
return child;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
stop() {
|
| 85 |
+
const dur = Date.now() - this._start;
|
| 86 |
+
Trace._totals += dur;
|
| 87 |
+
|
| 88 |
+
let causedCreation = false;
|
| 89 |
+
|
| 90 |
+
function printChild(n: number, trace: Trace) {
|
| 91 |
+
const res: string[] = [];
|
| 92 |
+
const prefix = '\t'.repeat(n);
|
| 93 |
+
for (const [id, first, child] of trace._dep) {
|
| 94 |
+
if (first && child) {
|
| 95 |
+
causedCreation = true;
|
| 96 |
+
res.push(`${prefix}CREATES -> ${String(id)}`);
|
| 97 |
+
const nested = printChild(n + 1, child);
|
| 98 |
+
if (nested) {
|
| 99 |
+
res.push(nested);
|
| 100 |
+
}
|
| 101 |
+
} else {
|
| 102 |
+
res.push(`${prefix}uses -> ${String(id)}`);
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
return res.join('\n');
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
const lines = [
|
| 109 |
+
`${this.type === TraceType.Creation ? 'CREATE' : 'CALL'} ${this.name}`,
|
| 110 |
+
printChild(1, this),
|
| 111 |
+
`DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)`,
|
| 112 |
+
];
|
| 113 |
+
|
| 114 |
+
if (dur > 2 || causedCreation) {
|
| 115 |
+
Trace.all.add(lines.join('\n'));
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export class InstantiationService implements IInstantiationService {
|
| 122 |
+
declare readonly _serviceBrand: undefined;
|
| 123 |
+
|
| 124 |
+
readonly _globalGraph?: Graph<string>;
|
| 125 |
+
|
| 126 |
+
protected readonly _parent?: InstantiationService;
|
| 127 |
+
|
| 128 |
+
protected readonly _ledger = new Ledger('InstantiationService');
|
| 129 |
+
|
| 130 |
+
get dependencyGraph(): DependencyGraph {
|
| 131 |
+
return this._tree.graph;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
private readonly _tree: CascadeTree;
|
| 135 |
+
|
| 136 |
+
readonly cascade: CascadeEngine;
|
| 137 |
+
|
| 138 |
+
private _parentLedgerEntry: LedgerEntry | undefined;
|
| 139 |
+
|
| 140 |
+
private readonly _instanceEntries = new Map<unknown, LedgerEntry>();
|
| 141 |
+
|
| 142 |
+
private readonly _provideEntries = new Map<
|
| 143 |
+
ServiceIdentifier<any>,
|
| 144 |
+
{ readonly entry: LedgerEntry; readonly core: TokenProvideCore }
|
| 145 |
+
>();
|
| 146 |
+
|
| 147 |
+
private _cascadeResolving = false;
|
| 148 |
+
|
| 149 |
+
protected readonly _children = new Set<InstantiationService>();
|
| 150 |
+
|
| 151 |
+
private readonly _inProgress: ServiceIdentifier<any>[] = [];
|
| 152 |
+
|
| 153 |
+
private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
|
| 154 |
+
|
| 155 |
+
private readonly _collectionStore: CollectionStore;
|
| 156 |
+
|
| 157 |
+
private readonly _collectionViews = new Map<
|
| 158 |
+
CollectionToken<unknown>,
|
| 159 |
+
CollectionViewImpl<any>
|
| 160 |
+
>();
|
| 161 |
+
|
| 162 |
+
private readonly _edgeNodes = new Set<object>();
|
| 163 |
+
|
| 164 |
+
debugLabel: string | undefined;
|
| 165 |
+
|
| 166 |
+
private _fiberHost: FiberHost | undefined;
|
| 167 |
+
|
| 168 |
+
private _disposed = false;
|
| 169 |
+
|
| 170 |
+
constructor(
|
| 171 |
+
private readonly _services: ServiceCollection = new ServiceCollection(),
|
| 172 |
+
private readonly _strict: boolean = false,
|
| 173 |
+
parent?: InstantiationService,
|
| 174 |
+
protected readonly _enableTracing: boolean = false,
|
| 175 |
+
) {
|
| 176 |
+
this._parent = parent;
|
| 177 |
+
this._globalGraph = _enableTracing ? parent?._globalGraph ?? new Graph(e => e) : undefined;
|
| 178 |
+
this._services.set(IInstantiationServiceDecorator, this);
|
| 179 |
+
this._tree = parent?._tree ?? new CascadeTree(new DependencyGraph());
|
| 180 |
+
this._collectionStore =
|
| 181 |
+
parent?._collectionStore ??
|
| 182 |
+
new CollectionStore((container) => (container as InstantiationService)._parent);
|
| 183 |
+
const host: CascadeHost = {
|
| 184 |
+
isRegistered: (token) => this._getServiceInstanceOrDescriptor(token) !== undefined,
|
| 185 |
+
ownerScopeOf: (token) => this._ownerOf(token),
|
| 186 |
+
isMaterialized: (token) => {
|
| 187 |
+
const value = this._services.get(token);
|
| 188 |
+
return value !== undefined && !(value instanceof SyncDescriptor);
|
| 189 |
+
},
|
| 190 |
+
materialize: (token) => {
|
| 191 |
+
this._cascadeResolving = true;
|
| 192 |
+
try {
|
| 193 |
+
return this._getOrCreateServiceInstance(
|
| 194 |
+
token,
|
| 195 |
+
Trace.traceCreation(false, CascadeEngine),
|
| 196 |
+
);
|
| 197 |
+
} finally {
|
| 198 |
+
this._cascadeResolving = false;
|
| 199 |
+
}
|
| 200 |
+
},
|
| 201 |
+
retire: (token) => this._retireUnit(token),
|
| 202 |
+
applyProvide: (token, descriptor, config) => {
|
| 203 |
+
this._services.set(token, descriptor, { config });
|
| 204 |
+
return this._services.uidOf(token)!;
|
| 205 |
+
},
|
| 206 |
+
applyProvideInstance: (token, instance, config) => {
|
| 207 |
+
this._services.set(token, instance, { config });
|
| 208 |
+
return this._services.uidOf(token)!;
|
| 209 |
+
},
|
| 210 |
+
applyUnprovide: (token) => {
|
| 211 |
+
this._services.delete(token);
|
| 212 |
+
},
|
| 213 |
+
recipeOf: (token) => {
|
| 214 |
+
const entry = this._services.entry(token);
|
| 215 |
+
if (entry === undefined) return undefined;
|
| 216 |
+
return entry.value instanceof SyncDescriptor ? entry.value : entry.recipe;
|
| 217 |
+
},
|
| 218 |
+
dependenciesOf: (recipe) =>
|
| 219 |
+
_util.getInstanceDependencies(recipe.ctor).map((dependency) => dependency.id),
|
| 220 |
+
};
|
| 221 |
+
this.cascade = new CascadeEngine(host, this, this._tree);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
get cascadeDisposed(): boolean {
|
| 225 |
+
return this._disposed;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
get cascadeDepth(): number {
|
| 229 |
+
return (this._parent?.cascadeDepth ?? -1) + 1;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
private _ownerOf(id: ServiceIdentifier<any>): InstantiationService | undefined {
|
| 233 |
+
if (this._services.has(id)) {
|
| 234 |
+
return this;
|
| 235 |
+
}
|
| 236 |
+
return this._parent?._ownerOf(id);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
invokeFunction<R, TS extends any[] = []>(
|
| 240 |
+
fn: (accessor: ServicesAccessor, ...args: TS) => R,
|
| 241 |
+
...args: TS
|
| 242 |
+
): R {
|
| 243 |
+
this._assertNotDisposed();
|
| 244 |
+
const _trace = Trace.traceInvocation(this._enableTracing, fn);
|
| 245 |
+
let done = false;
|
| 246 |
+
try {
|
| 247 |
+
const accessor: ServicesAccessor = {
|
| 248 |
+
get: <T>(id: ServiceIdentifier<T>): T => {
|
| 249 |
+
if (done) {
|
| 250 |
+
throw new Error(
|
| 251 |
+
'service accessor is only valid during the invocation of its target method',
|
| 252 |
+
);
|
| 253 |
+
}
|
| 254 |
+
const result = this._getOrCreateServiceInstance(id, _trace);
|
| 255 |
+
if (!result) {
|
| 256 |
+
this._throwIfStrict(`[invokeFunction] unknown service '${String(id)}'`, false);
|
| 257 |
+
}
|
| 258 |
+
return result;
|
| 259 |
+
},
|
| 260 |
+
};
|
| 261 |
+
return fn(accessor, ...args);
|
| 262 |
+
} finally {
|
| 263 |
+
done = true;
|
| 264 |
+
_trace.stop();
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
provide<T>(
|
| 269 |
+
id: ServiceIdentifier<T>,
|
| 270 |
+
instanceOrDescriptor: T | SyncDescriptor<T>,
|
| 271 |
+
options?: ProvideOptions,
|
| 272 |
+
): ProvideHandle {
|
| 273 |
+
this._assertNotDisposed();
|
| 274 |
+
const core = this._provideCore(id, instanceOrDescriptor, options);
|
| 275 |
+
const entry = this._ledger.register(() => {
|
| 276 |
+
void core.dispose();
|
| 277 |
+
}, `provide:${String(id)}`);
|
| 278 |
+
this._provideEntries.set(id, { entry, core });
|
| 279 |
+
return {
|
| 280 |
+
get uid(): number {
|
| 281 |
+
return core.uid();
|
| 282 |
+
},
|
| 283 |
+
dispose: () => {
|
| 284 |
+
void entry.dispose();
|
| 285 |
+
},
|
| 286 |
+
};
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
provideAll(entries: ReadonlyArray<ProvideAllEntry>): void {
|
| 290 |
+
this._assertNotDisposed();
|
| 291 |
+
const changes: CascadeChange[] = [];
|
| 292 |
+
for (const { id, descriptor, options } of entries) {
|
| 293 |
+
this._releaseProvideEntry(id);
|
| 294 |
+
changes.push({
|
| 295 |
+
action: 'provide',
|
| 296 |
+
token: id,
|
| 297 |
+
descriptor,
|
| 298 |
+
activation: options?.activation,
|
| 299 |
+
config: options?.config,
|
| 300 |
+
reason: `provide ${String(id)}`,
|
| 301 |
+
});
|
| 302 |
+
}
|
| 303 |
+
this.cascade.submitAll(changes).catch(onUnexpectedError);
|
| 304 |
+
for (const { id } of entries) {
|
| 305 |
+
const uid = this._services.uidOf(id);
|
| 306 |
+
if (uid === undefined) {
|
| 307 |
+
continue;
|
| 308 |
+
}
|
| 309 |
+
const entry = this._ledger.register(() => {
|
| 310 |
+
if (this._services.uidOf(id) === uid) {
|
| 311 |
+
void this._unprovideCore(id);
|
| 312 |
+
}
|
| 313 |
+
}, `provide:${String(id)}`);
|
| 314 |
+
this._provideEntries.set(id, {
|
| 315 |
+
entry,
|
| 316 |
+
core: {
|
| 317 |
+
uid: () => uid,
|
| 318 |
+
dispose: () => {
|
| 319 |
+
if (this._services.uidOf(id) === uid) {
|
| 320 |
+
return this._unprovideCore(id);
|
| 321 |
+
}
|
| 322 |
+
return Promise.resolve();
|
| 323 |
+
},
|
| 324 |
+
release: () => {},
|
| 325 |
+
},
|
| 326 |
+
});
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
private _provideCore<T>(
|
| 331 |
+
id: ServiceIdentifier<T>,
|
| 332 |
+
instanceOrDescriptor: T | SyncDescriptor<T>,
|
| 333 |
+
options?: ProvideOptions,
|
| 334 |
+
): TokenProvideCore {
|
| 335 |
+
this._releaseProvideEntry(id);
|
| 336 |
+
|
| 337 |
+
if (
|
| 338 |
+
!(instanceOrDescriptor instanceof SyncDescriptor) &&
|
| 339 |
+
this._services.get(id) === instanceOrDescriptor
|
| 340 |
+
) {
|
| 341 |
+
this._services.set(id, instanceOrDescriptor);
|
| 342 |
+
const uid = this._services.uidOf(id)!;
|
| 343 |
+
return {
|
| 344 |
+
uid: () => uid,
|
| 345 |
+
dispose: () => {
|
| 346 |
+
if (this._services.uidOf(id) === uid) {
|
| 347 |
+
return this._unprovideCore(id);
|
| 348 |
+
}
|
| 349 |
+
return Promise.resolve();
|
| 350 |
+
},
|
| 351 |
+
release: () => {},
|
| 352 |
+
};
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
const beforeUid = this._services.uidOf(id);
|
| 356 |
+
let appliedUid: number | undefined;
|
| 357 |
+
const availability = this._services.onDidChange(id, ({ newUid }) => {
|
| 358 |
+
if (newUid !== undefined && newUid !== beforeUid) {
|
| 359 |
+
appliedUid ??= newUid;
|
| 360 |
+
}
|
| 361 |
+
});
|
| 362 |
+
const release = (): void => {
|
| 363 |
+
availability.dispose();
|
| 364 |
+
};
|
| 365 |
+
const change: CascadeChange =
|
| 366 |
+
instanceOrDescriptor instanceof SyncDescriptor
|
| 367 |
+
? {
|
| 368 |
+
action: 'provide',
|
| 369 |
+
token: id,
|
| 370 |
+
descriptor: instanceOrDescriptor,
|
| 371 |
+
activation: options?.activation,
|
| 372 |
+
config: options?.config,
|
| 373 |
+
reason: `provide ${String(id)}`,
|
| 374 |
+
}
|
| 375 |
+
: {
|
| 376 |
+
action: 'provide',
|
| 377 |
+
token: id,
|
| 378 |
+
instance: instanceOrDescriptor,
|
| 379 |
+
config: options?.config,
|
| 380 |
+
reason: `provide ${String(id)}`,
|
| 381 |
+
};
|
| 382 |
+
this.cascade.submit(change).catch(onUnexpectedError);
|
| 383 |
+
return {
|
| 384 |
+
uid: () => {
|
| 385 |
+
if (appliedUid === undefined) {
|
| 386 |
+
throw new Error(
|
| 387 |
+
`provide of '${String(id)}' has not been applied yet (cascade in flight)`,
|
| 388 |
+
);
|
| 389 |
+
}
|
| 390 |
+
return appliedUid;
|
| 391 |
+
},
|
| 392 |
+
dispose: () => {
|
| 393 |
+
release();
|
| 394 |
+
if (appliedUid !== undefined && this._services.uidOf(id) === appliedUid) {
|
| 395 |
+
return this._unprovideCore(id);
|
| 396 |
+
}
|
| 397 |
+
return Promise.resolve();
|
| 398 |
+
},
|
| 399 |
+
release,
|
| 400 |
+
};
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
private _unprovideCore<T>(id: ServiceIdentifier<T>): Promise<void> {
|
| 404 |
+
if (this._disposed) {
|
| 405 |
+
return Promise.resolve();
|
| 406 |
+
}
|
| 407 |
+
this._releaseProvideEntry(id);
|
| 408 |
+
if (this._services.get(id) === undefined) {
|
| 409 |
+
return Promise.resolve();
|
| 410 |
+
}
|
| 411 |
+
return this.cascade
|
| 412 |
+
.submit({ action: 'unprovide', token: id, reason: `unprovide ${String(id)}` })
|
| 413 |
+
.catch((error: unknown) => {
|
| 414 |
+
onUnexpectedError(error);
|
| 415 |
+
});
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
unprovide<T>(id: ServiceIdentifier<T>): void {
|
| 419 |
+
void this._unprovideCore(id);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
private _releaseProvideEntry(id: ServiceIdentifier<any>): void {
|
| 423 |
+
const prev = this._provideEntries.get(id);
|
| 424 |
+
if (prev !== undefined) {
|
| 425 |
+
this._provideEntries.delete(id);
|
| 426 |
+
prev.entry.release();
|
| 427 |
+
prev.core.release();
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
private _retireUnit<T>(id: ServiceIdentifier<T>): void | Promise<void> {
|
| 432 |
+
const instance = this._services.get(id);
|
| 433 |
+
if (instance === undefined || instance instanceof SyncDescriptor) {
|
| 434 |
+
return undefined;
|
| 435 |
+
}
|
| 436 |
+
this._services.unmaterialize(id);
|
| 437 |
+
const entry = this._instanceEntries.get(instance);
|
| 438 |
+
if (entry === undefined) {
|
| 439 |
+
return undefined;
|
| 440 |
+
}
|
| 441 |
+
this._instanceEntries.delete(instance);
|
| 442 |
+
const serviceInstance = instance as object;
|
| 443 |
+
this._edgeNodes.delete(serviceInstance);
|
| 444 |
+
this._tree.graph.removeInstance(serviceInstance);
|
| 445 |
+
return entry.dispose();
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
private _nextUnitUid = 0;
|
| 449 |
+
|
| 450 |
+
get fiberHost(): FiberHost {
|
| 451 |
+
return this._getFiberHost();
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
get collectionStore(): CollectionStore {
|
| 455 |
+
return this._collectionStore;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
get ledger(): Ledger {
|
| 459 |
+
return this._ledger;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
get cascadeTree(): CascadeTree {
|
| 463 |
+
return this._tree;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
get children(): readonly InstantiationService[] {
|
| 467 |
+
return [...this._children];
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
servicesSnapshot(): { token: string; uid: number }[] {
|
| 471 |
+
const snapshot: { token: string; uid: number }[] = [];
|
| 472 |
+
this._services.forEach((id) => {
|
| 473 |
+
snapshot.push({
|
| 474 |
+
token: id.toString(),
|
| 475 |
+
uid: this._services.uidOf(id)!,
|
| 476 |
+
});
|
| 477 |
+
});
|
| 478 |
+
return snapshot;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
findIdentifier(tokenString: string): ServiceIdentifier<unknown> | undefined {
|
| 482 |
+
let found: ServiceIdentifier<unknown> | undefined;
|
| 483 |
+
this._services.forEach((id) => {
|
| 484 |
+
if (found === undefined && id.toString() === tokenString) {
|
| 485 |
+
found = id;
|
| 486 |
+
}
|
| 487 |
+
});
|
| 488 |
+
return found;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
anchorKernelEntry(disposer: Disposer, label: string): LedgerEntry {
|
| 492 |
+
return this._ledger.register(disposer, label);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
anchorKernelFinalizer(disposer: Disposer, label: string): LedgerEntry {
|
| 496 |
+
return this._ledger.registerFinalizer(disposer, label);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
private _getFiberHost(): FiberHost {
|
| 500 |
+
this._fiberHost ??= {
|
| 501 |
+
mintUid: () => ++this._root()._nextUnitUid,
|
| 502 |
+
provideToken: (id, descriptor, options) => this._provideCore(id, descriptor, options),
|
| 503 |
+
provideTokenInstance: <T>(id: ServiceIdentifier<T>, instance: T) =>
|
| 504 |
+
this._provideCore(id, instance, undefined),
|
| 505 |
+
tokenState: (id: ServiceIdentifier<any>) => {
|
| 506 |
+
const owner = this._ownerOf(id) ?? this;
|
| 507 |
+
return owner.cascade.stateOf(id);
|
| 508 |
+
},
|
| 509 |
+
updateToken: async (id, config, hasConfig) => {
|
| 510 |
+
const owner = this._ownerOf(id);
|
| 511 |
+
if (owner === undefined) {
|
| 512 |
+
throw new Error(`update of unregistered token '${String(id)}'`);
|
| 513 |
+
}
|
| 514 |
+
if (hasConfig) {
|
| 515 |
+
owner._services.setConfig(id, config);
|
| 516 |
+
}
|
| 517 |
+
await owner.cascade.update(id);
|
| 518 |
+
},
|
| 519 |
+
resolveTokenWhenAvailable: <T>(id: ServiceIdentifier<T>): Promise<T> =>
|
| 520 |
+
this.cascade.resolveWhenAvailable<T>(id),
|
| 521 |
+
resolveInstance: <T>(id: ServiceIdentifier<T>): T =>
|
| 522 |
+
this.invokeFunction((accessor) => accessor.get(id)),
|
| 523 |
+
materializedInstance: <T>(id: ServiceIdentifier<T>): T | undefined =>
|
| 524 |
+
this._materializedInstanceOf(id),
|
| 525 |
+
liveRef: <T>(id: ServiceIdentifier<T>): LiveRef<T> => this._liveRef(id),
|
| 526 |
+
recordInstanceEdge: (node: object | undefined, id: ServiceIdentifier<any>) => {
|
| 527 |
+
if (node === undefined) {
|
| 528 |
+
return;
|
| 529 |
+
}
|
| 530 |
+
const owner = this._ownerOf(id);
|
| 531 |
+
if (owner !== undefined) {
|
| 532 |
+
this._edgeNodes.add(node);
|
| 533 |
+
this.dependencyGraph.addEdge(node, { scope: owner, token: id }, 'instance');
|
| 534 |
+
}
|
| 535 |
+
},
|
| 536 |
+
collectionView: <T>(token: CollectionToken<T>): CollectionView<T> =>
|
| 537 |
+
this._collectionView(token),
|
| 538 |
+
addCollectionRecord: <T>(
|
| 539 |
+
token: CollectionToken<T>,
|
| 540 |
+
providerName: string,
|
| 541 |
+
providerBook: Ledger,
|
| 542 |
+
value: T,
|
| 543 |
+
) =>
|
| 544 |
+
this._collectionStore.addRecord(
|
| 545 |
+
token,
|
| 546 |
+
this,
|
| 547 |
+
providerName,
|
| 548 |
+
this._scopePath(),
|
| 549 |
+
providerBook,
|
| 550 |
+
value,
|
| 551 |
+
),
|
| 552 |
+
constructService: <T>(ctor: new (...args: any[]) => T, config: unknown): T => {
|
| 553 |
+
return this._createInstance(ctor, [], Trace.traceCreation(this._enableTracing, ctor), {
|
| 554 |
+
config,
|
| 555 |
+
});
|
| 556 |
+
},
|
| 557 |
+
};
|
| 558 |
+
return this._fiberHost;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
private _materializedInstanceOf<T>(id: ServiceIdentifier<T>): T | undefined {
|
| 562 |
+
const value = this._services.get(id);
|
| 563 |
+
if (value !== undefined) {
|
| 564 |
+
return value instanceof SyncDescriptor ? undefined : (value as T);
|
| 565 |
+
}
|
| 566 |
+
return this._parent?._materializedInstanceOf(id);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
private _liveRef<T>(id: ServiceIdentifier<T>): LiveRef<T> {
|
| 570 |
+
const change = new Emitter<void>();
|
| 571 |
+
const chain: InstantiationService[] = [this];
|
| 572 |
+
for (let c = this._parent; c !== undefined; c = c._parent) {
|
| 573 |
+
chain.push(c);
|
| 574 |
+
}
|
| 575 |
+
const subscriptions = chain.map((container) =>
|
| 576 |
+
container._services.onDidChange(id, () => {
|
| 577 |
+
change.fire();
|
| 578 |
+
}),
|
| 579 |
+
);
|
| 580 |
+
this._ledger.register(() => {
|
| 581 |
+
for (const subscription of subscriptions) {
|
| 582 |
+
subscription.dispose();
|
| 583 |
+
}
|
| 584 |
+
change.dispose();
|
| 585 |
+
}, `ref:${String(id)}`);
|
| 586 |
+
const current = (): T | undefined => this._materializedInstanceOf(id);
|
| 587 |
+
const ref: LiveRef<T> = {
|
| 588 |
+
get current(): T | undefined {
|
| 589 |
+
return current();
|
| 590 |
+
},
|
| 591 |
+
onDidChange: change.event,
|
| 592 |
+
};
|
| 593 |
+
return ref;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
private _collectionView<T>(token: CollectionToken<T>): CollectionView<T> {
|
| 597 |
+
let view = this._collectionViews.get(token as CollectionToken<unknown>);
|
| 598 |
+
if (view === undefined) {
|
| 599 |
+
view = this._collectionStore.createView(token, this);
|
| 600 |
+
this._collectionViews.set(token as CollectionToken<unknown>, view);
|
| 601 |
+
}
|
| 602 |
+
return view as CollectionViewImpl<T>;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
private _scopePath(): string {
|
| 606 |
+
const labels: string[] = [this.debugLabel ?? `#${this._tree.seqOf(this)}`];
|
| 607 |
+
for (
|
| 608 |
+
let c: InstantiationService | undefined = this._parent;
|
| 609 |
+
c !== undefined;
|
| 610 |
+
c = c._parent
|
| 611 |
+
) {
|
| 612 |
+
labels.unshift(c.debugLabel ?? `#${this._tree.seqOf(c)}`);
|
| 613 |
+
}
|
| 614 |
+
return labels.join('/');
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
createInstance<T>(descriptor: SyncDescriptor<T>, ...rest: any[]): T;
|
| 618 |
+
createInstance<T>(ctor: new (...args: any[]) => T, ...rest: any[]): T;
|
| 619 |
+
createInstance<T>(
|
| 620 |
+
ctorOrDescriptor: SyncDescriptor<T> | (new (...args: any[]) => T),
|
| 621 |
+
...rest: any[]
|
| 622 |
+
): T {
|
| 623 |
+
this._assertNotDisposed();
|
| 624 |
+
let _trace: Trace;
|
| 625 |
+
let result: T;
|
| 626 |
+
if (ctorOrDescriptor instanceof SyncDescriptor) {
|
| 627 |
+
_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);
|
| 628 |
+
result = this._createInstance(
|
| 629 |
+
ctorOrDescriptor.ctor,
|
| 630 |
+
ctorOrDescriptor.staticArguments.concat(rest),
|
| 631 |
+
_trace,
|
| 632 |
+
);
|
| 633 |
+
} else {
|
| 634 |
+
_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);
|
| 635 |
+
result = this._createInstance(ctorOrDescriptor, rest, _trace);
|
| 636 |
+
}
|
| 637 |
+
_trace.stop();
|
| 638 |
+
return result;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService {
|
| 642 |
+
this._assertNotDisposed();
|
| 643 |
+
if (!(services instanceof ServiceCollection)) {
|
| 644 |
+
throw new TypeError(
|
| 645 |
+
'createChild requires a ServiceCollection instance (got something else)',
|
| 646 |
+
);
|
| 647 |
+
}
|
| 648 |
+
const child = this._createChildService(services);
|
| 649 |
+
this._children.add(child);
|
| 650 |
+
child._parentLedgerEntry = this._ledger.register(() => {
|
| 651 |
+
child.dispose();
|
| 652 |
+
}, 'child-instantiation');
|
| 653 |
+
store?.add(child);
|
| 654 |
+
return child;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
protected _createChildService(services: ServiceCollection): InstantiationService {
|
| 658 |
+
return new InstantiationService(services, this._strict, this, this._enableTracing);
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
private _disposePromise: Promise<void> | undefined;
|
| 662 |
+
|
| 663 |
+
dispose(): void {
|
| 664 |
+
void this.disposeAsync();
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
disposeAsync(): Promise<void> {
|
| 668 |
+
this._disposePromise ??= this.disposeCore();
|
| 669 |
+
return this._disposePromise;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
private disposeCore(): Promise<void> {
|
| 673 |
+
if (this._disposed) {
|
| 674 |
+
return Promise.resolve();
|
| 675 |
+
}
|
| 676 |
+
this._disposed = true;
|
| 677 |
+
|
| 678 |
+
const childTeardowns: Promise<void>[] = [];
|
| 679 |
+
let teardown: void | Promise<void> = undefined;
|
| 680 |
+
try {
|
| 681 |
+
for (const child of Array.from(this._children)) {
|
| 682 |
+
childTeardowns.push(child.disposeAsync());
|
| 683 |
+
}
|
| 684 |
+
this._children.clear();
|
| 685 |
+
teardown = this._ledger.teardown('scope-close');
|
| 686 |
+
this._services.dispose();
|
| 687 |
+
this.cascade.dispose();
|
| 688 |
+
for (const view of this._collectionViews.values()) {
|
| 689 |
+
view.dispose();
|
| 690 |
+
}
|
| 691 |
+
this._collectionViews.clear();
|
| 692 |
+
for (const node of this._edgeNodes) {
|
| 693 |
+
this._tree.graph.removeInstance(node);
|
| 694 |
+
}
|
| 695 |
+
this._edgeNodes.clear();
|
| 696 |
+
this._tree.graph.removeScope(this);
|
| 697 |
+
} finally {
|
| 698 |
+
this._children.clear();
|
| 699 |
+
this._parentLedgerEntry?.release();
|
| 700 |
+
this._parentLedgerEntry = undefined;
|
| 701 |
+
if (this._parent) {
|
| 702 |
+
this._parent._children.delete(this);
|
| 703 |
+
}
|
| 704 |
+
}
|
| 705 |
+
return Promise.all([...childTeardowns, Promise.resolve(teardown)]).then(() => undefined);
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
private _createInstance<T>(ctor: any, args: unknown[], _trace: Trace, unit?: {
|
| 709 |
+
id?: ServiceIdentifier<any>;
|
| 710 |
+
config?: unknown;
|
| 711 |
+
}): T {
|
| 712 |
+
const serviceDependencies = _util.getServiceDependencies(ctor).toSorted((a, b) => a.index - b.index);
|
| 713 |
+
const serviceArgs: unknown[] = [];
|
| 714 |
+
for (const dependency of serviceDependencies) {
|
| 715 |
+
const kind = dependency.kind ?? 'instance';
|
| 716 |
+
if (kind === 'collection') {
|
| 717 |
+
serviceArgs.push(this._collectionView(dependency.id as unknown as CollectionToken<unknown>));
|
| 718 |
+
continue;
|
| 719 |
+
}
|
| 720 |
+
if (kind === 'ref') {
|
| 721 |
+
serviceArgs.push(this._liveRef(dependency.id));
|
| 722 |
+
continue;
|
| 723 |
+
}
|
| 724 |
+
const service = this._getOrCreateServiceInstance(dependency.id, _trace);
|
| 725 |
+
if (!service) {
|
| 726 |
+
this._throwIfStrict(
|
| 727 |
+
`[createInstance] ${ctor.name} depends on UNKNOWN service ${String(dependency.id)}.`,
|
| 728 |
+
false,
|
| 729 |
+
);
|
| 730 |
+
}
|
| 731 |
+
serviceArgs.push(service);
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
const firstServiceArgPos =
|
| 735 |
+
serviceDependencies.length > 0 ? serviceDependencies[0]!.index : args.length;
|
| 736 |
+
|
| 737 |
+
if (args.length !== firstServiceArgPos) {
|
| 738 |
+
globalThis.console.trace(
|
| 739 |
+
`[createInstance] First service dependency of ${(ctor as { name?: string }).name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`,
|
| 740 |
+
);
|
| 741 |
+
const delta = firstServiceArgPos - args.length;
|
| 742 |
+
if (delta > 0) {
|
| 743 |
+
args = args.concat(Array.from({ length: delta }));
|
| 744 |
+
} else {
|
| 745 |
+
args = args.slice(0, firstServiceArgPos);
|
| 746 |
+
}
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
const finalArgs = args.concat(serviceArgs);
|
| 750 |
+
if (!isServiceRecipe(ctor)) {
|
| 751 |
+
return Reflect.construct<unknown[], T>(ctor, finalArgs);
|
| 752 |
+
}
|
| 753 |
+
const frame: ConstructionFrame = {
|
| 754 |
+
ctor,
|
| 755 |
+
config: unit?.config,
|
| 756 |
+
token: unit?.id,
|
| 757 |
+
host: this._getFiberHost(),
|
| 758 |
+
};
|
| 759 |
+
pushConstructionFrame(frame);
|
| 760 |
+
let instance: T;
|
| 761 |
+
try {
|
| 762 |
+
instance = Reflect.construct<unknown[], T>(ctor as new (...args: any[]) => T, finalArgs);
|
| 763 |
+
} finally {
|
| 764 |
+
popConstructionFrame();
|
| 765 |
+
}
|
| 766 |
+
bindServiceUnit(instance as UnitInternals & IDisposable, frame);
|
| 767 |
+
return instance;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {
|
| 771 |
+
if (!this._cascadeResolving) {
|
| 772 |
+
if (this.cascade.isInFlight(id)) {
|
| 773 |
+
throw new CascadeConflictError(
|
| 774 |
+
String(id),
|
| 775 |
+
'token is inside an in-flight cascade transaction',
|
| 776 |
+
);
|
| 777 |
+
}
|
| 778 |
+
const failure = this.cascade.failureOf(id);
|
| 779 |
+
if (failure !== undefined) {
|
| 780 |
+
throw failure as Error;
|
| 781 |
+
}
|
| 782 |
+
}
|
| 783 |
+
const entry = this._getServiceInstanceOrDescriptor(id);
|
| 784 |
+
|
| 785 |
+
if (entry instanceof SyncDescriptor) {
|
| 786 |
+
const root = this._root();
|
| 787 |
+
if (root._inProgress.includes(id)) {
|
| 788 |
+
const path = [...root._inProgress, id].map(String);
|
| 789 |
+
throw new CyclicDependencyError(path);
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
return this._safeCreateAndCacheServiceInstance(id, entry, _trace.branch(id, true));
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
_trace.branch(id, false);
|
| 796 |
+
return entry as T;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
private _safeCreateAndCacheServiceInstance<T>(
|
| 800 |
+
id: ServiceIdentifier<T>,
|
| 801 |
+
desc: SyncDescriptor<T>,
|
| 802 |
+
_trace: Trace,
|
| 803 |
+
): T {
|
| 804 |
+
if (this._activeInstantiations.has(id)) {
|
| 805 |
+
throw new Error(`illegal state - RECURSIVELY instantiating service '${String(id)}'`);
|
| 806 |
+
}
|
| 807 |
+
this._activeInstantiations.add(id);
|
| 808 |
+
try {
|
| 809 |
+
return this._createAndCacheServiceInstance(id, desc, _trace);
|
| 810 |
+
} finally {
|
| 811 |
+
this._activeInstantiations.delete(id);
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
private _createAndCacheServiceInstance<T>(
|
| 816 |
+
id: ServiceIdentifier<T>,
|
| 817 |
+
desc: SyncDescriptor<T>,
|
| 818 |
+
_trace: Trace,
|
| 819 |
+
): T {
|
| 820 |
+
type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };
|
| 821 |
+
const graph = new Graph<Triple>(data => data.id.toString());
|
| 822 |
+
|
| 823 |
+
let cycleCount = 0;
|
| 824 |
+
const stack: Triple[] = [{ id, desc, _trace }];
|
| 825 |
+
const seen = new Set<string>();
|
| 826 |
+
while (stack.length > 0) {
|
| 827 |
+
const item = stack.pop()!;
|
| 828 |
+
|
| 829 |
+
if (seen.has(String(item.id))) {
|
| 830 |
+
continue;
|
| 831 |
+
}
|
| 832 |
+
seen.add(String(item.id));
|
| 833 |
+
|
| 834 |
+
graph.lookupOrInsertNode(item);
|
| 835 |
+
|
| 836 |
+
if (cycleCount++ > 1000) {
|
| 837 |
+
throw new CyclicDependencyError(graph);
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
for (const dependency of _util.getInstanceDependencies(item.desc.ctor)) {
|
| 841 |
+
const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
|
| 842 |
+
if (!instanceOrDesc) {
|
| 843 |
+
this._throwIfStrict(
|
| 844 |
+
`[createInstance] ${String(item.id)} depends on ${String(dependency.id)} which is NOT registered.`,
|
| 845 |
+
true,
|
| 846 |
+
);
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
this._globalGraph?.insertEdge(String(item.id), String(dependency.id));
|
| 850 |
+
|
| 851 |
+
if (instanceOrDesc instanceof SyncDescriptor) {
|
| 852 |
+
const d: Triple = {
|
| 853 |
+
id: dependency.id,
|
| 854 |
+
desc: instanceOrDesc,
|
| 855 |
+
_trace: item._trace.branch(dependency.id, true),
|
| 856 |
+
};
|
| 857 |
+
graph.insertEdge(item, d);
|
| 858 |
+
stack.push(d);
|
| 859 |
+
}
|
| 860 |
+
}
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
while (true) {
|
| 864 |
+
const roots = graph.roots();
|
| 865 |
+
|
| 866 |
+
if (roots.length === 0) {
|
| 867 |
+
if (!graph.isEmpty()) {
|
| 868 |
+
throw new CyclicDependencyError(graph);
|
| 869 |
+
}
|
| 870 |
+
break;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
for (const { data } of roots) {
|
| 874 |
+
const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
|
| 875 |
+
if (instanceOrDesc instanceof SyncDescriptor) {
|
| 876 |
+
const instance = this._createServiceInstanceWithOwner(
|
| 877 |
+
data.id,
|
| 878 |
+
data.desc.ctor,
|
| 879 |
+
data.desc.staticArguments,
|
| 880 |
+
data._trace,
|
| 881 |
+
);
|
| 882 |
+
this._setCreatedServiceInstance(data.id, instance);
|
| 883 |
+
}
|
| 884 |
+
graph.removeNode(data);
|
| 885 |
+
}
|
| 886 |
+
}
|
| 887 |
+
return this._getServiceInstanceOrDescriptor(id) as T;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
private _createServiceInstanceWithOwner<T>(
|
| 891 |
+
id: ServiceIdentifier<T>,
|
| 892 |
+
ctor: any,
|
| 893 |
+
args: ReadonlyArray<unknown> = [],
|
| 894 |
+
_trace: Trace,
|
| 895 |
+
): T {
|
| 896 |
+
if (this._services.get(id) instanceof SyncDescriptor) {
|
| 897 |
+
return this._createServiceInstance(id, ctor, args, _trace);
|
| 898 |
+
}
|
| 899 |
+
if (this._parent) {
|
| 900 |
+
return this._parent._createServiceInstanceWithOwner(
|
| 901 |
+
id,
|
| 902 |
+
ctor,
|
| 903 |
+
args,
|
| 904 |
+
_trace,
|
| 905 |
+
);
|
| 906 |
+
}
|
| 907 |
+
throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
private _createServiceInstance<T>(
|
| 911 |
+
id: ServiceIdentifier<T>,
|
| 912 |
+
ctor: any,
|
| 913 |
+
args: ReadonlyArray<unknown> = [],
|
| 914 |
+
_trace: Trace,
|
| 915 |
+
): T {
|
| 916 |
+
const root = this._root();
|
| 917 |
+
root._inProgress.push(id);
|
| 918 |
+
try {
|
| 919 |
+
const result = this._createInstance<T>(ctor, args.slice(), _trace, {
|
| 920 |
+
id,
|
| 921 |
+
config: this._services.configOf(id),
|
| 922 |
+
});
|
| 923 |
+
this.dependencyGraph.addInstance(result as object, this, id);
|
| 924 |
+
for (const dependency of _util.getServiceDependencies(ctor)) {
|
| 925 |
+
const kind = dependency.kind ?? 'instance';
|
| 926 |
+
if (kind === 'collection') {
|
| 927 |
+
this.dependencyGraph.addEdge(
|
| 928 |
+
result as object,
|
| 929 |
+
{ scope: this, token: dependency.id },
|
| 930 |
+
'collection',
|
| 931 |
+
);
|
| 932 |
+
continue;
|
| 933 |
+
}
|
| 934 |
+
if (kind === 'ref') {
|
| 935 |
+
continue;
|
| 936 |
+
}
|
| 937 |
+
const owner = this._ownerOf(dependency.id);
|
| 938 |
+
if (owner !== undefined) {
|
| 939 |
+
this.dependencyGraph.addEdge(
|
| 940 |
+
result as object,
|
| 941 |
+
{ scope: owner, token: dependency.id },
|
| 942 |
+
'instance',
|
| 943 |
+
);
|
| 944 |
+
}
|
| 945 |
+
}
|
| 946 |
+
const entry = this._ledger.register(() => {
|
| 947 |
+
this._instanceEntries.delete(result);
|
| 948 |
+
this.dependencyGraph.removeInstance(result as object);
|
| 949 |
+
if (isDisposable(result)) {
|
| 950 |
+
const out = result.dispose() as unknown as void | Promise<void>;
|
| 951 |
+
return out;
|
| 952 |
+
}
|
| 953 |
+
return undefined;
|
| 954 |
+
}, `service:${String(id)}`);
|
| 955 |
+
this._instanceEntries.set(result, entry);
|
| 956 |
+
this.cascade.observedMaterialization(id);
|
| 957 |
+
return result;
|
| 958 |
+
} finally {
|
| 959 |
+
const popIdx = root._inProgress.lastIndexOf(id);
|
| 960 |
+
if (popIdx >= 0) {
|
| 961 |
+
root._inProgress.splice(popIdx, 1);
|
| 962 |
+
}
|
| 963 |
+
}
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
|
| 967 |
+
if (this._services.get(id) instanceof SyncDescriptor) {
|
| 968 |
+
this._services.materialize(id, instance);
|
| 969 |
+
} else if (this._parent) {
|
| 970 |
+
this._parent._setCreatedServiceInstance(id, instance);
|
| 971 |
+
} else {
|
| 972 |
+
throw new Error(
|
| 973 |
+
`illegal state - setting UNKNOWN service instance '${String(id)}'`,
|
| 974 |
+
);
|
| 975 |
+
}
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
private _getServiceInstanceOrDescriptor<T>(
|
| 979 |
+
id: ServiceIdentifier<T>,
|
| 980 |
+
): T | SyncDescriptor<T> | undefined {
|
| 981 |
+
const instanceOrDesc = this._services.get(id);
|
| 982 |
+
if (instanceOrDesc === undefined && this._parent) {
|
| 983 |
+
return this._parent._getServiceInstanceOrDescriptor(id);
|
| 984 |
+
}
|
| 985 |
+
return instanceOrDesc;
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
private _throwIfStrict(msg: string, printWarning: boolean): void {
|
| 989 |
+
if (printWarning) {
|
| 990 |
+
globalThis.console.warn(msg);
|
| 991 |
+
}
|
| 992 |
+
if (this._strict) {
|
| 993 |
+
throw new Error(msg);
|
| 994 |
+
}
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
private _root(): InstantiationService {
|
| 998 |
+
return this._parent?._root() ?? this;
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
private _assertNotDisposed(): void {
|
| 1002 |
+
if (this._disposed) {
|
| 1003 |
+
throw new Error('InstantiationService has been disposed');
|
| 1004 |
+
}
|
| 1005 |
+
}
|
| 1006 |
+
}
|
packages/agent-core-v2/src/_base/di/lifecycle.ts
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { onUnexpectedError } from '../errors/unexpectedError';
|
| 2 |
+
import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
| 3 |
+
|
| 4 |
+
export interface IDisposableDebugLabel {
|
| 5 |
+
readonly debugLabel?: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
function disposableLabel(d: IDisposable): string {
|
| 9 |
+
const debugLabel = (d as IDisposableDebugLabel).debugLabel;
|
| 10 |
+
if (typeof debugLabel === 'string' && debugLabel.length > 0) {
|
| 11 |
+
return debugLabel;
|
| 12 |
+
}
|
| 13 |
+
return `disposable:${d.constructor?.name ?? 'anonymous'}`;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface IDisposableTracker {
|
| 17 |
+
trackDisposable(disposable: IDisposable): void;
|
| 18 |
+
setParent(child: IDisposable, parent: IDisposable | null): void;
|
| 19 |
+
markAsDisposed(disposable: IDisposable): void;
|
| 20 |
+
markAsSingleton(disposable: IDisposable): void;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
interface DisposableInfo {
|
| 24 |
+
value: IDisposable;
|
| 25 |
+
source: string | null;
|
| 26 |
+
parent: IDisposable | null;
|
| 27 |
+
isSingleton: boolean;
|
| 28 |
+
idx: number;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export class DisposableTracker implements IDisposableTracker {
|
| 32 |
+
private static idx = 0;
|
| 33 |
+
private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
|
| 34 |
+
|
| 35 |
+
private getDisposableData(d: IDisposable): DisposableInfo {
|
| 36 |
+
let val = this.livingDisposables.get(d);
|
| 37 |
+
if (!val) {
|
| 38 |
+
val = {
|
| 39 |
+
parent: null,
|
| 40 |
+
source: null,
|
| 41 |
+
isSingleton: false,
|
| 42 |
+
value: d,
|
| 43 |
+
idx: DisposableTracker.idx++,
|
| 44 |
+
};
|
| 45 |
+
this.livingDisposables.set(d, val);
|
| 46 |
+
}
|
| 47 |
+
return val;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
trackDisposable(d: IDisposable): void {
|
| 51 |
+
const data = this.getDisposableData(d);
|
| 52 |
+
data.source ??= new Error('Disposable tracking').stack ?? null;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
setParent(child: IDisposable, parent: IDisposable | null): void {
|
| 56 |
+
this.getDisposableData(child).parent = parent;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
markAsDisposed(x: IDisposable): void {
|
| 60 |
+
this.livingDisposables.delete(x);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
markAsSingleton(d: IDisposable): void {
|
| 64 |
+
this.getDisposableData(d).isSingleton = true;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
private getRootParent(
|
| 68 |
+
data: DisposableInfo,
|
| 69 |
+
cache: Map<DisposableInfo, DisposableInfo>,
|
| 70 |
+
): DisposableInfo {
|
| 71 |
+
const cached = cache.get(data);
|
| 72 |
+
if (cached) return cached;
|
| 73 |
+
const result = data.parent
|
| 74 |
+
? this.getRootParent(this.getDisposableData(data.parent), cache)
|
| 75 |
+
: data;
|
| 76 |
+
cache.set(data, result);
|
| 77 |
+
return result;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
getTrackedDisposables(): IDisposable[] {
|
| 81 |
+
const cache = new Map<DisposableInfo, DisposableInfo>();
|
| 82 |
+
return [...this.livingDisposables.entries()]
|
| 83 |
+
.filter(
|
| 84 |
+
([, v]) => v.source !== null && !this.getRootParent(v, cache).isSingleton,
|
| 85 |
+
)
|
| 86 |
+
.map(([k]) => k);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
let disposableTracker: IDisposableTracker | null = null;
|
| 91 |
+
|
| 92 |
+
export function setDisposableTracker(tracker: IDisposableTracker | null): void {
|
| 93 |
+
disposableTracker = tracker;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
export function trackDisposable<T extends IDisposable>(x: T): T {
|
| 97 |
+
disposableTracker?.trackDisposable(x);
|
| 98 |
+
return x;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
export function markAsDisposed(disposable: IDisposable): void {
|
| 102 |
+
disposableTracker?.markAsDisposed(disposable);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function setParentOfDisposable(
|
| 106 |
+
child: IDisposable,
|
| 107 |
+
parent: IDisposable | null,
|
| 108 |
+
): void {
|
| 109 |
+
disposableTracker?.setParent(child, parent);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function setParentOfDisposables(
|
| 113 |
+
children: IDisposable[],
|
| 114 |
+
parent: IDisposable | null,
|
| 115 |
+
): void {
|
| 116 |
+
if (!disposableTracker) return;
|
| 117 |
+
for (const child of children) {
|
| 118 |
+
disposableTracker.setParent(child, parent);
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export function markAsSingleton<T extends IDisposable>(singleton: T): T {
|
| 123 |
+
disposableTracker?.markAsSingleton(singleton);
|
| 124 |
+
return singleton;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
export interface IDisposable {
|
| 128 |
+
dispose(): void;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
export function isDisposable<E>(thing: E): thing is E & IDisposable {
|
| 132 |
+
return (
|
| 133 |
+
typeof thing === 'object' &&
|
| 134 |
+
thing !== null &&
|
| 135 |
+
typeof (thing as unknown as IDisposable).dispose === 'function' &&
|
| 136 |
+
(thing as unknown as IDisposable).dispose.length === 0
|
| 137 |
+
);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
export function dispose<T extends IDisposable>(disposable: T): T;
|
| 141 |
+
export function dispose<T extends IDisposable>(
|
| 142 |
+
disposable: T | undefined,
|
| 143 |
+
): T | undefined;
|
| 144 |
+
export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(
|
| 145 |
+
disposables: A,
|
| 146 |
+
): A;
|
| 147 |
+
export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
|
| 148 |
+
export function dispose<T extends IDisposable>(
|
| 149 |
+
disposables: ReadonlyArray<T>,
|
| 150 |
+
): ReadonlyArray<T>;
|
| 151 |
+
export function dispose<T extends IDisposable>(
|
| 152 |
+
arg: T | Iterable<T> | undefined,
|
| 153 |
+
): unknown {
|
| 154 |
+
if (arg === undefined || arg === null) return arg;
|
| 155 |
+
if (isIterable<T>(arg)) {
|
| 156 |
+
const errors: unknown[] = [];
|
| 157 |
+
for (const d of arg) {
|
| 158 |
+
if (d) {
|
| 159 |
+
try {
|
| 160 |
+
d.dispose();
|
| 161 |
+
} catch (error) {
|
| 162 |
+
errors.push(error);
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
if (errors.length === 1) {
|
| 168 |
+
throw errors[0];
|
| 169 |
+
}
|
| 170 |
+
if (errors.length > 1) {
|
| 171 |
+
throw new AggregateError(
|
| 172 |
+
errors,
|
| 173 |
+
'Encountered errors while disposing of store',
|
| 174 |
+
);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
return Array.isArray(arg) ? [] : arg;
|
| 178 |
+
}
|
| 179 |
+
(arg).dispose();
|
| 180 |
+
return arg;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
function isIterable<T>(arg: unknown): arg is Iterable<T> {
|
| 184 |
+
return (
|
| 185 |
+
typeof arg === 'object' &&
|
| 186 |
+
arg !== null &&
|
| 187 |
+
typeof (arg as { [Symbol.iterator]?: unknown })[Symbol.iterator] === 'function'
|
| 188 |
+
);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
export function disposeIfDisposable<T extends IDisposable | object>(
|
| 192 |
+
disposables: Array<T>,
|
| 193 |
+
): Array<T> {
|
| 194 |
+
const disposableValues: IDisposable[] = [];
|
| 195 |
+
for (const d of disposables) {
|
| 196 |
+
if (isDisposable(d)) {
|
| 197 |
+
disposableValues.push(d);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
dispose(disposableValues);
|
| 201 |
+
return [];
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
class FunctionDisposable implements IDisposable {
|
| 205 |
+
private _isDisposed = false;
|
| 206 |
+
private readonly _fn: () => void;
|
| 207 |
+
|
| 208 |
+
constructor(fn: () => void) {
|
| 209 |
+
this._fn = fn;
|
| 210 |
+
trackDisposable(this);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
dispose(): void {
|
| 214 |
+
if (this._isDisposed) return;
|
| 215 |
+
this._isDisposed = true;
|
| 216 |
+
markAsDisposed(this);
|
| 217 |
+
this._fn();
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
export function toDisposable(fn: () => void): IDisposable {
|
| 222 |
+
return new FunctionDisposable(fn);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
|
| 226 |
+
const parent = toDisposable(() => dispose(disposables));
|
| 227 |
+
setParentOfDisposables(disposables, parent);
|
| 228 |
+
return parent;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
export class DisposableStore implements IDisposable {
|
| 232 |
+
private readonly _ledger = new Ledger('DisposableStore');
|
| 233 |
+
private readonly _entries = new Map<IDisposable, LedgerEntry>();
|
| 234 |
+
private _isDisposed = false;
|
| 235 |
+
|
| 236 |
+
constructor() {
|
| 237 |
+
trackDisposable(this);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
get ledger(): Ledger {
|
| 241 |
+
return this._ledger;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
add<T extends IDisposable>(d: T): T {
|
| 245 |
+
if ((d as unknown as DisposableStore) === this) {
|
| 246 |
+
throw new Error('Cannot register a disposable on itself!');
|
| 247 |
+
}
|
| 248 |
+
setParentOfDisposable(d, this);
|
| 249 |
+
if (this._isDisposed) {
|
| 250 |
+
d.dispose();
|
| 251 |
+
return d;
|
| 252 |
+
}
|
| 253 |
+
if (!this._entries.has(d)) {
|
| 254 |
+
this._entries.set(
|
| 255 |
+
d,
|
| 256 |
+
this._ledger.register(() => {
|
| 257 |
+
d.dispose();
|
| 258 |
+
}, disposableLabel(d)),
|
| 259 |
+
);
|
| 260 |
+
}
|
| 261 |
+
return d;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
delete<T extends IDisposable>(d: T): void {
|
| 265 |
+
if (this._isDisposed) return;
|
| 266 |
+
if ((d as unknown as DisposableStore) === this) {
|
| 267 |
+
throw new Error('Cannot dispose a disposable on itself!');
|
| 268 |
+
}
|
| 269 |
+
const entry = this._entries.get(d);
|
| 270 |
+
if (entry) {
|
| 271 |
+
this._entries.delete(d);
|
| 272 |
+
entry.release();
|
| 273 |
+
}
|
| 274 |
+
d.dispose();
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
deleteAndLeak<T extends IDisposable>(d: T): void {
|
| 278 |
+
if (this._isDisposed) return;
|
| 279 |
+
const entry = this._entries.get(d);
|
| 280 |
+
if (entry) {
|
| 281 |
+
this._entries.delete(d);
|
| 282 |
+
entry.release();
|
| 283 |
+
setParentOfDisposable(d, null);
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
clear(): void {
|
| 288 |
+
if (this._entries.size === 0) return;
|
| 289 |
+
try {
|
| 290 |
+
void this._ledger.clear('scope-close');
|
| 291 |
+
} finally {
|
| 292 |
+
this._entries.clear();
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
dispose(): void {
|
| 297 |
+
if (this._isDisposed) return;
|
| 298 |
+
this._isDisposed = true;
|
| 299 |
+
markAsDisposed(this);
|
| 300 |
+
try {
|
| 301 |
+
void this._ledger.teardown('scope-close');
|
| 302 |
+
} finally {
|
| 303 |
+
this._entries.clear();
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
get isDisposed(): boolean {
|
| 308 |
+
return this._isDisposed;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
assertNotDisposed(): void {
|
| 312 |
+
if (this._isDisposed) {
|
| 313 |
+
onUnexpectedError(new Error('Object disposed'));
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
export abstract class Disposable implements IDisposable {
|
| 319 |
+
protected readonly _store = new DisposableStore();
|
| 320 |
+
|
| 321 |
+
constructor() {
|
| 322 |
+
trackDisposable(this);
|
| 323 |
+
setParentOfDisposable(this._store, this);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
protected _register<T extends IDisposable>(d: T): T {
|
| 327 |
+
if ((d as unknown as Disposable) === this) {
|
| 328 |
+
throw new Error('Cannot register a disposable on itself!');
|
| 329 |
+
}
|
| 330 |
+
return this._store.add(d);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
dispose(): void {
|
| 334 |
+
markAsDisposed(this);
|
| 335 |
+
this._store.dispose();
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
export namespace Disposable {
|
| 340 |
+
export const None: IDisposable = Object.freeze({
|
| 341 |
+
dispose(): void {},
|
| 342 |
+
});
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
export class MutableDisposable<T extends IDisposable> implements IDisposable {
|
| 346 |
+
private readonly _ledger = new Ledger('MutableDisposable');
|
| 347 |
+
private _entry: LedgerEntry | undefined;
|
| 348 |
+
private _value: T | undefined;
|
| 349 |
+
private _isDisposed = false;
|
| 350 |
+
|
| 351 |
+
constructor() {
|
| 352 |
+
trackDisposable(this);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
get value(): T | undefined {
|
| 356 |
+
return this._isDisposed ? undefined : this._value;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
set value(value: T | undefined) {
|
| 360 |
+
if (this._isDisposed) {
|
| 361 |
+
if (value !== undefined) {
|
| 362 |
+
value.dispose();
|
| 363 |
+
}
|
| 364 |
+
return;
|
| 365 |
+
}
|
| 366 |
+
if (this._value === value) return;
|
| 367 |
+
this._entry?.release();
|
| 368 |
+
this._entry = undefined;
|
| 369 |
+
this._value?.dispose();
|
| 370 |
+
if (value) setParentOfDisposable(value, this);
|
| 371 |
+
this._value = value;
|
| 372 |
+
if (value) {
|
| 373 |
+
this._entry = this._ledger.register(() => {
|
| 374 |
+
value.dispose();
|
| 375 |
+
}, disposableLabel(value));
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
dispose(): void {
|
| 380 |
+
if (this._isDisposed) return;
|
| 381 |
+
this._isDisposed = true;
|
| 382 |
+
markAsDisposed(this);
|
| 383 |
+
const entry = this._entry;
|
| 384 |
+
const prev = this._value;
|
| 385 |
+
this._entry = undefined;
|
| 386 |
+
this._value = undefined;
|
| 387 |
+
entry?.release();
|
| 388 |
+
void this._ledger.teardown('scope-close');
|
| 389 |
+
if (prev !== undefined) {
|
| 390 |
+
prev.dispose();
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
clear(): void {
|
| 395 |
+
if (this._isDisposed) return;
|
| 396 |
+
this.value = undefined;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
clearAndLeak(): T | undefined {
|
| 400 |
+
if (this._isDisposed) return undefined;
|
| 401 |
+
const prev = this._value;
|
| 402 |
+
this._value = undefined;
|
| 403 |
+
this._entry?.release();
|
| 404 |
+
this._entry = undefined;
|
| 405 |
+
if (prev !== undefined) setParentOfDisposable(prev, null);
|
| 406 |
+
return prev;
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
export class MandatoryMutableDisposable<T extends IDisposable> implements IDisposable {
|
| 411 |
+
private readonly _disposable = new MutableDisposable<T>();
|
| 412 |
+
private _isDisposed = false;
|
| 413 |
+
|
| 414 |
+
constructor(initialValue: T) {
|
| 415 |
+
this._disposable.value = initialValue;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
get value(): T {
|
| 419 |
+
return this._disposable.value!;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
set value(value: T) {
|
| 423 |
+
if (this._isDisposed || value === this._disposable.value) return;
|
| 424 |
+
this._disposable.value = value;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
dispose(): void {
|
| 428 |
+
if (this._isDisposed) return;
|
| 429 |
+
this._isDisposed = true;
|
| 430 |
+
this._disposable.dispose();
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
export class RefCountedDisposable {
|
| 435 |
+
private _counter = 1;
|
| 436 |
+
|
| 437 |
+
constructor(private readonly _disposable: IDisposable) {}
|
| 438 |
+
|
| 439 |
+
acquire(): this {
|
| 440 |
+
this._counter += 1;
|
| 441 |
+
return this;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
release(): this {
|
| 445 |
+
this._counter -= 1;
|
| 446 |
+
if (this._counter === 0) {
|
| 447 |
+
this._disposable.dispose();
|
| 448 |
+
}
|
| 449 |
+
return this;
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
export interface IReference<T> extends IDisposable {
|
| 454 |
+
readonly object: T;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
export abstract class ReferenceCollection<T> {
|
| 458 |
+
private readonly references = new Map<
|
| 459 |
+
string,
|
| 460 |
+
{ readonly object: T; counter: number }
|
| 461 |
+
>();
|
| 462 |
+
|
| 463 |
+
acquire(key: string, ...args: unknown[]): IReference<T> {
|
| 464 |
+
let reference = this.references.get(key);
|
| 465 |
+
if (!reference) {
|
| 466 |
+
reference = {
|
| 467 |
+
counter: 0,
|
| 468 |
+
object: this.createReferencedObject(key, ...args),
|
| 469 |
+
};
|
| 470 |
+
this.references.set(key, reference);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
const { object } = reference;
|
| 474 |
+
let disposed = false;
|
| 475 |
+
const dispose = () => {
|
| 476 |
+
if (disposed) return;
|
| 477 |
+
disposed = true;
|
| 478 |
+
reference.counter -= 1;
|
| 479 |
+
if (reference.counter === 0) {
|
| 480 |
+
this.destroyReferencedObject(key, reference.object);
|
| 481 |
+
this.references.delete(key);
|
| 482 |
+
}
|
| 483 |
+
};
|
| 484 |
+
|
| 485 |
+
reference.counter += 1;
|
| 486 |
+
return { object, dispose };
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
protected abstract createReferencedObject(key: string, ...args: unknown[]): T;
|
| 490 |
+
protected abstract destroyReferencedObject(key: string, object: T): void;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
export class AsyncReferenceCollection<T> {
|
| 494 |
+
constructor(private readonly referenceCollection: ReferenceCollection<Promise<T>>) {}
|
| 495 |
+
|
| 496 |
+
async acquire(key: string, ...args: unknown[]): Promise<IReference<T>> {
|
| 497 |
+
const ref = this.referenceCollection.acquire(key, ...args);
|
| 498 |
+
|
| 499 |
+
try {
|
| 500 |
+
const object = await ref.object;
|
| 501 |
+
return {
|
| 502 |
+
object,
|
| 503 |
+
dispose: () => { ref.dispose(); },
|
| 504 |
+
};
|
| 505 |
+
} catch (error) {
|
| 506 |
+
ref.dispose();
|
| 507 |
+
throw error;
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
export class ImmortalReference<T> implements IReference<T> {
|
| 513 |
+
constructor(public readonly object: T) {}
|
| 514 |
+
dispose(): void {}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
export class DisposableMap<K, V extends IDisposable = IDisposable>
|
| 518 |
+
implements IDisposable
|
| 519 |
+
{
|
| 520 |
+
private readonly _store: Map<K, V>;
|
| 521 |
+
private _isDisposed = false;
|
| 522 |
+
|
| 523 |
+
constructor(store: Map<K, V> = new Map<K, V>()) {
|
| 524 |
+
this._store = store;
|
| 525 |
+
trackDisposable(this);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
dispose(): void {
|
| 529 |
+
if (this._isDisposed) return;
|
| 530 |
+
this._isDisposed = true;
|
| 531 |
+
markAsDisposed(this);
|
| 532 |
+
this.clearAndDisposeAll();
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
clearAndDisposeAll(): void {
|
| 536 |
+
if (this._store.size === 0) return;
|
| 537 |
+
try {
|
| 538 |
+
dispose(this._store.values());
|
| 539 |
+
} finally {
|
| 540 |
+
this._store.clear();
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
has(key: K): boolean {
|
| 545 |
+
return this._store.has(key);
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
get size(): number {
|
| 549 |
+
return this._store.size;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
get(key: K): V | undefined {
|
| 553 |
+
return this._store.get(key);
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
set(key: K, value: V, skipDisposeOnOverwrite = false): void {
|
| 557 |
+
if (this._isDisposed) {
|
| 558 |
+
console.warn(
|
| 559 |
+
new Error(
|
| 560 |
+
'Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!',
|
| 561 |
+
).stack,
|
| 562 |
+
);
|
| 563 |
+
return;
|
| 564 |
+
}
|
| 565 |
+
if (!skipDisposeOnOverwrite) {
|
| 566 |
+
const prev = this._store.get(key);
|
| 567 |
+
if (prev !== undefined && prev !== value) {
|
| 568 |
+
prev.dispose();
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
this._store.set(key, value);
|
| 572 |
+
setParentOfDisposable(value, this);
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
deleteAndDispose(key: K): void {
|
| 576 |
+
const value = this._store.get(key);
|
| 577 |
+
if (value !== undefined) {
|
| 578 |
+
value.dispose();
|
| 579 |
+
}
|
| 580 |
+
this._store.delete(key);
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
deleteAndLeak(key: K): V | undefined {
|
| 584 |
+
const value = this._store.get(key);
|
| 585 |
+
if (value !== undefined) setParentOfDisposable(value, null);
|
| 586 |
+
this._store.delete(key);
|
| 587 |
+
return value;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
keys(): IterableIterator<K> {
|
| 591 |
+
return this._store.keys();
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
values(): IterableIterator<V> {
|
| 595 |
+
return this._store.values();
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
[Symbol.iterator](): IterableIterator<[K, V]> {
|
| 599 |
+
return this._store[Symbol.iterator]();
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
export class DisposableSet<V extends IDisposable = IDisposable>
|
| 604 |
+
implements IDisposable
|
| 605 |
+
{
|
| 606 |
+
private readonly _store: Set<V>;
|
| 607 |
+
private _isDisposed = false;
|
| 608 |
+
|
| 609 |
+
constructor(store: Set<V> = new Set<V>()) {
|
| 610 |
+
this._store = store;
|
| 611 |
+
trackDisposable(this);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
dispose(): void {
|
| 615 |
+
if (this._isDisposed) return;
|
| 616 |
+
this._isDisposed = true;
|
| 617 |
+
markAsDisposed(this);
|
| 618 |
+
this.clearAndDisposeAll();
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
clearAndDisposeAll(): void {
|
| 622 |
+
if (this._store.size === 0) return;
|
| 623 |
+
try {
|
| 624 |
+
dispose(this._store.values());
|
| 625 |
+
} finally {
|
| 626 |
+
this._store.clear();
|
| 627 |
+
}
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
has(value: V): boolean {
|
| 631 |
+
return this._store.has(value);
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
get size(): number {
|
| 635 |
+
return this._store.size;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
add(value: V): void {
|
| 639 |
+
if (this._isDisposed) {
|
| 640 |
+
console.warn(
|
| 641 |
+
new Error(
|
| 642 |
+
'Trying to add a disposable to a DisposableSet that has already been disposed of. The added object will be leaked!',
|
| 643 |
+
).stack,
|
| 644 |
+
);
|
| 645 |
+
return;
|
| 646 |
+
}
|
| 647 |
+
this._store.add(value);
|
| 648 |
+
setParentOfDisposable(value, this);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
deleteAndDispose(value: V): void {
|
| 652 |
+
if (this._store.delete(value)) {
|
| 653 |
+
value.dispose();
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
deleteAndLeak(value: V): V | undefined {
|
| 658 |
+
if (this._store.delete(value)) {
|
| 659 |
+
setParentOfDisposable(value, null);
|
| 660 |
+
return value;
|
| 661 |
+
}
|
| 662 |
+
return undefined;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
values(): IterableIterator<V> {
|
| 666 |
+
return this._store.values();
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
[Symbol.iterator](): IterableIterator<V> {
|
| 670 |
+
return this._store[Symbol.iterator]();
|
| 671 |
+
}
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
|
| 675 |
+
const store = new DisposableStore();
|
| 676 |
+
try {
|
| 677 |
+
fn(store);
|
| 678 |
+
} finally {
|
| 679 |
+
store.dispose();
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
export function thenIfNotDisposed<T>(
|
| 684 |
+
promise: Promise<T>,
|
| 685 |
+
then: (result: T) => void,
|
| 686 |
+
): IDisposable {
|
| 687 |
+
let disposed = false;
|
| 688 |
+
void promise.then((result) => {
|
| 689 |
+
if (disposed) return;
|
| 690 |
+
then(result);
|
| 691 |
+
});
|
| 692 |
+
return toDisposable(() => {
|
| 693 |
+
disposed = true;
|
| 694 |
+
});
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
export function thenRegisterOrDispose<T extends IDisposable>(
|
| 698 |
+
promise: Promise<T>,
|
| 699 |
+
store: DisposableStore,
|
| 700 |
+
): Promise<T> {
|
| 701 |
+
return promise.then((disposable) => {
|
| 702 |
+
if (store.isDisposed) {
|
| 703 |
+
disposable.dispose();
|
| 704 |
+
} else {
|
| 705 |
+
store.add(disposable);
|
| 706 |
+
}
|
| 707 |
+
return disposable;
|
| 708 |
+
});
|
| 709 |
+
}
|
packages/agent-core-v2/src/_base/di/scope.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BugIndicatingError } from '../errors/errors';
|
| 2 |
+
import { SyncDescriptor } from './descriptors';
|
| 3 |
+
import { ScopeActivation, type ProvideAllEntry } from './instantiation';
|
| 4 |
+
import type { ServiceIdentifier, ServicesAccessor, IInstantiationService } from './instantiation';
|
| 5 |
+
import { InstantiationService } from './instantiationService';
|
| 6 |
+
import { DisposableStore, type IDisposable } from './lifecycle';
|
| 7 |
+
import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
| 8 |
+
import { ServiceCollection } from './serviceCollection';
|
| 9 |
+
import { watchScopeUnits } from './scopeUnits';
|
| 10 |
+
|
| 11 |
+
export { ScopeActivation };
|
| 12 |
+
|
| 13 |
+
export type ScopeKind = string;
|
| 14 |
+
|
| 15 |
+
let _scopeTopology: readonly string[] | undefined;
|
| 16 |
+
|
| 17 |
+
export function setScopeTopology(kinds: readonly string[]): void {
|
| 18 |
+
if (_scopeTopology === undefined) {
|
| 19 |
+
_scopeTopology = [...kinds];
|
| 20 |
+
return;
|
| 21 |
+
}
|
| 22 |
+
if (
|
| 23 |
+
_scopeTopology.length === kinds.length &&
|
| 24 |
+
_scopeTopology.every((kind, index) => kind === kinds[index])
|
| 25 |
+
) {
|
| 26 |
+
return;
|
| 27 |
+
}
|
| 28 |
+
throw new BugIndicatingError(
|
| 29 |
+
`scope topology already declared as [${_scopeTopology.join(', ')}]; cannot redeclare as [${kinds.join(', ')}]`,
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export interface ScopedEntry {
|
| 34 |
+
readonly scope: ScopeKind;
|
| 35 |
+
readonly id: ServiceIdentifier<unknown>;
|
| 36 |
+
readonly descriptor: SyncDescriptor<unknown>;
|
| 37 |
+
readonly domain: string;
|
| 38 |
+
readonly activation: ScopeActivation;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const _scopedRegistry: ScopedEntry[] = [];
|
| 42 |
+
|
| 43 |
+
function findScopedEntryIndex(scope: ScopeKind, id: ServiceIdentifier<unknown>): number {
|
| 44 |
+
return _scopedRegistry.findIndex((entry) => entry.scope === scope && entry.id === id);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export function registerScopedService<T>(
|
| 48 |
+
scope: ScopeKind,
|
| 49 |
+
id: ServiceIdentifier<T>,
|
| 50 |
+
ctor: new (...args: any[]) => T,
|
| 51 |
+
activation: ScopeActivation = ScopeActivation.OnScopeCreated,
|
| 52 |
+
domain: string = 'unknown',
|
| 53 |
+
): void {
|
| 54 |
+
const existing = findScopedEntryIndex(scope, id as ServiceIdentifier<unknown>);
|
| 55 |
+
if (existing !== -1) {
|
| 56 |
+
throw new BugIndicatingError(
|
| 57 |
+
`duplicate scoped service registration for '${String(id)}' in scope '${scope}' (registered domain '${_scopedRegistry[existing]?.domain}', attempted domain '${domain}'); use overrideScopedService for intentional replacement`,
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
const descriptor = new SyncDescriptor<T>(ctor);
|
| 61 |
+
_scopedRegistry.push({
|
| 62 |
+
scope,
|
| 63 |
+
id: id as ServiceIdentifier<unknown>,
|
| 64 |
+
descriptor: descriptor as SyncDescriptor<unknown>,
|
| 65 |
+
domain,
|
| 66 |
+
activation,
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function overrideScopedService<T>(
|
| 71 |
+
scope: ScopeKind,
|
| 72 |
+
id: ServiceIdentifier<T>,
|
| 73 |
+
ctor: new (...args: any[]) => T,
|
| 74 |
+
activation: ScopeActivation = ScopeActivation.OnScopeCreated,
|
| 75 |
+
domain: string = 'unknown',
|
| 76 |
+
): void {
|
| 77 |
+
const index = findScopedEntryIndex(scope, id as ServiceIdentifier<unknown>);
|
| 78 |
+
if (index === -1) {
|
| 79 |
+
throw new BugIndicatingError(
|
| 80 |
+
`overrideScopedService found no registration for '${String(id)}' in scope '${scope}' (domain '${domain}'); use registerScopedService for the initial registration`,
|
| 81 |
+
);
|
| 82 |
+
}
|
| 83 |
+
const descriptor = new SyncDescriptor<T>(ctor);
|
| 84 |
+
_scopedRegistry[index] = {
|
| 85 |
+
scope,
|
| 86 |
+
id: id as ServiceIdentifier<unknown>,
|
| 87 |
+
descriptor: descriptor as SyncDescriptor<unknown>,
|
| 88 |
+
domain,
|
| 89 |
+
activation,
|
| 90 |
+
};
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
export function getScopedServiceDescriptors(scope: ScopeKind): ReadonlyArray<ScopedEntry> {
|
| 94 |
+
return _scopedRegistry.filter((entry) => entry.scope === scope);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export function _clearScopedRegistryForTests(): void {
|
| 98 |
+
_scopedRegistry.length = 0;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
export type ScopeSeed = ReadonlyArray<
|
| 102 |
+
readonly [ServiceIdentifier<any>, unknown]
|
| 103 |
+
>;
|
| 104 |
+
|
| 105 |
+
export interface ScopeOptions {
|
| 106 |
+
readonly id?: string;
|
| 107 |
+
readonly seeds?: ScopeSeed;
|
| 108 |
+
readonly configureContainer?: (container: InstantiationService) => void;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
export interface IScopeHandle<K extends ScopeKind = ScopeKind> {
|
| 112 |
+
readonly id: string;
|
| 113 |
+
readonly kind: K;
|
| 114 |
+
readonly accessor: ServicesAccessor;
|
| 115 |
+
dispose(): void | Promise<void>;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export type IAppScopeHandle = IScopeHandle<'app'>;
|
| 119 |
+
export type ISessionScopeHandle = IScopeHandle<'session'>;
|
| 120 |
+
export type IAgentScopeHandle = IScopeHandle<'agent'>;
|
| 121 |
+
|
| 122 |
+
function buildCollection(seeds?: ScopeSeed): ServiceCollection {
|
| 123 |
+
const collection = new ServiceCollection();
|
| 124 |
+
if (seeds) {
|
| 125 |
+
for (const [id, value] of seeds) {
|
| 126 |
+
collection.set(id, value);
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
return collection;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
function provideScopeServices(
|
| 133 |
+
instantiation: IInstantiationService,
|
| 134 |
+
kind: ScopeKind,
|
| 135 |
+
collection: ServiceCollection,
|
| 136 |
+
): void {
|
| 137 |
+
const entries: ProvideAllEntry[] = [];
|
| 138 |
+
for (const entry of _scopedRegistry) {
|
| 139 |
+
if (entry.scope !== kind || collection.get(entry.id) !== undefined) {
|
| 140 |
+
continue;
|
| 141 |
+
}
|
| 142 |
+
entries.push({
|
| 143 |
+
id: entry.id,
|
| 144 |
+
descriptor: entry.descriptor,
|
| 145 |
+
options: {
|
| 146 |
+
activation: entry.activation === ScopeActivation.OnDemand ? 'ondemand' : 'eager',
|
| 147 |
+
},
|
| 148 |
+
});
|
| 149 |
+
}
|
| 150 |
+
instantiation.provideAll(entries);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
export function createScopedChildHandle(
|
| 154 |
+
parent: IInstantiationService,
|
| 155 |
+
kind: ScopeKind,
|
| 156 |
+
id: string,
|
| 157 |
+
options: ScopeOptions = {},
|
| 158 |
+
): IScopeHandle {
|
| 159 |
+
const collection = buildCollection(options.seeds);
|
| 160 |
+
const child = parent.createChild(collection);
|
| 161 |
+
(child as InstantiationService).debugLabel = id;
|
| 162 |
+
const engine = (child as InstantiationService).cascade;
|
| 163 |
+
engine.suspendActivation();
|
| 164 |
+
try {
|
| 165 |
+
watchScopeUnits(child as InstantiationService, kind);
|
| 166 |
+
options.configureContainer?.(child as InstantiationService);
|
| 167 |
+
provideScopeServices(child, kind, collection);
|
| 168 |
+
} catch (error) {
|
| 169 |
+
child.dispose();
|
| 170 |
+
throw error;
|
| 171 |
+
} finally {
|
| 172 |
+
engine.resumeActivation();
|
| 173 |
+
}
|
| 174 |
+
const accessor: ServicesAccessor = {
|
| 175 |
+
get: <T>(serviceId: ServiceIdentifier<T>): T =>
|
| 176 |
+
child.invokeFunction((a) => a.get(serviceId)),
|
| 177 |
+
};
|
| 178 |
+
return { id, kind, accessor, dispose: () => child.disposeAsync() };
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
export class Scope implements IDisposable {
|
| 182 |
+
readonly children = new Map<string, Scope>();
|
| 183 |
+
readonly accessor: ServicesAccessor;
|
| 184 |
+
|
| 185 |
+
private readonly _store = new DisposableStore();
|
| 186 |
+
private readonly _ledger: Ledger;
|
| 187 |
+
private _ledgerEntry: LedgerEntry | undefined;
|
| 188 |
+
private _disposed = false;
|
| 189 |
+
|
| 190 |
+
private constructor(
|
| 191 |
+
readonly id: string,
|
| 192 |
+
readonly kind: ScopeKind,
|
| 193 |
+
readonly instantiation: IInstantiationService,
|
| 194 |
+
private readonly _parent?: Scope,
|
| 195 |
+
) {
|
| 196 |
+
this._ledger = new Ledger(`scope:${id}`);
|
| 197 |
+
this._ledger.register(() => {
|
| 198 |
+
this.instantiation.dispose();
|
| 199 |
+
}, 'instantiation');
|
| 200 |
+
this._ledger.register(() => {
|
| 201 |
+
this._store.dispose();
|
| 202 |
+
}, 'store');
|
| 203 |
+
this.accessor = {
|
| 204 |
+
get: <T>(serviceId: ServiceIdentifier<T>): T =>
|
| 205 |
+
instantiation.invokeFunction((a) => a.get(serviceId)),
|
| 206 |
+
};
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
get ledger(): Ledger {
|
| 210 |
+
return this._ledger;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
static createApp(options: ScopeOptions = {}): Scope {
|
| 214 |
+
const kind: ScopeKind = 'app';
|
| 215 |
+
const collection = buildCollection(options.seeds);
|
| 216 |
+
const instantiation = new InstantiationService(collection, true);
|
| 217 |
+
instantiation.debugLabel = options.id ?? 'app';
|
| 218 |
+
instantiation.cascade.suspendActivation();
|
| 219 |
+
try {
|
| 220 |
+
watchScopeUnits(instantiation, kind);
|
| 221 |
+
options.configureContainer?.(instantiation);
|
| 222 |
+
provideScopeServices(instantiation, kind, collection);
|
| 223 |
+
} catch (error) {
|
| 224 |
+
instantiation.dispose();
|
| 225 |
+
throw error;
|
| 226 |
+
} finally {
|
| 227 |
+
instantiation.cascade.resumeActivation();
|
| 228 |
+
}
|
| 229 |
+
return new Scope(options.id ?? 'app', kind, instantiation);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
private _assertNotDisposed(): void {
|
| 233 |
+
if (this._disposed) {
|
| 234 |
+
throw new Error(`Scope '${this.id}' has been disposed`);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
createChild(kind: ScopeKind, id: string, options: ScopeOptions = {}): Scope {
|
| 239 |
+
this._assertNotDisposed();
|
| 240 |
+
if (_scopeTopology !== undefined) {
|
| 241 |
+
const parentIndex = _scopeTopology.indexOf(this.kind);
|
| 242 |
+
const childIndex = _scopeTopology.indexOf(kind);
|
| 243 |
+
if (parentIndex === -1 || childIndex === -1 || childIndex <= parentIndex) {
|
| 244 |
+
throw new Error(
|
| 245 |
+
`child scope kind '${kind}' must be greater than parent kind '${this.kind}' in the declared scope topology`,
|
| 246 |
+
);
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
if (this.children.has(id)) {
|
| 250 |
+
throw new Error(`Scope '${this.id}' already has a child with id '${id}'`);
|
| 251 |
+
}
|
| 252 |
+
const collection = buildCollection(options.seeds);
|
| 253 |
+
const childInstantiation = this.instantiation.createChild(collection);
|
| 254 |
+
(childInstantiation as InstantiationService).debugLabel = id;
|
| 255 |
+
const engine = (childInstantiation as InstantiationService).cascade;
|
| 256 |
+
engine.suspendActivation();
|
| 257 |
+
try {
|
| 258 |
+
watchScopeUnits(childInstantiation as InstantiationService, kind);
|
| 259 |
+
options.configureContainer?.(childInstantiation as InstantiationService);
|
| 260 |
+
provideScopeServices(childInstantiation, kind, collection);
|
| 261 |
+
} catch (error) {
|
| 262 |
+
childInstantiation.dispose();
|
| 263 |
+
throw error;
|
| 264 |
+
} finally {
|
| 265 |
+
engine.resumeActivation();
|
| 266 |
+
}
|
| 267 |
+
const child = new Scope(id, kind, childInstantiation, this);
|
| 268 |
+
this.children.set(id, child);
|
| 269 |
+
child._ledgerEntry = this._ledger.register(() => {
|
| 270 |
+
child.dispose();
|
| 271 |
+
}, `scope:${id}`);
|
| 272 |
+
return child;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
toHandle(): IScopeHandle {
|
| 276 |
+
return { id: this.id, kind: this.kind, accessor: this.accessor, dispose: () => this.dispose() };
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
dispose(): void {
|
| 280 |
+
if (this._disposed) {
|
| 281 |
+
return;
|
| 282 |
+
}
|
| 283 |
+
this._disposed = true;
|
| 284 |
+
|
| 285 |
+
this._ledgerEntry?.release();
|
| 286 |
+
this._ledgerEntry = undefined;
|
| 287 |
+
try {
|
| 288 |
+
void this._ledger.teardown('scope-close');
|
| 289 |
+
} finally {
|
| 290 |
+
this.children.clear();
|
| 291 |
+
if (this._parent) {
|
| 292 |
+
this._parent.children.delete(this.id);
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
export function createAppScope(options: ScopeOptions = {}): Scope {
|
| 299 |
+
return Scope.createApp(options);
|
| 300 |
+
}
|
packages/agent-core-v2/src/_base/di/scopeUnits.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { onUnexpectedError } from '../errors/unexpectedError';
|
| 2 |
+
import type { IDisposable } from './lifecycle';
|
| 3 |
+
import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
| 4 |
+
import type { StoredRecord } from './collection';
|
| 5 |
+
import {
|
| 6 |
+
FiberRuntime,
|
| 7 |
+
isClassRecipe,
|
| 8 |
+
ScopeUnits,
|
| 9 |
+
type EffectBody,
|
| 10 |
+
type ServiceRecipe,
|
| 11 |
+
} from './fiber';
|
| 12 |
+
import type { InstantiationService } from './instantiationService';
|
| 13 |
+
import type { ScopeKind } from './scope';
|
| 14 |
+
|
| 15 |
+
export function watchScopeUnits(container: InstantiationService, kind: ScopeKind): void {
|
| 16 |
+
if (container.cascadeDisposed) {
|
| 17 |
+
return;
|
| 18 |
+
}
|
| 19 |
+
const token = ScopeUnits(kind);
|
| 20 |
+
const host = container.fiberHost;
|
| 21 |
+
const view = host.collectionView(token);
|
| 22 |
+
const foldLedger = new Ledger(`scope-units:${kind}`);
|
| 23 |
+
container.anchorKernelEntry((reason) => foldLedger.teardown(reason), `scope-units:${kind}`);
|
| 24 |
+
|
| 25 |
+
const materialized = new Map<number, () => void | Promise<void>>();
|
| 26 |
+
|
| 27 |
+
const materialize = (record: StoredRecord): void => {
|
| 28 |
+
const recipe = record.value as ServiceRecipe;
|
| 29 |
+
const name = record.providerName;
|
| 30 |
+
const unitLedger = new Ledger(`scope-units:${kind}:${name}`);
|
| 31 |
+
try {
|
| 32 |
+
if (isClassRecipe(recipe)) {
|
| 33 |
+
const instance = host.constructService(recipe, undefined) as Partial<IDisposable>;
|
| 34 |
+
unitLedger.register(() => {
|
| 35 |
+
return instance.dispose?.();
|
| 36 |
+
}, `unit:${name}`);
|
| 37 |
+
} else {
|
| 38 |
+
const facade = new FiberRuntime(
|
| 39 |
+
host,
|
| 40 |
+
unitLedger,
|
| 41 |
+
name,
|
| 42 |
+
undefined,
|
| 43 |
+
undefined,
|
| 44 |
+
new Set(recipe.inject ?? []),
|
| 45 |
+
undefined,
|
| 46 |
+
);
|
| 47 |
+
const out =
|
| 48 |
+
typeof recipe === 'function'
|
| 49 |
+
? recipe(facade, undefined)
|
| 50 |
+
: recipe.apply(facade, undefined);
|
| 51 |
+
unitLedger.effect((() => out) as EffectBody, `effect:${name}`);
|
| 52 |
+
}
|
| 53 |
+
} catch (error) {
|
| 54 |
+
void unitLedger.teardown('unload');
|
| 55 |
+
onUnexpectedError(error);
|
| 56 |
+
return;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
let retracted = false;
|
| 60 |
+
let providerEntry: LedgerEntry | undefined;
|
| 61 |
+
const retract = (): void | Promise<void> => {
|
| 62 |
+
if (retracted) {
|
| 63 |
+
return undefined;
|
| 64 |
+
}
|
| 65 |
+
retracted = true;
|
| 66 |
+
providerEntry?.release();
|
| 67 |
+
providerEntry = undefined;
|
| 68 |
+
materialized.delete(record.id);
|
| 69 |
+
return unitLedger.teardown('unload');
|
| 70 |
+
};
|
| 71 |
+
if (!record.providerBook.isActive) {
|
| 72 |
+
void retract();
|
| 73 |
+
return;
|
| 74 |
+
}
|
| 75 |
+
providerEntry = record.providerBook.register(() => {
|
| 76 |
+
void retract();
|
| 77 |
+
}, `scope-units:${kind}`);
|
| 78 |
+
foldLedger.register(() => {
|
| 79 |
+
return retract();
|
| 80 |
+
}, `record:${name}`);
|
| 81 |
+
materialized.set(record.id, retract);
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
const reconcile = (): void => {
|
| 85 |
+
if (!foldLedger.isActive) {
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
const records = container.collectionStore.storedRecordsFor(token, container);
|
| 89 |
+
const seen = new Set<number>();
|
| 90 |
+
for (const record of records) {
|
| 91 |
+
seen.add(record.id);
|
| 92 |
+
if (!materialized.has(record.id)) {
|
| 93 |
+
materialize(record);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
for (const [id, retract] of Array.from(materialized)) {
|
| 97 |
+
if (!seen.has(id)) {
|
| 98 |
+
void retract();
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
const subscription = view.onDidChange(() => {
|
| 104 |
+
reconcile();
|
| 105 |
+
});
|
| 106 |
+
foldLedger.register(() => {
|
| 107 |
+
subscription.dispose();
|
| 108 |
+
}, 'view-subscription');
|
| 109 |
+
reconcile();
|
| 110 |
+
}
|
packages/agent-core-v2/src/_base/di/service.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Emitter } from '../event';
|
| 2 |
+
import type { EffectBody } from '../lifecycle/disposer';
|
| 3 |
+
import type { Ledger } from '../lifecycle/ledger';
|
| 4 |
+
import type { CollectionToken } from './collection';
|
| 5 |
+
import {
|
| 6 |
+
currentConstruction,
|
| 7 |
+
FiberProtocolError,
|
| 8 |
+
FiberState,
|
| 9 |
+
PendingFiberHandle,
|
| 10 |
+
SERVICE_MARK,
|
| 11 |
+
type BufferedOp,
|
| 12 |
+
type Fiber,
|
| 13 |
+
type FiberHandle,
|
| 14 |
+
type FiberProvideOptions,
|
| 15 |
+
type FiberRuntime,
|
| 16 |
+
type RecipeStatics,
|
| 17 |
+
type ServiceClassRecipe,
|
| 18 |
+
type ServiceRecipe,
|
| 19 |
+
type UnitInternals,
|
| 20 |
+
} from './fiber';
|
| 21 |
+
import type { ServiceIdentifier, LiveRef } from './instantiation';
|
| 22 |
+
import { Disposable } from './lifecycle';
|
| 23 |
+
|
| 24 |
+
export abstract class Service extends Disposable implements Fiber, UnitInternals {
|
| 25 |
+
private __unitBuffer: BufferedOp[] | null;
|
| 26 |
+
private __unitRuntime: FiberRuntime | undefined;
|
| 27 |
+
|
| 28 |
+
readonly name: string;
|
| 29 |
+
readonly state: FiberState = FiberState.Active;
|
| 30 |
+
readonly config: unknown;
|
| 31 |
+
|
| 32 |
+
constructor() {
|
| 33 |
+
super();
|
| 34 |
+
const frame = currentConstruction();
|
| 35 |
+
if (
|
| 36 |
+
frame !== undefined &&
|
| 37 |
+
frame.ctor === (new.target as unknown as new (...args: any[]) => any)
|
| 38 |
+
) {
|
| 39 |
+
this.__unitBuffer = [];
|
| 40 |
+
this.config = frame.config;
|
| 41 |
+
} else {
|
| 42 |
+
this.__unitBuffer = null;
|
| 43 |
+
this.config = undefined;
|
| 44 |
+
}
|
| 45 |
+
this.name = (this.constructor as any).name || 'anonymous';
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
provide<T>(
|
| 49 |
+
id: ServiceIdentifier<T>,
|
| 50 |
+
recipe: ServiceClassRecipe,
|
| 51 |
+
opts?: FiberProvideOptions,
|
| 52 |
+
): FiberHandle<T>;
|
| 53 |
+
provide<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T>;
|
| 54 |
+
provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
|
| 55 |
+
provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
|
| 56 |
+
provide(
|
| 57 |
+
first: any,
|
| 58 |
+
second?: any,
|
| 59 |
+
third?: FiberProvideOptions,
|
| 60 |
+
): FiberHandle {
|
| 61 |
+
if (this.__unitBuffer !== null) {
|
| 62 |
+
const pending = new PendingFiberHandle(this._pendingName(first));
|
| 63 |
+
this.__unitBuffer.push((runtime) => {
|
| 64 |
+
pending.attach(runtime.provide(first, second, third));
|
| 65 |
+
});
|
| 66 |
+
return pending;
|
| 67 |
+
}
|
| 68 |
+
return this._runtime().provide(first, second, third);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
effect(body: EffectBody, label?: string): FiberHandle {
|
| 72 |
+
if (this.__unitBuffer !== null) {
|
| 73 |
+
const pending = new PendingFiberHandle(label ?? `effect:${this.name}`);
|
| 74 |
+
this.__unitBuffer.push((runtime) => {
|
| 75 |
+
pending.attach(runtime.effect(body, label));
|
| 76 |
+
});
|
| 77 |
+
return pending;
|
| 78 |
+
}
|
| 79 |
+
return this._runtime().effect(body, label);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle {
|
| 83 |
+
const label = typeof event === 'string' ? `on:${event}` : 'on:emitter';
|
| 84 |
+
if (this.__unitBuffer !== null) {
|
| 85 |
+
const pending = new PendingFiberHandle(label);
|
| 86 |
+
this.__unitBuffer.push((runtime) => {
|
| 87 |
+
pending.attach(runtime.on(event, handler));
|
| 88 |
+
});
|
| 89 |
+
return pending;
|
| 90 |
+
}
|
| 91 |
+
return this._runtime().on(event, handler);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
get<T>(id: ServiceIdentifier<T>): T {
|
| 95 |
+
return this._runtime().get(id);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
ref<T>(id: ServiceIdentifier<T>): LiveRef<T> {
|
| 99 |
+
return this._runtime().ref(id);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
get unitBook(): Ledger {
|
| 103 |
+
return this._store.ledger;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
takeUnitBuffer(): BufferedOp[] | null {
|
| 107 |
+
const buffer = this.__unitBuffer;
|
| 108 |
+
this.__unitBuffer = null;
|
| 109 |
+
return buffer;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
setUnitRuntime(runtime: FiberRuntime): void {
|
| 113 |
+
this.__unitRuntime = runtime;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
private _runtime(): FiberRuntime {
|
| 117 |
+
if (this.__unitRuntime === undefined) {
|
| 118 |
+
if (this.__unitBuffer !== null) {
|
| 119 |
+
throw new FiberProtocolError(
|
| 120 |
+
`unit '${this.name}': get/ref/provide reads are not available during construction — declare dependencies as constructor parameters (构造期只写不读)`,
|
| 121 |
+
);
|
| 122 |
+
}
|
| 123 |
+
throw new FiberProtocolError(
|
| 124 |
+
`unit '${this.name}': no unit runtime is bound — a Service created by manual \`new\` has no capabilities; construct it through the container`,
|
| 125 |
+
);
|
| 126 |
+
}
|
| 127 |
+
return this.__unitRuntime;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
private _pendingName(first: any): string {
|
| 131 |
+
if (typeof first === 'function') {
|
| 132 |
+
return (first as RecipeStatics).name ?? String(first);
|
| 133 |
+
}
|
| 134 |
+
return this.name;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
(Service.prototype as unknown as Record<symbol, unknown>)[SERVICE_MARK] = true;
|
packages/agent-core-v2/src/_base/di/serviceCollection.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Emitter } from '../event';
|
| 2 |
+
import { SyncDescriptor } from './descriptors';
|
| 3 |
+
import type { ServiceIdentifier } from './instantiation';
|
| 4 |
+
import type { IDisposable } from './lifecycle';
|
| 5 |
+
|
| 6 |
+
export interface ServiceCollectionEntry<T = unknown> {
|
| 7 |
+
readonly value: T | SyncDescriptor<T>;
|
| 8 |
+
readonly uid: number;
|
| 9 |
+
readonly config?: unknown;
|
| 10 |
+
readonly recipe?: SyncDescriptor<T>;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface AvailabilityChange {
|
| 14 |
+
readonly oldUid: number | undefined;
|
| 15 |
+
readonly newUid: number | undefined;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export class ServiceCollection {
|
| 19 |
+
private readonly _entries = new Map<ServiceIdentifier<any>, ServiceCollectionEntry<any>>();
|
| 20 |
+
private readonly _emitters = new Map<
|
| 21 |
+
ServiceIdentifier<any>,
|
| 22 |
+
Emitter<AvailabilityChange>
|
| 23 |
+
>();
|
| 24 |
+
private _nextUid = 0;
|
| 25 |
+
|
| 26 |
+
constructor(
|
| 27 |
+
...entries: ReadonlyArray<readonly [ServiceIdentifier<any>, unknown]>
|
| 28 |
+
) {
|
| 29 |
+
for (const [id, value] of entries) {
|
| 30 |
+
this.set(id, value);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
set<T>(
|
| 35 |
+
id: ServiceIdentifier<T>,
|
| 36 |
+
instanceOrDescriptor: T | SyncDescriptor<T>,
|
| 37 |
+
options?: { readonly config?: unknown },
|
| 38 |
+
): T | SyncDescriptor<T> | undefined {
|
| 39 |
+
const prev = this._entries.get(id);
|
| 40 |
+
const uid = ++this._nextUid;
|
| 41 |
+
this._entries.set(id, {
|
| 42 |
+
value: instanceOrDescriptor,
|
| 43 |
+
uid,
|
| 44 |
+
config: options?.config,
|
| 45 |
+
});
|
| 46 |
+
this._emitterFor(id).fire({ oldUid: prev?.uid, newUid: uid });
|
| 47 |
+
return prev?.value as T | SyncDescriptor<T> | undefined;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
materialize<T>(id: ServiceIdentifier<T>, instance: T): void {
|
| 51 |
+
const prev = this._entries.get(id);
|
| 52 |
+
if (prev === undefined || !(prev.value instanceof SyncDescriptor)) {
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
this._entries.set(id, {
|
| 56 |
+
value: instance,
|
| 57 |
+
uid: prev.uid,
|
| 58 |
+
config: prev.config,
|
| 59 |
+
recipe: prev.value,
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
unmaterialize<T>(id: ServiceIdentifier<T>): void {
|
| 64 |
+
const prev = this._entries.get(id);
|
| 65 |
+
if (prev === undefined || prev.recipe === undefined) {
|
| 66 |
+
return;
|
| 67 |
+
}
|
| 68 |
+
this._entries.set(id, {
|
| 69 |
+
value: prev.recipe,
|
| 70 |
+
uid: prev.uid,
|
| 71 |
+
config: prev.config,
|
| 72 |
+
});
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
setConfig<T>(id: ServiceIdentifier<T>, config: unknown): void {
|
| 76 |
+
const prev = this._entries.get(id);
|
| 77 |
+
if (prev === undefined) {
|
| 78 |
+
return;
|
| 79 |
+
}
|
| 80 |
+
this._entries.set(id, { ...prev, config });
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
delete<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> | undefined {
|
| 84 |
+
const prev = this._entries.get(id);
|
| 85 |
+
if (prev === undefined) {
|
| 86 |
+
return undefined;
|
| 87 |
+
}
|
| 88 |
+
this._entries.delete(id);
|
| 89 |
+
this._emitterFor(id).fire({ oldUid: prev.uid, newUid: undefined });
|
| 90 |
+
return prev.value as T | SyncDescriptor<T> | undefined;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
entry(id: ServiceIdentifier<any>): ServiceCollectionEntry | undefined {
|
| 94 |
+
return this._entries.get(id);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
uidOf(id: ServiceIdentifier<any>): number | undefined {
|
| 98 |
+
return this._entries.get(id)?.uid;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
configOf(id: ServiceIdentifier<any>): unknown {
|
| 102 |
+
return this._entries.get(id)?.config;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
onDidChange<T>(
|
| 106 |
+
id: ServiceIdentifier<T>,
|
| 107 |
+
listener: (change: AvailabilityChange) => void,
|
| 108 |
+
): IDisposable {
|
| 109 |
+
return this._emitterFor(id).event(listener);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
has(id: ServiceIdentifier<any>): boolean {
|
| 113 |
+
return this._entries.has(id);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> | undefined {
|
| 117 |
+
return this._entries.get(id)?.value as T | SyncDescriptor<T> | undefined;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
forEach(
|
| 121 |
+
callback: (
|
| 122 |
+
id: ServiceIdentifier<any>,
|
| 123 |
+
value: unknown,
|
| 124 |
+
) => void,
|
| 125 |
+
): void {
|
| 126 |
+
this._entries.forEach((entry, id) => { callback(id, entry.value); });
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
dispose(): void {
|
| 130 |
+
for (const emitter of this._emitters.values()) {
|
| 131 |
+
emitter.dispose();
|
| 132 |
+
}
|
| 133 |
+
this._emitters.clear();
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
private _emitterFor<T>(id: ServiceIdentifier<T>): Emitter<AvailabilityChange> {
|
| 137 |
+
let emitter = this._emitters.get(id);
|
| 138 |
+
if (emitter === undefined) {
|
| 139 |
+
emitter = new Emitter<AvailabilityChange>();
|
| 140 |
+
this._emitters.set(id, emitter);
|
| 141 |
+
}
|
| 142 |
+
return emitter;
|
| 143 |
+
}
|
| 144 |
+
}
|
packages/agent-core-v2/src/_base/di/test.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export {
|
| 2 |
+
createServices,
|
| 3 |
+
TestInstantiationService,
|
| 4 |
+
} from './testInstantiationService';
|
| 5 |
+
export type {
|
| 6 |
+
CreateServicesOptions,
|
| 7 |
+
ServiceGroup,
|
| 8 |
+
ServiceRegistration,
|
| 9 |
+
} from './testInstantiationService';
|
| 10 |
+
|
| 11 |
+
import { type ServiceIdentifier } from './instantiation';
|
| 12 |
+
import { createAppScope, createScopedChildHandle, Scope, type ScopeKind, type ScopeSeed } from './scope';
|
| 13 |
+
|
| 14 |
+
export interface ScopedTestHost {
|
| 15 |
+
readonly app: Scope;
|
| 16 |
+
child(kind: ScopeKind, id: string, stubs?: ScopeSeed): Scope;
|
| 17 |
+
childOf(parent: Scope, kind: ScopeKind, id: string, stubs?: ScopeSeed): Scope;
|
| 18 |
+
dispose(): void;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export function createScopedTestHost(appStubs: ScopeSeed = []): ScopedTestHost {
|
| 22 |
+
const app = createAppScope({ seeds: appStubs });
|
| 23 |
+
return {
|
| 24 |
+
app,
|
| 25 |
+
child(kind, id, stubs = []) {
|
| 26 |
+
if (kind === 'program') {
|
| 27 |
+
const handle = createScopedChildHandle(app.instantiation, kind, id, { seeds: stubs });
|
| 28 |
+
return {
|
| 29 |
+
id: handle.id,
|
| 30 |
+
kind: handle.kind,
|
| 31 |
+
accessor: handle.accessor,
|
| 32 |
+
dispose: () => {
|
| 33 |
+
void handle.dispose();
|
| 34 |
+
},
|
| 35 |
+
} as Scope;
|
| 36 |
+
}
|
| 37 |
+
return app.createChild(kind, id, { seeds: stubs });
|
| 38 |
+
},
|
| 39 |
+
childOf(parent, kind, id, stubs = []) {
|
| 40 |
+
return parent.createChild(kind, id, { seeds: stubs });
|
| 41 |
+
},
|
| 42 |
+
dispose() {
|
| 43 |
+
app.dispose();
|
| 44 |
+
},
|
| 45 |
+
};
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function stubPair<T>(
|
| 49 |
+
id: ServiceIdentifier<T>,
|
| 50 |
+
instance: T,
|
| 51 |
+
): readonly [ServiceIdentifier<T>, T] {
|
| 52 |
+
return [id, instance];
|
| 53 |
+
}
|
packages/agent-core-v2/src/_base/di/testInstantiationService.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as sinon from 'sinon';
|
| 2 |
+
|
| 3 |
+
import { SyncDescriptor, type SyncDescriptor0 } from './descriptors';
|
| 4 |
+
import {
|
| 5 |
+
type GetLeadingNonServiceArgs,
|
| 6 |
+
type ServiceIdentifier,
|
| 7 |
+
type ServicesAccessor,
|
| 8 |
+
} from './instantiation';
|
| 9 |
+
import { InstantiationService, Trace } from './instantiationService';
|
| 10 |
+
import { DisposableStore, dispose, isDisposable, toDisposable, type IDisposable } from './lifecycle';
|
| 11 |
+
import { ServiceCollection } from './serviceCollection';
|
| 12 |
+
|
| 13 |
+
type AnyConstructor<T = unknown> = new (...args: any[]) => T;
|
| 14 |
+
|
| 15 |
+
interface IServiceMock<T> {
|
| 16 |
+
id: ServiceIdentifier<T>;
|
| 17 |
+
service?: any;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const isSinonSpyLike = (fn: Function): fn is sinon.SinonSpy =>
|
| 21 |
+
fn && 'callCount' in fn;
|
| 22 |
+
|
| 23 |
+
export class TestInstantiationService extends InstantiationService implements IDisposable, ServicesAccessor {
|
| 24 |
+
private readonly _classStubs = new Map<Function, unknown>();
|
| 25 |
+
private readonly _parentTestService?: TestInstantiationService;
|
| 26 |
+
|
| 27 |
+
constructor(
|
| 28 |
+
private readonly _serviceCollection: ServiceCollection = new ServiceCollection(),
|
| 29 |
+
strict: boolean = false,
|
| 30 |
+
parent?: InstantiationService,
|
| 31 |
+
private readonly _properDispose: boolean = true,
|
| 32 |
+
) {
|
| 33 |
+
super(_serviceCollection, strict, parent);
|
| 34 |
+
if (parent instanceof TestInstantiationService) {
|
| 35 |
+
this._parentTestService = parent;
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
public get<T>(id: ServiceIdentifier<T>): T {
|
| 40 |
+
return super._getOrCreateServiceInstance(
|
| 41 |
+
id,
|
| 42 |
+
Trace.traceCreation(false, TestInstantiationService),
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
public set<T>(
|
| 47 |
+
id: ServiceIdentifier<T>,
|
| 48 |
+
instanceOrDescriptor: T | SyncDescriptor<T>,
|
| 49 |
+
): T | SyncDescriptor<T> | undefined {
|
| 50 |
+
const prev = this._serviceCollection.get(id);
|
| 51 |
+
this.provide(id, instanceOrDescriptor, { activation: 'ondemand' });
|
| 52 |
+
return prev;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
public mock<T>(id: ServiceIdentifier<T>): T | sinon.SinonMock {
|
| 56 |
+
return this._create({ id }, { mock: true });
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
public stubInstance<T>(ctor: AnyConstructor<T>, instance: Partial<T>): void {
|
| 60 |
+
this._classStubs.set(ctor, instance);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
protected _getClassStub(ctor: Function): unknown {
|
| 64 |
+
return this._classStubs.get(ctor) ?? this._parentTestService?._getClassStub(ctor);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
public override createInstance<T>(descriptor: SyncDescriptor0<T>): T;
|
| 68 |
+
public override createInstance<
|
| 69 |
+
Ctor extends AnyConstructor,
|
| 70 |
+
R extends InstanceType<Ctor>,
|
| 71 |
+
>(
|
| 72 |
+
ctor: Ctor,
|
| 73 |
+
...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>
|
| 74 |
+
): R;
|
| 75 |
+
public override createInstance(
|
| 76 |
+
ctorOrDescriptor: any,
|
| 77 |
+
...rest: unknown[]
|
| 78 |
+
): unknown {
|
| 79 |
+
const stub =
|
| 80 |
+
ctorOrDescriptor instanceof SyncDescriptor
|
| 81 |
+
? this._getClassStub(ctorOrDescriptor.ctor)
|
| 82 |
+
: this._getClassStub(ctorOrDescriptor);
|
| 83 |
+
|
| 84 |
+
if (stub !== undefined) {
|
| 85 |
+
return stub;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if (ctorOrDescriptor instanceof SyncDescriptor) {
|
| 89 |
+
return super.createInstance(ctorOrDescriptor, ...rest);
|
| 90 |
+
}
|
| 91 |
+
return super.createInstance(ctorOrDescriptor, ...rest);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
public stub<T>(
|
| 95 |
+
id: ServiceIdentifier<T>,
|
| 96 |
+
instanceOrDescriptor: Partial<NoInfer<T>> | SyncDescriptor<T>,
|
| 97 |
+
): T | SyncDescriptor<T>;
|
| 98 |
+
public stub<T>(id: ServiceIdentifier<T>, ctor: AnyConstructor<T>): T;
|
| 99 |
+
public stub<T, V>(
|
| 100 |
+
id: ServiceIdentifier<T>,
|
| 101 |
+
obj: Partial<NoInfer<T>> | Function,
|
| 102 |
+
property: string,
|
| 103 |
+
value: V,
|
| 104 |
+
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
| 105 |
+
public stub<T, V>(
|
| 106 |
+
id: ServiceIdentifier<T>,
|
| 107 |
+
property: string,
|
| 108 |
+
value: V,
|
| 109 |
+
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
| 110 |
+
public stub<T>(
|
| 111 |
+
id: ServiceIdentifier<T>,
|
| 112 |
+
arg2: any,
|
| 113 |
+
arg3?: string,
|
| 114 |
+
arg4?: any,
|
| 115 |
+
): T | SyncDescriptor<T> | sinon.SinonStub | sinon.SinonSpy {
|
| 116 |
+
if (arg2 instanceof SyncDescriptor && typeof arg3 !== 'string') {
|
| 117 |
+
this._serviceCollection.set(id, arg2);
|
| 118 |
+
return arg2;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (typeof arg2 !== 'string' && typeof arg3 !== 'string') {
|
| 122 |
+
const service = this._create(arg2, { stub: true }) as T;
|
| 123 |
+
this._serviceCollection.set(id, service);
|
| 124 |
+
return service;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
const service = typeof arg2 !== 'string' ? arg2 : undefined;
|
| 128 |
+
const property = typeof arg2 === 'string' ? arg2 : arg3;
|
| 129 |
+
const value = typeof arg2 === 'string' ? arg3 : arg4;
|
| 130 |
+
|
| 131 |
+
if (typeof property !== 'string') {
|
| 132 |
+
throw new TypeError('stub requires a method/property name');
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const serviceMock: IServiceMock<T> = { id, service };
|
| 136 |
+
const stubObject = this._create(serviceMock, { stub: true }, Boolean(service && !property)) as Record<string, unknown>;
|
| 137 |
+
const replacement = this._createReplacement(value);
|
| 138 |
+
|
| 139 |
+
const current = stubObject[property] as { restore?: () => void } | undefined;
|
| 140 |
+
if (current && typeof current.restore === 'function') {
|
| 141 |
+
current.restore();
|
| 142 |
+
}
|
| 143 |
+
stubObject[property] = replacement;
|
| 144 |
+
return replacement;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
public stubPromise<T>(
|
| 148 |
+
id?: ServiceIdentifier<T>,
|
| 149 |
+
fnProperty?: string,
|
| 150 |
+
value?: any,
|
| 151 |
+
): T | sinon.SinonStub;
|
| 152 |
+
public stubPromise<T, V>(
|
| 153 |
+
id?: ServiceIdentifier<T>,
|
| 154 |
+
ctor?: any,
|
| 155 |
+
fnProperty?: string,
|
| 156 |
+
value?: V,
|
| 157 |
+
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
| 158 |
+
public stubPromise<T, V>(
|
| 159 |
+
id?: ServiceIdentifier<T>,
|
| 160 |
+
obj?: any,
|
| 161 |
+
fnProperty?: string,
|
| 162 |
+
value?: V,
|
| 163 |
+
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
| 164 |
+
public stubPromise(
|
| 165 |
+
arg1?: any,
|
| 166 |
+
arg2?: any,
|
| 167 |
+
arg3?: any,
|
| 168 |
+
arg4?: any,
|
| 169 |
+
): unknown {
|
| 170 |
+
arg3 = typeof arg2 === 'string' ? Promise.resolve(arg3) : arg3;
|
| 171 |
+
arg4 = typeof arg2 !== 'string' && typeof arg3 === 'string' ? Promise.resolve(arg4) : arg4;
|
| 172 |
+
return this.stub(arg1, arg2, arg3, arg4);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
public spy<T>(id: ServiceIdentifier<T>, property: string): sinon.SinonSpy {
|
| 176 |
+
const spy = sinon.spy();
|
| 177 |
+
this.stub(id, property, spy);
|
| 178 |
+
return spy;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
private _create<T>(serviceMock: IServiceMock<T>, options: SinonOptions, reset?: boolean): T;
|
| 182 |
+
private _create<T>(ctor: any, options: SinonOptions): T | sinon.SinonMock;
|
| 183 |
+
private _create(arg1: any, options: SinonOptions, reset: boolean = false): any {
|
| 184 |
+
if (this._isServiceMock(arg1)) {
|
| 185 |
+
const service = this._getOrCreateService(arg1, options, reset);
|
| 186 |
+
if (options.mock) {
|
| 187 |
+
return sinon.mock(service);
|
| 188 |
+
}
|
| 189 |
+
this._serviceCollection.set(arg1.id, service);
|
| 190 |
+
return service;
|
| 191 |
+
}
|
| 192 |
+
return options.mock ? sinon.mock(arg1) : this._createStub(arg1);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
private _getOrCreateService<T>(
|
| 196 |
+
serviceMock: IServiceMock<T>,
|
| 197 |
+
opts: SinonOptions,
|
| 198 |
+
reset?: boolean,
|
| 199 |
+
): T {
|
| 200 |
+
const service = this._serviceCollection.get(serviceMock.id);
|
| 201 |
+
if (!reset && service && !(service instanceof SyncDescriptor)) {
|
| 202 |
+
if (opts.stub && this._hasSinonOption(service, 'stub')) {
|
| 203 |
+
return service as T;
|
| 204 |
+
}
|
| 205 |
+
if (opts.mock && this._hasSinonOption(service, 'mock')) {
|
| 206 |
+
return service as T;
|
| 207 |
+
}
|
| 208 |
+
return service as T;
|
| 209 |
+
}
|
| 210 |
+
return this._createService(serviceMock, opts);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
private _createService<T>(serviceMock: IServiceMock<T>, opts: SinonOptions): T {
|
| 214 |
+
const existing = this._serviceCollection.get(serviceMock.id);
|
| 215 |
+
const source =
|
| 216 |
+
serviceMock.service
|
| 217 |
+
?? (existing instanceof SyncDescriptor ? existing.ctor : undefined);
|
| 218 |
+
const service = this._createStub(source);
|
| 219 |
+
service.sinonOptions = opts;
|
| 220 |
+
return service as T;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
private _createStub(arg: any): any {
|
| 224 |
+
if (arg instanceof SyncDescriptor) {
|
| 225 |
+
return sinon.createStubInstance(arg.ctor);
|
| 226 |
+
}
|
| 227 |
+
if (typeof arg === 'function') {
|
| 228 |
+
return sinon.createStubInstance(arg);
|
| 229 |
+
}
|
| 230 |
+
if (arg && typeof arg === 'object') {
|
| 231 |
+
return arg;
|
| 232 |
+
}
|
| 233 |
+
return Object.create(null);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
private _createReplacement(value: any): sinon.SinonStub | sinon.SinonSpy {
|
| 237 |
+
if (typeof value === 'function') {
|
| 238 |
+
return isSinonSpyLike(value) ? value : sinon.spy(value);
|
| 239 |
+
}
|
| 240 |
+
return value ? sinon.stub().returns(value) : sinon.stub();
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
private _hasSinonOption(service: any, key: keyof SinonOptions): boolean {
|
| 244 |
+
return Boolean(service?.sinonOptions?.[key]);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
private _isServiceMock(arg: any): arg is IServiceMock<unknown> {
|
| 248 |
+
return typeof arg === 'object' && arg !== null && 'id' in arg;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
public override createChild(services: ServiceCollection): TestInstantiationService {
|
| 252 |
+
return super.createChild(services) as TestInstantiationService;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
protected override _createChildService(services: ServiceCollection): InstantiationService {
|
| 256 |
+
return new TestInstantiationService(services, false, this);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
public override dispose(): void {
|
| 260 |
+
sinon.restore();
|
| 261 |
+
if (this._properDispose) {
|
| 262 |
+
super.dispose();
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
public override disposeAsync(): Promise<void> {
|
| 267 |
+
sinon.restore();
|
| 268 |
+
if (this._properDispose) {
|
| 269 |
+
return super.disposeAsync();
|
| 270 |
+
}
|
| 271 |
+
return Promise.resolve();
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
interface SinonOptions {
|
| 276 |
+
mock?: boolean;
|
| 277 |
+
stub?: boolean;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
export interface ServiceRegistration {
|
| 281 |
+
define<T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T): void;
|
| 282 |
+
defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;
|
| 283 |
+
definePartialInstance<T>(id: ServiceIdentifier<T>, instance: Partial<T>): void;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
export type ServiceGroup = (reg: ServiceRegistration) => void;
|
| 287 |
+
|
| 288 |
+
export interface CreateServicesOptions {
|
| 289 |
+
readonly base?: readonly ServiceGroup[];
|
| 290 |
+
readonly additionalServices?: (reg: ServiceRegistration) => void;
|
| 291 |
+
readonly strict?: boolean;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
export function createServices(
|
| 295 |
+
disposables: DisposableStore,
|
| 296 |
+
options: CreateServicesOptions = {},
|
| 297 |
+
): TestInstantiationService {
|
| 298 |
+
const serviceCollection = new ServiceCollection();
|
| 299 |
+
const instanceIds = new Set<ServiceIdentifier<any>>();
|
| 300 |
+
|
| 301 |
+
const register = <T>(
|
| 302 |
+
id: ServiceIdentifier<T>,
|
| 303 |
+
value: T | Partial<T> | SyncDescriptor<T>,
|
| 304 |
+
isInstance: boolean,
|
| 305 |
+
overwrite: boolean,
|
| 306 |
+
): void => {
|
| 307 |
+
if (overwrite || !serviceCollection.has(id)) {
|
| 308 |
+
serviceCollection.set(id, value as T | SyncDescriptor<T>);
|
| 309 |
+
}
|
| 310 |
+
if (isInstance) {
|
| 311 |
+
instanceIds.add(id);
|
| 312 |
+
}
|
| 313 |
+
};
|
| 314 |
+
|
| 315 |
+
const baseReg: ServiceRegistration = {
|
| 316 |
+
define: (id, ctor) => register(id, new SyncDescriptor(ctor), false, false),
|
| 317 |
+
defineInstance: (id, instance) => register(id, instance, true, false),
|
| 318 |
+
definePartialInstance: (id, instance) => register(id, instance, true, false),
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
for (const group of options.base ?? []) {
|
| 322 |
+
group(baseReg);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
if (options.additionalServices) {
|
| 326 |
+
const overrideReg: ServiceRegistration = {
|
| 327 |
+
define: (id, ctor) => register(id, new SyncDescriptor(ctor), false, true),
|
| 328 |
+
defineInstance: (id, instance) => register(id, instance, true, true),
|
| 329 |
+
definePartialInstance: (id, instance) => register(id, instance, true, true),
|
| 330 |
+
};
|
| 331 |
+
options.additionalServices(overrideReg);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
const instantiationService = disposables.add(
|
| 335 |
+
new TestInstantiationService(serviceCollection, options.strict ?? false),
|
| 336 |
+
);
|
| 337 |
+
disposables.add(toDisposable(() => {
|
| 338 |
+
const serviceDisposables: IDisposable[] = [];
|
| 339 |
+
for (const id of instanceIds) {
|
| 340 |
+
const instance = serviceCollection.get(id);
|
| 341 |
+
if (isDisposable(instance)) {
|
| 342 |
+
serviceDisposables.push(instance);
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
dispose(serviceDisposables);
|
| 346 |
+
}));
|
| 347 |
+
return instantiationService;
|
| 348 |
+
}
|
packages/agent-core-v2/src/_base/di/util/linkedList.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class Node<E> {
|
| 2 |
+
static readonly Undefined = new Node<unknown>(undefined);
|
| 3 |
+
|
| 4 |
+
element: E;
|
| 5 |
+
next: Node<E> | typeof Node.Undefined;
|
| 6 |
+
prev: Node<E> | typeof Node.Undefined;
|
| 7 |
+
|
| 8 |
+
constructor(element: E) {
|
| 9 |
+
this.element = element;
|
| 10 |
+
this.next = Node.Undefined;
|
| 11 |
+
this.prev = Node.Undefined;
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export class LinkedList<E> {
|
| 16 |
+
private _first: Node<E> | typeof Node.Undefined = Node.Undefined;
|
| 17 |
+
private _last: Node<E> | typeof Node.Undefined = Node.Undefined;
|
| 18 |
+
private _size: number = 0;
|
| 19 |
+
|
| 20 |
+
get size(): number {
|
| 21 |
+
return this._size;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
isEmpty(): boolean {
|
| 25 |
+
return this._first === Node.Undefined;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
push(element: E): () => void {
|
| 29 |
+
const newNode = new Node(element);
|
| 30 |
+
if (this._first === Node.Undefined) {
|
| 31 |
+
this._first = newNode;
|
| 32 |
+
this._last = newNode;
|
| 33 |
+
} else {
|
| 34 |
+
const oldLast = this._last as Node<E>;
|
| 35 |
+
this._last = newNode;
|
| 36 |
+
newNode.prev = oldLast;
|
| 37 |
+
oldLast.next = newNode;
|
| 38 |
+
}
|
| 39 |
+
this._size += 1;
|
| 40 |
+
|
| 41 |
+
let didRemove = false;
|
| 42 |
+
return () => {
|
| 43 |
+
if (!didRemove) {
|
| 44 |
+
didRemove = true;
|
| 45 |
+
this._remove(newNode);
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
shift(): E | undefined {
|
| 51 |
+
if (this._first === Node.Undefined) {
|
| 52 |
+
return undefined;
|
| 53 |
+
}
|
| 54 |
+
const node = this._first as Node<E>;
|
| 55 |
+
this._remove(node);
|
| 56 |
+
return node.element;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
private _remove(node: Node<E>): void {
|
| 60 |
+
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
|
| 61 |
+
const anchor = node.prev as Node<E>;
|
| 62 |
+
anchor.next = node.next;
|
| 63 |
+
(node.next as Node<E>).prev = anchor;
|
| 64 |
+
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
|
| 65 |
+
this._first = Node.Undefined;
|
| 66 |
+
this._last = Node.Undefined;
|
| 67 |
+
} else if (node.next === Node.Undefined) {
|
| 68 |
+
this._last = (this._last as Node<E>).prev!;
|
| 69 |
+
(this._last as Node<E>).next = Node.Undefined;
|
| 70 |
+
} else if (node.prev === Node.Undefined) {
|
| 71 |
+
this._first = (this._first as Node<E>).next!;
|
| 72 |
+
(this._first as Node<E>).prev = Node.Undefined;
|
| 73 |
+
}
|
| 74 |
+
this._size -= 1;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
*[Symbol.iterator](): Iterator<E> {
|
| 78 |
+
let node = this._first;
|
| 79 |
+
while (node !== Node.Undefined) {
|
| 80 |
+
yield (node as Node<E>).element;
|
| 81 |
+
node = (node as Node<E>).next;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
packages/agent-core-v2/src/_base/errors/codes.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface ErrorInfo {
|
| 2 |
+
readonly title: string;
|
| 3 |
+
readonly retryable: boolean;
|
| 4 |
+
readonly public: boolean;
|
| 5 |
+
readonly action?: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export interface ErrorDomain {
|
| 9 |
+
readonly codes: { readonly [name: string]: string };
|
| 10 |
+
readonly retryable?: ReadonlyArray<string>;
|
| 11 |
+
readonly info?: { readonly [code: string]: ErrorInfo };
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const registeredCodes = new Map<string, object>();
|
| 15 |
+
const retryableCodes = new Set<string>();
|
| 16 |
+
const infoOverrides: { [code: string]: ErrorInfo } = {};
|
| 17 |
+
|
| 18 |
+
export function registerErrorDomain(domain: ErrorDomain): void {
|
| 19 |
+
for (const code of Object.values(domain.codes)) {
|
| 20 |
+
const owner = registeredCodes.get(code);
|
| 21 |
+
if (owner !== undefined && owner !== domain.codes) {
|
| 22 |
+
throw new Error(`error code '${code}' is registered by two different domains`);
|
| 23 |
+
}
|
| 24 |
+
registeredCodes.set(code, domain.codes);
|
| 25 |
+
}
|
| 26 |
+
for (const code of domain.retryable ?? []) {
|
| 27 |
+
retryableCodes.add(code);
|
| 28 |
+
}
|
| 29 |
+
for (const [code, info] of Object.entries(domain.info ?? {})) {
|
| 30 |
+
infoOverrides[code] = info;
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export function isErrorCode(code: unknown): code is string {
|
| 35 |
+
return typeof code === 'string' && registeredCodes.has(code);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function errorInfo(code: string): ErrorInfo {
|
| 39 |
+
const override = infoOverrides[code];
|
| 40 |
+
if (override !== undefined) return override;
|
| 41 |
+
return {
|
| 42 |
+
title: code,
|
| 43 |
+
retryable: retryableCodes.has(code),
|
| 44 |
+
public: true,
|
| 45 |
+
};
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export const CoreErrors = {
|
| 49 |
+
codes: {
|
| 50 |
+
INTERNAL: 'internal',
|
| 51 |
+
NOT_IMPLEMENTED: 'not_implemented',
|
| 52 |
+
VALIDATION_FAILED: 'validation.failed',
|
| 53 |
+
},
|
| 54 |
+
info: {
|
| 55 |
+
internal: {
|
| 56 |
+
title: 'Internal error',
|
| 57 |
+
retryable: false,
|
| 58 |
+
public: true,
|
| 59 |
+
action: 'Inspect logs or report the issue with diagnostics.',
|
| 60 |
+
},
|
| 61 |
+
not_implemented: {
|
| 62 |
+
title: 'Not implemented',
|
| 63 |
+
retryable: false,
|
| 64 |
+
public: true,
|
| 65 |
+
action: 'This feature is not implemented yet.',
|
| 66 |
+
},
|
| 67 |
+
},
|
| 68 |
+
} as const satisfies ErrorDomain;
|
| 69 |
+
|
| 70 |
+
registerErrorDomain(CoreErrors);
|
packages/agent-core-v2/src/_base/errors/errorMessage.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { isCodedError } from './serialize';
|
| 2 |
+
|
| 3 |
+
export function toErrorMessage(error: unknown, verbose = false): string {
|
| 4 |
+
if (isCodedError(error)) {
|
| 5 |
+
const base = `[${error.code}] ${error.message}`;
|
| 6 |
+
return verbose && error.details ? `${base} ${JSON.stringify(error.details)}` : base;
|
| 7 |
+
}
|
| 8 |
+
if (error instanceof Error) {
|
| 9 |
+
const base = error.message || error.name;
|
| 10 |
+
if (verbose && error.cause !== undefined) {
|
| 11 |
+
return `${base} (caused by: ${toErrorMessage(error.cause)})`;
|
| 12 |
+
}
|
| 13 |
+
return base;
|
| 14 |
+
}
|
| 15 |
+
if (typeof error === 'string') {
|
| 16 |
+
return error;
|
| 17 |
+
}
|
| 18 |
+
try {
|
| 19 |
+
return JSON.stringify(error);
|
| 20 |
+
} catch {
|
| 21 |
+
return String(error);
|
| 22 |
+
}
|
| 23 |
+
}
|
packages/agent-core-v2/src/_base/errors/errors.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { CoreErrors } from './codes';
|
| 2 |
+
import type { ErrorCode } from '#/errors';
|
| 3 |
+
|
| 4 |
+
export class ExpectedError extends Error {
|
| 5 |
+
readonly isExpected = true;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export class ErrorNoTelemetry extends Error {
|
| 9 |
+
constructor(message?: string) {
|
| 10 |
+
super(message);
|
| 11 |
+
this.name = 'CodeExpectedError';
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
static fromError(error: Error): ErrorNoTelemetry {
|
| 15 |
+
const wrapped = new ErrorNoTelemetry(error.message);
|
| 16 |
+
wrapped.stack = error.stack;
|
| 17 |
+
return wrapped;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
static isErrorNoTelemetry(error: unknown): error is ErrorNoTelemetry {
|
| 21 |
+
return error instanceof Error && error.name === 'CodeExpectedError';
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export class BugIndicatingError extends Error {
|
| 26 |
+
constructor(message?: string) {
|
| 27 |
+
super(message ?? 'An unexpected bug occurred.');
|
| 28 |
+
this.name = 'BugIndicatingError';
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export interface Error2Options {
|
| 33 |
+
readonly details?: Readonly<Record<string, unknown>>;
|
| 34 |
+
readonly cause?: unknown;
|
| 35 |
+
readonly name?: string;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export class Error2 extends Error {
|
| 39 |
+
readonly code: ErrorCode;
|
| 40 |
+
readonly details?: Readonly<Record<string, unknown>>;
|
| 41 |
+
|
| 42 |
+
constructor(code: ErrorCode, message: string, options?: Error2Options) {
|
| 43 |
+
super(message, options?.cause === undefined ? undefined : { cause: options.cause });
|
| 44 |
+
this.name = options?.name ?? 'Error2';
|
| 45 |
+
this.code = code;
|
| 46 |
+
this.details = options?.details;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export function isError2(error: unknown): error is Error2 {
|
| 51 |
+
return error instanceof Error2;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export function unwrapErrorCause(error: unknown): unknown {
|
| 55 |
+
let current = error;
|
| 56 |
+
while (current instanceof Error2 && current.cause !== undefined) {
|
| 57 |
+
current = current.cause;
|
| 58 |
+
}
|
| 59 |
+
return current;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export class NotImplementedError extends Error2 {
|
| 63 |
+
constructor(feature?: string) {
|
| 64 |
+
super(
|
| 65 |
+
CoreErrors.codes.NOT_IMPLEMENTED,
|
| 66 |
+
feature ? `Not implemented: ${feature}` : 'Not implemented',
|
| 67 |
+
);
|
| 68 |
+
this.name = 'NotImplementedError';
|
| 69 |
+
}
|
| 70 |
+
}
|
packages/agent-core-v2/src/_base/errors/serialize.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { CoreErrors, errorInfo, isErrorCode } from './codes';
|
| 2 |
+
import type { ErrorCode } from '#/errors';
|
| 3 |
+
import { Error2 } from './errors';
|
| 4 |
+
|
| 5 |
+
export interface ErrorPayload {
|
| 6 |
+
readonly code: ErrorCode;
|
| 7 |
+
readonly message: string;
|
| 8 |
+
readonly name?: string;
|
| 9 |
+
readonly details?: Readonly<Record<string, unknown>>;
|
| 10 |
+
readonly retryable: boolean;
|
| 11 |
+
readonly cause?: ErrorPayload;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export type KimiErrorPayload = ErrorPayload;
|
| 15 |
+
|
| 16 |
+
export interface CodedErrorShape {
|
| 17 |
+
readonly code: ErrorCode;
|
| 18 |
+
readonly message: string;
|
| 19 |
+
readonly name?: string;
|
| 20 |
+
readonly details?: Readonly<Record<string, unknown>>;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const MAX_CAUSE_DEPTH = 8;
|
| 24 |
+
|
| 25 |
+
export function isCodedError(error: unknown): error is CodedErrorShape {
|
| 26 |
+
if (error === null || typeof error !== 'object') {
|
| 27 |
+
return false;
|
| 28 |
+
}
|
| 29 |
+
const code = (error as { readonly code?: unknown }).code;
|
| 30 |
+
return isErrorCode(code);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function makeErrorPayload(
|
| 34 |
+
code: ErrorCode,
|
| 35 |
+
message: string,
|
| 36 |
+
options?: {
|
| 37 |
+
readonly details?: Readonly<Record<string, unknown>>;
|
| 38 |
+
readonly name?: string;
|
| 39 |
+
},
|
| 40 |
+
): ErrorPayload {
|
| 41 |
+
return {
|
| 42 |
+
code,
|
| 43 |
+
message,
|
| 44 |
+
name: options?.name,
|
| 45 |
+
details: options?.details,
|
| 46 |
+
retryable: errorInfo(code).retryable,
|
| 47 |
+
};
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export function toErrorPayload(error: unknown): ErrorPayload {
|
| 51 |
+
return toErrorPayloadAtDepth(error, 0);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function toErrorPayloadAtDepth(error: unknown, depth: number): ErrorPayload {
|
| 55 |
+
const payload = toShallowErrorPayload(error);
|
| 56 |
+
if (depth >= MAX_CAUSE_DEPTH) {
|
| 57 |
+
return payload;
|
| 58 |
+
}
|
| 59 |
+
const cause = readErrorCause(error);
|
| 60 |
+
if (cause === undefined) {
|
| 61 |
+
return payload;
|
| 62 |
+
}
|
| 63 |
+
return { ...payload, cause: toErrorPayloadAtDepth(cause, depth + 1) };
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function toShallowErrorPayload(error: unknown): ErrorPayload {
|
| 67 |
+
if (isCodedError(error)) {
|
| 68 |
+
return {
|
| 69 |
+
code: error.code,
|
| 70 |
+
message: error.message,
|
| 71 |
+
name: error.name,
|
| 72 |
+
details: error.details,
|
| 73 |
+
retryable: errorInfo(error.code).retryable,
|
| 74 |
+
};
|
| 75 |
+
}
|
| 76 |
+
if (error instanceof Error) {
|
| 77 |
+
return makeErrorPayload(CoreErrors.codes.INTERNAL, error.message, { name: error.name });
|
| 78 |
+
}
|
| 79 |
+
return makeErrorPayload(CoreErrors.codes.INTERNAL, String(error));
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function readErrorCause(error: unknown): unknown {
|
| 83 |
+
if (error === null || typeof error !== 'object') {
|
| 84 |
+
return undefined;
|
| 85 |
+
}
|
| 86 |
+
return (error as { readonly cause?: unknown }).cause;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export const toKimiErrorPayload = toErrorPayload;
|
| 90 |
+
|
| 91 |
+
export function fromErrorPayload(payload: ErrorPayload): Error2 {
|
| 92 |
+
return new Error2(payload.code, payload.message, {
|
| 93 |
+
name: payload.name,
|
| 94 |
+
details: payload.details,
|
| 95 |
+
cause: payload.cause === undefined ? undefined : fromErrorPayload(payload.cause),
|
| 96 |
+
});
|
| 97 |
+
}
|
packages/agent-core-v2/src/_base/errors/unexpectedError.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type UnexpectedErrorHandler = (err: unknown) => void;
|
| 2 |
+
|
| 3 |
+
const defaultHandler: UnexpectedErrorHandler = (err) => {
|
| 4 |
+
console.error('[unexpected]', err);
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
let currentHandler: UnexpectedErrorHandler = defaultHandler;
|
| 8 |
+
|
| 9 |
+
export function setUnexpectedErrorHandler(handler: UnexpectedErrorHandler): void {
|
| 10 |
+
currentHandler = handler;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function resetUnexpectedErrorHandler(): void {
|
| 14 |
+
currentHandler = defaultHandler;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function onUnexpectedError(err: unknown): void {
|
| 18 |
+
try {
|
| 19 |
+
currentHandler(err);
|
| 20 |
+
} catch (handlerErr) {
|
| 21 |
+
console.error('[unexpected] handler threw', handlerErr, 'while reporting', err);
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export function safelyCallListener(listener: () => void): void {
|
| 26 |
+
try {
|
| 27 |
+
listener();
|
| 28 |
+
} catch (err) {
|
| 29 |
+
onUnexpectedError(err);
|
| 30 |
+
}
|
| 31 |
+
}
|
packages/agent-core-v2/src/_base/execEnv/bufferedReadable.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Readable } from 'node:stream';
|
| 2 |
+
|
| 3 |
+
export class BufferedReadable extends Readable {
|
| 4 |
+
private readonly _source: Readable;
|
| 5 |
+
private _ended: boolean = false;
|
| 6 |
+
|
| 7 |
+
constructor(source: Readable) {
|
| 8 |
+
super({ highWaterMark: 128 * 1024 });
|
| 9 |
+
this._source = source;
|
| 10 |
+
this._source.on('data', this._onData);
|
| 11 |
+
this._source.on('end', this._onEnd);
|
| 12 |
+
this._source.on('close', this._onClose);
|
| 13 |
+
this._source.on('error', this._onError);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
override _read(): void {
|
| 17 |
+
if (!this._ended && !this.destroyed) {
|
| 18 |
+
this._source.resume();
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
|
| 23 |
+
this._source.off('data', this._onData);
|
| 24 |
+
this._source.off('end', this._onEnd);
|
| 25 |
+
this._source.off('close', this._onClose);
|
| 26 |
+
this._source.off('error', this._onError);
|
| 27 |
+
this._source.destroy();
|
| 28 |
+
callback(error);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
private readonly _onData = (chunk: string | Uint8Array): void => {
|
| 32 |
+
if (!this.push(chunk)) {
|
| 33 |
+
this._source.pause();
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
private readonly _onEnd = (): void => {
|
| 38 |
+
this._ended = true;
|
| 39 |
+
this.push(null);
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
private readonly _onClose = (): void => {
|
| 43 |
+
if (!this._ended) {
|
| 44 |
+
this._ended = true;
|
| 45 |
+
this.push(null);
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
private readonly _onError = (error: Error): void => {
|
| 50 |
+
this.destroy(error);
|
| 51 |
+
};
|
| 52 |
+
}
|
packages/agent-core-v2/src/_base/execEnv/decodeText.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type TextDecodeErrors = 'strict' | 'replace' | 'ignore';
|
| 2 |
+
|
| 3 |
+
export async function* readUtf8Lines(
|
| 4 |
+
source: AsyncIterable<Uint8Array>,
|
| 5 |
+
errors: TextDecodeErrors = 'strict',
|
| 6 |
+
): AsyncGenerator<string> {
|
| 7 |
+
let pending: Buffer[] = [];
|
| 8 |
+
let offset = 0;
|
| 9 |
+
let pendingOffset = 0;
|
| 10 |
+
for await (const bytes of source) {
|
| 11 |
+
const chunk = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
| 12 |
+
let start = 0;
|
| 13 |
+
for (let i = 0; i < chunk.length; i++) {
|
| 14 |
+
if (chunk[i] !== 0x0a) continue;
|
| 15 |
+
const piece = chunk.subarray(start, i + 1);
|
| 16 |
+
const lineOffset = pending.length === 0 ? offset + start : pendingOffset;
|
| 17 |
+
const line = pending.length === 0 ? piece : Buffer.concat([...pending, piece]);
|
| 18 |
+
yield decodeTextWithErrors(line, 'utf-8', errors, lineOffset !== 0);
|
| 19 |
+
pending = [];
|
| 20 |
+
start = i + 1;
|
| 21 |
+
}
|
| 22 |
+
if (start < chunk.length) {
|
| 23 |
+
if (pending.length === 0) pendingOffset = offset + start;
|
| 24 |
+
pending.push(Buffer.from(chunk.subarray(start)));
|
| 25 |
+
}
|
| 26 |
+
offset += chunk.length;
|
| 27 |
+
}
|
| 28 |
+
if (pending.length > 0) yield decodeTextWithErrors(Buffer.concat(pending), 'utf-8', errors, pendingOffset !== 0);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function isUtf8Continuation(byte: number): boolean {
|
| 32 |
+
return byte >= 0x80 && byte <= 0xbf;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function decodeUtf8Ignore(data: Buffer): string {
|
| 36 |
+
let output = '';
|
| 37 |
+
let i = 0;
|
| 38 |
+
|
| 39 |
+
while (i < data.length) {
|
| 40 |
+
const b0 = data[i];
|
| 41 |
+
if (b0 === undefined) break;
|
| 42 |
+
|
| 43 |
+
if (b0 <= 0x7f) {
|
| 44 |
+
output += String.fromCodePoint(b0);
|
| 45 |
+
i += 1;
|
| 46 |
+
continue;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if (b0 >= 0xc2 && b0 <= 0xdf) {
|
| 50 |
+
const b1 = data[i + 1];
|
| 51 |
+
if (b1 !== undefined && isUtf8Continuation(b1)) {
|
| 52 |
+
output += String.fromCodePoint(((b0 & 0x1f) << 6) | (b1 & 0x3f));
|
| 53 |
+
i += 2;
|
| 54 |
+
continue;
|
| 55 |
+
}
|
| 56 |
+
i += 1;
|
| 57 |
+
continue;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (b0 >= 0xe0 && b0 <= 0xef) {
|
| 61 |
+
const b1 = data[i + 1];
|
| 62 |
+
const b2 = data[i + 2];
|
| 63 |
+
const validSecond =
|
| 64 |
+
b1 !== undefined &&
|
| 65 |
+
((b0 === 0xe0 && b1 >= 0xa0 && b1 <= 0xbf) ||
|
| 66 |
+
(b0 >= 0xe1 && b0 <= 0xec && isUtf8Continuation(b1)) ||
|
| 67 |
+
(b0 === 0xed && b1 >= 0x80 && b1 <= 0x9f) ||
|
| 68 |
+
(b0 >= 0xee && b0 <= 0xef && isUtf8Continuation(b1)));
|
| 69 |
+
|
| 70 |
+
if (validSecond && b2 !== undefined && isUtf8Continuation(b2)) {
|
| 71 |
+
output += String.fromCodePoint(((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
|
| 72 |
+
i += 3;
|
| 73 |
+
continue;
|
| 74 |
+
}
|
| 75 |
+
i += 1;
|
| 76 |
+
continue;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
if (b0 >= 0xf0 && b0 <= 0xf4) {
|
| 80 |
+
const b1 = data[i + 1];
|
| 81 |
+
const b2 = data[i + 2];
|
| 82 |
+
const b3 = data[i + 3];
|
| 83 |
+
const validSecond =
|
| 84 |
+
b1 !== undefined &&
|
| 85 |
+
((b0 === 0xf0 && b1 >= 0x90 && b1 <= 0xbf) ||
|
| 86 |
+
(b0 >= 0xf1 && b0 <= 0xf3 && isUtf8Continuation(b1)) ||
|
| 87 |
+
(b0 === 0xf4 && b1 >= 0x80 && b1 <= 0x8f));
|
| 88 |
+
|
| 89 |
+
if (
|
| 90 |
+
validSecond &&
|
| 91 |
+
b2 !== undefined &&
|
| 92 |
+
b3 !== undefined &&
|
| 93 |
+
isUtf8Continuation(b2) &&
|
| 94 |
+
isUtf8Continuation(b3)
|
| 95 |
+
) {
|
| 96 |
+
output += String.fromCodePoint(
|
| 97 |
+
((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f),
|
| 98 |
+
);
|
| 99 |
+
i += 4;
|
| 100 |
+
continue;
|
| 101 |
+
}
|
| 102 |
+
i += 1;
|
| 103 |
+
continue;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
i += 1;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return output;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function decodeUtf16LeIgnore(data: Buffer): string {
|
| 113 |
+
let output = '';
|
| 114 |
+
let i = 0;
|
| 115 |
+
|
| 116 |
+
while (i + 1 < data.length) {
|
| 117 |
+
const first = data[i];
|
| 118 |
+
const second = data[i + 1];
|
| 119 |
+
if (first === undefined || second === undefined) break;
|
| 120 |
+
|
| 121 |
+
const codeUnit = first | (second << 8);
|
| 122 |
+
|
| 123 |
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
| 124 |
+
const lowFirst = data[i + 2];
|
| 125 |
+
const lowSecond = data[i + 3];
|
| 126 |
+
if (lowFirst !== undefined && lowSecond !== undefined) {
|
| 127 |
+
const low = lowFirst | (lowSecond << 8);
|
| 128 |
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
| 129 |
+
const codePoint = 0x10000 + ((codeUnit - 0xd800) << 10) + (low - 0xdc00);
|
| 130 |
+
output += String.fromCodePoint(codePoint);
|
| 131 |
+
i += 4;
|
| 132 |
+
continue;
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
i += 2;
|
| 136 |
+
continue;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
| 140 |
+
i += 2;
|
| 141 |
+
continue;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
output += String.fromCodePoint(codeUnit);
|
| 145 |
+
i += 2;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
return output;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
export function decodeTextWithErrors(
|
| 152 |
+
data: Buffer,
|
| 153 |
+
encoding: BufferEncoding,
|
| 154 |
+
errors: TextDecodeErrors = 'strict',
|
| 155 |
+
ignoreBOM: boolean = false,
|
| 156 |
+
): string {
|
| 157 |
+
let webLabel: string | undefined;
|
| 158 |
+
switch (encoding) {
|
| 159 |
+
case 'utf-8':
|
| 160 |
+
case 'utf8':
|
| 161 |
+
webLabel = 'utf-8';
|
| 162 |
+
break;
|
| 163 |
+
case 'utf16le':
|
| 164 |
+
case 'ucs2':
|
| 165 |
+
case 'ucs-2':
|
| 166 |
+
webLabel = 'utf-16le';
|
| 167 |
+
break;
|
| 168 |
+
default:
|
| 169 |
+
webLabel = undefined;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if (webLabel === undefined) {
|
| 173 |
+
return data.toString(encoding);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if (errors === 'strict') {
|
| 177 |
+
return new TextDecoder(webLabel, { fatal: true, ignoreBOM }).decode(data);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
if (errors === 'ignore') {
|
| 181 |
+
return webLabel === 'utf-8' ? decodeUtf8Ignore(data) : decodeUtf16LeIgnore(data);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
return new TextDecoder(webLabel, { fatal: false, ignoreBOM }).decode(data);
|
| 185 |
+
}
|
packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { execFile as nodeExecFile } from 'node:child_process';
|
| 2 |
+
import { constants as fsConstants } from 'node:fs';
|
| 3 |
+
import { access } from 'node:fs/promises';
|
| 4 |
+
import * as nodeOs from 'node:os';
|
| 5 |
+
import * as nodePath from 'node:path';
|
| 6 |
+
|
| 7 |
+
export type OsKind = string;
|
| 8 |
+
export type ShellName = 'bash' | 'sh';
|
| 9 |
+
export type PathClass = 'posix' | 'win32';
|
| 10 |
+
|
| 11 |
+
export class ProbeShellNotFoundError extends Error {
|
| 12 |
+
readonly checked: readonly string[];
|
| 13 |
+
|
| 14 |
+
constructor(message: string, checked: readonly string[]) {
|
| 15 |
+
super(message);
|
| 16 |
+
this.name = 'ProbeShellNotFoundError';
|
| 17 |
+
this.checked = checked;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export interface HostEnvironmentInfo {
|
| 22 |
+
readonly osKind: OsKind;
|
| 23 |
+
readonly osArch: string;
|
| 24 |
+
readonly osVersion: string;
|
| 25 |
+
readonly shellName: ShellName;
|
| 26 |
+
readonly shellPath: string;
|
| 27 |
+
readonly pathClass: PathClass;
|
| 28 |
+
readonly homeDir: string;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export interface HostEnvironmentProbeDeps {
|
| 32 |
+
readonly platform: string;
|
| 33 |
+
readonly arch: string;
|
| 34 |
+
readonly release: string;
|
| 35 |
+
readonly homeDir: string;
|
| 36 |
+
readonly env: Record<string, string | undefined>;
|
| 37 |
+
readonly isFile: (path: string) => Promise<boolean>;
|
| 38 |
+
readonly execFileText: (
|
| 39 |
+
file: string,
|
| 40 |
+
args: readonly string[],
|
| 41 |
+
timeoutMs: number,
|
| 42 |
+
) => Promise<string | undefined>;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const GIT_EXEC_PATH_TIMEOUT_MS = 5_000;
|
| 46 |
+
|
| 47 |
+
const MINGW_PREFIX_SET: ReadonlySet<string> = new Set([
|
| 48 |
+
'mingw32',
|
| 49 |
+
'mingw64',
|
| 50 |
+
'ucrt64',
|
| 51 |
+
'clang64',
|
| 52 |
+
'clangarm64',
|
| 53 |
+
]);
|
| 54 |
+
|
| 55 |
+
function resolveOsKind(platform: string): OsKind {
|
| 56 |
+
switch (platform) {
|
| 57 |
+
case 'darwin':
|
| 58 |
+
return 'macOS';
|
| 59 |
+
case 'linux':
|
| 60 |
+
return 'Linux';
|
| 61 |
+
case 'win32':
|
| 62 |
+
return 'Windows';
|
| 63 |
+
default:
|
| 64 |
+
return platform;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export async function probeHostEnvironment(
|
| 69 |
+
deps: HostEnvironmentProbeDeps,
|
| 70 |
+
): Promise<HostEnvironmentInfo> {
|
| 71 |
+
const osKind = resolveOsKind(deps.platform);
|
| 72 |
+
const osArch = deps.arch;
|
| 73 |
+
const osVersion = deps.release;
|
| 74 |
+
const pathClass: PathClass = deps.platform === 'win32' ? 'win32' : 'posix';
|
| 75 |
+
|
| 76 |
+
if (deps.platform === 'win32') {
|
| 77 |
+
const shellPath = await locateWindowsGitBash(deps);
|
| 78 |
+
return {
|
| 79 |
+
osKind,
|
| 80 |
+
osArch,
|
| 81 |
+
osVersion,
|
| 82 |
+
shellName: 'bash',
|
| 83 |
+
shellPath,
|
| 84 |
+
pathClass,
|
| 85 |
+
homeDir: deps.homeDir,
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const candidates: readonly string[] = ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash'];
|
| 90 |
+
let found: string | undefined;
|
| 91 |
+
for (const p of candidates) {
|
| 92 |
+
if (await deps.isFile(p)) {
|
| 93 |
+
found = p;
|
| 94 |
+
break;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
if (found !== undefined) {
|
| 98 |
+
return {
|
| 99 |
+
osKind,
|
| 100 |
+
osArch,
|
| 101 |
+
osVersion,
|
| 102 |
+
shellName: 'bash',
|
| 103 |
+
shellPath: found,
|
| 104 |
+
pathClass,
|
| 105 |
+
homeDir: deps.homeDir,
|
| 106 |
+
};
|
| 107 |
+
}
|
| 108 |
+
return {
|
| 109 |
+
osKind,
|
| 110 |
+
osArch,
|
| 111 |
+
osVersion,
|
| 112 |
+
shellName: 'sh',
|
| 113 |
+
shellPath: '/bin/sh',
|
| 114 |
+
pathClass,
|
| 115 |
+
homeDir: deps.homeDir,
|
| 116 |
+
};
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async function locateWindowsGitBash(deps: HostEnvironmentProbeDeps): Promise<string> {
|
| 120 |
+
const checked: string[] = [];
|
| 121 |
+
|
| 122 |
+
const override = deps.env['KIMI_SHELL_PATH']?.trim();
|
| 123 |
+
if (override !== undefined && override.length > 0) {
|
| 124 |
+
checked.push(override);
|
| 125 |
+
if (await deps.isFile(override)) {
|
| 126 |
+
return override;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
const gitExecutables = await findExecutablesOnPath(
|
| 131 |
+
'git.exe',
|
| 132 |
+
deps.env['PATH'],
|
| 133 |
+
deps.platform,
|
| 134 |
+
deps.isFile,
|
| 135 |
+
);
|
| 136 |
+
|
| 137 |
+
for (const gitExe of gitExecutables) {
|
| 138 |
+
const inferred = gitBashCandidatesFromGitExe(gitExe);
|
| 139 |
+
if (inferred !== undefined) {
|
| 140 |
+
for (const candidate of inferred) {
|
| 141 |
+
checked.push(candidate);
|
| 142 |
+
if (await deps.isFile(candidate)) {
|
| 143 |
+
return candidate;
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const gitExecPath = await readGitExecPath(deps, gitExe);
|
| 149 |
+
if (gitExecPath === undefined) {
|
| 150 |
+
continue;
|
| 151 |
+
}
|
| 152 |
+
for (const candidate of gitBashCandidatesFromGitExecPath(gitExecPath)) {
|
| 153 |
+
checked.push(candidate);
|
| 154 |
+
if (await deps.isFile(candidate)) {
|
| 155 |
+
return candidate;
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
const candidates: string[] = [
|
| 161 |
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
| 162 |
+
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
| 163 |
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
| 164 |
+
'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
|
| 165 |
+
];
|
| 166 |
+
const localAppData = deps.env['LOCALAPPDATA']?.trim();
|
| 167 |
+
if (localAppData !== undefined && localAppData.length > 0) {
|
| 168 |
+
candidates.push(`${localAppData}\\Programs\\Git\\bin\\bash.exe`);
|
| 169 |
+
candidates.push(`${localAppData}\\Programs\\Git\\usr\\bin\\bash.exe`);
|
| 170 |
+
}
|
| 171 |
+
for (const candidate of candidates) {
|
| 172 |
+
checked.push(candidate);
|
| 173 |
+
if (await deps.isFile(candidate)) {
|
| 174 |
+
return candidate;
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
throw new ProbeShellNotFoundError(
|
| 179 |
+
'Git Bash was not found on this Windows host. Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe.',
|
| 180 |
+
checked,
|
| 181 |
+
);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
async function readGitExecPath(
|
| 185 |
+
deps: HostEnvironmentProbeDeps,
|
| 186 |
+
gitExe: string,
|
| 187 |
+
): Promise<string | undefined> {
|
| 188 |
+
if (deps.platform === 'win32' && !isAbsoluteWindowsPath(gitExe)) return undefined;
|
| 189 |
+
|
| 190 |
+
const stdout = await deps.execFileText(gitExe, ['--exec-path'], GIT_EXEC_PATH_TIMEOUT_MS);
|
| 191 |
+
if (stdout === undefined) return undefined;
|
| 192 |
+
|
| 193 |
+
for (const line of stdout.split(/\r?\n/)) {
|
| 194 |
+
const execPath = line.trim();
|
| 195 |
+
if (execPath.length > 0) {
|
| 196 |
+
return execPath;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
return undefined;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function gitBashCandidatesFromGitExe(gitExe: string): readonly string[] | undefined {
|
| 203 |
+
const normalizedGitExe = nodePath.win32.normalize(normalizeWindowsPath(gitExe));
|
| 204 |
+
const gitDir = nodePath.win32.dirname(normalizedGitExe);
|
| 205 |
+
const gitDirName = nodePath.win32.basename(gitDir).toLowerCase();
|
| 206 |
+
if (gitDirName !== 'cmd' && gitDirName !== 'bin') {
|
| 207 |
+
return undefined;
|
| 208 |
+
}
|
| 209 |
+
return gitBashCandidatesFromGitRoot(nodePath.win32.dirname(gitDir));
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
function gitBashCandidatesFromGitExecPath(execPath: string): readonly string[] {
|
| 213 |
+
const normalized = nodePath.win32.normalize(normalizeWindowsPath(execPath));
|
| 214 |
+
const parts = normalized.split('\\');
|
| 215 |
+
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
| 216 |
+
const segment = parts[i]?.toLowerCase();
|
| 217 |
+
if (segment !== undefined && MINGW_PREFIX_SET.has(segment)) {
|
| 218 |
+
const root = parts.slice(0, i).join('\\');
|
| 219 |
+
if (root.length > 0) {
|
| 220 |
+
return gitBashCandidatesFromGitRoot(root);
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
return gitBashCandidatesFromGitRoot(nodePath.win32.join(normalized, '..', '..'));
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function gitBashCandidatesFromGitRoot(root: string): readonly string[] {
|
| 229 |
+
return [
|
| 230 |
+
nodePath.win32.normalize(nodePath.win32.join(root, 'bin', 'bash.exe')),
|
| 231 |
+
nodePath.win32.normalize(nodePath.win32.join(root, 'usr', 'bin', 'bash.exe')),
|
| 232 |
+
];
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
function normalizeWindowsPath(path: string): string {
|
| 236 |
+
return path.replaceAll('/', '\\');
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
function isAbsoluteWindowsPath(path: string): boolean {
|
| 240 |
+
return nodePath.win32.isAbsolute(normalizeWindowsPath(path));
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
function dedupeWindowsPaths(paths: readonly string[]): readonly string[] {
|
| 244 |
+
const deduped: string[] = [];
|
| 245 |
+
const seen = new Set<string>();
|
| 246 |
+
for (const path of paths) {
|
| 247 |
+
const key = normalizeWindowsPath(path).toLowerCase();
|
| 248 |
+
if (seen.has(key)) continue;
|
| 249 |
+
seen.add(key);
|
| 250 |
+
deduped.push(path);
|
| 251 |
+
}
|
| 252 |
+
return deduped;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
let cachedProbe: Promise<HostEnvironmentInfo> | undefined;
|
| 256 |
+
|
| 257 |
+
export function probeHostEnvironmentFromNode(): Promise<HostEnvironmentInfo> {
|
| 258 |
+
if (cachedProbe !== undefined) return cachedProbe;
|
| 259 |
+
const platform = process.platform;
|
| 260 |
+
const env = process.env as Record<string, string | undefined>;
|
| 261 |
+
const isFile = async (path: string): Promise<boolean> => {
|
| 262 |
+
try {
|
| 263 |
+
await access(path, fsConstants.F_OK);
|
| 264 |
+
return true;
|
| 265 |
+
} catch {
|
| 266 |
+
return false;
|
| 267 |
+
}
|
| 268 |
+
};
|
| 269 |
+
cachedProbe = probeHostEnvironment({
|
| 270 |
+
platform,
|
| 271 |
+
arch: process.arch,
|
| 272 |
+
release: nodeOs.release(),
|
| 273 |
+
homeDir: nodeOs.homedir(),
|
| 274 |
+
env,
|
| 275 |
+
isFile,
|
| 276 |
+
execFileText,
|
| 277 |
+
});
|
| 278 |
+
return cachedProbe;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
async function findExecutablesOnPath(
|
| 282 |
+
name: string,
|
| 283 |
+
pathEnv: string | undefined,
|
| 284 |
+
platform: string,
|
| 285 |
+
isFile: (p: string) => Promise<boolean>,
|
| 286 |
+
): Promise<readonly string[]> {
|
| 287 |
+
if (pathEnv === undefined || pathEnv.length === 0) return [];
|
| 288 |
+
const listSep = platform === 'win32' ? ';' : ':';
|
| 289 |
+
const dirSep = platform === 'win32' ? '\\' : '/';
|
| 290 |
+
const paths: string[] = [];
|
| 291 |
+
for (const rawDir of pathEnv.split(listSep)) {
|
| 292 |
+
const dir = rawDir.trim();
|
| 293 |
+
if (dir.length === 0) continue;
|
| 294 |
+
if (platform === 'win32' && !isAbsoluteWindowsPath(dir)) continue;
|
| 295 |
+
const candidate = dir.endsWith(dirSep) ? `${dir}${name}` : `${dir}${dirSep}${name}`;
|
| 296 |
+
if (await isFile(candidate)) {
|
| 297 |
+
paths.push(candidate);
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
return platform === 'win32' ? dedupeWindowsPaths(paths) : paths;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
export async function execFileText(
|
| 304 |
+
file: string,
|
| 305 |
+
args: readonly string[],
|
| 306 |
+
timeoutMs: number,
|
| 307 |
+
): Promise<string | undefined> {
|
| 308 |
+
return new Promise((resolve) => {
|
| 309 |
+
nodeExecFile(
|
| 310 |
+
file,
|
| 311 |
+
[...args],
|
| 312 |
+
{ encoding: 'utf8', timeout: timeoutMs, windowsHide: true },
|
| 313 |
+
(error, stdout) => {
|
| 314 |
+
if (error !== null) {
|
| 315 |
+
resolve(undefined);
|
| 316 |
+
return;
|
| 317 |
+
}
|
| 318 |
+
resolve(stdout);
|
| 319 |
+
},
|
| 320 |
+
);
|
| 321 |
+
});
|
| 322 |
+
}
|
packages/agent-core-v2/src/_base/execEnv/globPattern.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function globPatternToRegex(pattern: string, caseSensitive: boolean): RegExp {
|
| 2 |
+
let regex = '^';
|
| 3 |
+
for (let i = 0; i < pattern.length; i++) {
|
| 4 |
+
const ch = pattern[i];
|
| 5 |
+
if (ch === undefined) break;
|
| 6 |
+
switch (ch) {
|
| 7 |
+
case '*':
|
| 8 |
+
regex += '[^/]*';
|
| 9 |
+
break;
|
| 10 |
+
case '?':
|
| 11 |
+
regex += '[^/]';
|
| 12 |
+
break;
|
| 13 |
+
case '[': {
|
| 14 |
+
const end = pattern.indexOf(']', i + 1);
|
| 15 |
+
if (end === -1) {
|
| 16 |
+
regex += '\\[';
|
| 17 |
+
} else {
|
| 18 |
+
let charClass = pattern.slice(i + 1, end);
|
| 19 |
+
charClass = charClass.replace(/\\/g, '\\\\');
|
| 20 |
+
if (charClass.startsWith('!')) {
|
| 21 |
+
charClass = '^' + charClass.slice(1);
|
| 22 |
+
} else if (charClass.startsWith('^')) {
|
| 23 |
+
charClass = '\\' + charClass;
|
| 24 |
+
}
|
| 25 |
+
regex += '[' + charClass + ']';
|
| 26 |
+
i = end;
|
| 27 |
+
}
|
| 28 |
+
break;
|
| 29 |
+
}
|
| 30 |
+
case '\\': {
|
| 31 |
+
if (i + 1 < pattern.length) {
|
| 32 |
+
const next = pattern.charAt(i + 1);
|
| 33 |
+
regex += next.replaceAll(/[{}()+.\\[\]^$|]/g, '\\$&');
|
| 34 |
+
i++;
|
| 35 |
+
} else {
|
| 36 |
+
regex += '\\\\';
|
| 37 |
+
}
|
| 38 |
+
break;
|
| 39 |
+
}
|
| 40 |
+
default:
|
| 41 |
+
regex += ch.replaceAll(/[{}()+.\\[\]^$|]/g, '\\$&');
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
regex += '$';
|
| 45 |
+
return new RegExp(regex, caseSensitive ? '' : 'i');
|
| 46 |
+
}
|
packages/agent-core-v2/src/_base/execEnv/loginShellPath.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { userInfo } from 'node:os';
|
| 2 |
+
|
| 3 |
+
import { execFileText } from './environmentProbe';
|
| 4 |
+
|
| 5 |
+
export interface LoginShellPathDeps {
|
| 6 |
+
readonly platform: string;
|
| 7 |
+
readonly env: Record<string, string | undefined>;
|
| 8 |
+
readonly userShell: () => string | undefined;
|
| 9 |
+
readonly execFileText: (
|
| 10 |
+
file: string,
|
| 11 |
+
args: readonly string[],
|
| 12 |
+
timeoutMs: number,
|
| 13 |
+
) => Promise<string | undefined>;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const LOGIN_SHELL_ENV_TIMEOUT_MS = 5_000;
|
| 17 |
+
|
| 18 |
+
export async function probeLoginShellPath(deps: LoginShellPathDeps): Promise<string | undefined> {
|
| 19 |
+
if (deps.platform === 'win32') return undefined;
|
| 20 |
+
const envShell = deps.env['SHELL']?.trim();
|
| 21 |
+
const shell = envShell === undefined || envShell.length === 0 ? deps.userShell() : envShell;
|
| 22 |
+
if (shell === undefined || shell.length === 0) return undefined;
|
| 23 |
+
|
| 24 |
+
const stdout = await deps.execFileText(shell, ['-l', '-c', '/usr/bin/env'], LOGIN_SHELL_ENV_TIMEOUT_MS);
|
| 25 |
+
if (stdout === undefined) return undefined;
|
| 26 |
+
|
| 27 |
+
let path: string | undefined;
|
| 28 |
+
for (const line of stdout.split('\n')) {
|
| 29 |
+
if (line.startsWith('PATH=')) {
|
| 30 |
+
path = line.slice('PATH='.length).trim();
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
if (path === undefined || path.length === 0) return undefined;
|
| 34 |
+
return path;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export function mergeLoginShellPath(currentPath: string | undefined, loginShellPath: string): string {
|
| 38 |
+
const current = currentPath ?? '';
|
| 39 |
+
const seen = new Set(current.split(':').filter((entry) => entry.length > 0));
|
| 40 |
+
const additions: string[] = [];
|
| 41 |
+
for (const entry of loginShellPath.split(':')) {
|
| 42 |
+
if (!entry.startsWith('/') || seen.has(entry)) continue;
|
| 43 |
+
seen.add(entry);
|
| 44 |
+
additions.push(entry);
|
| 45 |
+
}
|
| 46 |
+
if (additions.length === 0) return current;
|
| 47 |
+
if (currentPath === undefined) return additions.join(':');
|
| 48 |
+
return `${current}:${additions.join(':')}`;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export async function applyLoginShellPath(deps: LoginShellPathDeps): Promise<void> {
|
| 52 |
+
const loginShellPath = await probeLoginShellPath(deps);
|
| 53 |
+
if (loginShellPath === undefined) return;
|
| 54 |
+
const currentPath = deps.env['PATH'];
|
| 55 |
+
const merged = mergeLoginShellPath(currentPath, loginShellPath);
|
| 56 |
+
if (merged === (currentPath ?? '')) return;
|
| 57 |
+
deps.env['PATH'] = merged;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function userShellFromNode(): string | undefined {
|
| 61 |
+
try {
|
| 62 |
+
const shell = userInfo().shell;
|
| 63 |
+
return shell === null || shell.length === 0 ? undefined : shell;
|
| 64 |
+
} catch {
|
| 65 |
+
return undefined;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
let appliedLoginShellPath: Promise<void> | undefined;
|
| 70 |
+
|
| 71 |
+
export function applyLoginShellPathFromNode(): Promise<void> {
|
| 72 |
+
if (appliedLoginShellPath !== undefined) return appliedLoginShellPath;
|
| 73 |
+
appliedLoginShellPath = applyLoginShellPath({
|
| 74 |
+
platform: process.platform,
|
| 75 |
+
env: process.env as Record<string, string | undefined>,
|
| 76 |
+
userShell: userShellFromNode,
|
| 77 |
+
execFileText,
|
| 78 |
+
});
|
| 79 |
+
return appliedLoginShellPath;
|
| 80 |
+
}
|
packages/agent-core-v2/src/_base/execEnv/shellPathBridge.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { execFileSync as nodeExecFileSync } from 'node:child_process';
|
| 2 |
+
import { existsSync } from 'node:fs';
|
| 3 |
+
import * as nodePath from 'node:path';
|
| 4 |
+
|
| 5 |
+
import type { HostEnvironmentInfo } from './environmentProbe';
|
| 6 |
+
|
| 7 |
+
export interface ShellPathBridge {
|
| 8 |
+
toShellPath(nativePath: string): string;
|
| 9 |
+
fromShellPath(path: string): string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export type ShellPathBridgeEnv = Pick<HostEnvironmentInfo, 'osKind' | 'shellName' | 'shellPath'>;
|
| 13 |
+
|
| 14 |
+
export interface ShellPathBridgeDeps {
|
| 15 |
+
readonly execFileSync: (file: string, args: readonly string[]) => string;
|
| 16 |
+
readonly isFile: (path: string) => boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const CYGPATH_TIMEOUT_MS = 5_000;
|
| 20 |
+
|
| 21 |
+
const DRIVE_COLON_RE = /^\/([a-zA-Z]):(?:[\\/]|$)/;
|
| 22 |
+
const CYGDRIVE_RE = /^\/cygdrive\/([a-zA-Z])(?:\/|$)/;
|
| 23 |
+
const DRIVE_RE = /^\/([a-zA-Z])(?:\/|$)/;
|
| 24 |
+
|
| 25 |
+
const VIRTUAL_FS_PREFIXES: readonly string[] = ['/dev/', '/proc/', '/sys/'];
|
| 26 |
+
|
| 27 |
+
const WIN32_DRIVE_ABSOLUTE_RE = /^[A-Za-z]:[\\/]/;
|
| 28 |
+
|
| 29 |
+
function joinDrive(letter: string, rest: string): string {
|
| 30 |
+
const normalizedRest = rest.replaceAll('\\', '/');
|
| 31 |
+
return normalizedRest === ''
|
| 32 |
+
? `${letter.toUpperCase()}:/`
|
| 33 |
+
: `${letter.toUpperCase()}:${normalizedRest}`;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export function translateShellDrivePath(path: string): string {
|
| 37 |
+
const colonMatch = DRIVE_COLON_RE.exec(path);
|
| 38 |
+
if (colonMatch !== null) {
|
| 39 |
+
return joinDrive(colonMatch[1]!, path.slice(3));
|
| 40 |
+
}
|
| 41 |
+
const cygdriveMatch = CYGDRIVE_RE.exec(path);
|
| 42 |
+
if (cygdriveMatch !== null) {
|
| 43 |
+
return joinDrive(cygdriveMatch[1]!, path.slice(`/cygdrive/${cygdriveMatch[1]!}`.length));
|
| 44 |
+
}
|
| 45 |
+
const driveMatch = DRIVE_RE.exec(path);
|
| 46 |
+
if (driveMatch !== null) {
|
| 47 |
+
return joinDrive(driveMatch[1]!, path.slice(2));
|
| 48 |
+
}
|
| 49 |
+
return path;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export function createShellPathBridge(
|
| 53 |
+
env: ShellPathBridgeEnv,
|
| 54 |
+
deps: ShellPathBridgeDeps,
|
| 55 |
+
): ShellPathBridge {
|
| 56 |
+
const enabled = env.osKind === 'Windows' && env.shellName === 'bash';
|
| 57 |
+
|
| 58 |
+
let cygpathExe: string | null | undefined;
|
| 59 |
+
const segmentCache = new Map<string, string>();
|
| 60 |
+
|
| 61 |
+
function locateCygpath(): string | null {
|
| 62 |
+
if (cygpathExe !== undefined) return cygpathExe;
|
| 63 |
+
const shellDir = nodePath.win32.dirname(env.shellPath);
|
| 64 |
+
const candidates = [nodePath.win32.join(shellDir, 'cygpath.exe')];
|
| 65 |
+
if (nodePath.win32.basename(shellDir).toLowerCase() === 'bin') {
|
| 66 |
+
candidates.push(nodePath.win32.join(shellDir, '..', 'usr', 'bin', 'cygpath.exe'));
|
| 67 |
+
}
|
| 68 |
+
cygpathExe = candidates.find((candidate) => deps.isFile(candidate)) ?? null;
|
| 69 |
+
return cygpathExe;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
function resolveRootSegment(firstSegment: string): string | null {
|
| 73 |
+
const cached = segmentCache.get(firstSegment);
|
| 74 |
+
if (cached !== undefined) return cached;
|
| 75 |
+
|
| 76 |
+
const exe = locateCygpath();
|
| 77 |
+
if (exe === null) return null;
|
| 78 |
+
let resolved: string;
|
| 79 |
+
try {
|
| 80 |
+
const output = deps.execFileSync(exe, ['-w', '-C', 'UTF8', '--', `/${firstSegment}`]);
|
| 81 |
+
const trimmed = output.replace(/\r?\n$/, '');
|
| 82 |
+
if (!WIN32_DRIVE_ABSOLUTE_RE.test(trimmed) && !trimmed.startsWith('\\\\')) return null;
|
| 83 |
+
resolved = trimmed.replace(/[\\/]$/, '');
|
| 84 |
+
} catch {
|
| 85 |
+
return null;
|
| 86 |
+
}
|
| 87 |
+
segmentCache.set(firstSegment, resolved);
|
| 88 |
+
return resolved;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function fromShellPath(path: string): string {
|
| 92 |
+
if (!enabled) return path;
|
| 93 |
+
|
| 94 |
+
if (path.startsWith('//')) return path;
|
| 95 |
+
|
| 96 |
+
if (path.startsWith('/')) {
|
| 97 |
+
const normalized = nodePath.posix.normalize(path);
|
| 98 |
+
const lexical = translateShellDrivePath(normalized);
|
| 99 |
+
if (lexical !== normalized) return lexical;
|
| 100 |
+
if (normalized === '/') return normalized;
|
| 101 |
+
if (VIRTUAL_FS_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return normalized;
|
| 102 |
+
const firstSegment = normalized.slice(1).split('/')[0]!;
|
| 103 |
+
const prefix = resolveRootSegment(firstSegment);
|
| 104 |
+
if (prefix === null) return normalized;
|
| 105 |
+
const remainder = normalized.slice(firstSegment.length + 1);
|
| 106 |
+
const joined = `${prefix}${remainder}`.replaceAll('\\', '/');
|
| 107 |
+
return /^[A-Za-z]:$/.test(joined) ? `${joined}/` : joined;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return path;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
function toShellPath(nativePath: string): string {
|
| 114 |
+
if (!enabled) return nativePath;
|
| 115 |
+
|
| 116 |
+
if (nativePath.startsWith('\\\\')) {
|
| 117 |
+
return nativePath.replaceAll('\\', '/');
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const driveMatch = /^([A-Za-z]):(?:[\\/]|$)/.exec(nativePath);
|
| 121 |
+
if (driveMatch !== null) {
|
| 122 |
+
const drive = driveMatch[1]!.toLowerCase();
|
| 123 |
+
const rest = nativePath.slice(2).replaceAll('\\', '/');
|
| 124 |
+
return `/${drive}${rest.startsWith('/') ? rest : `/${rest}`}`;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return nativePath.replaceAll('\\', '/');
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
return { toShellPath, fromShellPath };
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const bridgeCache = new Map<string, ShellPathBridge>();
|
| 134 |
+
|
| 135 |
+
export function getShellPathBridge(env: ShellPathBridgeEnv): ShellPathBridge {
|
| 136 |
+
const key = `${env.osKind} ${env.shellName} ${env.shellPath}`;
|
| 137 |
+
const cached = bridgeCache.get(key);
|
| 138 |
+
if (cached !== undefined) return cached;
|
| 139 |
+
const bridge = createShellPathBridge(env, {
|
| 140 |
+
execFileSync: (file, args) =>
|
| 141 |
+
nodeExecFileSync(file, [...args], {
|
| 142 |
+
encoding: 'utf8',
|
| 143 |
+
timeout: CYGPATH_TIMEOUT_MS,
|
| 144 |
+
windowsHide: true,
|
| 145 |
+
}),
|
| 146 |
+
isFile: (path) => existsSync(path),
|
| 147 |
+
});
|
| 148 |
+
bridgeCache.set(key, bridge);
|
| 149 |
+
return bridge;
|
| 150 |
+
}
|
packages/agent-core-v2/src/_base/lifecycle/disposer.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type TeardownReason = 'scope-close' | 'cascade' | 'unload';
|
| 2 |
+
|
| 3 |
+
export type Disposer = (reason: TeardownReason) => void | Promise<void>;
|
| 4 |
+
|
| 5 |
+
export type EffectResult =
|
| 6 |
+
| void
|
| 7 |
+
| Disposer
|
| 8 |
+
| Promise<Disposer | void>
|
| 9 |
+
| Iterable<Disposer | void>
|
| 10 |
+
| AsyncIterable<Disposer | void>;
|
| 11 |
+
|
| 12 |
+
export type EffectBody = () => EffectResult;
|
| 13 |
+
|
| 14 |
+
export function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
| 15 |
+
return (
|
| 16 |
+
typeof value === 'object' &&
|
| 17 |
+
value !== null &&
|
| 18 |
+
typeof (value as { then?: unknown }).then === 'function'
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export function isSyncIterable(value: unknown): value is Iterable<unknown> {
|
| 23 |
+
return (
|
| 24 |
+
typeof value === 'object' &&
|
| 25 |
+
value !== null &&
|
| 26 |
+
typeof (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] === 'function'
|
| 27 |
+
);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
| 31 |
+
return (
|
| 32 |
+
typeof value === 'object' &&
|
| 33 |
+
value !== null &&
|
| 34 |
+
typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] ===
|
| 35 |
+
'function'
|
| 36 |
+
);
|
| 37 |
+
}
|
packages/agent-core-v2/src/_base/lifecycle/errors.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export class LedgerDisposedError extends Error {
|
| 2 |
+
constructor(
|
| 3 |
+
readonly ledgerLabel: string,
|
| 4 |
+
readonly operation: string,
|
| 5 |
+
readonly ledgerState: 'disposing' | 'disposed',
|
| 6 |
+
) {
|
| 7 |
+
super(
|
| 8 |
+
`Ledger '${ledgerLabel}' is ${ledgerState}: cannot ${operation} on a ${ledgerState} ledger`,
|
| 9 |
+
);
|
| 10 |
+
this.name = 'LedgerDisposedError';
|
| 11 |
+
}
|
| 12 |
+
}
|
packages/agent-core-v2/src/_base/lifecycle/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './disposer';
|
| 2 |
+
export * from './errors';
|
| 3 |
+
export * from './ledger';
|
| 4 |
+
export * from './lifecycleMachine';
|
packages/agent-core-v2/src/_base/lifecycle/keyedResource.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface KeyedResourceGeneration {
|
| 2 |
+
readonly owner: string;
|
| 3 |
+
readonly generation: string | number;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export interface KeyedResource {
|
| 7 |
+
dispose(): void | Promise<void>;
|
| 8 |
+
abort?(reason?: unknown): void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export interface KeyedResourceLease<Resource> {
|
| 12 |
+
readonly resource: Resource;
|
| 13 |
+
release(): void;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface ResourceEntry<Resource extends KeyedResource> {
|
| 17 |
+
promise: Promise<Resource>;
|
| 18 |
+
resource?: Resource;
|
| 19 |
+
leases: number;
|
| 20 |
+
draining: boolean;
|
| 21 |
+
abortOnDrain: boolean;
|
| 22 |
+
aborted: boolean;
|
| 23 |
+
disposed: boolean;
|
| 24 |
+
drainPromise?: Promise<void>;
|
| 25 |
+
releaseDrain?: () => void;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export class KeyedResourceLeasePool<Key, Resource extends KeyedResource> {
|
| 29 |
+
private readonly entries = new Map<Key, ResourceEntry<Resource>>();
|
| 30 |
+
private withdrawn = false;
|
| 31 |
+
private withdrawal?: Promise<void>;
|
| 32 |
+
|
| 33 |
+
constructor(
|
| 34 |
+
readonly identity: KeyedResourceGeneration,
|
| 35 |
+
private readonly create: (key: Key) => Resource | Promise<Resource>,
|
| 36 |
+
) {}
|
| 37 |
+
|
| 38 |
+
acquire(key: Key): Promise<KeyedResourceLease<Resource>> {
|
| 39 |
+
if (this.withdrawn) return Promise.reject(this.unavailable());
|
| 40 |
+
let entry = this.entries.get(key);
|
| 41 |
+
if (entry === undefined) {
|
| 42 |
+
entry = this.createEntry(key);
|
| 43 |
+
this.entries.set(key, entry);
|
| 44 |
+
}
|
| 45 |
+
if (entry.draining) return Promise.reject(this.unavailable());
|
| 46 |
+
entry.leases += 1;
|
| 47 |
+
return entry.promise.then(
|
| 48 |
+
(resource) => {
|
| 49 |
+
let active = true;
|
| 50 |
+
return {
|
| 51 |
+
resource,
|
| 52 |
+
release: () => {
|
| 53 |
+
if (!active) return;
|
| 54 |
+
active = false;
|
| 55 |
+
entry.leases -= 1;
|
| 56 |
+
if (entry.leases === 0) entry.releaseDrain?.();
|
| 57 |
+
},
|
| 58 |
+
};
|
| 59 |
+
},
|
| 60 |
+
(error: unknown) => {
|
| 61 |
+
entry.leases -= 1;
|
| 62 |
+
if (entry.leases === 0) entry.releaseDrain?.();
|
| 63 |
+
throw error;
|
| 64 |
+
},
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
has(key: Key): boolean {
|
| 69 |
+
return this.entries.has(key);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
disposeKey(key: Key, reason?: unknown, abort = false): Promise<void> {
|
| 73 |
+
const entry = this.entries.get(key);
|
| 74 |
+
if (entry === undefined) return Promise.resolve();
|
| 75 |
+
this.entries.delete(key);
|
| 76 |
+
return this.drain(entry, reason, abort);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
withdraw(reason?: unknown): Promise<void> {
|
| 80 |
+
if (this.withdrawal !== undefined) return this.withdrawal;
|
| 81 |
+
this.withdrawn = true;
|
| 82 |
+
const entries = [...this.entries.values()];
|
| 83 |
+
this.entries.clear();
|
| 84 |
+
this.withdrawal = Promise.all(entries.map((entry) => this.drain(entry, reason, false))).then(
|
| 85 |
+
() => undefined,
|
| 86 |
+
);
|
| 87 |
+
return this.withdrawal;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
private createEntry(key: Key): ResourceEntry<Resource> {
|
| 91 |
+
const entry: ResourceEntry<Resource> = {
|
| 92 |
+
promise: undefined as unknown as Promise<Resource>,
|
| 93 |
+
leases: 0,
|
| 94 |
+
draining: false,
|
| 95 |
+
abortOnDrain: false,
|
| 96 |
+
aborted: false,
|
| 97 |
+
disposed: false,
|
| 98 |
+
};
|
| 99 |
+
entry.promise = Promise.resolve()
|
| 100 |
+
.then(() => this.create(key))
|
| 101 |
+
.then(
|
| 102 |
+
(resource) => {
|
| 103 |
+
entry.resource = resource;
|
| 104 |
+
if (entry.abortOnDrain) this.abort(entry);
|
| 105 |
+
return resource;
|
| 106 |
+
},
|
| 107 |
+
(error: unknown) => {
|
| 108 |
+
if (this.entries.get(key) === entry) this.entries.delete(key);
|
| 109 |
+
throw error;
|
| 110 |
+
},
|
| 111 |
+
);
|
| 112 |
+
return entry;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
private drain(entry: ResourceEntry<Resource>, reason?: unknown, abort = false): Promise<void> {
|
| 116 |
+
entry.abortOnDrain ||= abort;
|
| 117 |
+
entry.drainPromise ??= (async () => {
|
| 118 |
+
entry.draining = true;
|
| 119 |
+
try {
|
| 120 |
+
await entry.promise;
|
| 121 |
+
} catch {
|
| 122 |
+
return;
|
| 123 |
+
}
|
| 124 |
+
if (entry.abortOnDrain) this.abort(entry, reason);
|
| 125 |
+
if (entry.leases > 0) {
|
| 126 |
+
await new Promise<void>((resolve) => {
|
| 127 |
+
entry.releaseDrain = resolve;
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
if (entry.disposed) return;
|
| 131 |
+
entry.disposed = true;
|
| 132 |
+
await entry.resource!.dispose();
|
| 133 |
+
})();
|
| 134 |
+
return entry.drainPromise;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
private abort(entry: ResourceEntry<Resource>, reason?: unknown): void {
|
| 138 |
+
if (entry.aborted || entry.resource?.abort === undefined) return;
|
| 139 |
+
entry.aborted = true;
|
| 140 |
+
try {
|
| 141 |
+
entry.resource.abort(reason);
|
| 142 |
+
} catch {}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
private unavailable(): Error {
|
| 146 |
+
return new Error(
|
| 147 |
+
`resource generation ${this.identity.owner}:${String(this.identity.generation)} is withdrawn`,
|
| 148 |
+
);
|
| 149 |
+
}
|
| 150 |
+
}
|
packages/agent-core-v2/src/_base/lifecycle/ledger.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { onUnexpectedError } from '../errors/unexpectedError';
|
| 2 |
+
import {
|
| 3 |
+
isAsyncIterable,
|
| 4 |
+
isPromiseLike,
|
| 5 |
+
isSyncIterable,
|
| 6 |
+
type Disposer,
|
| 7 |
+
type EffectBody,
|
| 8 |
+
type TeardownReason,
|
| 9 |
+
} from './disposer';
|
| 10 |
+
import { LedgerDisposedError } from './errors';
|
| 11 |
+
|
| 12 |
+
export type LedgerState = 'active' | 'disposing' | 'disposed';
|
| 13 |
+
|
| 14 |
+
export interface LedgerEntryInfo {
|
| 15 |
+
readonly label: string;
|
| 16 |
+
readonly kind: 'disposer' | 'effect' | 'ledger';
|
| 17 |
+
readonly stack?: string;
|
| 18 |
+
readonly children?: readonly LedgerEntryInfo[];
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export interface LedgerEntry {
|
| 22 |
+
readonly label: string;
|
| 23 |
+
readonly disposed: boolean;
|
| 24 |
+
dispose(reason?: TeardownReason): void | Promise<void>;
|
| 25 |
+
release(): void;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
interface EntryRecord {
|
| 29 |
+
label: string;
|
| 30 |
+
kind: 'disposer' | 'effect' | 'ledger';
|
| 31 |
+
stack?: string;
|
| 32 |
+
active: boolean;
|
| 33 |
+
run: Disposer;
|
| 34 |
+
ledger?: Ledger;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export class Ledger {
|
| 38 |
+
static captureStacks = false;
|
| 39 |
+
|
| 40 |
+
private _state: LedgerState = 'active';
|
| 41 |
+
private readonly _records: EntryRecord[] = [];
|
| 42 |
+
private _teardownPromise: Promise<void> | undefined;
|
| 43 |
+
private _parentEntry: LedgerEntry | undefined;
|
| 44 |
+
|
| 45 |
+
constructor(readonly label: string = 'ledger') {}
|
| 46 |
+
|
| 47 |
+
get state(): LedgerState {
|
| 48 |
+
return this._state;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
get isActive(): boolean {
|
| 52 |
+
return this._state === 'active';
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
get isDisposed(): boolean {
|
| 56 |
+
return this._state === 'disposed';
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
get size(): number {
|
| 60 |
+
return this._records.reduce((count, record) => count + (record.active ? 1 : 0), 0);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
register(disposer: Disposer, label: string = 'disposer'): LedgerEntry {
|
| 64 |
+
this._assertActive('register');
|
| 65 |
+
return this._push({ label, kind: 'disposer', active: true, run: disposer });
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
registerFinalizer(disposer: Disposer, label: string = 'finalizer'): LedgerEntry {
|
| 69 |
+
this._assertActive('registerFinalizer');
|
| 70 |
+
return this._push({ label, kind: 'disposer', active: true, run: disposer }, true);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
effect(body: EffectBody, label: string = 'effect'): LedgerEntry {
|
| 74 |
+
this._assertActive('effect');
|
| 75 |
+
const out = body();
|
| 76 |
+
if (typeof out === 'function') {
|
| 77 |
+
return this._push({ label, kind: 'effect', active: true, run: out });
|
| 78 |
+
}
|
| 79 |
+
if (isPromiseLike(out)) {
|
| 80 |
+
const promise = Promise.resolve(out);
|
| 81 |
+
return this._push({
|
| 82 |
+
label,
|
| 83 |
+
kind: 'effect',
|
| 84 |
+
active: true,
|
| 85 |
+
run: async (reason) => {
|
| 86 |
+
const disposer = await promise;
|
| 87 |
+
if (typeof disposer === 'function') await disposer(reason);
|
| 88 |
+
},
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
if (isAsyncIterable(out)) {
|
| 92 |
+
const promise = driveAsyncEffect(out);
|
| 93 |
+
return this._push({
|
| 94 |
+
label,
|
| 95 |
+
kind: 'effect',
|
| 96 |
+
active: true,
|
| 97 |
+
run: async (reason) => {
|
| 98 |
+
const disposer = await promise;
|
| 99 |
+
await disposer(reason);
|
| 100 |
+
},
|
| 101 |
+
});
|
| 102 |
+
}
|
| 103 |
+
if (isSyncIterable(out)) {
|
| 104 |
+
const run = driveSyncEffect(out);
|
| 105 |
+
return this._push({ label, kind: 'effect', active: true, run });
|
| 106 |
+
}
|
| 107 |
+
return this._push({ label, kind: 'effect', active: true, run: () => {} });
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
createChild(label: string = 'ledger'): Ledger {
|
| 111 |
+
this._assertActive('createChild');
|
| 112 |
+
const child = new Ledger(label);
|
| 113 |
+
child._parentEntry = this._push({
|
| 114 |
+
label,
|
| 115 |
+
kind: 'ledger',
|
| 116 |
+
active: true,
|
| 117 |
+
run: (reason) => child.teardown(reason),
|
| 118 |
+
ledger: child,
|
| 119 |
+
});
|
| 120 |
+
return child;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
teardown(reason: TeardownReason = 'scope-close'): void | Promise<void> {
|
| 124 |
+
if (this._state !== 'active') {
|
| 125 |
+
return this._teardownPromise;
|
| 126 |
+
}
|
| 127 |
+
this._state = 'disposing';
|
| 128 |
+
this._detachFromParent();
|
| 129 |
+
const out = drainRecords(this._records, reason);
|
| 130 |
+
if (isPromiseLike(out)) {
|
| 131 |
+
this._teardownPromise = Promise.resolve(out).then(() => {
|
| 132 |
+
this._state = 'disposed';
|
| 133 |
+
});
|
| 134 |
+
return this._teardownPromise;
|
| 135 |
+
}
|
| 136 |
+
this._state = 'disposed';
|
| 137 |
+
return undefined;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
clear(reason: TeardownReason = 'scope-close'): void | Promise<void> {
|
| 141 |
+
this._assertActive('clear');
|
| 142 |
+
return drainRecords(this._records, reason);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
entries(): LedgerEntryInfo[] {
|
| 146 |
+
const infos: LedgerEntryInfo[] = [];
|
| 147 |
+
for (const record of this._records) {
|
| 148 |
+
if (!record.active) continue;
|
| 149 |
+
infos.push({
|
| 150 |
+
label: record.label,
|
| 151 |
+
kind: record.kind,
|
| 152 |
+
stack: record.stack,
|
| 153 |
+
children: record.ledger?.entries(),
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
return infos;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
private _push(record: EntryRecord, front = false): LedgerEntry {
|
| 160 |
+
if (Ledger.captureStacks) {
|
| 161 |
+
record.stack = new Error('Ledger registration').stack;
|
| 162 |
+
}
|
| 163 |
+
if (front) {
|
| 164 |
+
this._records.unshift(record);
|
| 165 |
+
} else {
|
| 166 |
+
this._records.push(record);
|
| 167 |
+
}
|
| 168 |
+
return {
|
| 169 |
+
label: record.label,
|
| 170 |
+
get disposed() {
|
| 171 |
+
return !record.active;
|
| 172 |
+
},
|
| 173 |
+
dispose: (reason: TeardownReason = 'scope-close') => {
|
| 174 |
+
if (!record.active) return undefined;
|
| 175 |
+
record.active = false;
|
| 176 |
+
this._remove(record);
|
| 177 |
+
return runGuarded(record, reason);
|
| 178 |
+
},
|
| 179 |
+
release: () => {
|
| 180 |
+
if (!record.active) return;
|
| 181 |
+
record.active = false;
|
| 182 |
+
this._remove(record);
|
| 183 |
+
},
|
| 184 |
+
};
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
private _remove(record: EntryRecord): void {
|
| 188 |
+
const index = this._records.indexOf(record);
|
| 189 |
+
if (index >= 0) {
|
| 190 |
+
this._records.splice(index, 1);
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
private _detachFromParent(): void {
|
| 195 |
+
this._parentEntry?.release();
|
| 196 |
+
this._parentEntry = undefined;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
private _assertActive(operation: string): void {
|
| 200 |
+
if (this._state !== 'active') {
|
| 201 |
+
throw new LedgerDisposedError(this.label, operation, this._state);
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
function runGuarded(record: EntryRecord, reason: TeardownReason): void | Promise<void> {
|
| 207 |
+
let out: void | Promise<void>;
|
| 208 |
+
try {
|
| 209 |
+
out = record.run(reason);
|
| 210 |
+
} catch (error) {
|
| 211 |
+
onUnexpectedError(tagged(error, record.label));
|
| 212 |
+
return undefined;
|
| 213 |
+
}
|
| 214 |
+
if (isPromiseLike(out)) {
|
| 215 |
+
return Promise.resolve(out).catch((error: unknown) => {
|
| 216 |
+
onUnexpectedError(tagged(error, record.label));
|
| 217 |
+
});
|
| 218 |
+
}
|
| 219 |
+
return undefined;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
function tagged(error: unknown, label: string): unknown {
|
| 223 |
+
if (error instanceof Error) {
|
| 224 |
+
error.message = `[ledger:${label}] ${error.message}`;
|
| 225 |
+
return error;
|
| 226 |
+
}
|
| 227 |
+
return new Error(`[ledger:${label}] ${String(error)}`);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function drainRecords(records: EntryRecord[], reason: TeardownReason): void | Promise<void> {
|
| 231 |
+
let index = records.length;
|
| 232 |
+
const step = (): void | Promise<void> => {
|
| 233 |
+
while (index > 0) {
|
| 234 |
+
index -= 1;
|
| 235 |
+
const record = records[index]!;
|
| 236 |
+
if (!record.active) continue;
|
| 237 |
+
record.active = false;
|
| 238 |
+
const out = runGuarded(record, reason);
|
| 239 |
+
if (isPromiseLike(out)) {
|
| 240 |
+
return Promise.resolve(out).then(step);
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
records.length = 0;
|
| 244 |
+
return undefined;
|
| 245 |
+
};
|
| 246 |
+
return step();
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
function runDisposersReverse(
|
| 250 |
+
disposers: readonly Disposer[],
|
| 251 |
+
reason: TeardownReason,
|
| 252 |
+
): void | Promise<void> {
|
| 253 |
+
let index = disposers.length;
|
| 254 |
+
const step = (): void | Promise<void> => {
|
| 255 |
+
while (index > 0) {
|
| 256 |
+
index -= 1;
|
| 257 |
+
const disposer = disposers[index]!;
|
| 258 |
+
let out: void | Promise<void>;
|
| 259 |
+
try {
|
| 260 |
+
out = disposer(reason);
|
| 261 |
+
} catch (error) {
|
| 262 |
+
onUnexpectedError(tagged(error, 'effect'));
|
| 263 |
+
continue;
|
| 264 |
+
}
|
| 265 |
+
if (isPromiseLike(out)) {
|
| 266 |
+
return Promise.resolve(out)
|
| 267 |
+
.catch((error: unknown) => { onUnexpectedError(tagged(error, 'effect')); })
|
| 268 |
+
.then(step);
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
return undefined;
|
| 272 |
+
};
|
| 273 |
+
return step();
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
function collect(step: IteratorResult<Disposer | void>, disposers: Disposer[]): void {
|
| 277 |
+
if (typeof step.value === 'function') {
|
| 278 |
+
disposers.push(step.value);
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function driveSyncEffect(iterable: Iterable<Disposer | void>): Disposer {
|
| 283 |
+
const disposers: Disposer[] = [];
|
| 284 |
+
const iterator = iterable[Symbol.iterator]();
|
| 285 |
+
try {
|
| 286 |
+
let step = iterator.next();
|
| 287 |
+
while (!step.done) {
|
| 288 |
+
collect(step, disposers);
|
| 289 |
+
step = iterator.next();
|
| 290 |
+
}
|
| 291 |
+
collect(step, disposers);
|
| 292 |
+
} catch (error) {
|
| 293 |
+
const rollback = runDisposersReverse(disposers, 'unload');
|
| 294 |
+
if (isPromiseLike(rollback)) {
|
| 295 |
+
Promise.resolve(rollback).catch((error: unknown) => { onUnexpectedError(error); });
|
| 296 |
+
}
|
| 297 |
+
throw error;
|
| 298 |
+
}
|
| 299 |
+
return (reason) => runDisposersReverse(disposers, reason);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
async function driveAsyncEffect(iterable: AsyncIterable<Disposer | void>): Promise<Disposer> {
|
| 303 |
+
const disposers: Disposer[] = [];
|
| 304 |
+
const iterator = iterable[Symbol.asyncIterator]();
|
| 305 |
+
try {
|
| 306 |
+
let step = await iterator.next();
|
| 307 |
+
while (!step.done) {
|
| 308 |
+
collect(step, disposers);
|
| 309 |
+
step = await iterator.next();
|
| 310 |
+
}
|
| 311 |
+
collect(step, disposers);
|
| 312 |
+
} catch (error) {
|
| 313 |
+
await runDisposersReverse(disposers, 'unload');
|
| 314 |
+
throw error;
|
| 315 |
+
}
|
| 316 |
+
return (reason) => runDisposersReverse(disposers, reason);
|
| 317 |
+
}
|
packages/agent-core-v2/src/_base/lifecycle/lifecycleMachine.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type LifecycleTransitionErrorReason =
|
| 2 |
+
| 'invalid_state'
|
| 3 |
+
| 'transition_conflict'
|
| 4 |
+
| 'missing_commit_state'
|
| 5 |
+
| 'missing_rollback_state'
|
| 6 |
+
| 'already_committed'
|
| 7 |
+
| 'already_rolled_back';
|
| 8 |
+
|
| 9 |
+
export interface LifecycleTransitionErrorOptions<TState extends string> {
|
| 10 |
+
readonly reason: LifecycleTransitionErrorReason;
|
| 11 |
+
readonly operation: string;
|
| 12 |
+
readonly state: TState;
|
| 13 |
+
readonly expected?: TState | readonly TState[];
|
| 14 |
+
readonly activeOperation?: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export class LifecycleTransitionError<TState extends string = string> extends Error {
|
| 18 |
+
readonly reason: LifecycleTransitionErrorReason;
|
| 19 |
+
readonly operation: string;
|
| 20 |
+
readonly state: TState;
|
| 21 |
+
readonly expected?: TState | readonly TState[];
|
| 22 |
+
readonly activeOperation?: string;
|
| 23 |
+
|
| 24 |
+
constructor(options: LifecycleTransitionErrorOptions<TState>) {
|
| 25 |
+
super(formatTransitionError(options));
|
| 26 |
+
this.name = 'LifecycleTransitionError';
|
| 27 |
+
this.reason = options.reason;
|
| 28 |
+
this.operation = options.operation;
|
| 29 |
+
this.state = options.state;
|
| 30 |
+
this.expected = options.expected;
|
| 31 |
+
this.activeOperation = options.activeOperation;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export interface LifecycleSnapshot<TState extends string> {
|
| 36 |
+
readonly state: TState;
|
| 37 |
+
readonly transitioning: boolean;
|
| 38 |
+
readonly operation?: string;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface LifecycleSwitchOptions<TState extends string> {
|
| 42 |
+
readonly operation: string;
|
| 43 |
+
readonly from: TState | readonly TState[];
|
| 44 |
+
readonly to: TState;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export interface LifecycleTransactionOptions<TState extends string> {
|
| 48 |
+
readonly operation: string;
|
| 49 |
+
readonly from: TState | readonly TState[];
|
| 50 |
+
readonly enter: TState;
|
| 51 |
+
readonly commit?: TState;
|
| 52 |
+
readonly rollback?: TState;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export interface LifecycleTransaction<TState extends string> {
|
| 56 |
+
defer(callback: LifecycleAction): void;
|
| 57 |
+
rollback(callback: LifecycleAction): void;
|
| 58 |
+
afterCommit(callback: LifecycleAction): void;
|
| 59 |
+
commit(state: TState): void;
|
| 60 |
+
rollbackTo(state: TState): void;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export type LifecycleAction = () => void | Promise<void>;
|
| 64 |
+
|
| 65 |
+
export class LifecycleMachine<TState extends string> {
|
| 66 |
+
private _state: TState;
|
| 67 |
+
private activeOperation: string | undefined;
|
| 68 |
+
|
| 69 |
+
constructor(initial: TState) {
|
| 70 |
+
this._state = initial;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
get state(): TState {
|
| 74 |
+
return this._state;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
get snapshot(): LifecycleSnapshot<TState> {
|
| 78 |
+
return this.activeOperation === undefined
|
| 79 |
+
? { state: this._state, transitioning: false }
|
| 80 |
+
: {
|
| 81 |
+
state: this._state,
|
| 82 |
+
transitioning: true,
|
| 83 |
+
operation: this.activeOperation,
|
| 84 |
+
};
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
is(...states: readonly TState[]): boolean {
|
| 88 |
+
return states.includes(this._state);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
switch(options: LifecycleSwitchOptions<TState>): void {
|
| 92 |
+
this.assertIdle(options.operation);
|
| 93 |
+
this.assertState(options.operation, options.from);
|
| 94 |
+
this._state = options.to;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
async transaction<TResult>(
|
| 98 |
+
options: LifecycleTransactionOptions<TState>,
|
| 99 |
+
callback: (transaction: LifecycleTransaction<TState>) => TResult | Promise<TResult>,
|
| 100 |
+
): Promise<TResult> {
|
| 101 |
+
this.assertIdle(options.operation);
|
| 102 |
+
this.assertState(options.operation, options.from);
|
| 103 |
+
|
| 104 |
+
this.activeOperation = options.operation;
|
| 105 |
+
this._state = options.enter;
|
| 106 |
+
|
| 107 |
+
const deferred: LifecycleAction[] = [];
|
| 108 |
+
const rollbacks: LifecycleAction[] = [];
|
| 109 |
+
const afterCommit: LifecycleAction[] = [];
|
| 110 |
+
let commitState = options.commit;
|
| 111 |
+
let rollbackState = options.rollback;
|
| 112 |
+
let commitSelected = false;
|
| 113 |
+
let rollbackSelected = false;
|
| 114 |
+
|
| 115 |
+
const transaction: LifecycleTransaction<TState> = {
|
| 116 |
+
defer: (action) => deferred.push(action),
|
| 117 |
+
rollback: (action) => rollbacks.push(action),
|
| 118 |
+
afterCommit: (action) => afterCommit.push(action),
|
| 119 |
+
commit: (state) => {
|
| 120 |
+
if (commitSelected) {
|
| 121 |
+
throw this.createError(options.operation, 'already_committed');
|
| 122 |
+
}
|
| 123 |
+
commitSelected = true;
|
| 124 |
+
commitState = state;
|
| 125 |
+
},
|
| 126 |
+
rollbackTo: (state) => {
|
| 127 |
+
if (rollbackSelected) {
|
| 128 |
+
throw this.createError(options.operation, 'already_rolled_back');
|
| 129 |
+
}
|
| 130 |
+
rollbackSelected = true;
|
| 131 |
+
rollbackState = state;
|
| 132 |
+
},
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
try {
|
| 136 |
+
let result: TResult;
|
| 137 |
+
try {
|
| 138 |
+
result = await callback(transaction);
|
| 139 |
+
} catch (error) {
|
| 140 |
+
const errors: unknown[] = [
|
| 141 |
+
error,
|
| 142 |
+
...(await runActions(rollbacks)),
|
| 143 |
+
...(await runActions(deferred)),
|
| 144 |
+
];
|
| 145 |
+
|
| 146 |
+
if (rollbackState === undefined) {
|
| 147 |
+
errors.push(this.createError(options.operation, 'missing_rollback_state'));
|
| 148 |
+
} else {
|
| 149 |
+
this._state = rollbackState;
|
| 150 |
+
}
|
| 151 |
+
throw aggregateErrors(errors, `Lifecycle transaction "${options.operation}" failed`);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
if (commitState === undefined) {
|
| 155 |
+
throw this.createError(options.operation, 'missing_commit_state');
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
const cleanupErrors = await runActions(deferred);
|
| 159 |
+
this._state = commitState;
|
| 160 |
+
const afterCommitErrors = await runActions(afterCommit);
|
| 161 |
+
const errors = [...cleanupErrors, ...afterCommitErrors];
|
| 162 |
+
if (errors.length > 0) {
|
| 163 |
+
throw aggregateErrors(
|
| 164 |
+
errors,
|
| 165 |
+
`Lifecycle transaction "${options.operation}" committed with action failures`,
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
return result;
|
| 169 |
+
} finally {
|
| 170 |
+
this.activeOperation = undefined;
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
private assertIdle(operation: string): void {
|
| 175 |
+
if (this.activeOperation === undefined) return;
|
| 176 |
+
throw this.createError(operation, 'transition_conflict', undefined, this.activeOperation);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
private assertState(operation: string, expected: TState | readonly TState[]): void {
|
| 180 |
+
const states = Array.isArray(expected) ? expected : [expected];
|
| 181 |
+
if (states.includes(this._state)) return;
|
| 182 |
+
throw this.createError(operation, 'invalid_state', expected);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
private createError(
|
| 186 |
+
operation: string,
|
| 187 |
+
reason: LifecycleTransitionErrorReason,
|
| 188 |
+
expected?: TState | readonly TState[],
|
| 189 |
+
activeOperation?: string,
|
| 190 |
+
): LifecycleTransitionError<TState> {
|
| 191 |
+
return new LifecycleTransitionError({
|
| 192 |
+
reason,
|
| 193 |
+
operation,
|
| 194 |
+
state: this._state,
|
| 195 |
+
expected,
|
| 196 |
+
activeOperation,
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
async function runActions(actions: readonly LifecycleAction[]): Promise<unknown[]> {
|
| 202 |
+
const errors: unknown[] = [];
|
| 203 |
+
for (let index = actions.length - 1; index >= 0; index -= 1) {
|
| 204 |
+
try {
|
| 205 |
+
await actions[index]!();
|
| 206 |
+
} catch (error) {
|
| 207 |
+
errors.push(error);
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
return errors;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
function aggregateErrors(errors: readonly unknown[], message: string): unknown {
|
| 214 |
+
if (errors.length === 1) return errors[0];
|
| 215 |
+
return new AggregateError(errors, message, { cause: errors[0] });
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
function formatTransitionError<TState extends string>(
|
| 219 |
+
options: LifecycleTransitionErrorOptions<TState>,
|
| 220 |
+
): string {
|
| 221 |
+
switch (options.reason) {
|
| 222 |
+
case 'invalid_state':
|
| 223 |
+
return `Lifecycle operation "${options.operation}" is not allowed from state "${options.state}"`;
|
| 224 |
+
case 'transition_conflict':
|
| 225 |
+
return `Lifecycle operation "${options.operation}" conflicts with active operation "${options.activeOperation}"`;
|
| 226 |
+
case 'missing_commit_state':
|
| 227 |
+
return `Lifecycle operation "${options.operation}" did not select a commit state`;
|
| 228 |
+
case 'missing_rollback_state':
|
| 229 |
+
return `Lifecycle operation "${options.operation}" did not select a rollback state`;
|
| 230 |
+
case 'already_committed':
|
| 231 |
+
return `Lifecycle operation "${options.operation}" already selected a commit state`;
|
| 232 |
+
case 'already_rolled_back':
|
| 233 |
+
return `Lifecycle operation "${options.operation}" already selected a rollback state`;
|
| 234 |
+
}
|
| 235 |
+
}
|
packages/agent-core-v2/src/_base/log/fileLog.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
| 2 |
+
import { mkdir, open, rename, stat, unlink } from 'node:fs/promises';
|
| 3 |
+
import { dirname } from 'pathe';
|
| 4 |
+
|
| 5 |
+
import { formatEntry, type FormatOptions } from './formatter';
|
| 6 |
+
import type { ILogWriter, LogEntry } from './log';
|
| 7 |
+
|
| 8 |
+
export const PENDING_MAX = 1000;
|
| 9 |
+
const STDERR_NOTICE_INTERVAL_MS = 30_000;
|
| 10 |
+
|
| 11 |
+
class AsyncSerialQueue {
|
| 12 |
+
private tail: Promise<unknown> = Promise.resolve();
|
| 13 |
+
|
| 14 |
+
run<T>(task: () => Promise<T>): Promise<T> {
|
| 15 |
+
const next = this.tail.then(task, task);
|
| 16 |
+
this.tail = next.catch(() => {});
|
| 17 |
+
return next;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export interface RotatingFileWriterOptions {
|
| 22 |
+
readonly path: string;
|
| 23 |
+
readonly maxBytes: number;
|
| 24 |
+
readonly files: number;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async function syncDir(dirPath: string): Promise<void> {
|
| 28 |
+
if (process.platform === 'win32') return;
|
| 29 |
+
const dirFh = await open(dirPath, 'r');
|
| 30 |
+
try {
|
| 31 |
+
await dirFh.sync();
|
| 32 |
+
} finally {
|
| 33 |
+
await dirFh.close();
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export class RotatingFileWriter {
|
| 38 |
+
private readonly queue = new AsyncSerialQueue();
|
| 39 |
+
private pending: string[] = [];
|
| 40 |
+
private dropped = 0;
|
| 41 |
+
private closed = false;
|
| 42 |
+
private lastStderrNotice = 0;
|
| 43 |
+
private currentBytes = -1;
|
| 44 |
+
private directorySynced = false;
|
| 45 |
+
|
| 46 |
+
constructor(private readonly options: RotatingFileWriterOptions) {}
|
| 47 |
+
|
| 48 |
+
enqueue(line: string): void {
|
| 49 |
+
if (this.closed) return;
|
| 50 |
+
if (this.pending.length >= PENDING_MAX) {
|
| 51 |
+
this.pending.shift();
|
| 52 |
+
this.dropped++;
|
| 53 |
+
}
|
| 54 |
+
this.pending.push(line);
|
| 55 |
+
this.scheduleDrain();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async flush(): Promise<boolean> {
|
| 59 |
+
return this.queue.run(() => this.drain());
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async close(): Promise<void> {
|
| 63 |
+
if (this.closed) return;
|
| 64 |
+
this.closed = true;
|
| 65 |
+
try {
|
| 66 |
+
await this.flush();
|
| 67 |
+
} catch {
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
flushSync(): void {
|
| 72 |
+
if (this.closed || this.pending.length === 0) return;
|
| 73 |
+
try {
|
| 74 |
+
mkdirSync(dirname(this.options.path), { recursive: true });
|
| 75 |
+
const body = this.pending.join('') + this.takeDroppedNotice();
|
| 76 |
+
this.pending = [];
|
| 77 |
+
appendFileSync(this.options.path, body);
|
| 78 |
+
} catch (error) {
|
| 79 |
+
this.noteFailure(error);
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
private scheduleDrain(): void {
|
| 84 |
+
if (this.closed) return;
|
| 85 |
+
queueMicrotask(() => {
|
| 86 |
+
if (this.closed || this.pending.length === 0) return;
|
| 87 |
+
this.queue.run(() => this.drain()).catch(() => {});
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
private async drain(): Promise<boolean> {
|
| 92 |
+
if (this.pending.length === 0) return true;
|
| 93 |
+
const droppedLine = this.takeDroppedNotice();
|
| 94 |
+
const lines = droppedLine === '' ? [...this.pending] : [...this.pending, droppedLine];
|
| 95 |
+
this.pending = [];
|
| 96 |
+
try {
|
| 97 |
+
await mkdir(dirname(this.options.path), { recursive: true });
|
| 98 |
+
if (this.currentBytes < 0) {
|
| 99 |
+
this.currentBytes = await this.statSize(this.options.path);
|
| 100 |
+
}
|
| 101 |
+
await this.appendLines(lines);
|
| 102 |
+
|
| 103 |
+
if (!this.directorySynced) {
|
| 104 |
+
await syncDir(dirname(this.options.path));
|
| 105 |
+
this.directorySynced = true;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return true;
|
| 109 |
+
} catch (error) {
|
| 110 |
+
this.noteFailure(error);
|
| 111 |
+
this.restorePending(lines);
|
| 112 |
+
return false;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
private restorePending(lines: readonly string[]): void {
|
| 117 |
+
const restored = [...lines, ...this.pending];
|
| 118 |
+
const overflow = restored.length - PENDING_MAX;
|
| 119 |
+
if (overflow <= 0) {
|
| 120 |
+
this.pending = restored;
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
this.dropped += overflow;
|
| 124 |
+
this.pending = restored.slice(overflow);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
private async appendLines(lines: readonly string[]): Promise<void> {
|
| 128 |
+
let chunk = '';
|
| 129 |
+
let chunkBytes = 0;
|
| 130 |
+
for (const line of lines) {
|
| 131 |
+
const lineBytes = Buffer.byteLength(line, 'utf-8');
|
| 132 |
+
if (
|
| 133 |
+
chunkBytes > 0 &&
|
| 134 |
+
(chunkBytes + lineBytes > this.options.maxBytes ||
|
| 135 |
+
this.currentBytes + chunkBytes + lineBytes > this.options.maxBytes)
|
| 136 |
+
) {
|
| 137 |
+
await this.appendChunk(chunk);
|
| 138 |
+
chunk = '';
|
| 139 |
+
chunkBytes = 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
if (
|
| 143 |
+
chunkBytes === 0 &&
|
| 144 |
+
this.currentBytes > 0 &&
|
| 145 |
+
this.currentBytes + lineBytes > this.options.maxBytes
|
| 146 |
+
) {
|
| 147 |
+
await this.rotate();
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
chunk += line;
|
| 151 |
+
chunkBytes += lineBytes;
|
| 152 |
+
}
|
| 153 |
+
if (chunkBytes > 0) {
|
| 154 |
+
await this.appendChunk(chunk);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
private async appendChunk(chunk: string): Promise<void> {
|
| 159 |
+
const fh = await open(this.options.path, 'a');
|
| 160 |
+
try {
|
| 161 |
+
await fh.appendFile(chunk, 'utf-8');
|
| 162 |
+
await fh.sync();
|
| 163 |
+
} finally {
|
| 164 |
+
await fh.close();
|
| 165 |
+
}
|
| 166 |
+
this.currentBytes += Buffer.byteLength(chunk, 'utf-8');
|
| 167 |
+
if (this.currentBytes >= this.options.maxBytes) {
|
| 168 |
+
await this.rotate();
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
private async rotate(): Promise<void> {
|
| 173 |
+
const { path, files } = this.options;
|
| 174 |
+
for (let i = files - 2; i >= 1; i--) {
|
| 175 |
+
const from = `${path}.${i}`;
|
| 176 |
+
const to = `${path}.${i + 1}`;
|
| 177 |
+
try {
|
| 178 |
+
await rename(from, to);
|
| 179 |
+
} catch (error) {
|
| 180 |
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
try {
|
| 184 |
+
await rename(path, `${path}.1`);
|
| 185 |
+
} catch (error) {
|
| 186 |
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
| 187 |
+
}
|
| 188 |
+
try {
|
| 189 |
+
await unlink(`${path}.${files}`);
|
| 190 |
+
} catch (error) {
|
| 191 |
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
| 192 |
+
}
|
| 193 |
+
this.currentBytes = 0;
|
| 194 |
+
this.directorySynced = false;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
private async statSize(p: string): Promise<number> {
|
| 198 |
+
try {
|
| 199 |
+
const s = await stat(p);
|
| 200 |
+
return s.size;
|
| 201 |
+
} catch (error) {
|
| 202 |
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 0;
|
| 203 |
+
throw error;
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
private takeDroppedNotice(): string {
|
| 208 |
+
if (this.dropped === 0) return '';
|
| 209 |
+
const line = `... dropped ${this.dropped} entries ...\n`;
|
| 210 |
+
this.dropped = 0;
|
| 211 |
+
return line;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
private noteFailure(error: unknown): void {
|
| 215 |
+
const now = Date.now();
|
| 216 |
+
if (now - this.lastStderrNotice < STDERR_NOTICE_INTERVAL_MS) return;
|
| 217 |
+
this.lastStderrNotice = now;
|
| 218 |
+
const code = (error as NodeJS.ErrnoException)?.code ?? 'UNKNOWN';
|
| 219 |
+
try {
|
| 220 |
+
process.stderr.write(`[logger] write failed: ${code}\n`);
|
| 221 |
+
} catch {}
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
export interface FileLogWriterOptions extends RotatingFileWriterOptions {
|
| 226 |
+
readonly format?: FormatOptions;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
export class FileLogWriter implements ILogWriter {
|
| 230 |
+
private readonly sink: RotatingFileWriter;
|
| 231 |
+
private readonly format: FormatOptions;
|
| 232 |
+
|
| 233 |
+
constructor(options: FileLogWriterOptions) {
|
| 234 |
+
this.sink = new RotatingFileWriter(options);
|
| 235 |
+
this.format = options.format ?? {};
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
write(entry: LogEntry): void {
|
| 239 |
+
const { text, dropped } = formatEntry(entry, this.format);
|
| 240 |
+
if (dropped) return;
|
| 241 |
+
this.sink.enqueue(text + '\n');
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
async flush(): Promise<void> {
|
| 245 |
+
await this.sink.flush();
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
close(): Promise<void> {
|
| 249 |
+
return this.sink.close();
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
flushSync(): void {
|
| 253 |
+
this.sink.flushSync();
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
export function createFileLogWriter(options: FileLogWriterOptions): FileLogWriter {
|
| 258 |
+
return new FileLogWriter(options);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
export class MemoryLogWriter implements ILogWriter {
|
| 262 |
+
readonly entries: LogEntry[] = [];
|
| 263 |
+
write(entry: LogEntry): void {
|
| 264 |
+
this.entries.push(entry);
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
export class ConsoleLogWriter implements ILogWriter {
|
| 269 |
+
write(entry: LogEntry): void {
|
| 270 |
+
const { text } = formatEntry(entry, { ansi: process.stderr.isTTY === true });
|
| 271 |
+
switch (entry.level) {
|
| 272 |
+
case 'error':
|
| 273 |
+
console.error(text);
|
| 274 |
+
break;
|
| 275 |
+
case 'warn':
|
| 276 |
+
console.warn(text);
|
| 277 |
+
break;
|
| 278 |
+
case 'debug':
|
| 279 |
+
console.debug(text);
|
| 280 |
+
break;
|
| 281 |
+
default:
|
| 282 |
+
console.log(text);
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
}
|
packages/agent-core-v2/src/_base/log/formatter.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { LogContext, LogEntry, LogEntryError } from './log';
|
| 2 |
+
|
| 3 |
+
export const MSG_MAX_CHARS = 200;
|
| 4 |
+
export const CTX_VALUE_MAX_CHARS = 2048;
|
| 5 |
+
export const STACK_MAX_BYTES = 2048;
|
| 6 |
+
export const ENTRY_MAX_BYTES = 4096;
|
| 7 |
+
export const REDACT_MAX_DEPTH = 10;
|
| 8 |
+
|
| 9 |
+
const REDACTED_KEYS: ReadonlySet<string> = new Set([
|
| 10 |
+
'authorization',
|
| 11 |
+
'apikey',
|
| 12 |
+
'token',
|
| 13 |
+
'refreshtoken',
|
| 14 |
+
'accesstoken',
|
| 15 |
+
'idtoken',
|
| 16 |
+
'password',
|
| 17 |
+
'secret',
|
| 18 |
+
'clientsecret',
|
| 19 |
+
'apisecret',
|
| 20 |
+
'cookie',
|
| 21 |
+
'setcookie',
|
| 22 |
+
'bearer',
|
| 23 |
+
]);
|
| 24 |
+
|
| 25 |
+
const SAFE_KEY_RE = /^[\w.-]+$/;
|
| 26 |
+
const ELLIPSIS = '…';
|
| 27 |
+
const TRUNCATED_TAIL = ` …truncated`;
|
| 28 |
+
const REDACTED = '[REDACTED]';
|
| 29 |
+
const RAW_SECRET_PATTERNS: readonly RegExp[] = [
|
| 30 |
+
/\b(authorization\s*[:=]\s*bearer\s+)[^\s"'`]+/gi,
|
| 31 |
+
/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|secret)\s*[:=]\s*)[^\s"'`]+/gi,
|
| 32 |
+
/\b(cookie\s*[:=]\s*)[^\r\n]+/gi,
|
| 33 |
+
];
|
| 34 |
+
|
| 35 |
+
const LEVEL_LABEL: Record<Exclude<LogEntry['level'], never>, string> = {
|
| 36 |
+
error: 'ERROR',
|
| 37 |
+
warn: 'WARN ',
|
| 38 |
+
info: 'INFO ',
|
| 39 |
+
debug: 'DEBUG',
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const ANSI_LEVEL: Record<Exclude<LogEntry['level'], never>, string> = {
|
| 43 |
+
error: '[31m',
|
| 44 |
+
warn: '[33m',
|
| 45 |
+
info: '[36m',
|
| 46 |
+
debug: '[90m',
|
| 47 |
+
};
|
| 48 |
+
const ANSI_RESET = '[0m';
|
| 49 |
+
|
| 50 |
+
function normalizeKey(key: string): string {
|
| 51 |
+
return key.toLowerCase().replaceAll(/[_\-.]/g, '');
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export function redactCtx(ctx: LogContext): LogContext {
|
| 55 |
+
const seen = new WeakSet<object>();
|
| 56 |
+
const walk = (value: unknown, depth: number): unknown => {
|
| 57 |
+
if (depth > REDACT_MAX_DEPTH) return '[REDACTED:depth]';
|
| 58 |
+
if (value === null || typeof value !== 'object') return value;
|
| 59 |
+
if (seen.has(value)) return '[REDACTED:cycle]';
|
| 60 |
+
seen.add(value);
|
| 61 |
+
if (Array.isArray(value)) {
|
| 62 |
+
return value.map((item) => walk(item, depth + 1));
|
| 63 |
+
}
|
| 64 |
+
const out: Record<string, unknown> = {};
|
| 65 |
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
| 66 |
+
out[key] = REDACTED_KEYS.has(normalizeKey(key)) ? REDACTED : walk(raw, depth + 1);
|
| 67 |
+
}
|
| 68 |
+
return out;
|
| 69 |
+
};
|
| 70 |
+
return walk(ctx, 0) as LogContext;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface FormatOptions {
|
| 74 |
+
readonly ansi?: boolean;
|
| 75 |
+
readonly omitContextKeys?: readonly string[];
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export interface FormattedEntry {
|
| 79 |
+
readonly text: string;
|
| 80 |
+
readonly dropped: boolean;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function truncate(value: string, max: number): string {
|
| 84 |
+
return value.length <= max ? value : value.slice(0, max - 1) + ELLIPSIS;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function serializeValue(raw: unknown): string {
|
| 88 |
+
if (typeof raw === 'string') return redactString(raw);
|
| 89 |
+
if (raw === undefined) return 'undefined';
|
| 90 |
+
if (raw === null) return 'null';
|
| 91 |
+
if (
|
| 92 |
+
typeof raw === 'number' ||
|
| 93 |
+
typeof raw === 'boolean' ||
|
| 94 |
+
typeof raw === 'bigint' ||
|
| 95 |
+
typeof raw === 'symbol'
|
| 96 |
+
) {
|
| 97 |
+
return String(raw);
|
| 98 |
+
}
|
| 99 |
+
try {
|
| 100 |
+
const json = JSON.stringify(raw);
|
| 101 |
+
if (json !== undefined) return json;
|
| 102 |
+
} catch {
|
| 103 |
+
}
|
| 104 |
+
if (typeof raw === 'function') return raw.name === '' ? '[Function]' : `[Function: ${raw.name}]`;
|
| 105 |
+
return Object.prototype.toString.call(raw);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
function redactString(value: string): string {
|
| 109 |
+
let out = value;
|
| 110 |
+
for (const pattern of RAW_SECRET_PATTERNS) {
|
| 111 |
+
out = out.replace(pattern, `$1${REDACTED}`);
|
| 112 |
+
}
|
| 113 |
+
return out;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
function quote(value: string): string {
|
| 117 |
+
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function formatPair(key: string, raw: unknown): string {
|
| 121 |
+
const limited = truncate(serializeValue(raw), CTX_VALUE_MAX_CHARS);
|
| 122 |
+
const renderedKey = SAFE_KEY_RE.test(key) ? key : quote(key);
|
| 123 |
+
const renderedVal = /[\s="\\]/.test(limited) || limited.length === 0 ? quote(limited) : limited;
|
| 124 |
+
return `${renderedKey}=${renderedVal}`;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function clipBytes(text: string, maxBytes: number): string {
|
| 128 |
+
if (Buffer.byteLength(text, 'utf-8') <= maxBytes) return text;
|
| 129 |
+
let lo = 0;
|
| 130 |
+
let hi = text.length;
|
| 131 |
+
while (lo < hi) {
|
| 132 |
+
const mid = (lo + hi + 1) >> 1;
|
| 133 |
+
if (
|
| 134 |
+
Buffer.byteLength(text.slice(0, mid), 'utf-8') <=
|
| 135 |
+
maxBytes - Buffer.byteLength(TRUNCATED_TAIL, 'utf-8')
|
| 136 |
+
) {
|
| 137 |
+
lo = mid;
|
| 138 |
+
} else {
|
| 139 |
+
hi = mid - 1;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
return text.slice(0, lo) + TRUNCATED_TAIL;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
function clipStack(stack: string): string {
|
| 146 |
+
if (Buffer.byteLength(stack, 'utf-8') <= STACK_MAX_BYTES) return stack;
|
| 147 |
+
return clipBytes(stack, STACK_MAX_BYTES);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function indentStack(stack: string): string {
|
| 151 |
+
return stack
|
| 152 |
+
.split('\n')
|
| 153 |
+
.map((line, i) => (i === 0 ? ` ${line}` : ` ${line.trimStart()}`))
|
| 154 |
+
.join('\n');
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
export function formatEntry(entry: LogEntry, options: FormatOptions = {}): FormattedEntry {
|
| 158 |
+
const ctx = entry.ctx ? redactCtx(entry.ctx) : undefined;
|
| 159 |
+
const omitContextKeys = new Set(options.omitContextKeys ?? []);
|
| 160 |
+
const msg = truncate(entry.msg, MSG_MAX_CHARS);
|
| 161 |
+
const pairs: string[] = [];
|
| 162 |
+
if (ctx) {
|
| 163 |
+
for (const [k, v] of Object.entries(ctx)) {
|
| 164 |
+
if (omitContextKeys.has(k)) continue;
|
| 165 |
+
if (v !== undefined) pairs.push(formatPair(k, v));
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
const time = new Date(entry.t).toISOString();
|
| 170 |
+
const label = LEVEL_LABEL[entry.level];
|
| 171 |
+
const rendered = pairs.length === 0
|
| 172 |
+
? `${time} ${label} ${msg}`
|
| 173 |
+
: `${time} ${label} ${msg} ${pairs.join(' ')}`;
|
| 174 |
+
|
| 175 |
+
let head = Buffer.byteLength(rendered, 'utf-8') > ENTRY_MAX_BYTES
|
| 176 |
+
? clipBytes(rendered, ENTRY_MAX_BYTES)
|
| 177 |
+
: rendered;
|
| 178 |
+
|
| 179 |
+
if (options.ansi === true) {
|
| 180 |
+
head = `${ANSI_LEVEL[entry.level]}${head}${ANSI_RESET}`;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
if (entry.error?.stack) {
|
| 184 |
+
head = `${head}\n${indentStack(clipStack(redactString(entry.error.stack)))}`;
|
| 185 |
+
} else if (entry.error?.message) {
|
| 186 |
+
head = `${head}\n Error: ${redactString(entry.error.message)}`;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
return { text: head, dropped: false };
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
export function extractError(value: Error): LogEntryError {
|
| 193 |
+
return typeof value.stack === 'string'
|
| 194 |
+
? { message: value.message, stack: value.stack }
|
| 195 |
+
: { message: value.message };
|
| 196 |
+
}
|
packages/agent-core-v2/src/_base/log/log.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
| 2 |
+
|
| 3 |
+
export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
|
| 4 |
+
|
| 5 |
+
export type LogContext = Record<string, unknown>;
|
| 6 |
+
|
| 7 |
+
export type LogPayload = unknown;
|
| 8 |
+
|
| 9 |
+
export interface LogEntryError {
|
| 10 |
+
readonly message: string;
|
| 11 |
+
readonly stack?: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export interface LogEntry {
|
| 15 |
+
readonly t: number;
|
| 16 |
+
readonly level: Exclude<LogLevel, 'off'>;
|
| 17 |
+
readonly msg: string;
|
| 18 |
+
readonly ctx?: LogContext;
|
| 19 |
+
readonly error?: LogEntryError;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface ILogWriter {
|
| 23 |
+
write(entry: LogEntry): void;
|
| 24 |
+
flush?(): Promise<void>;
|
| 25 |
+
close?(): Promise<void>;
|
| 26 |
+
flushSync?(): void;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface ILogger {
|
| 30 |
+
error(message: string, payload?: LogPayload): void;
|
| 31 |
+
warn(message: string, payload?: LogPayload): void;
|
| 32 |
+
info(message: string, payload?: LogPayload): void;
|
| 33 |
+
debug(message: string, payload?: LogPayload): void;
|
| 34 |
+
child(ctx: LogContext): ILogger;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export interface ILogService extends ILogger {
|
| 38 |
+
readonly _serviceBrand: undefined;
|
| 39 |
+
|
| 40 |
+
readonly level: LogLevel;
|
| 41 |
+
setLevel(level: LogLevel): void;
|
| 42 |
+
flush(): Promise<void>;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
export const ILogService: ServiceIdentifier<ILogService> =
|
| 46 |
+
createDecorator<ILogService>('logService');
|
| 47 |
+
|
| 48 |
+
const LEVEL_ORDER: Record<LogLevel, number> = {
|
| 49 |
+
off: 0,
|
| 50 |
+
error: 1,
|
| 51 |
+
warn: 2,
|
| 52 |
+
info: 3,
|
| 53 |
+
debug: 4,
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
export function levelEnabled(level: LogLevel, configured: LogLevel): boolean {
|
| 57 |
+
if (level === 'off' || configured === 'off') return false;
|
| 58 |
+
return LEVEL_ORDER[level] <= LEVEL_ORDER[configured];
|
| 59 |
+
}
|
packages/agent-core-v2/src/_base/log/logConfig.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { join } from 'pathe';
|
| 2 |
+
|
| 3 |
+
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
| 4 |
+
import type { ScopeSeed } from '#/_base/di/scope';
|
| 5 |
+
|
| 6 |
+
import type { LogLevel } from './log';
|
| 7 |
+
|
| 8 |
+
export const DEFAULT_LOG_LEVEL: LogLevel = 'info';
|
| 9 |
+
export const DEFAULT_GLOBAL_MAX_BYTES = 6 * 1024 * 1024;
|
| 10 |
+
export const DEFAULT_GLOBAL_FILES = 5;
|
| 11 |
+
export const DEFAULT_SESSION_MAX_BYTES = 5 * 1024 * 1024;
|
| 12 |
+
export const DEFAULT_SESSION_FILES = 3;
|
| 13 |
+
|
| 14 |
+
export interface LoggingConfig {
|
| 15 |
+
readonly level: LogLevel;
|
| 16 |
+
readonly globalLogPath: string;
|
| 17 |
+
readonly globalMaxBytes: number;
|
| 18 |
+
readonly globalFiles: number;
|
| 19 |
+
readonly sessionMaxBytes: number;
|
| 20 |
+
readonly sessionFiles: number;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface ILogOptions extends LoggingConfig {}
|
| 24 |
+
|
| 25 |
+
export const ILogOptions: ServiceIdentifier<ILogOptions> =
|
| 26 |
+
createDecorator<ILogOptions>('logOptions');
|
| 27 |
+
|
| 28 |
+
export interface ResolveLoggingInput {
|
| 29 |
+
readonly homeDir: string;
|
| 30 |
+
readonly env: NodeJS.ProcessEnv;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function resolveGlobalLogPath(homeDir: string): string {
|
| 34 |
+
return join(homeDir, 'logs', 'kimi-code.log');
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export function resolveSessionLogPath(sessionDir: string): string {
|
| 38 |
+
return join(sessionDir, 'logs', 'kimi-code.log');
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export function resolveLoggingConfig(input: ResolveLoggingInput): LoggingConfig {
|
| 42 |
+
const env = input.env;
|
| 43 |
+
return {
|
| 44 |
+
level: parseLevel(env['KIMI_LOG_LEVEL']) ?? DEFAULT_LOG_LEVEL,
|
| 45 |
+
globalLogPath: resolveGlobalLogPath(input.homeDir),
|
| 46 |
+
globalMaxBytes: parsePositiveInt(env['KIMI_LOG_GLOBAL_MAX_BYTES']) ?? DEFAULT_GLOBAL_MAX_BYTES,
|
| 47 |
+
globalFiles: parsePositiveInt(env['KIMI_LOG_GLOBAL_FILES']) ?? DEFAULT_GLOBAL_FILES,
|
| 48 |
+
sessionMaxBytes:
|
| 49 |
+
parsePositiveInt(env['KIMI_LOG_SESSION_MAX_BYTES']) ?? DEFAULT_SESSION_MAX_BYTES,
|
| 50 |
+
sessionFiles: parsePositiveInt(env['KIMI_LOG_SESSION_FILES']) ?? DEFAULT_SESSION_FILES,
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export function logSeed(config: LoggingConfig): ScopeSeed {
|
| 55 |
+
return [
|
| 56 |
+
[ILogOptions as ServiceIdentifier<unknown>, config satisfies ILogOptions],
|
| 57 |
+
];
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function parseLevel(value: string | undefined): LogLevel | undefined {
|
| 61 |
+
if (value === undefined) return undefined;
|
| 62 |
+
const v = value.toLowerCase().trim();
|
| 63 |
+
if (v === 'off' || v === 'error' || v === 'warn' || v === 'info' || v === 'debug') return v;
|
| 64 |
+
return undefined;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function parsePositiveInt(value: string | undefined): number | undefined {
|
| 68 |
+
if (value === undefined) return undefined;
|
| 69 |
+
const n = Number.parseInt(value, 10);
|
| 70 |
+
if (!Number.isFinite(n) || n <= 0) return undefined;
|
| 71 |
+
return n;
|
| 72 |
+
}
|
packages/agent-core-v2/src/_base/log/logService.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Service } from '#/_base/di/service';
|
| 2 |
+
import { LifecycleScope } from '#/app/scopes';
|
| 3 |
+
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
| 4 |
+
|
| 5 |
+
import {
|
| 6 |
+
type ILogger,
|
| 7 |
+
type ILogWriter,
|
| 8 |
+
type LogContext,
|
| 9 |
+
type LogEntry,
|
| 10 |
+
type LogEntryError,
|
| 11 |
+
type LogLevel,
|
| 12 |
+
type LogPayload,
|
| 13 |
+
ILogService,
|
| 14 |
+
levelEnabled,
|
| 15 |
+
} from './log';
|
| 16 |
+
import { createFileLogWriter, type FileLogWriter } from './fileLog';
|
| 17 |
+
import { ILogOptions } from './logConfig';
|
| 18 |
+
|
| 19 |
+
const pendingLogCloses = new Set<Promise<void>>();
|
| 20 |
+
|
| 21 |
+
export function trackLogClose(close: Promise<void>): void {
|
| 22 |
+
const tracked = close.then(
|
| 23 |
+
() => undefined,
|
| 24 |
+
() => undefined,
|
| 25 |
+
);
|
| 26 |
+
pendingLogCloses.add(tracked);
|
| 27 |
+
void tracked.finally(() => pendingLogCloses.delete(tracked));
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export async function drainLogCloses(): Promise<void> {
|
| 31 |
+
while (pendingLogCloses.size > 0) {
|
| 32 |
+
await Promise.all(pendingLogCloses);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
interface ExtractedPayload {
|
| 37 |
+
readonly ctx?: LogContext;
|
| 38 |
+
readonly error?: LogEntryError;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function errorEntry(error: Error): LogEntryError {
|
| 42 |
+
return { message: error.message, stack: error.stack };
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function stringifyPayload(payload: Exclude<LogPayload, undefined>): string {
|
| 46 |
+
if (typeof payload === 'string') return payload;
|
| 47 |
+
try {
|
| 48 |
+
const json = JSON.stringify(payload);
|
| 49 |
+
return json === undefined ? String(payload) : json;
|
| 50 |
+
} catch {
|
| 51 |
+
return String(payload);
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function extractPayload(payload: LogPayload): ExtractedPayload | undefined {
|
| 56 |
+
if (payload === undefined) return {};
|
| 57 |
+
if (payload instanceof Error) return { error: errorEntry(payload) };
|
| 58 |
+
if (typeof payload === 'object' && payload !== null) {
|
| 59 |
+
let entries: [string, unknown][];
|
| 60 |
+
try {
|
| 61 |
+
entries = Object.entries(payload as Record<string, unknown>);
|
| 62 |
+
} catch {
|
| 63 |
+
return undefined;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
let error: LogEntryError | undefined;
|
| 67 |
+
const ctx: LogContext = {};
|
| 68 |
+
for (const [key, value] of entries) {
|
| 69 |
+
if (key === 'error' && value instanceof Error) {
|
| 70 |
+
error = errorEntry(value);
|
| 71 |
+
continue;
|
| 72 |
+
}
|
| 73 |
+
ctx[key] = value;
|
| 74 |
+
}
|
| 75 |
+
return {
|
| 76 |
+
...(Object.keys(ctx).length > 0 ? { ctx } : {}),
|
| 77 |
+
...(error !== undefined ? { error } : {}),
|
| 78 |
+
};
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return { ctx: { reason: stringifyPayload(payload) } };
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export interface LogLevelState {
|
| 85 |
+
level: LogLevel;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
export class BoundLogger extends Service implements ILogger {
|
| 89 |
+
constructor(
|
| 90 |
+
protected readonly writer: ILogWriter,
|
| 91 |
+
private readonly levelState: LogLevelState,
|
| 92 |
+
private readonly bound: LogContext = {},
|
| 93 |
+
) {
|
| 94 |
+
super();
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
child(ctx: LogContext): ILogger {
|
| 98 |
+
return new BoundLogger(this.writer, this.levelState, { ...this.bound, ...ctx });
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
error(message: string, payload?: LogPayload): void {
|
| 102 |
+
this.emit('error', message, payload);
|
| 103 |
+
}
|
| 104 |
+
warn(message: string, payload?: LogPayload): void {
|
| 105 |
+
this.emit('warn', message, payload);
|
| 106 |
+
}
|
| 107 |
+
info(message: string, payload?: LogPayload): void {
|
| 108 |
+
this.emit('info', message, payload);
|
| 109 |
+
}
|
| 110 |
+
debug(message: string, payload?: LogPayload): void {
|
| 111 |
+
this.emit('debug', message, payload);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
private emit(
|
| 115 |
+
level: Exclude<LogLevel, 'off'>,
|
| 116 |
+
message: string,
|
| 117 |
+
payload?: LogPayload,
|
| 118 |
+
): void {
|
| 119 |
+
if (!levelEnabled(level, this.levelState.level)) return;
|
| 120 |
+
const extracted = extractPayload(payload);
|
| 121 |
+
if (extracted === undefined) return;
|
| 122 |
+
const payloadCtx = extracted.ctx;
|
| 123 |
+
const error = extracted.error;
|
| 124 |
+
const ctx =
|
| 125 |
+
payloadCtx !== undefined || Object.keys(this.bound).length > 0
|
| 126 |
+
? { ...payloadCtx, ...this.bound }
|
| 127 |
+
: undefined;
|
| 128 |
+
const entry: LogEntry = {
|
| 129 |
+
t: Date.now(),
|
| 130 |
+
level,
|
| 131 |
+
msg: message,
|
| 132 |
+
...(ctx !== undefined ? { ctx } : {}),
|
| 133 |
+
...(error !== undefined ? { error } : {}),
|
| 134 |
+
};
|
| 135 |
+
this.writer.write(entry);
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
export class AppLogService extends BoundLogger implements ILogService {
|
| 140 |
+
declare readonly _serviceBrand: undefined;
|
| 141 |
+
private readonly sink: FileLogWriter;
|
| 142 |
+
private readonly rootLevel: LogLevelState;
|
| 143 |
+
|
| 144 |
+
constructor(@ILogOptions options: ILogOptions) {
|
| 145 |
+
const sink = createFileLogWriter({
|
| 146 |
+
path: options.globalLogPath,
|
| 147 |
+
maxBytes: options.globalMaxBytes,
|
| 148 |
+
files: options.globalFiles,
|
| 149 |
+
});
|
| 150 |
+
const rootLevel: LogLevelState = { level: options.level };
|
| 151 |
+
super(sink, rootLevel);
|
| 152 |
+
this.sink = sink;
|
| 153 |
+
this.rootLevel = rootLevel;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
get level(): LogLevel {
|
| 157 |
+
return this.rootLevel.level;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
setLevel(level: LogLevel): void {
|
| 161 |
+
this.rootLevel.level = level;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
flush(): Promise<void> {
|
| 165 |
+
return this.sink.flush();
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
override dispose(): void {
|
| 169 |
+
this.sink.flushSync();
|
| 170 |
+
trackLogClose(this.sink.close());
|
| 171 |
+
super.dispose();
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
registerScopedService(
|
| 176 |
+
LifecycleScope.App,
|
| 177 |
+
ILogService,
|
| 178 |
+
AppLogService,
|
| 179 |
+
ScopeActivation.OnScopeCreated,
|
| 180 |
+
'log',
|
| 181 |
+
);
|
packages/agent-core-v2/src/_base/state/stateRegistry.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Disposable, type IDisposable, toDisposable } from '../di/lifecycle';
|
| 2 |
+
import { BugIndicatingError } from '../errors/errors';
|
| 3 |
+
import { Emitter, type Event } from '../event';
|
| 4 |
+
|
| 5 |
+
export interface StateKey<T> {
|
| 6 |
+
readonly name: string;
|
| 7 |
+
readonly initial: () => T;
|
| 8 |
+
readonly snapshotExcluded?: boolean;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export interface StateChange {
|
| 12 |
+
readonly key: string;
|
| 13 |
+
readonly value: unknown;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface StateInspection {
|
| 17 |
+
readonly scope: string;
|
| 18 |
+
readonly state: Record<string, unknown>;
|
| 19 |
+
readonly parent?: StateInspection;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface IStateRegistry {
|
| 23 |
+
contributeState<T>(key: StateKey<T>): IDisposable;
|
| 24 |
+
has(key: StateKey<unknown>): boolean;
|
| 25 |
+
get<T>(key: StateKey<T>): T;
|
| 26 |
+
set<T>(key: StateKey<T>, value: T): void;
|
| 27 |
+
onDidChange<T>(key: StateKey<T>): Event<T>;
|
| 28 |
+
readonly onDidChangeAny: Event<StateChange>;
|
| 29 |
+
entries(): readonly [string, unknown][];
|
| 30 |
+
snapshot(): Record<string, unknown>;
|
| 31 |
+
inspect(): StateInspection;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export class StateRegistry extends Disposable implements IStateRegistry {
|
| 35 |
+
private readonly values = new Map<string, unknown>();
|
| 36 |
+
private readonly registrations = new Map<string, object>();
|
| 37 |
+
private readonly excludedFromSnapshot = new Set<string>();
|
| 38 |
+
private readonly keyEmitters = new Map<string, Emitter<unknown>>();
|
| 39 |
+
private readonly anyEmitter = this._register(new Emitter<StateChange>());
|
| 40 |
+
readonly onDidChangeAny: Event<StateChange> = this.anyEmitter.event;
|
| 41 |
+
|
| 42 |
+
protected readonly inspectScope: string = 'unknown';
|
| 43 |
+
protected inspectParent?: IStateRegistry;
|
| 44 |
+
|
| 45 |
+
contributeState<T>(key: StateKey<T>): IDisposable {
|
| 46 |
+
const replayable = (key as StateKey<T> & { readonly replayable?: unknown }).replayable;
|
| 47 |
+
if (typeof replayable === 'object' && replayable !== null) {
|
| 48 |
+
throw new BugIndicatingError(
|
| 49 |
+
`replayable state key '${key.name}' must be contributed to the Agent-scope state service`,
|
| 50 |
+
);
|
| 51 |
+
}
|
| 52 |
+
return this.contributeKey(key);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
protected contributeKey<T>(key: StateKey<T>): IDisposable {
|
| 56 |
+
if (this.values.has(key.name)) {
|
| 57 |
+
throw new BugIndicatingError(`state key '${key.name}' is already registered`);
|
| 58 |
+
}
|
| 59 |
+
const registration = {};
|
| 60 |
+
this.registrations.set(key.name, registration);
|
| 61 |
+
this.values.set(key.name, key.initial());
|
| 62 |
+
if (key.snapshotExcluded === true) {
|
| 63 |
+
this.excludedFromSnapshot.add(key.name);
|
| 64 |
+
}
|
| 65 |
+
return toDisposable(() => {
|
| 66 |
+
if (this.registrations.get(key.name) !== registration) return;
|
| 67 |
+
this.registrations.delete(key.name);
|
| 68 |
+
this.values.delete(key.name);
|
| 69 |
+
this.excludedFromSnapshot.delete(key.name);
|
| 70 |
+
this.keyEmitters.get(key.name)?.dispose();
|
| 71 |
+
this.keyEmitters.delete(key.name);
|
| 72 |
+
});
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
has(key: StateKey<unknown>): boolean {
|
| 76 |
+
return this.values.has(key.name);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
get<T>(key: StateKey<T>): T {
|
| 80 |
+
if (!this.values.has(key.name)) {
|
| 81 |
+
throw new BugIndicatingError(`state key '${key.name}' is not registered`);
|
| 82 |
+
}
|
| 83 |
+
return this.values.get(key.name) as T;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
set<T>(key: StateKey<T>, value: T): void {
|
| 87 |
+
if (!this.values.has(key.name)) {
|
| 88 |
+
throw new BugIndicatingError(`state key '${key.name}' is not registered`);
|
| 89 |
+
}
|
| 90 |
+
this.values.set(key.name, value);
|
| 91 |
+
this.keyEmitters.get(key.name)?.fire(value);
|
| 92 |
+
this.anyEmitter.fire({ key: key.name, value });
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
onDidChange<T>(key: StateKey<T>): Event<T> {
|
| 96 |
+
let emitter = this.keyEmitters.get(key.name);
|
| 97 |
+
if (emitter === undefined) {
|
| 98 |
+
emitter = this._register(new Emitter<unknown>());
|
| 99 |
+
this.keyEmitters.set(key.name, emitter);
|
| 100 |
+
}
|
| 101 |
+
return emitter.event as Event<T>;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
entries(): readonly [string, unknown][] {
|
| 105 |
+
return Array.from(this.values.entries());
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
snapshot(): Record<string, unknown> {
|
| 109 |
+
const out: Record<string, unknown> = {};
|
| 110 |
+
for (const [key, value] of this.values) {
|
| 111 |
+
if (this.excludedFromSnapshot.has(key)) continue;
|
| 112 |
+
out[key] = toJsonSafe(value, new WeakSet());
|
| 113 |
+
}
|
| 114 |
+
return out;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
inspect(): StateInspection {
|
| 118 |
+
return {
|
| 119 |
+
scope: this.inspectScope,
|
| 120 |
+
state: this.snapshot(),
|
| 121 |
+
parent: this.inspectParent?.inspect(),
|
| 122 |
+
};
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function toJsonSafe(value: unknown, seen: WeakSet<object>): unknown {
|
| 127 |
+
if (value === undefined || value === null) return null;
|
| 128 |
+
if (typeof value === 'function') return '(function)';
|
| 129 |
+
if (typeof value === 'bigint') return value.toString();
|
| 130 |
+
if (typeof value !== 'object') return value;
|
| 131 |
+
if (seen.has(value)) return '(circular)';
|
| 132 |
+
seen.add(value);
|
| 133 |
+
try {
|
| 134 |
+
if (value instanceof Date) return value.toJSON();
|
| 135 |
+
if (Array.isArray(value)) return value.map((item) => toJsonSafe(item, seen));
|
| 136 |
+
if (value instanceof Map) {
|
| 137 |
+
const entries = [...value.entries()];
|
| 138 |
+
const objectKeys = entries.every(([key]) => ['string', 'number'].includes(typeof key));
|
| 139 |
+
if (objectKeys) {
|
| 140 |
+
return Object.fromEntries(
|
| 141 |
+
entries.map(([key, item]) => [key, toJsonSafe(item, seen)] as const),
|
| 142 |
+
);
|
| 143 |
+
}
|
| 144 |
+
return entries.map(([key, item]) => [toJsonSafe(key, seen), toJsonSafe(item, seen)]);
|
| 145 |
+
}
|
| 146 |
+
if (value instanceof Set) {
|
| 147 |
+
return [...value.values()].map((item) => toJsonSafe(item, seen));
|
| 148 |
+
}
|
| 149 |
+
const proto: unknown = Object.getPrototypeOf(value);
|
| 150 |
+
if (proto !== Object.prototype && proto !== null) {
|
| 151 |
+
const ctor = (value as { constructor?: { name?: string } }).constructor;
|
| 152 |
+
return `(${ctor?.name ?? 'object'})`;
|
| 153 |
+
}
|
| 154 |
+
const out: Record<string, unknown> = {};
|
| 155 |
+
for (const [key, item] of Object.entries(value)) {
|
| 156 |
+
if (typeof item === 'function') continue;
|
| 157 |
+
out[key] = toJsonSafe(item, seen);
|
| 158 |
+
}
|
| 159 |
+
return out;
|
| 160 |
+
} finally {
|
| 161 |
+
seen.delete(value);
|
| 162 |
+
}
|
| 163 |
+
}
|
packages/agent-core-v2/src/_base/text/encoding.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type UtfTextEncoding = 'utf-8' | 'utf-16le' | 'utf-16be';
|
| 2 |
+
|
| 3 |
+
export interface TextClassification {
|
| 4 |
+
readonly isBinary: boolean;
|
| 5 |
+
readonly encoding: UtfTextEncoding;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const FS_BINARY_NONPRINTABLE_FRACTION = 0.3;
|
| 9 |
+
|
| 10 |
+
export interface TextEncodingDetection {
|
| 11 |
+
readonly encoding: UtfTextEncoding;
|
| 12 |
+
readonly seemsBinary: boolean;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export const ENCODING_DETECTION_SAMPLE_BYTES = 512;
|
| 16 |
+
|
| 17 |
+
const MIN_ZERO_BYTES_FOR_UTF16 = 2;
|
| 18 |
+
|
| 19 |
+
const UTF16BE_BOM = [0xfe, 0xff] as const;
|
| 20 |
+
const UTF16LE_BOM = [0xff, 0xfe] as const;
|
| 21 |
+
const UTF8_BOM = [0xef, 0xbb, 0xbf] as const;
|
| 22 |
+
|
| 23 |
+
function sniffTextEncoding(sample: Uint8Array): TextEncodingDetection {
|
| 24 |
+
if (sample.length >= 2) {
|
| 25 |
+
const b0 = sample[0]!;
|
| 26 |
+
const b1 = sample[1]!;
|
| 27 |
+
if (b0 === UTF16BE_BOM[0] && b1 === UTF16BE_BOM[1]) {
|
| 28 |
+
return { encoding: 'utf-16be', seemsBinary: false };
|
| 29 |
+
}
|
| 30 |
+
if (b0 === UTF16LE_BOM[0] && b1 === UTF16LE_BOM[1]) {
|
| 31 |
+
return { encoding: 'utf-16le', seemsBinary: false };
|
| 32 |
+
}
|
| 33 |
+
if (sample.length >= 3 && b0 === UTF8_BOM[0] && b1 === UTF8_BOM[1] && sample[2] === UTF8_BOM[2]) {
|
| 34 |
+
return { encoding: 'utf-8', seemsBinary: false };
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
let zerosAtOdd = 0;
|
| 39 |
+
let zerosAtEven = 0;
|
| 40 |
+
const limit = Math.min(sample.length, ENCODING_DETECTION_SAMPLE_BYTES);
|
| 41 |
+
for (let i = 0; i < limit; i++) {
|
| 42 |
+
if (sample[i] !== 0) continue;
|
| 43 |
+
if (i % 2 === 1) zerosAtOdd++;
|
| 44 |
+
else zerosAtEven++;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
if (zerosAtOdd === 0 && zerosAtEven === 0) {
|
| 48 |
+
return { encoding: 'utf-8', seemsBinary: false };
|
| 49 |
+
}
|
| 50 |
+
if (zerosAtEven === 0 && zerosAtOdd >= MIN_ZERO_BYTES_FOR_UTF16) {
|
| 51 |
+
return { encoding: 'utf-16le', seemsBinary: false };
|
| 52 |
+
}
|
| 53 |
+
if (zerosAtOdd === 0 && zerosAtEven >= MIN_ZERO_BYTES_FOR_UTF16) {
|
| 54 |
+
return { encoding: 'utf-16be', seemsBinary: false };
|
| 55 |
+
}
|
| 56 |
+
return { encoding: 'utf-8', seemsBinary: true };
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function classifyTextSample(sample: Uint8Array): TextClassification {
|
| 60 |
+
const sniffed = sniffTextEncoding(sample);
|
| 61 |
+
if (sniffed.seemsBinary || sniffed.encoding !== 'utf-8') {
|
| 62 |
+
return { isBinary: sniffed.seemsBinary, encoding: sniffed.encoding };
|
| 63 |
+
}
|
| 64 |
+
if (sample.includes(0)) {
|
| 65 |
+
return { isBinary: true, encoding: 'utf-8' };
|
| 66 |
+
}
|
| 67 |
+
let end = sample.length;
|
| 68 |
+
for (let i = Math.max(0, sample.length - 3); i < sample.length; i++) {
|
| 69 |
+
const b = sample[i]!;
|
| 70 |
+
const expected =
|
| 71 |
+
b >= 0xc2 && b <= 0xdf ? 2 : b >= 0xe0 && b <= 0xef ? 3 : b >= 0xf0 && b <= 0xf4 ? 4 : 0;
|
| 72 |
+
if (expected === 0 || i + expected <= sample.length) continue;
|
| 73 |
+
let validPrefix = true;
|
| 74 |
+
for (let j = i + 1; j < sample.length; j++) {
|
| 75 |
+
const cb = sample[j]!;
|
| 76 |
+
if (cb < 0x80 || cb > 0xbf) {
|
| 77 |
+
validPrefix = false;
|
| 78 |
+
break;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
if (validPrefix) {
|
| 82 |
+
end = i;
|
| 83 |
+
break;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
let text: string;
|
| 87 |
+
try {
|
| 88 |
+
text = new TextDecoder('utf-8', { fatal: true }).decode(sample.subarray(0, end));
|
| 89 |
+
} catch {
|
| 90 |
+
return { isBinary: true, encoding: 'utf-8' };
|
| 91 |
+
}
|
| 92 |
+
let nonPrintable = 0;
|
| 93 |
+
let total = 0;
|
| 94 |
+
for (const ch of text) {
|
| 95 |
+
const cp = ch.codePointAt(0)!;
|
| 96 |
+
total++;
|
| 97 |
+
if (cp === 9 || cp === 10 || cp === 13) continue;
|
| 98 |
+
if (cp < 32 || (cp >= 0x7f && cp <= 0x9f)) nonPrintable++;
|
| 99 |
+
}
|
| 100 |
+
if (total > 0 && nonPrintable / total > FS_BINARY_NONPRINTABLE_FRACTION) {
|
| 101 |
+
return { isBinary: true, encoding: 'utf-8' };
|
| 102 |
+
}
|
| 103 |
+
return { isBinary: false, encoding: 'utf-8' };
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
export function detectTextEncoding(sample: Uint8Array): TextEncodingDetection {
|
| 107 |
+
const classification = classifyTextSample(sample);
|
| 108 |
+
return { encoding: classification.encoding, seemsBinary: classification.isBinary };
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
export function decodeUtfText(bytes: Uint8Array, encoding: UtfTextEncoding): string {
|
| 112 |
+
return new TextDecoder(encoding, { fatal: false }).decode(bytes);
|
| 113 |
+
}
|
packages/agent-core-v2/src/_base/text/frontmatter.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { load as loadYaml } from 'js-yaml';
|
| 2 |
+
|
| 3 |
+
export class FrontmatterError extends Error {
|
| 4 |
+
constructor(message: string, cause?: unknown) {
|
| 5 |
+
super(message);
|
| 6 |
+
this.name = 'FrontmatterError';
|
| 7 |
+
if (cause !== undefined) {
|
| 8 |
+
Object.defineProperty(this, 'cause', { value: cause, configurable: true });
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface ParsedFrontmatter {
|
| 14 |
+
readonly data: unknown;
|
| 15 |
+
readonly body: string;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const FENCE = '---';
|
| 19 |
+
|
| 20 |
+
export function parseFrontmatter(text: string): ParsedFrontmatter {
|
| 21 |
+
const lines = text.split(/\r?\n/);
|
| 22 |
+
if (lines[0]?.trim() !== FENCE) {
|
| 23 |
+
return { data: null, body: text };
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const close = lines.findIndex((line, index) => index > 0 && line.trim() === FENCE);
|
| 27 |
+
if (close === -1) {
|
| 28 |
+
throw new FrontmatterError('Missing closing frontmatter fence');
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const yamlText = lines.slice(1, close).join('\n').trim();
|
| 32 |
+
const body = lines.slice(close + 1).join('\n');
|
| 33 |
+
if (yamlText === '') {
|
| 34 |
+
return { data: {}, body };
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
try {
|
| 38 |
+
return { data: loadYaml(yamlText) ?? {}, body };
|
| 39 |
+
} catch (error) {
|
| 40 |
+
const message = error instanceof Error ? error.message : String(error);
|
| 41 |
+
throw new FrontmatterError(message, error);
|
| 42 |
+
}
|
| 43 |
+
}
|
packages/agent-core-v2/src/_base/text/line-endings.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type LineEndingStyle = 'lf' | 'crlf' | 'mixed';
|
| 2 |
+
|
| 3 |
+
export interface ModelTextView {
|
| 4 |
+
text: string;
|
| 5 |
+
lineEndingStyle: LineEndingStyle;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export function detectLineEndingStyle(text: string): LineEndingStyle {
|
| 9 |
+
let hasCrLf = false;
|
| 10 |
+
let hasLf = false;
|
| 11 |
+
let hasLoneCr = false;
|
| 12 |
+
|
| 13 |
+
for (let i = 0; i < text.length; i++) {
|
| 14 |
+
const code = text.codePointAt(i);
|
| 15 |
+
if (code === 13) {
|
| 16 |
+
if (text.codePointAt(i + 1) === 10) {
|
| 17 |
+
hasCrLf = true;
|
| 18 |
+
i++;
|
| 19 |
+
} else {
|
| 20 |
+
hasLoneCr = true;
|
| 21 |
+
}
|
| 22 |
+
} else if (code === 10) {
|
| 23 |
+
hasLf = true;
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
if (hasLoneCr || (hasCrLf && hasLf)) return 'mixed';
|
| 28 |
+
if (hasCrLf) return 'crlf';
|
| 29 |
+
return 'lf';
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export function toModelTextView(raw: string): ModelTextView {
|
| 33 |
+
const lineEndingStyle = detectLineEndingStyle(raw);
|
| 34 |
+
if (lineEndingStyle !== 'crlf') {
|
| 35 |
+
return { text: raw, lineEndingStyle };
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return {
|
| 39 |
+
text: raw.replaceAll('\r\n', '\n'),
|
| 40 |
+
lineEndingStyle,
|
| 41 |
+
};
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export function materializeModelText(text: string, lineEndingStyle: LineEndingStyle): string {
|
| 45 |
+
if (lineEndingStyle !== 'crlf') return text;
|
| 46 |
+
return text.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n');
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export function makeCarriageReturnsVisible(text: string): string {
|
| 50 |
+
return text.replaceAll('\r', '\\r');
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export function splitLinesKeepingTerminator(text: string): string[] {
|
| 54 |
+
if (text.length === 0) return [];
|
| 55 |
+
const lines: string[] = [];
|
| 56 |
+
let start = 0;
|
| 57 |
+
for (let i = 0; i < text.length; i += 1) {
|
| 58 |
+
if (text.codePointAt(i) === 0x0a) {
|
| 59 |
+
lines.push(text.slice(start, i + 1));
|
| 60 |
+
start = i + 1;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
if (start < text.length) {
|
| 64 |
+
lines.push(text.slice(start));
|
| 65 |
+
}
|
| 66 |
+
return lines;
|
| 67 |
+
}
|
packages/agent-core-v2/src/_base/utils/abort.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function abortError(message = 'Aborted'): Error {
|
| 2 |
+
const error = new Error(message);
|
| 3 |
+
error.name = 'AbortError';
|
| 4 |
+
return error;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export function isAbortError(error: unknown): error is Error {
|
| 8 |
+
return error instanceof Error && error.name === 'AbortError';
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export class UserCancellationError extends Error {
|
| 12 |
+
readonly userCancelled = true;
|
| 13 |
+
|
| 14 |
+
constructor() {
|
| 15 |
+
super('Aborted by the user');
|
| 16 |
+
this.name = 'AbortError';
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function userCancellationReason(): UserCancellationError {
|
| 21 |
+
return new UserCancellationError();
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function isUserCancellation(value: unknown): value is UserCancellationError {
|
| 25 |
+
return value instanceof UserCancellationError;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
| 29 |
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
| 30 |
+
return new Promise<T>((resolve, reject) => {
|
| 31 |
+
const onAbort = () => {
|
| 32 |
+
reject(abortReason(signal));
|
| 33 |
+
};
|
| 34 |
+
signal.addEventListener('abort', onAbort, { once: true });
|
| 35 |
+
promise.then(resolve, reject).finally(() => {
|
| 36 |
+
signal.removeEventListener('abort', onAbort);
|
| 37 |
+
});
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export function linkAbortSignal(source: AbortSignal, target: AbortController): () => void {
|
| 42 |
+
const onAbort = () => {
|
| 43 |
+
target.abort(source.reason);
|
| 44 |
+
};
|
| 45 |
+
if (source.aborted) {
|
| 46 |
+
onAbort();
|
| 47 |
+
return () => {};
|
| 48 |
+
}
|
| 49 |
+
source.addEventListener('abort', onAbort, { once: true });
|
| 50 |
+
return () => {
|
| 51 |
+
source.removeEventListener('abort', onAbort);
|
| 52 |
+
};
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function abortReason(signal: AbortSignal): Error {
|
| 56 |
+
if (signal.reason instanceof Error && !isDefaultAbortReason(signal.reason)) {
|
| 57 |
+
return signal.reason;
|
| 58 |
+
}
|
| 59 |
+
return abortError();
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function isDefaultAbortReason(reason: Error): boolean {
|
| 63 |
+
return reason.name === 'AbortError' && reason.message === 'This operation was aborted';
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export interface DeadlineAbortSignal {
|
| 67 |
+
readonly signal: AbortSignal;
|
| 68 |
+
readonly timedOut: () => boolean;
|
| 69 |
+
readonly clear: () => void;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export function createDeadlineAbortSignal(
|
| 73 |
+
source: AbortSignal,
|
| 74 |
+
timeoutMs: number,
|
| 75 |
+
): DeadlineAbortSignal {
|
| 76 |
+
const controller = new AbortController();
|
| 77 |
+
const unlinkAbortSignal = linkAbortSignal(source, controller);
|
| 78 |
+
let didTimeout = false;
|
| 79 |
+
let timeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
|
| 80 |
+
didTimeout = true;
|
| 81 |
+
controller.abort(abortError());
|
| 82 |
+
}, timeoutMs);
|
| 83 |
+
|
| 84 |
+
return {
|
| 85 |
+
signal: controller.signal,
|
| 86 |
+
timedOut: () => didTimeout,
|
| 87 |
+
clear: () => {
|
| 88 |
+
if (timeout !== undefined) clearTimeout(timeout);
|
| 89 |
+
timeout = undefined;
|
| 90 |
+
unlinkAbortSignal();
|
| 91 |
+
},
|
| 92 |
+
};
|
| 93 |
+
}
|
packages/agent-core-v2/src/_base/utils/canonical-args.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function canonicalTelemetryArgs(args: unknown): string {
|
| 2 |
+
const json = JSON.stringify(sortJsonValue(args));
|
| 3 |
+
return json ?? String(args);
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
function sortJsonValue(value: unknown): unknown {
|
| 7 |
+
if (Array.isArray(value)) {
|
| 8 |
+
return value.map(sortJsonValue);
|
| 9 |
+
}
|
| 10 |
+
if (!isPlainRecord(value)) {
|
| 11 |
+
return value;
|
| 12 |
+
}
|
| 13 |
+
const out: Record<string, unknown> = {};
|
| 14 |
+
for (const key of Object.keys(value).toSorted()) {
|
| 15 |
+
out[key] = sortJsonValue(value[key]);
|
| 16 |
+
}
|
| 17 |
+
return out;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
| 21 |
+
if (value === null || typeof value !== 'object') return false;
|
| 22 |
+
const proto = Object.getPrototypeOf(value);
|
| 23 |
+
return proto === Object.prototype || proto === null;
|
| 24 |
+
}
|
packages/agent-core-v2/src/_base/utils/env.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const TRUE_BOOLEAN_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
| 2 |
+
const FALSE_BOOLEAN_ENV_VALUES = new Set(['0', 'false', 'no', 'off']);
|
| 3 |
+
|
| 4 |
+
export function parseBooleanEnv(value: string | undefined): boolean | undefined {
|
| 5 |
+
const normalized = value?.trim().toLowerCase();
|
| 6 |
+
if (normalized === undefined || normalized.length === 0) return undefined;
|
| 7 |
+
if (TRUE_BOOLEAN_ENV_VALUES.has(normalized)) return true;
|
| 8 |
+
if (FALSE_BOOLEAN_ENV_VALUES.has(normalized)) return false;
|
| 9 |
+
return undefined;
|
| 10 |
+
}
|
packages/agent-core-v2/src/_base/utils/fileMeta.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { extname } from 'node:path';
|
| 2 |
+
|
| 3 |
+
import { classifyTextSample } from '#/_base/text/encoding';
|
| 4 |
+
|
| 5 |
+
export { FS_BINARY_NONPRINTABLE_FRACTION } from '#/_base/text/encoding';
|
| 6 |
+
|
| 7 |
+
export const FS_BINARY_SAMPLE_BYTES = 4096;
|
| 8 |
+
|
| 9 |
+
export interface FileMetaStat {
|
| 10 |
+
readonly size: number;
|
| 11 |
+
readonly mtimeMs?: number;
|
| 12 |
+
readonly ino?: number;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function detectBinary(buf: Uint8Array): boolean {
|
| 16 |
+
return classifyTextSample(buf).isBinary;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function countLines(text: string): number {
|
| 20 |
+
if (text.length === 0) return 0;
|
| 21 |
+
let n = 1;
|
| 22 |
+
for (let i = 0; i < text.length; i++) {
|
| 23 |
+
if (text.charCodeAt(i) === 10) n++;
|
| 24 |
+
}
|
| 25 |
+
if (text.charCodeAt(text.length - 1) === 10) n--;
|
| 26 |
+
return Math.max(0, n);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export function buildEtag(st: FileMetaStat): string {
|
| 30 |
+
const mtime = Math.floor(st.mtimeMs ?? 0);
|
| 31 |
+
const ino = st.ino ?? 0;
|
| 32 |
+
return [mtime.toString(36), st.size.toString(36), ino.toString(36)].join('-');
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const EXT_TO_MIME: Readonly<Record<string, string>> = {
|
| 36 |
+
'.ts': 'text/typescript',
|
| 37 |
+
'.tsx': 'text/typescript',
|
| 38 |
+
'.js': 'text/javascript',
|
| 39 |
+
'.jsx': 'text/javascript',
|
| 40 |
+
'.mjs': 'text/javascript',
|
| 41 |
+
'.cjs': 'text/javascript',
|
| 42 |
+
'.json': 'application/json',
|
| 43 |
+
'.md': 'text/markdown',
|
| 44 |
+
'.html': 'text/html',
|
| 45 |
+
'.css': 'text/css',
|
| 46 |
+
'.svg': 'image/svg+xml',
|
| 47 |
+
'.png': 'image/png',
|
| 48 |
+
'.jpg': 'image/jpeg',
|
| 49 |
+
'.jpeg': 'image/jpeg',
|
| 50 |
+
'.gif': 'image/gif',
|
| 51 |
+
'.pdf': 'application/pdf',
|
| 52 |
+
'.yaml': 'text/yaml',
|
| 53 |
+
'.yml': 'text/yaml',
|
| 54 |
+
'.toml': 'application/toml',
|
| 55 |
+
'.sh': 'text/x-shellscript',
|
| 56 |
+
'.py': 'text/x-python',
|
| 57 |
+
'.rs': 'text/rust',
|
| 58 |
+
'.go': 'text/x-go',
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
export function guessMime(path: string, isBinary: boolean): string {
|
| 62 |
+
const ext = extname(path).toLowerCase();
|
| 63 |
+
const mapped = EXT_TO_MIME[ext];
|
| 64 |
+
if (mapped !== undefined) return mapped;
|
| 65 |
+
return isBinary ? 'application/octet-stream' : 'text/plain';
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
const APPLICATION_TEXT_ALIASES: Readonly<Record<string, string>> = {
|
| 69 |
+
'application/javascript': 'text/javascript',
|
| 70 |
+
'application/x-javascript': 'text/javascript',
|
| 71 |
+
'application/ecmascript': 'text/javascript',
|
| 72 |
+
'application/yaml': 'text/yaml',
|
| 73 |
+
'application/x-yaml': 'text/yaml',
|
| 74 |
+
'application/sql': 'text/plain',
|
| 75 |
+
'application/graphql': 'text/plain',
|
| 76 |
+
'application/x-www-form-urlencoded': 'text/plain',
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
export function textExtensionForMime(mimeType: string): string | undefined {
|
| 80 |
+
const mime = mimeType.split(';')[0]!.trim().toLowerCase();
|
| 81 |
+
if (mime === 'application/json' || mime.endsWith('+json')) return '.json';
|
| 82 |
+
if (mime === 'application/xml' || mime.endsWith('+xml')) return '.xml';
|
| 83 |
+
if (mime.endsWith('+yaml')) return '.yaml';
|
| 84 |
+
if (mime === 'application/toml') return '.toml';
|
| 85 |
+
if (mime === 'text/csv') return '.csv';
|
| 86 |
+
const textMime = APPLICATION_TEXT_ALIASES[mime] ?? mime;
|
| 87 |
+
if (!textMime.startsWith('text/')) return undefined;
|
| 88 |
+
return Object.entries(EXT_TO_MIME).find(([, value]) => value === textMime)?.[0] ?? '.txt';
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
const EXT_TO_LANGUAGE: Readonly<Record<string, string>> = {
|
| 92 |
+
'.ts': 'typescript',
|
| 93 |
+
'.tsx': 'typescriptreact',
|
| 94 |
+
'.js': 'javascript',
|
| 95 |
+
'.jsx': 'javascriptreact',
|
| 96 |
+
'.mjs': 'javascript',
|
| 97 |
+
'.cjs': 'javascript',
|
| 98 |
+
'.json': 'json',
|
| 99 |
+
'.md': 'markdown',
|
| 100 |
+
'.html': 'html',
|
| 101 |
+
'.css': 'css',
|
| 102 |
+
'.yaml': 'yaml',
|
| 103 |
+
'.yml': 'yaml',
|
| 104 |
+
'.toml': 'toml',
|
| 105 |
+
'.sh': 'shellscript',
|
| 106 |
+
'.py': 'python',
|
| 107 |
+
'.rs': 'rust',
|
| 108 |
+
'.go': 'go',
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
export function guessLanguageId(path: string): string | undefined {
|
| 112 |
+
return EXT_TO_LANGUAGE[extname(path).toLowerCase()];
|
| 113 |
+
}
|
packages/agent-core-v2/src/_base/utils/fs.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomBytes } from 'node:crypto';
|
| 2 |
+
import { closeSync, fsyncSync, openSync } from 'node:fs';
|
| 3 |
+
import * as nodeFs from 'node:fs';
|
| 4 |
+
import { open, rename, unlink } from 'node:fs/promises';
|
| 5 |
+
import { dirname } from 'pathe';
|
| 6 |
+
|
| 7 |
+
export async function syncDir(dirPath: string): Promise<void> {
|
| 8 |
+
if (process.platform === 'win32') return;
|
| 9 |
+
const dirFh = await open(dirPath, 'r');
|
| 10 |
+
try {
|
| 11 |
+
await dirFh.sync();
|
| 12 |
+
} finally {
|
| 13 |
+
await dirFh.close();
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function syncDirSync(dirPath: string): void {
|
| 18 |
+
if (process.platform === 'win32') return;
|
| 19 |
+
const fd = openSync(dirPath, 'r');
|
| 20 |
+
try {
|
| 21 |
+
fsyncSync(fd);
|
| 22 |
+
} finally {
|
| 23 |
+
closeSync(fd);
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export async function writeFileAtomicDurable(
|
| 28 |
+
filePath: string,
|
| 29 |
+
content: string | Uint8Array,
|
| 30 |
+
): Promise<void> {
|
| 31 |
+
const tmpPath = filePath + '.tmp';
|
| 32 |
+
let renamed = false;
|
| 33 |
+
try {
|
| 34 |
+
const fh = await open(tmpPath, 'w');
|
| 35 |
+
try {
|
| 36 |
+
await fh.writeFile(content);
|
| 37 |
+
await fh.sync();
|
| 38 |
+
} finally {
|
| 39 |
+
await fh.close();
|
| 40 |
+
}
|
| 41 |
+
if (process.platform === 'win32') {
|
| 42 |
+
try {
|
| 43 |
+
await unlink(filePath);
|
| 44 |
+
} catch (error) {
|
| 45 |
+
const code = (error as NodeJS.ErrnoException).code;
|
| 46 |
+
if (code !== 'ENOENT') throw error;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
await rename(tmpPath, filePath);
|
| 50 |
+
renamed = true;
|
| 51 |
+
await syncDir(dirname(filePath));
|
| 52 |
+
} finally {
|
| 53 |
+
if (!renamed) {
|
| 54 |
+
try {
|
| 55 |
+
await unlink(tmpPath);
|
| 56 |
+
} catch {
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function syncFd(fd: number): Promise<void> {
|
| 63 |
+
return new Promise<void>((resolve, reject) => {
|
| 64 |
+
nodeFs.fsync(fd, (err) => {
|
| 65 |
+
if (err) {
|
| 66 |
+
reject(err);
|
| 67 |
+
return;
|
| 68 |
+
}
|
| 69 |
+
resolve();
|
| 70 |
+
});
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
export async function atomicWrite(
|
| 75 |
+
filePath: string,
|
| 76 |
+
content: string | Uint8Array,
|
| 77 |
+
_syncOverride?: (fd: number) => Promise<void>,
|
| 78 |
+
mode?: number,
|
| 79 |
+
signal?: AbortSignal,
|
| 80 |
+
): Promise<void> {
|
| 81 |
+
signal?.throwIfAborted();
|
| 82 |
+
const hex = randomBytes(4).toString('hex');
|
| 83 |
+
const tmpPath = `${filePath}.tmp.${process.pid}.${hex}`;
|
| 84 |
+
let renamed = false;
|
| 85 |
+
try {
|
| 86 |
+
const fh = await open(tmpPath, 'w', mode);
|
| 87 |
+
try {
|
| 88 |
+
signal?.throwIfAborted();
|
| 89 |
+
await fh.writeFile(content);
|
| 90 |
+
signal?.throwIfAborted();
|
| 91 |
+
await (_syncOverride ?? syncFd)(fh.fd);
|
| 92 |
+
} finally {
|
| 93 |
+
await fh.close();
|
| 94 |
+
}
|
| 95 |
+
if (process.platform === 'win32') {
|
| 96 |
+
try {
|
| 97 |
+
await unlink(filePath);
|
| 98 |
+
} catch (error) {
|
| 99 |
+
const code = (error as NodeJS.ErrnoException).code;
|
| 100 |
+
if (code !== 'ENOENT') throw error;
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
signal?.throwIfAborted();
|
| 104 |
+
await rename(tmpPath, filePath);
|
| 105 |
+
renamed = true;
|
| 106 |
+
} finally {
|
| 107 |
+
if (!renamed) {
|
| 108 |
+
try {
|
| 109 |
+
await unlink(tmpPath);
|
| 110 |
+
} catch {
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
export async function atomicWriteStream(
|
| 117 |
+
filePath: string,
|
| 118 |
+
source: AsyncIterable<Uint8Array>,
|
| 119 |
+
mode?: number,
|
| 120 |
+
signal?: AbortSignal,
|
| 121 |
+
): Promise<void> {
|
| 122 |
+
signal?.throwIfAborted();
|
| 123 |
+
const hex = randomBytes(4).toString('hex');
|
| 124 |
+
const tmpPath = `${filePath}.tmp.${process.pid}.${hex}`;
|
| 125 |
+
let renamed = false;
|
| 126 |
+
const destroyable = source as AsyncIterable<Uint8Array> & {
|
| 127 |
+
destroy?(error?: Error): void;
|
| 128 |
+
};
|
| 129 |
+
const onAbort = (): void => {
|
| 130 |
+
const reason = signal?.reason instanceof Error ? signal.reason : undefined;
|
| 131 |
+
destroyable.destroy?.(reason);
|
| 132 |
+
};
|
| 133 |
+
signal?.addEventListener('abort', onAbort, { once: true });
|
| 134 |
+
try {
|
| 135 |
+
const fh = await open(tmpPath, 'w', mode);
|
| 136 |
+
try {
|
| 137 |
+
for await (const chunk of source) {
|
| 138 |
+
signal?.throwIfAborted();
|
| 139 |
+
if (chunk.byteLength > 0) {
|
| 140 |
+
await fh.writeFile(chunk);
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
signal?.throwIfAborted();
|
| 144 |
+
await fh.sync();
|
| 145 |
+
} finally {
|
| 146 |
+
await fh.close();
|
| 147 |
+
}
|
| 148 |
+
if (process.platform === 'win32') {
|
| 149 |
+
try {
|
| 150 |
+
await unlink(filePath);
|
| 151 |
+
} catch (error) {
|
| 152 |
+
const code = (error as NodeJS.ErrnoException).code;
|
| 153 |
+
if (code !== 'ENOENT') throw error;
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
signal?.throwIfAborted();
|
| 157 |
+
await rename(tmpPath, filePath);
|
| 158 |
+
renamed = true;
|
| 159 |
+
} finally {
|
| 160 |
+
signal?.removeEventListener('abort', onAbort);
|
| 161 |
+
if (!renamed) {
|
| 162 |
+
try {
|
| 163 |
+
await unlink(tmpPath);
|
| 164 |
+
} catch {
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
}
|
packages/agent-core-v2/src/_base/utils/hero-slug.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomInt } from 'node:crypto';
|
| 2 |
+
|
| 3 |
+
export const HERO_NAMES = [
|
| 4 |
+
'iron-man',
|
| 5 |
+
'spider-man',
|
| 6 |
+
'captain-america',
|
| 7 |
+
'thor',
|
| 8 |
+
'hulk',
|
| 9 |
+
'black-widow',
|
| 10 |
+
'hawkeye',
|
| 11 |
+
'black-panther',
|
| 12 |
+
'doctor-strange',
|
| 13 |
+
'scarlet-witch',
|
| 14 |
+
'vision',
|
| 15 |
+
'falcon',
|
| 16 |
+
'war-machine',
|
| 17 |
+
'ant-man',
|
| 18 |
+
'wasp',
|
| 19 |
+
'captain-marvel',
|
| 20 |
+
'gamora',
|
| 21 |
+
'star-lord',
|
| 22 |
+
'groot',
|
| 23 |
+
'rocket',
|
| 24 |
+
'drax',
|
| 25 |
+
'mantis',
|
| 26 |
+
'nebula',
|
| 27 |
+
'shang-chi',
|
| 28 |
+
'moon-knight',
|
| 29 |
+
'ms-marvel',
|
| 30 |
+
'she-hulk',
|
| 31 |
+
'echo',
|
| 32 |
+
'wolverine',
|
| 33 |
+
'cyclops',
|
| 34 |
+
'storm',
|
| 35 |
+
'jean-grey',
|
| 36 |
+
'rogue',
|
| 37 |
+
'beast',
|
| 38 |
+
'nightcrawler',
|
| 39 |
+
'colossus',
|
| 40 |
+
'shadowcat',
|
| 41 |
+
'jubilee',
|
| 42 |
+
'cable',
|
| 43 |
+
'deadpool',
|
| 44 |
+
'bishop',
|
| 45 |
+
'magik',
|
| 46 |
+
'iceman',
|
| 47 |
+
'archangel',
|
| 48 |
+
'psylocke',
|
| 49 |
+
'dazzler',
|
| 50 |
+
'forge',
|
| 51 |
+
'havok',
|
| 52 |
+
'polaris',
|
| 53 |
+
'emma-frost',
|
| 54 |
+
'namor',
|
| 55 |
+
'silver-surfer',
|
| 56 |
+
'adam-warlock',
|
| 57 |
+
'nova',
|
| 58 |
+
'quasar',
|
| 59 |
+
'sentry',
|
| 60 |
+
'blue-marvel',
|
| 61 |
+
'spectrum',
|
| 62 |
+
'squirrel-girl',
|
| 63 |
+
'cloak',
|
| 64 |
+
'dagger',
|
| 65 |
+
'punisher',
|
| 66 |
+
'elektra',
|
| 67 |
+
'luke-cage',
|
| 68 |
+
'iron-fist',
|
| 69 |
+
'jessica-jones',
|
| 70 |
+
'daredevil',
|
| 71 |
+
'blade',
|
| 72 |
+
'ghost-rider',
|
| 73 |
+
'morbius',
|
| 74 |
+
'venom',
|
| 75 |
+
'carnage',
|
| 76 |
+
'silk',
|
| 77 |
+
'spider-gwen',
|
| 78 |
+
'miles-morales',
|
| 79 |
+
'america-chavez',
|
| 80 |
+
'kate-bishop',
|
| 81 |
+
'yelena-belova',
|
| 82 |
+
'white-tiger',
|
| 83 |
+
'moon-girl',
|
| 84 |
+
'devil-dinosaur',
|
| 85 |
+
'amadeus-cho',
|
| 86 |
+
'riri-williams',
|
| 87 |
+
'kamala-khan',
|
| 88 |
+
'sam-alexander',
|
| 89 |
+
'nova-prime',
|
| 90 |
+
'medusa',
|
| 91 |
+
'black-bolt',
|
| 92 |
+
'crystal',
|
| 93 |
+
'karnak',
|
| 94 |
+
'gorgon',
|
| 95 |
+
'lockjaw',
|
| 96 |
+
'quake',
|
| 97 |
+
'mockingbird',
|
| 98 |
+
'bobbi-morse',
|
| 99 |
+
'maria-hill',
|
| 100 |
+
'nick-fury',
|
| 101 |
+
'phil-coulson',
|
| 102 |
+
'winter-soldier',
|
| 103 |
+
'us-agent',
|
| 104 |
+
'patriot',
|
| 105 |
+
'speed',
|
| 106 |
+
'wiccan',
|
| 107 |
+
'hulkling',
|
| 108 |
+
'stature',
|
| 109 |
+
'yellowjacket',
|
| 110 |
+
'tigra',
|
| 111 |
+
'hellcat',
|
| 112 |
+
'valkyrie',
|
| 113 |
+
'sif',
|
| 114 |
+
'beta-ray-bill',
|
| 115 |
+
'hercules',
|
| 116 |
+
'wonder-man',
|
| 117 |
+
'taskmaster',
|
| 118 |
+
'domino',
|
| 119 |
+
'cannonball',
|
| 120 |
+
'sunspot',
|
| 121 |
+
'wolfsbane',
|
| 122 |
+
'warpath',
|
| 123 |
+
'multiple-man',
|
| 124 |
+
'banshee',
|
| 125 |
+
'siryn',
|
| 126 |
+
'monet',
|
| 127 |
+
'rictor',
|
| 128 |
+
'shatterstar',
|
| 129 |
+
'longshot',
|
| 130 |
+
'daken',
|
| 131 |
+
'x-23',
|
| 132 |
+
'fantomex',
|
| 133 |
+
'batman',
|
| 134 |
+
'superman',
|
| 135 |
+
'wonder-woman',
|
| 136 |
+
'flash',
|
| 137 |
+
'aquaman',
|
| 138 |
+
'green-lantern',
|
| 139 |
+
'martian-manhunter',
|
| 140 |
+
'cyborg',
|
| 141 |
+
'hawkgirl',
|
| 142 |
+
'green-arrow',
|
| 143 |
+
'black-canary',
|
| 144 |
+
'zatanna',
|
| 145 |
+
'constantine',
|
| 146 |
+
'shazam',
|
| 147 |
+
'blue-beetle',
|
| 148 |
+
'booster-gold',
|
| 149 |
+
'firestorm',
|
| 150 |
+
'atom',
|
| 151 |
+
'hawkman',
|
| 152 |
+
'plastic-man',
|
| 153 |
+
'red-tornado',
|
| 154 |
+
'starfire',
|
| 155 |
+
'raven',
|
| 156 |
+
'beast-boy',
|
| 157 |
+
'robin',
|
| 158 |
+
'nightwing',
|
| 159 |
+
'batgirl',
|
| 160 |
+
'batwoman',
|
| 161 |
+
'red-hood',
|
| 162 |
+
'signal',
|
| 163 |
+
'orphan',
|
| 164 |
+
'spoiler',
|
| 165 |
+
'catwoman',
|
| 166 |
+
'huntress',
|
| 167 |
+
'supergirl',
|
| 168 |
+
'superboy',
|
| 169 |
+
'power-girl',
|
| 170 |
+
'steel',
|
| 171 |
+
'stargirl',
|
| 172 |
+
'wildcat',
|
| 173 |
+
'doctor-fate',
|
| 174 |
+
'mister-terrific',
|
| 175 |
+
'hourman',
|
| 176 |
+
'sandman',
|
| 177 |
+
'spectre',
|
| 178 |
+
'phantom-stranger',
|
| 179 |
+
'swamp-thing',
|
| 180 |
+
'animal-man',
|
| 181 |
+
'deadman',
|
| 182 |
+
'vixen',
|
| 183 |
+
'black-lightning',
|
| 184 |
+
'static',
|
| 185 |
+
'icon',
|
| 186 |
+
'rocket-dc',
|
| 187 |
+
'captain-atom',
|
| 188 |
+
'fire',
|
| 189 |
+
'ice',
|
| 190 |
+
'elongated-man',
|
| 191 |
+
'metamorpho',
|
| 192 |
+
'black-hawk',
|
| 193 |
+
'crimson-avenger',
|
| 194 |
+
'doctor-mid-nite',
|
| 195 |
+
'jakeem-thunder',
|
| 196 |
+
'mister-miracle',
|
| 197 |
+
'big-barda',
|
| 198 |
+
'orion',
|
| 199 |
+
'lightray',
|
| 200 |
+
'forager',
|
| 201 |
+
'killer-frost',
|
| 202 |
+
'jessica-cruz',
|
| 203 |
+
'simon-baz',
|
| 204 |
+
'john-stewart',
|
| 205 |
+
'guy-gardner',
|
| 206 |
+
'kyle-rayner',
|
| 207 |
+
'hal-jordan',
|
| 208 |
+
'wally-west',
|
| 209 |
+
'barry-allen',
|
| 210 |
+
'jay-garrick',
|
| 211 |
+
'impulse',
|
| 212 |
+
'kid-flash',
|
| 213 |
+
'donna-troy',
|
| 214 |
+
'tempest',
|
| 215 |
+
'aqualad',
|
| 216 |
+
'miss-martian',
|
| 217 |
+
'terra',
|
| 218 |
+
'jericho',
|
| 219 |
+
'ravager',
|
| 220 |
+
'red-star',
|
| 221 |
+
'pantha',
|
| 222 |
+
'argent',
|
| 223 |
+
'damage',
|
| 224 |
+
'jade',
|
| 225 |
+
'obsidian',
|
| 226 |
+
'cyclone',
|
| 227 |
+
'atom-smasher',
|
| 228 |
+
'maxima',
|
| 229 |
+
'starman',
|
| 230 |
+
'liberty-belle',
|
| 231 |
+
'dove',
|
| 232 |
+
'hawk',
|
| 233 |
+
'blue-devil',
|
| 234 |
+
'creeper',
|
| 235 |
+
'ragman',
|
| 236 |
+
'thunder',
|
| 237 |
+
] as const satisfies readonly [string, ...string[]];
|
| 238 |
+
|
| 239 |
+
const MAX_ATTEMPTS = 20;
|
| 240 |
+
|
| 241 |
+
function pickHero(): string {
|
| 242 |
+
return HERO_NAMES[randomInt(HERO_NAMES.length)]!;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
function assembleSlug(): string {
|
| 246 |
+
return `${pickHero()}-${pickHero()}-${pickHero()}`;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
export function generateHeroSlug(id: string, existing: Set<string>): string {
|
| 250 |
+
let slug = '';
|
| 251 |
+
let collided = true;
|
| 252 |
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
| 253 |
+
slug = assembleSlug();
|
| 254 |
+
if (!existing.has(slug)) {
|
| 255 |
+
collided = false;
|
| 256 |
+
break;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
if (collided) {
|
| 260 |
+
slug = `${slug}-${id.slice(0, 8)}`;
|
| 261 |
+
}
|
| 262 |
+
return slug;
|
| 263 |
+
}
|