From eeeb23ac5503ca8726d33d71eebb85693fd3739d Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Wed, 19 Jun 2024 15:26:19 +0300 Subject: [PATCH 1/2] add prettier checks --- .github/workflows/ci.yml | 1 + .prettierrc.json | 10 ++++++++++ package.json | 2 ++ 3 files changed, 13 insertions(+) create mode 100644 .prettierrc.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6c4076..cceecbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,5 +26,6 @@ jobs: - uses: actions/checkout@v4 - uses: wyvox/action-setup-pnpm@v3 - run: pnpm install --no-lockfile + - run: pnpm lint - run: pnpm build - run: pnpm vitest ${{ matrix.testenv.args }} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..34169f3 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "embeddedLanguageFormatting": "off", + "singleQuote": true, + "semi": true, + "quoteProps": "preserve", + "bracketSpacing": false +} diff --git a/package.json b/package.json index 2b96126..891dd57 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "build": "tsc && vite build", "dev": "vite", "prepack": "npm run build", + "lint": "prettier --check .", + "lint:fix": "prettier --write .", "test": "vitest" }, "devDependencies": { From 4cf87cef28aa89e938f079e4d82e9bf10f6d0a4c Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Wed, 19 Jun 2024 15:26:47 +0300 Subject: [PATCH 2/2] run prettier --- .github/workflows/ci.yml | 12 +- .github/workflows/plan-release.yml | 12 +- .github/workflows/publish.yml | 6 +- CHANGELOG.md | 10 +- README.md | 28 +- RELEASE.md | 16 +- src/computed.ts | 13 +- src/equality.ts | 2 +- src/errors.ts | 2 +- src/graph.ts | 64 +-- src/index.ts | 2 +- src/signal.ts | 28 +- src/wrapper.ts | 451 +++++++++---------- tests/Signal/computed.test.ts | 14 +- tests/Signal/state.test.ts | 14 +- tests/Signal/subtle/currentComputed.test.ts | 12 +- tests/Signal/subtle/untrack.test.ts | 14 +- tests/Signal/subtle/watch-unwatch.test.ts | 12 +- tests/Signal/subtle/watcher.test.ts | 18 +- tests/behaviors/custom-equality.test.ts | 12 +- tests/behaviors/cycles.test.ts | 10 +- tests/behaviors/dynamic-dependencies.test.ts | 32 +- tests/behaviors/errors.test.ts | 48 +- tests/behaviors/liveness.test.ts | 10 +- tests/behaviors/prohibited-contexts.test.ts | 10 +- tests/behaviors/pruning.test.ts | 10 +- tests/behaviors/receivers.test.ts | 12 +- tests/behaviors/type-checking.test.ts | 72 +-- tsconfig.json | 17 +- vite.config.ts | 16 +- 30 files changed, 470 insertions(+), 509 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cceecbf..e8a1b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,15 @@ concurrency: jobs: test: - name: "Test ${{ matrix.testenv.name }}" + name: 'Test ${{ matrix.testenv.name }}' runs-on: ubuntu-latest timeout-minutes: 5 - strategy: + strategy: matrix: - testenv: - - { name: "Node", args: '' } - - { name: "Chrome", args: '--browser.name=chrome --browser.headless' } - - { name: "Firefox", args: '--browser.name=firefox --browser.headless' } + testenv: + - {name: 'Node', args: ''} + - {name: 'Chrome', args: '--browser.name=chrome --browser.headless'} + - {name: 'Firefox', args: '--browser.name=firefox --browser.headless'} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/plan-release.yml b/.github/workflows/plan-release.yml index 8b27a47..37d4f97 100644 --- a/.github/workflows/plan-release.yml +++ b/.github/workflows/plan-release.yml @@ -14,7 +14,7 @@ concurrency: jobs: check-plan: - name: "Check Release Plan" + name: 'Check Release Plan' runs-on: ubuntu-latest outputs: command: ${{ steps.check-release.outputs.command }} @@ -52,14 +52,14 @@ jobs: ref: 'main' - uses: wyvox/action-setup-pnpm@v3 - run: pnpm install --frozen-lockfile - - - name: "Generate Explanation and Prep Changelogs" + + - name: 'Generate Explanation and Prep Changelogs' id: explanation run: | set +e - + pnpm release-plan prepare 2> >(tee -a release-plan-stderr.txt >&2) - + if [ $? -ne 0 ]; then echo 'text<> $GITHUB_OUTPUT @@ -77,7 +77,7 @@ jobs: - uses: peter-evans/create-pull-request@v6 with: commit-message: "Prepare Release using 'release-plan'" - labels: "internal" + labels: 'internal' branch: release-preview title: Prepare Release body: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 903a41c..638cdc9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ concurrency: jobs: check-plan: - name: "Check Release Plan" + name: 'Check Release Plan' runs-on: ubuntu-latest outputs: command: ${{ steps.check-release.outputs.command }} @@ -34,7 +34,7 @@ jobs: run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT publish: - name: "NPM Publish" + name: 'NPM Publish' runs-on: ubuntu-latest needs: check-plan if: needs.check-plan.outputs.command == 'release' @@ -51,7 +51,7 @@ jobs: - run: pnpm install --frozen-lockfile - name: npm publish run: pnpm release-plan publish - + env: GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c861814..2cdc209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,12 @@ signal-polyfill 0.1.1 (patch) #### :house: Internal -* `signal-polyfill` - * [#6](https://github.com/proposal-signals/signal-polyfill/pull/6) Fix repo references ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) - * [#4](https://github.com/proposal-signals/signal-polyfill/pull/4) Setup release automation ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) - * [#1](https://github.com/proposal-signals/signal-polyfill/pull/1) Fix CI ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +- `signal-polyfill` + - [#6](https://github.com/proposal-signals/signal-polyfill/pull/6) Fix repo references ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + - [#4](https://github.com/proposal-signals/signal-polyfill/pull/4) Setup release automation ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + - [#1](https://github.com/proposal-signals/signal-polyfill/pull/1) Fix CI ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) #### Committers: 1 + - [@NullVoxPopuli](https://github.com/NullVoxPopuli) diff --git a/README.md b/README.md index ff8f908..578c18e 100644 --- a/README.md +++ b/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](https://github.com/tc39/proposal-signals) that enables one-way data flow through cells of state or computations derived from other state/computations. @@ -10,8 +10,8 @@ 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"; @@ -19,7 +19,7 @@ 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,11 +44,11 @@ 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"; @@ -64,7 +64,7 @@ const w = new Signal.subtle.Watcher(() => { function processPending() { needsEnqueue = true; - + for (const s of w.getPending()) { s.get(); } @@ -74,15 +74,15 @@ function processPending() { export function effect(callback) { let cleanup; - + const computed = new Signal.Computed(() => { typeof cleanup === "function" && cleanup(); cleanup = callback(); }); - + w.watch(computed); computed.get(); - + return () => { w.unwatch(computed); typeof cleanup === "function" && cleanup(); @@ -112,7 +112,7 @@ export function signal(target) { set(value) { get.call(this).set(value); }, - + init(value) { return new Signal.State(value); }, diff --git a/RELEASE.md b/RELEASE.md index 3c51885..5ffd224 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -4,21 +4,21 @@ Releases in this repo are mostly automated using [release-plan](https://github.c ## Preparation -Since the majority of the actual release process is automated, the remaining tasks before releasing are: +Since the majority of the actual release process is automated, the remaining tasks before releasing are: -- correctly labeling **all** pull requests that have been merged since the last release -- updating pull request titles so they make sense to our users +- correctly labeling **all** pull requests that have been merged since the last release +- updating pull request titles so they make sense to our users Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall guiding principle here is that changelogs are for humans, not machines. When reviewing merged PR's the labels to be used are: -* breaking - Used when the PR is considered a breaking change. -* enhancement - Used when the PR adds a new feature or enhancement. -* bug - Used when the PR fixes a bug included in a previous release. -* documentation - Used when the PR adds or updates documentation. -* internal - Internal changes or things that don't fit in any other category. +- breaking - Used when the PR is considered a breaking change. +- enhancement - Used when the PR adds a new feature or enhancement. +- bug - Used when the PR fixes a bug included in a previous release. +- documentation - Used when the PR adds or updates documentation. +- internal - Internal changes or things that don't fit in any other category. **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` diff --git a/src/computed.ts b/src/computed.ts index 50b3e85..6d99595 100644 --- a/src/computed.ts +++ b/src/computed.ts @@ -7,8 +7,15 @@ */ import {defaultEquals, ValueEqualityFn} from './equality.js'; -import {consumerAfterComputation, consumerBeforeComputation, producerAccessed, producerUpdateValueVersion, REACTIVE_NODE, ReactiveNode, SIGNAL} from './graph.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/src/equality.ts b/src/equality.ts index 55cb94c..20f9b9f 100644 --- a/src/equality.ts +++ b/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/src/errors.ts b/src/errors.ts index dbfba19..0f58fe9 100644 --- a/src/errors.ts +++ b/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/src/graph.ts b/src/graph.ts index f3e9dc5..3b56975 100644 --- a/src/graph.ts +++ b/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,13 @@ 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 +114,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 +131,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 +148,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 +214,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) { @@ -255,8 +255,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; } @@ -344,7 +345,7 @@ 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 +357,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; } @@ -427,7 +434,9 @@ export function consumerDestroy(node: ReactiveNode): void { } // 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,7 +450,10 @@ 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) { @@ -463,8 +475,9 @@ export function producerRemoveLiveConsumerAtIndex(node: ReactiveNode, idx: numbe 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)`); + throw new Error( + `Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`, + ); } if (node.liveConsumerNode.length === 1) { @@ -501,7 +514,6 @@ function consumerIsLive(node: ReactiveNode): boolean { return node.consumerIsAlwaysLive || (node?.liveConsumerNode?.length ?? 0) > 0; } - export function assertConsumerNode(node: ReactiveNode): asserts node is ConsumerNode { node.producerNode ??= []; node.producerIndexOfThis ??= []; @@ -511,4 +523,4 @@ export function assertConsumerNode(node: ReactiveNode): asserts node is Consumer export function assertProducerNode(node: ReactiveNode): asserts node is ProducerNode { node.liveConsumerNode ??= []; node.liveConsumerIndexOfThis ??= []; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 72b9910..ecab651 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export { Signal } from "./wrapper.js"; \ No newline at end of file +export {Signal} from './wrapper.js'; diff --git a/src/signal.ts b/src/signal.ts index 2bdfba3..7348c22 100644 --- a/src/signal.ts +++ b/src/signal.ts @@ -8,11 +8,19 @@ import {defaultEquals, ValueEqualityFn} from './equality.js'; import {throwInvalidWriteToSignalError} from './errors.js'; -import {producerAccessed, producerIncrementEpoch, producerNotifyConsumers, producerUpdatesAllowed, REACTIVE_NODE, ReactiveNode, SIGNAL} from './graph.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,14 @@ 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; @@ -95,4 +103,4 @@ function signalValueChanged(node: SignalNode): void { producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); -} \ No newline at end of file +} diff --git a/src/wrapper.ts b/src/wrapper.ts index a8eab3d..55a5e6e 100644 --- a/src/wrapper.ts +++ b/src/wrapper.ts @@ -15,298 +15,273 @@ * limitations under the License. */ +import {computedGet, createComputed, type ComputedNode} from './computed.js'; import { - computedGet, - createComputed, - type ComputedNode, -} from "./computed.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"; - -const NODE: unique symbol = Symbol("node"); - -let isState: (s: any) => boolean, - isComputed: (s: any) => boolean, - isWatcher: (s: any) => boolean; + SIGNAL, + getActiveConsumer, + isInNotificationPhase, + producerAccessed, + assertConsumerNode, + setActiveConsumer, + REACTIVE_NODE, + type ReactiveNode, + assertProducerNode, + producerRemoveLiveConsumerAtIndex, +} from './graph.js'; +import {createSignal, signalGetFn, signalSetFn, type SignalNode} from './signal.js'; + +const NODE: unique symbol = Symbol('node'); + +let isState: (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]); - } -} + // A Signal which is a formula based on other Signals + export class Computed { + readonly [NODE]: ComputedNode; -// 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); + #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'); + } + } } - this.#assertSignals(signals); - const node = this[NODE]; - assertConsumerNode(node); + // 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); - let indicesToShift = []; - for (let i = 0; i < node.producerNode.length; i++) { + 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); + } + + // 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++) { if (signals.includes(node.producerNode[i].wrapper)) { 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/tests/Signal/computed.test.ts b/tests/Signal/computed.test.ts index 96b4dcf..6c02416 100644 --- a/tests/Signal/computed.test.ts +++ b/tests/Signal/computed.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -describe("Computed", () => { - it("should work", () => { +describe('Computed', () => { + it('should work', () => { const stateSignal = new Signal.State(1); const computedSignal = new Signal.Computed(() => { @@ -18,8 +18,8 @@ describe("Computed", () => { expect(computedSignal.get()).toEqual(10); }); - describe("Comparison semantics", () => { - it("should track Computed by Object.is", () => { + describe('Comparison semantics', () => { + it('should track Computed by Object.is', () => { const state = new Signal.State(1); let value = 5; let calls = 0; @@ -43,7 +43,7 @@ describe("Computed", () => { 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), { diff --git a/tests/Signal/state.test.ts b/tests/Signal/state.test.ts index 9593554..067830a 100644 --- a/tests/Signal/state.test.ts +++ b/tests/Signal/state.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/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); @@ -11,8 +11,8 @@ describe("Signal.State", () => { expect(stateSignal.get()).toEqual(10); }); - 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(() => { @@ -27,7 +27,7 @@ describe("Signal.State", () => { expect(calls).toBe(1); }); - it("applies custom equality in State", () => { + it('applies custom equality in State', () => { let ecalls = 0; const state = new Signal.State(1, { equals() { diff --git a/tests/Signal/subtle/currentComputed.test.ts b/tests/Signal/subtle/currentComputed.test.ts index 3fcb503..a21c26d 100644 --- a/tests/Signal/subtle/currentComputed.test.ts +++ b/tests/Signal/subtle/currentComputed.test.ts @@ -1,13 +1,11 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../../src/wrapper.js'; -describe("currentComputed", () => { - it("works", () => { +describe('currentComputed', () => { + it('works', () => { expect(Signal.subtle.currentComputed()).toBe(undefined); let context; - let c = new Signal.Computed( - () => (context = Signal.subtle.currentComputed()), - ); + let c = new Signal.Computed(() => (context = Signal.subtle.currentComputed())); c.get(); expect(c).toBe(context); }); diff --git a/tests/Signal/subtle/untrack.test.ts b/tests/Signal/subtle/untrack.test.ts index cb8d821..5e06be7 100644 --- a/tests/Signal/subtle/untrack.test.ts +++ b/tests/Signal/subtle/untrack.test.ts @@ -1,18 +1,16 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../../src/wrapper.js'; -describe("Untrack", () => { - it("works", () => { +describe('Untrack', () => { + it('works', () => { const state = new Signal.State(1); - const computed = new Signal.Computed(() => - Signal.subtle.untrack(() => state.get()), - ); + const computed = new Signal.Computed(() => Signal.subtle.untrack(() => state.get())); expect(computed.get()).toBe(1); 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); diff --git a/tests/Signal/subtle/watch-unwatch.test.ts b/tests/Signal/subtle/watch-unwatch.test.ts index e49cdad..c1761c7 100644 --- a/tests/Signal/subtle/watch-unwatch.test.ts +++ b/tests/Signal/subtle/watch-unwatch.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../../src/wrapper.js'; -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; @@ -29,7 +29,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, @@ -52,7 +52,7 @@ describe("watch and unwatch", () => { u2++; }, }); - let which: { get(): number } = s1; + let which: {get(): number} = s1; let c = new Signal.Computed(() => (d++, which.get())); let w = new Signal.subtle.Watcher(() => n++); diff --git a/tests/Signal/subtle/watcher.test.ts b/tests/Signal/subtle/watcher.test.ts index 8bc4651..11521e0 100644 --- a/tests/Signal/subtle/watcher.test.ts +++ b/tests/Signal/subtle/watcher.test.ts @@ -1,7 +1,7 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { Signal } from "../../../src/wrapper.js"; +import {afterEach, describe, expect, it, vi} from 'vitest'; +import {Signal} from '../../../src/wrapper.js'; -describe("Watcher", () => { +describe('Watcher', () => { type Destructor = () => void; const notifySpy = vi.fn(); @@ -29,7 +29,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, { @@ -61,7 +61,7 @@ describe("Watcher", () => { output = stateSignal.get(); computedOutput = computedSignal.get(); calls++; - return () => { }; + return () => {}; }); // The signal is now watched @@ -142,17 +142,17 @@ describe("Watcher", () => { // Adding any other effect after an unwatch should work as expected const destructor2 = effect(() => { output = stateSignal.get(); - return () => { }; + return () => {}; }); stateSignal.set(300); flushPending(); }); - it("provides `this` to notify as normal function", () => { + it('provides `this` to notify as normal function', () => { const mockGetPending = vi.fn(); - const watcher = new Signal.subtle.Watcher(function() { + const watcher = new Signal.subtle.Watcher(function () { this.getPending(); }); watcher.getPending = mockGetPending; @@ -164,7 +164,7 @@ describe("Watcher", () => { expect(mockGetPending).toBeCalled(); }); - it("can be closed in if needed in notify as an arrow function", () => { + it('can be closed in if needed in notify as an arrow function', () => { const mockGetPending = vi.fn(); const watcher = new Signal.subtle.Watcher(() => { diff --git a/tests/behaviors/custom-equality.test.ts b/tests/behaviors/custom-equality.test.ts index f131851..3fc5416 100644 --- a/tests/behaviors/custom-equality.test.ts +++ b/tests/behaviors/custom-equality.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it, vi } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it, vi} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -describe("Custom equality", () => { - it("works for State", () => { +describe('Custom equality', () => { + it('works for State', () => { let answer = true; const s = new Signal.State(1, { equals() { @@ -31,7 +31,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); @@ -63,7 +63,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); diff --git a/tests/behaviors/cycles.test.ts b/tests/behaviors/cycles.test.ts index 837f5ac..5ebb7a4 100644 --- a/tests/behaviors/cycles.test.ts +++ b/tests/behaviors/cycles.test.ts @@ -1,13 +1,13 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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()); diff --git a/tests/behaviors/dynamic-dependencies.test.ts b/tests/behaviors/dynamic-dependencies.test.ts index ad24e6f..fbe404e 100644 --- a/tests/behaviors/dynamic-dependencies.test.ts +++ b/tests/behaviors/dynamic-dependencies.test.ts @@ -1,12 +1,12 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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; }); @@ -14,23 +14,17 @@ describe("Dynamic dependencies", () => { const w = new Signal.subtle.Watcher(() => {}); w.watch(computed); } - expect(computed.get()).toBe("abcdefgh"); - expect(Signal.subtle.introspectSources(computed).slice(1)).toStrictEqual( - states, - ); + 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(Signal.subtle.introspectSources(computed).slice(1)).toStrictEqual( - states.slice(0, 5), - ); + 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(Signal.subtle.introspectSources(computed).slice(1)).toStrictEqual( - states.slice(3), - ); + 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)); }); diff --git a/tests/behaviors/errors.test.ts b/tests/behaviors/errors.test.ts index 8a1bf8b..bf366a2 100644 --- a/tests/behaviors/errors.test.ts +++ b/tests/behaviors/errors.test.ts @@ -1,9 +1,9 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {afterEach, describe, expect, it, vi} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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++; @@ -15,26 +15,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++; @@ -44,23 +44,23 @@ 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"); + throw new Error('equals'); }, }); @@ -68,9 +68,9 @@ describe("Errors", () => { 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); }); }); diff --git a/tests/behaviors/liveness.test.ts b/tests/behaviors/liveness.test.ts index 99fb1fb..fce1773 100644 --- a/tests/behaviors/liveness.test.ts +++ b/tests/behaviors/liveness.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it, vi } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it, vi} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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, { @@ -34,7 +34,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); diff --git a/tests/behaviors/prohibited-contexts.test.ts b/tests/behaviors/prohibited-contexts.test.ts index 64a8597..2136a1b 100644 --- a/tests/behaviors/prohibited-contexts.test.ts +++ b/tests/behaviors/prohibited-contexts.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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); @@ -18,7 +18,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(); diff --git a/tests/behaviors/pruning.test.ts b/tests/behaviors/pruning.test.ts index 48102bb..bec8175 100644 --- a/tests/behaviors/pruning.test.ts +++ b/tests/behaviors/pruning.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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())); @@ -30,7 +30,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())); diff --git a/tests/behaviors/receivers.test.ts b/tests/behaviors/receivers.test.ts index a1c17e3..abd6683 100644 --- a/tests/behaviors/receivers.test.ts +++ b/tests/behaviors/receivers.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -describe("Receivers", () => { - it("is this for computed", () => { +describe('Receivers', () => { + it('is this for computed', () => { let receiver; const c = new Signal.Computed(function () { receiver = this; @@ -10,7 +10,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]() { @@ -29,7 +29,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() { diff --git a/tests/behaviors/type-checking.test.ts b/tests/behaviors/type-checking.test.ts index 2ad4341..b66fefd 100644 --- a/tests/behaviors/type-checking.test.ts +++ b/tests/behaviors/type-checking.test.ts @@ -1,19 +1,19 @@ -import { describe, expect, it } from "vitest"; -import { Signal } from "../../src/wrapper.js"; +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; -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("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(() => {}); @@ -34,46 +34,22 @@ describe("type checks", () => { expect(Signal.Computed.prototype.get.call(c)).toBe(undefined); expect(() => Signal.Computed.prototype.get.call(w)).toThrowError(TypeError); - expect(() => Signal.subtle.Watcher.prototype.watch.call(x, s)).toThrowError( - TypeError, - ); - expect(() => Signal.subtle.Watcher.prototype.watch.call(s, s)).toThrowError( - TypeError, - ); - expect(() => Signal.subtle.Watcher.prototype.watch.call(c, s)).toThrowError( - TypeError, - ); + expect(() => Signal.subtle.Watcher.prototype.watch.call(x, s)).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.watch.call(s, s)).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.watch.call(c, s)).toThrowError(TypeError); expect(Signal.subtle.Watcher.prototype.watch.call(w, s)).toBe(undefined); - expect(() => Signal.subtle.Watcher.prototype.watch.call(w, w)).toThrowError( - TypeError, - ); + expect(() => Signal.subtle.Watcher.prototype.watch.call(w, w)).toThrowError(TypeError); - expect(() => - Signal.subtle.Watcher.prototype.unwatch.call(x, s), - ).toThrowError(TypeError); - expect(() => - Signal.subtle.Watcher.prototype.unwatch.call(s, s), - ).toThrowError(TypeError); - expect(() => - Signal.subtle.Watcher.prototype.unwatch.call(c, s), - ).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.unwatch.call(x, s)).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.unwatch.call(s, s)).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.unwatch.call(c, s)).toThrowError(TypeError); expect(Signal.subtle.Watcher.prototype.unwatch.call(w, s)).toBe(undefined); - expect(() => - Signal.subtle.Watcher.prototype.unwatch.call(w, w), - ).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.unwatch.call(w, w)).toThrowError(TypeError); - expect(() => - Signal.subtle.Watcher.prototype.getPending.call(x, s), - ).toThrowError(TypeError); - expect(() => - Signal.subtle.Watcher.prototype.getPending.call(s, s), - ).toThrowError(TypeError); - expect(() => - Signal.subtle.Watcher.prototype.getPending.call(c, s), - ).toThrowError(TypeError); - expect(Signal.subtle.Watcher.prototype.getPending.call(w, s)).toStrictEqual( - [], - ); + expect(() => Signal.subtle.Watcher.prototype.getPending.call(x, s)).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.getPending.call(s, s)).toThrowError(TypeError); + expect(() => Signal.subtle.Watcher.prototype.getPending.call(c, s)).toThrowError(TypeError); + expect(Signal.subtle.Watcher.prototype.getPending.call(w, s)).toStrictEqual([]); // @ts-expect-error expect(() => Signal.subtle.introspectSources(x)).toThrowError(TypeError); diff --git a/tsconfig.json b/tsconfig.json index f223b6e..4a66bd8 100644 --- a/tsconfig.json +++ b/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/vite.config.ts b/vite.config.ts index c3885d2..ed7e667 100644 --- a/vite.config.ts +++ b/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" - } - } + formats: ['es'], + fileName: 'index', + }, + }, });