├── .gitignore
├── examples
├── pony-vid.gif
├── counter-vid.gif
├── debounce-vid.gif
├── concatenator-vid.gif
├── counter.html
├── concatenator.html
├── debounce.html
└── pony.html
├── .npmignore
├── .editorconfig
├── lib
├── ignore.js
├── patella.js
├── dispose.js
├── patella.d.ts
├── computed.js
├── reactive.js
└── util.js
├── LICENSE
├── .github
└── workflows
│ └── actions.yml
├── package.json
├── rollup.config.js
├── README.md
└── test
└── patella.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 |
--------------------------------------------------------------------------------
/examples/pony-vid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/pony-vid.gif
--------------------------------------------------------------------------------
/examples/counter-vid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/counter-vid.gif
--------------------------------------------------------------------------------
/examples/debounce-vid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/debounce-vid.gif
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | .editorconfig
3 | .gitignore
4 | test
5 | coverage
6 | examples
7 | rollup.config.js
8 |
--------------------------------------------------------------------------------
/examples/concatenator-vid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/concatenator-vid.gif
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | charset = utf-8
4 | end_of_line = lf
5 |
6 | indent_size = 2
7 | indent_style = space
8 |
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/lib/ignore.js:
--------------------------------------------------------------------------------
1 | import {
2 | isObject, isFunction,
3 | hasOwnProperty,
4 | HINT_OBSERVE, defineHint,
5 | MESSAGE_NOT_OBJECT, throwError
6 | } from "./util.js";
7 |
8 | /** See lib/patella.d.ts */
9 | export function ignore(object) {
10 | if (!isObject(object) && !isFunction(object)) {
11 | throwError(MESSAGE_NOT_OBJECT);
12 | }
13 |
14 | if (!hasOwnProperty(object, HINT_OBSERVE)) {
15 | defineHint(object, HINT_OBSERVE);
16 | }
17 |
18 | return object;
19 | }
20 |
--------------------------------------------------------------------------------
/examples/counter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Patella Example - Counter
8 |
9 |
10 |
11 |
12 | Click Counter
13 |
14 |
15 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Lua MacDougall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/actions.yml:
--------------------------------------------------------------------------------
1 | name: Continuous integration and deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | release:
11 | types:
12 | - published
13 |
14 | jobs:
15 | test:
16 | name: Run test suite and upload coverage
17 | runs-on: ubuntu-20.04
18 | steps:
19 | - uses: actions/checkout@v2
20 | - uses: actions/setup-node@v2
21 | with:
22 | node-version: 16
23 | - name: Install dependencies
24 | run: npm ci
25 | - name: Run tests
26 | run: npm test
27 | - name: Upload coverage
28 | uses: coverallsapp/github-action@v1.1.2
29 | with:
30 | github-token: ${{ secrets.GITHUB_TOKEN }}
31 | npm:
32 | name: Publish to npm
33 | if: github.event_name == 'release'
34 | needs: test
35 | runs-on: ubuntu-20.04
36 | steps:
37 | - uses: actions/checkout@v2
38 | - uses: actions/setup-node@v2
39 | with:
40 | node-version: 16
41 | registry-url: https://registry.npmjs.org
42 | - name: Install dependencies
43 | run: npm ci
44 | - name: Publish
45 | run: npm publish
46 | env:
47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
48 |
--------------------------------------------------------------------------------
/examples/concatenator.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Patella Example - Concatenator
8 |
9 |
10 |
11 |
12 | Concatenator
13 |
14 |
15 |
16 |
17 |
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/debounce.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Patella Example - Debounced Search
8 |
9 |
10 |
11 |
12 | Debounced Search
13 |
14 |
15 |
16 |
37 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/lib/patella.js:
--------------------------------------------------------------------------------
1 | /*
2 | * == Patella ==
3 | * Extremely small, fast and compatible reactive programming library.
4 | *
5 | * Version 2.2.2
6 | *
7 | * MIT License
8 | *
9 | * Copyright (c) 2021 Lua MacDougall
10 | *
11 | * Permission is hereby granted, free of charge, to any person obtaining a copy
12 | * of this software and associated documentation files (the "Software"), to deal
13 | * in the Software without restriction, including without limitation the rights
14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | * copies of the Software, and to permit persons to whom the Software is
16 | * furnished to do so, subject to the following conditions:
17 | *
18 | * The above copyright notice and this permission notice shall be included in
19 | * all copies or substantial portions of the Software.
20 | *
21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | * SOFTWARE.
28 | */
29 |
30 | export { observe } from "./reactive.js";
31 | export { ignore } from "./ignore.js";
32 | export { computed } from "./computed.js";
33 | export { dispose } from "./dispose.js";
34 |
--------------------------------------------------------------------------------
/lib/dispose.js:
--------------------------------------------------------------------------------
1 | import { computedLock, computedQueue, computedI } from "./computed.js";
2 | import {
3 | isFunction,
4 | hasOwnProperty,
5 | HINT_DISPOSE, HINT_DEPENDS, defineHint,
6 | MESSAGE_NOT_FUNCTION, throwError
7 | } from "./util.js";
8 |
9 | /** See lib/patella.d.ts */
10 | export function dispose(func, clean) {
11 | if (func == null) {
12 | func = computedQueue[computedI];
13 | if (!func) {
14 | throwError("Tried to dispose of current computed function while not running a computed function", true);
15 | }
16 | } else if (!isFunction(func)) {
17 | throwError(MESSAGE_NOT_FUNCTION);
18 | }
19 |
20 | // Only execute if the function has not been disposed yet
21 | if (!hasOwnProperty(func, HINT_DISPOSE)) {
22 | // Only define disposed property if we aren't cleaning
23 | if (!clean) defineHint(func, HINT_DISPOSE);
24 |
25 | // Remove from dependant reactive objects
26 | var depends = func[HINT_DEPENDS];
27 | if (depends) {
28 | defineHint(func, HINT_DEPENDS, clean ? [] : void 0);
29 | for (var i = 0; i < depends.length; i++) {
30 | depends[i](func);
31 | }
32 | }
33 |
34 | // Remove from the queue if locked and pending execution
35 | if (computedLock) { // Not required, but saves a `lastIndexOf` call on an empty array for like 6 bytes
36 | var i = computedQueue.lastIndexOf(func);
37 | if (i > computedI) computedQueue.splice(i, 1);
38 | }
39 | }
40 |
41 | // Only return the function if it was specified as an argument
42 | if (!computedLock) return func;
43 | }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "patella",
3 | "version": "2.2.2",
4 | "description": "Patella is a library for reactive programming in JavaScript, inspired by Hyperactiv and Vue.js.",
5 | "author": "Lua MacDougall (https://foxgirl.dev/)",
6 | "license": "MIT",
7 | "keywords": [
8 | "reactive",
9 | "react",
10 | "computed",
11 | "computed properties",
12 | "properties",
13 | "observable",
14 | "model",
15 | "mvc",
16 | "mvvc",
17 | "vue",
18 | "vue.js",
19 | "hyperactiv"
20 | ],
21 | "type": "module",
22 | "main": "./dist/patella.cjs.js",
23 | "module": "./lib/patella.js",
24 | "types": "./lib/patella.d.ts",
25 | "jsdelivr": "./dist/patella.iife.min.js",
26 | "unpkg": "./dist/patella.iife.min.js",
27 | "exports": {
28 | ".": {
29 | "require": "./dist/patella.cjs.js",
30 | "default": "./lib/patella.js"
31 | }
32 | },
33 | "scripts": {
34 | "test": "c8 --reporter lcov --reporter text-summary mocha --ui tdd ./test/patella.test.js",
35 | "build": "rollup --config rollup.config.js",
36 | "prepublishOnly": "npm test && npm run build"
37 | },
38 | "devDependencies": {
39 | "c8": "^7.8.0",
40 | "chai": "^4.3.4",
41 | "mocha": "^9.0.3",
42 | "rollup": "^2.56.2",
43 | "terser": "^5.7.1",
44 | "uglify-js": "^3.14.1"
45 | },
46 | "repository": {
47 | "type": "git",
48 | "url": "git+https://github.com/luavixen/Patella.git"
49 | },
50 | "bugs": {
51 | "url": "https://github.com/luavixen/Patella/issues"
52 | },
53 | "homepage": "https://github.com/luavixen/Patella#readme"
54 | }
55 |
--------------------------------------------------------------------------------
/lib/patella.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Makes an object and its properties reactive recursively.
3 | * Subobjects (but not subfunctions!) will also be observed.
4 | * Note that `observe` does not create a new object, it mutates the object passed into it: `observe(object) === object`.
5 | * @param {Object} object Object or function to make reactive
6 | * @returns {Object} Input `object`, now reactive
7 | */
8 | export declare function observe(object: T): T;
9 |
10 | /**
11 | * Prevents an object from being made reactive, `observe` will do nothing.
12 | * Note that `ignore` is not recursive, so subobjects can still be made reactive by calling `observe` on them directly.
13 | * @param {Object} object Object or function to ignore
14 | * @returns {Object} Input `object`, now permanently ignored
15 | */
16 | export declare function ignore(object: T): T;
17 |
18 | /**
19 | * Calls `func` with no arguments and records a list of all the reactive properties it accesses.
20 | * `func` will then be called again whenever any of the accessed properties are mutated.
21 | * Note that if `func` has been `dispose`d with `!!clean === false`, no operation will be performed.
22 | * @param {Function} func Function to execute
23 | * @returns {Function} Input `func`
24 | */
25 | export declare function computed void>(func: T): T;
26 |
27 | /**
28 | * "Disposes" a function that was run with `computed`, deregistering it so that it will no longer be called whenever any of its accessed reactive properties update.
29 | * The clean parameter controls whether calling `computed` with `func` will work or no-op.
30 | * @param {Function} [func] Function to dispose, omit to dispose the currently executing computed function
31 | * @param {boolean} [clean] If truthy, only deregister the function from all dependencies, but allow it to be used with `computed` again in the future
32 | * @returns {Function} Input `func` if `func` is valid, otherwise `undefined`
33 | */
34 | export declare function dispose(func?: null, clean?: boolean | null): void;
35 | export declare function dispose void>(func: T, clean?: boolean | null): T;
36 |
--------------------------------------------------------------------------------
/lib/computed.js:
--------------------------------------------------------------------------------
1 | import {
2 | isFunction,
3 | hasOwnProperty,
4 | HINT_DISPOSE, HINT_DEPENDS, defineHint,
5 | MESSAGE_NOT_FUNCTION, throwError
6 | } from "./util.js";
7 |
8 | /** Maximum queue length */
9 | var MAX_QUEUE = 2000;
10 |
11 | /** Is the queue being executed? */
12 | export var computedLock = false;
13 | /** Queue of computed functions to be called */
14 | export var computedQueue = [];
15 | /** Current index into `computedQueue` */
16 | export var computedI = 0;
17 |
18 | /**
19 | * Throws an error indicating that the computed queue has overflowed
20 | */
21 | function computedOverflow() {
22 | var message = "Computed queue overflow! Last 10 functions in the queue:";
23 |
24 | var length = computedQueue.length;
25 | for (var i = length - 11; i < length; i++) {
26 | var func = computedQueue[i];
27 | message +=
28 | "\n"
29 | + (i + 1)
30 | + ": "
31 | + (func.name || "anonymous");
32 | }
33 |
34 | throwError(message, true);
35 | }
36 |
37 | /**
38 | * Attempts to add a function to the computed queue, then attempts to lock and execute the computed queue
39 | * @param func Function to queue
40 | */
41 | export function computedNotify(func) {
42 | if (hasOwnProperty(func, HINT_DISPOSE)) return;
43 |
44 | // Only add to the queue if not already pending execution
45 | if (computedQueue.lastIndexOf(func) >= computedI) return;
46 | computedQueue.push(func);
47 |
48 | // Make sure that the function in question has a depends hint
49 | if (!hasOwnProperty(func, HINT_DEPENDS)) {
50 | defineHint(func, HINT_DEPENDS, []);
51 | }
52 |
53 | // Attempt to lock and execute the queue
54 | if (!computedLock) {
55 | computedLock = true;
56 |
57 | try {
58 | for (; computedI < computedQueue.length; computedI++) {
59 | // Indirectly call the function to avoid leaking `computedQueue` as `this`
60 | (0, computedQueue[computedI])();
61 | if (computedI > MAX_QUEUE) /* @__NOINLINE */ computedOverflow();
62 | }
63 | } finally {
64 | computedLock = false;
65 | computedQueue = [];
66 | computedI = 0;
67 | }
68 | }
69 | }
70 |
71 | /** See lib/patella.d.ts */
72 | export function computed(func) {
73 | if (!isFunction(func)) {
74 | throwError(MESSAGE_NOT_FUNCTION);
75 | }
76 |
77 | computedNotify(func);
78 | return func;
79 | }
80 |
--------------------------------------------------------------------------------
/lib/reactive.js:
--------------------------------------------------------------------------------
1 | import { computedQueue, computedI, computedNotify } from "./computed.js";
2 | import {
3 | isObject, isFunction,
4 | hasOwnProperty, defineProperty,
5 | HINT_OBSERVE, HINT_DEPENDS, defineHint,
6 | MESSAGE_NOT_OBJECT, throwError
7 | } from "./util.js";
8 |
9 | /**
10 | * Generates a property descriptor for a reactive property
11 | * @param value Initial property value
12 | * @returns Property descriptor object
13 | */
14 | function reactiveProperty(value) {
15 | if (isObject(value)) reactiveObserve(value);
16 |
17 | // List of computed functions that depend on this property
18 | var depends = [];
19 | /**
20 | * Remove a computed function from this reactive property
21 | * @param func Computed function to remove
22 | */
23 | function dependsRemove(func) {
24 | var i = depends.lastIndexOf(func);
25 | if (i >= 0) depends.splice(i, 1);
26 | }
27 |
28 | return {
29 | get: function() {
30 | // Add the current executing computed function to this reactive property's dependencies
31 | var func = computedQueue[computedI];
32 | if (func) {
33 | var i = depends.lastIndexOf(func);
34 | if (i < 0) {
35 | // Add them to our dependencies
36 | depends.push(func);
37 | // Add us to their dependants
38 | func[HINT_DEPENDS].push(dependsRemove);
39 | }
40 | }
41 |
42 | return value;
43 | },
44 | set: function(newValue) {
45 | if (isObject(newValue)) reactiveObserve(newValue);
46 | value = newValue;
47 |
48 | // Notify all dependencies
49 | for (var i = 0; i < depends.length; i++) {
50 | computedNotify(depends[i]);
51 | }
52 | }
53 | };
54 | }
55 |
56 | /**
57 | * Observes an object by making all of its enumerable properties reactive
58 | * @param object Object to observe
59 | */
60 | function reactiveObserve(object) {
61 | if (hasOwnProperty(object, HINT_OBSERVE)) return;
62 | defineHint(object, HINT_OBSERVE);
63 |
64 | for (var key in object) {
65 | if (hasOwnProperty(object, key)) {
66 | try {
67 | defineProperty(object, key, reactiveProperty(object[key]));
68 | } catch (err) {}
69 | }
70 | }
71 | }
72 |
73 | /** See lib/patella.d.ts */
74 | export function observe(object) {
75 | if (!isObject(object) && !isFunction(object)) {
76 | throwError(MESSAGE_NOT_OBJECT);
77 | }
78 |
79 | reactiveObserve(object);
80 | return object;
81 | }
82 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { minify as minifyTerser } from "terser";
2 | import { minify as minifyUglify } from "uglify-js";
3 |
4 | import { mkdir, writeFile } from "fs/promises";
5 |
6 | const uglify = {
7 | name: "uglify",
8 | config: {
9 | ie8: true,
10 | webkit: true,
11 | v8: true,
12 | compress: {
13 | passes: 4,
14 | hoist_funs: true,
15 | hoist_vars: true,
16 | hoist_props: true,
17 | inline: 2,
18 | reduce_funcs: false,
19 | typeofs: false,
20 | unsafe_comps: true
21 | },
22 | output: {
23 | comments: true
24 | }
25 | },
26 | renderChunk(code, chunk, options) {
27 | const result = minifyUglify(code, {
28 | ...uglify.config,
29 | sourceMap: !!options.sourcemap,
30 | toplevel: /^c(ommon)?js$/.test(options.format)
31 | });
32 | return result.error ? Promise.reject(result.error) : Promise.resolve(result);
33 | }
34 | };
35 |
36 | const terser = {
37 | name: "terser",
38 | config: {
39 | ecma: 5,
40 | ie8: true,
41 | safari10: true,
42 | compress: {
43 | passes: 4,
44 | collapse_vars: true,
45 | hoist_funs: true,
46 | hoist_vars: true,
47 | hoist_props: true,
48 | typeofs: false,
49 | unsafe_comps: true,
50 | pure_funcs: [
51 | "hasOwnProperty", "createSymbol", "isObject", "isFunction"
52 | ]
53 | }
54 | },
55 | renderChunk(code, chunk, options) {
56 | return minifyTerser(code, {
57 | ...terser.config,
58 | sourceMap: !!options.sourcemap,
59 | module: /^(esm?|module)$/.test(options.format),
60 | toplevel: /^c(ommon)?js$/.test(options.format)
61 | });
62 | }
63 | };
64 |
65 | const packageCJS = {
66 | name: "package-cjs",
67 | async buildEnd(error) {
68 | if (error) return;
69 | try { await mkdir("./dist"); } catch (error) {}
70 | await writeFile("./dist/package.json", `{ "type": "commonjs" }\n`);
71 | }
72 | };
73 |
74 | const input = (input, plugins = [], ...output) => ({ input, output, plugins });
75 | const output = (file, format, sourcemap = true, plugins = []) => ({
76 | file,
77 | format,
78 | sourcemap,
79 | plugins,
80 | name: "Patella",
81 | indent: " ",
82 | esModule: false,
83 | strict: false,
84 | freeze: false
85 | });
86 |
87 | export default input("lib/patella.js", [packageCJS],
88 | output("dist/patella.cjs.js", "cjs"),
89 | output("dist/patella.iife.min.js", "iife", "hidden", [uglify, terser])
90 | );
91 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | // Global object/function references
2 | var _Object = Object;
3 | var _Object_hasOwnProperty = _Object.hasOwnProperty;
4 | var _Array_isArray = Array.isArray;
5 | var _Error = Error;
6 | var _TypeError = TypeError;
7 |
8 | /** Reference to global Object.defineProperty */
9 | export var defineProperty = _Object.defineProperty;
10 |
11 | /**
12 | * Checks if an object has a specified property as its own property (ignores prototype properties and `__proto__`)
13 | * @param object Object to check
14 | * @param key Property key
15 | * @returns Does `object` have the property `key`?
16 | */
17 | export function hasOwnProperty(object, key) {
18 | return key !== "__proto__" && _Object_hasOwnProperty.call(object, key);
19 | }
20 |
21 | /**
22 | * Creates an ECMAScript 6 Symbol, falling back to a simple string in environments that do not support Symbols
23 | * @param description Symbol description
24 | * @returns Symbol object or string
25 | * @function
26 | */
27 | /* c8 ignore start */
28 | /* istanbul ignore next */
29 | var createSymbol =
30 | typeof Symbol === "function"
31 | ? Symbol
32 | : function (description) {
33 | return "__" + description;
34 | };
35 | /* c8 ignore stop */
36 |
37 | /** Hint property to indicate if an object has been observed */
38 | export var HINT_OBSERVE = createSymbol("observe");
39 | /** Hint property to indicate if a function has been disposed */
40 | export var HINT_DISPOSE = createSymbol("dispose");
41 | /** Hint property that contains a function's dependency disposal callbacks */
42 | export var HINT_DEPENDS = createSymbol("depends");
43 |
44 | /**
45 | * Defines a hint property on an object
46 | * @param object Object to define property on
47 | * @param hint Property key
48 | * @param {*} [value] Property value, property will be made non-configurable if this is unset (`undefined`)
49 | */
50 | export function defineHint(object, hint, value) {
51 | defineProperty(object, hint, {
52 | value: value,
53 | configurable: value !== void 0,
54 | enumerable: false,
55 | writable: false
56 | });
57 | }
58 |
59 | /**
60 | * Checks if a value is a normal object, ignores functions and arrays
61 | * @param value Value to check
62 | * @returns Is `value` a normal object?
63 | */
64 | export function isObject(value) {
65 | return value !== null && typeof value === "object" && !_Array_isArray(value);
66 | }
67 |
68 | /**
69 | * Checks if a value is a function
70 | * @param value Value to check
71 | * @return Is `value` a function?
72 | */
73 | export function isFunction(value) {
74 | return typeof value === "function";
75 | }
76 |
77 | /** Error message printed when an argument is of an incorrect type (not a normal object) */
78 | export var MESSAGE_NOT_OBJECT = "Argument 'object' is not an object";
79 | /** Error message printed when an argument is of an incorrect type (not a function) */
80 | export var MESSAGE_NOT_FUNCTION = "Argument 'func' is not a function";
81 |
82 | /**
83 | * Throws an error message
84 | * @param message Message to construct the error with
85 | * @param generic Should the more generic `Error` be thrown instead of `TypeError`?
86 | */
87 | export function throwError(message, generic) {
88 | throw new (generic ? _Error : _TypeError)(message);
89 | }
90 |
--------------------------------------------------------------------------------
/examples/pony.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Patella Example - Pony Browser
8 |
9 |
10 |
11 |
12 | Pony Browser
13 |
14 |
15 |
16 |
17 |
125 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | # Patella 🔁
26 | Patella, formerly known as Luar, is a library for reactive programming in JavaScript, inspired by [Hyperactiv](https://github.com/elbywan/hyperactiv) and [Vue.js](https://vuejs.org/).
27 | Patella is compatible with Chrome 5, Firefox 4, and Internet Explorer 9.
28 |
29 | The [patellar tendon is responsible for the well known "knee-jerk reaction"](https://wikipedia.org/wiki/Patellar_reflex).
30 |
31 | Jump to one of:
32 | - [Installation](#installation)
33 | - [Usage](#usage)
34 | - [Examples and snippets](#examples-and-snippets)
35 | - [Pitfalls](#pitfalls)
36 | - [API](#api)
37 | - [Authors](#authors)
38 | - [License](#license)
39 |
40 | ## Installation
41 | Patella is available via [npm](https://www.npmjs.com/package/patella):
42 | ```sh
43 | $ npm install patella
44 | ```
45 | ```javascript
46 | // ECMAScript module environments
47 | import { observe, ignore, computed, dispose } from "patella";
48 | // CommonJS environments
49 | const { observe, ignore, computed, dispose } = require("patella");
50 | ```
51 |
52 | Or, for people working without a bundler, it can be included from [UNPKG](https://www.unpkg.com/browse/patella@latest/):
53 | ```html
54 |
55 |
61 | ```
62 |
63 | Various other Patella builds are available in the [dist](./dist) folder, including sourcemaps and minified versions.
64 | Minification is performed using both [Terser](https://github.com/terser/terser) and [UglifyJS](https://github.com/mishoo/UglifyJS) using custom configurations designed for a balance of speed and size (Patella is a micro-library at 900~ bytes gzipped).
65 |
66 | ## Usage
67 | Patella provides functions for observing object mutations and acting on those mutations automatically.
68 | Possibly the best way to learn is by example, so let's take a page out of [Vue.js's guide](https://vuejs.org/v2/guide/events.html) and make a button that counts how many times it has been clicked using Patella's `observe(object)` and `computed(func)`:
69 | ```html
70 | Click Counter
71 |
72 |
83 | ```
84 | 
85 | View the [full source](./examples/counter.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/hL6g4emk/latest).
86 |
87 | Notice how in the above example, the `` doesn't do any extra magic to change its text when clicked; it just increments the model's click counter, which is "connected" to the button's text in the computed function.
88 |
89 | Now let's try doing some math, here's a snippet that adds and multiplies two numbers:
90 | ```javascript
91 | const calculator = Patella.observe({
92 | left: 1,
93 | right: 1,
94 | sum: 0,
95 | product: 0
96 | });
97 |
98 | // Connect left, right -> sum
99 | Patella.computed(() => calculator.sum = calculator.left + calculator.right);
100 | // Connect left, right -> product
101 | Patella.computed(() => calculator.product = calculator.left * calculator.right);
102 |
103 | calculator.left = 2;
104 | calculator.right = 10;
105 | console.log(calculator.sum, calculator.product); // Output: 12 20
106 |
107 | calcuator.left = 3;
108 | console.log(calculator.sum, calculator.product); // Output: 13 30
109 | ```
110 | Pretty cool, right?
111 | Patella's main goal is to be as simple as possible; you only need two functions to build almost anything.
112 |
113 | ## Examples and snippets
114 | Jump to one of:
115 | - [Concatenator](#concatenator)
116 | - [Debounced search](#debounced-search)
117 | - [Pony browser](#pony-browser)
118 | - [Multiple objects snippet](#multiple-objects-snippet)
119 | - [Linked computed functions snippet](#linked-computed-functions-snippet)
120 |
121 | ### Concatenator
122 | ```html
123 | Concatenator
124 |
125 |
126 |
127 |
141 | ```
142 | 
143 | View the [full source](./examples/concatenator.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/zvnm4jp7/latest).
144 |
145 | ### Debounced search
146 | ```html
147 | Debounced Search
148 |
149 |
150 |
171 | ```
172 | 
173 | View the [full source](./examples/debounce.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/abd3qxft/latest).
174 |
175 | ### Pony browser
176 | ```html
177 |
178 | Pony Browser
179 |
180 |
181 |
182 |
183 |
234 | ```
235 | 
236 | View the [full source](./examples/pony.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/84wmaz0g/latest).
237 |
238 | ## Multiple objects snippet
239 | ```javascript
240 | // Setting up some reactive objects that contain some data about a US president...
241 | // Disclaimer: I am not an American :P
242 | const person = Patella.observe({
243 | name: { first: "George", last: "Washington" },
244 | age: 288
245 | });
246 | const account = Patella.observe({
247 | user: "big-george12",
248 | password: "IHateTheQueen!1"
249 | });
250 |
251 | // Declare that we will output a log message whenever person.name.first, account.user, or person.age are updated
252 | Patella.computed(() => console.log(
253 | `${person.name.first}'s username is ${account.user} (${person.age} years old)`
254 | )); // Output: George's username is big-george12 (288 years old)
255 |
256 | // Changing reactive properties will only run computed functions that depend on them
257 | account.password = "not-telling"; // Does not output (no computed function depends on this)
258 |
259 | // All operators work when updating properties
260 | account.user += "3"; // Output: George's username is big-george123 (288 years old)
261 | person.age++; // Output: George's username is big-george123 (289 years old)
262 |
263 | // You can even replace objects entirely
264 | // This will automatically observe this new object and will still trigger dependant computed functions
265 | // Note: You should ideally use ignore or dispose to prevent depending on objects that get replaced, see pitfalls
266 | person.name = {
267 | first: "Abraham",
268 | last: "Lincoln"
269 | }; // Output: Abraham's username is big-george123 (289 years old)
270 |
271 | person.name.first = "Thomas"; // Output: Thomas's username is big-george123 (289 years old)
272 | ```
273 |
274 | ### Linked computed functions snippet
275 | ```javascript
276 | // Create our nums object, with some default values for properties that will be computed
277 | const nums = Patella.observe({
278 | a: 33, b: 23, c: 84,
279 | x: 0,
280 | sumAB: 0, sumAX: 0, sumCX: 0,
281 | sumAllSums: 0
282 | });
283 |
284 | // Declare that (x) will be equal to (a + b + c)
285 | Patella.computed(() => nums.x = nums.a + nums.b + nums.c);
286 | // Declare that (sumAB) will be equal to (a + b)
287 | Patella.computed(() => nums.sumAB = nums.a + nums.b);
288 | // Declare that (sumAX) will be equal to (a + x)
289 | Patella.computed(() => nums.sumAX = nums.a + nums.x);
290 | // Declare that (sumCX) will be equal to (c + x)
291 | Patella.computed(() => nums.sumCX = nums.c + nums.x);
292 | // Declare that (sumAllSums) will be equal to (sumAB + sumAX + sumCX)
293 | Patella.computed(() => nums.sumAllSums = nums.sumAB + nums.sumAX + nums.sumCX);
294 |
295 | // Now lets check the (sumAllSums) value
296 | console.log(nums.sumAllSums); // Output: 453
297 |
298 | // Notice that when we update one value ...
299 | nums.c += 2;
300 | // ... all the other values update! (since we declared them as such)
301 | console.log(nums.sumAllSums); // Output: 459
302 | ```
303 |
304 | ## Pitfalls
305 | Patella uses JavaScript's [getters](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Functions/get)[ and ](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)[setters](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Functions/set) to make all the reactivity magic possible, which comes with some tradeoffs that other libraries like [Hyperactiv](https://github.com/elbywan/hyperactiv) (which uses [Proxy](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy)) don't have to deal with.
306 | This section details some of the stuff to look out for when using Patella in your applications.
307 |
308 | ### Computed functions can cause infinite loops
309 | ```javascript
310 | const object = Patella.observe({ x: 10, y: 20 });
311 |
312 | Patella.computed(function one() {
313 | if (object.x > 20) object.y++;
314 | });
315 |
316 | Patella.computed(function two() {
317 | if (object.y > 20) object.x++;
318 | });
319 |
320 | object.x = 25;
321 | // Uncaught Error: Computed queue overflow! Last 10 functions in the queue:
322 | // 1993: one
323 | // 1994: two
324 | // 1995: one
325 | // 1996: two
326 | // 1997: one
327 | // 1998: two
328 | // 1999: one
329 | // 2000: two
330 | // 2001: one
331 | // 2002: two
332 | // 2003: one
333 | ```
334 |
335 | ### Array mutations do not trigger dependencies
336 | ```javascript
337 | const object = Patella.observe({
338 | array: [1, 2, 3]
339 | });
340 |
341 | Patella.computed(() => console.log(object.array)); // Output: 1,2,3
342 |
343 | object.array[2] = 4; // No output, arrays are not reactive!
344 | object.array.push(5); // Still no output, as Patella does not replace array methods
345 |
346 | // If you want to use arrays, do it like this:
347 | // 1. Run your operations
348 | object.array[2] = 3;
349 | object.array[3] = 4;
350 | object.array.push(5);
351 | // 2. Then set the array to itself
352 | object.array = object.array; // Output: 1,2,3,4,5
353 | ```
354 |
355 | ### Properties added after observation are not reactive
356 | ```javascript
357 | const object = Patella.observe({ x: 10 });
358 | object.y = 20;
359 |
360 | Patella.computed(() => console.log(object.x)); // Output: 10
361 | Patella.computed(() => console.log(object.y)); // Output: 20
362 |
363 | object.x += 2; // Output: 12
364 |
365 | object.y += 2; // No output, as this property was added after observation
366 |
367 | Patella.observe(object);
368 |
369 | object.y += 2; // Still no output, as objects cannot be re-observed
370 | ```
371 |
372 | ### Prototypes will not be made reactive unless explicitly observed
373 | ```javascript
374 | const object = { a: 20 };
375 | const prototype = { b: 10 };
376 | Object.setPrototypeOf(object, prototype);
377 |
378 | Patella.observe(object);
379 |
380 | Patella.computed(() => console.log(object.a)); // Output: 10
381 | Patella.computed(() => console.log(object.b)); // Output: 20
382 |
383 | object.a = 15; // Output: 15
384 |
385 | object.b = 30; // No output, as this isn't an actual property on the object
386 | prototype.b = 36; // No output, as prototypes are not made reactive by observe
387 |
388 | Patella.observe(prototype);
389 |
390 | prototype.b = 32; // Output: 32
391 | ```
392 |
393 | ### Non-enumerable and non-configurable properties will not be made reactive
394 | ```javascript
395 | const object = { x: 1 };
396 | Object.defineProperty(object, "y", {
397 | configurable: true,
398 | enumerable: false,
399 | value: 2
400 | });
401 | Object.defineProperty(object, "z", {
402 | configurable: false,
403 | enumerable: true,
404 | value: 3
405 | });
406 |
407 | Patella.observe(object);
408 |
409 | Patella.computed(() => console.log(object.x)); // Output: 1
410 | Patella.computed(() => console.log(object.y)); // Output: 2
411 | Patella.computed(() => console.log(object.z)); // Output: 3
412 |
413 | object.x--; // Output: 0
414 |
415 | object.y--; // No output as this property is non-enumerable
416 | object.z--; // No output as this property is non-configurable
417 | ```
418 |
419 | ### Enumerable and configurable but non-writable properties will be made writable
420 | ```javascript
421 | const object = {};
422 | Object.defineProperty(object, "val", {
423 | configurable: true,
424 | enumerable: true,
425 | writable: false,
426 | value: 10
427 | });
428 |
429 | object.val = 20; // Does nothing
430 | console.log(object.val); // Output: 10
431 |
432 | Patella.observe(object);
433 |
434 | object.val = 20; // Works because the property descriptor has been overwritten
435 | console.log(object.val); // Output: 20
436 | ```
437 |
438 | ### Getter/setter properties will be accessed then lose their getter/setters
439 | ```javascript
440 | const object = {
441 | get val() {
442 | console.log("Gotten!");
443 | return 10;
444 | }
445 | };
446 |
447 | object.val; // Output: Gotten!
448 |
449 | Patella.observe(object); // Output: Gotten!
450 |
451 | object.val; // No output as the getter has been overwritten
452 | ```
453 |
454 | ### Properties named `__proto__` are ignored
455 | ```javascript
456 | const object = {};
457 | Object.defineProperty(object, "__proto__", {
458 | configurable: true,
459 | enumerable: true,
460 | writable: true,
461 | value: 10
462 | });
463 |
464 | Patella.observe(object);
465 |
466 | Patella.computed(() => console.log(object.__proto__)); // Output: 10
467 |
468 | object.__proto__++; // No output as properties named __proto__ are ignored
469 | ```
470 |
471 | ## API
472 | function observe(object)
473 | Description:
474 |
475 |
476 | Makes an object and its properties reactive recursively.
477 | Subobjects (but not subfunctions!) will also be observed.
478 | Note that observe does not create a new object, it mutates the object passed into it: observe(object) === object.
479 |
480 |
481 | Parameters:
482 |
483 | object — Object or function to make reactive
484 |
485 | Returns:
486 |
487 | Input object, now reactive
488 |
489 |
490 | function ignore(object)
491 | Description:
492 |
493 |
494 | Prevents an object from being made reactive, observe will do nothing.
495 | Note that ignore is not recursive, so subobjects can still be made reactive by calling observe on them directly.
496 |
497 |
498 | Parameters:
499 |
500 | object — Object or function to ignore
501 |
502 | Returns:
503 |
504 | Input object, now permanently ignored
505 |
506 |
507 | function computed(func)
508 | Description:
509 |
510 |
511 | Calls func with no arguments and records a list of all the reactive properties it accesses.
512 | func will then be called again whenever any of the accessed properties are mutated.
513 | Note that if func has been disposed with !!clean === false, no operation will be performed.
514 |
515 |
516 | Parameters:
517 |
518 | func — Function to execute
519 |
520 | Returns:
521 |
524 |
525 | function dispose(func, clean)
526 | Description:
527 |
528 |
529 | "Disposes" a function that was run with computed, deregistering it so that it will no longer be called whenever any of its accessed reactive properties update.
530 | The clean parameter controls whether calling computed with func will work or no-op.
531 |
532 |
533 | Parameters:
534 |
535 | func — Function to dispose, omit to dispose the currently executing computed function
536 | clean — If truthy, only deregister the function from all dependencies, but allow it to be used with computed again in the future
537 |
538 | Returns:
539 |
540 | Input func if func is valid, otherwise undefined
541 |
542 |
543 | ## Authors
544 | Made with ❤ by Lua MacDougall ([foxgirl.dev](https://foxgirl.dev/))
545 |
546 | ## License
547 | This project is licensed under [MIT](LICENSE).
548 | More info in the [LICENSE](LICENSE) file.
549 |
550 | "A short, permissive software license. Basically, you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. There are many variations of this license in use." - [tl;drLegal](https://tldrlegal.com/license/mit-license)
551 |
--------------------------------------------------------------------------------
/test/patella.test.js:
--------------------------------------------------------------------------------
1 | import chai from "chai";
2 | const { assert } = chai;
3 |
4 | import { observe, ignore, computed, dispose } from "../lib/patella.js";
5 |
6 | suite("normal usage", () => {
7 |
8 | test("observed objects can have computed functions attached to them", () => {
9 | const doubler = observe({ number: 10, doubledNumber: undefined });
10 | computed(() => doubler.doubledNumber = doubler.number * 2);
11 |
12 | assert.strictEqual(doubler.doubledNumber, 20);
13 | doubler.number = 20;
14 | assert.strictEqual(doubler.doubledNumber, 40);
15 | });
16 |
17 | test("multiple computed functions attached to one reactive object", () => {
18 | const increments = observe({ x: 0, y: 0, z: 0 });
19 | computed(() => increments.y = increments.x + 1);
20 | computed(() => increments.z = increments.y + 1);
21 |
22 | assert.deepEqual(increments, { x: 0, y: 1, z: 2 });
23 | increments.x = 30;
24 | assert.deepEqual(increments, { x: 30, y: 31, z: 32 });
25 | });
26 |
27 | test("one computed function attached to multiple reactive objects", () => {
28 | const word1 = observe({ word: "Hello" });
29 | const word2 = observe({ word: "world" });
30 | let sentence; computed(() => sentence = word1.word + " " + word2.word);
31 |
32 | assert.strictEqual(sentence, "Hello world");
33 | word2.word = "you";
34 | assert.strictEqual(sentence, "Hello you");
35 | });
36 |
37 | test("computed functions can depend on multiple properties of a reactive object", () => {
38 | const myName = observe({
39 | fullName: "",
40 | firstName: "",
41 | middleNames: [],
42 | lastName: ""
43 | });
44 | computed(() => {
45 | const names = [];
46 |
47 | if (myName.firstName) names.push(myName.firstName);
48 | names.push(...myName.middleNames);
49 | if (myName.lastName) names.push(myName.lastName);
50 |
51 | myName.fullName = names.join(" ");
52 | });
53 |
54 | assert.strictEqual(myName.fullName, "");
55 | myName.firstName = "Lua";
56 | assert.strictEqual(myName.fullName, "Lua");
57 | myName.lastName = "MacDougall";
58 | assert.strictEqual(myName.fullName, "Lua MacDougall");
59 | myName.middleNames.push("\"WTF\""); // Does not notify dependencies
60 | myName.middleNames = myName.middleNames; // Forcefully notifies dependencies
61 | assert.strictEqual(myName.fullName, "Lua \"WTF\" MacDougall");
62 | });
63 |
64 | test("observe is recursive and will make subobjects reactive", () => {
65 | const coolness = observe({
66 | you: { cool: false },
67 | me: { cool: true },
68 | superCool: false
69 | });
70 | computed(() => coolness.superCool = coolness.you.cool && coolness.me.cool);
71 |
72 | assert.isFalse(coolness.superCool);
73 | coolness.you.cool = true;
74 | assert.isTrue(coolness.superCool);
75 | });
76 |
77 | test("setting an object onto a reactive property makes the object reactive", () => {
78 | const summit = observe({ nums: null, total: null });
79 | summit.nums = { a: 10, b: 20, c: 30 };
80 | computed(() =>
81 | summit.total =
82 | summit.nums.a
83 | + summit.nums.b
84 | + summit.nums.c
85 | );
86 |
87 | assert.strictEqual(summit.total, 60);
88 | summit.nums.b -= 10;
89 | assert.strictEqual(summit.total, 50);
90 | });
91 |
92 | test("setting an object onto a reactive property makes the entire object tree reactive", () => {
93 | const config = observe({ db: null });
94 | const db = {
95 | location: "mongodb://192.168.1.24"
96 | };
97 | const login = db.login = {
98 | user: "example",
99 | password: "password123!",
100 | string: ""
101 | };
102 | config.db = db;
103 | computed(() => config.db.login.string = config.db.login.user + ":" + config.db.login.password);
104 |
105 | assert.strictEqual(config.db.login.string, "example:password123!");
106 | login.password = "nottelling12";
107 | assert.strictEqual(config.db.login.string, "example:nottelling12");
108 | });
109 |
110 | test("multiple (implicitly) reactive objects with one computed function using iterators", () => {
111 | const nums = observe({ x: null, y: null, z: null, product: null });
112 | const numSet = () => ({ a: 10, b: 20, c: 30 });
113 | nums.x = numSet();
114 | nums.y = numSet();
115 | nums.z = numSet();
116 | computed(() => {
117 | let product;
118 | for (var numKey in nums) {
119 | var numSet = nums[numKey];
120 | for (var key in numSet) {
121 | product = product ? product * numSet[key] : numSet[key];
122 | }
123 | }
124 | nums.product = product;
125 | });
126 |
127 | assert.strictEqual(nums.product, 216000000000);
128 | nums.y.b += 2;
129 | assert.strictEqual(nums.product, 237600000000);
130 | });
131 |
132 | test("replace objects in reactive properties works as expected (but may leak the old object)", () => {
133 | const database = observe({ user: null });
134 | database.user = {
135 | login: "luawtf",
136 | password: "uwu",
137 | attempt: { password: null, correct: false }
138 | };
139 | computed(() =>
140 | database.user.attempt.correct =
141 | database.user.attempt.password === database.user.password
142 | );
143 |
144 | assert.isFalse(database.user.attempt.correct);
145 | database.user.attempt.password = "uwu";
146 | assert.isTrue(database.user.attempt.correct);
147 | // Next line is bad practice, will leak the original `database.user.attempt`
148 | // as the computed function still depends on it, consider cleaning up using
149 | // dispose (clean mode) and then computed
150 | database.user.attempt = { password: "owo", correct: true };
151 | assert.isFalse(database.user.attempt.correct);
152 | database.user.attempt.password = "uwu";
153 | assert.isTrue(database.user.attempt.correct);
154 | });
155 |
156 | test("ignored objects are still valid and can be used in reactive properties", () => {
157 | const summers = observe({
158 | sum1: { a: null, b: null, sum: null },
159 | sum2: ignore({ a: null, b: null, sum: null })
160 | });
161 | const summer = sumObject =>
162 | computed(() => sumObject.sum = sumObject.a + sumObject.b);
163 | summer(summers.sum1);
164 | summer(summers.sum2);
165 |
166 | assert.deepEqual(summers, {
167 | sum1: { a: null, b: null, sum: 0 },
168 | sum2: { a: null, b: null, sum: 0 }
169 | });
170 | summers.sum1.a = 10;
171 | summers.sum1.b = 20;
172 | assert.strictEqual(summers.sum1.sum, 30);
173 | summers.sum2.a = 10;
174 | summers.sum2.b = 20;
175 | assert.strictEqual(summers.sum2.sum, 0);
176 | });
177 |
178 | test("ignore can be used to replace objects in reactive properties without leaking", () => {
179 | const vectors = observe({
180 | v1: ignore({}),
181 | v2: ignore({}),
182 | sum: null,
183 | product: null
184 | });
185 | computed(() =>
186 | vectors.sum = ignore({
187 | x: vectors.v1.x + vectors.v2.x,
188 | y: vectors.v1.y + vectors.v2.y,
189 | z: vectors.v1.z + vectors.v2.z
190 | })
191 | );
192 | computed(() =>
193 | vectors.product = ignore({
194 | x: vectors.v1.x * vectors.v2.x,
195 | y: vectors.v1.y * vectors.v2.y,
196 | z: vectors.v1.z * vectors.v2.z
197 | })
198 | );
199 |
200 | assert.deepEqual(vectors, {
201 | v1: {}, v2: {},
202 | sum: { x: NaN, y: NaN, z: NaN },
203 | product: { x: NaN, y: NaN, z: NaN }
204 | });
205 | vectors.v1 = ignore({ x: 20, y: 0, z: 0 });
206 | vectors.v2 = ignore({ x: 20, y: 20, z: 20 });
207 | assert.deepEqual(vectors.sum, { x: 40, y: 20, z: 20 });
208 | assert.deepEqual(vectors.product, { x: 400, y: 0, z: 0});
209 | });
210 |
211 | test("ignored objects are not reactive", () => {
212 | const squarer = observe({
213 | numberToSquare: ignore({ number: 10 }),
214 | square: null
215 | });
216 | const squareComputer = computed(() => {
217 | squarer.square = squarer.numberToSquare.number ** 2;
218 | });
219 |
220 | assert.strictEqual(squarer.square, 100);
221 | squarer.numberToSquare.number = 8;
222 | assert.strictEqual(squarer.square, 100);
223 | computed(squareComputer);
224 | assert.strictEqual(squarer.square, 64);
225 | });
226 |
227 | test("computed functions will notify eachother continuously", () => {
228 | const telephone = observe({
229 | one: null,
230 | two: null,
231 | three: null,
232 | four: null,
233 | five: null,
234 | six: null
235 | });
236 | computed(() => telephone.two = telephone.one);
237 | computed(() => telephone.three = telephone.two);
238 | computed(() => telephone.four = telephone.three);
239 | computed(() => telephone.five = telephone.four);
240 | computed(() => telephone.six = telephone.five);
241 |
242 | assert.deepEqual(Object.values(telephone), [,,,,,,].fill(null));
243 | telephone.one = "Hello!";
244 | assert.deepEqual(Object.values(telephone), [,,,,,,].fill("Hello!"));
245 | telephone.one = 1;
246 | assert.deepEqual(Object.values(telephone), [,,,,,,].fill(1));
247 | });
248 |
249 | test("computed functions created¬ified inside of another computed function (nested) are not executed immediately but added to the queue", () => {
250 | let executed = false;
251 | computed(() => {
252 | computed(() => {
253 | executed = true;
254 | });
255 | assert.isFalse(executed);
256 | });
257 | assert.isTrue(executed);
258 | });
259 |
260 | test("nested computed functions do not share dependencies", () => {
261 | const updateCounter = observe({
262 | multiplyBy: 2,
263 | valueToMultiply: 2,
264 | timesUpdated: 0
265 | });
266 | computed(() => {
267 | computed(() => {
268 | updateCounter.valueToMultiply *= updateCounter.multiplyBy;
269 | });
270 | updateCounter.timesUpdated++;
271 | });
272 |
273 | assert.strictEqual(updateCounter.timesUpdated, 1);
274 | assert.strictEqual(updateCounter.valueToMultiply, 4);
275 | updateCounter.multiplyBy = 4;
276 | assert.strictEqual(updateCounter.timesUpdated, 1);
277 | assert.strictEqual(updateCounter.valueToMultiply, 16);
278 | updateCounter.multiplyBy = 2;
279 | assert.strictEqual(updateCounter.timesUpdated, 1);
280 | assert.strictEqual(updateCounter.valueToMultiply, 32);
281 | updateCounter.multiplyBy = 5;
282 | assert.strictEqual(updateCounter.timesUpdated, 1);
283 | assert.strictEqual(updateCounter.valueToMultiply, 160);
284 | });
285 |
286 | test("nested computed functions where the inner function notifies the outer function will cause chaos", () => {
287 | const chaos = observe({ x: 10 });
288 | computed(() => {
289 | chaos.x;
290 | computed(() => {
291 | if (chaos.x < 100) chaos.x++;
292 | });
293 | });
294 |
295 | assert.strictEqual(chaos.x, 100);
296 | });
297 |
298 | test("dispose removes all the dependencies from a computed function", () => {
299 | const numberCopier = observe({
300 | in: 0,
301 | out: 0
302 | });
303 | const numberCopierComputer = computed(() => {
304 | numberCopier.out = numberCopier.in;
305 | });
306 |
307 | assert.strictEqual(numberCopier.out, 0);
308 | numberCopier.in = 10;
309 | assert.strictEqual(numberCopier.out, 10);
310 | dispose(numberCopierComputer);
311 | numberCopier.in = 20;
312 | assert.strictEqual(numberCopier.out, 10);
313 | });
314 |
315 | test("dispose disposes and returns its first argument", () => {
316 | const counter = observe({
317 | value: 0,
318 | times: 0
319 | });
320 | computed(dispose(() => { // Does nothing, as computed ignores disposed funcs
321 | counter.value;
322 | counter.times++;
323 | }));
324 |
325 | assert.strictEqual(counter.times, 0);
326 | counter.value++;
327 | assert.strictEqual(counter.times, 0);
328 | });
329 |
330 | test("disposed functions cannot be manually notified", () => {
331 | const concat = observe({ start: "", end: "", full: "" });
332 | const concatComputed = computed(() => concat.full = concat.start + concat.end);
333 |
334 | assert.strictEqual(concat.full, "");
335 | concat.start = "Hello, ";
336 | concat.end = "world!";
337 | assert.strictEqual(concat.full, "Hello, world!");
338 | dispose(concatComputed);
339 | concat.start = "Goodbye, ";
340 | assert.strictEqual(concat.full, "Hello, world!");
341 | computed(concatComputed);
342 | concat.start = concat.start;
343 | assert.strictEqual(concat.full, "Hello, world!");
344 | });
345 |
346 | test("dispose called without an argument uses the current computed function", () => {
347 | const countToFour = observe({
348 | number: 0
349 | });
350 | function countByOne() {
351 | countToFour.number++;
352 | if (countToFour.number === 4) dispose();
353 | }
354 |
355 | assert.strictEqual(countToFour.number, 0);
356 | computed(countByOne);
357 | assert.strictEqual(countToFour.number, 1);
358 | computed(countByOne);
359 | assert.strictEqual(countToFour.number, 2);
360 | computed(countByOne);
361 | assert.strictEqual(countToFour.number, 3);
362 | computed(countByOne);
363 | assert.strictEqual(countToFour.number, 4);
364 | computed(countByOne); // No longer reactive
365 | assert.strictEqual(countToFour.number, 4);
366 | });
367 |
368 | test("dispose can be called in 'clean mode' which removes all dependencies but allows the computed function to be notified manually", () => {
369 | const rooter = observe({
370 | in: 0,
371 | out: 0
372 | });
373 | function rooterComputer() {
374 | rooter.out = Math.sqrt(rooter.in);
375 | }
376 | computed(rooterComputer);
377 |
378 | assert.strictEqual(rooter.out, 0);
379 | rooter.in = 64;
380 | assert.strictEqual(rooter.out, 8);
381 | dispose(rooterComputer, true);
382 | rooter.in = 4;
383 | assert.strictEqual(rooter.out, 8);
384 | computed(rooterComputer);
385 | assert.strictEqual(rooter.out, 2);
386 | });
387 |
388 | test("dispose usage in both clean and default mode", () => {
389 | const multiply = observe({ left: 0, right: 0, result: 0 });
390 | const divide = observe({ left: 0, right: 0, result: 0 });
391 | const computer = computed(() => {
392 | multiply.result = multiply.left * multiply.right;
393 | divide.result = divide.left / divide.right;
394 | });
395 |
396 | assert.strictEqual(multiply.result, 0);
397 | assert.isNaN(divide.result);
398 | multiply.left = 10;
399 | multiply.right = 2;
400 | assert.strictEqual(multiply.result, 20);
401 | divide.left = 4;
402 | divide.right = 2;
403 | assert.strictEqual(divide.result, 2);
404 | dispose(computer, true);
405 | multiply.left = 20;
406 | assert.strictEqual(multiply.result, 20);
407 | divide.left = 8;
408 | assert.strictEqual(divide.result, 2);
409 | computed(computer);
410 | assert.strictEqual(multiply.result, 40);
411 | assert.strictEqual(divide.result, 4);
412 | multiply.right = 3;
413 | assert.strictEqual(multiply.result, 60);
414 | divide.right = 3;
415 | assert.strictEqual(divide.result, 8 / 3);
416 | dispose(computer);
417 | });
418 |
419 | test("disposing a computed function that was notified (with execution pending) will cause it to be removed from the queue", () => {
420 | const power = observe({ in: 0, pow2: 0, pow4: 0 });
421 | function pow2() {
422 | power.pow2 = power.in ** 2;
423 | if (power.pow2 >= 64) dispose(pow4);
424 | }
425 | function pow4() {
426 | power.pow4 = power.pow2 ** 2;
427 | }
428 | computed(pow2);
429 | computed(pow4);
430 |
431 | assert.deepEqual(power, { in: 0, pow2: 0, pow4: 0 });
432 | power.in = 4;
433 | assert.deepEqual(power, { in: 4, pow2: 16, pow4: 256 });
434 | power.in = 6;
435 | assert.deepEqual(power, { in: 6, pow2: 36, pow4: 1296 });
436 | power.in = 8;
437 | assert.deepEqual(power, { in: 8, pow2: 64, pow4: 1296 });
438 | power.in = 6;
439 | assert.deepEqual(power, { in: 6, pow2: 36, pow4: 1296 });
440 | });
441 |
442 | });
443 |
444 | suite("causing problems :)", () => {
445 |
446 | const nonObjects = [
447 | undefined, null, false, true,
448 | 0, 1, -1, 0.5, -0.5,
449 | "", "Hello, world.",
450 | 0n, 1n, 1_000_000_000_000_000_000_000_000_000_000n,
451 | Symbol(), Symbol("Example"),
452 | [], [1, 2, 3, 4, 5], [
453 | "Arrays are not considered objects by isObject",
454 | "This is to prevent silly things like trying to observe an array with thousands of elements",
455 | "Or the fact that observation would most definitely break many important array features"
456 | ]
457 | ];
458 |
459 | const nonFunctions = [
460 | ...nonObjects,
461 | {}, { x: 10, y: true }, { obj: { hello() {} }, goodbye() {} },
462 | new Date(), new RegExp("test"), new Map()
463 | ];
464 |
465 | test("observe fails if passed a non-object (not including arrays) value", () => {
466 | nonObjects.forEach(val => assert.throws(() => observe(val)));
467 | });
468 |
469 | test("ignore fails if passed a non-object (not including arrays) value", () => {
470 | nonObjects.forEach(val => assert.throws(() => ignore(val)));
471 | });
472 |
473 | test("computed fails if passed a non-callable value", () => {
474 | nonFunctions.forEach(val => assert.throws(() => computed(val)));
475 | });
476 |
477 | test("computed fails if you create an infinite loop with two computed functions that depend on eachother", () => {
478 | const object = observe({ x: 10, y: 20 });
479 | computed(() => {
480 | object.x = object.y + 1;
481 | });
482 | function oops() {
483 | object.y = object.x + 1;
484 | }
485 |
486 | assert.throws(() => computed(oops));
487 | });
488 |
489 | test("dispose fails if passed a non-callable value", () => {
490 | nonFunctions.forEach(val => assert.throws(() => dispose(val)));
491 | });
492 |
493 | test("dispose fails if passed no arguments while no computed function is executing", () => {
494 | assert.throws(() => dispose());
495 | });
496 |
497 | });
498 |
499 | suite("edge cases", () => {
500 |
501 | test("properties added after observation are not reactive", () => {
502 | const object = observe({});
503 | object.val = 10;
504 | let val; computed(() => val = object.val);
505 |
506 | assert.strictEqual(val, 10);
507 | object.val = 20; // Not reactive
508 | assert.strictEqual(val, 10);
509 | });
510 |
511 | test("objects cannot be reobserved to make properties added after observation reactive", () => {
512 | const object = observe({});
513 | object.val = 10;
514 | observe(object); // Does nothing
515 | let val; computed(() => val = object.val);
516 |
517 | assert.strictEqual(val, 10);
518 | object.val = 20; // Not reactive
519 | assert.strictEqual(val, 10);
520 | });
521 |
522 | test("observe does not reactify prototypes", () => {
523 | const object = observe(Object.setPrototypeOf({ x: 10 }, { y: 20 }));
524 | let times = 0; computed(() => (object.x, object.y, times++));
525 |
526 | assert.strictEqual(object.x, 10);
527 | assert.strictEqual(object.y, 20);
528 | assert.strictEqual(times, 1);
529 |
530 | object.x = 30;
531 | assert.strictEqual(times, 2);
532 | object.y = 30; // Not reactive
533 | assert.strictEqual(times, 2);
534 | object.x = 50;
535 | assert.strictEqual(times, 3);
536 | object.y = 100; // Not reactive
537 | assert.strictEqual(times, 3);
538 | });
539 |
540 | test("prototypes can be observed but their children will not be made reactive", () => {
541 | const object = Object.setPrototypeOf({ x: 10 }, observe({ y: 20 }));
542 | let times = 0; computed(() => (object.x, object.y, times++));
543 |
544 | assert.strictEqual(object.x, 10);
545 | assert.strictEqual(object.y, 20);
546 | assert.strictEqual(times, 1);
547 |
548 | object.x = 30; // Not reactive
549 | assert.strictEqual(times, 1);
550 | object.y = 30;
551 | assert.strictEqual(times, 2);
552 | object.x = 50; // Not reactive
553 | assert.strictEqual(times, 2);
554 | object.y = 100;
555 | assert.strictEqual(times, 3);
556 | });
557 |
558 | test("properties that share names with Object.prototype properties work as expected", () => {
559 | const object = observe({ hasOwnProperty: 10 });
560 | let val; computed(() => val = object.hasOwnProperty);
561 |
562 | assert.strictEqual(val, 10);
563 | object.hasOwnProperty = 20;
564 | assert.strictEqual(val, 20);
565 | });
566 |
567 | test("nonconfigurable properties will not be made reactive", () => {
568 | const object = observe(Object.defineProperty({}, "val", {
569 | configurable: false,
570 | enumerable: true,
571 | writable: true,
572 | value: 10
573 | }));
574 | let val; computed(() => val = object.val);
575 |
576 | assert.strictEqual(val, 10);
577 | object.val = 20; // Not reactive
578 | assert.strictEqual(val, 10);
579 | });
580 |
581 | test("nonenumerable properties will not be made reactive", () => {
582 | const object = observe(Object.defineProperty({}, "val", {
583 | configurable: true,
584 | enumerable: false,
585 | writable: true,
586 | value: 10
587 | }));
588 | let val; computed(() => val = object.val);
589 |
590 | assert.strictEqual(val, 10);
591 | object.val = 20; // Not reactive
592 | assert.strictEqual(val, 10);
593 | });
594 |
595 | test("nonwritable but enumerable and configurable properties will be overwritten and made writable", () => {
596 | const object = observe(Object.defineProperty({}, "val", {
597 | configurable: true,
598 | enumerable: true,
599 | writable: false,
600 | value: 10
601 | }));
602 |
603 | assert.strictEqual(object.val, 10);
604 | object.val = 20;
605 | assert.strictEqual(object.val, 20);
606 | });
607 |
608 | test("enumerable and configurable properties will remain enumerable and configurable", () => {
609 | const oldDescriptor = {
610 | configurable: true,
611 | enumerable: true,
612 | writable: false,
613 | value: 10
614 | };
615 | const object = observe(Object.defineProperty({}, "val", oldDescriptor));
616 | const newDescriptor = Object.getOwnPropertyDescriptor(object, "val");
617 |
618 | assert.strictEqual(!!newDescriptor.enumerable, !!oldDescriptor.enumerable);
619 | assert.strictEqual(!!newDescriptor.configurable, !!oldDescriptor.configurable);
620 | });
621 |
622 | test("no new enumerable properties are added", () => {
623 | const object = { a: 10, b: 20, c: 30 };
624 |
625 | const enumeratedKeysBefore = Object.keys(object).sort();
626 | observe(object);
627 | const enumeratedKeysAfter = Object.keys(object).sort();
628 |
629 | assert.deepEqual(enumeratedKeysBefore, enumeratedKeysAfter);
630 | });
631 |
632 | test("getter/setter properties will be accessed then overwritten", () => {
633 | let accessCount = 0;
634 | const object = {
635 | get val() {
636 | accessCount++;
637 | return 10;
638 | }
639 | };
640 |
641 | assert.strictEqual(accessCount, 0);
642 | object.val;
643 | assert.strictEqual(accessCount, 1);
644 | observe(object);
645 | assert.strictEqual(accessCount, 2);
646 | object.val; // Overwritten by observe
647 | assert.strictEqual(accessCount, 2);
648 | });
649 |
650 | test("properties named __proto__ will not be made reactive", () => {
651 | const object = Object.defineProperty(Object.create(null), "__proto__", {
652 | configurable: true,
653 | enumerable: true,
654 | writable: true,
655 | value: 10
656 | });
657 |
658 | object.__proto__ = 20;
659 | assert.strictEqual(object.__proto__, 20);
660 | assert.strictEqual(Object.getPrototypeOf(object), null);
661 |
662 | const descriptor = () => Object.assign({}, Object.getOwnPropertyDescriptor(object, "__proto__"));
663 |
664 | const originalDescriptor = descriptor();
665 | observe(object);
666 | const observedDescriptor = descriptor();
667 |
668 | assert.deepEqual(observedDescriptor, originalDescriptor);
669 | });
670 |
671 | test("arrays are not reactive", () => {
672 | const object = observe({ array: [1, 2, 3] });
673 | let times = 0; computed(() => (object.array[1], times++));
674 |
675 | assert.strictEqual(times, 1);
676 | object.array[1] = 4; // Not reactive
677 | assert.strictEqual(times, 1);
678 | object.array.pop();
679 | object.array.pop();
680 | object.array.push(3); // Still not reactive :P (no array function hacks)
681 | assert.strictEqual(times, 1);
682 | object.array[1] = 10;
683 | object.array = object.array; // There we go!
684 | assert.strictEqual(times, 2);
685 | });
686 |
687 | test("computed does not leak `computedQueue` in the `this` value", () => {
688 | let thisValue;
689 | computed(function() {
690 | // Patella versions <= 2.1.0 could leak the `computedQueue` in the `this`
691 | // value when calling computed functions
692 | // This was fixed and now the global this value is used instead
693 | thisValue = this;
694 | });
695 |
696 | assert.isNotArray(thisValue);
697 | assert.strictEqual(thisValue, this);
698 | });
699 |
700 | suite("argument returns", () => {
701 |
702 | test("observe returns its first argument", () => {
703 | const object = {};
704 | assert.strictEqual(observe(object), object);
705 | assert.strictEqual(observe(observe(object)), object);
706 | });
707 |
708 | test("ignore returns its first argument", () => {
709 | const object = {};
710 | assert.strictEqual(ignore(object), object);
711 | assert.strictEqual(ignore(ignore(object)), object);
712 | });
713 |
714 | test("computed returns its first argument", () => {
715 | function func() {}
716 | assert.strictEqual(computed(func), func);
717 | assert.strictEqual(computed(computed(func)), func);
718 | });
719 |
720 | test("dispose returns its first argument", () => {
721 | function func() {}
722 | assert.strictEqual(dispose(func), func);
723 | assert.strictEqual(dispose(dispose(func)), func);
724 | });
725 |
726 | test("dispose returns nothing if called without a valid first argument", () => {
727 | computed(() => {
728 | assert.isUndefined(dispose());
729 | assert.isUndefined(dispose(null));
730 | assert.isUndefined(dispose(undefined));
731 | assert.isUndefined(dispose(null, null));
732 | assert.isUndefined(dispose(null, undefined));
733 | assert.isUndefined(dispose(null, false));
734 | assert.isUndefined(dispose(null, true));
735 | assert.isUndefined(dispose(undefined, null));
736 | assert.isUndefined(dispose(undefined, undefined));
737 | assert.isUndefined(dispose(undefined, false));
738 | assert.isUndefined(dispose(undefined, true));
739 | });
740 | });
741 |
742 | });
743 |
744 | suite("observed object compatibility", () => {
745 |
746 | test("reactive properties can be get/set like normal", () => {
747 | const object = observe({ value: 10 });
748 |
749 | assert.strictEqual(object.value, 10);
750 | object.value = 20;
751 | assert.strictEqual(object.value, 20);
752 | object.value = { x: 10 };
753 | assert.deepEqual(object.value, { x: 10 });
754 | object.value += 10;
755 | assert.strictEqual(object.value, "[object Object]10");
756 | });
757 |
758 | test("observed objects can be iterated through and spread", () => {
759 | const objectObserved = observe({ a: 10, b: 20, c: 30 });
760 | const objectSpread = { ...objectObserved };
761 | const objectIdentical = { a: 10, b: 20, c: 30 };
762 |
763 | assert.deepEqual(objectObserved, objectSpread);
764 | assert.deepEqual(objectObserved, objectIdentical);
765 | assert.deepEqual(objectSpread, objectIdentical);
766 |
767 | const keysExpected = ["a", "b", "c"];
768 |
769 | let keys = []; for (const key in objectObserved) keys.push(key);
770 | assert.deepEqual(keys, keysExpected);
771 |
772 | assert.deepEqual(Object.keys(objectObserved), keysExpected);
773 | assert.deepEqual(Object.values(objectObserved), [10, 20, 30]);
774 | });
775 |
776 | test("observed objects can have cyclic references", () => {
777 | const object1 = { object2: undefined, value: !0 };
778 | const object2 = { object1: undefined, value: !1 };
779 | object1.object2 = object2;
780 | object2.object1 = object1;
781 |
782 | observe(object1);
783 |
784 | assert.strictEqual(object1.object2, object2);
785 | assert.strictEqual(object2.object1, object1);
786 | assert.strictEqual(object1.object2.object1, object1);
787 | assert.strictEqual(object2.object1.object2, object2);
788 | assert.strictEqual(object1.object2.object1.object2.object1.object2, object2);
789 |
790 | assert.strictEqual(object1.object2.object1.object2.object1.value, true);
791 | assert.strictEqual(object2.object1.object2.object1.object2.value, false);
792 | });
793 |
794 | });
795 |
796 | suite("reactive functions", () => {
797 |
798 | test("functions can be made reactive", () => {
799 | function func() {}
800 | func.x = 10;
801 | observe(func);
802 |
803 | let times = 0; computed(() => (func.x, times++));
804 |
805 | assert.strictEqual(times, 1);
806 | func.x++;
807 | assert.strictEqual(times, 2);
808 | });
809 |
810 | test("reactive functions can be called", () => {
811 | let value;
812 | function func(newValue) {
813 | value = newValue;
814 | }
815 |
816 | observe(func);
817 |
818 | func(50);
819 | assert.strictEqual(value, 50);
820 | });
821 |
822 | test("functions are exempt from recursive reactivity", () => {
823 | function func() {}
824 | func.x = 10;
825 | const object = observe({ func });
826 |
827 | let times = 0; computed(() => (object.func.x, times++));
828 |
829 | assert.strictEqual(times, 1);
830 | object.func.x++;
831 | assert.strictEqual(times, 1);
832 | });
833 |
834 | test("functions are exempt from implicit reactivity", () => {
835 | function func() {}
836 | func.x = 10;
837 | const object = observe({ func: null });
838 | object.func = func;
839 |
840 | let times = 0; computed(() => (object.func.x, times++));
841 |
842 | assert.strictEqual(times, 1);
843 | object.func.x++;
844 | assert.strictEqual(times, 1);
845 | });
846 |
847 | });
848 |
849 | suite("order of execution", () => {
850 |
851 | test("computed functions execute in the order they are notified", () => {
852 | const values = [];
853 | const func1 = () => values.push(1);
854 | const func2 = () => values.push(2);
855 | const func3 = () => values.push(3);
856 | const func4 = () => values.push(4);
857 | computed(() => {
858 | computed(func2);
859 | computed(func4);
860 | computed(func1);
861 | computed(func3);
862 | assert.deepEqual(values, []);
863 | });
864 |
865 | assert.deepEqual(values, [2,4,1,3]);
866 | });
867 |
868 | test("computed functions can be notified multiple times but cannot be queued multiple times", () => {
869 | const values = [];
870 | const func1 = () => values.push(1);
871 | const func2 = () => values.push(2);
872 | const func3 = () => values.push(3);
873 | const func4 = () => values.push(4);
874 |
875 | computed(func1);
876 | computed(func2);
877 | computed(func1);
878 | computed(func3);
879 | computed(func4);
880 | computed(func3);
881 | assert.deepEqual(values, [1,2,1,3,4,3]);
882 |
883 | values.length = 0;
884 | computed(() => {
885 | computed(func1);
886 | computed(func2);
887 | computed(func1);
888 | computed(func3);
889 | computed(func4);
890 | computed(func3);
891 | assert.deepEqual(values, []);
892 | });
893 | assert.deepEqual(values, [1,2,3,4]);
894 | });
895 |
896 | test("disposing queued computed functions preserves the queue order", () => {
897 | const values = [];
898 | const func1 = () => { values.push(1); };
899 | const func2 = () => { values.push(2); };
900 | const func3 = () => { values.push(3); };
901 | const func4 = () => { values.push(4); dispose(func3) };
902 | const func5 = () => { values.push(5); };
903 |
904 | computed(() => {
905 | computed(func1);
906 | computed(func4);
907 | computed(func3);
908 | computed(func5);
909 | computed(func2);
910 | });
911 | assert.deepEqual(values, [1,4,5,2]);
912 | });
913 |
914 | test("object dependencies are notified in the order they are added", () => {
915 | const object = observe({ x: 10 });
916 |
917 | const values = [];
918 | const func1 = () => { object.x; values.push(1); };
919 | const func2 = () => { object.x; values.push(2); };
920 | const func3 = () => { object.x; values.push(3); };
921 | const func4 = () => { object.x; values.push(4); };
922 |
923 | computed(func2);
924 | computed(func3);
925 | computed(func1);
926 | computed(func4);
927 | assert.deepEqual(values, [2,3,1,4]);
928 |
929 | values.length = 0;
930 | object.x++;
931 | assert.deepEqual(values, [2,3,1,4]);
932 | });
933 |
934 | test("object dependencies are always notified in the order they are added, even when multiple objects get involved", () => {
935 | const object1 = observe({ x: 10 });
936 | const object2 = observe({ x: 10 });
937 |
938 | const values = [];
939 | let ignoreTwo = true;
940 | const createFunc = num => () => {
941 | object1.x, ignoreTwo || object2.x;
942 | values.push(num);
943 | };
944 | const func1 = createFunc(1);
945 | const func2 = createFunc(2);
946 | const func3 = createFunc(3);
947 | const func4 = createFunc(4);
948 |
949 | computed(func3);
950 | computed(func2);
951 | computed(func4);
952 | computed(func1);
953 | ignoreTwo = false;
954 | computed(func1);
955 | computed(func2);
956 | computed(func3);
957 | computed(func4);
958 |
959 | values.length = 0;
960 | object1.x++;
961 | assert.deepEqual(values, [3,2,4,1]);
962 |
963 | values.length = 0;
964 | object2.x++;
965 | assert.deepEqual(values, [1,2,3,4]);
966 | });
967 |
968 | test("disposing a dependant computed function preserves the order of the dependencies", () => {
969 | const object = observe({ x: 10 });
970 |
971 | const values = [];
972 | const func1 = () => { object.x; values.push(1); };
973 | const func2 = () => { object.x; values.push(2); };
974 | const func3 = () => { object.x; values.push(3); };
975 | const func4 = () => { object.x; values.push(4); };
976 |
977 | computed(func3);
978 | computed(func2);
979 | computed(func4);
980 | computed(func1);
981 | dispose(func4);
982 |
983 | values.length = 0;
984 | object.x++;
985 | assert.deepEqual(values, [3,2,1]);
986 | });
987 |
988 | });
989 |
990 | });
991 |
--------------------------------------------------------------------------------