roqa 0.0.1 → 0.0.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.3] - 2026-01-10
9
+
10
+ ### Fixed
11
+
12
+ - Fixed critical memory leak when using `<For>` with reactive bindings to cells outside the loop (e.g., `class={get(selected) === row.id ? "danger" : ""}`)
13
+ - Subscriptions to external cells are now properly cleaned up when items are removed or the array is cleared
14
+ - `reconcileFastClear` now calls cleanup functions before clearing DOM
15
+ - The compiler now generates cleanup functions that capture `bind()` unsubscribe calls inside `<For>` and `<Show>` blocks
16
+
17
+ ## [0.0.2] - 2026-01-10
18
+
19
+ ### Fixed
20
+
21
+ - Fixed reactive bindings in `<For>` loops not updating when the bound cell references closure variables from the loop callback (e.g., `class={get(selected) === row.id ? "danger" : ""}`)
22
+ - The compiler now correctly preserves `bind()` calls when the callback body contains closure variables that wouldn't be available at `set()` call sites
23
+
8
24
  ## [0.0.1] - 2026-01-10
9
25
 
10
26
  ### Added
package/README.md CHANGED
@@ -2,10 +2,16 @@
2
2
 
3
3
  Roqa is a compile-time reactive web framework for building user interfaces and applications.
4
4
 
5
- Learn more about it at https://roqa.dev.
6
-
7
5
  ## At a glance
8
6
 
