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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -48
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.mjs +71 -48
- package/dist/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/proxy-signal/index.d.ts +23 -0
- package/dist/proxy-signal/index.d.ts.map +1 -0
- package/dist/proxy-signal/index.js +136 -0
- package/dist/proxy-signal/index.mjs +116 -0
- package/dist/signal/index.d.ts +1 -22
- package/dist/signal/index.d.ts.map +1 -1
- package/dist/signal/index.js +45 -104
- package/dist/signal/index.mjs +46 -105
- package/lib/index.ts +133 -81
- package/lib/interfaces.ts +1 -0
- package/lib/proxy-signal/index.ts +187 -0
- package/lib/signal/index.ts +88 -168
- package/package.json +3 -4
package/dist/signal/index.mjs
CHANGED
|
@@ -1,114 +1,55 @@
|
|
|
1
1
|
// lib/signal/index.ts
|
|
2
|
-
import { update } from "valyrian.js";
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 (
|
|
579
|
-
(mainVnode as VnodeWithDom).dom.addEventListener(
|
|
580
|
-
eventListenerNames[
|
|
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-${
|
|
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
|
|
751
|
-
if (typeof newChild.tag
|
|
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
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
newChild.
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
|
849
|
-
|
|
850
|
-
//
|
|
851
|
-
|
|
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
|
|
856
|
-
|
|
857
|
-
//
|
|
858
|
-
|
|
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
|
+
}
|