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);