@jqhtml/core 2.3.71 → 2.3.72

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/dist/index.d.ts CHANGED
@@ -39,6 +39,8 @@ export { Jqhtml_Local_Storage, register_cache_class };
39
39
  export type { CacheMode, Cache_Class_Options } from './local-storage.js';
40
40
  import { Load_Coordinator } from './load-coordinator.js';
41
41
  export { Load_Coordinator };
42
+ import { tracked_attachment_count, attachment_observer_active } from './dom-attachment.js';
43
+ export { tracked_attachment_count, attachment_observer_active };
42
44
  import { start_data_capture, get_captured_data, stop_data_capture, set_preload_data, clear_preload_data } from './preload-data.js';
43
45
  export { start_data_capture, get_captured_data, stop_data_capture, set_preload_data, clear_preload_data };
44
46
  export type { PreloadEntry } from './preload-data.js';
@@ -118,6 +120,8 @@ declare const jqhtml: {
118
120
  stop_data_capture: typeof stop_data_capture;
119
121
  set_preload_data: typeof set_preload_data;
120
122
  clear_preload_data: typeof clear_preload_data;
123
+ tracked_attachment_count: typeof tracked_attachment_count;
124
+ attachment_observer_active: typeof attachment_observer_active;
121
125
  };
122
126
  export default jqhtml;
123
127
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -214,12 +214,17 @@ class LifecycleManager {
214
214
  return LifecycleManager.instance;
215
215
  }
216
216
  constructor() {
217
- this.active_components = new Set();
218
- // No automatic cleanup on DOM removal - components stop explicitly via:
219
- // 1. Parent component re-render (calls .empty() which stops children)
220
- // 2. Manual .empty(), .html(), .text() calls (overridden to stop children)
217
+ // Components stop explicitly via:
218
+ // 1. Parent component re-render (stops descendants before clearing innerHTML)
219
+ // 2. Any jQuery removal that reaches jQuery.cleanData (.remove(), .empty(),
220
+ // .html(x), .text(x), .replaceWith(), .replaceAll()) - hooked in jquery-plugin.ts
221
221
  // 3. Explicit .stop() call
222
- // This allows components to be moved in DOM without lifecycle interruption
222
+ // Native DOM removal is NOT observed for stop; 'detach' (dom-attachment.ts) is the
223
+ // signal for that. Moving a component within the document never stops it.
224
+ //
225
+ // There is deliberately no registry of live components here. The DOM (via
226
+ // element.data('_component')) is the only owner; a registry would pin every
227
+ // component that left the document without being stopped.
223
228
  }
224
229
  /**
225
230
  * Boot a component - run its full lifecycle
@@ -230,7 +235,6 @@ class LifecycleManager {
230
235
  * - _load_render_only: on_create + render + on_load + re-render. No on_render, no on_loaded, no on_ready.
231
236
  */
232
237
  async boot_component(component) {
233
- this.active_components.add(component);
234
238
  try {
235
239
  // Create phase - SYNCHRONOUS, runs BEFORE first render
236
240
  component.create();
@@ -391,12 +395,6 @@ class LifecycleManager {
391
395
  }
392
396
  }
393
397
  }
394
- /**
395
- * Unregister a component (called on destroy)
396
- */
397
- unregister_component(component) {
398
- this.active_components.delete(component);
399
- }
400
398
  }
401
399
 
