Skip to content

Commit 0bc8c5f

Browse files
committed
Fixes #31
1 parent 74161ae commit 0bc8c5f

File tree

3 files changed

+133
-36
lines changed

3 files changed

+133
-36
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ S.id(a); // kjhjkgasd
197197

198198
This id is not globally unique, or cryptographically random, its just a debugging thing.
199199

200+
You can also overwrite the generated id with your own to make debugging easier:
201+
202+
```js
203+
S.id(a, 'a')
204+
S.id() // 'a'
205+
```
206+
207+
> 🤔 In future we may just expose the id map instead of having this getter setter
208+
200209
### `S.sample`
201210

202211
Read the value of a signal within a computation without treating it as a dependency of that computation.

lib/index.ts

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type SyncComputationInternal<T> = {
66
value?: T;
77
next?: T;
88
compute: () => void;
9+
id: string;
910
};
1011

1112
type GeneratorComputationInternal<T> = {
@@ -14,13 +15,16 @@ type GeneratorComputationInternal<T> = {
1415
value?: T;
1516
next?: T;
1617
compute: () => void;
18+
id: string;
1719
};
1820

1921
type ComputationInternal<T> =
2022
| SyncComputationInternal<T>
2123
| GeneratorComputationInternal<T>;
2224

23-
type DataInternal<T> = { type: "Stream"; tag: "Data"; value?: T; next?: T };
25+
type DataInternal<T> = {
26+
type: "Stream"; tag: "Data"; value?: T; next?: T, id: string
27+
};
2428

2529
type StreamInternal<T> = DataInternal<T> | ComputationInternal<T>;
2630

@@ -70,7 +74,21 @@ const rootOfStream = new WeakMap<ComputationInternal<unknown>, Root>();
7074
/**
7175
* The computations that will need to rerun next tick
7276
*/
73-
export let toRun = new Set<ComputationInternal<unknown>>();
77+
export let runningNextTick = new Set<ComputationInternal<unknown>>();
78+
79+
/**
80+
* The computations that are scheduled to run in the current propagation
81+
*/
82+
export let runningThisTick = new Set<ComputationInternal<unknown>>();
83+
84+
/**
85+
* If a computation is nested within another computation
86+
* and the parent is scheduled to run in this tick along with the child
87+
* we only need to run the parent, as the child will automatically
88+
* run when the parent compute runs, so we need to skip
89+
* those child nodes to prevent double updates, that's what this set is for
90+
*/
91+
export let allChildren = new Set<ComputationInternal<unknown>>();
7492

7593
/**
7694
* The direct dependencies of a stream, used in computeDependents
@@ -224,7 +242,7 @@ function computeDependents(stream: StreamInternal<unknown>) {
224242
}
225243
}
226244

227-
toRun.add(x);
245+
runningNextTick.add(x);
228246
}
229247
}
230248

@@ -245,6 +263,9 @@ export function data<T>(
245263
tag: "Data",
246264
next: value,
247265
value,
266+
get id(){
267+
return ids.get( accessor )!
268+
}
248269
};
249270

250271
let accessor = (...args: T[] | [(x?: T) => T]) => {
@@ -325,6 +346,9 @@ export function computation<T>(
325346
streamsToResolve.add(stream);
326347
}
327348
},
349+
get id(){
350+
return ids.get(accessor)!
351+
}
328352
};
329353

330354
// whatever active was when this was defined
@@ -365,15 +389,21 @@ export function computation<T>(
365389
stream.compute();
366390
}
367391

368-
return record(stream, () => {
392+
const accessor = () => {
393+
if ( state === 'propagating' && runningThisTick.has(stream) ) {
394+
// compute now
395+
tickOne(stream)
396+
}
369397
if (active[0]) {
370398
xet(dependents, stream, () => new Set()).add(active[0]);
371399

372400
return stream.next!;
373401
} else {
374402
return stream.value!;
375403
}
376-
});
404+
}
405+
406+
return record(stream, accessor);
377407
}
378408

379409
// only defining these types as I couldn't
@@ -448,6 +478,9 @@ export function generator<T>(
448478
}
449479
});
450480
},
481+
get id(){
482+
return ids.get(accessor)!
483+
}
451484
};
452485

453486
async function iterate<T>(it: StreamIterator<T>) {
@@ -489,14 +522,19 @@ export function generator<T>(
489522

490523
stream.compute();
491524

492-
return record(stream, () => {
525+
const accessor = () => {
526+
if ( state === 'propagating' && runningThisTick.has(stream) ) {
527+
// compute now
528+
tickOne(stream)
529+
}
493530
if (active[0]) {
494531
xet(dependents, stream, () => new Set()).add(active[0]);
495532

496533
return stream.next;
497534
}
498535
return stream.value;
499-
});
536+
}
537+
return record(stream, accessor);
500538
}
501539

502540
export function freeze(f: VoidFunction): void {
@@ -548,7 +586,7 @@ export function root<T>(f: (dispose: VoidFunction) => T) {
548586
cleanup();
549587
}
550588
dependents.delete(x);
551-
toRun.delete(x);
589+
runningNextTick.delete(x);
552590
cleanups.delete(x);
553591
parents.delete(x);
554592
}
@@ -559,25 +597,57 @@ export function root<T>(f: (dispose: VoidFunction) => T) {
559597
return out;
560598
}
561599

600+
function tickOne(
601+
stream: ComputationInternal<unknown>
602+
){
603+
if ( !runningThisTick.has(stream) ) {
604+
// evalauted already by another compute
605+
return;
606+
}
607+
for (let cleanupFn of xet(cleanups, stream, () => new Set())) {
608+
cleanupFn();
609+
cleanups.get(stream)!.delete(cleanupFn);
610+
}
611+
612+
// if is a child, we only need to clean up
613+
if (allChildren.has(stream)) {
614+
return;
615+
} else {
616+
let oldActive = active;
617+
active = [...(parents.get(stream) ?? [])];
618+
619+
let oldActiveRoot = activeRoot;
620+
activeRoot = rootOfStream.get(stream) ?? null;
621+
622+
stream.compute();
623+
624+
active = oldActive;
625+
activeRoot = oldActiveRoot;
626+
stats.computations.evaluated++;
627+
}
628+
runningThisTick.delete(stream)
629+
}
630+
562631
export function tick() {
563632
if (state === "propagating" || state === "frozen") return;
564633

565634
stats.ticks++;
566635

567636
state = "propagating";
568637

569-
let oldToRun = new Set(toRun);
638+
runningThisTick = new Set(runningNextTick);
639+
640+
runningNextTick.clear();
570641

571-
toRun.clear();
642+
allChildren.clear()
572643

573-
let allChildren = new Set<ComputationInternal<unknown>>();
574-
for (let x of oldToRun) {
644+
for (let x of runningThisTick) {
575645
for (let child of children.get(x) ?? []) {
576646
// record the child exists
577647
allChildren.add(child);
578648
// add to the tick so we can run
579649
// the clean up fns (at most once)
580-
oldToRun.add(child);
650+
runningThisTick.add(child);
581651
parents.delete(child);
582652
streamsToResolve.delete(child);
583653
// should we delete the dependents too?
@@ -588,28 +658,8 @@ export function tick() {
588658
children.delete(x);
589659
}
590660

591-
for (let f of oldToRun) {
592-
for (let cleanupFn of xet(cleanups, f, () => new Set())) {
593-
cleanupFn();
594-
cleanups.get(f)!.delete(cleanupFn);
595-
}
596-
597-
// if is a child, we only need to clean up
598-
if (allChildren.has(f)) {
599-
continue;
600-
} else {
601-
let oldActive = active;
602-
active = [...(parents.get(f) ?? [])];
603-
604-
let oldActiveRoot = activeRoot;
605-
activeRoot = rootOfStream.get(f) ?? null;
606-
607-
f.compute();
608-
609-
active = oldActive;
610-
activeRoot = oldActiveRoot;
611-
stats.computations.evaluated++;
612-
}
661+
for (let stream of [...runningThisTick]) {
662+
tickOne(stream)
613663
}
614664

615665
for (let s of streamsToResolve) {
@@ -635,6 +685,7 @@ export function tick() {
635685
nextTicks.length = 0;
636686
for (let next of xs) {
637687
if (runawayTicks > MAX_TICKS) {
688+
state = 'idle'
638689
throw new RunawayTicks();
639690
}
640691
next();
@@ -659,6 +710,9 @@ export function sample<T>(signal: Signal<T>) {
659710
return value;
660711
}
661712

662-
export function id(s: Signal<any>) {
713+
export function id(s: Signal<any>, newId?: string) {
714+
if (newId != null ) {
715+
ids.set(s, newId)
716+
}
663717
return ids.get(s);
664718
}

test/test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,5 +1080,39 @@ test('Max ticks', t => {
10801080
}, S.RunawayTicks, 'Runaway ticks caught')
10811081

10821082
t.equals(S.stats.ticks, S.MAX_TICKS+1, 'Caught at max ticks')
1083+
t.end()
1084+
})
1085+
1086+
test('https://github.com/JAForbes/S/issues/31', t => {
1087+
1088+
const a = S.data(1)
1089+
1090+
const { b,c,d } = S.root(() => {
1091+
1092+
const b = S.computation(() => {
1093+
return a() * 1
1094+
})
1095+
1096+
const c = S.computation(() => {
1097+
return b()! * 2
1098+
})
1099+
1100+
const d = S.computation(() => {
1101+
return a() + c()!
1102+
})
1103+
1104+
return { b, c, d }
1105+
})
1106+
1107+
Object.entries({ a,b, c,d }).map( ([k,v]) => S.id(v,k) )
1108+
1109+
t.equals(c()!, 2, 'c propagated')
1110+
t.equals(d()!, 3, 'd propagated')
1111+
1112+
a(2)
1113+
1114+
t.equals(c()!, 4, 'c propagated')
1115+
t.equals(d()!, 6, 'd propagated')
1116+
10831117
t.end()
10841118
})

0 commit comments

Comments
 (0)