@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.
Files changed (75) hide show
  1. package/node/wrappers/models/hydrate.test.d.ts +1 -0
  2. package/node/wrappers/models/hydrate.test.js +204 -0
  3. package/node/wrappers/models/hydrate.test.js.map +1 -0
  4. package/node/wrappers/models/hydrate.test.ts +214 -0
  5. package/node/wrappers/models/lazy-hydrate-race.test.d.ts +1 -0
  6. package/node/wrappers/models/lazy-hydrate-race.test.js +153 -0
  7. package/node/wrappers/models/lazy-hydrate-race.test.js.map +1 -0
  8. package/node/wrappers/models/lazy-hydrate-race.test.ts +144 -0
  9. package/node/wrappers/models/lazy-hydrate.bench.test.d.ts +1 -0
  10. package/node/wrappers/models/lazy-hydrate.bench.test.js +121 -0
  11. package/node/wrappers/models/lazy-hydrate.bench.test.js.map +1 -0
  12. package/node/wrappers/models/lazy-hydrate.bench.test.ts +103 -0
  13. package/node/wrappers/models/portfolio/portfolio.d.ts +18 -0
  14. package/node/wrappers/models/portfolio/portfolio.js +93 -1
  15. package/node/wrappers/models/portfolio/portfolio.js.map +1 -1
  16. package/node/wrappers/models/portfolio/portfolio.ts +58 -1
  17. package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.d.ts +1 -0
  18. package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.js +158 -0
  19. package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.js.map +1 -0
  20. package/node/wrappers/models/portfolio-price-transaction.lazy-hydrate.test.ts +153 -0
  21. package/node/wrappers/models/price/Price.d.ts +5 -0
  22. package/node/wrappers/models/price/Price.js +48 -0
  23. package/node/wrappers/models/price/Price.js.map +1 -1
  24. package/node/wrappers/models/price/Price.ts +26 -0
  25. package/node/wrappers/models/security/identifier-validation.test.d.ts +1 -0
  26. package/node/wrappers/models/security/identifier-validation.test.js +104 -0
  27. package/node/wrappers/models/security/identifier-validation.test.js.map +1 -0
  28. package/node/wrappers/models/security/identifier-validation.test.ts +133 -0
  29. package/node/wrappers/models/security/identifier.d.ts +26 -0
  30. package/node/wrappers/models/security/identifier.js +54 -1
  31. package/node/wrappers/models/security/identifier.js.map +1 -1
  32. package/node/wrappers/models/security/identifier.test.js +6 -9
  33. package/node/wrappers/models/security/identifier.test.js.map +1 -1
  34. package/node/wrappers/models/security/identifier.test.ts +6 -9
  35. package/node/wrappers/models/security/identifier.ts +58 -0
  36. package/node/wrappers/models/security/security.d.ts +23 -5
  37. package/node/wrappers/models/security/security.js +53 -6
  38. package/node/wrappers/models/security/security.js.map +1 -1
  39. package/node/wrappers/models/security/security.ts +38 -6
  40. package/node/wrappers/models/transaction/transaction.d.ts +18 -0
  41. package/node/wrappers/models/transaction/transaction.js +98 -0
  42. package/node/wrappers/models/transaction/transaction.js.map +1 -1
  43. package/node/wrappers/models/transaction/transaction.ts +65 -0
  44. package/node/wrappers/services/portfolio-service/PortfolioService.js +35 -0
  45. package/node/wrappers/services/portfolio-service/PortfolioService.js.map +1 -1
  46. package/node/wrappers/services/portfolio-service/PortfolioService.ts +14 -2
  47. package/node/wrappers/services/price-service/PriceService.js +10 -0
  48. package/node/wrappers/services/price-service/PriceService.js.map +1 -1
  49. package/node/wrappers/services/price-service/PriceService.ts +12 -2
  50. package/node/wrappers/services/security-service/SecurityService.js +23 -0
  51. package/node/wrappers/services/security-service/SecurityService.js.map +1 -1
  52. package/node/wrappers/services/security-service/SecurityService.ts +27 -2
  53. package/node/wrappers/services/security.identifier-guard.test.d.ts +1 -0
  54. package/node/wrappers/services/security.identifier-guard.test.js +63 -0
  55. package/node/wrappers/services/security.identifier-guard.test.js.map +1 -0
  56. package/node/wrappers/services/security.identifier-guard.test.ts +70 -0
  57. package/node/wrappers/services/service-client-writethrough.test.d.ts +1 -0
  58. package/node/wrappers/services/service-client-writethrough.test.js +147 -0
  59. package/node/wrappers/services/service-client-writethrough.test.js.map +1 -0
  60. package/node/wrappers/services/service-client-writethrough.test.ts +141 -0
  61. package/node/wrappers/services/transaction-service/TransactionService.js +36 -0
  62. package/node/wrappers/services/transaction-service/TransactionService.js.map +1 -1
  63. package/node/wrappers/services/transaction-service/TransactionService.ts +13 -0
  64. package/node/wrappers/util/link-cache.d.ts +13 -6
  65. package/node/wrappers/util/link-cache.js +51 -15
  66. package/node/wrappers/util/link-cache.js.map +1 -1
  67. package/node/wrappers/util/link-cache.ts +51 -17
  68. package/node/wrappers/util/link-resolver.d.ts +39 -31
  69. package/node/wrappers/util/link-resolver.js +157 -97
  70. package/node/wrappers/util/link-resolver.js.map +1 -1
  71. package/node/wrappers/util/link-resolver.test.js +88 -2
  72. package/node/wrappers/util/link-resolver.test.js.map +1 -1
  73. package/node/wrappers/util/link-resolver.test.ts +76 -2
  74. package/node/wrappers/util/link-resolver.ts +143 -124
  75. 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
