client-source.client.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /** Client-face process fixture used by Host-side protocol integration tests. */
  2. import { parentPort, workerData } from 'node:worker_threads'
  3. import { Context, type Fiber } from '@deepseek-ai/cordis'
  4. import WebSocket from 'ws'
  5. import { ClientInspectorSource } from '../../src/client/bridge/transport.ts'
  6. import { ClientSourceCatalog } from '../../src/client/cdp/sources.ts'
  7. import { publishCordisTree } from '../../src/client/inspection/cordis.ts'
  8. import { inspectorId } from '../../src/shared/bridge/ids.ts'
  9. import type { InspectorClientBootstrap } from '../../src/shared/bridge/messages/control.ts'
  10. import type { InspectorJsonValue } from '../../src/shared/json.ts'
  11. import { createInspectorService } from '../../src/shared/service.ts'
  12. interface ClientFixtureInput {
  13. readonly bootstrap: InspectorClientBootstrap
  14. readonly label: string
  15. readonly sourceCatalog?: {
  16. readonly sourceText: string
  17. readonly sourceMap: string
  18. readonly sourceUrl: string
  19. readonly sourceMapUrl: string
  20. }
  21. }
  22. interface ClientFixtureRequest {
  23. readonly id: number
  24. readonly op:
  25. | 'add-fiber'
  26. | 'close'
  27. | 'disconnect'
  28. | 'get-tree'
  29. | 'log-cordis'
  30. | 'log-value'
  31. | 'publish'
  32. | 'refresh-tree'
  33. | 'remove-fiber'
  34. | 'set-global'
  35. | 'set-ingest-paused'
  36. readonly paused?: boolean
  37. readonly name?: string
  38. readonly value?: InspectorJsonValue
  39. readonly marker?: string
  40. readonly topic?: string
  41. }
  42. const port = parentPort
  43. if (port === null) throw new Error('Inspector Client fixture requires a Worker parent port')
  44. const input = workerData as ClientFixtureInput
  45. globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket
  46. console.log = () => {}
  47. const context = new Context()
  48. const childFiber = context.plugin({ name: 'client-child', apply() {} })
  49. await childFiber.await()
  50. Reflect.set(globalThis, '__cordisClientProbe', context)
  51. Reflect.set(globalThis, '__cordisClientFiberProbe', childFiber)
  52. const sourceCatalog = input.sourceCatalog === undefined
  53. ? undefined
  54. : new ClientSourceCatalog([{
  55. scriptKey: inspectorId<'RuntimeScriptKey'>('bundle', 'scriptKey'),
  56. url: input.sourceCatalog.sourceUrl,
  57. hash: 'test',
  58. sourceMapUrl: input.sourceCatalog.sourceMapUrl,
  59. isModule: false,
  60. loadSource: async () => input.sourceCatalog!.sourceText,
  61. loadSourceMap: async () => input.sourceCatalog!.sourceMap,
  62. }])
  63. const source = new ClientInspectorSource(input.bootstrap, input.label, sourceCatalog)
  64. const disposeCordis = publishCordisTree(context, source, {
  65. maxNodes: input.bootstrap.maxCordisNodes,
  66. maxBytes: input.bootstrap.maxFrameBytes - 4_096,
  67. })
  68. const service = createInspectorService(source)
  69. let addedFiber: Fiber | undefined
  70. port.on('message', (message: ClientFixtureRequest) => {
  71. void dispatch(message).then(
  72. (value) => {
  73. port.postMessage({ type: 'response', id: message.id, ok: true, value })
  74. if (message.op === 'close') port.close()
  75. },
  76. (error: unknown) => {
  77. port.postMessage({
  78. type: 'response',
  79. id: message.id,
  80. ok: false,
  81. error: error instanceof Error ? error.message : String(error),
  82. })
  83. },
  84. )
  85. })
  86. port.postMessage({ type: 'ready', fiberUid: childFiber.uid })
  87. async function dispatch(message: ClientFixtureRequest): Promise<unknown> {
  88. switch (message.op) {
  89. case 'publish':
  90. source.publish(requiredString(message.topic, 'topic'), message.value ?? null)
  91. return undefined
  92. case 'set-global':
  93. Reflect.set(globalThis, requiredString(message.name, 'name'), message.value)
  94. return undefined
  95. case 'log-value':
  96. console.log(message.value, requiredString(message.marker, 'marker'))
  97. return undefined
  98. case 'log-cordis':
  99. console.log(context, childFiber, requiredString(message.marker, 'marker'))
  100. return undefined
  101. case 'get-tree':
  102. return await service.cordis.getTree()
  103. case 'set-ingest-paused': {
  104. const socket = Reflect.get(source, 'socket') as WebSocket | undefined
  105. if (socket === undefined) throw new Error('Inspector Client ingest socket is unavailable')
  106. if (message.paused) socket.pause()
  107. else socket.resume()
  108. return undefined
  109. }
  110. case 'disconnect': {
  111. const socket = Reflect.get(source, 'socket') as WebSocket | undefined
  112. socket?.terminate()
  113. return undefined
  114. }
  115. case 'refresh-tree':
  116. context.emit('internal/status', childFiber.ctx.fiber, childFiber.ctx.fiber.state)
  117. return undefined
  118. case 'add-fiber':
  119. addedFiber = context.plugin({ name: 'dynamic-client-child', apply() {} }).ctx.fiber
  120. await addedFiber.await()
  121. return addedFiber.uid
  122. case 'remove-fiber':
  123. await addedFiber?.dispose()
  124. addedFiber = undefined
  125. return undefined
  126. case 'close':
  127. await addedFiber?.dispose()
  128. disposeCordis()
  129. source.close()
  130. await context.fiber.dispose()
  131. return undefined
  132. }
  133. }
  134. function requiredString(value: string | undefined, field: string): string {
  135. if (value === undefined) throw new Error(`Inspector Client fixture ${field} is required`)
  136. return value
  137. }