Skip to content

Commit f32b1ab

Browse files
committed
Shrink pan-ready boot graph
1 parent f351bb0 commit f32b1ab

11 files changed

Lines changed: 265 additions & 250 deletions

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ section before making changes there.
2828
- Disk persistence + stored-SVG rewrite/load path: [svg board store](./server/persistence/svg_board_store.mjs), [stored SVG path helpers](./server/persistence/svg_board_paths.mjs), [stored SVG item codec](./server/persistence/stored_svg_item_codec.mjs), [streaming stored SVG scan](./server/persistence/streaming_stored_svg_scan.mjs). This layer owns primary-vs-`.svg.bak` fallback, streaming rewrites, and summary/full decode split; cold board load and canonical indexing must stay on summary-only decode. Stored SVG load is parse-only and per-item fault tolerant: malformed items are skipped with logging instead of being normalized or repaired.
2929
- Board state tracks stored SVG root extent incrementally from loaded root dimensions and accepted item writes; saves write that extent to root `width`/`height`. Ordinary deletes do not shrink it, and clear resets it to the default SVG size.
3030
- Env parsing + rate-limit profile construction must stay cold. Never do unneeded work in the hot path.
31-
- Shared geometry/id/color/text helpers **[hot]**: [message primitives](./client-data/js/message_common.js); live server validation uses the non-repairing range/geometry checks there, while client tools own UX-side clamping/normalization before optimistic draw/send.
31+
- Shared geometry/id/color/text helpers **[hot]**: [message primitives](./client-data/js/message_common.js), with tiny numeric clamp/limit helpers split into [message limits](./client-data/js/message_limits.js) so pan-ready client boot can clamp viewport/preference values without importing the full validation surface; live server validation uses the non-repairing range/geometry checks there, while client tools own UX-side clamping/normalization before optimistic draw/send.
3232
- Live socket/runtime mutations use numeric `tool` codes from the ordered [tool registry](./client-data/tools/index.js) and numeric mutation `type` codes from [mutation types](./client-data/js/mutation_type.js); replay/control socket payloads are event-typed by their Socket.IO event name and do not carry redundant string `type` discriminators. Stored SVG keeps string tool ids and tag names such as `rectangle`, `rect`, `pencil`, `path`, and `text` because that is document syntax, not the runtime protocol.
3333
- Native geometry model is integer board space with `scale = 1` as max zoom; legacy `.json` migration remains the only place that converts units from old coordinate semantics.
3434
- Page shell that server-renders the toolbar and loads the module entrypoint for the board runtime: [board document](./client-data/board.html), [board module boot](./client-data/js/board_main.js). For stored `.svg` boards, the board document now streams the authoritative SVG baseline straight through the HTML response so the browser can render it progressively as bytes arrive, without materializing the full SVG in server JS; the head provides `modulepreload` hints only for the pan-ready boot closure, while tool implementation modules, tool stylesheets, the always-on `cursor` module, and Pencil's path-data polyfill are loaded on demand by the tool registry. The async board module entrypoint creates a metadata-only runtime shell as soon as the shell is ready, attaches the streamed board DOM, installs viewport/wheel/hash handling plus temporary mouse pan, restores the viewport, then hydrates the full runtime, opens Socket.IO with the attached baseline seq, boots the hand tool as the critical interactive tool, and lazy-boots rendered tools plus `cursor`; the server-rendered localized loading HUD remains visible until that lazy boot has completed and socket replay no longer needs it. Static app assets are served at stable URLs without version query params; production caching relies on normal HTTP revalidation headers for those assets rather than query-string cache busting. Board HTML, canonical SVG, and preview HTTP responses negotiate built-in Node compression from `Accept-Encoding`, preferring `zstd` when both sides support it, then `br`, then `gzip`; HTTP request-duration metrics classify the final response content coding with the standard `http.response.header.content-encoding` attribute, using `identity` for uncompressed responses. Canonical `/boards/*.svg` responses use seq-based weak ETags and short revalidation (`public, max-age=3, must-revalidate` in production, `no-store` in development); matching `If-None-Match` returns 304 without streaming the SVG body. The board chrome (HUD, menu, JSON payloads, module entrypoint) must stay before the streamed board markup in the HTML so the visible UI can paint and react before the SVG finishes streaming. Legacy `.json` fallback boards still render a generated inline SVG. Board boot publishes explicit DOM phases on `document.documentElement.dataset.boardPhase` and dispatches `wbo:board-phase` events.
35-
- Client state machine + staged tool boot + send/receive plumbing: [board runtime hydration](./client-data/js/board.js), with metadata-only runtime setup in [core app tools](./client-data/js/app_tools_core.js) and [board bootstrap](./client-data/js/board_bootstrap.js), the full runtime root in [app tools](./client-data/js/app_tools.js), shared DOM attachment in [board DOM bootstrap](./client-data/js/board_dom_bootstrap.js), page chrome wiring in [board shell module](./client-data/js/board_shell_module.js), message hooks/routing in [board message module](./client-data/js/board_message_module.js), tool boot/registry state in [board tool registry module](./client-data/js/board_tool_registry_module.js), board access/status/write/optimistic/replay/connection/presence modules in [board access module](./client-data/js/board_access_module.js), [board status module](./client-data/js/board_status_module.js), [board write module](./client-data/js/board_write_module.js), [board optimistic module](./client-data/js/board_optimistic_module.js), [board replay module](./client-data/js/board_replay_module.js), [board connection module](./client-data/js/board_connection_module.js), and [board presence module](./client-data/js/board_presence_module.js), and independent runtime module classes in [board runtime core](./client-data/js/board_runtime_core.js). Board viewport panning, zooming, scale math, wheel/pinch ownership, URL hash sync, root SVG extent growth, and scaled document scroll bounds are centralized in [viewport controller](./client-data/js/board_viewport.js), with content-only board-space extent derivation in [board extent helpers](./client-data/js/board_extent.js). Generic message hooks must derive extents from persistent/content payload data only; ephemeral messages such as cursor updates must not grow the scroll extent. Minimum zoom materializes the full logical board extent so browser scrollbar ends map to board ends. SVG layout measurement such as `getBBox()` is allowed only in narrow tool interaction paths that already operate on a small selected/updated element set and must be documented at the call helper. Gesture handlers there must stay O(1) and never traverse or measure board SVG contents.
35+
- Client state machine + staged tool boot + send/receive plumbing: [board runtime hydration](./client-data/js/board.js), with metadata-only runtime setup in [core app tools](./client-data/js/app_tools_core.js) and [board bootstrap](./client-data/js/board_bootstrap.js), the full runtime root in [app tools](./client-data/js/app_tools.js), full-runtime-only asset/id/interaction/rate-limit helpers in [full runtime modules](./client-data/js/board_full_runtime_modules.js), shared DOM attachment in [board DOM bootstrap](./client-data/js/board_dom_bootstrap.js), page chrome wiring in [board shell module](./client-data/js/board_shell_module.js), message hooks/routing in [board message module](./client-data/js/board_message_module.js), tool boot/registry state in [board tool registry module](./client-data/js/board_tool_registry_module.js), board access/status/write/optimistic/replay/connection/presence modules in [board access module](./client-data/js/board_access_module.js), [board status module](./client-data/js/board_status_module.js), [board write module](./client-data/js/board_write_module.js), [board optimistic module](./client-data/js/board_optimistic_module.js), [board replay module](./client-data/js/board_replay_module.js), [board connection module](./client-data/js/board_connection_module.js), and [board presence module](./client-data/js/board_presence_module.js), and pan-ready runtime module classes in [board runtime core](./client-data/js/board_runtime_core.js). Board viewport panning, zooming, scale math, wheel/pinch ownership, URL hash sync, root SVG extent growth, and scaled document scroll bounds are centralized in [viewport controller](./client-data/js/board_viewport.js), with content-only board-space extent derivation in [board extent helpers](./client-data/js/board_extent.js). Generic message hooks must derive extents from persistent/content payload data only; ephemeral messages such as cursor updates must not grow the scroll extent. Minimum zoom materializes the full logical board extent so browser scrollbar ends map to board ends. SVG layout measurement such as `getBBox()` is allowed only in narrow tool interaction paths that already operate on a small selected/updated element set and must be documented at the call helper. Gesture handlers there must stay O(1) and never traverse or measure board SVG contents.
3636
- Shared socket transport utilities: [transport helpers](./client-data/js/board_transport.js).
3737
- Shared canonical board-name normalization + validation for landing-page input, board-page redirects, and valid-name checks: [board name helpers](./client-data/js/board_name.js). Canonical names are lowercase, Unicode letter/digit based, and persist directly to `board-<name>.{svg,json}` filenames.
3838
- Shared pronounceable name generator for readable socket user labels and random private board names: [pronounceable names](./server/shared/pronounceable_name.mjs).

