@yuants/app-virtual-exchange 0.9.3 → 0.9.4

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,7 @@
1
+ export const createSortedPrefixMatcher = (entries) => {
2
+ const sorted = [...entries].sort((a, b) => b.prefix.length - a.prefix.length);
3
+ return {
4
+ match: (value) => sorted.filter((x) => value.startsWith(x.prefix)).map((x) => x.value),
5
+ };
6
+ };
7
+ //# sourceMappingURL=prefix-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefix-matcher.js","sourceRoot":"","sources":["../../src/quote/prefix-matcher.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,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","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,20 @@
1
+ import { TextEncoder } from 'util';
2
+ import { fnv1a64Hex } from '@yuants/utils';
3
+ const SEP_BYTE = new Uint8Array([0xff]);
4
+ const encodeStrings = (parts) => {
5
+ const buffers = [];
6
+ for (const part of parts) {
7
+ buffers.push(new TextEncoder().encode(part));
8
+ buffers.push(SEP_BYTE);
9
+ }
10
+ const totalLength = buffers.reduce((sum, b) => sum + b.length, 0);
11
+ const result = new Uint8Array(totalLength);
12
+ let offset = 0;
13
+ for (const b of buffers) {
14
+ result.set(b, offset);
15
+ offset += b.length;
16
+ }
17
+ return result;
18
+ };
19
+ export const fnv1a64HexFromStrings = (parts) => fnv1a64Hex(encodeStrings(parts));
20
+ //# sourceMappingURL=request-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-key.js","sourceRoot":"","sources":["../../src/quote/request-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAExC,MAAM,aAAa,GAAG,CAAC,KAAe,EAAc,EAAE;IACpD,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;QACxB,OAAO,CAAC,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;KACxB;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE;QACvB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACtB,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC;KACpB;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAe,EAAU,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC","sourcesContent":["import { TextEncoder } from 'util';\n\nimport { fnv1a64Hex } from '@yuants/utils';\n\nconst SEP_BYTE = new Uint8Array([0xff]);\n\nconst encodeStrings = (parts: string[]): Uint8Array => {\n const buffers: Uint8Array[] = [];\n for (const part of parts) {\n buffers.push(new TextEncoder().encode(part));\n buffers.push(SEP_BYTE);\n }\n const totalLength = buffers.reduce((sum, b) => sum + b.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const b of buffers) {\n result.set(b, offset);\n offset += b.length;\n }\n return result;\n};\n\nexport const fnv1a64HexFromStrings = (parts: string[]): string => fnv1a64Hex(encodeStrings(parts));\n"]}
@@ -1,14 +1,48 @@
1
1
  import { Terminal } from '@yuants/protocol';
2
+ import { newError } from '@yuants/utils';
2
3
  import { createQuoteState } from './state';
4
+ import { fillQuoteStateFromUpstream } from './upstream-routing';
3
5
  const terminal = Terminal.fromNodeEnv();
4
6
  const quoteState = createQuoteState();
7
+ const assertFreshnessSatisfied = (data, params) => {
8
+ var _a;
9
+ console.info('[VEX][Quote] Asserting freshness satisfied for requested quotes.', JSON.stringify(params), JSON.stringify(data));
10
+ const { product_ids, fields, updated_at } = params;
11
+ const stillMissed = [];
12
+ for (const product_id of product_ids) {
13
+ for (const field of fields) {
14
+ if (!((_a = data[product_id]) === null || _a === void 0 ? void 0 : _a[field])) {
15
+ stillMissed.push({ product_id, field });
16
+ }
17
+ }
18
+ }
19
+ if (stillMissed.length > 0) {
20
+ throw newError('VEX_QUOTE_FRESHNESS_NOT_SATISFIED', {
21
+ updated_at,
22
+ missed: stillMissed.slice(0, 200),
23
+ missed_total: stillMissed.length,
24
+ });
25
+ }
26
+ };
5
27
  terminal.server.provideService('VEX/UpdateQuotes', {}, async (msg) => {
6
28
  quoteState.update(msg.req);
7
29
  return { res: { code: 0, message: 'OK' } };
8
30
  });
