output-json.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /** JSON string-prefix accounting for the outer-output ledger. @module @deepseek-ai/dsh-code-runtime-worker-thread/output-json */
  2. import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
  3. type IntrinsicCallable = (this: unknown, ...args: unknown[]) => unknown
  4. const intrinsicReflectApply = Reflect.apply as (
  5. target: IntrinsicCallable,
  6. thisArgument: unknown,
  7. argumentsList: readonly unknown[],
  8. ) => unknown
  9. const intrinsicArrayIsArray = Array.isArray
  10. const IntrinsicBuffer = Buffer
  11. const intrinsicBufferByteLength = Reflect.get(Buffer, 'byteLength') as IntrinsicCallable
  12. const intrinsicObjectCreate = Object.create
  13. const intrinsicObjectDefineProperty = Object.defineProperty
  14. const intrinsicObjectKeys = Object.keys
  15. const intrinsicString = String
  16. const intrinsicStringCharCodeAt = Reflect.get(String.prototype, 'charCodeAt') as IntrinsicCallable
  17. const intrinsicStringCodePointAt = Reflect.get(String.prototype, 'codePointAt') as IntrinsicCallable
  18. const intrinsicStringSlice = Reflect.get(String.prototype, 'slice') as IntrinsicCallable
  19. /** Build a data descriptor that cannot inherit model-defined accessor fields. */
  20. function dataDescriptor(value: unknown): PropertyDescriptor {
  21. const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor
  22. descriptor.value = value
  23. return descriptor
  24. }
  25. /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
  26. function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void {
  27. const descriptor = dataDescriptor(value)
  28. descriptor.enumerable = true
  29. descriptor.configurable = true
  30. descriptor.writable = true
  31. intrinsicObjectDefineProperty(target, key, descriptor)
  32. }
  33. /** UTF-8 byte length through the module-captured Node intrinsic. */
  34. function byteLength(text: string): number {
  35. return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, 'utf8']) as number
  36. }
  37. /** Append without consulting a model-mutated `Array.prototype`. */
  38. function append<T>(target: T[], value: T): void {
  39. defineEnumerableDataProperty(target, target.length, value)
  40. }
  41. /** Pop without consulting a model-mutated `Array.prototype`. */
  42. function takeLast<T>(target: T[]): T | undefined {
  43. if (target.length === 0) return undefined
  44. const index = target.length - 1
  45. const value = target[index]
  46. intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index))
  47. return value
  48. }
  49. /** One code-point-aligned character from a string. */
  50. function characterAt(text: string, index: number): string {
  51. const codePoint = intrinsicReflectApply(intrinsicStringCodePointAt, text, [index]) as number
  52. const width = codePoint > 0xffff ? 2 : 1
  53. return intrinsicReflectApply(intrinsicStringSlice, text, [index, index + width]) as string
  54. }
  55. /** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
  56. function serializedCharacterBytes(character: string): number {
  57. if (character.length === 2) return 4
  58. if (character === '"' || character === '\\') return 2
  59. const code = intrinsicReflectApply(intrinsicStringCharCodeAt, character, [0]) as number
  60. if (code >= 0xd800 && code <= 0xdfff) return 6
  61. if (code < 0x20) return code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6
  62. return byteLength(character)
  63. }
  64. /**
  65. * Measure one JSON string without materializing its complete escaped form.
  66. * @param text - the candidate string.
  67. * @param maxBytes - largest serialized size the caller can admit.
  68. * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
  69. */
  70. export function jsonStringBytesUpTo(text: string, maxBytes: number): number | undefined {
  71. if (maxBytes < 2) return undefined
  72. let bytes = 2
  73. for (let index = 0; index < text.length;) {
  74. const character = characterAt(text, index)
  75. bytes += serializedCharacterBytes(character)
  76. if (bytes > maxBytes) return undefined
  77. index += character.length
  78. }
  79. return bytes
  80. }
  81. /**
  82. * Measure one lossless JSON value without allocating its serialized form.
  83. * @param value - already validated lossless JSON.
  84. * @param maxBytes - largest serialized size the caller can admit.
  85. * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
  86. */
  87. export function jsonValueBytesUpTo(value: CodeJsonValue, maxBytes: number): number | undefined {
  88. type Task =
  89. | { kind: 'value'; value: CodeJsonValue }
  90. | { kind: 'array'; value: CodeJsonValue[]; index: number }
  91. | { kind: 'object'; value: Record<string, CodeJsonValue>; keys: string[]; index: number }
  92. let bytes = 0
  93. const add = (cost: number): boolean => {
  94. bytes += cost
  95. return bytes <= maxBytes
  96. }
  97. const tasks: Task[] = [{ kind: 'value', value }]
  98. for (let task = takeLast(tasks); task !== undefined; task = takeLast(tasks)) {
  99. if (task.kind === 'value') {
  100. const current = task.value
  101. if (current === null) {
  102. if (!add(4)) return undefined
  103. } else if (typeof current === 'string') {
  104. const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes)
  105. if (stringBytes === undefined) return undefined
  106. bytes += stringBytes
  107. } else if (typeof current === 'number') {
  108. if (!add(byteLength(intrinsicString(current)))) return undefined
  109. } else if (typeof current === 'boolean') {
  110. if (!add(current ? 4 : 5)) return undefined
  111. } else if (intrinsicArrayIsArray(current)) {
  112. if (!add(2)) return undefined
  113. if (current.length > 0) append(tasks, { kind: 'array', value: current, index: 0 })
  114. } else {
  115. if (!add(2)) return undefined
  116. const keys = intrinsicObjectKeys(current)
  117. if (keys.length > 0) append(tasks, { kind: 'object', value: current, keys, index: 0 })
  118. }
  119. continue
  120. }
  121. if (task.index > 0 && !add(1)) return undefined
  122. if (task.kind === 'array') {
  123. const item = task.value[task.index]
  124. if (item === undefined) return undefined
  125. if (task.index + 1 < task.value.length) append(tasks, { ...task, index: task.index + 1 })
  126. append(tasks, { kind: 'value', value: item })
  127. continue
  128. }
  129. const key = task.keys[task.index]
  130. /* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
  131. if (key === undefined) return undefined
  132. const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes)
  133. if (keyBytes === undefined) return undefined
  134. if (!add(keyBytes + 1)) return undefined
  135. const item = task.value[key]
  136. if (item === undefined) return undefined
  137. if (task.index + 1 < task.keys.length) append(tasks, { ...task, index: task.index + 1 })
  138. append(tasks, { kind: 'value', value: item })
  139. }
  140. return bytes
  141. }
  142. /**
  143. * Return the longest code-point-aligned prefix whose JSON string encoding,
  144. * including its surrounding quotes, fits `maxBytes`.
  145. *
  146. * @param text - the candidate string.
  147. * @param maxBytes - serialized JSON-string bytes available.
  148. * @returns the fitting prefix, or an empty string when even useful content cannot fit.
  149. */
  150. export function truncateJsonStringBytes(text: string, maxBytes: number): string {
  151. if (maxBytes < 2) return ''
  152. let bytes = 2
  153. let end = 0
  154. for (let index = 0; index < text.length;) {
  155. const character = characterAt(text, index)
  156. const cost = serializedCharacterBytes(character)
  157. if (bytes + cost > maxBytes) break
  158. bytes += cost
  159. end += character.length
  160. index += character.length
  161. }
  162. return end === text.length ? text : intrinsicReflectApply(intrinsicStringSlice, text, [0, end]) as string
  163. }