@huaqiu/dsh-tool-schematic-gen 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +8 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +106004 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +35 -0
- package/lib/index.mjs +1439 -0
- package/package.json +58 -0
- package/src/client/ecad.ts +147 -0
- package/src/client/hit-card.tsx +378 -0
- package/src/client/i18n.ts +148 -0
- package/src/client/index.ts +176 -0
- package/src/client/login-url.ts +91 -0
- package/src/client/parse.ts +153 -0
- package/src/client/progress.ts +161 -0
- package/src/client/stack-frame.tsx +269 -0
- package/src/client/theme.ts +228 -0
- package/src/client/trace-names.ts +308 -0
- package/src/client/trace-tree.ts +185 -0
- package/src/config.ts +203 -0
- package/src/index.ts +130 -0
- package/src/progress.ts +333 -0
- package/src/routes.ts +80 -0
- package/src/sse.ts +448 -0
- package/src/tools.ts +590 -0
- package/src/trace.ts +336 -0
package/src/sse.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-tool-schematic-gen` — CopilotKit SSE consumption and the
|
|
3
|
+
* export-zip POST (dependency-free; `fetch` is injected).
|
|
4
|
+
*
|
|
5
|
+
* @module @huaqiu/dsh-tool-schematic-gen
|
|
6
|
+
*/
|
|
7
|
+
import type { EdaAccount, SchematicGenConfig } from './config.js'
|
|
8
|
+
import { buildExportHeaders } from './config.js'
|
|
9
|
+
import {
|
|
10
|
+
collectTraceEvents, isTraceEventName, ToolCallTracker,
|
|
11
|
+
type TraceEvent,
|
|
12
|
+
} from './trace.js'
|
|
13
|
+
import type { ProgressNote, TodoItem } from './progress.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Overall SSE / zip budget — the eda.cn design agents routinely run 9–12+
|
|
17
|
+
* minutes per generation (observed runs exceeded the old 10-min cap), so
|
|
18
|
+
* allow 30 minutes before aborting the stream. This single constant drives
|
|
19
|
+
* both the backend SSE stream timeout and the agent-facing tool timeout hint
|
|
20
|
+
* (`TOOL_TIMEOUT_MS` in tools.ts references it).
|
|
21
|
+
*/
|
|
22
|
+
export const HTTP_TIMEOUT_MS = 1_800_000
|
|
23
|
+
|
|
24
|
+
// ── STATE_DELTA application ──────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Apply one `STATE_DELTA` op set to the accumulating state. Only **top-level**
|
|
28
|
+
* patches (`/key`) are applied; nested paths are ignored because the final
|
|
29
|
+
* `STATE_SNAPSHOT` is authoritative for the deliverable fields.
|
|
30
|
+
*/
|
|
31
|
+
export function applyDelta(delta: unknown, state: Record<string, unknown>): void {
|
|
32
|
+
if (!Array.isArray(delta)) return
|
|
33
|
+
for (const op of delta) {
|
|
34
|
+
if (!op || typeof op !== 'object') continue
|
|
35
|
+
const record = op as { op?: unknown; path?: unknown; value?: unknown }
|
|
36
|
+
const path = typeof record.path === 'string' ? record.path : ''
|
|
37
|
+
const m = /^\/([^/]+)$/.exec(path)
|
|
38
|
+
if (!m) continue
|
|
39
|
+
const key = m[1]!
|
|
40
|
+
if (record.op === 'remove') delete state[key]
|
|
41
|
+
else if (record.op === 'add' || record.op === 'replace') state[key] = record.value
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Custom-event name the `modular_circuit` (system design) agent uses for
|
|
47
|
+
* everything that is NOT a tool call: a rolling todo list and human-readable
|
|
48
|
+
* stage announcements (`packages/agents/system-design/src/utils/progress.ts`).
|
|
49
|
+
*/
|
|
50
|
+
export const SYSTEM_DESIGN_EVENT_NAME = 'SYSTEM_DESIGN_EVENT'
|
|
51
|
+
|
|
52
|
+
/** Read a string field that may be camelCase (AG-UI) or snake_case (older builds). */
|
|
53
|
+
function strField(rec: Record<string, unknown>, ...keys: string[]): string {
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
const v = rec[key]
|
|
56
|
+
if (typeof v === 'string' && v.length > 0) return v
|
|
57
|
+
}
|
|
58
|
+
return ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parse one `todo_progress` entry; anything else is dropped. */
|
|
62
|
+
function parseTodo(value: unknown): TodoItem | null {
|
|
63
|
+
if (!value || typeof value !== 'object') return null
|
|
64
|
+
const rec = value as Record<string, unknown>
|
|
65
|
+
const content = typeof rec['content'] === 'string' ? rec['content'] : ''
|
|
66
|
+
if (content.length === 0) return null
|
|
67
|
+
const status = rec['status']
|
|
68
|
+
return {
|
|
69
|
+
content,
|
|
70
|
+
status: status === 'completed' ? 'completed' : status === 'in_progress' ? 'in_progress' : 'pending',
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parse a design-stage announcement (`kind: "progress"`). */
|
|
75
|
+
function parseNote(value: Record<string, unknown>): ProgressNote | null {
|
|
76
|
+
const message = typeof value['message'] === 'string' ? value['message'].trim() : ''
|
|
77
|
+
if (message.length === 0) return null
|
|
78
|
+
const phase = value['phase']
|
|
79
|
+
const stage = typeof value['stage'] === 'string' ? value['stage'] : ''
|
|
80
|
+
return {
|
|
81
|
+
phase: phase === 'complete' ? 'complete' : phase === 'error' ? 'error' : 'start',
|
|
82
|
+
stage,
|
|
83
|
+
message,
|
|
84
|
+
ts: typeof value['ts'] === 'number' && Number.isFinite(value['ts']) ? value['ts'] : Date.now(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface HandleEventResult {
|
|
89
|
+
text?: string
|
|
90
|
+
finished?: boolean
|
|
91
|
+
error?: string
|
|
92
|
+
/** Execution-trace events decoded from an AG-UI `CUSTOM` frame. */
|
|
93
|
+
trace?: TraceEvent[]
|
|
94
|
+
/** Set when `state` was mutated, so the caller can resample progress. */
|
|
95
|
+
stateChanged?: boolean
|
|
96
|
+
/** Rolling todo list from the system-design agent. */
|
|
97
|
+
todos?: TodoItem[]
|
|
98
|
+
/** Human-readable stage announcement from the system-design agent. */
|
|
99
|
+
note?: ProgressNote
|
|
100
|
+
/** `RUN_STARTED` — reset any per-run bookkeeping. */
|
|
101
|
+
runStarted?: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handle one decoded SSE event, mutating `state` and returning any text /
|
|
106
|
+
* lifecycle signals for the caller to aggregate.
|
|
107
|
+
*
|
|
108
|
+
* `tracker` is required only for agents that report progress through the
|
|
109
|
+
* standard AG-UI tool-call lifecycle instead of `CUSTOM` trace events —
|
|
110
|
+
* that is, `modular_circuit` (system design).
|
|
111
|
+
*/
|
|
112
|
+
export function handleEvent(
|
|
113
|
+
evt: unknown,
|
|
114
|
+
state: Record<string, unknown>,
|
|
115
|
+
tracker?: ToolCallTracker,
|
|
116
|
+
): HandleEventResult {
|
|
117
|
+
if (!evt || typeof evt !== 'object') return {}
|
|
118
|
+
const record = evt as {
|
|
119
|
+
type?: unknown
|
|
120
|
+
snapshot?: unknown
|
|
121
|
+
delta?: unknown
|
|
122
|
+
error?: unknown
|
|
123
|
+
message?: unknown
|
|
124
|
+
/** `CUSTOM` only — the custom event name, e.g. `SCHEMATIC_GENERATOR_TRACE`. */
|
|
125
|
+
name?: unknown
|
|
126
|
+
/** `CUSTOM` only — the payload; here one (or many) `TraceEvent`s. */
|
|
127
|
+
value?: unknown
|
|
128
|
+
}
|
|
129
|
+
const rec = evt as Record<string, unknown>
|
|
130
|
+
const type = record.type
|
|
131
|
+
if (type === 'STATE_SNAPSHOT') {
|
|
132
|
+
if (record.snapshot && typeof record.snapshot === 'object') {
|
|
133
|
+
Object.assign(state, record.snapshot)
|
|
134
|
+
}
|
|
135
|
+
return { stateChanged: true }
|
|
136
|
+
}
|
|
137
|
+
if (type === 'STATE_DELTA') {
|
|
138
|
+
applyDelta(record.delta, state)
|
|
139
|
+
return { stateChanged: true }
|
|
140
|
+
}
|
|
141
|
+
if (type === 'RUN_STARTED') {
|
|
142
|
+
tracker?.reset()
|
|
143
|
+
return { runStarted: true }
|
|
144
|
+
}
|
|
145
|
+
// ── standard AG-UI tool-call lifecycle ────────────────────────────────
|
|
146
|
+
// This is how `modular_circuit` reports its stack. The schematic agent uses
|
|
147
|
+
// CUSTOM trace events for the same purpose, so `tracker` is only supplied
|
|
148
|
+
// for the system tool and these branches stay inert otherwise.
|
|
149
|
+
if (type === 'TOOL_CALL_START' && tracker) {
|
|
150
|
+
const id = strField(rec, 'toolCallId', 'tool_call_id')
|
|
151
|
+
const name = strField(rec, 'toolCallName', 'tool_call_name') || 'unknown'
|
|
152
|
+
return { trace: [tracker.start(id, name)] }
|
|
153
|
+
}
|
|
154
|
+
if (type === 'TOOL_CALL_END' && tracker) {
|
|
155
|
+
const id = strField(rec, 'toolCallId', 'tool_call_id')
|
|
156
|
+
const ev = tracker.end(id)
|
|
157
|
+
return ev ? { trace: [ev] } : {}
|
|
158
|
+
}
|
|
159
|
+
if (type === 'CUSTOM') {
|
|
160
|
+
const name = typeof record.name === 'string' ? record.name : ''
|
|
161
|
+
// System-design progress: a todo list and/or a stage announcement. Neither
|
|
162
|
+
// is a TraceEvent, so it must be checked BEFORE the trace-name test —
|
|
163
|
+
// `SYSTEM_DESIGN_EVENT` does not end in `_TRACE` and would be dropped.
|
|
164
|
+
if (name === SYSTEM_DESIGN_EVENT_NAME) {
|
|
165
|
+
const value = record.value
|
|
166
|
+
if (value && typeof value === 'object') {
|
|
167
|
+
const payload = value as Record<string, unknown>
|
|
168
|
+
if (payload['kind'] === 'todo_progress' && Array.isArray(payload['todos'])) {
|
|
169
|
+
const todos: TodoItem[] = []
|
|
170
|
+
for (const item of payload['todos']) {
|
|
171
|
+
const todo = parseTodo(item)
|
|
172
|
+
if (todo) todos.push(todo)
|
|
173
|
+
}
|
|
174
|
+
return todos.length > 0 ? { todos } : {}
|
|
175
|
+
}
|
|
176
|
+
if (payload['kind'] === 'progress') {
|
|
177
|
+
const note = parseNote(payload)
|
|
178
|
+
return note ? { note } : {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {}
|
|
182
|
+
}
|
|
183
|
+
if (!isTraceEventName(name)) return {}
|
|
184
|
+
const events = collectTraceEvents(record.value)
|
|
185
|
+
return events.length > 0 ? { trace: events } : {}
|
|
186
|
+
}
|
|
187
|
+
if (type === 'TEXT_MESSAGE_CONTENT') {
|
|
188
|
+
return { text: typeof (evt as { delta?: unknown }).delta === 'string' ? (evt as { delta: string }).delta : '' }
|
|
189
|
+
}
|
|
190
|
+
if (type === 'RUN_FINISHED') {
|
|
191
|
+
return { finished: true }
|
|
192
|
+
}
|
|
193
|
+
if (type === 'RUN_ERROR') {
|
|
194
|
+
return { error: typeof record.error === 'string' ? record.error : (typeof record.message === 'string' ? record.message : 'unknown run error') }
|
|
195
|
+
}
|
|
196
|
+
return {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Live stream accumulator. The optional callbacks fire as each event arrives
|
|
201
|
+
* rather than only at the end — that is what makes a 10-minute run report
|
|
202
|
+
* progress while it is still running.
|
|
203
|
+
*/
|
|
204
|
+
interface Accumulator {
|
|
205
|
+
text: string
|
|
206
|
+
finished: boolean
|
|
207
|
+
error: string
|
|
208
|
+
trace: TraceEvent[]
|
|
209
|
+
/** Called with each batch of trace events, immediately as they decode. */
|
|
210
|
+
onTrace?: (events: TraceEvent[]) => void
|
|
211
|
+
/** Called after any state mutation, so the progress ladder can resample. */
|
|
212
|
+
onState?: (state: Record<string, unknown>) => void
|
|
213
|
+
/** Called with a fresh todo list from the system-design agent. */
|
|
214
|
+
onTodos?: (todos: TodoItem[]) => void
|
|
215
|
+
/** Called with each stage announcement from the system-design agent. */
|
|
216
|
+
onNote?: (note: ProgressNote) => void
|
|
217
|
+
/**
|
|
218
|
+
* Tool-call lifecycle → trace adapter. Present only for agents that report
|
|
219
|
+
* their stack through `TOOL_CALL_START`/`TOOL_CALL_END`.
|
|
220
|
+
*/
|
|
221
|
+
tracker?: ToolCallTracker
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Parse one `data: …` SSE block into event(s) and route them through `handleEvent`. */
|
|
225
|
+
function dispatchRaw(raw: string, state: Record<string, unknown>, acc: Accumulator): void {
|
|
226
|
+
const lines = raw.split(/\r?\n/)
|
|
227
|
+
for (const line of lines) {
|
|
228
|
+
const trimmed = line.trim()
|
|
229
|
+
if (!trimmed.startsWith('data:')) continue
|
|
230
|
+
const payload = trimmed.slice(5).trim()
|
|
231
|
+
if (!payload) continue
|
|
232
|
+
let evt: unknown
|
|
233
|
+
try {
|
|
234
|
+
evt = JSON.parse(payload)
|
|
235
|
+
} catch {
|
|
236
|
+
continue // keep-alives / comments are ignored
|
|
237
|
+
}
|
|
238
|
+
const r = handleEvent(evt, state, acc.tracker)
|
|
239
|
+
if (r.text) acc.text += r.text
|
|
240
|
+
if (r.finished) acc.finished = true
|
|
241
|
+
if (r.error) acc.error = r.error
|
|
242
|
+
if (r.trace && r.trace.length > 0) {
|
|
243
|
+
for (const ev of r.trace) acc.trace.push(ev)
|
|
244
|
+
acc.onTrace?.(r.trace)
|
|
245
|
+
}
|
|
246
|
+
if (r.todos && r.todos.length > 0) acc.onTodos?.(r.todos)
|
|
247
|
+
if (r.note) acc.onNote?.(r.note)
|
|
248
|
+
if (r.stateChanged) acc.onState?.(state)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Decode a chunk, split on SSE boundaries, dispatch complete events, return
|
|
253
|
+
* the unterminated remainder. */
|
|
254
|
+
function feed(chunkStr: string, state: Record<string, unknown>, leftover: string, acc: Accumulator): string {
|
|
255
|
+
const combined = leftover + chunkStr
|
|
256
|
+
const parts = combined.split(/\r?\n\r?\n/)
|
|
257
|
+
const newLeftover = parts.pop() || ''
|
|
258
|
+
for (const raw of parts) {
|
|
259
|
+
if (raw.trim().length === 0) continue
|
|
260
|
+
dispatchRaw(raw, state, acc)
|
|
261
|
+
}
|
|
262
|
+
return newLeftover
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface ConsumeOptions {
|
|
266
|
+
signal?: AbortSignal | null
|
|
267
|
+
timeoutMs?: number
|
|
268
|
+
fetchImpl?: typeof fetch
|
|
269
|
+
/** Live execution-trace events, pushed as each `CUSTOM` frame decodes. */
|
|
270
|
+
onTrace?: (events: TraceEvent[]) => void
|
|
271
|
+
/** Called after every state mutation (snapshot or delta), for the stage ladder. */
|
|
272
|
+
onState?: (state: Record<string, unknown>) => void
|
|
273
|
+
/** Called with a fresh todo list from the system-design agent. */
|
|
274
|
+
onTodos?: (todos: TodoItem[]) => void
|
|
275
|
+
/** Called with each human-readable stage announcement. */
|
|
276
|
+
onNote?: (note: ProgressNote) => void
|
|
277
|
+
/**
|
|
278
|
+
* Invoked once when the design API rejects the request with HTTP 401, i.e. the
|
|
279
|
+
* supplied `x-user-token` is dead. Hook for reactive invalidation: the caller
|
|
280
|
+
* clears its credential cache so the next request re-resolves instead of
|
|
281
|
+
* replaying a dead token (spec §6.5).
|
|
282
|
+
*/
|
|
283
|
+
onUnauthorized?: () => void
|
|
284
|
+
/**
|
|
285
|
+
* Synthesize trace events from the standard AG-UI `TOOL_CALL_START` /
|
|
286
|
+
* `TOOL_CALL_END` lifecycle.
|
|
287
|
+
*
|
|
288
|
+
* **Only enable this for `modular_circuit` (system design).** The schematic
|
|
289
|
+
* agent reports the same calls through `CUSTOM` trace events, so enabling
|
|
290
|
+
* both would double every row in the stack.
|
|
291
|
+
*/
|
|
292
|
+
toolCallTrace?: boolean
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* POST to the CopilotKit endpoint and consume the SSE stream until it ends or
|
|
297
|
+
* the budget elapses, accumulating the agent state.
|
|
298
|
+
*/
|
|
299
|
+
export async function consumeCopilotkit(
|
|
300
|
+
url: string,
|
|
301
|
+
body: Record<string, unknown>,
|
|
302
|
+
headers: Record<string, string>,
|
|
303
|
+
options: ConsumeOptions = {},
|
|
304
|
+
): Promise<{ state: Record<string, unknown>; finished: boolean; text: string; trace: TraceEvent[] }> {
|
|
305
|
+
const { signal, timeoutMs = HTTP_TIMEOUT_MS, fetchImpl = fetch } = options
|
|
306
|
+
const controller = new AbortController()
|
|
307
|
+
const timer = setTimeout(
|
|
308
|
+
() => controller.abort(new Error('schematic-gen: the design agent did not finish within ' + timeoutMs + 'ms')),
|
|
309
|
+
timeoutMs,
|
|
310
|
+
)
|
|
311
|
+
let onAbort: (() => void) | null = null
|
|
312
|
+
if (signal) {
|
|
313
|
+
if (signal.aborted) controller.abort()
|
|
314
|
+
else {
|
|
315
|
+
onAbort = () => controller.abort()
|
|
316
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let res: Response
|
|
321
|
+
try {
|
|
322
|
+
res = await fetchImpl(url, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers,
|
|
325
|
+
body: JSON.stringify(body),
|
|
326
|
+
signal: controller.signal,
|
|
327
|
+
})
|
|
328
|
+
} catch (err) {
|
|
329
|
+
clearTimeout(timer)
|
|
330
|
+
if (onAbort && signal) signal.removeEventListener('abort', onAbort)
|
|
331
|
+
throw new Error('schematic-gen: failed to reach the design API: ' + String((err as Error)?.message || err))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!res || !res.ok) {
|
|
335
|
+
clearTimeout(timer)
|
|
336
|
+
if (onAbort && signal) signal.removeEventListener('abort', onAbort)
|
|
337
|
+
if (res && res.status === 401) {
|
|
338
|
+
try { options.onUnauthorized?.() } catch { /* invalidation is best-effort */ }
|
|
339
|
+
}
|
|
340
|
+
throw new Error('schematic-gen: the design API returned HTTP ' +
|
|
341
|
+
String(res && res.status) + ' (expected 200 with an SSE stream)')
|
|
342
|
+
}
|
|
343
|
+
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
344
|
+
clearTimeout(timer)
|
|
345
|
+
if (onAbort && signal) signal.removeEventListener('abort', onAbort)
|
|
346
|
+
throw new Error('schematic-gen: the design API response had no stream body')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const reader = res.body.getReader()
|
|
350
|
+
const decoder = new TextDecoder()
|
|
351
|
+
let buf = ''
|
|
352
|
+
const state: Record<string, unknown> = {}
|
|
353
|
+
const acc: Accumulator = {
|
|
354
|
+
text: '',
|
|
355
|
+
finished: false,
|
|
356
|
+
error: '',
|
|
357
|
+
trace: [],
|
|
358
|
+
onTrace: options.onTrace,
|
|
359
|
+
onState: options.onState,
|
|
360
|
+
onTodos: options.onTodos,
|
|
361
|
+
onNote: options.onNote,
|
|
362
|
+
tracker: options.toolCallTrace ? new ToolCallTracker() : undefined,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
for (;;) {
|
|
367
|
+
const { done, value } = await reader.read()
|
|
368
|
+
if (done) break
|
|
369
|
+
buf = feed(decoder.decode(value, { stream: true }), state, buf, acc)
|
|
370
|
+
}
|
|
371
|
+
// Flush a final event not terminated by a blank line.
|
|
372
|
+
if (buf.length > 0) dispatchRaw(buf, state, acc)
|
|
373
|
+
} finally {
|
|
374
|
+
clearTimeout(timer)
|
|
375
|
+
if (onAbort && signal) signal.removeEventListener('abort', onAbort)
|
|
376
|
+
try { await reader.cancel() } catch { /* already done */ }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (acc.error) {
|
|
380
|
+
throw new Error('schematic-gen: the design agent reported an error: ' + acc.error +
|
|
381
|
+
(acc.text ? ' — ' + acc.text.slice(0, 300) : ''))
|
|
382
|
+
}
|
|
383
|
+
return { state, finished: acc.finished, text: acc.text, trace: acc.trace }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface ExportZipOptions {
|
|
387
|
+
signal?: AbortSignal | null
|
|
388
|
+
timeoutMs?: number
|
|
389
|
+
fetchImpl?: typeof fetch
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* POST the module graph to the production export-zip route and return the zip
|
|
394
|
+
* as a Buffer. The route reads `req.json()` as the `MODULE_GRAPH` and responds
|
|
395
|
+
* with `application/zip`.
|
|
396
|
+
*/
|
|
397
|
+
export async function exportModuleGraphZip(
|
|
398
|
+
exportZipUrl: string,
|
|
399
|
+
moduleGraph: Record<string, unknown>,
|
|
400
|
+
config: SchematicGenConfig,
|
|
401
|
+
account: EdaAccount,
|
|
402
|
+
options: ExportZipOptions = {},
|
|
403
|
+
): Promise<Buffer> {
|
|
404
|
+
const { signal, timeoutMs = HTTP_TIMEOUT_MS, fetchImpl = fetch } = options
|
|
405
|
+
const controller = new AbortController()
|
|
406
|
+
const timer = setTimeout(
|
|
407
|
+
() => controller.abort(new Error('schematic-gen: the export-zip service did not respond within ' + timeoutMs + 'ms')),
|
|
408
|
+
timeoutMs,
|
|
409
|
+
)
|
|
410
|
+
let onAbort: (() => void) | null = null
|
|
411
|
+
if (signal) {
|
|
412
|
+
if (signal.aborted) controller.abort()
|
|
413
|
+
else {
|
|
414
|
+
onAbort = () => controller.abort()
|
|
415
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let res: Response
|
|
420
|
+
try {
|
|
421
|
+
res = await fetchImpl(exportZipUrl, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: buildExportHeaders(config, account),
|
|
424
|
+
body: JSON.stringify(moduleGraph),
|
|
425
|
+
signal: controller.signal,
|
|
426
|
+
})
|
|
427
|
+
} catch (err) {
|
|
428
|
+
clearTimeout(timer)
|
|
429
|
+
if (onAbort && signal) signal.removeEventListener('abort', onAbort)
|
|
430
|
+
throw new Error('schematic-gen: failed to export the module graph to zip: ' +
|
|
431
|
+
String((err as Error)?.message || err))
|
|
432
|
+
} finally {
|
|
433
|
+
clearTimeout(timer)
|
|
434
|
+
if (onAbort && signal) signal.removeEventListener('abort', onAbort)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!res || !res.ok) {
|
|
438
|
+
let detail = ''
|
|
439
|
+
try {
|
|
440
|
+
const j = (await res.json()) as { error?: unknown }
|
|
441
|
+
detail = typeof j.error === 'string' ? j.error : ''
|
|
442
|
+
} catch { /* not JSON */ }
|
|
443
|
+
throw new Error('schematic-gen: the export-zip service returned HTTP ' +
|
|
444
|
+
String(res && res.status) + (detail ? ' — ' + detail : ''))
|
|
445
|
+
}
|
|
446
|
+
const ab = await res.arrayBuffer()
|
|
447
|
+
return Buffer.from(ab)
|
|
448
|
+
}
|