-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
144 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { expect, it, vi } from "vitest"; | ||
import { computed, reactive } from "./ReactiveImpl"; | ||
import { subtleWatchDirty } from "./watchDirty"; | ||
|
||
it("notifies the callback when the signal changed", () => { | ||
const v = reactive(0); | ||
const callback = vi.fn(); | ||
subtleWatchDirty(v, callback); | ||
|
||
expect(callback).toHaveBeenCalledTimes(0); | ||
|
||
v.value = 1; | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
|
||
v.value = 2; | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it("stops notifications when the watch is destroyed", () => { | ||
const v = reactive(0); | ||
const callback = vi.fn(); | ||
const { destroy } = subtleWatchDirty(v, callback); | ||
|
||
v.value = 1; | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
|
||
destroy(); | ||
v.value = 2; | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it("does not trigger computed signals", () => { | ||
const count = reactive(0); | ||
const compute = vi.fn(() => count.value + 1); | ||
const signal = computed(compute); | ||
expect(compute).toHaveBeenCalledTimes(0); | ||
|
||
const callback = vi.fn(); | ||
subtleWatchDirty(signal, callback); | ||
expect(callback).toHaveBeenCalledTimes(0); | ||
expect(compute).toHaveBeenCalledTimes(1); // watchDirty accesses the value once during setup | ||
|
||
count.value = 1; | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
expect(compute).toHaveBeenCalledTimes(1); // not called again | ||
|
||
signal.value; | ||
expect(compute).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it("tracks the signal even if the initial access throws", () => { | ||
const count = reactive(0); | ||
const compute = vi.fn(() => { | ||
if (count.value === 0) { | ||
throw new Error("oops!"); | ||
} | ||
return count.value + 1; | ||
}); | ||
const signal = computed(compute); | ||
|
||
const callback = vi.fn(); | ||
subtleWatchDirty(signal, callback); | ||
expect(callback).toHaveBeenCalledTimes(0); | ||
|
||
// Was called during setup and the error was ignored. | ||
expect(compute).toHaveBeenCalledTimes(1); | ||
expect(compute.mock.results).toMatchInlineSnapshot(` | ||
[ | ||
{ | ||
"type": "throw", | ||
"value": [Error: oops!], | ||
}, | ||
] | ||
`); | ||
|
||
// Update triggers notification | ||
count.value = 1; | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { effect as rawEffect } from "@preact/signals-core"; | ||
import { CleanupHandle, ReadonlyReactive } from "./types"; | ||
|
||
/** | ||
* **Experimental**. | ||
* Notifies the given `callback` whenever the `signal` might have changed, | ||
* without recomputing the signal's current value. | ||
* | ||
* This is a difficult to use, low level API that can be used to build higher level abstractions. | ||
* | ||
* Things to keep in mind when using this function: | ||
* The `callback` should be cheap to invoke (as a signal might change often) and it **must not** throw an exception. | ||
* It should also not make use of any reactive values. | ||
* | ||
* @param signal The signal being watched. | ||
* @param callback The callback to be called whenever the signal might have changed. | ||
* | ||
* @group Watching | ||
*/ | ||
export function subtleWatchDirty<T>(signal: ReadonlyReactive<T>, callback: () => void): CleanupHandle { | ||
// Uses the effect's internals to track signal invalidations. | ||
// The effect body is only called once! | ||
// See https://github.com/preactjs/signals/issues/593#issuecomment-2349672856 | ||
let start!: () => () => void; | ||
const destroy = rawEffect(function (this: RawEffectInternals) { | ||
this[_NOTIFY] = callback.bind(undefined); // hide 'this' | ||
start = this[_START].bind(this); | ||
}); | ||
|
||
const end = start(); | ||
try { | ||
signal.value; // Tracked | ||
} catch (ignored) { | ||
// We only care about the dependency being set up correctly. | ||
void ignored; | ||
} finally { | ||
end(); | ||
} | ||
|
||
return { | ||
destroy | ||
}; | ||
} | ||
|
||
// Mangled member names. See https://github.com/preactjs/signals/blob/main/mangle.json. | ||
const _NOTIFY = "N"; | ||
const _START = "S"; | ||
|
||
interface RawEffectInternals { | ||
// Notifies the effect that a dependency has changed. | ||
// This usually schedules the effect to run again (when not overridden). | ||
[_NOTIFY](): void; | ||
|
||
// Starts the effect and returns a function to stop it again. | ||
// Signal accesses are tracked while the effect is running. | ||
[_START](): () => void; | ||
} |