@traxjs/trax
Version:
Reactive state management
1,163 lines (1,017 loc) • 78.3 kB
text/typescript
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'",