@yuants/app-virtual-exchange 0.10.0 → 0.10.2
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/dist/quote/service.js +102 -29
- package/dist/quote/service.js.map +1 -1
- package/dist/quote/types.js.map +1 -1
- package/dist/quote/upstream/executor.js +98 -0
- package/dist/quote/upstream/executor.js.map +1 -0
- package/dist/quote/upstream/index.js +2 -0
- package/dist/quote/upstream/index.js.map +1 -0
- package/dist/quote/upstream/prefix-matcher.js.map +1 -0
- package/dist/quote/upstream/registry.js +116 -0
- package/dist/quote/upstream/registry.js.map +1 -0
- package/dist/quote/upstream/router.js +119 -0
- package/dist/quote/upstream/router.js.map +1 -0
- package/lib/quote/service.js +101 -28
- package/lib/quote/service.js.map +1 -1
- package/lib/quote/types.d.ts +18 -0
- package/lib/quote/types.d.ts.map +1 -1
- package/lib/quote/types.js.map +1 -1
- package/lib/quote/upstream/executor.d.ts +8 -0
- package/lib/quote/upstream/executor.d.ts.map +1 -0
- package/lib/quote/upstream/executor.js +102 -0
- package/lib/quote/upstream/executor.js.map +1 -0
- package/lib/quote/upstream/index.d.ts +3 -0
- package/lib/quote/upstream/index.d.ts.map +1 -0
- package/lib/quote/upstream/index.js +6 -0
- package/lib/quote/upstream/index.js.map +1 -0
- package/lib/quote/upstream/prefix-matcher.d.ts.map +1 -0
- package/lib/quote/upstream/prefix-matcher.js.map +1 -0
- package/lib/quote/upstream/registry.d.ts +18 -0
- package/lib/quote/upstream/registry.d.ts.map +1 -0
- package/lib/quote/upstream/registry.js +120 -0
- package/lib/quote/upstream/registry.js.map +1 -0
- package/lib/quote/upstream/router.d.ts +27 -0
- package/lib/quote/upstream/router.d.ts.map +1 -0
- package/lib/quote/upstream/router.js +124 -0
- package/lib/quote/upstream/router.js.map +1 -0
- package/package.json +1 -1
- package/temp/package-deps.json +11 -8
- package/dist/quote/prefix-matcher.js.map +0 -1
- package/dist/quote/request-key.js +0 -20
- package/dist/quote/request-key.js.map +0 -1
- package/dist/quote/upstream-routing.js +0 -300
- package/dist/quote/upstream-routing.js.map +0 -1
- package/lib/quote/prefix-matcher.d.ts.map +0 -1
- package/lib/quote/prefix-matcher.js.map +0 -1
- package/lib/quote/request-key.d.ts +0 -2
- package/lib/quote/request-key.d.ts.map +0 -1
- package/lib/quote/request-key.js +0 -24
- package/lib/quote/request-key.js.map +0 -1
- package/lib/quote/upstream-routing.d.ts +0 -15
- package/lib/quote/upstream-routing.d.ts.map +0 -1
- package/lib/quote/upstream-routing.js +0 -304
- package/lib/quote/upstream-routing.js.map +0 -1
- /package/dist/quote/{prefix-matcher.js → upstream/prefix-matcher.js} +0 -0
- /package/lib/quote/{prefix-matcher.d.ts → upstream/prefix-matcher.d.ts} +0 -0
- /package/lib/quote/{prefix-matcher.js → upstream/prefix-matcher.js} +0 -0
package/lib/quote/service.js
CHANGED
|
@@ -2,30 +2,108 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const protocol_1 = require("@yuants/protocol");
|
|
4
4
|
const utils_1 = require("@yuants/utils");
|
|
5
|
+
const rxjs_1 = require("rxjs");
|
|
5
6
|
const state_1 = require("./state");
|
|
6
|
-
const
|
|
7
|
+
const upstream_1 = require("./upstream");
|
|
7
8
|
const terminal = protocol_1.Terminal.fromNodeEnv();
|
|
8
9
|
const quoteState = (0, state_1.createQuoteState)();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
10
|
+
const quoteProviderRegistry = (0, upstream_1.createQuoteProviderRegistry)(terminal);
|
|
11
|
+
const normalizeStrings = (values) => [...new Set(values)].sort();
|
|
12
|
+
const normalizeFields = (values) => [...new Set(values)].sort();
|
|
13
|
+
const analyzeRequestedQuotes = (quoteState, product_ids, fields, updated_at) => {
|
|
14
|
+
const missing = [];
|
|
15
|
+
const needUpdate = [];
|
|
14
16
|
for (const product_id of product_ids) {
|
|
15
17
|
for (const field of fields) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const tuple = quoteState.getValueTuple(product_id, field);
|
|
19
|
+
if (tuple === undefined) {
|
|
20
|
+
missing.push({ product_id, field });
|
|
21
|
+
needUpdate.push({ product_id, field });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (tuple[1] < updated_at) {
|
|
25
|
+
needUpdate.push({ product_id, field });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { missing, needUpdate };
|
|
30
|
+
};
|
|
31
|
+
const filterLatest = (quoteState, product_ids, fields) => {
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const product_id of product_ids) {
|
|
34
|
+
result[product_id] = {};
|
|
35
|
+
for (const field of fields) {
|
|
36
|
+
const tuple = quoteState.getValueTuple(product_id, field);
|
|
37
|
+
if (tuple) {
|
|
38
|
+
result[product_id][field] = tuple;
|
|
18
39
|
}
|
|
19
40
|
}
|
|
20
41
|
}
|
|
21
|
-
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
const assertNotMissing = (missing, updated_at) => {
|
|
45
|
+
if (missing.length > 0) {
|
|
22
46
|
throw (0, utils_1.newError)('VEX_QUOTE_FRESHNESS_NOT_SATISFIED', {
|
|
23
47
|
updated_at,
|
|
24
|
-
missed:
|
|
25
|
-
missed_total:
|
|
48
|
+
missed: missing.slice(0, 200),
|
|
49
|
+
missed_total: missing.length,
|
|
26
50
|
});
|
|
27
51
|
}
|
|
28
52
|
};
|
|
53
|
+
const updateQueue$ = new rxjs_1.Subject();
|
|
54
|
+
const updateQueueStats = {
|
|
55
|
+
queued_total: 0,
|
|
56
|
+
started_total: 0,
|
|
57
|
+
processed_total: 0,
|
|
58
|
+
};
|
|
59
|
+
const getQueueStatus = () => {
|
|
60
|
+
const pending = updateQueueStats.queued_total - updateQueueStats.started_total;
|
|
61
|
+
const in_flight = updateQueueStats.started_total - updateQueueStats.processed_total;
|
|
62
|
+
return Object.assign({ pending,
|
|
63
|
+
in_flight }, updateQueueStats);
|
|
64
|
+
};
|
|
65
|
+
const enqueueUpdateTask = (task) => {
|
|
66
|
+
updateQueueStats.queued_total++;
|
|
67
|
+
updateQueueStats.last_enqueued_at = Date.now();
|
|
68
|
+
updateQueue$.next(task);
|
|
69
|
+
};
|
|
70
|
+
const summarizeError = (error) => {
|
|
71
|
+
if (typeof error === 'object' && error !== null) {
|
|
72
|
+
const code = 'code' in error ? error.code : undefined;
|
|
73
|
+
const message = 'message' in error ? error.message : undefined;
|
|
74
|
+
return {
|
|
75
|
+
code: typeof code === 'string' ? code : undefined,
|
|
76
|
+
message: typeof message === 'string' ? message : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {};
|
|
80
|
+
};
|
|
81
|
+
const processUpdateTask = async (task) => {
|
|
82
|
+
const { needUpdate } = analyzeRequestedQuotes(quoteState, task.product_ids, task.fields, task.updated_at);
|
|
83
|
+
await quoteProviderRegistry.fillQuoteStateFromUpstream({
|
|
84
|
+
quoteState,
|
|
85
|
+
cacheMissed: needUpdate,
|
|
86
|
+
updated_at: task.updated_at,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
updateQueue$
|
|
90
|
+
.pipe((0, rxjs_1.concatMap)((task) => (0, rxjs_1.defer)(async () => {
|
|
91
|
+
updateQueueStats.started_total++;
|
|
92
|
+
updateQueueStats.last_started_at = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
await processUpdateTask(task);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const summary = summarizeError(error);
|
|
98
|
+
updateQueueStats.last_error = Object.assign({ at: Date.now() }, summary);
|
|
99
|
+
console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]UpdateQueueTaskFailed`, `product_ids=${task.product_ids.length} fields=${task.fields.length} updated_at=${task.updated_at}`, JSON.stringify(summary));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
updateQueueStats.processed_total++;
|
|
103
|
+
updateQueueStats.last_processed_at = Date.now();
|
|
104
|
+
}
|
|
105
|
+
})))
|
|
106
|
+
.subscribe();
|
|
29
107
|
terminal.server.provideService('VEX/UpdateQuotes', {}, async (msg) => {
|
|
30
108
|
quoteState.update(msg.req);
|
|
31
109
|
return { res: { code: 0, message: 'OK' } };
|
|
@@ -33,18 +111,6 @@ terminal.server.provideService('VEX/UpdateQuotes', {}, async (msg) => {
|
|
|
33
111
|
terminal.server.provideService('VEX/DumpQuoteState', {}, async () => {
|
|
34
112
|
return { res: { code: 0, message: 'OK', data: quoteState.dumpAsObject() } };
|
|
35
113
|
});
|
|
36
|
-
const computeCacheMissed = (quoteState, product_ids, fields, updated_at) => {
|
|
37
|
-
const cacheMissed = [];
|
|
38
|
-
for (const product_id of product_ids) {
|
|
39
|
-
for (const field of fields) {
|
|
40
|
-
const tuple = quoteState.getValueTuple(product_id, field);
|
|
41
|
-
if (tuple === undefined || tuple[1] < updated_at) {
|
|
42
|
-
cacheMissed.push({ product_id, field });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return cacheMissed;
|
|
47
|
-
};
|
|
48
114
|
terminal.server.provideService('VEX/QueryQuotes', {
|
|
49
115
|
type: 'object',
|
|
50
116
|
required: ['product_ids', 'fields', 'updated_at'],
|
|
@@ -60,11 +126,18 @@ terminal.server.provideService('VEX/QueryQuotes', {
|
|
|
60
126
|
updated_at: { type: 'number' },
|
|
61
127
|
},
|
|
62
128
|
}, async (msg) => {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
129
|
+
const product_ids = normalizeStrings(msg.req.product_ids);
|
|
130
|
+
const fields = normalizeFields(msg.req.fields);
|
|
131
|
+
const { updated_at } = msg.req;
|
|
132
|
+
const { missing, needUpdate } = analyzeRequestedQuotes(quoteState, product_ids, fields, updated_at);
|
|
133
|
+
if (needUpdate.length > 0) {
|
|
134
|
+
enqueueUpdateTask({ product_ids, fields, updated_at });
|
|
135
|
+
}
|
|
136
|
+
const data = filterLatest(quoteState, product_ids, fields);
|
|
137
|
+
assertNotMissing(missing, updated_at);
|
|
68
138
|
return { res: { code: 0, message: 'OK', data } };
|
|
69
139
|
});
|
|
140
|
+
terminal.server.provideService('VEX/QuoteUpdateQueueStatus', {}, async () => {
|
|
141
|
+
return { res: { code: 0, message: 'OK', data: getQueueStatus() } };
|
|
142
|
+
});
|
|
70
143
|
//# sourceMappingURL=service.js.map
|
package/lib/quote/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/quote/service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Terminal } from '@yuants/protocol';\nimport { newError } from '@yuants/utils';\nimport { createQuoteState } from './state';\nimport { IQuoteKey, IQuoteState, IQuoteUpdateAction } from './types';\nimport { fillQuoteStateFromUpstream, IQuoteMiss } from './upstream-routing';\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteState = createQuoteState();\n\nconst assertFreshnessSatisfied = (\n data: IQuoteUpdateAction,\n params: { product_ids: string[]; fields: IQuoteKey[]; updated_at: number },\n) => {\n console.info(\n '[VEX][Quote] Asserting freshness satisfied for requested quotes.',\n JSON.stringify(params),\n JSON.stringify(data),\n );\n const { product_ids, fields, updated_at } = params;\n const stillMissed: Array<{ product_id: string; field: IQuoteKey }> = [];\n for (const product_id of product_ids) {\n for (const field of fields) {\n if (!data[product_id]?.[field]) {\n stillMissed.push({ product_id, field });\n }\n }\n }\n if (stillMissed.length > 0) {\n throw newError('VEX_QUOTE_FRESHNESS_NOT_SATISFIED', {\n updated_at,\n missed: stillMissed.slice(0, 200),\n missed_total: stillMissed.length,\n });\n }\n};\n\nterminal.server.provideService<IQuoteUpdateAction>('VEX/UpdateQuotes', {}, async (msg) => {\n quoteState.update(msg.req);\n return { res: { code: 0, message: 'OK' } };\n});\n\nterminal.server.provideService<{}, IQuoteUpdateAction>('VEX/DumpQuoteState', {}, async () => {\n return { res: { code: 0, message: 'OK', data: quoteState.dumpAsObject() } };\n});\n\nconst computeCacheMissed = (\n quoteState: IQuoteState,\n product_ids: string[],\n fields: IQuoteKey[],\n updated_at: number,\n): IQuoteMiss[] => {\n const cacheMissed: IQuoteMiss[] = [];\n for (const product_id of product_ids) {\n for (const field of fields) {\n const tuple = quoteState.getValueTuple(product_id, field);\n if (tuple === undefined || tuple[1] < updated_at) {\n cacheMissed.push({ product_id, field });\n }\n }\n }\n return cacheMissed;\n};\n\nterminal.server.provideService<\n { product_ids: string[]; fields: IQuoteKey[]; updated_at: number },\n IQuoteUpdateAction\n>(\n 'VEX/QueryQuotes',\n {\n type: 'object',\n required: ['product_ids', 'fields', 'updated_at'],\n properties: {\n product_ids: {\n type: 'array',\n items: { type: 'string' },\n },\n fields: {\n type: 'array',\n items: { type: 'string' },\n },\n updated_at: { type: 'number' },\n },\n },\n async (msg) => {\n const { product_ids, fields, updated_at } = msg.req;\n\n const cacheMissed = computeCacheMissed(quoteState, product_ids, fields, updated_at);\n await fillQuoteStateFromUpstream({ terminal, quoteState, cacheMissed, updated_at });\n\n const data = quoteState.filter(product_ids, fields, updated_at);\n assertFreshnessSatisfied(data, { product_ids, fields, updated_at });\n return { res: { code: 0, message: 'OK', data } };\n },\n);\n"]}
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/quote/service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Terminal } from '@yuants/protocol';\nimport { formatTime, newError } from '@yuants/utils';\nimport { Subject, concatMap, defer } from 'rxjs';\nimport { createQuoteState } from './state';\nimport { IQuoteKey, IQuoteRequire, IQuoteState, IQuoteUpdateAction } from './types';\nimport { createQuoteProviderRegistry } from './upstream';\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteState = createQuoteState();\nconst quoteProviderRegistry = createQuoteProviderRegistry(terminal);\n\ntype UpdateTask = { product_ids: string[]; fields: IQuoteKey[]; updated_at: number };\n\ntype IQuoteUpdateQueueStatus = {\n pending: number;\n in_flight: number;\n queued_total: number;\n started_total: number;\n processed_total: number;\n last_enqueued_at?: number;\n last_started_at?: number;\n last_processed_at?: number;\n last_error?: { at: number; code?: string; message?: string };\n};\n\nconst normalizeStrings = (values: string[]) => [...new Set(values)].sort();\nconst normalizeFields = (values: IQuoteKey[]) => [...new Set(values)].sort();\n\nconst analyzeRequestedQuotes = (\n quoteState: IQuoteState,\n product_ids: string[],\n fields: IQuoteKey[],\n updated_at: number,\n): { missing: IQuoteRequire[]; needUpdate: IQuoteRequire[] } => {\n const missing: IQuoteRequire[] = [];\n const needUpdate: IQuoteRequire[] = [];\n for (const product_id of product_ids) {\n for (const field of fields) {\n const tuple = quoteState.getValueTuple(product_id, field);\n if (tuple === undefined) {\n missing.push({ product_id, field });\n needUpdate.push({ product_id, field });\n continue;\n }\n if (tuple[1] < updated_at) {\n needUpdate.push({ product_id, field });\n }\n }\n }\n return { missing, needUpdate };\n};\n\nconst filterLatest = (\n quoteState: IQuoteState,\n product_ids: string[],\n fields: IQuoteKey[],\n): IQuoteUpdateAction => {\n const result: IQuoteUpdateAction = {};\n for (const product_id of product_ids) {\n result[product_id] = {};\n for (const field of fields) {\n const tuple = quoteState.getValueTuple(product_id, field);\n if (tuple) {\n result[product_id][field] = tuple;\n }\n }\n }\n return result;\n};\n\nconst assertNotMissing = (missing: IQuoteRequire[], updated_at: number) => {\n if (missing.length > 0) {\n throw newError('VEX_QUOTE_FRESHNESS_NOT_SATISFIED', {\n updated_at,\n missed: missing.slice(0, 200),\n missed_total: missing.length,\n });\n }\n};\n\nconst updateQueue$ = new Subject<UpdateTask>();\nconst updateQueueStats: Omit<IQuoteUpdateQueueStatus, 'pending' | 'in_flight'> = {\n queued_total: 0,\n started_total: 0,\n processed_total: 0,\n};\n\nconst getQueueStatus = (): IQuoteUpdateQueueStatus => {\n const pending = updateQueueStats.queued_total - updateQueueStats.started_total;\n const in_flight = updateQueueStats.started_total - updateQueueStats.processed_total;\n return {\n pending,\n in_flight,\n ...updateQueueStats,\n };\n};\n\nconst enqueueUpdateTask = (task: UpdateTask) => {\n updateQueueStats.queued_total++;\n updateQueueStats.last_enqueued_at = Date.now();\n updateQueue$.next(task);\n};\n\nconst summarizeError = (error: unknown): { code?: string; message?: string } => {\n if (typeof error === 'object' && error !== null) {\n const code = 'code' in error ? (error as any).code : undefined;\n const message = 'message' in error ? (error as any).message : undefined;\n return {\n code: typeof code === 'string' ? code : undefined,\n message: typeof message === 'string' ? message : undefined,\n };\n }\n return {};\n};\n\nconst processUpdateTask = async (task: UpdateTask) => {\n const { needUpdate } = analyzeRequestedQuotes(quoteState, task.product_ids, task.fields, task.updated_at);\n await quoteProviderRegistry.fillQuoteStateFromUpstream({\n quoteState,\n cacheMissed: needUpdate,\n updated_at: task.updated_at,\n });\n};\n\nupdateQueue$\n .pipe(\n concatMap((task) =>\n defer(async () => {\n updateQueueStats.started_total++;\n updateQueueStats.last_started_at = Date.now();\n\n try {\n await processUpdateTask(task);\n } catch (error) {\n const summary = summarizeError(error);\n updateQueueStats.last_error = { at: Date.now(), ...summary };\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]UpdateQueueTaskFailed`,\n `product_ids=${task.product_ids.length} fields=${task.fields.length} updated_at=${task.updated_at}`,\n JSON.stringify(summary),\n );\n } finally {\n updateQueueStats.processed_total++;\n updateQueueStats.last_processed_at = Date.now();\n }\n }),\n ),\n )\n .subscribe();\n\nterminal.server.provideService<IQuoteUpdateAction>('VEX/UpdateQuotes', {}, async (msg) => {\n quoteState.update(msg.req);\n return { res: { code: 0, message: 'OK' } };\n});\n\nterminal.server.provideService<{}, IQuoteUpdateAction>('VEX/DumpQuoteState', {}, async () => {\n return { res: { code: 0, message: 'OK', data: quoteState.dumpAsObject() } };\n});\n\nterminal.server.provideService<\n { product_ids: string[]; fields: IQuoteKey[]; updated_at: number },\n IQuoteUpdateAction\n>(\n 'VEX/QueryQuotes',\n {\n type: 'object',\n required: ['product_ids', 'fields', 'updated_at'],\n properties: {\n product_ids: {\n type: 'array',\n items: { type: 'string' },\n },\n fields: {\n type: 'array',\n items: { type: 'string' },\n },\n updated_at: { type: 'number' },\n },\n },\n async (msg) => {\n const product_ids = normalizeStrings(msg.req.product_ids);\n const fields = normalizeFields(msg.req.fields);\n const { updated_at } = msg.req;\n\n const { missing, needUpdate } = analyzeRequestedQuotes(quoteState, product_ids, fields, updated_at);\n if (needUpdate.length > 0) {\n enqueueUpdateTask({ product_ids, fields, updated_at });\n }\n\n const data = filterLatest(quoteState, product_ids, fields);\n assertNotMissing(missing, updated_at);\n return { res: { code: 0, message: 'OK', data } };\n },\n);\n\nterminal.server.provideService<{}, IQuoteUpdateQueueStatus>('VEX/QuoteUpdateQueueStatus', {}, async () => {\n return { res: { code: 0, message: 'OK', data: getQueueStatus() } };\n});\n"]}
|
package/lib/quote/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IQuote } from '@yuants/data-quote';
|
|
2
|
+
import { IQuoteServiceMetadata } from '@yuants/exchange';
|
|
2
3
|
export declare type IQuoteKey = Exclude<keyof IQuote, 'datasource_id' | 'product_id' | 'updated_at'>;
|
|
3
4
|
/**
|
|
4
5
|
* 用于批量更新的数据结构
|
|
@@ -27,4 +28,21 @@ export interface IQuoteState {
|
|
|
27
28
|
getValueTuple: (product_id: string, field: IQuoteKey) => [string, number] | undefined;
|
|
28
29
|
filter: (product_ids: string[], fields: IQuoteKey[], updated_at: number) => IQuoteUpdateAction;
|
|
29
30
|
}
|
|
31
|
+
export interface IQuoteProviderInstance {
|
|
32
|
+
terminal_id: string;
|
|
33
|
+
service_id: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A "provider group" is a capability signature of `GetQuotes`.
|
|
37
|
+
* Multiple vendor terminals may provide the same capability; VEX load-balances across instances.
|
|
38
|
+
*/
|
|
39
|
+
export interface IQuoteProviderGroup {
|
|
40
|
+
group_id: string;
|
|
41
|
+
meta: IQuoteServiceMetadata;
|
|
42
|
+
mapTerminalIdToInstance: Map<string, IQuoteProviderInstance>;
|
|
43
|
+
}
|
|
44
|
+
export interface IQuoteRequire {
|
|
45
|
+
product_id: string;
|
|
46
|
+
field: IQuoteKey;
|
|
47
|
+
}
|
|
30
48
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/quote/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/quote/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/quote/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,oBAAY,SAAS,GAAG,OAAO,CAAC,MAAM,MAAM,EAAE,eAAe,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,oBAAY,kBAAkB,GAAG,MAAM,CACrC,MAAM,EACN,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAChE,CAAC;AACF,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC7C,YAAY,EAAE,MAAM,kBAAkB,CAAC;IACvC,aAAa,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACtF,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,KAAK,kBAAkB,CAAC;CAChG;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,uBAAuB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,SAAS,CAAC;CAClB"}
|
package/lib/quote/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/quote/types.ts"],"names":[],"mappings":"","sourcesContent":["import { IQuote } from '@yuants/data-quote';\n\nexport type IQuoteKey = Exclude<keyof IQuote, 'datasource_id' | 'product_id' | 'updated_at'>;\n\n/**\n * 用于批量更新的数据结构\n * 结构为:\n * product_id -> field_name (keyof IQuote) -> [value: string, updated_at: number]\n * 这样设计的目的是为了减少更新的数据量,同时保留每个字段的更新时间\n *\n * 例如:\n * ```json\n * {\n * \"product_123\": {\n * \"last_price\": [\"100.5\", 1627890123456],\n * \"volume\": [\"1500\", 1627890123456]\n * },\n * \"product_456\": {\n * \"last_price\": [\"200.0\", 1627890123456]\n * }\n * }\n * ```\n * @public\n */\nexport type IQuoteUpdateAction = Record<\n string,\n Partial<Record<IQuoteKey, [value: string, updated_at: number]>>\n>;\nexport interface IQuoteState {\n update: (action: IQuoteUpdateAction) => void;\n dumpAsObject: () => IQuoteUpdateAction;\n getValueTuple: (product_id: string, field: IQuoteKey) => [string, number] | undefined;\n filter: (product_ids: string[], fields: IQuoteKey[], updated_at: number) => IQuoteUpdateAction;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/quote/types.ts"],"names":[],"mappings":"","sourcesContent":["import { IQuote } from '@yuants/data-quote';\nimport { IQuoteServiceMetadata } from '@yuants/exchange';\n\nexport type IQuoteKey = Exclude<keyof IQuote, 'datasource_id' | 'product_id' | 'updated_at'>;\n\n/**\n * 用于批量更新的数据结构\n * 结构为:\n * product_id -> field_name (keyof IQuote) -> [value: string, updated_at: number]\n * 这样设计的目的是为了减少更新的数据量,同时保留每个字段的更新时间\n *\n * 例如:\n * ```json\n * {\n * \"product_123\": {\n * \"last_price\": [\"100.5\", 1627890123456],\n * \"volume\": [\"1500\", 1627890123456]\n * },\n * \"product_456\": {\n * \"last_price\": [\"200.0\", 1627890123456]\n * }\n * }\n * ```\n * @public\n */\nexport type IQuoteUpdateAction = Record<\n string,\n Partial<Record<IQuoteKey, [value: string, updated_at: number]>>\n>;\nexport interface IQuoteState {\n update: (action: IQuoteUpdateAction) => void;\n dumpAsObject: () => IQuoteUpdateAction;\n getValueTuple: (product_id: string, field: IQuoteKey) => [string, number] | undefined;\n filter: (product_ids: string[], fields: IQuoteKey[], updated_at: number) => IQuoteUpdateAction;\n}\n\nexport interface IQuoteProviderInstance {\n terminal_id: string;\n service_id: string;\n}\n\n/**\n * A \"provider group\" is a capability signature of `GetQuotes`.\n * Multiple vendor terminals may provide the same capability; VEX load-balances across instances.\n */\nexport interface IQuoteProviderGroup {\n group_id: string;\n meta: IQuoteServiceMetadata;\n mapTerminalIdToInstance: Map<string, IQuoteProviderInstance>;\n}\n\nexport interface IQuoteRequire {\n product_id: string;\n field: IQuoteKey;\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IQuoteUpdateAction as IExchangeQuoteUpdateAction } from '@yuants/exchange';
|
|
2
|
+
import { Terminal } from '@yuants/protocol';
|
|
3
|
+
import { IPlannedRequestWithKey } from './router';
|
|
4
|
+
export interface IGetQuotesExecutor {
|
|
5
|
+
execute: (requests: IPlannedRequestWithKey[]) => Promise<IExchangeQuoteUpdateAction[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare const createGetQuotesExecutor: (terminal: Terminal) => IGetQuotesExecutor;
|
|
8
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/quote/upstream/executor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,IAAI,0BAA0B,EAEjD,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAyFlD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,QAAQ,EAAE,sBAAsB,EAAE,KAAK,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;CACxF;AAED,eAAO,MAAM,uBAAuB,aAAc,QAAQ,KAAG,kBAmC5D,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGetQuotesExecutor = void 0;
|
|
4
|
+
const utils_1 = require("@yuants/utils");
|
|
5
|
+
const rxjs_1 = require("rxjs");
|
|
6
|
+
const createConcurrencyLimiter = (concurrency) => {
|
|
7
|
+
const queue = [];
|
|
8
|
+
let active = 0;
|
|
9
|
+
const next = () => {
|
|
10
|
+
if (active >= concurrency)
|
|
11
|
+
return;
|
|
12
|
+
const task = queue.shift();
|
|
13
|
+
if (!task)
|
|
14
|
+
return;
|
|
15
|
+
active++;
|
|
16
|
+
task();
|
|
17
|
+
};
|
|
18
|
+
return async (fn) => await new Promise((resolve, reject) => {
|
|
19
|
+
queue.push(async () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(await fn());
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
reject(e);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
active--;
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
next();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const pickInstance = (params) => {
|
|
35
|
+
var _a;
|
|
36
|
+
const { group_id, instances, mapGroupIdToRoundRobinIndex } = params;
|
|
37
|
+
if (instances.length === 0)
|
|
38
|
+
throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_INSTANCE_EMPTY', { group_id });
|
|
39
|
+
const nextIndex = ((_a = mapGroupIdToRoundRobinIndex.get(group_id)) !== null && _a !== void 0 ? _a : 0) % instances.length;
|
|
40
|
+
mapGroupIdToRoundRobinIndex.set(group_id, nextIndex + 1);
|
|
41
|
+
return instances[nextIndex];
|
|
42
|
+
};
|
|
43
|
+
const requestGetQuotes = async (params) => {
|
|
44
|
+
const { terminal, instance, req } = params;
|
|
45
|
+
const res = await (0, rxjs_1.firstValueFrom)(terminal.client
|
|
46
|
+
.request('GetQuotes', instance.terminal_id, req, instance.service_id)
|
|
47
|
+
.pipe((0, rxjs_1.map)((msg) => msg.res), (0, rxjs_1.filter)((v) => v !== undefined)));
|
|
48
|
+
if (res.code !== 0)
|
|
49
|
+
throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_ERROR', { instance, res });
|
|
50
|
+
if (res.data === undefined)
|
|
51
|
+
throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });
|
|
52
|
+
return res.data;
|
|
53
|
+
};
|
|
54
|
+
const runWithProviderGroupConcurrencyLimit1 = async (params) => {
|
|
55
|
+
var _a;
|
|
56
|
+
const { group_id, mapGroupIdToTailPromise, fn } = params;
|
|
57
|
+
const prev = (_a = mapGroupIdToTailPromise.get(group_id)) !== null && _a !== void 0 ? _a : Promise.resolve();
|
|
58
|
+
let resolveCurrent = () => { };
|
|
59
|
+
const current = new Promise((resolve) => {
|
|
60
|
+
resolveCurrent = resolve;
|
|
61
|
+
});
|
|
62
|
+
mapGroupIdToTailPromise.set(group_id, prev.then(() => current));
|
|
63
|
+
await prev;
|
|
64
|
+
try {
|
|
65
|
+
return await fn();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
resolveCurrent();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const createGetQuotesExecutor = (terminal) => {
|
|
72
|
+
const mapGroupIdToRoundRobinIndex = new Map();
|
|
73
|
+
const mapGroupIdToTailPromise = new Map();
|
|
74
|
+
const limitGetQuotes = createConcurrencyLimiter(32);
|
|
75
|
+
const mapKeyToInFlightGetQuotesPromise = new Map();
|
|
76
|
+
const requestGetQuotesInFlight = (key, planned) => {
|
|
77
|
+
const existing = mapKeyToInFlightGetQuotesPromise.get(key);
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
const promise = limitGetQuotes(() => runWithProviderGroupConcurrencyLimit1({
|
|
81
|
+
group_id: planned.group_id,
|
|
82
|
+
mapGroupIdToTailPromise,
|
|
83
|
+
fn: async () => {
|
|
84
|
+
const instance = pickInstance({
|
|
85
|
+
group_id: planned.group_id,
|
|
86
|
+
instances: planned.instances,
|
|
87
|
+
mapGroupIdToRoundRobinIndex,
|
|
88
|
+
});
|
|
89
|
+
return await requestGetQuotes({ terminal, instance, req: planned.req });
|
|
90
|
+
},
|
|
91
|
+
})).finally(() => {
|
|
92
|
+
mapKeyToInFlightGetQuotesPromise.delete(key);
|
|
93
|
+
});
|
|
94
|
+
mapKeyToInFlightGetQuotesPromise.set(key, promise);
|
|
95
|
+
return promise;
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
execute: async (requests) => await Promise.all(requests.map(async ({ key, planned }) => await requestGetQuotesInFlight(key, planned))),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
exports.createGetQuotesExecutor = createGetQuotesExecutor;
|
|
102
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../src/quote/upstream/executor.ts"],"names":[],"mappings":";;;AAKA,yCAAyC;AACzC,+BAAmD;AAMnD,MAAM,wBAAwB,GAAG,CAAC,WAAmB,EAAE,EAAE;IACvD,MAAM,KAAK,GAAsB,EAAE,CAAC;IACpC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,IAAI,GAAG,GAAG,EAAE;QAChB,IAAI,MAAM,IAAI,WAAW;YAAE,OAAO;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;IACF,OAAO,KAAK,EAAK,EAAoB,EAAc,EAAE,CACnD,MAAM,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACvC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACpB,IAAI;gBACF,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;aACrB;YAAC,OAAO,CAAC,EAAE;gBACV,MAAM,CAAC,CAAC,CAAC,CAAC;aACX;oBAAS;gBACR,MAAM,EAAE,CAAC;gBACT,IAAI,EAAE,CAAC;aACR;QACH,CAAC,CAAC,CAAC;QACH,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;AACP,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,MAIrB,EAA0B,EAAE;;IAC3B,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,2BAA2B,EAAE,GAAG,MAAM,CAAC;IACpE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAA,gBAAQ,EAAC,mCAAmC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9F,MAAM,SAAS,GAAG,CAAC,MAAA,2BAA2B,CAAC,GAAG,CAAC,QAAQ,CAAC,mCAAI,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC;IACtF,2BAA2B,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC;IACzD,OAAO,SAAS,CAAC,SAAS,CAAC,CAAC;AAC9B,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,KAAK,EAAE,MAI/B,EAAuC,EAAE;IACxC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAc,EAC9B,QAAQ,CAAC,MAAM;SACZ,OAAO,CACN,WAAW,EACX,QAAQ,CAAC,WAAW,EACpB,GAAG,EACH,QAAQ,CAAC,UAAU,CACpB;SACA,IAAI,CACH,IAAA,UAAG,EAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EACrB,IAAA,aAAM,EAAC,CAAC,CAAC,EAAqC,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAClE,CACJ,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;QAAE,MAAM,IAAA,gBAAQ,EAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;IAClF,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;QAAE,MAAM,IAAA,gBAAQ,EAAC,iCAAiC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;IACjG,OAAO,GAAG,CAAC,IAAW,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,qCAAqC,GAAG,KAAK,EAAK,MAIvD,EAAc,EAAE;;IACf,MAAM,EAAE,QAAQ,EAAE,uBAAuB,EAAE,EAAE,EAAE,GAAG,MAAM,CAAC;IACzD,MAAM,IAAI,GAAG,MAAA,uBAAuB,CAAC,GAAG,CAAC,QAAQ,CAAC,mCAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IACxE,IAAI,cAAc,GAAe,GAAG,EAAE,GAAE,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAC5C,cAAc,GAAG,OAAO,CAAC;IAC3B,CAAC,CAAC,CAAC;IACH,uBAAuB,CAAC,GAAG,CACzB,QAAQ,EACR,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CACzB,CAAC;IACF,MAAM,IAAI,CAAC;IACX,IAAI;QACF,OAAO,MAAM,EAAE,EAAE,CAAC;KACnB;YAAS;QACR,cAAc,EAAE,CAAC;KAClB;AACH,CAAC,CAAC;AAMK,MAAM,uBAAuB,GAAG,CAAC,QAAkB,EAAsB,EAAE;IAChF,MAAM,2BAA2B,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9D,MAAM,uBAAuB,GAAG,IAAI,GAAG,EAAyB,CAAC;IACjE,MAAM,cAAc,GAAG,wBAAwB,CAAC,EAAE,CAAC,CAAC;IAEpD,MAAM,gCAAgC,GAAG,IAAI,GAAG,EAA+C,CAAC;IAChG,MAAM,wBAAwB,GAAG,CAAC,GAAW,EAAE,OAAwB,EAAE,EAAE;QACzE,MAAM,QAAQ,GAAG,gCAAgC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3D,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,CAClC,qCAAqC,CAAC;YACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,uBAAuB;YACvB,EAAE,EAAE,KAAK,IAAI,EAAE;gBACb,MAAM,QAAQ,GAAG,YAAY,CAAC;oBAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,2BAA2B;iBAC5B,CAAC,CAAC;gBACH,OAAO,MAAM,gBAAgB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YAC1E,CAAC;SACF,CAAC,CACH,CAAC,OAAO,CAAC,GAAG,EAAE;YACb,gCAAgC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QACH,gCAAgC,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO;QACL,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAC1B,MAAM,OAAO,CAAC,GAAG,CACf,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,MAAM,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CACvF;KACJ,CAAC;AACJ,CAAC,CAAC;AAnCW,QAAA,uBAAuB,2BAmClC","sourcesContent":["import {\n IQuoteUpdateAction as IExchangeQuoteUpdateAction,\n IQuoteServiceRequestByVEX,\n} from '@yuants/exchange';\nimport { Terminal } from '@yuants/protocol';\nimport { newError } from '@yuants/utils';\nimport { filter, firstValueFrom, map } from 'rxjs';\nimport { IQuoteProviderInstance } from '../types';\nimport { IPlannedRequestWithKey } from './router';\n\ntype IPlannedRequest = IPlannedRequestWithKey['planned'];\n\nconst createConcurrencyLimiter = (concurrency: number) => {\n const queue: Array<() => void> = [];\n let active = 0;\n const next = () => {\n if (active >= concurrency) return;\n const task = queue.shift();\n if (!task) return;\n active++;\n task();\n };\n return async <T>(fn: () => Promise<T>): Promise<T> =>\n await new Promise<T>((resolve, reject) => {\n queue.push(async () => {\n try {\n resolve(await fn());\n } catch (e) {\n reject(e);\n } finally {\n active--;\n next();\n }\n });\n next();\n });\n};\n\nconst pickInstance = (params: {\n group_id: string;\n instances: IQuoteProviderInstance[];\n mapGroupIdToRoundRobinIndex: Map<string, number>;\n}): IQuoteProviderInstance => {\n const { group_id, instances, mapGroupIdToRoundRobinIndex } = params;\n if (instances.length === 0) throw newError('VEX_QUOTE_PROVIDER_INSTANCE_EMPTY', { group_id });\n const nextIndex = (mapGroupIdToRoundRobinIndex.get(group_id) ?? 0) % instances.length;\n mapGroupIdToRoundRobinIndex.set(group_id, nextIndex + 1);\n return instances[nextIndex];\n};\n\nconst requestGetQuotes = async (params: {\n terminal: Terminal;\n instance: IQuoteProviderInstance;\n req: IQuoteServiceRequestByVEX;\n}): Promise<IExchangeQuoteUpdateAction> => {\n const { terminal, instance, req } = params;\n const res = await firstValueFrom(\n terminal.client\n .request<IQuoteServiceRequestByVEX, IExchangeQuoteUpdateAction>(\n 'GetQuotes',\n instance.terminal_id,\n req,\n instance.service_id,\n )\n .pipe(\n map((msg) => msg.res),\n filter((v): v is Exclude<typeof v, undefined> => v !== undefined),\n ),\n );\n if (res.code !== 0) throw newError('VEX_QUOTE_PROVIDER_ERROR', { instance, res });\n if (res.data === undefined) throw newError('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });\n return res.data as any;\n};\n\nconst runWithProviderGroupConcurrencyLimit1 = async <T>(params: {\n group_id: string;\n mapGroupIdToTailPromise: Map<string, Promise<void>>;\n fn: () => Promise<T>;\n}): Promise<T> => {\n const { group_id, mapGroupIdToTailPromise, fn } = params;\n const prev = mapGroupIdToTailPromise.get(group_id) ?? Promise.resolve();\n let resolveCurrent: () => void = () => {};\n const current = new Promise<void>((resolve) => {\n resolveCurrent = resolve;\n });\n mapGroupIdToTailPromise.set(\n group_id,\n prev.then(() => current),\n );\n await prev;\n try {\n return await fn();\n } finally {\n resolveCurrent();\n }\n};\n\nexport interface IGetQuotesExecutor {\n execute: (requests: IPlannedRequestWithKey[]) => Promise<IExchangeQuoteUpdateAction[]>;\n}\n\nexport const createGetQuotesExecutor = (terminal: Terminal): IGetQuotesExecutor => {\n const mapGroupIdToRoundRobinIndex = new Map<string, number>();\n const mapGroupIdToTailPromise = new Map<string, Promise<void>>();\n const limitGetQuotes = createConcurrencyLimiter(32);\n\n const mapKeyToInFlightGetQuotesPromise = new Map<string, Promise<IExchangeQuoteUpdateAction>>();\n const requestGetQuotesInFlight = (key: string, planned: IPlannedRequest) => {\n const existing = mapKeyToInFlightGetQuotesPromise.get(key);\n if (existing) return existing;\n const promise = limitGetQuotes(() =>\n runWithProviderGroupConcurrencyLimit1({\n group_id: planned.group_id,\n mapGroupIdToTailPromise,\n fn: async () => {\n const instance = pickInstance({\n group_id: planned.group_id,\n instances: planned.instances,\n mapGroupIdToRoundRobinIndex,\n });\n return await requestGetQuotes({ terminal, instance, req: planned.req });\n },\n }),\n ).finally(() => {\n mapKeyToInFlightGetQuotesPromise.delete(key);\n });\n mapKeyToInFlightGetQuotesPromise.set(key, promise);\n return promise;\n };\n\n return {\n execute: async (requests) =>\n await Promise.all(\n requests.map(async ({ key, planned }) => await requestGetQuotesInFlight(key, planned)),\n ),\n };\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/quote/upstream/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createQuoteProviderRegistry = void 0;
|
|
4
|
+
var registry_1 = require("./registry");
|
|
5
|
+
Object.defineProperty(exports, "createQuoteProviderRegistry", { enumerable: true, get: function () { return registry_1.createQuoteProviderRegistry; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/quote/upstream/index.ts"],"names":[],"mappings":";;;AAAA,uCAAyD;AAAhD,uHAAA,2BAA2B,OAAA","sourcesContent":["export { createQuoteProviderRegistry } from './registry';\nexport type { IQuoteProviderRegistry } from './registry';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefix-matcher.d.ts","sourceRoot":"","sources":["../../../src/quote/upstream/prefix-matcher.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,EAAE,CAAC;CAC/B;AAED,eAAO,MAAM,yBAAyB;YACX,MAAM;;yBAMhC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefix-matcher.js","sourceRoot":"","sources":["../../../src/quote/upstream/prefix-matcher.ts"],"names":[],"mappings":";;;AAIO,MAAM,yBAAyB,GAAG,CACvC,OAA4C,EACzB,EAAE;IACrB,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9E,OAAO;QACL,KAAK,EAAE,CAAC,KAAa,EAAO,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;KACpG,CAAC;AACJ,CAAC,CAAC;AAPW,QAAA,yBAAyB,6BAOpC","sourcesContent":["export interface IPrefixMatcher<T> {\n match: (value: string) => T[];\n}\n\nexport const createSortedPrefixMatcher = <T>(\n entries: Array<{ prefix: string; value: T }>,\n): IPrefixMatcher<T> => {\n const sorted = [...entries].sort((a, b) => b.prefix.length - a.prefix.length);\n return {\n match: (value: string): T[] => sorted.filter((x) => value.startsWith(x.prefix)).map((x) => x.value),\n };\n};\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Terminal } from '@yuants/protocol';
|
|
2
|
+
import { IQuoteProviderGroup, IQuoteRequire, IQuoteState, IQuoteUpdateAction } from '../types';
|
|
3
|
+
import { IProviderIndices, IPlannedRequestWithKey, QuoteUpstreamPlan } from './router';
|
|
4
|
+
export interface IQuoteProviderRegistry {
|
|
5
|
+
snapshot: () => {
|
|
6
|
+
groups: IQuoteProviderGroup[];
|
|
7
|
+
indices: IProviderIndices;
|
|
8
|
+
};
|
|
9
|
+
planOrThrow: (misses: IQuoteRequire[], updated_at: number) => QuoteUpstreamPlan;
|
|
10
|
+
execute: (requests: IPlannedRequestWithKey[]) => Promise<IQuoteUpdateAction[]>;
|
|
11
|
+
fillQuoteStateFromUpstream: (params: {
|
|
12
|
+
quoteState: IQuoteState;
|
|
13
|
+
cacheMissed: IQuoteRequire[];
|
|
14
|
+
updated_at: number;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare const createQuoteProviderRegistry: (terminal: Terminal) => IQuoteProviderRegistry;
|
|
18
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/quote/upstream/registry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAEL,mBAAmB,EAEnB,aAAa,EACb,WAAW,EACX,kBAAkB,EACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAGL,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM;QAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAC7E,WAAW,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,KAAK,iBAAiB,CAAC;IAChF,OAAO,EAAE,CAAC,QAAQ,EAAE,sBAAsB,EAAE,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAC/E,0BAA0B,EAAE,CAAC,MAAM,EAAE;QACnC,UAAU,EAAE,WAAW,CAAC;QACxB,WAAW,EAAE,aAAa,EAAE,CAAC;QAC7B,UAAU,EAAE,MAAM,CAAC;KACpB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAOD,eAAO,MAAM,2BAA2B,aAAc,QAAQ,KAAG,sBAsKhE,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createQuoteProviderRegistry = void 0;
|
|
4
|
+
const exchange_1 = require("@yuants/exchange");
|
|
5
|
+
const utils_1 = require("@yuants/utils");
|
|
6
|
+
const rxjs_1 = require("rxjs");
|
|
7
|
+
const executor_1 = require("./executor");
|
|
8
|
+
const router_1 = require("./router");
|
|
9
|
+
const normalizeMetadataForGroup = (metadata) => {
|
|
10
|
+
const fields = [...metadata.fields].sort();
|
|
11
|
+
return Object.assign(Object.assign({}, metadata), { fields });
|
|
12
|
+
};
|
|
13
|
+
const createQuoteProviderRegistry = (terminal) => {
|
|
14
|
+
const router = (0, router_1.createQuoteRouter)();
|
|
15
|
+
const executor = (0, executor_1.createGetQuotesExecutor)(terminal);
|
|
16
|
+
const mapGroupIdToGroup = new Map();
|
|
17
|
+
let version = 0;
|
|
18
|
+
let cachedIndices;
|
|
19
|
+
const quoteServiceInfos$ = terminal.terminalInfos$.pipe((0, rxjs_1.map)((infos) => infos.flatMap((info) => {
|
|
20
|
+
var _a;
|
|
21
|
+
return Object.values((_a = info.serviceInfo) !== null && _a !== void 0 ? _a : {})
|
|
22
|
+
.filter((serviceInfo) => serviceInfo.method === 'GetQuotes')
|
|
23
|
+
.map((serviceInfo) => ({ terminal_id: info.terminal_id, serviceInfo }));
|
|
24
|
+
})));
|
|
25
|
+
quoteServiceInfos$
|
|
26
|
+
.pipe((0, utils_1.listWatch)((v) => v.serviceInfo.service_id, (v) => {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]DiscoveringGetQuotesProvider...`, `from terminal ${v.terminal_id}`, `service ${v.serviceInfo.service_id}`, `schema: ${JSON.stringify(v.serviceInfo.schema)}`);
|
|
29
|
+
try {
|
|
30
|
+
const metadata = normalizeMetadataForGroup((0, exchange_1.parseQuoteServiceMetadataFromSchema)(v.serviceInfo.schema));
|
|
31
|
+
const group_id = (0, utils_1.encodePath)(metadata.product_id_prefix, metadata.fields.join(','), (_a = metadata.max_products_per_request) !== null && _a !== void 0 ? _a : '');
|
|
32
|
+
const provider = {
|
|
33
|
+
terminal_id: v.terminal_id,
|
|
34
|
+
service_id: v.serviceInfo.service_id || v.serviceInfo.method,
|
|
35
|
+
};
|
|
36
|
+
const group = (_b = mapGroupIdToGroup.get(group_id)) !== null && _b !== void 0 ? _b : (() => {
|
|
37
|
+
const next = {
|
|
38
|
+
group_id,
|
|
39
|
+
meta: metadata,
|
|
40
|
+
mapTerminalIdToInstance: new Map(),
|
|
41
|
+
};
|
|
42
|
+
mapGroupIdToGroup.set(group_id, next);
|
|
43
|
+
return next;
|
|
44
|
+
})();
|
|
45
|
+
group.mapTerminalIdToInstance.set(provider.terminal_id, provider);
|
|
46
|
+
version++;
|
|
47
|
+
return (0, rxjs_1.of)(void 0).pipe((0, rxjs_1.tap)({
|
|
48
|
+
unsubscribe: () => {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
(_a = mapGroupIdToGroup.get(group_id)) === null || _a === void 0 ? void 0 : _a.mapTerminalIdToInstance.delete(v.terminal_id);
|
|
51
|
+
if (((_b = mapGroupIdToGroup.get(group_id)) === null || _b === void 0 ? void 0 : _b.mapTerminalIdToInstance.size) === 0) {
|
|
52
|
+
mapGroupIdToGroup.delete(group_id);
|
|
53
|
+
}
|
|
54
|
+
version++;
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
catch (_c) {
|
|
59
|
+
console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]IgnoredGetQuotesProvider`, `Ignored GetQuotes provider from terminal ${v.terminal_id}`, `service ${v.serviceInfo.service_id} due to invalid schema.`);
|
|
60
|
+
return rxjs_1.EMPTY;
|
|
61
|
+
}
|
|
62
|
+
}))
|
|
63
|
+
.subscribe();
|
|
64
|
+
const snapshot = () => {
|
|
65
|
+
const groups = Array.from(mapGroupIdToGroup.values());
|
|
66
|
+
if (!cachedIndices || cachedIndices.version !== version) {
|
|
67
|
+
cachedIndices = { version, indices: (0, router_1.buildProviderIndices)(groups) };
|
|
68
|
+
}
|
|
69
|
+
return { groups, indices: cachedIndices.indices };
|
|
70
|
+
};
|
|
71
|
+
const planOrThrow = (misses, updated_at) => {
|
|
72
|
+
const { groups, indices } = snapshot();
|
|
73
|
+
if (groups.length === 0)
|
|
74
|
+
throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });
|
|
75
|
+
return router.planOrThrow(misses, indices, updated_at);
|
|
76
|
+
};
|
|
77
|
+
const execute = async (requests) => (await executor.execute(requests));
|
|
78
|
+
const fillQuoteStateFromUpstream = async (params) => {
|
|
79
|
+
const { quoteState, cacheMissed, updated_at } = params;
|
|
80
|
+
if (cacheMissed.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const { groups } = snapshot();
|
|
83
|
+
console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]UpstreamProviderDiscovery`, ` Discovered ${groups.length} GetQuotes provider groups from terminal infos.`, JSON.stringify(groups));
|
|
84
|
+
const plan = planOrThrow(cacheMissed, updated_at);
|
|
85
|
+
const mapGroupIdToProductIds = new Map();
|
|
86
|
+
for (const { planned } of plan.requests) {
|
|
87
|
+
let productIds = mapGroupIdToProductIds.get(planned.group_id);
|
|
88
|
+
if (!productIds) {
|
|
89
|
+
productIds = new Set();
|
|
90
|
+
mapGroupIdToProductIds.set(planned.group_id, productIds);
|
|
91
|
+
}
|
|
92
|
+
for (const product_id of planned.req.product_ids)
|
|
93
|
+
productIds.add(product_id);
|
|
94
|
+
}
|
|
95
|
+
console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]RouteDispatched`, ` Routed ${cacheMissed.length} missed quotes to ${mapGroupIdToProductIds.size} provider groups.`, JSON.stringify({
|
|
96
|
+
productsByGroupId: [...mapGroupIdToProductIds.entries()].map(([group_id, productIds]) => ({
|
|
97
|
+
group_id,
|
|
98
|
+
product_ids: [...productIds],
|
|
99
|
+
})),
|
|
100
|
+
default_action: plan.defaultAction,
|
|
101
|
+
}));
|
|
102
|
+
if (Object.keys(plan.defaultAction).length > 0) {
|
|
103
|
+
quoteState.update(plan.defaultAction);
|
|
104
|
+
}
|
|
105
|
+
console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]RequestPlanned`, `Planned ${plan.requests.length} upstream GetQuotes requests.`, JSON.stringify(plan.requests.map(({ key, planned }) => ({
|
|
106
|
+
key,
|
|
107
|
+
group_id: planned.group_id,
|
|
108
|
+
product_ids: planned.req.product_ids,
|
|
109
|
+
fields: planned.req.fields,
|
|
110
|
+
}))));
|
|
111
|
+
const actions = await execute(plan.requests);
|
|
112
|
+
console.debug((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]RequestReceived`, `Received ${actions.length} upstream GetQuotes responses.`, JSON.stringify(actions));
|
|
113
|
+
for (const action of actions) {
|
|
114
|
+
quoteState.update(action);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return { snapshot, planOrThrow, execute, fillQuoteStateFromUpstream };
|
|
118
|
+
};
|
|
119
|
+
exports.createQuoteProviderRegistry = createQuoteProviderRegistry;
|
|
120
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/quote/upstream/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { IQuoteServiceMetadata, parseQuoteServiceMetadataFromSchema } from '@yuants/exchange';\nimport { Terminal } from '@yuants/protocol';\nimport { encodePath, formatTime, listWatch, newError } from '@yuants/utils';\nimport { EMPTY, map, of, tap } from 'rxjs';\nimport {\n IQuoteKey,\n IQuoteProviderGroup,\n IQuoteProviderInstance,\n IQuoteRequire,\n IQuoteState,\n IQuoteUpdateAction,\n} from '../types';\nimport { createGetQuotesExecutor } from './executor';\nimport {\n buildProviderIndices,\n createQuoteRouter,\n IProviderIndices,\n IPlannedRequestWithKey,\n QuoteUpstreamPlan,\n} from './router';\n\nexport interface IQuoteProviderRegistry {\n snapshot: () => { groups: IQuoteProviderGroup[]; indices: IProviderIndices };\n planOrThrow: (misses: IQuoteRequire[], updated_at: number) => QuoteUpstreamPlan;\n execute: (requests: IPlannedRequestWithKey[]) => Promise<IQuoteUpdateAction[]>;\n fillQuoteStateFromUpstream: (params: {\n quoteState: IQuoteState;\n cacheMissed: IQuoteRequire[];\n updated_at: number;\n }) => Promise<void>;\n}\n\nconst normalizeMetadataForGroup = (metadata: IQuoteServiceMetadata): IQuoteServiceMetadata => {\n const fields = [...(metadata.fields as unknown as IQuoteKey[])].sort();\n return { ...metadata, fields };\n};\n\nexport const createQuoteProviderRegistry = (terminal: Terminal): IQuoteProviderRegistry => {\n const router = createQuoteRouter();\n const executor = createGetQuotesExecutor(terminal);\n\n const mapGroupIdToGroup = new Map<string, IQuoteProviderGroup>();\n let version = 0;\n let cachedIndices: { version: number; indices: IProviderIndices } | undefined;\n\n const quoteServiceInfos$ = terminal.terminalInfos$.pipe(\n map((infos) =>\n infos.flatMap((info) =>\n Object.values(info.serviceInfo ?? {})\n .filter((serviceInfo) => serviceInfo.method === 'GetQuotes')\n .map((serviceInfo) => ({ terminal_id: info.terminal_id, serviceInfo })),\n ),\n ),\n );\n\n quoteServiceInfos$\n .pipe(\n listWatch(\n (v) => v.serviceInfo.service_id,\n (v) => {\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]DiscoveringGetQuotesProvider...`,\n `from terminal ${v.terminal_id}`,\n `service ${v.serviceInfo.service_id}`,\n `schema: ${JSON.stringify(v.serviceInfo.schema)}`,\n );\n try {\n const metadata = normalizeMetadataForGroup(\n parseQuoteServiceMetadataFromSchema(v.serviceInfo.schema),\n );\n const group_id = encodePath(\n metadata.product_id_prefix,\n (metadata.fields as any[]).join(','),\n metadata.max_products_per_request ?? '',\n );\n const provider: IQuoteProviderInstance = {\n terminal_id: v.terminal_id,\n service_id: v.serviceInfo.service_id || v.serviceInfo.method,\n };\n const group =\n mapGroupIdToGroup.get(group_id) ??\n (() => {\n const next: IQuoteProviderGroup = {\n group_id,\n meta: metadata,\n mapTerminalIdToInstance: new Map<string, IQuoteProviderInstance>(),\n };\n mapGroupIdToGroup.set(group_id, next);\n return next;\n })();\n group.mapTerminalIdToInstance.set(provider.terminal_id, provider);\n version++;\n return of(void 0).pipe(\n tap({\n unsubscribe: () => {\n mapGroupIdToGroup.get(group_id)?.mapTerminalIdToInstance.delete(v.terminal_id);\n if (mapGroupIdToGroup.get(group_id)?.mapTerminalIdToInstance.size === 0) {\n mapGroupIdToGroup.delete(group_id);\n }\n version++;\n },\n }),\n );\n } catch {\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]IgnoredGetQuotesProvider`,\n `Ignored GetQuotes provider from terminal ${v.terminal_id}`,\n `service ${v.serviceInfo.service_id} due to invalid schema.`,\n );\n return EMPTY;\n }\n },\n ),\n )\n .subscribe();\n\n const snapshot = () => {\n const groups = Array.from(mapGroupIdToGroup.values());\n if (!cachedIndices || cachedIndices.version !== version) {\n cachedIndices = { version, indices: buildProviderIndices(groups) };\n }\n return { groups, indices: cachedIndices.indices };\n };\n\n const planOrThrow = (misses: IQuoteRequire[], updated_at: number): QuoteUpstreamPlan => {\n const { groups, indices } = snapshot();\n if (groups.length === 0) throw newError('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });\n return router.planOrThrow(misses, indices, updated_at);\n };\n\n const execute = async (requests: IPlannedRequestWithKey[]): Promise<IQuoteUpdateAction[]> =>\n (await executor.execute(requests)) as any;\n\n const fillQuoteStateFromUpstream: IQuoteProviderRegistry['fillQuoteStateFromUpstream'] = async (params) => {\n const { quoteState, cacheMissed, updated_at } = params;\n if (cacheMissed.length === 0) return;\n\n const { groups } = snapshot();\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]UpstreamProviderDiscovery`,\n ` Discovered ${groups.length} GetQuotes provider groups from terminal infos.`,\n JSON.stringify(groups),\n );\n\n const plan = planOrThrow(cacheMissed, updated_at);\n\n const mapGroupIdToProductIds = new Map<string, Set<string>>();\n for (const { planned } of plan.requests) {\n let productIds = mapGroupIdToProductIds.get(planned.group_id);\n if (!productIds) {\n productIds = new Set<string>();\n mapGroupIdToProductIds.set(planned.group_id, productIds);\n }\n for (const product_id of planned.req.product_ids) productIds.add(product_id);\n }\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RouteDispatched`,\n ` Routed ${cacheMissed.length} missed quotes to ${mapGroupIdToProductIds.size} provider groups.`,\n JSON.stringify({\n productsByGroupId: [...mapGroupIdToProductIds.entries()].map(([group_id, productIds]) => ({\n group_id,\n product_ids: [...productIds],\n })),\n default_action: plan.defaultAction,\n }),\n );\n\n if (Object.keys(plan.defaultAction).length > 0) {\n quoteState.update(plan.defaultAction);\n }\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RequestPlanned`,\n `Planned ${plan.requests.length} upstream GetQuotes requests.`,\n JSON.stringify(\n plan.requests.map(({ key, planned }) => ({\n key,\n group_id: planned.group_id,\n product_ids: planned.req.product_ids,\n fields: planned.req.fields,\n })),\n ),\n );\n\n const actions = await execute(plan.requests);\n console.debug(\n formatTime(Date.now()),\n `[VEX][Quote]RequestReceived`,\n `Received ${actions.length} upstream GetQuotes responses.`,\n JSON.stringify(actions),\n );\n\n for (const action of actions) {\n quoteState.update(action);\n }\n };\n\n return { snapshot, planOrThrow, execute, fillQuoteStateFromUpstream };\n};\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IQuoteServiceRequestByVEX } from '@yuants/exchange';
|
|
2
|
+
import { IPrefixMatcher } from './prefix-matcher';
|
|
3
|
+
import { IQuoteKey, IQuoteProviderGroup, IQuoteProviderInstance, IQuoteRequire, IQuoteUpdateAction } from '../types';
|
|
4
|
+
export declare type IPlannedRequest = {
|
|
5
|
+
group_id: string;
|
|
6
|
+
instances: IQuoteProviderInstance[];
|
|
7
|
+
req: IQuoteServiceRequestByVEX;
|
|
8
|
+
};
|
|
9
|
+
export declare type IPlannedRequestWithKey = {
|
|
10
|
+
key: string;
|
|
11
|
+
planned: IPlannedRequest;
|
|
12
|
+
};
|
|
13
|
+
export declare type QuoteUpstreamPlan = {
|
|
14
|
+
defaultAction: IQuoteUpdateAction;
|
|
15
|
+
requests: IPlannedRequestWithKey[];
|
|
16
|
+
};
|
|
17
|
+
export declare type IProviderIndices = {
|
|
18
|
+
mapGroupIdToGroup: Map<string, IQuoteProviderGroup>;
|
|
19
|
+
prefixMatcher: IPrefixMatcher<string>;
|
|
20
|
+
mapFieldToGroupIds: Map<IQuoteKey, Set<string>>;
|
|
21
|
+
};
|
|
22
|
+
export declare const buildProviderIndices: (groups: IQuoteProviderGroup[]) => IProviderIndices;
|
|
23
|
+
export interface IQuoteRouter {
|
|
24
|
+
planOrThrow: (misses: IQuoteRequire[], indices: IProviderIndices, updated_at: number) => QuoteUpstreamPlan;
|
|
25
|
+
}
|
|
26
|
+
export declare const createQuoteRouter: () => IQuoteRouter;
|
|
27
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../src/quote/upstream/router.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,OAAO,EAA6B,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,sBAAsB,EACtB,aAAa,EACb,kBAAkB,EACnB,MAAM,UAAU,CAAC;AAsBlB,oBAAY,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,sBAAsB,EAAE,CAAC;IACpC,GAAG,EAAE,yBAAyB,CAAC;CAChC,CAAC;AAEF,oBAAY,sBAAsB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAE/E,oBAAY,iBAAiB,GAAG;IAC9B,aAAa,EAAE,kBAAkB,CAAC;IAClC,QAAQ,EAAE,sBAAsB,EAAE,CAAC;CACpC,CAAC;AAEF,oBAAY,gBAAgB,GAAG;IAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACpD,aAAa,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACtC,kBAAkB,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CACjD,CAAC;AAEF,eAAO,MAAM,oBAAoB,WAAY,mBAAmB,EAAE,KAAG,gBAiBpE,CAAC;AAgCF,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,KAAK,iBAAiB,CAAC;CAC5G;AAED,eAAO,MAAM,iBAAiB,QAAO,YA4DnC,CAAC"}
|