From da689e03ac983499f9682c46889d454195e7231b Mon Sep 17 00:00:00 2001 From: monwiss <547445+monwiss@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:15:39 -0400 Subject: [PATCH 1/2] refactor(providers): move the tile budget helpers out of the TomTom module utcDayKey, normalizeBudget and isOverBudget move unchanged to src/data/tileBudget.js. Nothing in them was TomTom-specific; the next keyed tile proxy (Xweather) counts a monthly allowance through the same two functions. tomtomTiles.js re-exports them, so the traffic provider and its tests keep their imports. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/format-scope.json | 2 ++ scripts/package-boundaries.json | 12 ++++++-- src/data/tileBudget.js | 53 +++++++++++++++++++++++++++++++++ src/data/tileBudget.test.mjs | 26 ++++++++++++++++ src/data/tomtomTiles.js | 41 ++----------------------- 5 files changed, 94 insertions(+), 40 deletions(-) create mode 100644 src/data/tileBudget.js create mode 100644 src/data/tileBudget.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index eee546dd5..9b82af4b7 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -79,6 +79,8 @@ "src/data/terrainHeightsProxy.test.mjs", "src/data/tomtomTiles.js", "src/data/tomtomTiles.test.mjs", + "src/data/tileBudget.js", + "src/data/tileBudget.test.mjs", "src/data/firmsCsv.js", "src/data/firmsCsv.test.mjs", "src/tooling/environmentProviders.test.mjs", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 189192f10..748f11d44 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -113,12 +113,16 @@ "traffic-provider": { "runtime": "node", "exports": ["./server/providers/traffic"], - "modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"], + "modules": [ + "server/providers/traffic.js", + "src/data/tomtomTiles.js", + "src/data/tileBudget.js" + ], "external": [] }, "traffic-source": { "exports": ["./sources/traffic"], - "modules": ["src/data/tomtomTiles.js"], + "modules": ["src/data/tomtomTiles.js", "src/data/tileBudget.js"], "external": [] }, "firms-csv": { @@ -1009,6 +1013,7 @@ "src/data/cameraSensitivity.js", "src/data/flowMatch.js", "src/data/tomtomTiles.js", + "src/data/tileBudget.js", "src/data/trafficBounds.js", "src/data/trafficFlowStyle.js", "src/data/trafficPresetStyle.js", @@ -1424,6 +1429,7 @@ "src/data/telegeographySubmarineCables.js", "src/data/terrainHeights.js", "src/data/tomtomTiles.js", + "src/data/tileBudget.js", "src/data/tr3bRegistry.js", "src/data/trackedCamera.js", "src/data/trackedModelRegime.js", @@ -2071,6 +2077,7 @@ "src/data/scenePick.js", "src/data/spriteOrder.js", "src/data/tomtomTiles.js", + "src/data/tileBudget.js", "src/data/tr3bRegistry.js", "src/data/trackedCamera.js", "src/data/trackedModelRegime.js", @@ -2707,6 +2714,7 @@ "exports": ["./layers/traffic/source"], "modules": [ "src/data/tomtomTiles.js", + "src/data/tileBudget.js", "src/layers/traffic/flowDecode.js", "src/layers/traffic/flowSource.js", "src/layers/traffic/source.js", diff --git a/src/data/tileBudget.js b/src/data/tileBudget.js new file mode 100644 index 000000000..dcc44a28c --- /dev/null +++ b/src/data/tileBudget.js @@ -0,0 +1,53 @@ +/** + * @file Upstream-request accounting shared by the keyed tile proxies. + * + * A soft cap per UTC period, persisted as `{date, count}` so it survives a + * restart. Nothing here is provider-specific: the caller picks the period by + * the key it passes, and decides what happens over the cap. + * + * Zero dependencies and Cesium-free, so providers and browser helpers can + * import it and node:test can exercise it directly. + * + * @module data/tileBudget + */ + +/** + * UTC calendar-day key for budget bucketing. + * + * @param {number} [epochMs=Date.now()] - Timestamp in ms. + * @returns {string} 'YYYY-MM-DD' in UTC. + */ +export function utcDayKey(epochMs = Date.now()) { + return new Date(epochMs).toISOString().slice(0, 10); +} + +/** + * Normalize a persisted budget state against today's UTC day key. + * Rolls the counter to zero on day change; replaces missing/corrupt state. + * Returns the SAME object when it is already valid for `dayKey` (cheap to + * call on every request). + * + * @param {{date:string, count:number}|null|undefined} state - Persisted state. + * @param {string} dayKey - Today's UTC day key (from `utcDayKey`). + * @returns {{date:string, count:number}} Valid state for `dayKey`. + */ +export function normalizeBudget(state, dayKey) { + const valid = + Boolean(state) && + state.date === dayKey && + Number.isFinite(state.count) && + state.count >= 0; + return valid ? state : { date: dayKey, count: 0 }; +} + +/** + * Whether the daily soft cap has been reached. + * + * @param {{count:number}} state - Normalized budget state. + * @param {number} limit - Daily tile budget; non-positive/invalid never blocks. + * @returns {boolean} True when `count >= limit`. + */ +export function isOverBudget(state, limit) { + if (!Number.isFinite(limit) || limit <= 0) return false; + return state.count >= limit; +} diff --git a/src/data/tileBudget.test.mjs b/src/data/tileBudget.test.mjs new file mode 100644 index 000000000..fa48440bb --- /dev/null +++ b/src/data/tileBudget.test.mjs @@ -0,0 +1,26 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { isOverBudget, normalizeBudget, utcDayKey } from './tileBudget.js'; +import * as tomtom from './tomtomTiles.js'; + +test('tile budget helpers live in one module and TomTom re-exports them', () => { + assert.equal(tomtom.utcDayKey, utcDayKey); + assert.equal(tomtom.normalizeBudget, normalizeBudget); + assert.equal(tomtom.isOverBudget, isOverBudget); +}); + +test('normalizeBudget keeps a valid state and resets another period', () => { + const state = { date: '2026-09-24', count: 3 }; + assert.equal(normalizeBudget(state, '2026-09-24'), state); + assert.deepEqual(normalizeBudget(state, '2026-09-25'), { + date: '2026-09-25', + count: 0, + }); + assert.deepEqual( + normalizeBudget({ date: '2026-09-24', count: -1 }, '2026-09-24'), + { + date: '2026-09-24', + count: 0, + }, + ); +}); diff --git a/src/data/tomtomTiles.js b/src/data/tomtomTiles.js index af4fc95b3..a1dbc18cb 100644 --- a/src/data/tomtomTiles.js +++ b/src/data/tomtomTiles.js @@ -131,42 +131,7 @@ export function tilesForBounds(bounds, zoom = 12, { maxTiles = 64 } = {}) { // ─── Daily budget accounting ─────────────────────────────── /** - * UTC calendar-day key for budget bucketing. - * - * @param {number} [epochMs=Date.now()] - Timestamp in ms. - * @returns {string} 'YYYY-MM-DD' in UTC. + * Re-exported from `./tileBudget.js`, which keyed tile proxies share. The + * traffic provider and its tests keep one import for tile math and budget. */ -export function utcDayKey(epochMs = Date.now()) { - return new Date(epochMs).toISOString().slice(0, 10); -} - -/** - * Normalize a persisted budget state against today's UTC day key. - * Rolls the counter to zero on day change; replaces missing/corrupt state. - * Returns the SAME object when it is already valid for `dayKey` (cheap to - * call on every request). - * - * @param {{date:string, count:number}|null|undefined} state - Persisted state. - * @param {string} dayKey - Today's UTC day key (from `utcDayKey`). - * @returns {{date:string, count:number}} Valid state for `dayKey`. - */ -export function normalizeBudget(state, dayKey) { - const valid = - Boolean(state) && - state.date === dayKey && - Number.isFinite(state.count) && - state.count >= 0; - return valid ? state : { date: dayKey, count: 0 }; -} - -/** - * Whether the daily soft cap has been reached. - * - * @param {{count:number}} state - Normalized budget state. - * @param {number} limit - Daily tile budget; non-positive/invalid never blocks. - * @returns {boolean} True when `count >= limit`. - */ -export function isOverBudget(state, limit) { - if (!Number.isFinite(limit) || limit <= 0) return false; - return state.count >= limit; -} +export { utcDayKey, normalizeBudget, isOverBudget } from './tileBudget.js'; From 75e8e7ddbdb68c01616ed074b00b80bfd1734274 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Mon, 9 Nov 2026 19:41:51 +0400 Subject: [PATCH 2/2] refactor(data): describe tile budgets per period, drop a duplicate test, add the changelog entry --- changelog.d/762-tile-budget-module.md | 4 ++++ src/data/tileBudget.js | 22 +++++++++++----------- src/data/tileBudget.test.mjs | 16 ---------------- src/data/tomtomTiles.js | 5 +++-- 4 files changed, 18 insertions(+), 29 deletions(-) create mode 100644 changelog.d/762-tile-budget-module.md diff --git a/changelog.d/762-tile-budget-module.md b/changelog.d/762-tile-budget-module.md new file mode 100644 index 000000000..6d000d357 --- /dev/null +++ b/changelog.d/762-tile-budget-module.md @@ -0,0 +1,4 @@ +- Move the tile budget helpers `utcDayKey`, `normalizeBudget` and + `isOverBudget` out of the TomTom module into `src/data/tileBudget.js`, so + other keyed tile proxies can share them. `src/data/tomtomTiles.js` + re-exports them; behaviour does not change (monwiss, #762). diff --git a/src/data/tileBudget.js b/src/data/tileBudget.js index dcc44a28c..83cf9b693 100644 --- a/src/data/tileBudget.js +++ b/src/data/tileBudget.js @@ -22,29 +22,29 @@ export function utcDayKey(epochMs = Date.now()) { } /** - * Normalize a persisted budget state against today's UTC day key. - * Rolls the counter to zero on day change; replaces missing/corrupt state. - * Returns the SAME object when it is already valid for `dayKey` (cheap to - * call on every request). + * Normalize a persisted budget state against the current period key. + * Rolls the counter to zero when the period changes; replaces missing/corrupt + * state. Returns the SAME object when it is already valid for `periodKey` + * (cheap to call on every request). * * @param {{date:string, count:number}|null|undefined} state - Persisted state. - * @param {string} dayKey - Today's UTC day key (from `utcDayKey`). - * @returns {{date:string, count:number}} Valid state for `dayKey`. + * @param {string} periodKey - Current period key, e.g. from `utcDayKey`. + * @returns {{date:string, count:number}} Valid state for `periodKey`. */ -export function normalizeBudget(state, dayKey) { +export function normalizeBudget(state, periodKey) { const valid = Boolean(state) && - state.date === dayKey && + state.date === periodKey && Number.isFinite(state.count) && state.count >= 0; - return valid ? state : { date: dayKey, count: 0 }; + return valid ? state : { date: periodKey, count: 0 }; } /** - * Whether the daily soft cap has been reached. + * Whether the soft cap for the current period has been reached. * * @param {{count:number}} state - Normalized budget state. - * @param {number} limit - Daily tile budget; non-positive/invalid never blocks. + * @param {number} limit - Budget per period; non-positive/invalid never blocks. * @returns {boolean} True when `count >= limit`. */ export function isOverBudget(state, limit) { diff --git a/src/data/tileBudget.test.mjs b/src/data/tileBudget.test.mjs index fa48440bb..c1baa4235 100644 --- a/src/data/tileBudget.test.mjs +++ b/src/data/tileBudget.test.mjs @@ -8,19 +8,3 @@ test('tile budget helpers live in one module and TomTom re-exports them', () => assert.equal(tomtom.normalizeBudget, normalizeBudget); assert.equal(tomtom.isOverBudget, isOverBudget); }); - -test('normalizeBudget keeps a valid state and resets another period', () => { - const state = { date: '2026-09-24', count: 3 }; - assert.equal(normalizeBudget(state, '2026-09-24'), state); - assert.deepEqual(normalizeBudget(state, '2026-09-25'), { - date: '2026-09-25', - count: 0, - }); - assert.deepEqual( - normalizeBudget({ date: '2026-09-24', count: -1 }, '2026-09-24'), - { - date: '2026-09-24', - count: 0, - }, - ); -}); diff --git a/src/data/tomtomTiles.js b/src/data/tomtomTiles.js index 9f3b1f0b9..14101c831 100644 --- a/src/data/tomtomTiles.js +++ b/src/data/tomtomTiles.js @@ -1,9 +1,10 @@ /** - * @file Pure tile math + budget accounting for the TomTom traffic-flow proxy. + * @file Pure tile math for the TomTom traffic-flow proxy; budget helpers are + * re-exported from `./tileBudget.js`. * * Shared by the `/api/tomtom` vite plugin (server-side: coordinate validation, * daily budget governor) and `src/layers/traffic/flowSource.js` (client-side: which tiles - * cover the current traffic fetch bounds). Zero dependencies, Cesium-free, so + * cover the current traffic fetch bounds). No external dependencies, Cesium-free, so * both sides can unit-test against it with node:test. * * Slippy scheme: standard Web Mercator XYZ, y grows southward