@yuants/app-virtual-exchange 0.9.4 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/position.js +17 -0
  2. package/dist/position.js.map +1 -1
  3. package/dist/quote/__tests__/implementations.test.js +218 -0
  4. package/dist/quote/__tests__/implementations.test.js.map +1 -0
  5. package/dist/quote/implementations/v0.js +7 -9
  6. package/dist/quote/implementations/v0.js.map +1 -1
  7. package/dist/quote/implementations/v1.js +10 -4
  8. package/dist/quote/implementations/v1.js.map +1 -1
  9. package/dist/quote/service.js +103 -30
  10. package/dist/quote/service.js.map +1 -1
  11. package/dist/quote/types.js.map +1 -1
  12. package/dist/quote/upstream/executor.js +98 -0
  13. package/dist/quote/upstream/executor.js.map +1 -0
  14. package/dist/quote/upstream/index.js +2 -0
  15. package/dist/quote/upstream/index.js.map +1 -0
  16. package/dist/quote/upstream/prefix-matcher.js.map +1 -0
  17. package/dist/quote/upstream/registry.js +116 -0
  18. package/dist/quote/upstream/registry.js.map +1 -0
  19. package/dist/quote/upstream/router.js +119 -0
  20. package/dist/quote/upstream/router.js.map +1 -0
  21. package/lib/position.d.ts.map +1 -1
  22. package/lib/position.js +17 -0
  23. package/lib/position.js.map +1 -1
  24. package/lib/quote/__tests__/implementations.test.d.ts +2 -0
  25. package/lib/quote/__tests__/implementations.test.d.ts.map +1 -0
  26. package/lib/quote/__tests__/implementations.test.js +220 -0
  27. package/lib/quote/__tests__/implementations.test.js.map +1 -0
  28. package/lib/quote/implementations/v0.d.ts.map +1 -1
  29. package/lib/quote/implementations/v0.js +7 -9
  30. package/lib/quote/implementations/v0.js.map +1 -1
  31. package/lib/quote/implementations/v1.d.ts.map +1 -1
  32. package/lib/quote/implementations/v1.js +10 -4
  33. package/lib/quote/implementations/v1.js.map +1 -1
  34. package/lib/quote/service.js +102 -29
  35. package/lib/quote/service.js.map +1 -1
  36. package/lib/quote/types.d.ts +18 -0
  37. package/lib/quote/types.d.ts.map +1 -1
  38. package/lib/quote/types.js.map +1 -1
  39. package/lib/quote/upstream/executor.d.ts +8 -0
  40. package/lib/quote/upstream/executor.d.ts.map +1 -0
  41. package/lib/quote/upstream/executor.js +102 -0
  42. package/lib/quote/upstream/executor.js.map +1 -0
  43. package/lib/quote/upstream/index.d.ts +3 -0
  44. package/lib/quote/upstream/index.d.ts.map +1 -0
  45. package/lib/quote/upstream/index.js +6 -0
  46. package/lib/quote/upstream/index.js.map +1 -0
  47. package/lib/quote/upstream/prefix-matcher.d.ts.map +1 -0
  48. package/lib/quote/upstream/prefix-matcher.js.map +1 -0
  49. package/lib/quote/upstream/registry.d.ts +18 -0
  50. package/lib/quote/upstream/registry.d.ts.map +1 -0
  51. package/lib/quote/upstream/registry.js +120 -0
  52. package/lib/quote/upstream/registry.js.map +1 -0
  53. package/lib/quote/upstream/router.d.ts +27 -0
  54. package/lib/quote/upstream/router.d.ts.map +1 -0
  55. package/lib/quote/upstream/router.js +124 -0
  56. package/lib/quote/upstream/router.js.map +1 -0
  57. package/package.json +3 -3
  58. package/temp/package-deps.json +17 -13
  59. package/dist/quote/prefix-matcher.js.map +0 -1
  60. package/dist/quote/request-key.js +0 -20
  61. package/dist/quote/request-key.js.map +0 -1
  62. package/dist/quote/upstream-routing.js +0 -300
  63. package/dist/quote/upstream-routing.js.map +0 -1
  64. package/lib/quote/prefix-matcher.d.ts.map +0 -1
  65. package/lib/quote/prefix-matcher.js.map +0 -1
  66. package/lib/quote/request-key.d.ts +0 -2
  67. package/lib/quote/request-key.d.ts.map +0 -1
  68. package/lib/quote/request-key.js +0 -24
  69. package/lib/quote/request-key.js.map +0 -1
  70. package/lib/quote/upstream-routing.d.ts +0 -15
  71. package/lib/quote/upstream-routing.d.ts.map +0 -1
  72. package/lib/quote/upstream-routing.js +0 -304
  73. package/lib/quote/upstream-routing.js.map +0 -1
  74. /package/dist/quote/{prefix-matcher.js → upstream/prefix-matcher.js} +0 -0
  75. /package/lib/quote/{prefix-matcher.d.ts → upstream/prefix-matcher.d.ts} +0 -0
  76. /package/lib/quote/{prefix-matcher.js → upstream/prefix-matcher.js} +0 -0
@@ -1,304 +0,0 @@
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
@@ -1 +0,0 @@
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"]}