btca-server 1.0.962 → 2.0.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/package.json +3 -3
- package/src/agent/agent.test.ts +31 -24
- package/src/agent/index.ts +8 -2
- package/src/agent/loop.ts +303 -346
- package/src/agent/service.ts +252 -233
- package/src/agent/types.ts +2 -2
- package/src/collections/index.ts +2 -1
- package/src/collections/service.ts +352 -345
- package/src/config/config.test.ts +3 -1
- package/src/config/index.ts +615 -727
- package/src/config/remote.ts +214 -369
- package/src/context/index.ts +6 -12
- package/src/context/transaction.ts +23 -30
- package/src/effect/errors.ts +45 -0
- package/src/effect/layers.ts +26 -0
- package/src/effect/runtime.ts +19 -0
- package/src/effect/services.ts +154 -0
- package/src/index.ts +291 -369
- package/src/metrics/index.ts +46 -46
- package/src/pricing/models-dev.ts +104 -106
- package/src/providers/auth.ts +159 -200
- package/src/providers/index.ts +19 -2
- package/src/providers/model.ts +115 -135
- package/src/providers/openai.ts +3 -3
- package/src/resources/impls/git.ts +123 -146
- package/src/resources/impls/npm.test.ts +16 -5
- package/src/resources/impls/npm.ts +66 -75
- package/src/resources/index.ts +6 -1
- package/src/resources/schema.ts +7 -6
- package/src/resources/service.test.ts +13 -12
- package/src/resources/service.ts +153 -112
- package/src/stream/index.ts +1 -1
- package/src/stream/service.test.ts +5 -5
- package/src/stream/service.ts +282 -293
- package/src/tools/glob.ts +126 -141
- package/src/tools/grep.ts +205 -210
- package/src/tools/index.ts +8 -4
- package/src/tools/list.ts +118 -140
- package/src/tools/read.ts +209 -235
- package/src/tools/virtual-sandbox.ts +91 -83
- package/src/validation/index.ts +18 -22
- package/src/vfs/virtual-fs.test.ts +37 -25
- package/src/vfs/virtual-fs.ts +218 -216
package/src/stream/service.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { stripUserQuestionFromStart, extractCoreQuestion } from '@btca/shared';
|
|
2
|
-
import { Result } from 'better-result';
|
|
3
2
|
|
|
4
3
|
import { getErrorHint, getErrorMessage, getErrorTag } from '../errors.ts';
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
4
|
+
import { metricsError, metricsErrorInfo, metricsInfo } from '../metrics/index.ts';
|
|
5
|
+
import type { AgentEvent } from '../agent/loop.ts';
|
|
7
6
|
|
|
8
7
|
import type {
|
|
9
8
|
BtcaStreamDoneEvent,
|
|
@@ -36,318 +35,308 @@ const hasAnyDefined = (record: Record<string, unknown> | undefined) =>
|
|
|
36
35
|
const costFor = (tokens: number | undefined, usdPerMTokens: number | undefined) =>
|
|
37
36
|
tokens == null || usdPerMTokens == null ? undefined : (tokens / 1_000_000) * usdPerMTokens;
|
|
38
37
|
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Track accumulated text and tool state
|
|
78
|
-
let accumulatedText = '';
|
|
79
|
-
let emittedText = '';
|
|
80
|
-
let accumulatedReasoning = '';
|
|
81
|
-
const toolsByCallId = new Map<string, Omit<BtcaStreamToolUpdatedEvent, 'type'>>();
|
|
82
|
-
let textEvents = 0;
|
|
83
|
-
let toolEvents = 0;
|
|
84
|
-
let reasoningEvents = 0;
|
|
85
|
-
|
|
86
|
-
const requestStartMs = args.requestStartMs ?? performance.now();
|
|
87
|
-
let streamStartMs = requestStartMs;
|
|
88
|
-
|
|
89
|
-
// Extract the core question for stripping echoed user message from final response
|
|
90
|
-
const coreQuestion = extractCoreQuestion(args.question);
|
|
91
|
-
|
|
92
|
-
return new ReadableStream<Uint8Array>({
|
|
93
|
-
start(controller) {
|
|
94
|
-
streamStartMs = performance.now();
|
|
95
|
-
Metrics.info('stream.start', {
|
|
96
|
-
collectionKey: args.meta.collection.key,
|
|
97
|
-
resources: args.meta.resources,
|
|
98
|
-
model: args.meta.model
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
emit(controller, args.meta);
|
|
102
|
-
|
|
103
|
-
(async () => {
|
|
104
|
-
const result = await Result.tryPromise(async () => {
|
|
105
|
-
for await (const event of args.eventStream) {
|
|
106
|
-
switch (event.type) {
|
|
107
|
-
case 'text-delta': {
|
|
108
|
-
textEvents += 1;
|
|
109
|
-
accumulatedText += event.text;
|
|
110
|
-
|
|
111
|
-
const nextText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
|
|
112
|
-
const delta = nextText.slice(emittedText.length);
|
|
113
|
-
if (delta) {
|
|
114
|
-
emittedText = nextText;
|
|
115
|
-
const msg: BtcaStreamTextDeltaEvent = {
|
|
116
|
-
type: 'text.delta',
|
|
117
|
-
delta
|
|
118
|
-
};
|
|
119
|
-
emit(controller, msg);
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
38
|
+
export const createSseStream = (args: {
|
|
39
|
+
meta: BtcaStreamMetaEvent;
|
|
40
|
+
eventStream: AsyncIterable<AgentEvent>;
|
|
41
|
+
question?: string; // Original question - used to filter echoed user message
|
|
42
|
+
requestStartMs?: number;
|
|
43
|
+
pricing?: {
|
|
44
|
+
lookup: (args: { providerId: string; modelId: string; timeoutMs?: number }) => Promise<{
|
|
45
|
+
source: 'models.dev';
|
|
46
|
+
modelKey: string;
|
|
47
|
+
ratesUsdPerMTokens: {
|
|
48
|
+
input?: number;
|
|
49
|
+
output?: number;
|
|
50
|
+
reasoning?: number;
|
|
51
|
+
cacheRead?: number;
|
|
52
|
+
cacheWrite?: number;
|
|
53
|
+
};
|
|
54
|
+
} | null>;
|
|
55
|
+
};
|
|
56
|
+
pricingTimeoutMs?: number;
|
|
57
|
+
}): ReadableStream<Uint8Array> => {
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
|
|
60
|
+
let closed = false;
|
|
61
|
+
|
|
62
|
+
const emit = (
|
|
63
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
64
|
+
event: BtcaStreamEvent
|
|
65
|
+
) => {
|
|
66
|
+
if (closed) return;
|
|
67
|
+
try {
|
|
68
|
+
controller.enqueue(encoder.encode(toSse(event)));
|
|
69
|
+
} catch {
|
|
70
|
+
// If the client disconnects/cancels, the controller may already be closed.
|
|
71
|
+
closed = true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
123
74
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
75
|
+
// Track accumulated text and tool state
|
|
76
|
+
let accumulatedText = '';
|
|
77
|
+
let emittedText = '';
|
|
78
|
+
let accumulatedReasoning = '';
|
|
79
|
+
const toolsByCallId = new Map<string, Omit<BtcaStreamToolUpdatedEvent, 'type'>>();
|
|
80
|
+
let textEvents = 0;
|
|
81
|
+
let toolEvents = 0;
|
|
82
|
+
let reasoningEvents = 0;
|
|
83
|
+
|
|
84
|
+
const requestStartMs = args.requestStartMs ?? performance.now();
|
|
85
|
+
let streamStartMs = requestStartMs;
|
|
86
|
+
|
|
87
|
+
// Extract the core question for stripping echoed user message from final response
|
|
88
|
+
const coreQuestion = extractCoreQuestion(args.question);
|
|
89
|
+
|
|
90
|
+
return new ReadableStream<Uint8Array>({
|
|
91
|
+
start(controller) {
|
|
92
|
+
streamStartMs = performance.now();
|
|
93
|
+
metricsInfo('stream.start', {
|
|
94
|
+
collectionKey: args.meta.collection.key,
|
|
95
|
+
resources: args.meta.resources,
|
|
96
|
+
model: args.meta.model
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
emit(controller, args.meta);
|
|
100
|
+
|
|
101
|
+
(async () => {
|
|
102
|
+
try {
|
|
103
|
+
for await (const event of args.eventStream) {
|
|
104
|
+
switch (event.type) {
|
|
105
|
+
case 'text-delta': {
|
|
106
|
+
textEvents += 1;
|
|
107
|
+
accumulatedText += event.text;
|
|
108
|
+
|
|
109
|
+
const nextText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
|
|
110
|
+
const delta = nextText.slice(emittedText.length);
|
|
111
|
+
if (delta) {
|
|
112
|
+
emittedText = nextText;
|
|
113
|
+
const msg: BtcaStreamTextDeltaEvent = {
|
|
114
|
+
type: 'text.delta',
|
|
115
|
+
delta
|
|
130
116
|
};
|
|
131
117
|
emit(controller, msg);
|
|
132
|
-
break;
|
|
133
118
|
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
134
121
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
input: event.input
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const update: BtcaStreamToolUpdatedEvent = {
|
|
150
|
-
type: 'tool.updated',
|
|
151
|
-
callID,
|
|
152
|
-
tool: event.toolName,
|
|
153
|
-
state: {
|
|
154
|
-
status: 'running',
|
|
155
|
-
input: event.input
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
emit(controller, update);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
122
|
+
case 'reasoning-delta': {
|
|
123
|
+
reasoningEvents += 1;
|
|
124
|
+
accumulatedReasoning += event.text;
|
|
125
|
+
const msg: BtcaStreamReasoningDeltaEvent = {
|
|
126
|
+
type: 'reasoning.delta',
|
|
127
|
+
delta: event.text
|
|
128
|
+
};
|
|
129
|
+
emit(controller, msg);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
161
132
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
133
|
+
case 'tool-call': {
|
|
134
|
+
toolEvents += 1;
|
|
135
|
+
const callID = `tool-${toolEvents}`;
|
|
136
|
+
|
|
137
|
+
// Store tool call info
|
|
138
|
+
toolsByCallId.set(callID, {
|
|
139
|
+
callID,
|
|
140
|
+
tool: event.toolName,
|
|
141
|
+
state: {
|
|
142
|
+
status: 'running',
|
|
143
|
+
input: event.input
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const update: BtcaStreamToolUpdatedEvent = {
|
|
148
|
+
type: 'tool.updated',
|
|
149
|
+
callID,
|
|
150
|
+
tool: event.toolName,
|
|
151
|
+
state: {
|
|
152
|
+
status: 'running',
|
|
153
|
+
input: event.input
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
emit(controller, update);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'tool-result': {
|
|
161
|
+
// Find the tool call and update its state
|
|
162
|
+
for (const [callID, tool] of toolsByCallId) {
|
|
163
|
+
if (tool.tool === event.toolName && tool.state?.status === 'running') {
|
|
164
|
+
tool.state = {
|
|
165
|
+
status: 'completed',
|
|
166
|
+
input: tool.state.input,
|
|
167
|
+
output: event.output
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const update: BtcaStreamToolUpdatedEvent = {
|
|
171
|
+
type: 'tool.updated',
|
|
172
|
+
callID,
|
|
173
|
+
tool: event.toolName,
|
|
174
|
+
state: tool.state
|
|
175
|
+
};
|
|
176
|
+
emit(controller, update);
|
|
177
|
+
break;
|
|
181
178
|
}
|
|
182
|
-
break;
|
|
183
179
|
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case 'finish': {
|
|
184
|
+
const finishedAtMs = performance.now();
|
|
185
|
+
const tools = Array.from(toolsByCallId.values());
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
// Strip the echoed user question from the final text
|
|
188
|
+
const finalText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
|
|
189
|
+
emittedText = finalText;
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
const usage = hasAnyDefined(event.usage as Record<string, unknown> | undefined)
|
|
192
|
+
? {
|
|
193
|
+
inputTokens: event.usage?.inputTokens,
|
|
194
|
+
outputTokens: event.usage?.outputTokens,
|
|
195
|
+
reasoningTokens: event.usage?.reasoningTokens,
|
|
196
|
+
totalTokens: event.usage?.totalTokens
|
|
197
|
+
}
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
const totalMs = Math.max(0, finishedAtMs - requestStartMs);
|
|
201
|
+
const genMs = Math.max(0, finishedAtMs - streamStartMs);
|
|
192
202
|
|
|
193
|
-
|
|
203
|
+
const throughput =
|
|
204
|
+
genMs > 0 && usage
|
|
194
205
|
? {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
outputTokensPerSecond:
|
|
207
|
+
usage.outputTokens == null
|
|
208
|
+
? undefined
|
|
209
|
+
: usage.outputTokens / (genMs / 1000),
|
|
210
|
+
totalTokensPerSecond:
|
|
211
|
+
usage.totalTokens == null ? undefined : usage.totalTokens / (genMs / 1000)
|
|
199
212
|
}
|
|
200
213
|
: undefined;
|
|
201
214
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
const pricingTimeoutMs = args.pricingTimeoutMs ?? 250;
|
|
216
|
+
const pricingLookup = args.pricing
|
|
217
|
+
? withTimeout(
|
|
218
|
+
args.pricing.lookup({
|
|
219
|
+
providerId: args.meta.model.provider,
|
|
220
|
+
modelId: args.meta.model.model,
|
|
221
|
+
timeoutMs: pricingTimeoutMs
|
|
222
|
+
}),
|
|
223
|
+
pricingTimeoutMs
|
|
224
|
+
).catch(() => null)
|
|
225
|
+
: Promise.resolve(null);
|
|
226
|
+
|
|
227
|
+
const pricingResult = await pricingLookup;
|
|
228
|
+
|
|
229
|
+
const pricing =
|
|
230
|
+
pricingResult && usage
|
|
231
|
+
? (() => {
|
|
232
|
+
const rates = pricingResult.ratesUsdPerMTokens;
|
|
233
|
+
const input = costFor(usage.inputTokens, rates.input);
|
|
234
|
+
const output = costFor(usage.outputTokens, rates.output);
|
|
235
|
+
const reasoning = costFor(usage.reasoningTokens, rates.reasoning);
|
|
236
|
+
const hasAnyCostPart = input != null || output != null || reasoning != null;
|
|
237
|
+
const total = (input ?? 0) + (output ?? 0) + (reasoning ?? 0);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
source: 'models.dev' as const,
|
|
241
|
+
modelKey: pricingResult.modelKey,
|
|
242
|
+
ratesUsdPerMTokens: rates,
|
|
243
|
+
...(hasAnyCostPart
|
|
244
|
+
? {
|
|
245
|
+
costUsd: {
|
|
246
|
+
...(input == null ? {} : { input }),
|
|
247
|
+
...(output == null ? {} : { output }),
|
|
248
|
+
...(reasoning == null ? {} : { reasoning }),
|
|
249
|
+
total
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
: {})
|
|
253
|
+
};
|
|
254
|
+
})()
|
|
255
|
+
: pricingResult
|
|
207
256
|
? {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
: usage.outputTokens / (genMs / 1000),
|
|
212
|
-
totalTokensPerSecond:
|
|
213
|
-
usage.totalTokens == null
|
|
214
|
-
? undefined
|
|
215
|
-
: usage.totalTokens / (genMs / 1000)
|
|
257
|
+
source: 'models.dev' as const,
|
|
258
|
+
modelKey: pricingResult.modelKey,
|
|
259
|
+
ratesUsdPerMTokens: pricingResult.ratesUsdPerMTokens
|
|
216
260
|
}
|
|
217
261
|
: undefined;
|
|
218
262
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
...(hasAnyCostPart
|
|
249
|
-
? {
|
|
250
|
-
costUsd: {
|
|
251
|
-
...(input == null ? {} : { input }),
|
|
252
|
-
...(output == null ? {} : { output }),
|
|
253
|
-
...(reasoning == null ? {} : { reasoning }),
|
|
254
|
-
total
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
: {})
|
|
258
|
-
};
|
|
259
|
-
})()
|
|
260
|
-
: pricingResult
|
|
261
|
-
? {
|
|
262
|
-
source: 'models.dev' as const,
|
|
263
|
-
modelKey: pricingResult.modelKey,
|
|
264
|
-
ratesUsdPerMTokens: pricingResult.ratesUsdPerMTokens
|
|
265
|
-
}
|
|
266
|
-
: undefined;
|
|
267
|
-
|
|
268
|
-
Metrics.info('stream.done', {
|
|
269
|
-
collectionKey: args.meta.collection.key,
|
|
270
|
-
textLength: finalText.length,
|
|
271
|
-
reasoningLength: accumulatedReasoning.length,
|
|
272
|
-
toolCount: tools.length,
|
|
273
|
-
textEvents,
|
|
274
|
-
toolEvents,
|
|
275
|
-
reasoningEvents,
|
|
276
|
-
finishReason: event.finishReason,
|
|
277
|
-
totalMs,
|
|
278
|
-
genMs,
|
|
279
|
-
usage,
|
|
280
|
-
pricingModelKey: pricingResult?.modelKey ?? null
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const done: BtcaStreamDoneEvent = {
|
|
284
|
-
type: 'done',
|
|
285
|
-
text: finalText,
|
|
286
|
-
reasoning: accumulatedReasoning,
|
|
287
|
-
tools,
|
|
288
|
-
...(usage ? { usage } : {}),
|
|
289
|
-
metrics: {
|
|
290
|
-
timing: { totalMs, genMs },
|
|
291
|
-
...(throughput ? { throughput } : {}),
|
|
292
|
-
...(pricing ? { pricing } : {})
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
emit(controller, done);
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
case 'error': {
|
|
300
|
-
Metrics.error('stream.error', {
|
|
301
|
-
collectionKey: args.meta.collection.key,
|
|
302
|
-
error: Metrics.errorInfo(event.error)
|
|
303
|
-
});
|
|
304
|
-
const err: BtcaStreamErrorEvent = {
|
|
305
|
-
type: 'error',
|
|
306
|
-
tag: getErrorTag(event.error),
|
|
307
|
-
message: getErrorMessage(event.error),
|
|
308
|
-
hint: getErrorHint(event.error)
|
|
309
|
-
};
|
|
310
|
-
emit(controller, err);
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
263
|
+
metricsInfo('stream.done', {
|
|
264
|
+
collectionKey: args.meta.collection.key,
|
|
265
|
+
textLength: finalText.length,
|
|
266
|
+
reasoningLength: accumulatedReasoning.length,
|
|
267
|
+
toolCount: tools.length,
|
|
268
|
+
textEvents,
|
|
269
|
+
toolEvents,
|
|
270
|
+
reasoningEvents,
|
|
271
|
+
finishReason: event.finishReason,
|
|
272
|
+
totalMs,
|
|
273
|
+
genMs,
|
|
274
|
+
usage,
|
|
275
|
+
pricingModelKey: pricingResult?.modelKey ?? null
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const done: BtcaStreamDoneEvent = {
|
|
279
|
+
type: 'done',
|
|
280
|
+
text: finalText,
|
|
281
|
+
reasoning: accumulatedReasoning,
|
|
282
|
+
tools,
|
|
283
|
+
...(usage ? { usage } : {}),
|
|
284
|
+
metrics: {
|
|
285
|
+
timing: { totalMs, genMs },
|
|
286
|
+
...(throughput ? { throughput } : {}),
|
|
287
|
+
...(pricing ? { pricing } : {})
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
emit(controller, done);
|
|
291
|
+
break;
|
|
313
292
|
}
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
293
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
294
|
+
case 'error': {
|
|
295
|
+
metricsError('stream.error', {
|
|
296
|
+
collectionKey: args.meta.collection.key,
|
|
297
|
+
error: metricsErrorInfo(event.error)
|
|
298
|
+
});
|
|
299
|
+
const err: BtcaStreamErrorEvent = {
|
|
300
|
+
type: 'error',
|
|
301
|
+
tag: getErrorTag(event.error),
|
|
302
|
+
message: getErrorMessage(event.error),
|
|
303
|
+
hint: getErrorHint(event.error)
|
|
304
|
+
};
|
|
305
|
+
emit(controller, err);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
331
308
|
}
|
|
309
|
+
}
|
|
310
|
+
} catch (cause) {
|
|
311
|
+
metricsError('stream.error', {
|
|
312
|
+
collectionKey: args.meta.collection.key,
|
|
313
|
+
error: metricsErrorInfo(cause)
|
|
332
314
|
});
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
315
|
+
const err: BtcaStreamErrorEvent = {
|
|
316
|
+
type: 'error',
|
|
317
|
+
tag: getErrorTag(cause),
|
|
318
|
+
message: getErrorMessage(cause),
|
|
319
|
+
hint: getErrorHint(cause)
|
|
320
|
+
};
|
|
321
|
+
emit(controller, err);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
{
|
|
325
|
+
metricsInfo('stream.closed', { collectionKey: args.meta.collection.key });
|
|
326
|
+
if (!closed) {
|
|
327
|
+
closed = true;
|
|
328
|
+
try {
|
|
329
|
+
controller.close();
|
|
330
|
+
} catch {
|
|
331
|
+
// Ignore double-close: cancellation/termination may have already closed the stream.
|
|
343
332
|
}
|
|
344
333
|
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
}
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
cancel() {
|
|
339
|
+
closed = true;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
};
|