a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1/**
2 * Scope metadata management system
3 *
4 * Stores metadata for each reactive scope using WeakMap to avoid polluting scope objects.
5 * Metadata includes origin element, pin registry, UID counter, and optional parent reference.
6 */
7
8import type { Scope, ScopeMetadata } from "$types/volt";
9
10/**
11 * WeakMap storing metadata for each scope.
12 * WeakMap ensures metadata is garbage collected when scope is no longer referenced.
13 */
14const scopeMetadataMap = new WeakMap<Scope, ScopeMetadata>();
15
16/**
17 * Create and store metadata for a scope.
18 *
19 * @param scope - The reactive scope object
20 * @param origin - The root element that owns this scope
21 * @param parent - Optional parent scope for debugging/inspection
22 * @returns The created metadata object
23 *
24 * @example
25 * ```ts
26 * const scope = { count: signal(0) };
27 * const metadata = createScopeMetadata(scope, rootElement);
28 * ```
29 */
30export function createScopeMetadata(scope: Scope, origin: Element, parent?: Scope): ScopeMetadata {
31 const metadata: ScopeMetadata = { origin, pins: new Map<string, Element>(), uidCounter: 0, parent };
32
33 scopeMetadataMap.set(scope, metadata);
34 return metadata;
35}
36
37/**
38 * Get metadata for a scope.
39 *
40 * @param scope - The scope to get metadata for
41 * @returns The metadata object, or undefined if not found
42 *
43 * @example
44 * ```ts
45 * const metadata = getScopeMetadata(scope);
46 * if (metadata) {
47 * console.log('Origin element:', metadata.origin);
48 * }
49 * ```
50 */
51export function getScopeMetadata(scope: Scope): ScopeMetadata | undefined {
52 return scopeMetadataMap.get(scope);
53}
54
55/**
56 * Register a pinned element in the scope's pin registry.
57 *
58 * @param scope - The scope to register the pin in
59 * @param name - The pin name
60 * @param element - The element to pin
61 *
62 * @example
63 * ```ts
64 * registerPin(scope, 'submitButton', buttonElement);
65 * // Later accessible via $pins.submitButton
66 * ```
67 */
68export function registerPin(scope: Scope, name: string, element: Element): void {
69 const metadata = scopeMetadataMap.get(scope);
70 if (metadata) {
71 metadata.pins.set(name, element);
72 }
73}
74
75/**
76 * Get a pinned element by name from the scope.
77 *
78 * @param scope - The scope to search in
79 * @param name - The pin name to retrieve
80 * @returns The pinned element, or undefined if not found
81 *
82 * @example
83 * ```ts
84 * const button = getPin(scope, 'submitButton');
85 * if (button) {
86 * button.focus();
87 * }
88 * ```
89 */
90export function getPin(scope: Scope, name: string): Element | undefined {
91 const metadata = scopeMetadataMap.get(scope);
92 return metadata?.pins.get(name);
93}
94
95/**
96 * Get all pins for a scope as a record object.
97 * This is what gets injected as $pins in the scope.
98 *
99 * @param scope - The scope to get pins for
100 * @returns Record mapping pin names to elements
101 *
102 * @example
103 * ```ts
104 * const pins = getPins(scope);
105 * // Access as: pins.submitButton, pins.inputField, etc.
106 * ```
107 */
108export function getPins(scope: Scope): Record<string, Element> {
109 const metadata = scopeMetadataMap.get(scope);
110 if (!metadata) return {};
111
112 const pins: Record<string, Element> = {};
113 for (const [name, element] of metadata.pins) {
114 pins[name] = element;
115 }
116 return pins;
117}
118
119/**
120 * Increment and return the UID counter for a scope.
121 * Used internally by $uid() to generate deterministic IDs.
122 *
123 * @param scope - The scope to increment counter for
124 * @returns The next UID number
125 */
126export function incrementUidCounter(scope: Scope): number {
127 const metadata = scopeMetadataMap.get(scope);
128 if (!metadata) return 0;
129
130 return ++metadata.uidCounter;
131}