402
400
  /**
@@ -1615,7 +1613,7 @@ function serialize_for_cache_key(value) {
1615
1613
  const class_registry = Object.create(null);
1616
1614
  // Markers for serialized class instances (unique to avoid collisions with user data)
1617
1615
  // Replaced at build time by rollup (see rollup.config.js).
1618
- const CORE_VERSION = '2.3.71';
1616
+ const CORE_VERSION = '2.3.72';
1619
1617
  const CLASS_MARKER = '__jqhtml_class__';
1620
1618
  const PROPS_MARKER = '__jqhtml_props__';
1621
1619
  /**
@@ -3176,6 +3174,85 @@ async function execute_on_load_detached(component, dedup_key = null) {
3176
3174
  };
3177
3175
  }
3178
3176
 
3177
+ /**
3178
+ * JQHTML DOM Attachment Tracking
3179
+ *
3180
+ * Backs the component 'attach' / 'detach' lifecycle events.
3181
+ *
3182
+ * One document-level MutationObserver (childList + subtree) is installed the first
3183
+ * time any component expresses interest in attachment, and disconnected when the
3184
+ * last interested component is stopped or collected. The observer never inspects
3185
+ * mutation records: it is only a "the tree changed" tick. On each batch it walks
3186
+ * the tracked components and asks each to compare its root's isConnected against
3187
+ * its own recorded attachment state (Jqhtml_Component._attachment_check()). Cost is
3188
+ * O(tracked components) per mutation batch, independent of how much of the page
3189
+ * changed, and zero when nothing has opted in.
3190
+ *
3191
+ * Because MutationObserver delivers per microtask, a node moved synchronously
3192
+ * (remove + insert in one task, e.g. appendTo) never shows a transition. Only a
3193
+ * component whose root is actually off-document when the batch flushes is detached.
3194
+ *
3195
+ * Tracked components are held by WeakRef. A component that is removed from the
3196
+ * document natively (never stopped) and then dropped by everything else is therefore
3197
+ * collectable: the tracker pins nothing. Dead refs are pruned on the next batch.
3198
+ */
3199
+ const tracked = new Set();
3200
+ const ref_of = new WeakMap();
3201
+ let observer = null;
3202
+ function on_mutation() {
3203
+ for (const ref of tracked) {
3204
+ const component = ref.deref();
3205
+ if (!component || component._stopped) {
3206
+ tracked.delete(ref);
3207
+ continue;
3208
+ }
3209
+ component._attachment_check();
3210
+ }
3211
+ if (tracked.size === 0)
3212
+ disconnect();
3213
+ }
3214
+ function connect() {
3215
+ if (observer !== null)
3216
+ return;
3217
+ if (typeof MutationObserver === 'undefined' || typeof document === 'undefined')
3218
+ return;
3219
+ observer = new MutationObserver(on_mutation);
3220
+ observer.observe(document.documentElement, { childList: true, subtree: true });
3221
+ }
3222
+ function disconnect() {
3223
+ if (observer === null)
3224
+ return;
3225
+ observer.disconnect();
3226
+ observer = null;
3227
+ }
3228
+ /** Start watching a component's root for attach/detach transitions. Idempotent. */
3229
+ function track_attachment(component) {
3230
+ if (ref_of.has(component))
3231
+ return;
3232
+ const ref = new WeakRef(component);
3233
+ ref_of.set(component, ref);
3234
+ tracked.add(ref);
3235
+ connect();
3236
+ }
3237
+ /** Stop watching a component (called from _stop()). Idempotent. */
3238
+ function untrack_attachment(component) {
3239
+ const ref = ref_of.get(component);
3240
+ if (!ref)
3241
+ return;
3242
+ ref_of.delete(component);
3243
+ tracked.delete(ref);
3244
+ if (tracked.size === 0)
3245
+ disconnect();
3246
+ }
3247
+ /** Number of components currently tracked (diagnostics / tests). */
3248
+ function tracked_attachment_count() {
3249
+ return tracked.size;
3250
+ }
3251
+ /** Whether the document observer is currently installed (diagnostics / tests). */
3252
+ function attachment_observer_active() {
3253
+ return observer !== null;
3254
+ }
3255
+
3179
3256
  /**
3180
3257
  * JQHTML Component Cache Integration
3181
3258
  *
@@ -4084,6 +4161,12 @@ class Jqhtml_Component {
4084
4161
  this.__load_gates = []; // Pending gates (cleared once the first load begins)
4085
4162
  this.__first_load_started = false; // Latch: true once the first load phase begins (enforces one-shot + throw-after)
4086
4163
  this.__gate_resume = null; // Resolver to release an in-progress gate wait early (reload/refresh/stop)
4164
+ // DOM attachment state machine - see _attachment_check() / dom-attachment.ts.
4165
+ // _attached is the component's OWN belief about whether it is in the document;
4166
+ // it flips at most once per direction, and 'attach' / 'detach' fire only on a flip.
4167
+ this._attached = false;
4168
+ this._attachment_checked = false; // First check has run (after the first 'ready' dispatch)
4169
+ this._attachment_tracking = false; // Something is interested: on_attach/on_detach overridden, or .on('attach'|'detach')
4087
4170
  // Detect custom on_load() override immediately
4088
4171
  // Prototype chain is set up before constructor runs, so this.on_load
4089
4172
  // correctly resolves to child class override if one exists
@@ -4157,6 +4240,12 @@ class Jqhtml_Component {
4157
4240
  // Initialize this.state as an empty object (convention for arbitrary component state)
4158
4241
  // No special meaning to framework - mutable anywhere, not cached, not frozen
4159
4242
  this.state = {};
4243
+ // Overriding on_attach()/on_detach() is interest in attachment tracking. Tracking
4244
+ // itself only starts after the first 'ready' dispatch (see _attachment_check()).
4245
+ if (this.on_attach !== Jqhtml_Component.prototype.on_attach ||
4246
+ this.on_detach !== Jqhtml_Component.prototype.on_detach) {
4247
+ this._attachment_tracking = true;
4248
+ }
4160
4249
  this._log_lifecycle('construct', 'complete');
4161
4250
  }
4162
4251
  /**
@@ -4170,7 +4259,9 @@ class Jqhtml_Component {
4170
4259
  on_render: 'Use render() to trigger a re-render.',
4171
4260
  on_load: 'Use reload() to refresh data.',
4172
4261
  on_ready: 'Called automatically when ready.',
4173
- on_stop: 'Use stop() to stop the component.'
4262
+ on_stop: 'Use stop() to stop the component.',
4263
+ on_attach: 'Called automatically when the component enters the document.',
4264
+ on_detach: 'Called automatically when the component leaves the document.'
4174
4265
  };
4175
4266
  const impls = {};
4176
4267
  const self = this;
@@ -5524,8 +5615,13 @@ class Jqhtml_Component {
5524
5615
  // stopped children into the parent's registry.
5525
5616
  this._log_lifecycle('destroy', 'start');
5526
5617
  this.$.addClass('_Component_Stopped');
5527
- // Unregister from lifecycle manager
5528
- this._lifecycle_manager.unregister_component(this);
5618
+ // A component that believes it is attached is detached first, so 'detach' always
5619
+ // precedes 'stop' and subscribers can rely on strict attach/detach alternation.
5620
+ // A component that never fired 'attach' (stopped before ready, or born and kept
5621
+ // off-document) fires no 'detach' either. Either way it leaves the tracker.
5622
+ if (this._attached)
5623
+ this._transition_attachment(false);
5624
+ untrack_attachment(this);
5529
5625
  // Call user's on_stop() hook with authorization (sync) - only when actually
5530
5626
  // overridden, since _call_lifecycle_sync() on the base no-op costs per child
5531
5627
  // on every re-render.
@@ -5570,6 +5666,89 @@ class Jqhtml_Component {
5570
5666
  on_loaded() { } // Override to process loaded data (e.g., clone this.data to this.state)
5571
5667
  async on_ready() { }
5572
5668
  on_stop() { }
5669
+ on_attach() { } // Sync. Root entered the document (first checked after 'ready')
5670
+ on_detach() { } // Sync. Root left the document, or the component is being stopped while attached
5671
+ // -------------------------------------------------------------------------
5672
+ // DOM attachment state machine
5673
+ // -------------------------------------------------------------------------
5674
+ /**
5675
+ * Whether this component currently considers itself attached to the document.
5676
+ *
5677
+ * This is the component's recorded state, not a live isConnected read: it is
5678
+ * false until the first check after 'ready', and from then on flips only when a
5679
+ * transition is observed (or on stop()). It is exactly the state 'attach' and
5680
+ * 'detach' have reported.
5681
+ */
5682
+ is_attached() {
5683
+ return this._attached;
5684
+ }
5685
+ /**
5686
+ * Flip the recorded state and fire the matching hook + event. The opposite
5687
+ * sticky event is invalidated first, so a late subscriber only ever sees the
5688
+ * current state. Hook errors are isolated; the event still fires.
5689
+ * @private
5690
+ */
5691
+ _transition_attachment(attached) {
5692
+ this._attached = attached;
5693
+ const event_name = attached ? 'attach' : 'detach';
5694
+ const hook_name = attached ? 'on_attach' : 'on_detach';
5695
+ this.invalidate(attached ? 'detach' : 'attach');
5696
+ if (this[hook_name] !== Jqhtml_Component.prototype[hook_name]) {
5697
+ try {
5698
+ const result = this._call_lifecycle_sync(hook_name);
5699
+ if (result && typeof result.then === 'function') {
5700
+ console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from ${hook_name}(). ` +
5701
+ `${hook_name}() must be synchronous code. Remove 'async' from the function declaration.`);
5702
+ }
5703
+ }
5704
+ catch (error) {
5705
+ console.error(`[JQHTML] Error in ${hook_name}() for ${this.component_name()}:`, error);
5706
+ }
5707
+ }
5708
+ this.trigger(event_name);
5709
+ }
5710
+ /**
5711
+ * Compare the root's live connectivity with the recorded state and transition
5712
+ * on a difference. Idempotent. Called by the document observer on every
5713
+ * mutation batch for tracked components, and directly by the framework after
5714
+ * 'ready' and when interest is registered late.
5715
+ * @private
5716
+ */
5717
+ _attachment_check() {
5718
+ if (this._stopped)
5719
+ return;
5720
+ const connected = !!this.$[0].isConnected;
5721
+ if (connected !== this._attached)
5722
+ this._transition_attachment(connected);
5723
+ }
5724
+ /**
5725
+ * The first check, run once the 'ready' handlers have completed. Fires 'attach'
5726
+ * if the component was born attached, even with no listener, so a later
5727
+ * .on('attach') gets the sticky fire. Deferred interest starts tracking here.
5728
+ * @private
5729
+ */
5730
+ _attachment_first_check() {
5731
+ const first = !this._attachment_checked;
5732
+ this._attachment_checked = true;
5733
+ this._attachment_check();
5734
+ if (first && this._attachment_tracking)
5735
+ track_attachment(this);
5736
+ }
5737
+ /**
5738
+ * Register interest in attach/detach. Before the first ready check this only
5739
+ * records the interest; after it, tracking starts immediately and a check runs
5740
+ * at once, so a late subscriber on a natively-removed component sees 'detach'.
5741
+ * @private
5742
+ */
5743
+ _watch_attachment() {
5744
+ if (this._attachment_tracking)
5745
+ return;
5746
+ this._attachment_tracking = true;
5747
+ if (this._attachment_checked && !this._stopped) {
5748
+ track_attachment(this);
5749
+ this._attachment_check();
5750
+ }
5751
+ }
5573
5752
  /**
5574
5753
  * Called with the viewport width in CSS pixels:
5575
5754
  * - after every on_render()
@@ -5634,6 +5813,8 @@ class Jqhtml_Component {
5634
5813
  * @see component-events.ts for full documentation
5635
5814
  */
5636
5815
  on(event_name, callback) {
5816
+ if (event_name === 'attach' || event_name === 'detach')
5817
+ this._watch_attachment();
5637
5818
  return event_on(this, event_name, callback);
5638
5819
  }
5639
5820
  /**
@@ -5642,14 +5823,24 @@ class Jqhtml_Component {
5642
5823
  * @see component-events.ts for full documentation
5643
5824
  */
5644
5825
  once(event_name, callback) {
5826
+ if (event_name === 'attach' || event_name === 'detach')
5827
+ this._watch_attachment();
5645
5828
  return event_once(this, event_name, callback);
5646
5829
  }
5647
5830
  /**
5648
5831
  * Trigger a lifecycle event - delegates to component-events.ts
5649
5832
  * @see component-events.ts for full documentation
5833
+ *
5834
+ * 'ready' is special: once its handlers have run, the component runs its first
5835
+ * attachment check (and every later one, idempotently). Doing it here rather than
5836
+ * at each of the call sites that trigger 'ready' guarantees the order
5837
+ * on_ready() -> 'ready' handlers -> 'attach', so everything on_ready() set up is
5838
+ * complete before anyone learns the component is in the document.
5650
5839
  */
5651
5840
  trigger(event_name, data) {
5652
5841
  event_trigger(this, event_name, data);
5842
+ if (event_name === 'ready' && !this._stopped)
5843
+ this._attachment_first_check();
5653
5844
  }
5654
5845
  /**
5655
5846
  * Check if any callbacks are registered for a given event
@@ -7083,9 +7274,42 @@ function init_jquery_plugin(jQuery) {
7083
7274
  return jQuery(results);
7084
7275
  };
7085
7276
  // Store original jQuery methods
7086
- const originalEmpty = jQuery.fn.empty;
7087
7277
  const originalHtml = jQuery.fn.html;
7088
- const originalText = jQuery.fn.text;
7278
+ // ---------------------------------------------------------------------------
7279
+ // Stop components on every jQuery-driven removal by hooking jQuery.cleanData.
7280
+ //
7281
+ // jQuery routes .remove(), .empty(), .html(x), .text(x), .replaceWith() and
7282
+ // .replaceAll() through jQuery.cleanData(elems) to drop the data store and
7283
+ // handlers of every element about to leave the tree - and `elems` is already the
7284
+ // flattened subtree, in document order (parent before child). Stopping here is
7285
+ // therefore one classList check per removed element, no extra traversal, and
7286
+ // covers every jQuery removal path at once. It is the same hook jQuery UI's
7287
+ // widget factory uses for the same reason.
7288
+ //
7289
+ // It MUST be installed on the original jQuery object: jQuery's own code calls
7290
+ // cleanData through its closure-local reference, not through our wrapper. The
7291
+ // wrapper's copied static is updated below so both names agree.
7292
+ //
7293
+ // .detach() does NOT call cleanData, by jQuery's own design (it keeps data for
7294
+ // reinsertion), and native DOM removal (el.remove(), innerHTML = '') bypasses
7295
+ // jQuery entirely. Neither stops a component; the 'detach' lifecycle event is
7296
+ // the signal for those. Moving a component (.appendTo) is not a removal.
7297
+ // ---------------------------------------------------------------------------
7298
+ const originalCleanData = _jqhtml_original_jquery.cleanData;
7299
+ const cleanDataWithComponentStop = function (elems) {
7300
+ for (let i = 0; i < elems.length; i++) {
7301
+ const el = elems[i];
7302
+ if (el.nodeType !== 1 || !el.classList.contains('Component'))
7303
+ continue;
7304
+ const component = _jqhtml_original_jquery.data(el, '_component');
7305
+ if (component && !component._stopped) {
7306
+ component._stop(); // Not recursive: every descendant is in elems too
7307
+ }
7308
+ }
7309
+ return originalCleanData(elems);
7310
+ };
7311
+ _jqhtml_original_jquery.cleanData = cleanDataWithComponentStop;
7312
+ jQuery.cleanData = cleanDataWithComponentStop;
7089
7313
  // ---------------------------------------------------------------------------
7090
7314
  // Guard against hand-creating elements with data-sid via jQuery.
7091
7315
  //
@@ -7142,53 +7366,15 @@ function init_jquery_plugin(jQuery) {
7142
7366
  ` • <div class="MyComponent__${sid}"> → .find('.MyComponent__${sid}')`);
7143
7367
  };
7144
7368
  /**
7145
- * Override jQuery.fn.empty() to stop child components before clearing DOM
7146
- * This ensures component cleanup hooks fire when DOM is cleared
7147
- */
7148
- jQuery.fn.empty = function () {
7149
- return this.each(function () {
7150
- // Stop all child components before clearing DOM
7151
- jQuery(this).find('.Component').each(function () {
7152
- const component = jQuery(this).data('_component');
7153
- if (component && !component._stopped) {
7154
- component._stop(); // Stop just the component, DOM will be cleared anyway
7155
- }
7156
- });
7157
- // Call original empty
7158
- originalEmpty.call(jQuery(this));
7159
- });
7160
- };
7161
- /**
7162
- * Override jQuery.fn.html() to stop child components when setting new content
7163
- * When used as setter, calls .empty() first to properly cleanup components
7369
+ * Override jQuery.fn.html() setter to reject hand-written data-sid before the
7370
+ * DOM is mutated. Component stopping is handled by the cleanData hook above.
7164
7371
  */
7165
7372
  jQuery.fn.html = function (value) {
7166
- // Getter - just pass through
7167
7373
  if (arguments.length === 0) {
7168
7374
  return originalHtml.call(this);
7169
7375
  }
7170
- // Reject hand-written data-sid before mutating the DOM
7171
7376
  assert_no_manual_sid(value, '.html()');
7172
- // Setter - empty first (which stops components), then set content
7173
- return this.each(function () {
7174
- jQuery(this).empty();
7175
- originalHtml.call(jQuery(this), value);
7176
- });
7177
- };
7178
- /**
7179
- * Override jQuery.fn.text() to stop child components when setting new content
7180
- * When used as setter, calls .empty() first to properly cleanup components
7181
- */
7182
- jQuery.fn.text = function (value) {
7183
- // Getter - just pass through
7184
- if (arguments.length === 0) {
7185
- return originalText.call(this);
7186
- }
7187
- // Setter - empty first (which stops components), then set content
7188
- return this.each(function () {
7189
- jQuery(this).empty();
7190
- originalText.call(jQuery(this), value);
7191
- });
7377
+ return originalHtml.call(this, value);
7192
7378
  };
7193
7379
  // Store original jQuery.fn.on
7194
7380
  const originalOn = jQuery.fn.on;
@@ -7330,7 +7516,7 @@ function init(jQuery, config) {
7330
7516
  }
7331
7517
  }
7332
7518
  // Version - will be replaced during build with actual version from package.json
7333
- const version = '2.3.71';
7519
+ const version = '2.3.72';
7334
7520
  // Default export with all functionality
7335
7521
  const jqhtml = {
7336
7522
  // Core
@@ -7445,7 +7631,11 @@ const jqhtml = {
7445
7631
  get_captured_data,
7446
7632
  stop_data_capture,
7447
7633
  set_preload_data,
7448
- clear_preload_data
7634
+ clear_preload_data,
7635
+ // Attachment tracking diagnostics - how many components are watched for
7636
+ // attach/detach and whether the document observer is currently installed
7637
+ tracked_attachment_count,
7638
+ attachment_observer_active
7449
7639
  };
7450
7640
  // Auto-register on window for browser environments
7451
7641
  // This is REQUIRED for compiled templates which use window.jqhtml.register_template()
@@ -7475,5 +7665,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
7475
7665
  }
7476
7666
  }
7477
7667
 
7478
- export { COMPONENT_NAME_PATTERN, COMPONENT_NAME_RULE, Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, add_object_printer, applyDebugDelay, boot, clear_preload_data, configure, create_component, debug_overlay, jqhtml as default, devWarn, dynamic_component_name, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_config, get_object_printers, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, is_component_name, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, print_object, process_instructions, register, register_cache_class, register_component, register_template, set_preload_data, start_data_capture, stop_data_capture, version };
7668
+ export { COMPONENT_NAME_PATTERN, COMPONENT_NAME_RULE, Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, add_object_printer, applyDebugDelay, attachment_observer_active, boot, clear_preload_data, configure, create_component, debug_overlay, jqhtml as default, devWarn, dynamic_component_name, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_config, get_object_printers, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, is_component_name, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, print_object, process_instructions, register, register_cache_class, register_component, register_template, set_preload_data, start_data_capture, stop_data_capture, tracked_attachment_count, version };
7479
7669
  //# sourceMappingURL=index.js.map