valyrian.js 7.1.2 → 7.2.0

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.
@@ -1,114 +1,55 @@
1
1
  // lib/signal/index.ts
2
- import { update } from "valyrian.js";
3
- function makeUnsubscribe(subscriptions, computed, handler, cleanup) {
4
- if (typeof cleanup === "function") {
5
- computed.cleanup = cleanup;
2
+ import { current, update, updateVnode, v } from "valyrian.js";
3
+ function Signal(initialValue) {
4
+ const context = { ...current };
5
+ if (context.vnode) {
6
+ if (!context.vnode.signals) {
7
+ context.vnode.signals = context.oldVnode?.signals || [];
8
+ context.vnode.calls = -1;
9
+ context.vnode.subscribers = context.oldVnode?.subscribers || [];
10
+ context.vnode.initialChildren = [...context.vnode.children];
11
+ }
12
+ let signal2 = context.vnode.signals[++context.vnode.calls];
13
+ if (signal2) {
14
+ return signal2;
15
+ }
6
16
  }
7
- computed.unsubscribe = () => {
8
- subscriptions.delete(handler);
9
- computed?.cleanup();
17
+ let value = initialValue;
18
+ const subscribers = [];
19
+ const subscribe = (callback) => {
20
+ if (subscribers.indexOf(callback) === -1) {
21
+ subscribers.push(callback);
22
+ }
10
23
  };
11
- }
12
- function createSubscription(signal, subscriptions, handler) {
13
- if (subscriptions.has(handler) === false) {
14
- let computed = Signal(() => handler(signal.value));
15
- let cleanup = computed();
16
- makeUnsubscribe(subscriptions, computed, handler, cleanup);
17
- subscriptions.set(handler, computed);
24
+ function get() {
25
+ return value;
18
26
  }
19
- return subscriptions.get(handler);
20
- }
21
- var updateTimeout;
22
- function delayedUpdate() {
23
- clearTimeout(updateTimeout);
24
- updateTimeout = setTimeout(update);
25
- }
26
- function Signal(value) {
27
- let subscriptions = /* @__PURE__ */ new Map();
28
- let getters = {};
29
- let forceUpdate = false;
30
- let signal = new Proxy(
31
- function(valOrPath, handler) {
32
- if (typeof valOrPath === "undefined") {
33
- return signal.value;
34
- }
35
- if (typeof valOrPath === "function") {
36
- return createSubscription(signal, subscriptions, valOrPath);
37
- }
38
- if (typeof valOrPath === "string" && typeof handler !== "undefined") {
39
- let parsed = valOrPath.split(".");
40
- let result = signal.value;
41
- let next;
42
- while (parsed.length) {
43
- next = parsed.shift();
44
- if (parsed.length > 0) {
45
- if (typeof result[next] !== "object") {
46
- result[next] = {};
47
- }
48
- result = result[next];
49
- } else {
50
- result[next] = typeof handler === "function" ? handler(result[next]) : handler;
51
- }
52
- }
53
- forceUpdate = true;
54
- signal.value = signal.value;
55
- return signal.value;
56
- }
57
- signal.value = valOrPath;
58
- return signal.value;
59
- },
60
- {
61
- set(state, prop, val) {
62
- if (prop === "value" || prop === "unsubscribe" || prop === "cleanup") {
63
- let old = state[prop];
64
- state[prop] = val;
65
- if (prop === "value" && (forceUpdate || val !== old)) {
66
- forceUpdate = false;
67
- for (let [handler, computed] of subscriptions) {
68
- computed.cleanup();
69
- let cleanup = handler(val);
70
- makeUnsubscribe(subscriptions, computed, handler, cleanup);
71
- }
72
- delayedUpdate();
73
- }
74
- return true;
75
- }
76
- return false;
77
- },
78
- get(state, prop) {
79
- if (prop === "value") {
80
- return typeof state.value === "function" ? state.value() : state.value;
81
- }
82
- if (prop === "cleanup" || prop === "unsubscribe" || prop === "getter") {
83
- return state[prop];
84
- }
85
- if (prop in getters) {
86
- return getters[prop](state.value);
87
- }
88
- }
27
+ get.value = value;
28
+ get.toJSON = get.valueOf = get;
29
+ get.toString = () => `${value}`;
30
+ const set = (newValue) => {
31
+ value = newValue;
32
+ get.value = value;
33
+ for (let i = 0, l = subscribers.length; i < l; i++) {
34
+ subscribers[i](value);
89
35
  }
90
- );
91
- Object.defineProperties(signal, {
92
- value: { value, writable: true, enumerable: true },
93
- cleanup: {
94
- value() {
95
- for (let [handler, computed] of subscriptions) {
96
- computed.unsubscribe();
97
- }
98
- },
99
- writable: true,
100
- enumerable: true
101
- },
102
- getter: {
103
- value(name, handler) {
104
- if (name in getters) {
105
- throw new Error("Named computed already exists.");
106
- }
107
- getters[name] = handler;
108
- },
109
- enumerable: true
36
+ if (context.vnode) {
37
+ let newVnode = v(context.vnode.tag, context.vnode.props, ...context.vnode.initialChildren);
38
+ newVnode.dom = context.vnode.dom;
39
+ newVnode.isSVG = context.vnode.isSVG;
40
+ context.vnode.subscribers.forEach(
41
+ (subscribers2) => subscribers2.length = 0
42
+ );
43
+ context.vnode.subscribers = [];
44
+ return updateVnode(newVnode, context.vnode);
110
45
  }
111
- });
46
+ return update();
47
+ };
48
+ let signal = [get, set, subscribe];
49
+ if (context.vnode) {
50
+ context.vnode.signals.push(signal);
51
+ context.vnode.subscribers.push(subscribers);
52
+ }
112
53
  return signal;
113
54
  }
114
55
  export {
package/lib/index.ts CHANGED
@@ -574,12 +574,15 @@ function sharedSetAttribute(name: string, value: any, newVnode: VnodeWithDom, ol
574
574
  // If the attribute value is a function, add an event listener for the attribute
575
575
  // name to the DOM element represented by mainVnode.
576
576
  if (typeof value === "function") {
577
+ // We change the name of the event to lowercase to avoid issues with case sensitivity.
578
+ // Ex "onClick" and "onclick" are the same event.
579
+ let lowercaseName = name.toLowerCase();
577
580
  // Only add the event listener if it hasn't been added yet.
578
- if (name in eventListenerNames === false) {
579
- (mainVnode as VnodeWithDom).dom.addEventListener(name.slice(2), eventListener);
580
- eventListenerNames[name] = true;
581
+ if (lowercaseName in eventListenerNames === false) {
582
+ (mainVnode as VnodeWithDom).dom.addEventListener(lowercaseName.slice(2), eventListener);
583
+ eventListenerNames[lowercaseName] = true;
581
584
  }
582
- newVnode.dom[`v-${name}`] = value;
585
+ newVnode.dom[`v-${lowercaseName}`] = value;
583
586
  return;
584
587
  }
585
588
 
@@ -742,69 +745,30 @@ export function patch(newVnode: VnodeWithDom, oldVnode?: VnodeWithDom): void {
742
745
  current.oldVnode = oldVnode;
743
746
 
744
747
  // Flatten the new tree
748
+ // Take into account that is necessary to flatten the tree before the patch process
749
+ // to let the hooks and signals work properly
745
750
  for (let i = 0; i < newTree.length; i++) {
746
751
  let newChild = newTree[i];
747
752
 
748
753
  // If the new child is a Vnode and is not a text node
749
754
  if (newChild instanceof Vnode && newChild.tag !== textTag) {
750
- // If the tag of the new child is not a string, it is a component
751
- if (typeof newChild.tag !== "string") {
752
- // Set the current component to the tag of the new child
753
- current.component = newChild.tag;
754
- // Replace the new child with the result of calling its view or bind method, passing in the props and children as arguments
755
- newTree.splice(
756
- i--,
757
- 1,
758
- ("view" in newChild.tag ? newChild.tag.view.bind(newChild.tag) : newChild.tag.bind(newChild.tag))(
759
- newChild.props,
760
- ...newChild.children
761
- )
762
- );
755
+ // If the new child tag is a string just continue the loop
756
+ if (typeof newChild.tag === "string") {
763
757
  continue;
764
758
  }
765
759
 
766
- // Set the isSVG flag for the new child if it is an SVG element or if the parent is an SVG element
767
- newChild.isSVG = newVnode.isSVG || newChild.tag === "svg";
768
-
769
- // If there is an old child at the same index
770
- if (i < oldTreeLength) {
771
- let oldChild = oldTree[i];
772
- // If the tag of the new child is the same as the tag of the old child
773
- if (newChild.tag === oldChild.tag) {
774
- // Set the dom property of the new child to the dom property of the old child
775
- newChild.dom = oldChild.dom;
776
- // If the v-keep prop is the same for both the new and old child, set the children of the new child to the children of the old child
777
- if ("v-keep" in newChild.props && newChild.props["v-keep"] === oldChild.props["v-keep"]) {
778
- newChild.children = oldChild.children;
779
- continue;
780
- }
781
-
782
- // Update the attributes of the new child based on the old child
783
- updateAttributes(newChild as VnodeWithDom, oldChild);
784
- // Recursively patch the new and old children
785
- patch(newChild as VnodeWithDom, oldChild);
786
- continue;
787
- }
788
-
789
- // Create a new dom element for the new child
790
- newChild.dom = createDomElement(newChild.tag, newChild.isSVG);
791
- // Update the attributes of the new child
792
- updateAttributes(newChild as VnodeWithDom);
793
- // Replace the old child in the dom with the new child
794
- newVnode.dom.replaceChild(newChild.dom, oldChild.dom);
795
- // Recursively patch the new child
796
- patch(newChild as VnodeWithDom);
797
- continue;
798
- }
799
-
800
- // Create a new dom element for the new child
801
- newChild.dom = createDomElement(newChild.tag, newChild.isSVG);
802
- // Update the attributes of the new child
803
- updateAttributes(newChild as VnodeWithDom);
804
- // Append the new child to the dom
805
- newVnode.dom.appendChild(newChild.dom);
806
- // Recursively patch the new child
807
- patch(newChild as VnodeWithDom);
760
+ // If the tag of the new child is not a string, it is a component
761
+ // Set the current component to the tag of the new child
762
+ current.component = newChild.tag;
763
+ // Replace the new child with the result of calling its view or bind method, passing in the props and children as arguments
764
+ newTree.splice(
765
+ i--,
766
+ 1,
767
+ ("view" in newChild.tag ? newChild.tag.view.bind(newChild.tag) : newChild.tag.bind(newChild.tag))(
768
+ newChild.props,
769
+ ...newChild.children
770
+ )
771
+ );
808
772
  continue;
809
773
  }
810
774
 
@@ -820,42 +784,96 @@ export function patch(newVnode: VnodeWithDom, oldVnode?: VnodeWithDom): void {
820
784
  continue;
821
785
  }
822
786
 
823
- // If the new child is not a Vnode, wrap it in a text Vnode
824
- newTree[i] = new Vnode(textTag, {}, []);
825
- // If the new child is a Vnode, set the dom property of the text Vnode to the dom property of the new child
787
+ // If the new child is a Vnode, set the text of the Vnode to the text content of its dom property
826
788
  if (newChild instanceof Vnode) {
827
- newTree[i].dom = newChild.dom;
828
789
  // Set the new child to the text content of its dom property
829
- newChild = (newChild as VnodeWithDom).dom.textContent;
790
+ newChild.children[0] = (newChild as VnodeWithDom).dom.textContent;
791
+ continue;
830
792
  }
831
793
 
794
+ // If the new child is not a Vnode, wrap it in a text Vnode
795
+ newChild = newTree[i] = new Vnode(textTag, {}, [newChild]);
796
+ }
797
+
798
+ // Patch the the old tree
799
+ for (let i = 0; i < newTree.length; i++) {
800
+ let newChild = newTree[i];
801
+
802
+ // If the new child is a text vnode
803
+ if (newChild.tag === textTag) {
804
+ // If there is an old child at the same index
805
+ if (i < oldTreeLength) {
806
+ let oldChild = oldTree[i];
807
+
808
+ // If the old child is a text node
809
+ if (oldChild.tag === textTag) {
810
+ // Set the dom property of the text Vnode to the dom property of the old child
811
+ newChild.dom = oldChild.dom;
812
+ // If the text content of the old child is different from the new child, update the text content of the old child
813
+ // eslint-disable-next-line eqeqeq
814
+ if (newChild.children[0] != oldChild.dom.textContent) {
815
+ oldChild.dom.textContent = newChild.children[0];
816
+ }
817
+ continue;
818
+ }
819
+
820
+ // Create a new text node for the new child
821
+ newChild.dom = document.createTextNode(newChild.children[0]);
822
+ // Replace the old child in the dom with the new text node
823
+ newVnode.dom.replaceChild(newChild.dom, oldChild.dom);
824
+ continue;
825
+ }
826
+
827
+ // Create a new text node for the new child
828
+ newChild.dom = document.createTextNode(newChild.children[0]);
829
+ // Append the new text node to the dom
830
+ newVnode.dom.appendChild(newChild.dom);
831
+ continue;
832
+ }
833
+
834
+ // If the new child is not a text node
835
+ // Set the isSVG flag for the new child if it is an SVG element or if the parent is an SVG element
836
+ newChild.isSVG = newVnode.isSVG || newChild.tag === "svg";
837
+
832
838
  // If there is an old child at the same index
833
839
  if (i < oldTreeLength) {
834
840
  let oldChild = oldTree[i];
835
-
836
- // If the old child is a text node
837
- if (oldChild.tag === textTag) {
838
- // Set the dom property of the text Vnode to the dom property of the old child
839
- newTree[i].dom = oldChild.dom;
840
- // If the text content of the old child is different from the new child, update the text content of the old child
841
- // eslint-disable-next-line eqeqeq
842
- if (newChild != oldChild.dom.textContent) {
843
- oldChild.dom.textContent = newChild;
841
+ // If the tag of the new child is the same as the tag of the old child
842
+ if (newChild.tag === oldChild.tag) {
843
+ // Set the dom property of the new child to the dom property of the old child
844
+ newChild.dom = oldChild.dom;
845
+ // If the v-keep prop is the same for both the new and old child, set the children of the new child to the children of the old child
846
+ if ("v-keep" in newChild.props && newChild.props["v-keep"] === oldChild.props["v-keep"]) {
847
+ newChild.children = oldChild.children;
848
+ continue;
844
849
  }
850
+
851
+ // Update the attributes of the new child based on the old child
852
+ updateAttributes(newChild as VnodeWithDom, oldChild);
853
+ // Recursively patch the new and old children
854
+ patch(newChild as VnodeWithDom, oldChild);
845
855
  continue;
846
856
  }
847
857
 
848
- // Create a new text node for the new child
849
- newTree[i].dom = document.createTextNode(newChild);
850
- // Replace the old child in the dom with the new text node
851
- newVnode.dom.replaceChild(newTree[i].dom, oldChild.dom);
858
+ // Create a new dom element for the new child
859
+ newChild.dom = createDomElement(newChild.tag as string, newChild.isSVG);
860
+ // Update the attributes of the new child
861
+ updateAttributes(newChild as VnodeWithDom);
862
+ // Replace the old child in the dom with the new child
863
+ newVnode.dom.replaceChild(newChild.dom, oldChild.dom);
864
+ // Recursively patch the new child
865
+ patch(newChild as VnodeWithDom);
852
866
  continue;
853
867
  }
854
868
 
855
- // Create a new text node for the new child
856
- newTree[i].dom = document.createTextNode(newChild);
857
- // Append the new text node to the dom
858
- newVnode.dom.appendChild(newTree[i].dom);
869
+ // Create a new dom element for the new child
870
+ newChild.dom = createDomElement(newChild.tag as string, newChild.isSVG);
871
+ // Update the attributes of the new child
872
+ updateAttributes(newChild as VnodeWithDom);
873
+ // Append the new child to the dom
874
+ newVnode.dom.appendChild(newChild.dom);
875
+ // Recursively patch the new child
876
+ patch(newChild as VnodeWithDom);
859
877
  }
860
878
 
861
879
  // Remove any old children that are no longer present in the new tree
@@ -898,6 +916,40 @@ export function update(): void | string {
898
916
  }
899
917
  }
900
918
 
919
+ // Update custom Vnode
920
+ // It is assumed that a first mount has already occurred, so,
921
+ // the oldVnode is not null and the dom property of the oldVnode is not null
922
+ // You need to set the dom property of the newVnode to the dom property of the oldVnode
923
+ // The same with the isSVG property
924
+ // Prefer this function over patch to allow for cleanup, onUpdate and onMount sets to be called
925
+ export function updateVnode(vnode: VnodeWithDom, oldVnode: VnodeWithDom): string | void {
926
+ // Call any cleanup functions that are registered with the onCleanupSet set
927
+ callSet(onCleanupSet);
928
+
929
+ // Recursively patch the new and old main Vnodes
930
+ patch(vnode, oldVnode);
931
+
932
+ // Set the vnode properties to the old vnode
933
+ oldVnode.tag = vnode.tag;
934
+ oldVnode.props = { ...vnode.props };
935
+ oldVnode.children = [...vnode.children];
936
+
937
+ // Call any update or mount functions that are registered with the onUpdateSet or onMountSet set
938
+ callSet(isMounted ? onUpdateSet : onMountSet);
939
+
940
+ // Set the isMounted flag to true
941
+ isMounted = true;
942
+
943
+ // Reset the current vnode, oldVnode, and component properties
944
+ current.vnode = null;
945
+ current.oldVnode = null;
946
+ current.component = null;
947
+
948
+ if (isNodeJs) {
949
+ return vnode.dom.innerHTML;
950
+ }
951
+ }
952
+
901
953
  // Unmount the main Vnode
902
954
  export function unmount() {
903
955
  // If the main Vnode exists
package/lib/interfaces.ts CHANGED
@@ -89,6 +89,7 @@ declare module "valyrian.js" {
89
89
  export function updateAttributes(newVnode: VnodeWithDom, oldVnode?: VnodeWithDom): void;
90
90
  export function patch(newVnode: VnodeWithDom, oldVnode?: VnodeWithDom): void;
91
91
  export function update(): void | string;
92
+ export function updateVnode(vnode: VnodeWithDom, oldVnode: VnodeWithDom): string | void;
92
93
  export function unmount(): string | void;
93
94
  export function mount(dom: any, component: any): string | void;
94
95
  export const v: V;
@@ -0,0 +1,187 @@
1
+ import { update } from "valyrian.js";
2
+
3
+ /* eslint-disable no-use-before-define */
4
+ interface Cleanup {
5
+ (): void;
6
+ }
7
+
8
+ interface Subscription {
9
+ // eslint-disable-next-line no-unused-vars
10
+ (value: ProxySignal["value"]): void | Cleanup;
11
+ }
12
+
13
+ interface Subscriptions extends Map<Subscription, Cleanup> {}
14
+
15
+ interface Getter {
16
+ // eslint-disable-next-line no-unused-vars
17
+ (value: ProxySignal["value"]): any;
18
+ }
19
+
20
+ interface Getters {
21
+ [key: string | symbol]: Getter;
22
+ }
23
+
24
+ interface ProxySignal {
25
+ // Works as a getter of the value
26
+ (): ProxySignal["value"];
27
+ // Works as a subscription to the value
28
+ // eslint-disable-next-line no-unused-vars
29
+ (value: Subscription): ProxySignal;
30
+ // Works as a setter with a path and a handler
31
+ // eslint-disable-next-line no-unused-vars
32
+ (path: string, handler: (valueAtPathPosition: any) => any): ProxySignal["value"];
33
+ // Works as a setter with a path and a value
34
+ // eslint-disable-next-line no-unused-vars
35
+ (path: string, value: any): ProxySignal["value"];
36
+ // Works as a setter with a value
37
+ // eslint-disable-next-line no-unused-vars
38
+ (value: any): ProxySignal["value"];
39
+ // Gets the current value of the signal.
40
+ value: any;
41
+ // Cleanup function to be called to remove all subscriptions.
42
+ cleanup: () => void;
43
+ // Creates a getter on the signal.
44
+ // eslint-disable-next-line no-unused-vars
45
+ getter: (name: string, handler: Getter) => any;
46
+ // To access the getters on the signal.
47
+ [key: string | number | symbol]: any;
48
+ }
49
+
50
+ function makeUnsubscribe(
51
+ subscriptions: Subscriptions,
52
+ computed: ProxySignal,
53
+ handler: Subscription,
54
+ cleanup?: Cleanup
55
+ ) {
56
+ if (typeof cleanup === "function") {
57
+ computed.cleanup = cleanup;
58
+ }
59
+ computed.unsubscribe = () => {
60
+ subscriptions.delete(handler);
61
+ computed?.cleanup();
62
+ };
63
+ }
64
+
65
+ function createSubscription(signal: ProxySignal, subscriptions: Subscriptions, handler: Subscription) {
66
+ if (subscriptions.has(handler) === false) {
67
+ // eslint-disable-next-line no-use-before-define
68
+ let computed = ProxySignal(() => handler(signal.value));
69
+ let cleanup = computed(); // Execute to register itself
70
+ makeUnsubscribe(subscriptions, computed, handler, cleanup);
71
+ subscriptions.set(handler, computed);
72
+ }
73
+
74
+ return subscriptions.get(handler);
75
+ }
76
+
77
+ let updateTimeout: any;
78
+ function delayedUpdate() {
79
+ clearTimeout(updateTimeout);
80
+ updateTimeout = setTimeout(update);
81
+ }
82
+
83
+ // eslint-disable-next-line sonarjs/cognitive-complexity
84
+ export function ProxySignal(value: any): ProxySignal {
85
+ let subscriptions = new Map();
86
+ let getters: Getters = {};
87
+
88
+ let forceUpdate = false;
89
+
90
+ let signal: ProxySignal = new Proxy(
91
+ // eslint-disable-next-line no-unused-vars
92
+ function (valOrPath?: any | Subscription, handler?: (valueAtPathPosition: any) => any) {
93
+ // Works as a getter
94
+ if (typeof valOrPath === "undefined") {
95
+ return signal.value;
96
+ }
97
+
98
+ // Works as a subscription
99
+ if (typeof valOrPath === "function") {
100
+ return createSubscription(signal, subscriptions, valOrPath);
101
+ }
102
+
103
+ // Works as a setter with a path
104
+ if (typeof valOrPath === "string" && typeof handler !== "undefined") {
105
+ let parsed = valOrPath.split(".");
106
+ let result = signal.value;
107
+ let next;
108
+ while (parsed.length) {
109
+ next = parsed.shift() as string;
110
+ if (parsed.length > 0) {
111
+ if (typeof result[next] !== "object") {
112
+ result[next] = {};
113
+ }
114
+ result = result[next];
115
+ } else {
116
+ result[next] = typeof handler === "function" ? handler(result[next]) : handler;
117
+ }
118
+ }
119
+ forceUpdate = true;
120
+ signal.value = signal.value;
121
+ return signal.value;
122
+ }
123
+
124
+ // Works as a setter with a value
125
+ signal.value = valOrPath;
126
+ return signal.value;
127
+ } as ProxySignal,
128
+ {
129
+ set(state, prop, val) {
130
+ if (prop === "value" || prop === "unsubscribe" || prop === "cleanup") {
131
+ let old = state[prop];
132
+ state[prop] = val;
133
+ if (prop === "value" && (forceUpdate || val !== old)) {
134
+ forceUpdate = false;
135
+ for (let [handler, computed] of subscriptions) {
136
+ computed.cleanup();
137
+ let cleanup = handler(val);
138
+ makeUnsubscribe(subscriptions, computed, handler, cleanup);
139
+ }
140
+ delayedUpdate();
141
+ }
142
+ return true;
143
+ }
144
+ return false;
145
+ },
146
+ get(state, prop) {
147
+ if (prop === "value") {
148
+ return typeof state.value === "function" ? state.value() : state.value;
149
+ }
150
+
151
+ if (prop === "cleanup" || prop === "unsubscribe" || prop === "getter") {
152
+ return state[prop];
153
+ }
154
+
155
+ if (prop in getters) {
156
+ return getters[prop](state.value);
157
+ }
158
+ }
159
+ }
160
+ );
161
+
162
+ Object.defineProperties(signal, {
163
+ value: { value, writable: true, enumerable: true },
164
+ cleanup: {
165
+ value() {
166
+ // eslint-disable-next-line no-unused-vars
167
+ for (let [handler, computed] of subscriptions) {
168
+ computed.unsubscribe();
169
+ }
170
+ },
171
+ writable: true,
172
+ enumerable: true
173
+ },
174
+ getter: {
175
+ value(name: string, handler: Getter) {
176
+ if (name in getters) {
177
+ throw new Error("Named computed already exists.");
178
+ }
179
+
180
+ getters[name] = handler;
181
+ },
182
+ enumerable: true
183
+ }
184
+ });
185
+
186
+ return signal;
187
+ }