Skip to content

Commit b28f5c9

Browse files
committed
wip - add topological sorting
1 parent 9767c80 commit b28f5c9

File tree

10 files changed

+247
-88
lines changed

10 files changed

+247
-88
lines changed

build/cami.cdn.js

Lines changed: 35 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/cami.cdn.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/cami.module.js

Lines changed: 35 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/cami.module.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/javascripts/cami.cdn.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,6 +2051,26 @@ var cami = (() => {
20512051
static clearGraph() {
20522052
_DependencyTracker.dependencyGraph.clear();
20532053
}
2054+
static topologicalSort() {
2055+
const visited = /* @__PURE__ */ new Set();
2056+
const stack = [];
2057+
function dfs(node) {
2058+
visited.add(node);
2059+
const neighbors = _DependencyTracker.dependencyGraph.get(node) || /* @__PURE__ */ new Set();
2060+
for (const neighbor of neighbors) {
2061+
if (!visited.has(neighbor)) {
2062+
dfs(neighbor);
2063+
}
2064+
}
2065+
stack.push(node);
2066+
}
2067+
for (const node of _DependencyTracker.dependencyGraph.keys()) {
2068+
if (!visited.has(node)) {
2069+
dfs(node);
2070+
}
2071+
}
2072+
return stack.reverse();
2073+
}
20542074
};
20552075
__publicField(_DependencyTracker, "current", null);
20562076
__publicField(_DependencyTracker, "dependencyGraph", /* @__PURE__ */ new Map());
@@ -2422,13 +2442,25 @@ var cami = (() => {
24222442
const tracker = {
24232443
addDependency: (observable) => {
24242444
if (!dependencies.has(observable)) {
2425-
const subscription = observable.onValue(_runEffect);
2445+
const subscription = observable.onValue(() => scheduleEffect());
24262446
dependencies.add(observable);
24272447
subscriptions.set(observable, subscription);
24282448
}
24292449
}
24302450
};
2431-
const _runEffect = () => {
2451+
const scheduleEffect = () => {
2452+
if (typeof window !== "undefined") {
2453+
requestAnimationFrame(runEffectInOrder);
2454+
} else {
2455+
queueMicrotask(runEffectInOrder);
2456+
}
2457+
};
2458+
const runEffectInOrder = () => {
2459+
const order = DependencyTracker.topologicalSort();
2460+
const relevantOrder = order.filter((obs) => dependencies.has(obs));
2461+
for (const obs of relevantOrder) {
2462+
obs.__applyUpdates();
2463+
}
24322464
cleanup();
24332465
DependencyTracker.current = tracker;
24342466
try {
@@ -2445,11 +2477,7 @@ var cami = (() => {
24452477
console.warn(error.message);
24462478
}
24472479
};
2448-
if (typeof window !== "undefined") {
2449-
requestAnimationFrame(_runEffect);
2450-
} else {
2451-
queueMicrotask(_runEffect);
2452-
}
2480+
scheduleEffect();
24532481
const dispose = () => {
24542482
subscriptions.forEach((subscription) => {
24552483
subscription.unsubscribe();

src/observables/observable-state.js

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,32 @@ class DependencyTracker {
8282
static clearGraph() {
8383
DependencyTracker.dependencyGraph.clear();
8484
}
85+
86+
static topologicalSort() {
87+
const visited = new Set();
88+
const stack = [];
89+
90+
function dfs(node) {
91+
visited.add(node);
92+
93+
const neighbors = DependencyTracker.dependencyGraph.get(node) || new Set();
94+
for (const neighbor of neighbors) {
95+
if (!visited.has(neighbor)) {
96+
dfs(neighbor);
97+
}
98+
}
99+
100+
stack.push(node);
101+
}
102+
103+
for (const node of DependencyTracker.dependencyGraph.keys()) {
104+
if (!visited.has(node)) {
105+
dfs(node);
106+
}
107+
}
108+
109+
return stack.reverse();
110+
}
85111
}
86112

87113
/**
@@ -496,40 +522,38 @@ const effect = function(effectFn) {
496522
let dependencies = new Set();
497523
let subscriptions = new Map();
498524

499-
/**
500-
* The tracker object is used to keep track of dependencies for the effect function.
501-
* It provides a method to add a dependency (an observable) to the dependencies set.
502-
* If the observable is not already a dependency, it is added to the set and a subscription is created
503-
* to run the effect function whenever the observable's value changes.
504-
* This mechanism allows the effect function to respond to state changes in its dependencies.
505-
*/
506525
const tracker = {
507526
addDependency: (observable) => {
508527
if (!dependencies.has(observable)) {
509-
const subscription = observable.onValue(_runEffect);
528+
const subscription = observable.onValue(() => scheduleEffect());
510529
dependencies.add(observable);
511530
subscriptions.set(observable, subscription);
512531
}
513532
}
514533
};
515534

516-
/**
517-
* The _runEffect function is responsible for running the effect function and managing its dependencies.
518-
* Before the effect function is run, any cleanup from the previous run is performed and the current tracker
519-
* is set to this tracker. This allows the effect function to add dependencies via the tracker while it is running.
520-
* After the effect function has run, the current tracker is set back to null to prevent further dependencies
521-
* from being added outside of the effect function.
522-
* The effect function is expected to return a cleanup function, which is saved for the next run.
523-
* The cleanup function, initially empty, is replaced by the one returned from effectFn (run by the observable) before each new run and on effect disposal.
524-
*/
525-
const _runEffect = () => {
535+
const scheduleEffect = () => {
536+
if (typeof window !== 'undefined') {
537+
requestAnimationFrame(runEffectInOrder);
538+
} else {
539+
queueMicrotask(runEffectInOrder);
540+
}
541+
};
542+
543+
const runEffectInOrder = () => {
544+
const order = DependencyTracker.topologicalSort();
545+
const relevantOrder = order.filter(obs => dependencies.has(obs));
546+
547+
for (const obs of relevantOrder) {
548+
obs.__applyUpdates();
549+
}
550+
526551
cleanup();
527552
DependencyTracker.current = tracker;
528553
try {
529554
cleanup = effectFn() || (() => {});
530555
} catch (error) {
531556
console.warn(error.message);
532-
// Optionally, you can add more detailed logging here
533557
} finally {
534558
DependencyTracker.current = null;
535559
}
@@ -541,20 +565,8 @@ const effect = function(effectFn) {
541565
}
542566
};
543567

544-
if (typeof window !== 'undefined') {
545-
requestAnimationFrame(_runEffect);
546-
} else {
547-
queueMicrotask(_runEffect);
548-
}
568+
scheduleEffect();
549569

550-
/**
551-
* @method
552-
* @description Unsubscribes from all dependencies and runs cleanup function
553-
* @returns {void}
554-
* @example
555-
* // Assuming `dispose` is the function returned by `effect`
556-
* dispose(); // This will unsubscribe from all dependencies and run cleanup function
557-
*/
558570
const dispose = () => {
559571
subscriptions.forEach((subscription) => {
560572
subscription.unsubscribe();

tests/spec/CounterSpec.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,33 @@ describe('Integers are Observable - CounterElement', () => {
55
counter = document.createElement('counter-test');
66
document.body.appendChild(counter);
77
await window.customElements.whenDefined('counter-test');
8-
await counter.updateComplete;
9-
await new Promise(resolve => setTimeout(resolve, 50));
108
});
119

1210
afterEach(() => {
1311
document.body.removeChild(counter);
1412
});
1513

16-
it('increments count', () => {
14+
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
15+
16+
it('increments count', async () => {
1717
const incrementButton = counter.querySelector('button:nth-child(2)');
1818
incrementButton.click();
1919
incrementButton.click();
20+
await wait(50); // Add a small delay
2021
const countDiv = counter.querySelector('div');
2122
expect(countDiv.textContent).toEqual(`Count: 2`);
2223
});
2324

24-
it('decrements count', () => {
25+
it('decrements count', async () => {
2526
const decrementButton = counter.querySelector('button:nth-child(1)');
2627
decrementButton.click();
2728
decrementButton.click();
29+
await wait(50); // Add a small delay
2830
const countDiv = counter.querySelector('div');
2931
expect(countDiv.textContent).toEqual(`Count: -2`);
3032
});
3133

32-
it('renders count correctly', () => {
34+
it('renders count correctly', async () => {
3335
const countDiv = counter.querySelector('div');
3436
expect(countDiv.textContent).toEqual(`Count: ${counter.count}`);
3537
});

0 commit comments

Comments
 (0)