@fintekkers/ledger-models 0.4.10 → 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 +12 -2
- package/node/wrappers/models/portfolio/portfolio.js +42 -3
- package/node/wrappers/models/portfolio/portfolio.js.map +1 -1
- package/node/wrappers/models/portfolio/portfolio.ts +27 -3
- 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 +12 -1
- package/node/wrappers/models/transaction/transaction.js +39 -2
- package/node/wrappers/models/transaction/transaction.js.map +1 -1
- package/node/wrappers/models/transaction/transaction.ts +27 -2
- package/node/wrappers/services/security-service/SecurityService.js +8 -0
- package/node/wrappers/services/security-service/SecurityService.js.map +1 -1
- package/node/wrappers/services/security-service/SecurityService.ts +10 -0
- 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/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 +132 -124
- package/node/wrappers/util/link-resolver.js.map +1 -1
- package/node/wrappers/util/link-resolver.test.js +13 -2
- package/node/wrappers/util/link-resolver.test.js.map +1 -1
- package/node/wrappers/util/link-resolver.test.ts +14 -2
- package/node/wrappers/util/link-resolver.ts +141 -151
- 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,16 +2,26 @@ 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;
|
|
9
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>;
|
|
10
19
|
/**
|
|
11
20
|
* Lazy hydration. On a link-mode proto, swap in the resolved proto from
|
|
12
21
|
* LinkCache. On cache miss, throws — caller must pre-warm via
|
|
13
|
-
* LinkResolver. Cache-only by design (same
|
|
14
|
-
* keeps the sync getter API).
|
|
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.
|
|
15
25
|
*/
|
|
16
26
|
private ensureHydrated;
|
|
17
27
|
getID(): UUID;
|
|
@@ -22,11 +22,24 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
22
22
|
__setModuleDefault(result, mod);
|
|
23
23
|
return result;
|
|
24
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
|
+
};
|
|
25
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
38
|
const field_pb_1 = require("../../../fintekkers/models/position/field_pb");
|
|
27
39
|
const datetime_1 = require("../utils/datetime");
|
|
28
40
|
const uuid_1 = require("../utils/uuid");
|
|
29
41
|
const LinkCacheModule = __importStar(require("../../util/link-cache"));
|
|
42
|
+
const link_resolver_1 = __importDefault(require("../../util/link-resolver"));
|
|
30
43
|
class Portfolio {
|
|
31
44
|
constructor(proto) {
|
|
32
45
|
this.proto = proto;
|
|
@@ -37,11 +50,36 @@ class Portfolio {
|
|
|
37
50
|
isLink() {
|
|
38
51
|
return this.proto.getIsLink();
|
|
39
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
|
+
}
|
|
40
77
|
/**
|
|
41
78
|
* Lazy hydration. On a link-mode proto, swap in the resolved proto from
|
|
42
79
|
* LinkCache. On cache miss, throws — caller must pre-warm via
|
|
43
|
-
* LinkResolver. Cache-only by design (same
|
|
44
|
-
* keeps the sync getter API).
|
|
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.
|
|
45
83
|
*/
|
|
46
84
|
ensureHydrated() {
|
|
47
85
|
if (!this.proto.getIsLink())
|
|
@@ -59,7 +97,8 @@ class Portfolio {
|
|
|
59
97
|
return;
|
|
60
98
|
}
|
|
61
99
|
throw new Error(`Cannot read fields on link-mode Portfolio uuid=${uuidKey} `
|
|
62
|
-
+ `— LinkCache miss.
|
|
100
|
+
+ `— LinkCache miss. Call \`await portfolio.hydrate()\` first, `
|
|
101
|
+
+ `or pre-warm via LinkResolver. `
|
|
63
102
|
+ `See docs/adr/lazy-link-hydration.md.`);
|
|
64
103
|
}
|
|
65
104
|
getID() {
|
|
@@ -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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"}
|
|
@@ -3,6 +3,7 @@ import { FieldProto } from "../../../fintekkers/models/position/field_pb";
|
|
|
3
3
|
import { ZonedDateTime } from "../utils/datetime";
|
|
4
4
|
import { UUID } from "../utils/uuid";
|
|
5
5
|
import * as LinkCacheModule from "../../util/link-cache";
|
|
6
|
+
import LinkResolver from "../../util/link-resolver";
|
|
6
7
|
|
|
7
8
|
class Portfolio {
|
|
8
9
|
proto: PortfolioProto;
|
|
@@ -19,11 +20,33 @@ class Portfolio {
|
|
|
19
20
|
return this.proto.getIsLink();
|
|
20
21
|
}
|
|
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
|
+
|
|
22
44
|
/**
|
|
23
45
|
* Lazy hydration. On a link-mode proto, swap in the resolved proto from
|
|
24
46
|
* LinkCache. On cache miss, throws — caller must pre-warm via
|
|
25
|
-
* LinkResolver. Cache-only by design (same
|
|
26
|
-
* keeps the sync getter API).
|
|
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.
|
|
27
50
|
*/
|
|
28
51
|
private ensureHydrated(): void {
|
|
29
52
|
if (!this.proto.getIsLink()) return;
|
|
@@ -41,7 +64,8 @@ class Portfolio {
|
|
|
41
64
|
}
|
|
42
65
|
throw new Error(
|
|
43
66
|
`Cannot read fields on link-mode Portfolio uuid=${uuidKey} `
|
|
44
|
-
+ `— LinkCache miss.
|
|
67
|
+
+ `— LinkCache miss. Call \`await portfolio.hydrate()\` first, `
|
|
68
|
+
+ `or pre-warm via LinkResolver. `
|
|
45
69
|
+ `See docs/adr/lazy-link-hydration.md.`
|
|
46
70
|
);
|
|
47
71
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Client-side identifier guard tests (FinTekkers/second-brain#347).
|
|
3
|
+
// Pins behaviour of the consumer-side reject so callers fail fast on the
|
|
4
|
+
// client before the gRPC round-trip, mirroring the server's
|
|
5
|
+
// SecurityAPIGRPCImpl.validateCreateRequest UNKNOWN_IDENTIFIER_TYPE check.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const identifier_pb_1 = require("../../../fintekkers/models/security/identifier/identifier_pb");
|
|
8
|
+
const identifier_type_pb_1 = require("../../../fintekkers/models/security/identifier/identifier_type_pb");
|
|
9
|
+
const security_pb_1 = require("../../../fintekkers/models/security/security_pb");
|
|
10
|
+
const identifier_1 = require("./identifier");
|
|
11
|
+
function makeIdentifier(type, value) {
|
|
12
|
+
const p = new identifier_pb_1.IdentifierProto();
|
|
13
|
+
p.setIdentifierType(type);
|
|
14
|
+
p.setIdentifierValue(value);
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
// ---------- validateIdentifierProto ----------
|
|
18
|
+
describe('validateIdentifierProto', () => {
|
|
19
|
+
test('rejects UNKNOWN_IDENTIFIER_TYPE with a helpful message', () => {
|
|
20
|
+
const bad = makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.UNKNOWN_IDENTIFIER_TYPE, 'some-uuid-hex');
|
|
21
|
+
let err;
|
|
22
|
+
try {
|
|
23
|
+
(0, identifier_1.validateIdentifierProto)(bad);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
err = e;
|
|
27
|
+
}
|
|
28
|
+
expect(err).toBeInstanceOf(identifier_1.IdentifierValidationError);
|
|
29
|
+
expect(err.message).toMatch(/UNKNOWN_IDENTIFIER_TYPE/);
|
|
30
|
+
// Surfaces the valid alternatives so the caller can fix the typo
|
|
31
|
+
expect(err.message).toMatch(/EXCH_TICKER/);
|
|
32
|
+
expect(err.message).toMatch(/#347/);
|
|
33
|
+
});
|
|
34
|
+
test('rejects a default-constructed identifier (type=0, empty value)', () => {
|
|
35
|
+
const bad = new identifier_pb_1.IdentifierProto();
|
|
36
|
+
expect(() => (0, identifier_1.validateIdentifierProto)(bad)).toThrow(identifier_1.IdentifierValidationError);
|
|
37
|
+
});
|
|
38
|
+
test('rejects empty identifier_value with the type name in the message', () => {
|
|
39
|
+
const bad = makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.EXCH_TICKER, '');
|
|
40
|
+
let err;
|
|
41
|
+
try {
|
|
42
|
+
(0, identifier_1.validateIdentifierProto)(bad);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
err = e;
|
|
46
|
+
}
|
|
47
|
+
expect(err).toBeInstanceOf(identifier_1.IdentifierValidationError);
|
|
48
|
+
expect(err.message).toMatch(/empty/);
|
|
49
|
+
expect(err.message).toMatch(/EXCH_TICKER/);
|
|
50
|
+
});
|
|
51
|
+
test('rejects whitespace-only identifier_value', () => {
|
|
52
|
+
const bad = makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.CUSIP, ' ');
|
|
53
|
+
expect(() => (0, identifier_1.validateIdentifierProto)(bad)).toThrow(identifier_1.IdentifierValidationError);
|
|
54
|
+
});
|
|
55
|
+
test.each([
|
|
56
|
+
['EXCH_TICKER', identifier_type_pb_1.IdentifierTypeProto.EXCH_TICKER, 'AAPL'],
|
|
57
|
+
['ISIN', identifier_type_pb_1.IdentifierTypeProto.ISIN, 'US0378331005'],
|
|
58
|
+
['CUSIP', identifier_type_pb_1.IdentifierTypeProto.CUSIP, '037833100'],
|
|
59
|
+
['FIGI', identifier_type_pb_1.IdentifierTypeProto.FIGI, 'BBG000B9XRY4'],
|
|
60
|
+
['OSI', identifier_type_pb_1.IdentifierTypeProto.OSI, 'AAPL 250620C00150000'],
|
|
61
|
+
['SERIES_ID', identifier_type_pb_1.IdentifierTypeProto.SERIES_ID, 'GS10'],
|
|
62
|
+
['INDEX_NAME', identifier_type_pb_1.IdentifierTypeProto.INDEX_NAME, 'SPX'],
|
|
63
|
+
['CASH', identifier_type_pb_1.IdentifierTypeProto.CASH, 'USD'],
|
|
64
|
+
])('accepts every real identifier type (%s)', (_name, type, value) => {
|
|
65
|
+
const good = makeIdentifier(type, value);
|
|
66
|
+
expect(() => (0, identifier_1.validateIdentifierProto)(good)).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
test('IdentifierValidationError is an Error', () => {
|
|
69
|
+
// Catch-by-Error still works for callers that don't import the
|
|
70
|
+
// specific subclass.
|
|
71
|
+
const err = new identifier_1.IdentifierValidationError('x');
|
|
72
|
+
expect(err).toBeInstanceOf(Error);
|
|
73
|
+
expect(err.name).toBe('IdentifierValidationError');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// ---------- validateIdentifiersForCreate ----------
|
|
77
|
+
describe('validateIdentifiersForCreate', () => {
|
|
78
|
+
test('passes when every identifier on the SecurityProto is well-typed', () => {
|
|
79
|
+
const security = new security_pb_1.SecurityProto();
|
|
80
|
+
security.addIdentifiers(makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.EXCH_TICKER, 'AAPL'));
|
|
81
|
+
security.addIdentifiers(makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.ISIN, 'US0378331005'));
|
|
82
|
+
expect(() => (0, identifier_1.validateIdentifiersForCreate)(security)).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
test('rejects when any identifier in the list is UNKNOWN_IDENTIFIER_TYPE', () => {
|
|
85
|
+
const security = new security_pb_1.SecurityProto();
|
|
86
|
+
security.addIdentifiers(makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.EXCH_TICKER, 'AAPL'));
|
|
87
|
+
security.addIdentifiers(makeIdentifier(identifier_type_pb_1.IdentifierTypeProto.UNKNOWN_IDENTIFIER_TYPE, 'stale-uuid'));
|
|
88
|
+
expect(() => (0, identifier_1.validateIdentifiersForCreate)(security)).toThrow(identifier_1.IdentifierValidationError);
|
|
89
|
+
});
|
|
90
|
+
test('skips link-mode securities (is_link=true)', () => {
|
|
91
|
+
// Link-mode = reference handle (uuid + as_of only); no identifiers
|
|
92
|
+
// attached. The guard must skip rather than over-trigger.
|
|
93
|
+
const link = new security_pb_1.SecurityProto();
|
|
94
|
+
link.setIsLink(true);
|
|
95
|
+
expect(() => (0, identifier_1.validateIdentifiersForCreate)(link)).not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
test('passes on empty identifiers list (server enforces "at least one")', () => {
|
|
98
|
+
// Our consumer-side check polices the *type* of every attached
|
|
99
|
+
// identifier; the "must have ≥1 identifier" rule lives server-side.
|
|
100
|
+
const security = new security_pb_1.SecurityProto();
|
|
101
|
+
expect(() => (0, identifier_1.validateIdentifiersForCreate)(security)).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=identifier-validation.test.js.map
|