client-data/board.html

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,7 @@
1717
<link rel="modulepreload" href="../js/board_runtime_core.js" />
1818
<link rel="modulepreload" href="../js/board_viewport.js" />
1919
<link rel="modulepreload" href="../js/frontend_logging.js" />
20-
<link rel="modulepreload" href="../js/message_common.js" />
21-
<link rel="modulepreload" href="../js/message_tool_metadata.js" />
22-
<link rel="modulepreload" href="../js/mutation_type.js" />
23-
<link rel="modulepreload" href="../js/rate_limit_common.js" />
24-
<link rel="modulepreload" href="../tools/manifest.js" />
25-
<link rel="modulepreload" href="../tools/tool-defaults.js" />
26-
<link rel="modulepreload" href="../tools/tool-order.js" />
20+
<link rel="modulepreload" href="../js/message_limits.js" />
2721
<script id="socketio-client" async src="../socket.io/socket.io.js"></script>
2822
<meta name="description" content="{{translations.tagline}}" />
2923
<meta name="keywords"

client-data/js/app_tools.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { initializeCoreRuntime } from "./app_tools_core.js";
2+
import {
3+
AssetModule,
4+
IdModule,
5+
InteractionModule,
6+
normalizeBoardAssetPath,
7+
RateLimitModule,
8+
} from "./board_full_runtime_modules.js";
29
import { AccessModule } from "./board_access_module.js";
310
import { ConnectionModule } from "./board_connection_module.js";
411
import {
@@ -38,6 +45,10 @@ export class AppTools {
3845
* @returns {import("../../types/app-runtime").AppToolsState}
3946
*/
4047
export function attachFullRuntimeModules(tools, options) {
48+
tools.assets = new AssetModule(normalizeBoardAssetPath);
49+
tools.interaction = new InteractionModule();
50+
tools.ids = new IdModule();
51+
tools.rateLimits = new RateLimitModule(tools.config, tools.identity);
4152
tools.toolRegistry = new ToolRegistryModule(
4253
() => tools,
4354
options.logBoardEvent,

client-data/js/app_tools_core.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import {
2-
AssetModule,
32
AttachedBoardDomRuntimeModule,
43
ConfigModule,
54
CoordinateModule,
65
DetachedBoardDomRuntimeModule,
76
I18nModule,
87
IdentityModule,
9-
IdModule,
10-
InteractionModule,
11-
normalizeBoardAssetPath,
128
PreferenceModule,
13-
RateLimitModule,
149
ViewportStateModule,
1510
} from "./board_runtime_core.js";
1611
import { createViewportController } from "./board_viewport.js";
@@ -28,10 +23,7 @@ export function initializeCoreRuntime(target, options) {
2823
i18n: new I18nModule(options.translations),
2924
config: new ConfigModule(options.serverConfig),
3025
identity: new IdentityModule(options.boardName, options.token),
31-
assets: new AssetModule(normalizeBoardAssetPath),
3226
dom: new DetachedBoardDomRuntimeModule(),
33-
interaction: new InteractionModule(),
34-
ids: new IdModule(),
3527
preferences: new PreferenceModule(
3628
options.colorPresets,
3729
options.initialPreferences,
@@ -45,7 +37,6 @@ export function initializeCoreRuntime(target, options) {
4537
},
4638
});
4739
const runtime = /** @type {any} */ (target);
48-
runtime.rateLimits = new RateLimitModule(runtime.config, runtime.identity);
4940
const viewportController = createViewportController(runtime);
5041
runtime.viewportState = new ViewportStateModule(viewportController);
5142
runtime.coordinates = new CoordinateModule(
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { getToolRuntimeAssetPath } from "../tools/tool-defaults.js";
2+
import RateLimitCommon from "./rate_limit_common.js";
3+
4+
/** @import { ConfigModule, IdentityModule } from "./board_runtime_core.js" */
5+
/** @import { LiveBoardMessage, RateLimitKind } from "../../types/app-runtime" */
6+
7+
const RATE_LIMIT_KINDS = /** @type {RateLimitKind[]} */ (
8+
RateLimitCommon.RATE_LIMIT_KINDS
9+
);
10+
11+
/**
12+
* @param {string} assetPath
13+
* @returns {string}
14+
*/
15+
export function normalizeBoardAssetPath(assetPath) {
16+
if (
17+
assetPath.startsWith("./") ||
18+
assetPath.startsWith("../") ||
19+
assetPath.startsWith("/") ||
20+
assetPath.startsWith("data:") ||
21+
assetPath.startsWith("http://") ||
22+
assetPath.startsWith("https://")
23+
) {
24+
return assetPath;
25+
}
26+
return `../${assetPath}`;
27+
}
28+
29+
export class AssetModule {
30+
/** @param {(assetPath: string) => string} resolveAssetPath */
31+
constructor(resolveAssetPath) {
32+
this.resolveAssetPath = resolveAssetPath;
33+
}
34+
35+
/**
36+
* @param {string} toolName
37+
* @param {string} assetFile
38+
*/
39+
getToolAssetUrl(toolName, assetFile) {
40+
return this.resolveAssetPath(getToolRuntimeAssetPath(toolName, assetFile));
41+
}
42+
}
43+
44+
export class InteractionModule {
45+
constructor() {
46+
this.drawingEvent = true;
47+
this.showMarker = true;
48+
this.showOtherCursors = true;
49+
this.showMyCursor = true;
50+
}
51+
}
52+
53+
export class IdModule {
54+
/**
55+
* @param {string} [prefix]
56+
* @param {string} [suffix]
57+
*/
58+
generateUID(prefix, suffix) {
59+
let uid = Date.now().toString(36);
60+
uid += Math.round(Math.random() * 36).toString(36);
61+
if (prefix) uid = prefix + uid;
62+
if (suffix) uid = uid + suffix;
63+
return uid;
64+
}
65+
}
66+
67+
const rateLimitModuleState = new WeakMap();
68+
69+
export class RateLimitModule {
70+
/**
71+
* @param {ConfigModule} config
72+
* @param {IdentityModule} identity
73+
*/
74+
constructor(config, identity) {
75+
rateLimitModuleState.set(this, { config, identity });
76+
}
77+
78+
/** @param {RateLimitKind} kind */
79+
getRateLimitDefinition(kind) {
80+
const state =
81+
/** @type {{config: ConfigModule, identity: IdentityModule}} */ (
82+
rateLimitModuleState.get(this)
83+
);
84+
const configured = state.config.serverConfig.RATE_LIMITS || {};
85+
if (configured && configured[kind]) return configured[kind];
86+
87+
return {
88+
limit: 0,
89+
anonymousLimit: 0,
90+
periodMs: 0,
91+
};
92+
}
93+
94+
/** @param {RateLimitKind} kind */
95+
getEffectiveRateLimit(kind) {
96+
const state =
97+
/** @type {{config: ConfigModule, identity: IdentityModule}} */ (
98+
rateLimitModuleState.get(this)
99+
);
100+
return RateLimitCommon.getEffectiveRateLimitDefinition(
101+
this.getRateLimitDefinition(kind),
102+
state.identity.boardName,
103+
);
104+
}
105+
106+
/** @param {LiveBoardMessage} message */
107+
getBufferedWriteCosts(message) {
108+
return RATE_LIMIT_KINDS.reduce(
109+
(costs, kind) => {
110+
costs[kind] = RateLimitCommon.getRateLimitCost(kind, message);
111+
return costs;
112+
},
113+
/** @type {import("../../types/app-runtime").RateLimitCosts} */ ({}),
114+
);
115+
}
116+
}

0 commit comments

Comments
 (0)