@repobit/dex-store-elements 1.2.2 → 1.2.4
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/CHANGELOG.md +24 -0
- package/README.md +15 -1
- package/dist/src/actions/action.button.d.ts +2 -1
- package/dist/src/actions/action.button.js +2 -1
- package/dist/src/actions/action.button.js.map +1 -1
- package/dist/src/actions/action.input.d.ts +2 -1
- package/dist/src/actions/action.input.js.map +1 -1
- package/dist/src/actions/action.select.d.ts +2 -1
- package/dist/src/actions/action.select.js.map +1 -1
- package/dist/src/actions/index.js +12 -1
- package/dist/src/actions/index.js.map +1 -1
- package/dist/src/actions/utilty.d.ts +7 -1
- package/dist/src/actions/utilty.js +36 -5
- package/dist/src/actions/utilty.js.map +1 -1
- package/dist/src/events/events.d.ts +15 -0
- package/dist/src/events/events.js +22 -0
- package/dist/src/events/events.js.map +1 -1
- package/dist/src/nodes/node.context.d.ts +1 -0
- package/dist/src/nodes/node.context.js +4 -0
- package/dist/src/nodes/node.context.js.map +1 -1
- package/dist/src/nodes/node.option.js +10 -2
- package/dist/src/nodes/node.option.js.map +1 -1
- package/dist/src/nodes/node.product.js +10 -2
- package/dist/src/nodes/node.product.js.map +1 -1
- package/dist/src/nodes/node.state.d.ts +23 -0
- package/dist/src/nodes/node.state.js +320 -34
- package/dist/src/nodes/node.state.js.map +1 -1
- package/dist/src/renders/index.js +11 -1
- package/dist/src/renders/index.js.map +1 -1
- package/dist/src/renders/utility.d.ts +4 -1
- package/dist/src/renders/utility.js +52 -2
- package/dist/src/renders/utility.js.map +1 -1
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ import { derivedContext } from "../contexts/context.derived.js";
|
|
|
8
8
|
import { eventContext } from "../contexts/context.event.js";
|
|
9
9
|
import { stateContext } from "../contexts/context.state.js";
|
|
10
10
|
import { storeContext } from "../contexts/context.store.js";
|
|
11
|
-
import { ActionEvent, CollectActionEvent, CollectOptionEvent, CollectUpdateByDeltaEvent, UpdateByDeltaEvent } from "../events/events.js";
|
|
11
|
+
import { ActionEvent, CollectActionEvent, CollectChildEvent, CollectChildRemovedEvent, CollectOptionEvent, CollectUpdateByDeltaEvent, UpdateByDeltaEvent } from "../events/events.js";
|
|
12
12
|
import { toDSLContext } from "../renders/context.js";
|
|
13
13
|
import eta from "../templating/eta.js";
|
|
14
14
|
import { consume, provide } from "@lit/context";
|
|
@@ -32,6 +32,11 @@ export class StateNode extends LitElement {
|
|
|
32
32
|
this.autoForward = true;
|
|
33
33
|
this.noCollect = false;
|
|
34
34
|
this.storeName = Symbol("bd-state");
|
|
35
|
+
// Configurable delay for mutation-triggered Eta coalescing (ms).
|
|
36
|
+
// - Use 0 for next-tick behavior (runs ASAP, more visits)
|
|
37
|
+
// - Use a higher value (e.g., 50–150) to batch SPA bursts
|
|
38
|
+
this.etaMutationDelay = 10;
|
|
39
|
+
this.etaMutationDelayMax = 100;
|
|
35
40
|
// If true, ignore events coming from parent context; only react to
|
|
36
41
|
// events originating inside this subtree (DOM-bubbled via listeners).
|
|
37
42
|
this.ignoreEventsParent = false;
|
|
@@ -234,11 +239,54 @@ export class StateNode extends LitElement {
|
|
|
234
239
|
if (!this.shouldRunEtaStateRender())
|
|
235
240
|
return;
|
|
236
241
|
// Provide 'it' derived from DSL context
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
239
260
|
},
|
|
240
261
|
args: () => [this.state, this._derived]
|
|
241
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
|
+
};
|
|
242
290
|
}
|
|
243
291
|
/**
|
|
244
292
|
* all options computed by this node
|
|
@@ -250,12 +298,78 @@ export class StateNode extends LitElement {
|
|
|
250
298
|
// Allow subclasses (e.g., OptionNode) to disable the base Eta render
|
|
251
299
|
// when they provide their own option-specific rendering pipeline.
|
|
252
300
|
shouldRunEtaStateRender() { return true; }
|
|
301
|
+
_isEtaIdle() {
|
|
302
|
+
return !this._etaRenderInProgress && !this._etaRenderDebounce && !this._etaRenderNeedsRun;
|
|
303
|
+
}
|
|
304
|
+
_waitEtaSettled() {
|
|
305
|
+
if (this._isEtaIdle())
|
|
306
|
+
return Promise.resolve();
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
this._etaIdleWaiters.push(resolve);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
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);
|
|
347
|
+
}
|
|
253
348
|
_isStateNodeElement(el) {
|
|
254
349
|
if (el instanceof StateNode)
|
|
255
350
|
return true;
|
|
256
351
|
const t = el.tagName;
|
|
257
352
|
return t === 'BD-STATE' || t === 'BD-PRODUCT' || t === 'BD-OPTION' || t === 'BD-CONTEXT';
|
|
258
353
|
}
|
|
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 HTMLElement)) {
|
|
359
|
+
cur = cur.parentNode;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (this._isStateNodeElement(cur) && cur !== this)
|
|
363
|
+
return null; // nested provider boundary
|
|
364
|
+
if (cur.hasAttribute('data-store-render'))
|
|
365
|
+
return null; // render-managed boundary
|
|
366
|
+
const parentProvider = cur.parentElement?.closest('bd-state,bd-product,bd-option,bd-context');
|
|
367
|
+
if (parentProvider === this || cur.parentElement === this)
|
|
368
|
+
return cur;
|
|
369
|
+
cur = cur.parentNode;
|
|
370
|
+
}
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
259
373
|
_hasNestedStateNode(el) {
|
|
260
374
|
return !!el.querySelector?.('bd-state,bd-product,bd-option,bd-context');
|
|
261
375
|
}
|
|
@@ -280,11 +394,16 @@ export class StateNode extends LitElement {
|
|
|
280
394
|
cache = new Map();
|
|
281
395
|
this._tplAttrTemplates.set(el, cache);
|
|
282
396
|
}
|
|
397
|
+
// Fast path: if we've previously seen no templated attributes, skip scan
|
|
398
|
+
if (this._noEtaAttrs && this._noEtaAttrs.has(el))
|
|
399
|
+
return;
|
|
400
|
+
let foundTemplateAttr = false;
|
|
283
401
|
// Implicit any-attribute templates (heuristic: contains '{{')
|
|
284
402
|
for (const a of names) {
|
|
285
403
|
const raw = el.getAttribute(a);
|
|
286
404
|
if (!raw || !raw.includes('{{'))
|
|
287
405
|
continue;
|
|
406
|
+
foundTemplateAttr = true;
|
|
288
407
|
const key = `imp:${a}`;
|
|
289
408
|
let entry = cache.get(key);
|
|
290
409
|
if (!entry || entry.src !== raw) {
|
|
@@ -299,11 +418,27 @@ export class StateNode extends LitElement {
|
|
|
299
418
|
catch { /* ignore */ }
|
|
300
419
|
}
|
|
301
420
|
}
|
|
421
|
+
// If no templated attributes were found on this element, mark it to skip next time
|
|
422
|
+
if (!foundTemplateAttr) {
|
|
423
|
+
if (!this._noEtaAttrs)
|
|
424
|
+
this._noEtaAttrs = new WeakSet();
|
|
425
|
+
this._noEtaAttrs.add(el);
|
|
426
|
+
}
|
|
302
427
|
}
|
|
303
428
|
_hasRenderNodes(el) {
|
|
304
|
-
|
|
429
|
+
const h = el;
|
|
430
|
+
if (!h)
|
|
431
|
+
return false;
|
|
432
|
+
if (h.hasAttribute && h.hasAttribute('data-store-render'))
|
|
433
|
+
return true;
|
|
434
|
+
// Heuristic: check innerHTML for render markers to avoid expensive queries
|
|
435
|
+
const html = h.innerHTML;
|
|
436
|
+
return typeof html === 'string' && html.includes('data-store-render');
|
|
305
437
|
}
|
|
306
438
|
async _morphElementFromHTML(el, html) {
|
|
439
|
+
// Fast path: nothing changed
|
|
440
|
+
if (el.innerHTML === html)
|
|
441
|
+
return;
|
|
307
442
|
try {
|
|
308
443
|
const wrapper = el.cloneNode(false);
|
|
309
444
|
wrapper.innerHTML = html;
|
|
@@ -316,10 +451,19 @@ export class StateNode extends LitElement {
|
|
|
316
451
|
async _renderEtaTemplates(context) {
|
|
317
452
|
// Traverse descendants; for any element that does NOT contain a nested state node,
|
|
318
453
|
// treat its innerHTML as a single Eta template and render/morph the entire subtree.
|
|
454
|
+
let _processed = 0;
|
|
319
455
|
const visit = async (root) => {
|
|
456
|
+
// If a follow-up Eta run is already requested (DOM still changing)
|
|
457
|
+
// and this pass was triggered by mutation, bail out to avoid unstable writes.
|
|
458
|
+
if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
|
|
459
|
+
return;
|
|
320
460
|
for (const child of Array.from(root.children)) {
|
|
461
|
+
if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
|
|
462
|
+
return;
|
|
321
463
|
if (!(child instanceof HTMLElement))
|
|
322
464
|
continue;
|
|
465
|
+
// Read current HTML source once
|
|
466
|
+
const currentSrc = child.innerHTML ?? '';
|
|
323
467
|
// Always allow attribute-level Eta on any node in this subtree
|
|
324
468
|
await this._renderEtaAttributes(child, context);
|
|
325
469
|
if (this._isStateNodeElement(child) && child !== this) {
|
|
@@ -330,6 +474,7 @@ export class StateNode extends LitElement {
|
|
|
330
474
|
await visit(child);
|
|
331
475
|
continue;
|
|
332
476
|
}
|
|
477
|
+
// Cheap render-node detection using current HTML source
|
|
333
478
|
// If subtree contains render nodes, recurse into it but don't morph at this level
|
|
334
479
|
if (this._hasRenderNodes(child)) {
|
|
335
480
|
await visit(child);
|
|
@@ -339,30 +484,118 @@ export class StateNode extends LitElement {
|
|
|
339
484
|
if (child.hasAttribute('data-store-render')) {
|
|
340
485
|
continue;
|
|
341
486
|
}
|
|
487
|
+
// Reuse cached template, but refresh if DOM source changed (e.g., SPA mutated innerHTML)
|
|
342
488
|
let entry = this._tplElementTemplates.get(child);
|
|
489
|
+
const hadTemplate = Boolean(entry && entry.src && entry.src.includes('{{'));
|
|
490
|
+
// If this element and its attributes contain no templates, it is not a
|
|
491
|
+
// special provider/render container, and it never had a template cached,
|
|
492
|
+
// skip further work.
|
|
493
|
+
const noAttrs = this._noEtaAttrs?.has(child) ?? false;
|
|
494
|
+
if (!currentSrc.includes('{{') && noAttrs && !hadTemplate && !this._hasNestedStateNode(child)) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
343
497
|
if (!entry) {
|
|
344
|
-
|
|
345
|
-
entry = { src };
|
|
498
|
+
entry = { src: currentSrc };
|
|
346
499
|
this._tplElementTemplates.set(child, entry);
|
|
347
500
|
}
|
|
501
|
+
else if (entry.src !== currentSrc) {
|
|
502
|
+
const isTemplateLike = currentSrc.includes('{{');
|
|
503
|
+
// Accept SPA-authored changes (even without templates) only when this pass is mutation-triggered.
|
|
504
|
+
if (isTemplateLike || this._etaRenderCurrentFromMutation) {
|
|
505
|
+
entry.src = currentSrc;
|
|
506
|
+
delete entry.fn;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
348
509
|
const out = this._safeEtaRender(entry, context, { onErrorReturnInput: true });
|
|
510
|
+
if (this._etaRenderCurrentFromMutation) {
|
|
511
|
+
// During mutation-triggered passes, avoid writing to DOM to prevent
|
|
512
|
+
// racing with SPA changes. We refreshed caches above so subsequent
|
|
513
|
+
// non-mutation renders have the latest source.
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (this._etaRenderCurrentFromMutation && this._etaRenderNeedsRun)
|
|
517
|
+
return;
|
|
349
518
|
await this._morphElementFromHTML(child, out);
|
|
519
|
+
// Periodically yield to avoid long tasks
|
|
520
|
+
if ((++_processed % 50) === 0) {
|
|
521
|
+
await Promise.resolve();
|
|
522
|
+
}
|
|
350
523
|
}
|
|
351
524
|
};
|
|
352
|
-
|
|
525
|
+
// If this is a mutation-triggered pass and we have specific dirty roots, only refresh those
|
|
526
|
+
if (this._etaRenderCurrentFromMutation && this._etaDirtyRoots && this._etaDirtyRoots.size) {
|
|
527
|
+
for (const r of this._etaDirtyRoots) {
|
|
528
|
+
await visit(r);
|
|
529
|
+
}
|
|
530
|
+
this._etaDirtyRoots.clear();
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
await visit(this);
|
|
534
|
+
}
|
|
353
535
|
}
|
|
354
536
|
connectedCallback() {
|
|
355
537
|
super.connectedCallback();
|
|
538
|
+
// Listen for child registration/unregistration
|
|
539
|
+
this.addEventListener(CollectChildEvent.eventName, this._onCollectChild);
|
|
540
|
+
this.addEventListener(CollectChildRemovedEvent.eventName, this._onCollectChildRemoved);
|
|
356
541
|
this.addEventListener(CollectActionEvent.eventName, this._collectActionEvent);
|
|
357
542
|
this.addEventListener(CollectUpdateByDeltaEvent.eventName, this._collectUpdateByDeltaEvent);
|
|
358
543
|
this.addEventListener(CollectOptionEvent.eventName, this._collectOptionEvent);
|
|
359
544
|
[ActionEvent, UpdateByDeltaEvent].forEach(e => this.addEventListener(e.eventName, this._eventChange));
|
|
545
|
+
// Observe slot/light DOM changes to trigger Eta re-rendering in SPA scenarios
|
|
546
|
+
if (!this._slotMo && this.shouldRunEtaStateRender()) {
|
|
547
|
+
this._slotMo = new MutationObserver((muts) => {
|
|
548
|
+
for (const m of muts) {
|
|
549
|
+
// Consider only targets that aren't within nested-provider or render-managed subtrees
|
|
550
|
+
const targets = [];
|
|
551
|
+
if (m.type === 'characterData' || m.type === 'attributes') {
|
|
552
|
+
targets.push(m.target);
|
|
553
|
+
// If attributes changed on an element previously marked as having no Eta attrs,
|
|
554
|
+
// clear the cache so it can be rescanned on the next pass.
|
|
555
|
+
if (m.type === 'attributes' && this._noEtaAttrs && m.target instanceof HTMLElement) {
|
|
556
|
+
this._noEtaAttrs.delete(m.target);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
targets.push(...Array.from(m.addedNodes));
|
|
561
|
+
}
|
|
562
|
+
const eligibles = [];
|
|
563
|
+
for (const t of targets) {
|
|
564
|
+
const root = this._findEligibleRoot(t);
|
|
565
|
+
if (root)
|
|
566
|
+
eligibles.push(root);
|
|
567
|
+
}
|
|
568
|
+
if (!eligibles.length)
|
|
569
|
+
continue;
|
|
570
|
+
// Track dirty roots for this cycle
|
|
571
|
+
if (!this._etaDirtyRoots)
|
|
572
|
+
this._etaDirtyRoots = new Set();
|
|
573
|
+
eligibles.forEach(r => this._etaDirtyRoots.add(r));
|
|
574
|
+
this._scheduleEtaRenderFromMutation();
|
|
575
|
+
break; // one schedule is enough per batch
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
this._slotMo.observe(this, {
|
|
579
|
+
childList: true,
|
|
580
|
+
subtree: true,
|
|
581
|
+
characterData: true,
|
|
582
|
+
attributes: true
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
// Announce this node as a contextual child of its nearest parent StateNode
|
|
586
|
+
// so parents can await our updateComplete as part of theirs.
|
|
587
|
+
this._announceAsContextualChild();
|
|
360
588
|
}
|
|
361
589
|
remove() {
|
|
590
|
+
// Announce removal to parent before DOM detaches, so it can unregister us
|
|
591
|
+
this._announceContextualChildRemoved();
|
|
362
592
|
this.dispatchEvent(new CollectOptionEvent({ name: this.storeName, options: null }));
|
|
363
593
|
super.remove();
|
|
364
594
|
}
|
|
365
595
|
disconnectedCallback() {
|
|
596
|
+
// Stop listening for contextual child events
|
|
597
|
+
this.removeEventListener(CollectChildEvent.eventName, this._onCollectChild);
|
|
598
|
+
this.removeEventListener(CollectChildRemovedEvent.eventName, this._onCollectChildRemoved);
|
|
366
599
|
this.removeEventListener(CollectActionEvent.eventName, this._collectActionEvent);
|
|
367
600
|
this.removeEventListener(CollectUpdateByDeltaEvent.eventName, this._collectUpdateByDeltaEvent);
|
|
368
601
|
this.removeEventListener(CollectOptionEvent.eventName, this._collectOptionEvent);
|
|
@@ -370,8 +603,21 @@ export class StateNode extends LitElement {
|
|
|
370
603
|
this._options.clear();
|
|
371
604
|
this._actions.clear();
|
|
372
605
|
this._partialOptions.clear();
|
|
606
|
+
this._contextualChildren.clear();
|
|
607
|
+
if (this._etaRenderDebounce) {
|
|
608
|
+
clearTimeout(this._etaRenderDebounce);
|
|
609
|
+
this._etaRenderDebounce = undefined;
|
|
610
|
+
}
|
|
611
|
+
this._slotMo?.disconnect();
|
|
612
|
+
this._slotMo = undefined;
|
|
373
613
|
super.disconnectedCallback();
|
|
374
614
|
}
|
|
615
|
+
_announceAsContextualChild() {
|
|
616
|
+
this.dispatchEvent(new CollectChildEvent({ child: this }));
|
|
617
|
+
}
|
|
618
|
+
_announceContextualChildRemoved() {
|
|
619
|
+
this.dispatchEvent(new CollectChildRemovedEvent({ child: this }));
|
|
620
|
+
}
|
|
375
621
|
_eventChange(e) {
|
|
376
622
|
e.stopPropagation();
|
|
377
623
|
// ignore if source matches ignore list
|
|
@@ -446,28 +692,41 @@ export class StateNode extends LitElement {
|
|
|
446
692
|
this._notifyParent();
|
|
447
693
|
}
|
|
448
694
|
async _computeState(isActive) {
|
|
695
|
+
// We'll iteratively expand the set of options using actions and delta updates
|
|
696
|
+
// until we reach a fixed point (no new options discovered). This ensures
|
|
697
|
+
// combinations across different dimensions are included (e.g., 10-24).
|
|
449
698
|
const computed = new Set();
|
|
450
|
-
|
|
699
|
+
const queue = [];
|
|
700
|
+
const enqueue = (opt) => {
|
|
701
|
+
if (!computed.has(opt)) {
|
|
702
|
+
computed.add(opt);
|
|
703
|
+
queue.push(opt);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
// 1) collect base options + partialOptions and seed the queue
|
|
451
707
|
for (const optsPromise of this._options.values()) {
|
|
452
708
|
isActive();
|
|
453
709
|
const opts = await optsPromise;
|
|
454
710
|
for (const opt of opts) {
|
|
455
711
|
isActive();
|
|
456
|
-
|
|
457
|
-
await this._applyPartials(opt,
|
|
712
|
+
enqueue(opt);
|
|
713
|
+
const partials = await this._applyPartials(opt, isActive);
|
|
714
|
+
for (const p of partials)
|
|
715
|
+
enqueue(p);
|
|
458
716
|
}
|
|
459
717
|
}
|
|
460
|
-
// 2) apply
|
|
461
|
-
|
|
462
|
-
isActive();
|
|
463
|
-
await this._applyActions(opt, computed, isActive);
|
|
464
|
-
}
|
|
465
|
-
// 3) apply full "delta updates"
|
|
466
|
-
for (const opt of [...computed]) {
|
|
718
|
+
// 2) Repeatedly apply actions and delta updates to closure
|
|
719
|
+
while (queue.length) {
|
|
467
720
|
isActive();
|
|
468
|
-
|
|
721
|
+
const current = queue.shift();
|
|
722
|
+
const actionResults = await this._applyActions(current, isActive);
|
|
723
|
+
for (const a of actionResults)
|
|
724
|
+
enqueue(a);
|
|
725
|
+
const deltaResults = await this._applyDeltaUpdates(current, isActive);
|
|
726
|
+
for (const d of deltaResults)
|
|
727
|
+
enqueue(d);
|
|
469
728
|
}
|
|
470
|
-
// 3) combine bundles
|
|
729
|
+
// 3) combine bundles on the full set
|
|
471
730
|
for (const opt of [...computed]) {
|
|
472
731
|
isActive();
|
|
473
732
|
const bundles = [...this._partialBundleOptions.values()];
|
|
@@ -476,7 +735,8 @@ export class StateNode extends LitElement {
|
|
|
476
735
|
}
|
|
477
736
|
return [...computed];
|
|
478
737
|
}
|
|
479
|
-
async _applyPartials(baseOpt,
|
|
738
|
+
async _applyPartials(baseOpt, isActive) {
|
|
739
|
+
const results = [];
|
|
480
740
|
for (const partial of this._partialOptions.values()) {
|
|
481
741
|
isActive();
|
|
482
742
|
if (!partial.id) {
|
|
@@ -485,27 +745,37 @@ export class StateNode extends LitElement {
|
|
|
485
745
|
const newProduct = await baseOpt.switchProduct({ id: partial.id, campaign: partial.campaign });
|
|
486
746
|
const newOpt = await newProduct?.getOption({ devices: partial.devices, subscription: partial.subscription });
|
|
487
747
|
if (newOpt)
|
|
488
|
-
|
|
748
|
+
results.push(newOpt);
|
|
489
749
|
}
|
|
750
|
+
return results;
|
|
490
751
|
}
|
|
491
|
-
async _applyActions(baseOpt,
|
|
752
|
+
async _applyActions(baseOpt, isActive) {
|
|
753
|
+
const results = [];
|
|
492
754
|
for (const action of this._actions.values()) {
|
|
493
755
|
isActive();
|
|
756
|
+
const targetDevices = action.devices ?? baseOpt.getDevices();
|
|
757
|
+
const targetSubscription = action.subscription ?? baseOpt.getSubscription();
|
|
758
|
+
// Skip no-op actions that don't change anything
|
|
759
|
+
if (targetDevices === baseOpt.getDevices() && targetSubscription === baseOpt.getSubscription()) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
494
762
|
const newOpt = await baseOpt.getOption({
|
|
495
|
-
devices:
|
|
496
|
-
subscription:
|
|
763
|
+
devices: targetDevices,
|
|
764
|
+
subscription: targetSubscription
|
|
497
765
|
});
|
|
498
766
|
if (newOpt)
|
|
499
|
-
|
|
767
|
+
results.push(newOpt);
|
|
500
768
|
}
|
|
769
|
+
return results;
|
|
501
770
|
}
|
|
502
|
-
async _applyDeltaUpdates(baseOpt,
|
|
771
|
+
async _applyDeltaUpdates(baseOpt, isActive) {
|
|
772
|
+
const results = [];
|
|
503
773
|
const product = baseOpt.getProduct();
|
|
504
774
|
const baseDevices = product.getDevices().values;
|
|
505
775
|
const baseSubscriptions = product.getSubscriptions().values;
|
|
506
776
|
let devices = baseOpt.getDevices();
|
|
507
777
|
let subscription = baseOpt.getSubscription();
|
|
508
|
-
//corner case for input types that define an interval by themselves
|
|
778
|
+
// corner case for input types that define an interval by themselves
|
|
509
779
|
const getValue = (action, startValue, minValue) => {
|
|
510
780
|
if (action.useAsValue) {
|
|
511
781
|
action.delta = 1;
|
|
@@ -520,9 +790,7 @@ export class StateNode extends LitElement {
|
|
|
520
790
|
// Loop until we can no longer apply the delta
|
|
521
791
|
while (true) {
|
|
522
792
|
isActive();
|
|
523
|
-
const values = action.type === "devices"
|
|
524
|
-
? baseDevices
|
|
525
|
-
: baseSubscriptions;
|
|
793
|
+
const values = action.type === "devices" ? baseDevices : baseSubscriptions;
|
|
526
794
|
const current = action.type === "devices"
|
|
527
795
|
? getValue(action, devices, baseDevices[0])
|
|
528
796
|
: getValue(action, subscription, baseSubscriptions[0]);
|
|
@@ -536,13 +804,13 @@ export class StateNode extends LitElement {
|
|
|
536
804
|
else {
|
|
537
805
|
subscription = newValue;
|
|
538
806
|
}
|
|
539
|
-
// Fetch the new option and
|
|
807
|
+
// Fetch the new option and collect it if exists
|
|
540
808
|
const newOpt = await baseOpt.getOption({ devices, subscription });
|
|
541
|
-
if (newOpt)
|
|
542
|
-
|
|
543
|
-
}
|
|
809
|
+
if (newOpt)
|
|
810
|
+
results.push(newOpt);
|
|
544
811
|
}
|
|
545
812
|
}
|
|
813
|
+
return results;
|
|
546
814
|
}
|
|
547
815
|
/**
|
|
548
816
|
* Attempts to apply the given delta action to the current value.
|
|
@@ -653,6 +921,18 @@ export class StateNode extends LitElement {
|
|
|
653
921
|
await this._syncEventTask.taskComplete;
|
|
654
922
|
await this._collectToggleTask.taskComplete;
|
|
655
923
|
await this._etaRenderTask.taskComplete;
|
|
924
|
+
// Ensure any debounced/mutation-triggered Eta runs have settled
|
|
925
|
+
await this._waitEtaSettled();
|
|
926
|
+
// Await all registered contextual children (nearest descendants only)
|
|
927
|
+
try {
|
|
928
|
+
const children = Array.from(this._contextualChildren);
|
|
929
|
+
const waits = children
|
|
930
|
+
.map((c) => c.updateComplete)
|
|
931
|
+
.filter((p) => !!p);
|
|
932
|
+
if (waits.length > 0)
|
|
933
|
+
await Promise.allSettled(waits);
|
|
934
|
+
}
|
|
935
|
+
catch { /* ignore */ }
|
|
656
936
|
return result;
|
|
657
937
|
}
|
|
658
938
|
isDeviceAndSubscriptionChange(evt) {
|
|
@@ -708,6 +988,12 @@ __decorate([
|
|
|
708
988
|
__decorate([
|
|
709
989
|
property({ attribute: 'store-name' })
|
|
710
990
|
], StateNode.prototype, "storeName", void 0);
|
|
991
|
+
__decorate([
|
|
992
|
+
property({ attribute: 'eta-mutation-delay', type: Number })
|
|
993
|
+
], StateNode.prototype, "etaMutationDelay", void 0);
|
|
994
|
+
__decorate([
|
|
995
|
+
property({ attribute: 'eta-mutation-delay-max', type: Number })
|
|
996
|
+
], StateNode.prototype, "etaMutationDelayMax", void 0);
|
|
711
997
|
__decorate([
|
|
712
998
|
property({ type: Boolean, attribute: 'ignore-events-parent' })
|
|
713
999
|
], StateNode.prototype, "ignoreEventsParent", void 0);
|