@jqhtml/core 2.3.35 → 2.3.54
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.
- package/LICENSE +1 -1
- package/dist/cache-key-serializer.d.ts +47 -0
- package/dist/cache-key-serializer.d.ts.map +1 -0
- package/dist/component-cache.d.ts +57 -0
- package/dist/component-cache.d.ts.map +1 -0
- package/dist/component-events.d.ts +68 -0
- package/dist/component-events.d.ts.map +1 -0
- package/dist/component-queue.d.ts +47 -0
- package/dist/component-queue.d.ts.map +1 -0
- package/dist/component.d.ts +118 -67
- package/dist/component.d.ts.map +1 -1
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/data-proxy.d.ts +37 -0
- package/dist/data-proxy.d.ts.map +1 -0
- package/dist/index.cjs +1860 -733
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1854 -734
- package/dist/index.js.map +1 -1
- package/dist/instruction-processor.d.ts.map +1 -1
- package/dist/jqhtml-core.esm.js +1855 -735
- package/dist/jqhtml-core.esm.js.map +1 -1
- package/dist/jquery-plugin.d.ts.map +1 -1
- package/dist/lifecycle-manager.d.ts +3 -5
- package/dist/lifecycle-manager.d.ts.map +1 -1
- package/dist/load-coordinator.d.ts +18 -1
- package/dist/load-coordinator.d.ts.map +1 -1
- package/dist/local-storage.d.ts +2 -0
- package/dist/local-storage.d.ts.map +1 -1
- package/dist/preload-data.d.ts +69 -0
- package/dist/preload-data.d.ts.map +1 -0
- package/dist/viewport.d.ts +36 -0
- package/dist/viewport.d.ts.map +1 -0
- package/package.json +16 -3
package/dist/index.js
CHANGED
|
@@ -34,11 +34,9 @@ class LifecycleManager {
|
|
|
34
34
|
* Boot a component - run its full lifecycle
|
|
35
35
|
* Called when component is created
|
|
36
36
|
*
|
|
37
|
-
* Supports lifecycle
|
|
38
|
-
* -
|
|
39
|
-
*
|
|
40
|
-
* - skip_ready: Skip on_ready only.
|
|
41
|
-
* Component reports ready after on_render completes (after potential re-render from on_load).
|
|
37
|
+
* Supports lifecycle truncation flags:
|
|
38
|
+
* - _load_only: on_create + on_load only. No render, no children, no on_render, no on_loaded, no on_ready.
|
|
39
|
+
* - _load_render_only: on_create + render + on_load + re-render. No on_render, no on_loaded, no on_ready.
|
|
42
40
|
*/
|
|
43
41
|
async boot_component(component) {
|
|
44
42
|
this.active_components.add(component);
|
|
@@ -50,20 +48,25 @@ class LifecycleManager {
|
|
|
50
48
|
return;
|
|
51
49
|
// Trigger create event
|
|
52
50
|
component.trigger('create');
|
|
53
|
-
// Check for
|
|
54
|
-
const
|
|
55
|
-
const
|
|
51
|
+
// Check for lifecycle truncation flags
|
|
52
|
+
const load_only = component._load_only;
|
|
53
|
+
const load_render_only = component._load_render_only;
|
|
54
|
+
const is_detached = component._is_detached;
|
|
56
55
|
let render_id;
|
|
57
|
-
if (
|
|
58
|
-
//
|
|
59
|
-
// Still need to set render_id to 0 for tracking
|
|
56
|
+
if (load_only || is_detached) {
|
|
57
|
+
// _load_only or detached: skip initial render entirely
|
|
60
58
|
render_id = 0;
|
|
61
59
|
component._render_count = 0;
|
|
62
60
|
}
|
|
61
|
+
else if (load_render_only) {
|
|
62
|
+
// _load_render_only: render to create children, but skip on_render()
|
|
63
|
+
render_id = component._render(null, { skip_on_render: true });
|
|
64
|
+
// Check if stopped during render
|
|
65
|
+
if (component._stopped)
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
63
68
|
else {
|
|
64
|
-
//
|
|
65
|
-
// Note: _render() now calls on_render() internally after DOM update
|
|
66
|
-
// Capture render ID to detect if another render happens before ready
|
|
69
|
+
// Normal: render with on_render()
|
|
67
70
|
render_id = component._render();
|
|
68
71
|
// Check if stopped during render
|
|
69
72
|
if (component._stopped)
|
|
@@ -72,30 +75,39 @@ class LifecycleManager {
|
|
|
72
75
|
// Load phase - ASYNC, but only if on_load() is overridden
|
|
73
76
|
// Skip entirely if component uses default stub to avoid unnecessary yield
|
|
74
77
|
if (component._has_on_load()) {
|
|
78
|
+
// Load gates: await any promises registered via gate_load() during
|
|
79
|
+
// on_create() before the FIRST on_load(). Gates delay ONLY this load
|
|
80
|
+
// phase — create/render/on_render/cached-paint already happened above.
|
|
81
|
+
// Released early by reload()/refresh()/stop() while gated (see component).
|
|
82
|
+
// Returns null (no await) for the common no-gate case to keep timing
|
|
83
|
+
// identical for ungated components.
|
|
84
|
+
const gate_wait = component._await_load_gates();
|
|
85
|
+
if (gate_wait) {
|
|
86
|
+
await gate_wait;
|
|
87
|
+
// Check if stopped while awaiting gates
|
|
88
|
+
if (component._stopped)
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
75
91
|
await component._load();
|
|
76
92
|
// Yield to microtask queue after on_load completes
|
|
77
93
|
// This ensures async queue tasks always run before the on_load process completes,
|
|
78
94
|
// even if on_load() itself completes instantly (deterministic behavior)
|
|
79
95
|
await Promise.resolve();
|
|
80
96
|
}
|
|
81
|
-
// Trigger 'loaded' event - fires after on_load completes (or would have completed)
|
|
82
|
-
// This happens before the render/on_render second step
|
|
83
|
-
component.trigger('loaded');
|
|
84
97
|
// Check if stopped during load
|
|
85
98
|
if (component._stopped)
|
|
86
99
|
return;
|
|
87
|
-
// If
|
|
88
|
-
if (
|
|
89
|
-
// Set ready state and trigger event
|
|
100
|
+
// If _load_only, mark component as ready now and return
|
|
101
|
+
if (load_only) {
|
|
90
102
|
component._ready_state = 4;
|
|
91
103
|
component._update_debug_attrs();
|
|
92
|
-
component._log_lifecycle('ready', 'complete (
|
|
104
|
+
component._log_lifecycle('ready', 'complete (_load_only)');
|
|
93
105
|
component.trigger('ready');
|
|
94
106
|
return;
|
|
95
107
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
if (component._should_rerender()) {
|
|
108
|
+
// Detached elements always render here (initial render was skipped)
|
|
109
|
+
// Normal elements re-render only if data changed during on_load
|
|
110
|
+
if (is_detached || component._should_rerender()) {
|
|
99
111
|
// Disable animations during re-render to prevent visual artifacts
|
|
100
112
|
// from CSS transitions/animations firing on newly rendered elements
|
|
101
113
|
const $el = component.$;
|
|
@@ -112,11 +124,25 @@ class LifecycleManager {
|
|
|
112
124
|
}, 5);
|
|
113
125
|
});
|
|
114
126
|
}
|
|
115
|
-
|
|
127
|
+
// _load_render_only: re-render with skip_on_render to suppress DOM hooks
|
|
128
|
+
if (load_render_only) {
|
|
129
|
+
render_id = component._render(null, { skip_on_render: true });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
render_id = component._render();
|
|
133
|
+
}
|
|
116
134
|
// Check if stopped during re-render
|
|
117
135
|
if (component._stopped)
|
|
118
136
|
return;
|
|
119
137
|
}
|
|
138
|
+
// _load_render_only: skip waiting for children, on_ready — mark ready and return
|
|
139
|
+
if (load_render_only) {
|
|
140
|
+
component._ready_state = 4;
|
|
141
|
+
component._update_debug_attrs();
|
|
142
|
+
component._log_lifecycle('ready', 'complete (_load_render_only)');
|
|
143
|
+
component.trigger('ready');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
120
146
|
// Fire 'rendered' event - component has completed its synchronous render chain
|
|
121
147
|
// This happens once, after the final on_render (whether from first render or re-render after on_load)
|
|
122
148
|
if (!component._has_rendered) {
|
|
@@ -140,15 +166,6 @@ class LifecycleManager {
|
|
|
140
166
|
if (component._render_count !== render_id) {
|
|
141
167
|
return; // Stale render, don't call ready
|
|
142
168
|
}
|
|
143
|
-
// If skip_ready, mark component as ready now without calling _ready()
|
|
144
|
-
if (skip_ready) {
|
|
145
|
-
// Set ready state and trigger event
|
|
146
|
-
component._ready_state = 4;
|
|
147
|
-
component._update_debug_attrs();
|
|
148
|
-
component._log_lifecycle('ready', 'complete (skip_ready)');
|
|
149
|
-
component.trigger('ready');
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
169
|
// Ready phase - waits for children, then calls on_ready()
|
|
153
170
|
await component._ready();
|
|
154
171
|
// Check if stopped during ready
|
|
@@ -479,6 +496,70 @@ function register(source) {
|
|
|
479
496
|
'class name mangling, define static component_name or use register_component().');
|
|
480
497
|
}
|
|
481
498
|
|
|
499
|
+
/**
|
|
500
|
+
* JQHTML Runtime Configuration
|
|
501
|
+
*
|
|
502
|
+
* Integration-level settings supplied by the host application (RSpade, Laravel,
|
|
503
|
+
* a bundler entry point) when jqhtml is loaded. Distinct from `jqhtml.debug`,
|
|
504
|
+
* which is a developer's interactive tracing switch: config here describes the
|
|
505
|
+
* ENVIRONMENT the app is running in, and jqhtml derives behaviour from it.
|
|
506
|
+
*
|
|
507
|
+
* jqhtml.init($, { mode: 'production' });
|
|
508
|
+
* jqhtml.configure({ mode: 'development' });
|
|
509
|
+
*
|
|
510
|
+
* Default mode is 'development', so an integration that passes nothing keeps
|
|
511
|
+
* every diagnostic. Production is always opt-in.
|
|
512
|
+
*/
|
|
513
|
+
/** Flags implied by each mode. Add future conventions here, not at call sites. */
|
|
514
|
+
const MODE_DEFAULTS = {
|
|
515
|
+
development: {
|
|
516
|
+
warn_uncacheable_args: true,
|
|
517
|
+
debug_attributes: true,
|
|
518
|
+
},
|
|
519
|
+
production: {
|
|
520
|
+
warn_uncacheable_args: false,
|
|
521
|
+
debug_attributes: false,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
let _mode = 'development';
|
|
525
|
+
let _flags = { ...MODE_DEFAULTS.development };
|
|
526
|
+
/**
|
|
527
|
+
* Apply integration configuration.
|
|
528
|
+
*
|
|
529
|
+
* `mode` resets every flag to that mode's defaults; any explicit flag in the
|
|
530
|
+
* same call then overrides it. Calling with only flags leaves the mode alone.
|
|
531
|
+
* Repeated calls merge.
|
|
532
|
+
*/
|
|
533
|
+
function configure(config) {
|
|
534
|
+
if (!config)
|
|
535
|
+
return;
|
|
536
|
+
if (config.mode) {
|
|
537
|
+
if (!MODE_DEFAULTS[config.mode]) {
|
|
538
|
+
throw new Error(`[JQHTML] Unknown mode '${config.mode}'. Expected 'development' or 'production'.`);
|
|
539
|
+
}
|
|
540
|
+
_mode = config.mode;
|
|
541
|
+
_flags = { ...MODE_DEFAULTS[config.mode] };
|
|
542
|
+
}
|
|
543
|
+
if (config.warn_uncacheable_args !== undefined) {
|
|
544
|
+
_flags.warn_uncacheable_args = !!config.warn_uncacheable_args;
|
|
545
|
+
}
|
|
546
|
+
if (config.debug_attributes !== undefined) {
|
|
547
|
+
_flags.debug_attributes = !!config.debug_attributes;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/** Current mode plus every resolved flag. */
|
|
551
|
+
function get_config() {
|
|
552
|
+
return { mode: _mode, ..._flags };
|
|
553
|
+
}
|
|
554
|
+
/** True when debug-only DOM attributes (data-sid) should be emitted. */
|
|
555
|
+
function debug_attributes_enabled() {
|
|
556
|
+
return _flags.debug_attributes;
|
|
557
|
+
}
|
|
558
|
+
/** True when uncacheable-args warnings should be printed. */
|
|
559
|
+
function warn_uncacheable_args_enabled() {
|
|
560
|
+
return _flags.warn_uncacheable_args;
|
|
561
|
+
}
|
|
562
|
+
|
|
482
563
|
/**
|
|
483
564
|
* JQHTML v2 Instruction Processor
|
|
484
565
|
*
|
|
@@ -616,6 +697,9 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
|
|
616
697
|
}
|
|
617
698
|
// Add simple string/numeric attributes directly
|
|
618
699
|
for (const [key, value] of Object.entries(attrs)) {
|
|
700
|
+
if (key === 'data-sid' && !debug_attributes_enabled()) {
|
|
701
|
+
continue; // debug-only attribute; the scoped id is what $sid() resolves
|
|
702
|
+
}
|
|
619
703
|
if (!key.startsWith('$') && !key.startsWith('on') && !key.startsWith('@') &&
|
|
620
704
|
!key.startsWith('data-bind-') && !key.startsWith('data-__-on-') &&
|
|
621
705
|
(typeof value === 'string' || typeof value === 'number')) {
|
|
@@ -654,20 +738,26 @@ function process_component_to_html(instruction, html, components, context) {
|
|
|
654
738
|
// pass this behavior to all child components rendered in its template
|
|
655
739
|
let props = originalProps;
|
|
656
740
|
const parentArgs = context.args;
|
|
657
|
-
// Check if we need to propagate any flags
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
741
|
+
// Check if we need to propagate any flags.
|
|
742
|
+
// Template-authored flags arrive with the '$' still on the key ($_load_only=false
|
|
743
|
+
// compiles to props['$_load_only']) - honor both spellings so a child template can
|
|
744
|
+
// opt out of a cascading parent flag with an explicit false.
|
|
745
|
+
const own_use_cached_data = props.use_cached_data !== undefined ? props.use_cached_data : props['$use_cached_data'];
|
|
746
|
+
const own_load_only = props._load_only !== undefined ? props._load_only : props['$_load_only'];
|
|
747
|
+
const own_load_render_only = props._load_render_only !== undefined ? props._load_render_only : props['$_load_render_only'];
|
|
748
|
+
const propagate_use_cached_data = parentArgs?.use_cached_data === true && own_use_cached_data === undefined;
|
|
749
|
+
const propagate_load_only = parentArgs?._load_only === true && own_load_only === undefined;
|
|
750
|
+
const propagate_load_render_only = parentArgs?._load_render_only === true && own_load_render_only === undefined;
|
|
751
|
+
if (propagate_use_cached_data || propagate_load_only || propagate_load_render_only) {
|
|
662
752
|
props = { ...originalProps };
|
|
663
753
|
if (propagate_use_cached_data) {
|
|
664
754
|
props.use_cached_data = true;
|
|
665
755
|
}
|
|
666
|
-
if (
|
|
667
|
-
props.
|
|
756
|
+
if (propagate_load_only) {
|
|
757
|
+
props._load_only = true;
|
|
668
758
|
}
|
|
669
|
-
if (
|
|
670
|
-
props.
|
|
759
|
+
if (propagate_load_render_only) {
|
|
760
|
+
props._load_render_only = true;
|
|
671
761
|
}
|
|
672
762
|
}
|
|
673
763
|
// Determine if third parameter is a function (default content) or object (named slots)
|
|
@@ -699,7 +789,10 @@ function process_component_to_html(instruction, html, components, context) {
|
|
|
699
789
|
const baseId = props['data-sid'];
|
|
700
790
|
// The compiled code ALWAYS sets props['id'] with the correct scoped value
|
|
701
791
|
// Just use it directly - it already has the correct parent _cid baked in
|
|
702
|
-
|
|
792
|
+
// data-sid is debug-only: $sid() resolves via the scoped id, never this.
|
|
793
|
+
html.push(debug_attributes_enabled()
|
|
794
|
+
? ` id="${props['id']}" data-sid="${baseId}"`
|
|
795
|
+
: ` id="${props['id']}"`);
|
|
703
796
|
}
|
|
704
797
|
// Regular id passes through unchanged
|
|
705
798
|
else if (props['id']) {
|
|
@@ -841,6 +934,10 @@ function apply_attributes(element, attrs, context) {
|
|
|
841
934
|
}
|
|
842
935
|
}
|
|
843
936
|
else if (key.startsWith('data-')) {
|
|
937
|
+
// data-sid is debug-only - suppressed entirely in production
|
|
938
|
+
if (key === 'data-sid' && !debug_attributes_enabled()) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
844
941
|
// Data attributes - go into DOM
|
|
845
942
|
const attrValue = typeof value === "string" ? value.trim() : value;
|
|
846
943
|
element.attr(key, attrValue);
|
|
@@ -983,6 +1080,14 @@ async function initialize_component(element, compData) {
|
|
|
983
1080
|
if (ComponentClass.name !== name) {
|
|
984
1081
|
options._component_name = name;
|
|
985
1082
|
}
|
|
1083
|
+
// Pass lifecycle truncation flags via options (not DOM attributes, since _ prefix is filtered).
|
|
1084
|
+
// Accept both the plain key (cascaded/programmatic) and the '$'-prefixed key (template-authored).
|
|
1085
|
+
if (props._load_only === true || props['$_load_only'] === true) {
|
|
1086
|
+
options._load_only = true;
|
|
1087
|
+
}
|
|
1088
|
+
if (props._load_render_only === true || props['$_load_render_only'] === true) {
|
|
1089
|
+
options._load_render_only = true;
|
|
1090
|
+
}
|
|
986
1091
|
// Create component instance - element first, then options
|
|
987
1092
|
const instance = new ComponentClass(element, options);
|
|
988
1093
|
// Set the instantiator (the component that rendered this one in their template)
|
|
@@ -1195,6 +1300,146 @@ function handleComponentError(component, phase, error) {
|
|
|
1195
1300
|
// 9. Performance Budgets - Warn when components exceed size/time limits
|
|
1196
1301
|
// 10. Accessibility Auditing - Check for missing ARIA attributes
|
|
1197
1302
|
|
|
1303
|
+
/**
|
|
1304
|
+
* JQHTML Cache Key Serializer
|
|
1305
|
+
*
|
|
1306
|
+
* Produces a deterministic string for a plain-data argument value, so that two
|
|
1307
|
+
* structurally equal objects built on separate renders yield the SAME cache key.
|
|
1308
|
+
* Templates rebuild `{contact_id: 12}` on every render, so identity-based keying
|
|
1309
|
+
* can never match and permanently opts the receiving component out of caching.
|
|
1310
|
+
*
|
|
1311
|
+
* ---------------------------------------------------------------------------
|
|
1312
|
+
* THIS IS NOT `process_for_serialization()` IN local-storage.ts. DO NOT MERGE THEM.
|
|
1313
|
+
*
|
|
1314
|
+
* That serializer stores cache VALUES and deliberately degrades: a function, a
|
|
1315
|
+
* symbol or a circular reference is dropped and serialization continues, because
|
|
1316
|
+
* losing a method while keeping the data is the right trade for a payload.
|
|
1317
|
+
*
|
|
1318
|
+
* For a cache KEY that policy manufactures false hits. Two argument objects that
|
|
1319
|
+
* differ ONLY by a callback would serialize identically, and a component would
|
|
1320
|
+
* render with another component's cached content - strictly worse than not
|
|
1321
|
+
* caching at all. So this serializer DECLINES on the first value it cannot
|
|
1322
|
+
* express exactly. It never drops anything.
|
|
1323
|
+
* ---------------------------------------------------------------------------
|
|
1324
|
+
*
|
|
1325
|
+
* Allow-list, and nothing else: null, undefined, boolean, number, string, Date,
|
|
1326
|
+
* arrays, and plain objects (prototype Object.prototype or null), in any nesting.
|
|
1327
|
+
* Everything else declines with a reason that reaches `data-nocache`.
|
|
1328
|
+
*/
|
|
1329
|
+
/**
|
|
1330
|
+
* Values larger than this are declined rather than keyed. The supported case is a
|
|
1331
|
+
* handful of key/value pairs or a list of ids - not a payload. Declining loudly
|
|
1332
|
+
* beats spending the budget on a key nobody benefits from.
|
|
1333
|
+
*/
|
|
1334
|
+
const MAX_KEY_BYTES = 500;
|
|
1335
|
+
class Decline extends Error {
|
|
1336
|
+
constructor(reason) {
|
|
1337
|
+
super(reason);
|
|
1338
|
+
this.reason = reason;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function push(state, text) {
|
|
1342
|
+
state.size += text.length;
|
|
1343
|
+
if (state.size > MAX_KEY_BYTES) {
|
|
1344
|
+
throw new Decline('too-large');
|
|
1345
|
+
}
|
|
1346
|
+
return text;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Length-prefix strings so that no combination of separators can make two
|
|
1350
|
+
* different structures produce the same text.
|
|
1351
|
+
*/
|
|
1352
|
+
function encode_string(state, prefix, value) {
|
|
1353
|
+
return push(state, `${prefix}${value.length}:${value}`);
|
|
1354
|
+
}
|
|
1355
|
+
function walk(value, state) {
|
|
1356
|
+
// --- primitives, each tagged so shapes cannot collide ---
|
|
1357
|
+
if (value === null)
|
|
1358
|
+
return push(state, 'n');
|
|
1359
|
+
if (value === undefined)
|
|
1360
|
+
return push(state, 'u');
|
|
1361
|
+
const type = typeof value;
|
|
1362
|
+
if (type === 'boolean')
|
|
1363
|
+
return push(state, value ? 'b1' : 'b0');
|
|
1364
|
+
if (type === 'number')
|
|
1365
|
+
return push(state, `d${String(value)}`);
|
|
1366
|
+
if (type === 'string')
|
|
1367
|
+
return encode_string(state, 's', value);
|
|
1368
|
+
if (type === 'function')
|
|
1369
|
+
throw new Decline('function');
|
|
1370
|
+
if (type === 'symbol')
|
|
1371
|
+
throw new Decline('symbol');
|
|
1372
|
+
if (type === 'bigint')
|
|
1373
|
+
throw new Decline('bigint');
|
|
1374
|
+
if (type !== 'object')
|
|
1375
|
+
throw new Decline(type);
|
|
1376
|
+
// --- objects: reject the impostors before treating anything as data ---
|
|
1377
|
+
if (typeof value.nodeType === 'number')
|
|
1378
|
+
throw new Decline('dom-node');
|
|
1379
|
+
if (value.jquery)
|
|
1380
|
+
throw new Decline('jquery');
|
|
1381
|
+
if (state.seen.has(value))
|
|
1382
|
+
throw new Decline('circular');
|
|
1383
|
+
state.seen.add(value);
|
|
1384
|
+
try {
|
|
1385
|
+
if (value instanceof Date) {
|
|
1386
|
+
const time = value.getTime();
|
|
1387
|
+
if (Number.isNaN(time))
|
|
1388
|
+
throw new Decline('invalid-date');
|
|
1389
|
+
return push(state, `t${value.toISOString()}`);
|
|
1390
|
+
}
|
|
1391
|
+
if (Array.isArray(value)) {
|
|
1392
|
+
// Order is meaningful - never sorted.
|
|
1393
|
+
const parts = [];
|
|
1394
|
+
push(state, 'a[');
|
|
1395
|
+
for (let i = 0; i < value.length; i++) {
|
|
1396
|
+
parts.push(walk(value[i], state));
|
|
1397
|
+
}
|
|
1398
|
+
push(state, ']');
|
|
1399
|
+
return `a[${parts.join(',')}]`;
|
|
1400
|
+
}
|
|
1401
|
+
// Plain object ONLY. A class instance, Map, Set, RegExp, Error, etc. is
|
|
1402
|
+
// declined: two different classes with identical fields would otherwise
|
|
1403
|
+
// serialize the same and collide.
|
|
1404
|
+
const proto = Object.getPrototypeOf(value);
|
|
1405
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
1406
|
+
throw new Decline('non-plain-object');
|
|
1407
|
+
}
|
|
1408
|
+
// Recursive key sort - the existing top-level sort is not enough for nesting.
|
|
1409
|
+
const keys = Object.keys(value).sort();
|
|
1410
|
+
const parts = [];
|
|
1411
|
+
push(state, 'o{');
|
|
1412
|
+
for (const key of keys) {
|
|
1413
|
+
const encoded_key = encode_string(state, 'k', key);
|
|
1414
|
+
parts.push(`${encoded_key}=${walk(value[key], state)}`);
|
|
1415
|
+
}
|
|
1416
|
+
push(state, '}');
|
|
1417
|
+
return `o{${parts.join(',')}}`;
|
|
1418
|
+
}
|
|
1419
|
+
finally {
|
|
1420
|
+
state.seen.delete(value);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Serialize a value for use in a cache key.
|
|
1425
|
+
*
|
|
1426
|
+
* @returns `{ok: true, text}` when the value is exactly expressible, or
|
|
1427
|
+
* `{ok: false, reason}` naming why it is not. Never throws.
|
|
1428
|
+
*/
|
|
1429
|
+
function serialize_for_cache_key(value) {
|
|
1430
|
+
const state = { seen: new WeakSet(), size: 0 };
|
|
1431
|
+
try {
|
|
1432
|
+
return { ok: true, text: walk(value, state) };
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
if (error instanceof Decline) {
|
|
1436
|
+
return { ok: false, reason: error.reason };
|
|
1437
|
+
}
|
|
1438
|
+
// A throwing getter or exotic proxy trap - decline rather than guess.
|
|
1439
|
+
return { ok: false, reason: 'unserializable' };
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1198
1443
|
/**
|
|
1199
1444
|
* Load Coordinator - Request deduplication for component on_load() calls
|
|
1200
1445
|
*
|
|
@@ -1231,8 +1476,9 @@ class Load_Coordinator {
|
|
|
1231
1476
|
* - key: Cache key string, or null if uncacheable
|
|
1232
1477
|
* - uncacheable_property: Name of first property that prevented caching (for debugging)
|
|
1233
1478
|
*/
|
|
1234
|
-
static generate_invocation_key(component_name, args) {
|
|
1479
|
+
static generate_invocation_key(component_name, args, options = {}) {
|
|
1235
1480
|
let uncacheable_property;
|
|
1481
|
+
let uncacheable_reason;
|
|
1236
1482
|
// Filter out internal properties (starting with _) and serialize args
|
|
1237
1483
|
const serializable_args = {};
|
|
1238
1484
|
for (const key of Object.keys(args).sort()) {
|
|
@@ -1240,7 +1486,8 @@ class Load_Coordinator {
|
|
|
1240
1486
|
continue; // Skip internal properties
|
|
1241
1487
|
}
|
|
1242
1488
|
// Skip framework properties that shouldn't affect cache identity
|
|
1243
|
-
|
|
1489
|
+
// Note: _load_only and _load_render_only are already filtered by the _ prefix check above
|
|
1490
|
+
if (key === 'use_cached_data') {
|
|
1244
1491
|
continue;
|
|
1245
1492
|
}
|
|
1246
1493
|
const value = args[key];
|
|
@@ -1270,21 +1517,37 @@ class Load_Coordinator {
|
|
|
1270
1517
|
// Method threw error - treat as uncacheable
|
|
1271
1518
|
if (!uncacheable_property) {
|
|
1272
1519
|
uncacheable_property = key;
|
|
1520
|
+
uncacheable_reason = 'cache-id-threw';
|
|
1273
1521
|
}
|
|
1274
|
-
return { key: null, uncacheable_property };
|
|
1522
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
// No author-supplied id. The cache may fall back to keying by
|
|
1526
|
+
// deterministic content; deduplication deliberately may not.
|
|
1527
|
+
if (options.allow_content_serialization) {
|
|
1528
|
+
const serialized = serialize_for_cache_key(value);
|
|
1529
|
+
if (serialized.ok) {
|
|
1530
|
+
serializable_args[key] = `__JQHTML_CONTENT__:${serialized.text}`;
|
|
1531
|
+
continue;
|
|
1275
1532
|
}
|
|
1533
|
+
if (!uncacheable_property) {
|
|
1534
|
+
uncacheable_property = key;
|
|
1535
|
+
uncacheable_reason = serialized.reason;
|
|
1536
|
+
}
|
|
1537
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1276
1538
|
}
|
|
1277
|
-
// No cache ID available - this property is uncacheable
|
|
1278
1539
|
if (!uncacheable_property) {
|
|
1279
1540
|
uncacheable_property = key;
|
|
1541
|
+
uncacheable_reason = value_type === 'function' ? 'function' : 'object';
|
|
1280
1542
|
}
|
|
1281
|
-
return { key: null, uncacheable_property };
|
|
1543
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1282
1544
|
}
|
|
1283
1545
|
// Unknown type (symbol, bigint, etc.) - uncacheable
|
|
1284
1546
|
if (!uncacheable_property) {
|
|
1285
1547
|
uncacheable_property = key;
|
|
1548
|
+
uncacheable_reason = value_type;
|
|
1286
1549
|
}
|
|
1287
|
-
return { key: null, uncacheable_property };
|
|
1550
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1288
1551
|
}
|
|
1289
1552
|
// Try to serialize - if it fails (shouldn't happen now, but safety net), return null
|
|
1290
1553
|
try {
|
|
@@ -1302,6 +1565,12 @@ class Load_Coordinator {
|
|
|
1302
1565
|
*/
|
|
1303
1566
|
static should_execute_on_load(component) {
|
|
1304
1567
|
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1568
|
+
// No identity means no coordination. Without this, every un-keyable component
|
|
1569
|
+
// would share the single `null` registry entry and adopt an unrelated
|
|
1570
|
+
// component's data - the worst failure this system can produce.
|
|
1571
|
+
if (key === null) {
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1305
1574
|
const entry = this._registry.get(key);
|
|
1306
1575
|
if (!entry) {
|
|
1307
1576
|
// No existing request - this component becomes the leader
|
|
@@ -1324,6 +1593,10 @@ class Load_Coordinator {
|
|
|
1324
1593
|
*/
|
|
1325
1594
|
static register_leader(component, on_load_promise) {
|
|
1326
1595
|
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1596
|
+
// Un-keyable component: run alone, register nothing. See should_execute_on_load().
|
|
1597
|
+
if (key === null) {
|
|
1598
|
+
return () => { };
|
|
1599
|
+
}
|
|
1327
1600
|
// Create a promise that we control - it resolves when complete_coordination is called
|
|
1328
1601
|
let resolve_promise;
|
|
1329
1602
|
const coordination_promise = new Promise((resolve) => {
|
|
@@ -1545,6 +1818,8 @@ Load_Coordinator._registry = new Map();
|
|
|
1545
1818
|
// Registry mapping class names to constructors
|
|
1546
1819
|
const class_registry = Object.create(null);
|
|
1547
1820
|
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
1821
|
+
// Replaced at build time by rollup (see rollup.config.js).
|
|
1822
|
+
const CORE_VERSION = '2.3.54';
|
|
1548
1823
|
const CLASS_MARKER = '__jqhtml_class__';
|
|
1549
1824
|
const PROPS_MARKER = '__jqhtml_props__';
|
|
1550
1825
|
/**
|
|
@@ -1618,7 +1893,13 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1618
1893
|
// Return undefined to omit this value (not fail entire serialization)
|
|
1619
1894
|
return undefined;
|
|
1620
1895
|
}
|
|
1621
|
-
// Handle circular references
|
|
1896
|
+
// Handle circular references.
|
|
1897
|
+
// `seen` tracks only the current ancestor path (the DFS stack): an object is
|
|
1898
|
+
// added on the way down and removed on the way back up (see finally below).
|
|
1899
|
+
// This flags a genuine cycle (a back-edge to an ancestor) while allowing a
|
|
1900
|
+
// shared reference in a DAG to be serialized independently on each visit.
|
|
1901
|
+
// JSON round-trip semantics do not preserve identity anyway, so emitting a
|
|
1902
|
+
// copy per reference is correct; silently omitting the property is not.
|
|
1622
1903
|
if (seen.has(value)) {
|
|
1623
1904
|
if (verbose) {
|
|
1624
1905
|
console.warn('[JQHTML Cache] Circular reference detected - cannot serialize');
|
|
@@ -1626,69 +1907,89 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1626
1907
|
return undefined; // Fail entire serialization
|
|
1627
1908
|
}
|
|
1628
1909
|
seen.add(value);
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1910
|
+
try {
|
|
1911
|
+
// Handle arrays
|
|
1912
|
+
if (Array.isArray(value)) {
|
|
1913
|
+
const result = [];
|
|
1914
|
+
for (let i = 0; i < value.length; i++) {
|
|
1915
|
+
const item = value[i];
|
|
1916
|
+
const processed = process_for_serialization(item, verbose, seen);
|
|
1917
|
+
// For arrays, we keep undefined as null to preserve indices
|
|
1918
|
+
result.push(processed === undefined ? null : processed);
|
|
1919
|
+
}
|
|
1920
|
+
return result;
|
|
1637
1921
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
[PROPS_MARKER]: value.toISOString()
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
// Handle Map objects
|
|
1648
|
-
if (value instanceof Map) {
|
|
1649
|
-
const entries = [];
|
|
1650
|
-
for (const [k, v] of value) {
|
|
1651
|
-
const processedKey = process_for_serialization(k, verbose, seen);
|
|
1652
|
-
const processedValue = process_for_serialization(v, verbose, seen);
|
|
1653
|
-
entries.push([processedKey, processedValue]);
|
|
1654
|
-
}
|
|
1655
|
-
return {
|
|
1656
|
-
[CLASS_MARKER]: 'Map',
|
|
1657
|
-
[PROPS_MARKER]: entries
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
// Handle Set objects
|
|
1661
|
-
if (value instanceof Set) {
|
|
1662
|
-
const items = [];
|
|
1663
|
-
for (const item of value) {
|
|
1664
|
-
items.push(process_for_serialization(item, verbose, seen));
|
|
1922
|
+
// Handle Date objects specially
|
|
1923
|
+
if (value instanceof Date) {
|
|
1924
|
+
return {
|
|
1925
|
+
[CLASS_MARKER]: 'Date',
|
|
1926
|
+
[PROPS_MARKER]: value.toISOString()
|
|
1927
|
+
};
|
|
1665
1928
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
[
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1929
|
+
// Handle Map objects
|
|
1930
|
+
if (value instanceof Map) {
|
|
1931
|
+
const entries = [];
|
|
1932
|
+
for (const [k, v] of value) {
|
|
1933
|
+
const processedKey = process_for_serialization(k, verbose, seen);
|
|
1934
|
+
const processedValue = process_for_serialization(v, verbose, seen);
|
|
1935
|
+
entries.push([processedKey, processedValue]);
|
|
1936
|
+
}
|
|
1937
|
+
return {
|
|
1938
|
+
[CLASS_MARKER]: 'Map',
|
|
1939
|
+
[PROPS_MARKER]: entries
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
// Handle Set objects
|
|
1943
|
+
if (value instanceof Set) {
|
|
1944
|
+
const items = [];
|
|
1945
|
+
for (const item of value) {
|
|
1946
|
+
items.push(process_for_serialization(item, verbose, seen));
|
|
1683
1947
|
}
|
|
1948
|
+
return {
|
|
1949
|
+
[CLASS_MARKER]: 'Set',
|
|
1950
|
+
[PROPS_MARKER]: items
|
|
1951
|
+
};
|
|
1684
1952
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1953
|
+
// Get the constructor
|
|
1954
|
+
const ctor = value.constructor;
|
|
1955
|
+
// Handle registered class instances
|
|
1956
|
+
if (ctor && ctor.name && class_registry[ctor.name]) {
|
|
1957
|
+
const props = {};
|
|
1958
|
+
// Extract own enumerable properties (bypass any toJSON method)
|
|
1959
|
+
for (const key of Object.keys(value)) {
|
|
1960
|
+
const propValue = value[key];
|
|
1961
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
1962
|
+
// Only include if not undefined (failed serialization)
|
|
1963
|
+
if (processed !== undefined || propValue === undefined) {
|
|
1964
|
+
props[key] = processed;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
[CLASS_MARKER]: ctor.name,
|
|
1969
|
+
[PROPS_MARKER]: props
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
// Handle plain objects (constructor is Object or no constructor)
|
|
1973
|
+
if (ctor === Object || ctor === undefined || ctor === null) {
|
|
1974
|
+
const result = {};
|
|
1975
|
+
for (const key of Object.keys(value)) {
|
|
1976
|
+
const propValue = value[key];
|
|
1977
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
1978
|
+
// Only include if not undefined (unless original was undefined)
|
|
1979
|
+
if (processed !== undefined || propValue === undefined) {
|
|
1980
|
+
result[key] = processed;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return result;
|
|
1984
|
+
}
|
|
1985
|
+
// Unregistered class instance - convert to plain object for hot/cold parity
|
|
1986
|
+
// This ensures developers catch missing class registrations during development
|
|
1987
|
+
// (methods will be lost, only properties preserved)
|
|
1988
|
+
if (verbose) {
|
|
1989
|
+
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
|
|
1990
|
+
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
|
|
1991
|
+
}
|
|
1992
|
+
// Convert to plain object - properties are preserved, prototype methods are lost
|
|
1692
1993
|
const result = {};
|
|
1693
1994
|
for (const key of Object.keys(value)) {
|
|
1694
1995
|
const propValue = value[key];
|
|
@@ -1700,24 +2001,10 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1700
2001
|
}
|
|
1701
2002
|
return result;
|
|
1702
2003
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
if (verbose) {
|
|
1707
|
-
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
|
|
1708
|
-
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
|
|
1709
|
-
}
|
|
1710
|
-
// Convert to plain object - properties are preserved, prototype methods are lost
|
|
1711
|
-
const result = {};
|
|
1712
|
-
for (const key of Object.keys(value)) {
|
|
1713
|
-
const propValue = value[key];
|
|
1714
|
-
const processed = process_for_serialization(propValue, verbose, seen);
|
|
1715
|
-
// Only include if not undefined (unless original was undefined)
|
|
1716
|
-
if (processed !== undefined || propValue === undefined) {
|
|
1717
|
-
result[key] = processed;
|
|
1718
|
-
}
|
|
2004
|
+
finally {
|
|
2005
|
+
// Pop this object off the ancestor path now that its subtree is done.
|
|
2006
|
+
seen.delete(value);
|
|
1719
2007
|
}
|
|
1720
|
-
return result;
|
|
1721
2008
|
}
|
|
1722
2009
|
// ============================================================================
|
|
1723
2010
|
// Deserialization (String → Object)
|
|
@@ -1822,6 +2109,12 @@ class Jqhtml_Local_Storage {
|
|
|
1822
2109
|
*/
|
|
1823
2110
|
static set_cache_key(cache_key, cache_mode = 'data') {
|
|
1824
2111
|
this._cache_key = cache_key;
|
|
2112
|
+
// Key SHAPE is a function of the library (see cache-key-serializer.ts), so an
|
|
2113
|
+
// upgrade that changes it must invalidate everything persisted under the old shape.
|
|
2114
|
+
// Version the SCOPE MARKER rather than the key itself: _validate_scope() already
|
|
2115
|
+
// clears every jqhtml key when this value changes, and leaving the per-entry key
|
|
2116
|
+
// format alone keeps stored keys readable and avoids repeating the version in each.
|
|
2117
|
+
this._scope_marker = `${CORE_VERSION}::${cache_key}`;
|
|
1825
2118
|
this._cache_mode = cache_mode;
|
|
1826
2119
|
this._init();
|
|
1827
2120
|
}
|
|
@@ -1891,21 +2184,21 @@ class Jqhtml_Local_Storage {
|
|
|
1891
2184
|
}
|
|
1892
2185
|
try {
|
|
1893
2186
|
const stored_key = localStorage.getItem('_jqhtml_cache_key');
|
|
1894
|
-
// If
|
|
1895
|
-
if (stored_key !== null && stored_key !== this.
|
|
1896
|
-
console.log('[JQHTML Local Storage] Cache
|
|
2187
|
+
// If the scope marker (core version + developer key) changed, clear only JQHTML keys
|
|
2188
|
+
if (stored_key !== null && stored_key !== this._scope_marker) {
|
|
2189
|
+
console.log('[JQHTML Local Storage] Cache scope changed, clearing JQHTML keys:', {
|
|
1897
2190
|
old_key: stored_key,
|
|
1898
|
-
new_key: this.
|
|
2191
|
+
new_key: this._scope_marker,
|
|
1899
2192
|
});
|
|
1900
2193
|
this._clear_jqhtml_keys();
|
|
1901
|
-
localStorage.setItem('_jqhtml_cache_key', this.
|
|
2194
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
1902
2195
|
}
|
|
1903
2196
|
else if (stored_key === null) {
|
|
1904
2197
|
// First time JQHTML is using this storage - just set the key, don't clear
|
|
1905
|
-
console.log('[JQHTML Local Storage] Initializing cache
|
|
1906
|
-
new_key: this.
|
|
2198
|
+
console.log('[JQHTML Local Storage] Initializing cache scope (first use):', {
|
|
2199
|
+
new_key: this._scope_marker,
|
|
1907
2200
|
});
|
|
1908
|
-
localStorage.setItem('_jqhtml_cache_key', this.
|
|
2201
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
1909
2202
|
}
|
|
1910
2203
|
}
|
|
1911
2204
|
catch (e) {
|
|
@@ -2137,10 +2430,964 @@ class Jqhtml_Local_Storage {
|
|
|
2137
2430
|
}
|
|
2138
2431
|
}
|
|
2139
2432
|
Jqhtml_Local_Storage._cache_key = null;
|
|
2433
|
+
/** Versioned scope identity - compared in _validate_scope() to clear on a core upgrade. */
|
|
2434
|
+
Jqhtml_Local_Storage._scope_marker = null;
|
|
2140
2435
|
Jqhtml_Local_Storage._cache_mode = 'data';
|
|
2141
2436
|
Jqhtml_Local_Storage._storage_available = null;
|
|
2142
2437
|
Jqhtml_Local_Storage._initialized = false;
|
|
2143
2438
|
|
|
2439
|
+
/**
|
|
2440
|
+
* JQHTML Component Event System
|
|
2441
|
+
*
|
|
2442
|
+
* Extracted from component.ts - handles lifecycle event registration,
|
|
2443
|
+
* triggering, and state tracking.
|
|
2444
|
+
*
|
|
2445
|
+
* Events have "sticky" semantics for lifecycle events: if an event has already
|
|
2446
|
+
* occurred, new .on() handlers fire immediately with the stored data.
|
|
2447
|
+
*/
|
|
2448
|
+
/**
|
|
2449
|
+
* Register a callback for an event.
|
|
2450
|
+
*
|
|
2451
|
+
* Lifecycle events (create, render, load, loaded, ready) are "sticky":
|
|
2452
|
+
* If a lifecycle event has already occurred, the callback fires immediately
|
|
2453
|
+
* AND registers for future occurrences.
|
|
2454
|
+
*
|
|
2455
|
+
* Custom events only fire when explicitly triggered via trigger().
|
|
2456
|
+
*
|
|
2457
|
+
* @param component - The component instance
|
|
2458
|
+
* @param event_name - Name of the event
|
|
2459
|
+
* @param callback - Callback: (component, data?) => void
|
|
2460
|
+
*/
|
|
2461
|
+
function event_on(component, event_name, callback) {
|
|
2462
|
+
// Initialize callback array for this event if needed
|
|
2463
|
+
if (!component._lifecycle_callbacks.has(event_name)) {
|
|
2464
|
+
component._lifecycle_callbacks.set(event_name, []);
|
|
2465
|
+
}
|
|
2466
|
+
// Add callback to queue
|
|
2467
|
+
component._lifecycle_callbacks.get(event_name).push(callback);
|
|
2468
|
+
// If this lifecycle event has already occurred, fire the callback immediately
|
|
2469
|
+
// with the stored data from when trigger() was called
|
|
2470
|
+
if (component._lifecycle_states.has(event_name)) {
|
|
2471
|
+
try {
|
|
2472
|
+
const stored_data = component._lifecycle_states.get(event_name);
|
|
2473
|
+
callback(component, stored_data);
|
|
2474
|
+
}
|
|
2475
|
+
catch (error) {
|
|
2476
|
+
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return component;
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Register a callback that fires exactly once.
|
|
2483
|
+
*
|
|
2484
|
+
* - If the event already occurred (sticky), fires immediately and does NOT register.
|
|
2485
|
+
* - If the event has not occurred, registers and auto-deregisters after first fire.
|
|
2486
|
+
*
|
|
2487
|
+
* @param component - The component instance
|
|
2488
|
+
* @param event_name - Name of the event
|
|
2489
|
+
* @param callback - Callback: (component, data?) => void
|
|
2490
|
+
*/
|
|
2491
|
+
function event_once(component, event_name, callback) {
|
|
2492
|
+
// If event already occurred, fire immediately and we're done
|
|
2493
|
+
if (component._lifecycle_states.has(event_name)) {
|
|
2494
|
+
try {
|
|
2495
|
+
const stored_data = component._lifecycle_states.get(event_name);
|
|
2496
|
+
callback(component, stored_data);
|
|
2497
|
+
}
|
|
2498
|
+
catch (error) {
|
|
2499
|
+
console.error(`[JQHTML] Error in ${event_name} once callback:`, error);
|
|
2500
|
+
}
|
|
2501
|
+
return component;
|
|
2502
|
+
}
|
|
2503
|
+
// Wrap callback to auto-deregister after first fire
|
|
2504
|
+
const wrapper = (comp, data) => {
|
|
2505
|
+
// Remove ourselves from the callback list
|
|
2506
|
+
const callbacks = component._lifecycle_callbacks.get(event_name);
|
|
2507
|
+
if (callbacks) {
|
|
2508
|
+
const idx = callbacks.indexOf(wrapper);
|
|
2509
|
+
if (idx !== -1)
|
|
2510
|
+
callbacks.splice(idx, 1);
|
|
2511
|
+
}
|
|
2512
|
+
callback(comp, data);
|
|
2513
|
+
};
|
|
2514
|
+
// Initialize callback array for this event if needed
|
|
2515
|
+
if (!component._lifecycle_callbacks.has(event_name)) {
|
|
2516
|
+
component._lifecycle_callbacks.set(event_name, []);
|
|
2517
|
+
}
|
|
2518
|
+
component._lifecycle_callbacks.get(event_name).push(wrapper);
|
|
2519
|
+
return component;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Trigger an event - fires all registered callbacks.
|
|
2523
|
+
* Marks event as occurred so future .on() calls fire immediately.
|
|
2524
|
+
*
|
|
2525
|
+
* @param component - The component instance
|
|
2526
|
+
* @param event_name - Name of the event to trigger
|
|
2527
|
+
* @param data - Optional data to pass to callbacks as second parameter
|
|
2528
|
+
*/
|
|
2529
|
+
function event_trigger(component, event_name, data) {
|
|
2530
|
+
// Mark this event as occurred and store the data for late subscribers
|
|
2531
|
+
component._lifecycle_states.set(event_name, data);
|
|
2532
|
+
// Fire all registered callbacks for this event.
|
|
2533
|
+
// Iterate a SNAPSHOT: handlers registered via once() splice themselves out of the
|
|
2534
|
+
// live array while it is being iterated, which would shift later handlers into
|
|
2535
|
+
// already-visited slots and skip them. The snapshot also means a handler
|
|
2536
|
+
// registered during dispatch does not fire in this dispatch (event_on's sticky
|
|
2537
|
+
// immediate-fire already delivered it).
|
|
2538
|
+
const callbacks = component._lifecycle_callbacks.get(event_name);
|
|
2539
|
+
if (callbacks) {
|
|
2540
|
+
for (const callback of [...callbacks]) {
|
|
2541
|
+
try {
|
|
2542
|
+
callback.bind(component)(component, data);
|
|
2543
|
+
}
|
|
2544
|
+
catch (error) {
|
|
2545
|
+
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Check if any callbacks are registered for a given event.
|
|
2552
|
+
*
|
|
2553
|
+
* @param component - The component instance
|
|
2554
|
+
* @param event_name - Name of the event to check
|
|
2555
|
+
*/
|
|
2556
|
+
function event_on_registered(component, event_name) {
|
|
2557
|
+
const callbacks = component._lifecycle_callbacks.get(event_name);
|
|
2558
|
+
return !!(callbacks && callbacks.length > 0);
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Invalidate a lifecycle event - removes the "already occurred" marker.
|
|
2562
|
+
*
|
|
2563
|
+
* After invalidate() is called:
|
|
2564
|
+
* - New .on() handlers will NOT fire immediately
|
|
2565
|
+
* - The ready() promise will NOT resolve immediately
|
|
2566
|
+
* - Handlers wait for the next trigger() call
|
|
2567
|
+
*
|
|
2568
|
+
* Existing registered callbacks are NOT removed - they'll fire on next trigger().
|
|
2569
|
+
*
|
|
2570
|
+
* Use case: Call invalidate('ready') at the start of reload() or render()
|
|
2571
|
+
* so that any new .on('ready') handlers wait for the reload/render to complete.
|
|
2572
|
+
*
|
|
2573
|
+
* @param component - The component instance
|
|
2574
|
+
* @param event_name - Name of the event to invalidate
|
|
2575
|
+
*/
|
|
2576
|
+
function event_invalidate(component, event_name) {
|
|
2577
|
+
component._lifecycle_states.delete(event_name);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/**
|
|
2581
|
+
* JQHTML Data Proxy System
|
|
2582
|
+
*
|
|
2583
|
+
* Extracted from component.ts - handles:
|
|
2584
|
+
* - Data freeze/unfreeze enforcement via Proxy
|
|
2585
|
+
* - Detached on_load() execution with restricted access
|
|
2586
|
+
*/
|
|
2587
|
+
/**
|
|
2588
|
+
* Set up the `this.data` property on a component using Object.defineProperty
|
|
2589
|
+
* with a Proxy that enforces freeze/unfreeze semantics.
|
|
2590
|
+
*
|
|
2591
|
+
* After setup:
|
|
2592
|
+
* - `this.data` reads/writes go through a Proxy
|
|
2593
|
+
* - When `component.__data_frozen === true`, writes throw errors
|
|
2594
|
+
* - When frozen is false, writes pass through normally
|
|
2595
|
+
*
|
|
2596
|
+
* @param component - The component instance to set up data on
|
|
2597
|
+
*/
|
|
2598
|
+
function setup_data_property(component) {
|
|
2599
|
+
let _data = {};
|
|
2600
|
+
// Helper to create frozen proxy for data object
|
|
2601
|
+
const create_proxy = (obj) => {
|
|
2602
|
+
return new Proxy(obj, {
|
|
2603
|
+
set: (target, prop, value) => {
|
|
2604
|
+
if (component.__data_frozen) {
|
|
2605
|
+
console.error(`[JQHTML] ERROR: Component "${component.component_name()}" attempted to modify this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
2606
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2607
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2608
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2609
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
2610
|
+
`FIX: Modify this.data in on_create() (for defaults) or on_load() (for fetched data):\n` +
|
|
2611
|
+
` ❌ In on_ready(): this.data.${String(prop)} = ${JSON.stringify(value)};\n` +
|
|
2612
|
+
` ✅ In on_create(): this.data.${String(prop)} = ${JSON.stringify(value)}; // Set default\n` +
|
|
2613
|
+
` ✅ In on_load(): this.data.${String(prop)} = ${JSON.stringify(value)}; // Fetch from API\n` +
|
|
2614
|
+
` ✅ For component state: this.args.${String(prop)} = ${JSON.stringify(value)}; (accessible in on_load)`);
|
|
2615
|
+
throw new Error(`[JQHTML] Cannot modify this.data.${String(prop)} outside of on_create() or on_load(). ` +
|
|
2616
|
+
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
2617
|
+
}
|
|
2618
|
+
target[prop] = value;
|
|
2619
|
+
return true;
|
|
2620
|
+
},
|
|
2621
|
+
deleteProperty: (target, prop) => {
|
|
2622
|
+
if (component.__data_frozen) {
|
|
2623
|
+
console.error(`[JQHTML] ERROR: Component "${component.component_name()}" attempted to delete this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
2624
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2625
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2626
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2627
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.`);
|
|
2628
|
+
throw new Error(`[JQHTML] Cannot delete this.data.${String(prop)} outside of on_create() or on_load(). ` +
|
|
2629
|
+
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
2630
|
+
}
|
|
2631
|
+
delete target[prop];
|
|
2632
|
+
return true;
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
};
|
|
2636
|
+
// Create initial proxied data object
|
|
2637
|
+
_data = create_proxy({});
|
|
2638
|
+
Object.defineProperty(component, 'data', {
|
|
2639
|
+
get: () => _data,
|
|
2640
|
+
set: (value) => {
|
|
2641
|
+
if (component.__data_frozen) {
|
|
2642
|
+
console.error(`[JQHTML] ERROR: Component "${component.component_name()}" attempted to reassign this.data outside of on_create() or on_load().\n\n` +
|
|
2643
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2644
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2645
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2646
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
2647
|
+
`FIX: Modify this.data in on_create() (for defaults) or on_load() (for fetched data):\n` +
|
|
2648
|
+
` ❌ In on_ready(): this.data = {...};\n` +
|
|
2649
|
+
` ✅ In on_create(): this.data.count = 0; // Set default\n` +
|
|
2650
|
+
` ✅ In on_load(): this.data = await fetch(...); // Fetch from API\n` +
|
|
2651
|
+
` ✅ For component state: this.args.count = 5; (accessible in on_load)`);
|
|
2652
|
+
throw new Error(`[JQHTML] Cannot reassign this.data outside of on_create() or on_load(). ` +
|
|
2653
|
+
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
2654
|
+
}
|
|
2655
|
+
// When setting, wrap the new value in a proxy too
|
|
2656
|
+
_data = create_proxy(value);
|
|
2657
|
+
},
|
|
2658
|
+
enumerable: true,
|
|
2659
|
+
configurable: false
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Execute on_load() in a detached context with restricted access.
|
|
2664
|
+
*
|
|
2665
|
+
* Creates a sandboxed environment where on_load() can only access:
|
|
2666
|
+
* - this.args (read-only)
|
|
2667
|
+
* - this.data (read/write, cloned from __initial_data_snapshot)
|
|
2668
|
+
*
|
|
2669
|
+
* All other property access (this.$, this.$sid, etc.) throws errors.
|
|
2670
|
+
*
|
|
2671
|
+
* @param component - The component instance
|
|
2672
|
+
* @param use_load_coordinator - Whether to use Load_Coordinator for deduplication
|
|
2673
|
+
* @returns The resulting data and optional coordination completion function
|
|
2674
|
+
*/
|
|
2675
|
+
async function execute_on_load_detached(component, use_load_coordinator = false) {
|
|
2676
|
+
// Clone this.data from the snapshot captured after on_create()
|
|
2677
|
+
const data_clone = component.__initial_data_snapshot
|
|
2678
|
+
? JSON.parse(JSON.stringify(component.__initial_data_snapshot))
|
|
2679
|
+
: {};
|
|
2680
|
+
// Create a read-only proxy for args that blocks all modifications
|
|
2681
|
+
// Can't use JSON clone because args may contain functions (_slots, callbacks)
|
|
2682
|
+
const component_name = component.component_name();
|
|
2683
|
+
const create_readonly_proxy = (obj, path = 'this.args') => {
|
|
2684
|
+
if (obj === null || typeof obj !== 'object')
|
|
2685
|
+
return obj;
|
|
2686
|
+
return new Proxy(obj, {
|
|
2687
|
+
get(target, prop) {
|
|
2688
|
+
const value = target[prop];
|
|
2689
|
+
// Recursively wrap nested objects (but not functions)
|
|
2690
|
+
if (value !== null && typeof value === 'object' && typeof value !== 'function') {
|
|
2691
|
+
return create_readonly_proxy(value, `${path}.${String(prop)}`);
|
|
2692
|
+
}
|
|
2693
|
+
return value;
|
|
2694
|
+
},
|
|
2695
|
+
set(_target, prop, value) {
|
|
2696
|
+
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to modify ${path}.${String(prop)} during on_load().\n\n` +
|
|
2697
|
+
`RESTRICTION: this.args is READ-ONLY during on_load().\n\n` +
|
|
2698
|
+
`WHY: this.args configures what data to fetch. Modifying it during on_load() creates circular dependencies.\n\n` +
|
|
2699
|
+
`FIX: Modify this.args BEFORE on_load() runs (in on_create() or before calling reload()):\n` +
|
|
2700
|
+
` ❌ Inside on_load(): ${path}.${String(prop)} = ${JSON.stringify(value)};\n` +
|
|
2701
|
+
` ✅ In on_create(): this.args.${String(prop)} = defaultValue;`);
|
|
2702
|
+
throw new Error(`[JQHTML] Cannot modify ${path}.${String(prop)} during on_load(). ` +
|
|
2703
|
+
`this.args is read-only in on_load().`);
|
|
2704
|
+
},
|
|
2705
|
+
deleteProperty(_target, prop) {
|
|
2706
|
+
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to delete ${path}.${String(prop)} during on_load().\n\n` +
|
|
2707
|
+
`RESTRICTION: this.args is READ-ONLY during on_load().`);
|
|
2708
|
+
throw new Error(`[JQHTML] Cannot delete ${path}.${String(prop)} during on_load(). ` +
|
|
2709
|
+
`this.args is read-only in on_load().`);
|
|
2710
|
+
}
|
|
2711
|
+
});
|
|
2712
|
+
};
|
|
2713
|
+
// Create a detached context object that on_load will operate on
|
|
2714
|
+
const detached_context = {
|
|
2715
|
+
args: create_readonly_proxy(component.args), // Read-only proxy wrapping real args
|
|
2716
|
+
data: data_clone // Cloned data - modifications stay isolated
|
|
2717
|
+
};
|
|
2718
|
+
// Create restricted proxy that operates on the detached context
|
|
2719
|
+
const restricted_this = new Proxy(detached_context, {
|
|
2720
|
+
get(target, prop) {
|
|
2721
|
+
// Only allow access to args and data
|
|
2722
|
+
if (prop === 'args') {
|
|
2723
|
+
return target.args;
|
|
2724
|
+
}
|
|
2725
|
+
if (prop === 'data') {
|
|
2726
|
+
return target.data;
|
|
2727
|
+
}
|
|
2728
|
+
// Block everything else
|
|
2729
|
+
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to access this.${String(prop)} during on_load().\n\n` +
|
|
2730
|
+
`RESTRICTION: on_load() may ONLY access:\n` +
|
|
2731
|
+
` - this.args (read-only)\n` +
|
|
2732
|
+
` - this.data (read/write)\n\n` +
|
|
2733
|
+
`WHY: on_load() is for data fetching only. All other component functionality should happen in other lifecycle methods.\n\n` +
|
|
2734
|
+
`FIX:\n` +
|
|
2735
|
+
` - DOM manipulation → use on_render() or on_ready()\n` +
|
|
2736
|
+
` - Component methods → call them before/after on_load(), not inside it\n` +
|
|
2737
|
+
` - Other properties → restructure code to only use this.args and this.data in on_load()`);
|
|
2738
|
+
throw new Error(`[JQHTML] Cannot access this.${String(prop)} during on_load(). ` +
|
|
2739
|
+
`on_load() may only access this.args and this.data.`);
|
|
2740
|
+
},
|
|
2741
|
+
set(target, prop, value) {
|
|
2742
|
+
// Only allow setting data
|
|
2743
|
+
if (prop === 'data') {
|
|
2744
|
+
target.data = value;
|
|
2745
|
+
return true;
|
|
2746
|
+
}
|
|
2747
|
+
// Block setting args
|
|
2748
|
+
if (prop === 'args') {
|
|
2749
|
+
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to modify this.args during on_load().\n\n` +
|
|
2750
|
+
`RESTRICTION: on_load() may ONLY modify:\n` +
|
|
2751
|
+
` - this.data (read/write)\n\n` +
|
|
2752
|
+
`WHY: this.args is component state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.\n\n` +
|
|
2753
|
+
`FIX: Modify this.args BEFORE calling on_load() (in on_create() or other lifecycle methods), not inside on_load():\n` +
|
|
2754
|
+
` ❌ Inside on_load(): this.args.filter = 'new_value';\n` +
|
|
2755
|
+
` ✅ In on_create(): this.args.filter = this.args.initial_filter || 'default';`);
|
|
2756
|
+
throw new Error(`[JQHTML] Cannot modify this.args during on_load(). ` +
|
|
2757
|
+
`Modify this.args in other lifecycle methods, not inside on_load().`);
|
|
2758
|
+
}
|
|
2759
|
+
// Block setting any other properties
|
|
2760
|
+
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to modify this.${String(prop)} during on_load().\n\n` +
|
|
2761
|
+
`RESTRICTION: on_load() may ONLY modify:\n` +
|
|
2762
|
+
` - this.data (read/write)\n\n` +
|
|
2763
|
+
`WHY: on_load() is for data fetching only. Setting properties on the component instance should happen in other lifecycle methods.\n\n` +
|
|
2764
|
+
`FIX: Store your data in this.data instead:\n` +
|
|
2765
|
+
` ❌ this.${String(prop)} = value;\n` +
|
|
2766
|
+
` ✅ this.data.${String(prop)} = value;`);
|
|
2767
|
+
throw new Error(`[JQHTML] Cannot modify this.${String(prop)} during on_load(). ` +
|
|
2768
|
+
`Only this.data can be modified in on_load().`);
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
// Create promise for this on_load() call
|
|
2772
|
+
const on_load_promise = (async () => {
|
|
2773
|
+
try {
|
|
2774
|
+
await component._call_lifecycle('on_load', restricted_this);
|
|
2775
|
+
}
|
|
2776
|
+
catch (error) {
|
|
2777
|
+
if (use_load_coordinator) {
|
|
2778
|
+
// Handle error and notify coordinator
|
|
2779
|
+
Load_Coordinator.handle_leader_error(component, error);
|
|
2780
|
+
}
|
|
2781
|
+
throw error;
|
|
2782
|
+
}
|
|
2783
|
+
})();
|
|
2784
|
+
// If using Load_Coordinator, register as leader
|
|
2785
|
+
// Note: We store the completion function but DON'T call it here
|
|
2786
|
+
// It will be called by _load() after _apply_load_result() updates this.data
|
|
2787
|
+
let complete_coordination = null;
|
|
2788
|
+
if (use_load_coordinator) {
|
|
2789
|
+
complete_coordination = Load_Coordinator.register_leader(component, on_load_promise);
|
|
2790
|
+
}
|
|
2791
|
+
await on_load_promise;
|
|
2792
|
+
// Note: We don't validate args changes here because external code (like parent
|
|
2793
|
+
// components calling reload()) is allowed to modify component.args during on_load().
|
|
2794
|
+
// The read-only proxy only prevents code INSIDE on_load() from modifying args.
|
|
2795
|
+
// Return the data from the detached context AND the completion function
|
|
2796
|
+
return {
|
|
2797
|
+
data: detached_context.data,
|
|
2798
|
+
complete_coordination
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
/**
|
|
2803
|
+
* JQHTML Component Cache Integration
|
|
2804
|
+
*
|
|
2805
|
+
* Extracted from component.ts - handles cache key generation,
|
|
2806
|
+
* cache reads during create(), cache checks during reload(),
|
|
2807
|
+
* and HTML cache snapshots during ready().
|
|
2808
|
+
*
|
|
2809
|
+
* Uses Jqhtml_Local_Storage as the underlying storage layer.
|
|
2810
|
+
*/
|
|
2811
|
+
/**
|
|
2812
|
+
* Generate a cache key for a component using its name + args.
|
|
2813
|
+
* Supports custom cache_id() override.
|
|
2814
|
+
*
|
|
2815
|
+
* @param component - The component instance
|
|
2816
|
+
* @returns Cache key string or null if caching is disabled
|
|
2817
|
+
*/
|
|
2818
|
+
function generate_cache_key(component) {
|
|
2819
|
+
if (typeof component.cache_id === 'function') {
|
|
2820
|
+
try {
|
|
2821
|
+
const custom_cache_id = component.cache_id();
|
|
2822
|
+
return { cache_key: `${component.component_name()}::${String(custom_cache_id)}` };
|
|
2823
|
+
}
|
|
2824
|
+
catch (error) {
|
|
2825
|
+
return { cache_key: null, uncacheable_property: 'cache_id()', uncacheable_reason: 'cache-id-threw' };
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
// Standard args-based key. The CACHE opts into content serialization for plain-data
|
|
2829
|
+
// object args; deduplication does not - see InvocationKeyOptions for why.
|
|
2830
|
+
const result = Load_Coordinator.generate_invocation_key(component.component_name(), component.args, { allow_content_serialization: true });
|
|
2831
|
+
return {
|
|
2832
|
+
cache_key: result.key,
|
|
2833
|
+
uncacheable_property: result.uncacheable_property,
|
|
2834
|
+
uncacheable_reason: result.uncacheable_reason,
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
// One warning per component+property. A list of 50 rows must not print 50 warnings.
|
|
2838
|
+
const _warned_uncacheable = new Set();
|
|
2839
|
+
/**
|
|
2840
|
+
* Development warning: this component fetches data but cannot be cached, because
|
|
2841
|
+
* its args do not reduce to a stable key and it defines no cache_id() to say what
|
|
2842
|
+
* its identity is. Nothing is broken - the component works - but it silently opts
|
|
2843
|
+
* out of cache reuse and of load deduplication.
|
|
2844
|
+
*
|
|
2845
|
+
* Controlled by the `warn_uncacheable_args` integration flag (see config.ts).
|
|
2846
|
+
*/
|
|
2847
|
+
function warn_uncacheable_component(component, uncacheable_property, uncacheable_reason) {
|
|
2848
|
+
if (!warn_uncacheable_args_enabled())
|
|
2849
|
+
return;
|
|
2850
|
+
let name = 'unknown';
|
|
2851
|
+
try {
|
|
2852
|
+
name = component.component_name();
|
|
2853
|
+
}
|
|
2854
|
+
catch { /* name unavailable */ }
|
|
2855
|
+
const dedupe_key = `${name}::${uncacheable_property || '?'}::${uncacheable_reason || '?'}`;
|
|
2856
|
+
if (_warned_uncacheable.has(dedupe_key))
|
|
2857
|
+
return;
|
|
2858
|
+
_warned_uncacheable.add(dedupe_key);
|
|
2859
|
+
// cache_id() exists but threw - a different problem with a different fix
|
|
2860
|
+
if (uncacheable_property === 'cache_id()' || uncacheable_reason === 'cache-id-threw') {
|
|
2861
|
+
console.warn(`[JQHTML] <${name}> defines cache_id() but it threw, so this component is not cached ` +
|
|
2862
|
+
`and its loads are not deduplicated. Fix cache_id() so it returns a stable string.`, { component: name, cid: component._cid });
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const REASONS = {
|
|
2866
|
+
'function': 'it contains a function - a callback is real identity that content cannot express',
|
|
2867
|
+
'symbol': 'it contains a symbol',
|
|
2868
|
+
'bigint': 'it contains a bigint',
|
|
2869
|
+
'dom-node': 'it contains a DOM node',
|
|
2870
|
+
'jquery': 'it contains a jQuery object',
|
|
2871
|
+
'circular': 'it contains a circular reference',
|
|
2872
|
+
'non-plain-object': 'it contains a class instance, Map, Set or similar - only plain objects and arrays can be keyed by content',
|
|
2873
|
+
'too-large': 'it serializes to more than 500 bytes - args are meant to identify data, not carry it',
|
|
2874
|
+
'invalid-date': 'it contains an invalid Date',
|
|
2875
|
+
};
|
|
2876
|
+
const why = REASONS[uncacheable_reason || ''] || 'it cannot be serialized deterministically';
|
|
2877
|
+
console.warn(`[JQHTML] <${name}> cannot be cached: arg $${uncacheable_property} is not keyable because ` +
|
|
2878
|
+
`${why}.\n` +
|
|
2879
|
+
` The component works, but it will not be restored from cache when reused.\n` +
|
|
2880
|
+
` Fix: define cache_id() on the component to state its identity explicitly, e.g.\n` +
|
|
2881
|
+
` cache_id() { return \`${name.toLowerCase()}_\${this.args.some_id}\`; }\n` +
|
|
2882
|
+
` Or pass plain data (objects/arrays of primitives are keyed automatically) instead of ` +
|
|
2883
|
+
`$${uncacheable_property}.\n` +
|
|
2884
|
+
` Find it in DevTools with the selector ` +
|
|
2885
|
+
`[data-nocache="${uncacheable_property}:${uncacheable_reason || ''}"].`, {
|
|
2886
|
+
component: name,
|
|
2887
|
+
cid: component._cid,
|
|
2888
|
+
uncacheable_arg: uncacheable_property,
|
|
2889
|
+
reason: uncacheable_reason,
|
|
2890
|
+
all_args: component.args ? Object.keys(component.args) : [],
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Read cache during create() phase.
|
|
2895
|
+
* Sets component._cache_key, _cached_html, _use_cached_data_hit as side effects.
|
|
2896
|
+
*
|
|
2897
|
+
* @param component - The component instance
|
|
2898
|
+
*/
|
|
2899
|
+
function read_cache_in_create(component) {
|
|
2900
|
+
const { cache_key, uncacheable_property, uncacheable_reason } = generate_cache_key(component);
|
|
2901
|
+
// If cache_key is null, caching disabled
|
|
2902
|
+
if (cache_key === null) {
|
|
2903
|
+
// Set data-nocache attribute for debugging
|
|
2904
|
+
if (uncacheable_property) {
|
|
2905
|
+
// "<arg>:<reason>" - a bare property name left authors reverse-engineering why
|
|
2906
|
+
component.$.attr('data-nocache', uncacheable_reason ? `${uncacheable_property}:${uncacheable_reason}` : uncacheable_property);
|
|
2907
|
+
}
|
|
2908
|
+
warn_uncacheable_component(component, uncacheable_property, uncacheable_reason);
|
|
2909
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2910
|
+
console.log(`[Cache] Component ${component._cid} (${component.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
|
|
2911
|
+
}
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
// Store cache key for later use
|
|
2915
|
+
component._cache_key = cache_key;
|
|
2916
|
+
// Get cache mode
|
|
2917
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
2918
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2919
|
+
console.log(`[Cache ${cache_mode}] Component ${component._cid} (${component.component_name()}) checking cache in create()`, { cache_key, cache_mode, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
|
|
2920
|
+
}
|
|
2921
|
+
if (cache_mode === 'html') {
|
|
2922
|
+
// HTML cache mode - check for cached HTML to inject on first render
|
|
2923
|
+
const html_cache_key = `${cache_key}::html`;
|
|
2924
|
+
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
|
|
2925
|
+
if (cached_html !== null && typeof cached_html === 'string') {
|
|
2926
|
+
component._cached_html = cached_html;
|
|
2927
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2928
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
else {
|
|
2932
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2933
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) cache miss`, { cache_key: html_cache_key });
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
// Warn if use_cached_data is set in html cache mode
|
|
2937
|
+
if (component.args.use_cached_data === true) {
|
|
2938
|
+
console.warn(`[JQHTML] Component "${component.component_name()}" has use_cached_data=true but cache mode is 'html'.\n` +
|
|
2939
|
+
`use_cached_data only applies to 'data' cache mode. In 'html' mode, the entire rendered HTML is cached.\n` +
|
|
2940
|
+
`The use_cached_data flag will be ignored.`);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
else {
|
|
2944
|
+
// Data cache mode (default) - check for cached data to hydrate this.data
|
|
2945
|
+
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
2946
|
+
if (cached_data !== null && typeof cached_data === 'object') {
|
|
2947
|
+
// Hydrate this.data with cached data
|
|
2948
|
+
component.data = cached_data;
|
|
2949
|
+
if (component.args.use_cached_data === true) {
|
|
2950
|
+
component._use_cached_data_hit = true;
|
|
2951
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2952
|
+
console.log(`[Cache data] Component ${component._cid} (${component.component_name()}) using cached data (use_cached_data=true, skipping on_load)`, { cache_key, data: cached_data });
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
else {
|
|
2956
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2957
|
+
console.log(`[Cache data] Component ${component._cid} (${component.component_name()}) hydrated from cache`, { cache_key, data: cached_data });
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
else {
|
|
2962
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2963
|
+
console.log(`[Cache data] Component ${component._cid} (${component.component_name()}) cache miss`, { cache_key });
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Check cache during reload() when args have changed.
|
|
2970
|
+
* If cache hit, hydrates data/html and triggers an immediate render.
|
|
2971
|
+
*
|
|
2972
|
+
* @param component - The component instance
|
|
2973
|
+
* @returns true if rendered from cache, false otherwise
|
|
2974
|
+
*/
|
|
2975
|
+
function check_cache_on_reload(component) {
|
|
2976
|
+
const { cache_key } = generate_cache_key(component);
|
|
2977
|
+
if (cache_key === null) {
|
|
2978
|
+
return false;
|
|
2979
|
+
}
|
|
2980
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
2981
|
+
component._cache_key = cache_key;
|
|
2982
|
+
if (cache_mode === 'html') {
|
|
2983
|
+
// HTML cache mode - check for cached HTML
|
|
2984
|
+
const html_cache_key = `${cache_key}::html`;
|
|
2985
|
+
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
|
|
2986
|
+
if (cached_html !== null && typeof cached_html === 'string') {
|
|
2987
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2988
|
+
console.log(`[Cache html] reload() - Component ${component._cid} (${component.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
2989
|
+
}
|
|
2990
|
+
component._cached_html = cached_html;
|
|
2991
|
+
// Call _render() directly (not render()) since _reload() manages the full lifecycle
|
|
2992
|
+
// Using render() would trigger a separate queue entry and interfere with _reload()'s flow
|
|
2993
|
+
component._render();
|
|
2994
|
+
return true;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
else {
|
|
2998
|
+
// Data cache mode (default) - check for cached data
|
|
2999
|
+
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
3000
|
+
if (cached_data !== null && typeof cached_data === 'object' && JSON.stringify(cached_data) !== '{}') {
|
|
3001
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3002
|
+
console.log(`[Cache data] reload() - Component ${component._cid} (${component.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
|
|
3003
|
+
}
|
|
3004
|
+
// Hydrate this.data with cached data
|
|
3005
|
+
component.__data_frozen = false;
|
|
3006
|
+
component.data = cached_data;
|
|
3007
|
+
component.__data_frozen = true;
|
|
3008
|
+
// Call _render() directly (not render()) since _reload() manages the full lifecycle
|
|
3009
|
+
component._render();
|
|
3010
|
+
return true;
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
return false;
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Write HTML cache snapshot during ready() phase.
|
|
3017
|
+
* Only caches if the component is dynamic (data changed during on_load).
|
|
3018
|
+
*
|
|
3019
|
+
* @param component - The component instance
|
|
3020
|
+
*/
|
|
3021
|
+
function write_html_cache_snapshot(component) {
|
|
3022
|
+
if (!component._should_cache_html_after_ready || !component._cache_key) {
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
if (component._is_dynamic) {
|
|
3026
|
+
component._should_cache_html_after_ready = false;
|
|
3027
|
+
const html = component.$.html();
|
|
3028
|
+
const html_cache_key = `${component._cache_key}::html`;
|
|
3029
|
+
Jqhtml_Local_Storage.set(html_cache_key, html);
|
|
3030
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3031
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
else {
|
|
3035
|
+
component._should_cache_html_after_ready = false;
|
|
3036
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3037
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) is static (no data change) - skipping HTML cache`);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Write data/HTML to cache after on_load() completes.
|
|
3043
|
+
* Called from _apply_load_result().
|
|
3044
|
+
*
|
|
3045
|
+
* @param component - The component instance
|
|
3046
|
+
* @param data_changed - Whether this.data changed during on_load
|
|
3047
|
+
*/
|
|
3048
|
+
function write_cache_on_loaded(component, data_changed) {
|
|
3049
|
+
if (!data_changed || !component._cache_key) {
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
3053
|
+
if (cache_mode === 'html') {
|
|
3054
|
+
// HTML cache mode - flag to cache HTML after children ready in _ready()
|
|
3055
|
+
component._should_cache_html_after_ready = true;
|
|
3056
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3057
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) will cache HTML after ready`, { cache_key: component._cache_key });
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
else {
|
|
3061
|
+
// Data cache mode (default) - write this.data to cache
|
|
3062
|
+
Jqhtml_Local_Storage.set(component._cache_key, component.data);
|
|
3063
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3064
|
+
console.log(`[Cache data] Component ${component._cid} (${component.component_name()}) cached data after on_load()`, { cache_key: component._cache_key, data: component.data });
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
/**
|
|
3070
|
+
* JQHTML Component Lifecycle Queue
|
|
3071
|
+
*
|
|
3072
|
+
* Serializes lifecycle operations (render, reload, refresh) per component.
|
|
3073
|
+
* Replaces _create_debounced_function with a proper queue that:
|
|
3074
|
+
*
|
|
3075
|
+
* - At most one operation runs at a time per component
|
|
3076
|
+
* - At most one operation is pending (waiting for the current to finish)
|
|
3077
|
+
* - Same-type pending operations collapse (fan-in: all callers share one execution)
|
|
3078
|
+
* - Different-type pending operations: new replaces old (old callers get the new result)
|
|
3079
|
+
*
|
|
3080
|
+
* Boot bypasses this queue entirely - it runs the lifecycle directly.
|
|
3081
|
+
*/
|
|
3082
|
+
class Component_Queue {
|
|
3083
|
+
constructor() {
|
|
3084
|
+
/** Currently executing operation */
|
|
3085
|
+
this._current = null;
|
|
3086
|
+
/** Next operation waiting to execute after current completes */
|
|
3087
|
+
this._pending = null;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Enqueue a lifecycle operation.
|
|
3091
|
+
*
|
|
3092
|
+
* Behavior:
|
|
3093
|
+
* - Nothing running → execute immediately
|
|
3094
|
+
* - Something running, no pending → create pending entry
|
|
3095
|
+
* - Something running, same type pending → collapse (share pending promise)
|
|
3096
|
+
* - Something running, different type pending → replace pending (all callers get new result)
|
|
3097
|
+
*
|
|
3098
|
+
* @param type - Operation type ('render', 'reload', 'refresh', 'load')
|
|
3099
|
+
* @param executor - The async function to execute
|
|
3100
|
+
* @returns Promise that resolves when the operation completes
|
|
3101
|
+
*/
|
|
3102
|
+
enqueue(type, executor) {
|
|
3103
|
+
// If nothing running, execute immediately
|
|
3104
|
+
if (!this._current) {
|
|
3105
|
+
return this._execute(type, executor);
|
|
3106
|
+
}
|
|
3107
|
+
// Something is running — add to pending
|
|
3108
|
+
if (this._pending) {
|
|
3109
|
+
if (this._pending.type === type) {
|
|
3110
|
+
// Same type pending → collapse (fan-in)
|
|
3111
|
+
// All callers share the same future execution
|
|
3112
|
+
return new Promise((resolve, reject) => {
|
|
3113
|
+
this._pending.resolvers.push(resolve);
|
|
3114
|
+
this._pending.rejecters.push(reject);
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
else {
|
|
3118
|
+
// Different type pending → replace
|
|
3119
|
+
// Old pending callers join the new operation
|
|
3120
|
+
// (their operation was superseded by a different type)
|
|
3121
|
+
const old_resolvers = this._pending.resolvers;
|
|
3122
|
+
const old_rejecters = this._pending.rejecters;
|
|
3123
|
+
return new Promise((resolve, reject) => {
|
|
3124
|
+
this._pending = {
|
|
3125
|
+
type,
|
|
3126
|
+
executor,
|
|
3127
|
+
resolvers: [...old_resolvers, resolve],
|
|
3128
|
+
rejecters: [...old_rejecters, reject]
|
|
3129
|
+
};
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
// No pending entry yet — create one
|
|
3134
|
+
return new Promise((resolve, reject) => {
|
|
3135
|
+
this._pending = {
|
|
3136
|
+
type,
|
|
3137
|
+
executor,
|
|
3138
|
+
resolvers: [resolve],
|
|
3139
|
+
rejecters: [reject]
|
|
3140
|
+
};
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Execute an operation and handle pending queue after completion.
|
|
3145
|
+
* @private
|
|
3146
|
+
*/
|
|
3147
|
+
_execute(type, executor) {
|
|
3148
|
+
const promise = (async () => {
|
|
3149
|
+
try {
|
|
3150
|
+
await executor();
|
|
3151
|
+
}
|
|
3152
|
+
finally {
|
|
3153
|
+
this._current = null;
|
|
3154
|
+
// Run pending if exists
|
|
3155
|
+
if (this._pending) {
|
|
3156
|
+
const pending = this._pending;
|
|
3157
|
+
this._pending = null;
|
|
3158
|
+
try {
|
|
3159
|
+
await this._execute(pending.type, pending.executor);
|
|
3160
|
+
for (const resolve of pending.resolvers)
|
|
3161
|
+
resolve();
|
|
3162
|
+
}
|
|
3163
|
+
catch (err) {
|
|
3164
|
+
for (const reject of pending.rejecters)
|
|
3165
|
+
reject(err);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
})();
|
|
3170
|
+
this._current = { type, promise };
|
|
3171
|
+
return promise;
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Check if any operation is currently running.
|
|
3175
|
+
*/
|
|
3176
|
+
get is_busy() {
|
|
3177
|
+
return this._current !== null;
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Check if there's a pending operation waiting.
|
|
3181
|
+
*/
|
|
3182
|
+
get has_pending() {
|
|
3183
|
+
return this._pending !== null;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
/**
|
|
3188
|
+
* JQHTML SSR Preload Data System
|
|
3189
|
+
*
|
|
3190
|
+
* Captures component data during SSR rendering and replays it on the client
|
|
3191
|
+
* to skip redundant on_load() calls during hydration.
|
|
3192
|
+
*
|
|
3193
|
+
* Server-side: start_data_capture() → render → get_captured_data()
|
|
3194
|
+
* Client-side: set_preload_data(entries) → components boot with preloaded data → clear_preload_data()
|
|
3195
|
+
*/
|
|
3196
|
+
// --- Capture (Server Side) ---
|
|
3197
|
+
let _capture_enabled = false;
|
|
3198
|
+
let _capture_buffer = [];
|
|
3199
|
+
// Track captured keys to avoid duplicates from Load Coordinator followers
|
|
3200
|
+
let _captured_keys = new Set();
|
|
3201
|
+
/**
|
|
3202
|
+
* Enable data capture mode. After this call, every component that completes
|
|
3203
|
+
* on_load() has its final this.data recorded.
|
|
3204
|
+
*
|
|
3205
|
+
* Idempotent — calling twice is fine.
|
|
3206
|
+
*/
|
|
3207
|
+
function start_data_capture() {
|
|
3208
|
+
_capture_enabled = true;
|
|
3209
|
+
_capture_buffer = [];
|
|
3210
|
+
_captured_keys.clear();
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Record a component's data after on_load() completes.
|
|
3214
|
+
* Called internally from _apply_load_result().
|
|
3215
|
+
*
|
|
3216
|
+
* @param component_name - Component name
|
|
3217
|
+
* @param args - Component args at time of on_load()
|
|
3218
|
+
* @param data - this.data after on_load() completed
|
|
3219
|
+
* @param cache_key - The cache key for deduplication (null = always capture)
|
|
3220
|
+
*/
|
|
3221
|
+
function capture_component_data(component_name, args, data, cache_key) {
|
|
3222
|
+
if (!_capture_enabled)
|
|
3223
|
+
return;
|
|
3224
|
+
// Deduplicate: if Load Coordinator already captured this key, skip
|
|
3225
|
+
if (cache_key !== null) {
|
|
3226
|
+
if (_captured_keys.has(cache_key))
|
|
3227
|
+
return;
|
|
3228
|
+
_captured_keys.add(cache_key);
|
|
3229
|
+
}
|
|
3230
|
+
// Deep clone args and data to plain objects (no proxies, no class instances)
|
|
3231
|
+
try {
|
|
3232
|
+
const cloned_args = JSON.parse(JSON.stringify(args));
|
|
3233
|
+
const cloned_data = JSON.parse(JSON.stringify(data));
|
|
3234
|
+
_capture_buffer.push({
|
|
3235
|
+
component: component_name,
|
|
3236
|
+
args: cloned_args,
|
|
3237
|
+
data: cloned_data,
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
catch (error) {
|
|
3241
|
+
// Non-serializable data — skip capture for this component
|
|
3242
|
+
if (typeof console !== 'undefined') {
|
|
3243
|
+
console.warn(`[JQHTML SSR Capture] Failed to serialize data for ${component_name}:`, error);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Returns all captured component data and resets the capture buffer.
|
|
3249
|
+
* One-shot: calling clears the buffer.
|
|
3250
|
+
*/
|
|
3251
|
+
function get_captured_data() {
|
|
3252
|
+
const result = _capture_buffer;
|
|
3253
|
+
_capture_buffer = [];
|
|
3254
|
+
_captured_keys.clear();
|
|
3255
|
+
return result;
|
|
3256
|
+
}
|
|
3257
|
+
/**
|
|
3258
|
+
* Check if data capture is currently enabled.
|
|
3259
|
+
*/
|
|
3260
|
+
function is_capture_enabled() {
|
|
3261
|
+
return _capture_enabled;
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Stop data capture and clear all state.
|
|
3265
|
+
*/
|
|
3266
|
+
function stop_data_capture() {
|
|
3267
|
+
_capture_enabled = false;
|
|
3268
|
+
_capture_buffer = [];
|
|
3269
|
+
_captured_keys.clear();
|
|
3270
|
+
}
|
|
3271
|
+
// --- Preload (Client Side) ---
|
|
3272
|
+
let _preload_cache = new Map();
|
|
3273
|
+
/**
|
|
3274
|
+
* Seed the preload cache with SSR-captured data.
|
|
3275
|
+
* Must be called before any component _load() runs.
|
|
3276
|
+
*
|
|
3277
|
+
* @param entries - Array of { component, args, data } entries from get_captured_data()
|
|
3278
|
+
*/
|
|
3279
|
+
function set_preload_data(entries) {
|
|
3280
|
+
if (!entries || entries.length === 0)
|
|
3281
|
+
return;
|
|
3282
|
+
_preload_cache.clear();
|
|
3283
|
+
for (const entry of entries) {
|
|
3284
|
+
// Generate key using the same algorithm as Load Coordinator
|
|
3285
|
+
const result = Load_Coordinator.generate_invocation_key(entry.component, entry.args);
|
|
3286
|
+
if (result.key !== null) {
|
|
3287
|
+
_preload_cache.set(result.key, entry.data);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* Check if preloaded data exists for a given cache key.
|
|
3293
|
+
* If found, returns the data and removes the entry (one-shot).
|
|
3294
|
+
*
|
|
3295
|
+
* @param cache_key - The cache key (same format as Load Coordinator)
|
|
3296
|
+
* @returns The preloaded data, or null if no match
|
|
3297
|
+
*/
|
|
3298
|
+
function consume_preload_data(cache_key) {
|
|
3299
|
+
if (_preload_cache.size === 0)
|
|
3300
|
+
return null;
|
|
3301
|
+
const data = _preload_cache.get(cache_key);
|
|
3302
|
+
if (data !== undefined) {
|
|
3303
|
+
_preload_cache.delete(cache_key);
|
|
3304
|
+
return data;
|
|
3305
|
+
}
|
|
3306
|
+
return null;
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Clear any remaining unconsumed preload entries.
|
|
3310
|
+
* Call after initial page hydration is complete.
|
|
3311
|
+
*/
|
|
3312
|
+
function clear_preload_data() {
|
|
3313
|
+
_preload_cache.clear();
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Check if any preload data is available (for fast early exit in _load).
|
|
3317
|
+
*/
|
|
3318
|
+
function has_preload_data() {
|
|
3319
|
+
return _preload_cache.size > 0;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
/**
|
|
3323
|
+
* JQHTML Viewport Resize Dispatch
|
|
3324
|
+
*
|
|
3325
|
+
* One framework-owned, debounced window 'resize' listener fans out to every live
|
|
3326
|
+
* component in the document by calling on_viewport_resize(viewport_width).
|
|
3327
|
+
*
|
|
3328
|
+
* Components never bind their own window listener, so there is nothing to unbind
|
|
3329
|
+
* in on_stop() - a component that is stopped or removed from the document simply
|
|
3330
|
+
* stops being found by the dispatcher.
|
|
3331
|
+
*/
|
|
3332
|
+
/** Trailing-edge debounce applied to the window resize listener. */
|
|
3333
|
+
const VIEWPORT_RESIZE_DEBOUNCE_MS = 30;
|
|
3334
|
+
let debounce_timer = null;
|
|
3335
|
+
let listener_installed = false;
|
|
3336
|
+
/**
|
|
3337
|
+
* Current viewport width in CSS pixels.
|
|
3338
|
+
*
|
|
3339
|
+
* window.innerWidth is used (not $(window).width()) because it matches the width
|
|
3340
|
+
* CSS media queries resolve against. It includes the scrollbar gutter.
|
|
3341
|
+
*
|
|
3342
|
+
* Returns 0 outside a browser (SSR).
|
|
3343
|
+
*/
|
|
3344
|
+
function get_viewport_width() {
|
|
3345
|
+
return typeof window !== 'undefined' ? window.innerWidth : 0;
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Call on_viewport_resize(width) on every component currently in the document.
|
|
3349
|
+
*
|
|
3350
|
+
* Walks $('.Component') in document order (parents before children). Elements
|
|
3351
|
+
* with no component instance and stopped components are skipped. A component
|
|
3352
|
+
* that throws is logged and the walk continues - one bad handler must not stop
|
|
3353
|
+
* the rest of the page from resizing.
|
|
3354
|
+
*/
|
|
3355
|
+
function dispatch_viewport_resize(jQuery) {
|
|
3356
|
+
const viewport_width = get_viewport_width();
|
|
3357
|
+
jQuery('.Component').each(function () {
|
|
3358
|
+
const component = jQuery(this).data('_component');
|
|
3359
|
+
if (!component || component._stopped)
|
|
3360
|
+
return;
|
|
3361
|
+
if (typeof component.on_viewport_resize !== 'function')
|
|
3362
|
+
return;
|
|
3363
|
+
try {
|
|
3364
|
+
component.on_viewport_resize(viewport_width);
|
|
3365
|
+
}
|
|
3366
|
+
catch (error) {
|
|
3367
|
+
console.error(`[JQHTML] Error in on_viewport_resize() for ${component.component_name?.() || 'component'}:`, error);
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Install the single window resize listener. Idempotent - repeated init() calls
|
|
3373
|
+
* and non-browser environments are no-ops.
|
|
3374
|
+
*/
|
|
3375
|
+
function init_viewport_resize(jQuery) {
|
|
3376
|
+
if (typeof window === 'undefined')
|
|
3377
|
+
return;
|
|
3378
|
+
if (listener_installed)
|
|
3379
|
+
return;
|
|
3380
|
+
listener_installed = true;
|
|
3381
|
+
jQuery(window).on('resize.jqhtml_viewport', function () {
|
|
3382
|
+
if (debounce_timer !== null)
|
|
3383
|
+
clearTimeout(debounce_timer);
|
|
3384
|
+
debounce_timer = setTimeout(function () {
|
|
3385
|
+
debounce_timer = null;
|
|
3386
|
+
dispatch_viewport_resize(jQuery);
|
|
3387
|
+
}, VIEWPORT_RESIZE_DEBOUNCE_MS);
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
|
|
2144
3391
|
/**
|
|
2145
3392
|
* JQHTML v2 Component Base Class
|
|
2146
3393
|
*
|
|
@@ -2171,6 +3418,7 @@ class Jqhtml_Component {
|
|
|
2171
3418
|
this._data_on_last_render = null; // Data snapshot from last render (for refresh() comparison)
|
|
2172
3419
|
this.__initial_data_snapshot = null; // Snapshot of this.data after on_create() for restoration before on_load()
|
|
2173
3420
|
this.__data_frozen = false; // Track if this.data is currently frozen
|
|
3421
|
+
this._queue = new Component_Queue(); // Lifecycle operation queue (render/reload/refresh)
|
|
2174
3422
|
this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
|
|
2175
3423
|
this.__lifecycle_authorized = false; // Flag for lifecycle method protection
|
|
2176
3424
|
// Cache mode properties
|
|
@@ -2184,12 +3432,17 @@ class Jqhtml_Component {
|
|
|
2184
3432
|
this._on_render_complete = false; // True after on_render() has been called post-on_load
|
|
2185
3433
|
// use_cached_data feature - skip on_load() when cache hit occurs
|
|
2186
3434
|
this._use_cached_data_hit = false; // True if use_cached_data=true AND cache was used
|
|
2187
|
-
//
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
//
|
|
2192
|
-
|
|
3435
|
+
// _load_only: create + load only. No render, no children, no on_render, no on_loaded, no on_ready.
|
|
3436
|
+
this._load_only = false;
|
|
3437
|
+
// _load_render_only: create + render + load + re-render. No on_render, no on_loaded, no on_ready.
|
|
3438
|
+
this._load_render_only = false;
|
|
3439
|
+
// Detached optimization: true when element is not in the DOM at boot time.
|
|
3440
|
+
// Skips initial render and cache read — just on_load then render.
|
|
3441
|
+
// Override with _force_initial_render to keep normal double-render behavior.
|
|
3442
|
+
this._is_detached = false;
|
|
3443
|
+
// _force_initial_render: override detached optimization — render even when not in DOM.
|
|
3444
|
+
// Use when you need the loading spinner visible immediately after appending.
|
|
3445
|
+
this._force_initial_render = false;
|
|
2193
3446
|
// rendered event - fires once after the synchronous render chain completes
|
|
2194
3447
|
// (after on_load's re-render if applicable, or after first render if no on_load)
|
|
2195
3448
|
this._has_rendered = false;
|
|
@@ -2200,6 +3453,11 @@ class Jqhtml_Component {
|
|
|
2200
3453
|
// Set in constructor (prototype chain is established before constructor runs)
|
|
2201
3454
|
// Used to skip expensive cache/load operations for static components
|
|
2202
3455
|
this.__has_custom_on_load = false;
|
|
3456
|
+
// gate_load() — "load gates" registered during on_create() that must settle
|
|
3457
|
+
// before the component's FIRST on_load() runs. See gate_load() / _await_load_gates().
|
|
3458
|
+
this.__load_gates = []; // Pending gates (cleared once the first load begins)
|
|
3459
|
+
this.__first_load_started = false; // Latch: true once the first load phase begins (enforces one-shot + throw-after)
|
|
3460
|
+
this.__gate_resume = null; // Resolver to release an in-progress gate wait early (reload/refresh/stop)
|
|
2203
3461
|
// Detect custom on_load() override immediately
|
|
2204
3462
|
// Prototype chain is set up before constructor runs, so this.on_load
|
|
2205
3463
|
// correctly resolves to child class override if one exists
|
|
@@ -2241,84 +3499,27 @@ class Jqhtml_Component {
|
|
|
2241
3499
|
// Merge in order: defineArgs (defaults from Define tag) < dataAttrs < args (invocation overrides)
|
|
2242
3500
|
const defineArgs = template_for_args?.defineArgs || {};
|
|
2243
3501
|
this.args = { ...defineArgs, ...dataAttrs, ...args };
|
|
2244
|
-
// Set lifecycle
|
|
2245
|
-
if (this.args.
|
|
2246
|
-
this.
|
|
3502
|
+
// Set lifecycle truncation flags from args
|
|
3503
|
+
if (this.args._load_only === true) {
|
|
3504
|
+
this._load_only = true;
|
|
2247
3505
|
}
|
|
2248
|
-
if (this.args.
|
|
2249
|
-
this.
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
this
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
this.
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
if (this.__data_frozen) {
|
|
2266
|
-
console.error(`[JQHTML] ERROR: Component "${this.component_name()}" attempted to modify this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
2267
|
-
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2268
|
-
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2269
|
-
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2270
|
-
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
2271
|
-
`FIX: Modify this.data in on_create() (for defaults) or on_load() (for fetched data):\n` +
|
|
2272
|
-
` ❌ In on_ready(): this.data.${String(prop)} = ${JSON.stringify(value)};\n` +
|
|
2273
|
-
` ✅ In on_create(): this.data.${String(prop)} = ${JSON.stringify(value)}; // Set default\n` +
|
|
2274
|
-
` ✅ In on_load(): this.data.${String(prop)} = ${JSON.stringify(value)}; // Fetch from API\n` +
|
|
2275
|
-
` ✅ For component state: this.args.${String(prop)} = ${JSON.stringify(value)}; (accessible in on_load)`);
|
|
2276
|
-
throw new Error(`[JQHTML] Cannot modify this.data.${String(prop)} outside of on_create() or on_load(). ` +
|
|
2277
|
-
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
2278
|
-
}
|
|
2279
|
-
target[prop] = value;
|
|
2280
|
-
return true;
|
|
2281
|
-
},
|
|
2282
|
-
deleteProperty: (target, prop) => {
|
|
2283
|
-
if (this.__data_frozen) {
|
|
2284
|
-
console.error(`[JQHTML] ERROR: Component "${this.component_name()}" attempted to delete this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
2285
|
-
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2286
|
-
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2287
|
-
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2288
|
-
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.`);
|
|
2289
|
-
throw new Error(`[JQHTML] Cannot delete this.data.${String(prop)} outside of on_create() or on_load(). ` +
|
|
2290
|
-
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
2291
|
-
}
|
|
2292
|
-
delete target[prop];
|
|
2293
|
-
return true;
|
|
2294
|
-
}
|
|
2295
|
-
});
|
|
2296
|
-
};
|
|
2297
|
-
// Create initial proxied data object
|
|
2298
|
-
_data = createDataProxy({});
|
|
2299
|
-
Object.defineProperty(this, 'data', {
|
|
2300
|
-
get: () => _data,
|
|
2301
|
-
set: (value) => {
|
|
2302
|
-
if (this.__data_frozen) {
|
|
2303
|
-
console.error(`[JQHTML] ERROR: Component "${this.component_name()}" attempted to reassign this.data outside of on_create() or on_load().\n\n` +
|
|
2304
|
-
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2305
|
-
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2306
|
-
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2307
|
-
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
2308
|
-
`FIX: Modify this.data in on_create() (for defaults) or on_load() (for fetched data):\n` +
|
|
2309
|
-
` ❌ In on_ready(): this.data = {...};\n` +
|
|
2310
|
-
` ✅ In on_create(): this.data.count = 0; // Set default\n` +
|
|
2311
|
-
` ✅ In on_load(): this.data = await fetch(...); // Fetch from API\n` +
|
|
2312
|
-
` ✅ For component state: this.args.count = 5; (accessible in on_load)`);
|
|
2313
|
-
throw new Error(`[JQHTML] Cannot reassign this.data outside of on_create() or on_load(). ` +
|
|
2314
|
-
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
2315
|
-
}
|
|
2316
|
-
// When setting, wrap the new value in a proxy too
|
|
2317
|
-
_data = createDataProxy(value);
|
|
2318
|
-
},
|
|
2319
|
-
enumerable: true,
|
|
2320
|
-
configurable: false
|
|
2321
|
-
});
|
|
3506
|
+
if (this.args._load_render_only === true) {
|
|
3507
|
+
this._load_render_only = true;
|
|
3508
|
+
}
|
|
3509
|
+
if (this.args._force_initial_render === true) {
|
|
3510
|
+
this._force_initial_render = true;
|
|
3511
|
+
}
|
|
3512
|
+
// Attach component to element
|
|
3513
|
+
this.$.data('_component', this);
|
|
3514
|
+
// Apply CSS classes and attributes
|
|
3515
|
+
this._apply_css_classes();
|
|
3516
|
+
this._apply_default_attributes(); // Apply defaultAttributes from template
|
|
3517
|
+
this._set_attributes();
|
|
3518
|
+
// Find DOM parent component (for lifecycle coordination)
|
|
3519
|
+
this._find_dom_parent();
|
|
3520
|
+
// Setup data property with freeze enforcement using Proxy
|
|
3521
|
+
// @see data-proxy.ts for full implementation
|
|
3522
|
+
setup_data_property(this);
|
|
2322
3523
|
// Initialize this.state as an empty object (convention for arbitrary component state)
|
|
2323
3524
|
// No special meaning to framework - mutable anywhere, not cached, not frozen
|
|
2324
3525
|
this.state = {};
|
|
@@ -2381,6 +3582,21 @@ class Jqhtml_Component {
|
|
|
2381
3582
|
// Call original directly - no flag needed since we bypass the wrapper
|
|
2382
3583
|
return impl.call(this);
|
|
2383
3584
|
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Fire on_viewport_resize() with the current viewport width.
|
|
3587
|
+
* Called after every on_render() and on_ready(); the debounced window
|
|
3588
|
+
* listener in viewport.ts drives it from then on. Errors are isolated so a
|
|
3589
|
+
* bad handler cannot break the lifecycle.
|
|
3590
|
+
* @private
|
|
3591
|
+
*/
|
|
3592
|
+
_fire_viewport_resize() {
|
|
3593
|
+
try {
|
|
3594
|
+
this.on_viewport_resize(get_viewport_width());
|
|
3595
|
+
}
|
|
3596
|
+
catch (error) {
|
|
3597
|
+
console.error(`[JQHTML] Error in on_viewport_resize() for ${this.component_name()}:`, error);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
2384
3600
|
/**
|
|
2385
3601
|
* Check if on_load() is overridden in a subclass
|
|
2386
3602
|
* Used to skip the load phase entirely for components that don't fetch data
|
|
@@ -2420,7 +3636,7 @@ class Jqhtml_Component {
|
|
|
2420
3636
|
* @returns The current _render_count after incrementing (used to detect stale renders)
|
|
2421
3637
|
* @private
|
|
2422
3638
|
*/
|
|
2423
|
-
_render(id = null) {
|
|
3639
|
+
_render(id = null, options = {}) {
|
|
2424
3640
|
// Increment render count to track this specific render
|
|
2425
3641
|
this._render_count++;
|
|
2426
3642
|
const current_render_id = this._render_count;
|
|
@@ -2441,7 +3657,7 @@ class Jqhtml_Component {
|
|
|
2441
3657
|
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
|
2442
3658
|
`Add $redrawable attribute or make it a proper component.`);
|
|
2443
3659
|
}
|
|
2444
|
-
return child._render();
|
|
3660
|
+
return child._render(null, options);
|
|
2445
3661
|
}
|
|
2446
3662
|
this._log_lifecycle('render', 'start');
|
|
2447
3663
|
// HTML CACHE MODE - If we have cached HTML, inject it directly and skip template rendering
|
|
@@ -2458,13 +3674,14 @@ class Jqhtml_Component {
|
|
|
2458
3674
|
// Mark first render complete
|
|
2459
3675
|
this._did_first_render = true;
|
|
2460
3676
|
this._log_lifecycle('render', 'complete (cached HTML)');
|
|
2461
|
-
//
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3677
|
+
// Call on_render() after cache inject (unless suppressed by _load_render_only)
|
|
3678
|
+
if (!options.skip_on_render) {
|
|
3679
|
+
const cacheRenderResult = this._call_lifecycle_sync('on_render');
|
|
3680
|
+
if (cacheRenderResult && typeof cacheRenderResult.then === 'function') {
|
|
3681
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
3682
|
+
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
3683
|
+
}
|
|
3684
|
+
this._fire_viewport_resize();
|
|
2468
3685
|
}
|
|
2469
3686
|
// Emit lifecycle event
|
|
2470
3687
|
this.trigger('render');
|
|
@@ -2649,18 +3866,14 @@ class Jqhtml_Component {
|
|
|
2649
3866
|
// Don't update ready state here - let phases complete in order
|
|
2650
3867
|
this._update_debug_attrs();
|
|
2651
3868
|
this._log_lifecycle('render', 'complete');
|
|
2652
|
-
// Call on_render()
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
const renderResult = this._call_lifecycle_sync('on_render');
|
|
2661
|
-
if (renderResult && typeof renderResult.then === 'function') {
|
|
2662
|
-
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
2663
|
-
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
3869
|
+
// Call on_render() after render completes (unless suppressed by _load_render_only)
|
|
3870
|
+
if (!options.skip_on_render) {
|
|
3871
|
+
const renderResult = this._call_lifecycle_sync('on_render');
|
|
3872
|
+
if (renderResult && typeof renderResult.then === 'function') {
|
|
3873
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
3874
|
+
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
3875
|
+
}
|
|
3876
|
+
this._fire_viewport_resize();
|
|
2664
3877
|
}
|
|
2665
3878
|
// Emit lifecycle event
|
|
2666
3879
|
this.trigger('render');
|
|
@@ -2690,18 +3903,18 @@ class Jqhtml_Component {
|
|
|
2690
3903
|
* Public render method - re-renders component and completes lifecycle
|
|
2691
3904
|
* This is what users should call when they want to update a component.
|
|
2692
3905
|
*
|
|
2693
|
-
* Lifecycle sequence:
|
|
3906
|
+
* Lifecycle sequence (serialized via component queue):
|
|
2694
3907
|
* 1. _render() - Updates DOM synchronously, calls on_render(), fires 'render' event
|
|
2695
|
-
* 2.
|
|
2696
|
-
*
|
|
2697
|
-
*
|
|
2698
|
-
* - trigger('ready') - Fires ready event
|
|
3908
|
+
* 2. _wait_for_children_ready() - Waits for all children to reach ready state
|
|
3909
|
+
* 3. on_ready() - Calls user's ready hook
|
|
3910
|
+
* 4. trigger('ready') - Fires ready event
|
|
2699
3911
|
*
|
|
2700
|
-
*
|
|
3912
|
+
* Goes through the component queue to prevent concurrent lifecycle operations.
|
|
3913
|
+
* Returns a Promise that resolves when the full render lifecycle completes.
|
|
2701
3914
|
*/
|
|
2702
3915
|
render(id = null) {
|
|
2703
3916
|
if (this._stopped)
|
|
2704
|
-
return;
|
|
3917
|
+
return Promise.resolve();
|
|
2705
3918
|
// Invalidate ready event so new handlers wait for this render cycle to complete
|
|
2706
3919
|
// This prevents .on('ready') handlers from firing immediately based on previous lifecycle
|
|
2707
3920
|
this.invalidate('ready');
|
|
@@ -2720,10 +3933,12 @@ class Jqhtml_Component {
|
|
|
2720
3933
|
}
|
|
2721
3934
|
return child.render();
|
|
2722
3935
|
}
|
|
2723
|
-
//
|
|
2724
|
-
|
|
2725
|
-
//
|
|
2726
|
-
(async () => {
|
|
3936
|
+
// Enqueue the full render lifecycle through the component queue
|
|
3937
|
+
// Return the queue promise so `await this.render()` resolves when the
|
|
3938
|
+
// full render lifecycle (DOM update, children ready, on_ready) completes
|
|
3939
|
+
return this._queue.enqueue('render', async () => {
|
|
3940
|
+
// Execute render phase synchronously and capture render ID
|
|
3941
|
+
const render_id = this._render();
|
|
2727
3942
|
// Wait for all child components to be ready
|
|
2728
3943
|
await this._wait_for_children_ready();
|
|
2729
3944
|
// Check if this render is still current before calling on_ready
|
|
@@ -2733,9 +3948,10 @@ class Jqhtml_Component {
|
|
|
2733
3948
|
}
|
|
2734
3949
|
// Call on_ready hook with authorization
|
|
2735
3950
|
await this._call_lifecycle('on_ready');
|
|
3951
|
+
this._fire_viewport_resize();
|
|
2736
3952
|
// Trigger ready event
|
|
2737
|
-
|
|
2738
|
-
})
|
|
3953
|
+
this.trigger('ready');
|
|
3954
|
+
});
|
|
2739
3955
|
}
|
|
2740
3956
|
/**
|
|
2741
3957
|
* Alias for render() - re-renders component with current data
|
|
@@ -2744,6 +3960,70 @@ class Jqhtml_Component {
|
|
|
2744
3960
|
redraw(id = null) {
|
|
2745
3961
|
return this.render(id);
|
|
2746
3962
|
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Re-fetch data by executing on_load() without triggering re-render or lifecycle.
|
|
3965
|
+
*
|
|
3966
|
+
* Runs on_load() on a detached proxy (same restrictions as during boot):
|
|
3967
|
+
* - this.data is restored to on_create() snapshot, then unfrozen for writing
|
|
3968
|
+
* - this.args is read-only
|
|
3969
|
+
* - All other properties (this.$, this.$sid, etc.) are blocked
|
|
3970
|
+
*
|
|
3971
|
+
* After on_load() completes, this.data is atomically updated and re-frozen.
|
|
3972
|
+
* The developer decides what to do next (e.g., call this.render()).
|
|
3973
|
+
*
|
|
3974
|
+
* Pattern: load() invokes on_load(), just as render() invokes on_render().
|
|
3975
|
+
*
|
|
3976
|
+
* @returns true if this.data changed, false if unchanged
|
|
3977
|
+
*/
|
|
3978
|
+
async load() {
|
|
3979
|
+
if (this._stopped)
|
|
3980
|
+
return false;
|
|
3981
|
+
// Capture result through queue closure
|
|
3982
|
+
let data_changed = false;
|
|
3983
|
+
await this._queue.enqueue('load', async () => {
|
|
3984
|
+
// Snapshot current data for change detection
|
|
3985
|
+
const data_before = JSON.stringify(this.data);
|
|
3986
|
+
// Execute on_load() on detached proxy (restores data to on_create snapshot)
|
|
3987
|
+
const { data: result_data } = await this._execute_on_load_detached(false);
|
|
3988
|
+
// Atomically update this.data: unfreeze → assign → normalize → refreeze
|
|
3989
|
+
this.__data_frozen = false;
|
|
3990
|
+
this.data = result_data;
|
|
3991
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
3992
|
+
if (cache_mode === 'data') {
|
|
3993
|
+
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
|
|
3994
|
+
this.data = normalized;
|
|
3995
|
+
}
|
|
3996
|
+
this.__data_frozen = true;
|
|
3997
|
+
// Detect if data changed
|
|
3998
|
+
const data_after = JSON.stringify(this.data);
|
|
3999
|
+
data_changed = data_before !== data_after;
|
|
4000
|
+
// Update cache with fresh data if changed
|
|
4001
|
+
if (data_changed) {
|
|
4002
|
+
// Regenerate cache key (args may have changed since boot)
|
|
4003
|
+
let cache_key = null;
|
|
4004
|
+
if (typeof this.cache_id === 'function') {
|
|
4005
|
+
try {
|
|
4006
|
+
cache_key = `${this.component_name()}::${String(this.cache_id())}`;
|
|
4007
|
+
}
|
|
4008
|
+
catch { /* cache_id() threw - skip caching */ }
|
|
4009
|
+
}
|
|
4010
|
+
else {
|
|
4011
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
4012
|
+
cache_key = result.key;
|
|
4013
|
+
}
|
|
4014
|
+
if (cache_key && cache_mode !== 'html') {
|
|
4015
|
+
Jqhtml_Local_Storage.set(cache_key, this.data);
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
// Call on_loaded() on the real component (this.data frozen, full access to this.$, this.state)
|
|
4019
|
+
// Suppressed by _load_only and _load_render_only flags (preloading mode)
|
|
4020
|
+
if (!this._load_only && !this._load_render_only) {
|
|
4021
|
+
await this._call_lifecycle('on_loaded');
|
|
4022
|
+
this.trigger('loaded');
|
|
4023
|
+
}
|
|
4024
|
+
});
|
|
4025
|
+
return data_changed;
|
|
4026
|
+
}
|
|
2747
4027
|
/**
|
|
2748
4028
|
* Create phase - Quick setup, prepare UI
|
|
2749
4029
|
* SYNCHRONOUS - no awaits, no yields
|
|
@@ -2759,97 +4039,17 @@ class Jqhtml_Component {
|
|
|
2759
4039
|
`on_create() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
2760
4040
|
// Don't await - on_create MUST be sync. The warning is enough.
|
|
2761
4041
|
}
|
|
4042
|
+
// Detect detached elements — skip cache and initial render for elements not in DOM
|
|
4043
|
+
// _force_initial_render overrides this optimization
|
|
4044
|
+
this._is_detached = !this.$[0].isConnected && !this._force_initial_render;
|
|
2762
4045
|
// OPTIMIZATION: Skip cache operations and snapshot if no custom on_load()
|
|
2763
4046
|
// Components without on_load() don't fetch data, so nothing to cache or restore
|
|
2764
4047
|
if (this.__has_custom_on_load) {
|
|
2765
4048
|
// CACHE CHECK - Read from cache based on cache mode ('data' or 'html')
|
|
2766
|
-
//
|
|
2767
|
-
//
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
if (typeof this.cache_id === 'function') {
|
|
2771
|
-
try {
|
|
2772
|
-
const custom_cache_id = this.cache_id();
|
|
2773
|
-
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
2774
|
-
}
|
|
2775
|
-
catch (error) {
|
|
2776
|
-
// cache_id() threw error - disable caching
|
|
2777
|
-
uncacheable_property = 'cache_id()';
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
else {
|
|
2781
|
-
// Use standard args-based cache key generation
|
|
2782
|
-
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
2783
|
-
cache_key = result.key;
|
|
2784
|
-
uncacheable_property = result.uncacheable_property;
|
|
2785
|
-
}
|
|
2786
|
-
// If cache_key is null, caching disabled
|
|
2787
|
-
if (cache_key === null) {
|
|
2788
|
-
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
|
2789
|
-
if (uncacheable_property) {
|
|
2790
|
-
this.$.attr('data-nocache', uncacheable_property);
|
|
2791
|
-
}
|
|
2792
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2793
|
-
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
|
|
2794
|
-
}
|
|
2795
|
-
// Don't return - continue to snapshot this.data for on_load restoration
|
|
2796
|
-
}
|
|
2797
|
-
else {
|
|
2798
|
-
// Store cache key for later use
|
|
2799
|
-
this._cache_key = cache_key;
|
|
2800
|
-
// Get cache mode
|
|
2801
|
-
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
2802
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2803
|
-
console.log(`[Cache ${cache_mode}] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, cache_mode, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
|
|
2804
|
-
}
|
|
2805
|
-
if (cache_mode === 'html') {
|
|
2806
|
-
// HTML cache mode - check for cached HTML to inject on first render
|
|
2807
|
-
const html_cache_key = `${cache_key}::html`;
|
|
2808
|
-
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
|
|
2809
|
-
if (cached_html !== null && typeof cached_html === 'string') {
|
|
2810
|
-
// Store cached HTML for injection in _render()
|
|
2811
|
-
this._cached_html = cached_html;
|
|
2812
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2813
|
-
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
else {
|
|
2817
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2818
|
-
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key: html_cache_key });
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
// Warn if use_cached_data is set in html cache mode - it has no effect
|
|
2822
|
-
if (this.args.use_cached_data === true) {
|
|
2823
|
-
console.warn(`[JQHTML] Component "${this.component_name()}" has use_cached_data=true but cache mode is 'html'.\n` +
|
|
2824
|
-
`use_cached_data only applies to 'data' cache mode. In 'html' mode, the entire rendered HTML is cached.\n` +
|
|
2825
|
-
`The use_cached_data flag will be ignored.`);
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2828
|
-
else {
|
|
2829
|
-
// Data cache mode (default) - check for cached data to hydrate this.data
|
|
2830
|
-
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
2831
|
-
if (cached_data !== null && typeof cached_data === 'object') {
|
|
2832
|
-
// Hydrate this.data with cached data
|
|
2833
|
-
this.data = cached_data;
|
|
2834
|
-
// If use_cached_data=true, skip on_load() entirely - use cached data as final data
|
|
2835
|
-
if (this.args.use_cached_data === true) {
|
|
2836
|
-
this._use_cached_data_hit = true;
|
|
2837
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2838
|
-
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) using cached data (use_cached_data=true, skipping on_load)`, { cache_key, data: cached_data });
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
else {
|
|
2842
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2843
|
-
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) hydrated from cache`, { cache_key, data: cached_data });
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
else {
|
|
2848
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
2849
|
-
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key });
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
4049
|
+
// Skip cache for detached elements — no point hydrating from cache when not in DOM
|
|
4050
|
+
// @see component-cache.ts for full implementation
|
|
4051
|
+
if (!this._is_detached) {
|
|
4052
|
+
read_cache_in_create(this);
|
|
2853
4053
|
}
|
|
2854
4054
|
// Snapshot this.data after on_create() completes
|
|
2855
4055
|
// This will be restored before each on_load() execution to reset state
|
|
@@ -2863,6 +4063,90 @@ class Jqhtml_Component {
|
|
|
2863
4063
|
// Emit lifecycle event
|
|
2864
4064
|
this.trigger('create');
|
|
2865
4065
|
}
|
|
4066
|
+
/**
|
|
4067
|
+
* Register a "load gate" — a promise that must settle before this component's
|
|
4068
|
+
* FIRST on_load() runs.
|
|
4069
|
+
*
|
|
4070
|
+
* Register gates during on_create(). Multiple calls accumulate; all gates are
|
|
4071
|
+
* awaited together (via Promise.allSettled) at the seam between create and the
|
|
4072
|
+
* first load. A REJECTED gate does NOT block or abort the load — it is an
|
|
4073
|
+
* ordering hint, not a data dependency, so loading proceeds after settlement
|
|
4074
|
+
* (rejections are logged through the debug channel).
|
|
4075
|
+
*
|
|
4076
|
+
* Gates are ONE-SHOT: they delay the first on_load() only. Once that load
|
|
4077
|
+
* begins the gate list is cleared, and reload()/refresh() never re-await gates.
|
|
4078
|
+
* Calling reload()/refresh() while a gate is still holding the first load
|
|
4079
|
+
* releases the wait immediately and resumes the lifecycle (a later gate
|
|
4080
|
+
* settlement then does nothing).
|
|
4081
|
+
*
|
|
4082
|
+
* Gates delay ONLY the on_load phase — never create(), the initial render,
|
|
4083
|
+
* on_render(), or the cached-content first paint (stale-while-revalidate is
|
|
4084
|
+
* preserved). Components with no custom on_load() ignore gates entirely.
|
|
4085
|
+
*
|
|
4086
|
+
* No-op during SSR (the server cannot await arbitrary client promises).
|
|
4087
|
+
*
|
|
4088
|
+
* jqhtml has no knowledge of WHAT is being awaited — the caller supplies the
|
|
4089
|
+
* promise, and policy such as timeouts is the caller's responsibility (bake it
|
|
4090
|
+
* into the promise you pass).
|
|
4091
|
+
*
|
|
4092
|
+
* @param promise The promise to await before the first on_load().
|
|
4093
|
+
* @throws If called after the component's first load has already started.
|
|
4094
|
+
*/
|
|
4095
|
+
gate_load(promise) {
|
|
4096
|
+
if (this.__first_load_started) {
|
|
4097
|
+
throw new Error(`[JQHTML] gate_load() may only be called before the component's first load - ` +
|
|
4098
|
+
`register gates in on_create(). ` +
|
|
4099
|
+
`Component: ${this.component_name()} (_cid: ${this._cid})`);
|
|
4100
|
+
}
|
|
4101
|
+
this.__load_gates.push(promise);
|
|
4102
|
+
}
|
|
4103
|
+
/**
|
|
4104
|
+
* Await all registered load gates before the first on_load().
|
|
4105
|
+
*
|
|
4106
|
+
* Called exactly once, at the boot seam immediately before the load phase
|
|
4107
|
+
* (only for components with a custom on_load()). Latches "first load started"
|
|
4108
|
+
* so any later gate_load() throws and reload()/refresh() never re-await.
|
|
4109
|
+
*
|
|
4110
|
+
* Returns a promise to await when there are gates to wait on, or `null` when
|
|
4111
|
+
* there is nothing to wait for (no gates, or SSR). Returning `null` keeps the
|
|
4112
|
+
* common no-gate path free of an extra microtask hop, so it does not shift the
|
|
4113
|
+
* timing of ungated components.
|
|
4114
|
+
*
|
|
4115
|
+
* The returned promise resolves as soon as any of the following happens
|
|
4116
|
+
* (whichever is first — the rest become no-ops):
|
|
4117
|
+
* - all gates settle (Promise.allSettled),
|
|
4118
|
+
* - reload()/refresh() is called while gated (they resolve __gate_resume),
|
|
4119
|
+
* - the component is stopped (stop() resolves __gate_resume).
|
|
4120
|
+
*
|
|
4121
|
+
* @private
|
|
4122
|
+
*/
|
|
4123
|
+
_await_load_gates() {
|
|
4124
|
+
// Latch (synchronous): from here on, gate_load() throws and this runs at
|
|
4125
|
+
// most once — set even when there are no gates so point 2 always holds.
|
|
4126
|
+
this.__first_load_started = true;
|
|
4127
|
+
const gates = this.__load_gates;
|
|
4128
|
+
this.__load_gates = [];
|
|
4129
|
+
// Nothing to gate, or SSR (cannot await arbitrary client promises) — proceed now.
|
|
4130
|
+
if (gates.length === 0 || is_capture_enabled())
|
|
4131
|
+
return null;
|
|
4132
|
+
// Promise.allSettled never rejects; rejected gates are logged (debug channel)
|
|
4133
|
+
// whenever they settle, independent of whether the wait was resumed early.
|
|
4134
|
+
const settled = Promise.allSettled(gates);
|
|
4135
|
+
settled.then((results) => {
|
|
4136
|
+
for (const result of results) {
|
|
4137
|
+
if (result.status === 'rejected' && window.jqhtml?.debug?.verbose) {
|
|
4138
|
+
console.log(`[JQHTML gate_load] Component ${this._cid} (${this.component_name()}) gate rejected (ignored, load proceeds)`, result.reason);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
// Race gate settlement against an early-resume signal (reload/refresh/stop while gated).
|
|
4143
|
+
return new Promise((resolve) => {
|
|
4144
|
+
this.__gate_resume = resolve;
|
|
4145
|
+
settled.then(() => resolve());
|
|
4146
|
+
}).then(() => {
|
|
4147
|
+
this.__gate_resume = null;
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
2866
4150
|
/**
|
|
2867
4151
|
* Load phase - Fetch asynchronous data
|
|
2868
4152
|
* Called bottom-up, fully parallel
|
|
@@ -2885,11 +4169,24 @@ class Jqhtml_Component {
|
|
|
2885
4169
|
this._update_debug_attrs();
|
|
2886
4170
|
this._log_lifecycle('load', 'complete (use_cached_data - skipped on_load)');
|
|
2887
4171
|
this.trigger('load');
|
|
4172
|
+
if (!this._load_only && !this._load_render_only) {
|
|
4173
|
+
await this._call_lifecycle('on_loaded');
|
|
4174
|
+
this.trigger('loaded');
|
|
4175
|
+
}
|
|
2888
4176
|
return;
|
|
2889
4177
|
}
|
|
2890
|
-
//
|
|
4178
|
+
// TWO identities, deliberately computed separately.
|
|
4179
|
+
//
|
|
4180
|
+
// cache_key - may key a plain-data object arg by deterministic CONTENT, and may come
|
|
4181
|
+
// from the author's cache_id(). A wrong key here is corrected by
|
|
4182
|
+
// stale-while-revalidate.
|
|
4183
|
+
// dedup_key - args only, no content serialization, no cache_id(). A deduplicated
|
|
4184
|
+
// follower never runs on_load() and adopts the leader's data with NO
|
|
4185
|
+
// revalidation, so a wrong key here is permanently wrong data. Redundant
|
|
4186
|
+
// concurrent requests are the cheaper failure.
|
|
2891
4187
|
let cache_key = null;
|
|
2892
4188
|
let uncacheable_property;
|
|
4189
|
+
let uncacheable_reason;
|
|
2893
4190
|
if (typeof this.cache_id === 'function') {
|
|
2894
4191
|
try {
|
|
2895
4192
|
const custom_cache_id = this.cache_id();
|
|
@@ -2898,33 +4195,49 @@ class Jqhtml_Component {
|
|
|
2898
4195
|
catch (error) {
|
|
2899
4196
|
// cache_id() threw error - disable caching
|
|
2900
4197
|
uncacheable_property = 'cache_id()';
|
|
4198
|
+
uncacheable_reason = 'cache-id-threw';
|
|
2901
4199
|
}
|
|
2902
4200
|
}
|
|
2903
4201
|
else {
|
|
2904
|
-
|
|
2905
|
-
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
4202
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args, { allow_content_serialization: true });
|
|
2906
4203
|
cache_key = result.key;
|
|
2907
4204
|
uncacheable_property = result.uncacheable_property;
|
|
4205
|
+
uncacheable_reason = result.uncacheable_reason;
|
|
4206
|
+
}
|
|
4207
|
+
const dedup_key = Load_Coordinator.generate_invocation_key(this.component_name(), this.args).key;
|
|
4208
|
+
// SSR preload check: if preloaded data exists for this component+args, use it
|
|
4209
|
+
if (cache_key !== null && has_preload_data()) {
|
|
4210
|
+
const preloaded_data = consume_preload_data(cache_key);
|
|
4211
|
+
if (preloaded_data !== null) {
|
|
4212
|
+
this._cache_key = cache_key;
|
|
4213
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4214
|
+
console.log(`[SSR Preload] Component ${this._cid} (${this.component_name()}) using preloaded data`, { cache_key });
|
|
4215
|
+
}
|
|
4216
|
+
const data_before_load = JSON.stringify(this.data);
|
|
4217
|
+
await this._apply_load_result(preloaded_data, data_before_load);
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
2908
4220
|
}
|
|
2909
4221
|
// Store cache key for later use
|
|
2910
4222
|
this._cache_key = cache_key;
|
|
2911
|
-
//
|
|
2912
|
-
if (cache_key === null) {
|
|
2913
|
-
|
|
2914
|
-
if (
|
|
2915
|
-
this
|
|
4223
|
+
// Caching disabled - mark the element with the offending arg AND why
|
|
4224
|
+
if (cache_key === null && uncacheable_property) {
|
|
4225
|
+
this.$.attr('data-nocache', uncacheable_reason ? `${uncacheable_property}:${uncacheable_reason}` : uncacheable_property);
|
|
4226
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4227
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-keyable args - caching disabled`, { uncacheable_property, uncacheable_reason });
|
|
2916
4228
|
}
|
|
4229
|
+
}
|
|
4230
|
+
// Store "before" snapshot for comparison after on_load()
|
|
4231
|
+
const data_before_load = JSON.stringify(this.data);
|
|
4232
|
+
// No dedup identity - run on_load() alone rather than risk sharing a load
|
|
4233
|
+
if (dedup_key === null) {
|
|
2917
4234
|
if (window.jqhtml?.debug?.verbose) {
|
|
2918
|
-
console.log(`[
|
|
4235
|
+
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) has no dedup key - loading independently`);
|
|
2919
4236
|
}
|
|
2920
|
-
// Execute on_load on detached proxy without deduplication
|
|
2921
4237
|
const { data: result_data } = await this._execute_on_load_detached();
|
|
2922
|
-
|
|
2923
|
-
await this._apply_load_result(result_data, null);
|
|
4238
|
+
await this._apply_load_result(result_data, data_before_load);
|
|
2924
4239
|
return;
|
|
2925
4240
|
}
|
|
2926
|
-
// Store "before" snapshot for comparison after on_load()
|
|
2927
|
-
const data_before_load = JSON.stringify(this.data);
|
|
2928
4241
|
// Check if this component should execute on_load() or wait for existing request
|
|
2929
4242
|
const should_execute = Load_Coordinator.should_execute_on_load(this);
|
|
2930
4243
|
if (!should_execute) {
|
|
@@ -2961,6 +4274,10 @@ class Jqhtml_Component {
|
|
|
2961
4274
|
this._update_debug_attrs();
|
|
2962
4275
|
this._log_lifecycle('load', 'complete (follower)');
|
|
2963
4276
|
this.trigger('load');
|
|
4277
|
+
if (!this._load_only && !this._load_render_only) {
|
|
4278
|
+
await this._call_lifecycle('on_loaded');
|
|
4279
|
+
this.trigger('loaded');
|
|
4280
|
+
}
|
|
2964
4281
|
return;
|
|
2965
4282
|
}
|
|
2966
4283
|
// This component is a leader - execute on_load() on detached proxy
|
|
@@ -2979,143 +4296,11 @@ class Jqhtml_Component {
|
|
|
2979
4296
|
}
|
|
2980
4297
|
/**
|
|
2981
4298
|
* Execute on_load() on a fully detached proxy.
|
|
2982
|
-
*
|
|
2983
|
-
* The proxy has:
|
|
2984
|
-
* - A CLONE of this.data (from __initial_data_snapshot)
|
|
2985
|
-
* - Read-only access to this.args
|
|
2986
|
-
* - No access to anything else (this.$, this.sid, etc.)
|
|
2987
|
-
*
|
|
2988
|
-
* This ensures on_load runs completely isolated from the component instance.
|
|
2989
|
-
*
|
|
2990
|
-
* @param use_load_coordinator - If true, register with Load_Coordinator for deduplication
|
|
2991
|
-
* @returns The resulting data from the proxy after on_load completes
|
|
4299
|
+
* @see data-proxy.ts for full implementation
|
|
2992
4300
|
* @private
|
|
2993
4301
|
*/
|
|
2994
4302
|
async _execute_on_load_detached(use_load_coordinator = false) {
|
|
2995
|
-
|
|
2996
|
-
const data_clone = this.__initial_data_snapshot
|
|
2997
|
-
? JSON.parse(JSON.stringify(this.__initial_data_snapshot))
|
|
2998
|
-
: {};
|
|
2999
|
-
// Create a read-only proxy for args that blocks all modifications
|
|
3000
|
-
// Can't use JSON clone because args may contain functions (_slots, callbacks)
|
|
3001
|
-
const create_readonly_proxy = (obj, path = 'this.args') => {
|
|
3002
|
-
if (obj === null || typeof obj !== 'object')
|
|
3003
|
-
return obj;
|
|
3004
|
-
return new Proxy(obj, {
|
|
3005
|
-
get(target, prop) {
|
|
3006
|
-
const value = target[prop];
|
|
3007
|
-
// Recursively wrap nested objects (but not functions)
|
|
3008
|
-
if (value !== null && typeof value === 'object' && typeof value !== 'function') {
|
|
3009
|
-
return create_readonly_proxy(value, `${path}.${String(prop)}`);
|
|
3010
|
-
}
|
|
3011
|
-
return value;
|
|
3012
|
-
},
|
|
3013
|
-
set(target, prop, value) {
|
|
3014
|
-
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to modify ${path}.${String(prop)} during on_load().\n\n` +
|
|
3015
|
-
`RESTRICTION: this.args is READ-ONLY during on_load().\n\n` +
|
|
3016
|
-
`WHY: this.args configures what data to fetch. Modifying it during on_load() creates circular dependencies.\n\n` +
|
|
3017
|
-
`FIX: Modify this.args BEFORE on_load() runs (in on_create() or before calling reload()):\n` +
|
|
3018
|
-
` ❌ Inside on_load(): ${path}.${String(prop)} = ${JSON.stringify(value)};\n` +
|
|
3019
|
-
` ✅ In on_create(): this.args.${String(prop)} = defaultValue;`);
|
|
3020
|
-
throw new Error(`[JQHTML] Cannot modify ${path}.${String(prop)} during on_load(). ` +
|
|
3021
|
-
`this.args is read-only in on_load().`);
|
|
3022
|
-
},
|
|
3023
|
-
deleteProperty(target, prop) {
|
|
3024
|
-
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to delete ${path}.${String(prop)} during on_load().\n\n` +
|
|
3025
|
-
`RESTRICTION: this.args is READ-ONLY during on_load().`);
|
|
3026
|
-
throw new Error(`[JQHTML] Cannot delete ${path}.${String(prop)} during on_load(). ` +
|
|
3027
|
-
`this.args is read-only in on_load().`);
|
|
3028
|
-
}
|
|
3029
|
-
});
|
|
3030
|
-
};
|
|
3031
|
-
// Create a detached context object that on_load will operate on
|
|
3032
|
-
const detached_context = {
|
|
3033
|
-
args: create_readonly_proxy(this.args), // Read-only proxy wrapping real args
|
|
3034
|
-
data: data_clone // Cloned data - modifications stay isolated
|
|
3035
|
-
};
|
|
3036
|
-
// Create restricted proxy that operates on the detached context
|
|
3037
|
-
const component_name = this.component_name();
|
|
3038
|
-
const restricted_this = new Proxy(detached_context, {
|
|
3039
|
-
get(target, prop) {
|
|
3040
|
-
// Only allow access to args and data
|
|
3041
|
-
if (prop === 'args') {
|
|
3042
|
-
return target.args;
|
|
3043
|
-
}
|
|
3044
|
-
if (prop === 'data') {
|
|
3045
|
-
return target.data;
|
|
3046
|
-
}
|
|
3047
|
-
// Block everything else
|
|
3048
|
-
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to access this.${String(prop)} during on_load().\n\n` +
|
|
3049
|
-
`RESTRICTION: on_load() may ONLY access:\n` +
|
|
3050
|
-
` - this.args (read-only)\n` +
|
|
3051
|
-
` - this.data (read/write)\n\n` +
|
|
3052
|
-
`WHY: on_load() is for data fetching only. All other component functionality should happen in other lifecycle methods.\n\n` +
|
|
3053
|
-
`FIX:\n` +
|
|
3054
|
-
` - DOM manipulation → use on_render() or on_ready()\n` +
|
|
3055
|
-
` - Component methods → call them before/after on_load(), not inside it\n` +
|
|
3056
|
-
` - Other properties → restructure code to only use this.args and this.data in on_load()`);
|
|
3057
|
-
throw new Error(`[JQHTML] Cannot access this.${String(prop)} during on_load(). ` +
|
|
3058
|
-
`on_load() may only access this.args and this.data.`);
|
|
3059
|
-
},
|
|
3060
|
-
set(target, prop, value) {
|
|
3061
|
-
// Only allow setting data
|
|
3062
|
-
if (prop === 'data') {
|
|
3063
|
-
target.data = value;
|
|
3064
|
-
return true;
|
|
3065
|
-
}
|
|
3066
|
-
// Block setting args
|
|
3067
|
-
if (prop === 'args') {
|
|
3068
|
-
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to modify this.args during on_load().\n\n` +
|
|
3069
|
-
`RESTRICTION: on_load() may ONLY modify:\n` +
|
|
3070
|
-
` - this.data (read/write)\n\n` +
|
|
3071
|
-
`WHY: this.args is component state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.\n\n` +
|
|
3072
|
-
`FIX: Modify this.args BEFORE calling on_load() (in on_create() or other lifecycle methods), not inside on_load():\n` +
|
|
3073
|
-
` ❌ Inside on_load(): this.args.filter = 'new_value';\n` +
|
|
3074
|
-
` ✅ In on_create(): this.args.filter = this.args.initial_filter || 'default';`);
|
|
3075
|
-
throw new Error(`[JQHTML] Cannot modify this.args during on_load(). ` +
|
|
3076
|
-
`Modify this.args in other lifecycle methods, not inside on_load().`);
|
|
3077
|
-
}
|
|
3078
|
-
// Block setting any other properties
|
|
3079
|
-
console.error(`[JQHTML] ERROR: Component "${component_name}" attempted to modify this.${String(prop)} during on_load().\n\n` +
|
|
3080
|
-
`RESTRICTION: on_load() may ONLY modify:\n` +
|
|
3081
|
-
` - this.data (read/write)\n\n` +
|
|
3082
|
-
`WHY: on_load() is for data fetching only. Setting properties on the component instance should happen in other lifecycle methods.\n\n` +
|
|
3083
|
-
`FIX: Store your data in this.data instead:\n` +
|
|
3084
|
-
` ❌ this.${String(prop)} = value;\n` +
|
|
3085
|
-
` ✅ this.data.${String(prop)} = value;`);
|
|
3086
|
-
throw new Error(`[JQHTML] Cannot modify this.${String(prop)} during on_load(). ` +
|
|
3087
|
-
`Only this.data can be modified in on_load().`);
|
|
3088
|
-
}
|
|
3089
|
-
});
|
|
3090
|
-
// Create promise for this on_load() call
|
|
3091
|
-
const on_load_promise = (async () => {
|
|
3092
|
-
try {
|
|
3093
|
-
await this._call_lifecycle('on_load', restricted_this);
|
|
3094
|
-
}
|
|
3095
|
-
catch (error) {
|
|
3096
|
-
if (use_load_coordinator) {
|
|
3097
|
-
// Handle error and notify coordinator
|
|
3098
|
-
Load_Coordinator.handle_leader_error(this, error);
|
|
3099
|
-
}
|
|
3100
|
-
throw error;
|
|
3101
|
-
}
|
|
3102
|
-
})();
|
|
3103
|
-
// If using Load_Coordinator, register as leader
|
|
3104
|
-
// Note: We store the completion function but DON'T call it here
|
|
3105
|
-
// It will be called by _load() after _apply_load_result() updates this.data
|
|
3106
|
-
let complete_coordination = null;
|
|
3107
|
-
if (use_load_coordinator) {
|
|
3108
|
-
complete_coordination = Load_Coordinator.register_leader(this, on_load_promise);
|
|
3109
|
-
}
|
|
3110
|
-
await on_load_promise;
|
|
3111
|
-
// Note: We don't validate args changes here because external code (like parent
|
|
3112
|
-
// components calling reload()) is allowed to modify component.args during on_load().
|
|
3113
|
-
// The read-only proxy only prevents code INSIDE on_load() from modifying args.
|
|
3114
|
-
// Return the data from the detached context AND the completion function
|
|
3115
|
-
return {
|
|
3116
|
-
data: detached_context.data,
|
|
3117
|
-
complete_coordination
|
|
3118
|
-
};
|
|
4303
|
+
return execute_on_load_detached(this, use_load_coordinator);
|
|
3119
4304
|
}
|
|
3120
4305
|
/**
|
|
3121
4306
|
* Apply the result of on_load() to this.data via the sequential queue.
|
|
@@ -3153,34 +4338,30 @@ class Jqhtml_Component {
|
|
|
3153
4338
|
}
|
|
3154
4339
|
// Freeze this.data
|
|
3155
4340
|
this.__data_frozen = true;
|
|
4341
|
+
// SSR data capture: record this component's data for preloading
|
|
4342
|
+
if (is_capture_enabled() && this._has_on_load()) {
|
|
4343
|
+
capture_component_data(this.component_name(), this.args, this.data, this._cache_key);
|
|
4344
|
+
}
|
|
3156
4345
|
// Calculate if data changed
|
|
3157
4346
|
const data_after_load = JSON.stringify(this.data);
|
|
3158
4347
|
const data_changed = data_before_load !== null && data_after_load !== data_before_load;
|
|
3159
4348
|
// Track if component is "dynamic" (this.data changed during on_load)
|
|
3160
4349
|
// Used by HTML cache mode for synchronization - static parents don't block children
|
|
3161
4350
|
this._is_dynamic = data_changed && data_after_load !== '{}';
|
|
3162
|
-
// CACHE WRITE -
|
|
3163
|
-
|
|
3164
|
-
if (cache_mode === 'html') {
|
|
3165
|
-
// HTML cache mode - flag to cache HTML after children ready in _ready()
|
|
3166
|
-
this._should_cache_html_after_ready = true;
|
|
3167
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3168
|
-
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready`, { cache_key: this._cache_key });
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
else {
|
|
3172
|
-
// Data cache mode (default) - write this.data to cache
|
|
3173
|
-
Jqhtml_Local_Storage.set(this._cache_key, this.data);
|
|
3174
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3175
|
-
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load()`, { cache_key: this._cache_key, data: this.data });
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
}
|
|
4351
|
+
// CACHE WRITE - @see component-cache.ts
|
|
4352
|
+
write_cache_on_loaded(this, this._is_dynamic);
|
|
3179
4353
|
this._ready_state = 2;
|
|
3180
4354
|
this._update_debug_attrs();
|
|
3181
4355
|
this._log_lifecycle('load', 'complete');
|
|
3182
4356
|
// Emit lifecycle event
|
|
3183
4357
|
this.trigger('load');
|
|
4358
|
+
// Call on_loaded() - runs on the REAL component (not detached proxy)
|
|
4359
|
+
// this.data is frozen (read-only), but this.$, this.state, this.args are accessible
|
|
4360
|
+
// Suppressed by _load_only and _load_render_only flags (preloading mode)
|
|
4361
|
+
if (!this._load_only && !this._load_render_only) {
|
|
4362
|
+
await this._call_lifecycle('on_loaded');
|
|
4363
|
+
this.trigger('loaded');
|
|
4364
|
+
}
|
|
3184
4365
|
}
|
|
3185
4366
|
finally {
|
|
3186
4367
|
// Signal next in queue
|
|
@@ -3196,36 +4377,16 @@ class Jqhtml_Component {
|
|
|
3196
4377
|
if (this._stopped || this._ready_state >= 4)
|
|
3197
4378
|
return;
|
|
3198
4379
|
this._log_lifecycle('ready', 'start');
|
|
3199
|
-
// HTML CACHE MODE -
|
|
3200
|
-
//
|
|
3201
|
-
// 2. Take HTML snapshot BEFORE waiting for children ready
|
|
3202
|
-
// 3. This ensures we capture the DOM after on_render but before on_ready manipulations
|
|
4380
|
+
// HTML CACHE MODE - Wait for children on_render, then snapshot
|
|
4381
|
+
// @see component-cache.ts for write_html_cache_snapshot implementation
|
|
3203
4382
|
if (this._should_cache_html_after_ready && this._cache_key) {
|
|
3204
|
-
// Wait for all children to complete their on_render
|
|
3205
4383
|
await this._wait_for_children_on_render();
|
|
3206
|
-
|
|
3207
|
-
// Static parents don't cache - they just let children proceed
|
|
3208
|
-
if (this._is_dynamic) {
|
|
3209
|
-
this._should_cache_html_after_ready = false;
|
|
3210
|
-
// Cache the rendered HTML
|
|
3211
|
-
const html = this.$.html();
|
|
3212
|
-
const html_cache_key = `${this._cache_key}::html`;
|
|
3213
|
-
Jqhtml_Local_Storage.set(html_cache_key, html);
|
|
3214
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3215
|
-
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
else {
|
|
3219
|
-
// Static component - just clear the flag, don't cache
|
|
3220
|
-
this._should_cache_html_after_ready = false;
|
|
3221
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3222
|
-
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) is static (no data change) - skipping HTML cache`);
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
4384
|
+
write_html_cache_snapshot(this);
|
|
3225
4385
|
}
|
|
3226
4386
|
// Wait for all children to reach ready state (bottom-up execution)
|
|
3227
4387
|
await this._wait_for_children_ready();
|
|
3228
4388
|
await this._call_lifecycle('on_ready');
|
|
4389
|
+
this._fire_viewport_resize();
|
|
3229
4390
|
this._ready_state = 4;
|
|
3230
4391
|
this._update_debug_attrs();
|
|
3231
4392
|
this._log_lifecycle('ready', 'complete');
|
|
@@ -3382,6 +4543,17 @@ class Jqhtml_Component {
|
|
|
3382
4543
|
* If false, only re-render if data actually changed.
|
|
3383
4544
|
*/
|
|
3384
4545
|
async reload(always_render) {
|
|
4546
|
+
// Load-gate short-circuit: if a gate is currently holding the first on_load,
|
|
4547
|
+
// reload()/refresh() resumes the paused boot lifecycle instead of running its
|
|
4548
|
+
// normal debounced re-fetch. The boot lifecycle then proceeds to run on_load
|
|
4549
|
+
// (the fresh fetch) exactly as reload() would want. Any later gate settlement
|
|
4550
|
+
// becomes a no-op. Return a promise that resolves when the component is ready.
|
|
4551
|
+
if (this.__gate_resume) {
|
|
4552
|
+
const resume = this.__gate_resume;
|
|
4553
|
+
this.__gate_resume = null;
|
|
4554
|
+
resume();
|
|
4555
|
+
return this.ready();
|
|
4556
|
+
}
|
|
3385
4557
|
// Default to true if undefined
|
|
3386
4558
|
const force_refresh = always_render !== undefined ? always_render : true;
|
|
3387
4559
|
// State machine: reload(true) takes precedence over reload(false)
|
|
@@ -3394,11 +4566,9 @@ class Jqhtml_Component {
|
|
|
3394
4566
|
this.next_reload_force_refresh = false;
|
|
3395
4567
|
}
|
|
3396
4568
|
}
|
|
3397
|
-
//
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
}
|
|
3401
|
-
return this._reload_debounced();
|
|
4569
|
+
// Enqueue through component queue (replaces _create_debounced_function)
|
|
4570
|
+
// Same-type pending operations collapse (multiple reload() calls share one execution)
|
|
4571
|
+
return this._queue.enqueue('reload', () => this._reload());
|
|
3402
4572
|
}
|
|
3403
4573
|
/**
|
|
3404
4574
|
* Refresh component - re-fetch data and re-render only if data changed (debounced)
|
|
@@ -3461,6 +4631,7 @@ class Jqhtml_Component {
|
|
|
3461
4631
|
this._render();
|
|
3462
4632
|
await this._wait_for_children_ready();
|
|
3463
4633
|
await this._call_lifecycle('on_ready');
|
|
4634
|
+
this._fire_viewport_resize();
|
|
3464
4635
|
this.trigger('ready');
|
|
3465
4636
|
this._log_lifecycle('reload', 'complete (no on_load)');
|
|
3466
4637
|
return;
|
|
@@ -3480,57 +4651,8 @@ class Jqhtml_Component {
|
|
|
3480
4651
|
}
|
|
3481
4652
|
}
|
|
3482
4653
|
if (args_changed) {
|
|
3483
|
-
//
|
|
3484
|
-
|
|
3485
|
-
if (typeof this.cache_id === 'function') {
|
|
3486
|
-
try {
|
|
3487
|
-
const custom_cache_id = this.cache_id();
|
|
3488
|
-
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
3489
|
-
}
|
|
3490
|
-
catch (error) {
|
|
3491
|
-
// cache_id() threw error - disable caching
|
|
3492
|
-
cache_key = null;
|
|
3493
|
-
}
|
|
3494
|
-
}
|
|
3495
|
-
else {
|
|
3496
|
-
// Use standard args-based cache key generation
|
|
3497
|
-
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
3498
|
-
cache_key = result.key;
|
|
3499
|
-
}
|
|
3500
|
-
// Check for cached data/html when args changed
|
|
3501
|
-
if (cache_key !== null) {
|
|
3502
|
-
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
3503
|
-
this._cache_key = cache_key;
|
|
3504
|
-
if (cache_mode === 'html') {
|
|
3505
|
-
// HTML cache mode - check for cached HTML
|
|
3506
|
-
const html_cache_key = `${cache_key}::html`;
|
|
3507
|
-
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
|
|
3508
|
-
if (cached_html !== null && typeof cached_html === 'string') {
|
|
3509
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3510
|
-
console.log(`[Cache html] reload() - Component ${this._cid} (${this.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
3511
|
-
}
|
|
3512
|
-
// Store cached HTML for injection in _render()
|
|
3513
|
-
this._cached_html = cached_html;
|
|
3514
|
-
this.render();
|
|
3515
|
-
rendered_from_cache = true;
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
else {
|
|
3519
|
-
// Data cache mode (default) - check for cached data
|
|
3520
|
-
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
3521
|
-
if (cached_data !== null && typeof cached_data === 'object' && JSON.stringify(cached_data) !== '{}') {
|
|
3522
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3523
|
-
console.log(`[Cache data] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
|
|
3524
|
-
}
|
|
3525
|
-
// Hydrate this.data with cached data
|
|
3526
|
-
this.__data_frozen = false;
|
|
3527
|
-
this.data = cached_data;
|
|
3528
|
-
this.__data_frozen = true;
|
|
3529
|
-
this.render();
|
|
3530
|
-
rendered_from_cache = true;
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
4654
|
+
// @see component-cache.ts for full implementation
|
|
4655
|
+
rendered_from_cache = check_cache_on_reload(this);
|
|
3534
4656
|
}
|
|
3535
4657
|
// Capture data state before on_load for comparison
|
|
3536
4658
|
data_before_load = JSON.stringify(this.data);
|
|
@@ -3584,6 +4706,7 @@ class Jqhtml_Component {
|
|
|
3584
4706
|
if (rendered_from_cache || should_render) {
|
|
3585
4707
|
await this._wait_for_children_ready();
|
|
3586
4708
|
await this._call_lifecycle('on_ready');
|
|
4709
|
+
this._fire_viewport_resize();
|
|
3587
4710
|
// Trigger ready event so parent callbacks fire on subsequent reloads
|
|
3588
4711
|
this.trigger('ready');
|
|
3589
4712
|
}
|
|
@@ -3604,11 +4727,18 @@ class Jqhtml_Component {
|
|
|
3604
4727
|
if (this._stopped)
|
|
3605
4728
|
return;
|
|
3606
4729
|
this._stopped = true;
|
|
4730
|
+
// If a load gate is holding the first on_load, release the wait so the boot
|
|
4731
|
+
// lifecycle unwinds cleanly (it will see _stopped and abandon the load).
|
|
4732
|
+
if (this.__gate_resume) {
|
|
4733
|
+
const resume = this.__gate_resume;
|
|
4734
|
+
this.__gate_resume = null;
|
|
4735
|
+
resume();
|
|
4736
|
+
}
|
|
3607
4737
|
// Early bailout: skip expensive cleanup if no handlers registered
|
|
3608
4738
|
// Only matters for aborting boot() lifecycle - minimal cleanup sufficient
|
|
3609
4739
|
const has_custom_stop = this.on_stop !== Jqhtml_Component.prototype.on_stop;
|
|
3610
|
-
const
|
|
3611
|
-
if (!has_custom_stop && !
|
|
4740
|
+
const has_stop_callbacks = this._on_registered('stop');
|
|
4741
|
+
if (!has_custom_stop && !has_stop_callbacks) {
|
|
3612
4742
|
// Fast path: no cleanup logic defined, just mark as stopped
|
|
3613
4743
|
this._lifecycle_manager.unregister_component(this);
|
|
3614
4744
|
this._ready_state = 99;
|
|
@@ -3656,8 +4786,19 @@ class Jqhtml_Component {
|
|
|
3656
4786
|
on_render() { }
|
|
3657
4787
|
on_create() { }
|
|
3658
4788
|
on_load() { } // Override to fetch data asynchronously
|
|
4789
|
+
on_loaded() { } // Override to process loaded data (e.g., clone this.data to this.state)
|
|
3659
4790
|
async on_ready() { }
|
|
3660
4791
|
on_stop() { }
|
|
4792
|
+
/**
|
|
4793
|
+
* Called with the viewport width in CSS pixels:
|
|
4794
|
+
* - after every on_render()
|
|
4795
|
+
* - after every on_ready()
|
|
4796
|
+
* - on window resize, debounced 30ms, for every component in the document
|
|
4797
|
+
*
|
|
4798
|
+
* MUST be synchronous. Prefer CSS media/container queries for pure styling -
|
|
4799
|
+
* override this only when layout requires imperative measurement.
|
|
4800
|
+
*/
|
|
4801
|
+
on_viewport_resize(viewport_width) { }
|
|
3661
4802
|
/**
|
|
3662
4803
|
* Should component re-render after load?
|
|
3663
4804
|
* By default, only re-renders if data has changed
|
|
@@ -3701,85 +4842,39 @@ class Jqhtml_Component {
|
|
|
3701
4842
|
return this.constructor.name;
|
|
3702
4843
|
}
|
|
3703
4844
|
/**
|
|
3704
|
-
* Register event callback
|
|
3705
|
-
*
|
|
3706
|
-
* Lifecycle event callbacks fire after the lifecycle method completes
|
|
3707
|
-
* If a lifecycle event has already occurred, the callback fires immediately AND registers for future occurrences
|
|
3708
|
-
* Custom events only fire when explicitly triggered via .trigger()
|
|
3709
|
-
*
|
|
3710
|
-
* Callback signature: (component, data?) => void
|
|
3711
|
-
* - component: The component instance that triggered the event
|
|
3712
|
-
* - data: Optional data passed as second parameter to trigger()
|
|
4845
|
+
* Register event callback - delegates to component-events.ts
|
|
4846
|
+
* @see component-events.ts for full documentation
|
|
3713
4847
|
*/
|
|
3714
4848
|
on(event_name, callback) {
|
|
3715
|
-
|
|
3716
|
-
if (!this._lifecycle_callbacks.has(event_name)) {
|
|
3717
|
-
this._lifecycle_callbacks.set(event_name, []);
|
|
3718
|
-
}
|
|
3719
|
-
// Add callback to queue
|
|
3720
|
-
this._lifecycle_callbacks.get(event_name).push(callback);
|
|
3721
|
-
// If this lifecycle event has already occurred, fire the callback immediately
|
|
3722
|
-
// with the stored data from when trigger() was called
|
|
3723
|
-
if (this._lifecycle_states.has(event_name)) {
|
|
3724
|
-
try {
|
|
3725
|
-
const stored_data = this._lifecycle_states.get(event_name);
|
|
3726
|
-
callback(this, stored_data);
|
|
3727
|
-
}
|
|
3728
|
-
catch (error) {
|
|
3729
|
-
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
|
|
3730
|
-
}
|
|
3731
|
-
}
|
|
3732
|
-
return this;
|
|
4849
|
+
return event_on(this, event_name, callback);
|
|
3733
4850
|
}
|
|
3734
4851
|
/**
|
|
3735
|
-
*
|
|
3736
|
-
*
|
|
3737
|
-
*
|
|
3738
|
-
|
|
3739
|
-
|
|
4852
|
+
* Register a callback that fires exactly once - delegates to component-events.ts
|
|
4853
|
+
* If the event already occurred, fires immediately and does not register.
|
|
4854
|
+
* @see component-events.ts for full documentation
|
|
4855
|
+
*/
|
|
4856
|
+
once(event_name, callback) {
|
|
4857
|
+
return event_once(this, event_name, callback);
|
|
4858
|
+
}
|
|
4859
|
+
/**
|
|
4860
|
+
* Trigger a lifecycle event - delegates to component-events.ts
|
|
4861
|
+
* @see component-events.ts for full documentation
|
|
3740
4862
|
*/
|
|
3741
4863
|
trigger(event_name, data) {
|
|
3742
|
-
|
|
3743
|
-
this._lifecycle_states.set(event_name, data);
|
|
3744
|
-
// Fire all registered callbacks for this event
|
|
3745
|
-
const callbacks = this._lifecycle_callbacks.get(event_name);
|
|
3746
|
-
if (callbacks) {
|
|
3747
|
-
for (const callback of callbacks) {
|
|
3748
|
-
try {
|
|
3749
|
-
callback.bind(this)(this, data);
|
|
3750
|
-
}
|
|
3751
|
-
catch (error) {
|
|
3752
|
-
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
|
|
3753
|
-
}
|
|
3754
|
-
}
|
|
3755
|
-
}
|
|
4864
|
+
event_trigger(this, event_name, data);
|
|
3756
4865
|
}
|
|
3757
4866
|
/**
|
|
3758
4867
|
* Check if any callbacks are registered for a given event
|
|
3759
|
-
* Used to determine if cleanup logic needs to run
|
|
3760
4868
|
*/
|
|
3761
4869
|
_on_registered(event_name) {
|
|
3762
|
-
|
|
3763
|
-
return !!(callbacks && callbacks.length > 0);
|
|
4870
|
+
return event_on_registered(this, event_name);
|
|
3764
4871
|
}
|
|
3765
4872
|
/**
|
|
3766
|
-
* Invalidate a lifecycle event -
|
|
3767
|
-
*
|
|
3768
|
-
* This is the opposite of trigger(). After invalidate() is called:
|
|
3769
|
-
* - New .on() handlers will NOT fire immediately
|
|
3770
|
-
* - The ready() promise will NOT resolve immediately
|
|
3771
|
-
* - Handlers wait for the next trigger() call
|
|
3772
|
-
*
|
|
3773
|
-
* Existing registered callbacks are NOT removed - they'll fire on next trigger().
|
|
3774
|
-
*
|
|
3775
|
-
* Use case: Call invalidate('ready') at the start of reload() or render()
|
|
3776
|
-
* so that any new .on('ready') handlers wait for the reload/render to complete
|
|
3777
|
-
* rather than firing immediately based on the previous lifecycle's state.
|
|
3778
|
-
*
|
|
3779
|
-
* @param event_name - Name of the event to invalidate
|
|
4873
|
+
* Invalidate a lifecycle event - delegates to component-events.ts
|
|
4874
|
+
* @see component-events.ts for full documentation
|
|
3780
4875
|
*/
|
|
3781
4876
|
invalidate(event_name) {
|
|
3782
|
-
this
|
|
4877
|
+
event_invalidate(this, event_name);
|
|
3783
4878
|
}
|
|
3784
4879
|
/**
|
|
3785
4880
|
* Find element by scoped ID
|
|
@@ -4078,8 +5173,13 @@ class Jqhtml_Component {
|
|
|
4078
5173
|
}
|
|
4079
5174
|
}
|
|
4080
5175
|
_set_attributes() {
|
|
4081
|
-
//
|
|
4082
|
-
|
|
5176
|
+
// data-cid on the rendered element is debug-only: it mirrors this._cid so component
|
|
5177
|
+
// boundaries are legible in DevTools. The runtime never reads it - scoping uses the
|
|
5178
|
+
// _cid property, and the transient data-cid placeholder the instruction processor
|
|
5179
|
+
// matches on is written and removed before this ever runs.
|
|
5180
|
+
if (debug_attributes_enabled()) {
|
|
5181
|
+
this.$.attr('data-cid', this._cid);
|
|
5182
|
+
}
|
|
4083
5183
|
// Only set lifecycle state attribute if verbose mode enabled
|
|
4084
5184
|
if (window.jqhtml?.debug?.verbose) {
|
|
4085
5185
|
this.$.attr('data-_lifecycle-state', this._ready_state.toString());
|
|
@@ -4151,84 +5251,6 @@ class Jqhtml_Component {
|
|
|
4151
5251
|
window.JQHTML_DEBUG.log(this.component_name(), 'debug', `${action}: ${args.map(a => JSON.stringify(a)).join(', ')}`);
|
|
4152
5252
|
}
|
|
4153
5253
|
}
|
|
4154
|
-
/**
|
|
4155
|
-
* Creates a debounced function with exclusivity and promise fan-in
|
|
4156
|
-
*
|
|
4157
|
-
* When invoked, immediately runs the callback exclusively.
|
|
4158
|
-
* For subsequent invocations, applies a delay before running the callback exclusively again.
|
|
4159
|
-
* The delay starts after the current asynchronous operation resolves.
|
|
4160
|
-
*
|
|
4161
|
-
* If delay is 0, the function only prevents enqueueing multiple executions,
|
|
4162
|
-
* but will still run them immediately in an exclusive sequential manner.
|
|
4163
|
-
*
|
|
4164
|
-
* The most recent invocation's parameters are used when the function executes.
|
|
4165
|
-
* Returns a promise that resolves when the next exclusive execution completes.
|
|
4166
|
-
*
|
|
4167
|
-
* @private
|
|
4168
|
-
*/
|
|
4169
|
-
_create_debounced_function(callback, delay) {
|
|
4170
|
-
let running = false;
|
|
4171
|
-
let queued = false;
|
|
4172
|
-
let last_end_time = 0; // timestamp of last completed run
|
|
4173
|
-
let timer = null;
|
|
4174
|
-
let next_args = [];
|
|
4175
|
-
let resolve_queue = [];
|
|
4176
|
-
let reject_queue = [];
|
|
4177
|
-
const run_function = async () => {
|
|
4178
|
-
const these_resolves = resolve_queue;
|
|
4179
|
-
const these_rejects = reject_queue;
|
|
4180
|
-
const args = next_args;
|
|
4181
|
-
resolve_queue = [];
|
|
4182
|
-
reject_queue = [];
|
|
4183
|
-
next_args = [];
|
|
4184
|
-
queued = false;
|
|
4185
|
-
running = true;
|
|
4186
|
-
try {
|
|
4187
|
-
const result = await callback(...args);
|
|
4188
|
-
for (const resolve of these_resolves)
|
|
4189
|
-
resolve(result);
|
|
4190
|
-
}
|
|
4191
|
-
catch (err) {
|
|
4192
|
-
for (const reject of these_rejects)
|
|
4193
|
-
reject(err);
|
|
4194
|
-
}
|
|
4195
|
-
finally {
|
|
4196
|
-
running = false;
|
|
4197
|
-
last_end_time = Date.now();
|
|
4198
|
-
if (queued) {
|
|
4199
|
-
clearTimeout(timer);
|
|
4200
|
-
timer = setTimeout(run_function, Math.max(delay, 0));
|
|
4201
|
-
}
|
|
4202
|
-
else {
|
|
4203
|
-
timer = null;
|
|
4204
|
-
}
|
|
4205
|
-
}
|
|
4206
|
-
};
|
|
4207
|
-
return function (...args) {
|
|
4208
|
-
next_args = args;
|
|
4209
|
-
return new Promise((resolve, reject) => {
|
|
4210
|
-
resolve_queue.push(resolve);
|
|
4211
|
-
reject_queue.push(reject);
|
|
4212
|
-
// Nothing running and nothing scheduled
|
|
4213
|
-
if (!running && !timer) {
|
|
4214
|
-
const first_call = last_end_time === 0;
|
|
4215
|
-
const since = first_call ? Infinity : Date.now() - last_end_time;
|
|
4216
|
-
if (since >= delay) {
|
|
4217
|
-
run_function();
|
|
4218
|
-
}
|
|
4219
|
-
else {
|
|
4220
|
-
const wait = Math.max(delay - since, 0);
|
|
4221
|
-
clearTimeout(timer);
|
|
4222
|
-
timer = setTimeout(run_function, wait);
|
|
4223
|
-
}
|
|
4224
|
-
return;
|
|
4225
|
-
}
|
|
4226
|
-
// If already running or timer exists, just mark queued
|
|
4227
|
-
// The finally{} of run_function handles scheduling after full delay
|
|
4228
|
-
queued = true;
|
|
4229
|
-
});
|
|
4230
|
-
};
|
|
4231
|
-
}
|
|
4232
5254
|
}
|
|
4233
5255
|
// Static properties
|
|
4234
5256
|
Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() detection
|
|
@@ -4884,6 +5906,61 @@ function init_jquery_plugin(jQuery) {
|
|
|
4884
5906
|
const originalEmpty = jQuery.fn.empty;
|
|
4885
5907
|
const originalHtml = jQuery.fn.html;
|
|
4886
5908
|
const originalText = jQuery.fn.text;
|
|
5909
|
+
// ---------------------------------------------------------------------------
|
|
5910
|
+
// Guard against hand-creating elements with data-sid via jQuery.
|
|
5911
|
+
//
|
|
5912
|
+
// $sid / data-sid is provisioned ONLY by the jqhtml template renderer, which
|
|
5913
|
+
// ALWAYS emits data-sid paired with a scoped id ("<sid>:<component_cid>").
|
|
5914
|
+
// That scoped id is what binds the element to its component; data-sid on its
|
|
5915
|
+
// own does nothing. So an element whose data-sid is NOT paired with such an id
|
|
5916
|
+
// was hand-written (e.g. .html('<div data-sid="x">')) and the framework never
|
|
5917
|
+
// provisioned it - this.$sid('x') can never find it. We detect exactly that.
|
|
5918
|
+
//
|
|
5919
|
+
// The framework's own rendering injects data-sid via native innerHTML, never
|
|
5920
|
+
// through these jQuery methods, and the one internal jQuery .html() copy
|
|
5921
|
+
// (the .component() tag-mismatch path) carries the provisioned id - so neither
|
|
5922
|
+
// trips this guard.
|
|
5923
|
+
// ---------------------------------------------------------------------------
|
|
5924
|
+
// Return the offending data-sid value if `html` contains a hand-written
|
|
5925
|
+
// data-sid element (data-sid without the scoped "<sid>:<cid>" id), else null.
|
|
5926
|
+
const find_manual_sid = (html) => {
|
|
5927
|
+
if (typeof html !== 'string' || html.indexOf('data-sid') === -1)
|
|
5928
|
+
return null;
|
|
5929
|
+
if (typeof document === 'undefined')
|
|
5930
|
+
return null; // no DOM to parse (SSR)
|
|
5931
|
+
const tpl = document.createElement('template');
|
|
5932
|
+
tpl.innerHTML = html; // native parse - does not route back through jQuery
|
|
5933
|
+
const candidates = tpl.content.querySelectorAll('[data-sid]');
|
|
5934
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
5935
|
+
const el = candidates[i];
|
|
5936
|
+
const sid = el.getAttribute('data-sid') || '';
|
|
5937
|
+
const id = el.getAttribute('id') || '';
|
|
5938
|
+
// Provisioned elements have id="<sid>:<cid>". Anything else is hand-written.
|
|
5939
|
+
if (id.indexOf(sid + ':') !== 0)
|
|
5940
|
+
return sid;
|
|
5941
|
+
}
|
|
5942
|
+
return null;
|
|
5943
|
+
};
|
|
5944
|
+
const assert_no_manual_sid = (value, where) => {
|
|
5945
|
+
const sid = find_manual_sid(value);
|
|
5946
|
+
if (sid === null)
|
|
5947
|
+
return;
|
|
5948
|
+
throw new Error(`[JQHTML] Manually creating an element with data-sid="${sid}" via jQuery (${where}) is not allowed.\n\n` +
|
|
5949
|
+
`data-sid is NOT a normal attribute you can hand-write. It is provisioned by the jqhtml template\n` +
|
|
5950
|
+
`renderer, which scopes the element to its component by pairing data-sid with a unique id\n` +
|
|
5951
|
+
`("${sid}:<component_cid>"). An element you create yourself has no such scoped id, so\n` +
|
|
5952
|
+
`this.$sid('${sid}') / this.sid('${sid}') can never find it - scoping to a component is the entire\n` +
|
|
5953
|
+
`point of data-sid.\n\n` +
|
|
5954
|
+
`CORRECT USAGE:\n` +
|
|
5955
|
+
` • Put the element in your component template with $sid:\n` +
|
|
5956
|
+
` <div $sid="${sid}"></div>\n` +
|
|
5957
|
+
` then access it from the component: this.$sid('${sid}')\n` +
|
|
5958
|
+
` • Need it created dynamically? Use a child component or a $redrawable element in the template\n` +
|
|
5959
|
+
` and render() it, instead of injecting raw HTML.\n\n` +
|
|
5960
|
+
`If you only need to select or mark a plain element, use a normal attribute or class instead:\n` +
|
|
5961
|
+
` • <div data-role="${sid}"> → .find('[data-role="${sid}"]')\n` +
|
|
5962
|
+
` • <div class="MyComponent__${sid}"> → .find('.MyComponent__${sid}')`);
|
|
5963
|
+
};
|
|
4887
5964
|
/**
|
|
4888
5965
|
* Override jQuery.fn.empty() to stop child components before clearing DOM
|
|
4889
5966
|
* This ensures component cleanup hooks fire when DOM is cleared
|
|
@@ -4910,6 +5987,8 @@ function init_jquery_plugin(jQuery) {
|
|
|
4910
5987
|
if (arguments.length === 0) {
|
|
4911
5988
|
return originalHtml.call(this);
|
|
4912
5989
|
}
|
|
5990
|
+
// Reject hand-written data-sid before mutating the DOM
|
|
5991
|
+
assert_no_manual_sid(value, '.html()');
|
|
4913
5992
|
// Setter - empty first (which stops components), then set content
|
|
4914
5993
|
return this.each(function () {
|
|
4915
5994
|
jQuery(this).empty();
|
|
@@ -5012,6 +6091,36 @@ function init_jquery_plugin(jQuery) {
|
|
|
5012
6091
|
// Pass through to original .find()
|
|
5013
6092
|
return originalFind.call(this, selector);
|
|
5014
6093
|
};
|
|
6094
|
+
// Guard the content-insertion methods that build elements from HTML strings,
|
|
6095
|
+
// plus $() construction. Wrapped once so repeated init() calls don't re-wrap.
|
|
6096
|
+
if (!jQuery.fn.__jqhtml_sid_guarded) {
|
|
6097
|
+
jQuery.fn.__jqhtml_sid_guarded = true;
|
|
6098
|
+
// .append('<div data-sid>'), .prepend, .before, .after, .replaceWith
|
|
6099
|
+
for (const name of ['append', 'prepend', 'before', 'after', 'replaceWith']) {
|
|
6100
|
+
const original = jQuery.fn[name];
|
|
6101
|
+
if (typeof original !== 'function')
|
|
6102
|
+
continue;
|
|
6103
|
+
jQuery.fn[name] = function (...args) {
|
|
6104
|
+
for (const arg of args)
|
|
6105
|
+
assert_no_manual_sid(arg, '.' + name + '()');
|
|
6106
|
+
return original.apply(this, args);
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
// $('<div data-sid="x">') - catch the element at construction time. A
|
|
6110
|
+
// selector like $('[data-sid="x"]') parses to no data-sid element, so it is
|
|
6111
|
+
// not flagged here (selector misuse is handled by .find()/.on()).
|
|
6112
|
+
const originalInit = jQuery.fn.init;
|
|
6113
|
+
const PatchedInit = function (selector, context, root) {
|
|
6114
|
+
if (typeof selector === 'string')
|
|
6115
|
+
assert_no_manual_sid(selector, '$()');
|
|
6116
|
+
return new originalInit(selector, context, root);
|
|
6117
|
+
};
|
|
6118
|
+
PatchedInit.prototype = jQuery.fn;
|
|
6119
|
+
jQuery.fn.init = PatchedInit;
|
|
6120
|
+
}
|
|
6121
|
+
// Install the single debounced window resize listener that drives
|
|
6122
|
+
// on_viewport_resize() on every component. Idempotent.
|
|
6123
|
+
init_viewport_resize(jQuery);
|
|
5015
6124
|
}
|
|
5016
6125
|
// Try to auto-initialize if global jQuery exists
|
|
5017
6126
|
if (typeof window !== 'undefined' && window.jQuery) {
|
|
@@ -5025,7 +6134,9 @@ if (typeof window !== 'undefined' && window.jQuery) {
|
|
|
5025
6134
|
*/
|
|
5026
6135
|
// Core exports
|
|
5027
6136
|
// Main initialization function
|
|
5028
|
-
function init(jQuery) {
|
|
6137
|
+
function init(jQuery, config) {
|
|
6138
|
+
// Integration config first - it governs behaviour during the very first render
|
|
6139
|
+
configure(config);
|
|
5029
6140
|
// If jQuery is provided, initialize the plugin
|
|
5030
6141
|
if (jQuery) {
|
|
5031
6142
|
init_jquery_plugin(jQuery);
|
|
@@ -5039,7 +6150,7 @@ function init(jQuery) {
|
|
|
5039
6150
|
}
|
|
5040
6151
|
}
|
|
5041
6152
|
// Version - will be replaced during build with actual version from package.json
|
|
5042
|
-
const version = '2.3.
|
|
6153
|
+
const version = '2.3.54';
|
|
5043
6154
|
// Default export with all functionality
|
|
5044
6155
|
const jqhtml = {
|
|
5045
6156
|
// Core
|
|
@@ -5067,6 +6178,9 @@ const jqhtml = {
|
|
|
5067
6178
|
__version: version,
|
|
5068
6179
|
// state facts
|
|
5069
6180
|
tombstone: 'pepperoni and cheese',
|
|
6181
|
+
// Integration configuration - see config.ts
|
|
6182
|
+
configure,
|
|
6183
|
+
get_config,
|
|
5070
6184
|
// Debug settings
|
|
5071
6185
|
debug: {
|
|
5072
6186
|
enabled: false,
|
|
@@ -5136,7 +6250,13 @@ const jqhtml = {
|
|
|
5136
6250
|
// Required for ES6 class instances to be stored and restored from localStorage
|
|
5137
6251
|
register_cache_class,
|
|
5138
6252
|
// Boot - hydrate server-rendered component placeholders
|
|
5139
|
-
boot
|
|
6253
|
+
boot,
|
|
6254
|
+
// SSR Preload - capture data during SSR, replay on client to skip on_load()
|
|
6255
|
+
start_data_capture,
|
|
6256
|
+
get_captured_data,
|
|
6257
|
+
stop_data_capture,
|
|
6258
|
+
set_preload_data,
|
|
6259
|
+
clear_preload_data
|
|
5140
6260
|
};
|
|
5141
6261
|
// Auto-register on window for browser environments
|
|
5142
6262
|
// This is REQUIRED for compiled templates which use window.jqhtml.register_template()
|
|
@@ -5166,5 +6286,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
|
|
|
5166
6286
|
}
|
|
5167
6287
|
}
|
|
5168
6288
|
|
|
5169
|
-
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, escape_html_nl2br, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, version };
|
|
6289
|
+
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, clear_preload_data, configure, create_component, jqhtml as default, devWarn, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_config, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, set_preload_data, start_data_capture, stop_data_capture, version };
|
|
5170
6290
|
//# sourceMappingURL=index.js.map
|