7
+ Roqa ships a familiar API and syntax for writing component based web UIs, with a few unique differences.
8
+
9
+ Under the hood every Roqa component is transformed into unbelievably optimized and performant custom elements & vanilla JavaScript. This means a compiled Roqa component is extremely portable and can be used in any other web framework or web-based environment. Additionally, by default Roqa does not use Shadow DOM, so you won't have to have to fight the styling gods to create beautiful web pages and applications –– you can use any of your favorite styling solutions out of the box.
10
+
11
+ The reactive primitive of Roqa is a `cell`. It can roughly be thought of as a signal, but at compile time, this "signal" is compiled to an ultra-lightweight plain JavaScript object. A handful of functions (i.e. `get`, `set`, `put`) are provided to manipulate the data in this object and reactive updates are automatically applied after a change is made.
12
+
13
+ Continue to learn more about Roqa at https://roqa.dev.
14
+
9
15
  ```jsx
10
16
  import { defineComponent, cell, get, set } from "roqa";
11
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roqa",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Roqa is a reactive UI framework",
5
5
  "keywords": [
6
6
  "UI",
@@ -701,16 +701,38 @@ function generateForBlock(
701
701
  // Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
702
702
  const cellRefCounts = new Map();
703
703
 
704
+ // Track cleanup functions needed from bind() calls
705
+ const cleanupVars = [];
706
+
704
707
  // Bindings inside for block
705
708
  for (const binding of processedBindings) {
706
- const bindCode = generateBinding(code, binding, usedImports, itemParam, true, cellRefCounts);
709
+ const { bindCode, cleanupVar } = generateBindingWithCleanup(
710
+ code,
711
+ binding,
712
+ usedImports,
713
+ itemParam,
714
+ true,
715
+ cellRefCounts,
716
+ cleanupVars.length,
717
+ );
707
718
  lines.push(` ${bindCode}`);
719
+ if (cleanupVar) {
720
+ cleanupVars.push(cleanupVar);
721
+ }
708
722
  }
709
723
 
710
- // Insert before anchor and return range
724
+ // Insert before anchor and return range (with cleanup if needed)
711
725
  lines.push("");
712
726
  lines.push(` anchor.before(${firstElementVar});`);
713
- lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
727
+
728
+ if (cleanupVars.length > 0) {
729
+ const cleanupCalls = cleanupVars.map((v) => `${v}()`).join("; ");
730
+ lines.push(
731
+ ` return { start: ${firstElementVar}, end: ${firstElementVar}, cleanup: () => { ${cleanupCalls}; } };`,
732
+ );
733
+ } else {
734
+ lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
735
+ }
714
736
  lines.push(` });`);
715
737
 
716
738
  return lines.join("\n");
@@ -861,16 +883,38 @@ function generateShowBlock(code, showBlock, nameGen, templateRegistry, usedImpor
861
883
  // Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
862
884
  const cellRefCounts = new Map();
863
885
 
886
+ // Track cleanup functions needed from bind() calls
887
+ const cleanupVars = [];
888
+
864
889
  // Bindings inside show block
865
890
  for (const binding of processedBindings) {
866
- const bindCode = generateBinding(code, binding, usedImports, null, true, cellRefCounts);
891
+ const { bindCode, cleanupVar } = generateBindingWithCleanup(
892
+ code,
893
+ binding,
894
+ usedImports,
895
+ null,
896
+ true,
897
+ cellRefCounts,
898
+ cleanupVars.length,
899
+ );
867
900
  lines.push(` ${bindCode}`);
901
+ if (cleanupVar) {
902
+ cleanupVars.push(cleanupVar);
903
+ }
868
904
  }
869
905
 
870
- // Insert before anchor and return range
906
+ // Insert before anchor and return range (with cleanup if needed)
871
907
  lines.push("");
872
908
  lines.push(` anchor.before(${firstElementVar});`);
873
- lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
909
+
910
+ if (cleanupVars.length > 0) {
911
+ const cleanupCalls = cleanupVars.map((v) => `${v}()`).join("; ");
912
+ lines.push(
913
+ ` return { start: ${firstElementVar}, end: ${firstElementVar}, cleanup: () => { ${cleanupCalls}; } };`,
914
+ );
915
+ } else {
916
+ lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
917
+ }
874
918
  lines.push(` }${depsCode});`);
875
919
 
876
920
  return lines.join("\n");
@@ -919,10 +963,83 @@ function generateBinding(
919
963
  itemParam = null,
920
964
  insideForBlock = false,
921
965
  cellRefCounts = null,
966
+ ) {
967
+ // At component level (not inside forBlock), we don't need cleanup tracking
968
+ // because cleanup is handled by the component's disconnectedCallback
969
+ const result = generateBindingCore(
970
+ code,
971
+ binding,
972
+ usedImports,
973
+ itemParam,
974
+ insideForBlock,
975
+ cellRefCounts,
976
+ false, // Don't generate cleanup variables
977
+ 0,
978
+ );
979
+ return result.bindCode;
980
+ }
981
+
982
+ /**
983
+ * Generate code for a binding, returning both the code and any cleanup variable
984
+ * Used inside forBlock/showBlock where cleanup is needed on item destruction
985
+ * @param {string} code - Original source code
986
+ * @param {object} binding - Binding info
987
+ * @param {Set} usedImports - Set of imports to track
988
+ * @param {string} itemParam - Item parameter name (for forBlock context)
989
+ * @param {boolean} insideForBlock - Whether we're inside a forBlock callback
990
+ * @param {Map} cellRefCounts - Map to track ref counts per cell (for numbered refs)
991
+ * @param {number} cleanupIndex - Index for naming cleanup variables
992
+ * @returns {{ bindCode: string, cleanupVar: string|null }}
993
+ */
994
+ function generateBindingWithCleanup(
995
+ code,
996
+ binding,
997
+ usedImports,
998
+ itemParam = null,
999
+ insideForBlock = false,
1000
+ cellRefCounts = null,
1001
+ cleanupIndex = 0,
1002
+ ) {
1003
+ return generateBindingCore(
1004
+ code,
1005
+ binding,
1006
+ usedImports,
1007
+ itemParam,
1008
+ insideForBlock,
1009
+ cellRefCounts,
1010
+ true, // Generate cleanup variables
1011
+ cleanupIndex,
1012
+ );
1013
+ }
1014
+
1015
+ /**
1016
+ * Core binding generation logic
1017
+ * @param {string} code - Original source code
1018
+ * @param {object} binding - Binding info
1019
+ * @param {Set} usedImports - Set of imports to track
1020
+ * @param {string} itemParam - Item parameter name (for forBlock context)
1021
+ * @param {boolean} insideForBlock - Whether we're inside a forBlock callback
1022
+ * @param {Map} cellRefCounts - Map to track ref counts per cell (for numbered refs)
1023
+ * @param {boolean} generateCleanup - Whether to generate cleanup variables for bind() calls
1024
+ * @param {number} cleanupIndex - Index for naming cleanup variables
1025
+ * @returns {{ bindCode: string, cleanupVar: string|null }}
1026
+ */
1027
+ function generateBindingCore(
1028
+ code,
1029
+ binding,
1030
+ usedImports,
1031
+ itemParam = null,
1032
+ insideForBlock = false,
1033
+ cellRefCounts = null,
1034
+ generateCleanup = false,
1035
+ cleanupIndex = 0,
922
1036
  ) {
923
1037
  // Handle prop bindings (for custom elements)
924
1038
  if (binding.type === "prop") {
925
- return generatePropBinding(code, binding, usedImports, insideForBlock);
1039
+ return {
1040
+ bindCode: generatePropBinding(code, binding, usedImports, insideForBlock),
1041
+ cleanupVar: null,
1042
+ };
926
1043
  }
927
1044
 
928
1045
  const {
@@ -940,7 +1057,16 @@ function generateBinding(
940
1057
 
941
1058
  // Handle new contentParts format (concatenated text content)
942
1059
  if (contentParts) {
943
- return generateContentPartsBinding(code, binding, usedImports, insideForBlock, cellRefCounts);
1060
+ return {
1061
+ bindCode: generateContentPartsBinding(
1062
+ code,
1063
+ binding,
1064
+ usedImports,
1065
+ insideForBlock,
1066
+ cellRefCounts,
1067
+ ),
1068
+ cleanupVar: null,
1069
+ };
944
1070
  }
945
1071
 
946
1072
  // Build prefix string if we have static text before the dynamic expression
@@ -957,9 +1083,15 @@ function generateBinding(
957
1083
  // Static assignment
958
1084
  const exprCode = generateExpr(code, fullExpression);
959
1085
  if (needsSetAttribute) {
960
- return `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${exprCode});`;
1086
+ return {
1087
+ bindCode: `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${exprCode});`,
1088
+ cleanupVar: null,
1089
+ };
961
1090
  }
962
- return `${targetVar}.${targetProperty} = ${prefixCode}${exprCode};`;
1091
+ return {
1092
+ bindCode: `${targetVar}.${targetProperty} = ${prefixCode}${exprCode};`,
1093
+ cleanupVar: null,
1094
+ };
963
1095
  }
964
1096
 
965
1097
  // Generate the cell argument code
@@ -982,8 +1114,11 @@ function generateBinding(
982
1114
 
983
1115
  // Emit: initial value assignment + ref storage on cell
984
1116
  // cell.ref_N = element;
985
- return `${targetVar}.${targetProperty} = ${initialExprCode};
986
- ${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
1117
+ return {
1118
+ bindCode: `${targetVar}.${targetProperty} = ${initialExprCode};
1119
+ ${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`,
1120
+ cleanupVar: null,
1121
+ };
987
1122
  }
