DevtoolsExtensionRegistry
class exported by @mwillbanks/tuil-devtools.
class
Public class exported by @mwillbanks/tuil-devtools.
export class DevtoolsExtensionRegistry {
readonly #extensions = new Map<string, DevtoolsExtension>();
readonly #capabilities: ReadonlySet<DevtoolsCapability>;
readonly #history: DevtoolsActionRecord[] = [];
readonly #historyLimit: number;
#historyDropped = 0;
readonly #development: boolean;
readonly #transport?: ProtocolTransport;
readonly #redact: (value: unknown) => unknown;
constructor(
options: {
readonly capabilities?: ReadonlySet<DevtoolsCapability>;
readonly development?: boolean;
readonly transport?: ProtocolTransport;
readonly redact?: (value: unknown) => unknown;
readonly actionHistoryLimit?: number;
} = {},
) {
this.#capabilities =
options.capabilities ??
new Set<DevtoolsCapability>([
"runtime",
"services",
"commands",
"focus",
"semantics",
"layout",
"pointer",
"frames",
"scroll",
"operations",
"workflows",
"routes",
"plugins",
"themes",
"editors",
"logs",
"performance",
"errors",
]);
this.#development = options.development ?? false;
this.#historyLimit = options.actionHistoryLimit ?? 200;
if (!Number.isSafeInteger(this.#historyLimit) || this.#historyLimit <= 0) {
throw new Error(
"Devtools actionHistoryLimit must be a positive safe integer",
);
}
this.#transport = options.transport;
this.#redact = options.redact
? (value) =>
redactDevtoolsValue(options.redact?.(redactDevtoolsValue(value)))
: redactDevtoolsValue;
}
register(extension: DevtoolsExtension): () => void {
validateDevtoolsExtension(extension, this.#extensions, this.#capabilities);
if (extension.activation && !extension.activation()) {
return () => {};
}
this.#extensions.set(extension.id, extension);
void Promise.resolve(
this.#transport?.send(
createProtocolMessage("contribution", {
id: extension.id,
kind: extension.kind,
}),
),
).catch(() => undefined);
return () => {
if (this.#extensions.get(extension.id) !== extension) return;
this.#extensions.delete(extension.id);
extension.dispose?.();
};
}
list<TKind extends DevtoolsExtension["kind"]>(
kind?: TKind,
): readonly Extract<DevtoolsExtension, { readonly kind: TKind }>[] {
return Object.freeze(
[...this.#extensions.values()].filter(
(
extension,
): extension is Extract<DevtoolsExtension, { readonly kind: TKind }> =>
!kind || extension.kind === kind,
),
);
}
inspect(id: string): unknown {
const panel = this.#extensions.get(id);
if (panel?.kind !== "panel") {
throw new Error(`Devtools panel "${id}" is unavailable`);
}
return this.#redact(panel.inspect());
}
search(query: string): readonly DevtoolsPanelContribution[] {
const needle = query.toLowerCase();
return Object.freeze(
this.list("panel").filter((panel) =>
`${panel.title} ${panel.searchText?.() ?? ""}`
.toLowerCase()
.includes(needle),
),
);
}
async execute(id: string, input?: unknown): Promise<unknown> {
const extension = this.#extensions.get(id);
if (extension?.kind !== "action") {
throw new Error(`Devtools action "${id}" is unavailable`);
}
if (!extension.permissions.has("write")) {
throw new Error(`Devtools action "${id}" lacks write permission`);
}
if (!this.#development) {
throw new Error(`Devtools action "${id}" requires development mode`);
}
let result: unknown;
try {
result = await extension.run(input, {
development: true,
record: (entry) => {
this.#record(entry);
},
});
} catch (error) {
const message = redactDevtoolsValue(
error instanceof Error ? error.message : String(error),
) as string;
this.#record({
id,
timestamp: Date.now(),
input,
ok: false,
error: message,
});
throw new Error(message);
}
const entry = this.#record({
id,
timestamp: Date.now(),
input,
ok: true,
});
try {
await this.#transport?.send(createProtocolMessage("command", entry));
} catch {
// Delivery is observational. The completed action must not become a
// retryable failure when a devtools transport is unavailable.
}
return result;
}
#record(entry: DevtoolsActionRecord): DevtoolsActionRecord {
const redacted = Object.freeze({
...entry,
input: this.#redact(entry.input),
error:
entry.error === undefined
? undefined
: (this.#redact(entry.error) as string),
});
this.#history.push(redacted);
const overflow = this.#history.length - this.#historyLimit;
if (overflow > 0) {
this.#history.splice(0, overflow);
this.#historyDropped += overflow;
}
return redacted;
}
actionHistory(): readonly DevtoolsActionRecord[] {
return Object.freeze([...this.#history]);
}
actionHistorySnapshot(): DevtoolsActionHistorySnapshot {
return Object.freeze({
records: this.actionHistory(),
limit: this.#historyLimit,
dropped: this.#historyDropped,
truncated: this.#historyDropped > 0,
});
}
async query(id: string, input: string): Promise<unknown> {
const extension = this.#extensions.get(id);
if (extension?.kind !== "query") {
throw new Error(`Devtools query "${id}" is unavailable`);
}
return extension.query(input);
}
async observe(id: string, input?: unknown): Promise<unknown> {
const extension = this.#extensions.get(id);
if (
!extension ||
extension.kind === "panel" ||
extension.kind === "action" ||
extension.kind === "query"
) {
throw new Error(`Devtools observer "${id}" is unavailable`);
}
return extension.observe(input);
}
diagnosticsBundle(): string {
const panels = Object.fromEntries(
this.list("panel").map((panel) => [panel.id, panel.inspect()]),
);
return JSON.stringify(
this.#redact({
version: 1,
generatedAt: new Date(0).toISOString(),
panels,
actions: this.#history,
actionHistory: {
limit: this.#historyLimit,
dropped: this.#historyDropped,
truncated: this.#historyDropped > 0,
},
}),
null,
2,
);
}
dispose(): void {
for (const extension of this.#extensions.values()) {
extension.dispose?.();
}
this.#extensions.clear();
}
}Members
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
#extensions | Map<string, DevtoolsExtension> | Yes | The #extensions member uses the Map<string, DevtoolsExtension> contract. | DevtoolsExtension |
#capabilities | ReadonlySet<DevtoolsCapability> | Yes | The #capabilities member uses the ReadonlySet<DevtoolsCapability> contract. | DevtoolsCapability |
#history | DevtoolsActionRecord[] | Yes | The #history member uses the DevtoolsActionRecord[] contract. | DevtoolsActionRecord |
#historyLimit | number | Yes | The #historyLimit member uses the number contract. | — |
#historyDropped | number | Yes | The #historyDropped member uses the number contract. | — |
#development | boolean | Yes | The #development member uses the boolean contract. | — |
#transport | ProtocolTransport | undefined | No | The #transport member uses the ProtocolTransport | undefined contract. | — |
#redact | (value: unknown) => unknown | Yes | The #redact member uses the (value: unknown) => unknown contract. | — |
__constructor | any | Yes | The __constructor member uses the any contract. | — |
register | (extension: DevtoolsExtension) => () => void | Yes | The register member uses the (extension: DevtoolsExtension) => () => void contract. | DevtoolsExtension |
list | <TKind extends DevtoolsExtension["kind"]>(kind?: TKind) => readonly Extract<DevtoolsExtension, { readonly kind: TKind; }>[] | Yes | The list member uses the <TKind extends DevtoolsExtension["kind"]>(kind?: TKind) => readonly Extract<DevtoolsExtension, { readonly kind: TKind; }>[] contract. | DevtoolsExtension |
inspect | (id: string) => unknown | Yes | The inspect member uses the (id: string) => unknown contract. | — |
search | (query: string) => readonly DevtoolsPanelContribution[] | Yes | The search member uses the (query: string) => readonly DevtoolsPanelContribution[] contract. | DevtoolsPanelContribution |
execute | (id: string, input?: unknown) => Promise<unknown> | Yes | The execute member uses the (id: string, input?: unknown) => Promise<unknown> contract. | — |
#record | (entry: DevtoolsActionRecord) => DevtoolsActionRecord | Yes | The #record member uses the (entry: DevtoolsActionRecord) => DevtoolsActionRecord contract. | DevtoolsActionRecord |
actionHistory | () => readonly DevtoolsActionRecord[] | Yes | The actionHistory member uses the () => readonly DevtoolsActionRecord[] contract. | DevtoolsActionRecord |
actionHistorySnapshot | () => DevtoolsActionHistorySnapshot | Yes | The actionHistorySnapshot member uses the () => DevtoolsActionHistorySnapshot contract. | DevtoolsActionHistorySnapshot |
query | (id: string, input: string) => Promise<unknown> | Yes | The query member uses the (id: string, input: string) => Promise<unknown> contract. | — |
observe | (id: string, input?: unknown) => Promise<unknown> | Yes | The observe member uses the (id: string, input?: unknown) => Promise<unknown> contract. | — |
diagnosticsBundle | () => string | Yes | The diagnosticsBundle member uses the () => string contract. | — |
dispose | () => void | Yes | The dispose member uses the () => void contract. | — |
Parameters
This declaration has no public members.
Returns
This declaration does not return a value.
Throws
No thrown errors are documented for this declaration.
Related types
DevtoolsActionHistorySnapshotDevtoolsActionRecordDevtoolsCapabilityDevtoolsExtensionDevtoolsPanelContribution
Source
View the secondary source reference