@yuants/app-virtual-exchange 0.11.7 → 0.12.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.
@@ -0,0 +1,25 @@
1
+ export const createFifoQueue = () => {
2
+ let items = [];
3
+ let head = 0;
4
+ return {
5
+ enqueue: (value) => {
6
+ items.push(value);
7
+ },
8
+ dequeue: () => {
9
+ if (head >= items.length)
10
+ return undefined;
11
+ const value = items[head++];
12
+ if (head > 1024 && head * 2 > items.length) {
13
+ items = items.slice(head);
14
+ head = 0;
15
+ }
16
+ return value;
17
+ },
18
+ size: () => items.length - head,
19
+ snapshot: (limit = 20) => {
20
+ const safeLimit = Math.max(0, Math.floor(limit));
21
+ return items.slice(head, head + safeLimit);
22
+ },
23
+ };
24
+ };
25
+ //# sourceMappingURL=fifo-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fifo-queue.js","sourceRoot":"","sources":["../../src/series-data/fifo-queue.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,eAAe,GAAG,GAAiB,EAAE;IAChD,IAAI,KAAK,GAAQ,EAAE,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO;QACL,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5B,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE;gBAC1C,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC1B,IAAI,GAAG,CAAC,CAAC;aACV;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI;QAC/B,QAAQ,EAAE,CAAC,KAAK,GAAG,EAAE,EAAE,EAAE;YACvB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;YACjD,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["export interface IQueue<T> {\n enqueue: (value: T) => void;\n dequeue: () => T | undefined;\n size: () => number;\n snapshot: (limit?: number) => T[];\n}\n\nexport const createFifoQueue = <T>(): IQueue<T> => {\n let items: T[] = [];\n let head = 0;\n\n return {\n enqueue: (value) => {\n items.push(value);\n },\n dequeue: () => {\n if (head >= items.length) return undefined;\n const value = items[head++];\n if (head > 1024 && head * 2 > items.length) {\n items = items.slice(head);\n head = 0;\n }\n return value;\n },\n size: () => items.length - head,\n snapshot: (limit = 20) => {\n const safeLimit = Math.max(0, Math.floor(limit));\n return items.slice(head, head + safeLimit);\n },\n };\n};\n"]}
@@ -0,0 +1,2 @@
1
+ import './scheduler';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/series-data/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC","sourcesContent":["import './scheduler';\n"]}
@@ -0,0 +1,568 @@
1
+ var _a, _b;
2
+ import { encodeInterestRateSeriesId } from '@yuants/data-interest-rate';
3
+ import { encodeOHLCSeriesId } from '@yuants/data-ohlc';
4
+ import { parseInterestRateServiceMetadataFromSchema, parseOHLCServiceMetadataFromSchema, } from '@yuants/exchange';
5
+ import { Terminal } from '@yuants/protocol';
6
+ import { escapeSQL, requestSQL } from '@yuants/sql';
7
+ import { convertDurationToOffset, encodePath, formatTime } from '@yuants/utils';
8
+ import { catchError, defer, EMPTY, filter, from, map, mergeMap, repeat, tap, timer, toArray } from 'rxjs';
9
+ import { createFifoQueue } from './fifo-queue';
10
+ const terminal = Terminal.fromNodeEnv();
11
+ const isEnabled = process.env.VEX_SERIES_DATA_ENABLED === '1';
12
+ const onlyProductIdPrefix = ((_a = process.env.VEX_SERIES_DATA_ONLY_PRODUCT_ID_PREFIX) !== null && _a !== void 0 ? _a : '').trim();
13
+ const CONFIG = {
14
+ tickIntervalMs: 1000,
15
+ scanFullLoopIntervalMs: 5 * 60000,
16
+ /**
17
+ * Maximum number of inflight series data ingest requests.
18
+ */
19
+ maxInflight: 20,
20
+ /**
21
+ * Only run tail jobs when the global head backlog is below this threshold.
22
+ */
23
+ tailOnlyWhenGlobalHeadBelow: 20,
24
+ defaultInterestRateHeadLagMs: 8 * 60 * 60000,
25
+ defaultForwardSeedWindowMs: 24 * 60 * 60000,
26
+ maxBackoffMs: 5 * 60000,
27
+ backoffStepMs: 5000,
28
+ };
29
+ const capabilities = [];
30
+ const mapCapKeyToCapability = new Map();
31
+ const mapCapKeyToState = new Map();
32
+ const mapSeriesKeyToState = new Map();
33
+ let inflight = 0;
34
+ let scanIndex = 0;
35
+ let capRunIndex = 0;
36
+ const LOG_QUEUE_INTERVAL_MS = Number((_b = process.env.VEX_SERIES_DATA_LOG_QUEUE_INTERVAL_MS) !== null && _b !== void 0 ? _b : '10000');
37
+ const getOrCreateCapState = (capKey) => {
38
+ const existing = mapCapKeyToState.get(capKey);
39
+ if (existing)
40
+ return existing;
41
+ const next = {
42
+ capKey,
43
+ headQueue: createFifoQueue(),
44
+ tailQueue: createFifoQueue(),
45
+ pendingHead: new Set(),
46
+ pendingTail: new Set(),
47
+ inflight: false,
48
+ nextEligibleAt: 0,
49
+ backoffMs: 0,
50
+ };
51
+ mapCapKeyToState.set(capKey, next);
52
+ return next;
53
+ };
54
+ const computeSeriesId = (seriesType, product_id, duration) => {
55
+ if (seriesType === 'ohlc')
56
+ return encodeOHLCSeriesId(product_id, duration !== null && duration !== void 0 ? duration : '');
57
+ return encodeInterestRateSeriesId(product_id);
58
+ };
59
+ const getOrCreateSeriesState = (params) => {
60
+ const { capKey, seriesType, product_id, duration, direction } = params;
61
+ const table_name = seriesType === 'ohlc' ? 'ohlc_v2' : 'interest_rate';
62
+ const series_id = computeSeriesId(seriesType, product_id, duration);
63
+ const seriesKey = encodePath(table_name, direction, product_id, duration !== null && duration !== void 0 ? duration : '');
64
+ const existing = mapSeriesKeyToState.get(seriesKey);
65
+ if (existing) {
66
+ existing.capKey = capKey;
67
+ return existing;
68
+ }
69
+ const next = {
70
+ capKey,
71
+ seriesKey,
72
+ seriesType,
73
+ table_name,
74
+ series_id,
75
+ product_id,
76
+ duration,
77
+ direction,
78
+ ranges: [],
79
+ };
80
+ mapSeriesKeyToState.set(seriesKey, next);
81
+ return next;
82
+ };
83
+ const computeHeadLagMs = (series) => {
84
+ var _a;
85
+ if (series.seriesType === 'interest_rate')
86
+ return CONFIG.defaultInterestRateHeadLagMs;
87
+ const offset = convertDurationToOffset((_a = series.duration) !== null && _a !== void 0 ? _a : '');
88
+ if (!isFinite(offset) || offset <= 0)
89
+ return 60000;
90
+ return Math.max(60000, offset);
91
+ };
92
+ const computeOverlapMs = (series) => {
93
+ var _a;
94
+ const maxOverlapMs = 60 * 60000;
95
+ let overlapMs = 60000;
96
+ if (series.seriesType === 'ohlc') {
97
+ const offset = convertDurationToOffset((_a = series.duration) !== null && _a !== void 0 ? _a : '');
98
+ if (isFinite(offset) && offset > 0)
99
+ overlapMs = offset;
100
+ }
101
+ else {
102
+ overlapMs = 60 * 60000;
103
+ }
104
+ if (series.last_window_ms && isFinite(series.last_window_ms) && series.last_window_ms > 0) {
105
+ overlapMs = Math.min(overlapMs, Math.max(1, series.last_window_ms - 1));
106
+ }
107
+ return Math.max(1, Math.min(maxOverlapMs, overlapMs));
108
+ };
109
+ const applyOverlapToRequestTime = (series, baseTime) => {
110
+ const hasAnyRange = series.union_start_ms !== undefined || series.union_end_ms !== undefined || series.ranges.length > 0;
111
+ if (!hasAnyRange)
112
+ return baseTime;
113
+ const overlapMs = computeOverlapMs(series);
114
+ if (series.direction === 'backward')
115
+ return Math.min(baseTime + overlapMs, Date.now());
116
+ return Math.max(0, baseTime - overlapMs);
117
+ };
118
+ const computeBackoffMs = (current) => {
119
+ const next = current <= 0 ? CONFIG.backoffStepMs : Math.min(CONFIG.maxBackoffMs, current + CONFIG.backoffStepMs);
120
+ return next;
121
+ };
122
+ const enqueueCapJob = (capKey, kind, seriesKey) => {
123
+ const capState = getOrCreateCapState(capKey);
124
+ const pendingSet = kind === 'head' ? capState.pendingHead : capState.pendingTail;
125
+ const queue = kind === 'head' ? capState.headQueue : capState.tailQueue;
126
+ if (pendingSet.has(seriesKey))
127
+ return;
128
+ pendingSet.add(seriesKey);
129
+ queue.enqueue({ kind, seriesKey });
130
+ };
131
+ const scheduleSeries = (series) => {
132
+ const now = Date.now();
133
+ const headLagMs = computeHeadLagMs(series);
134
+ const needHead = series.union_end_ms === undefined || now - series.union_end_ms > headLagMs;
135
+ if (needHead) {
136
+ enqueueCapJob(series.capKey, 'head', series.seriesKey);
137
+ return;
138
+ }
139
+ if (series.union_start_ms === undefined)
140
+ return;
141
+ enqueueCapJob(series.capKey, 'tail', series.seriesKey);
142
+ };
143
+ const mergeRangesAndGetUnion = async (series_id, table_name) => {
144
+ const rows = await requestSQL(terminal, `
145
+ WITH locked AS (
146
+ SELECT series_id, table_name, start_time, end_time
147
+ FROM series_data_range
148
+ WHERE series_id = ${escapeSQL(series_id)} AND table_name = ${escapeSQL(table_name)}
149
+ ORDER BY start_time ASC, end_time ASC
150
+ FOR UPDATE
151
+ ),
152
+ ordered AS (
153
+ SELECT
154
+ start_time,
155
+ end_time,
156
+ max(end_time) OVER (
157
+ ORDER BY start_time ASC, end_time ASC
158
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
159
+ ) AS running_end
160
+ FROM locked
161
+ ),
162
+ marks AS (
163
+ SELECT
164
+ start_time,
165
+ end_time,
166
+ running_end,
167
+ CASE
168
+ WHEN start_time >= COALESCE(
169
+ lag(running_end) OVER (ORDER BY start_time ASC, end_time ASC),
170
+ '-infinity'::timestamptz
171
+ ) THEN 1
172
+ ELSE 0
173
+ END AS is_new_group
174
+ FROM ordered
175
+ ),
176
+ groups AS (
177
+ SELECT
178
+ start_time,
179
+ end_time,
180
+ sum(is_new_group) OVER (
181
+ ORDER BY start_time ASC, end_time ASC
182
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
183
+ ) AS grp
184
+ FROM marks
185
+ ),
186
+ merged AS (
187
+ SELECT min(start_time) AS start_time, max(end_time) AS end_time
188
+ FROM groups
189
+ GROUP BY grp
190
+ ),
191
+ to_delete AS (
192
+ SELECT l.series_id, l.table_name, l.start_time, l.end_time
193
+ FROM locked l
194
+ WHERE NOT EXISTS (
195
+ SELECT 1
196
+ FROM merged m
197
+ WHERE m.start_time = l.start_time AND m.end_time = l.end_time
198
+ )
199
+ ),
200
+ deleted AS (
201
+ DELETE FROM series_data_range t
202
+ USING to_delete d
203
+ WHERE
204
+ t.series_id = d.series_id
205
+ AND t.table_name = d.table_name
206
+ AND t.start_time = d.start_time
207
+ AND t.end_time = d.end_time
208
+ RETURNING 1
209
+ ),
210
+ to_insert AS (
211
+ SELECT m.start_time, m.end_time
212
+ FROM merged m
213
+ WHERE NOT EXISTS (
214
+ SELECT 1
215
+ FROM locked l
216
+ WHERE l.start_time = m.start_time AND l.end_time = m.end_time
217
+ )
218
+ ),
219
+ inserted AS (
220
+ INSERT INTO series_data_range (series_id, table_name, start_time, end_time)
221
+ SELECT ${escapeSQL(series_id)}, ${escapeSQL(table_name)}, start_time, end_time
222
+ FROM to_insert
223
+ ON CONFLICT DO NOTHING
224
+ RETURNING 1
225
+ )
226
+ SELECT start_time, end_time
227
+ FROM series_data_range
228
+ WHERE series_id = ${escapeSQL(series_id)} AND table_name = ${escapeSQL(table_name)}
229
+ ORDER BY start_time ASC, end_time ASC;
230
+ `);
231
+ if (rows.length === 0)
232
+ return { segments: [] };
233
+ const segments = [];
234
+ let startMs = Number.POSITIVE_INFINITY;
235
+ let endMs = Number.NEGATIVE_INFINITY;
236
+ for (const r of rows) {
237
+ const s = Date.parse(r.start_time);
238
+ const e = Date.parse(r.end_time);
239
+ if (!isNaN(s) && !isNaN(e) && e >= s) {
240
+ segments.push({ startMs: s, endMs: e });
241
+ startMs = Math.min(startMs, s);
242
+ endMs = Math.max(endMs, e);
243
+ }
244
+ }
245
+ if (!isFinite(startMs) || !isFinite(endMs))
246
+ return { segments };
247
+ return { segments, union: { startMs, endMs } };
248
+ };
249
+ const findNearestGap = (segments) => {
250
+ for (let i = segments.length - 2; i >= 0; i--) {
251
+ const left = segments[i];
252
+ const right = segments[i + 1];
253
+ if (left.endMs < right.startMs)
254
+ return { left, right };
255
+ }
256
+ };
257
+ const computeTailTime = (series, now) => {
258
+ var _a, _b, _c, _d, _e;
259
+ const gap = findNearestGap(series.ranges);
260
+ if (gap) {
261
+ return series.direction === 'backward' ? gap.right.startMs : gap.left.endMs;
262
+ }
263
+ const first = series.ranges[0];
264
+ const mostRecent = series.ranges[series.ranges.length - 1];
265
+ if (series.direction === 'backward') {
266
+ return (_c = (_b = (_a = series.union_start_ms) !== null && _a !== void 0 ? _a : first === null || first === void 0 ? void 0 : first.startMs) !== null && _b !== void 0 ? _b : mostRecent === null || mostRecent === void 0 ? void 0 : mostRecent.startMs) !== null && _c !== void 0 ? _c : now;
267
+ }
268
+ const windowMs = (_d = series.last_window_ms) !== null && _d !== void 0 ? _d : CONFIG.defaultForwardSeedWindowMs;
269
+ const start = (_e = series.union_start_ms) !== null && _e !== void 0 ? _e : Math.max(0, now - windowMs);
270
+ return Math.max(0, start - windowMs);
271
+ };
272
+ const computeRequestTime = (series, kind) => {
273
+ var _a;
274
+ const now = Date.now();
275
+ if (series.direction === 'backward') {
276
+ if (kind === 'head')
277
+ return applyOverlapToRequestTime(series, now);
278
+ return applyOverlapToRequestTime(series, computeTailTime(series, now));
279
+ }
280
+ const windowMs = (_a = series.last_window_ms) !== null && _a !== void 0 ? _a : CONFIG.defaultForwardSeedWindowMs;
281
+ if (kind === 'head') {
282
+ if (series.union_end_ms !== undefined)
283
+ return applyOverlapToRequestTime(series, series.union_end_ms);
284
+ return applyOverlapToRequestTime(series, Math.max(0, now - windowMs));
285
+ }
286
+ return applyOverlapToRequestTime(series, computeTailTime(series, now));
287
+ };
288
+ const requestIngest = async (series, time) => {
289
+ var _a;
290
+ if (series.seriesType === 'ohlc') {
291
+ const req = {
292
+ product_id: series.product_id,
293
+ duration: (_a = series.duration) !== null && _a !== void 0 ? _a : '',
294
+ direction: series.direction,
295
+ time,
296
+ };
297
+ return terminal.client.requestForResponseData('IngestOHLC', req);
298
+ }
299
+ const req = {
300
+ product_id: series.product_id,
301
+ direction: series.direction,
302
+ time,
303
+ };
304
+ return terminal.client.requestForResponseData('IngestInterestRate', req);
305
+ };
306
+ const executeJob = async (capState, job) => {
307
+ const series = mapSeriesKeyToState.get(job.seriesKey);
308
+ if (!series)
309
+ return;
310
+ if (job.kind === 'head')
311
+ capState.pendingHead.delete(job.seriesKey);
312
+ else
313
+ capState.pendingTail.delete(job.seriesKey);
314
+ try {
315
+ // make sure each request overlaps existing ranges
316
+ const time = computeRequestTime(series, job.kind);
317
+ const result = await requestIngest(series, time);
318
+ capState.backoffMs = 0;
319
+ capState.nextEligibleAt = 0;
320
+ if (!result.range) {
321
+ capState.backoffMs = computeBackoffMs(capState.backoffMs);
322
+ capState.nextEligibleAt = Date.now() + capState.backoffMs;
323
+ scheduleSeries(series);
324
+ return;
325
+ }
326
+ const startMs = Date.parse(result.range.start_time);
327
+ const endMs = Date.parse(result.range.end_time);
328
+ if (!isNaN(startMs) && !isNaN(endMs) && endMs > startMs) {
329
+ series.last_window_ms = endMs - startMs;
330
+ }
331
+ const merged = await mergeRangesAndGetUnion(series.series_id, series.table_name);
332
+ series.ranges = merged.segments;
333
+ if (merged.union) {
334
+ series.union_start_ms = merged.union.startMs;
335
+ series.union_end_ms = merged.union.endMs;
336
+ }
337
+ scheduleSeries(series);
338
+ }
339
+ catch (e) {
340
+ capState.backoffMs = computeBackoffMs(capState.backoffMs);
341
+ capState.nextEligibleAt = Date.now() + capState.backoffMs;
342
+ console.warn(formatTime(Date.now()), '[VEX][SeriesData]CapFailed', `cap=${capState.capKey}`, `backoff_ms=${capState.backoffMs}`, `${e}`);
343
+ scheduleSeries(series);
344
+ }
345
+ };
346
+ const computeGlobalHeadBacklog = () => {
347
+ let total = 0;
348
+ for (const s of mapCapKeyToState.values()) {
349
+ total += s.headQueue.size();
350
+ }
351
+ return total;
352
+ };
353
+ const pickNextRunnableCap = (now) => {
354
+ if (capabilities.length === 0)
355
+ return;
356
+ const n = capabilities.length;
357
+ for (let i = 0; i < n; i++) {
358
+ // Round-robin pick
359
+ const cap = capabilities[capRunIndex++ % n];
360
+ const capState = getOrCreateCapState(cap.capKey);
361
+ if (capState.inflight)
362
+ continue;
363
+ if (capState.nextEligibleAt > now)
364
+ continue;
365
+ if (capState.headQueue.size() === 0 && capState.tailQueue.size() === 0)
366
+ continue;
367
+ return capState;
368
+ }
369
+ };
370
+ const dequeueFromCap = (capState, canRunTail) => {
371
+ const head = capState.headQueue.dequeue();
372
+ if (head)
373
+ return { job: head, kind: 'head' };
374
+ if (!canRunTail)
375
+ return;
376
+ const tail = capState.tailQueue.dequeue();
377
+ if (!tail)
378
+ return;
379
+ return { job: tail, kind: 'tail' };
380
+ };
381
+ const scanProductsOnce = async () => {
382
+ var _a;
383
+ if (capabilities.length === 0)
384
+ return;
385
+ const now = Date.now();
386
+ const cap = capabilities[scanIndex++ % capabilities.length];
387
+ if (cap.nextScanAt > now)
388
+ return;
389
+ const where = [
390
+ `product_id LIKE ${escapeSQL(`${cap.product_id_prefix}%`)}`,
391
+ onlyProductIdPrefix ? `product_id LIKE ${escapeSQL(`${onlyProductIdPrefix}%`)}` : '',
392
+ cap.seriesType === 'interest_rate' ? `COALESCE(no_interest_rate, false) = false` : '',
393
+ ]
394
+ .filter(Boolean)
395
+ .join(' AND ');
396
+ const rows = await requestSQL(terminal, `
397
+ SELECT product_id
398
+ FROM product
399
+ WHERE ${where}
400
+ ORDER BY product_id ASC
401
+ `);
402
+ cap.nextScanAt = now + CONFIG.scanFullLoopIntervalMs;
403
+ for (const row of rows) {
404
+ if (cap.seriesType === 'ohlc') {
405
+ for (const duration of (_a = cap.duration_list) !== null && _a !== void 0 ? _a : []) {
406
+ const series = getOrCreateSeriesState({
407
+ capKey: cap.capKey,
408
+ seriesType: 'ohlc',
409
+ product_id: row.product_id,
410
+ duration,
411
+ direction: cap.direction,
412
+ });
413
+ scheduleSeries(series);
414
+ }
415
+ }
416
+ else {
417
+ const series = getOrCreateSeriesState({
418
+ capKey: cap.capKey,
419
+ seriesType: 'interest_rate',
420
+ product_id: row.product_id,
421
+ direction: cap.direction,
422
+ });
423
+ scheduleSeries(series);
424
+ }
425
+ }
426
+ };
427
+ const tick = async () => {
428
+ if (!isEnabled)
429
+ return;
430
+ try {
431
+ await scanProductsOnce();
432
+ }
433
+ catch (e) {
434
+ console.error(formatTime(Date.now()), '[VEX][SeriesData]ScanFailed', `${e}`);
435
+ }
436
+ const now = Date.now();
437
+ while (inflight < CONFIG.maxInflight) {
438
+ const capState = pickNextRunnableCap(now);
439
+ if (!capState)
440
+ return;
441
+ const globalHeadBacklog = computeGlobalHeadBacklog();
442
+ const canRunTail = globalHeadBacklog < CONFIG.tailOnlyWhenGlobalHeadBelow && capState.headQueue.size() === 0;
443
+ const next = dequeueFromCap(capState, canRunTail);
444
+ if (!next)
445
+ return;
446
+ capState.inflight = true;
447
+ inflight++;
448
+ executeJob(capState, next.job).finally(() => {
449
+ capState.inflight = false;
450
+ inflight--;
451
+ });
452
+ }
453
+ };
454
+ if (!isEnabled) {
455
+ console.info(formatTime(Date.now()), '[VEX][SeriesData]Disabled', 'VEX_SERIES_DATA_ENABLED!=1');
456
+ }
457
+ else {
458
+ terminal.server.provideService('VEX/SeriesData/Peek', {}, async () => {
459
+ const now = Date.now();
460
+ const caps = capabilities.map((cap) => {
461
+ const s = getOrCreateCapState(cap.capKey);
462
+ return {
463
+ capKey: cap.capKey,
464
+ method: cap.method,
465
+ product_id_prefix: cap.product_id_prefix,
466
+ direction: cap.direction,
467
+ head_queue_size: s.headQueue.size(),
468
+ tail_queue_size: s.tailQueue.size(),
469
+ inflight: s.inflight,
470
+ backoff_ms: s.backoffMs,
471
+ nextEligibleAt: s.nextEligibleAt > now ? formatTime(s.nextEligibleAt) : undefined,
472
+ };
473
+ });
474
+ return {
475
+ res: {
476
+ code: 0,
477
+ message: 'OK',
478
+ data: {
479
+ enabled: true,
480
+ only_product_id_prefix: onlyProductIdPrefix || undefined,
481
+ inflight,
482
+ cap_count: capabilities.length,
483
+ global_head_backlog: computeGlobalHeadBacklog(),
484
+ series_count: mapSeriesKeyToState.size,
485
+ caps: caps.slice(0, 50),
486
+ },
487
+ },
488
+ };
489
+ });
490
+ // Setup trace log
491
+ timer(0, LOG_QUEUE_INTERVAL_MS).subscribe(() => {
492
+ const now = Date.now();
493
+ const caps = capabilities
494
+ .map((cap) => {
495
+ const s = getOrCreateCapState(cap.capKey);
496
+ return {
497
+ capKey: cap.capKey,
498
+ head: s.headQueue.size(),
499
+ tail: s.tailQueue.size(),
500
+ inflight: s.inflight,
501
+ backoffMs: s.backoffMs,
502
+ nextEligibleAt: s.nextEligibleAt,
503
+ };
504
+ })
505
+ .filter((x) => x.inflight || x.head > 0 || x.tail > 0 || x.nextEligibleAt > now)
506
+ .slice(0, 20)
507
+ .map((x) => `cap=${x.capKey} head=${x.head} tail=${x.tail} inflight=${x.inflight ? 1 : 0} backoff_ms=${x.backoffMs}${x.nextEligibleAt > now ? ` next=${formatTime(x.nextEligibleAt)}` : ''}`);
508
+ console.info(formatTime(now), '[VEX][SeriesData]Queues', `inflight=${inflight}`, `cap_count=${capabilities.length}`, `global_head_backlog=${computeGlobalHeadBacklog()}`, `series_count=${mapSeriesKeyToState.size}`, caps.length ? `caps=[${caps.join(' | ')}]` : 'caps=[]');
509
+ });
510
+ terminal.terminalInfos$
511
+ .pipe(mergeMap((terminalInfos) => from(terminalInfos).pipe(mergeMap((terminalInfo) => from(Object.values(terminalInfo.serviceInfo || {})).pipe(filter((serviceInfo) => serviceInfo.method === 'IngestOHLC' || serviceInfo.method === 'IngestInterestRate'), map((serviceInfo) => {
512
+ try {
513
+ if (serviceInfo.method === 'IngestOHLC') {
514
+ const meta = parseOHLCServiceMetadataFromSchema(serviceInfo.schema);
515
+ const capKey = encodePath('IngestOHLC', meta.product_id_prefix, meta.direction);
516
+ const existing = mapCapKeyToCapability.get(capKey);
517
+ const duration_list = [...new Set(meta.duration_list)].sort();
518
+ if (existing) {
519
+ existing.duration_list = duration_list;
520
+ return existing;
521
+ }
522
+ return {
523
+ capKey,
524
+ method: 'IngestOHLC',
525
+ seriesType: 'ohlc',
526
+ product_id_prefix: meta.product_id_prefix,
527
+ direction: meta.direction,
528
+ duration_list,
529
+ nextScanAt: 0,
530
+ };
531
+ }
532
+ const meta = parseInterestRateServiceMetadataFromSchema(serviceInfo.schema);
533
+ const capKey = encodePath('IngestInterestRate', meta.product_id_prefix, meta.direction);
534
+ const existing = mapCapKeyToCapability.get(capKey);
535
+ if (existing)
536
+ return existing;
537
+ return {
538
+ capKey,
539
+ method: 'IngestInterestRate',
540
+ seriesType: 'interest_rate',
541
+ product_id_prefix: meta.product_id_prefix,
542
+ direction: meta.direction,
543
+ nextScanAt: 0,
544
+ };
545
+ }
546
+ catch (_a) {
547
+ console.info(formatTime(Date.now()), '[VEX][SeriesData]ParseServiceMetadataFailed', `method=${serviceInfo.method}`, `terminal_id=${terminalInfo.terminal_id}`);
548
+ return;
549
+ }
550
+ }), filter((x) => !!x))), toArray(), tap((nextCaps) => {
551
+ const nextMap = new Map();
552
+ for (const cap of nextCaps) {
553
+ nextMap.set(cap.capKey, cap);
554
+ }
555
+ mapCapKeyToCapability.clear();
556
+ nextMap.forEach((v, k) => mapCapKeyToCapability.set(k, v));
557
+ capabilities.length = 0;
558
+ nextMap.forEach((v) => capabilities.push(v));
559
+ }))))
560
+ .subscribe();
561
+ defer(() => from(tick()))
562
+ .pipe(catchError((e) => {
563
+ console.error(formatTime(Date.now()), '[VEX][SeriesData]TickError', `${e}`);
564
+ return EMPTY;
565
+ }), repeat({ delay: CONFIG.tickIntervalMs }))
566
+ .subscribe();
567
+ }
568
+ //# sourceMappingURL=scheduler.js.map