feat(serializer): add class aot serialization support
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
enumOf,
|
||||
flags,
|
||||
Writer,
|
||||
Serializable,
|
||||
} from '@perf/serializer';
|
||||
|
||||
// ─── Tee output to console + <pre id="out"> if we're in a browser ──────────
|
||||
@@ -206,3 +207,179 @@ const dispatched1 = proto.decode(framedTicker);
|
||||
const dispatched2 = proto.decode(framedBook);
|
||||
log(' dispatched ticker symbol:', (dispatched1 as Ticker).symbol);
|
||||
log(' dispatched book bids[0]:', (dispatched2 as typeof book).bids[0]);
|
||||
|
||||
// ─── Example 7: class with [Symbol.serializable] contract ──────────────────
|
||||
//
|
||||
// Attach the schema to a class via the well-known `Symbol.serializable`. The
|
||||
// AOT plugin sees this and:
|
||||
// • generates a class-aware decoder that returns `Object.create(Position.prototype)`
|
||||
// instances, so methods/getters work on decoded values;
|
||||
// • auto-registers the codec into the runtime registry on module load — no
|
||||
// `registerClass(Position)` call needed;
|
||||
// • makes `Position[Serializable]` the codec itself, usable directly.
|
||||
|
||||
class Position {
|
||||
side!: 'long' | 'short';
|
||||
qty!: number;
|
||||
entryPrice!: number;
|
||||
|
||||
static [Serializable] = type('Position', {
|
||||
side: enumOf(['long', 'short'] as const),
|
||||
qty: f64,
|
||||
entryPrice: f64,
|
||||
});
|
||||
|
||||
get notional(): number {
|
||||
return this.qty * this.entryPrice;
|
||||
}
|
||||
|
||||
pnl(currentPrice: number): number {
|
||||
return this.side === 'long'
|
||||
? (currentPrice - this.entryPrice) * this.qty
|
||||
: (this.entryPrice - currentPrice) * this.qty;
|
||||
}
|
||||
}
|
||||
|
||||
const pos = Object.assign(new Position(), {
|
||||
side: 'long' as const,
|
||||
qty: 2,
|
||||
entryPrice: 67500,
|
||||
});
|
||||
|
||||
// `Position[Serializable]` IS the codec — no `registerClass(...)` needed.
|
||||
const PositionCodec = Position[Serializable]!;
|
||||
const posBytes = PositionCodec.encode(pos);
|
||||
const back = PositionCodec.decode(posBytes) as Position;
|
||||
|
||||
log('\nExample 7: class with Symbol.serializable');
|
||||
log(` encoded: ${posBytes.length}b`);
|
||||
log(` pos.notional: ${pos.notional}`);
|
||||
log(` back.notional: ${back.notional} (method works on decoded value after AOT)`);
|
||||
log(` back instanceof Position: ${back instanceof Position}`);
|
||||
log(` back.pnl(68000): ${back.pnl(68000)}`);
|
||||
|
||||
// ─── Example 8: deeply-nested portfolio (5 levels, ~770 objects) ────────────
|
||||
//
|
||||
// Realistic deep-tree data: Portfolio → Accounts → Holdings → Trades →
|
||||
// Executions. Demonstrates that the codec handles arbitrary nesting; under AOT
|
||||
// the inner-loop encoder/decoder is fully inlined — no per-level function
|
||||
// dispatch.
|
||||
|
||||
const Execution = type('Execution', {
|
||||
id: u53,
|
||||
ts: u53,
|
||||
price: f64,
|
||||
qty: f64,
|
||||
venue: enumOf(['NYSE', 'NASDAQ', 'BATS', 'IEX'] as const),
|
||||
});
|
||||
|
||||
const Trade = type('Trade', {
|
||||
id: u53,
|
||||
symbol: str,
|
||||
side: enumOf(['buy', 'sell'] as const),
|
||||
executions: list(Execution),
|
||||
});
|
||||
|
||||
const Holding = type('Holding', {
|
||||
symbol: str,
|
||||
qty: f64,
|
||||
avgEntry: f64,
|
||||
trades: list(Trade),
|
||||
});
|
||||
|
||||
const Account = type('Account', {
|
||||
id: u53,
|
||||
name: str,
|
||||
currency: enumOf(['USD', 'EUR', 'GBP'] as const),
|
||||
balance: f64,
|
||||
holdings: list(Holding),
|
||||
});
|
||||
|
||||
const Portfolio = type('Portfolio', {
|
||||
ownerId: u53,
|
||||
ts: u53,
|
||||
accounts: list(Account),
|
||||
});
|
||||
|
||||
type PortfolioT = typeof Portfolio.$infer;
|
||||
|
||||
function buildPortfolio(): PortfolioT {
|
||||
const venues = ['NYSE', 'NASDAQ', 'BATS', 'IEX'] as const;
|
||||
const symbols = ['AAPL', 'MSFT', 'GOOG', 'AMZN', 'TSLA'] as const;
|
||||
const currencies = ['USD', 'EUR', 'GBP'] as const;
|
||||
|
||||
return {
|
||||
ownerId: 100001,
|
||||
ts: 1716100000000,
|
||||
accounts: Array.from({ length: 3 }, (_, a) => ({
|
||||
id: 10 + a,
|
||||
name: `Account-${a + 1}`,
|
||||
currency: currencies[a]!,
|
||||
balance: 100_000 * (a + 1),
|
||||
holdings: Array.from({ length: 5 }, (_, p) => ({
|
||||
symbol: symbols[p % symbols.length]!,
|
||||
qty: 100 * (p + 1),
|
||||
avgEntry: 100 + p * 25,
|
||||
trades: Array.from({ length: 10 }, (_, t) => ({
|
||||
id: a * 1000 + p * 100 + t,
|
||||
symbol: symbols[(a + p + t) % symbols.length]!,
|
||||
side: t % 2 === 0 ? ('buy' as const) : ('sell' as const),
|
||||
executions: Array.from({ length: 4 }, (_, e) => ({
|
||||
id: a * 10000 + p * 1000 + t * 10 + e,
|
||||
ts: 1716100000000 + (a * 100 + p * 50 + t * 5 + e) * 1000,
|
||||
price: 100 + ((a + p + t + e) % 100) * 2,
|
||||
qty: 5 + ((p + t + e) % 50),
|
||||
venue: venues[e % 4]!,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const portfolio = buildPortfolio();
|
||||
|
||||
// Counts at each level.
|
||||
const nAccounts = portfolio.accounts.length;
|
||||
const nHoldings = portfolio.accounts.reduce((s, a) => s + a.holdings.length, 0);
|
||||
const nTrades = portfolio.accounts.reduce(
|
||||
(s, a) => s + a.holdings.reduce((ss, h) => ss + h.trades.length, 0),
|
||||
0,
|
||||
);
|
||||
const nExecs = portfolio.accounts.reduce(
|
||||
(s, a) =>
|
||||
s +
|
||||
a.holdings.reduce(
|
||||
(ss, h) => ss + h.trades.reduce((sss, t) => sss + t.executions.length, 0),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
const tEnc0 = performance.now();
|
||||
const portfolioBytes = Portfolio.encode(portfolio);
|
||||
const tEnc1 = performance.now();
|
||||
const portfolioJSON = JSON.stringify(portfolio);
|
||||
const tEnc2 = performance.now();
|
||||
const decodedPortfolio = Portfolio.decode(portfolioBytes);
|
||||
const tEnc3 = performance.now();
|
||||
JSON.parse(portfolioJSON);
|
||||
const tEnc4 = performance.now();
|
||||
|
||||
const total = 1 + nAccounts + nHoldings + nTrades + nExecs;
|
||||
|
||||
log('\nExample 8: deeply-nested portfolio (5 levels)');
|
||||
log(
|
||||
` ${nAccounts} accounts × ${nHoldings / nAccounts} holdings × ${nTrades / nHoldings} trades × ${nExecs / nTrades} executions = ${total} objects total`,
|
||||
);
|
||||
log(
|
||||
` encoded: ${portfolioBytes.length}b in ${(tEnc1 - tEnc0).toFixed(2)}ms | JSON: ${portfolioJSON.length}b in ${(tEnc2 - tEnc1).toFixed(2)}ms (${((portfolioBytes.length / portfolioJSON.length) * 100).toFixed(0)}% of JSON)`,
|
||||
);
|
||||
log(
|
||||
` decoded: ${(tEnc3 - tEnc2).toFixed(2)}ms | JSON.parse: ${(tEnc4 - tEnc3).toFixed(2)}ms`,
|
||||
);
|
||||
const sampleExec = decodedPortfolio.accounts[0]!.holdings[0]!.trades[0]!.executions[0]!;
|
||||
const origExec = portfolio.accounts[0]!.holdings[0]!.trades[0]!.executions[0]!;
|
||||
log(
|
||||
` round-trip preserves leaf data: ${sampleExec.id === origExec.id && sampleExec.venue === origExec.venue}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user