mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(inspector): define shared protocol foundation
This commit is contained in:
@@ -45,6 +45,15 @@
|
|||||||
"@deepseek-ai/dsh-client-ui-directory-picker-native"
|
"@deepseek-ai/dsh-client-ui-directory-picker-native"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"packages/experimental/inspector": {
|
||||||
|
"entry": [
|
||||||
|
"tests/**/*.e2e.ts"
|
||||||
|
],
|
||||||
|
"project": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"tests/**/*.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
"packages/extensions/cordis-host-runner": {
|
"packages/extensions/cordis-host-runner": {
|
||||||
"entry": [
|
"entry": [
|
||||||
"tests/**/*.spec.ts"
|
"tests/**/*.spec.ts"
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-experimental-inspector",
|
||||||
|
"description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection",
|
||||||
|
"version": "0.1.1-rc.2",
|
||||||
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||||
|
"directory": "packages/experimental/inspector"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/types/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/types/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./client": {
|
||||||
|
"types": "./lib/types/client/index.d.ts",
|
||||||
|
"default": "./lib/client.js"
|
||||||
|
},
|
||||||
|
"./invariant": {
|
||||||
|
"types": "./lib/types/invariant.d.ts",
|
||||||
|
"default": "./lib/invariant.js"
|
||||||
|
},
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"dsh": {
|
||||||
|
"client": {
|
||||||
|
"inject": [],
|
||||||
|
"platform": "web",
|
||||||
|
"immediately": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/index.js",
|
||||||
|
"lib/invariant.js",
|
||||||
|
"lib/client.js",
|
||||||
|
"lib/types/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-util-crypto": "workspace:^",
|
||||||
|
"@deepseek-ai/schemastery": "workspace:^",
|
||||||
|
"ws": "^8.21.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@deepseek-ai/cordis": "workspace:^",
|
||||||
|
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@deepseek-ai/cordis": "workspace:^",
|
||||||
|
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
||||||
|
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"playwright": "^1.49.0",
|
||||||
|
"tsx": "^4.19.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/** Package-owned invariant companion for the experimental Inspector. */
|
||||||
|
|
||||||
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
|
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||||
|
|
||||||
|
const PACKAGE_NAME = '@deepseek-ai/dsh-experimental-inspector'
|
||||||
|
|
||||||
|
/** Cordis companion plugin name. */
|
||||||
|
export const name = 'experimental-inspector-invariant'
|
||||||
|
|
||||||
|
/** Service required before the companion can reserve package ownership. */
|
||||||
|
export const inject = ['invariants']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No runtime invariant: wire parsing, generations, Worker lifecycle, and CDP
|
||||||
|
* sessions reject invalid relationships in their owning operations.
|
||||||
|
*/
|
||||||
|
const install: InvariantInstaller = () => {}
|
||||||
|
|
||||||
|
/** Register this package's invariant companion. */
|
||||||
|
export const apply = (ctx: Context): Promise<() => void> =>
|
||||||
|
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/** Realm-neutral bounded buffering for Host and Client observation sources. */
|
||||||
|
|
||||||
|
import type { InspectorSourceGeneration, InspectorSourceId } from './ids.ts'
|
||||||
|
import { isJsonValue, jsonByteLength, type InspectorJsonValue } from '../json.ts'
|
||||||
|
import type { InspectorRecordInput, SourceAppendFrame, SourceReplaceFrame } from './messages/observation.ts'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION } from './version.ts'
|
||||||
|
|
||||||
|
const SOURCE_FRAME_OVERHEAD_BYTES = 4_096
|
||||||
|
|
||||||
|
/** Limits and declared topics shared by both source transports. */
|
||||||
|
export interface InspectorSourceBufferOptions {
|
||||||
|
readonly topics: readonly string[]
|
||||||
|
readonly maxQueuedRecords: number
|
||||||
|
readonly maxQueuedBytes: number
|
||||||
|
readonly maxRecordsPerFrame: number
|
||||||
|
readonly maxFrameBytes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueuedRecord {
|
||||||
|
sequence: number
|
||||||
|
readonly bytes: number
|
||||||
|
readonly record: InspectorRecordInput
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns retained state, queued events, and source-local sequencing independently
|
||||||
|
* of whether frames travel over MessagePort or WebSocket.
|
||||||
|
*/
|
||||||
|
export class InspectorSourceBuffer {
|
||||||
|
private readonly queue: QueuedRecord[] = []
|
||||||
|
private readonly state = new Map<string, InspectorRecordInput>()
|
||||||
|
private queuedBytes = 0
|
||||||
|
private nextSequence = 1
|
||||||
|
private expectedSequence = 1
|
||||||
|
|
||||||
|
constructor(private readonly options: InspectorSourceBufferOptions) {}
|
||||||
|
|
||||||
|
/** Whether at least one observation is waiting for transport. */
|
||||||
|
get hasPending(): boolean {
|
||||||
|
return this.queue.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and enqueue one observation, dropping the oldest prefix as needed.
|
||||||
|
* @param topic - Declared domain topic.
|
||||||
|
* @param payload - Lossless JSON payload.
|
||||||
|
* @param monotonicMs - Finite source-clock timestamp.
|
||||||
|
*/
|
||||||
|
publish(topic: string, payload: InspectorJsonValue, monotonicMs: number): void {
|
||||||
|
this.enqueue(this.record(topic, payload, monotonicMs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace one retained topic and enqueue the same observation for live delivery.
|
||||||
|
* @param topic - Declared state topic.
|
||||||
|
* @param payload - Lossless JSON payload retained for replacement frames.
|
||||||
|
* @param monotonicMs - Finite source-clock timestamp.
|
||||||
|
*/
|
||||||
|
setState(topic: string, payload: InspectorJsonValue, monotonicMs: number): void {
|
||||||
|
const record = this.record(topic, payload, monotonicMs)
|
||||||
|
const previous = this.state.get(topic)
|
||||||
|
this.state.set(topic, record)
|
||||||
|
if (!this.stateFits()) {
|
||||||
|
if (previous === undefined) this.state.delete(topic)
|
||||||
|
else this.state.set(topic, previous)
|
||||||
|
throw new Error('inspector: source state exceeds the source-frame byte limit')
|
||||||
|
}
|
||||||
|
this.enqueue(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a complete state replacement and absorb every preceding queue drop.
|
||||||
|
* @param sourceId - Logical source identity.
|
||||||
|
* @param generation - Current transport generation.
|
||||||
|
* @returns A replacement frame whose sequence is the next append position.
|
||||||
|
*/
|
||||||
|
replacement(sourceId: InspectorSourceId, generation: InspectorSourceGeneration): SourceReplaceFrame {
|
||||||
|
const nextSequence = this.queue[0]?.sequence ?? this.nextSequence
|
||||||
|
this.expectedSequence = nextSequence
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'source/replace',
|
||||||
|
sourceId,
|
||||||
|
generation,
|
||||||
|
nextSequence,
|
||||||
|
records: [...this.state.values()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove and sequence the next transport-sized observation batch.
|
||||||
|
* @param sourceId - Logical source identity.
|
||||||
|
* @param generation - Current transport generation.
|
||||||
|
* @returns The next append frame, or `undefined` when the queue is empty.
|
||||||
|
*/
|
||||||
|
takeBatch(sourceId: InspectorSourceId, generation: InspectorSourceGeneration): SourceAppendFrame | undefined {
|
||||||
|
if (this.queue.length === 0) return undefined
|
||||||
|
const batch: QueuedRecord[] = []
|
||||||
|
let batchBytes = SOURCE_FRAME_OVERHEAD_BYTES
|
||||||
|
const first = this.queue[0]
|
||||||
|
if (first === undefined) throw new Error('inspector: non-empty source queue has no first record')
|
||||||
|
while (batch.length < this.options.maxRecordsPerFrame && this.queue.length > 0) {
|
||||||
|
const candidate = this.queue[0]
|
||||||
|
if (candidate === undefined) break
|
||||||
|
if (candidate.sequence !== first.sequence + batch.length) break
|
||||||
|
if (batch.length > 0 && batchBytes + candidate.bytes > this.options.maxFrameBytes) break
|
||||||
|
this.queue.shift()
|
||||||
|
batch.push(candidate)
|
||||||
|
batchBytes += candidate.bytes
|
||||||
|
}
|
||||||
|
this.queuedBytes -= batch.reduce((sum, item) => sum + item.bytes, 0)
|
||||||
|
const firstSequence = first.sequence
|
||||||
|
const frame: SourceAppendFrame = {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'source/append',
|
||||||
|
sourceId,
|
||||||
|
generation,
|
||||||
|
firstSequence,
|
||||||
|
droppedBefore: firstSequence - this.expectedSequence,
|
||||||
|
records: batch.map(item => item.record),
|
||||||
|
}
|
||||||
|
this.expectedSequence = firstSequence + frame.records.length
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
private record(topic: string, payload: InspectorJsonValue, monotonicMs: number): InspectorRecordInput {
|
||||||
|
if (topic.length === 0 || topic.length > 128) {
|
||||||
|
throw new Error('inspector: topic must contain 1 to 128 characters')
|
||||||
|
}
|
||||||
|
if (!this.options.topics.includes('*') && !this.options.topics.includes(topic)) {
|
||||||
|
throw new Error(`inspector: source does not declare topic ${JSON.stringify(topic)}`)
|
||||||
|
}
|
||||||
|
if (!isJsonValue(payload)) throw new Error('inspector: source payload must be lossless JSON data')
|
||||||
|
if (!Number.isFinite(monotonicMs)) throw new Error('inspector: monotonicMs must be finite')
|
||||||
|
return { monotonicMs, topic, payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enqueue(record: InspectorRecordInput): void {
|
||||||
|
const bytes = jsonByteLength(record as unknown as InspectorJsonValue)
|
||||||
|
const sequence = this.nextSequence++
|
||||||
|
if (bytes + SOURCE_FRAME_OVERHEAD_BYTES > this.options.maxFrameBytes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.queue.push({ sequence, bytes, record })
|
||||||
|
this.queuedBytes += bytes
|
||||||
|
while (this.queue.length > this.options.maxQueuedRecords || this.queuedBytes > this.options.maxQueuedBytes) {
|
||||||
|
const dropped = this.queue.shift()
|
||||||
|
if (dropped === undefined) break
|
||||||
|
this.queuedBytes -= dropped.bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stateFits(): boolean {
|
||||||
|
return jsonByteLength([...this.state.values()] as unknown as InspectorJsonValue) + SOURCE_FRAME_OVERHEAD_BYTES
|
||||||
|
<= this.options.maxFrameBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/** Bridge-facing exports for lossless JSON values and common wire validators. */
|
||||||
|
|
||||||
|
export * from '../json.ts'
|
||||||
|
export * from '../validation.ts'
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/** Exact decoders for Host, Worker, and injected Client lifecycle values. */
|
||||||
|
|
||||||
|
import type {
|
||||||
|
InspectorClientBootstrap,
|
||||||
|
InspectorHostControl,
|
||||||
|
InspectorWorkerConfig,
|
||||||
|
InspectorWorkerControl,
|
||||||
|
} from './messages/control.ts'
|
||||||
|
import { isPlainObject } from '../json.ts'
|
||||||
|
import { exactKeys, exactObject } from '../validation.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the structured-cloned Worker configuration.
|
||||||
|
* @param value - Untrusted workerData config value.
|
||||||
|
* @returns The validated Worker configuration.
|
||||||
|
*/
|
||||||
|
export function parseInspectorWorkerConfig(value: unknown): InspectorWorkerConfig {
|
||||||
|
const record = exactObject(value, [
|
||||||
|
'host', 'startPort', 'targetId', 'clientToken', 'clientOrigins', 'maxSourceFrameBytes',
|
||||||
|
'maxSourceRecordsPerFrame', 'maxRetainedRequests', 'maxJournalBytes', 'clientRuntimeTimeoutMs', 'maxCordisNodes',
|
||||||
|
'maxDisconnectedCordisTrees', 'maxClientSourceBytes',
|
||||||
|
], 'Worker config')
|
||||||
|
if (record.host !== '127.0.0.1') throw new Error('inspector protocol: Worker host must be 127.0.0.1')
|
||||||
|
if (typeof record.targetId !== 'string' || record.targetId.length === 0) {
|
||||||
|
throw new Error('inspector protocol: Worker targetId must be a non-empty string')
|
||||||
|
}
|
||||||
|
if (typeof record.clientToken !== 'string' || record.clientToken.length === 0) {
|
||||||
|
throw new Error('inspector protocol: Worker clientToken must be a non-empty string')
|
||||||
|
}
|
||||||
|
if (!Array.isArray(record.clientOrigins) || !record.clientOrigins.every(origin => typeof origin === 'string')) {
|
||||||
|
throw new Error('inspector protocol: Worker clientOrigins must be strings')
|
||||||
|
}
|
||||||
|
const startPort = natural(record.startPort, 'startPort', true)
|
||||||
|
if (startPort > 65_535) throw new Error('inspector protocol: Worker startPort must not exceed 65535')
|
||||||
|
return {
|
||||||
|
host: record.host,
|
||||||
|
startPort,
|
||||||
|
targetId: record.targetId,
|
||||||
|
clientToken: record.clientToken,
|
||||||
|
clientOrigins: record.clientOrigins,
|
||||||
|
maxSourceFrameBytes: natural(record.maxSourceFrameBytes, 'maxSourceFrameBytes'),
|
||||||
|
maxSourceRecordsPerFrame: natural(record.maxSourceRecordsPerFrame, 'maxSourceRecordsPerFrame'),
|
||||||
|
maxRetainedRequests: natural(record.maxRetainedRequests, 'maxRetainedRequests'),
|
||||||
|
maxJournalBytes: natural(record.maxJournalBytes, 'maxJournalBytes'),
|
||||||
|
clientRuntimeTimeoutMs: natural(record.clientRuntimeTimeoutMs, 'clientRuntimeTimeoutMs'),
|
||||||
|
maxClientSourceBytes: natural(record.maxClientSourceBytes, 'maxClientSourceBytes'),
|
||||||
|
maxCordisNodes: natural(record.maxCordisNodes, 'maxCordisNodes'),
|
||||||
|
maxDisconnectedCordisTrees: natural(record.maxDisconnectedCordisTrees, 'maxDisconnectedCordisTrees', true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode one Host-to-Worker lifecycle command.
|
||||||
|
* @param value - Untrusted control message.
|
||||||
|
* @returns The validated Host command.
|
||||||
|
*/
|
||||||
|
export function parseInspectorHostControl(value: unknown): InspectorHostControl {
|
||||||
|
const record = exactObject(value, ['type'], 'Host control message')
|
||||||
|
if (record.type !== 'shutdown') throw new Error('inspector protocol: unknown Host control message')
|
||||||
|
return { type: 'shutdown' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode one Worker-to-Host lifecycle event.
|
||||||
|
* @param value - Untrusted control message.
|
||||||
|
* @returns The validated Worker event.
|
||||||
|
*/
|
||||||
|
export function parseInspectorWorkerControl(value: unknown): InspectorWorkerControl {
|
||||||
|
const record = exactObjectByType(value, 'Worker control message')
|
||||||
|
switch (record.type) {
|
||||||
|
case 'ready':
|
||||||
|
exactKeys(record, ['type', 'host', 'port', 'targetId'], 'Worker ready message')
|
||||||
|
if (typeof record.host !== 'string' || typeof record.targetId !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid Worker ready identity')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'ready',
|
||||||
|
host: record.host,
|
||||||
|
port: natural(record.port, 'port', true),
|
||||||
|
targetId: record.targetId,
|
||||||
|
}
|
||||||
|
case 'failure':
|
||||||
|
exactKeys(record, ['type', 'message'], 'Worker failure message')
|
||||||
|
if (typeof record.message !== 'string') throw new Error('inspector protocol: invalid Worker failure')
|
||||||
|
return { type: 'failure', message: record.message }
|
||||||
|
case 'stopped':
|
||||||
|
exactKeys(record, ['type'], 'Worker stopped message')
|
||||||
|
return { type: 'stopped' }
|
||||||
|
default:
|
||||||
|
throw new Error('inspector protocol: unknown Worker control message')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode bootstrap data injected into the browser global.
|
||||||
|
* @param value - Untrusted injected value.
|
||||||
|
* @returns The validated Client bootstrap.
|
||||||
|
*/
|
||||||
|
export function parseInspectorClientBootstrap(value: unknown): InspectorClientBootstrap {
|
||||||
|
const record = exactObject(value, [
|
||||||
|
'endpoint', 'protocol', 'maxQueuedRecords', 'maxQueuedBytes', 'maxRecordsPerFrame', 'maxFrameBytes',
|
||||||
|
'reconnectBaseMs', 'reconnectMaxMs', 'queryTimeoutMs', 'maxRuntimeObjectsPerSession',
|
||||||
|
'maxRuntimePropertiesPerResult', 'maxCordisNodes', 'maxClientSourceBytes',
|
||||||
|
], 'Client bootstrap')
|
||||||
|
if (typeof record.endpoint !== 'string' || typeof record.protocol !== 'string') {
|
||||||
|
throw new Error('inspector protocol: Client bootstrap endpoint and protocol must be strings')
|
||||||
|
}
|
||||||
|
let endpoint: URL
|
||||||
|
try {
|
||||||
|
endpoint = new URL(record.endpoint)
|
||||||
|
} catch {
|
||||||
|
throw new Error('inspector protocol: Client bootstrap endpoint must be an absolute URL')
|
||||||
|
}
|
||||||
|
if (endpoint.protocol !== 'ws:' || endpoint.hostname !== '127.0.0.1') {
|
||||||
|
throw new Error('inspector protocol: Client bootstrap endpoint must use ws on 127.0.0.1')
|
||||||
|
}
|
||||||
|
if (record.protocol.length === 0 || record.protocol.length > 256) {
|
||||||
|
throw new Error('inspector protocol: Client bootstrap protocol must contain 1 to 256 characters')
|
||||||
|
}
|
||||||
|
const bootstrap: InspectorClientBootstrap = {
|
||||||
|
endpoint: record.endpoint,
|
||||||
|
protocol: record.protocol,
|
||||||
|
maxQueuedRecords: natural(record.maxQueuedRecords, 'maxQueuedRecords'),
|
||||||
|
maxQueuedBytes: natural(record.maxQueuedBytes, 'maxQueuedBytes'),
|
||||||
|
maxRecordsPerFrame: natural(record.maxRecordsPerFrame, 'maxRecordsPerFrame'),
|
||||||
|
maxFrameBytes: natural(record.maxFrameBytes, 'maxFrameBytes'),
|
||||||
|
reconnectBaseMs: natural(record.reconnectBaseMs, 'reconnectBaseMs'),
|
||||||
|
reconnectMaxMs: natural(record.reconnectMaxMs, 'reconnectMaxMs'),
|
||||||
|
queryTimeoutMs: natural(record.queryTimeoutMs, 'queryTimeoutMs'),
|
||||||
|
maxRuntimeObjectsPerSession: natural(record.maxRuntimeObjectsPerSession, 'maxRuntimeObjectsPerSession'),
|
||||||
|
maxRuntimePropertiesPerResult: natural(record.maxRuntimePropertiesPerResult, 'maxRuntimePropertiesPerResult'),
|
||||||
|
maxClientSourceBytes: natural(record.maxClientSourceBytes, 'maxClientSourceBytes'),
|
||||||
|
maxCordisNodes: natural(record.maxCordisNodes, 'maxCordisNodes'),
|
||||||
|
}
|
||||||
|
if (bootstrap.reconnectMaxMs < bootstrap.reconnectBaseMs) {
|
||||||
|
throw new Error('inspector protocol: reconnectMaxMs must be at least reconnectBaseMs')
|
||||||
|
}
|
||||||
|
return bootstrap
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactObjectByType(value: unknown, label: string): Record<string, unknown> {
|
||||||
|
if (!isPlainObject(value) || typeof value.type !== 'string') {
|
||||||
|
throw new Error(`inspector protocol: ${label} must have a type`)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function natural(value: unknown, label: string, zero = false): number {
|
||||||
|
if (!Number.isSafeInteger(value) || (value as number) < (zero ? 0 : 1)) {
|
||||||
|
throw new Error(`inspector protocol: ${label} must be ${zero ? 'a non-negative' : 'a positive'} safe integer`)
|
||||||
|
}
|
||||||
|
return value as number
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/** Opaque identifiers owned by the cross-realm Inspector bridge. */
|
||||||
|
|
||||||
|
import type { InspectorId } from '../identity.ts'
|
||||||
|
|
||||||
|
export { inspectorId } from '../identity.ts'
|
||||||
|
export type { InspectorId } from '../identity.ts'
|
||||||
|
|
||||||
|
/** Stable identity of one logical observation source. */
|
||||||
|
export type InspectorSourceId = InspectorId<'InspectorSourceId'>
|
||||||
|
|
||||||
|
/** Identity of one source connection generation. */
|
||||||
|
export type InspectorSourceGeneration = InspectorId<'InspectorSourceGeneration'>
|
||||||
|
|
||||||
|
/** Identity of one DevTools connection's Client Runtime state. */
|
||||||
|
export type ClientRuntimeSessionId = InspectorId<'ClientRuntimeSessionId'>
|
||||||
|
|
||||||
|
/** Identity of one in-flight Worker-to-Client Runtime operation. */
|
||||||
|
export type ClientRuntimeRequestId = InspectorId<'ClientRuntimeRequestId'>
|
||||||
|
|
||||||
|
/** Identity of one DevTools connection's Client source catalog session. */
|
||||||
|
export type ClientSourceSessionId = InspectorId<'ClientSourceSessionId'>
|
||||||
|
|
||||||
|
/** Identity of one in-flight Worker-to-Client source operation. */
|
||||||
|
export type ClientSourceRequestId = InspectorId<'ClientSourceRequestId'>
|
||||||
|
|
||||||
|
/** Opaque reference to an object retained inside one Client Runtime session. */
|
||||||
|
export type ClientRemoteObjectHandle = InspectorId<'ClientRemoteObjectHandle'>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/** Host-to-Worker lifecycle messages and Worker readiness results. */
|
||||||
|
|
||||||
|
/** Fully resolved Worker configuration. */
|
||||||
|
export interface InspectorWorkerConfig {
|
||||||
|
readonly host: '127.0.0.1'
|
||||||
|
/** First port to bind; zero delegates selection to the operating system. */
|
||||||
|
readonly startPort: number
|
||||||
|
readonly targetId: string
|
||||||
|
readonly clientToken: string
|
||||||
|
readonly clientOrigins: readonly string[]
|
||||||
|
readonly maxSourceFrameBytes: number
|
||||||
|
readonly maxSourceRecordsPerFrame: number
|
||||||
|
readonly maxRetainedRequests: number
|
||||||
|
readonly maxJournalBytes: number
|
||||||
|
readonly clientRuntimeTimeoutMs: number
|
||||||
|
readonly maxClientSourceBytes: number
|
||||||
|
readonly maxCordisNodes: number
|
||||||
|
readonly maxDisconnectedCordisTrees: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Structured-clone payload used to start the Inspector Worker. */
|
||||||
|
export interface InspectorWorkerBoot<Port> {
|
||||||
|
readonly config: InspectorWorkerConfig
|
||||||
|
readonly hostSourcePort: Port
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Host request to stop accepting traffic and close every Worker-owned resource. */
|
||||||
|
export interface InspectorWorkerShutdown {
|
||||||
|
readonly type: 'shutdown'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every control message sent from Host to Worker after boot. */
|
||||||
|
export type InspectorHostControl = InspectorWorkerShutdown
|
||||||
|
|
||||||
|
/** Worker endpoint readiness. */
|
||||||
|
export interface InspectorWorkerReady {
|
||||||
|
readonly type: 'ready'
|
||||||
|
readonly host: string
|
||||||
|
readonly port: number
|
||||||
|
readonly targetId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker startup or runtime failure. */
|
||||||
|
export interface InspectorWorkerFailure {
|
||||||
|
readonly type: 'failure'
|
||||||
|
readonly message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker completed graceful shutdown. */
|
||||||
|
export interface InspectorWorkerStopped {
|
||||||
|
readonly type: 'stopped'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every control message sent from Worker to Host. */
|
||||||
|
export type InspectorWorkerControl = InspectorWorkerReady | InspectorWorkerFailure | InspectorWorkerStopped
|
||||||
|
|
||||||
|
/** Browser bootstrap injected by the Host plugin. */
|
||||||
|
export interface InspectorClientBootstrap {
|
||||||
|
readonly endpoint: string
|
||||||
|
readonly protocol: string
|
||||||
|
readonly maxQueuedRecords: number
|
||||||
|
readonly maxQueuedBytes: number
|
||||||
|
readonly maxRecordsPerFrame: number
|
||||||
|
readonly maxFrameBytes: number
|
||||||
|
readonly reconnectBaseMs: number
|
||||||
|
readonly reconnectMaxMs: number
|
||||||
|
readonly queryTimeoutMs: number
|
||||||
|
readonly maxRuntimeObjectsPerSession: number
|
||||||
|
readonly maxRuntimePropertiesPerResult: number
|
||||||
|
readonly maxClientSourceBytes: number
|
||||||
|
readonly maxCordisNodes: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/** Versioned source lifecycle, observation, and extension frames shared by both carriers. */
|
||||||
|
|
||||||
|
import { inspectorId, type InspectorSourceGeneration, type InspectorSourceId } from '../ids.ts'
|
||||||
|
import { isJsonValue, isPlainObject, type InspectorJsonValue } from '../../json.ts'
|
||||||
|
import { exactKeys } from '../../validation.ts'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION } from '../version.ts'
|
||||||
|
import {
|
||||||
|
parseClientConsoleCapability,
|
||||||
|
parseClientConsoleControlFrame,
|
||||||
|
parseClientConsoleEventFrame,
|
||||||
|
parseClientRuntimeCapability,
|
||||||
|
parseClientRuntimeRequestFrame,
|
||||||
|
parseClientRuntimeResponseFrame,
|
||||||
|
parseClientRuntimeSessionClosedFrame,
|
||||||
|
type ClientConsoleCapability,
|
||||||
|
type ClientConsoleDisableFrame,
|
||||||
|
type ClientConsoleEnableFrame,
|
||||||
|
type ClientConsoleEventFrame,
|
||||||
|
type ClientRuntimeCapability,
|
||||||
|
type ClientRuntimeRequestFrame,
|
||||||
|
type ClientRuntimeResponseFrame,
|
||||||
|
type ClientRuntimeSessionClosedFrame,
|
||||||
|
} from './runtime/index.ts'
|
||||||
|
import {
|
||||||
|
parseClientSourceRequestFrame,
|
||||||
|
parseClientSourceResponseFrame,
|
||||||
|
parseClientSourceSessionClosedFrame,
|
||||||
|
parseClientSourcesCapability,
|
||||||
|
type ClientSourceRequestFrame,
|
||||||
|
type ClientSourceResponseFrame,
|
||||||
|
type ClientSourceSessionClosedFrame,
|
||||||
|
type ClientSourcesCapability,
|
||||||
|
} from './sources/index.ts'
|
||||||
|
|
||||||
|
export { INSPECTOR_PROTOCOL_VERSION } from '../version.ts'
|
||||||
|
|
||||||
|
/** Realm producing observations. */
|
||||||
|
export type InspectorSourceKind = 'host' | 'client'
|
||||||
|
|
||||||
|
/** Optional protocols implemented by one source generation. */
|
||||||
|
export type InspectorSourceCapability = ClientRuntimeCapability | ClientConsoleCapability | ClientSourcesCapability
|
||||||
|
|
||||||
|
/** One logical source and connection generation. */
|
||||||
|
export interface InspectorSourceDescriptor {
|
||||||
|
/** Producer identity retained across transport reconnects. */
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
/** One transport admission, replaced on every reconnect. */
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly kind: InspectorSourceKind
|
||||||
|
readonly label: string
|
||||||
|
readonly timeOriginMs: number
|
||||||
|
readonly capabilities: readonly InspectorSourceCapability[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One domain-owned observation before its sequence is assigned. */
|
||||||
|
export interface InspectorRecordInput {
|
||||||
|
readonly monotonicMs: number
|
||||||
|
readonly topic: string
|
||||||
|
readonly payload: InspectorJsonValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initial source handshake. */
|
||||||
|
export interface SourceOpenFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/open'
|
||||||
|
readonly source: InspectorSourceDescriptor
|
||||||
|
readonly topics: readonly string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replace one source's current state after opening or resynchronization. */
|
||||||
|
export interface SourceReplaceFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/replace'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly nextSequence: number
|
||||||
|
readonly records: readonly InspectorRecordInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Append one contiguous observation batch. */
|
||||||
|
export interface SourceAppendFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/append'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly firstSequence: number
|
||||||
|
readonly droppedBefore: number
|
||||||
|
readonly records: readonly InspectorRecordInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clean source closure. */
|
||||||
|
export interface SourceCloseFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/close'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every source-to-Worker frame. */
|
||||||
|
export type SourceToWorkerFrame =
|
||||||
|
| SourceOpenFrame
|
||||||
|
| SourceReplaceFrame
|
||||||
|
| SourceAppendFrame
|
||||||
|
| SourceCloseFrame
|
||||||
|
| ClientConsoleEventFrame
|
||||||
|
| ClientRuntimeResponseFrame
|
||||||
|
| ClientSourceResponseFrame
|
||||||
|
|
||||||
|
/** Worker acceptance of one source generation. */
|
||||||
|
export interface SourceAcceptedFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/accepted'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker request for a complete source-state replacement. */
|
||||||
|
export interface SourceResnapshotFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/resnapshot'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly expectedSequence: number
|
||||||
|
readonly reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rejection of one malformed or incompatible source connection. */
|
||||||
|
export interface SourceRejectedFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'source/rejected'
|
||||||
|
readonly code: 'invalid-frame' | 'version-mismatch' | 'unauthorized'
|
||||||
|
readonly message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every Worker-to-source control frame. */
|
||||||
|
export type WorkerToSourceFrame =
|
||||||
|
| SourceAcceptedFrame
|
||||||
|
| SourceResnapshotFrame
|
||||||
|
| SourceRejectedFrame
|
||||||
|
| ClientConsoleEnableFrame
|
||||||
|
| ClientConsoleDisableFrame
|
||||||
|
| ClientRuntimeRequestFrame
|
||||||
|
| ClientRuntimeSessionClosedFrame
|
||||||
|
| ClientSourceRequestFrame
|
||||||
|
| ClientSourceSessionClosedFrame
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one Worker control frame received by a source.
|
||||||
|
* @param value - Untrusted decoded wire value.
|
||||||
|
* @returns The validated Worker-to-source frame.
|
||||||
|
*/
|
||||||
|
export function parseWorkerSourceFrame(value: unknown): WorkerToSourceFrame {
|
||||||
|
if (!isJsonValue(value)
|
||||||
|
|| !isPlainObject(value)
|
||||||
|
|| value.v !== INSPECTOR_PROTOCOL_VERSION
|
||||||
|
|| typeof value.t !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid Worker source frame')
|
||||||
|
}
|
||||||
|
if (value.t === 'source/rejected') {
|
||||||
|
exactKeys(value, ['v', 't', 'code', 'message'], 'source/rejected frame')
|
||||||
|
if ((value.code !== 'invalid-frame' && value.code !== 'version-mismatch' && value.code !== 'unauthorized')
|
||||||
|
|| typeof value.message !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid source/rejected frame')
|
||||||
|
}
|
||||||
|
return { v: INSPECTOR_PROTOCOL_VERSION, t: 'source/rejected', code: value.code, message: value.message }
|
||||||
|
}
|
||||||
|
if (value.t === 'client-runtime/request') return parseClientRuntimeRequestFrame(value)
|
||||||
|
if (value.t === 'client-runtime/session-closed') return parseClientRuntimeSessionClosedFrame(value)
|
||||||
|
if (value.t === 'client-sources/request') return parseClientSourceRequestFrame(value)
|
||||||
|
if (value.t === 'client-sources/session-closed') return parseClientSourceSessionClosedFrame(value)
|
||||||
|
if (value.t === 'client-console/enable' || value.t === 'client-console/disable') {
|
||||||
|
return parseClientConsoleControlFrame(value)
|
||||||
|
}
|
||||||
|
const common = {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
sourceId: sourceId(value.sourceId),
|
||||||
|
generation: generation(value.generation),
|
||||||
|
} as const
|
||||||
|
if (value.t === 'source/accepted') {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation'], 'source/accepted frame')
|
||||||
|
return { ...common, t: 'source/accepted' }
|
||||||
|
}
|
||||||
|
if (value.t === 'source/resnapshot'
|
||||||
|
&& typeof value.reason === 'string') {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'expectedSequence', 'reason'], 'source/resnapshot frame')
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
t: 'source/resnapshot',
|
||||||
|
expectedSequence: natural(value.expectedSequence, 'expectedSequence'),
|
||||||
|
reason: value.reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`inspector protocol: unknown Worker source frame ${JSON.stringify(value.t)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one source frame received at a process or network boundary.
|
||||||
|
* @param value - Untrusted decoded wire value.
|
||||||
|
* @param maxRecords - Maximum records admitted in one frame.
|
||||||
|
* @returns The validated source-to-Worker frame.
|
||||||
|
*/
|
||||||
|
export function parseSourceFrame(value: unknown, maxRecords: number): SourceToWorkerFrame {
|
||||||
|
if (!isJsonValue(value) || !isPlainObject(value)) {
|
||||||
|
throw new Error('inspector protocol: source frame must be a lossless JSON object')
|
||||||
|
}
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION) {
|
||||||
|
throw new Error(`inspector protocol: unsupported version ${JSON.stringify(value.v)}`)
|
||||||
|
}
|
||||||
|
switch (value.t) {
|
||||||
|
case 'source/open':
|
||||||
|
return parseOpen(value)
|
||||||
|
case 'source/replace':
|
||||||
|
return parseRecordsFrame(value, maxRecords, true)
|
||||||
|
case 'source/append':
|
||||||
|
return parseRecordsFrame(value, maxRecords, false)
|
||||||
|
case 'source/close':
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation'], 'source/close frame')
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'source/close',
|
||||||
|
sourceId: sourceId(value.sourceId),
|
||||||
|
generation: generation(value.generation),
|
||||||
|
}
|
||||||
|
case 'client-runtime/response':
|
||||||
|
return parseClientRuntimeResponseFrame(value)
|
||||||
|
case 'client-console/event':
|
||||||
|
return parseClientConsoleEventFrame(value)
|
||||||
|
case 'client-sources/response':
|
||||||
|
return parseClientSourceResponseFrame(value)
|
||||||
|
default:
|
||||||
|
throw new Error(`inspector protocol: unknown source frame ${JSON.stringify(value.t)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOpen(value: Record<string, unknown>): SourceOpenFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'source', 'topics'], 'source/open frame')
|
||||||
|
if (!isPlainObject(value.source) || !Array.isArray(value.topics)) {
|
||||||
|
throw new Error('inspector protocol: source/open needs source and topics')
|
||||||
|
}
|
||||||
|
const source = value.source
|
||||||
|
exactKeys(source, ['sourceId', 'generation', 'kind', 'label', 'timeOriginMs', 'capabilities'], 'source descriptor')
|
||||||
|
const kind = source.kind
|
||||||
|
if (kind !== 'host' && kind !== 'client') throw new Error('inspector protocol: invalid source kind')
|
||||||
|
if (typeof source.label !== 'string' || source.label.length === 0 || source.label.length > 256) {
|
||||||
|
throw new Error('inspector protocol: source label must contain 1 to 256 characters')
|
||||||
|
}
|
||||||
|
if (typeof source.timeOriginMs !== 'number' || !Number.isFinite(source.timeOriginMs)) {
|
||||||
|
throw new Error('inspector protocol: source timeOriginMs must be finite')
|
||||||
|
}
|
||||||
|
if (!Array.isArray(source.capabilities)) {
|
||||||
|
throw new Error('inspector protocol: source capabilities must be an array')
|
||||||
|
}
|
||||||
|
const capabilities = source.capabilities.map(parseSourceCapability)
|
||||||
|
const capabilityTypes = new Set<string>()
|
||||||
|
for (const capability of capabilities) {
|
||||||
|
if (capabilityTypes.has(capability.type)) {
|
||||||
|
throw new Error(`inspector protocol: source declares ${capability.type} more than once`)
|
||||||
|
}
|
||||||
|
capabilityTypes.add(capability.type)
|
||||||
|
}
|
||||||
|
if (kind !== 'client' && capabilities.length > 0) {
|
||||||
|
throw new Error('inspector protocol: Host sources cannot declare Client capabilities')
|
||||||
|
}
|
||||||
|
const topics = value.topics.map((topic) => {
|
||||||
|
if (typeof topic !== 'string' || topic.length === 0 || topic.length > 128) {
|
||||||
|
throw new Error('inspector protocol: every source topic must contain 1 to 128 characters')
|
||||||
|
}
|
||||||
|
return topic
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'source/open',
|
||||||
|
source: {
|
||||||
|
sourceId: sourceId(source.sourceId),
|
||||||
|
generation: generation(source.generation),
|
||||||
|
kind,
|
||||||
|
label: source.label,
|
||||||
|
timeOriginMs: source.timeOriginMs,
|
||||||
|
capabilities,
|
||||||
|
},
|
||||||
|
topics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSourceCapability(value: unknown): InspectorSourceCapability {
|
||||||
|
if (!isPlainObject(value) || typeof value.type !== 'string') {
|
||||||
|
throw new Error('inspector protocol: source capability must have a type')
|
||||||
|
}
|
||||||
|
switch (value.type) {
|
||||||
|
case 'client-runtime': return parseClientRuntimeCapability(value)
|
||||||
|
case 'client-console': return parseClientConsoleCapability(value)
|
||||||
|
case 'client-sources': return parseClientSourcesCapability(value)
|
||||||
|
default: throw new Error(`inspector protocol: unknown source capability ${JSON.stringify(value.type)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecordsFrame(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
maxRecords: number,
|
||||||
|
replace: boolean,
|
||||||
|
): SourceReplaceFrame | SourceAppendFrame {
|
||||||
|
exactKeys(
|
||||||
|
value,
|
||||||
|
replace
|
||||||
|
? ['v', 't', 'sourceId', 'generation', 'nextSequence', 'records']
|
||||||
|
: ['v', 't', 'sourceId', 'generation', 'firstSequence', 'droppedBefore', 'records'],
|
||||||
|
replace ? 'source/replace frame' : 'source/append frame',
|
||||||
|
)
|
||||||
|
if (!Array.isArray(value.records) || value.records.length > maxRecords) {
|
||||||
|
throw new Error(`inspector protocol: source batch exceeds ${String(maxRecords)} records`)
|
||||||
|
}
|
||||||
|
const records = value.records.map(parseRecord)
|
||||||
|
const common = {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
sourceId: sourceId(value.sourceId),
|
||||||
|
generation: generation(value.generation),
|
||||||
|
records,
|
||||||
|
} as const
|
||||||
|
if (replace) {
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
t: 'source/replace',
|
||||||
|
nextSequence: natural(value.nextSequence, 'nextSequence'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
t: 'source/append',
|
||||||
|
firstSequence: natural(value.firstSequence, 'firstSequence'),
|
||||||
|
droppedBefore: natural(value.droppedBefore, 'droppedBefore'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecord(value: unknown): InspectorRecordInput {
|
||||||
|
if (!isPlainObject(value)
|
||||||
|
|| typeof value.monotonicMs !== 'number'
|
||||||
|
|| !Number.isFinite(value.monotonicMs)
|
||||||
|
|| typeof value.topic !== 'string'
|
||||||
|
|| value.topic.length === 0
|
||||||
|
|| value.topic.length > 128
|
||||||
|
|| !isJsonValue(value.payload)) {
|
||||||
|
throw new Error('inspector protocol: invalid observation record')
|
||||||
|
}
|
||||||
|
exactKeys(value, ['monotonicMs', 'topic', 'payload'], 'observation record')
|
||||||
|
return { monotonicMs: value.monotonicMs, topic: value.topic, payload: value.payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceId(value: unknown): InspectorSourceId {
|
||||||
|
if (typeof value !== 'string') throw new Error('inspector protocol: sourceId must be a string')
|
||||||
|
return inspectorId<'InspectorSourceId'>(value, 'sourceId')
|
||||||
|
}
|
||||||
|
|
||||||
|
function generation(value: unknown): InspectorSourceGeneration {
|
||||||
|
if (typeof value !== 'string') throw new Error('inspector protocol: generation must be a string')
|
||||||
|
return inspectorId<'InspectorSourceGeneration'>(value, 'generation')
|
||||||
|
}
|
||||||
|
|
||||||
|
function natural(value: unknown, label: string): number {
|
||||||
|
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||||
|
throw new Error(`inspector protocol: ${label} must be a non-negative safe integer`)
|
||||||
|
}
|
||||||
|
return value as number
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/** Exact wire decoder for Client Runtime commands. */
|
||||||
|
|
||||||
|
import { isJsonValue, isPlainObject } from '../../../json.ts'
|
||||||
|
import { exactKeys, optionalBoolean, optionalNonNegativeNumber, optionalString, wireId } from '../../../validation.ts'
|
||||||
|
import type { ClientCallArgument, ClientRuntimeCallFunctionCommand, ClientRuntimeCommand } from './commands.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one Runtime command before it enters the Client realm.
|
||||||
|
* @param value - Untrusted command value.
|
||||||
|
* @returns The validated command union member.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeCommand(value: unknown): ClientRuntimeCommand {
|
||||||
|
if (!isPlainObject(value) || typeof value.op !== 'string') {
|
||||||
|
throw new Error('inspector protocol: Client Runtime command must have an op')
|
||||||
|
}
|
||||||
|
switch (value.op) {
|
||||||
|
case 'evaluate': {
|
||||||
|
exactKeys(value, [
|
||||||
|
'op', 'expression', 'objectGroup', 'includeCommandLineAPI', 'silent', 'returnByValue',
|
||||||
|
'generatePreview', 'userGesture', 'awaitPromise', 'disableBreaks', 'replMode',
|
||||||
|
'allowUnsafeEvalBlockedByCSP', 'timeoutMs',
|
||||||
|
], 'evaluate command')
|
||||||
|
if (typeof value.expression !== 'string') throw new Error('inspector protocol: evaluate expression must be a string')
|
||||||
|
return {
|
||||||
|
op: 'evaluate',
|
||||||
|
expression: value.expression,
|
||||||
|
...optionalString(value, 'objectGroup'),
|
||||||
|
...optionalBoolean(value, 'includeCommandLineAPI'),
|
||||||
|
...optionalBoolean(value, 'silent'),
|
||||||
|
...optionalBoolean(value, 'returnByValue'),
|
||||||
|
...optionalBoolean(value, 'generatePreview'),
|
||||||
|
...optionalBoolean(value, 'userGesture'),
|
||||||
|
...optionalBoolean(value, 'awaitPromise'),
|
||||||
|
...optionalBoolean(value, 'disableBreaks'),
|
||||||
|
...optionalBoolean(value, 'replMode'),
|
||||||
|
...optionalBoolean(value, 'allowUnsafeEvalBlockedByCSP'),
|
||||||
|
...optionalNonNegativeNumber(value, 'timeoutMs'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'get-properties':
|
||||||
|
exactKeys(value, [
|
||||||
|
'op', 'handle', 'ownProperties', 'accessorPropertiesOnly', 'generatePreview', 'nonIndexedPropertiesOnly',
|
||||||
|
], 'get-properties command')
|
||||||
|
return {
|
||||||
|
op: 'get-properties',
|
||||||
|
handle: wireId<'ClientRemoteObjectHandle'>(value.handle, 'handle'),
|
||||||
|
...optionalBoolean(value, 'ownProperties'),
|
||||||
|
...optionalBoolean(value, 'accessorPropertiesOnly'),
|
||||||
|
...optionalBoolean(value, 'generatePreview'),
|
||||||
|
...optionalBoolean(value, 'nonIndexedPropertiesOnly'),
|
||||||
|
}
|
||||||
|
case 'call-function':
|
||||||
|
return parseCallFunction(value)
|
||||||
|
case 'await-promise':
|
||||||
|
exactKeys(value, ['op', 'promise', 'returnByValue', 'generatePreview'], 'await-promise command')
|
||||||
|
return {
|
||||||
|
op: 'await-promise',
|
||||||
|
promise: wireId<'ClientRemoteObjectHandle'>(value.promise, 'promise'),
|
||||||
|
...optionalBoolean(value, 'returnByValue'),
|
||||||
|
...optionalBoolean(value, 'generatePreview'),
|
||||||
|
}
|
||||||
|
case 'release-object':
|
||||||
|
exactKeys(value, ['op', 'handle'], 'release-object command')
|
||||||
|
return {
|
||||||
|
op: 'release-object',
|
||||||
|
handle: wireId<'ClientRemoteObjectHandle'>(value.handle, 'handle'),
|
||||||
|
}
|
||||||
|
case 'release-object-group':
|
||||||
|
exactKeys(value, ['op', 'objectGroup'], 'release-object-group command')
|
||||||
|
if (typeof value.objectGroup !== 'string') throw new Error('inspector protocol: objectGroup must be a string')
|
||||||
|
return { op: 'release-object-group', objectGroup: value.objectGroup }
|
||||||
|
case 'global-lexical-scope-names':
|
||||||
|
exactKeys(value, ['op'], 'global-lexical-scope-names command')
|
||||||
|
return { op: 'global-lexical-scope-names' }
|
||||||
|
default:
|
||||||
|
throw new Error(`inspector protocol: unknown Client Runtime command ${JSON.stringify(value.op)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCallFunction(value: Record<string, unknown>): ClientRuntimeCallFunctionCommand {
|
||||||
|
exactKeys(value, [
|
||||||
|
'op', 'functionDeclaration', 'receiver', 'arguments', 'objectGroup', 'silent', 'returnByValue',
|
||||||
|
'generatePreview', 'userGesture', 'awaitPromise',
|
||||||
|
], 'call-function command')
|
||||||
|
if (typeof value.functionDeclaration !== 'string') {
|
||||||
|
throw new Error('inspector protocol: functionDeclaration must be a string')
|
||||||
|
}
|
||||||
|
let args: readonly ClientCallArgument[] | undefined
|
||||||
|
if (value.arguments !== undefined) {
|
||||||
|
if (!Array.isArray(value.arguments)) throw new Error('inspector protocol: call arguments must be an array')
|
||||||
|
args = value.arguments.map(parseCallArgument)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
op: 'call-function',
|
||||||
|
functionDeclaration: value.functionDeclaration,
|
||||||
|
...(value.receiver === undefined
|
||||||
|
? {}
|
||||||
|
: { receiver: wireId<'ClientRemoteObjectHandle'>(value.receiver, 'receiver') }),
|
||||||
|
...(args === undefined ? {} : { arguments: args }),
|
||||||
|
...optionalString(value, 'objectGroup'),
|
||||||
|
...optionalBoolean(value, 'silent'),
|
||||||
|
...optionalBoolean(value, 'returnByValue'),
|
||||||
|
...optionalBoolean(value, 'generatePreview'),
|
||||||
|
...optionalBoolean(value, 'userGesture'),
|
||||||
|
...optionalBoolean(value, 'awaitPromise'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCallArgument(value: unknown): ClientCallArgument {
|
||||||
|
if (!isPlainObject(value) || typeof value.kind !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime call argument')
|
||||||
|
}
|
||||||
|
switch (value.kind) {
|
||||||
|
case 'value':
|
||||||
|
exactKeys(value, ['kind', 'value'], 'value call argument')
|
||||||
|
if (!isJsonValue(value.value)) throw new Error('inspector protocol: call argument value must be JSON')
|
||||||
|
return { kind: 'value', value: value.value }
|
||||||
|
case 'unserializable':
|
||||||
|
exactKeys(value, ['kind', 'value'], 'unserializable call argument')
|
||||||
|
if (typeof value.value !== 'string') throw new Error('inspector protocol: unserializable argument must be a string')
|
||||||
|
return { kind: 'unserializable', value: value.value }
|
||||||
|
case 'object':
|
||||||
|
exactKeys(value, ['kind', 'handle'], 'object call argument')
|
||||||
|
return {
|
||||||
|
kind: 'object',
|
||||||
|
handle: wireId<'ClientRemoteObjectHandle'>(value.handle, 'handle'),
|
||||||
|
}
|
||||||
|
case 'undefined':
|
||||||
|
exactKeys(value, ['kind'], 'undefined call argument')
|
||||||
|
return { kind: 'undefined' }
|
||||||
|
default:
|
||||||
|
throw new Error(`inspector protocol: unknown call argument ${JSON.stringify(value.kind)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/** Closed command/result protocol for Runtime operations executed by a Client. */
|
||||||
|
|
||||||
|
import type { ClientRemoteObjectHandle } from '../../ids.ts'
|
||||||
|
import type {
|
||||||
|
RuntimeExceptionDetails,
|
||||||
|
RuntimeInternalPropertyDescriptor,
|
||||||
|
RuntimeCallArgument,
|
||||||
|
RuntimeAwaitPromiseRequest,
|
||||||
|
RuntimeCallFunctionRequest,
|
||||||
|
RuntimeCompletion,
|
||||||
|
RuntimeEvaluateRequest,
|
||||||
|
RuntimeGetPropertiesRequest,
|
||||||
|
RuntimePropertyDescriptor,
|
||||||
|
RuntimeRemoteObject,
|
||||||
|
} from '../../../cdp/index.ts'
|
||||||
|
|
||||||
|
/** Runtime object serialized with one Client-session handle when retained. */
|
||||||
|
export type ClientRuntimeRemoteObject = RuntimeRemoteObject<ClientRemoteObjectHandle>
|
||||||
|
|
||||||
|
/** Property descriptor whose retained values use Client-session handles. */
|
||||||
|
export type ClientRuntimePropertyDescriptor = RuntimePropertyDescriptor<ClientRemoteObjectHandle>
|
||||||
|
|
||||||
|
/** Internal property descriptor whose retained values use Client-session handles. */
|
||||||
|
export type ClientRuntimeInternalPropertyDescriptor = RuntimeInternalPropertyDescriptor<ClientRemoteObjectHandle>
|
||||||
|
|
||||||
|
/** Exception details whose retained value uses a Client-session handle. */
|
||||||
|
export type ClientRuntimeExceptionDetails = RuntimeExceptionDetails<ClientRemoteObjectHandle>
|
||||||
|
|
||||||
|
/** One argument supplied to a function in the Client realm. */
|
||||||
|
export type ClientCallArgument = RuntimeCallArgument<ClientRemoteObjectHandle>
|
||||||
|
|
||||||
|
/** Evaluate source text in the Client global execution context. */
|
||||||
|
export interface ClientRuntimeEvaluateCommand extends RuntimeEvaluateRequest {
|
||||||
|
readonly op: 'evaluate'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enumerate properties of one retained Client object. */
|
||||||
|
export interface ClientRuntimeGetPropertiesCommand extends RuntimeGetPropertiesRequest<ClientRemoteObjectHandle> {
|
||||||
|
readonly op: 'get-properties'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invoke a function declaration with Client-local receivers and arguments. */
|
||||||
|
export interface ClientRuntimeCallFunctionCommand extends RuntimeCallFunctionRequest<ClientRemoteObjectHandle> {
|
||||||
|
readonly op: 'call-function'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Await one retained Client promise. */
|
||||||
|
export interface ClientRuntimeAwaitPromiseCommand extends RuntimeAwaitPromiseRequest<ClientRemoteObjectHandle> {
|
||||||
|
readonly op: 'await-promise'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release one retained Client object. */
|
||||||
|
export interface ClientRuntimeReleaseObjectCommand {
|
||||||
|
readonly op: 'release-object'
|
||||||
|
readonly handle: ClientRemoteObjectHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release every Client object retained under one DevTools object group. */
|
||||||
|
export interface ClientRuntimeReleaseObjectGroupCommand {
|
||||||
|
readonly op: 'release-object-group'
|
||||||
|
readonly objectGroup: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read names visible in the Client global lexical scope. */
|
||||||
|
export interface ClientRuntimeGlobalLexicalScopeNamesCommand {
|
||||||
|
readonly op: 'global-lexical-scope-names'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closed command set implemented by the Client Runtime transport. */
|
||||||
|
export type ClientRuntimeCommand =
|
||||||
|
| ClientRuntimeEvaluateCommand
|
||||||
|
| ClientRuntimeGetPropertiesCommand
|
||||||
|
| ClientRuntimeCallFunctionCommand
|
||||||
|
| ClientRuntimeAwaitPromiseCommand
|
||||||
|
| ClientRuntimeReleaseObjectCommand
|
||||||
|
| ClientRuntimeReleaseObjectGroupCommand
|
||||||
|
| ClientRuntimeGlobalLexicalScopeNamesCommand
|
||||||
|
|
||||||
|
/** Shared result of evaluation, function calls, and promise awaiting. */
|
||||||
|
export type ClientRuntimeCompletion = RuntimeCompletion<ClientRemoteObjectHandle>
|
||||||
|
|
||||||
|
/** Result discriminant mirrors the command and prevents cross-method settlement. */
|
||||||
|
export type ClientRuntimeResult =
|
||||||
|
| { readonly op: 'evaluate'; readonly completion: ClientRuntimeCompletion }
|
||||||
|
| {
|
||||||
|
readonly op: 'get-properties'
|
||||||
|
readonly properties: readonly ClientRuntimePropertyDescriptor[]
|
||||||
|
readonly internalProperties?: readonly ClientRuntimeInternalPropertyDescriptor[]
|
||||||
|
readonly exceptionDetails?: ClientRuntimeExceptionDetails
|
||||||
|
}
|
||||||
|
| { readonly op: 'call-function'; readonly completion: ClientRuntimeCompletion }
|
||||||
|
| { readonly op: 'await-promise'; readonly completion: ClientRuntimeCompletion }
|
||||||
|
| { readonly op: 'release-object' }
|
||||||
|
| { readonly op: 'release-object-group' }
|
||||||
|
| { readonly op: 'global-lexical-scope-names'; readonly names: readonly string[] }
|
||||||
|
|
||||||
|
/** Stable transport-level failures distinct from evaluated JavaScript exceptions. */
|
||||||
|
export interface ClientRuntimeError {
|
||||||
|
readonly code: 'invalid-request' | 'object-not-found' | 'unsupported' | 'timeout' | 'result-too-large' | 'internal-error'
|
||||||
|
readonly message: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/** Typed transport for Client Console sessions and events. */
|
||||||
|
|
||||||
|
import type { ClientRemoteObjectHandle, ClientRuntimeSessionId, InspectorSourceGeneration, InspectorSourceId } from '../../ids.ts'
|
||||||
|
import { isPlainObject } from '../../../json.ts'
|
||||||
|
import type { RuntimeConsoleBackendEvent, RuntimeConsoleType } from '../../../cdp/index.ts'
|
||||||
|
import { exactKeys, exactObject, wireId } from '../../../validation.ts'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION } from '../../version.ts'
|
||||||
|
import {
|
||||||
|
parseClientRuntimeExceptionDetails,
|
||||||
|
parseClientRuntimeRemoteObject,
|
||||||
|
parseClientRuntimeStackTrace,
|
||||||
|
} from './value-codec.ts'
|
||||||
|
|
||||||
|
/** Source capability that permits Client Console event forwarding. */
|
||||||
|
export interface ClientConsoleCapability {
|
||||||
|
readonly type: 'client-console'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker request to start Console observation for one DevTools session. */
|
||||||
|
export interface ClientConsoleEnableFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-console/enable'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientRuntimeSessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker request to stop Console observation for one DevTools session. */
|
||||||
|
export interface ClientConsoleDisableFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-console/disable'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientRuntimeSessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client Console event carrying objects retained for one DevTools session. */
|
||||||
|
export interface ClientConsoleEventFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-console/event'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientRuntimeSessionId
|
||||||
|
readonly event: RuntimeConsoleBackendEvent<ClientRemoteObjectHandle>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the marker capability for Client Console forwarding.
|
||||||
|
* @param value - Untrusted capability declaration.
|
||||||
|
* @returns The validated marker capability.
|
||||||
|
*/
|
||||||
|
export function parseClientConsoleCapability(value: unknown): ClientConsoleCapability {
|
||||||
|
const record = exactObject(value, ['type'], 'Client Console capability')
|
||||||
|
if (record.type !== 'client-console') throw new Error('inspector protocol: invalid Client Console capability')
|
||||||
|
return { type: 'client-console' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a Worker-to-Client Console lifecycle frame.
|
||||||
|
* @param value - Untrusted decoded frame.
|
||||||
|
* @returns A validated enable or disable frame.
|
||||||
|
*/
|
||||||
|
export function parseClientConsoleControlFrame(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
): ClientConsoleEnableFrame | ClientConsoleDisableFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId'], 'Client Console control frame')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION
|
||||||
|
|| (value.t !== 'client-console/enable' && value.t !== 'client-console/disable')) {
|
||||||
|
throw new Error('inspector protocol: invalid Client Console control frame')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: value.t,
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientRuntimeSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one Client-to-Worker Console event.
|
||||||
|
* @param value - Untrusted decoded frame.
|
||||||
|
* @returns A validated Console event frame.
|
||||||
|
*/
|
||||||
|
export function parseClientConsoleEventFrame(value: Record<string, unknown>): ClientConsoleEventFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId', 'event'], 'Client Console event frame')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-console/event') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Console event envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-console/event',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientRuntimeSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
event: parseEvent(value.event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEvent(value: unknown): RuntimeConsoleBackendEvent<ClientRemoteObjectHandle> {
|
||||||
|
if (!isPlainObject(value) || (value.type !== 'console-api' && value.type !== 'exception')) {
|
||||||
|
throw new Error('inspector protocol: invalid Client Console event')
|
||||||
|
}
|
||||||
|
if (value.type === 'console-api') {
|
||||||
|
exactKeys(value, ['type', 'event'], 'Client Console API event')
|
||||||
|
const event = exactObject(value.event, ['type', 'arguments', 'timestamp', 'contextId', 'stackTrace'], 'Console API event')
|
||||||
|
if (!CONSOLE_TYPES.has(event.type as RuntimeConsoleType)
|
||||||
|
|| !Array.isArray(event.arguments)
|
||||||
|
|| typeof event.timestamp !== 'number'
|
||||||
|
|| !Number.isFinite(event.timestamp)) {
|
||||||
|
throw new Error('inspector protocol: invalid Console API event')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'console-api',
|
||||||
|
event: {
|
||||||
|
type: event.type as RuntimeConsoleType,
|
||||||
|
arguments: event.arguments.map(parseClientRuntimeRemoteObject),
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
...(event.contextId === undefined ? {} : { contextId: integer(event.contextId, 'contextId') }),
|
||||||
|
...(event.stackTrace === undefined ? {} : { stackTrace: parseClientRuntimeStackTrace(event.stackTrace) }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exactKeys(value, ['type', 'event'], 'Client exception event')
|
||||||
|
const event = exactObject(value.event, ['timestamp', 'contextId', 'details'], 'Client exception event payload')
|
||||||
|
if (typeof event.timestamp !== 'number' || !Number.isFinite(event.timestamp)) {
|
||||||
|
throw new Error('inspector protocol: invalid Client exception timestamp')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'exception',
|
||||||
|
event: {
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
...(event.contextId === undefined ? {} : { contextId: integer(event.contextId, 'contextId') }),
|
||||||
|
details: parseClientRuntimeExceptionDetails(event.details),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(value: unknown, label: string): number {
|
||||||
|
if (!Number.isSafeInteger(value)) throw new Error(`inspector protocol: ${label} must be an integer`)
|
||||||
|
return value as number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONSOLE_TYPES = new Set<RuntimeConsoleType>([
|
||||||
|
'log', 'debug', 'info', 'error', 'warning', 'dir', 'dirxml', 'table', 'trace', 'clear',
|
||||||
|
'startGroup', 'startGroupCollapsed', 'endGroup', 'assert', 'profile', 'profileEnd', 'count', 'timeEnd',
|
||||||
|
])
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/** Versioned envelopes for Worker-to-Client Runtime operations. */
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ClientRuntimeRequestId,
|
||||||
|
ClientRuntimeSessionId,
|
||||||
|
InspectorSourceGeneration,
|
||||||
|
InspectorSourceId,
|
||||||
|
} from '../../ids.ts'
|
||||||
|
import { isPlainObject } from '../../../json.ts'
|
||||||
|
import { exactKeys, exactObject, wireId } from '../../../validation.ts'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION } from '../../version.ts'
|
||||||
|
import { parseClientRuntimeCommand } from './command-codec.ts'
|
||||||
|
import { parseClientRuntimeResult } from './value-codec.ts'
|
||||||
|
import type { ClientRuntimeCommand, ClientRuntimeError, ClientRuntimeResult } from './commands.ts'
|
||||||
|
|
||||||
|
/** Source capability that permits synthetic Runtime execution contexts. */
|
||||||
|
export interface ClientRuntimeCapability {
|
||||||
|
readonly type: 'client-runtime'
|
||||||
|
readonly origin: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker request for one operation in a specific source generation and DevTools session. */
|
||||||
|
export interface ClientRuntimeRequestFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-runtime/request'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientRuntimeSessionId
|
||||||
|
readonly requestId: ClientRuntimeRequestId
|
||||||
|
readonly command: ClientRuntimeCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client response to one typed Runtime request. */
|
||||||
|
export interface ClientRuntimeResponseFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-runtime/response'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientRuntimeSessionId
|
||||||
|
readonly requestId: ClientRuntimeRequestId
|
||||||
|
readonly outcome:
|
||||||
|
| { readonly ok: true; readonly result: ClientRuntimeResult }
|
||||||
|
| { readonly ok: false; readonly error: ClientRuntimeError }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One-way cleanup when a DevTools connection or its Runtime domain closes. */
|
||||||
|
export interface ClientRuntimeSessionClosedFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-runtime/session-closed'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientRuntimeSessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild a Client Runtime capability.
|
||||||
|
* @param value - Untrusted capability declaration.
|
||||||
|
* @returns The validated capability.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeCapability(value: unknown): ClientRuntimeCapability {
|
||||||
|
const record = exactObject(value, ['type', 'origin'], 'Client Runtime capability')
|
||||||
|
if (record.type !== 'client-runtime' || typeof record.origin !== 'string' || record.origin.length > 2_048) {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime capability')
|
||||||
|
}
|
||||||
|
return { type: 'client-runtime', origin: record.origin }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one Worker-to-Client Runtime request.
|
||||||
|
* @param value - Untrusted request frame.
|
||||||
|
* @returns The validated request frame.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeRequestFrame(value: Record<string, unknown>): ClientRuntimeRequestFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId', 'requestId', 'command'], 'Client Runtime request')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-runtime/request') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime request envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-runtime/request',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientRuntimeSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
requestId: wireId<'ClientRuntimeRequestId'>(value.requestId, 'requestId'),
|
||||||
|
command: parseClientRuntimeCommand(value.command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one Client-to-Worker Runtime response.
|
||||||
|
* @param value - Untrusted response frame.
|
||||||
|
* @returns The validated response frame.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeResponseFrame(value: Record<string, unknown>): ClientRuntimeResponseFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId', 'requestId', 'outcome'], 'Client Runtime response')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-runtime/response') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime response envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-runtime/response',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientRuntimeSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
requestId: wireId<'ClientRuntimeRequestId'>(value.requestId, 'requestId'),
|
||||||
|
outcome: parseOutcome(value.outcome),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one Runtime-session cleanup notification.
|
||||||
|
* @param value - Untrusted cleanup frame.
|
||||||
|
* @returns The validated cleanup frame.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeSessionClosedFrame(value: Record<string, unknown>): ClientRuntimeSessionClosedFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId'], 'Client Runtime session close')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-runtime/session-closed') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime session close envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-runtime/session-closed',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientRuntimeSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOutcome(value: unknown): ClientRuntimeResponseFrame['outcome'] {
|
||||||
|
if (!isPlainObject(value) || typeof value.ok !== 'boolean') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime outcome')
|
||||||
|
}
|
||||||
|
if (value.ok) {
|
||||||
|
exactKeys(value, ['ok', 'result'], 'successful Client Runtime outcome')
|
||||||
|
return { ok: true, result: parseClientRuntimeResult(value.result) }
|
||||||
|
}
|
||||||
|
exactKeys(value, ['ok', 'error'], 'failed Client Runtime outcome')
|
||||||
|
const error = exactObject(value.error, ['code', 'message'], 'Client Runtime error')
|
||||||
|
if (!ERROR_CODES.has(error.code as ClientRuntimeError['code']) || typeof error.message !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid Client Runtime error')
|
||||||
|
}
|
||||||
|
return { ok: false, error: { code: error.code as ClientRuntimeError['code'], message: error.message } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_CODES = new Set<ClientRuntimeError['code']>([
|
||||||
|
'invalid-request', 'object-not-found', 'unsupported', 'timeout', 'result-too-large', 'internal-error',
|
||||||
|
])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** Public types and boundary decoders for the Client Runtime wire protocol. */
|
||||||
|
|
||||||
|
export * from './commands.ts'
|
||||||
|
export * from './console-frames.ts'
|
||||||
|
export * from './frames.ts'
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
/** Exact wire decoder for Client Runtime results and RemoteObject data. */
|
||||||
|
|
||||||
|
import { isJsonValue, isPlainObject } from '../../../json.ts'
|
||||||
|
import { exactKeys, exactObject, optionalBoolean, optionalString, wireId } from '../../../validation.ts'
|
||||||
|
import { parseInspectorObjectReference } from '../../../cordis/object-reference.ts'
|
||||||
|
import type {
|
||||||
|
RuntimeCallFrame,
|
||||||
|
RuntimeObjectPreview,
|
||||||
|
RuntimePropertyPreview,
|
||||||
|
RuntimeRemoteObjectDescriptor,
|
||||||
|
RuntimeRemoteObjectSubtype,
|
||||||
|
RuntimeRemoteObjectType,
|
||||||
|
RuntimeStackTrace,
|
||||||
|
} from '../../../cdp/index.ts'
|
||||||
|
import type {
|
||||||
|
ClientRuntimeCompletion,
|
||||||
|
ClientRuntimeExceptionDetails,
|
||||||
|
ClientRuntimeInternalPropertyDescriptor,
|
||||||
|
ClientRuntimePropertyDescriptor,
|
||||||
|
ClientRuntimeRemoteObject,
|
||||||
|
ClientRuntimeResult,
|
||||||
|
} from './commands.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and rebuild one successful Client Runtime result.
|
||||||
|
* @param value - Untrusted result value.
|
||||||
|
* @returns The validated result union member.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeResult(value: unknown): ClientRuntimeResult {
|
||||||
|
if (!isPlainObject(value) || typeof value.op !== 'string') {
|
||||||
|
throw new Error('inspector protocol: Client Runtime result must have an op')
|
||||||
|
}
|
||||||
|
switch (value.op) {
|
||||||
|
case 'evaluate':
|
||||||
|
case 'call-function':
|
||||||
|
case 'await-promise':
|
||||||
|
exactKeys(value, ['op', 'completion'], `${value.op} result`)
|
||||||
|
return { op: value.op, completion: parseCompletion(value.completion) }
|
||||||
|
case 'get-properties': {
|
||||||
|
exactKeys(value, ['op', 'properties', 'internalProperties', 'exceptionDetails'], 'get-properties result')
|
||||||
|
if (!Array.isArray(value.properties)) throw new Error('inspector protocol: properties must be an array')
|
||||||
|
const internal = value.internalProperties
|
||||||
|
if (internal !== undefined && !Array.isArray(internal)) {
|
||||||
|
throw new Error('inspector protocol: internalProperties must be an array')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
op: 'get-properties',
|
||||||
|
properties: value.properties.map(parsePropertyDescriptor),
|
||||||
|
...(internal === undefined ? {} : { internalProperties: internal.map(parseInternalPropertyDescriptor) }),
|
||||||
|
...(value.exceptionDetails === undefined
|
||||||
|
? {}
|
||||||
|
: { exceptionDetails: parseClientRuntimeExceptionDetails(value.exceptionDetails) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'release-object':
|
||||||
|
case 'release-object-group':
|
||||||
|
exactKeys(value, ['op'], `${value.op} result`)
|
||||||
|
return { op: value.op }
|
||||||
|
case 'global-lexical-scope-names':
|
||||||
|
exactKeys(value, ['op', 'names'], 'global-lexical-scope-names result')
|
||||||
|
if (!Array.isArray(value.names) || !value.names.every(name => typeof name === 'string')) {
|
||||||
|
throw new Error('inspector protocol: lexical scope names must be strings')
|
||||||
|
}
|
||||||
|
return { op: 'global-lexical-scope-names', names: value.names }
|
||||||
|
default:
|
||||||
|
throw new Error(`inspector protocol: unknown Client Runtime result ${JSON.stringify(value.op)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCompletion(value: unknown): ClientRuntimeCompletion {
|
||||||
|
const record = exactObject(value, ['result', 'exceptionDetails'], 'Client Runtime completion')
|
||||||
|
return {
|
||||||
|
result: parseClientRuntimeRemoteObject(record.result),
|
||||||
|
...(record.exceptionDetails === undefined
|
||||||
|
? {}
|
||||||
|
: { exceptionDetails: parseClientRuntimeExceptionDetails(record.exceptionDetails) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode one Client Runtime object carrying an optional session-local handle.
|
||||||
|
* @param value - Untrusted wire value.
|
||||||
|
* @returns The validated realm-neutral object value.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeRemoteObject(value: unknown): ClientRuntimeRemoteObject {
|
||||||
|
const record = exactObject(value, ['descriptor', 'object', 'semanticReference'], 'Client Runtime object')
|
||||||
|
const descriptor = parseRemoteObjectDescriptor(record.descriptor)
|
||||||
|
const object = record.object === undefined
|
||||||
|
? undefined
|
||||||
|
: exactObject(record.object, ['handle'], 'Client Runtime object reference')
|
||||||
|
const remote: ClientRuntimeRemoteObject = {
|
||||||
|
descriptor,
|
||||||
|
...(object === undefined
|
||||||
|
? {}
|
||||||
|
: { object: { handle: wireId<'ClientRemoteObjectHandle'>(object.handle, 'handle') } }),
|
||||||
|
...(record.semanticReference === undefined
|
||||||
|
? {}
|
||||||
|
: { semanticReference: parseInspectorObjectReference(record.semanticReference) }),
|
||||||
|
}
|
||||||
|
validateRemoteObject(remote)
|
||||||
|
return remote
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRemoteObjectDescriptor(value: unknown): RuntimeRemoteObjectDescriptor {
|
||||||
|
const record = exactObject(value, [
|
||||||
|
'type', 'subtype', 'className', 'value', 'unserializableValue', 'description', 'preview',
|
||||||
|
], 'Runtime object descriptor')
|
||||||
|
if (!REMOTE_TYPES.has(record.type as RuntimeRemoteObjectType)) {
|
||||||
|
throw new Error('inspector protocol: invalid Client RemoteObject type')
|
||||||
|
}
|
||||||
|
if (record.subtype !== undefined && !REMOTE_SUBTYPES.has(record.subtype as RuntimeRemoteObjectSubtype)) {
|
||||||
|
throw new Error('inspector protocol: invalid Client RemoteObject subtype')
|
||||||
|
}
|
||||||
|
if (record.value !== undefined && !isJsonValue(record.value)) {
|
||||||
|
throw new Error('inspector protocol: Client RemoteObject value must be JSON')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: record.type as RuntimeRemoteObjectType,
|
||||||
|
...(record.subtype === undefined ? {} : { subtype: record.subtype as RuntimeRemoteObjectSubtype }),
|
||||||
|
...optionalString(record, 'className'),
|
||||||
|
...(record.value === undefined ? {} : { value: record.value }),
|
||||||
|
...optionalString(record, 'unserializableValue'),
|
||||||
|
...optionalString(record, 'description'),
|
||||||
|
...(record.preview === undefined ? {} : { preview: parseObjectPreview(record.preview) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseObjectPreview(value: unknown): RuntimeObjectPreview {
|
||||||
|
const record = exactObject(value, ['type', 'subtype', 'description', 'overflow', 'properties'], 'object preview')
|
||||||
|
if (!REMOTE_TYPES.has(record.type as RuntimeRemoteObjectType)
|
||||||
|
|| (record.subtype !== undefined && !REMOTE_SUBTYPES.has(record.subtype as RuntimeRemoteObjectSubtype))
|
||||||
|
|| typeof record.overflow !== 'boolean'
|
||||||
|
|| !Array.isArray(record.properties)) {
|
||||||
|
throw new Error('inspector protocol: invalid object preview')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: record.type as RuntimeRemoteObjectType,
|
||||||
|
...(record.subtype === undefined ? {} : { subtype: record.subtype as RuntimeRemoteObjectSubtype }),
|
||||||
|
...optionalString(record, 'description'),
|
||||||
|
overflow: record.overflow,
|
||||||
|
properties: record.properties.map(parsePropertyPreview),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePropertyPreview(value: unknown): RuntimePropertyPreview {
|
||||||
|
const record = exactObject(value, ['name', 'type', 'value', 'valuePreview', 'subtype'], 'property preview')
|
||||||
|
if (typeof record.name !== 'string'
|
||||||
|
|| (record.type !== 'accessor' && !REMOTE_TYPES.has(record.type as RuntimeRemoteObjectType))
|
||||||
|
|| (record.subtype !== undefined && !REMOTE_SUBTYPES.has(record.subtype as RuntimeRemoteObjectSubtype))) {
|
||||||
|
throw new Error('inspector protocol: invalid property preview')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: record.name,
|
||||||
|
type: record.type as RuntimePropertyPreview['type'],
|
||||||
|
...optionalString(record, 'value'),
|
||||||
|
...(record.valuePreview === undefined ? {} : { valuePreview: parseObjectPreview(record.valuePreview) }),
|
||||||
|
...(record.subtype === undefined ? {} : { subtype: record.subtype as RuntimeRemoteObjectSubtype }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePropertyDescriptor(value: unknown): ClientRuntimePropertyDescriptor {
|
||||||
|
const record = exactObject(value, [
|
||||||
|
'name', 'value', 'writable', 'get', 'set', 'configurable', 'enumerable', 'wasThrown', 'isOwn', 'symbol',
|
||||||
|
], 'property descriptor')
|
||||||
|
if (typeof record.name !== 'string' || typeof record.configurable !== 'boolean' || typeof record.enumerable !== 'boolean') {
|
||||||
|
throw new Error('inspector protocol: invalid property descriptor')
|
||||||
|
}
|
||||||
|
const dataDescriptor = record.value !== undefined || record.writable !== undefined
|
||||||
|
const accessorDescriptor = record.get !== undefined || record.set !== undefined
|
||||||
|
if (dataDescriptor && accessorDescriptor) {
|
||||||
|
throw new Error('inspector protocol: property descriptor mixes data and accessor fields')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: record.name,
|
||||||
|
...(record.value === undefined ? {} : { value: parseClientRuntimeRemoteObject(record.value) }),
|
||||||
|
...optionalBoolean(record, 'writable'),
|
||||||
|
...(record.get === undefined ? {} : { get: parseClientRuntimeRemoteObject(record.get) }),
|
||||||
|
...(record.set === undefined ? {} : { set: parseClientRuntimeRemoteObject(record.set) }),
|
||||||
|
configurable: record.configurable,
|
||||||
|
enumerable: record.enumerable,
|
||||||
|
...optionalBoolean(record, 'wasThrown'),
|
||||||
|
...optionalBoolean(record, 'isOwn'),
|
||||||
|
...(record.symbol === undefined ? {} : { symbol: parseClientRuntimeRemoteObject(record.symbol) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInternalPropertyDescriptor(value: unknown): ClientRuntimeInternalPropertyDescriptor {
|
||||||
|
const record = exactObject(value, ['name', 'value'], 'internal property descriptor')
|
||||||
|
if (typeof record.name !== 'string') throw new Error('inspector protocol: invalid internal property descriptor')
|
||||||
|
return {
|
||||||
|
name: record.name,
|
||||||
|
...(record.value === undefined ? {} : { value: parseClientRuntimeRemoteObject(record.value) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode Client exception details used by command results and events.
|
||||||
|
* @param value - Untrusted wire value.
|
||||||
|
* @returns Validated exception details.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeExceptionDetails(value: unknown): ClientRuntimeExceptionDetails {
|
||||||
|
const record = exactObject(value, [
|
||||||
|
'text', 'lineNumber', 'columnNumber', 'url', 'stackTrace', 'exception',
|
||||||
|
], 'exception details')
|
||||||
|
if (typeof record.text !== 'string'
|
||||||
|
|| !Number.isSafeInteger(record.lineNumber)
|
||||||
|
|| (record.lineNumber as number) < 0
|
||||||
|
|| !Number.isSafeInteger(record.columnNumber)
|
||||||
|
|| (record.columnNumber as number) < 0) {
|
||||||
|
throw new Error('inspector protocol: invalid exception details')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: record.text,
|
||||||
|
lineNumber: record.lineNumber as number,
|
||||||
|
columnNumber: record.columnNumber as number,
|
||||||
|
...optionalString(record, 'url'),
|
||||||
|
...(record.stackTrace === undefined ? {} : { stackTrace: parseClientRuntimeStackTrace(record.stackTrace) }),
|
||||||
|
...(record.exception === undefined ? {} : { exception: parseClientRuntimeRemoteObject(record.exception) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a stack trace carried by a Client Runtime or Console frame.
|
||||||
|
* @param value - Untrusted stack-trace value.
|
||||||
|
* @returns The validated realm-neutral stack trace.
|
||||||
|
*/
|
||||||
|
export function parseClientRuntimeStackTrace(value: unknown): RuntimeStackTrace {
|
||||||
|
const record = exactObject(value, ['description', 'callFrames', 'parent'], 'stack trace')
|
||||||
|
if (!Array.isArray(record.callFrames)) throw new Error('inspector protocol: stack callFrames must be an array')
|
||||||
|
return {
|
||||||
|
...optionalString(record, 'description'),
|
||||||
|
callFrames: record.callFrames.map(parseCallFrame),
|
||||||
|
...(record.parent === undefined ? {} : { parent: parseClientRuntimeStackTrace(record.parent) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCallFrame(value: unknown): RuntimeCallFrame {
|
||||||
|
const record = exactObject(value, ['functionName', 'scriptKey', 'url', 'lineNumber', 'columnNumber'], 'stack call frame')
|
||||||
|
if (typeof record.functionName !== 'string'
|
||||||
|
|| typeof record.url !== 'string'
|
||||||
|
|| !Number.isSafeInteger(record.lineNumber)
|
||||||
|
|| !Number.isSafeInteger(record.columnNumber)) {
|
||||||
|
throw new Error('inspector protocol: invalid stack call frame')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
functionName: record.functionName,
|
||||||
|
...(record.scriptKey === undefined ? {} : { scriptKey: wireId<'RuntimeScriptKey'>(record.scriptKey, 'scriptKey') }),
|
||||||
|
url: record.url,
|
||||||
|
lineNumber: record.lineNumber as number,
|
||||||
|
columnNumber: record.columnNumber as number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOTE_TYPES = new Set<RuntimeRemoteObjectType>([
|
||||||
|
'object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint',
|
||||||
|
])
|
||||||
|
|
||||||
|
const REMOTE_SUBTYPES = new Set<RuntimeRemoteObjectSubtype>([
|
||||||
|
'array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'iterator', 'generator',
|
||||||
|
'error', 'proxy', 'promise', 'typedarray', 'arraybuffer', 'dataview', 'webassemblymemory', 'wasmvalue',
|
||||||
|
])
|
||||||
|
|
||||||
|
function validateRemoteObject(value: ClientRuntimeRemoteObject): void {
|
||||||
|
if (value.semanticReference !== undefined && value.object === undefined) {
|
||||||
|
throw new Error('inspector protocol: semanticReference requires a retained Client object')
|
||||||
|
}
|
||||||
|
const descriptor = value.descriptor
|
||||||
|
if (descriptor.subtype !== undefined && descriptor.type !== 'object') {
|
||||||
|
throw new Error('inspector protocol: only object RemoteObjects may have a subtype')
|
||||||
|
}
|
||||||
|
if (descriptor.preview !== undefined && descriptor.type !== 'object') {
|
||||||
|
throw new Error('inspector protocol: only object RemoteObjects may have a preview')
|
||||||
|
}
|
||||||
|
const hasValue = descriptor.value !== undefined
|
||||||
|
const hasUnserializableValue = descriptor.unserializableValue !== undefined
|
||||||
|
const hasObject = value.object !== undefined
|
||||||
|
switch (descriptor.type) {
|
||||||
|
case 'undefined':
|
||||||
|
requireRepresentations(descriptor.type, hasValue, hasUnserializableValue, hasObject, false, false, false)
|
||||||
|
return
|
||||||
|
case 'string':
|
||||||
|
requireRepresentations(descriptor.type, typeof descriptor.value === 'string', hasUnserializableValue, hasObject, true, false, false)
|
||||||
|
return
|
||||||
|
case 'boolean':
|
||||||
|
requireRepresentations(descriptor.type, typeof descriptor.value === 'boolean', hasUnserializableValue, hasObject, true, false, false)
|
||||||
|
return
|
||||||
|
case 'number': {
|
||||||
|
const finite = typeof descriptor.value === 'number'
|
||||||
|
&& Number.isFinite(descriptor.value)
|
||||||
|
&& !Object.is(descriptor.value, -0)
|
||||||
|
const special = descriptor.unserializableValue === 'NaN'
|
||||||
|
|| descriptor.unserializableValue === 'Infinity'
|
||||||
|
|| descriptor.unserializableValue === '-Infinity'
|
||||||
|
|| descriptor.unserializableValue === '-0'
|
||||||
|
if (hasObject || finite === special) throw new Error('inspector protocol: invalid number RemoteObject representation')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 'bigint':
|
||||||
|
if (hasValue || hasObject || !/^-?(?:0|[1-9]\d*)n$/u.test(descriptor.unserializableValue ?? '')) {
|
||||||
|
throw new Error('inspector protocol: invalid bigint RemoteObject representation')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case 'symbol':
|
||||||
|
case 'function':
|
||||||
|
requireRepresentations(descriptor.type, hasValue, hasUnserializableValue, hasObject, false, false, true)
|
||||||
|
return
|
||||||
|
case 'object':
|
||||||
|
if (descriptor.subtype === 'null') {
|
||||||
|
if (descriptor.value !== null || hasObject || hasUnserializableValue) {
|
||||||
|
throw new Error('inspector protocol: invalid null RemoteObject representation')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (hasUnserializableValue || hasValue === hasObject) {
|
||||||
|
throw new Error('inspector protocol: object RemoteObject needs exactly one value or backend object')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireRepresentations(
|
||||||
|
type: RuntimeRemoteObjectType,
|
||||||
|
hasValue: boolean,
|
||||||
|
hasUnserializableValue: boolean,
|
||||||
|
hasObject: boolean,
|
||||||
|
expectedValue: boolean,
|
||||||
|
expectedUnserializableValue: boolean,
|
||||||
|
expectedObject: boolean,
|
||||||
|
): void {
|
||||||
|
if (hasValue !== expectedValue
|
||||||
|
|| hasUnserializableValue !== expectedUnserializableValue
|
||||||
|
|| hasObject !== expectedObject) {
|
||||||
|
throw new Error(`inspector protocol: invalid ${type} RemoteObject representation`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/** Exact decoders for Client source catalog operations and values. */
|
||||||
|
|
||||||
|
import { isPlainObject } from '../../../json.ts'
|
||||||
|
import type { RuntimeScript } from '../../../cdp/index.ts'
|
||||||
|
import { exactKeys, exactObject, optionalBoolean, optionalString, wireId } from '../../../validation.ts'
|
||||||
|
import type {
|
||||||
|
ClientScriptDescriptor,
|
||||||
|
ClientSourceCommand,
|
||||||
|
ClientSourceContentKind,
|
||||||
|
ClientSourceResult,
|
||||||
|
} from './commands.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one Worker-to-Client source command.
|
||||||
|
* @param value - Untrusted decoded command.
|
||||||
|
* @returns The validated command.
|
||||||
|
*/
|
||||||
|
export function parseClientSourceCommand(value: unknown): ClientSourceCommand {
|
||||||
|
if (!isPlainObject(value) || typeof value.op !== 'string') {
|
||||||
|
throw new Error('inspector protocol: Client source command must have an op')
|
||||||
|
}
|
||||||
|
if (value.op === 'list-scripts') {
|
||||||
|
exactKeys(value, ['op'], 'Client source list command')
|
||||||
|
return { op: 'list-scripts' }
|
||||||
|
}
|
||||||
|
if (value.op !== 'get-content-chunk') {
|
||||||
|
throw new Error(`inspector protocol: unknown Client source command ${JSON.stringify(value.op)}`)
|
||||||
|
}
|
||||||
|
exactKeys(value, ['op', 'scriptKey', 'content', 'offset', 'maxBytes'], 'Client source chunk command')
|
||||||
|
return {
|
||||||
|
op: 'get-content-chunk',
|
||||||
|
scriptKey: wireId<'RuntimeScriptKey'>(value.scriptKey, 'scriptKey'),
|
||||||
|
content: contentKind(value.content),
|
||||||
|
offset: natural(value.offset, 'offset', true),
|
||||||
|
maxBytes: natural(value.maxBytes, 'maxBytes', false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one successful Client source result.
|
||||||
|
* @param value - Untrusted decoded result.
|
||||||
|
* @returns The validated result.
|
||||||
|
*/
|
||||||
|
export function parseClientSourceResult(value: unknown): ClientSourceResult {
|
||||||
|
if (!isPlainObject(value) || typeof value.op !== 'string') {
|
||||||
|
throw new Error('inspector protocol: Client source result must have an op')
|
||||||
|
}
|
||||||
|
if (value.op === 'list-scripts') {
|
||||||
|
exactKeys(value, ['op', 'scripts'], 'Client source list result')
|
||||||
|
if (!Array.isArray(value.scripts)) throw new Error('inspector protocol: Client source scripts must be an array')
|
||||||
|
return { op: 'list-scripts', scripts: value.scripts.map(parseScript) }
|
||||||
|
}
|
||||||
|
if (value.op !== 'get-content-chunk') {
|
||||||
|
throw new Error(`inspector protocol: unknown Client source result ${JSON.stringify(value.op)}`)
|
||||||
|
}
|
||||||
|
if (value.available === false) {
|
||||||
|
exactKeys(value, ['op', 'scriptKey', 'content', 'available'], 'unavailable Client source chunk')
|
||||||
|
return {
|
||||||
|
op: 'get-content-chunk',
|
||||||
|
scriptKey: wireId<'RuntimeScriptKey'>(value.scriptKey, 'scriptKey'),
|
||||||
|
content: contentKind(value.content),
|
||||||
|
available: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exactKeys(
|
||||||
|
value,
|
||||||
|
['op', 'scriptKey', 'content', 'available', 'offset', 'nextOffset', 'data', 'eof'],
|
||||||
|
'Client source chunk result',
|
||||||
|
)
|
||||||
|
if (value.available !== true || typeof value.data !== 'string' || typeof value.eof !== 'boolean') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source chunk result')
|
||||||
|
}
|
||||||
|
const offset = natural(value.offset, 'offset', true)
|
||||||
|
const nextOffset = natural(value.nextOffset, 'nextOffset', true)
|
||||||
|
if (nextOffset < offset || !BASE64.test(value.data)) {
|
||||||
|
throw new Error('inspector protocol: invalid Client source chunk data')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
op: 'get-content-chunk',
|
||||||
|
scriptKey: wireId<'RuntimeScriptKey'>(value.scriptKey, 'scriptKey'),
|
||||||
|
content: contentKind(value.content),
|
||||||
|
available: true,
|
||||||
|
offset,
|
||||||
|
nextOffset,
|
||||||
|
data: value.data,
|
||||||
|
eof: value.eof,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScript(value: unknown): ClientScriptDescriptor {
|
||||||
|
const record = exactObject(value, [
|
||||||
|
'scriptKey', 'url', 'hash', 'buildId', 'sourceMapUrl', 'startLine', 'startColumn', 'endLine', 'endColumn',
|
||||||
|
'isModule', 'length',
|
||||||
|
], 'Client script descriptor')
|
||||||
|
if (typeof record.url !== 'string' || record.url.length > 8_192 || typeof record.hash !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid Client script identity')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
scriptKey: wireId<'RuntimeScriptKey'>(record.scriptKey, 'scriptKey'),
|
||||||
|
url: record.url,
|
||||||
|
hash: record.hash,
|
||||||
|
...optionalString(record, 'buildId'),
|
||||||
|
...optionalString(record, 'sourceMapUrl'),
|
||||||
|
startLine: natural(record.startLine, 'startLine', true),
|
||||||
|
startColumn: natural(record.startColumn, 'startColumn', true),
|
||||||
|
endLine: natural(record.endLine, 'endLine', true),
|
||||||
|
endColumn: natural(record.endColumn, 'endColumn', true),
|
||||||
|
...optionalBoolean(record, 'isModule'),
|
||||||
|
...(record.length === undefined ? {} : { length: natural(record.length, 'length', true) }),
|
||||||
|
} satisfies Omit<RuntimeScript, 'executionContextId'>
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentKind(value: unknown): ClientSourceContentKind {
|
||||||
|
if (value !== 'source' && value !== 'source-map') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source content kind')
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function natural(value: unknown, label: string, zero: boolean): number {
|
||||||
|
if (!Number.isSafeInteger(value) || (value as number) < (zero ? 0 : 1)) {
|
||||||
|
throw new Error(`inspector protocol: ${label} must be ${zero ? 'a non-negative' : 'a positive'} integer`)
|
||||||
|
}
|
||||||
|
return value as number
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE64 = /^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/u
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/** Operations and values exchanged with a Client realm's read-only source catalog. */
|
||||||
|
|
||||||
|
import type { RuntimeScriptKey } from '../../../cdp/ids.ts'
|
||||||
|
import type { RuntimeScript } from '../../../cdp/index.ts'
|
||||||
|
|
||||||
|
/** Script metadata that excludes the Worker-owned execution-context id. */
|
||||||
|
export type ClientScriptDescriptor = Omit<RuntimeScript, 'executionContextId'>
|
||||||
|
|
||||||
|
/** Content stored for one Client script. */
|
||||||
|
export type ClientSourceContentKind = 'source' | 'source-map'
|
||||||
|
|
||||||
|
/** Read-only operation accepted by the Client source catalog. */
|
||||||
|
export type ClientSourceCommand =
|
||||||
|
| { readonly op: 'list-scripts' }
|
||||||
|
| {
|
||||||
|
readonly op: 'get-content-chunk'
|
||||||
|
readonly scriptKey: RuntimeScriptKey
|
||||||
|
readonly content: ClientSourceContentKind
|
||||||
|
readonly offset: number
|
||||||
|
readonly maxBytes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Successful result of one Client source operation. */
|
||||||
|
export type ClientSourceResult =
|
||||||
|
| { readonly op: 'list-scripts'; readonly scripts: readonly ClientScriptDescriptor[] }
|
||||||
|
| {
|
||||||
|
readonly op: 'get-content-chunk'
|
||||||
|
readonly scriptKey: RuntimeScriptKey
|
||||||
|
readonly content: ClientSourceContentKind
|
||||||
|
readonly available: false
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly op: 'get-content-chunk'
|
||||||
|
readonly scriptKey: RuntimeScriptKey
|
||||||
|
readonly content: ClientSourceContentKind
|
||||||
|
readonly available: true
|
||||||
|
readonly offset: number
|
||||||
|
readonly nextOffset: number
|
||||||
|
readonly data: string
|
||||||
|
readonly eof: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deliberate failure returned by the Client source catalog. */
|
||||||
|
export interface ClientSourceError {
|
||||||
|
readonly code: 'invalid-request' | 'script-not-found' | 'load-failed' | 'result-too-large' | 'internal-error'
|
||||||
|
readonly message: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/** Versioned envelopes for Client source catalog operations. */
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ClientSourceRequestId,
|
||||||
|
ClientSourceSessionId,
|
||||||
|
InspectorSourceGeneration,
|
||||||
|
InspectorSourceId,
|
||||||
|
} from '../../ids.ts'
|
||||||
|
import { isPlainObject } from '../../../json.ts'
|
||||||
|
import { exactKeys, exactObject, wireId } from '../../../validation.ts'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION } from '../../version.ts'
|
||||||
|
import { parseClientSourceCommand, parseClientSourceResult } from './codec.ts'
|
||||||
|
import type { ClientSourceCommand, ClientSourceError, ClientSourceResult } from './commands.ts'
|
||||||
|
|
||||||
|
/** Source capability that permits read-only Client script discovery. */
|
||||||
|
export interface ClientSourcesCapability {
|
||||||
|
readonly type: 'client-sources'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worker request for one operation in a Client source catalog. */
|
||||||
|
export interface ClientSourceRequestFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-sources/request'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientSourceSessionId
|
||||||
|
readonly requestId: ClientSourceRequestId
|
||||||
|
readonly command: ClientSourceCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client response to one source catalog operation. */
|
||||||
|
export interface ClientSourceResponseFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-sources/response'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientSourceSessionId
|
||||||
|
readonly requestId: ClientSourceRequestId
|
||||||
|
readonly outcome:
|
||||||
|
| { readonly ok: true; readonly result: ClientSourceResult }
|
||||||
|
| { readonly ok: false; readonly error: ClientSourceError }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One-way cleanup for in-flight operations owned by a closed DevTools session. */
|
||||||
|
export interface ClientSourceSessionClosedFrame {
|
||||||
|
readonly v: typeof INSPECTOR_PROTOCOL_VERSION
|
||||||
|
readonly t: 'client-sources/session-closed'
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sessionId: ClientSourceSessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the marker capability for a Client source catalog.
|
||||||
|
* @param value - Untrusted capability declaration.
|
||||||
|
* @returns The validated marker capability.
|
||||||
|
*/
|
||||||
|
export function parseClientSourcesCapability(value: unknown): ClientSourcesCapability {
|
||||||
|
const record = exactObject(value, ['type'], 'Client Sources capability')
|
||||||
|
if (record.type !== 'client-sources') throw new Error('inspector protocol: invalid Client Sources capability')
|
||||||
|
return { type: 'client-sources' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one Worker-to-Client source request.
|
||||||
|
* @param value - Untrusted decoded request.
|
||||||
|
* @returns The validated request frame.
|
||||||
|
*/
|
||||||
|
export function parseClientSourceRequestFrame(value: Record<string, unknown>): ClientSourceRequestFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId', 'requestId', 'command'], 'Client source request')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-sources/request') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source request envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-sources/request',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientSourceSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
requestId: wireId<'ClientSourceRequestId'>(value.requestId, 'requestId'),
|
||||||
|
command: parseClientSourceCommand(value.command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one Client-to-Worker source response.
|
||||||
|
* @param value - Untrusted decoded response.
|
||||||
|
* @returns The validated response frame.
|
||||||
|
*/
|
||||||
|
export function parseClientSourceResponseFrame(value: Record<string, unknown>): ClientSourceResponseFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId', 'requestId', 'outcome'], 'Client source response')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-sources/response') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source response envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-sources/response',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientSourceSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
requestId: wireId<'ClientSourceRequestId'>(value.requestId, 'requestId'),
|
||||||
|
outcome: parseOutcome(value.outcome),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one Client source-session cleanup notification.
|
||||||
|
* @param value - Untrusted decoded notification.
|
||||||
|
* @returns The validated cleanup frame.
|
||||||
|
*/
|
||||||
|
export function parseClientSourceSessionClosedFrame(value: Record<string, unknown>): ClientSourceSessionClosedFrame {
|
||||||
|
exactKeys(value, ['v', 't', 'sourceId', 'generation', 'sessionId'], 'Client source session close')
|
||||||
|
if (value.v !== INSPECTOR_PROTOCOL_VERSION || value.t !== 'client-sources/session-closed') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source session close envelope')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'client-sources/session-closed',
|
||||||
|
sourceId: wireId<'InspectorSourceId'>(value.sourceId, 'sourceId'),
|
||||||
|
generation: wireId<'InspectorSourceGeneration'>(value.generation, 'generation'),
|
||||||
|
sessionId: wireId<'ClientSourceSessionId'>(value.sessionId, 'sessionId'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOutcome(value: unknown): ClientSourceResponseFrame['outcome'] {
|
||||||
|
if (!isPlainObject(value) || typeof value.ok !== 'boolean') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source outcome')
|
||||||
|
}
|
||||||
|
if (value.ok) {
|
||||||
|
exactKeys(value, ['ok', 'result'], 'successful Client source outcome')
|
||||||
|
return { ok: true, result: parseClientSourceResult(value.result) }
|
||||||
|
}
|
||||||
|
exactKeys(value, ['ok', 'error'], 'failed Client source outcome')
|
||||||
|
const error = exactObject(value.error, ['code', 'message'], 'Client source error')
|
||||||
|
if (!ERROR_CODES.has(error.code as ClientSourceError['code']) || typeof error.message !== 'string') {
|
||||||
|
throw new Error('inspector protocol: invalid Client source error')
|
||||||
|
}
|
||||||
|
return { ok: false, error: { code: error.code as ClientSourceError['code'], message: error.message } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_CODES = new Set<ClientSourceError['code']>([
|
||||||
|
'invalid-request', 'script-not-found', 'load-failed', 'result-too-large', 'internal-error',
|
||||||
|
])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** Public types and decoders for the Client source catalog protocol. */
|
||||||
|
|
||||||
|
export * from './codec.ts'
|
||||||
|
export * from './commands.ts'
|
||||||
|
export * from './frames.ts'
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/** Source-side interfaces shared by MessagePort and WebSocket bridge implementations. */
|
||||||
|
|
||||||
|
import type { InspectorJsonValue } from '../json.ts'
|
||||||
|
import type { InspectorQueryRequester } from './messages/query/commands.ts'
|
||||||
|
|
||||||
|
/** Transport-independent observation publisher. */
|
||||||
|
export interface InspectorPublisher {
|
||||||
|
/** Publish one validated observation. */
|
||||||
|
publish(topic: string, payload: InspectorJsonValue, monotonicMs?: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Publisher that also retains the latest value of stateful observation topics. */
|
||||||
|
export interface InspectorStatePublisher extends InspectorPublisher {
|
||||||
|
/**
|
||||||
|
* Replace one topic's retained state and publish the replacement.
|
||||||
|
* @param topic - Domain-owned state topic.
|
||||||
|
* @param payload - Latest JSON state, replayed after source resynchronization.
|
||||||
|
* @param monotonicMs - Source-clock timestamp; defaults to `performance.now()`.
|
||||||
|
*/
|
||||||
|
setState(topic: string, payload: InspectorJsonValue, monotonicMs?: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared capabilities exposed above a Host MessagePort or Client WebSocket carrier. */
|
||||||
|
export interface InspectorConnection extends InspectorStatePublisher, InspectorQueryRequester {}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/** Shared Host/Client owner of correlated non-CDP query requests. */
|
||||||
|
|
||||||
|
import { inspectorId, type InspectorSourceGeneration, type InspectorSourceId } from './ids.ts'
|
||||||
|
import { jsonByteLength, type InspectorJsonValue } from '../json.ts'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION } from './version.ts'
|
||||||
|
import type {
|
||||||
|
InspectorQuery,
|
||||||
|
InspectorQueryError,
|
||||||
|
InspectorQueryRequester,
|
||||||
|
InspectorQueryResult,
|
||||||
|
InspectorQueryResultFor,
|
||||||
|
} from './messages/query/commands.ts'
|
||||||
|
import { isInspectorQueryResponseEnvelope, parseInspectorQueryResponseFrame } from './messages/query/codec.ts'
|
||||||
|
import type { InspectorQueryRequestFrame, InspectorQueryRequestId } from './messages/query/frames.ts'
|
||||||
|
|
||||||
|
/** Active carrier write used by the shared query owner. */
|
||||||
|
export interface InspectorQuerySender {
|
||||||
|
/**
|
||||||
|
* Send one validated query request frame.
|
||||||
|
* @param frame - Request belonging to the active source generation.
|
||||||
|
*/
|
||||||
|
send(frame: InspectorQueryRequestFrame): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounds applied by one Host or Client query connection. */
|
||||||
|
export interface InspectorQueryConnectionOptions {
|
||||||
|
readonly timeoutMs: number
|
||||||
|
readonly maxFrameBytes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingQuery {
|
||||||
|
readonly op: string
|
||||||
|
readonly resolve: (result: InspectorQueryResult) => void
|
||||||
|
readonly reject: (error: Error) => void
|
||||||
|
readonly timer: ReturnType<typeof setTimeout>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryGeneration {
|
||||||
|
readonly sourceId: InspectorSourceId
|
||||||
|
readonly generation: InspectorSourceGeneration
|
||||||
|
readonly sender: InspectorQuerySender
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Failure deliberately returned by the Worker query handler. */
|
||||||
|
export class InspectorQueryRemoteError extends Error {
|
||||||
|
constructor(readonly code: InspectorQueryError['code'], message: string) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Correlates requests for one reconnecting Host or Client source. */
|
||||||
|
export class InspectorQueryConnection implements InspectorQueryRequester {
|
||||||
|
private readonly pending = new Map<InspectorQueryRequestId, PendingQuery>()
|
||||||
|
private active: QueryGeneration | undefined
|
||||||
|
private nextRequestId = 0
|
||||||
|
private closed = false
|
||||||
|
|
||||||
|
constructor(private readonly options: InspectorQueryConnectionOptions) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admit the source generation acknowledged by the Worker.
|
||||||
|
* @param sourceId - Stable source identity.
|
||||||
|
* @param generation - Newly accepted transport generation.
|
||||||
|
* @param sender - Carrier writer valid for that generation.
|
||||||
|
*/
|
||||||
|
connect(sourceId: InspectorSourceId, generation: InspectorSourceGeneration, sender: InspectorQuerySender): void {
|
||||||
|
if (this.closed) throw new Error('inspector query connection is closed')
|
||||||
|
this.disconnect('Inspector source generation replaced')
|
||||||
|
this.active = { sourceId, generation, sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a query against the currently accepted source generation.
|
||||||
|
* @param query - Closed typed query command.
|
||||||
|
* @returns The result with the same operation discriminant.
|
||||||
|
*/
|
||||||
|
request<Query extends InspectorQuery>(query: Query): Promise<InspectorQueryResultFor<Query>> {
|
||||||
|
const active = this.active
|
||||||
|
if (this.closed || active === undefined) {
|
||||||
|
return Promise.reject(new Error('Inspector query transport is not connected'))
|
||||||
|
}
|
||||||
|
const requestId = inspectorId<'InspectorQueryRequestId'>(`query-${String(++this.nextRequestId)}`, 'requestId')
|
||||||
|
const frame: InspectorQueryRequestFrame = {
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'query/request',
|
||||||
|
sourceId: active.sourceId,
|
||||||
|
generation: active.generation,
|
||||||
|
requestId,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
if (jsonByteLength(frame as unknown as InspectorJsonValue) > this.options.maxFrameBytes) {
|
||||||
|
return Promise.reject(new Error(`Inspector query request exceeds ${String(this.options.maxFrameBytes)} bytes`))
|
||||||
|
}
|
||||||
|
const result = new Promise<InspectorQueryResult>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.pending.delete(requestId)
|
||||||
|
reject(new Error(`Inspector query ${query.op} timed out after ${String(this.options.timeoutMs)}ms`))
|
||||||
|
}, this.options.timeoutMs)
|
||||||
|
this.pending.set(requestId, { op: query.op, resolve, reject, timer })
|
||||||
|
try {
|
||||||
|
active.sender.send(frame)
|
||||||
|
} catch (error) {
|
||||||
|
this.rejectPending(requestId, renderError(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result as Promise<InspectorQueryResultFor<Query>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume a decoded carrier value when it is a query response.
|
||||||
|
* @param value - Untrusted Worker-to-source value.
|
||||||
|
* @returns Whether the value belonged to the query protocol.
|
||||||
|
*/
|
||||||
|
receive(value: unknown): boolean {
|
||||||
|
if (!isInspectorQueryResponseEnvelope(value)) return false
|
||||||
|
let frame
|
||||||
|
try {
|
||||||
|
frame = parseInspectorQueryResponseFrame(value)
|
||||||
|
if (jsonByteLength(frame as unknown as InspectorJsonValue) > this.options.maxFrameBytes) {
|
||||||
|
throw new Error(`inspector protocol: query response exceeds ${String(this.options.maxFrameBytes)} bytes`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.disconnect(`Invalid Inspector query response: ${renderError(error).message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
const pending = this.pending.get(frame.requestId)
|
||||||
|
if (pending === undefined) return true
|
||||||
|
const active = this.active
|
||||||
|
if (active === undefined || frame.sourceId !== active.sourceId || frame.generation !== active.generation) {
|
||||||
|
this.rejectPending(frame.requestId, new Error('Inspector query response source generation does not match'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!frame.outcome.ok) {
|
||||||
|
this.rejectPending(frame.requestId, new InspectorQueryRemoteError(
|
||||||
|
frame.outcome.error.code,
|
||||||
|
frame.outcome.error.message,
|
||||||
|
))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (frame.outcome.result.op !== pending.op) {
|
||||||
|
this.rejectPending(frame.requestId, new Error(
|
||||||
|
`Inspector query response op ${frame.outcome.result.op} does not match ${pending.op}`,
|
||||||
|
))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
this.pending.delete(frame.requestId)
|
||||||
|
pending.resolve(frame.outcome.result)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject active requests while permitting a later source generation.
|
||||||
|
* @param reason - Failure reported to every pending caller.
|
||||||
|
*/
|
||||||
|
disconnect(reason: string): void {
|
||||||
|
this.active = undefined
|
||||||
|
for (const requestId of [...this.pending.keys()]) this.rejectPending(requestId, new Error(reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently reject requests and prevent later reconnection.
|
||||||
|
* @param reason - Failure reported to every pending caller.
|
||||||
|
*/
|
||||||
|
close(reason = 'Inspector query connection closed'): void {
|
||||||
|
if (this.closed) return
|
||||||
|
this.closed = true
|
||||||
|
this.disconnect(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
private rejectPending(requestId: InspectorQueryRequestId, error: Error): void {
|
||||||
|
const pending = this.pending.get(requestId)
|
||||||
|
if (pending === undefined) return
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
this.pending.delete(requestId)
|
||||||
|
pending.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderError(error: unknown): Error {
|
||||||
|
return error instanceof Error ? error : new Error(String(error))
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/** Bridge-facing exports for the shared untrusted-value validation primitives. */
|
||||||
|
|
||||||
|
export * from '../validation.ts'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/** Current Inspector wire version. Pre-release peers reject every other version. */
|
||||||
|
export const INSPECTOR_PROTOCOL_VERSION = 0 as const
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/** Explicit operation support advertised by each Inspector realm. */
|
||||||
|
|
||||||
|
/** Runtime operations implemented by a realm backend. */
|
||||||
|
export type RuntimeOperation =
|
||||||
|
| 'evaluate'
|
||||||
|
| 'get-properties'
|
||||||
|
| 'call-function'
|
||||||
|
| 'await-promise'
|
||||||
|
| 'release-object'
|
||||||
|
| 'release-object-group'
|
||||||
|
| 'global-lexical-scope-names'
|
||||||
|
|
||||||
|
/** Console operations implemented by a realm backend. */
|
||||||
|
export type ConsoleOperation = 'events' | 'exceptions' | 'clear'
|
||||||
|
|
||||||
|
/** Source catalog operations implemented by a realm backend. */
|
||||||
|
export type SourceOperation = 'catalog' | 'content' | 'source-map'
|
||||||
|
|
||||||
|
/** Active debugger operations implemented by a realm backend. */
|
||||||
|
export type DebuggerOperation = 'breakpoint' | 'pause' | 'resume' | 'step' | 'call-frame'
|
||||||
|
|
||||||
|
/** Complete capability declaration for one inspected realm. */
|
||||||
|
export interface InspectorRealmCapabilities {
|
||||||
|
readonly runtime: readonly RuntimeOperation[]
|
||||||
|
readonly console: readonly ConsoleOperation[]
|
||||||
|
readonly sources: readonly SourceOperation[]
|
||||||
|
readonly debugger: readonly DebuggerOperation[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/** Realm-neutral Console events emitted by Runtime backends. */
|
||||||
|
|
||||||
|
import type { RuntimeRemoteObject } from './remote-object.ts'
|
||||||
|
import type { RuntimeExceptionDetails, RuntimeStackTrace } from './errors.ts'
|
||||||
|
|
||||||
|
/** Console API categories exposed by CDP Runtime. */
|
||||||
|
export type RuntimeConsoleType =
|
||||||
|
| 'log'
|
||||||
|
| 'debug'
|
||||||
|
| 'info'
|
||||||
|
| 'error'
|
||||||
|
| 'warning'
|
||||||
|
| 'dir'
|
||||||
|
| 'dirxml'
|
||||||
|
| 'table'
|
||||||
|
| 'trace'
|
||||||
|
| 'clear'
|
||||||
|
| 'startGroup'
|
||||||
|
| 'startGroupCollapsed'
|
||||||
|
| 'endGroup'
|
||||||
|
| 'assert'
|
||||||
|
| 'profile'
|
||||||
|
| 'profileEnd'
|
||||||
|
| 'count'
|
||||||
|
| 'timeEnd'
|
||||||
|
|
||||||
|
/** One Console event associated with a single inspected realm. */
|
||||||
|
export interface RuntimeConsoleEvent<Handle extends string> {
|
||||||
|
readonly type: RuntimeConsoleType
|
||||||
|
readonly arguments: readonly RuntimeRemoteObject<Handle>[]
|
||||||
|
readonly timestamp: number
|
||||||
|
readonly contextId?: number
|
||||||
|
readonly stackTrace?: RuntimeStackTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One uncaught exception observed in an inspected realm. */
|
||||||
|
export interface RuntimeExceptionEvent<Handle extends string> {
|
||||||
|
readonly timestamp: number
|
||||||
|
readonly contextId?: number
|
||||||
|
readonly details: RuntimeExceptionDetails<Handle>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Console-domain event emitted by a realm backend. */
|
||||||
|
export type RuntimeConsoleBackendEvent<Handle extends string> =
|
||||||
|
| { readonly type: 'console-api'; readonly event: RuntimeConsoleEvent<Handle> }
|
||||||
|
| { readonly type: 'exception'; readonly event: RuntimeExceptionEvent<Handle> }
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/** Realm-neutral values used by active debugger backends. */
|
||||||
|
|
||||||
|
import type { InspectorJsonValue } from '../json.ts'
|
||||||
|
import type { RuntimeScriptKey } from './ids.ts'
|
||||||
|
import type { RuntimeStackTrace } from './errors.ts'
|
||||||
|
import type { RuntimeCompletion } from './operations.ts'
|
||||||
|
import type { RuntimeRemoteObject } from './remote-object.ts'
|
||||||
|
|
||||||
|
/** One source location independent of a CDP ScriptId allocation policy. */
|
||||||
|
export interface RuntimeDebuggerLocation {
|
||||||
|
readonly scriptKey: RuntimeScriptKey
|
||||||
|
readonly lineNumber: number
|
||||||
|
readonly columnNumber?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One lexical scope attached to a paused call frame. */
|
||||||
|
export interface RuntimeDebuggerScope<Handle extends string> {
|
||||||
|
readonly type: string
|
||||||
|
readonly object: RuntimeRemoteObject<Handle>
|
||||||
|
readonly name?: string
|
||||||
|
readonly startLocation?: RuntimeDebuggerLocation
|
||||||
|
readonly endLocation?: RuntimeDebuggerLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One paused JavaScript call frame. */
|
||||||
|
export interface RuntimeDebuggerCallFrame<Handle extends string> {
|
||||||
|
readonly callFrameId: string
|
||||||
|
readonly functionName: string
|
||||||
|
readonly functionLocation?: RuntimeDebuggerLocation
|
||||||
|
readonly location: RuntimeDebuggerLocation
|
||||||
|
readonly url: string
|
||||||
|
readonly scopeChain: readonly RuntimeDebuggerScope<Handle>[]
|
||||||
|
readonly thisObject: RuntimeRemoteObject<Handle>
|
||||||
|
readonly returnValue?: RuntimeRemoteObject<Handle>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Engine-independent evaluation request for one paused call frame. */
|
||||||
|
export interface RuntimeCallFrameEvaluationRequest {
|
||||||
|
readonly callFrameId: string
|
||||||
|
readonly expression: string
|
||||||
|
readonly objectGroup?: string
|
||||||
|
readonly includeCommandLineAPI?: boolean
|
||||||
|
readonly silent?: boolean
|
||||||
|
readonly returnByValue?: boolean
|
||||||
|
readonly generatePreview?: boolean
|
||||||
|
readonly throwOnSideEffect?: boolean
|
||||||
|
readonly timeoutMs?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Optional native script-cache limit requested while enabling Debugger. */
|
||||||
|
export interface RuntimeDebuggerEnableRequest {
|
||||||
|
readonly maxScriptsCacheSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Optional termination requested while resuming a native debugger. */
|
||||||
|
export interface RuntimeDebuggerResumeRequest {
|
||||||
|
readonly terminateOnResume?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debugger lifecycle notification emitted by a realm backend. */
|
||||||
|
export type RuntimeDebuggerEvent<Handle extends string> =
|
||||||
|
| {
|
||||||
|
readonly type: 'paused'
|
||||||
|
readonly callFrames: readonly RuntimeDebuggerCallFrame<Handle>[]
|
||||||
|
readonly reason: string
|
||||||
|
readonly data?: InspectorJsonValue
|
||||||
|
readonly hitBreakpoints?: readonly string[]
|
||||||
|
readonly asyncStackTrace?: RuntimeStackTrace
|
||||||
|
}
|
||||||
|
| { readonly type: 'resumed' }
|
||||||
|
| {
|
||||||
|
readonly type: 'breakpoint-resolved'
|
||||||
|
readonly breakpointId: string
|
||||||
|
readonly location: RuntimeDebuggerLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Active debugger operation result containing a Runtime value. */
|
||||||
|
export type RuntimeCallFrameEvaluation<Handle extends string> = RuntimeCompletion<Handle>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/** Realm-neutral JavaScript exception and stack information. */
|
||||||
|
|
||||||
|
import type { RuntimeScriptKey } from './ids.ts'
|
||||||
|
import type { RuntimeRemoteObject } from './remote-object.ts'
|
||||||
|
|
||||||
|
/** One source location in a Runtime exception stack. */
|
||||||
|
export interface RuntimeCallFrame {
|
||||||
|
readonly functionName: string
|
||||||
|
readonly scriptKey?: RuntimeScriptKey
|
||||||
|
readonly url: string
|
||||||
|
readonly lineNumber: number
|
||||||
|
readonly columnNumber: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JavaScript stack information independent of a Debugger script id. */
|
||||||
|
export interface RuntimeStackTrace {
|
||||||
|
readonly description?: string
|
||||||
|
readonly callFrames: readonly RuntimeCallFrame[]
|
||||||
|
readonly parent?: RuntimeStackTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JavaScript exception produced while executing one Runtime command. */
|
||||||
|
export interface RuntimeExceptionDetails<Handle extends string> {
|
||||||
|
readonly text: string
|
||||||
|
readonly lineNumber: number
|
||||||
|
readonly columnNumber: number
|
||||||
|
readonly url?: string
|
||||||
|
readonly stackTrace?: RuntimeStackTrace
|
||||||
|
readonly exception?: RuntimeRemoteObject<Handle>
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** Opaque identifiers owned by normalized realm backends. */
|
||||||
|
|
||||||
|
import type { InspectorId } from '../identity.ts'
|
||||||
|
|
||||||
|
/** Worker identity of one active Host or Client realm incarnation. */
|
||||||
|
export type InspectorRealmId = InspectorId<'InspectorRealmId'>
|
||||||
|
|
||||||
|
/** Backend-owned object handle interpreted only by its realm session. */
|
||||||
|
export type RuntimeBackendObjectHandle = InspectorId<'RuntimeBackendObjectHandle'>
|
||||||
|
|
||||||
|
/** Backend-independent identity of one script in a realm catalog. */
|
||||||
|
export type RuntimeScriptKey = InspectorId<'RuntimeScriptKey'>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** Realm-neutral Runtime, Console, Source, and Debugger protocol types. */
|
||||||
|
|
||||||
|
export * from './capabilities.ts'
|
||||||
|
export * from './console.ts'
|
||||||
|
export * from './debugger.ts'
|
||||||
|
export * from './errors.ts'
|
||||||
|
export * from './ids.ts'
|
||||||
|
export * from './operations.ts'
|
||||||
|
export * from './property.ts'
|
||||||
|
export * from './remote-object.ts'
|
||||||
|
export * from './sources.ts'
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/** Realm-neutral Runtime operations and results. */
|
||||||
|
|
||||||
|
import type { InspectorJsonObject, InspectorJsonValue } from '../json.ts'
|
||||||
|
import type { RuntimeExceptionDetails } from './errors.ts'
|
||||||
|
import type {
|
||||||
|
RuntimeInternalPropertyDescriptor,
|
||||||
|
RuntimePrivatePropertyDescriptor,
|
||||||
|
RuntimePropertyDescriptor,
|
||||||
|
} from './property.ts'
|
||||||
|
import type { RuntimeRemoteObject } from './remote-object.ts'
|
||||||
|
|
||||||
|
/** One argument supplied to a function in an inspected realm. */
|
||||||
|
export type RuntimeCallArgument<Handle extends string> =
|
||||||
|
| { readonly kind: 'value'; readonly value: InspectorJsonValue }
|
||||||
|
| { readonly kind: 'unserializable'; readonly value: string }
|
||||||
|
| { readonly kind: 'object'; readonly handle: Handle }
|
||||||
|
| { readonly kind: 'undefined' }
|
||||||
|
|
||||||
|
/** Engine-independent evaluation options supported by Runtime backends. */
|
||||||
|
export interface RuntimeEvaluateRequest {
|
||||||
|
readonly expression: string
|
||||||
|
readonly objectGroup?: string
|
||||||
|
readonly includeCommandLineAPI?: boolean
|
||||||
|
readonly silent?: boolean
|
||||||
|
readonly returnByValue?: boolean
|
||||||
|
readonly generatePreview?: boolean
|
||||||
|
readonly userGesture?: boolean
|
||||||
|
readonly awaitPromise?: boolean
|
||||||
|
readonly disableBreaks?: boolean
|
||||||
|
readonly replMode?: boolean
|
||||||
|
readonly allowUnsafeEvalBlockedByCSP?: boolean
|
||||||
|
readonly throwOnSideEffect?: boolean
|
||||||
|
readonly serializationOptions?: InspectorJsonObject
|
||||||
|
readonly timeoutMs?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Property enumeration request for one backend object. */
|
||||||
|
export interface RuntimeGetPropertiesRequest<Handle extends string> {
|
||||||
|
readonly handle: Handle
|
||||||
|
readonly ownProperties?: boolean
|
||||||
|
readonly accessorPropertiesOnly?: boolean
|
||||||
|
readonly generatePreview?: boolean
|
||||||
|
readonly nonIndexedPropertiesOnly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Function invocation request within one inspected realm. */
|
||||||
|
export interface RuntimeCallFunctionRequest<Handle extends string> {
|
||||||
|
readonly functionDeclaration: string
|
||||||
|
readonly receiver?: Handle
|
||||||
|
readonly arguments?: readonly RuntimeCallArgument<Handle>[]
|
||||||
|
readonly objectGroup?: string
|
||||||
|
readonly silent?: boolean
|
||||||
|
readonly returnByValue?: boolean
|
||||||
|
readonly generatePreview?: boolean
|
||||||
|
readonly userGesture?: boolean
|
||||||
|
readonly awaitPromise?: boolean
|
||||||
|
readonly throwOnSideEffect?: boolean
|
||||||
|
readonly serializationOptions?: InspectorJsonObject
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Promise-await request for one retained backend object. */
|
||||||
|
export interface RuntimeAwaitPromiseRequest<Handle extends string> {
|
||||||
|
readonly promise: Handle
|
||||||
|
readonly returnByValue?: boolean
|
||||||
|
readonly generatePreview?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared result of evaluation, function calls, and promise awaiting. */
|
||||||
|
export interface RuntimeCompletion<Handle extends string> {
|
||||||
|
readonly result: RuntimeRemoteObject<Handle>
|
||||||
|
readonly exceptionDetails?: RuntimeExceptionDetails<Handle>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared result of property enumeration. */
|
||||||
|
export interface RuntimeProperties<Handle extends string> {
|
||||||
|
readonly properties: readonly RuntimePropertyDescriptor<Handle>[]
|
||||||
|
readonly internalProperties?: readonly RuntimeInternalPropertyDescriptor<Handle>[]
|
||||||
|
readonly privateProperties?: readonly RuntimePrivatePropertyDescriptor<Handle>[]
|
||||||
|
readonly exceptionDetails?: RuntimeExceptionDetails<Handle>
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/** Realm-neutral property descriptors returned by Runtime backends. */
|
||||||
|
|
||||||
|
import type { RuntimeRemoteObject } from './remote-object.ts'
|
||||||
|
|
||||||
|
/** One JavaScript property descriptor returned without invoking accessors. */
|
||||||
|
export interface RuntimePropertyDescriptor<Handle extends string> {
|
||||||
|
readonly name: string
|
||||||
|
readonly value?: RuntimeRemoteObject<Handle>
|
||||||
|
readonly writable?: boolean
|
||||||
|
readonly get?: RuntimeRemoteObject<Handle>
|
||||||
|
readonly set?: RuntimeRemoteObject<Handle>
|
||||||
|
readonly configurable: boolean
|
||||||
|
readonly enumerable: boolean
|
||||||
|
readonly wasThrown?: boolean
|
||||||
|
readonly isOwn?: boolean
|
||||||
|
readonly symbol?: RuntimeRemoteObject<Handle>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One engine-owned property such as `[[Prototype]]`. */
|
||||||
|
export interface RuntimeInternalPropertyDescriptor<Handle extends string> {
|
||||||
|
readonly name: string
|
||||||
|
readonly value?: RuntimeRemoteObject<Handle>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One engine private property exposed when a backend supports it. */
|
||||||
|
export interface RuntimePrivatePropertyDescriptor<Handle extends string> {
|
||||||
|
readonly name: string
|
||||||
|
readonly value?: RuntimeRemoteObject<Handle>
|
||||||
|
readonly get?: RuntimeRemoteObject<Handle>
|
||||||
|
readonly set?: RuntimeRemoteObject<Handle>
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/** Environment-independent backend interfaces for inspected JavaScript realms. */
|
||||||
|
|
||||||
|
import type { RuntimeBackendObjectHandle, RuntimeScriptKey } from './ids.ts'
|
||||||
|
import type {
|
||||||
|
RuntimeAwaitPromiseRequest,
|
||||||
|
RuntimeCallFunctionRequest,
|
||||||
|
RuntimeCompletion,
|
||||||
|
RuntimeConsoleBackendEvent,
|
||||||
|
RuntimeDebuggerEvent,
|
||||||
|
RuntimeDebuggerEnableRequest,
|
||||||
|
RuntimeDebuggerResumeRequest,
|
||||||
|
RuntimeCallFrameEvaluationRequest,
|
||||||
|
RuntimeEvaluateRequest,
|
||||||
|
RuntimeGetPropertiesRequest,
|
||||||
|
RuntimeProperties,
|
||||||
|
RuntimeScript,
|
||||||
|
} from './index.ts'
|
||||||
|
|
||||||
|
/** Raw notification emitted by a native engine protocol backend. */
|
||||||
|
export interface NativeProtocolNotification {
|
||||||
|
readonly method: string
|
||||||
|
readonly params?: Readonly<Record<string, unknown>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Explicitly supported or unsupported realm capability. */
|
||||||
|
export type RealmCapability<Backend> =
|
||||||
|
| { readonly state: 'supported'; readonly backend: Backend }
|
||||||
|
| { readonly state: 'unsupported'; readonly reason: string }
|
||||||
|
|
||||||
|
/** Runtime operations implemented inside one per-connection realm session. */
|
||||||
|
export interface RuntimeBackend {
|
||||||
|
/** Prepare Runtime events and execution state for this connection. */
|
||||||
|
enable(): Promise<void>
|
||||||
|
/** Disable Runtime events and release backend session state. */
|
||||||
|
disable(): Promise<void>
|
||||||
|
/**
|
||||||
|
* Evaluate source in this realm.
|
||||||
|
* @param request - Engine-independent evaluation request.
|
||||||
|
* @returns Completion containing a value or JavaScript exception.
|
||||||
|
*/
|
||||||
|
evaluate(request: RuntimeEvaluateRequest): Promise<RuntimeCompletion<RuntimeBackendObjectHandle>>
|
||||||
|
/**
|
||||||
|
* Enumerate one retained object's properties.
|
||||||
|
* @param request - Property request containing this backend's object handle.
|
||||||
|
* @returns Property descriptors and optional exception details.
|
||||||
|
*/
|
||||||
|
getProperties(
|
||||||
|
request: RuntimeGetPropertiesRequest<RuntimeBackendObjectHandle>,
|
||||||
|
): Promise<RuntimeProperties<RuntimeBackendObjectHandle>>
|
||||||
|
/**
|
||||||
|
* Invoke a function with references owned by this realm session.
|
||||||
|
* @param request - Function source, receiver, arguments, and result options.
|
||||||
|
* @returns Completion containing the invocation result or JavaScript exception.
|
||||||
|
*/
|
||||||
|
callFunction(
|
||||||
|
request: RuntimeCallFunctionRequest<RuntimeBackendObjectHandle>,
|
||||||
|
): Promise<RuntimeCompletion<RuntimeBackendObjectHandle>>
|
||||||
|
/**
|
||||||
|
* Await one retained Promise.
|
||||||
|
* @param request - Promise handle and result options.
|
||||||
|
* @returns Completion containing the fulfilled value or rejection.
|
||||||
|
*/
|
||||||
|
awaitPromise(
|
||||||
|
request: RuntimeAwaitPromiseRequest<RuntimeBackendObjectHandle>,
|
||||||
|
): Promise<RuntimeCompletion<RuntimeBackendObjectHandle>>
|
||||||
|
/** @returns Names visible in the realm's global lexical scope. */
|
||||||
|
globalLexicalScopeNames(): Promise<readonly string[]>
|
||||||
|
/**
|
||||||
|
* Release one backend object reference.
|
||||||
|
* @param handle - Handle owned by this realm session.
|
||||||
|
*/
|
||||||
|
releaseObject(handle: RuntimeBackendObjectHandle): Promise<void>
|
||||||
|
/**
|
||||||
|
* Release every backend object retained under one group.
|
||||||
|
* @param group - DevTools object-group name.
|
||||||
|
*/
|
||||||
|
releaseObjectGroup(group: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Realm Console event source. */
|
||||||
|
export interface ConsoleBackend {
|
||||||
|
/**
|
||||||
|
* Subscribe to Console and uncaught-exception events.
|
||||||
|
* @param listener - Connection-local event consumer.
|
||||||
|
* @returns A disposer for the subscription.
|
||||||
|
*/
|
||||||
|
subscribe(listener: (event: RuntimeConsoleBackendEvent<RuntimeBackendObjectHandle>) => void): () => void
|
||||||
|
/** Clear backend-owned Console history when supported. */
|
||||||
|
clear(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Realm script catalog independent of CDP ScriptId allocation. */
|
||||||
|
export interface SourceBackend {
|
||||||
|
/** @returns Every script currently known to this realm. */
|
||||||
|
listScripts(): Promise<readonly RuntimeScript[]>
|
||||||
|
/**
|
||||||
|
* Read source text for one realm-local script key.
|
||||||
|
* @param scriptKey - Script identity allocated by this realm.
|
||||||
|
* @returns The complete source text.
|
||||||
|
*/
|
||||||
|
getScriptSource(scriptKey: RuntimeScriptKey): Promise<string>
|
||||||
|
/**
|
||||||
|
* Read an optional source map for one realm-local script key.
|
||||||
|
* @param scriptKey - Script identity allocated by this realm.
|
||||||
|
* @returns Source-map JSON when one exists.
|
||||||
|
*/
|
||||||
|
getSourceMap(scriptKey: RuntimeScriptKey): Promise<string | undefined>
|
||||||
|
/**
|
||||||
|
* Subscribe to scripts discovered after the initial catalog read.
|
||||||
|
* @param listener - Consumer of newly discovered scripts.
|
||||||
|
* @returns A disposer for the subscription.
|
||||||
|
*/
|
||||||
|
subscribe(listener: (script: RuntimeScript) => void): () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Active JavaScript debugging backend for one realm session. */
|
||||||
|
export interface DebuggerBackend {
|
||||||
|
/** Enable debugger events for this connection. */
|
||||||
|
enable(request: RuntimeDebuggerEnableRequest): Promise<Readonly<Record<string, unknown>>>
|
||||||
|
/** Disable debugger events for this connection. */
|
||||||
|
disable(): Promise<Readonly<Record<string, unknown>>>
|
||||||
|
/** Pause this realm. */
|
||||||
|
pause(): Promise<Readonly<Record<string, unknown>>>
|
||||||
|
/** Resume this realm. */
|
||||||
|
resume(request: RuntimeDebuggerResumeRequest): Promise<Readonly<Record<string, unknown>>>
|
||||||
|
/**
|
||||||
|
* Evaluate an expression in one paused frame.
|
||||||
|
* @param request - Frame identity, expression, and result options.
|
||||||
|
* @returns A common Runtime completion.
|
||||||
|
*/
|
||||||
|
evaluateOnCallFrame(
|
||||||
|
request: RuntimeCallFrameEvaluationRequest,
|
||||||
|
): Promise<RuntimeCompletion<RuntimeBackendObjectHandle>>
|
||||||
|
/**
|
||||||
|
* Subscribe to paused, resumed, and breakpoint events.
|
||||||
|
* @param listener - Connection-local debugger event consumer.
|
||||||
|
* @returns A disposer removing the consumer.
|
||||||
|
*/
|
||||||
|
subscribe(listener: (event: RuntimeDebuggerEvent<RuntimeBackendObjectHandle>) => void): () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Explicit Host-only native protocol adapter for domains not yet normalized. */
|
||||||
|
export interface NativeDomainBackend {
|
||||||
|
/**
|
||||||
|
* Execute one native protocol request.
|
||||||
|
* @param method - CDP method owned by the native engine.
|
||||||
|
* @param params - Parsed CDP parameters.
|
||||||
|
* @returns Native response fields.
|
||||||
|
*/
|
||||||
|
request(method: string, params: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>>>
|
||||||
|
/**
|
||||||
|
* Subscribe to native protocol notifications.
|
||||||
|
* @param listener - Notification consumer.
|
||||||
|
* @returns A disposer removing the consumer.
|
||||||
|
*/
|
||||||
|
subscribe(listener: (message: NativeProtocolNotification) => void): () => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/** Realm-neutral JavaScript value descriptions used by Inspector backends. */
|
||||||
|
|
||||||
|
import type { InspectorObjectReference } from '../cordis/object-reference.ts'
|
||||||
|
import type { InspectorJsonValue } from '../json.ts'
|
||||||
|
|
||||||
|
/** Runtime value kinds represented by CDP `Runtime.RemoteObject`. */
|
||||||
|
export type RuntimeRemoteObjectType =
|
||||||
|
| 'object'
|
||||||
|
| 'function'
|
||||||
|
| 'undefined'
|
||||||
|
| 'string'
|
||||||
|
| 'number'
|
||||||
|
| 'boolean'
|
||||||
|
| 'symbol'
|
||||||
|
| 'bigint'
|
||||||
|
|
||||||
|
/** Runtime object subtype hints understood by Chrome DevTools. */
|
||||||
|
export type RuntimeRemoteObjectSubtype =
|
||||||
|
| 'array'
|
||||||
|
| 'null'
|
||||||
|
| 'node'
|
||||||
|
| 'regexp'
|
||||||
|
| 'date'
|
||||||
|
| 'map'
|
||||||
|
| 'set'
|
||||||
|
| 'weakmap'
|
||||||
|
| 'weakset'
|
||||||
|
| 'iterator'
|
||||||
|
| 'generator'
|
||||||
|
| 'error'
|
||||||
|
| 'proxy'
|
||||||
|
| 'promise'
|
||||||
|
| 'typedarray'
|
||||||
|
| 'arraybuffer'
|
||||||
|
| 'dataview'
|
||||||
|
| 'webassemblymemory'
|
||||||
|
| 'wasmvalue'
|
||||||
|
|
||||||
|
/** Shallow property rendered inline by DevTools. */
|
||||||
|
export interface RuntimePropertyPreview {
|
||||||
|
readonly name: string
|
||||||
|
readonly type: RuntimeRemoteObjectType | 'accessor'
|
||||||
|
readonly value?: string
|
||||||
|
readonly valuePreview?: RuntimeObjectPreview
|
||||||
|
readonly subtype?: RuntimeRemoteObjectSubtype
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shallow object rendering that never carries a live-object reference. */
|
||||||
|
export interface RuntimeObjectPreview {
|
||||||
|
readonly type: RuntimeRemoteObjectType
|
||||||
|
readonly subtype?: RuntimeRemoteObjectSubtype
|
||||||
|
readonly description?: string
|
||||||
|
readonly overflow: boolean
|
||||||
|
readonly properties: readonly RuntimePropertyPreview[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Engine-independent description of one JavaScript value. */
|
||||||
|
export interface RuntimeRemoteObjectDescriptor {
|
||||||
|
readonly type: RuntimeRemoteObjectType
|
||||||
|
readonly subtype?: RuntimeRemoteObjectSubtype
|
||||||
|
readonly className?: string
|
||||||
|
readonly value?: InspectorJsonValue
|
||||||
|
readonly unserializableValue?: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly preview?: RuntimeObjectPreview
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Backend-owned reference to a retained object in one realm session. */
|
||||||
|
export interface RuntimeBackendObjectReference<Handle extends string> {
|
||||||
|
readonly handle: Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Realm-neutral value plus optional backend and Cordis identities. */
|
||||||
|
export interface RuntimeRemoteObject<Handle extends string> {
|
||||||
|
readonly descriptor: RuntimeRemoteObjectDescriptor
|
||||||
|
readonly object?: RuntimeBackendObjectReference<Handle>
|
||||||
|
readonly semanticReference?: InspectorObjectReference
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/** Realm-neutral script metadata used by source backends. */
|
||||||
|
|
||||||
|
import type { RuntimeScriptKey } from './ids.ts'
|
||||||
|
|
||||||
|
/** One script visible in a realm's source catalog. */
|
||||||
|
export interface RuntimeScript {
|
||||||
|
readonly scriptKey: RuntimeScriptKey
|
||||||
|
readonly url: string
|
||||||
|
readonly hash: string
|
||||||
|
readonly buildId?: string
|
||||||
|
readonly sourceMapUrl?: string
|
||||||
|
readonly startLine: number
|
||||||
|
readonly startColumn: number
|
||||||
|
readonly endLine: number
|
||||||
|
readonly endColumn: number
|
||||||
|
readonly executionContextId?: number
|
||||||
|
readonly isModule?: boolean
|
||||||
|
readonly length?: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/** Shared branded-identifier construction without assigning protocol ownership. */
|
||||||
|
|
||||||
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||||
|
|
||||||
|
/** String branded with one Inspector identity role. */
|
||||||
|
export type InspectorId<Role extends string> = Branded<Role>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and brand a non-empty identifier received from or sent across a runtime boundary.
|
||||||
|
* @param value - Untrusted identifier text.
|
||||||
|
* @param label - Field name used in validation errors.
|
||||||
|
* @returns The role-branded identifier.
|
||||||
|
*/
|
||||||
|
export function inspectorId<Role extends string>(value: string, label: string): InspectorId<Role> {
|
||||||
|
if (value.length === 0 || value.length > 256) {
|
||||||
|
throw new Error(`inspector protocol: ${label} must contain 1 to 256 characters`)
|
||||||
|
}
|
||||||
|
return value as InspectorId<Role>
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/** Environment-independent Inspector models and bridge protocol exports. */
|
||||||
|
|
||||||
|
export * from './bridge/messages/control.ts'
|
||||||
|
export * from './bridge/control-codec.ts'
|
||||||
|
export * from './cordis/snapshot.ts'
|
||||||
|
export * from './bridge/messages/cordis.ts'
|
||||||
|
export * from './bridge/messages/runtime/index.ts'
|
||||||
|
export * from './bridge/messages/sources/index.ts'
|
||||||
|
export * from './bridge/messages/network.ts'
|
||||||
|
export * from './network/observation.ts'
|
||||||
|
export * from './bridge/ids.ts'
|
||||||
|
export * from './json.ts'
|
||||||
|
export * from './cordis/object-reference.ts'
|
||||||
|
export * from './bridge/messages/query/index.ts'
|
||||||
|
export * from './bridge/query-reader.ts'
|
||||||
|
export * from './bridge/rpc.ts'
|
||||||
|
export * from './cdp/index.ts'
|
||||||
|
export * from './bridge/messages/observation.ts'
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/** JSON values admitted by every Inspector cross-realm message. */
|
||||||
|
|
||||||
|
/** JSON scalar accepted by Inspector transports. */
|
||||||
|
export type InspectorJsonPrimitive = null | boolean | number | string
|
||||||
|
|
||||||
|
/** Recursively JSON-compatible value accepted by Inspector transports. */
|
||||||
|
export type InspectorJsonValue =
|
||||||
|
| InspectorJsonPrimitive
|
||||||
|
| readonly InspectorJsonValue[]
|
||||||
|
| InspectorJsonObject
|
||||||
|
|
||||||
|
/** JSON-compatible object accepted by Inspector transports. */
|
||||||
|
export interface InspectorJsonObject {
|
||||||
|
readonly [key: string]: InspectorJsonValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a value can cross both MessagePort and JSON WebSocket carriers without coercion.
|
||||||
|
* @param value - Candidate wire value.
|
||||||
|
* @returns Whether the value is lossless JSON data.
|
||||||
|
*/
|
||||||
|
export function isJsonValue(value: unknown): value is InspectorJsonValue {
|
||||||
|
return visitJson(value, new Set<object>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require a plain JSON object and return it with a narrowed type.
|
||||||
|
* @param value - Candidate wire value.
|
||||||
|
* @param label - Field name used in validation errors.
|
||||||
|
* @returns The validated JSON object.
|
||||||
|
*/
|
||||||
|
export function requireJsonObject(value: unknown, label: string): InspectorJsonObject {
|
||||||
|
if (!isPlainObject(value) || !isJsonValue(value)) {
|
||||||
|
throw new Error(`inspector protocol: ${label} must be a JSON object`)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the UTF-8 byte length of a JSON wire value.
|
||||||
|
* @param value - Validated JSON value.
|
||||||
|
* @returns Its encoded byte length.
|
||||||
|
*/
|
||||||
|
export function jsonByteLength(value: InspectorJsonValue): number {
|
||||||
|
return new TextEncoder().encode(JSON.stringify(value)).byteLength
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether a value is a plain object with string own keys.
|
||||||
|
* @param value - Candidate object.
|
||||||
|
* @returns Whether the value has `Object.prototype` or a null prototype.
|
||||||
|
*/
|
||||||
|
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
||||||
|
const prototype = Reflect.getPrototypeOf(value)
|
||||||
|
return prototype === Object.prototype || prototype === null
|
||||||
|
}
|
||||||
|
|
||||||
|
function visitJson(value: unknown, ancestors: Set<object>): value is InspectorJsonValue {
|
||||||
|
if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
|
||||||
|
if (typeof value === 'number') return Number.isFinite(value) && !Object.is(value, -0)
|
||||||
|
if (typeof value !== 'object' || ancestors.has(value)) return false
|
||||||
|
ancestors.add(value)
|
||||||
|
try {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (Object.getPrototypeOf(value) !== Array.prototype || Reflect.ownKeys(value).length !== value.length + 1) return false
|
||||||
|
return value.every(item => visitJson(item, ancestors))
|
||||||
|
}
|
||||||
|
if (!isPlainObject(value)) return false
|
||||||
|
for (const key of Reflect.ownKeys(value)) {
|
||||||
|
if (typeof key !== 'string') return false
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
||||||
|
if (descriptor?.enumerable !== true || !('value' in descriptor) || !visitJson(descriptor.value, ancestors)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} finally {
|
||||||
|
ancestors.delete(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/** Shared exact-object readers for versioned Inspector wire protocols. */
|
||||||
|
|
||||||
|
import { inspectorId, type InspectorId } from './identity.ts'
|
||||||
|
import { isPlainObject } from './json.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require a plain object containing only the listed fields.
|
||||||
|
* @param value - Candidate object.
|
||||||
|
* @param keys - Complete field allowlist.
|
||||||
|
* @param label - Object name used in validation errors.
|
||||||
|
* @returns The validated plain object.
|
||||||
|
*/
|
||||||
|
export function exactObject(value: unknown, keys: readonly string[], label: string): Record<string, unknown> {
|
||||||
|
if (!isPlainObject(value)) throw new Error(`inspector protocol: ${label} must be an object`)
|
||||||
|
exactKeys(value, keys, label)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject fields outside one versioned object's declared field set.
|
||||||
|
* @param value - Plain object being validated.
|
||||||
|
* @param keys - Complete field allowlist.
|
||||||
|
* @param label - Object name used in validation errors.
|
||||||
|
*/
|
||||||
|
export function exactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
|
||||||
|
const allowed = new Set(keys)
|
||||||
|
for (const key of Reflect.ownKeys(value)) {
|
||||||
|
if (typeof key !== 'string' || !allowed.has(key)) {
|
||||||
|
throw new Error(`inspector protocol: ${label} has unknown field ${JSON.stringify(String(key))}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one non-empty opaque identifier.
|
||||||
|
* @param value - Candidate identifier.
|
||||||
|
* @param label - Field name used in validation errors.
|
||||||
|
* @returns The role-branded identifier.
|
||||||
|
*/
|
||||||
|
export function wireId<Role extends string>(value: unknown, label: string): InspectorId<Role> {
|
||||||
|
if (typeof value !== 'string') throw new Error(`inspector protocol: ${label} must be a string`)
|
||||||
|
return inspectorId<Role>(value, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one optional string field.
|
||||||
|
* @param value - Object containing the field.
|
||||||
|
* @param key - Field name.
|
||||||
|
* @returns An empty object or the validated field.
|
||||||
|
*/
|
||||||
|
export function optionalString<Key extends string>(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
key: Key,
|
||||||
|
): { readonly [Property in Key]?: string } {
|
||||||
|
const item = value[key]
|
||||||
|
if (item === undefined) return {}
|
||||||
|
if (typeof item !== 'string') throw new Error(`inspector protocol: ${key} must be a string`)
|
||||||
|
return { [key]: item } as { readonly [Property in Key]?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one optional boolean field.
|
||||||
|
* @param value - Object containing the field.
|
||||||
|
* @param key - Field name.
|
||||||
|
* @returns An empty object or the validated field.
|
||||||
|
*/
|
||||||
|
export function optionalBoolean<Key extends string>(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
key: Key,
|
||||||
|
): { readonly [Property in Key]?: boolean } {
|
||||||
|
const item = value[key]
|
||||||
|
if (item === undefined) return {}
|
||||||
|
if (typeof item !== 'boolean') throw new Error(`inspector protocol: ${key} must be a boolean`)
|
||||||
|
return { [key]: item } as { readonly [Property in Key]?: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one optional non-negative finite number field.
|
||||||
|
* @param value - Object containing the field.
|
||||||
|
* @param key - Field name.
|
||||||
|
* @returns An empty object or the validated field.
|
||||||
|
*/
|
||||||
|
export function optionalNonNegativeNumber<Key extends string>(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
key: Key,
|
||||||
|
): { readonly [Property in Key]?: number } {
|
||||||
|
const item = value[key]
|
||||||
|
if (item === undefined) return {}
|
||||||
|
if (typeof item !== 'number' || !Number.isFinite(item) || item < 0) {
|
||||||
|
throw new Error(`inspector protocol: ${key} must be a non-negative finite number`)
|
||||||
|
}
|
||||||
|
return { [key]: item } as { readonly [Property in Key]?: number }
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
/** Worker and shared protocol behavior. */
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { INSPECTOR_PROTOCOL_VERSION, parseSourceFrame, parseWorkerSourceFrame } from '../src/shared/bridge/messages/observation.ts'
|
||||||
|
import { InspectorSourceRegistry, type InspectorRecordConsumer, type SourceConnection } from '../src/worker/bridge/hub.ts'
|
||||||
|
|
||||||
|
describe('Inspector source protocol', () => {
|
||||||
|
it('rebuilds a valid source frame and rejects non-JSON payloads', () => {
|
||||||
|
const frame = parseSourceFrame({
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'source/append',
|
||||||
|
sourceId: 'host-1',
|
||||||
|
generation: 'generation-1',
|
||||||
|
firstSequence: 1,
|
||||||
|
droppedBefore: 0,
|
||||||
|
records: [{ monotonicMs: 12, topic: 'probe', payload: { ok: true } }],
|
||||||
|
}, 4)
|
||||||
|
expect(frame.t).toBe('source/append')
|
||||||
|
expect(() => parseSourceFrame({
|
||||||
|
v: INSPECTOR_PROTOCOL_VERSION,
|
||||||
|
t: 'source/append',
|
||||||
|
sourceId: 'host-1',
|
||||||
|
generation: 'generation-1',
|
||||||
|
firstSequence: 1,
|
||||||
|
droppedBefore: 0,
|
||||||
|
records: [{ monotonicMs: 12, topic: 'probe', payload: { bad: undefined } }],
|
||||||
|
}, 4)).toThrow('lossless JSON object')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isolates generations and reports sequence gaps', () => {
|
||||||
|
const replace = vi.fn()
|
||||||
|
const append = vi.fn()
|
||||||
|
const close = vi.fn()
|
||||||
|
const consumer: InspectorRecordConsumer = {
|
||||||
|
topics: new Set(['probe']),
|
||||||
|
replace,
|
||||||
|
append,
|
||||||
|
close,
|
||||||
|
}
|
||||||
|
const replies: unknown[] = []
|
||||||
|
const send = vi.fn((frame: unknown) => { replies.push(frame) })
|
||||||
|
const closeConnection = vi.fn()
|
||||||
|
const connection: SourceConnection = {
|
||||||
|
kind: 'host',
|
||||||
|
send,
|
||||||
|
close: closeConnection,
|
||||||
|
}
|
||||||
|
const registry = new InspectorSourceRegistry([consumer], 16_384, 4)
|
||||||
|
registry.receive(connection, {
|
||||||
|
v: 0,
|
||||||
|
t: 'source/open',
|
||||||
|
source: {
|
||||||
|
sourceId: 'host-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
kind: 'host',
|
||||||
|
label: 'Host',
|
||||||
|
timeOriginMs: 1_000,
|
||||||
|
capabilities: [],
|
||||||
|
},
|
||||||
|
topics: ['probe'],
|
||||||
|
})
|
||||||
|
registry.receive(connection, {
|
||||||
|
v: 0,
|
||||||
|
t: 'source/append',
|
||||||
|
sourceId: 'host-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
firstSequence: 2,
|
||||||
|
droppedBefore: 1,
|
||||||
|
records: [{ monotonicMs: 1, topic: 'probe', payload: { value: 1 } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(append).toHaveBeenCalledOnce()
|
||||||
|
expect(registry.describe()[0]).toMatchObject({ expectedSequence: 3, dropped: 1, topics: { probe: 1 } })
|
||||||
|
|
||||||
|
registry.receive(connection, {
|
||||||
|
v: 0,
|
||||||
|
t: 'source/append',
|
||||||
|
sourceId: 'host-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
firstSequence: 5,
|
||||||
|
droppedBefore: 0,
|
||||||
|
records: [],
|
||||||
|
})
|
||||||
|
expect(replies.at(-1)).toMatchObject({ t: 'source/resnapshot', expectedSequence: 3 })
|
||||||
|
expect(append).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes only a malformed source connection', () => {
|
||||||
|
const send = vi.fn()
|
||||||
|
const closeConnection = vi.fn()
|
||||||
|
const connection: SourceConnection = {
|
||||||
|
kind: 'client',
|
||||||
|
send,
|
||||||
|
close: closeConnection,
|
||||||
|
}
|
||||||
|
const registry = new InspectorSourceRegistry([], 1_024, 2)
|
||||||
|
registry.receive(connection, { v: 99, t: 'source/open' })
|
||||||
|
expect(send).toHaveBeenCalledWith(expect.objectContaining({ t: 'source/rejected' }))
|
||||||
|
expect(closeConnection).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('decodes Runtime commands and rejects undeclared fields', () => {
|
||||||
|
const request = parseWorkerSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-runtime/request',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
requestId: 'request-1',
|
||||||
|
command: {
|
||||||
|
op: 'call-function',
|
||||||
|
functionDeclaration: 'function () { return this.value }',
|
||||||
|
receiver: 'object-1',
|
||||||
|
arguments: [{ kind: 'unserializable', value: 'NaN' }],
|
||||||
|
returnByValue: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(request).toMatchObject({
|
||||||
|
t: 'client-runtime/request',
|
||||||
|
command: { op: 'call-function', receiver: 'object-1', returnByValue: true },
|
||||||
|
})
|
||||||
|
if (request.t !== 'client-runtime/request') throw new Error('unexpected frame type')
|
||||||
|
expect(() => parseWorkerSourceFrame({
|
||||||
|
...request,
|
||||||
|
command: { ...request.command, unversionedExtension: true },
|
||||||
|
})).toThrow('unknown field')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects invalid RemoteObject representations', () => {
|
||||||
|
expect(() => parseSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-runtime/response',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
requestId: 'request-1',
|
||||||
|
outcome: {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
op: 'evaluate',
|
||||||
|
completion: {
|
||||||
|
result: {
|
||||||
|
descriptor: { type: 'number', value: 1 },
|
||||||
|
object: { handle: 'object-1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, 4)).toThrow('invalid number RemoteObject representation')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('decodes exact Client Console lifecycle and event frames', () => {
|
||||||
|
expect(parseWorkerSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-console/enable',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
})).toMatchObject({ t: 'client-console/enable', sessionId: 'session-1' })
|
||||||
|
|
||||||
|
const frame = parseSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-console/event',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
event: {
|
||||||
|
type: 'console-api',
|
||||||
|
event: {
|
||||||
|
type: 'log',
|
||||||
|
arguments: [{
|
||||||
|
descriptor: { type: 'object', className: 'Object', description: 'Object' },
|
||||||
|
object: { handle: 'object-1' },
|
||||||
|
}],
|
||||||
|
timestamp: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, 4)
|
||||||
|
expect(frame).toMatchObject({
|
||||||
|
t: 'client-console/event',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
event: {
|
||||||
|
type: 'console-api',
|
||||||
|
event: { type: 'log', arguments: [{ object: { handle: 'object-1' } }] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => parseWorkerSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-console/disable',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
extra: true,
|
||||||
|
})).toThrow('unknown field')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('decodes bounded Client source commands and responses', () => {
|
||||||
|
expect(parseWorkerSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-sources/request',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'source-session-1',
|
||||||
|
requestId: 'source-request-1',
|
||||||
|
command: {
|
||||||
|
op: 'get-content-chunk',
|
||||||
|
scriptKey: 'bundle',
|
||||||
|
content: 'source',
|
||||||
|
offset: 0,
|
||||||
|
maxBytes: 1024,
|
||||||
|
},
|
||||||
|
})).toMatchObject({
|
||||||
|
t: 'client-sources/request',
|
||||||
|
command: { op: 'get-content-chunk', maxBytes: 1024 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parseSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-sources/response',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'source-session-1',
|
||||||
|
requestId: 'source-request-1',
|
||||||
|
outcome: {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
op: 'get-content-chunk',
|
||||||
|
scriptKey: 'bundle',
|
||||||
|
content: 'source',
|
||||||
|
available: true,
|
||||||
|
offset: 0,
|
||||||
|
nextOffset: 3,
|
||||||
|
data: 'YWJj',
|
||||||
|
eof: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, 4)).toMatchObject({
|
||||||
|
t: 'client-sources/response',
|
||||||
|
outcome: { ok: true, result: { data: 'YWJj', eof: true } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => parseSourceFrame({
|
||||||
|
v: 0,
|
||||||
|
t: 'client-sources/response',
|
||||||
|
sourceId: 'client-1',
|
||||||
|
generation: 'g-1',
|
||||||
|
sessionId: 'source-session-1',
|
||||||
|
requestId: 'source-request-1',
|
||||||
|
outcome: {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
op: 'get-content-chunk',
|
||||||
|
scriptKey: 'bundle',
|
||||||
|
content: 'source',
|
||||||
|
available: true,
|
||||||
|
offset: 0,
|
||||||
|
nextOffset: 3,
|
||||||
|
data: 'not base64',
|
||||||
|
eof: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, 4)).toThrow('chunk data')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/** Worker-side source buffer behavior. */
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { inspectorId } from '../src/shared/bridge/ids.ts'
|
||||||
|
import { InspectorSourceBuffer } from '../src/shared/bridge/buffer.ts'
|
||||||
|
|
||||||
|
const sourceId = inspectorId<'InspectorSourceId'>('source-buffer-test', 'sourceId')
|
||||||
|
const generation = inspectorId<'InspectorSourceGeneration'>('generation-buffer-test', 'generation')
|
||||||
|
|
||||||
|
function buffer(maxQueuedRecords = 2): InspectorSourceBuffer {
|
||||||
|
return new InspectorSourceBuffer({
|
||||||
|
topics: ['*'],
|
||||||
|
maxQueuedRecords,
|
||||||
|
maxQueuedBytes: 32_768,
|
||||||
|
maxRecordsPerFrame: 8,
|
||||||
|
maxFrameBytes: 32_768,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Inspector source buffer', () => {
|
||||||
|
it('absorbs pre-replacement queue loss exactly once', () => {
|
||||||
|
const records = buffer(1)
|
||||||
|
records.publish('test/event', { ordinal: 1 }, 1)
|
||||||
|
records.publish('test/event', { ordinal: 2 }, 2)
|
||||||
|
|
||||||
|
expect(records.replacement(sourceId, generation)).toMatchObject({
|
||||||
|
nextSequence: 2,
|
||||||
|
records: [],
|
||||||
|
})
|
||||||
|
expect(records.takeBatch(sourceId, generation)).toMatchObject({
|
||||||
|
firstSequence: 2,
|
||||||
|
droppedBefore: 0,
|
||||||
|
records: [{ topic: 'test/event', payload: { ordinal: 2 } }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates records before either carrier can enqueue them', () => {
|
||||||
|
const records = buffer()
|
||||||
|
|
||||||
|
expect(() => { records.publish('', {}, 1) }).toThrow('topic must contain 1 to 128 characters')
|
||||||
|
expect(() => { records.publish('test/event', {}, Number.NaN) }).toThrow('monotonicMs must be finite')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.client.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib/types",
|
||||||
|
"tsBuildInfoFile": "lib/tsconfig.client.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/client/bridge/controller.ts",
|
||||||
|
"src/client/bridge/dispatcher.ts",
|
||||||
|
"src/client/bridge/lifecycle.ts",
|
||||||
|
"src/client/bridge/publisher.ts",
|
||||||
|
"src/client/bridge/rpc.ts",
|
||||||
|
"src/client/bridge/transport.ts",
|
||||||
|
"src/client/cdp/console.ts",
|
||||||
|
"src/client/cdp/debugger.ts",
|
||||||
|
"src/client/cdp/errors.ts",
|
||||||
|
"src/client/cdp/heap-profiler.ts",
|
||||||
|
"src/client/cdp/index.ts",
|
||||||
|
"src/client/cdp/objects.ts",
|
||||||
|
"src/client/cdp/profiler.ts",
|
||||||
|
"src/client/cdp/properties.ts",
|
||||||
|
"src/client/cdp/runtime.ts",
|
||||||
|
"src/client/cdp/sources.ts",
|
||||||
|
"src/client/cdp/stack.ts",
|
||||||
|
"src/client/index.ts",
|
||||||
|
"src/client/inspection/cordis.ts",
|
||||||
|
"src/client/inspection/network.ts",
|
||||||
|
"src/client/inspection/realm.ts",
|
||||||
|
"src/client/plugin.ts",
|
||||||
|
"src/shared/bridge/buffer.ts",
|
||||||
|
"src/shared/bridge/codec.ts",
|
||||||
|
"src/shared/bridge/control-codec.ts",
|
||||||
|
"src/shared/bridge/ids.ts",
|
||||||
|
"src/shared/bridge/messages/control.ts",
|
||||||
|
"src/shared/bridge/messages/cordis.ts",
|
||||||
|
"src/shared/bridge/messages/network.ts",
|
||||||
|
"src/shared/bridge/messages/observation.ts",
|
||||||
|
"src/shared/bridge/messages/query/codec.ts",
|
||||||
|
"src/shared/bridge/messages/query/commands.ts",
|
||||||
|
"src/shared/bridge/messages/query/frames.ts",
|
||||||
|
"src/shared/bridge/messages/query/index.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/command-codec.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/commands.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/console-frames.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/frames.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/index.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/value-codec.ts",
|
||||||
|
"src/shared/bridge/messages/sources/codec.ts",
|
||||||
|
"src/shared/bridge/messages/sources/commands.ts",
|
||||||
|
"src/shared/bridge/messages/sources/frames.ts",
|
||||||
|
"src/shared/bridge/messages/sources/index.ts",
|
||||||
|
"src/shared/bridge/publisher.ts",
|
||||||
|
"src/shared/bridge/query-reader.ts",
|
||||||
|
"src/shared/bridge/rpc.ts",
|
||||||
|
"src/shared/bridge/validation.ts",
|
||||||
|
"src/shared/bridge/version.ts",
|
||||||
|
"src/shared/cdp/capabilities.ts",
|
||||||
|
"src/shared/cdp/console.ts",
|
||||||
|
"src/shared/cdp/debugger.ts",
|
||||||
|
"src/shared/cdp/errors.ts",
|
||||||
|
"src/shared/cdp/ids.ts",
|
||||||
|
"src/shared/cdp/index.ts",
|
||||||
|
"src/shared/cdp/operations.ts",
|
||||||
|
"src/shared/cdp/property.ts",
|
||||||
|
"src/shared/cdp/realm.ts",
|
||||||
|
"src/shared/cdp/remote-object.ts",
|
||||||
|
"src/shared/cdp/sources.ts",
|
||||||
|
"src/shared/cordis/collector.ts",
|
||||||
|
"src/shared/cordis/ids.ts",
|
||||||
|
"src/shared/cordis/model.ts",
|
||||||
|
"src/shared/cordis/object-reference.ts",
|
||||||
|
"src/shared/cordis/object-registry.ts",
|
||||||
|
"src/shared/cordis/observer.ts",
|
||||||
|
"src/shared/cordis/projector.ts",
|
||||||
|
"src/shared/cordis/reader.ts",
|
||||||
|
"src/shared/cordis/snapshot.ts",
|
||||||
|
"src/shared/identity.ts",
|
||||||
|
"src/shared/index.ts",
|
||||||
|
"src/shared/json.ts",
|
||||||
|
"src/shared/network/observation.ts",
|
||||||
|
"src/shared/service.ts",
|
||||||
|
"src/shared/validation.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../util/brand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../util/crypto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib/types",
|
||||||
|
"tsBuildInfoFile": "lib/tsconfig.host.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/host/bridge/controller.ts",
|
||||||
|
"src/host/bridge/dispatcher.ts",
|
||||||
|
"src/host/bridge/lifecycle.ts",
|
||||||
|
"src/host/bridge/publisher.ts",
|
||||||
|
"src/host/bridge/rpc.ts",
|
||||||
|
"src/host/bridge/transport.ts",
|
||||||
|
"src/host/cdp/console.ts",
|
||||||
|
"src/host/cdp/debugger.ts",
|
||||||
|
"src/host/cdp/errors.ts",
|
||||||
|
"src/host/cdp/heap-profiler.ts",
|
||||||
|
"src/host/cdp/index.ts",
|
||||||
|
"src/host/cdp/objects.ts",
|
||||||
|
"src/host/cdp/profiler.ts",
|
||||||
|
"src/host/cdp/properties.ts",
|
||||||
|
"src/host/cdp/runtime.ts",
|
||||||
|
"src/host/cdp/sources.ts",
|
||||||
|
"src/host/cdp/stack.ts",
|
||||||
|
"src/host/index.ts",
|
||||||
|
"src/host/inspection/cordis.ts",
|
||||||
|
"src/host/inspection/network.ts",
|
||||||
|
"src/host/inspection/realm.ts",
|
||||||
|
"src/host/plugin.ts",
|
||||||
|
"src/index.ts",
|
||||||
|
"src/invariant.ts",
|
||||||
|
"src/shared/bridge/buffer.ts",
|
||||||
|
"src/shared/bridge/codec.ts",
|
||||||
|
"src/shared/bridge/control-codec.ts",
|
||||||
|
"src/shared/bridge/ids.ts",
|
||||||
|
"src/shared/bridge/messages/control.ts",
|
||||||
|
"src/shared/bridge/messages/cordis.ts",
|
||||||
|
"src/shared/bridge/messages/network.ts",
|
||||||
|
"src/shared/bridge/messages/observation.ts",
|
||||||
|
"src/shared/bridge/messages/query/codec.ts",
|
||||||
|
"src/shared/bridge/messages/query/commands.ts",
|
||||||
|
"src/shared/bridge/messages/query/frames.ts",
|
||||||
|
"src/shared/bridge/messages/query/index.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/command-codec.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/commands.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/console-frames.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/frames.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/index.ts",
|
||||||
|
"src/shared/bridge/messages/runtime/value-codec.ts",
|
||||||
|
"src/shared/bridge/messages/sources/codec.ts",
|
||||||
|
"src/shared/bridge/messages/sources/commands.ts",
|
||||||
|
"src/shared/bridge/messages/sources/frames.ts",
|
||||||
|
"src/shared/bridge/messages/sources/index.ts",
|
||||||
|
"src/shared/bridge/publisher.ts",
|
||||||
|
"src/shared/bridge/query-reader.ts",
|
||||||
|
"src/shared/bridge/rpc.ts",
|
||||||
|
"src/shared/bridge/validation.ts",
|
||||||
|
"src/shared/bridge/version.ts",
|
||||||
|
"src/shared/cdp/capabilities.ts",
|
||||||
|
"src/shared/cdp/console.ts",
|
||||||
|
"src/shared/cdp/debugger.ts",
|
||||||
|
"src/shared/cdp/errors.ts",
|
||||||
|
"src/shared/cdp/ids.ts",
|
||||||
|
"src/shared/cdp/index.ts",
|
||||||
|
"src/shared/cdp/operations.ts",
|
||||||
|
"src/shared/cdp/property.ts",
|
||||||
|
"src/shared/cdp/realm.ts",
|
||||||
|
"src/shared/cdp/remote-object.ts",
|
||||||
|
"src/shared/cdp/sources.ts",
|
||||||
|
"src/shared/cordis/collector.ts",
|
||||||
|
"src/shared/cordis/ids.ts",
|
||||||
|
"src/shared/cordis/model.ts",
|
||||||
|
"src/shared/cordis/object-reference.ts",
|
||||||
|
"src/shared/cordis/object-registry.ts",
|
||||||
|
"src/shared/cordis/observer.ts",
|
||||||
|
"src/shared/cordis/projector.ts",
|
||||||
|
"src/shared/cordis/reader.ts",
|
||||||
|
"src/shared/cordis/snapshot.ts",
|
||||||
|
"src/shared/identity.ts",
|
||||||
|
"src/shared/index.ts",
|
||||||
|
"src/shared/json.ts",
|
||||||
|
"src/shared/network/observation.ts",
|
||||||
|
"src/shared/service.ts",
|
||||||
|
"src/shared/validation.ts",
|
||||||
|
"src/worker/bridge/endpoint.ts",
|
||||||
|
"src/worker/bridge/hub.ts",
|
||||||
|
"src/worker/bridge/runtime-rpc.ts",
|
||||||
|
"src/worker/bridge/session.ts",
|
||||||
|
"src/worker/bridge/source-rpc.ts",
|
||||||
|
"src/worker/cdp/domains/debugger/cdp-params.ts",
|
||||||
|
"src/worker/cdp/domains/debugger/index.ts",
|
||||||
|
"src/worker/cdp/domains/debugger/projector.ts",
|
||||||
|
"src/worker/cdp/domains/debugger/script-registry.ts",
|
||||||
|
"src/worker/cdp/domains/debugger/session.ts",
|
||||||
|
"src/worker/cdp/domains/dom/index.ts",
|
||||||
|
"src/worker/cdp/domains/dom/model.ts",
|
||||||
|
"src/worker/cdp/domains/dom/session.ts",
|
||||||
|
"src/worker/cdp/domains/native.ts",
|
||||||
|
"src/worker/cdp/domains/network/session.ts",
|
||||||
|
"src/worker/cdp/domains/runtime/cdp-params.ts",
|
||||||
|
"src/worker/cdp/domains/runtime/index.ts",
|
||||||
|
"src/worker/cdp/domains/runtime/object-table.ts",
|
||||||
|
"src/worker/cdp/domains/runtime/session.ts",
|
||||||
|
"src/worker/cdp/ids.ts",
|
||||||
|
"src/worker/cdp/protocol.ts",
|
||||||
|
"src/worker/cdp/realm-sessions.ts",
|
||||||
|
"src/worker/cdp/session.ts",
|
||||||
|
"src/worker/cdp/target.ts",
|
||||||
|
"src/worker/entry.ts",
|
||||||
|
"src/worker/inspection/cordis-query.ts",
|
||||||
|
"src/worker/inspection/cordis-store.ts",
|
||||||
|
"src/worker/inspection/network-store.ts",
|
||||||
|
"src/worker/inspection/query-router.ts",
|
||||||
|
"src/worker/inspection/realm-store.ts",
|
||||||
|
"src/worker/inspection/realm.ts",
|
||||||
|
"src/worker/realms/client/bridge.ts",
|
||||||
|
"src/worker/realms/client/console.ts",
|
||||||
|
"src/worker/realms/client/debugger.ts",
|
||||||
|
"src/worker/realms/client/index.ts",
|
||||||
|
"src/worker/realms/client/runtime.ts",
|
||||||
|
"src/worker/realms/client/scripts.ts",
|
||||||
|
"src/worker/realms/client/sources.ts",
|
||||||
|
"src/worker/realms/client/values.ts",
|
||||||
|
"src/worker/realms/host/bridge.ts",
|
||||||
|
"src/worker/realms/host/console.ts",
|
||||||
|
"src/worker/realms/host/debugger.ts",
|
||||||
|
"src/worker/realms/host/index.ts",
|
||||||
|
"src/worker/realms/host/runtime.ts",
|
||||||
|
"src/worker/realms/host/scripts.ts",
|
||||||
|
"src/worker/realms/host/sources.ts",
|
||||||
|
"src/worker/realms/host/values.ts",
|
||||||
|
"src/worker/server.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../util/brand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/schemastery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../host/webserver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../runtime-diagnostics/invariants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../util/crypto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.host.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.client.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { UserConfig } from 'tsdown'
|
||||||
|
import { clientBundle } from '../../client/tsdown.client.ts'
|
||||||
|
|
||||||
|
const worker: UserConfig = {
|
||||||
|
entry: { worker: 'lib/types/worker/entry.js' },
|
||||||
|
outDir: 'lib',
|
||||||
|
format: ['esm'],
|
||||||
|
platform: 'node',
|
||||||
|
target: 'es2024',
|
||||||
|
fixedExtension: false,
|
||||||
|
dts: false,
|
||||||
|
clean: false,
|
||||||
|
outputOptions: { inlineDynamicImports: true },
|
||||||
|
deps: { neverBundle: specifier => specifier === 'ws' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the Host plugin and Worker during the Host pass, and the dynamic Client plugin during the Client pass. */
|
||||||
|
export default clientBundle(
|
||||||
|
'@deepseek-ai/dsh-experimental-inspector',
|
||||||
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
|
{ hostPhase: true, companions: [worker] },
|
||||||
|
)
|
||||||
Generated
+43
@@ -4794,6 +4794,49 @@ importers:
|
|||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
packages/experimental/inspector:
|
||||||
|
dependencies:
|
||||||
|
'@deepseek-ai/dsh-brand':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../util/brand
|
||||||
|
'@deepseek-ai/dsh-client-modules':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../client/modules
|
||||||
|
'@deepseek-ai/dsh-util-crypto':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../util/crypto
|
||||||
|
'@deepseek-ai/schemastery':
|
||||||
|
specifier: link:../../../vendor/schemastery
|
||||||
|
version: link:../../../vendor/schemastery
|
||||||
|
ws:
|
||||||
|
specifier: ^8.21.0
|
||||||
|
version: 8.21.0
|
||||||
|
devDependencies:
|
||||||
|
'@deepseek-ai/cordis':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/cordis
|
||||||
|
'@deepseek-ai/cordis-plugin-include':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/include
|
||||||
|
'@deepseek-ai/cordis-plugin-loader':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/loader
|
||||||
|
'@deepseek-ai/dsh-host-webserver':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../host/webserver
|
||||||
|
'@deepseek-ai/dsh-invariants':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../runtime-diagnostics/invariants
|
||||||
|
'@types/ws':
|
||||||
|
specifier: ^8.18.1
|
||||||
|
version: 8.18.1
|
||||||
|
playwright:
|
||||||
|
specifier: ^1.49.0
|
||||||
|
version: 1.61.1
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.19.2
|
||||||
|
version: 4.22.4
|
||||||
|
|
||||||
packages/experimental/tool-agent-team:
|
packages/experimental/tool-agent-team:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@deepseek-ai/schemastery':
|
'@deepseek-ai/schemastery':
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
"@deepseek-ai/dsh-experimental-tool-agent-team/invariant": ["./packages/experimental/tool-agent-team/src/invariant.ts"],
|
"@deepseek-ai/dsh-experimental-tool-agent-team/invariant": ["./packages/experimental/tool-agent-team/src/invariant.ts"],
|
||||||
"@deepseek-ai/dsh-experimental-webworker-runtime/invariant": ["./packages/experimental/webworker-runtime/src/invariant.ts"],
|
"@deepseek-ai/dsh-experimental-webworker-runtime/invariant": ["./packages/experimental/webworker-runtime/src/invariant.ts"],
|
||||||
"@deepseek-ai/dsh-experimental-webworker-packer/invariant": ["./packages/experimental/webworker-packer/src/invariant.ts"],
|
"@deepseek-ai/dsh-experimental-webworker-packer/invariant": ["./packages/experimental/webworker-packer/src/invariant.ts"],
|
||||||
|
"@deepseek-ai/dsh-experimental-inspector/invariant": ["./packages/experimental/inspector/src/invariant.ts"],
|
||||||
"@deepseek-ai/dsh-util-crypto/invariant": ["./packages/util/crypto/src/invariant.ts"],
|
"@deepseek-ai/dsh-util-crypto/invariant": ["./packages/util/crypto/src/invariant.ts"],
|
||||||
"@deepseek-ai/dsh-*/invariant": [
|
"@deepseek-ai/dsh-*/invariant": [
|
||||||
"./packages/core/*/src/invariant.ts",
|
"./packages/core/*/src/invariant.ts",
|
||||||
@@ -271,6 +272,8 @@
|
|||||||
"@deepseek-ai/dsh-experimental-tool-agent-team": ["./packages/experimental/tool-agent-team/src"],
|
"@deepseek-ai/dsh-experimental-tool-agent-team": ["./packages/experimental/tool-agent-team/src"],
|
||||||
"@deepseek-ai/dsh-experimental-webworker-runtime": ["./packages/experimental/webworker-runtime/src"],
|
"@deepseek-ai/dsh-experimental-webworker-runtime": ["./packages/experimental/webworker-runtime/src"],
|
||||||
"@deepseek-ai/dsh-experimental-webworker-packer": ["./packages/experimental/webworker-packer/src"],
|
"@deepseek-ai/dsh-experimental-webworker-packer": ["./packages/experimental/webworker-packer/src"],
|
||||||
|
"@deepseek-ai/dsh-experimental-inspector": ["./packages/experimental/inspector/src"],
|
||||||
|
"@deepseek-ai/dsh-experimental-inspector/client": ["./packages/experimental/inspector/src/client/index.ts"],
|
||||||
"@deepseek-ai/dsh-util-crypto": ["./packages/util/crypto/src"],
|
"@deepseek-ai/dsh-util-crypto": ["./packages/util/crypto/src"],
|
||||||
"@deepseek-ai/dsh-*": [
|
"@deepseek-ai/dsh-*": [
|
||||||
"./packages/core/*/src",
|
"./packages/core/*/src",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
{ "path": "./packages/client/ui-attachment" },
|
{ "path": "./packages/client/ui-attachment" },
|
||||||
{ "path": "./packages/client/ui-primitives" },
|
{ "path": "./packages/client/ui-primitives" },
|
||||||
{ "path": "./packages/client/modules" },
|
{ "path": "./packages/client/modules" },
|
||||||
|
{ "path": "./packages/experimental/inspector/tsconfig.client.json" },
|
||||||
{ "path": "./packages/client/hmr" },
|
{ "path": "./packages/client/hmr" },
|
||||||
{ "path": "./packages/client/connection/tsconfig.client.json" },
|
{ "path": "./packages/client/connection/tsconfig.client.json" },
|
||||||
{ "path": "./packages/typert/registry" },
|
{ "path": "./packages/typert/registry" },
|
||||||
|
|||||||
@@ -276,6 +276,7 @@
|
|||||||
{ "path": "./packages/test-support/loader-smoke" },
|
{ "path": "./packages/test-support/loader-smoke" },
|
||||||
{ "path": "./packages/test-support/llm-mock-server" },
|
{ "path": "./packages/test-support/llm-mock-server" },
|
||||||
{ "path": "./packages/experimental/webworker-packer" },
|
{ "path": "./packages/experimental/webworker-packer" },
|
||||||
|
{ "path": "./packages/experimental/inspector/tsconfig.host.json" },
|
||||||
{ "path": "./packages/subagent/subagent" },
|
{ "path": "./packages/subagent/subagent" },
|
||||||
{ "path": "./packages/subagent/tool-subagent" },
|
{ "path": "./packages/subagent/tool-subagent" },
|
||||||
{ "path": "./packages/subagent/tool-subagent-control" },
|
{ "path": "./packages/subagent/tool-subagent-control" },
|
||||||
|
|||||||
@@ -251,6 +251,12 @@ export default defineConfig({
|
|||||||
// coverage lane exists.
|
// coverage lane exists.
|
||||||
'packages/experimental/webworker-runtime/src/**',
|
'packages/experimental/webworker-runtime/src/**',
|
||||||
'packages/experimental/webworker-packer/src/*',
|
'packages/experimental/webworker-packer/src/*',
|
||||||
|
// Inspector behavior spans a Node Worker, the Host isolate, and a real
|
||||||
|
// browser realm. Its focused specs cover pure logic, while its Worker,
|
||||||
|
// Debugger, Chromium, and Loader suites run the assembled paths that
|
||||||
|
// the parent Vitest process cannot attribute. TODO(inspector): remove
|
||||||
|
// when the coverage lane can merge cross-realm V8 coverage.
|
||||||
|
'packages/experimental/inspector/src/**',
|
||||||
'packages/client/modules/src/client/system.ts',
|
'packages/client/modules/src/client/system.ts',
|
||||||
'packages/client/hmr/src/client/index.ts',
|
'packages/client/hmr/src/client/index.ts',
|
||||||
// Web config-tree boot round: the new host-side web-transport halves
|
// Web config-tree boot round: the new host-side web-transport halves
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export default defineConfig({
|
|||||||
// apps/cli only, not apps/*: apps/web/tests/*.e2e.ts needs the built
|
// apps/cli only, not apps/*: apps/web/tests/*.e2e.ts needs the built
|
||||||
// frontend dist and runs under vitest.web.config.ts (the test:web job).
|
// frontend dist and runs under vitest.web.config.ts (the test:web job).
|
||||||
include: ['packages/*/*/tests/**/*.e2e.ts', 'apps/cli/tests/**/*.e2e.ts'],
|
include: ['packages/*/*/tests/**/*.e2e.ts', 'apps/cli/tests/**/*.e2e.ts'],
|
||||||
exclude: ['**/*.expected.e2e.ts'],
|
exclude: [
|
||||||
|
'**/*.expected.e2e.ts',
|
||||||
|
'packages/experimental/inspector/tests/client-browser.e2e.ts',
|
||||||
|
],
|
||||||
// Real model calls: generous timeouts, and retries for transient flakes
|
// Real model calls: generous timeouts, and retries for transient flakes
|
||||||
// (the shared internal key hits concurrency quotas). No coverage — the
|
// (the shared internal key hits concurrency quotas). No coverage — the
|
||||||
// unit suites own the coverage gate.
|
// unit suites own the coverage gate.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default defineConfig({
|
|||||||
include: [
|
include: [
|
||||||
'apps/web/tests/**/*.e2e.ts',
|
'apps/web/tests/**/*.e2e.ts',
|
||||||
'apps/web/tests/**/*.snapshot.ts',
|
'apps/web/tests/**/*.snapshot.ts',
|
||||||
|
'packages/experimental/inspector/tests/client-browser.e2e.ts',
|
||||||
],
|
],
|
||||||
// Local and record runs stay serial. CI runs workspace-mutating HMR and
|
// Local and record runs stay serial. CI runs workspace-mutating HMR and
|
||||||
// dynamic Cordis lifecycle coverage before parallelizing the remaining files.
|
// dynamic Cordis lifecycle coverage before parallelizing the remaining files.
|
||||||
|
|||||||
Reference in New Issue
Block a user