988
1123
 
989
1124
  // Fall back to bind() for complex bindings (or SVG attributes)
@@ -1000,18 +1135,50 @@ ${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
1000
1135
  bindExprCode = "v";
1001
1136
  }
1002
1137
 
1003
- // Set initial value AND bind for updates
1138
+ // Determine indentation based on context
1139
+ const indent = insideForBlock ? " " : " ";
1140
+
1141
+ // Generate with or without cleanup variable capture
1142
+ if (generateCleanup) {
1143
+ const cleanupVar = `_cleanup_${cleanupIndex}`;
1144
+
1145
+ if (needsSetAttribute) {
1146
+ return {
1147
+ bindCode: `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
1148
+ ${indent}const ${cleanupVar} = bind(${cellCode}, (v) => {
1149
+ ${indent} ${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
1150
+ ${indent}});`,
1151
+ cleanupVar,
1152
+ };
1153
+ }
1154
+
1155
+ return {
1156
+ bindCode: `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
1157
+ ${indent}const ${cleanupVar} = bind(${cellCode}, (v) => {
1158
+ ${indent} ${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
1159
+ ${indent}});`,
1160
+ cleanupVar,
1161
+ };
1162
+ }
1163
+
1164
+ // No cleanup needed - just generate the bind without capturing
1004
1165
  if (needsSetAttribute) {
1005
- return `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
1006
- bind(${cellCode}, (v) => {
1007
- ${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
1008
- });`;
1166
+ return {
1167
+ bindCode: `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
1168
+ ${indent}bind(${cellCode}, (v) => {
1169
+ ${indent} ${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
1170
+ ${indent}});`,
1171
+ cleanupVar: null,
1172
+ };
1009
1173
  }
