UNPKG

@traxjs/trax

Version:

Reactive state management

1,163 lines (1,017 loc) 78.3 kB
import { beforeEach, describe, expect, it } from 'vitest'; import { createTraxEnv, tmd } from '../core'; import { Store, Trax, TraxProcessor } from '../types'; import { Person, SimpleFamilyStore, printEvents, ArrayFamilyStore } from './utils'; describe('Sync Processors', () => { let trax: Trax; beforeEach(() => { trax = createTraxEnv(); }); interface Values { v1: string; v2: string; v3: string; } function printLogs(minCycleId = 0, ignoreCycleEvents = true): string[] { return printEvents(trax.log, ignoreCycleEvents, minCycleId); } function createPStore(addPrettyNameProcessor = true) { return trax.createStore("PStore", (store: Store<Person>) => { const p = store.init({ firstName: "Homer", lastName: "Simpson" }); if (addPrettyNameProcessor) { store.compute("PrettyName", () => { let nm = ""; if (p.firstName) { nm = p.firstName + " " + p.lastName; } else { nm = p.lastName; } p.prettyName = nm; p.prettyNameLength = nm.length; }); } }); } describe('Eager Compute', () => { it('should be able to output non trax values', async () => { const ps = createPStore(false); const p = ps.data; let output = ""; const pr = ps.compute("Render", () => { output = p.firstName + " " + p.lastName; }); expect(output).toBe("Homer Simpson"); expect(pr.autoCompute).toBe(true); expect(pr.computeCount).toBe(1); expect(pr.dirty).toBe(false); expect(pr.priority).toBe(1); trax.log.info("-----------------------------"); p.firstName = "Bart"; expect(pr.dirty).toBe(true); await trax.reconciliation(); expect(output).toBe("Bart Simpson"); expect(pr.computeCount).toBe(2); expect(pr.dirty).toBe(false); trax.log.info("END"); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !PCE - 0:1", "0:5 !NEW - P: PStore#Render", "0:6 !PCS - !Compute #1 (PStore#Render) P1 Init", "0:7 !GET - PStore/data.firstName -> 'Homer'", "0:8 !GET - PStore/data.lastName -> 'Simpson'", "0:9 !PCE - 0:6", "0:10 !LOG - -----------------------------", "0:11 !SET - PStore/data.firstName = 'Bart' (prev: 'Homer')", "0:12 !DRT - PStore#Render <- PStore/data.firstName", "0:13 !PCS - !Reconciliation #1 - 1 processor", "0:14 !PCS - !Compute #2 (PStore#Render) P1 Reconciliation - parentId=0:13", "0:15 !GET - PStore/data.firstName -> 'Bart'", "0:16 !GET - PStore/data.lastName -> 'Simpson'", "0:17 !PCE - 0:14", "0:18 !PCE - 0:13", "1:1 !LOG - END", ]); expect(pr.dependencies).toMatchObject([ "PStore/data.firstName", "PStore/data.lastName" ]); }); it('should support to be forced event if processor is not dirty', async () => { const ps = createPStore(false); const p = ps.data; let output = ""; const pr = ps.compute("Render", () => { output = p.firstName + " " + p.lastName; }); expect(output).toBe("Homer Simpson"); await trax.reconciliation(); expect(pr.dirty).toBe(false); trax.log.info("A"); pr.compute(); // not executed trax.log.info("B"); pr.compute(true); trax.log.info("C"); expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !LOG - B", "1:3 !PCS - !Compute #2 (PStore#Render) P1 DirectCall", "1:4 !GET - PStore/data.firstName -> 'Homer'", "1:5 !GET - PStore/data.lastName -> 'Simpson'", "1:6 !PCE - 1:3", "1:7 !LOG - C", ]); }); it('should support array ids', async () => { const ps = trax.createStore(["Some", "Store", 42], (store: Store<Person>) => { store.init({ firstName: "Homer", lastName: "Simpson" }); }); expect(ps.id).toBe("Some:Store:42"); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (Some:Store:42)", "0:2 !NEW - S: Some:Store:42", "0:3 !NEW - O: Some:Store:42/data", "0:4 !PCE - 0:1", ]); }); it('should return processors that have already been created', async () => { const ps = createPStore(); const pr = ps.compute("PrettyName", () => { const p = ps.data; const nm = p.firstName + " " + p.lastName; p.prettyName = nm; p.prettyNameLength = nm.length; }); expect(typeof pr.compute).toBe("function"); expect(ps.getProcessor("PrettyName")).toBe(pr); ps.data.firstName = "Bart"; await trax.reconciliation(); expect(ps.data.prettyName).toBe("Bart Simpson"); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !NEW - P: PStore#PrettyName", "0:5 !PCS - !Compute #1 (PStore#PrettyName) P1 Init - parentId=0:1", "0:6 !GET - PStore/data.firstName -> 'Homer'", "0:7 !GET - PStore/data.firstName -> 'Homer'", "0:8 !GET - PStore/data.lastName -> 'Simpson'", "0:9 !SET - PStore/data.prettyName = 'Homer Simpson' (prev: undefined)", "0:10 !SET - PStore/data.prettyNameLength = 13 (prev: undefined)", "0:11 !PCE - 0:5", "0:12 !PCE - 0:1", "0:13 !SET - PStore/data.firstName = 'Bart' (prev: 'Homer')", "0:14 !DRT - PStore#PrettyName <- PStore/data.firstName", "0:15 !PCS - !Reconciliation #1 - 1 processor", "0:16 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=0:15", "0:17 !GET - PStore/data.firstName -> 'Bart'", "0:18 !GET - PStore/data.lastName -> 'Simpson'", "0:19 !SET - PStore/data.prettyName = 'Bart Simpson' (prev: 'Homer Simpson')", "0:20 !SET - PStore/data.prettyNameLength = 12 (prev: 13)", "0:21 !PCE - 0:16", "0:22 !PCE - 0:15", "1:1 !GET - PStore/data.prettyName -> 'Bart Simpson'", ]); }); it('should support manual compute and onDirty callbacks', async () => { const ps = createPStore(false); const p = ps.data; let output = "", onDirtyCount = 0; const pr = ps.compute("Render", () => { output = p.firstName + " " + p.lastName; }, false); pr.onDirty = () => { onDirtyCount++; } expect(pr.dirty).toBe(true); expect(onDirtyCount).toBe(0); expect(pr.autoCompute).toBe(false); expect(output).toBe(""); // not computed expect(pr.computeCount).toBe(0); trax.log.info("A"); await trax.reconciliation(); expect(pr.dirty).toBe(true); expect(output).toBe(""); pr.compute(); expect(output).toBe("Homer Simpson"); expect(pr.dirty).toBe(false); expect(pr.computeCount).toBe(1); expect(onDirtyCount).toBe(0); trax.log.info("B"); await trax.reconciliation(); p.firstName = "Bart"; expect(onDirtyCount).toBe(1); p.lastName = "SIMPSON"; expect(onDirtyCount).toBe(1); expect(pr.dirty).toBe(true); expect(pr.computeCount).toBe(1); pr.compute(); expect(output).toBe("Bart SIMPSON"); expect(pr.dirty).toBe(false); expect(pr.computeCount).toBe(2); p.firstName = "BART"; expect(onDirtyCount).toBe(2); expect(pr.dirty).toBe(true); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !PCE - 0:1", "0:5 !NEW - P: PStore#Render", "0:6 !LOG - A", "1:1 !PCS - !Compute #1 (PStore#Render) P1 DirectCall", "1:2 !GET - PStore/data.firstName -> 'Homer'", "1:3 !GET - PStore/data.lastName -> 'Simpson'", "1:4 !PCE - 1:1", "1:5 !LOG - B", "2:1 !SET - PStore/data.firstName = 'Bart' (prev: 'Homer')", "2:2 !DRT - PStore#Render <- PStore/data.firstName", "2:3 !SET - PStore/data.lastName = 'SIMPSON' (prev: 'Simpson')", "2:4 !PCS - !Compute #2 (PStore#Render) P1 DirectCall", "2:5 !GET - PStore/data.firstName -> 'Bart'", "2:6 !GET - PStore/data.lastName -> 'SIMPSON'", "2:7 !PCE - 2:4", "2:8 !SET - PStore/data.firstName = 'BART' (prev: 'Bart')", "2:9 !DRT - PStore#Render <- PStore/data.firstName", ]); }); it('should support auto compute and onDirty callbacks', async () => { const ps = createPStore(false); const p = ps.data; let output = "", onDirtyCount = 0; const pr = ps.compute("Render", () => { output = p.firstName + " " + p.lastName; }); pr.onDirty = () => { onDirtyCount++; } expect(pr.dirty).toBe(false); expect(onDirtyCount).toBe(0); expect(pr.autoCompute).toBe(true); expect(output).toBe("Homer Simpson"); expect(pr.computeCount).toBe(1); trax.log.info("A"); await trax.reconciliation(); expect(pr.dirty).toBe(false); pr.compute(); // no effect expect(output).toBe("Homer Simpson"); expect(pr.dirty).toBe(false); expect(pr.computeCount).toBe(1); expect(onDirtyCount).toBe(0); trax.log.info("B"); await trax.reconciliation(); p.firstName = "Bart"; expect(onDirtyCount).toBe(1); p.lastName = "SIMPSON"; expect(onDirtyCount).toBe(1); expect(pr.dirty).toBe(true); expect(pr.computeCount).toBe(1); trax.log.info("C"); await trax.reconciliation(); expect(output).toBe("Bart SIMPSON"); expect(pr.dirty).toBe(false); expect(pr.computeCount).toBe(2); expect(onDirtyCount).toBe(1); trax.log.info("D"); p.firstName = "BART"; expect(onDirtyCount).toBe(2); expect(pr.dirty).toBe(true); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !PCE - 0:1", "0:5 !NEW - P: PStore#Render", "0:6 !PCS - !Compute #1 (PStore#Render) P1 Init", "0:7 !GET - PStore/data.firstName -> 'Homer'", "0:8 !GET - PStore/data.lastName -> 'Simpson'", "0:9 !PCE - 0:6", "0:10 !LOG - A", "1:1 !LOG - B", "2:1 !SET - PStore/data.firstName = 'Bart' (prev: 'Homer')", "2:2 !DRT - PStore#Render <- PStore/data.firstName", "2:3 !SET - PStore/data.lastName = 'SIMPSON' (prev: 'Simpson')", "2:4 !LOG - C", "2:5 !PCS - !Reconciliation #1 - 1 processor", "2:6 !PCS - !Compute #2 (PStore#Render) P1 Reconciliation - parentId=2:5", "2:7 !GET - PStore/data.firstName -> 'Bart'", "2:8 !GET - PStore/data.lastName -> 'SIMPSON'", "2:9 !PCE - 2:6", "2:10 !PCE - 2:5", "3:1 !LOG - D", "3:2 !SET - PStore/data.firstName = 'BART' (prev: 'Bart')", "3:3 !DRT - PStore#Render <- PStore/data.firstName", ]); }); it('should support conditional processing', async () => { const ps = createPStore(); const p = ps.data; expect(p.prettyName).toBe("Homer Simpson"); p.firstName = ""; expect(p.prettyName).toBe("Homer Simpson"); await trax.reconciliation(); expect(p.prettyName).toBe("Simpson"); trax.log.info("A"); const ccp = trax.reconciliation(); p.firstName = "Bart"; p.lastName = "S"; await ccp; expect(p.prettyName).toBe("Bart S"); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !NEW - P: PStore#PrettyName", "0:5 !PCS - !Compute #1 (PStore#PrettyName) P1 Init - parentId=0:1", "0:6 !GET - PStore/data.firstName -> 'Homer'", "0:7 !GET - PStore/data.firstName -> 'Homer'", // read twice in the processor "0:8 !GET - PStore/data.lastName -> 'Simpson'", "0:9 !SET - PStore/data.prettyName = 'Homer Simpson' (prev: undefined)", "0:10 !SET - PStore/data.prettyNameLength = 13 (prev: undefined)", "0:11 !PCE - 0:5", "0:12 !PCE - 0:1", "0:13 !GET - PStore/data.prettyName -> 'Homer Simpson'", "0:14 !SET - PStore/data.firstName = '' (prev: 'Homer')", "0:15 !DRT - PStore#PrettyName <- PStore/data.firstName", "0:16 !GET - PStore/data.prettyName -> 'Homer Simpson'", "0:17 !PCS - !Reconciliation #1 - 1 processor", "0:18 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=0:17", "0:19 !GET - PStore/data.firstName -> ''", "0:20 !GET - PStore/data.lastName -> 'Simpson'", "0:21 !SET - PStore/data.prettyName = 'Simpson' (prev: 'Homer Simpson')", "0:22 !SET - PStore/data.prettyNameLength = 7 (prev: 13)", "0:23 !PCE - 0:18", "0:24 !PCE - 0:17", "1:1 !GET - PStore/data.prettyName -> 'Simpson'", "1:2 !LOG - A", "1:3 !SET - PStore/data.firstName = 'Bart' (prev: '')", "1:4 !DRT - PStore#PrettyName <- PStore/data.firstName", "1:5 !SET - PStore/data.lastName = 'S' (prev: 'Simpson')", "1:6 !PCS - !Reconciliation #2 - 1 processor", "1:7 !PCS - !Compute #3 (PStore#PrettyName) P1 Reconciliation - parentId=1:6", "1:8 !GET - PStore/data.firstName -> 'Bart'", "1:9 !GET - PStore/data.firstName -> 'Bart'", "1:10 !GET - PStore/data.lastName -> 'S'", "1:11 !SET - PStore/data.prettyName = 'Bart S' (prev: 'Simpson')", "1:12 !SET - PStore/data.prettyNameLength = 6 (prev: 7)", "1:13 !PCE - 1:7", "1:14 !PCE - 1:6", "2:1 !GET - PStore/data.prettyName -> 'Bart S'", ]); }); it('should support conditional processing and clean previous listeners (multiple objects)', async () => { const fs = trax.createStore("FStore", (s: Store<ArrayFamilyStore>) => { const data = s.init({ familyName: "", members: [ { firstName: "Homer", lastName: "Simpson" }, { firstName: "Marge", lastName: "Simpson" } ] }); s.compute("Names", () => { // display even or odd names depending on the members length const members = data.members; const len = members.length; const arr: string[] = []; for (let i = len % 2; i < len; i += 2) { arr.push(members[i].firstName); } data.names = arr.join(", "); }); }); const data = fs.data; const members = data.members; const namesPr = fs.getProcessor("Names")!; expect(data.names).toBe("Homer"); expect(hasListener(members[0], namesPr)).toBe(true); expect(hasListener(members[1], namesPr)).toBe(false); await trax.reconciliation(); trax.log.info("A"); members.push({ firstName: "Bart", lastName: "Simpson" }); trax.processChanges(); expect(data.names).toBe("Marge"); expect(hasListener(members[0], namesPr)).toBe(false); expect(hasListener(members[1], namesPr)).toBe(true); expect(hasListener(members[2], namesPr)).toBe(false); members.push({ firstName: "Lisa", lastName: "Simpson" }); trax.processChanges(); expect(data.names).toBe("Homer, Bart"); expect(hasListener(members[0], namesPr)).toBe(true); expect(hasListener(members[1], namesPr)).toBe(false); expect(hasListener(members[2], namesPr)).toBe(true); expect(hasListener(members[3], namesPr)).toBe(false); function hasListener(o: any, pr: TraxProcessor) { return (tmd(o)?.propListeners as Set<TraxProcessor>)?.has(pr) || false; } }); it('should support eager content processors', async () => { const fs = trax.createStore("FStore", (s: Store<ArrayFamilyStore>) => { const data = s.init({ familyName: "", members: [ { firstName: "Homer", lastName: "Simpson" }, { firstName: "Marge", lastName: "Simpson" } ] }, { names: (d) => { // display even or odd names depending on the members length const members = d.members; const len = members.length; const arr: string[] = []; for (let i = len % 2; i < len; i += 2) { arr.push(members[i].firstName); } d.names = arr.join(", "); } }); }); const data = fs.data; const members = data.members; const namesPr = fs.getProcessor("data[names]")!; expect(namesPr).not.toBe(undefined); expect(data.names).toBe("Homer"); expect(hasListener(members[0], namesPr)).toBe(true); expect(hasListener(members[1], namesPr)).toBe(false); await trax.reconciliation(); trax.log.info("A"); members.push({ firstName: "Bart", lastName: "Simpson" }); trax.processChanges(); expect(data.names).toBe("Marge"); expect(hasListener(members[0], namesPr)).toBe(false); expect(hasListener(members[1], namesPr)).toBe(true); expect(hasListener(members[2], namesPr)).toBe(false); members.push({ firstName: "Lisa", lastName: "Simpson" }); trax.processChanges(); expect(data.names).toBe("Homer, Bart"); expect(hasListener(members[0], namesPr)).toBe(true); expect(hasListener(members[1], namesPr)).toBe(false); expect(hasListener(members[2], namesPr)).toBe(true); expect(hasListener(members[3], namesPr)).toBe(false); function hasListener(o: any, pr: TraxProcessor) { return (tmd(o)?.propListeners as Set<TraxProcessor>)?.has(pr) || false; } }); it('should immediatly dispose run-once content processors', async () => { const fs = trax.createStore("FStore", (s: Store<ArrayFamilyStore>) => { const data = s.init({ familyName: "", members: [ { firstName: "Homer", lastName: "Simpson" }, { firstName: "Marge", lastName: "Simpson" } ] }, { names: (d, cc) => { cc.maxComputeCount = 1; // run once -> will be automatically disposed // display even or odd names depending on the members length const members = d.members; const len = members.length; const arr: string[] = []; for (let i = len % 2; i < len; i += 2) { arr.push(members[i].firstName); } d.names = arr.join(", "); } }); }); const data = fs.data; const members = data.members; const namesPr = fs.getProcessor("data[names]")!; expect(namesPr).toBe(undefined); // run once -> already disposed expect(data.names).toBe("Homer"); }); it('should support dependencies from multiple objects and auto-wrap objects set as JSON', async () => { const fst = trax.createStore("SimpleFamilyStore", (store: Store<SimpleFamilyStore>) => { store.init({ father: { firstName: "Homer", lastName: "Simpson" } }); }); const family = fst.data; await trax.reconciliation(); // skip first cycle fst.data.child1 = fst.add<Person>("Bart", { firstName: "Bart", lastName: "Simpson" }); fst.data.child2 = fst.add<Person>("Lisa", { firstName: "Lisa", lastName: "Simpson" }); fst.compute("ChildNames", () => { const names: string[] = []; for (const nm of ["child1", "child2", "child3"]) { const child = family[nm] as Person; if (child) { names.push(child.firstName); } } family.childNames = names.join(", "); }); expect(family.childNames).toBe("Bart, Lisa"); family.child3 = { // will be automatically wrapped firstName: "Maggie", lastName: "Simpson" } expect(family.childNames).toBe("Bart, Lisa"); // not reprocessed yet await trax.reconciliation(); expect(family.childNames).toBe("Bart, Lisa, Maggie"); expect(printLogs(1)).toMatchObject([ "1:1 !NEW - O: SimpleFamilyStore/Bart", "1:2 !SET - SimpleFamilyStore/data.child1 = '[TRAX SimpleFamilyStore/Bart]' (prev: undefined)", "1:3 !NEW - O: SimpleFamilyStore/Lisa", "1:4 !SET - SimpleFamilyStore/data.child2 = '[TRAX SimpleFamilyStore/Lisa]' (prev: undefined)", "1:5 !NEW - P: SimpleFamilyStore#ChildNames", "1:6 !PCS - !Compute #1 (SimpleFamilyStore#ChildNames) P1 Init", "1:7 !GET - SimpleFamilyStore/data.child1 -> '[TRAX SimpleFamilyStore/Bart]'", "1:8 !GET - SimpleFamilyStore/Bart.firstName -> 'Bart'", "1:9 !GET - SimpleFamilyStore/data.child2 -> '[TRAX SimpleFamilyStore/Lisa]'", "1:10 !GET - SimpleFamilyStore/Lisa.firstName -> 'Lisa'", "1:11 !GET - SimpleFamilyStore/data.child3 -> undefined", "1:12 !SET - SimpleFamilyStore/data.childNames = 'Bart, Lisa' (prev: undefined)", "1:13 !PCE - 1:6", "1:14 !GET - SimpleFamilyStore/data.childNames -> 'Bart, Lisa'", "1:15 !NEW - O: SimpleFamilyStore/data*child3", "1:16 !SET - SimpleFamilyStore/data.child3 = '[TRAX SimpleFamilyStore/data*child3]' (prev: undefined)", "1:17 !DRT - SimpleFamilyStore#ChildNames <- SimpleFamilyStore/data.child3", "1:18 !GET - SimpleFamilyStore/data.childNames -> 'Bart, Lisa'", "1:19 !PCS - !Reconciliation #1 - 1 processor", "1:20 !PCS - !Compute #2 (SimpleFamilyStore#ChildNames) P1 Reconciliation - parentId=1:19", "1:21 !GET - SimpleFamilyStore/data.child1 -> '[TRAX SimpleFamilyStore/Bart]'", "1:22 !GET - SimpleFamilyStore/Bart.firstName -> 'Bart'", "1:23 !GET - SimpleFamilyStore/data.child2 -> '[TRAX SimpleFamilyStore/Lisa]'", "1:24 !GET - SimpleFamilyStore/Lisa.firstName -> 'Lisa'", "1:25 !GET - SimpleFamilyStore/data.child3 -> '[TRAX SimpleFamilyStore/data*child3]'", "1:26 !GET - SimpleFamilyStore/data*child3.firstName -> 'Maggie'", "1:27 !SET - SimpleFamilyStore/data.childNames = 'Bart, Lisa, Maggie' (prev: 'Bart, Lisa')", "1:28 !PCE - 1:20", "1:29 !PCE - 1:19", "2:1 !GET - SimpleFamilyStore/data.childNames -> 'Bart, Lisa, Maggie'", ]); }); it('should create processors that can be retrieved throug store.get()', async () => { const ps = createPStore(false); const p = ps.data; let output = ""; const pr = ps.compute("Render", () => { output = p.firstName + " " + p.lastName; }); let prg = ps.get("Render"); expect(prg).toBe(undefined); prg = ps.getProcessor("Render")!; expect(prg).toBe(pr); prg = ps.getProcessor("Render2"); expect(prg).toBe(undefined); }); it('should support multiple parallel processors with imbalanded branches', async () => { interface ValueObject { value: string; } interface ValueSet { v0: ValueObject; v1: ValueObject; v2: ValueObject; v3: ValueObject; v4: ValueObject; v5: ValueObject; } const st = trax.createStore("PStore", (store: Store<ValueSet>) => { const v = store.init({ v0: { value: "v0initValue" }, v1: { value: "v1initValue" }, v2: { value: "v2initValue" }, v3: { value: "v3initValue" }, v4: { value: "v4initValue" }, v5: { value: "v5initValue" }, }); const v0 = v.v0; const v1 = v.v1; const v2 = v.v2; const v3 = v.v3; const v4 = v.v4; const v5 = v.v5; // v0 -> P1 -> v1 -> P3 -> v3 -> P4 -> v4 -> P5 -> v5 // -> P2 -> v2 -------------------------> P5 -> v5 store.compute("P1", () => { v1.value = `P1(${v0.value})`; }) store.compute("P2", () => { v2.value = `P2(${v0.value})`; }); store.compute("P3", () => { v3.value = `P3(${v1.value})`; }); store.compute("P4", () => { v4.value = `P4(${v3.value})`; }); store.compute("P5", () => { v5.value = `P5(${v4.value}+${v2.value})`; }); }); const v = st.data; expect(v.v5.value).toBe("P5(P4(P3(P1(v0initValue)))+P2(v0initValue))"); expect(printLogs(0)).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !NEW - O: PStore/data*v0", "0:5 !GET - PStore/data.v0 -> '[TRAX PStore/data*v0]'", "0:6 !NEW - O: PStore/data*v1", "0:7 !GET - PStore/data.v1 -> '[TRAX PStore/data*v1]'", "0:8 !NEW - O: PStore/data*v2", "0:9 !GET - PStore/data.v2 -> '[TRAX PStore/data*v2]'", "0:10 !NEW - O: PStore/data*v3", "0:11 !GET - PStore/data.v3 -> '[TRAX PStore/data*v3]'", "0:12 !NEW - O: PStore/data*v4", "0:13 !GET - PStore/data.v4 -> '[TRAX PStore/data*v4]'", "0:14 !NEW - O: PStore/data*v5", "0:15 !GET - PStore/data.v5 -> '[TRAX PStore/data*v5]'", "0:16 !NEW - P: PStore#P1", "0:17 !PCS - !Compute #1 (PStore#P1) P1 Init - parentId=0:1", "0:18 !GET - PStore/data*v0.value -> 'v0initValue'", "0:19 !SET - PStore/data*v1.value = 'P1(v0initValue)' (prev: 'v1initValue')", "0:20 !PCE - 0:17", "0:21 !NEW - P: PStore#P2", "0:22 !PCS - !Compute #1 (PStore#P2) P2 Init - parentId=0:1", "0:23 !GET - PStore/data*v0.value -> 'v0initValue'", "0:24 !SET - PStore/data*v2.value = 'P2(v0initValue)' (prev: 'v2initValue')", "0:25 !PCE - 0:22", "0:26 !NEW - P: PStore#P3", "0:27 !PCS - !Compute #1 (PStore#P3) P3 Init - parentId=0:1", "0:28 !GET - PStore/data*v1.value -> 'P1(v0initValue)'", "0:29 !SET - PStore/data*v3.value = 'P3(P1(v0initValue))' (prev: 'v3initValue')", "0:30 !PCE - 0:27", "0:31 !NEW - P: PStore#P4", "0:32 !PCS - !Compute #1 (PStore#P4) P4 Init - parentId=0:1", "0:33 !GET - PStore/data*v3.value -> 'P3(P1(v0initValue))'", "0:34 !SET - PStore/data*v4.value = 'P4(P3(P1(v0initValue)))' (prev: 'v4initValue')", "0:35 !PCE - 0:32", "0:36 !NEW - P: PStore#P5", "0:37 !PCS - !Compute #1 (PStore#P5) P5 Init - parentId=0:1", "0:38 !GET - PStore/data*v4.value -> 'P4(P3(P1(v0initValue)))'", "0:39 !GET - PStore/data*v2.value -> 'P2(v0initValue)'", "0:40 !SET - PStore/data*v5.value = 'P5(P4(P3(P1(v0initValue)))+P2(v0initValue))' (prev: 'v5initValue')", "0:41 !PCE - 0:37", "0:42 !PCE - 0:1", "0:43 !GET - PStore/data.v5 -> '[TRAX PStore/data*v5]'", "0:44 !GET - PStore/data*v5.value -> 'P5(P4(P3(P1(v0initValue)))+P2(v0initValue))'", ]); await trax.reconciliation(); trax.log.info("-----------------") v.v0.value = "NEWV0"; await trax.reconciliation(); expect(v.v5.value).toBe("P5(P4(P3(P1(NEWV0)))+P2(NEWV0))"); expect(printLogs(1)).toMatchObject([ "1:1 !LOG - -----------------", "1:2 !GET - PStore/data.v0 -> '[TRAX PStore/data*v0]'", "1:3 !SET - PStore/data*v0.value = 'NEWV0' (prev: 'v0initValue')", "1:4 !DRT - PStore#P1 <- PStore/data*v0.value", "1:5 !DRT - PStore#P2 <- PStore/data*v0.value", "1:6 !PCS - !Reconciliation #1 - 5 processors", "1:7 !PCS - !Compute #2 (PStore#P1) P1 Reconciliation - parentId=1:6", "1:8 !GET - PStore/data*v0.value -> 'NEWV0'", "1:9 !SET - PStore/data*v1.value = 'P1(NEWV0)' (prev: 'P1(v0initValue)')", "1:10 !DRT - PStore#P3 <- PStore/data*v1.value", "1:11 !PCE - 1:7", "1:12 !PCS - !Compute #2 (PStore#P2) P2 Reconciliation - parentId=1:6", "1:13 !GET - PStore/data*v0.value -> 'NEWV0'", "1:14 !SET - PStore/data*v2.value = 'P2(NEWV0)' (prev: 'P2(v0initValue)')", "1:15 !DRT - PStore#P5 <- PStore/data*v2.value", "1:16 !PCE - 1:12", "1:17 !PCS - !Compute #2 (PStore#P3) P3 Reconciliation - parentId=1:6", "1:18 !GET - PStore/data*v1.value -> 'P1(NEWV0)'", "1:19 !SET - PStore/data*v3.value = 'P3(P1(NEWV0))' (prev: 'P3(P1(v0initValue))')", "1:20 !DRT - PStore#P4 <- PStore/data*v3.value", "1:21 !PCE - 1:17", "1:22 !PCS - !Compute #2 (PStore#P4) P4 Reconciliation - parentId=1:6", "1:23 !GET - PStore/data*v3.value -> 'P3(P1(NEWV0))'", "1:24 !SET - PStore/data*v4.value = 'P4(P3(P1(NEWV0)))' (prev: 'P4(P3(P1(v0initValue)))')", "1:25 !PCE - 1:22", "1:26 !PCS - !Compute #2 (PStore#P5) P5 Reconciliation - parentId=1:6", "1:27 !GET - PStore/data*v4.value -> 'P4(P3(P1(NEWV0)))'", "1:28 !GET - PStore/data*v2.value -> 'P2(NEWV0)'", "1:29 !SET - PStore/data*v5.value = 'P5(P4(P3(P1(NEWV0)))+P2(NEWV0))' (prev: 'P5(P4(P3(P1(v0initValue)))+P2(v0initValue))')", "1:30 !PCE - 1:26", "1:31 !PCE - 1:6", "2:1 !GET - PStore/data.v5 -> '[TRAX PStore/data*v5]'", "2:2 !GET - PStore/data*v5.value -> 'P5(P4(P3(P1(NEWV0)))+P2(NEWV0))'", ]); // TODO: dispose store and check that processor count is back to 0 }); it('should support multiple parallel processors and dispose', async () => { const st = trax.createStore("PStore", (store: Store<Values>) => { const v = store.init({ v1: "A", v2: "B", v3: "C", }); store.compute("P1", () => { v.v2 = "P1(" + v.v1 + ")"; }); store.compute("P2", () => { v.v3 = "P2(" + v.v1 + ")"; }); }); const v = st.data; const p1 = st.getProcessor("P1")!; const p2 = st.getProcessor("P2")!; await trax.reconciliation(); expect(v.v2).toBe("P1(A)"); expect(v.v3).toBe("P2(A)"); expect(p1.dependencies).toMatchObject([ "PStore/data.v1", ]); v.v1 = "X"; await trax.reconciliation(); expect(v.v2).toBe("P1(X)"); expect(v.v3).toBe("P2(X)"); p1.dispose(); expect(p1.dependencies).toMatchObject([]); v.v1 = "Y"; await trax.reconciliation(); expect(v.v2).toBe("P1(X)"); expect(v.v3).toBe("P2(Y)"); p2.dispose(); v.v1 = "Z"; await trax.reconciliation(); expect(v.v2).toBe("P1(X)"); expect(v.v3).toBe("P2(Y)"); }); it('should allow to create renderer processors', async () => { const ps = createPStore(); const p = ps.data; let output = ""; const r = ps.compute("Render", () => { if ((p as any).then === undefined) { // this test is to ensure dependencies on then are not logged output = p.firstName + " " + p.lastName; } }, false, true); expect(ps.getProcessor("PrettyName")!.isRenderer).toBe(false); expect(r.isRenderer).toBe(true); r.compute(); expect(printLogs(0)).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !NEW - P: PStore#PrettyName", "0:5 !PCS - !Compute #1 (PStore#PrettyName) P1 Init - parentId=0:1", "0:6 !GET - PStore/data.firstName -> 'Homer'", "0:7 !GET - PStore/data.firstName -> 'Homer'", "0:8 !GET - PStore/data.lastName -> 'Simpson'", "0:9 !SET - PStore/data.prettyName = 'Homer Simpson' (prev: undefined)", "0:10 !SET - PStore/data.prettyNameLength = 13 (prev: undefined)", "0:11 !PCE - 0:5", "0:12 !PCE - 0:1", "0:13 !NEW - P: PStore#Render", "0:14 !PCS - !Compute #1 (PStore#Render) P2R DirectCall", "0:15 !GET - PStore/data.firstName -> 'Homer'", "0:16 !GET - PStore/data.lastName -> 'Simpson'", "0:17 !PCE - 0:14", ]); expect(output).toBe("Homer Simpson"); }); it('should support processors with dependencies on different stores', async () => { const ps = trax.createStore("PStore", (s: Store<Person>) => { s.init({ firstName: "Homer", lastName: "Simpson" }); }); const main = ps.data; const fs = trax.createStore("FStore", (s: Store<ArrayFamilyStore>) => { const data = s.init({ familyName: "", members: [ { firstName: "Homer", lastName: "Simpson" }, { firstName: "Marge", lastName: "Simpson" } ] }); s.compute("Names", () => { // display even or odd names depending on the members length const members = data.members; data.names = members.map((m) => { return (m.firstName === main.firstName) ? "MAIN" : m.firstName; }).join(", "); }); }); const data = fs.data; const namesPr = fs.getProcessor("Names")!; expect(data.names).toBe("MAIN, Marge"); main.firstName = "Marge"; trax.processChanges(); expect(data.names).toBe("Homer, MAIN"); await trax.reconciliation(); // move to next cycle trax.log.info("A"); // disposing the main store will not trigger a change in namesPr // this has to be managed manually expect(namesPr.dirty).toBe(false); ps.dispose(); expect(namesPr.dirty).toBe(false); }); it('should support trax.getActiveProcessor()', async () => { let lastActiveProcessor: TraxProcessor | void = undefined; expect(trax.getActiveProcessor()).toBe(undefined); const ps = trax.createStore("PStore", (store: Store<Person>) => { const p = store.init({ firstName: "Homer", lastName: "Simpson" }); store.compute("PrettyName", () => { let nm = p.firstName + " " + p.lastName; lastActiveProcessor = trax.getActiveProcessor(); p.prettyName = nm; p.prettyNameLength = nm.length; }); }); const pr = ps.getProcessor("PrettyName"); expect(lastActiveProcessor).toBe(pr); expect(trax.getActiveProcessor()).toBe(undefined); }); it('should update compute function when a processor is retrieved', async () => { const ps = createPStore(false); const p = ps.data; let output = ""; const pr = ps.compute("Render", () => { output = "A " + p.firstName + " " + p.lastName; }); expect(output).toBe("A Homer Simpson"); await trax.reconciliation(); expect(pr.dirty).toBe(false); trax.log.info("A"); const pr2 = ps.compute("Render", () => { output = "B " + p.firstName + " " + p.lastName; }); expect(pr2).toBe(pr); trax.log.info("B"); p.firstName = "H"; trax.log.info("C"); await trax.reconciliation(); expect(output).toBe("B H Simpson"); expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !LOG - B", "1:3 !SET - PStore/data.firstName = 'H' (prev: 'Homer')", "1:4 !DRT - PStore#Render <- PStore/data.firstName", "1:5 !LOG - C", "1:6 !PCS - !Reconciliation #1 - 1 processor", "1:7 !PCS - !Compute #2 (PStore#Render) P1 Reconciliation - parentId=1:6", "1:8 !GET - PStore/data.firstName -> 'H'", "1:9 !GET - PStore/data.lastName -> 'Simpson'", "1:10 !PCE - 1:7", "1:11 !PCE - 1:6", ]); }); }); describe('Lazy Compute', () => { it('store.init should allow to create a root object processor', async () => { const pstore = trax.createStore("PStore", (store: Store<Person>) => { store.init({ firstName: "Homer", lastName: "Simpson" }, { "~prettyNames": (person) => { const nm = person.firstName + " " + person.lastName; person.prettyName = nm; person.prettyNameLength = nm.length; } }); }); const proot = pstore.data; let output = ""; pstore.compute("Render", () => { output = "VIEW: " + proot.prettyName; }, true, true); const rootId = "PStore/data"; const processorId = "PStore#data[~prettyNames]"; expect(trax.getTraxId(pstore.data)).toBe(rootId); const pr = pstore.getProcessor("data[~prettyNames]"); expect(pr).not.toBe(undefined); expect(pr!.id).toBe(processorId); expect(output).toBe("VIEW: Homer Simpson"); proot.firstName = "HOMER"; await trax.reconciliation(); expect(output).toBe("VIEW: HOMER Simpson"); expect(pr!.disposed).toBe(false); }); it('store.init should allow to create multiple root object processors', async () => { let count1 = 0, count2 = 0; const pstore = trax.createStore("PStore", (store: Store<Person>) => { const p = store.init({ firstName: "Homer", lastName: "Simpson" }, { "~prettyName": (person) => { count1++; const nm = person.firstName + " " + person.lastName; person.prettyName = nm; }, "~prettyNameLength": (person) => { count2++; person.prettyNameLength = (person.prettyName || "").length; } }); }); const proot = pstore.data; let output = ""; expect(count1).toBe(0); expect(count2).toBe(0); const r1 = pstore.compute("Render", () => { output = "VIEW: " + proot.prettyName; }, true, true); expect(count1).toBe(1); expect(count2).toBe(1); expect(output).toBe("VIEW: Homer Simpson"); expect(proot.prettyNameLength).toBe(13); expect(count1).toBe(1); expect(count2).toBe(1); proot.firstName = "H"; expect(count1).toBe(1); expect(count2).toBe(1); await trax.reconciliation(); expect(count1).toBe(2); expect(count2).toBe(2); expect(output).toBe("VIEW: H Simpson"); expect(proot.prettyNameLength).toBe(9); r1.dispose(); proot.firstName = "HOMER"; expect(count1).toBe(2); expect(count2).toBe(2); await trax.reconciliation(); expect(count1).toBe(2); // dirty but not called expect(count2).toBe(2); const r2 = pstore.compute("Render", () => { output = "VIEW2: " + proot.prettyName; }, true, true); expect(count1).toBe(3); expect(count2).toBe(3); expect(output).toBe("VIEW2: HOMER Simpson"); }); it('store.add should allow to create and dipose multiple processors', async () => { let nm1 = "", nm2 = ""; const fstore = trax.createStore("FStore", (store: Store<SimpleFamilyStore>) => { const root = store.init({ childNames: "S" }); const f = store.add<Person>("Father", { firstName: "Homer", lastName: "Simpson" }, { "~pn": (o, cc) => { o.prettyName = o.firstName + "/" + o.lastName + "/" + root.childNames; nm1 = cc.processorName; }, "~pnl": (o, cc) => { o.prettyNameLength = (o.prettyName || "").length; nm2 = cc.processorName; } }); root.father = f; }); const fam = fstore.data; let output = ""; fstore.compute("Render", () => { output = "VIEW: " + fam.father!.prettyName; }, true, true); expect(output).toBe("VIEW: Homer/Simpson/S"); expect(fam.father!.prettyNameLength).toBe(15); expect(nm1).toBe("~pn"); expect(nm2).toBe("~pnl"); expect(trax.isTraxObject(fam.father)).toBe(true); fam.childNames = "SIMS" await trax.reconciliation(); expect(output).toBe("VIEW: Homer/Simpson/SIMS"); expect(fam.father!.prettyNameLength).toBe(18); expect(nm1).toBe("~pn"); expect(nm2).toBe("~pnl"); const father = fam.father!; fstore.remove(father); // warnint: cannot call fam.father here otherwise it may re-wrap the object expect(trax.isTraxObject(father)).toBe(false); fam.childNames = "S" await trax.reconciliation(); expect(printLogs(1)).toMatchObject([ "1:1 !GET - FStore/data.father -> '[TRAX FStore/Father]'", "1:2 !GET - FStore/Father.prettyNameLength -> 18", "1:3 !GET - FStore/data.father -> '[TRAX FStore/Father]'", "1:4 !DEL - FStore#Father[~pn]", "1:5 !DEL - FStore#Father[~pnl]", "1:6 !DEL - FStore/Father", "1:7 !SET - FStore/data.childNames = 'S' (prev: 'SIMS')", // no call to FStore#Father[0] or FStore#Father[1] ]); }); it('should not be reprocessed when listeners get disposed', async () => { const fstore = trax.createStore("FStore", (store: Store<SimpleFamilyStore>) => { const root = store.init({ childNames: "S" }); const f = store.add<Person>("Father", { firstName: "Homer", lastName: "Simpson" }, { "~prettyName": (o) => { o.prettyName = o.firstName + "/" + o.lastName + "/" + root.childNames; }, "~prettyNameLength": (o) => { o.prettyNameLength = (o.prettyName || "").length; } }); root.father = f; }); const fam = fstore.data; let output = ""; const render1 = fstore.compute("Render", () => { output = "VIEW: " + fam.father!.prettyName; }, true, true); expect(output).toBe("VIEW: Homer/Simpson/S"); expect(printLogs(0)).toMatchObject([ "0:1 !PCS - !StoreInit (FStore)", "0:2 !NEW - S: FStore", "0:3 !NEW - O: FStore/data", "0:4 !NEW - O: FStore/Father", "0:5 !NEW - P: FStore#Father[~prettyName]", "0:6 !SKP - FStore#Father[~prettyName]", "0:7 !NEW - P: FStore#Father[~prettyNameLength]", "0:8 !SKP - FStore#Father[~prettyNameLength]", "0:9 !SET - FStore/data.father = '[TRAX FStore/Father]' (prev: undefined)", "0:10 !PCE - 0:1", "0:11 !NEW - P: FStore#Render", "0:12 !PCS - !Compute #1 (FStore#Render) P3R Init", "0:13 !GET - FStore/data.father -> '[TRAX FStore/Father]'", "0:14 !PCS - !Compute #1 (FStore#Father[~prettyName]) P1 TargetRead - parentId=0:12", "0:15 !GET - FStore/Father.firstName -> 'Homer'", "0:16 !GET - FStore/Father.lastName -> 'Simpson'", "0:17 !GET - FStore/data.childNames -> 'S'",