UNPKG

deep-state-observer

Version:

Deep state observer is an state management library that will fire listeners only when specified object node (which also can be a wildcard) was changed.

1,512 lines (1,357 loc) 47.8 kB
import State from "../index.esm.js"; import path from "path"; import fs from "fs"; describe("State", () => { it("should match simple wildcards", () => { const state = new State({}); expect(state.match("te*t", "test")).toEqual(true); expect(state.match("*est", "test")).toEqual(true); expect(state.match("te*", "test")).toEqual(true); expect(state.match("*", "test")).toEqual(true); expect(state.match("*", "")).toEqual(true); expect(state.match("", "test")).toEqual(false); expect(state.match("xy*", "test")).toEqual(false); expect(state.match("*xy", "test")).toEqual(false); expect(state.match("one.two.three.*.five", "one.two.three.four.five")).toEqual(true); expect(state.match("one.two.three.*.five", "one.two.three.four")).toEqual(false); }); it("should check existence of methods and data", () => { const state = new State({ test: "123" }); expect(typeof state).toEqual("object"); expect(typeof state.subscribe).toBe("function"); expect(typeof state.subscribeAll).toBe("function"); expect(typeof state.update).toBe("function"); expect(typeof state.get).toBe("function"); expect(typeof state.destroy).toBe("function"); state.destroy(); }); it("should call State", () => { const state = new State({ a: "a", b: "b", c: { d: "d" } }); let $d; state.subscribe("c.d", (d) => { $d = d; }); expect($d).toEqual("d"); state.destroy(); }); it("should update and watch", () => { const state = new State({ test: { test2: 123, }, }); let test2 = 0; let event = 0; state.subscribe("test.test2", (value) => { test2 = value; if (event === 0) { expect(value).toEqual(123); } else { expect(value).toEqual(100); } event++; }); expect(test2).toEqual(123); state.update("test.test2", (oldValue) => { return 100; }); expect(test2).toEqual(100); state.destroy(); }); it("should notify nested subscribers about change", () => { const state = new State({ one: { two: { three: { four: "go!" } } }, }); const paths = []; const values = []; state.subscribe("one.two.three.four", (value, eventInfo) => { values.push(value); paths.push(eventInfo.path.resolved); }); expect(paths[0]).toEqual("one.two.three.four"); expect(values[0]).toEqual("go!"); state.update("one", { two: { three: { four: "modified" } } }); expect(paths[1]).toEqual("one.two.three.four"); expect(values[1]).toEqual("modified"); }); it("should watch all paths", () => { const state = new State({ x: 10, y: 20, z: { xyz: 50 } }); let result = {}; const paths = []; state.subscribeAll(["x", "y", "z.xyz"], (value, eventInfo) => { paths.push(eventInfo.path.resolved); }); expect(paths).toEqual(["x", "y", "z.xyz"]); state.destroy(); }); it("should accept value instead of function inside update", () => { const state = new State({ x: 10, y: 20, z: { xyz: 50 } }); expect(state.get()).toEqual({ x: 10, y: 20, z: { xyz: 50 } }); state.update("z.xyz", "string instead of fn"); expect(state.get("z.xyz")).toEqual("string instead of fn"); state.destroy(); }); it("should match wildcards (object)", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } }, 2: { three: { four: { five: 5, six: 6 } } }, }, }); const paths = []; const values = []; state.subscribe("one.*.three", (value, eventInfo) => { paths.push(eventInfo.path.resolved); // 2 values.push(value); }); state.subscribe("one.*.*.four.*", (value, eventInfo) => { paths.push(eventInfo.path.resolved); // 3 values.push(value); }); expect(paths.length).toEqual(5); expect(values.length).toEqual(5); expect(values[0]).toEqual({ four: { five: 5, six: 6 } }); expect(values[1]).toEqual({ four: { five: 5 } }); expect(values[2]).toEqual(5); expect(values[3]).toEqual(6); expect(values[4]).toEqual(5); const fullPath = "one.two.three.four.five"; state.update(fullPath, "mod"); expect(paths.length).toEqual(7); expect(values.length).toEqual(7); expect(values[5]).toEqual({ four: { five: "mod" } }); expect(values[6]).toEqual("mod"); }); it("should match wildcards (array)", () => { const state = new State({ one: [{ two: 2 }, { two: 22 }, { two: 222 }, { three: 3 }, [{ test: "x" }]], }); const paths = []; const values = []; state.subscribe("one.*.two", (value, eventInfo) => { paths.push(eventInfo.path.resolved); // 3 values.push(value); }); state.subscribe("one.*.*.test", (value, eventInfo) => { paths.push(eventInfo.path.resolved); // 1 values.push(value); }); state.subscribe("one.*.three", (value, eventInfo) => { paths.push(eventInfo.path.resolved); // 1 values.push(value); }); expect(paths.length).toEqual(5); expect(values.length).toEqual(5); expect(values[0]).toEqual(2); expect(values[1]).toEqual(22); expect(values[2]).toEqual(222); expect(values[3]).toEqual("x"); expect(values[4]).toEqual(3); const fullPath = "one.0.two"; state.update(fullPath, "mod"); expect(paths.length).toEqual(6); expect(values.length).toEqual(6); expect(values[5]).toEqual("mod"); }); it("should watch recursively", () => { const state = new State({ one: { two: { three: 3 }, 2: 2 } }); const paths = []; const values = []; state.subscribe("one", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths.length).toEqual(1); expect(values.length).toEqual(1); expect(paths[0]).toEqual("one"); expect(values[0]).toEqual({ two: { three: 3 }, 2: 2 }); state.update("one.two.three", 33); expect(paths.length).toEqual(2); expect(values.length).toEqual(2); expect(paths[1]).toEqual("one.two.three"); expect(values[1]).toEqual({ two: { three: 33 }, 2: 2 }); }); it("should watch recursively within path (subscribe)", () => { const state = new State({ one: { two: { three: { four: 4 } }, 2: 2 } }); const paths = []; const values = []; state.subscribe("one.two.three", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths.length).toEqual(1); expect(values.length).toEqual(1); expect(paths[0]).toEqual("one.two.three"); expect(values[0]).toEqual({ four: 4 }); state.update("one.two.three", { four: 44 }); expect(paths.length).toEqual(2); expect(values.length).toEqual(2); expect(paths[1]).toEqual("one.two.three"); expect(values[1]).toEqual({ four: 44 }); }); it("should watch recursively within path (subscribeAll)", () => { const state = new State({ one: { two: { three: { four: [{ x: 1 }, { y: 2 }, { z: 3 }] } }, 2: 2 }, }); const paths = []; const values = []; state.subscribeAll(["one.two.three.four.0"], (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths.length).toEqual(1); expect(values.length).toEqual(1); expect(paths[0]).toEqual("one.two.three.four.0"); expect(values[0]).toEqual({ x: 1 }); state.update("one.two.three.four", [{ x: 2 }, { y: 2 }, { z: 3 }]); expect(paths.length).toEqual(2); expect(values.length).toEqual(2); expect(paths[1]).toEqual("one.two.three.four.0"); expect(values[1]).toEqual({ x: 2 }); }); it("should watch recursively within path and return final value if it is not an object/array", () => { const state = new State({ one: { two: { three: { four: [{ x: 1 }, { y: 2 }, { z: 3 }] } }, 2: 2 }, }); const paths = []; const values = []; state.subscribeAll(["one.two.three.four.0.x"], (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths.length).toEqual(1); expect(values.length).toEqual(1); expect(paths[0]).toEqual("one.two.three.four.0.x"); expect(values[0]).toEqual(1); state.update("one.two.three.four", [{ x: 2 }, { y: 2 }, { z: 3 }]); expect(paths.length).toEqual(2); expect(values.length).toEqual(2); expect(paths[1]).toEqual("one.two.three.four.0.x"); expect(values[1]).toEqual(2); }); it("should watch recursively within object with numeric values", () => { const state = new State({ one: { two: { 1: { x: 1 }, 2: { x: 2 }, 3: { x: 3 }, }, }, }); const paths = []; const values = []; state.subscribeAll(["one.two"], (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths.length).toEqual(1); expect(values.length).toEqual(1); expect(paths[0]).toEqual("one.two"); expect(values[0]).toEqual({ 1: { x: 1 }, 2: { x: 2 }, 3: { x: 3 }, }); state.update("one.two.2.x", 22); expect(paths.length).toEqual(2); expect(values.length).toEqual(2); expect(paths[1]).toEqual("one.two.2.x"); expect(values[1]).toEqual({ 1: { x: 1 }, 2: { x: 22 }, 3: { x: 3 }, }); }); it("should match simple params", () => { const state = new State({ users: { 1: { name: "john", age: 35 }, 2: { name: "alice", age: 30 } }, }); const paths = []; const values = []; const params = []; state.subscribe("users.:id.name", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); params.push(eventInfo.params); }); expect(paths.length).toEqual(2); expect(paths[0]).toEqual("users.1.name"); expect(paths[1]).toEqual("users.2.name"); expect(values[0]).toEqual("john"); expect(values[1]).toEqual("alice"); state.update("users.1.name", "madmax"); expect(paths.length).toEqual(3); expect(paths[2]).toEqual("users.1.name"); expect(values[2]).toEqual("madmax"); }); it("should match simple params and return bulk value", () => { const state = new State({ users: { 1: { name: "john", age: 35 }, 2: { name: "alice", age: 30 } }, }); const paths = []; const values = []; const params = []; state.subscribe( "users.:id.name", (bulk, eventInfo) => { bulk.forEach((item) => { paths.push(item.path); values.push(item.value); params.push(item.params); }); }, { bulk: true } ); expect(paths.length).toEqual(2); expect(paths[0]).toEqual("users.1.name"); expect(paths[1]).toEqual("users.2.name"); expect(values[0]).toEqual("john"); expect(values[1]).toEqual("alice"); state.update("users.1.name", "madmax"); expect(paths.length).toEqual(3); expect(paths[2]).toEqual("users.1.name"); expect(values[2]).toEqual("madmax"); }); it("should update proper leaf", () => { const state = new State({ config: { list: { rows: { 1: { id: "id-1" }, 2: { id: "id-2" }, 3: { id: "id-3" }, }, }, }, internal: { list: { rows: { 1: { id: "id-1" }, 2: { id: "id-2" }, 3: { id: "id-3" }, }, }, }, }); const paths = []; const values = []; state.subscribe("config.list.rows", (value, eventInfo) => { paths.push(eventInfo.path.resolved); state.update("internal.list.rows", { ...value }); }); expect(paths[0]).toEqual("config.list.rows"); }); it("should bulk wildcarded subscribeAll", () => { const state = new State({ something: [{ x: 1 }, { x: 1 }, { x: 1 }, { x: 1 }], }); let count = 0; state.subscribeAll( ["something", "something.*.x"], (bulk) => { count++; }, { bulk: true } ); expect(count).toEqual(2); state.update("something", [...state.get("something"), { x: "added" }]); expect(count).toEqual(4); }); it("should update values from wildcard path", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } } }, }); const paths = []; const values = []; state.subscribe("one.*.three.four.five", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths.length).toEqual(1); expect(paths[0]).toEqual("one.two.three.four.five"); expect(values[0]).toEqual(5); state.update("one.two.*.four.five", 55); expect(paths.length).toEqual(2); expect(paths[1]).toEqual("one.two.three.four.five"); expect(state.get("one.two.three.four.five")).toEqual(55); expect(values[1]).toEqual(55); state.update("one.two.*.four.five", 555); expect(paths[2]).toEqual("one.two.three.four.five"); expect(values[2]).toEqual(555); expect(state.get("one.two.three.four.five")).toEqual(555); state.update("*.two.*.four.five", 5555); expect(paths[3]).toEqual("one.two.three.four.five"); expect(values[3]).toEqual(5555); expect(state.get("one.two.three.four.five")).toEqual(5555); }); it("should not listen to recursive changes", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } } }, }); const paths = []; const values = []; state.subscribe("one.*.three;", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths[0]).toEqual("one.two.three"); expect(values[0]).toEqual({ four: { five: 5 } }); state.update("one.two.*.four.five", 55); expect(paths.length).toEqual(1); expect(values.length).toEqual(1); expect(state.get("one.two.three.four.five")).toEqual(55); state.update("one.two.*", { four: { five: 555 } }); expect(paths.length).toEqual(2); expect(paths[1]).toEqual("one.two.three"); expect(values[1]).toEqual({ four: { five: 555 } }); expect(state.get("one.two.three.four.five")).toEqual(555); state.update("*.two.*.four.five", 5555); expect(paths.length).toEqual(2); expect(values.length).toEqual(2); expect(state.get("one.two.three.four.five")).toEqual(5555); }); it("should update values from wildcard path (children)", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } } }, }); const paths = []; const values = []; state.subscribe("one.*.three.four.five", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths[0]).toEqual("one.two.three.four.five"); expect(values[0]).toEqual(5); state.update("one.two.*.four", { five: 55 }); expect(paths[1]).toEqual("one.two.three.four.five"); expect(values[1]).toEqual(55); expect(state.get("one.two.three.four.five")).toEqual(55); state.update("one.two.*.four", { five: 555 }); expect(paths[2]).toEqual("one.two.three.four.five"); expect(values[2]).toEqual(555); expect(state.get("one.two.three.four.five")).toEqual(555); state.update("*.two.*.four", { five: 5555 }); expect(paths[3]).toEqual("one.two.three.four.five"); expect(values[3]).toEqual(5555); expect(state.get("one.two.three.four.five")).toEqual(5555); }); it("should update values from wildcard path (recursive)", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } } }, }); const paths = []; const values = []; state.subscribe("one.two.three.four", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths[0]).toEqual("one.two.three.four"); expect(values[0]).toEqual({ five: 5 }); state.update("one.two.*.four.five", 55); expect(paths[1]).toEqual("one.two.three.four.five"); expect(values[1]).toEqual({ five: 55 }); expect(state.get("one.two.three.four.five")).toEqual(55); state.update("one.two.*.four.five", 555); expect(paths[2]).toEqual("one.two.three.four.five"); expect(values[2]).toEqual({ five: 555 }); expect(state.get("one.two.three.four.five")).toEqual(555); state.update("*.two.*.four.five", 5555); expect(paths[3]).toEqual("one.two.three.four.five"); expect(values[3]).toEqual({ five: 5555 }); expect(state.get("one.two.three.four.five")).toEqual(5555); }); it("should update values from wildcard path (recursive & wildcard)", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } } }, }); const paths = []; const values = []; state.subscribe("one.*.three.four", (value, eventInfo) => { paths.push(eventInfo.path.resolved); values.push(value); }); expect(paths[0]).toEqual("one.two.three.four"); expect(values[0]).toEqual({ five: 5 }); state.update("one.two.*.four.five", 55); expect(paths[1]).toEqual("one.two.three.four.five"); expect(values[1]).toEqual({ five: 55 }); expect(state.get("one.two.three.four.five")).toEqual(55); state.update("one.two.*.four.five", 555); expect(paths[2]).toEqual("one.two.three.four.five"); expect(values[2]).toEqual({ five: 555 }); expect(state.get("one.two.three.four.five")).toEqual(555); state.update("*.two.*.four.five", 5555); expect(paths[3]).toEqual("one.two.three.four.five"); expect(values[3]).toEqual({ five: 5555 }); expect(state.get("one.two.three.four.five")).toEqual(5555); }); it("should notify only specified strict listeners", () => { const state = new State({ one: { two: { three: 3 } }, }); const values = []; const paths = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe("one.two.three", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); expect(values.length).toEqual(2); state.update("one.two", { three: 33 }, { only: ["three"] }); expect(paths.length).toEqual(3); expect(values[2]).toEqual(33); expect(paths[2]).toEqual("one.two.three"); expect(state.get("one.two.three")).toEqual(33); }); it("should notify only specified strict listeners #2", () => { const state = new State({ one: { two: { three: { four: { five: 5 } } } }, }); const values = []; const paths = []; state.subscribe("one.two.*.four", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe("one.two.three.*.five", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); expect(values.length).toEqual(2); state.update( "one.two.three", function three(value) { value.four = { five: 55 }; return value; }, { only: ["four"] } ); expect(paths.length).toEqual(3); expect(values[2]).toEqual({ five: 55 }); expect(paths[2]).toEqual("one.two.three.four"); expect(state.get("one.two.three.four.five")).toEqual(55); }); it("should notify only specified strict listeners (nested)", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const paths = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe("one.two.three.four", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); expect(values.length).toEqual(2); state.update("one.two", { three: { four: 44 } }, { only: ["three.four"] }); expect(paths.length).toEqual(3); expect(values[2]).toEqual(44); expect(paths[2]).toEqual("one.two.three.four"); expect(state.get("one.two.three.four")).toEqual(44); }); it("should notify only specified strict listeners (nested & wildcard)", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const paths = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe("one.two.three.four", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); expect(values.length).toEqual(2); state.update("one.two", { three: { four: 44 } }, { only: ["*.four"] }); expect(paths.length).toEqual(3); expect(values[2]).toEqual(44); expect(paths[2]).toEqual("one.two.three.four"); expect(state.get("one.two.three.four")).toEqual(44); }); it("should notify only specified strict listeners (nested & wildcard 2)", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const paths = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe("one.two.*.four", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); expect(values.length).toEqual(2); state.update("one.two", { three: { four: 44 } }, { only: ["*.four"] }); expect(paths.length).toEqual(3); expect(values[2]).toEqual(44); expect(paths[2]).toEqual("one.two.three.four"); expect(state.get("one.two.three.four")).toEqual(44); }); it("should notify only specified strict listeners (nested & wildcard & bulk)", () => { const base = { one: { two: { three: { four: 4 } } }, }; for (let i = 1; i < 10; i++) { base.one.two["three" + i] = { four: 4 }; } const state = new State(base); const values = []; const paths = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe( "one.two.*.four", (bulk) => { values.push("bulk"); paths.push("bulk"); }, { bulk: true } ); expect(values.length).toEqual(2); state.update( "one.two", (current) => { const two = state.get("one.two"); for (const three in two) { two[three] = { four: 44 }; } return current; }, { only: ["*.four"] } ); expect(paths.length).toEqual(3); expect(values[2]).toEqual("bulk"); expect(paths[2]).toEqual("bulk"); expect(state.get("one.two.three.four")).toEqual(44); expect(state.get("one.two.three8.four")).toEqual(44); }); it("should notify only specified strict listeners (nested & wildcard)", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const paths = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); state.subscribe("one.two.three.four", (val, eventInfo) => { values.push(val); paths.push(eventInfo.path.resolved); }); expect(values.length).toEqual(2); state.update("one.two", { three: { four: 44 } }, { only: ["*.four"] }); expect(paths.length).toEqual(3); expect(values[2]).toEqual(44); expect(paths[2]).toEqual("one.two.three.four"); expect(state.get("one.two.three.four")).toEqual(44); }); it("should destroy listeners", () => { const state = new State({ test: "x" }); const values = []; const first = state.subscribe("test", (test) => { values.push(test); }); const second = state.subscribe("test", (test) => { values.push(test + "2"); }); expect(values.length).toEqual(2); expect(values[0]).toEqual("x"); expect(values[1]).toEqual("x2"); state.update("test", "x3"); expect(values.length).toEqual(4); expect(values[2]).toEqual("x3"); expect(values[3]).toEqual("x32"); first(); state.update("test", "xx"); expect(values.length).toEqual(5); expect(values[4]).toEqual("xx2"); second(); state.update("test", "xxx"); expect(values.length).toEqual(5); expect(values[4]).toEqual("xx2"); expect(state.listeners.size).toEqual(0); }); it("should add valid event info path object", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const events = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); events.push(eventInfo); }); state.subscribe("one.two.*.four", (val, eventInfo) => { values.push(val); events.push(eventInfo); }); state.subscribe("one.two.three.four", (val, eventInfo) => { values.push(val); events.push(eventInfo); }); expect(events.length).toEqual(3); expect(events[0].path.resolved).toEqual("one.two"); expect(events[1].path.resolved).toEqual("one.two.three.four"); expect(events[2].path.resolved).toEqual("one.two.three.four"); expect(events[0].path.update).toEqual(undefined); expect(events[1].path.update).toEqual(undefined); expect(events[2].path.update).toEqual(undefined); expect(events[0].path.listener).toEqual("one.two"); expect(events[1].path.listener).toEqual("one.two.*.four"); expect(events[2].path.listener).toEqual("one.two.three.four"); state.update("one.two.three.four", 44); expect(events.length).toEqual(6); expect(events[3].path.resolved).toEqual("one.two.three.four"); expect(events[4].path.resolved).toEqual("one.two.three.four"); expect(events[5].path.resolved).toEqual("one.two.three.four"); expect(events[3].path.update).toEqual("one.two.three.four"); expect(events[4].path.update).toEqual("one.two.three.four"); expect(events[5].path.update).toEqual("one.two.three.four"); expect(events[3].path.listener).toEqual("one.two"); expect(events[4].path.listener).toEqual("one.two.*.four"); expect(events[5].path.listener).toEqual("one.two.three.four"); state.update("one.two.*.four", 444); expect(events.length).toEqual(9); expect(events[6].path.resolved).toEqual("one.two.three.four"); expect(events[7].path.resolved).toEqual("one.two.three.four"); expect(events[8].path.resolved).toEqual("one.two.three.four"); expect(events[6].path.update).toEqual("one.two.*.four"); expect(events[7].path.update).toEqual("one.two.*.four"); expect(events[8].path.update).toEqual("one.two.*.four"); expect(events[6].path.listener).toEqual("one.two"); expect(events[7].path.listener).toEqual("one.two.*.four"); expect(events[8].path.listener).toEqual("one.two.three.four"); }); it("should add valid event info type", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const events = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); events.push(eventInfo); }); state.subscribe("one.two.*.four", (val, eventInfo) => { values.push(val); events.push(eventInfo); }); state.subscribe("one.two.three.four", (val, eventInfo) => { values.push(val); events.push(eventInfo); }); expect(events.length).toEqual(3); expect(events[0].type).toEqual("subscribe"); expect(events[1].type).toEqual("subscribe"); expect(events[2].type).toEqual("subscribe"); state.update("one.*.three.four", 44); expect(events.length).toEqual(6); expect(events[3].type).toEqual("update"); expect(events[4].type).toEqual("update"); expect(events[5].type).toEqual("update"); }); it("should add two listeners with the same path but with different recursive option", () => { const state = new State({ one: { two: { three: { four: 4 } } }, }); const values = []; const events = []; state.subscribe("one.two", (val, eventInfo) => { values.push(val); events.push({ ...eventInfo, ...{ listenersCollection: eventInfo.listenersCollection }, }); }); state.subscribe("one.two;", (val, eventInfo) => { values.push(val); events.push({ ...eventInfo, ...{ listenersCollection: eventInfo.listenersCollection }, }); }); expect(values.length).toEqual(2); expect(events[0].path.listener).toEqual("one.two"); expect(events[1].path.listener).toEqual("one.two;"); expect(events[0].listenersCollection.isRecursive).toEqual(true); expect(events[1].listenersCollection.isRecursive).toEqual(false); expect(events[0].listenersCollection).not.toEqual(events[1].listenersCollection); expect(state.listeners.get("one.two").count).toEqual(1); expect(state.listeners.get("one.two;").count).toEqual(1); }); it("should change data without update", () => { const state = new State({ x: { y: { z: 2 } }, xx: 22 }); expect(state.get("x.y.z")).toEqual(2); const y = state.get("x.y"); y.z = 22; expect(state.get("x.y.z")).toEqual(22); expect(state.data.x.y.z).toEqual(22); }); it("should ignore ignored changes", () => { const state = new State({ one: { two: { three: { four: { five: 0 } } } } }); const values = []; state.subscribe( "one.two.three", (val) => { values.push(val); }, { ignore: ["one.two.three.four"] } ); expect(values.length).toEqual(1); expect(values[0]).toEqual({ four: { five: 0 } }); state.update("one.two.three.four.five", 1); expect(values.length).toEqual(1); state.update("one.two.three.*.five", 1); expect(values.length).toEqual(1); state.update("one.two.three.four", 1); expect(values.length).toEqual(1); state.update("one.two.*.four", 2); expect(values.length).toEqual(1); state.update("one.two.three", 1); expect(values.length).toEqual(2); expect(values[1]).toEqual(1); }); it("should ignore wildcard ignored changes", () => { const state = new State({ one: { two: { three: { four: { five: 0 } } } } }); const values = []; state.subscribe( "one.two.three", (val) => { values.push(val); }, { ignore: ["one.two.*.four"] } ); expect(values.length).toEqual(1); expect(values[0]).toEqual({ four: { five: 0 } }); state.update("one.two.three.four.five", 1); expect(values.length).toEqual(1); state.update("one.two.three.*.five", 1); expect(values.length).toEqual(1); state.update("one.two.three.four", 1); expect(values.length).toEqual(1); state.update("one.two.*.four", 2); expect(values.length).toEqual(1); state.update("one.two.three", 1); expect(values.length).toEqual(2); expect(values[1]).toEqual(1); }); it("should work with experimental matcher", async () => { const state = new State({ one: { two: { three: { four: { five: 0 } } } } }); await state.loadWasmMatcher(fs.readFileSync(path.resolve("./wildcard_matcher_bg.wasm"))); const values = []; state.subscribe( "one.two.three", (val) => { values.push(val); }, { ignore: ["one.two.*.four"] } ); expect(values.length).toEqual(1); expect(values[0]).toEqual({ four: { five: 0 } }); state.update("one.two.three.four.five", 1); expect(values.length).toEqual(1); state.update("one.two.three.*.five", 1); expect(values.length).toEqual(1); state.update("one.two.three.four", 1); expect(values.length).toEqual(1); state.update("one.two.*.four", 2); expect(values.length).toEqual(1); state.update("one.two.three", 1); expect(values.length).toEqual(2); expect(values[1]).toEqual(1); }); it("should force update and notify listeners even if value is the same", () => { const state = new State({ one: { two: { three: { four: { five: 0 } } } } }); const values1 = [], values2 = []; state.subscribe("one.two.three.four.five", (val) => { values1.push(val); }); state.subscribe("one.*.three.four.five", (val) => { values2.push(val); }); expect(values1.length).toEqual(1); expect(values2.length).toEqual(1); state.update("one.*.three.four.five", 6); expect(values1.length).toEqual(2); expect(values2.length).toEqual(2); expect(values1[1]).toEqual(6); expect(values2[1]).toEqual(6); state.update("one.*.three.four.five", 6); expect(values1.length).toEqual(2); expect(values2.length).toEqual(2); expect(values1[1]).toEqual(6); expect(values2[1]).toEqual(6); state.update("one.*.three.four.five", 6, { force: true }); expect(values1.length).toEqual(3); expect(values2.length).toEqual(3); expect(values1[2]).toEqual(6); expect(values2[2]).toEqual(6); state.update("one.two.three.four.five", 6); expect(values1.length).toEqual(3); expect(values2.length).toEqual(3); expect(values1[2]).toEqual(6); expect(values2[2]).toEqual(6); state.update("one.two.three.four.five", 6, { force: true }); expect(values1.length).toEqual(4); expect(values2.length).toEqual(4); expect(values1[3]).toEqual(6); expect(values2[3]).toEqual(6); }); it("should mute some updates", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; state.subscribe("x.*.o", (val) => { values.push(val); }); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); state.mute("x.i.o"); state.update("x.i.o", "oo"); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); }); it("should mute some wildcard updates #1", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; state.subscribe("x.i.o", (val) => { values.push(val); }); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); state.mute("x.*.o"); state.update("x.i.o", "oo"); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); }); it("should mute some wildcard updates #2", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; state.subscribe("x.*.o", (val) => { values.push(val); }); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); state.mute("x.*.o"); state.update("x.i.o", "oo"); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); }); it("should mute some wildcard updates #3", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; state.subscribe("x.*.o", (val) => { values.push(val); }); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); state.mute("x.*.o"); state.update("x.*.o", "oo"); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); }); it("should mute nested properties", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; state.subscribe("x.i.o", (val) => { values.push(val); }); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); state.mute("x"); state.update("x.*.o", "oo"); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); }); it("should not mute nested properties", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; state.subscribe("x.i.o", (val) => { values.push(val); }); const values2 = []; let lastX = 0; state.subscribe("x", (val) => { values2.push(lastX++); }); expect(values.length).toEqual(1); expect(values[0]).toEqual("o"); expect(values2.length).toEqual(1); expect(values2[0]).toEqual(0); state.mute("x;"); state.update("x.*.o", "oo"); expect(values.length).toEqual(2); expect(values[1]).toEqual("oo"); expect(values2.length).toEqual(1); expect(values2[0]).toEqual(0); state.unmute("x;"); state.update("x.*.o", "ooo"); expect(values.length).toEqual(3); expect(values[2]).toEqual("ooo"); expect(values2.length).toEqual(2); expect(values2[1]).toEqual(1); }); // fit("should notify listener with undefined value that listen below changed node", () => { // const state = new State({ x: { y: { z: { a: 1 } } } }); // const values = []; // state.subscribe("x.y.z.a", (val) => { // values.push(val); // }); // expect(values[0]).toEqual(1); // state.update("x.y", { u: { g: 2 } }); // expect(values.length).toEqual(2); // expect(values[1]).toEqual(undefined); // }); it("should update nested listeners", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; function listener1() { values.push("1"); } function listener2() { values.push("2"); } function listener3() { values.push("3"); } function listener4() { values.push("4"); } state.subscribe("x.i.o", listener1); state.subscribe("x.i.o", listener2); state.subscribe("x.y", listener3); state.subscribe("x.i", listener4); expect(values).toEqual(["1", "2", "3", "4"]); values.length = 0; state.update("x", { z: "zz", i: { o: "oooo" } }); expect(values).toEqual(["1", "2", "4"]); }); it("should mute specified listeners", () => { const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" }); const values = []; function listener1() { values.push("1"); } function listener2() { values.push("2"); } function listener3() { values.push("3"); } function listener4() { values.push("4"); } state.subscribe("x.i.o", listener1); state.subscribe("x.i.o", listener2); state.subscribe("x.y", listener3); state.subscribe("x.i", listener4); expect(values.length).toEqual(4); expect(values).toEqual(["1", "2", "3", "4"]); state.mute(listener2); expect(state.isMuted(listener1)).toEqual(false); expect(state.isMuted(listener2)).toEqual(true); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); values.length = 0; state.update("x.i.o", "oo"); expect(values.length).toEqual(2); expect(values).toEqual(["1", "4"]); expect(state.isMuted(listener1)).toEqual(false); expect(state.isMuted(listener2)).toEqual(true); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); values.length = 0; state.update("x.i", { o: "ooo" }); expect(values.length).toEqual(2); expect(values).toEqual(["1", "4"]); expect(state.isMuted(listener1)).toEqual(false); expect(state.isMuted(listener2)).toEqual(true); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); values.length = 0; state.update("x", { z: "zz", i: { o: "oooo" } }); expect(values).toEqual(["1", "4"]); expect(state.isMuted(listener1)).toEqual(false); expect(state.isMuted(listener2)).toEqual(true); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); state.unmute(listener2); state.mute(listener1); expect(state.isMuted(listener1)).toEqual(true); expect(state.isMuted(listener2)).toEqual(false); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); values.length = 0; state.update("x", { z: "zzz", i: { o: "ooooo" } }); expect(values).toEqual(["2", "4"]); expect(state.isMuted(listener1)).toEqual(true); expect(state.isMuted(listener2)).toEqual(false); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); state.unmute(listener1); expect(state.isMuted(listener1)).toEqual(false); expect(state.isMuted(listener2)).toEqual(false); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); values.length = 0; state.update("x", { z: "zz", i: { o: "oooooo" } }); expect(values).toEqual(["1", "2", "4"]); expect(state.isMuted(listener1)).toEqual(false); expect(state.isMuted(listener2)).toEqual(false); expect(state.isMuted(listener3)).toEqual(false); expect(state.isMuted(listener4)).toEqual(false); }); it("should add two wildcard listeners - one without and one with parameter", () => { const state = new State({ nested: { value: { equals: "x" } } }); const values = []; state.subscribe("nested.*.equals", (val, info) => { values.push(val); expect(info.params).toEqual(undefined); }); expect(values.length).toEqual(1); state.subscribe("nested.:val.equals", (val, info) => { values.push(val); expect(info.params).toEqual({ val: "value" }); }); expect(values.length).toEqual(2); state.update("nested.value.equals", "y"); expect(values.length).toEqual(4); }); it("should run listeners with proper order", () => { const state = new State({ nested: { value: { equals: { test: "x" } } } }); let values = []; function first() { values.push(1); } function second() { values.push(2); } function third() { values.push(3); } function fourth() { values.push(4); } state.subscribe("nested;", first); state.subscribe("nested.value.equals", second); state.subscribe("nested.value.equals", third); state.subscribe("nested", fourth); expect(values).toEqual([1, 2, 3, 4]); values = []; state.update("nested", () => { return { value: { equals: { test: "x", }, }, }; }); expect(values).toEqual([1, 2, 3, 4]); }); it("should run listeners with proper order #2", () => { const state = new State({ nested: { value: { equals: { test: { x: "x" } } } }, }); let values = []; function first() { values.push(1); } function second() { values.push(2); } state.subscribe("nested.value.equals.*;", first); // state.subscribeAll(["nested.value.equals.*.x"], second, { // bulk: true, // }); // expect(values).toEqual([1, 2]); // state.update("nested.value.equals.test2", { // x: "y", // }); // expect(values).toEqual([1, 2, 1, 2]); }); it("should run listeners with proper order (bulk)", () => { const state = new State({ nested: { value: { equals: { test: "x" } } } }); let values = []; function first() { values.push(1); } function second() { values.push(2); } function third() { values.push(3); } function fourth() { values.push(4); } state.subscribeAll(["nested.value;"], first, { bulk: true }); state.subscribe("nested.value.equals", second); state.subscribe("nested.value.equals", third); state.subscribe("nested.value", fourth, { bulk: true, ignore: ["nested.value.*.test"], }); expect(values).toEqual([1, 2, 3, 4]); values = []; state.update("nested.value", () => { return { equals: { test: "x", }, }; }); expect(values).toEqual([1, 2, 3, 4]); values = []; state.update("nested.value.equals", () => { return { test: "x", }; }); expect(values).toEqual([2, 3, 4]); values = []; state.update("nested", () => { return { value: { equals: { test: "x", }, }, }; }); expect(values).toEqual([1, 2, 3, 4]); }); it("should not run nested listener even if it is a wildcard property", () => { const state = new State({ nested: { value: { equals: { test: { x: "x" } } } }, }); let values = []; function first() { values.push(1); } function second() { values.push(2); } function third() { values.push(3); } state.subscribe("nested.value.equals.*;", first); state.subscribeAll(["nested.value.equals.*;"], second, { bulk: true, }); state.subscribe("nested.value.equals.test;", third); expect(values).toEqual([1, 2, 3]); state.update("nested.value.equals.test.x", "z"); expect(values).toEqual([1, 2, 3]); }); it("should run listeners in proper order #3", () => { const state = new State({ config: { list: { rows: { 1: { parentId: null, expanded: false, }, }, }, chart: { items: { 1: { rowId: "1" }, }, }, }, }); const values = []; const paths = []; function full(val, info) { values.push("full"); paths.push(info.path.listener); } function partialFull(val, info) { values.push("partialFull"); paths.push(info.path.listener); } function partial(val, info) { values.push("partial"); paths.push(info.path.listener); } state.subscribeAll(["config.chart.items;", "config.list.rows;"], full); state.subscribeAll(["config.list.rows.*;", "config.list.rows.*.parentId"], partialFull, { bulk: true, }); state.subscribeAll(["config.chart.items.*.rowId", "config.list.rows.*.expanded"], partial, { bulk: true, }); expect(values).toEqual(["full", "full", "partialFull", "partialFull", "partial", "partial"]); values.length = 0; paths.length = 0; state.update("config.list.rows", { 2: { parentId: null, expanded: false, }, }); expect(values).toEqual(["full", "partialFull", "partialFull", "partial"]); //console.log(paths); }); it("should properly clean not recursive path", () => { const state = new State({}); expect(state.cleanNotRecursivePath("config.list.rows;")).toEqual("config.list.rows"); expect(state.cutPath("config.list.rows", "config.list.rows")).toEqual("config.list.rows"); }); it("should replace whole state", () => { const state = new State({ x: "x" }); expect(state.get()).toEqual({ x: "x" }); state.update("", { y: "y" }); expect(state.get()).toEqual({ y: "y" }); }); it("should subscribe to empty path", () => { const obj = { x: { y: { z: "z" } } }; const state = new State(obj); const values = []; const unsub = state.subscribe("", (val) => { values.push(JSON.stringify(val)); }); expect(values.length).toEqual(1); expect(values[0]).toEqual(JSON.stringify(obj)); state.update("", { a: { b: { c: "c" } } }); expect(values.length).toEqual(2); expect(values[1]).toEqual(JSON.stringify({ a: { b: { c: "c" } } })); }); it("should notify listeners when full state was replaced", () => { const state = new State({ a: { b: { c: "c" } } }); const values = []; state.subscribe("a.b", (val) => { values.push(JSON.stringify(val)); }); expect(values.length).toEqual(1); expect(values[0]).toEqual(JSON.stringify({ c: "c" })); state.update("a", { b: { c: "cc" } }); expect(values.length).toEqual(2); expect(values[1]).toEqual(JSON.stringify({ c: "cc" })); state.update("", { a: { b: { c: "ccc" } } }); expect(values.length).toEqual(3); expect(values[2]).toEqual(JSON.stringify({ c: "ccc" })); }); it("should replace array properly", () => { const state = new State({ something: { a: [["a"], ["b"]] } }); state.update("something", { a: [["a"]] }); const something = state.get("something"); expect(something).toEqual({ a: [["a"]] }); }); });