@repobit/dex-store-elements 1.4.2 → 1.4.3

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +15 -4
  3. package/dist/src/actions/action.button.js +4 -11
  4. package/dist/src/actions/action.button.js.map +1 -1
  5. package/dist/src/actions/action.input.js +28 -22
  6. package/dist/src/actions/action.input.js.map +1 -1
  7. package/dist/src/actions/action.select.js +10 -8
  8. package/dist/src/actions/action.select.js.map +1 -1
  9. package/dist/src/actions/index.d.ts +1 -1
  10. package/dist/src/actions/index.js +45 -49
  11. package/dist/src/actions/index.js.map +1 -1
  12. package/dist/src/actions/utilty.d.ts +1 -0
  13. package/dist/src/actions/utilty.js +59 -29
  14. package/dist/src/actions/utilty.js.map +1 -1
  15. package/dist/src/contexts/context.datalayer.js.map +1 -1
  16. package/dist/src/contexts/context.derived.js.map +1 -1
  17. package/dist/src/contexts/context.option.js.map +1 -1
  18. package/dist/src/contexts/context.product.js.map +1 -1
  19. package/dist/src/contexts/context.state.js.map +1 -1
  20. package/dist/src/contexts/context.store.js.map +1 -1
  21. package/dist/src/controllers/collect-controller.d.ts +39 -0
  22. package/dist/src/controllers/collect-controller.js +161 -0
  23. package/dist/src/controllers/collect-controller.js.map +1 -0
  24. package/dist/src/controllers/compute-controller.d.ts +55 -0
  25. package/dist/src/controllers/compute-controller.js +350 -0
  26. package/dist/src/controllers/compute-controller.js.map +1 -0
  27. package/dist/src/controllers/eta-renderer/eta-dom-renderer.d.ts +21 -0
  28. package/dist/src/controllers/eta-renderer/eta-dom-renderer.js +152 -0
  29. package/dist/src/controllers/eta-renderer/eta-dom-renderer.js.map +1 -0
  30. package/dist/src/controllers/eta-renderer/eta-mutation-observer.d.ts +28 -0
  31. package/dist/src/controllers/eta-renderer/eta-mutation-observer.js +206 -0
  32. package/dist/src/controllers/eta-renderer/eta-mutation-observer.js.map +1 -0
  33. package/dist/src/controllers/eta-renderer/eta-renderer.d.ts +21 -0
  34. package/dist/src/controllers/eta-renderer/eta-renderer.js +77 -0
  35. package/dist/src/controllers/eta-renderer/eta-renderer.js.map +1 -0
  36. package/dist/src/controllers/eta-renderer/eta-template-cache.d.ts +21 -0
  37. package/dist/src/controllers/eta-renderer/eta-template-cache.js +52 -0
  38. package/dist/src/controllers/eta-renderer/eta-template-cache.js.map +1 -0
  39. package/dist/src/controllers/eta-renderer/eta-types.d.ts +11 -0
  40. package/dist/src/controllers/eta-renderer/eta-types.js +1 -0
  41. package/dist/src/controllers/eta-renderer/eta-types.js.map +1 -0
  42. package/dist/src/controllers/eta-renderer/index.d.ts +2 -0
  43. package/dist/src/controllers/eta-renderer/index.js +2 -0
  44. package/dist/src/controllers/eta-renderer/index.js.map +1 -0
  45. package/dist/src/controllers/event-pipeline-controller.d.ts +28 -0
  46. package/dist/src/controllers/event-pipeline-controller.js +72 -0
  47. package/dist/src/controllers/event-pipeline-controller.js.map +1 -0
  48. package/dist/src/dsl/compilers/array/compiler.js.map +1 -1
  49. package/dist/src/dsl/compilers/boolean/compiler.js.map +1 -1
  50. package/dist/src/dsl/compilers/enum/compiler.js.map +1 -1
  51. package/dist/src/dsl/compilers/index.js.map +1 -1
  52. package/dist/src/dsl/utilty.js.map +1 -1
  53. package/dist/src/events/events.js.map +1 -1
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/nodes/node.context.js.map +1 -1
  56. package/dist/src/nodes/node.option.d.ts +4 -2
  57. package/dist/src/nodes/node.option.js +56 -56
  58. package/dist/src/nodes/node.option.js.map +1 -1
  59. package/dist/src/nodes/node.product.d.ts +4 -3
  60. package/dist/src/nodes/node.product.js +16 -28
  61. package/dist/src/nodes/node.product.js.map +1 -1
  62. package/dist/src/nodes/node.root.js.map +1 -1
  63. package/dist/src/nodes/node.state.d.ts +18 -70
  64. package/dist/src/nodes/node.state.js +42 -862
  65. package/dist/src/nodes/node.state.js.map +1 -1
  66. package/dist/src/renders/attributes/buyLink.js +12 -8
  67. package/dist/src/renders/attributes/buyLink.js.map +1 -1
  68. package/dist/src/renders/attributes/devices.js +10 -10
  69. package/dist/src/renders/attributes/devices.js.map +1 -1
  70. package/dist/src/renders/attributes/discount.js +60 -81
  71. package/dist/src/renders/attributes/discount.js.map +1 -1
  72. package/dist/src/renders/attributes/hide.js +21 -26
  73. package/dist/src/renders/attributes/hide.js.map +1 -1
  74. package/dist/src/renders/attributes/index.js.map +1 -1
  75. package/dist/src/renders/attributes/price.js +60 -81
  76. package/dist/src/renders/attributes/price.js.map +1 -1
  77. package/dist/src/renders/attributes/subscription.js +11 -11
  78. package/dist/src/renders/attributes/subscription.js.map +1 -1
  79. package/dist/src/renders/attributes/trialLink.js +5 -4
  80. package/dist/src/renders/attributes/trialLink.js.map +1 -1
  81. package/dist/src/renders/attributes/utilty.d.ts +2 -0
  82. package/dist/src/renders/attributes/utilty.js +3 -0
  83. package/dist/src/renders/attributes/utilty.js.map +1 -0
  84. package/dist/src/renders/context.js.map +1 -1
  85. package/dist/src/renders/format.js.map +1 -1
  86. package/dist/src/renders/index.d.ts +1 -1
  87. package/dist/src/renders/index.js +46 -50
  88. package/dist/src/renders/index.js.map +1 -1
  89. package/dist/src/renders/observe.js.map +1 -1
  90. package/dist/src/renders/utility.js +46 -38
  91. package/dist/src/renders/utility.js.map +1 -1
  92. package/dist/src/templating/eta.js.map +1 -1
  93. package/package.json +2 -2
  94. package/dist/src/contexts/context.event.d.ts +0 -5
  95. package/dist/src/contexts/context.event.js +0 -3
  96. package/dist/src/contexts/context.event.js.map +0 -1