- * Later Portfolio can likely be 1 day, security 1 day, transaction 1 minute,
22
- * price 30 seconds — once per-entity TTLs are wired up, the shared singletons
23
- * below should be constructed with those values.
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
- return this._sameAsOf(entry.asOf, requestedAsOf) ? entry.value : undefined;
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 || !asOf) {
60
- throw new Error(`uuidKey / value / asOf must all be set; got uuidKey=${uuidKey} value=${value ? '<non-null>' : 'null'} asOf=${asOf}`);
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
- exports.SECURITY = new LinkCache();
89
- exports.PORTFOLIO = new LinkCache();
90
- exports.PRICE = new LinkCache();
91
- exports.TRANSACTION = new LinkCache();
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":"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"}
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
- * Later Portfolio can likely be 1 day, security 1 day, transaction 1 minute,
25
- * price 30 seconds — once per-entity TTLs are wired up, the shared singletons
26
- * below should be constructed with those values.
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
- asOf: ZonedDateTime;
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(private ttlForLatestMs: number = DEFAULT_TTL_FOR_LATEST_MS) {
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
- return entry.value;
76
+ } else {
77
+ if (entry.asOf == null) return undefined;
78
+ if (!this._sameAsOf(entry.asOf, requestedAsOf)) return undefined;
63
79
  }
64
- return this._sameAsOf(entry.asOf, requestedAsOf) ? entry.value : undefined;
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 || !asOf) {
91
+ put(uuidKey: string, value: V, asOf: ZonedDateTime | null): void {
92
+ if (!uuidKey || !value) {
73
93
  throw new Error(
74
- `uuidKey / value / asOf must all be set; got uuidKey=${uuidKey} value=${value ? '<non-null>' : 'null'} asOf=${asOf}`
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
- export const SECURITY = new LinkCache<SecurityProto>();
108
- export const PORTFOLIO = new LinkCache<PortfolioProto>();
109
- export const PRICE = new LinkCache<PriceProto>();
110
- export const TRANSACTION = new LinkCache<TransactionProto>();
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, mutates each item's proto
21
- * to swap the link sub-message for the resolved full entity (with
22
- * is_link=false on the embedded copy).
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
- * Mutation semantic: when bulk-resolving, the embedded sub-message is
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 securityCache;
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. Not part of the stable API. */
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
  /**