@fintekkers/ledger-models 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node/wrappers/models/hydrate.test.d.ts +1 -0
- package/node/wrappers/models/hydrate.test.js +204 -0
- package/node/wrappers/models/hydrate.test.js.map +1 -0
- package/node/wrappers/models/hydrate.test.ts +214 -0
- package/node/wrappers/models/lazy-hydrate-race.test.d.ts +1 -0
- package/node/wrappers/models/lazy-hydrate-race.test.js +153 -0
- package/node/wrappers/models/lazy-hydrate-race.test.js.map +1 -0
- package/node/wrappers/models/lazy-hydrate-race.test.ts +144 -0
- package/node/wrappers/models/lazy-hydrate.bench.test.d.ts +1 -0
- package/node/wrappers/models/lazy-hydrate.bench.test.js +121 -0
- package/node/wrappers/models/lazy-hydrate.bench.test.js.map +1 -0
- package/node/wrappers/models/lazy-hydrate.bench.test.ts +103 -0
- package/node/wrappers/models/portfolio/portfolio.d.ts +18 -0
- package/node/wrappers/models/portfolio/portfolio.js +93 -1
- package/node/wrappers/models/portfolio/portfolio.js.map +1 -1
- package/node/wrappers/models/portfolio/portfolio.ts +58 -1
- package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.d.ts +1 -0
- package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.js +158 -0
- package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.js.map +1 -0
- package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.ts +153 -0
- package/node/wrappers/models/price/Price.d.ts +5 -0
- package/node/wrappers/models/price/Price.js +48 -0
- package/node/wrappers/models/price/Price.js.map +1 -1
- package/node/wrappers/models/price/Price.ts +26 -0
- package/node/wrappers/models/security/identifier-validation.test.d.ts +1 -0
- package/node/wrappers/models/security/identifier-validation.test.js +104 -0
- package/node/wrappers/models/security/identifier-validation.test.js.map +1 -0
- package/node/wrappers/models/security/identifier-validation.test.ts +133 -0
- package/node/wrappers/models/security/identifier.d.ts +26 -0
- package/node/wrappers/models/security/identifier.js +54 -1
- package/node/wrappers/models/security/identifier.js.map +1 -1
- package/node/wrappers/models/security/identifier.test.js +6 -9
- package/node/wrappers/models/security/identifier.test.js.map +1 -1
- package/node/wrappers/models/security/identifier.test.ts +6 -9
- package/node/wrappers/models/security/identifier.ts +58 -0
- package/node/wrappers/models/security/security.d.ts +23 -5
- package/node/wrappers/models/security/security.js +53 -6
- package/node/wrappers/models/security/security.js.map +1 -1
- package/node/wrappers/models/security/security.ts +38 -6
- package/node/wrappers/models/transaction/transaction.d.ts +18 -0
- package/node/wrappers/models/transaction/transaction.js +98 -0
- package/node/wrappers/models/transaction/transaction.js.map +1 -1
- package/node/wrappers/models/transaction/transaction.ts +65 -0
- package/node/wrappers/services/portfolio-service/PortfolioService.js +35 -0
- package/node/wrappers/services/portfolio-service/PortfolioService.js.map +1 -1
- package/node/wrappers/services/portfolio-service/PortfolioService.ts +14 -2
- package/node/wrappers/services/price-service/PriceService.js +10 -0
- package/node/wrappers/services/price-service/PriceService.js.map +1 -1
- package/node/wrappers/services/price-service/PriceService.ts +12 -2
- package/node/wrappers/services/security-service/SecurityService.js +23 -0
- package/node/wrappers/services/security-service/SecurityService.js.map +1 -1
- package/node/wrappers/services/security-service/SecurityService.ts +27 -2
- package/node/wrappers/services/security.identifier-guard.test.d.ts +1 -0
- package/node/wrappers/services/security.identifier-guard.test.js +63 -0
- package/node/wrappers/services/security.identifier-guard.test.js.map +1 -0
- package/node/wrappers/services/security.identifier-guard.test.ts +70 -0
- package/node/wrappers/services/service-client-writethrough.test.d.ts +1 -0
- package/node/wrappers/services/service-client-writethrough.test.js +147 -0
- package/node/wrappers/services/service-client-writethrough.test.js.map +1 -0
- package/node/wrappers/services/service-client-writethrough.test.ts +141 -0
- package/node/wrappers/services/transaction-service/TransactionService.js +36 -0
- package/node/wrappers/services/transaction-service/TransactionService.js.map +1 -1
- package/node/wrappers/services/transaction-service/TransactionService.ts +13 -0
- package/node/wrappers/util/link-cache.d.ts +13 -6
- package/node/wrappers/util/link-cache.js +51 -15
- package/node/wrappers/util/link-cache.js.map +1 -1
- package/node/wrappers/util/link-cache.ts +51 -17
- package/node/wrappers/util/link-resolver.d.ts +39 -31
- package/node/wrappers/util/link-resolver.js +157 -97
- package/node/wrappers/util/link-resolver.js.map +1 -1
- package/node/wrappers/util/link-resolver.test.js +88 -2
- package/node/wrappers/util/link-resolver.test.js.map +1 -1
- package/node/wrappers/util/link-resolver.test.ts +76 -2
- package/node/wrappers/util/link-resolver.ts +143 -124
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TRANSACTION = exports.PRICE = exports.PORTFOLIO = exports.SECURITY = exports.LinkCache = exports.DEFAULT_TTL_FOR_LATEST_MS = void 0;
|
|
3
|
+
exports.TRANSACTION = exports.PRICE = exports.PORTFOLIO = exports.SECURITY = exports.LinkCache = exports.DEFAULT_MAX_ENTRIES = exports.DEFAULT_TTL_FOR_LATEST_MS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* LinkCache — process-wide cache of resolved proto bodies backing link-mode
|
|
6
6
|
* wrappers. TypeScript mirror of `common.util.LinkCache` (Java) and
|
|
@@ -18,18 +18,29 @@ exports.TRANSACTION = exports.PRICE = exports.PORTFOLIO = exports.SECURITY = exp
|
|
|
18
18
|
* Write semantics (`put`): newest-vintage wins. An older-vintage put does
|
|
19
19
|
* not evict a newer cached entry.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Eviction: bounded LRU. When `put` causes the map to exceed `maxEntries`,
|
|
22
|
+
* the least-recently-used entry is removed. `get` bumps recency.
|
|
23
|
+
*
|
|
24
|
+
* Per-entity singletons SECURITY/PORTFOLIO/PRICE/TRANSACTION are tuned
|
|
25
|
+
* for the typical access pattern of each entity (Portfolio + Security
|
|
26
|
+
* change slowly so TTL is 1 day; Price + Transaction change quickly so
|
|
27
|
+
* TTL is short).
|
|
24
28
|
*/
|
|
25
29
|
exports.DEFAULT_TTL_FOR_LATEST_MS = 600000;
|
|
30
|
+
exports.DEFAULT_MAX_ENTRIES = 10000;
|
|
26
31
|
class LinkCache {
|
|
27
|
-
constructor(ttlForLatestMs = exports.DEFAULT_TTL_FOR_LATEST_MS) {
|
|
32
|
+
constructor(ttlForLatestMs = exports.DEFAULT_TTL_FOR_LATEST_MS, maxEntries = exports.DEFAULT_MAX_ENTRIES) {
|
|
28
33
|
this.ttlForLatestMs = ttlForLatestMs;
|
|
34
|
+
this.maxEntries = maxEntries;
|
|
35
|
+
// Map iteration order = insertion order in JS; on hit we delete+re-insert
|
|
36
|
+
// to bump recency, on overflow we drop the oldest (first) key.
|
|
29
37
|
this.map = new Map();
|
|
30
38
|
if (ttlForLatestMs < 0) {
|
|
31
39
|
throw new Error(`ttlForLatestMs must be non-negative; got ${ttlForLatestMs}`);
|
|
32
40
|
}
|
|
41
|
+
if (maxEntries <= 0) {
|
|
42
|
+
throw new Error(`maxEntries must be > 0; got ${maxEntries}`);
|
|
43
|
+
}
|
|
33
44
|
}
|
|
34
45
|
/**
|
|
35
46
|
* @param uuidKey the entity uuid rendered as a stable string key
|
|
@@ -47,23 +58,43 @@ class LinkCache {
|
|
|
47
58
|
if (Date.now() - entry.cachedAtMs > this.ttlForLatestMs) {
|
|
48
59
|
return undefined;
|
|
49
60
|
}
|
|
50
|
-
return entry.value;
|
|
51
61
|
}
|
|
52
|
-
|
|
62
|
+
else {
|
|
63
|
+
if (entry.asOf == null)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (!this._sameAsOf(entry.asOf, requestedAsOf))
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
// Hit — bump recency.
|
|
69
|
+
this.map.delete(uuidKey);
|
|
70
|
+
this.map.set(uuidKey, entry);
|
|
71
|
+
return entry.value;
|
|
53
72
|
}
|
|
54
73
|
/**
|
|
55
74
|
* Newest-wins write: if a cached entry for `uuidKey` already exists with
|
|
56
|
-
* an asOf strictly after the incoming asOf, the write is ignored
|
|
75
|
+
* an asOf strictly after the incoming asOf, the write is ignored (but
|
|
76
|
+
* recency is still bumped — the caller saw a fresh reference).
|
|
57
77
|
*/
|
|
58
78
|
put(uuidKey, value, asOf) {
|
|
59
|
-
if (!uuidKey || !value
|
|
60
|
-
throw new Error(`uuidKey / value
|
|
79
|
+
if (!uuidKey || !value) {
|
|
80
|
+
throw new Error(`uuidKey / value must be set; got uuidKey=${uuidKey} value=${value ? '<non-null>' : 'null'}`);
|
|
61
81
|
}
|
|
62
82
|
const existing = this.map.get(uuidKey);
|
|
63
|
-
if (existing && this._isStrictlyAfter(existing.asOf, asOf)) {
|
|
83
|
+
if (existing && existing.asOf != null && asOf != null && this._isStrictlyAfter(existing.asOf, asOf)) {
|
|
84
|
+
// Recency bump only — older vintage doesn't displace newer cached entry.
|
|
85
|
+
this.map.delete(uuidKey);
|
|
86
|
+
this.map.set(uuidKey, existing);
|
|
64
87
|
return;
|
|
65
88
|
}
|
|
89
|
+
if (this.map.has(uuidKey))
|
|
90
|
+
this.map.delete(uuidKey);
|
|
66
91
|
this.map.set(uuidKey, { value, asOf, cachedAtMs: Date.now() });
|
|
92
|
+
while (this.map.size > this.maxEntries) {
|
|
93
|
+
const oldest = this.map.keys().next().value;
|
|
94
|
+
if (oldest === undefined)
|
|
95
|
+
break;
|
|
96
|
+
this.map.delete(oldest);
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
evict(uuidKey) {
|
|
69
100
|
this.map.delete(uuidKey);
|
|
@@ -85,8 +116,13 @@ class LinkCache {
|
|
|
85
116
|
}
|
|
86
117
|
}
|
|
87
118
|
exports.LinkCache = LinkCache;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
// Per-entity singletons. Tuned for the access pattern of each entity:
|
|
120
|
+
// Portfolio / Security: 1-day TTL on null-as_of reads (entities change
|
|
121
|
+
// infrequently); large caps because the universe is large.
|
|
122
|
+
// Transaction: 1-minute TTL (high churn).
|
|
123
|
+
// Price: 30-second TTL (very high churn).
|
|
124
|
+
exports.SECURITY = new LinkCache(86400000, 100000);
|
|
125
|
+
exports.PORTFOLIO = new LinkCache(86400000, 10000);
|
|
126
|
+
exports.PRICE = new LinkCache(30000, 200000);
|
|
127
|
+
exports.TRANSACTION = new LinkCache(60000, 100000);
|
|
92
128
|
//# sourceMappingURL=link-cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-cache.js","sourceRoot":"","sources":["link-cache.ts"],"names":[],"mappings":";;;AAMA
|
|
1
|
+
{"version":3,"file":"link-cache.js","sourceRoot":"","sources":["link-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,27 +21,41 @@ import { ZonedDateTime } from '../models/utils/datetime';
|
|
|
21
21
|
* Write semantics (`put`): newest-vintage wins. An older-vintage put does
|
|
22
22
|
* not evict a newer cached entry.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Eviction: bounded LRU. When `put` causes the map to exceed `maxEntries`,
|
|
25
|
+
* the least-recently-used entry is removed. `get` bumps recency.
|
|
26
|
+
*
|
|
27
|
+
* Per-entity singletons SECURITY/PORTFOLIO/PRICE/TRANSACTION are tuned
|
|
28
|
+
* for the typical access pattern of each entity (Portfolio + Security
|
|
29
|
+
* change slowly so TTL is 1 day; Price + Transaction change quickly so
|
|
30
|
+
* TTL is short).
|
|
27
31
|
*/
|
|
28
32
|
export const DEFAULT_TTL_FOR_LATEST_MS = 600_000;
|
|
33
|
+
export const DEFAULT_MAX_ENTRIES = 10_000;
|
|
29
34
|
|
|
30
35
|
interface CacheEntry<V> {
|
|
31
36
|
value: V;
|
|
32
|
-
/** Bitemporal asOf carried by the cached value
|
|
33
|
-
|
|
37
|
+
/** Bitemporal asOf carried by the cached value; null when put with no asOf
|
|
38
|
+
* (e.g. lazy resolve for a link with no explicit as_of). */
|
|
39
|
+
asOf: ZonedDateTime | null;
|
|
34
40
|
/** Wall-clock ms at the moment this entry was cached. */
|
|
35
41
|
cachedAtMs: number;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
export class LinkCache<V> {
|
|
45
|
+
// Map iteration order = insertion order in JS; on hit we delete+re-insert
|
|
46
|
+
// to bump recency, on overflow we drop the oldest (first) key.
|
|
39
47
|
private map = new Map<string, CacheEntry<V>>();
|
|
40
48
|
|
|
41
|
-
constructor(
|
|
49
|
+
constructor(
|
|
50
|
+
private ttlForLatestMs: number = DEFAULT_TTL_FOR_LATEST_MS,
|
|
51
|
+
private maxEntries: number = DEFAULT_MAX_ENTRIES,
|
|
52
|
+
) {
|
|
42
53
|
if (ttlForLatestMs < 0) {
|
|
43
54
|
throw new Error(`ttlForLatestMs must be non-negative; got ${ttlForLatestMs}`);
|
|
44
55
|
}
|
|
56
|
+
if (maxEntries <= 0) {
|
|
57
|
+
throw new Error(`maxEntries must be > 0; got ${maxEntries}`);
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
/**
|
|
@@ -59,26 +73,41 @@ export class LinkCache<V> {
|
|
|
59
73
|
if (Date.now() - entry.cachedAtMs > this.ttlForLatestMs) {
|
|
60
74
|
return undefined;
|
|
61
75
|
}
|
|
62
|
-
|
|
76
|
+
} else {
|
|
77
|
+
if (entry.asOf == null) return undefined;
|
|
78
|
+
if (!this._sameAsOf(entry.asOf, requestedAsOf)) return undefined;
|
|
63
79
|
}
|
|
64
|
-
|
|
80
|
+
// Hit — bump recency.
|
|
81
|
+
this.map.delete(uuidKey);
|
|
82
|
+
this.map.set(uuidKey, entry);
|
|
83
|
+
return entry.value;
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
/**
|
|
68
87
|
* Newest-wins write: if a cached entry for `uuidKey` already exists with
|
|
69
|
-
* an asOf strictly after the incoming asOf, the write is ignored
|
|
88
|
+
* an asOf strictly after the incoming asOf, the write is ignored (but
|
|
89
|
+
* recency is still bumped — the caller saw a fresh reference).
|
|
70
90
|
*/
|
|
71
|
-
put(uuidKey: string, value: V, asOf: ZonedDateTime): void {
|
|
72
|
-
if (!uuidKey || !value
|
|
91
|
+
put(uuidKey: string, value: V, asOf: ZonedDateTime | null): void {
|
|
92
|
+
if (!uuidKey || !value) {
|
|
73
93
|
throw new Error(
|
|
74
|
-
`uuidKey / value
|
|
94
|
+
`uuidKey / value must be set; got uuidKey=${uuidKey} value=${value ? '<non-null>' : 'null'}`
|
|
75
95
|
);
|
|
76
96
|
}
|
|
77
97
|
const existing = this.map.get(uuidKey);
|
|
78
|
-
if (existing && this._isStrictlyAfter(existing.asOf, asOf)) {
|
|
98
|
+
if (existing && existing.asOf != null && asOf != null && this._isStrictlyAfter(existing.asOf, asOf)) {
|
|
99
|
+
// Recency bump only — older vintage doesn't displace newer cached entry.
|
|
100
|
+
this.map.delete(uuidKey);
|
|
101
|
+
this.map.set(uuidKey, existing);
|
|
79
102
|
return;
|
|
80
103
|
}
|
|
104
|
+
if (this.map.has(uuidKey)) this.map.delete(uuidKey);
|
|
81
105
|
this.map.set(uuidKey, { value, asOf, cachedAtMs: Date.now() });
|
|
106
|
+
while (this.map.size > this.maxEntries) {
|
|
107
|
+
const oldest = this.map.keys().next().value;
|
|
108
|
+
if (oldest === undefined) break;
|
|
109
|
+
this.map.delete(oldest);
|
|
110
|
+
}
|
|
82
111
|
}
|
|
83
112
|
|
|
84
113
|
evict(uuidKey: string): void {
|
|
@@ -104,7 +133,12 @@ export class LinkCache<V> {
|
|
|
104
133
|
}
|
|
105
134
|
}
|
|
106
135
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
// Per-entity singletons. Tuned for the access pattern of each entity:
|
|
137
|
+
// Portfolio / Security: 1-day TTL on null-as_of reads (entities change
|
|
138
|
+
// infrequently); large caps because the universe is large.
|
|
139
|
+
// Transaction: 1-minute TTL (high churn).
|
|
140
|
+
// Price: 30-second TTL (very high churn).
|
|
141
|
+
export const SECURITY = new LinkCache<SecurityProto>(86_400_000, 100_000);
|
|
142
|
+
export const PORTFOLIO = new LinkCache<PortfolioProto>(86_400_000, 10_000);
|
|
143
|
+
export const PRICE = new LinkCache<PriceProto>(30_000, 200_000);
|
|
144
|
+
export const TRANSACTION = new LinkCache<TransactionProto>(60_000, 100_000);
|
|
@@ -3,41 +3,33 @@ import { PortfolioProto } from '../../fintekkers/models/portfolio/portfolio_pb';
|
|
|
3
3
|
import { LocalTimestampProto } from '../../fintekkers/models/util/local_timestamp_pb';
|
|
4
4
|
import { SecurityClient } from '../../fintekkers/services/security-service/security_service_grpc_pb';
|
|
5
5
|
import { PortfolioClient } from '../../fintekkers/services/portfolio-service/portfolio_service_grpc_pb';
|
|
6
|
+
import { TransactionClient } from '../../fintekkers/services/transaction-service/transaction_service_grpc_pb';
|
|
6
7
|
import Security from '../models/security/security';
|
|
7
8
|
import Portfolio from '../models/portfolio/portfolio';
|
|
9
|
+
import Transaction from '../models/transaction/transaction';
|
|
8
10
|
import { UUID } from '../models/utils/uuid';
|
|
9
11
|
/**
|
|
10
12
|
* LinkResolver — bulk hydration of `is_link=true` entity references into
|
|
11
13
|
* full entities. Implements the consumer side of the `is_link` pattern
|
|
12
14
|
* documented in `docs/adr/is_link_pattern.md`.
|
|
13
15
|
*
|
|
16
|
+
* W4: the resolver no longer owns its own LRU. Cache reads/writes route
|
|
17
|
+
* through the process-wide `LinkCache.SECURITY` / `LinkCache.PORTFOLIO`
|
|
18
|
+
* singletons. The resolver still does concurrent-call dedup (single
|
|
19
|
+
* in-flight RPC per (uuid, as_of)) and bulk per-bucket batching; storage
|
|
20
|
+
* and eviction live in LinkCache.
|
|
21
|
+
*
|
|
14
22
|
* Two surface methods:
|
|
15
23
|
* - getSecurity(uuid) / getPortfolio(uuid): single-UUID resolution. Cached
|
|
16
24
|
* and concurrent-deduped.
|
|
17
25
|
* - resolveSecurities(items) / resolvePortfolios(items): bulk in-place
|
|
18
26
|
* mutation across a heterogeneous list of items that each have a
|
|
19
27
|
* proto-style getter+setter for the embedded entity. Collects unique
|
|
20
|
-
* link UUIDs, fires one batched GetByIds RPC
|
|
21
|
-
* to swap the link sub-message for the resolved
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Caching:
|
|
25
|
-
* - Process-level LRU keyed on UUID string. Default 1000 entries, no TTL
|
|
26
|
-
* (entries live until evicted by LRU). Long-running services that need
|
|
27
|
-
* freshness should pass `{ ttlMs: <ms> }`. Tests can disable with
|
|
28
|
-
* `{ cacheSize: 0 }`.
|
|
29
|
-
* - Concurrent same-UUID requests are deduped via an in-flight promise
|
|
30
|
-
* map — N parallel callers for the same UUID share one RPC.
|
|
31
|
-
*
|
|
32
|
-
* RPC choice: uses `GetByIds` (unary, UUID-keyed bulk) per the ADR. The
|
|
33
|
-
* existing `SecurityService.search` (streaming) would also work but
|
|
34
|
-
* requires more wrapper plumbing for batched-by-UUID semantics.
|
|
28
|
+
* link UUIDs, fires one batched GetByIds RPC per as_of bucket, mutates
|
|
29
|
+
* each item's proto to swap the link sub-message for the resolved
|
|
30
|
+
* full entity.
|
|
35
31
|
*
|
|
36
|
-
*
|
|
37
|
-
* replaced (not the outer entity). Outer Price.proto.is_link is unchanged;
|
|
38
|
-
* only the inner SecurityProto is swapped from link-stub to full entity.
|
|
39
|
-
* Wrapper objects that read through the proto (`price.getSecurity()`)
|
|
40
|
-
* automatically see the resolved data.
|
|
32
|
+
* RPC choice: uses `GetByIds` (unary, UUID-keyed bulk) per the ADR.
|
|
41
33
|
*
|
|
42
34
|
* Time-travel (`as_of`) semantic: per is_link_pattern.md addendum, when
|
|
43
35
|
* a link sub-message has only `uuid` set the resolver fetches the latest
|
|
@@ -50,24 +42,35 @@ import { UUID } from '../models/utils/uuid';
|
|
|
50
42
|
export interface LinkResolverOptions {
|
|
51
43
|
/** Optional API key. If omitted, EnvConfig.apiCredentials is used. */
|
|
52
44
|
apiKey?: string;
|
|
53
|
-
/** LRU max entries. Default 1000. Set to 0 to disable caching. */
|
|
54
|
-
cacheSize?: number;
|
|
55
|
-
/** Per-entry TTL in ms. Default undefined (no expiry). */
|
|
56
|
-
ttlMs?: number;
|
|
57
45
|
/**
|
|
58
46
|
* Test injection: clients to use instead of constructing real ones.
|
|
59
47
|
* Production callers should not set these.
|
|
60
48
|
*/
|
|
61
49
|
securityClient?: SecurityClient;
|
|
62
50
|
portfolioClient?: PortfolioClient;
|
|
51
|
+
transactionClient?: TransactionClient;
|
|
63
52
|
}
|
|
64
53
|
declare class LinkResolver {
|
|
65
54
|
private securityClient;
|
|
66
55
|
private portfolioClient;
|
|
67
|
-
private
|
|
68
|
-
private portfolioCache;
|
|
56
|
+
private transactionClient;
|
|
69
57
|
private securityInFlight;
|
|
70
58
|
private portfolioInFlight;
|
|
59
|
+
private transactionInFlight;
|
|
60
|
+
/**
|
|
61
|
+
* Process-wide singleton — lazily constructed with default options
|
|
62
|
+
* (env-derived endpoint via `EnvConfig`). The wrapper `hydrate()`
|
|
63
|
+
* methods reach for this when no resolver is passed explicitly, so
|
|
64
|
+
* users get auto-resolve on link-mode wrappers without threading a
|
|
65
|
+
* resolver through every call site.
|
|
66
|
+
*/
|
|
67
|
+
static getDefault(): LinkResolver;
|
|
68
|
+
/**
|
|
69
|
+
* Replace the process-wide default. Call once at process start for
|
|
70
|
+
* tests with mocked clients or to point at a non-default endpoint.
|
|
71
|
+
* Pass `undefined` to clear (next `getDefault()` call rebuilds).
|
|
72
|
+
*/
|
|
73
|
+
static setDefault(resolver: LinkResolver | undefined): void;
|
|
71
74
|
constructor(opts?: LinkResolverOptions);
|
|
72
75
|
/**
|
|
73
76
|
* Resolve a single SecurityProto by UUID. If `asOf` is supplied, fetch
|
|
@@ -81,6 +84,11 @@ declare class LinkResolver {
|
|
|
81
84
|
* Cached + concurrent-deduped on (uuid, asOf).
|
|
82
85
|
*/
|
|
83
86
|
getPortfolio(uuid: UUID, asOf?: LocalTimestampProto): Promise<Portfolio>;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a single TransactionProto by UUID, optionally as of `asOf`.
|
|
89
|
+
* Cached + concurrent-deduped on (uuid, asOf).
|
|
90
|
+
*/
|
|
91
|
+
getTransaction(uuid: UUID, asOf?: LocalTimestampProto): Promise<Transaction>;
|
|
84
92
|
/**
|
|
85
93
|
* Walk `items`, find the ones whose embedded security is `is_link=true`,
|
|
86
94
|
* batch-fetch the unique (uuid, as_of) pairs (grouped by as_of so each
|
|
@@ -88,10 +96,6 @@ declare class LinkResolver {
|
|
|
88
96
|
* place so subsequent `item.getSecurity()` calls return the full entity.
|
|
89
97
|
* Returns the same array for chaining.
|
|
90
98
|
*
|
|
91
|
-
* Honors per-link `as_of`: if the embedded sub-message has `as_of` set,
|
|
92
|
-
* the resolver fetches the version of the entity at that timestamp,
|
|
93
|
-
* not the latest.
|
|
94
|
-
*
|
|
95
99
|
* `T` is structural: anything with a `proto` field that exposes
|
|
96
100
|
* `getSecurity()` / `setSecurity()` works (Price, Transaction, etc).
|
|
97
101
|
*/
|
|
@@ -101,11 +105,15 @@ declare class LinkResolver {
|
|
|
101
105
|
* Honors per-link `as_of` the same way.
|
|
102
106
|
*/
|
|
103
107
|
resolvePortfolios<T extends ResolvablePortfolio>(items: T[]): Promise<T[]>;
|
|
104
|
-
/** Test/debug helper.
|
|
108
|
+
/** Test/debug helper. Clears in-flight maps; the process-wide LinkCache
|
|
109
|
+
* is left alone (tests that need to drop a specific cached entry call
|
|
110
|
+
* `LinkCache.SECURITY.evict(uuid)` directly). */
|
|
105
111
|
clearCache(): void;
|
|
106
112
|
private fetchSecurityProto;
|
|
107
113
|
private fetchPortfolioProto;
|
|
114
|
+
private fetchTransactionProto;
|
|
108
115
|
private batchFetchSecurities;
|
|
116
|
+
private batchFetchTransactions;
|
|
109
117
|
private batchFetchPortfolios;
|
|
110
118
|
}
|
|
111
119
|
/**
|