1010
1174
 
1011
- return `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
1012
- bind(${cellCode}, (v) => {
1013
- ${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
1014
- });`;
1175
+ return {
1176
+ bindCode: `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
1177
+ ${indent}bind(${cellCode}, (v) => {
1178
+ ${indent} ${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
1179
+ ${indent}});`,
1180
+ cleanupVar: null,
1181
+ };
1015
1182
  }
1016
1183
 
1017
1184
  /**
@@ -458,7 +458,7 @@ function findBlockInfo(ast, code) {
458
458
  * Returns a map from cell code -> array of callback info
459
459
  */
460
460
  function findBindCallbacks(ast, code) {
461
- // Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd }]
461
+ // Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd, hasClosureVars }]
462
462
  const bindCallbacks = new Map();
463
463
  // Track ref numbers per cell
464
464
  const refCounters = new Map();
@@ -495,7 +495,9 @@ function findBindCallbacks(ast, code) {
495
495
  }
496
496
 
497
497
  // Find element variables used in the callback (e.g., p_1_text, tr_1)
498
- const elementVars = findElementVariables(body);
498
+ // Also detect closure variables that would prevent inlining
499
+ const { elementVars, closureVars } = findElementVariables(body, code, paramName, cellCode);
500
+ const hasClosureVars = closureVars.size > 0;
499
501
 
500
502
  // Get or create ref number for this cell
501
503
  const currentRef = refCounters.get(cellCode) || 0;
@@ -514,6 +516,7 @@ function findBindCallbacks(ast, code) {
514
516
  paramName,
515
517
  statementStart: path.node.start,
516
518
  statementEnd: path.node.end,
519
+ hasClosureVars,
517
520
  });
518
521
  },
519
522
  noScope: true,
