@rayadesu/dsh-llm-billing 0.1.0 → 0.2.0

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.
@@ -4,6 +4,15 @@
4
4
  * the credential/environment seams, prices each session's billed usage with the
5
5
  * peak/off-peak table, and exposes the `billing` Remote (`getBalance`, the
6
6
  * per-session `getSessionSpend`, and the all-sessions `getTodaySpend`).
7
+ *
8
+ * Today's spend never scans every session log per request: a 60-second
9
+ * Beijing-day cache with in-flight coalescing serves the message-triggered
10
+ * reads, the manual refresh may bypass the time window (`force`), and the
11
+ * computation behind a miss reads only sessions whose persisted revision
12
+ * changed since the last resolution (see today-spend.ts). When the
13
+ * session-projection registry is composed, the plugin additionally registers
14
+ * the `billingTodaySpend` projection unit, which folds each session's spend
15
+ * eagerly and lets cold reads ride the projection-cache ladder.
7
16
  * @module @rayadesu/dsh-llm-billing
8
17
  */
9
18
  import z from '@deepseek-ai/schemastery';
@@ -11,9 +20,13 @@ import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm';
11
20
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
12
21
  import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
13
22
  import { DeepSeekBalanceGateway, fetchDeepSeekBalance } from "./balance.js";
