@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Concurrent-hydrate race test for Portfolio + Transaction wrappers.
|
|
2
|
+
// TypeScript has no shared-state threads, but Promise.all + a mock LinkResolver
|
|
3
|
+
// covers the same shape: N parallel `await wrapper.hydrate()` calls on
|
|
4
|
+
// wrappers that share a UUID. Contract:
|
|
5
|
+
//
|
|
6
|
+
// 1. Resolver's in-flight dedup collapses N hydrate() calls into one RPC.
|
|
7
|
+
// 2. Every awaiter sees the resolved proto.
|
|
8
|
+
// 3. The shared LinkCache singleton ends on the resolved entry.
|
|
9
|
+
|
|
10
|
+
import LinkResolver from "../util/link-resolver";
|
|
11
|
+
import * as LinkCacheModule from "../util/link-cache";
|
|
12
|
+
import Portfolio from "./portfolio/portfolio";
|
|
13
|
+
import Transaction from "./transaction/transaction";
|
|
14
|
+
import { UUID } from "./utils/uuid";
|
|
15
|
+
|
|
16
|
+
import { PortfolioProto } from "../../fintekkers/models/portfolio/portfolio_pb";
|
|
17
|
+
import { TransactionProto } from "../../fintekkers/models/transaction/transaction_pb";
|
|
18
|
+
import { LocalTimestampProto } from "../../fintekkers/models/util/local_timestamp_pb";
|
|
19
|
+
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
|
|
20
|
+
import { QueryPortfolioResponseProto } from "../../fintekkers/requests/portfolio/query_portfolio_response_pb";
|
|
21
|
+
import { QueryTransactionResponseProto } from "../../fintekkers/requests/transaction/query_transaction_response_pb";
|
|
22
|
+
|
|
23
|
+
function makeAsOf(seconds = 1_700_000_000): LocalTimestampProto {
|
|
24
|
+
const ts = new Timestamp();
|
|
25
|
+
ts.setSeconds(seconds);
|
|
26
|
+
ts.setNanos(0);
|
|
27
|
+
const lt = new LocalTimestampProto();
|
|
28
|
+
lt.setTimestamp(ts);
|
|
29
|
+
lt.setTimeZone("UTC");
|
|
30
|
+
return lt;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CallLog {
|
|
34
|
+
count: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mockPortfolioClient(resolved: PortfolioProto, log: CallLog) {
|
|
38
|
+
return {
|
|
39
|
+
getByIds: (
|
|
40
|
+
_req: any,
|
|
41
|
+
cb: (err: Error | null, res: QueryPortfolioResponseProto) => void,
|
|
42
|
+
) => {
|
|
43
|
+
log.count++;
|
|
44
|
+
const r = new QueryPortfolioResponseProto();
|
|
45
|
+
r.setPortfolioResponseList([resolved]);
|
|
46
|
+
setImmediate(() => cb(null, r));
|
|
47
|
+
},
|
|
48
|
+
} as any;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mockTransactionClient(resolved: TransactionProto, log: CallLog) {
|
|
52
|
+
return {
|
|
53
|
+
getByIds: (
|
|
54
|
+
_req: any,
|
|
55
|
+
cb: (err: Error | null, res: QueryTransactionResponseProto) => void,
|
|
56
|
+
) => {
|
|
57
|
+
log.count++;
|
|
58
|
+
const r = new QueryTransactionResponseProto();
|
|
59
|
+
r.setTransactionResponseList([resolved]);
|
|
60
|
+
setImmediate(() => cb(null, r));
|
|
61
|
+
},
|
|
62
|
+
} as any;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Throw-on-call client for the unused entity types.
|
|
66
|
+
function throwingClient(name: string) {
|
|
67
|
+
return {
|
|
68
|
+
getByIds: () => {
|
|
69
|
+
throw new Error(`${name} client should not be invoked in this test`);
|
|
70
|
+
},
|
|
71
|
+
} as any;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("lazy hydrate — concurrent hydrate() collapses to one RPC", () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
LinkCacheModule.SECURITY.clear();
|
|
77
|
+
LinkCacheModule.PORTFOLIO.clear();
|
|
78
|
+
LinkCacheModule.TRANSACTION.clear();
|
|
79
|
+
LinkResolver.setDefault(undefined);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("Portfolio: 16 concurrent hydrate() calls → 1 RPC, all observe RESOLVED", async () => {
|
|
83
|
+
const uuid = UUID.random();
|
|
84
|
+
const asOf = makeAsOf();
|
|
85
|
+
|
|
86
|
+
const resolved = new PortfolioProto();
|
|
87
|
+
resolved.setUuid(uuid.toUUIDProto());
|
|
88
|
+
resolved.setAsOf(asOf);
|
|
89
|
+
resolved.setIsLink(false);
|
|
90
|
+
resolved.setPortfolioName("RESOLVED");
|
|
91
|
+
|
|
92
|
+
const log: CallLog = { count: 0 };
|
|
93
|
+
const resolver = new LinkResolver({
|
|
94
|
+
portfolioClient: mockPortfolioClient(resolved, log),
|
|
95
|
+
securityClient: throwingClient("security"),
|
|
96
|
+
transactionClient: throwingClient("transaction"),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Build 16 wrappers from the same link-mode proto.
|
|
100
|
+
const linkProto = new PortfolioProto();
|
|
101
|
+
linkProto.setUuid(uuid.toUUIDProto());
|
|
102
|
+
linkProto.setAsOf(asOf);
|
|
103
|
+
linkProto.setIsLink(true);
|
|
104
|
+
const wrappers = Array.from({ length: 16 }, () => new Portfolio(linkProto));
|
|
105
|
+
|
|
106
|
+
const hydrated = await Promise.all(wrappers.map((w) => w.hydrate(resolver)));
|
|
107
|
+
|
|
108
|
+
expect(log.count).toBe(1);
|
|
109
|
+
for (const w of hydrated) {
|
|
110
|
+
expect(w.getPortfolioName()).toBe("RESOLVED");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("Transaction: 16 concurrent hydrate() calls → 1 RPC, all observe resolved trade name", async () => {
|
|
115
|
+
const uuid = UUID.random();
|
|
116
|
+
const asOf = makeAsOf();
|
|
117
|
+
|
|
118
|
+
const resolved = new TransactionProto();
|
|
119
|
+
resolved.setUuid(uuid.toUUIDProto());
|
|
120
|
+
resolved.setAsOf(asOf);
|
|
121
|
+
resolved.setIsLink(false);
|
|
122
|
+
resolved.setTradeName("RESOLVED-TRADE");
|
|
123
|
+
|
|
124
|
+
const log: CallLog = { count: 0 };
|
|
125
|
+
const resolver = new LinkResolver({
|
|
126
|
+
transactionClient: mockTransactionClient(resolved, log),
|
|
127
|
+
securityClient: throwingClient("security"),
|
|
128
|
+
portfolioClient: throwingClient("portfolio"),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const linkProto = new TransactionProto();
|
|
132
|
+
linkProto.setUuid(uuid.toUUIDProto());
|
|
133
|
+
linkProto.setAsOf(asOf);
|
|
134
|
+
linkProto.setIsLink(true);
|
|
135
|
+
const wrappers = Array.from({ length: 16 }, () => new Transaction(linkProto));
|
|
136
|
+
|
|
137
|
+
const hydrated = await Promise.all(wrappers.map((w) => w.hydrate(resolver)));
|
|
138
|
+
|
|
139
|
+
expect(log.count).toBe(1);
|
|
140
|
+
for (const w of hydrated) {
|
|
141
|
+
expect(w.proto.getTradeName()).toBe("RESOLVED-TRADE");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// End-to-end perf bench for Transaction wrapper lazy hydration.
|
|
3
|
+
// Run: `npx jest node/wrappers/models/lazy-hydrate.bench.ts` (jest runs
|
|
4
|
+
// .ts files via ts-jest; stdout shows the bench output).
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
22
|
+
if (mod && mod.__esModule) return mod;
|
|
23
|
+
var result = {};
|
|
24
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
25
|
+
__setModuleDefault(result, mod);
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
29
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
const LinkCacheModule = __importStar(require("../util/link-cache"));
|
|
33
|
+
const transaction_1 = __importDefault(require("./transaction/transaction"));
|
|
34
|
+
const uuid_1 = require("./utils/uuid");
|
|
35
|
+
const portfolio_pb_1 = require("../../fintekkers/models/portfolio/portfolio_pb");
|
|
36
|
+
const transaction_pb_1 = require("../../fintekkers/models/transaction/transaction_pb");
|
|
37
|
+
const local_timestamp_pb_1 = require("../../fintekkers/models/util/local_timestamp_pb");
|
|
38
|
+
const datetime_1 = require("./utils/datetime");
|
|
39
|
+
const timestamp_pb_1 = require("google-protobuf/google/protobuf/timestamp_pb");
|
|
40
|
+
const SIZES = [10, 100, 1000, 10000];
|
|
41
|
+
function makeAsOf() {
|
|
42
|
+
const ts = new timestamp_pb_1.Timestamp();
|
|
43
|
+
ts.setSeconds(1700000000);
|
|
44
|
+
ts.setNanos(0);
|
|
45
|
+
const lt = new local_timestamp_pb_1.LocalTimestampProto();
|
|
46
|
+
lt.setTimestamp(ts);
|
|
47
|
+
lt.setTimeZone("UTC");
|
|
48
|
+
return lt;
|
|
49
|
+
}
|
|
50
|
+
function runBench(n) {
|
|
51
|
+
const asOf = makeAsOf();
|
|
52
|
+
const asOfZdt = new datetime_1.ZonedDateTime(asOf);
|
|
53
|
+
const links = [];
|
|
54
|
+
const txnUuids = [];
|
|
55
|
+
const portUuids = [];
|
|
56
|
+
for (let i = 0; i < n; i++) {
|
|
57
|
+
const txnUuid = uuid_1.UUID.random();
|
|
58
|
+
const portUuid = uuid_1.UUID.random();
|
|
59
|
+
txnUuids.push(txnUuid.toString());
|
|
60
|
+
portUuids.push(portUuid.toString());
|
|
61
|
+
const resolvedPortfolio = new portfolio_pb_1.PortfolioProto();
|
|
62
|
+
resolvedPortfolio.setUuid(portUuid.toUUIDProto());
|
|
63
|
+
resolvedPortfolio.setAsOf(asOf);
|
|
64
|
+
resolvedPortfolio.setIsLink(false);
|
|
65
|
+
resolvedPortfolio.setPortfolioName(`P-${portUuid.toString().slice(0, 8)}`);
|
|
66
|
+
const resolved = new transaction_pb_1.TransactionProto();
|
|
67
|
+
resolved.setUuid(txnUuid.toUUIDProto());
|
|
68
|
+
resolved.setAsOf(asOf);
|
|
69
|
+
resolved.setIsLink(false);
|
|
70
|
+
resolved.setTradeName(`T-${txnUuid.toString().slice(0, 8)}`);
|
|
71
|
+
resolved.setPortfolio(resolvedPortfolio);
|
|
72
|
+
LinkCacheModule.TRANSACTION.put(txnUuid.toString(), resolved, asOfZdt);
|
|
73
|
+
LinkCacheModule.PORTFOLIO.put(portUuid.toString(), resolvedPortfolio, asOfZdt);
|
|
74
|
+
const link = new transaction_pb_1.TransactionProto();
|
|
75
|
+
link.setUuid(txnUuid.toUUIDProto());
|
|
76
|
+
link.setAsOf(asOf);
|
|
77
|
+
link.setIsLink(true);
|
|
78
|
+
links.push(link);
|
|
79
|
+
}
|
|
80
|
+
if (global.gc)
|
|
81
|
+
global.gc();
|
|
82
|
+
const heapBefore = process.memoryUsage().heapUsed;
|
|
83
|
+
const t0 = process.hrtime.bigint();
|
|
84
|
+
let sink = 0;
|
|
85
|
+
for (const link of links) {
|
|
86
|
+
const t = new transaction_1.default(link);
|
|
87
|
+
// Trigger lazy hydrate via getter that calls ensureHydrated.
|
|
88
|
+
const name = t.proto.getIsLink() ? "" : t.proto.getTradeName();
|
|
89
|
+
// Workaround: TS wrappers' ensureHydrated is private and accessors
|
|
90
|
+
// on Transaction are reads through proto. Hit a typed accessor:
|
|
91
|
+
const portfolio = t.getPortfolio();
|
|
92
|
+
if (portfolio)
|
|
93
|
+
sink++;
|
|
94
|
+
if (name === "" && portfolio)
|
|
95
|
+
sink++;
|
|
96
|
+
}
|
|
97
|
+
const t1 = process.hrtime.bigint();
|
|
98
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
99
|
+
const elapsedNs = Number(t1 - t0);
|
|
100
|
+
const elapsedMs = elapsedNs / 1e6;
|
|
101
|
+
const perOpUs = elapsedNs / n / 1000;
|
|
102
|
+
const heapDeltaKb = (heapAfter - heapBefore) / 1024;
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.log(`N=${n.toString().padStart(6)} elapsed=${elapsedMs.toFixed(2).padStart(9)} ms ` +
|
|
105
|
+
`per_op=${perOpUs.toFixed(2).padStart(8)} us ` +
|
|
106
|
+
`heap_delta=${heapDeltaKb.toFixed(2).padStart(8)} KiB reads=${sink}`);
|
|
107
|
+
for (const u of txnUuids)
|
|
108
|
+
LinkCacheModule.TRANSACTION.evict(u);
|
|
109
|
+
for (const u of portUuids)
|
|
110
|
+
LinkCacheModule.PORTFOLIO.evict(u);
|
|
111
|
+
}
|
|
112
|
+
// Wrap in describe so jest treats it as a suite; the test body is the bench.
|
|
113
|
+
describe("lazy-hydrate bench", () => {
|
|
114
|
+
test("Transaction across 10/100/1000/10000 sizes", () => {
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log("# ts bench: lazy-hydrate Transaction via pre-warmed LinkCache");
|
|
117
|
+
for (const n of SIZES)
|
|
118
|
+
runBench(n);
|
|
119
|
+
}, 60000);
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=lazy-hydrate.bench.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazy-hydrate.bench.test.js","sourceRoot":"","sources":["lazy-hydrate.bench.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// End-to-end perf bench for Transaction wrapper lazy hydration.
|
|
2
|
+
// Run: `npx jest node/wrappers/models/lazy-hydrate.bench.ts` (jest runs
|
|
3
|
+
// .ts files via ts-jest; stdout shows the bench output).
|
|
4
|
+
|
|
5
|
+
import * as LinkCacheModule from "../util/link-cache";
|
|
6
|
+
import Transaction from "./transaction/transaction";
|
|
7
|
+
import { UUID } from "./utils/uuid";
|
|
8
|
+
|
|
9
|
+
import { PortfolioProto } from "../../fintekkers/models/portfolio/portfolio_pb";
|
|
10
|
+
import { TransactionProto } from "../../fintekkers/models/transaction/transaction_pb";
|
|
11
|
+
import { LocalTimestampProto } from "../../fintekkers/models/util/local_timestamp_pb";
|
|
12
|
+
import { ZonedDateTime } from "./utils/datetime";
|
|
13
|
+
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
|
|
14
|
+
|
|
15
|
+
const SIZES = [10, 100, 1_000, 10_000];
|
|
16
|
+
|
|
17
|
+
function makeAsOf(): LocalTimestampProto {
|
|
18
|
+
const ts = new Timestamp();
|
|
19
|
+
ts.setSeconds(1_700_000_000);
|
|
20
|
+
ts.setNanos(0);
|
|
21
|
+
const lt = new LocalTimestampProto();
|
|
22
|
+
lt.setTimestamp(ts);
|
|
23
|
+
lt.setTimeZone("UTC");
|
|
24
|
+
return lt;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runBench(n: number) {
|
|
28
|
+
const asOf = makeAsOf();
|
|
29
|
+
const asOfZdt = new ZonedDateTime(asOf);
|
|
30
|
+
const links: TransactionProto[] = [];
|
|
31
|
+
const txnUuids: string[] = [];
|
|
32
|
+
const portUuids: string[] = [];
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < n; i++) {
|
|
35
|
+
const txnUuid = UUID.random();
|
|
36
|
+
const portUuid = UUID.random();
|
|
37
|
+
txnUuids.push(txnUuid.toString());
|
|
38
|
+
portUuids.push(portUuid.toString());
|
|
39
|
+
|
|
40
|
+
const resolvedPortfolio = new PortfolioProto();
|
|
41
|
+
resolvedPortfolio.setUuid(portUuid.toUUIDProto());
|
|
42
|
+
resolvedPortfolio.setAsOf(asOf);
|
|
43
|
+
resolvedPortfolio.setIsLink(false);
|
|
44
|
+
resolvedPortfolio.setPortfolioName(`P-${portUuid.toString().slice(0, 8)}`);
|
|
45
|
+
|
|
46
|
+
const resolved = new TransactionProto();
|
|
47
|
+
resolved.setUuid(txnUuid.toUUIDProto());
|
|
48
|
+
resolved.setAsOf(asOf);
|
|
49
|
+
resolved.setIsLink(false);
|
|
50
|
+
resolved.setTradeName(`T-${txnUuid.toString().slice(0, 8)}`);
|
|
51
|
+
resolved.setPortfolio(resolvedPortfolio);
|
|
52
|
+
|
|
53
|
+
LinkCacheModule.TRANSACTION.put(txnUuid.toString(), resolved, asOfZdt);
|
|
54
|
+
LinkCacheModule.PORTFOLIO.put(portUuid.toString(), resolvedPortfolio, asOfZdt);
|
|
55
|
+
|
|
56
|
+
const link = new TransactionProto();
|
|
57
|
+
link.setUuid(txnUuid.toUUIDProto());
|
|
58
|
+
link.setAsOf(asOf);
|
|
59
|
+
link.setIsLink(true);
|
|
60
|
+
links.push(link);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (global.gc) global.gc();
|
|
64
|
+
const heapBefore = process.memoryUsage().heapUsed;
|
|
65
|
+
const t0 = process.hrtime.bigint();
|
|
66
|
+
let sink = 0;
|
|
67
|
+
for (const link of links) {
|
|
68
|
+
const t = new Transaction(link);
|
|
69
|
+
// Trigger lazy hydrate via getter that calls ensureHydrated.
|
|
70
|
+
const name = t.proto.getIsLink() ? "" : t.proto.getTradeName();
|
|
71
|
+
// Workaround: TS wrappers' ensureHydrated is private and accessors
|
|
72
|
+
// on Transaction are reads through proto. Hit a typed accessor:
|
|
73
|
+
const portfolio = t.getPortfolio();
|
|
74
|
+
if (portfolio) sink++;
|
|
75
|
+
if (name === "" && portfolio) sink++;
|
|
76
|
+
}
|
|
77
|
+
const t1 = process.hrtime.bigint();
|
|
78
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
79
|
+
|
|
80
|
+
const elapsedNs = Number(t1 - t0);
|
|
81
|
+
const elapsedMs = elapsedNs / 1e6;
|
|
82
|
+
const perOpUs = elapsedNs / n / 1000;
|
|
83
|
+
const heapDeltaKb = (heapAfter - heapBefore) / 1024;
|
|
84
|
+
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(
|
|
87
|
+
`N=${n.toString().padStart(6)} elapsed=${elapsedMs.toFixed(2).padStart(9)} ms ` +
|
|
88
|
+
`per_op=${perOpUs.toFixed(2).padStart(8)} us ` +
|
|
89
|
+
`heap_delta=${heapDeltaKb.toFixed(2).padStart(8)} KiB reads=${sink}`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
for (const u of txnUuids) LinkCacheModule.TRANSACTION.evict(u);
|
|
93
|
+
for (const u of portUuids) LinkCacheModule.PORTFOLIO.evict(u);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Wrap in describe so jest treats it as a suite; the test body is the bench.
|
|
97
|
+
describe("lazy-hydrate bench", () => {
|
|
98
|
+
test("Transaction across 10/100/1000/10000 sizes", () => {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log("# ts bench: lazy-hydrate Transaction via pre-warmed LinkCache");
|
|
101
|
+
for (const n of SIZES) runBench(n);
|
|
102
|
+
}, 60_000);
|
|
103
|
+
});
|
|
@@ -2,10 +2,28 @@ import { PortfolioProto } from "../../../fintekkers/models/portfolio/portfolio_p
|
|
|
2
2
|
import { FieldProto } from "../../../fintekkers/models/position/field_pb";
|
|
3
3
|
import { ZonedDateTime } from "../utils/datetime";
|
|
4
4
|
import { UUID } from "../utils/uuid";
|
|
5
|
+
import LinkResolver from "../../util/link-resolver";
|
|
5
6
|
declare class Portfolio {
|
|
6
7
|
proto: PortfolioProto;
|
|
7
8
|
constructor(proto: PortfolioProto);
|
|
8
9
|
toString(): string;
|
|
10
|
+
isLink(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Async hydration via `LinkResolver`. Mirrors `Security.hydrate()`.
|
|
13
|
+
* Returns `this` so it can be chained:
|
|
14
|
+
*
|
|
15
|
+
* const p = await new Portfolio(linkProto).hydrate();
|
|
16
|
+
* console.log(p.getPortfolioName());
|
|
17
|
+
*/
|
|
18
|
+
hydrate(resolver?: LinkResolver): Promise<this>;
|
|
19
|
+
/**
|
|
20
|
+
* Lazy hydration. On a link-mode proto, swap in the resolved proto from
|
|
21
|
+
* LinkCache. On cache miss, throws — caller must pre-warm via
|
|
22
|
+
* `await portfolio.hydrate()` or LinkResolver. Cache-only by design (same
|
|
23
|
+
* rationale as Security wrapper: keeps the sync getter API).
|
|
24
|
+
* See docs/adr/lazy-link-hydration.md.
|
|
25
|
+
*/
|
|
26
|
+
private ensureHydrated;
|
|
9
27
|
getID(): UUID;
|
|
10
28
|
getAsOf(): ZonedDateTime;
|
|
11
29
|
getPortfolioName(): string;
|
|
@@ -1,14 +1,105 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
2
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
38
|
const field_pb_1 = require("../../../fintekkers/models/position/field_pb");
|
|
4
39
|
const datetime_1 = require("../utils/datetime");
|
|
5
40
|
const uuid_1 = require("../utils/uuid");
|
|
41
|
+
const LinkCacheModule = __importStar(require("../../util/link-cache"));
|
|
42
|
+
const link_resolver_1 = __importDefault(require("../../util/link-resolver"));
|
|
6
43
|
class Portfolio {
|
|
7
44
|
constructor(proto) {
|
|
8
45
|
this.proto = proto;
|
|
9
46
|
}
|
|
10
47
|
toString() {
|
|
11
|
-
return this.getPortfolioName();
|
|
48
|
+
return this.isLink() ? `<link ${this.getID().toString()}>` : this.getPortfolioName();
|
|
49
|
+
}
|
|
50
|
+
isLink() {
|
|
51
|
+
return this.proto.getIsLink();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Async hydration via `LinkResolver`. Mirrors `Security.hydrate()`.
|
|
55
|
+
* Returns `this` so it can be chained:
|
|
56
|
+
*
|
|
57
|
+
* const p = await new Portfolio(linkProto).hydrate();
|
|
58
|
+
* console.log(p.getPortfolioName());
|
|
59
|
+
*/
|
|
60
|
+
hydrate(resolver) {
|
|
61
|
+
var _a;
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
if (!this.proto.getIsLink())
|
|
64
|
+
return this;
|
|
65
|
+
const uuidProto = this.proto.getUuid();
|
|
66
|
+
if (!uuidProto) {
|
|
67
|
+
throw new Error("Cannot hydrate a link-mode Portfolio with no UUID set.");
|
|
68
|
+
}
|
|
69
|
+
const uuid = uuid_1.UUID.fromU8Array(uuidProto.getRawUuid_asU8());
|
|
70
|
+
const asOfProto = (_a = this.proto.getAsOf()) !== null && _a !== void 0 ? _a : undefined;
|
|
71
|
+
const r = resolver !== null && resolver !== void 0 ? resolver : link_resolver_1.default.getDefault();
|
|
72
|
+
const resolved = yield r.getPortfolio(uuid, asOfProto);
|
|
73
|
+
this.proto = resolved.proto;
|
|
74
|
+
return this;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Lazy hydration. On a link-mode proto, swap in the resolved proto from
|
|
79
|
+
* LinkCache. On cache miss, throws — caller must pre-warm via
|
|
80
|
+
* `await portfolio.hydrate()` or LinkResolver. Cache-only by design (same
|
|
81
|
+
* rationale as Security wrapper: keeps the sync getter API).
|
|
82
|
+
* See docs/adr/lazy-link-hydration.md.
|
|
83
|
+
*/
|
|
84
|
+
ensureHydrated() {
|
|
85
|
+
if (!this.proto.getIsLink())
|
|
86
|
+
return;
|
|
87
|
+
const uuidProto = this.proto.getUuid();
|
|
88
|
+
if (!uuidProto) {
|
|
89
|
+
throw new Error("Cannot read fields on link-mode Portfolio with no UUID set.");
|
|
90
|
+
}
|
|
91
|
+
const uuidKey = uuid_1.UUID.fromU8Array(uuidProto.getRawUuid_asU8()).toString();
|
|
92
|
+
const asOfProto = this.proto.getAsOf();
|
|
93
|
+
const asOf = asOfProto ? new datetime_1.ZonedDateTime(asOfProto) : null;
|
|
94
|
+
const cached = LinkCacheModule.PORTFOLIO.get(uuidKey, asOf);
|
|
95
|
+
if (cached) {
|
|
96
|
+
this.proto = cached;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Cannot read fields on link-mode Portfolio uuid=${uuidKey} `
|
|
100
|
+
+ `— LinkCache miss. Call \`await portfolio.hydrate()\` first, `
|
|
101
|
+
+ `or pre-warm via LinkResolver. `
|
|
102
|
+
+ `See docs/adr/lazy-link-hydration.md.`);
|
|
12
103
|
}
|
|
13
104
|
getID() {
|
|
14
105
|
const uuid = this.proto.getUuid();
|
|
@@ -23,6 +114,7 @@ class Portfolio {
|
|
|
23
114
|
return new datetime_1.ZonedDateTime(asOf);
|
|
24
115
|
}
|
|
25
116
|
getPortfolioName() {
|
|
117
|
+
this.ensureHydrated();
|
|
26
118
|
return this.proto.getPortfolioName();
|
|
27
119
|
}
|
|
28
120
|
getFields() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portfolio.js","sourceRoot":"","sources":["portfolio.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"portfolio.js","sourceRoot":"","sources":["portfolio.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,8 @@ import { PortfolioProto } from "../../../fintekkers/models/portfolio/portfolio_p
|
|
|
2
2
|
import { FieldProto } from "../../../fintekkers/models/position/field_pb";
|
|
3
3
|
import { ZonedDateTime } from "../utils/datetime";
|
|
4
4
|
import { UUID } from "../utils/uuid";
|
|
5
|
+
import * as LinkCacheModule from "../../util/link-cache";
|
|
6
|
+
import LinkResolver from "../../util/link-resolver";
|
|
5
7
|
|
|
6
8
|
class Portfolio {
|
|
7
9
|
proto: PortfolioProto;
|
|
@@ -11,7 +13,61 @@ class Portfolio {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
toString(): string {
|
|
14
|
-
return this.getPortfolioName();
|
|
16
|
+
return this.isLink() ? `<link ${this.getID().toString()}>` : this.getPortfolioName();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isLink(): boolean {
|
|
20
|
+
return this.proto.getIsLink();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Async hydration via `LinkResolver`. Mirrors `Security.hydrate()`.
|
|
25
|
+
* Returns `this` so it can be chained:
|
|
26
|
+
*
|
|
27
|
+
* const p = await new Portfolio(linkProto).hydrate();
|
|
28
|
+
* console.log(p.getPortfolioName());
|
|
29
|
+
*/
|
|
30
|
+
async hydrate(resolver?: LinkResolver): Promise<this> {
|
|
31
|
+
if (!this.proto.getIsLink()) return this;
|
|
32
|
+
const uuidProto = this.proto.getUuid();
|
|
33
|
+
if (!uuidProto) {
|
|
34
|
+
throw new Error("Cannot hydrate a link-mode Portfolio with no UUID set.");
|
|
35
|
+
}
|
|
36
|
+
const uuid = UUID.fromU8Array(uuidProto.getRawUuid_asU8());
|
|
37
|
+
const asOfProto = this.proto.getAsOf() ?? undefined;
|
|
38
|
+
const r = resolver ?? LinkResolver.getDefault();
|
|
39
|
+
const resolved = await r.getPortfolio(uuid, asOfProto);
|
|
40
|
+
this.proto = resolved.proto;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lazy hydration. On a link-mode proto, swap in the resolved proto from
|
|
46
|
+
* LinkCache. On cache miss, throws — caller must pre-warm via
|
|
47
|
+
* `await portfolio.hydrate()` or LinkResolver. Cache-only by design (same
|
|
48
|
+
* rationale as Security wrapper: keeps the sync getter API).
|
|
49
|
+
* See docs/adr/lazy-link-hydration.md.
|
|
50
|
+
*/
|
|
51
|
+
private ensureHydrated(): void {
|
|
52
|
+
if (!this.proto.getIsLink()) return;
|
|
53
|
+
const uuidProto = this.proto.getUuid();
|
|
54
|
+
if (!uuidProto) {
|
|
55
|
+
throw new Error("Cannot read fields on link-mode Portfolio with no UUID set.");
|
|
56
|
+
}
|
|
57
|
+
const uuidKey = UUID.fromU8Array(uuidProto.getRawUuid_asU8()).toString();
|
|
58
|
+
const asOfProto = this.proto.getAsOf();
|
|
59
|
+
const asOf = asOfProto ? new ZonedDateTime(asOfProto) : null;
|
|
60
|
+
const cached = LinkCacheModule.PORTFOLIO.get(uuidKey, asOf);
|
|
61
|
+
if (cached) {
|
|
62
|
+
this.proto = cached;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Cannot read fields on link-mode Portfolio uuid=${uuidKey} `
|
|
67
|
+
+ `— LinkCache miss. Call \`await portfolio.hydrate()\` first, `
|
|
68
|
+
+ `or pre-warm via LinkResolver. `
|
|
69
|
+
+ `See docs/adr/lazy-link-hydration.md.`
|
|
70
|
+
);
|
|
15
71
|
}
|
|
16
72
|
|
|
17
73
|
getID(): UUID {
|
|
@@ -27,6 +83,7 @@ class Portfolio {
|
|
|
27
83
|
}
|
|
28
84
|
|
|
29
85
|
getPortfolioName(): string {
|
|
86
|
+
this.ensureHydrated();
|
|
30
87
|
return this.proto.getPortfolioName();
|
|
31
88
|
}
|
|
32
89
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|