Skip to content

Commit

Permalink
Add watched and unwatched
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Dec 27, 2024
1 parent 9022d4a commit 1fe46fe
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 5 deletions.
32 changes: 27 additions & 5 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ declare class Signal<T = any> {
/** @internal */
_targets?: Node;

constructor(value?: T);
constructor(value?: T, options?: SignalOptions<T>);

/** @internal */
_refresh(): boolean;
Expand All @@ -250,6 +250,12 @@ declare class Signal<T = any> {
/** @internal */
_unsubscribe(node: Node): void;

/** @internal */
_watched?(this: Signal<T>): void;

/** @internal */
_unwatched?(this: Signal<T>): void;

subscribe(fn: (value: T) => void): () => void;

valueOf(): T;
Expand All @@ -266,6 +272,11 @@ declare class Signal<T = any> {
set value(value: T);
}

interface SignalOptions<T = any> {
watched: (this: Signal<T>) => void;
unwatched: (this: Signal<T>) => void;
}

/** @internal */
// @ts-ignore: "Cannot redeclare exported variable 'Signal'."
//
Expand All @@ -274,11 +285,13 @@ declare class Signal<T = any> {
//
// The previously declared class is implemented here with ES5-style prototypes.
// This enables better control of the transpiled output size.
function Signal(this: Signal, value?: unknown) {
function Signal(this: Signal, value?: unknown, options?: SignalOptions) {
this._value = value;
this._version = 0;
this._node = undefined;
this._targets = undefined;
this._watched = options?.watched;
this._unwatched = options?.unwatched;
}

Signal.prototype.brand = BRAND_SYMBOL;
Expand All @@ -288,6 +301,9 @@ Signal.prototype._refresh = function () {
};

Signal.prototype._subscribe = function (node) {
if (this._watched != null && this._targets === undefined) {
this._watched();
}
if (this._targets !== node && node._prevTarget === undefined) {
node._nextTarget = this._targets;
if (this._targets !== undefined) {
Expand All @@ -306,13 +322,19 @@ Signal.prototype._unsubscribe = function (node) {
prev._nextTarget = next;
node._prevTarget = undefined;
}

if (next !== undefined) {
next._prevTarget = prev;
node._nextTarget = undefined;
}

if (node === this._targets) {
this._targets = next;
}

if (this._unwatched != null && this._targets === undefined) {
this._unwatched();
}
}
};

Expand Down Expand Up @@ -392,10 +414,10 @@ Object.defineProperty(Signal.prototype, "value", {
* @param value The initial value for the signal.
* @returns A new signal.
*/
export function signal<T>(value: T): Signal<T>;
export function signal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
export function signal<T = undefined>(): Signal<T | undefined>;
export function signal<T>(value?: T): Signal<T> {
return new Signal(value);
export function signal<T>(value?: T, options?: SignalOptions<T>): Signal<T> {
return new Signal(value, options);
}

function needsToRecompute(target: Computed | Effect): boolean {
Expand Down
16 changes: 16 additions & 0 deletions packages/core/test/signal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ describe("signal", () => {
});
});

describe.only(".(un)watched()", () => {
it("should call watched when first subscription occurs", () => {
const watched = sinon.spy();
const unwatched = sinon.spy();
const s = signal(1, { watched, unwatched });
expect(watched).to.not.be.called;
const unsubscribe = s.subscribe(() => {});
expect(watched).to.be.calledOnce;
const unsubscribe2 = s.subscribe(() => {});
expect(watched).to.be.calledOnce;
unsubscribe();
unsubscribe2();
expect(unwatched).to.be.calledOnce;
});
});

it("signals should be identified with a symbol", () => {
const a = signal(0);
expect(a.brand).to.equal(Symbol.for("preact-signals"));
Expand Down

0 comments on commit 1fe46fe

Please sign in to comment.