14
- import { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, resolveBilling, } from "./billing.js";
23
+ import { computeSessionSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, resolveBilling, } from "./billing.js";
24
+ import { billingTodaySpendDefinition } from "./projection.js";
25
+ import { TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
15
26
  export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from "./balance.js";
16
- export { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, isPeak, resolveBilling, } from "./billing.js";
27
+ export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, isPeak, mergeTodaySpend, priceEvent, resolveBilling, } from "./billing.js";
28
+ export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit } from "./projection.js";
29
+ export { TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
17
30
  export const name = 'llm-billing';
18
31
  const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY';
19
32
  const BASE_URL_ENV = 'DEEPSEEK_BASE_URL';
@@ -50,9 +63,14 @@ export const Config = z.object({
50
63
  models: z.array(billingModel).default(DEFAULT_MODELS),
51
64
  billing: billingConfig,
52
65
  });
66
+ /** How often a Beijing-day "today spend" value may be recomputed (60s). */
67
+ export const TODAY_SPEND_CACHE_MS = 60_000;
68
+ /** Hard cap on today's events collected by the events scan path. */
69
+ export const TODAY_SPEND_MAX_EVENTS = 200_000;
53
70
  /**
54
71
  * Read one session's event log: the live SessionStore first, then the
55
- * persistence backend for a flushed session.
72
+ * persistence backend for a flushed session (inspected directly by id — no
73
+ * header listing).
56
74
  * @param ctx - plugin context carrying the SessionStore and optional persistence.
57
75
  * @param sessionId - the session to read.
58
76
  * @returns the session's complete event log.
@@ -65,52 +83,14 @@ async function sessionEvents(ctx, sessionId) {
65
83
  return live.events;
66
84
  const persistence = ctx.get('sessionPersistence');
67
85
  if (persistence !== undefined) {
68
- for (const header of await persistence.list()) {
69
- if (header.id !== sessionId)
70
- continue;
71
- const inspection = await persistence.inspect(sessionId);
72
- return inspection.events;
86
+ try {
87
+ return (await persistence.inspect(sessionId)).events;
73
88
  }
74
- }
75
- throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND');
76
- }
77
- /**
78
- * Read every session's event log, concatenated: each live SessionStore
79
- * session first (its log may hold events not yet flushed), then each persisted
80
- * session that is not live, so no event is counted twice. Events are appended
81
- * one at a time: spreading a very large log into `push(...)` exceeds the
82
- * engine's argument limit and throws a stack RangeError.
83
- * @param ctx - plugin context carrying the SessionStore and optional persistence.
84
- * @returns every session's complete event log, concatenated.
85
- */
86
- async function allSessionEvents(ctx) {
87
- const events = [];
88
- const sessions = ctx.get('sessions');
89
- const liveIds = new Set();
90
- if (sessions !== undefined) {
91
- for (const session of sessions.list()) {
92
- liveIds.add(session.id);
93
- for (const event of session.events)
94
- events.push(event);
89
+ catch (error) {
90
+ throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND', { cause: error });
95
91
  }
96
92
  }
97
- const persistence = ctx.get('sessionPersistence');
98
- if (persistence !== undefined) {
99
- for (const header of await persistence.list()) {
100
- if (liveIds.has(header.id))
101
- continue;
102
- try {
103
- const inspection = await persistence.inspect(header.id);
104
- for (const event of inspection.events)
105
- events.push(event);
106
- }
107
- catch (error) {
108
- // One unreadable session must not blank the whole-day aggregate.
109
- ctx.logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
110
- }
111
- }
112
- }
113
- return events;
93
+ throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND');
114
94
  }
115
95
  /**
116
96
  * Register the `billing` Remote under the `billing` namespace.
@@ -141,16 +121,46 @@ export function apply(ctx, config) {
141
121
  const apiKey = await resolveApiKey();
142
122
  return fetchDeepSeekBalance(baseURL(), apiKey);
143
123
  };
124
+ const billing = resolveBilling(config.billing);
125
+ const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
144
126
  const fetchSessionSpend = async (sessionId) => {
145
- const billing = resolveBilling(config.billing);
146
- const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
147
127
  return computeSessionSpend(await sessionEvents(ctx, sessionId), billing, catalog);
148
128
  };
149
- const fetchTodaySpend = async () => {
150
- const billing = resolveBilling(config.billing);
151
- const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
152
- return computeTodaySpend(await allSessionEvents(ctx), billing, catalog);
129
+ // Plan C: register the per-session spend projection unit on the projection
130
+ // registry. Registration is lazy — it happens on the first projection-path
131
+ // scan, not through `ctx.inject` (whose plugin-mount wait would also engage
132
+ // the test-invariant host in suites that never provide the registry). The
133
+ // registry builds cells lazily over the in-memory log, so events committed
134
+ // before registration are folded on first touch; without the registry the
135
+ // events path serves today's spend.
136
+ const unit = billingTodaySpendDefinition(billing, catalog);
137
+ let unitRegistered = false;
138
+ const ensureUnit = () => {
139
+ if (unitRegistered)
140
+ return;
141
+ const registry = ctx.get('sessionProjections');
142
+ if (registry === undefined)
143
+ return;
144
+ registry.register(unit);
145
+ unitRegistered = true;
153
146
  };
147
+ // Plans A1–A3: 60s Beijing-day cache with in-flight coalescing and a force
148
+ // bypass, over a revision-gated scanner (projection path when the registry
149
+ // is composed, events path otherwise).
150
+ const scanner = new TodaySpendScanner({
151
+ sessions: () => ctx.get('sessions'),
152
+ persistence: () => ctx.get('sessionPersistence'),
153
+ projections: () => ctx.get('sessionProjections'),
154
+ projectionCache: () => ctx.get('sessionProjectionCache'),
155
+ ensureUnit,
156
+ unit,
157
+ maxEvents: TODAY_SPEND_MAX_EVENTS,
158
+ logger: ctx.logger,
159
+ billing,
160
+ catalog,
161
+ });
162
+ const todayCache = new TodaySpendCache(dayKey => scanner.scan(dayKey), TODAY_SPEND_CACHE_MS);
163
+ const fetchTodaySpend = async (force = false) => todayCache.get(force);
154
164
  new DeepSeekBalanceGateway(ctx, { fetchBalance, fetchSessionSpend, fetchTodaySpend });
155
165
  }
156
166
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `billingTodaySpend` session-projection unit: per-session, per-Beijing-day
3
+ * billed spend, folded eagerly by the DSH projection drive over committed
4
+ * session events and checkpointed by the projection cache. The unit keeps only
5
+ * the spend of the session's LATEST priced day (events are append-only and
6
+ * chronological, so a day strictly older than the state's day never returns);
7
+ * the aggregate "today" read sums the units whose `dayKey` matches the current
8
+ * Beijing day — zero full-log scans once the fold is warm.
9
+ *
10
+ * The unit's fold shares {@link priceEvent} with the events-scan paths
11
+ * (`computeTodaySpend`), so both price with the same table. The unit is
12
+ * client-visible (`wire` = identity) because the persisted-cache read ladder
13
+ * (`sessionProjectionCache.coldSnapshot` / registry `restore`) serves only
14
+ * wired units; the wire value is the state itself.
15
+ * @module @rayadesu/dsh-llm-billing/projection
16
+ */
17
+ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
18
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
19
+ import type { ResolvedBilling } from './billing.ts';
20
+ import type { DeepSeekTodaySpend } from './types.ts';
21
+ /** The projection key this unit owns. */
22
+ export declare const BILLING_UNIT_KEY = "billingTodaySpend";
23
+ /**
24
+ * Per-session unit state: the Beijing day of the session's latest priced
25
+ * event and that day's billed spend. `dayKey` is `''` while the session has no
26
+ * priced usage, and the state only ever describes ONE day (the latest) —
27
+ * plain JSON, as the persisted-cache contract requires.
28
+ */
29
+ export interface BillingUnitState {
30
+ /** Beijing-time calendar-day key of the state's spend; `''` for no priced usage. */
31
+ dayKey: string;
32
+ /** The spend of the session's latest priced Beijing day. */
33
+ spend: DeepSeekTodaySpend;
34
+ }
35
+ declare module '@deepseek-ai/dsh-session-projection/types' {
36
+ interface SessionProjectionStateMap {
37
+ billingTodaySpend: BillingUnitState;
38
+ }
39
+ interface SessionProjectionMap {
40
+ billingTodaySpend: BillingUnitState;
41
+ }
42
+ }
43
+ /**
44
+ * The unit definition with a required `wire` — the shape {@link register}
45
+ * accepts for a client-visible unit (the plain `ProjectionDefinition` type
46
+ * leaves `wire` optional).
47
+ */
48
+ export type BillingUnitDefinition = Omit<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'wire'> & {
49
+ wire: NonNullable<ProjectionDefinition<'billingTodaySpend', BillingUnitState>['wire']>;
50
+ };
51
+ /**
52
+ * Build the `billingTodaySpend` unit for one resolved pricing table. The
53
+ * pricing closure is fixed at registration; a pricing-table change therefore
54
+ * prices only events folded after the change (historical spend keeps its
55
+ * historical rates), unlike the events-scan paths which re-price the whole
56
+ * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
57
+ * shape or fold semantics change, so persisted checkpoint rows are discarded
58
+ * instead of folded forward.
59
+ * @param billing - resolved pricing with peak-hour windows.
60
+ * @param catalog - model display rows, in presentation order.
61
+ * @returns the unit definition to register on `ctx.sessionProjections`.
62
+ */
63
+ export declare function billingTodaySpendDefinition(billing: ResolvedBilling, catalog: readonly {
64
+ id: string;
65
+ name: string;
66
+ }[]): BillingUnitDefinition;
67
+ /** Fold a unit from init over one session's event log (the detached cold recipe). */
68
+ export declare function foldBillingUnit(unit: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>, events: readonly SessionEvent[]): BillingUnitState;
69
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `billingTodaySpend` session-projection unit: per-session, per-Beijing-day
3
+ * billed spend, folded eagerly by the DSH projection drive over committed
4
+ * session events and checkpointed by the projection cache. The unit keeps only
5
+ * the spend of the session's LATEST priced day (events are append-only and
6
+ * chronological, so a day strictly older than the state's day never returns);
7
+ * the aggregate "today" read sums the units whose `dayKey` matches the current
8
+ * Beijing day — zero full-log scans once the fold is warm.
9
+ *
10
+ * The unit's fold shares {@link priceEvent} with the events-scan paths
11
+ * (`computeTodaySpend`), so both price with the same table. The unit is
12
+ * client-visible (`wire` = identity) because the persisted-cache read ladder
13
+ * (`sessionProjectionCache.coldSnapshot` / registry `restore`) serves only
14
+ * wired units; the wire value is the state itself.
15
+ * @module @rayadesu/dsh-llm-billing/projection
16
+ */
17
+ import { z } from 'zod';
18
+ import { addEventContribution, emptyTodaySpend, priceEvent } from "./billing.js";
19
+ /** The projection key this unit owns. */
20
+ export const BILLING_UNIT_KEY = 'billingTodaySpend';
21
+ const modelRowSchema = z.object({
22
+ model: z.string(),
23
+ displayName: z.string(),
24
+ cost: z.number().nonnegative(),
25
+ peakCost: z.number().nonnegative(),
26
+ offPeakCost: z.number().nonnegative(),
27
+ cacheHitInputTokens: z.number().int().nonnegative(),
28
+ cacheMissInputTokens: z.number().int().nonnegative(),
29
+ outputTokens: z.number().int().nonnegative(),
30
+ cacheHitInputCost: z.number().nonnegative(),
31
+ cacheMissInputCost: z.number().nonnegative(),
32
+ outputCost: z.number().nonnegative(),
33
+ }).strict();
34
+ const todaySpendSchema = z.object({
35
+ total: z.number().nonnegative(),
36
+ models: z.array(modelRowSchema),
37
+ }).strict();
38
+ const billingUnitSchema = z.object({
39
+ dayKey: z.string(),
40
+ spend: todaySpendSchema,
41
+ }).strict();
42
+ /**
43
+ * Build the `billingTodaySpend` unit for one resolved pricing table. The
44
+ * pricing closure is fixed at registration; a pricing-table change therefore
45
+ * prices only events folded after the change (historical spend keeps its
46
+ * historical rates), unlike the events-scan paths which re-price the whole
47
+ * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
48
+ * shape or fold semantics change, so persisted checkpoint rows are discarded
49
+ * instead of folded forward.
50
+ * @param billing - resolved pricing with peak-hour windows.
51
+ * @param catalog - model display rows, in presentation order.
52
+ * @returns the unit definition to register on `ctx.sessionProjections`.
53
+ */
54
+ export function billingTodaySpendDefinition(billing, catalog) {
55
+ const names = new Map(catalog.map(model => [model.id, model.name]));
56
+ return {
57
+ key: BILLING_UNIT_KEY,
58
+ stateVersion: 1,
59
+ stateSchema: billingUnitSchema,
60
+ init: () => ({ dayKey: '', spend: emptyTodaySpend() }),
61
+ apply: (state, event) => {
62
+ const priced = priceEvent(event, billing, names);
63
+ if (priced === undefined)
64
+ return state;
65
+ if (state.dayKey === priced.dayKey) {
66
+ return { dayKey: state.dayKey, spend: addEventContribution(state.spend, priced) };
67
+ }
68
+ // The session log is append-only and chronological, so an event whose
69
+ // Beijing day is strictly older than the state's day cannot legally
70
+ // follow it; ignore defensively to keep the persisted fold
71
+ // deterministic under reordered or clock-skewed timestamps.
72
+ if (state.dayKey !== '' && priced.dayKey < state.dayKey)
73
+ return state;
74
+ // First priced event, or the session's first priced event of a new day:
75
+ // the state resets to that day's spend.
76
+ return { dayKey: priced.dayKey, spend: addEventContribution(emptyTodaySpend(), priced) };
77
+ },
78
+ wire: { viewSchema: billingUnitSchema, view: state => state },
79
+ };
80
+ }
81
+ /** Fold a unit from init over one session's event log (the detached cold recipe). */
82
+ export function foldBillingUnit(unit, events) {
83
+ let state = unit.init();
84
+ for (const event of events)
85
+ state = unit.apply(state, event);
86
+ return state;
87
+ }
88
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Today-spend read path: the 60-second Beijing-day cache with in-flight
3
+ * coalescing and a force bypass (plan A1), plus the two scan strategies that
4
+ * compute the aggregate behind a cache miss:
5
+ *
6
+ * - projection path (plan C): live sessions read their eagerly folded
7
+ * `billingTodaySpend` projection cell; cold sessions resolve through the
8
+ * projection-cache ladder (cached row + tail replay + registry restore,
9
+ * with write-back) or, without the cache service, one detached local fold
10
+ * over a full `inspect`. Persisted revisions gate every cold read, so a
11
+ * session whose log did not change since the last resolution costs nothing.
12
+ * - events path (plans A2/A3): collect only today's events (per-event
13
+ * Beijing-day filter during collection) with a hard cap, skipping sessions
14
+ * whose persisted revision is unchanged since the last scan.
15
+ *
16
+ * Both strategies run behind the same {@link TodaySpendCache}, so a miss
17
+ * happens at most once per 60 seconds per process, and a manual refresh
18
+ * (`force`) bypasses the time window but keeps the revision caches — an
19
+ * unchanged log provably cannot change the aggregate.
20
+ * @module @rayadesu/dsh-llm-billing/today-spend
21
+ */
22
+ import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
23
+ import type { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence';
24
+ import type { ResolvedBilling } from './billing.ts';
25
+ import type { DeepSeekTodaySpend } from './types.ts';
26
+ import { BILLING_UNIT_KEY, type BillingUnitState } from './projection.ts';
27
+ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
28
+ /** Structural slice of a live session the scanner reads. */
29
+ export interface ScannerSession {
30
+ readonly id: SessionId;
31
+ readonly events: readonly SessionEvent[];
32
+ }
33
+ /** Structural slice of a listed persisted session. */
34
+ export interface ScannerPersistedHeader {
35
+ readonly id: SessionId;
36
+ }
37
+ /** Structural slices of the optional services the scanner reads through. */
38
+ export interface TodaySpendScannerDeps {
39
+ /** Resolves the live SessionStore at scan time (absent in headless assemblies). */
40
+ sessions?: () => {
41
+ list(): readonly ScannerSession[];
42
+ } | undefined;
43
+ /** Resolves the persistence backend at scan time (absent without persistence). */
44
+ persistence?: () => {
45
+ listSnapshots(): Promise<readonly {
46
+ header: ScannerPersistedHeader;
47
+ revision: SessionPersistenceRevision;
48
+ }[]>;
49
+ inspect(id: SessionId): Promise<{
50
+ events: readonly SessionEvent[];
51
+ }>;
52
+ } | undefined;
53
+ /** Resolves the session-projection registry at scan time (absent → events path). */
54
+ projections?: () => {
55
+ stateOf(session: ScannerSession, key: typeof BILLING_UNIT_KEY): BillingUnitState | undefined;
56
+ } | undefined;
57
+ /** Resolves the projection cache at scan time (absent → detached fold for cold sessions). */
58
+ projectionCache?: () => {
59
+ coldSnapshot(id: SessionId): Promise<{
60
+ values: Partial<Record<typeof BILLING_UNIT_KEY, BillingUnitState>>;
61
+ }>;
62
+ } | undefined;
63
+ /**
64
+ * Registers the billing unit on the projection registry, called once before
65
+ * the first projection-path scan. The registry builds cells lazily over the
66
+ * in-memory log, so events committed before registration are folded on
67
+ * first touch — late registration is safe by design.
68
+ */
69
+ ensureUnit?: () => void;
70
+ /** The billing unit's fold (the projection path's detached cold recipe). */
71
+ unit: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>;
72
+ /** Hard cap on today's events collected by the events path. */
73
+ maxEvents: number;
74
+ /** Warn sink for truncation and unreadable sessions. */
75
+ logger: {
76
+ warn(message: string): void;
77
+ };
78
+ /** Pricing table resolved from the plugin config. */
79
+ billing: ResolvedBilling;
80
+ /** Model display rows, in presentation order. */
81
+ catalog: readonly {
82
+ id: string;
83
+ name: string;
84
+ }[];
85
+ }
86
+ /**
87
+ * The A1 cache: one Beijing-day key + a 60s window, an in-flight promise that
88
+ * coalesces concurrent misses, and a `force` bypass for the manual refresh
89
+ * path. Cross-day invalidation is automatic (the day key changes); a failed
90
+ * scan leaves the previous value in place and retries on the next call.
91
+ */
92
+ export declare class TodaySpendCache {
93
+ private readonly scan;
94
+ private readonly ttlMs;
95
+ private readonly now;
96
+ private cachedDayKey;
97
+ private cachedValue;
98
+ private cachedAt;
99
+ private inFlight;
100
+ /**
101
+ * @param ttlMs - time window in milliseconds (default 60 000).
102
+ * @param now - clock source (injectable for tests).
103
+ * @param scan - the aggregate computation behind a miss.
104
+ */
105
+ constructor(scan: (dayKey: string) => Promise<DeepSeekTodaySpend>, ttlMs?: number, now?: () => Date);
106
+ /**
107
+ * Read today's spend, cached per Beijing day within the TTL window.
108
+ * @param force - bypass the time window (manual refresh); the day-key gate
109
+ * and the in-flight coalescing still apply to non-force callers.
110
+ * @returns today's spend.
111
+ */
112
+ get(force?: boolean): Promise<DeepSeekTodaySpend>;
113
+ }
114
+ /**
115
+ * The aggregate computation behind a cache miss. Chooses the projection path
116
+ * when the projection registry is composed, the events path otherwise; both
117
+ * gate cold reads on persisted revisions so steady-state scans touch only
118
+ * sessions whose logs actually changed.
119
+ */
120
+ export declare class TodaySpendScanner {
121
+ private readonly deps;
122
+ /** Cold sessions resolved on the projection path: id → revision + unit state. */
123
+ private readonly coldResolved;
124
+ /** Cold sessions resolved on the events path: id → revision (events were collected). */
125
+ private lastEventsScan;
126
+ constructor(deps: TodaySpendScannerDeps);
127
+ /**
128
+ * Compute today's aggregate for one Beijing day.
129
+ * @param dayKey - the Beijing-time calendar-day key to aggregate.
130
+ * @returns today's spend across every session.
131
+ */
132
+ scan(dayKey: string): Promise<DeepSeekTodaySpend>;
133
+ /** Projection path: eager cells for live sessions, revision-gated cold ladder for the rest. */
134
+ private scanProjections;
135
+ /** Events path: collect only today's events (capped), gated by revisions. */
136
+ private scanEvents;
137
+ }
138
+ //# sourceMappingURL=today-spend.d.ts.map