@stack-spot/portal-network 0.214.0 → 0.215.1-alpha.0
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/CHANGELOG.md +14 -0
- package/dist/api/codeShift.d.ts +12 -1
- package/dist/api/codeShift.d.ts.map +1 -1
- package/dist/api/codeShift.js.map +1 -1
- package/dist/api/genAiInference.d.ts +49 -2
- package/dist/api/genAiInference.d.ts.map +1 -1
- package/dist/api/genAiInference.js +55 -2
- package/dist/api/genAiInference.js.map +1 -1
- package/dist/client/ai.d.ts +1 -3
- package/dist/client/ai.d.ts.map +1 -1
- package/dist/client/ai.js +2 -249
- package/dist/client/ai.js.map +1 -1
- package/dist/client/discover.d.ts +2 -2
- package/dist/client/discover.d.ts.map +1 -1
- package/dist/client/discover.js +4 -3
- package/dist/client/discover.js.map +1 -1
- package/dist/client/gen-ai-inference.d.ts +4 -0
- package/dist/client/gen-ai-inference.d.ts.map +1 -1
- package/dist/client/gen-ai-inference.js +267 -0
- package/dist/client/gen-ai-inference.js.map +1 -1
- package/dist/client/types.d.ts +13 -14
- package/dist/client/types.d.ts.map +1 -1
- package/dist/utils/StreamedJson.d.ts +9 -1
- package/dist/utils/StreamedJson.d.ts.map +1 -1
- package/dist/utils/StreamedJson.js +22 -2
- package/dist/utils/StreamedJson.js.map +1 -1
- package/package.json +1 -1
- package/src/api/codeShift.ts +12 -1
- package/src/api/genAiInference.ts +119 -3
- package/src/client/ai.ts +2 -265
- package/src/client/discover.ts +7 -6
- package/src/client/gen-ai-inference.ts +281 -0
- package/src/client/types.ts +14 -14
- package/src/utils/StreamedJson.tsx +19 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { HttpError } from '@oazapfts/runtime'
|
|
3
|
+
import { findLast, last } from 'lodash'
|
|
3
4
|
import { getApiAddresses } from '../api-addresses'
|
|
4
5
|
import { addSelfHostedModelV1LlmModelsPost, agentChatV1AgentAgentIdChatPost, defaults, deleteModelResourcesV1LlmResourcesResourceIdDelete, deleteV1LlmModelsModelIdDelete, getModelV1LlmModelsModelIdGet, listLlmProvidersV1LlmProvidersGet, listModelsV1LlmModelsGet, saveOrUpdateModelResourcesV1LlmModelsModelIdResourcesPut, toggleModelStatusV1LlmModelsModelIdPatch, updateV1LlmModelsModelIdPut } from '../api/genAiInference'
|
|
5
6
|
import { DefaultAPIError } from '../error/DefaultAPIError'
|
|
@@ -7,6 +8,10 @@ import { inferenceDictionary } from '../error/dictionary/ai-inference'
|
|
|
7
8
|
import { StackspotAPIError } from '../error/StackspotAPIError'
|
|
8
9
|
import { ReactQueryNetworkClient } from '../network/ReactQueryNetworkClient'
|
|
9
10
|
import { removeAuthorizationParam } from '../utils/remove-authorization-param'
|
|
11
|
+
import { StreamedJson } from '../utils/StreamedJson'
|
|
12
|
+
import { formatJson } from '../utils/string'
|
|
13
|
+
import { agentToolsClient } from './agent-tools'
|
|
14
|
+
import { AgentInfo, ChatAgentTool, ChatResponseWithSteps, FixedChatRequest, FixedChatResponse, StepChatStep } from './types'
|
|
10
15
|
|
|
11
16
|
class GenAiInference extends ReactQueryNetworkClient {
|
|
12
17
|
constructor() {
|
|
@@ -60,6 +65,282 @@ class GenAiInference extends ReactQueryNetworkClient {
|
|
|
60
65
|
* Interaction with a specific AI agent
|
|
61
66
|
*/
|
|
62
67
|
sendAgentMessage = this.mutation(removeAuthorizationParam(agentChatV1AgentAgentIdChatPost))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
private static async toolsOfAgent(agentId?: string) {
|
|
71
|
+
try {
|
|
72
|
+
const agent = agentId ? await agentToolsClient.agent.query({ agentId }) : undefined
|
|
73
|
+
if (!agent) return []
|
|
74
|
+
const tools: (Omit<ChatAgentTool, 'duration' | 'prompt' | 'output'>)[] = []
|
|
75
|
+
agent.toolkits?.builtin_toolkits?.forEach(kit => kit.tools?.forEach(({ id, name, description }) => {
|
|
76
|
+
if (id) tools.push({ image: kit.image_url, id, name: name || id, description })
|
|
77
|
+
}))
|
|
78
|
+
agent.toolkits?.custom_toolkits?.forEach(kit => kit.tools?.forEach(({ id, name, description }) => {
|
|
79
|
+
if (id) tools.push({ image: kit.avatar ?? undefined, id, name: name || id, description })
|
|
80
|
+
}))
|
|
81
|
+
return tools
|
|
82
|
+
} catch {
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sendChatMessage(request: FixedChatRequest, minChangeIntervalMS?: number): StreamedJson<ChatResponseWithSteps> {
|
|
88
|
+
const abortController = new AbortController()
|
|
89
|
+
const { context, ...body } = request ?? {}
|
|
90
|
+
const headers = {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
'Accept': 'text/event-stream',
|
|
93
|
+
'x-platform': context?.platform ?? '',
|
|
94
|
+
'x-os': context?.os ?? '',
|
|
95
|
+
'x-platform-version': context?.platform_version ?? '',
|
|
96
|
+
'x-stackspot-ai-version': context?.stackspot_ai_version ?? '',
|
|
97
|
+
'x-request-origin': 'chat',
|
|
98
|
+
}
|
|
99
|
+
const events = this.stream(
|
|
100
|
+
this.resolveURL(`/v1/agent/${context?.agent_id}/chat`),
|
|
101
|
+
{ method: 'post', body: JSON.stringify(body), headers, signal: abortController.signal },
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const DYNAMIC_TOOL_ID = 'dynamic'
|
|
105
|
+
function isDynamicTool(info: AgentInfo) {
|
|
106
|
+
return info.type === 'tool' && info.id === DYNAMIC_TOOL_ID
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* This function treats events in the streaming that deals with the execution of tools. Since these events are not concatenated like
|
|
110
|
+
* normal streamings of data, we need this separate function to deal with them. It transforms the internal data model of the
|
|
111
|
+
* StreamedJson object whenever an event is triggered.
|
|
112
|
+
*/
|
|
113
|
+
async function transform(event: Partial<FixedChatResponse>, data: Partial<ChatResponseWithSteps>) {
|
|
114
|
+
// The API send a final snapshot containing the full `agent_info`.
|
|
115
|
+
// When `stop_reason === 'stop'` we can ignore it to avoid duplicating steps.
|
|
116
|
+
if (event?.stop_reason === 'stop') return
|
|
117
|
+
|
|
118
|
+
const infoList = event.agent_info ?? []
|
|
119
|
+
if (infoList.length === 0) return
|
|
120
|
+
|
|
121
|
+
const tools = await GenAiInference.toolsOfAgent(request.context?.agent_id)
|
|
122
|
+
data.steps = data.steps ? [...data.steps] : []
|
|
123
|
+
|
|
124
|
+
for (const info of infoList) {
|
|
125
|
+
|
|
126
|
+
if (info.type === 'planning' && info.action === 'end') {
|
|
127
|
+
data.steps.push({
|
|
128
|
+
id: 'planning',
|
|
129
|
+
type: 'planning',
|
|
130
|
+
status: 'success',
|
|
131
|
+
duration: info.duration || 0,
|
|
132
|
+
steps: info.data?.steps?.map(s => s.goal) ?? [],
|
|
133
|
+
goal: info.data?.plan_goal ?? '',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
info.data?.steps.forEach(s => data.steps?.push({
|
|
137
|
+
id: s.id,
|
|
138
|
+
type: 'step',
|
|
139
|
+
status: 'pending',
|
|
140
|
+
input: s.goal,
|
|
141
|
+
attempts: [{
|
|
142
|
+
tools: s.tools?.map(t => ({
|
|
143
|
+
...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
|
|
144
|
+
executionId: t.tool_execution_id,
|
|
145
|
+
goal: t.goal,
|
|
146
|
+
})),
|
|
147
|
+
}],
|
|
148
|
+
}))
|
|
149
|
+
data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (info.type === 'planning' && info.action === 'awaiting_approval') {
|
|
153
|
+
data.steps.push({
|
|
154
|
+
id: 'planning',
|
|
155
|
+
type: 'planning',
|
|
156
|
+
status: 'awaiting_approval',
|
|
157
|
+
user_question: info.data?.user_question,
|
|
158
|
+
duration: info.duration || 0,
|
|
159
|
+
steps: info.data?.steps?.map(s => s.goal) ?? [],
|
|
160
|
+
goal: info.data?.plan_goal ?? '',
|
|
161
|
+
})
|
|
162
|
+
info.data?.steps.forEach(s => data.steps?.push({
|
|
163
|
+
id: s.id,
|
|
164
|
+
type: 'step',
|
|
165
|
+
status: 'pending',
|
|
166
|
+
input: s.goal,
|
|
167
|
+
attempts: [{
|
|
168
|
+
tools: s.tools?.map(t => ({
|
|
169
|
+
...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
|
|
170
|
+
executionId: t.tool_execution_id,
|
|
171
|
+
goal: t.goal,
|
|
172
|
+
})),
|
|
173
|
+
}],
|
|
174
|
+
}))
|
|
175
|
+
data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (info.type === 'step' && info.action === 'start') {
|
|
179
|
+
const step = data.steps.find(s => s.id === info.id)
|
|
180
|
+
if (step) step.status = 'running'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (info.type === 'step' && info.action === 'end') {
|
|
184
|
+
const step = data.steps.find(s => s.id === info.id) as StepChatStep
|
|
185
|
+
if (step) {
|
|
186
|
+
step.status = 'success'
|
|
187
|
+
step.duration = info.duration
|
|
188
|
+
const lastToolId = last(step.attempts[0].tools)?.id
|
|
189
|
+
const lastAttemptOfLastTool = findLast(step.attempts.map(a => a.tools).flat(), t => t?.id === lastToolId)
|
|
190
|
+
step.output = lastAttemptOfLastTool?.output
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (info.type === 'tool_calls' && info.action === 'start') {
|
|
195
|
+
const hasPlanning = data.steps.find(s => s.type === 'planning')
|
|
196
|
+
// On the first tool_calls:start, create the synthetic planning ("dynamic") step.
|
|
197
|
+
if (!hasPlanning) {
|
|
198
|
+
const userPrompt = request.user_prompt === 'string' ? request.user_prompt : JSON.stringify(request.user_prompt)
|
|
199
|
+
data.steps.push({
|
|
200
|
+
id: 'dynamic',
|
|
201
|
+
type: 'planning',
|
|
202
|
+
status: 'success',
|
|
203
|
+
steps: [],
|
|
204
|
+
goal: userPrompt,
|
|
205
|
+
user_question: userPrompt,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
const toolsStepId = data.steps.filter(s => s.id === 'tools' || s.id.startsWith('tools-')).length + 1
|
|
209
|
+
data.steps.push({
|
|
210
|
+
id: `tools-${toolsStepId.toString()}`,
|
|
211
|
+
type: 'step',
|
|
212
|
+
status: 'running',
|
|
213
|
+
attempts: [{ tools: [] }],
|
|
214
|
+
} as StepChatStep)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (info.type === 'tool_calls' && info.action === 'end') {
|
|
218
|
+
const lastStep = findLast(data.steps, s => s.id === 'tools' || s.id.startsWith('tools-')) as StepChatStep
|
|
219
|
+
if (lastStep) {
|
|
220
|
+
lastStep.status = 'success'
|
|
221
|
+
lastStep.duration = info.duration
|
|
222
|
+
const lastAttemptOfLastTool = last(lastStep.attempts.map(a => a.tools).flat())
|
|
223
|
+
lastStep.output = lastAttemptOfLastTool?.output
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (info.type === 'tool' && info.action === 'awaiting_approval') {
|
|
228
|
+
const tool = tools.find(({ id }) => id === info.data?.tool_id)
|
|
229
|
+
data.steps.push({
|
|
230
|
+
id: info.id,
|
|
231
|
+
type: 'tool',
|
|
232
|
+
status: 'awaiting_approval',
|
|
233
|
+
duration: info.duration || 0,
|
|
234
|
+
input: info.data?.input,
|
|
235
|
+
user_question: info.data?.user_question,
|
|
236
|
+
attempts: [{
|
|
237
|
+
tools: [{
|
|
238
|
+
executionId: info.id,
|
|
239
|
+
id: info.data?.tool_id ?? '',
|
|
240
|
+
name: tool?.name ?? '',
|
|
241
|
+
goal: tool?.goal,
|
|
242
|
+
...tool,
|
|
243
|
+
}],
|
|
244
|
+
}],
|
|
245
|
+
})
|
|
246
|
+
data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (info.type === 'tool' && info.action === 'start') {
|
|
250
|
+
if (!info.data) return
|
|
251
|
+
const input = formatJson(info.data.input)
|
|
252
|
+
const tool = findLast(tools, ({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
|
|
253
|
+
|
|
254
|
+
const currentStep = findLast(data.steps, s => s.status === 'running') as StepChatStep
|
|
255
|
+
|
|
256
|
+
//There might be a tool with status awaiting_approval, so we want to inform tool has already started
|
|
257
|
+
if (!currentStep || !currentStep?.attempts?.[0]?.tools) {
|
|
258
|
+
data.steps.push({
|
|
259
|
+
id: info.id,
|
|
260
|
+
type: 'tool',
|
|
261
|
+
status: 'running',
|
|
262
|
+
duration: info.duration || 0,
|
|
263
|
+
input: info.data?.input,
|
|
264
|
+
user_question: info.data?.user_question,
|
|
265
|
+
attempts: [{
|
|
266
|
+
tools: [{ ...tool, executionId: info.id, input }],
|
|
267
|
+
}],
|
|
268
|
+
})
|
|
269
|
+
} else {
|
|
270
|
+
const toolInFirstAttempt = findLast(currentStep?.attempts?.[0]?.tools, t => t.executionId === info.id)
|
|
271
|
+
//One step might have multiple tools. When in an approval mode, we might not have all the tools in the array yet.
|
|
272
|
+
//For dynamic tools (id === 'dynamic'), we always push a new tool, since dynamic executions can trigger
|
|
273
|
+
//multiple tool runs in the same step and do not follow the planned tool structure.
|
|
274
|
+
//So we make sure to add any tools that are not in there, or always add for dynamic tools.
|
|
275
|
+
if (!toolInFirstAttempt || isDynamicTool(info)) {
|
|
276
|
+
currentStep.attempts?.[0].tools?.push({
|
|
277
|
+
...tool,
|
|
278
|
+
executionId: info.id,
|
|
279
|
+
input,
|
|
280
|
+
status: 'running',
|
|
281
|
+
})
|
|
282
|
+
} else {
|
|
283
|
+
const input = formatJson(info.data.input)
|
|
284
|
+
if (info.data.attempt === 1) {
|
|
285
|
+
toolInFirstAttempt.input = input
|
|
286
|
+
} else {
|
|
287
|
+
currentStep.attempts[info.data.attempt - 1] ??= { tools: [] }
|
|
288
|
+
currentStep.attempts[info.data.attempt - 1].tools?.push({
|
|
289
|
+
...tool,
|
|
290
|
+
executionId: info.id,
|
|
291
|
+
input,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (info.type === 'tool' && info.action === 'end') {
|
|
299
|
+
const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
|
|
300
|
+
if (!currentStep || !info.data) return
|
|
301
|
+
|
|
302
|
+
// attempt index for tool execution starts at 0 for dynamically executed tools,while for planned tools it starts at 1
|
|
303
|
+
const attempt = isDynamicTool(info) ? info.data.attempt : info.data.attempt - 1
|
|
304
|
+
const tool = last(currentStep?.attempts?.[attempt]?.tools)
|
|
305
|
+
if (tool) {
|
|
306
|
+
tool.output = formatJson(info.data.output)
|
|
307
|
+
tool.duration = info.duration
|
|
308
|
+
tool.status = 'success'
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (info.type === 'final_answer' && info.action === 'start') {
|
|
313
|
+
const answerStep = last(data.steps)
|
|
314
|
+
if (answerStep) answerStep.status = 'running'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if (info.type === 'chat' && info.action === 'end') {
|
|
319
|
+
const lastStep = last(data.steps)
|
|
320
|
+
if (lastStep?.type === 'answer') {
|
|
321
|
+
lastStep.status = 'success'
|
|
322
|
+
lastStep.duration = info.duration
|
|
323
|
+
} else {
|
|
324
|
+
data.steps.push({ id: 'answer', type: 'answer', status: 'success' })
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return new StreamedJson({
|
|
331
|
+
eventsPromise: events,
|
|
332
|
+
abortController,
|
|
333
|
+
ignoreKeys: ['agent_info'],
|
|
334
|
+
transform,
|
|
335
|
+
textFromErrorEvent: data => data.message ?? 'Unknown error',
|
|
336
|
+
minChangeIntervalMS,
|
|
337
|
+
minChangeIntervalMSFromEvent: (event) => {
|
|
338
|
+
if (event?.agent_info?.length) return 0
|
|
339
|
+
return minChangeIntervalMS
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
|
|
63
344
|
}
|
|
64
345
|
|
|
65
346
|
export const genAiInferenceClient = new GenAiInference()
|
package/src/client/types.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RequestOpts } from '@oazapfts/runtime'
|
|
2
2
|
import { AccountScmInfoSaveRequest, AccountScmInfoUpdateRequest, AccountScmStatusResponse, GroupsFromResourceResponse, MembersFromResourceResponse } from '../api/account'
|
|
3
3
|
import { AgentVisibilityLevelEnum, HttpMethod, ListAgentResponse, VisibilityLevelEnum } from '../api/agent-tools'
|
|
4
|
-
import {
|
|
4
|
+
import { ChatResponse3, ContentDependencyResponse, ConversationHistoryResponse, ConversationResponse, DependencyResponse, SourceKnowledgeSource, SourceProjectFile3, SourceStackAi } from '../api/ai'
|
|
5
5
|
import { ConnectAccountRequestV2, ManagedAccountProvisionRequest } from '../api/cloudAccount'
|
|
6
6
|
import { AllocationCostRequest, AllocationCostResponse, ChargePeriod, getAllocationCostFilters, ManagedService, ServiceResource } from '../api/cloudServices'
|
|
7
|
+
import { ChatRequest } from '../api/genAiInference'
|
|
7
8
|
import { Action } from '../api/workspace-ai'
|
|
8
9
|
import { ActivityResponse, FullInputContextResponse, InputConditionResponse, InputValuesContextResponse, PaginatedActivityResponse, PluginForAppCreationV2Response, PluginInputValuesInConsolidatedContextResponse, ValueByEnvResponse, WorkflowForCreationResponse } from '../api/workspaceManager'
|
|
9
10
|
|
|
@@ -172,20 +173,11 @@ export interface FixedPaginatedActivityResponse extends Omit<PaginatedActivityRe
|
|
|
172
173
|
|
|
173
174
|
export interface FixedChatRequest extends ChatRequest {
|
|
174
175
|
context?: {
|
|
175
|
-
workspace?: string,
|
|
176
|
-
conversation_id?: string,
|
|
177
|
-
stack_id?: string,
|
|
178
|
-
language?: string,
|
|
179
|
-
project_recent_files?: string,
|
|
180
|
-
knowledge_sources?: string[],
|
|
181
|
-
agent_id?: string,
|
|
182
|
-
agent_built_in?: boolean,
|
|
183
176
|
platform?: string,
|
|
184
177
|
platform_version?: string,
|
|
185
178
|
stackspot_ai_version?: string,
|
|
186
179
|
os?: string,
|
|
187
|
-
|
|
188
|
-
selected_model_id?: string,
|
|
180
|
+
agent_id?: string,
|
|
189
181
|
},
|
|
190
182
|
}
|
|
191
183
|
|
|
@@ -366,9 +358,17 @@ export interface ToolCallsAgentInfo extends BaseAgentInfo {
|
|
|
366
358
|
|
|
367
359
|
export type AgentInfo = GenericAgentInfo | PlanningAgentInfo | StepAgentInfo | ToolAgentInfo | ToolCallsAgentInfo
|
|
368
360
|
|
|
369
|
-
export
|
|
370
|
-
|
|
371
|
-
|
|
361
|
+
export type ChatResponse = {
|
|
362
|
+
message: string,
|
|
363
|
+
cross_account_source: SourceKnowledgeSource[],
|
|
364
|
+
source: (SourceStackAi | SourceKnowledgeSource | SourceProjectFile3)[],
|
|
365
|
+
message_id: string | null,
|
|
366
|
+
stop_reason: 'stop',
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export interface FixedChatResponse extends ChatResponse {
|
|
370
|
+
agent_info: AgentInfo[],
|
|
371
|
+
tools_id?: string[],
|
|
372
372
|
}
|
|
373
373
|
export interface OpportunitiesPMAgent {
|
|
374
374
|
title: string,
|
|
@@ -21,6 +21,11 @@ interface ConstructorParams<T> {
|
|
|
21
21
|
* @default 50
|
|
22
22
|
*/
|
|
23
23
|
minChangeIntervalMS?: number,
|
|
24
|
+
/**
|
|
25
|
+
* Allows overriding the minimum interval by event.
|
|
26
|
+
* Useful when some events must flush immediately (agent_info events).
|
|
27
|
+
*/
|
|
28
|
+
minChangeIntervalMSFromEvent?: (event: Partial<T>, data: Partial<T>) => number | undefined,
|
|
24
29
|
/**
|
|
25
30
|
* Optional. If set, this function will be called with every streaming event and must transform the current data object according to the
|
|
26
31
|
* message received.
|
|
@@ -31,6 +36,7 @@ interface ConstructorParams<T> {
|
|
|
31
36
|
* of merged.
|
|
32
37
|
*/
|
|
33
38
|
ignoreKeys?: (keyof T)[],
|
|
39
|
+
textFromErrorEvent?: (data: Partial<T>) => string,
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -44,16 +50,20 @@ export class StreamedJson<T> {
|
|
|
44
50
|
private abortController: AbortController | undefined
|
|
45
51
|
private transform?: (event: Partial<T>, data: Partial<T>) => void | Promise<void>
|
|
46
52
|
private ignoreKeys?: (keyof T)[]
|
|
53
|
+
private minChangeIntervalMSFromEvent?: (event: Partial<T>, data: Partial<T>) => number | undefined
|
|
54
|
+
private textFromErrorEvent: (data: Partial<T>) => string
|
|
47
55
|
|
|
48
56
|
/**
|
|
49
57
|
* @param response the fetch response.
|
|
50
58
|
* @param minChangeIntervalMS a stream can be too fast. This sets a minimum interval between running the listeners. The default is 50ms.
|
|
51
59
|
*/
|
|
52
|
-
constructor({ eventsPromise, abortController, minChangeIntervalMS = 50, transform, ignoreKeys }: ConstructorParams<T>) {
|
|
60
|
+
constructor({ eventsPromise, abortController, minChangeIntervalMS = 50, minChangeIntervalMSFromEvent, transform, ignoreKeys, textFromErrorEvent }: ConstructorParams<T>) {
|
|
53
61
|
this.abortController = abortController
|
|
54
62
|
this.transform = transform
|
|
55
63
|
this.ignoreKeys = ignoreKeys
|
|
64
|
+
this.minChangeIntervalMSFromEvent = minChangeIntervalMSFromEvent
|
|
56
65
|
this.run(eventsPromise, minChangeIntervalMS)
|
|
66
|
+
this.textFromErrorEvent = textFromErrorEvent ?? ((data) => JSON.stringify(data))
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
private async run(eventsPromise: Promise<FetchEventStream>, minChangeIntervalMS: number) {
|
|
@@ -74,7 +84,9 @@ export class StreamedJson<T> {
|
|
|
74
84
|
}
|
|
75
85
|
await this.transform?.(json, this.data)
|
|
76
86
|
this.merge(json, this.data)
|
|
77
|
-
|
|
87
|
+
|
|
88
|
+
const intervalMS = this.minChangeIntervalMSFromEvent?.(json, this.data) ?? minChangeIntervalMS
|
|
89
|
+
if (event.event !== 'error' && new Date().getTime() - lastChangeCall >= intervalMS) {
|
|
78
90
|
this.onChangeListeners.forEach(l => l(this.data))
|
|
79
91
|
lastChangeCall = new Date().getTime()
|
|
80
92
|
flushed = true
|
|
@@ -82,6 +94,11 @@ export class StreamedJson<T> {
|
|
|
82
94
|
flushed = false
|
|
83
95
|
}
|
|
84
96
|
}
|
|
97
|
+
if (event.event === 'error') {
|
|
98
|
+
const error = new Error(this.textFromErrorEvent(this.data))
|
|
99
|
+
this.data = {}
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
if (!flushed) this.onChangeListeners.forEach(l => l(this.data))
|
|
87
104
|
} catch (error: any) {
|