9
- terminal.server.provideService('VEX/DumpQuoteState', {}, async (msg) => {
31
+ terminal.server.provideService('VEX/DumpQuoteState', {}, async () => {
10
32
  return { res: { code: 0, message: 'OK', data: quoteState.dumpAsObject() } };
11
33
  });
34
+ const computeCacheMissed = (quoteState, product_ids, fields, updated_at) => {
35
+ const cacheMissed = [];
36
+ for (const product_id of product_ids) {
37
+ for (const field of fields) {
38
+ const tuple = quoteState.getValueTuple(product_id, field);
39
+ if (tuple === undefined || tuple[1] < updated_at) {
40
+ cacheMissed.push({ product_id, field });
41
+ }
42
+ }
43
+ }
44
+ return cacheMissed;
45
+ };
12
46
  terminal.server.provideService('VEX/QueryQuotes', {
13
47
  type: 'object',
14
48
  required: ['product_ids', 'fields', 'updated_at'],
@@ -25,21 +59,10 @@ terminal.server.provideService('VEX/QueryQuotes', {
25
59
  },
26
60
  }, async (msg) => {
27
61
  const { product_ids, fields, updated_at } = msg.req;
28
- // 分析缓存缺失的字段
29
- const cacheMissed = [];
30
- for (const product_id of product_ids) {
31
- for (const field of fields) {
32
- const tuple = quoteState.getValueTuple(product_id, field);
33
- if (tuple === undefined || tuple[1] < updated_at) {
34
- cacheMissed.push({ product_id, field });
35
- }
36
- }
37
- }
38
- // TODO: 集中规划需要发送的查询请求,并更新到状态中
39
- // 注意需要限制在途请求数量和复用在途请求的结果,以免过载和浪费资源
40
- // await Promise.all;
41
- // 从状态中获取数据返回
62
+ const cacheMissed = computeCacheMissed(quoteState, product_ids, fields, updated_at);
63
+ await fillQuoteStateFromUpstream({ terminal, quoteState, cacheMissed, updated_at });
42
64
  const data = quoteState.filter(product_ids, fields, updated_at);
65
+ assertFreshnessSatisfied(data, { product_ids, fields, updated_at });
43
66
  return { res: { code: 0, message: 'OK', data } };
44
67
  });
45
68
  //# sourceMappingURL=service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/quote/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAG3C,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;AAExC,MAAM,UAAU,GAAG,gBAAgB,EAAE,CAAC;AAEtC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAqB,kBAAkB,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IACvF,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;AAC7C,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAyB,oBAAoB,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAC7F,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,YAAY,EAAE,EAAE,EAAE,CAAC;AAC9E,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,MAAM,CAAC,cAAc,CAI5B,iBAAiB,EACjB;IACE,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,aAAa,EAAE,QAAQ,EAAE,YAAY,CAAC;IACjD,UAAU,EAAE;QACV,WAAW,EAAE;YACX,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SAC1B;QACD,MAAM,EAAE;YACN,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SAC1B;QACD,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAC/B;CACF,EACD,KAAK,EAAE,GAAG,EAAE,EAAE;IACZ,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC;IACpD,YAAY;IACZ,MAAM,WAAW,GAAoD,EAAE,CAAC;IACxE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE;QACpC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;YAC1B,MAAM,KAAK,GAAG,UAAU,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,UAAU,EAAE;gBAChD,WAAW,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;aACzC;SACF;KACF;IACD,8BAA8B;IAC9B,mCAAmC;IACnC,qBAAqB;IAErB,aAAa;IACb,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAEhE,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;AACnD,CAAC,CACF,CAAC","sourcesContent":["import { Terminal } from '@yuants/protocol';\nimport { createQuoteState } from './state';\nimport { IQuoteKey, IQuoteUpdateAction } from './types';\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteState = createQuoteState();\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 (msg) => {\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, fields, updated_at } = msg.req;\n // 分析缓存缺失的字段\n const cacheMissed: Array<{ product_id: string; field: IQuoteKey }> = [];\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 // TODO: 集中规划需要发送的查询请求,并更新到状态中\n // 注意需要限制在途请求数量和复用在途请求的结果,以免过载和浪费资源\n // await Promise.all;\n\n // 从状态中获取数据返回\n const data = quoteState.filter(product_ids, fields, updated_at);\n\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 { 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"]}
@@ -0,0 +1,300 @@
1
+ import { parseQuoteServiceMetadataFromSchema, } from '@yuants/exchange';
2
+ import { Terminal } from '@yuants/protocol';
3
+ import { encodePath, formatTime, listWatch, newError } from '@yuants/utils';
4
+ import { EMPTY, filter, firstValueFrom, from, map, mergeMap, of, tap, toArray } from 'rxjs';
5
+ import { createSortedPrefixMatcher } from './prefix-matcher';
6
+ import { fnv1a64HexFromStrings } from './request-key';
7
+ const terminal = Terminal.fromNodeEnv();
8
+ const quoteServiceInfos$ = terminal.terminalInfos$.pipe(mergeMap((infos) => from(infos).pipe(
9
+ //
10
+ mergeMap((info) => {
11
+ var _a;
12
+ return from(Object.values((_a = info.serviceInfo) !== null && _a !== void 0 ? _a : {})).pipe(filter((serviceInfo) => serviceInfo.method === 'GetQuotes'), map((serviceInfo) => ({
13
+ terminal_id: info.terminal_id,
14
+ serviceInfo,
15
+ })), toArray());
16
+ }))));
17
+ const mapGroupIdToGroup = new Map();
18
+ /**
19
+ * Build provider groups from runtime terminal infos.
20
+ *
21
+ * Note: `fields` is schema `const`, so VEX must keep a stable order (lexicographical sort).
22
+ */
23
+ quoteServiceInfos$
24
+ .pipe(listWatch((v) => v.serviceInfo.service_id, (v) => {
25
+ var _a;
26
+ console.info(formatTime(Date.now()), `[VEX][QUOTE]DiscoveringGetQuotesProvider...`, `from terminal ${v.terminal_id}`, `service ${v.serviceInfo.service_id}`, `schema: ${JSON.stringify(v.serviceInfo.schema)}`);
27
+ try {
28
+ const metadata = parseQuoteServiceMetadataFromSchema(v.serviceInfo.schema);
29
+ const fields = [...metadata.fields].sort();
30
+ const group_id = encodePath(metadata.product_id_prefix, fields.join(','), (_a = metadata.max_products_per_request) !== null && _a !== void 0 ? _a : '');
31
+ const provider = {
32
+ terminal_id: v.terminal_id,
33
+ service_id: v.serviceInfo.service_id || v.serviceInfo.method,
34
+ };
35
+ if (mapGroupIdToGroup.get(group_id)) {
36
+ mapGroupIdToGroup.get(group_id).mapTerminalIdToInstance.set(provider.terminal_id, provider);
37
+ }
38
+ else {
39
+ const next = {
40
+ group_id,
41
+ meta: metadata,
42
+ mapTerminalIdToInstance: new Map([[provider.terminal_id, provider]]),
43
+ };
44
+ mapGroupIdToGroup.set(group_id, next);
45
+ console.info('11111111', [...mapGroupIdToGroup.values()]);
46
+ }
47
+ return of(void 0).pipe(
48
+ //
49
+ tap({
50
+ unsubscribe: () => {
51
+ var _a, _b;
52
+ (_a = mapGroupIdToGroup.get(group_id)) === null || _a === void 0 ? void 0 : _a.mapTerminalIdToInstance.delete(v.terminal_id);
53
+ if (((_b = mapGroupIdToGroup.get(group_id)) === null || _b === void 0 ? void 0 : _b.mapTerminalIdToInstance.size) === 0) {
54
+ mapGroupIdToGroup.delete(group_id);
55
+ }
56
+ },
57
+ }));
58
+ }
59
+ catch (_b) {
60
+ // Ignore invalid schemas/providers
61
+ console.info(`[VEX][Quote] Ignored GetQuotes provider from terminal ${v.terminal_id} `, `service ${v.serviceInfo.service_id} due to invalid schema.`);
62
+ return EMPTY;
63
+ }
64
+ }))
65
+ .subscribe();
66
+ // -----------------------------------------------------------------------------
67
+ // Load balancing & upstream request execution
68
+ // -----------------------------------------------------------------------------
69
+ const mapGroupIdToRoundRobinIndex = new Map();
70
+ const pickInstance = (group_id, instances) => {
71
+ var _a;
72
+ if (instances.length === 0)
73
+ throw newError('VEX_QUOTE_PROVIDER_INSTANCE_EMPTY', { group_id });
74
+ const nextIndex = ((_a = mapGroupIdToRoundRobinIndex.get(group_id)) !== null && _a !== void 0 ? _a : 0) % instances.length;
75
+ mapGroupIdToRoundRobinIndex.set(group_id, nextIndex + 1);
76
+ return instances[nextIndex];
77
+ };
78
+ /**
79
+ * Call a specified vendor terminal + service instance.
80
+ *
81
+ * Any non-0 response is treated as fatal (strict freshness requirement).
82
+ */
83
+ const requestGetQuotes = async (terminal, instance, req) => {
84
+ const res = await firstValueFrom(terminal.client
85
+ .request('GetQuotes', instance.terminal_id, req, instance.service_id)
86
+ .pipe(map((msg) => msg.res), filter((v) => v !== undefined)));
87
+ if (res.code !== 0) {
88
+ throw newError('VEX_QUOTE_PROVIDER_ERROR', { instance, res });
89
+ }
90
+ if (res.data === undefined) {
91
+ throw newError('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });
92
+ }
93
+ return res.data;
94
+ };
95
+ /**
96
+ * Per-provider (group_id) concurrency limit: 1.
97
+ * Implemented as a per-group promise tail.
98
+ */
99
+ const mapGroupIdToTailPromise = new Map();
100
+ const runWithProviderGroupConcurrencyLimit1 = async (group_id, fn) => {
101
+ var _a;
102
+ const prev = (_a = mapGroupIdToTailPromise.get(group_id)) !== null && _a !== void 0 ? _a : Promise.resolve();
103
+ let resolveCurrent = () => { };
104
+ const current = new Promise((resolve) => {
105
+ resolveCurrent = resolve;
106
+ });
107
+ mapGroupIdToTailPromise.set(group_id, prev.then(() => current));
108
+ await prev;
109
+ try {
110
+ return await fn();
111
+ }
112
+ finally {
113
+ resolveCurrent();
114
+ }
115
+ };
116
+ /**
117
+ * A tiny async limiter: used as a global concurrency cap to avoid request explosions.
118
+ */
119
+ const createConcurrencyLimiter = (concurrency) => {
120
+ const queue = [];
121
+ let active = 0;
122
+ const next = () => {
123
+ if (active >= concurrency)
124
+ return;
125
+ const task = queue.shift();
126
+ if (!task)
127
+ return;
128
+ active++;
129
+ task();
130
+ };
131
+ return async (fn) => {
132
+ return await new Promise((resolve, reject) => {
133
+ queue.push(async () => {
134
+ try {
135
+ resolve(await fn());
136
+ }
137
+ catch (e) {
138
+ reject(e);
139
+ }
140
+ finally {
141
+ active--;
142
+ next();
143
+ }
144
+ });
145
+ next();
146
+ });
147
+ };
148
+ };
149
+ // Global concurrency cap for upstream `GetQuotes` calls (provider-level cap is handled separately).
150
+ const limitGetQuotes = createConcurrencyLimiter(32);
151
+ /**
152
+ * In-flight dedup:
153
+ * Same (provider group + product batch) should share a single upstream request promise.
154
+ */
155
+ const mapKeyToInFlightGetQuotesPromise = new Map();
156
+ const requestGetQuotesInFlight = (terminal, key, planned) => {
157
+ const existing = mapKeyToInFlightGetQuotesPromise.get(key);
158
+ if (existing)
159
+ return existing;
160
+ const promise = limitGetQuotes(() => runWithProviderGroupConcurrencyLimit1(planned.group_id, async () => {
161
+ const instance = pickInstance(planned.group_id, planned.instances);
162
+ return await requestGetQuotes(terminal, instance, planned.req);
163
+ })).finally(() => {
164
+ mapKeyToInFlightGetQuotesPromise.delete(key);
165
+ });
166
+ mapKeyToInFlightGetQuotesPromise.set(key, promise);
167
+ return promise;
168
+ };
169
+ const buildProviderIndices = (groups) => {
170
+ const mapGroupIdToGroup = new Map(groups.map((x) => [x.group_id, x]));
171
+ const prefixMatcher = createSortedPrefixMatcher(groups.map((group) => ({ prefix: group.meta.product_id_prefix, value: group.group_id })));
172
+ const mapFieldToGroupIds = new Map();
173
+ for (const group of groups) {
174
+ for (const field of group.meta.fields) {
175
+ let groupIds = mapFieldToGroupIds.get(field);
176
+ if (!groupIds) {
177
+ groupIds = new Set();
178
+ mapFieldToGroupIds.set(field, groupIds);
179
+ }
180
+ groupIds.add(group.group_id);
181
+ }
182
+ }
183
+ return { mapGroupIdToGroup, prefixMatcher, mapFieldToGroupIds };
184
+ };
185
+ /**
186
+ * L1 quote routing (per `docs/zh-Hans/code-guidelines/exchange.md`):
187
+ * For each missed (product_id, field), route to `S_product_id ∩ S_field`.
188
+ */
189
+ const routeMisses = (cacheMissed, indices, updated_at) => {
190
+ const { prefixMatcher, mapFieldToGroupIds } = indices;
191
+ const mapProductIdToGroupIds = new Map();
192
+ const productsByGroupId = new Map();
193
+ const unroutableProducts = new Set();
194
+ // Field unavailable: return "" but keep updated_at satisfied to avoid repeated misses.
195
+ const unavailableAction = {};
196
+ for (const miss of cacheMissed) {
197
+ const { product_id, field } = miss;
198
+ let productGroupIds = mapProductIdToGroupIds.get(product_id);
199
+ if (!productGroupIds) {
200
+ productGroupIds = prefixMatcher.match(product_id);
201
+ mapProductIdToGroupIds.set(product_id, productGroupIds);
202
+ }
203
+ if (productGroupIds.length === 0) {
204
+ unroutableProducts.add(product_id);
205
+ continue;
206
+ }
207
+ const fieldGroupIds = mapFieldToGroupIds.get(field);
208
+ if (!fieldGroupIds) {
209
+ if (!unavailableAction[product_id])
210
+ unavailableAction[product_id] = {};
211
+ unavailableAction[product_id][field] = ['', updated_at];
212
+ continue;
213
+ }
214
+ let matched = false;
215
+ for (const group_id of productGroupIds) {
216
+ if (!fieldGroupIds.has(group_id))
217
+ continue;
218
+ matched = true;
219
+ let productIds = productsByGroupId.get(group_id);
220
+ if (!productIds) {
221
+ productIds = new Set();
222
+ productsByGroupId.set(group_id, productIds);
223
+ }
224
+ productIds.add(product_id);
225
+ }
226
+ if (!matched) {
227
+ if (!unavailableAction[product_id])
228
+ unavailableAction[product_id] = {};
229
+ unavailableAction[product_id][field] = ['', updated_at];
230
+ }
231
+ }
232
+ return { productsByGroupId, unavailableAction, unroutableProducts };
233
+ };
234
+ const createRequestKey = (group_id, batchProductIds) => encodePath(group_id, fnv1a64HexFromStrings(batchProductIds));
235
+ const planRequests = (productsByGroupId, mapGroupIdToGroup) => {
236
+ var _a;
237
+ const plannedRequests = [];
238
+ for (const [group_id, productIdSet] of productsByGroupId) {
239
+ const group = mapGroupIdToGroup.get(group_id);
240
+ if (!group)
241
+ continue;
242
+ const sortedProductIds = [...productIdSet].sort();
243
+ const max = (_a = group.meta.max_products_per_request) !== null && _a !== void 0 ? _a : sortedProductIds.length;
244
+ for (let i = 0; i < sortedProductIds.length; i += max) {
245
+ const batchProductIds = sortedProductIds.slice(i, i + max);
246
+ const key = createRequestKey(group_id, batchProductIds);
247
+ plannedRequests.push({
248
+ key,
249
+ planned: {
250
+ group_id,
251
+ instances: Array.from(group.mapTerminalIdToInstance.values()),
252
+ req: { product_ids: batchProductIds, fields: group.meta.fields },
253
+ },
254
+ });
255
+ }
256
+ }
257
+ return plannedRequests;
258
+ };
259
+ export const fillQuoteStateFromUpstream = async (params) => {
260
+ const { terminal, quoteState, cacheMissed, updated_at } = params;
261
+ if (cacheMissed.length === 0)
262
+ return;
263
+ const providerGroups = Array.from(mapGroupIdToGroup.values());
264
+ console.info(formatTime(Date.now()), `[VEX][Quote]UpstreamProviderDiscovery`, ` Discovered ${providerGroups.length} GetQuotes provider groups from terminal infos.`, JSON.stringify(providerGroups));
265
+ if (providerGroups.length === 0) {
266
+ throw newError('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });
267
+ }
268
+ const indices = buildProviderIndices(providerGroups);
269
+ const { productsByGroupId, unavailableAction, unroutableProducts } = routeMisses(cacheMissed, indices, updated_at);
270
+ console.info(formatTime(Date.now()), `[VEX][Quote]RouteDispatched`, ` Routed ${cacheMissed.length} missed quotes to ${productsByGroupId.size} provider groups, ` +
271
+ `${unroutableProducts.size} unroutable products.`, JSON.stringify({
272
+ productsByGroupId: [...productsByGroupId.entries()].map(([group_id, productIds]) => ({
273
+ group_id,
274
+ product_ids: [...productIds],
275
+ })),
276
+ unroutable_products: [...unroutableProducts],
277
+ unavailable_action: unavailableAction,
278
+ }));
279
+ if (unroutableProducts.size !== 0) {
280
+ throw newError('VEX_QUOTE_PRODUCT_UNROUTABLE', {
281
+ updated_at,
282
+ unroutable_products: [...unroutableProducts].slice(0, 200),
283
+ unroutable_products_total: unroutableProducts.size,
284
+ });
285
+ }
286
+ quoteState.update(unavailableAction);
287
+ const plannedRequests = planRequests(productsByGroupId, indices.mapGroupIdToGroup);
288
+ console.info(formatTime(Date.now()), `[VEX][Quote]RequestPlanned`, `Planned ${plannedRequests.length} upstream GetQuotes requests.`, JSON.stringify(plannedRequests.map(({ key, planned }) => ({
289
+ key,
290
+ group_id: planned.group_id,
291
+ product_ids: planned.req.product_ids,
292
+ fields: planned.req.fields,
293
+ }))));
294
+ const actions = await Promise.all(plannedRequests.map(async ({ key, planned }) => await requestGetQuotesInFlight(terminal, key, planned)));
295
+ console.debug(formatTime(Date.now()), `[VEX][Quote]RequestReceived`, `Received ${actions.length} upstream GetQuotes responses.`);
296
+ for (const action of actions) {
297
+ quoteState.update(action);
298
+ }
299
+ };
300
+ //# sourceMappingURL=upstream-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-routing.js","sourceRoot":"","sources":["../../src/quote/upstream-routing.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n IQuoteUpdateAction as IExchangeQuoteUpdateAction,\n IQuoteServiceMetadata,\n IQuoteServiceRequestByVEX,\n parseQuoteServiceMetadataFromSchema,\n} from '@yuants/exchange';\nimport { Terminal } from '@yuants/protocol';\nimport { encodePath, formatTime, listWatch, newError } from '@yuants/utils';\nimport { EMPTY, filter, firstValueFrom, from, map, mergeMap, of, tap, timer, toArray } from 'rxjs';\nimport { createSortedPrefixMatcher } from './prefix-matcher';\nimport { fnv1a64HexFromStrings } from './request-key';\nimport { IQuoteKey, IQuoteState, IQuoteUpdateAction } from './types';\n\nexport interface IQuoteMiss {\n product_id: string;\n field: IQuoteKey;\n}\n\n// -----------------------------------------------------------------------------\n// Provider discovery (schema-driven)\n// -----------------------------------------------------------------------------\n\ninterface 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 */\ninterface IQuoteProviderGroup {\n group_id: string;\n meta: IQuoteServiceMetadata;\n mapTerminalIdToInstance: Map<string, IQuoteProviderInstance>;\n}\n\ntype IPlannedRequest = {\n group_id: string;\n instances: IQuoteProviderInstance[];\n req: IQuoteServiceRequestByVEX;\n};\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteServiceInfos$ = terminal.terminalInfos$.pipe(\n mergeMap((infos) =>\n from(infos).pipe(\n //\n mergeMap((info) =>\n from(Object.values(info.serviceInfo ?? {})).pipe(\n filter((serviceInfo) => serviceInfo.method === 'GetQuotes'),\n map((serviceInfo) => ({\n terminal_id: info.terminal_id,\n serviceInfo,\n })),\n toArray(),\n ),\n ),\n ),\n ),\n);\n\nconst mapGroupIdToGroup = new Map<string, IQuoteProviderGroup>();\n\n/**\n * Build provider groups from runtime terminal infos.\n *\n * Note: `fields` is schema `const`, so VEX must keep a stable order (lexicographical sort).\n */\nquoteServiceInfos$\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 = parseQuoteServiceMetadataFromSchema(v.serviceInfo.schema);\n const fields = [...(metadata.fields as unknown as IQuoteKey[])].sort();\n const group_id = encodePath(\n metadata.product_id_prefix,\n fields.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 if (mapGroupIdToGroup.get(group_id)) {\n mapGroupIdToGroup.get(group_id)!.mapTerminalIdToInstance.set(provider.terminal_id, provider);\n } else {\n const next: IQuoteProviderGroup = {\n group_id,\n meta: metadata,\n mapTerminalIdToInstance: new Map([[provider.terminal_id, provider]]),\n };\n mapGroupIdToGroup.set(group_id, next);\n console.info('11111111', [...mapGroupIdToGroup.values()]);\n }\n return of(void 0).pipe(\n //\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 },\n }),\n );\n } catch {\n // Ignore invalid schemas/providers\n console.info(\n `[VEX][Quote] 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// -----------------------------------------------------------------------------\n// Load balancing & upstream request execution\n// -----------------------------------------------------------------------------\n\nconst mapGroupIdToRoundRobinIndex = new Map<string, number>();\nconst pickInstance = (group_id: string, instances: IQuoteProviderInstance[]): IQuoteProviderInstance => {\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\n/**\n * Call a specified vendor terminal + service instance.\n *\n * Any non-0 response is treated as fatal (strict freshness requirement).\n */\nconst requestGetQuotes = async (\n terminal: Terminal,\n instance: IQuoteProviderInstance,\n req: IQuoteServiceRequestByVEX,\n): Promise<IExchangeQuoteUpdateAction> => {\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) {\n throw newError('VEX_QUOTE_PROVIDER_ERROR', { instance, res });\n }\n if (res.data === undefined) {\n throw newError('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });\n }\n return res.data as any;\n};\n\n/**\n * Per-provider (group_id) concurrency limit: 1.\n * Implemented as a per-group promise tail.\n */\nconst mapGroupIdToTailPromise = new Map<string, Promise<void>>();\nconst runWithProviderGroupConcurrencyLimit1 = async <T>(\n group_id: string,\n fn: () => Promise<T>,\n): Promise<T> => {\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\n/**\n * A tiny async limiter: used as a global concurrency cap to avoid request explosions.\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 return 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};\n\n// Global concurrency cap for upstream `GetQuotes` calls (provider-level cap is handled separately).\nconst limitGetQuotes = createConcurrencyLimiter(32);\n\n/**\n * In-flight dedup:\n * Same (provider group + product batch) should share a single upstream request promise.\n */\nconst mapKeyToInFlightGetQuotesPromise = new Map<string, Promise<IExchangeQuoteUpdateAction>>();\nconst requestGetQuotesInFlight = (terminal: Terminal, key: string, planned: IPlannedRequest) => {\n const existing = mapKeyToInFlightGetQuotesPromise.get(key);\n if (existing) return existing;\n const promise = limitGetQuotes(() =>\n runWithProviderGroupConcurrencyLimit1(planned.group_id, async () => {\n const instance = pickInstance(planned.group_id, planned.instances);\n return await requestGetQuotes(terminal, instance, planned.req);\n }),\n ).finally(() => {\n mapKeyToInFlightGetQuotesPromise.delete(key);\n });\n mapKeyToInFlightGetQuotesPromise.set(key, promise);\n return promise;\n};\n\n// -----------------------------------------------------------------------------\n// Routing indices (prefix + field inverted index)\n// -----------------------------------------------------------------------------\n\ntype IProviderIndices = ReturnType<typeof buildProviderIndices>;\n\nconst buildProviderIndices = (groups: IQuoteProviderGroup[]) => {\n const mapGroupIdToGroup = new Map(groups.map((x) => [x.group_id, x] as const));\n const prefixMatcher = createSortedPrefixMatcher(\n groups.map((group) => ({ prefix: group.meta.product_id_prefix, value: group.group_id })),\n );\n const mapFieldToGroupIds = new Map<IQuoteKey, Set<string>>();\n for (const group of groups) {\n for (const field of group.meta.fields) {\n let groupIds = mapFieldToGroupIds.get(field);\n if (!groupIds) {\n groupIds = new Set<string>();\n mapFieldToGroupIds.set(field, groupIds);\n }\n groupIds.add(group.group_id);\n }\n }\n return { mapGroupIdToGroup, prefixMatcher, mapFieldToGroupIds };\n};\n\n/**\n * L1 quote routing (per `docs/zh-Hans/code-guidelines/exchange.md`):\n * For each missed (product_id, field), route to `S_product_id ∩ S_field`.\n */\nconst routeMisses = (\n cacheMissed: IQuoteMiss[],\n indices: IProviderIndices,\n updated_at: number,\n): {\n productsByGroupId: Map<string, Set<string>>;\n unavailableAction: IQuoteUpdateAction;\n unroutableProducts: Set<string>;\n} => {\n const { prefixMatcher, mapFieldToGroupIds } = indices;\n\n const mapProductIdToGroupIds = new Map<string, string[]>();\n\n const productsByGroupId = new Map<string, Set<string>>();\n const unroutableProducts = new Set<string>();\n // Field unavailable: return \"\" but keep updated_at satisfied to avoid repeated misses.\n const unavailableAction: IQuoteUpdateAction = {};\n\n for (const miss of cacheMissed) {\n const { product_id, field } = miss;\n\n let productGroupIds = mapProductIdToGroupIds.get(product_id);\n if (!productGroupIds) {\n productGroupIds = prefixMatcher.match(product_id);\n mapProductIdToGroupIds.set(product_id, productGroupIds);\n }\n if (productGroupIds.length === 0) {\n unroutableProducts.add(product_id);\n continue;\n }\n\n const fieldGroupIds = mapFieldToGroupIds.get(field);\n if (!fieldGroupIds) {\n if (!unavailableAction[product_id]) unavailableAction[product_id] = {};\n unavailableAction[product_id]![field] = ['', updated_at];\n continue;\n }\n\n let matched = false;\n for (const group_id of productGroupIds) {\n if (!fieldGroupIds.has(group_id)) continue;\n matched = true;\n let productIds = productsByGroupId.get(group_id);\n if (!productIds) {\n productIds = new Set<string>();\n productsByGroupId.set(group_id, productIds);\n }\n productIds.add(product_id);\n }\n\n if (!matched) {\n if (!unavailableAction[product_id]) unavailableAction[product_id] = {};\n unavailableAction[product_id]![field] = ['', updated_at];\n }\n }\n\n return { productsByGroupId, unavailableAction, unroutableProducts };\n};\n\nconst createRequestKey = (group_id: string, batchProductIds: string[]) =>\n encodePath(group_id, fnv1a64HexFromStrings(batchProductIds));\n\nconst planRequests = (\n productsByGroupId: Map<string, Set<string>>,\n mapGroupIdToGroup: Map<string, IQuoteProviderGroup>,\n): Array<{ key: string; planned: IPlannedRequest }> => {\n const plannedRequests: Array<{ key: string; planned: IPlannedRequest }> = [];\n for (const [group_id, productIdSet] of productsByGroupId) {\n const group = mapGroupIdToGroup.get(group_id);\n if (!group) continue;\n const sortedProductIds = [...productIdSet].sort();\n const max = group.meta.max_products_per_request ?? sortedProductIds.length;\n for (let i = 0; i < sortedProductIds.length; i += max) {\n const batchProductIds = sortedProductIds.slice(i, i + max);\n const key = createRequestKey(group_id, batchProductIds);\n plannedRequests.push({\n key,\n planned: {\n group_id,\n instances: Array.from(group.mapTerminalIdToInstance.values()),\n req: { product_ids: batchProductIds, fields: group.meta.fields as any },\n },\n });\n }\n }\n return plannedRequests;\n};\n\nexport const fillQuoteStateFromUpstream = async (params: {\n terminal: Terminal;\n quoteState: IQuoteState;\n cacheMissed: IQuoteMiss[];\n updated_at: number;\n}): Promise<void> => {\n const { terminal, quoteState, cacheMissed, updated_at } = params;\n if (cacheMissed.length === 0) return;\n\n const providerGroups = Array.from(mapGroupIdToGroup.values());\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]UpstreamProviderDiscovery`,\n ` Discovered ${providerGroups.length} GetQuotes provider groups from terminal infos.`,\n JSON.stringify(providerGroups),\n );\n if (providerGroups.length === 0) {\n throw newError('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });\n }\n\n const indices = buildProviderIndices(providerGroups);\n const { productsByGroupId, unavailableAction, unroutableProducts } = routeMisses(\n cacheMissed,\n indices,\n updated_at,\n );\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RouteDispatched`,\n ` Routed ${cacheMissed.length} missed quotes to ${productsByGroupId.size} provider groups, ` +\n `${unroutableProducts.size} unroutable products.`,\n JSON.stringify({\n productsByGroupId: [...productsByGroupId.entries()].map(([group_id, productIds]) => ({\n group_id,\n product_ids: [...productIds],\n })),\n unroutable_products: [...unroutableProducts],\n unavailable_action: unavailableAction,\n }),\n );\n\n if (unroutableProducts.size !== 0) {\n throw newError('VEX_QUOTE_PRODUCT_UNROUTABLE', {\n updated_at,\n unroutable_products: [...unroutableProducts].slice(0, 200),\n unroutable_products_total: unroutableProducts.size,\n });\n }\n\n quoteState.update(unavailableAction);\n\n const plannedRequests = planRequests(productsByGroupId, indices.mapGroupIdToGroup);\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RequestPlanned`,\n `Planned ${plannedRequests.length} upstream GetQuotes requests.`,\n JSON.stringify(\n plannedRequests.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 Promise.all(\n plannedRequests.map(async ({ key, planned }) => await requestGetQuotesInFlight(terminal, key, planned)),\n );\n\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 as unknown as IQuoteUpdateAction);\n }\n};\n"]}
@@ -0,0 +1,8 @@
1
+ export interface IPrefixMatcher<T> {
2
+ match: (value: string) => T[];
3
+ }
4
+ export declare const createSortedPrefixMatcher: <T>(entries: {
5
+ prefix: string;
6
+ value: T;
7
+ }[]) => IPrefixMatcher<T>;
8
+ //# sourceMappingURL=prefix-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefix-matcher.d.ts","sourceRoot":"","sources":["../../src/quote/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,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSortedPrefixMatcher = void 0;
4
+ const createSortedPrefixMatcher = (entries) => {
5
+ const sorted = [...entries].sort((a, b) => b.prefix.length - a.prefix.length);
6
+ return {
7
+ match: (value) => sorted.filter((x) => value.startsWith(x.prefix)).map((x) => x.value),
8
+ };
9
+ };
10
+ exports.createSortedPrefixMatcher = createSortedPrefixMatcher;
11
+ //# sourceMappingURL=prefix-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefix-matcher.js","sourceRoot":"","sources":["../../src/quote/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,2 @@
1
+ export declare const fnv1a64HexFromStrings: (parts: string[]) => string;
2
+ //# sourceMappingURL=request-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-key.d.ts","sourceRoot":"","sources":["../../src/quote/request-key.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,qBAAqB,UAAW,MAAM,EAAE,KAAG,MAA0C,CAAC"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fnv1a64HexFromStrings = void 0;
4
+ const util_1 = require("util");
5
+ const utils_1 = require("@yuants/utils");
6
+ const SEP_BYTE = new Uint8Array([0xff]);
7
+ const encodeStrings = (parts) => {
8
+ const buffers = [];
9
+ for (const part of parts) {
10
+ buffers.push(new util_1.TextEncoder().encode(part));
11
+ buffers.push(SEP_BYTE);
12
+ }
13
+ const totalLength = buffers.reduce((sum, b) => sum + b.length, 0);
14
+ const result = new Uint8Array(totalLength);
15
+ let offset = 0;
16
+ for (const b of buffers) {
17
+ result.set(b, offset);
18
+ offset += b.length;
19
+ }
20
+ return result;
21
+ };
22
+ const fnv1a64HexFromStrings = (parts) => (0, utils_1.fnv1a64Hex)(encodeStrings(parts));
23
+ exports.fnv1a64HexFromStrings = fnv1a64HexFromStrings;
24
+ //# sourceMappingURL=request-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-key.js","sourceRoot":"","sources":["../../src/quote/request-key.ts"],"names":[],"mappings":";;;AAAA,+BAAmC;AAEnC,yCAA2C;AAE3C,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAExC,MAAM,aAAa,GAAG,CAAC,KAAe,EAAc,EAAE;IACpD,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;QACxB,OAAO,CAAC,IAAI,CAAC,IAAI,kBAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;KACxB;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE;QACvB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACtB,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC;KACpB;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEK,MAAM,qBAAqB,GAAG,CAAC,KAAe,EAAU,EAAE,CAAC,IAAA,kBAAU,EAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;AAAtF,QAAA,qBAAqB,yBAAiE","sourcesContent":["import { TextEncoder } from 'util';\n\nimport { fnv1a64Hex } from '@yuants/utils';\n\nconst SEP_BYTE = new Uint8Array([0xff]);\n\nconst encodeStrings = (parts: string[]): Uint8Array => {\n const buffers: Uint8Array[] = [];\n for (const part of parts) {\n buffers.push(new TextEncoder().encode(part));\n buffers.push(SEP_BYTE);\n }\n const totalLength = buffers.reduce((sum, b) => sum + b.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const b of buffers) {\n result.set(b, offset);\n offset += b.length;\n }\n return result;\n};\n\nexport const fnv1a64HexFromStrings = (parts: string[]): string => fnv1a64Hex(encodeStrings(parts));\n"]}
@@ -1,16 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const protocol_1 = require("@yuants/protocol");
4
+ const utils_1 = require("@yuants/utils");
4
5
  const state_1 = require("./state");
6
+ const upstream_routing_1 = require("./upstream-routing");
5
7
  const terminal = protocol_1.Terminal.fromNodeEnv();
6
8
  const quoteState = (0, state_1.createQuoteState)();
9
+ const assertFreshnessSatisfied = (data, params) => {
10
+ var _a;
11
+ console.info('[VEX][Quote] Asserting freshness satisfied for requested quotes.', JSON.stringify(params), JSON.stringify(data));
12
+ const { product_ids, fields, updated_at } = params;
13
+ const stillMissed = [];
14
+ for (const product_id of product_ids) {
15
+ for (const field of fields) {
16
+ if (!((_a = data[product_id]) === null || _a === void 0 ? void 0 : _a[field])) {
17
+ stillMissed.push({ product_id, field });
18
+ }
19
+ }
20
+ }
21
+ if (stillMissed.length > 0) {
22
+ throw (0, utils_1.newError)('VEX_QUOTE_FRESHNESS_NOT_SATISFIED', {
23
+ updated_at,
24
+ missed: stillMissed.slice(0, 200),
25
+ missed_total: stillMissed.length,
26
+ });
27
+ }
28
+ };
7
29
  terminal.server.provideService('VEX/UpdateQuotes', {}, async (msg) => {
8
30
  quoteState.update(msg.req);
9
31
  return { res: { code: 0, message: 'OK' } };
10
32
  });
11
- terminal.server.provideService('VEX/DumpQuoteState', {}, async (msg) => {
33
+ terminal.server.provideService('VEX/DumpQuoteState', {}, async () => {
12
34
  return { res: { code: 0, message: 'OK', data: quoteState.dumpAsObject() } };
13
35
  });
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
+ };
14
48
  terminal.server.provideService('VEX/QueryQuotes', {
15
49
  type: 'object',
16
50
  required: ['product_ids', 'fields', 'updated_at'],
@@ -27,21 +61,10 @@ terminal.server.provideService('VEX/QueryQuotes', {
27
61
  },
28
62
  }, async (msg) => {
29
63
  const { product_ids, fields, updated_at } = msg.req;
30
- // 分析缓存缺失的字段
31
- const cacheMissed = [];
32
- for (const product_id of product_ids) {
33
- for (const field of fields) {
34
- const tuple = quoteState.getValueTuple(product_id, field);
35
- if (tuple === undefined || tuple[1] < updated_at) {
36
- cacheMissed.push({ product_id, field });
37
- }
38
- }
39
- }
40
- // TODO: 集中规划需要发送的查询请求,并更新到状态中
41
- // 注意需要限制在途请求数量和复用在途请求的结果,以免过载和浪费资源
42
- // await Promise.all;
43
- // 从状态中获取数据返回
64
+ const cacheMissed = computeCacheMissed(quoteState, product_ids, fields, updated_at);
65
+ await (0, upstream_routing_1.fillQuoteStateFromUpstream)({ terminal, quoteState, cacheMissed, updated_at });
44
66
  const data = quoteState.filter(product_ids, fields, updated_at);
67
+ assertFreshnessSatisfied(data, { product_ids, fields, updated_at });
45
68
  return { res: { code: 0, message: 'OK', data } };
46
69
  });
47
70
  //# sourceMappingURL=service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/quote/service.ts"],"names":[],"mappings":";;AAAA,+CAA4C;AAC5C,mCAA2C;AAG3C,MAAM,QAAQ,GAAG,mBAAQ,CAAC,WAAW,EAAE,CAAC;AAExC,MAAM,UAAU,GAAG,IAAA,wBAAgB,GAAE,CAAC;AAEtC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAqB,kBAAkB,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IACvF,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;AAC7C,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAyB,oBAAoB,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAC7F,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,YAAY,EAAE,EAAE,EAAE,CAAC;AAC9E,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,MAAM,CAAC,cAAc,CAI5B,iBAAiB,EACjB;IACE,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,aAAa,EAAE,QAAQ,EAAE,YAAY,CAAC;IACjD,UAAU,EAAE;QACV,WAAW,EAAE;YACX,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SAC1B;QACD,MAAM,EAAE;YACN,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SAC1B;QACD,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAC/B;CACF,EACD,KAAK,EAAE,GAAG,EAAE,EAAE;IACZ,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC;IACpD,YAAY;IACZ,MAAM,WAAW,GAAoD,EAAE,CAAC;IACxE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE;QACpC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;YAC1B,MAAM,KAAK,GAAG,UAAU,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,UAAU,EAAE;gBAChD,WAAW,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;aACzC;SACF;KACF;IACD,8BAA8B;IAC9B,mCAAmC;IACnC,qBAAqB;IAErB,aAAa;IACb,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAEhE,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;AACnD,CAAC,CACF,CAAC","sourcesContent":["import { Terminal } from '@yuants/protocol';\nimport { createQuoteState } from './state';\nimport { IQuoteKey, IQuoteUpdateAction } from './types';\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteState = createQuoteState();\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 (msg) => {\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, fields, updated_at } = msg.req;\n // 分析缓存缺失的字段\n const cacheMissed: Array<{ product_id: string; field: IQuoteKey }> = [];\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 // TODO: 集中规划需要发送的查询请求,并更新到状态中\n // 注意需要限制在途请求数量和复用在途请求的结果,以免过载和浪费资源\n // await Promise.all;\n\n // 从状态中获取数据返回\n const data = quoteState.filter(product_ids, fields, updated_at);\n\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 { 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"]}
@@ -0,0 +1,15 @@
1
+ import { Terminal } from '@yuants/protocol';
2
+ import { IQuoteKey, IQuoteState } from './types';
3
+ export interface IQuoteMiss {
4
+ product_id: string;
5
+ field: IQuoteKey;
6
+ }
7
+ declare const terminal: Terminal;
8
+ export declare const fillQuoteStateFromUpstream: (params: {
9
+ terminal: Terminal;
10
+ quoteState: IQuoteState;
11
+ cacheMissed: IQuoteMiss[];
12
+ updated_at: number;
13
+ }) => Promise<void>;
14
+ export {};
15
+ //# sourceMappingURL=upstream-routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-routing.d.ts","sourceRoot":"","sources":["../../src/quote/upstream-routing.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAK5C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAErE,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,SAAS,CAAC;CAClB;AA2BD,QAAA,MAAM,QAAQ,UAAyB,CAAC;AAsUxC,eAAO,MAAM,0BAA0B,WAAkB;IACvD,UAAU,QAAQ,CAAC;IACnB,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB,KAAG,QAAQ,IAAI,CA6Ef,CAAC"}
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fillQuoteStateFromUpstream = void 0;
4
+ const exchange_1 = require("@yuants/exchange");
5
+ const protocol_1 = require("@yuants/protocol");
6
+ const utils_1 = require("@yuants/utils");
7
+ const rxjs_1 = require("rxjs");
8
+ const prefix_matcher_1 = require("./prefix-matcher");
9
+ const request_key_1 = require("./request-key");
10
+ const terminal = protocol_1.Terminal.fromNodeEnv();
11
+ const quoteServiceInfos$ = terminal.terminalInfos$.pipe((0, rxjs_1.mergeMap)((infos) => (0, rxjs_1.from)(infos).pipe(
12
+ //
13
+ (0, rxjs_1.mergeMap)((info) => {
14
+ var _a;
15
+ return (0, rxjs_1.from)(Object.values((_a = info.serviceInfo) !== null && _a !== void 0 ? _a : {})).pipe((0, rxjs_1.filter)((serviceInfo) => serviceInfo.method === 'GetQuotes'), (0, rxjs_1.map)((serviceInfo) => ({
16
+ terminal_id: info.terminal_id,
17
+ serviceInfo,
18
+ })), (0, rxjs_1.toArray)());
19
+ }))));
20
+ const mapGroupIdToGroup = new Map();
21
+ /**
22
+ * Build provider groups from runtime terminal infos.
23
+ *
24
+ * Note: `fields` is schema `const`, so VEX must keep a stable order (lexicographical sort).
25
+ */
26
+ quoteServiceInfos$
27
+ .pipe((0, utils_1.listWatch)((v) => v.serviceInfo.service_id, (v) => {
28
+ var _a;
29
+ 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)}`);
30
+ try {
31
+ const metadata = (0, exchange_1.parseQuoteServiceMetadataFromSchema)(v.serviceInfo.schema);
32
+ const fields = [...metadata.fields].sort();
33
+ const group_id = (0, utils_1.encodePath)(metadata.product_id_prefix, fields.join(','), (_a = metadata.max_products_per_request) !== null && _a !== void 0 ? _a : '');
34
+ const provider = {
35
+ terminal_id: v.terminal_id,
36
+ service_id: v.serviceInfo.service_id || v.serviceInfo.method,
37
+ };
38
+ if (mapGroupIdToGroup.get(group_id)) {
39
+ mapGroupIdToGroup.get(group_id).mapTerminalIdToInstance.set(provider.terminal_id, provider);
40
+ }
41
+ else {
42
+ const next = {
43
+ group_id,
44
+ meta: metadata,
45
+ mapTerminalIdToInstance: new Map([[provider.terminal_id, provider]]),
46
+ };
47
+ mapGroupIdToGroup.set(group_id, next);
48
+ console.info('11111111', [...mapGroupIdToGroup.values()]);
49
+ }
50
+ return (0, rxjs_1.of)(void 0).pipe(
51
+ //
52
+ (0, rxjs_1.tap)({
53
+ unsubscribe: () => {
54
+ var _a, _b;
55
+ (_a = mapGroupIdToGroup.get(group_id)) === null || _a === void 0 ? void 0 : _a.mapTerminalIdToInstance.delete(v.terminal_id);
56
+ if (((_b = mapGroupIdToGroup.get(group_id)) === null || _b === void 0 ? void 0 : _b.mapTerminalIdToInstance.size) === 0) {
57
+ mapGroupIdToGroup.delete(group_id);
58
+ }
59
+ },
60
+ }));
61
+ }
62
+ catch (_b) {
63
+ // Ignore invalid schemas/providers
64
+ console.info(`[VEX][Quote] Ignored GetQuotes provider from terminal ${v.terminal_id} `, `service ${v.serviceInfo.service_id} due to invalid schema.`);
65
+ return rxjs_1.EMPTY;
66
+ }
67
+ }))
68
+ .subscribe();
69
+ // -----------------------------------------------------------------------------
70
+ // Load balancing & upstream request execution
71
+ // -----------------------------------------------------------------------------
72
+ const mapGroupIdToRoundRobinIndex = new Map();
73
+ const pickInstance = (group_id, instances) => {
74
+ var _a;
75
+ if (instances.length === 0)
76
+ throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_INSTANCE_EMPTY', { group_id });
77
+ const nextIndex = ((_a = mapGroupIdToRoundRobinIndex.get(group_id)) !== null && _a !== void 0 ? _a : 0) % instances.length;
78
+ mapGroupIdToRoundRobinIndex.set(group_id, nextIndex + 1);
79
+ return instances[nextIndex];
80
+ };
81
+ /**
82
+ * Call a specified vendor terminal + service instance.
83
+ *
84
+ * Any non-0 response is treated as fatal (strict freshness requirement).
85
+ */
86
+ const requestGetQuotes = async (terminal, instance, req) => {
87
+ const res = await (0, rxjs_1.firstValueFrom)(terminal.client
88
+ .request('GetQuotes', instance.terminal_id, req, instance.service_id)
89
+ .pipe((0, rxjs_1.map)((msg) => msg.res), (0, rxjs_1.filter)((v) => v !== undefined)));
90
+ if (res.code !== 0) {
91
+ throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_ERROR', { instance, res });
92
+ }
93
+ if (res.data === undefined) {
94
+ throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });
95
+ }
96
+ return res.data;
97
+ };
98
+ /**
99
+ * Per-provider (group_id) concurrency limit: 1.
100
+ * Implemented as a per-group promise tail.
101
+ */
102
+ const mapGroupIdToTailPromise = new Map();
103
+ const runWithProviderGroupConcurrencyLimit1 = async (group_id, fn) => {
104
+ var _a;
105
+ const prev = (_a = mapGroupIdToTailPromise.get(group_id)) !== null && _a !== void 0 ? _a : Promise.resolve();
106
+ let resolveCurrent = () => { };
107
+ const current = new Promise((resolve) => {
108
+ resolveCurrent = resolve;
109
+ });
110
+ mapGroupIdToTailPromise.set(group_id, prev.then(() => current));
111
+ await prev;
112
+ try {
113
+ return await fn();
114
+ }
115
+ finally {
116
+ resolveCurrent();
117
+ }
118
+ };
119
+ /**
120
+ * A tiny async limiter: used as a global concurrency cap to avoid request explosions.
121
+ */
122
+ const createConcurrencyLimiter = (concurrency) => {
123
+ const queue = [];
124
+ let active = 0;
125
+ const next = () => {
126
+ if (active >= concurrency)
127
+ return;
128
+ const task = queue.shift();
129
+ if (!task)
130
+ return;
131
+ active++;
132
+ task();
133
+ };
134
+ return async (fn) => {
135
+ return await new Promise((resolve, reject) => {
136
+ queue.push(async () => {
137
+ try {
138
+ resolve(await fn());
139
+ }
140
+ catch (e) {
141
+ reject(e);
142
+ }
143
+ finally {
144
+ active--;
145
+ next();
146
+ }
147
+ });
148
+ next();
149
+ });
150
+ };
151
+ };
152
+ // Global concurrency cap for upstream `GetQuotes` calls (provider-level cap is handled separately).
153
+ const limitGetQuotes = createConcurrencyLimiter(32);
154
+ /**
155
+ * In-flight dedup:
156
+ * Same (provider group + product batch) should share a single upstream request promise.
157
+ */
158
+ const mapKeyToInFlightGetQuotesPromise = new Map();
159
+ const requestGetQuotesInFlight = (terminal, key, planned) => {
160
+ const existing = mapKeyToInFlightGetQuotesPromise.get(key);
161
+ if (existing)
162
+ return existing;
163
+ const promise = limitGetQuotes(() => runWithProviderGroupConcurrencyLimit1(planned.group_id, async () => {
164
+ const instance = pickInstance(planned.group_id, planned.instances);
165
+ return await requestGetQuotes(terminal, instance, planned.req);
166
+ })).finally(() => {
167
+ mapKeyToInFlightGetQuotesPromise.delete(key);
168
+ });
169
+ mapKeyToInFlightGetQuotesPromise.set(key, promise);
170
+ return promise;
171
+ };
172
+ const buildProviderIndices = (groups) => {
173
+ const mapGroupIdToGroup = new Map(groups.map((x) => [x.group_id, x]));
174
+ const prefixMatcher = (0, prefix_matcher_1.createSortedPrefixMatcher)(groups.map((group) => ({ prefix: group.meta.product_id_prefix, value: group.group_id })));
175
+ const mapFieldToGroupIds = new Map();
176
+ for (const group of groups) {
177
+ for (const field of group.meta.fields) {
178
+ let groupIds = mapFieldToGroupIds.get(field);
179
+ if (!groupIds) {
180
+ groupIds = new Set();
181
+ mapFieldToGroupIds.set(field, groupIds);
182
+ }
183
+ groupIds.add(group.group_id);
184
+ }
185
+ }
186
+ return { mapGroupIdToGroup, prefixMatcher, mapFieldToGroupIds };
187
+ };
188
+ /**
189
+ * L1 quote routing (per `docs/zh-Hans/code-guidelines/exchange.md`):
190
+ * For each missed (product_id, field), route to `S_product_id ∩ S_field`.
191
+ */
192
+ const routeMisses = (cacheMissed, indices, updated_at) => {
193
+ const { prefixMatcher, mapFieldToGroupIds } = indices;
194
+ const mapProductIdToGroupIds = new Map();
195
+ const productsByGroupId = new Map();
196
+ const unroutableProducts = new Set();
197
+ // Field unavailable: return "" but keep updated_at satisfied to avoid repeated misses.
198
+ const unavailableAction = {};
199
+ for (const miss of cacheMissed) {
200
+ const { product_id, field } = miss;
201
+ let productGroupIds = mapProductIdToGroupIds.get(product_id);
202
+ if (!productGroupIds) {
203
+ productGroupIds = prefixMatcher.match(product_id);
204
+ mapProductIdToGroupIds.set(product_id, productGroupIds);
205
+ }
206
+ if (productGroupIds.length === 0) {
207
+ unroutableProducts.add(product_id);
208
+ continue;
209
+ }
210
+ const fieldGroupIds = mapFieldToGroupIds.get(field);
211
+ if (!fieldGroupIds) {
212
+ if (!unavailableAction[product_id])
213
+ unavailableAction[product_id] = {};
214
+ unavailableAction[product_id][field] = ['', updated_at];
215
+ continue;
216
+ }
217
+ let matched = false;
218
+ for (const group_id of productGroupIds) {
219
+ if (!fieldGroupIds.has(group_id))
220
+ continue;
221
+ matched = true;
222
+ let productIds = productsByGroupId.get(group_id);
223
+ if (!productIds) {
224
+ productIds = new Set();
225
+ productsByGroupId.set(group_id, productIds);
226
+ }
227
+ productIds.add(product_id);
228
+ }
229
+ if (!matched) {
230
+ if (!unavailableAction[product_id])
231
+ unavailableAction[product_id] = {};
232
+ unavailableAction[product_id][field] = ['', updated_at];
233
+ }
234
+ }
235
+ return { productsByGroupId, unavailableAction, unroutableProducts };
236
+ };
237
+ const createRequestKey = (group_id, batchProductIds) => (0, utils_1.encodePath)(group_id, (0, request_key_1.fnv1a64HexFromStrings)(batchProductIds));
238
+ const planRequests = (productsByGroupId, mapGroupIdToGroup) => {
239
+ var _a;
240
+ const plannedRequests = [];
241
+ for (const [group_id, productIdSet] of productsByGroupId) {
242
+ const group = mapGroupIdToGroup.get(group_id);
243
+ if (!group)
244
+ continue;
245
+ const sortedProductIds = [...productIdSet].sort();
246
+ const max = (_a = group.meta.max_products_per_request) !== null && _a !== void 0 ? _a : sortedProductIds.length;
247
+ for (let i = 0; i < sortedProductIds.length; i += max) {
248
+ const batchProductIds = sortedProductIds.slice(i, i + max);
249
+ const key = createRequestKey(group_id, batchProductIds);
250
+ plannedRequests.push({
251
+ key,
252
+ planned: {
253
+ group_id,
254
+ instances: Array.from(group.mapTerminalIdToInstance.values()),
255
+ req: { product_ids: batchProductIds, fields: group.meta.fields },
256
+ },
257
+ });
258
+ }
259
+ }
260
+ return plannedRequests;
261
+ };
262
+ const fillQuoteStateFromUpstream = async (params) => {
263
+ const { terminal, quoteState, cacheMissed, updated_at } = params;
264
+ if (cacheMissed.length === 0)
265
+ return;
266
+ const providerGroups = Array.from(mapGroupIdToGroup.values());
267
+ console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]UpstreamProviderDiscovery`, ` Discovered ${providerGroups.length} GetQuotes provider groups from terminal infos.`, JSON.stringify(providerGroups));
268
+ if (providerGroups.length === 0) {
269
+ throw (0, utils_1.newError)('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });
270
+ }
271
+ const indices = buildProviderIndices(providerGroups);
272
+ const { productsByGroupId, unavailableAction, unroutableProducts } = routeMisses(cacheMissed, indices, updated_at);
273
+ console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]RouteDispatched`, ` Routed ${cacheMissed.length} missed quotes to ${productsByGroupId.size} provider groups, ` +
274
+ `${unroutableProducts.size} unroutable products.`, JSON.stringify({
275
+ productsByGroupId: [...productsByGroupId.entries()].map(([group_id, productIds]) => ({
276
+ group_id,
277
+ product_ids: [...productIds],
278
+ })),
279
+ unroutable_products: [...unroutableProducts],
280
+ unavailable_action: unavailableAction,
281
+ }));
282
+ if (unroutableProducts.size !== 0) {
283
+ throw (0, utils_1.newError)('VEX_QUOTE_PRODUCT_UNROUTABLE', {
284
+ updated_at,
285
+ unroutable_products: [...unroutableProducts].slice(0, 200),
286
+ unroutable_products_total: unroutableProducts.size,
287
+ });
288
+ }
289
+ quoteState.update(unavailableAction);
290
+ const plannedRequests = planRequests(productsByGroupId, indices.mapGroupIdToGroup);
291
+ console.info((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]RequestPlanned`, `Planned ${plannedRequests.length} upstream GetQuotes requests.`, JSON.stringify(plannedRequests.map(({ key, planned }) => ({
292
+ key,
293
+ group_id: planned.group_id,
294
+ product_ids: planned.req.product_ids,
295
+ fields: planned.req.fields,
296
+ }))));
297
+ const actions = await Promise.all(plannedRequests.map(async ({ key, planned }) => await requestGetQuotesInFlight(terminal, key, planned)));
298
+ console.debug((0, utils_1.formatTime)(Date.now()), `[VEX][Quote]RequestReceived`, `Received ${actions.length} upstream GetQuotes responses.`);
299
+ for (const action of actions) {
300
+ quoteState.update(action);
301
+ }
302
+ };
303
+ exports.fillQuoteStateFromUpstream = fillQuoteStateFromUpstream;
304
+ //# sourceMappingURL=upstream-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-routing.js","sourceRoot":"","sources":["../../src/quote/upstream-routing.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n IQuoteUpdateAction as IExchangeQuoteUpdateAction,\n IQuoteServiceMetadata,\n IQuoteServiceRequestByVEX,\n parseQuoteServiceMetadataFromSchema,\n} from '@yuants/exchange';\nimport { Terminal } from '@yuants/protocol';\nimport { encodePath, formatTime, listWatch, newError } from '@yuants/utils';\nimport { EMPTY, filter, firstValueFrom, from, map, mergeMap, of, tap, timer, toArray } from 'rxjs';\nimport { createSortedPrefixMatcher } from './prefix-matcher';\nimport { fnv1a64HexFromStrings } from './request-key';\nimport { IQuoteKey, IQuoteState, IQuoteUpdateAction } from './types';\n\nexport interface IQuoteMiss {\n product_id: string;\n field: IQuoteKey;\n}\n\n// -----------------------------------------------------------------------------\n// Provider discovery (schema-driven)\n// -----------------------------------------------------------------------------\n\ninterface 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 */\ninterface IQuoteProviderGroup {\n group_id: string;\n meta: IQuoteServiceMetadata;\n mapTerminalIdToInstance: Map<string, IQuoteProviderInstance>;\n}\n\ntype IPlannedRequest = {\n group_id: string;\n instances: IQuoteProviderInstance[];\n req: IQuoteServiceRequestByVEX;\n};\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteServiceInfos$ = terminal.terminalInfos$.pipe(\n mergeMap((infos) =>\n from(infos).pipe(\n //\n mergeMap((info) =>\n from(Object.values(info.serviceInfo ?? {})).pipe(\n filter((serviceInfo) => serviceInfo.method === 'GetQuotes'),\n map((serviceInfo) => ({\n terminal_id: info.terminal_id,\n serviceInfo,\n })),\n toArray(),\n ),\n ),\n ),\n ),\n);\n\nconst mapGroupIdToGroup = new Map<string, IQuoteProviderGroup>();\n\n/**\n * Build provider groups from runtime terminal infos.\n *\n * Note: `fields` is schema `const`, so VEX must keep a stable order (lexicographical sort).\n */\nquoteServiceInfos$\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 = parseQuoteServiceMetadataFromSchema(v.serviceInfo.schema);\n const fields = [...(metadata.fields as unknown as IQuoteKey[])].sort();\n const group_id = encodePath(\n metadata.product_id_prefix,\n fields.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 if (mapGroupIdToGroup.get(group_id)) {\n mapGroupIdToGroup.get(group_id)!.mapTerminalIdToInstance.set(provider.terminal_id, provider);\n } else {\n const next: IQuoteProviderGroup = {\n group_id,\n meta: metadata,\n mapTerminalIdToInstance: new Map([[provider.terminal_id, provider]]),\n };\n mapGroupIdToGroup.set(group_id, next);\n console.info('11111111', [...mapGroupIdToGroup.values()]);\n }\n return of(void 0).pipe(\n //\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 },\n }),\n );\n } catch {\n // Ignore invalid schemas/providers\n console.info(\n `[VEX][Quote] 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// -----------------------------------------------------------------------------\n// Load balancing & upstream request execution\n// -----------------------------------------------------------------------------\n\nconst mapGroupIdToRoundRobinIndex = new Map<string, number>();\nconst pickInstance = (group_id: string, instances: IQuoteProviderInstance[]): IQuoteProviderInstance => {\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\n/**\n * Call a specified vendor terminal + service instance.\n *\n * Any non-0 response is treated as fatal (strict freshness requirement).\n */\nconst requestGetQuotes = async (\n terminal: Terminal,\n instance: IQuoteProviderInstance,\n req: IQuoteServiceRequestByVEX,\n): Promise<IExchangeQuoteUpdateAction> => {\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) {\n throw newError('VEX_QUOTE_PROVIDER_ERROR', { instance, res });\n }\n if (res.data === undefined) {\n throw newError('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });\n }\n return res.data as any;\n};\n\n/**\n * Per-provider (group_id) concurrency limit: 1.\n * Implemented as a per-group promise tail.\n */\nconst mapGroupIdToTailPromise = new Map<string, Promise<void>>();\nconst runWithProviderGroupConcurrencyLimit1 = async <T>(\n group_id: string,\n fn: () => Promise<T>,\n): Promise<T> => {\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\n/**\n * A tiny async limiter: used as a global concurrency cap to avoid request explosions.\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 return 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};\n\n// Global concurrency cap for upstream `GetQuotes` calls (provider-level cap is handled separately).\nconst limitGetQuotes = createConcurrencyLimiter(32);\n\n/**\n * In-flight dedup:\n * Same (provider group + product batch) should share a single upstream request promise.\n */\nconst mapKeyToInFlightGetQuotesPromise = new Map<string, Promise<IExchangeQuoteUpdateAction>>();\nconst requestGetQuotesInFlight = (terminal: Terminal, key: string, planned: IPlannedRequest) => {\n const existing = mapKeyToInFlightGetQuotesPromise.get(key);\n if (existing) return existing;\n const promise = limitGetQuotes(() =>\n runWithProviderGroupConcurrencyLimit1(planned.group_id, async () => {\n const instance = pickInstance(planned.group_id, planned.instances);\n return await requestGetQuotes(terminal, instance, planned.req);\n }),\n ).finally(() => {\n mapKeyToInFlightGetQuotesPromise.delete(key);\n });\n mapKeyToInFlightGetQuotesPromise.set(key, promise);\n return promise;\n};\n\n// -----------------------------------------------------------------------------\n// Routing indices (prefix + field inverted index)\n// -----------------------------------------------------------------------------\n\ntype IProviderIndices = ReturnType<typeof buildProviderIndices>;\n\nconst buildProviderIndices = (groups: IQuoteProviderGroup[]) => {\n const mapGroupIdToGroup = new Map(groups.map((x) => [x.group_id, x] as const));\n const prefixMatcher = createSortedPrefixMatcher(\n groups.map((group) => ({ prefix: group.meta.product_id_prefix, value: group.group_id })),\n );\n const mapFieldToGroupIds = new Map<IQuoteKey, Set<string>>();\n for (const group of groups) {\n for (const field of group.meta.fields) {\n let groupIds = mapFieldToGroupIds.get(field);\n if (!groupIds) {\n groupIds = new Set<string>();\n mapFieldToGroupIds.set(field, groupIds);\n }\n groupIds.add(group.group_id);\n }\n }\n return { mapGroupIdToGroup, prefixMatcher, mapFieldToGroupIds };\n};\n\n/**\n * L1 quote routing (per `docs/zh-Hans/code-guidelines/exchange.md`):\n * For each missed (product_id, field), route to `S_product_id ∩ S_field`.\n */\nconst routeMisses = (\n cacheMissed: IQuoteMiss[],\n indices: IProviderIndices,\n updated_at: number,\n): {\n productsByGroupId: Map<string, Set<string>>;\n unavailableAction: IQuoteUpdateAction;\n unroutableProducts: Set<string>;\n} => {\n const { prefixMatcher, mapFieldToGroupIds } = indices;\n\n const mapProductIdToGroupIds = new Map<string, string[]>();\n\n const productsByGroupId = new Map<string, Set<string>>();\n const unroutableProducts = new Set<string>();\n // Field unavailable: return \"\" but keep updated_at satisfied to avoid repeated misses.\n const unavailableAction: IQuoteUpdateAction = {};\n\n for (const miss of cacheMissed) {\n const { product_id, field } = miss;\n\n let productGroupIds = mapProductIdToGroupIds.get(product_id);\n if (!productGroupIds) {\n productGroupIds = prefixMatcher.match(product_id);\n mapProductIdToGroupIds.set(product_id, productGroupIds);\n }\n if (productGroupIds.length === 0) {\n unroutableProducts.add(product_id);\n continue;\n }\n\n const fieldGroupIds = mapFieldToGroupIds.get(field);\n if (!fieldGroupIds) {\n if (!unavailableAction[product_id]) unavailableAction[product_id] = {};\n unavailableAction[product_id]![field] = ['', updated_at];\n continue;\n }\n\n let matched = false;\n for (const group_id of productGroupIds) {\n if (!fieldGroupIds.has(group_id)) continue;\n matched = true;\n let productIds = productsByGroupId.get(group_id);\n if (!productIds) {\n productIds = new Set<string>();\n productsByGroupId.set(group_id, productIds);\n }\n productIds.add(product_id);\n }\n\n if (!matched) {\n if (!unavailableAction[product_id]) unavailableAction[product_id] = {};\n unavailableAction[product_id]![field] = ['', updated_at];\n }\n }\n\n return { productsByGroupId, unavailableAction, unroutableProducts };\n};\n\nconst createRequestKey = (group_id: string, batchProductIds: string[]) =>\n encodePath(group_id, fnv1a64HexFromStrings(batchProductIds));\n\nconst planRequests = (\n productsByGroupId: Map<string, Set<string>>,\n mapGroupIdToGroup: Map<string, IQuoteProviderGroup>,\n): Array<{ key: string; planned: IPlannedRequest }> => {\n const plannedRequests: Array<{ key: string; planned: IPlannedRequest }> = [];\n for (const [group_id, productIdSet] of productsByGroupId) {\n const group = mapGroupIdToGroup.get(group_id);\n if (!group) continue;\n const sortedProductIds = [...productIdSet].sort();\n const max = group.meta.max_products_per_request ?? sortedProductIds.length;\n for (let i = 0; i < sortedProductIds.length; i += max) {\n const batchProductIds = sortedProductIds.slice(i, i + max);\n const key = createRequestKey(group_id, batchProductIds);\n plannedRequests.push({\n key,\n planned: {\n group_id,\n instances: Array.from(group.mapTerminalIdToInstance.values()),\n req: { product_ids: batchProductIds, fields: group.meta.fields as any },\n },\n });\n }\n }\n return plannedRequests;\n};\n\nexport const fillQuoteStateFromUpstream = async (params: {\n terminal: Terminal;\n quoteState: IQuoteState;\n cacheMissed: IQuoteMiss[];\n updated_at: number;\n}): Promise<void> => {\n const { terminal, quoteState, cacheMissed, updated_at } = params;\n if (cacheMissed.length === 0) return;\n\n const providerGroups = Array.from(mapGroupIdToGroup.values());\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]UpstreamProviderDiscovery`,\n ` Discovered ${providerGroups.length} GetQuotes provider groups from terminal infos.`,\n JSON.stringify(providerGroups),\n );\n if (providerGroups.length === 0) {\n throw newError('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });\n }\n\n const indices = buildProviderIndices(providerGroups);\n const { productsByGroupId, unavailableAction, unroutableProducts } = routeMisses(\n cacheMissed,\n indices,\n updated_at,\n );\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RouteDispatched`,\n ` Routed ${cacheMissed.length} missed quotes to ${productsByGroupId.size} provider groups, ` +\n `${unroutableProducts.size} unroutable products.`,\n JSON.stringify({\n productsByGroupId: [...productsByGroupId.entries()].map(([group_id, productIds]) => ({\n group_id,\n product_ids: [...productIds],\n })),\n unroutable_products: [...unroutableProducts],\n unavailable_action: unavailableAction,\n }),\n );\n\n if (unroutableProducts.size !== 0) {\n throw newError('VEX_QUOTE_PRODUCT_UNROUTABLE', {\n updated_at,\n unroutable_products: [...unroutableProducts].slice(0, 200),\n unroutable_products_total: unroutableProducts.size,\n });\n }\n\n quoteState.update(unavailableAction);\n\n const plannedRequests = planRequests(productsByGroupId, indices.mapGroupIdToGroup);\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RequestPlanned`,\n `Planned ${plannedRequests.length} upstream GetQuotes requests.`,\n JSON.stringify(\n plannedRequests.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 Promise.all(\n plannedRequests.map(async ({ key, planned }) => await requestGetQuotesInFlight(terminal, key, planned)),\n );\n\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 as unknown as IQuoteUpdateAction);\n }\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuants/app-virtual-exchange",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "main": "lib/index.js",
5
5
  "files": [
6
6
  "dist",
@@ -8,16 +8,16 @@
8
8
  "temp"
9
9
  ],
10
10
  "dependencies": {
11
- "@yuants/protocol": "0.53.2",
12
- "@yuants/utils": "0.14.0",
13
- "@yuants/data-product": "0.5.0",
14
- "@yuants/data-account": "0.10.1",
15
- "@yuants/data-order": "0.7.0",
16
- "@yuants/data-quote": "0.3.0",
17
- "@yuants/secret": "0.4.0",
18
- "@yuants/sql": "0.9.30",
19
- "@yuants/exchange": "0.6.0",
20
- "@yuants/cache": "0.3.3",
11
+ "@yuants/protocol": "0.53.3",
12
+ "@yuants/utils": "0.15.0",
13
+ "@yuants/data-product": "0.5.1",
14
+ "@yuants/data-account": "0.10.2",
15
+ "@yuants/data-order": "0.7.1",
16
+ "@yuants/data-quote": "0.3.1",
17
+ "@yuants/secret": "0.4.1",
18
+ "@yuants/sql": "0.9.31",
19
+ "@yuants/exchange": "0.6.1",
20
+ "@yuants/cache": "0.3.4",
21
21
  "rxjs": "~7.5.6",
22
22
  "ajv": "~8.12.0"
23
23
  },
@@ -1,12 +1,12 @@
1
1
  {
2
- "apps/virtual-exchange/CHANGELOG.json": "7367c9083f6d173ed725b45088215eac3d44e132",
3
- "apps/virtual-exchange/CHANGELOG.md": "22f9356033d531f79fd57e78ce0e5d88b165e21d",
2
+ "apps/virtual-exchange/CHANGELOG.json": "e2a0b1afcf50a4608047e28c8689dc080ece72ef",
3
+ "apps/virtual-exchange/CHANGELOG.md": "f4af697e8de0beecc45c8c91f5ebb393b3a5d8ce",
4
4
  "apps/virtual-exchange/api-extractor.json": "62f4fd324425b9a235f0c117975967aab09ced0c",
5
5
  "apps/virtual-exchange/config/jest.config.json": "4bb17bde3ee911163a3edb36a6eb71491d80b1bd",
6
6
  "apps/virtual-exchange/config/rig.json": "f6c7b5537dc77a3170ba9f008bae3b6c3ee11956",
7
7
  "apps/virtual-exchange/config/typescript.json": "854907e8a821f2050f6533368db160c649c25348",
8
8
  "apps/virtual-exchange/etc/app-virtual-exchange.api.md": "6cb40ec1fa2d40a31a7b0dd3f02b8b24a4d7c4de",
9
- "apps/virtual-exchange/package.json": "aeb6642eb4bc7f546c292e450662aa9d8c45aab3",
9
+ "apps/virtual-exchange/package.json": "a16eef7a4d52d83a164be90adce11b04cb4f56e6",
10
10
  "apps/virtual-exchange/src/credential.ts": "7ff9cfe06e46005b2a69e60b5596eb1f59d42bba",
11
11
  "apps/virtual-exchange/src/general.ts": "b3d0cd8c57975b9711008beaa05ad7f6812bd57e",
12
12
  "apps/virtual-exchange/src/index.ts": "67e1963facf0ee2aedd871496361cff90bf49356",
@@ -27,21 +27,24 @@
27
27
  "apps/virtual-exchange/src/quote/implementations/v1.ts": "1a9c482587db58d070e2ee7ae9adbf6b5e7b3906",
28
28
  "apps/virtual-exchange/src/quote/implementations/v2.ts": "601c0a28523316514dba166f6fc96e6c3e5f224c",
29
29
  "apps/virtual-exchange/src/quote/implementations/v3.ts": "162ca17088bf4c1d42e314cb34179f4d6fbc5fb6",
30
- "apps/virtual-exchange/src/quote/service.ts": "6464c8ead2d713f99e4d5e4e26ba9c293521640b",
30
+ "apps/virtual-exchange/src/quote/prefix-matcher.ts": "079066de8c1d5d91b63ffc2bf67c2163e7b8a540",
31
+ "apps/virtual-exchange/src/quote/request-key.ts": "600a2fbfe3ba7b6f0476b83236817b4f11d577f6",
32
+ "apps/virtual-exchange/src/quote/service.ts": "21b00f9c49afa5e66da5ba1310932003262d9edd",
31
33
  "apps/virtual-exchange/src/quote/state.benchmark.ts": "075d3c5bab0ae6f7b61accfe977f7d35233b06ad",
32
34
  "apps/virtual-exchange/src/quote/state.ts": "a13be6e84d30b54de16f90e1cb3feeeab75957df",
33
35
  "apps/virtual-exchange/src/quote/types.ts": "e07316e2e0095f0a7a8ad410a8f952a7d4b96e3e",
36
+ "apps/virtual-exchange/src/quote/upstream-routing.ts": "ca026c8dc3194e8f698160b7ea1e9ea6e4da819f",
34
37
  "apps/virtual-exchange/tsconfig.json": "22f94ca28b507f8ddcc21b9053158eefd3f726a9",
35
38
  "apps/virtual-exchange/.rush/temp/shrinkwrap-deps.json": "2c8344167a574161e8a89138bc1eb686bf6339de",
36
- "libraries/protocol/temp/package-deps.json": "0bd43721e96039b52d7b59c834dc6df45cf75e3f",
37
- "libraries/utils/temp/package-deps.json": "6d58e9b325e8d16de8a878c32010f626b12a01da",
38
- "libraries/data-product/temp/package-deps.json": "a03f08f30800d5fb52c5d019bda4f8e7ec04e344",
39
- "libraries/data-account/temp/package-deps.json": "e6ba0067a1b68f17266564f15987936ab2672eb9",
40
- "libraries/data-order/temp/package-deps.json": "ccdc9819f254f37a3591bb37876a86c703df33a6",
41
- "libraries/data-quote/temp/package-deps.json": "d5fd89abda84b46f6c14925eda0c381c36b94dfe",
42
- "libraries/secret/temp/package-deps.json": "28f70cf8095162d1adb59070f30bf21f452537df",
43
- "libraries/sql/temp/package-deps.json": "4a9a7ec55f04b20459e664e81e76fa74b6c77b39",
44
- "libraries/exchange/temp/package-deps.json": "8fdfc5722901308d3c49c438eed674f35508c062",
45
- "libraries/cache/temp/package-deps.json": "a4afa15e6462983f9d3735d31dc1ed8a683fb4dc",
39
+ "libraries/protocol/temp/package-deps.json": "c94931cb0eab7180d90a06d740c1a97c0ac0c6b6",
40
+ "libraries/utils/temp/package-deps.json": "7f1eab07acae68df931b001594a522f050ace396",
41
+ "libraries/data-product/temp/package-deps.json": "899bba8ace3f04b2035488ff9059e40732ef1d97",
42
+ "libraries/data-account/temp/package-deps.json": "61641a5eb18cbaceedb085230d86589ea8d076ae",
43
+ "libraries/data-order/temp/package-deps.json": "8aa3e27ff80629d417bfe920e85d8525fbb52f42",
44
+ "libraries/data-quote/temp/package-deps.json": "272c0fe62ababfa66a7589139c31231a6fdce2b4",
45
+ "libraries/secret/temp/package-deps.json": "360293dbcad7870a579c7c043d7f8f87a1b68c48",
46
+ "libraries/sql/temp/package-deps.json": "041074e0102f9a01820c0fddcf82b9eaee226c79",
47
+ "libraries/exchange/temp/package-deps.json": "3928985332795cb9b230ced140003ccf2287af41",
48
+ "libraries/cache/temp/package-deps.json": "e1f94620bc6245add32a4f9d379ed2901129d818",
46
49
  "tools/toolkit/temp/package-deps.json": "23e053490eb8feade23e4d45de4e54883e322711"
47
50
  }