@@ -5,17 +5,17 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { derivedContext } from "../contexts/context.derived.js";
8
- import { eventContext } from "../contexts/context.event.js";
9
8
  import { stateContext } from "../contexts/context.state.js";
10
9
  import { storeContext } from "../contexts/context.store.js";
11
- import { ActionEvent, CollectActionEvent, CollectChildEvent, CollectChildRemovedEvent, CollectOptionEvent, CollectUpdateByDeltaEvent, UpdateByDeltaEvent } from "../events/events.js";
10
+ import { CollectController } from "../controllers/collect-controller.js";
11
+ import { ComputeController } from "../controllers/compute-controller.js";
12
+ import { EtaRenderController } from "../controllers/eta-renderer/index.js";
13
+ import { EventPipelineController } from "../controllers/event-pipeline-controller.js";
14
+ import { ActionEvent, CollectOptionEvent, UpdateByDeltaEvent } from "../events/events.js";
12
15
  import { toDSLContext } from "../renders/context.js";
13
- import eta from "../templating/eta.js";
14
16
  import { consume, provide } from "@lit/context";
15
- import { Task } from "@lit/task";
16
17
  import { html, LitElement } from "lit";
17
18
  import { property } from "lit/decorators.js";
18
- import morph from 'nanomorph';
19
19
  if (!window.Promise.withResolvers) {
20
20
  window.Promise.withResolvers = function () {
21
21
  let resolve, reject;
@@ -147,598 +147,50 @@ export class StateNode extends LitElement {
147
147
  }
148
148
  }
149
149
  };
150
- this._computeTask = new Task(this, {
151
- task: async (_, { signal }) => {
152
- const isActive = () => {
153
- if (signal.aborted) {
154
- throw new DOMException('Task aborted', `AbortError ${this.storeName.toString()}`);
155
- }
156
- };
157
- // runs whenever _store or version changes
158
- try {
159
- const computed = await this._computeState(isActive);
160
- await this._computeContext(computed, isActive);
161
- return computed ?? [];
162
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
163
- }
164
- catch (e) { /* empty */ }
165
- },
166
- args: () => [this._store]
167
- });
168
- // Default event forwarder for simple pass-through nodes (e.g., root/context)
169
- this._defaultForwardEventTask = new Task(this, {
170
- task: async ([evt, auto]) => {
171
- if (!evt || !auto)
172
- return;
173
- this._forwardEvent(evt);
174
- this._notifyParent();
175
- },
176
- args: () => [this._event, this.autoForward]
177
- });
178
- // Keep `_event` in sync with either DOM events or parent context,
179
- // depending on `ignoreEventsParent`.
180
- this._syncEventTask = new Task(this, {
181
- task: async ([fromParent, fromDom, ignoreParent]) => {
182
- // Track last-seen refs so we can choose the freshest source
183
- const domChanged = fromDom !== this._prevDomEventRef;
184
- const parentChanged = fromParent !== this._prevParentEventRef;
185
- this._prevDomEventRef = fromDom;
186
- this._prevParentEventRef = fromParent;
187
- if (ignoreParent) {
188
- this._event = fromDom ?? undefined;
189
- return;
190
- }
191
- if (domChanged && !parentChanged) {
192
- this._event = fromDom ?? undefined;
193
- return;
194
- }
195
- if (parentChanged && !domChanged) {
196
- this._event = fromParent ?? undefined;
197
- return;
198
- }
199
- if (domChanged && parentChanged) {
200
- // If both changed in the same microtask, prefer DOM-originated
201
- this._event = fromDom ?? fromParent ?? undefined;
202
- return;
203
- }
204
- // Neither changed; keep current, but if nothing set yet, fall back
205
- if (this._event === undefined) {
206
- this._event = fromDom ?? fromParent ?? undefined;
207
- }
208
- },
209
- args: () => [this._eventParent, this._eventDom, this.ignoreEventsParent]
210
- });
211
- this._collectToggleTask = new Task(this, {
212
- task: async ([noCollect]) => {
213
- const prev = this._prevCollect;
214
- this._prevCollect = !noCollect; // track last 'collect' state for edge detection
215
- if (prev === undefined) {
216
- // Initial mount: register only if collecting (noCollect=false)
217
- if (!noCollect)
218
- this._notifyParent();
219
- return;
220
- }
221
- // prev represents prior collect state
222
- const currentCollect = !noCollect;
223
- if (currentCollect && !prev) {
224
- this._notifyParent();
225
- }
226
- else if (!currentCollect && prev) {
227
- this.dispatchEvent(new CollectOptionEvent({ name: this.storeName, options: null }));
228
- }
229
- },
230
- args: () => [this.noCollect]
231
- });
232
- // Cache original templates per element so we can re-render from the original innerHTML
233
- this._tplElementTemplates = new WeakMap();
234
- this._tplAttrTemplates = new WeakMap();
235
- this._etaRenderTask = new Task(this, {
236
- task: async ([state, derived]) => {
237
- if (!state)
238
- return;
239
- if (!this.shouldRunEtaStateRender())
240
- return;
241
- // Provide 'it' derived from DSL context
242
- // Mark cause (mutation-triggered vs state/derived-triggered)
243
- this._etaRenderCurrentFromMutation = this._etaRenderTriggeredByMutation;
244
- this._etaRenderTriggeredByMutation = false;
245
- this._etaRenderInProgress = true;
246
- try {
247
- const it = await toDSLContext({ state, derived });
248
- await this._renderEtaTemplates(it);
249
- }
250
- finally {
251
- this._etaRenderInProgress = false;
252
- this._etaRenderCurrentFromMutation = false;
253
- // If mutations arrived while rendering, schedule one more pass
254
- if (this._etaRenderNeedsRun) {
255
- this._etaRenderNeedsRun = false;
256
- this._scheduleEtaRenderFromMutation();
257
- }
258
- this._notifyEtaIdle();
259
- }
260
- },
261
- args: () => [this.state, this._derived]
262
- });
263
- // Track DOM changes in the light DOM (slot content) to rerun Eta when SPA rewrites
264
- this._etaRenderInProgress = false;
265
- this._etaRenderScheduled = false;
266
- this._etaRenderNeedsRun = false;
267
- this._etaRenderTriggeredByMutation = false;
268
- this._etaRenderCurrentFromMutation = false;
269
- this._etaIdleWaiters = [];
270
- // Track nearest contextual children (register via CollectChildEvent)
271
- this._contextualChildren = new Set();
272
- this._onCollectChild = (e) => {
273
- if (e.target === this)
274
- return;
275
- e.stopPropagation();
276
- const { child } = e.detail;
277
- if (child && child !== this) {
278
- this._contextualChildren.add(child);
279
- }
280
- };
281
- this._onCollectChildRemoved = (e) => {
282
- if (e.target === this)
283
- return;
284
- e.stopPropagation();
285
- const { child } = e.detail;
286
- if (child && child !== this) {
287
- this._contextualChildren.delete(child);
288
- }
289
- };
150
+ this._compute = new ComputeController(this);
151
+ this._events = new EventPipelineController(this, (e) => this._forwardEvent(e), () => this._notifyParent());
152
+ this._collect = new CollectController(this, () => this._compute.abort(), () => this._compute.run(), () => this._notifyParent());
153
+ this._eta = new EtaRenderController(this);
290
154
  }
