From dacd0de49dcfb8d3cfb7b51fe78289ae1d830005 Mon Sep 17 00:00:00 2001 From: Matthew Zito Date: Fri, 24 Dec 2021 18:03:32 -0800 Subject: [PATCH] docs: update package identifiers --- README.md | 66 ++++++++++++++++++++++++-------- __tests__/index.test.ts | 24 ++++++------ __tests__/{util.ts => utils.ts} | 0 docs/index.md | 12 ++++++ docs/resonant.effect.md | 24 ++++++++++++ docs/resonant.md | 19 +++++++++ docs/resonant.resonant.md | 24 ++++++++++++ docs/resonant.revokes.md | 13 +++++++ etc/resonant.api.md | 20 ++++++++++ package.json | 20 +++++----- pnpm-lock.yaml | 8 ++-- src/index.ts | 68 +++++++++++++++++++++++++++++---- 12 files changed, 249 insertions(+), 49 deletions(-) rename __tests__/{util.ts => utils.ts} (100%) create mode 100644 docs/index.md create mode 100644 docs/resonant.effect.md create mode 100644 docs/resonant.md create mode 100644 docs/resonant.resonant.md create mode 100644 docs/resonant.revokes.md create mode 100644 etc/resonant.api.md diff --git a/README.md b/README.md index 1eb1075..b738eeb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ -# effect +# resonant -[![Coverage Status](https://coveralls.io/repos/github/MatthewZito/effect/badge.svg?branch=master)](https://coveralls.io/github/MatthewZito/effect?branch=master) -[![Continuous Deployment](https://github.com/MatthewZito/effect/actions/workflows/cd.yml/badge.svg)](https://github.com/MatthewZito/effect/actions/workflows/cd.yml) -[![Continuous Integration](https://github.com/MatthewZito/effect/actions/workflows/ci.yml/badge.svg)](https://github.com/MatthewZito/effect/actions/workflows/ci.yml) -[![npm version](https://badge.fury.io/js/effect.svg)](https://badge.fury.io/js/effect) +[![Coverage Status](https://coveralls.io/repos/github/MatthewZito/resonant/badge.svg?branch=master)](https://coveralls.io/github/MatthewZito/resonant?branch=master) +[![Continuous Deployment](https://github.com/MatthewZito/resonant/actions/workflows/cd.yml/badge.svg)](https://github.com/MatthewZito/resonant/actions/workflows/cd.yml) +[![Continuous Integration](https://github.com/MatthewZito/resonant/actions/workflows/ci.yml/badge.svg)](https://github.com/MatthewZito/resonant/actions/workflows/ci.yml) +[![npm version](https://badge.fury.io/js/resonant.svg)](https://badge.fury.io/js/resonant) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +Reactive effects with automatic dependency management, caching, and leak-free finalization. + ## Table of Contents - [Install](#install) @@ -16,31 +18,63 @@ ## Installation ```bash -npm install effect -``` - -OR - -```bash -yarn add effect +npm install resonant ``` ### Supported Environments -`effect` currently supports UMD, CommonJS (node versions >= 10), and ESM build-targets +`resonant` currently supports UMD, CommonJS (node versions >= 10), and ESM build-targets Commonjs: ```js -const { isDefined } = require('effect'); +const { resonant, effect } = require('resonant'); ``` ESM: ```js -import { isDefined } from 'effect'; +import { resonant, effect } from 'resonant'; ``` ## Documentation -Full documentation can be found [here](https://matthewzito.github.io/effect/effect.html) +Inspired by React's `useEffect` and Vue's `watchEffect`, `resonant` is a compact utility library that mitigates the inherent burdens of managing observable data, including dependency tracking; caching and cache invalidation; and object dereferencing and finalization. + +To create an effect, you must first make the target object reactive with the `resonant` function: + +```ts +import { resonant } from 'resonant'; + +const plainObject = { + x: 1, + y: 1 +}; + +const r = resonant(plainObject); +``` + +Now, `r` is equipped with deep reactivity. All get / set operations will trigger any dependencies (i.e. effects) that happen to be observing the data. + +Let's create an effect: + +```ts +import { resonant, effect } from 'resonant'; + +const plainObject = { + x: 1, + y: 1 +}; + +const r = resonant(plainObject); + +let count = 0; + +effect(() => { + count += r.x + r.y; +}); +``` + +The effect will run immediately. It has now been cached and will execute whenever its properties change. `resonant` also uses weak references; deleted properties to which there are no references will be finalized so they may be garbage collected, as will all of that property's dependencies and effects. + +Full documentation can be found [here](https://matthewzito.github.io/resonant/resonant.html) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index afb2f65..7232ead 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1,8 +1,8 @@ -import { reactive, effect } from '../src'; +import { resonant, effect } from '../src'; -import { forMocks } from './util'; +import { forMocks } from './utils'; -const r = reactive({ +const r = resonant({ a: 1, b: 2, c: 3 @@ -22,7 +22,7 @@ describe('run effect', () => { expect(mock).toHaveBeenCalledTimes(1); }); - it('reruns the effect when a reactive property dependency has been mutated', () => { + it('reruns the effect when a resonant property dependency has been mutated', () => { const mock = jest.fn(() => { r.a; }); @@ -34,7 +34,7 @@ describe('run effect', () => { expect(mock).toHaveBeenCalledTimes(2); }); - it('only runs effects which depend on a reactive property', () => { + it('only runs effects which depend on a resonant property', () => { const mock = jest.fn(() => {}); const mock2 = jest.fn(() => { r.a; @@ -86,7 +86,7 @@ describe('run effect', () => { }); it('triggers effects on nested object mutations', () => { - const r2 = reactive({ + const r2 = resonant({ a: 1, b: { x: { @@ -109,7 +109,7 @@ describe('run effect', () => { }); it('tracks and runs multiple effects', () => { - const r1 = reactive({ + const r1 = resonant({ x: 1, y: 2, z: { @@ -118,7 +118,7 @@ describe('run effect', () => { } }); - const r2 = reactive({ + const r2 = resonant({ c: 3, d: 4 }); @@ -175,7 +175,7 @@ describe('run effect', () => { const mock1 = jest.fn(); const mock2 = jest.fn(); - const r = reactive({ + const r = resonant({ price: 1, quantity: 2, meta: { @@ -243,10 +243,10 @@ describe('run effect', () => { expect(total).toBe(19); }); - it('handles nested reactive properties', () => { + it('handles nested resonant properties', () => { const mocks = Array.from({ length: 10 }, jest.fn); - const r = reactive({ + const r = resonant({ a: false, x: { y: { @@ -392,7 +392,7 @@ describe('run effect', () => { const mock = jest.fn(); const mock2 = jest.fn(); - const r = reactive({ + const r = resonant({ x: 1, y: 1 }); diff --git a/__tests__/util.ts b/__tests__/utils.ts similarity index 100% rename from __tests__/util.ts rename to __tests__/utils.ts diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c6c5aff --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [resonant](./resonant.md) | | + diff --git a/docs/resonant.effect.md b/docs/resonant.effect.md new file mode 100644 index 0000000..2c17613 --- /dev/null +++ b/docs/resonant.effect.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [resonant](./resonant.md) > [effect](./resonant.effect.md) + +## effect() function + +Create a resonant effect. All get / set operations within the handler will be tracked. An effect is run upon initialization, and subsequent to any mutations to its dependencies. + +Signature: + +```typescript +export declare function effect(handler: () => void): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| handler | () => void | | + +Returns: + +void + diff --git a/docs/resonant.md b/docs/resonant.md new file mode 100644 index 0000000..24859b5 --- /dev/null +++ b/docs/resonant.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [resonant](./resonant.md) + +## resonant package + +## Functions + +| Function | Description | +| --- | --- | +| [effect(handler)](./resonant.effect.md) | Create a resonant effect. All get / set operations within the handler will be tracked. An effect is run upon initialization, and subsequent to any mutations to its dependencies. | +| [resonant(target)](./resonant.resonant.md) | Make an object resonant. All object properties will be eligible dependencies for an effect. | + +## Variables + +| Variable | Description | +| --- | --- | +| [revokes](./resonant.revokes.md) | A map of revoke handlers. Pass in a proxy reference to retrieve its corresponding handler. | + diff --git a/docs/resonant.resonant.md b/docs/resonant.resonant.md new file mode 100644 index 0000000..c53a22c --- /dev/null +++ b/docs/resonant.resonant.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [resonant](./resonant.md) > [resonant](./resonant.resonant.md) + +## resonant() function + +Make an object resonant. All object properties will be eligible dependencies for an effect. + +Signature: + +```typescript +export declare function resonant>(target: T): T; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| target | T | | + +Returns: + +T + diff --git a/docs/resonant.revokes.md b/docs/resonant.revokes.md new file mode 100644 index 0000000..2bc1549 --- /dev/null +++ b/docs/resonant.revokes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [resonant](./resonant.md) > [revokes](./resonant.revokes.md) + +## revokes variable + +A map of revoke handlers. Pass in a proxy reference to retrieve its corresponding handler. + +Signature: + +```typescript +revokes: WeakMap, IRevokeHandler> +``` diff --git a/etc/resonant.api.md b/etc/resonant.api.md new file mode 100644 index 0000000..53b56bf --- /dev/null +++ b/etc/resonant.api.md @@ -0,0 +1,20 @@ +## API Report File for "resonant" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export function effect(handler: () => void): void; + +// @public +export function resonant>(target: T): T; + +// Warning: (ae-forgotten-export) The symbol "IRevokeHandler" needs to be exported by the entry point index.d.ts +// +// @public +export const revokes: WeakMap, IRevokeHandler>; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/package.json b/package.json index 807a88b..7e6c84a 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,24 @@ { - "name": "effect", + "name": "resonant", "version": "0.0.0-development", "description": "", "keywords": [], "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/MatthewZito/effect.git" + "url": "https://github.com/MatthewZito/resonant.git" }, "author": "Matthew T Zito (goldmund)", "files": [ "dist/" ], "exports": { - "require": "./dist/effect.cjs.js", - "import": "./dist/effect.es.js" + "require": "./dist/resonant.cjs.js", + "import": "./dist/resonant.es.js" }, - "main": "./dist/effect.cjs.js", - "browser": "./dist/effect.umd.js", - "module": "./dist/effect.es.js", + "main": "./dist/resonant.cjs.js", + "browser": "./dist/resonant.umd.js", + "module": "./dist/resonant.es.js", "types": "dist/index.d.ts", "engines": { "node": ">= 10" @@ -47,9 +47,9 @@ ] }, "bugs": { - "url": "https://github.com/MatthewZito/effect/issues" + "url": "https://github.com/MatthewZito/resonant/issues" }, - "homepage": "https://github.com/MatthewZito/effect#readme", + "homepage": "https://github.com/MatthewZito/resonant#readme", "devDependencies": { "@babel/cli": "7.15.7", "@babel/core": "7.15.8", @@ -62,7 +62,7 @@ "@microsoft/api-extractor": "^7.10.4", "@rollup/plugin-babel": "5.3.0", "@rollup/plugin-commonjs": "21.0.1", - "@rollup/plugin-node-resolve": "13.0.6", + "@rollup/plugin-node-resolve": "13.1.1", "@rollup/plugin-typescript": "8.3.0", "@types/jest": "27.0.2", "cz-conventional-changelog": "^3.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3db6f63..424eedb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ specifiers: '@microsoft/api-extractor': ^7.10.4 '@rollup/plugin-babel': 5.3.0 '@rollup/plugin-commonjs': 21.0.1 - '@rollup/plugin-node-resolve': 13.0.6 + '@rollup/plugin-node-resolve': 13.1.1 '@rollup/plugin-typescript': 8.3.0 '@types/jest': 27.0.2 cz-conventional-changelog: ^3.3.0 @@ -40,7 +40,7 @@ devDependencies: '@microsoft/api-extractor': 7.19.2 '@rollup/plugin-babel': 5.3.0_@babel+core@7.15.8+rollup@2.58.0 '@rollup/plugin-commonjs': 21.0.1_rollup@2.58.0 - '@rollup/plugin-node-resolve': 13.0.6_rollup@2.58.0 + '@rollup/plugin-node-resolve': 13.1.1_rollup@2.58.0 '@rollup/plugin-typescript': 8.3.0_rollup@2.58.0+typescript@4.5.2 '@types/jest': 27.0.2 cz-conventional-changelog: 3.3.0 @@ -2007,8 +2007,8 @@ packages: rollup: 2.58.0 dev: true - /@rollup/plugin-node-resolve/13.0.6_rollup@2.58.0: - resolution: {integrity: sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==} + /@rollup/plugin-node-resolve/13.1.1_rollup@2.58.0: + resolution: {integrity: sha512-6QKtRevXLrmEig9UiMYt2fSvee9TyltGRfw+qSs6xjUnxwjOzTOqy+/Lpxsgjb8mJn1EQNbCDAvt89O4uzL5kw==} engines: {node: '>= 10.0.0'} peerDependencies: rollup: ^2.42.0 diff --git a/src/index.ts b/src/index.ts index 43702da..783f40e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,14 +14,34 @@ type ITargetMap = WeakMap; type IEffects = Set; type IEffectsMap = Map; +/** + * A map of revoke handlers. Pass in a proxy reference to retrieve its corresponding handler. + * @public + */ export const revokes = new WeakMap< - ReturnType, + ReturnType, IRevokeHandler >(); +/** + * A map of all tracked targets and their dependencies + * @internal + */ const targetMap: ITargetMap = new WeakMap(); + +/** + * A stack for tracking active effects + * @internal + */ const effectStack: IEffectFunction[] = []; +/** + * Create a resonant effect. All get / set operations within the handler will be tracked. + * An effect is run upon initialization, and subsequent to any mutations to its dependencies. + * @param handler + * + * @public + */ export function effect(handler: () => void) { const newEffect: IEffectFunction = () => { run(newEffect); @@ -33,17 +53,23 @@ export function effect(handler: () => void) { newEffect(); } -export function reactive>(target: T) { +/** + * Make an object resonant. All object properties will be eligible dependencies for an effect. + * @param target + * + * @public + */ +export function resonant>(target: T) { const { proxy, revoke } = Proxy.revocable(target, { get(target, key, receiver): T { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const result: T[keyof T] = Reflect.get(target, key, receiver); - // ensure we set nested objects and arrays and make them reactive + // ensure we set nested objects and arrays and make them resonant if (isObject(result)) { track(result, key); - return reactive(result); + return resonant(result); } track(target, key); @@ -78,6 +104,12 @@ export function reactive>(target: T) { return proxy; } +/** + * Push the given effect onto the stack, run it, then pop it off + * @param effect + * + * @internal + */ function run(effect: IEffectFunction) { if (!effect.active) { effect.handler(); @@ -98,18 +130,33 @@ function run(effect: IEffectFunction) { } } +/** + * Revoke the proxy and clear all effects + * + * @internal + */ function revokeAndCleanup(this: { target: T; revoke: () => void }) { const { target, revoke } = this; + revoke(); + const effectsMap = targetMap.get(target); - if (!effectsMap) return; + if (!effectsMap) { + return; + } - revoke(); effectsMap.clear(); } +/** + * For a given target, track the given key + * @param target + * @param key + * + * @internal + */ function track(target: T, key: PropertyKey) { - // grab the last run effect - this is the one in which the reactive property is being tracked + // grab the last run effect - this is the one in which the resonant property is being tracked const activeEffect = effectStack[effectStack.length - 1]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -130,6 +177,13 @@ function track(target: T, key: PropertyKey) { } } +/** + * Trigger all effects for a given key of a given target + * @param target + * @param key + * + * @internal + */ function trigger(target: any, key: PropertyKey) { const effectsMap = targetMap.get(target);