From 451ae6ec513a7458984716c1997c4cfc629fc1db Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:50:30 -0400 Subject: [PATCH 1/2] Add prettier --- .github/workflows/test.yml | 2 + packages/signal-polyfill/.prettierrc.js | 6 + packages/signal-polyfill/package.json | 3 + packages/signal-polyfill/readme.md | 40 +- packages/signal-polyfill/src/computed.ts | 15 +- packages/signal-polyfill/src/equality.ts | 2 +- packages/signal-polyfill/src/errors.ts | 2 +- packages/signal-polyfill/src/graph.ts | 123 +++-- packages/signal-polyfill/src/index.ts | 2 +- packages/signal-polyfill/src/signal.ts | 39 +- packages/signal-polyfill/src/wrapper.spec.ts | 169 +++---- packages/signal-polyfill/src/wrapper.ts | 471 ++++++++++--------- packages/signal-polyfill/tsconfig.json | 17 +- packages/signal-polyfill/vite.config.ts | 18 +- 14 files changed, 491 insertions(+), 418 deletions(-) create mode 100644 packages/signal-polyfill/.prettierrc.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d5f1dc..5e2b55d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,3 +21,5 @@ jobs: - uses: actions/setup-node@v4 - run: npm install - run: npm test + - run: npm run format:check + working-directory: packages/signal-polyfill diff --git a/packages/signal-polyfill/.prettierrc.js b/packages/signal-polyfill/.prettierrc.js new file mode 100644 index 0000000..246a62c --- /dev/null +++ b/packages/signal-polyfill/.prettierrc.js @@ -0,0 +1,6 @@ +/** @type {import("prettier").Config} */ +const config = { + singleQuote: true, +}; + +export default config; diff --git a/packages/signal-polyfill/package.json b/packages/signal-polyfill/package.json index d783e91..82a5290 100644 --- a/packages/signal-polyfill/package.json +++ b/packages/signal-polyfill/package.json @@ -17,6 +17,8 @@ "types": "dist/index.d.ts", "scripts": { "dev": "vite", + "format:check": "prettier --check .", + "format": "prettier --write .", "build": "tsc && vite build", "test": "vitest" }, @@ -24,6 +26,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.11.25", + "prettier": "^3.2.5", "typescript": "latest", "vite": "^5.2.6", "vite-plugin-dts": "^3.7.3", diff --git a/packages/signal-polyfill/readme.md b/packages/signal-polyfill/readme.md index 2a8294f..4ed92be 100644 --- a/packages/signal-polyfill/readme.md +++ b/packages/signal-polyfill/readme.md @@ -1,6 +1,6 @@ # Signal Polyfill -## ⚠️ This polyfill is a preview of an in-progress proposal and could change at any time. Do not use this in production. ⚠️ +## ⚠️ This polyfill is a preview of an in-progress proposal and could change at any time. Do not use this in production. ⚠️ A "signal" is [a proposed first-class JavaScript data type](../../README.md) that enables one-way data flow through cells of state or computations derived from other state/computations. @@ -10,16 +10,16 @@ This is a polyfill for the `Signal` API. ### Using signals -* Use `Signal.State(value)` to create a single "cell" of data that can flow through the unidirectional state graph. -* Use `Signal.Computed(callback)` to define a computation based on state or other computations flowing through the graph. +- Use `Signal.State(value)` to create a single "cell" of data that can flow through the unidirectional state graph. +- Use `Signal.Computed(callback)` to define a computation based on state or other computations flowing through the graph. ```js -import { Signal } from "signal-polyfill"; -import { effect } from "./effect.js"; +import { Signal } from 'signal-polyfill'; +import { effect } from './effect.js'; const counter = new Signal.State(0); const isEven = new Signal.Computed(() => (counter.get() & 1) == 0); -const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd"); +const parity = new Signal.Computed(() => (isEven.get() ? 'even' : 'odd')); effect(() => console.log(parity.get())); // Console logs "even" immediately. setInterval(() => counter.set(counter.get() + 1), 1000); // Changes the counter every 1000ms. @@ -44,14 +44,14 @@ Depending on how the effect is implemented, the above code could result in an in ### Creating a simple effect -* You can use `Signal.subtle.Watch(callback)` combined with `Signal.Computed(callback)` to create a simple _effect_ implementation. -* The `Signal.subtle.Watch` `callback` is invoked synchronously when a watched signal becomes dirty. -* To batch effect updates, library authors are expected to implement their own schedulers. -* Use `Signal.subtle.Watch#getPending()` to retrieve an array of dirty signals. -* Calling `Signal.subtle.Watch#watch()` with no arguments will re-watch the list of tracked signals again. +- You can use `Signal.subtle.Watch(callback)` combined with `Signal.Computed(callback)` to create a simple _effect_ implementation. +- The `Signal.subtle.Watch` `callback` is invoked synchronously when a watched signal becomes dirty. +- To batch effect updates, library authors are expected to implement their own schedulers. +- Use `Signal.subtle.Watch#getPending()` to retrieve an array of dirty signals. +- Calling `Signal.subtle.Watch#watch()` with no arguments will re-watch the list of tracked signals again. ```js -import { Signal } from "signal-polyfill"; +import { Signal } from 'signal-polyfill'; let needsEnqueue = true; @@ -64,7 +64,7 @@ const w = new Signal.subtle.Watcher(() => { function processPending() { needsEnqueue = true; - + for (const s of w.getPending()) { s.get(); } @@ -74,18 +74,18 @@ function processPending() { export function effect(callback) { let cleanup; - + const computed = new Signal.Computed(() => { - typeof cleanup === "function" && cleanup(); + typeof cleanup === 'function' && cleanup(); cleanup = callback(); }); - + w.watch(computed); computed.get(); - + return () => { w.unwatch(computed); - typeof cleanup === "function" && cleanup(); + typeof cleanup === 'function' && cleanup(); }; } ``` @@ -98,7 +98,7 @@ export function effect(callback) { A class accessor decorator can be combined with the `Signal.State()` API to enable improved DX. ```js -import { Signal } from "signal-polyfill"; +import { Signal } from 'signal-polyfill'; export function signal(target) { const { get } = target; @@ -111,7 +111,7 @@ export function signal(target) { set(value) { get.call(this).set(value); }, - + init(value) { return new Signal.State(value); }, diff --git a/packages/signal-polyfill/src/computed.ts b/packages/signal-polyfill/src/computed.ts index 50b3e85..2934fd6 100644 --- a/packages/signal-polyfill/src/computed.ts +++ b/packages/signal-polyfill/src/computed.ts @@ -6,9 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {defaultEquals, ValueEqualityFn} from './equality.js'; -import {consumerAfterComputation, consumerBeforeComputation, producerAccessed, producerUpdateValueVersion, REACTIVE_NODE, ReactiveNode, SIGNAL} from './graph.js'; - +import { defaultEquals, ValueEqualityFn } from './equality.js'; +import { + consumerAfterComputation, + consumerBeforeComputation, + producerAccessed, + producerUpdateValueVersion, + REACTIVE_NODE, + ReactiveNode, + SIGNAL, +} from './graph.js'; /** * A computation, which derives a value from a declarative reactive expression. @@ -36,7 +43,7 @@ export interface ComputedNode extends ReactiveNode { equal: ValueEqualityFn; } -export type ComputedGetter = (() => T)&{ +export type ComputedGetter = (() => T) & { [SIGNAL]: ComputedNode; }; diff --git a/packages/signal-polyfill/src/equality.ts b/packages/signal-polyfill/src/equality.ts index 55cb94c..20f9b9f 100644 --- a/packages/signal-polyfill/src/equality.ts +++ b/packages/signal-polyfill/src/equality.ts @@ -16,4 +16,4 @@ export type ValueEqualityFn = (a: T, b: T) => boolean; */ export function defaultEquals(a: T, b: T) { return Object.is(a, b); -} \ No newline at end of file +} diff --git a/packages/signal-polyfill/src/errors.ts b/packages/signal-polyfill/src/errors.ts index dbfba19..0f58fe9 100644 --- a/packages/signal-polyfill/src/errors.ts +++ b/packages/signal-polyfill/src/errors.ts @@ -18,4 +18,4 @@ export function throwInvalidWriteToSignalError() { export function setThrowInvalidWriteToSignalError(fn: () => never): void { throwInvalidWriteToSignalErrorFn = fn; -} \ No newline at end of file +} diff --git a/packages/signal-polyfill/src/graph.ts b/packages/signal-polyfill/src/graph.ts index 8a18dac..dc62c1b 100644 --- a/packages/signal-polyfill/src/graph.ts +++ b/packages/signal-polyfill/src/graph.ts @@ -8,18 +8,17 @@ // Required as the signals library is in a separate package, so we need to explicitly ensure the // global `ngDevMode` type is defined. -declare const ngDevMode: boolean|undefined; - +declare const ngDevMode: boolean | undefined; /** * The currently active consumer `ReactiveNode`, if running code in a reactive context. * * Change this via `setActiveConsumer`. */ -let activeConsumer: ReactiveNode|null = null; +let activeConsumer: ReactiveNode | null = null; let inNotificationPhase = false; -type Version = number&{__brand: 'Version'}; +type Version = number & { __brand: 'Version' }; /** * Global epoch counter. Incremented whenever a source signal is set. @@ -33,13 +32,15 @@ let epoch: Version = 1 as Version; */ export const SIGNAL = /* @__PURE__ */ Symbol('SIGNAL'); -export function setActiveConsumer(consumer: ReactiveNode|null): ReactiveNode|null { +export function setActiveConsumer( + consumer: ReactiveNode | null, +): ReactiveNode | null { const prev = activeConsumer; activeConsumer = consumer; return prev; } -export function getActiveConsumer(): ReactiveNode|null { +export function getActiveConsumer(): ReactiveNode | null { return activeConsumer; } @@ -115,14 +116,14 @@ export interface ReactiveNode { * * Uses the same indices as the `producerLastReadVersion` and `producerIndexOfThis` arrays. */ - producerNode: ReactiveNode[]|undefined; + producerNode: ReactiveNode[] | undefined; /** * `Version` of the value last read by a given producer. * * Uses the same indices as the `producerNode` and `producerIndexOfThis` arrays. */ - producerLastReadVersion: Version[]|undefined; + producerLastReadVersion: Version[] | undefined; /** * Index of `this` (consumer) in each producer's `liveConsumers` array. @@ -132,7 +133,7 @@ export interface ReactiveNode { * * Uses the same indices as the `producerNode` and `producerLastReadVersion` arrays. */ - producerIndexOfThis: number[]|undefined; + producerIndexOfThis: number[] | undefined; /** * Index into the producer arrays that the next dependency of this node as a consumer will use. @@ -149,14 +150,14 @@ export interface ReactiveNode { * * `liveConsumerNode.length` is effectively our reference count for this node. */ - liveConsumerNode: ReactiveNode[]|undefined; + liveConsumerNode: ReactiveNode[] | undefined; /** * Index of `this` (producer) in each consumer's `producerNode` array. * * Uses the same indices as the `liveConsumerNode` array. */ - liveConsumerIndexOfThis: number[]|undefined; + liveConsumerIndexOfThis: number[] | undefined; /** * Whether writes to signals are allowed when this consumer is the `activeConsumer`. @@ -215,9 +216,10 @@ interface ProducerNode extends ReactiveNode { export function producerAccessed(node: ReactiveNode): void { if (inNotificationPhase) { throw new Error( - typeof ngDevMode !== 'undefined' && ngDevMode ? - `Assertion error: signal read during notification phase` : - ''); + typeof ngDevMode !== 'undefined' && ngDevMode + ? `Assertion error: signal read during notification phase` + : '', + ); } if (activeConsumer === null) { @@ -232,7 +234,10 @@ export function producerAccessed(node: ReactiveNode): void { assertConsumerNode(activeConsumer); - if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) { + if ( + idx < activeConsumer.producerNode.length && + activeConsumer.producerNode[idx] !== node + ) { // There's been a change in producers since the last execution of `activeConsumer`. // `activeConsumer.producerNode[idx]` holds a stale dependency which will be be removed and // replaced with `this`. @@ -242,7 +247,10 @@ export function producerAccessed(node: ReactiveNode): void { // to remove it from the stale producer's `liveConsumer`s. if (consumerIsLive(activeConsumer)) { const staleProducer = activeConsumer.producerNode[idx]; - producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]); + producerRemoveLiveConsumerAtIndex( + staleProducer, + activeConsumer.producerIndexOfThis[idx], + ); // At this point, the only record of `staleProducer` is the reference at // `activeConsumer.producerNode[idx]` which will be overwritten below. @@ -255,8 +263,9 @@ export function producerAccessed(node: ReactiveNode): void { // If the active consumer is live, then add it as a live consumer. If not, then use 0 as a // placeholder value. - activeConsumer.producerIndexOfThis[idx] = - consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0; + activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) + ? producerAddLiveConsumer(node, activeConsumer, idx) + : 0; } activeConsumer.producerLastReadVersion[idx] = node.version; } @@ -287,7 +296,10 @@ export function producerUpdateValueVersion(node: ReactiveNode): void { return; } - if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) { + if ( + !node.producerMustRecompute(node) && + !consumerPollProducersForChange(node) + ) { // None of our producers report a change since the last time they were read, so no // recomputation of our value is necessary, and we can consider ourselves clean. node.dirty = false; @@ -344,7 +356,9 @@ export function consumerMarkDirty(node: ReactiveNode): void { * Must be called by subclasses which represent reactive computations, before those computations * begin. */ -export function consumerBeforeComputation(node: ReactiveNode|null): ReactiveNode|null { +export function consumerBeforeComputation( + node: ReactiveNode | null, +): ReactiveNode | null { node && (node.nextProducerIndex = 0); return setActiveConsumer(node); } @@ -356,11 +370,17 @@ export function consumerBeforeComputation(node: ReactiveNode|null): ReactiveNode * have finished. */ export function consumerAfterComputation( - node: ReactiveNode|null, prevConsumer: ReactiveNode|null): void { + node: ReactiveNode | null, + prevConsumer: ReactiveNode | null, +): void { setActiveConsumer(prevConsumer); - if (!node || node.producerNode === undefined || node.producerIndexOfThis === undefined || - node.producerLastReadVersion === undefined) { + if ( + !node || + node.producerNode === undefined || + node.producerIndexOfThis === undefined || + node.producerLastReadVersion === undefined + ) { return; } @@ -368,7 +388,10 @@ export function consumerAfterComputation( // For live consumers, we need to remove the producer -> consumer edge for any stale producers // which weren't dependencies after the recomputation. for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); + producerRemoveLiveConsumerAtIndex( + node.producerNode[i], + node.producerIndexOfThis[i], + ); } } @@ -422,12 +445,17 @@ export function consumerDestroy(node: ReactiveNode): void { if (consumerIsLive(node)) { // Drop all connections from the graph to this node. for (let i = 0; i < node.producerNode.length; i++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); + producerRemoveLiveConsumerAtIndex( + node.producerNode[i], + node.producerIndexOfThis[i], + ); } } // Truncate all the arrays to drop all connection from this node to the graph. - node.producerNode.length = node.producerLastReadVersion.length = node.producerIndexOfThis.length = + node.producerNode.length = + node.producerLastReadVersion.length = + node.producerIndexOfThis.length = 0; if (node.liveConsumerNode) { node.liveConsumerNode.length = node.liveConsumerIndexOfThis!.length = 0; @@ -441,14 +469,21 @@ export function consumerDestroy(node: ReactiveNode): void { * a live consumer of all of its current producers. */ function producerAddLiveConsumer( - node: ReactiveNode, consumer: ReactiveNode, indexOfThis: number): number { + node: ReactiveNode, + consumer: ReactiveNode, + indexOfThis: number, +): number { assertProducerNode(node); assertConsumerNode(node); if (node.liveConsumerNode.length === 0) { node.watched?.call(node.wrapper); // When going from 0 to 1 live consumers, we become a live consumer to our producers. for (let i = 0; i < node.producerNode.length; i++) { - node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i); + node.producerIndexOfThis[i] = producerAddLiveConsumer( + node.producerNode[i], + node, + i, + ); } } node.liveConsumerIndexOfThis.push(indexOfThis); @@ -458,13 +493,21 @@ function producerAddLiveConsumer( /** * Remove the live consumer at `idx`. */ -export function producerRemoveLiveConsumerAtIndex(node: ReactiveNode, idx: number): void { +export function producerRemoveLiveConsumerAtIndex( + node: ReactiveNode, + idx: number, +): void { assertProducerNode(node); assertConsumerNode(node); - if (typeof ngDevMode !== 'undefined' && ngDevMode && idx >= node.liveConsumerNode.length) { - throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${ - node.liveConsumerNode.length} consumers)`); + if ( + typeof ngDevMode !== 'undefined' && + ngDevMode && + idx >= node.liveConsumerNode.length + ) { + throw new Error( + `Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`, + ); } if (node.liveConsumerNode.length === 1) { @@ -473,7 +516,10 @@ export function producerRemoveLiveConsumerAtIndex(node: ReactiveNode, idx: numbe // liveness as well). node.unwatched?.call(node.wrapper); for (let i = 0; i < node.producerNode.length; i++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); + producerRemoveLiveConsumerAtIndex( + node.producerNode[i], + node.producerIndexOfThis[i], + ); } } @@ -501,14 +547,17 @@ function consumerIsLive(node: ReactiveNode): boolean { return node.consumerIsAlwaysLive || (node?.liveConsumerNode?.length ?? 0) > 0; } - -export function assertConsumerNode(node: ReactiveNode): asserts node is ConsumerNode { +export function assertConsumerNode( + node: ReactiveNode, +): asserts node is ConsumerNode { node.producerNode ??= []; node.producerIndexOfThis ??= []; node.producerLastReadVersion ??= []; } -export function assertProducerNode(node: ReactiveNode): asserts node is ProducerNode { +export function assertProducerNode( + node: ReactiveNode, +): asserts node is ProducerNode { node.liveConsumerNode ??= []; node.liveConsumerIndexOfThis ??= []; -} \ No newline at end of file +} diff --git a/packages/signal-polyfill/src/index.ts b/packages/signal-polyfill/src/index.ts index 72b9910..962b1f5 100644 --- a/packages/signal-polyfill/src/index.ts +++ b/packages/signal-polyfill/src/index.ts @@ -1 +1 @@ -export { Signal } from "./wrapper.js"; \ No newline at end of file +export { Signal } from './wrapper.js'; diff --git a/packages/signal-polyfill/src/signal.ts b/packages/signal-polyfill/src/signal.ts index 2bdfba3..3fb4305 100644 --- a/packages/signal-polyfill/src/signal.ts +++ b/packages/signal-polyfill/src/signal.ts @@ -6,13 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import {defaultEquals, ValueEqualityFn} from './equality.js'; -import {throwInvalidWriteToSignalError} from './errors.js'; -import {producerAccessed, producerIncrementEpoch, producerNotifyConsumers, producerUpdatesAllowed, REACTIVE_NODE, ReactiveNode, SIGNAL} from './graph.js'; +import { defaultEquals, ValueEqualityFn } from './equality.js'; +import { throwInvalidWriteToSignalError } from './errors.js'; +import { + producerAccessed, + producerIncrementEpoch, + producerNotifyConsumers, + producerUpdatesAllowed, + REACTIVE_NODE, + ReactiveNode, + SIGNAL, +} from './graph.js'; // Required as the signals library is in a separate package, so we need to explicitly ensure the // global `ngDevMode` type is defined. -declare const ngDevMode: boolean|undefined; +declare const ngDevMode: boolean | undefined; /** * If set, called after `WritableSignal`s are updated. @@ -20,19 +28,19 @@ declare const ngDevMode: boolean|undefined; * This hook can be used to achieve various effects, such as running effects synchronously as part * of setting a signal. */ -let postSignalSetFn: (() => void)|null = null; +let postSignalSetFn: (() => void) | null = null; export interface SignalNode extends ReactiveNode { value: T; equal: ValueEqualityFn; } -export type SignalBaseGetter = (() => T)&{readonly[SIGNAL]: unknown}; +export type SignalBaseGetter = (() => T) & { readonly [SIGNAL]: unknown }; // Note: Closure *requires* this to be an `interface` and not a type, which is why the // `SignalBaseGetter` type exists to provide the correct shape. export interface SignalGetter extends SignalBaseGetter { - readonly[SIGNAL]: SignalNode; + readonly [SIGNAL]: SignalNode; } /** @@ -42,14 +50,16 @@ export function createSignal(initialValue: T): SignalGetter { const node: SignalNode = Object.create(SIGNAL_NODE); node.value = initialValue; const getter = (() => { - producerAccessed(node); - return node.value; - }) as SignalGetter; + producerAccessed(node); + return node.value; + }) as SignalGetter; (getter as any)[SIGNAL] = node; return getter; } -export function setPostSignalSetFn(fn: (() => void)|null): (() => void)|null { +export function setPostSignalSetFn( + fn: (() => void) | null, +): (() => void) | null { const prev = postSignalSetFn; postSignalSetFn = fn; return prev; @@ -71,7 +81,10 @@ export function signalSetFn(node: SignalNode, newValue: T) { } } -export function signalUpdateFn(node: SignalNode, updater: (value: T) => T): void { +export function signalUpdateFn( + node: SignalNode, + updater: (value: T) => T, +): void { if (!producerUpdatesAllowed()) { throwInvalidWriteToSignalError(); } @@ -95,4 +108,4 @@ function signalValueChanged(node: SignalNode): void { producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); -} \ No newline at end of file +} diff --git a/packages/signal-polyfill/src/wrapper.spec.ts b/packages/signal-polyfill/src/wrapper.spec.ts index 9e76fcf..fce75d1 100644 --- a/packages/signal-polyfill/src/wrapper.spec.ts +++ b/packages/signal-polyfill/src/wrapper.spec.ts @@ -15,11 +15,11 @@ */ /* eslint-disable @typescript-eslint/no-this-alias */ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { Signal } from './wrapper.js'; -describe("Signal.State", () => { - it("should work", () => { +describe('Signal.State', () => { + it('should work', () => { const stateSignal = new Signal.State(0); expect(stateSignal.get()).toEqual(0); @@ -29,8 +29,8 @@ describe("Signal.State", () => { }); }); -describe("Computed", () => { - it("should work", () => { +describe('Computed', () => { + it('should work', () => { const stateSignal = new Signal.State(1); const computedSignal = new Signal.Computed(() => { @@ -47,7 +47,7 @@ describe("Computed", () => { }); }); -describe("Watcher", () => { +describe('Watcher', () => { type Destructor = () => void; const notifySpy = vi.fn(); @@ -75,7 +75,7 @@ describe("Watcher", () => { afterEach(() => watcher.unwatch(...Signal.subtle.introspectSources(watcher))); - it("should work", () => { + it('should work', () => { const watchedSpy = vi.fn(); const unwatchedSpy = vi.fn(); const stateSignal = new Signal.State(1, { @@ -193,23 +193,22 @@ describe("Watcher", () => { stateSignal.set(300); flushPending(); - }); }); -describe("Expected class shape", () => { - it("should be on the prototype", () => { - expect(typeof Signal.State.prototype.get).toBe("function"); - expect(typeof Signal.State.prototype.set).toBe("function"); - expect(typeof Signal.Computed.prototype.get).toBe("function"); - expect(typeof Signal.subtle.Watcher.prototype.watch).toBe("function"); - expect(typeof Signal.subtle.Watcher.prototype.unwatch).toBe("function"); - expect(typeof Signal.subtle.Watcher.prototype.getPending).toBe("function"); +describe('Expected class shape', () => { + it('should be on the prototype', () => { + expect(typeof Signal.State.prototype.get).toBe('function'); + expect(typeof Signal.State.prototype.set).toBe('function'); + expect(typeof Signal.Computed.prototype.get).toBe('function'); + expect(typeof Signal.subtle.Watcher.prototype.watch).toBe('function'); + expect(typeof Signal.subtle.Watcher.prototype.unwatch).toBe('function'); + expect(typeof Signal.subtle.Watcher.prototype.getPending).toBe('function'); }); }); -describe("Comparison semantics", () => { - it("should cache State by Object.is", () => { +describe('Comparison semantics', () => { + it('should cache State by Object.is', () => { const state = new Signal.State(NaN); let calls = 0; const computed = new Signal.Computed(() => { @@ -224,7 +223,7 @@ describe("Comparison semantics", () => { expect(calls).toBe(1); }); - it("should track Computed by Object.is", () => { + it('should track Computed by Object.is', () => { const state = new Signal.State(1); let value = 5; let calls = 0; @@ -248,7 +247,7 @@ describe("Comparison semantics", () => { expect(calls).toBe(2); }); - it("applies custom equality in State", () => { + it('applies custom equality in State', () => { let ecalls = 0; const state = new Signal.State(1, { equals() { @@ -275,7 +274,7 @@ describe("Comparison semantics", () => { expect(calls).toBe(2); }); - it("applies custom equality in Computed", () => { + it('applies custom equality in Computed', () => { const s = new Signal.State(5); let ecalls = 0; const c1 = new Signal.Computed(() => (s.get(), 1), { @@ -304,8 +303,8 @@ describe("Comparison semantics", () => { }); }); -describe("Untrack", () => { - it("works", () => { +describe('Untrack', () => { + it('works', () => { const state = new Signal.State(1); const computed = new Signal.Computed(() => Signal.subtle.untrack(() => state.get()), @@ -314,7 +313,7 @@ describe("Untrack", () => { state.set(2); expect(computed.get()).toBe(1); }); - it("works differently without untrack", () => { + it('works differently without untrack', () => { const state = new Signal.State(1); const computed = new Signal.Computed(() => state.get()); expect(computed.get()).toBe(1); @@ -323,8 +322,8 @@ describe("Untrack", () => { }); }); -describe("liveness", () => { - it("only changes on first and last descendant", () => { +describe('liveness', () => { + it('only changes on first and last descendant', () => { const watchedSpy = vi.fn(); const unwatchedSpy = vi.fn(); const state = new Signal.State(1, { @@ -356,7 +355,7 @@ describe("liveness", () => { expect(unwatchedSpy).toBeCalledTimes(1); }); - it("is tracked well on computed signals", () => { + it('is tracked well on computed signals', () => { const watchedSpy = vi.fn(); const unwatchedSpy = vi.fn(); const s = new Signal.State(1); @@ -380,9 +379,9 @@ describe("liveness", () => { }); }); -describe("Errors", () => { - it("are cached by computed signals", () => { - const s = new Signal.State("first"); +describe('Errors', () => { + it('are cached by computed signals', () => { + const s = new Signal.State('first'); let n = 0; const c = new Signal.Computed(() => { n++; @@ -394,26 +393,26 @@ describe("Errors", () => { return c.get(); }); expect(n).toBe(0); - expect(() => c.get()).toThrowError("first"); - expect(() => c2.get()).toThrowError("first"); + expect(() => c.get()).toThrowError('first'); + expect(() => c2.get()).toThrowError('first'); expect(n).toBe(1); expect(n2).toBe(1); - expect(() => c.get()).toThrowError("first"); - expect(() => c2.get()).toThrowError("first"); + expect(() => c.get()).toThrowError('first'); + expect(() => c2.get()).toThrowError('first'); expect(n).toBe(1); expect(n2).toBe(1); - s.set("second"); - expect(() => c.get()).toThrowError("second"); - expect(() => c2.get()).toThrowError("second"); + s.set('second'); + expect(() => c.get()).toThrowError('second'); + expect(() => c2.get()).toThrowError('second'); expect(n).toBe(2); expect(n2).toBe(2); // Doesn't retrigger on setting state to the same value - s.set("second"); + s.set('second'); expect(n).toBe(2); }); - it("are cached by computed signals when watched", () => { - const s = new Signal.State("first"); + it('are cached by computed signals when watched', () => { + const s = new Signal.State('first'); let n = 0; const c = new Signal.Computed(() => { n++; @@ -423,41 +422,43 @@ describe("Errors", () => { w.watch(c); expect(n).toBe(0); - expect(() => c.get()).toThrowError("first"); + expect(() => c.get()).toThrowError('first'); expect(n).toBe(1); - expect(() => c.get()).toThrowError("first"); + expect(() => c.get()).toThrowError('first'); expect(n).toBe(1); - s.set("second"); - expect(() => c.get()).toThrowError("second"); + s.set('second'); + expect(() => c.get()).toThrowError('second'); expect(n).toBe(2); - s.set("second"); + s.set('second'); expect(n).toBe(2); }); - it("are cached by computed signals when equals throws", () => { + it('are cached by computed signals when equals throws', () => { const s = new Signal.State(0); const cSpy = vi.fn(() => s.get()); const c = new Signal.Computed(cSpy, { - equals() { throw new Error("equals"); }, + equals() { + throw new Error('equals'); + }, }); c.get(); s.set(1); // Error is cached; c throws again without needing to rerun. - expect(() => c.get()).toThrowError("equals"); + expect(() => c.get()).toThrowError('equals'); expect(cSpy).toBeCalledTimes(2); - expect(() => c.get()).toThrowError("equals"); + expect(() => c.get()).toThrowError('equals'); expect(cSpy).toBeCalledTimes(2); - }) + }); }); -describe("Cycles", () => { - it("detects trivial cycles", () => { +describe('Cycles', () => { + it('detects trivial cycles', () => { const c = new Signal.Computed(() => c.get()); expect(() => c.get()).toThrow(); }); - it("detects slightly larger cycles", () => { + it('detects slightly larger cycles', () => { const c = new Signal.Computed(() => c2.get()); const c2 = new Signal.Computed(() => c.get()); const c3 = new Signal.Computed(() => c2.get()); @@ -465,8 +466,8 @@ describe("Cycles", () => { }); }); -describe("Pruning", () => { - it("only recalculates until things are equal", () => { +describe('Pruning', () => { + it('only recalculates until things are equal', () => { const s = new Signal.State(0); let n = 0; const c = new Signal.Computed(() => (n++, s.get())); @@ -494,7 +495,7 @@ describe("Pruning", () => { expect(n2).toBe(2); expect(n3).toBe(1); }); - it("does similar pruning for live signals", () => { + it('does similar pruning for live signals', () => { const s = new Signal.State(0); let n = 0; const c = new Signal.Computed(() => (n++, s.get())); @@ -530,8 +531,8 @@ describe("Pruning", () => { }); }); -describe("Prohibited contexts", () => { - it("allows writes during computed", () => { +describe('Prohibited contexts', () => { + it('allows writes during computed', () => { const s = new Signal.State(1); const c = new Signal.Computed(() => (s.set(s.get() + 1), s.get())); expect(c.get()).toBe(2); @@ -547,7 +548,7 @@ describe("Prohibited contexts", () => { expect(c.get()).toBe(4); expect(s.get()).toBe(4); }); - it("disallows reads and writes during watcher notify", () => { + it('disallows reads and writes during watcher notify', () => { const s = new Signal.State(1); const w = new Signal.subtle.Watcher(() => { s.get(); @@ -567,8 +568,8 @@ describe("Prohibited contexts", () => { }); }); -describe("Custom equality", () => { - it("works for State", () => { +describe('Custom equality', () => { + it('works for State', () => { let answer = true; const s = new Signal.State(1, { equals() { @@ -597,7 +598,7 @@ describe("Custom equality", () => { expect(c.get()).toBe(2); expect(n).toBe(3); }); - it("works for Computed", () => { + it('works for Computed', () => { let answer = true; let value = 1; const u = new Signal.State(1); @@ -629,7 +630,7 @@ describe("Custom equality", () => { expect(c.get()).toBe(2); expect(n).toBe(3); }); - it("does not leak tracking information", () => { + it('does not leak tracking information', () => { const exact = new Signal.State(1); const epsilon = new Signal.State(0.1); const counter = new Signal.State(1); @@ -637,7 +638,7 @@ describe("Custom equality", () => { const cutoff = vi.fn((a, b) => Math.abs(a - b) < epsilon.get()); const innerFn = vi.fn(() => exact.get()); const inner = new Signal.Computed(innerFn, { - equals: cutoff + equals: cutoff, }); const outerFn = vi.fn(() => { @@ -654,7 +655,7 @@ describe("Custom equality", () => { exact.set(2); counter.set(2); - outer.get() + outer.get(); // `outer` reruns because `counter` changed, `inner` reruns when called by // `outer`, and `cutoff` is called for the first time. @@ -673,8 +674,8 @@ describe("Custom equality", () => { }); }); -describe("Receivers", () => { - it("is this for computed", () => { +describe('Receivers', () => { + it('is this for computed', () => { let receiver; const c = new Signal.Computed(function () { receiver = this; @@ -682,7 +683,7 @@ describe("Receivers", () => { expect(c.get()).toBe(undefined); expect(receiver).toBe(c); }); - it("is this for watched/unwatched", () => { + it('is this for watched/unwatched', () => { let r1, r2; const s = new Signal.State(1, { [Signal.subtle.watched]() { @@ -701,7 +702,7 @@ describe("Receivers", () => { w.unwatch(s); expect(r2).toBe(s); }); - it("is this for equals", () => { + it('is this for equals', () => { let receiver; const options = { equals() { @@ -721,12 +722,12 @@ describe("Receivers", () => { }); }); -describe("Dynamic dependencies", () => { +describe('Dynamic dependencies', () => { function run(live) { - const states = Array.from("abcdefgh").map((s) => new Signal.State(s)); + const states = Array.from('abcdefgh').map((s) => new Signal.State(s)); const sources = new Signal.State(states); const computed = new Signal.Computed(() => { - let str = ""; + let str = ''; for (const state of sources.get()) str += state.get(); return str; }); @@ -734,29 +735,29 @@ describe("Dynamic dependencies", () => { const w = new Signal.subtle.Watcher(() => {}); w.watch(computed); } - expect(computed.get()).toBe("abcdefgh"); + expect(computed.get()).toBe('abcdefgh'); expect(Signal.subtle.introspectSources(computed).slice(1)).toStrictEqual( states, ); sources.set(states.slice(0, 5)); - expect(computed.get()).toBe("abcde"); + expect(computed.get()).toBe('abcde'); expect(Signal.subtle.introspectSources(computed).slice(1)).toStrictEqual( states.slice(0, 5), ); sources.set(states.slice(3)); - expect(computed.get()).toBe("defgh"); + expect(computed.get()).toBe('defgh'); expect(Signal.subtle.introspectSources(computed).slice(1)).toStrictEqual( states.slice(3), ); } - it("works live", () => run(true)); - it("works not live", () => run(false)); + it('works live', () => run(true)); + it('works not live', () => run(false)); }); -describe("watch and unwatch", () => { - it("handles multiple watchers well", () => { +describe('watch and unwatch', () => { + it('handles multiple watchers well', () => { const s = new Signal.State(1); const s2 = new Signal.State(2); let n = 0; @@ -783,7 +784,7 @@ describe("watch and unwatch", () => { s.set(2); expect(n).toBe(3); }); - it("understands dynamic dependency sets", () => { + it('understands dynamic dependency sets', () => { let w1 = 0, u1 = 0, w2 = 0, @@ -916,8 +917,8 @@ describe("watch and unwatch", () => { }); }); -describe("type checks", () => { - it("checks types in methods", () => { +describe('type checks', () => { + it('checks types in methods', () => { let x = {}; let s = new Signal.State(1); let c = new Signal.Computed(() => {}); @@ -1002,8 +1003,8 @@ describe("type checks", () => { }); }); -describe("currentComputed", () => { - it("works", () => { +describe('currentComputed', () => { + it('works', () => { expect(Signal.subtle.currentComputed()).toBe(undefined); let context; let c = new Signal.Computed( diff --git a/packages/signal-polyfill/src/wrapper.ts b/packages/signal-polyfill/src/wrapper.ts index a8eab3d..e7aed89 100644 --- a/packages/signal-polyfill/src/wrapper.ts +++ b/packages/signal-polyfill/src/wrapper.ts @@ -15,298 +15,299 @@ * limitations under the License. */ +import { computedGet, createComputed, type ComputedNode } from './computed.js'; import { - computedGet, - createComputed, - type ComputedNode, -} from "./computed.js"; + SIGNAL, + getActiveConsumer, + isInNotificationPhase, + producerAccessed, + assertConsumerNode, + setActiveConsumer, + REACTIVE_NODE, + type ReactiveNode, + assertProducerNode, + producerRemoveLiveConsumerAtIndex, +} from './graph.js'; import { -SIGNAL, -getActiveConsumer, -isInNotificationPhase, -producerAccessed, -assertConsumerNode, -setActiveConsumer, -REACTIVE_NODE, -type ReactiveNode, -assertProducerNode, -producerRemoveLiveConsumerAtIndex, -} from "./graph.js"; -import { -createSignal, -signalGetFn, -signalSetFn, -type SignalNode, -} from "./signal.js"; + createSignal, + signalGetFn, + signalSetFn, + type SignalNode, +} from './signal.js'; -const NODE: unique symbol = Symbol("node"); +const NODE: unique symbol = Symbol('node'); let isState: (s: any) => boolean, - isComputed: (s: any) => boolean, - isWatcher: (s: any) => boolean; + isComputed: (s: any) => boolean, + isWatcher: (s: any) => boolean; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Signal { -// A read-write Signal -export class State { - readonly [NODE]: SignalNode; - #brand() {} + // A read-write Signal + export class State { + readonly [NODE]: SignalNode; + #brand() {} - static { - isState = s => #brand in s; - } + static { + isState = (s) => #brand in s; + } - constructor(initialValue: T, options: Signal.Options = {}) { - const ref = createSignal(initialValue); - const node: SignalNode = ref[SIGNAL]; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) { - node.equal = equals; + constructor(initialValue: T, options: Signal.Options = {}) { + const ref = createSignal(initialValue); + const node: SignalNode = ref[SIGNAL]; + this[NODE] = node; + node.wrapper = this; + if (options) { + const equals = options.equals; + if (equals) { + node.equal = equals; + } + node.watched = options[Signal.subtle.watched]; + node.unwatched = options[Signal.subtle.unwatched]; } - node.watched = options[Signal.subtle.watched]; - node.unwatched = options[Signal.subtle.unwatched]; } - } - - public get(): T { - if (!isState(this)) - throw new TypeError( - "Wrong receiver type for Signal.State.prototype.get", - ); - return (signalGetFn).call(this[NODE]); - } - public set(newValue: T): void { - if (!isState(this)) - throw new TypeError( - "Wrong receiver type for Signal.State.prototype.set", - ); - if (isInNotificationPhase()) { - throw new Error( - "Writes to signals not permitted during Watcher callback", - ); + public get(): T { + if (!isState(this)) + throw new TypeError( + 'Wrong receiver type for Signal.State.prototype.get', + ); + return (signalGetFn).call(this[NODE]); } - const ref = this[NODE]; - signalSetFn(ref, newValue); - } -} - -// A Signal which is a formula based on other Signals -export class Computed { - readonly [NODE]: ComputedNode; - - #brand() {} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static { - isComputed = (c: any) => #brand in c; - } - // Create a Signal which evaluates to the value returned by the callback. - // Callback is called with this signal as the parameter. - constructor(computation: () => T, options?: Signal.Options) { - const ref = createComputed(computation); - const node = ref[SIGNAL]; - node.consumerAllowSignalWrites = true; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) { - node.equal = equals; + public set(newValue: T): void { + if (!isState(this)) + throw new TypeError( + 'Wrong receiver type for Signal.State.prototype.set', + ); + if (isInNotificationPhase()) { + throw new Error( + 'Writes to signals not permitted during Watcher callback', + ); } - node.watched = options[Signal.subtle.watched]; - node.unwatched = options[Signal.subtle.unwatched]; + const ref = this[NODE]; + signalSetFn(ref, newValue); } } - get(): T { - if (!isComputed(this)) - throw new TypeError( - "Wrong receiver type for Signal.Computed.prototype.get", - ); - return computedGet(this[NODE]); - } -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnySignal = State | Computed; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnySink = Computed | subtle.Watcher; + // A Signal which is a formula based on other Signals + export class Computed { + readonly [NODE]: ComputedNode; -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace subtle { - // Run a callback with all tracking disabled (even for nested computed). - export function untrack(cb: () => T): T { - let output: T; - let prevActiveConsumer = null; - try { - prevActiveConsumer = setActiveConsumer(null); - output = cb(); - } finally { - setActiveConsumer(prevActiveConsumer); + #brand() {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static { + isComputed = (c: any) => #brand in c; } - return output; - } - // Returns ordered list of all signals which this one referenced - // during the last time it was evaluated - export function introspectSources(sink: AnySink): AnySignal[] { - if (!isComputed(sink) && !isWatcher(sink)) { - throw new TypeError( - "Called introspectSources without a Computed or Watcher argument", - ); + // Create a Signal which evaluates to the value returned by the callback. + // Callback is called with this signal as the parameter. + constructor(computation: () => T, options?: Signal.Options) { + const ref = createComputed(computation); + const node = ref[SIGNAL]; + node.consumerAllowSignalWrites = true; + this[NODE] = node; + node.wrapper = this; + if (options) { + const equals = options.equals; + if (equals) { + node.equal = equals; + } + node.watched = options[Signal.subtle.watched]; + node.unwatched = options[Signal.subtle.unwatched]; + } } - return sink[NODE].producerNode?.map((n) => n.wrapper) ?? []; - } - // Returns the subset of signal sinks which recursively - // lead to an Effect which has not been disposed - // Note: Only watched Computed signals will be in this list. - export function introspectSinks(signal: AnySignal): AnySink[] { - if (!isComputed(signal) && !isState(signal)) { - throw new TypeError("Called introspectSinks without a Signal argument"); + get(): T { + if (!isComputed(this)) + throw new TypeError( + 'Wrong receiver type for Signal.Computed.prototype.get', + ); + return computedGet(this[NODE]); } - return signal[NODE].liveConsumerNode?.map((n) => n.wrapper) ?? []; } - // True iff introspectSinks() is non-empty - export function hasSinks(signal: AnySignal): boolean { - if (!isComputed(signal) && !isState(signal)) { - throw new TypeError("Called hasSinks without a Signal argument"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type AnySignal = State | Computed; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type AnySink = Computed | subtle.Watcher; + + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace subtle { + // Run a callback with all tracking disabled (even for nested computed). + export function untrack(cb: () => T): T { + let output: T; + let prevActiveConsumer = null; + try { + prevActiveConsumer = setActiveConsumer(null); + output = cb(); + } finally { + setActiveConsumer(prevActiveConsumer); + } + return output; } - const liveConsumerNode = signal[NODE].liveConsumerNode; - if (!liveConsumerNode) return false; - return liveConsumerNode.length > 0; - } - // True iff introspectSources() is non-empty - export function hasSources(signal: AnySink): boolean { - if (!isComputed(signal) && !isWatcher(signal)) { - throw new TypeError("Called hasSources without a Computed or Watcher argument"); + // Returns ordered list of all signals which this one referenced + // during the last time it was evaluated + export function introspectSources(sink: AnySink): AnySignal[] { + if (!isComputed(sink) && !isWatcher(sink)) { + throw new TypeError( + 'Called introspectSources without a Computed or Watcher argument', + ); + } + return sink[NODE].producerNode?.map((n) => n.wrapper) ?? []; } - const producerNode = signal[NODE].producerNode; - if (!producerNode) return false; - return producerNode.length > 0; - } - - export class Watcher { - readonly [NODE]: ReactiveNode; - #brand() {} - static { - isWatcher = (w: any): w is Watcher => #brand in w; + // Returns the subset of signal sinks which recursively + // lead to an Effect which has not been disposed + // Note: Only watched Computed signals will be in this list. + export function introspectSinks(signal: AnySignal): AnySink[] { + if (!isComputed(signal) && !isState(signal)) { + throw new TypeError('Called introspectSinks without a Signal argument'); + } + return signal[NODE].liveConsumerNode?.map((n) => n.wrapper) ?? []; } - // When a (recursive) source of Watcher is written to, call this callback, - // if it hasn't already been called since the last `watch` call. - // No signals may be read or written during the notify. - constructor(notify: (this: Watcher) => void) { - let node = Object.create(REACTIVE_NODE); - node.wrapper = this; - node.consumerMarkedDirty = notify; - node.consumerIsAlwaysLive = true; - node.consumerAllowSignalWrites = false; - node.producerNode = []; - this[NODE] = node; + // True iff introspectSinks() is non-empty + export function hasSinks(signal: AnySignal): boolean { + if (!isComputed(signal) && !isState(signal)) { + throw new TypeError('Called hasSinks without a Signal argument'); + } + const liveConsumerNode = signal[NODE].liveConsumerNode; + if (!liveConsumerNode) return false; + return liveConsumerNode.length > 0; } - #assertSignals(signals: AnySignal[]): void { - for (const signal of signals) { - if (!isComputed(signal) && !isState(signal)) { - throw new TypeError( - "Called watch/unwatch without a Computed or State argument", - ); - } + // True iff introspectSources() is non-empty + export function hasSources(signal: AnySink): boolean { + if (!isComputed(signal) && !isWatcher(signal)) { + throw new TypeError( + 'Called hasSources without a Computed or Watcher argument', + ); } + const producerNode = signal[NODE].producerNode; + if (!producerNode) return false; + return producerNode.length > 0; } - // Add these signals to the Watcher's set, and set the watcher to run its - // notify callback next time any signal in the set (or one of its dependencies) changes. - // Can be called with no arguments just to reset the "notified" state, so that - // the notify callback will be invoked again. - watch(...signals: AnySignal[]): void { - if (!isWatcher(this)) { - throw new TypeError("Called unwatch without Watcher receiver"); + export class Watcher { + readonly [NODE]: ReactiveNode; + + #brand() {} + static { + isWatcher = (w: any): w is Watcher => #brand in w; } - this.#assertSignals(signals); - const node = this[NODE]; - node.dirty = false; // Give the watcher a chance to trigger again - const prev = setActiveConsumer(node); - for (const signal of signals) { - producerAccessed(signal[NODE]); + // When a (recursive) source of Watcher is written to, call this callback, + // if it hasn't already been called since the last `watch` call. + // No signals may be read or written during the notify. + constructor(notify: (this: Watcher) => void) { + let node = Object.create(REACTIVE_NODE); + node.wrapper = this; + node.consumerMarkedDirty = notify; + node.consumerIsAlwaysLive = true; + node.consumerAllowSignalWrites = false; + node.producerNode = []; + this[NODE] = node; } - setActiveConsumer(prev); - } - // Remove these signals from the watched set (e.g., for an effect which is disposed) - unwatch(...signals: AnySignal[]): void { - if (!isWatcher(this)) { - throw new TypeError("Called unwatch without Watcher receiver"); + #assertSignals(signals: AnySignal[]): void { + for (const signal of signals) { + if (!isComputed(signal) && !isState(signal)) { + throw new TypeError( + 'Called watch/unwatch without a Computed or State argument', + ); + } + } + } + + // Add these signals to the Watcher's set, and set the watcher to run its + // notify callback next time any signal in the set (or one of its dependencies) changes. + // Can be called with no arguments just to reset the "notified" state, so that + // the notify callback will be invoked again. + watch(...signals: AnySignal[]): void { + if (!isWatcher(this)) { + throw new TypeError('Called unwatch without Watcher receiver'); + } + this.#assertSignals(signals); + + const node = this[NODE]; + node.dirty = false; // Give the watcher a chance to trigger again + const prev = setActiveConsumer(node); + for (const signal of signals) { + producerAccessed(signal[NODE]); + } + setActiveConsumer(prev); } - this.#assertSignals(signals); - const node = this[NODE]; - assertConsumerNode(node); + // Remove these signals from the watched set (e.g., for an effect which is disposed) + unwatch(...signals: AnySignal[]): void { + if (!isWatcher(this)) { + throw new TypeError('Called unwatch without Watcher receiver'); + } + this.#assertSignals(signals); + + const node = this[NODE]; + assertConsumerNode(node); - let indicesToShift = []; - for (let i = 0; i < node.producerNode.length; i++) { + let indicesToShift = []; + for (let i = 0; i < node.producerNode.length; i++) { if (signals.includes(node.producerNode[i].wrapper)) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); + producerRemoveLiveConsumerAtIndex( + node.producerNode[i], + node.producerIndexOfThis[i], + ); indicesToShift.push(i); } + } + for (const idx of indicesToShift) { + // Logic copied from producerRemoveLiveConsumerAtIndex, but reversed + const lastIdx = node.producerNode!.length - 1; + node.producerNode![idx] = node.producerNode![lastIdx]; + node.producerIndexOfThis[idx] = node.producerIndexOfThis[lastIdx]; + + node.producerNode.length--; + node.producerIndexOfThis.length--; + node.nextProducerIndex--; + + if (idx < node.producerNode.length) { + const idxConsumer = node.producerIndexOfThis[idx]; + const producer = node.producerNode[idx]; + assertProducerNode(producer); + producer.liveConsumerIndexOfThis[idxConsumer] = idx; + } + } } - for (const idx of indicesToShift) { - // Logic copied from producerRemoveLiveConsumerAtIndex, but reversed - const lastIdx = node.producerNode!.length - 1; - node.producerNode![idx] = node.producerNode![lastIdx]; - node.producerIndexOfThis[idx] = node.producerIndexOfThis[lastIdx]; - - node.producerNode.length--; - node.producerIndexOfThis.length--; - node.nextProducerIndex--; - - if (idx < node.producerNode.length) { - const idxConsumer = node.producerIndexOfThis[idx]; - const producer = node.producerNode[idx]; - assertProducerNode(producer); - producer.liveConsumerIndexOfThis[idxConsumer] = idx; + + // Returns the set of computeds in the Watcher's set which are still yet + // to be re-evaluated + getPending(): Computed[] { + if (!isWatcher(this)) { + throw new TypeError('Called getPending without Watcher receiver'); } + const node = this[NODE]; + return node.producerNode!.filter((n) => n.dirty).map((n) => n.wrapper); } } - // Returns the set of computeds in the Watcher's set which are still yet - // to be re-evaluated - getPending(): Computed[] { - if (!isWatcher(this)) { - throw new TypeError("Called getPending without Watcher receiver"); - } - const node = this[NODE]; - return node.producerNode!.filter((n) => n.dirty).map((n) => n.wrapper); + export function currentComputed(): Computed | undefined { + return getActiveConsumer()?.wrapper; } - } - export function currentComputed(): Computed | undefined { - return getActiveConsumer()?.wrapper; + // Hooks to observe being watched or no longer watched + export const watched = Symbol('watched'); + export const unwatched = Symbol('unwatched'); } - // Hooks to observe being watched or no longer watched - export const watched = Symbol("watched"); - export const unwatched = Symbol("unwatched"); -} - -export interface Options { - // Custom comparison function between old and new value. Default: Object.is. - // The signal is passed in as an optionally-used third parameter for context. - equals?: (this: AnySignal, t: T, t2: T) => boolean; + export interface Options { + // Custom comparison function between old and new value. Default: Object.is. + // The signal is passed in as an optionally-used third parameter for context. + equals?: (this: AnySignal, t: T, t2: T) => boolean; - // Callback called when hasSinks becomes true, if it was previously false - [Signal.subtle.watched]?: (this: AnySignal) => void; + // Callback called when hasSinks becomes true, if it was previously false + [Signal.subtle.watched]?: (this: AnySignal) => void; - // Callback called whenever hasSinks becomes false, if it was previously true - [Signal.subtle.unwatched]?: (this: AnySignal) => void; -} + // Callback called whenever hasSinks becomes false, if it was previously true + [Signal.subtle.unwatched]?: (this: AnySignal) => void; + } } diff --git a/packages/signal-polyfill/tsconfig.json b/packages/signal-polyfill/tsconfig.json index f223b6e..4a66bd8 100644 --- a/packages/signal-polyfill/tsconfig.json +++ b/packages/signal-polyfill/tsconfig.json @@ -10,20 +10,11 @@ "declaration": true, "declarationMap": true, "noEmitOnError": false, - "lib": [ - "DOM", - "ES2021" - ], + "lib": ["DOM", "ES2021"], "strict": true, "composite": true, "forceConsistentCasingInFileNames": true }, - "exclude": [ - "**/node_modules/**", - "**/*.spec.ts", - "**/dist/**/*" - ], - "include": [ - "src" - ] -} \ No newline at end of file + "exclude": ["**/node_modules/**", "**/*.spec.ts", "**/dist/**/*"], + "include": ["src"] +} diff --git a/packages/signal-polyfill/vite.config.ts b/packages/signal-polyfill/vite.config.ts index 1fcdc1c..fc32df4 100644 --- a/packages/signal-polyfill/vite.config.ts +++ b/packages/signal-polyfill/vite.config.ts @@ -1,7 +1,7 @@ -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vite"; -import dts from "vite-plugin-dts"; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; const entry = join(dirname(fileURLToPath(import.meta.url)), './src/index.ts'); @@ -10,8 +10,8 @@ export default defineConfig({ build: { lib: { entry, - formats: ["es"], - fileName: "index" - } - } -}); \ No newline at end of file + formats: ['es'], + fileName: 'index', + }, + }, +}); From cf70505998c99d81acf17f3c9473f32b65bcf0b7 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:53:14 -0400 Subject: [PATCH 2/2] Update workflow --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e2b55d..5c3eaad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,5 +21,7 @@ jobs: - uses: actions/setup-node@v4 - run: npm install - run: npm test - - run: npm run format:check + - run: | + npm install + npm run format:check working-directory: packages/signal-polyfill