291
155
  /**
292
156
  * all options computed by this node
293
157
  * options * actions
294
158
  */
295
159
  get computedOptions() {
296
- return this._computeTask.value;
160
+ return this._compute.value;
297
161
  }
298
- // Allow subclasses (e.g., OptionNode) to disable the base Eta render
299
- // when they provide their own option-specific rendering pipeline.
162
+ // Allow subclasses to disable Eta rendering entirely.
300
163
  shouldRunEtaStateRender() { return true; }
301
- _isEtaIdle() {
302
- return !this._etaRenderInProgress && !this._etaRenderDebounce && !this._etaRenderNeedsRun;
164
+ // Allow subclasses to opt out of mutation-triggered Eta runs.
165
+ shouldObserveEtaMutations() {
166
+ return this.shouldRunEtaStateRender();
303
167
  }
304
- _waitEtaSettled() {
305
- if (this._isEtaIdle())
306
- return Promise.resolve();
307
- return new Promise((resolve) => {
308
- this._etaIdleWaiters.push(resolve);
309
- });
168
+ getEtaArgs() {
169
+ return [this.state, this._derived];
310
170
  }
311
- _notifyEtaIdle() {
312
- if (!this._isEtaIdle())
313
- return;
314
- if (this._etaIdleWaiters.length === 0)
315
- return;
316
- const waiters = this._etaIdleWaiters.splice(0);
317
- for (const w of waiters)
318
- w();
319
- }
320
- _scheduleEtaRenderFromMutation() {
321
- // If a render is already in progress, remember to run again once it finishes.
322
- if (this._etaRenderInProgress) {
323
- this._etaRenderNeedsRun = true;
324
- return;
325
- }
326
- if (!this.shouldRunEtaStateRender())
327
- return;
328
- // Adaptive coalescing: restart timer on every mutation and, if bursts continue,
329
- // increase delay up to a configured max to batch work.
330
- const base = Math.max(0, Number(this.etaMutationDelay) || 0);
331
- const max = Math.max(base, Number(this.etaMutationDelayMax) || 0);
332
- let delay = base;
333
- if (this._etaRenderDebounce) {
334
- clearTimeout(this._etaRenderDebounce);
335
- const prev = this._etaCoalesceDelay ?? base;
336
- delay = Math.min(prev > 0 ? prev * 2 : base, max);
337
- }
338
- this._etaCoalesceDelay = delay;
339
- this._etaRenderTriggeredByMutation = true;
340
- this._etaRenderDebounce = window.setTimeout(() => {
341
- this._etaRenderDebounce = undefined;
342
- this._etaCoalesceDelay = base;
343
- this._etaRenderTask.run();
344
- // If nothing else is scheduled and not in-progress, resolve any idle waiters
345
- queueMicrotask(() => this._notifyEtaIdle());
346
- }, delay);
171
+ async buildEtaContext() {
172
+ return await toDSLContext({ state: this.state, derived: this._derived });
347
173
  }
348
- _isStateNodeElement(el) {
174
+ isStateNodeElement(el) {
349
175
  if (el instanceof StateNode)
350
176
  return true;
351
177
  const t = el.tagName;
352
178
  return t === 'BD-STATE' || t === 'BD-PRODUCT' || t === 'BD-OPTION' || t === 'BD-CONTEXT';
353
179
  }
354
- // Find the nearest eligible element under this provider to use as a refresh root
355
- _findEligibleRoot(n) {
356
- let cur = n;
357
- while (cur && cur !== this) {
358
- if (cur instanceof ShadowRoot) {
359
- cur = cur.host;
360
- continue;
361
- }
362
- if (!(cur instanceof HTMLElement)) {
363
- cur = cur.parentNode;
364
- continue;
365
- }
366
- if (this._isStateNodeElement(cur) && cur !== this)
367
- return null; // nested provider boundary
368
- if (cur.hasAttribute('data-store-render'))
369
- return null; // render-managed boundary
370
- const parentProvider = cur.parentElement?.closest('bd-state,bd-product,bd-option,bd-context');
371
- if (parentProvider === this || cur.parentElement === this)
372
- return cur;
373
- cur = cur.parentNode;
374
- }
375
- return this;
376
- }
377
- _hasNestedStateNode(el) {
378
- return !!el.querySelector?.('bd-state,bd-product,bd-option,bd-context');
379
- }
380
- _safeEtaRender(entry, data, { onErrorReturnInput = false } = {}) {
381
- try {
382
- if (!entry.fn) {
383
- entry.fn = eta.compile(entry.src);
384
- }
385
- const out = eta.render(entry.fn, data);
386
- return typeof out === 'string' ? out : String(out ?? '');
387
- }
388
- catch (err) {
389
- console.error('Eta render error:', err);
390
- return onErrorReturnInput ? entry.src : '';
391
- }
392
- }
393
- async _renderEtaAttributes(el, data) {
394
- // any attribute whose value contains '{{' is treated as an Eta template
395
- const names = el.getAttributeNames();
396
- let cache = this._tplAttrTemplates.get(el);
397
- if (!cache) {
398
- cache = new Map();
399
- this._tplAttrTemplates.set(el, cache);
400
- }
401
- // Fast path: if we've previously seen no templated attributes, skip scan
402
- if (this._noEtaAttrs && this._noEtaAttrs.has(el))
403
- return;
404
- let foundTemplateAttr = false;
405
- // Implicit any-attribute templates (heuristic: contains '{{')
406
- for (const a of names) {
407
- const raw = el.getAttribute(a);
408
- if (!raw || !raw.includes('{{'))
409
- continue;
410
- foundTemplateAttr = true;
411
- const key = `imp:${a}`;
412
- let entry = cache.get(key);
413
- if (!entry || entry.src !== raw) {
414
- entry = { src: raw };
415
- cache.set(key, entry);
416
- }
417
- const rendered = this._safeEtaRender(entry, data, { onErrorReturnInput: true });
418
- if (rendered !== raw) {
419
- try {
420
- el.setAttribute(a, rendered);
421
- }
422
- catch { /* ignore */ }
423
- }
424
- }
425
- // If no templated attributes were found on this element, mark it to skip next time
426
- if (!foundTemplateAttr) {
427
- if (!this._noEtaAttrs)
428
- this._noEtaAttrs = new WeakSet();
429
- this._noEtaAttrs.add(el);
430
- }
431
- }
432
- _hasRenderNodes(el) {
433
- const h = el;
434
- if (!h)
435
- return false;
436
- if (h.hasAttribute && h.hasAttribute('data-store-render'))
437
- return true;
438
- // Heuristic: check innerHTML for render markers to avoid expensive queries
439
- const html = h.innerHTML;
440
- return typeof html === 'string' && html.includes('data-store-render');
441
- }
442
- async _morphElementFromHTML(el, html) {
443
- // Fast path: nothing changed
444
- if (el.innerHTML === html)
445
- return;
446
- try {
447
- const wrapper = el.cloneNode(false);
448
- wrapper.innerHTML = html;
449
- morph(el, wrapper);
450
- }
451
- catch {
452
- el.innerHTML = html;
453
- }
454
- }
455
- async _renderEtaTemplates(context) {
456
- // Traverse descendants (including opted-in shadow roots); for any element that
457
- // does NOT contain a nested state node, treat its innerHTML as a single Eta
458
- // template and render/morph the entire subtree.
459
- let _processed = 0;
460
- const waitForCustomUpdate = async (el) => {
461
- const maybe = el.updateComplete;
462
- if (maybe && typeof maybe.then === 'function') {
463
- try {
464
- await maybe;
465
- }
466
- catch { /* ignore */ }
467
- }
468
- };
469
- const visit = async (root) => {
470
- // If a follow-up Eta run is already requested (DOM still changing)
471
- // and this pass was triggered by mutation, bail out to avoid unstable writes.
472
- if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
473
- return;
474
- for (const child of Array.from(root.children)) {
475
- if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
476
- return;
477
- if (!(child instanceof HTMLElement))
478
- continue;
479
- if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
480
- return;
481
- await waitForCustomUpdate(child);
482
- const shadowRoot = child.hasAttribute('shadow') ? child.shadowRoot : null;
483
- const traverseShadow = async () => {
484
- if (shadowRoot) {
485
- await visit(shadowRoot);
486
- }
487
- };
488
- if (shadowRoot) {
489
- this._observeShadowRoot(shadowRoot, child);
490
- }
491
- // Read current HTML source once
492
- const currentSrc = child.innerHTML ?? '';
493
- // Always allow attribute-level Eta on any node in this subtree
494
- await this._renderEtaAttributes(child, context);
495
- if (this._isStateNodeElement(child) && child !== this) {
496
- continue; // nested provider; let it handle its subtree
497
- }
498
- const hasNestedStateNode = this._hasNestedStateNode(child);
499
- if (hasNestedStateNode) {
500
- // drill down until we reach leaves without nested state nodes
501
- await visit(child);
502
- await traverseShadow();
503
- continue;
504
- }
505
- // Cheap render-node detection using current HTML source
506
- // If subtree contains render nodes, recurse into it but don't morph at this level
507
- if (this._hasRenderNodes(child)) {
508
- await visit(child);
509
- await traverseShadow();
510
- continue;
511
- }
512
- // Skip elements managed by the render pipeline
513
- if (child.hasAttribute('data-store-render')) {
514
- continue;
515
- }
516
- // Reuse cached template, but refresh if DOM source changed (e.g., SPA mutated innerHTML)
517
- let entry = this._tplElementTemplates.get(child);
518
- const hadTemplate = Boolean(entry && entry.src && entry.src.includes('{{'));
519
- // If this element and its attributes contain no templates, it is not a
520
- // special provider/render container, and it never had a template cached,
521
- // skip further work.
522
- const noAttrs = this._noEtaAttrs?.has(child) ?? false;
523
- if (!currentSrc.includes('{{') && noAttrs && !hadTemplate && !hasNestedStateNode) {
524
- await traverseShadow();
525
- continue;
526
- }
527
- if (!entry) {
528
- entry = { src: currentSrc };
529
- this._tplElementTemplates.set(child, entry);
530
- }
531
- else if (entry.src !== currentSrc) {
532
- const isTemplateLike = currentSrc.includes('{{');
533
- // Accept SPA-authored changes (even without templates) only when this pass is mutation-triggered.
534
- if (isTemplateLike || this._etaRenderCurrentFromMutation) {
535
- entry.src = currentSrc;
536
- delete entry.fn;
537
- }
538
- }
539
- const out = this._safeEtaRender(entry, context, { onErrorReturnInput: true });
540
- if (this._etaRenderCurrentFromMutation) {
541
- // During mutation-triggered passes, avoid writing to DOM to prevent
542
- // racing with SPA changes. We refreshed caches above so subsequent
543
- // non-mutation renders have the latest source.
544
- await traverseShadow();
545
- continue;
546
- }
547
- if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
548
- return;
549
- await this._morphElementFromHTML(child, out);
550
- await traverseShadow();
551
- // Periodically yield to avoid long tasks
552
- if ((++_processed % 50) === 0) {
553
- await Promise.resolve();
554
- }
555
- }
556
- };
557
- const visitWithShadow = async (root) => {
558
- await visit(root);
559
- if (root.hasAttribute('shadow') && root.shadowRoot) {
560
- await visit(root.shadowRoot);
561
- }
562
- };
563
- // If this is a mutation-triggered pass and we have specific dirty roots, only refresh those
564
- if (this._etaRenderCurrentFromMutation && this._etaDirtyRoots && this._etaDirtyRoots.size) {
565
- for (const r of this._etaDirtyRoots) {
566
- await visitWithShadow(r);
567
- }
568
- this._etaDirtyRoots.clear();
569
- }
570
- else {
571
- await visitWithShadow(this);
572
- }
573
- }
574
180
  connectedCallback() {
575
181
  super.connectedCallback();
576
- // Listen for child registration/unregistration
577
- this.addEventListener(CollectChildEvent.eventName, this._onCollectChild);
578
- this.addEventListener(CollectChildRemovedEvent.eventName, this._onCollectChildRemoved);
579
- this.addEventListener(CollectActionEvent.eventName, this._collectActionEvent);
580
- this.addEventListener(CollectUpdateByDeltaEvent.eventName, this._collectUpdateByDeltaEvent);
581
- this.addEventListener(CollectOptionEvent.eventName, this._collectOptionEvent);
582
182
  [ActionEvent, UpdateByDeltaEvent].forEach(e => this.addEventListener(e.eventName, this._eventChange));
583
- // Observe slot/light DOM changes to trigger Eta re-rendering in SPA scenarios
584
- if (!this._slotMo && this.shouldRunEtaStateRender()) {
585
- this._slotMo = new MutationObserver((muts) => this._handleEtaMutations(muts));
586
- this._slotMo.observe(this, {
587
- childList: true,
588
- subtree: true,
589
- characterData: true,
590
- attributes: true
591
- });
592
- this.querySelectorAll('[shadow]').forEach(host => {
593
- if (host.shadowRoot)
594
- this._observeShadowRoot(host.shadowRoot, host);
595
- });
596
- }
597
- // Announce this node as a contextual child of its nearest parent StateNode
598
- // so parents can await our updateComplete as part of theirs.
599
- this._announceAsContextualChild();
600
183
  }
601
184
  remove() {
602
185
  // Announce removal to parent before DOM detaches, so it can unregister us
603
- this._announceContextualChildRemoved();
186
+ this._collect.announceRemoval();
604
187
  this.dispatchEvent(new CollectOptionEvent({ name: this.storeName, options: null }));
605
188
  super.remove();
606
189
  }
607
190
  disconnectedCallback() {
608
- // Stop listening for contextual child events
609
- this.removeEventListener(CollectChildEvent.eventName, this._onCollectChild);
610
- this.removeEventListener(CollectChildRemovedEvent.eventName, this._onCollectChildRemoved);
611
- this.removeEventListener(CollectActionEvent.eventName, this._collectActionEvent);
612
- this.removeEventListener(CollectUpdateByDeltaEvent.eventName, this._collectUpdateByDeltaEvent);
613
- this.removeEventListener(CollectOptionEvent.eventName, this._collectOptionEvent);
614
191
  [ActionEvent, UpdateByDeltaEvent].forEach(e => this.removeEventListener(e.eventName, this._eventChange));
615
- this._options.clear();
616
- this._actions.clear();
617
- this._partialOptions.clear();
618
- this._contextualChildren.clear();
619
- if (this._etaRenderDebounce) {
620
- clearTimeout(this._etaRenderDebounce);
621
- this._etaRenderDebounce = undefined;
622
- }
623
- this._slotMo?.disconnect();
624
- this._slotMo = undefined;
625
- if (this._shadowSlotMos) {
626
- for (const [sr, mo] of this._shadowSlotMos.entries()) {
627
- try {
628
- mo.disconnect();
629
- }
630
- catch { /* ignore */ }
631
- try {
632
- this._cleanupShadowRoot(sr);
633
- }
634
- catch { /* ignore */ }
635
- }
636
- this._shadowSlotMos.clear();
637
- this._shadowSlotMos = undefined;
638
- }
639
192
  super.disconnectedCallback();
640
193
  }
641
- _announceAsContextualChild() {
642
- this.dispatchEvent(new CollectChildEvent({ child: this }));
643
- }
644
- _announceContextualChildRemoved() {
645
- this.dispatchEvent(new CollectChildRemovedEvent({ child: this }));
646
- }
647
- _cleanupShadowRoot(sr) {
648
- if (this._shadowSlotMos && this._shadowSlotMos.has(sr)) {
649
- const mo = this._shadowSlotMos.get(sr);
650
- try {
651
- mo?.disconnect();
652
- }
653
- catch { /* ignore */ }
654
- this._shadowSlotMos.delete(sr);
655
- }
656
- sr.querySelectorAll('[shadow]').forEach(host => {
657
- if (host.shadowRoot)
658
- this._cleanupShadowRoot(host.shadowRoot);
659
- });
660
- }
661
- _handleEtaMutations(muts) {
662
- for (const m of muts) {
663
- if (m.type === 'attributes') {
664
- const attr = m.attributeName?.toLowerCase();
665
- if (attr === 'class' || attr === 'style')
666
- continue; // Ignore cosmetic-only mutations
667
- }
668
- // Consider only targets that aren't within nested-provider or render-managed subtrees
669
- const targets = [];
670
- if (m.type === 'characterData' || m.type === 'attributes') {
671
- targets.push(m.target);
672
- // If attributes changed on an element previously marked as having no Eta attrs,
673
- // clear the cache so it can be rescanned on the next pass.
674
- if (m.type === 'attributes' && this._noEtaAttrs && m.target instanceof HTMLElement) {
675
- this._noEtaAttrs.delete(m.target);
676
- }
677
- }
678
- else {
679
- targets.push(...Array.from(m.addedNodes));
680
- for (const removed of Array.from(m.removedNodes)) {
681
- if (!(removed instanceof HTMLElement))
682
- continue;
683
- if (removed.hasAttribute('shadow') && removed.shadowRoot) {
684
- this._cleanupShadowRoot(removed.shadowRoot);
685
- }
686
- removed
687
- .querySelectorAll('[shadow]')
688
- .forEach(host => host.shadowRoot && this._cleanupShadowRoot(host.shadowRoot));
689
- }
690
- }
691
- const eligibles = [];
692
- for (const t of targets) {
693
- if (t instanceof HTMLElement && t.hasAttribute('shadow') && t.shadowRoot) {
694
- this._observeShadowRoot(t.shadowRoot, t);
695
- }
696
- if (t instanceof HTMLElement) {
697
- t.querySelectorAll('[shadow]').forEach(host => {
698
- if (host.shadowRoot)
699
- this._observeShadowRoot(host.shadowRoot, host);
700
- });
701
- }
702
- const root = this._findEligibleRoot(t);
703
- if (root)
704
- eligibles.push(root);
705
- }
706
- if (!eligibles.length)
707
- continue;
708
- // Track dirty roots for this cycle
709
- if (!this._etaDirtyRoots)
710
- this._etaDirtyRoots = new Set();
711
- eligibles.forEach(r => this._etaDirtyRoots.add(r));
712
- this._scheduleEtaRenderFromMutation();
713
- break; // one schedule is enough per batch
714
- }
715
- }
716
- _observeShadowRoot(sr, host) {
717
- if (!this.shouldRunEtaStateRender())
718
- return;
719
- const rootHost = host ?? (sr.host instanceof HTMLElement ? sr.host : null);
720
- if (rootHost) {
721
- const eligible = this._findEligibleRoot(rootHost);
722
- if (!eligible)
723
- return;
724
- }
725
- if (!this._shadowSlotMos)
726
- this._shadowSlotMos = new Map();
727
- if (this._shadowSlotMos.has(sr))
728
- return;
729
- const mo = new MutationObserver((muts) => this._handleEtaMutations(muts));
730
- mo.observe(sr, {
731
- childList: true,
732
- subtree: true,
733
- characterData: true,
734
- attributes: true
735
- });
736
- this._shadowSlotMos.set(sr, mo);
737
- sr.querySelectorAll('[shadow]').forEach(host => {
738
- if (host.shadowRoot)
739
- this._observeShadowRoot(host.shadowRoot, host);
740
- });
741
- }
742
194
  _eventChange(e) {
743
195
  e.stopPropagation();
744
196
  // ignore if source matches ignore list
@@ -748,315 +200,48 @@ export class StateNode extends LitElement {
748
200
  this._eventDom = e;
749
201
  }
750
202
  _forwardEvent(e = this._event) {
751
- this._fEvent = e;
752
- }
753
- _collectOptionEvent(e) {
754
- if (e.target === this) {
755
- return;
756
- }
757
- e.stopPropagation();
758
- this.collectOption(e.detail);
759
- }
760
- collectOption({ name, options }) {
761
- //node has disconected
762
- if (options === null) {
763
- this._options.delete(name);
764
- }
765
- else {
766
- this._options.set(name, options);
767
- }
768
- this._computeTask.abort();
769
- this._computeTask.run();
770
- this._notifyParent();
203
+ this._pushEventToChildren(e);
771
204
  }
772
- _collectActionEvent(e) {
773
- if (e.target === this) {
205
+ _receiveParentEvent(e) {
206
+ if (this.ignoreEventsParent) {
207
+ this._eventParent = undefined;
774
208
  return;
775
209
  }
776
- e.stopPropagation();
777
- const { name, action } = e.detail;
778
- //node has disconected
779
- if (action === null) {
780
- this._actions.delete(name);
781
- this._partialOptions.delete(name);
782
- this._partialBundleOptions.delete(name);
783
- }
784
- else if (!action.id && (action.devices || action.subscription)) {
785
- this._actions.set(name, action);
786
- }
787
- else {
788
- if (action.bundle) {
789
- this._partialBundleOptions.set(name, action);
790
- }
791
- else {
792
- this._partialOptions.set(name, action);
793
- }
794
- }
795
- this._computeTask.abort();
796
- this._computeTask.run();
797
- this._notifyParent();
210
+ this._eventParent = e;
798
211
  }
799
- _collectUpdateByDeltaEvent(e) {
800
- if (e.target === this) {
212
+ _pushEventToChildren(e) {
213
+ if (!e)
801
214
  return;
802
- }
803
- const { name, update } = e.detail;
804
- //node has disconected
805
- if (update === null) {
806
- this._deltaUpdates.delete(name);
807
- }
808
- else {
809
- this._deltaUpdates.set(name, update);
810
- }
811
- this._computeTask.abort();
812
- this._computeTask.run();
813
- this._notifyParent();
814
- }
815
- async _computeState(isActive) {
816
- // We'll iteratively expand the set of options using actions and delta updates
817
- // until we reach a fixed point (no new options discovered). This ensures
818
- // combinations across different dimensions are included (e.g., 10-24).
819
- const computed = new Set();
820
- const queue = [];
821
- const enqueue = (opt) => {
822
- if (!computed.has(opt)) {
823
- computed.add(opt);
824
- queue.push(opt);
825
- }
826
- };
827
- // 1) collect base options + partialOptions and seed the queue
828
- for (const optsPromise of this._options.values()) {
829
- isActive();
830
- const opts = await optsPromise;
831
- for (const opt of opts) {
832
- isActive();
833
- enqueue(opt);
834
- const partials = await this._applyPartials(opt, isActive);
835
- for (const p of partials)
836
- enqueue(p);
837
- }
838
- }
839
- // 2) Repeatedly apply actions and delta updates to closure
840
- while (queue.length) {
841
- isActive();
842
- const current = queue.shift();
843
- const actionResults = await this._applyActions(current, isActive);
844
- for (const a of actionResults)
845
- enqueue(a);
846
- const deltaResults = await this._applyDeltaUpdates(current, isActive);
847
- for (const d of deltaResults)
848
- enqueue(d);
849
- }
850
- // 3) combine bundles on the full set
851
- for (const opt of [...computed]) {
852
- isActive();
853
- const bundles = [...this._partialBundleOptions.values()];
854
- const combos = await this._applyBundles(opt, bundles);
855
- combos.forEach(cb => cb && computed.add(cb));
856
- }
857
- return [...computed];
858
- }
859
- async _applyPartials(baseOpt, isActive) {
860
- const results = [];
861
- for (const partial of this._partialOptions.values()) {
862
- isActive();
863
- if (!partial.id) {
864
- continue;
865
- }
866
- const newProduct = await baseOpt.switchProduct({ id: partial.id, campaign: partial.campaign });
867
- const newOpt = await newProduct?.getOption({
868
- devices: partial.devices || baseOpt.getDevices(),
869
- subscription: partial.subscription || baseOpt.getSubscription()
870
- });
871
- if (newOpt)
872
- results.push(newOpt);
873
- }
874
- return results;
875
- }
876
- async _applyActions(baseOpt, isActive) {
877
- const results = [];
878
- for (const action of this._actions.values()) {
879
- isActive();
880
- const targetDevices = action.devices ?? baseOpt.getDevices();
881
- const targetSubscription = action.subscription ?? baseOpt.getSubscription();
882
- // Skip no-op actions that don't change anything
883
- if (targetDevices === baseOpt.getDevices() && targetSubscription === baseOpt.getSubscription()) {
215
+ const children = this._collect.getContextualChildren();
216
+ for (const child of children) {
217
+ if (child === this)
884
218
  continue;
219
+ if (child instanceof StateNode) {
220
+ child._receiveParentEvent(e);
885
221
  }
886
- const newOpt = await baseOpt.getOption({
887
- devices: targetDevices,
888
- subscription: targetSubscription
889
- });
890
- if (newOpt)
891
- results.push(newOpt);
892
222
  }
893
- return results;
894
223
  }
895
- async _applyDeltaUpdates(baseOpt, isActive) {
896
- const results = [];
897
- const product = baseOpt.getProduct();
898
- const baseDevices = product.getDevices().values;
899
- const baseSubscriptions = product.getSubscriptions().values;
900
- let devices = baseOpt.getDevices();
901
- let subscription = baseOpt.getSubscription();
902
- // corner case for input types that define an interval by themselves
903
- const getValue = (action, startValue, minValue) => {
904
- if (action.useAsValue) {
905
- action.delta = 1;
906
- return Number(action.min) || minValue;
907
- }
908
- else {
909
- return startValue;
910
- }
911
- };
912
- for (const action of this._deltaUpdates.values()) {
913
- isActive();
914
- // Loop until we can no longer apply the delta
915
- while (true) {
916
- isActive();
917
- const values = action.type === "devices" ? baseDevices : baseSubscriptions;
918
- const current = action.type === "devices"
919
- ? getValue(action, devices, baseDevices[0])
920
- : getValue(action, subscription, baseSubscriptions[0]);
921
- const { newValue, done } = this._computeDelta(values, current, action);
922
- if (done)
923
- break;
924
- // Update the appropriate variable
925
- if (action.type === "devices") {
926
- devices = newValue;
927
- }
928
- else {
929
- subscription = newValue;
930
- }
931
- // Fetch the new option and collect it if exists
932
- const newOpt = await baseOpt.getOption({ devices, subscription });
933
- if (newOpt)
934
- results.push(newOpt);
935
- }
936
- }
937
- return results;
938
- }
939
- /**
940
- * Attempts to apply the given delta action to the current value.
941
- * Returns the updated value and whether we've exhausted variants.
942
- */
943
- _computeDelta(values, current, action) {
944
- const min = Number(action.min) || Number.MAX_SAFE_INTEGER;
945
- const max = Number(action.max) || Number.MIN_SAFE_INTEGER;
946
- // Handle "next" / "prev" stepping through a discrete list
947
- if (action.delta === "next" || action.delta === "prev") {
948
- const idx = values.findIndex(v => v === current);
949
- const step = action.delta === "next" ? 1 : -1;
950
- const candidate = values.at(idx + step);
951
- if (candidate && (action.delta === "next" ? candidate <= min : candidate >= max)) {
952
- return { newValue: candidate, done: false };
953
- }
954
- return { newValue: current, done: true };
955
- }
956
- // Handle numeric delta
957
- const candidate = current + action.delta;
958
- const isValid = action.delta > 0 ? candidate <= min : candidate >= max;
959
- if (isValid) {
960
- return { newValue: candidate, done: false };
961
- }
962
- return { newValue: current, done: true };
963
- }
964
- async _computeContext(options, isActive) {
965
- function updateMinMax(range, value, formatted) {
966
- if (range.min.value == null || value < range.min.value) {
967
- range.min.value = value;
968
- range.min.fmt = formatted;
969
- }
970
- if (range.max.value == null || value > range.max.value) {
971
- range.max.value = value;
972
- range.max.fmt = formatted;
973
- }
974
- }
975
- for (const option of options) {
976
- isActive();
977
- updateMinMax(this.state.price, option.getPrice({ currency: false }), option.getPrice());
978
- updateMinMax(this.state.price.monthly, option.getPrice({ monthly: true, currency: false }), option.getPrice({ monthly: true }));
979
- updateMinMax(this.state.discountedPrice, option.getDiscountedPrice({ currency: false }), option.getDiscountedPrice());
980
- updateMinMax(this.state.discountedPrice.monthly, option.getDiscountedPrice({ monthly: true, currency: false }), option.getDiscountedPrice({ monthly: true }));
981
- updateMinMax(this.state.discount, option.getDiscount({ symbol: false }), option.getDiscount());
982
- updateMinMax(this.state.discount.monthly, option.getDiscount({ monthly: true, symbol: false }), option.getDiscount({ monthly: true }));
983
- updateMinMax(this.state.discount.percentage, option.getDiscount({ percentage: true, symbol: false }), option.getDiscount({ percentage: true }));
984
- updateMinMax(this.state.discount.percentage.monthly, option.getDiscount({ monthly: true, percentage: true, symbol: false }), option.getDiscount({ monthly: true, percentage: true }));
985
- }
986
- this.state = { ...this.state };
987
- }
988
- async _getOption({ id, campaign, devices, subscription }, bundle = []) {
989
- if (id && devices && subscription) {
990
- const product = await this._store?.getProduct({ id, campaign });
991
- return await product?.getOption({ devices, subscription, bundle });
992
- }
993
- return null;
994
- }
995
- async _applyBundles(baseOpt, bundles) {
996
- const result = [];
997
- const recurse = async (prefix, start) => {
998
- for (let i = start; i < bundles.length; i++) {
999
- const bundleOption = await this._getOption({
1000
- id: bundles[i].id,
1001
- campaign: bundles[i].campaign,
1002
- devices: bundles[i].devices ?? baseOpt.getDevices(),
1003
- subscription: bundles[i].subscription ?? baseOpt.getSubscription()
1004
- }, []);
1005
- if (!bundleOption) {
1006
- continue;
1007
- }
1008
- const next = prefix.concat({
1009
- devicesFixed: Boolean(bundles[i].devices),
1010
- subscriptionFixed: Boolean(bundles[i].subscription),
1011
- option: bundleOption
1012
- });
1013
- result.push(next);
1014
- await recurse(next, i + 1);
1015
- }
1016
- };
1017
- const bundleOption = async (option, bundles = []) => {
1018
- for (const bundle of bundles) {
1019
- const newOpt = await option.toogleBundle(bundle);
1020
- if (newOpt) {
1021
- option = newOpt;
1022
- }
1023
- else {
1024
- return null;
1025
- }
1026
- }
1027
- return option;
1028
- };
1029
- await recurse([], 0);
1030
- const x = await Promise.all(result.map(bundles => bundleOption(baseOpt, bundles)));
1031
- return x;
224
+ collectOption({ name, options }) {
225
+ this._collect.collectOption({ name, options });
1032
226
  }
1033
227
  _notifyParent() {
1034
228
  if (this.noCollect)
1035
229
  return;
1036
230
  this.dispatchEvent(new CollectOptionEvent({
1037
231
  name: this.storeName,
1038
- options: this._computeTask.taskComplete
232
+ options: this._compute.taskComplete
1039
233
  }));
1040
234
  }
1041
235
  async getUpdateComplete() {
1042
236
  const result = await super.getUpdateComplete();
1043
- await this._computeTask.taskComplete;
1044
- await this._defaultForwardEventTask.taskComplete;
1045
- await this._syncEventTask.taskComplete;
1046
- await this._collectToggleTask.taskComplete;
1047
- await this._etaRenderTask.taskComplete;
237
+ await this._compute.taskComplete;
238
+ await this._events.forwardComplete;
239
+ await this._events.syncComplete;
240
+ await this._collect.toggleComplete;
241
+ await this._eta.taskComplete;
1048
242
  // Ensure any debounced/mutation-triggered Eta runs have settled
1049
- await this._waitEtaSettled();
1050
- // Await all registered contextual children (nearest descendants only)
1051
- try {
1052
- const children = Array.from(this._contextualChildren);
1053
- const waits = children
1054
- .map((c) => c.updateComplete)
1055
- .filter((p) => !!p);
1056
- if (waits.length > 0)
1057
- await Promise.allSettled(waits);
1058
- }
1059
- catch { /* ignore */ }
243
+ await this._eta.waitForIdle();
244
+ await this._collect.waitForChildrenUpdateComplete();
1060
245
  return result;
1061
246
  }
1062
247
  isDeviceAndSubscriptionChange(evt) {
@@ -1126,7 +311,6 @@ __decorate([
1126
311
  property({ attribute: false })
1127
312
  ], StateNode.prototype, "_store", void 0);
1128
313
  __decorate([
1129
- consume({ context: eventContext, subscribe: true }),
1130
314
  property({ attribute: false })
1131
315
  ], StateNode.prototype, "_eventParent", void 0);
1132
316
  __decorate([
@@ -1139,10 +323,6 @@ __decorate([
1139
323
  __decorate([
1140
324
  property({ attribute: false })
1141
325
  ], StateNode.prototype, "_eventDom", void 0);
1142
- __decorate([
1143
- provide({ context: eventContext }),
1144
- property({ attribute: false })
1145
- ], StateNode.prototype, "_fEvent", void 0);
1146
326
  __decorate([
1147
327
  provide({ context: stateContext }),
1148
328
  property({ attribute: false })