@@ -525,15 +528,44 @@ function findBindCallbacks(ast, code) {
525
528
  /**
526
529
  * Find element variables used in a callback body
527
530
  * Looks for element.property = ... patterns
531
+ * Also returns closure variables that would prevent inlining
528
532
  */
529
- function findElementVariables(body) {
533
+ function findElementVariables(body, code, paramName, cellCode) {
530
534
  const elementVars = [];
535
+ const closureVars = new Set();
531
536
  const seen = new Set();
532
537
 
538
+ // Extract the cell's base identifier (e.g., "selected" from "selected" or "row.label" from "row.label")
539
+ const cellBaseMatch = cellCode.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
540
+ const cellBase = cellBaseMatch ? cellBaseMatch[1] : cellCode;
541
+
542
+ // Track identifiers that are used as property names (not variables)
543
+ const propertyNames = new Set();
544
+
545
+ // First pass: identify property names in member expressions
546
+ function findPropertyNames(node) {
547
+ if (!node) return;
548
+ if (node.type === "MemberExpression" && node.property.type === "Identifier" && !node.computed) {
549
+ // The property is accessed with dot notation, so it's not a variable reference
550
+ propertyNames.add(node.property);
551
+ }
552
+ for (const key of Object.keys(node)) {
553
+ const child = node[key];
554
+ if (child && typeof child === "object") {
555
+ if (Array.isArray(child)) {
556
+ child.forEach(findPropertyNames);
557
+ } else if (child.type) {
558
+ findPropertyNames(child);
559
+ }
560
+ }
561
+ }
562
+ }
563
+ findPropertyNames(body);
564
+
533
565
  function visit(node) {
534
566
  if (!node) return;
535
567
 
536
- // Look for element.property = ... patterns
568
+ // Look for element.property = ... patterns (element variable assignments)
537
569
  if (
538
570
  node.type === "AssignmentExpression" &&
539
571
  node.left.type === "MemberExpression" &&
@@ -546,6 +578,57 @@ function findElementVariables(body) {
546
578
  }
547
579
  }
548
580
 
581
+ // Look for identifier.property patterns (potential closure variables)
582
+ // e.g., row.id, item.name - these are closure variables from forBlock
583
+ if (node.type === "MemberExpression" && node.object.type === "Identifier") {
584
+ const varName = node.object.name;
585
+ // Skip if it's the callback parameter, the cell, or an already identified element var
586
+ if (varName !== paramName && varName !== cellBase && !seen.has(varName)) {
587
+ // This could be a closure variable - mark it
588
+ closureVars.add(varName);
589
+ }
590
+ }
591
+
592
+ // Also check for standalone identifiers that could be closure variables
593
+ // But skip identifiers that are property names in member expressions
594
+ if (node.type === "Identifier" && !propertyNames.has(node)) {
595
+ const varName = node.name;
596
+ // Skip common globals and the callback parameter
597
+ const knownGlobals = new Set([
598
+ "undefined",
599
+ "null",
600
+ "true",
601
+ "false",
602
+ "NaN",
603
+ "Infinity",
604
+ "console",
605
+ "window",
606
+ "document",
607
+ "Math",
608
+ "JSON",
609
+ "Array",
610
+ "Object",
611
+ "String",
612
+ "Number",
613
+ "Boolean",
614
+ "Date",
615
+ "RegExp",
616
+ "Error",
617
+ "Promise",
618
+ "Map",
619
+ "Set",
620
+ ]);
621
+ if (
622
+ varName !== paramName &&
623
+ varName !== cellBase &&
624
+ !seen.has(varName) &&
625
+ !knownGlobals.has(varName)
626
+ ) {
627
+ // Could be a closure variable
628
+ closureVars.add(varName);
629
+ }
630
+ }
631
+
549
632
  // Recurse into child nodes
550
633
  for (const key of Object.keys(node)) {
551
634
  const child = node[key];
@@ -560,7 +643,13 @@ function findElementVariables(body) {
560
643
  }
561
644
 
562
645
  visit(body);
563
- return elementVars;
646
+
647
+ // Remove element vars from closure vars (they're defined in the same scope level)
648
+ for (const { varName } of elementVars) {
649
+ closureVars.delete(varName);
650
+ }
651
+
652
+ return { elementVars, closureVars };
564
653
  }
565
654
 
566
655
  /**
@@ -811,8 +900,12 @@ export function inlineGetCalls(code, filename) {
811
900
  }
812
901
 
813
902
  if (callbacks && callbacks.length > 0) {
814
- // Filter to only include callbacks that have element vars (were fully inlined)
815
- const inlinableCallbacks = callbacks.filter((cb) => cb.elementVars.length > 0);
903
+ // Filter to only include callbacks that:
904
+ // 1. Have element vars (can update DOM elements)
905
+ // 2. Don't have closure vars (can be inlined at set() call sites)
906
+ const inlinableCallbacks = callbacks.filter(
907
+ (cb) => cb.elementVars.length > 0 && !cb.hasClosureVars,
908
+ );
816
909
  return inlinableCallbacks
817
910
  .map((cb) => {
818
911
  let body = transformCallbackBody(
@@ -927,11 +1020,15 @@ export function inlineGetCalls(code, filename) {
927
1020
  const blockUpdate = call.blockVar ? `${call.blockVar}.update();` : "";
928
1021
 
929
1022
  // Check if there are non-inlined bind callbacks for this cell
930
- // These are callbacks without element vars that were kept as runtime bind() calls
1023
+ // These are callbacks that:
1024
+ // 1. Have no element vars (can't update DOM), OR
1025
+ // 2. Have closure vars (can't be inlined at set() call sites)
931
1026
  let hasNonInlinedBinds = false;
932
1027
  const cellCallbacks = bindCallbacks.get(call.cellCode);
933
1028
  if (cellCallbacks) {
934
- hasNonInlinedBinds = cellCallbacks.some((cb) => cb.elementVars.length === 0);
1029
+ hasNonInlinedBinds = cellCallbacks.some(
1030
+ (cb) => cb.elementVars.length === 0 || cb.hasClosureVars,
1031
+ );
935
1032
  }
936
1033
 
937
1034
  // Check if this cell is a source for forBlock/showBlock
@@ -978,6 +1075,13 @@ export function inlineGetCalls(code, filename) {
978
1075
  bindStatementsToRemove.sort((a, b) => b.start - a.start);
979
1076
 
980
1077
  for (const { start, end, cellCode, callback } of bindStatementsToRemove) {
1078
+ // Don't inline bind() calls that have closure variables (e.g., from forBlock item parameter)
1079
+ // These callbacks capture variables like `row` that won't be in scope at set() call sites
1080
+ if (callback.hasClosureVars) {
1081
+ // Keep the bind() call - runtime handles execution
1082
+ continue;
1083
+ }
1084
+
981
1085
  // Generate ref assignment for each element variable
982
1086
  let refAssignment = "";
983
1087
  for (const { varName } of callback.elementVars) {
@@ -997,8 +1101,10 @@ export function inlineGetCalls(code, filename) {
997
1101
 
998
1102
  // Remove imports that are no longer needed
999
1103
  // Collect all imports to remove first
1000
- // Only remove bind import if ALL bind calls were fully inlined (had element vars)
1001
- const allBindsInlined = bindStatementsToRemove.every((b) => b.callback.elementVars.length > 0);
1104
+ // Only remove bind import if ALL bind calls were fully inlined (had element vars and no closure vars)
1105
+ const allBindsInlined = bindStatementsToRemove.every(
1106
+ (b) => b.callback.elementVars.length > 0 && !b.callback.hasClosureVars,
1107
+ );
1002
1108
  const shouldRemoveBind = bindStatementsToRemove.length > 0 && allBindsInlined;
1003
1109
  const importsToActuallyRemove = importsToRemove.filter(({ name }) => {
1004
1110
  return (
@@ -122,6 +122,13 @@ function destroyItem(item) {
122
122
  * Fast path: clear all items when going from non-empty to empty
123
123
  */
124
124
  function reconcileFastClear(anchor, forState, array) {
125
+ // Run cleanup for all items before clearing DOM
126
+ const items = forState.items;
127
+ for (let i = 0; i < items.length; i++) {
128
+ const state = items[i].s;
129
+ if (state.cleanup) state.cleanup();
130
+ }
131
+
125
132
  const parent_node = anchor.parentNode;
126
133
  parent_node.textContent = "";
127
134
  parent_node.append(anchor);