diff --git a/README.md b/README.md index 4bfcd3b7..f783fdf6 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,52 @@ # Realar - -Object oriented state manager for React based on [reactive mathematic](https://github.com/betula/reactive-box). - -[Light](https://bundlephobia.com/result?p=realar), [Fast](https://github.com/betula/reactive-box-performance), and Pretty looked :kissing_heart: - -Realar targeted to clean code, modulable architecture, and time to delivery user experience. - -Transparent functional reactive programming with classes, decorators and [babel jsx wrapper](https://github.com/betula/babel-plugin-realar) - -```javascript -class Ticker { - @prop count = 0 - tick = () => ++this.count; -} - -const ticker = new Ticker(); -setInterval(ticker.tick, 200); - -const App = () => ( -

{ticker.count}

-) -``` -[Try wrapped version on CodeSandbox](https://codesandbox.io/s/realar-ticker-classes-c9819?file=/src/App.tsx) -Realar **targeted to** all scale applications up to complex enterprise solutions on micro apps architecture. +[![npm version](https://img.shields.io/npm/v/realar?style=flat-square)](https://www.npmjs.com/package/realar) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/realar?style=flat-square)](https://bundlephobia.com/result?p=realar) [![code coverage](https://img.shields.io/coveralls/github/betula/realar?style=flat-square)](https://coveralls.io/github/betula/realar) [![typescript supported](https://img.shields.io/npm/types/typescript?style=flat-square)](./src/index.ts) -You can use as many from Realar as you want. For small websites or theme switchers, two functions are enough:ok_hand: Step by step on applications scale stairs you can take more and more. From sharing state to all application parts, to modulable architecture with micro apps composition. +State manager to reduce developers' coding time and increase the lifetime of your codebase. -- __Decorators for clasess lovers__. And babel plugin for automatic wrap all arrow functions defined in the global scope with JSX inside to observe wrapper for the total implementation of transparent functional reactive programming (TFRP) in javascript with React. +Realar targeted to all scale applications up to complex enterprise solutions on modular architecture. -- __Logic free React components__. Perfect instruments for moving all component logic to the class outside. Your React component will be pure from any unnecessary code, only view, only JSX, no more. +- __Logic free React components__. Perfect instruments for moving all component logic outside. Your React component will be pure from any unnecessary code, only view, only JSX, no more. -- __Shared stateful logic decomposition__. The pattern for decomposing applications logic to separate independent or one direction dependent modules. Each module can have its own set of reactive values. (ssr, comfort “mock” mechanism for simple unit testing). Shared stateful logic is a single instantiated class with total accessibility from all parts of your application. In another terminology - services. +- __Lightweight and Fast__. Less then 5kB. Aims at the smallest size of the resulting bundle. And only parts are updated in which is really necessary to make changes. -- __Lightweight and Fast__. Really light ~3kB. And only those components are updated in which it is really necessary to make changes. +- __Value and Signal__ is the big elephants remind Store and Action from Redux. Allows you to perform familiar coding techniques, and also add many modern features. -- __React component context level scopes__. Declaration one scope and use as many reactive values as you want without the need to define a new React context for each changeable value. +- __Modular Architecture__. Possibilities for the implementation of three levels of logic availability. + - Shared stateful logic pattern (known as "service") for decomposing applications logic to separate independent or one direction dependent modules with global accessibility. + - Declaration one scope and use as many reactive values as you want without the need to define a new React context for each changeable value with context level accessibility. + - And enjoy clean React components with local logic decomposition. -- __Signals__ are a necessary part of reactive communication, well knows for most javascript developers as actions or events. In Realar that possibility provides through signal abstraction. Possibility for subscribing to signal, call signal and wait for the next signal value everywhere on the codebase. And for a tasty, reading the last called value from a signal. +- __Decorators for clasess lovers__. Support OOP as one of the primary syntax. The implementation of transparent functional reactive programming (TFRP) with React (looks similar to Mobx). And the babel plugin for automatic wrap all arrow functions defined in the global scope with JSX inside to observe wrapper. ### Usage -It looks likes very clear and natively, and you can start development knows only two functions. - -`prop`. Reactive value marker. Each reactive value has an immutable state. If the immutable state will update, all React components that depend on It will refresh. - -`shared`. One of the primary reasons for using state manager in your application is a shared state accessing, and using shared logic between scattered React components and any place of your code. +The start piece of code with basic operations of reactive value and signals ```javascript -import React from 'react'; -import { prop, shared } from 'realar'; - -class Counter { - @prop value = 0; - - inc = () => this.value += 1; - dec = () => this.value -= 1; -} - -const sharedCounter = () => shared(Counter); - -const Count = () => { - const { value } = sharedCounter(); - return

{value}

; -}; - -const Buttons = () => { - const { inc, dec } = sharedCounter(); - return ( - <> - - - - ); -}; - -const App = () => ( - <> - - - - - -); - -export default App; -``` - -For best possibilities use [realar babel plugin](https://github.com/betula/babel-plugin-realar), your code will be so beautiful to look like. - -But otherwise necessary to wrap all React function components that use reactive values inside to `observe` wrapper. [Try wrapped version on CodeSandbox](https://codesandbox.io/s/realar-counter-k9kmw?file=/src/App.tsx). +import { value } from 'realar' -### Access visibility levels - -The basic level of scopes for React developers is a **component level scope** (_for example `useState`, and other standard React hooks has that level_). - -Every React component instance has its own local state, which is saved every render for the component as long as the component is mounted. - -In the Realar ecosystem `useLocal` hook used to make components local state. - -```javascript -class CounterLogic { - @prop value = 0; - inc = () => this.value += 1 -} +// Realar's adventure will start from "value", +// is an immutable reactive container such as +// "store" from Redux terminology +const store = value(0) -const Counter = () => { - const { value, inc } = useLocal(CounterLogic); +// You can easily make functional update +// signals similar to an "action" from Redux +const inc = store.updater(state => state + 1) +const add = store.updater((state, num: number) => state + num) - return ( -

{value}

- ); -} +// Watch updating +store.to((state) => console.log(state)) -export const App = () => ( - <> - - - -); +// And run signals as usual functions +inc() // console output: 1 +add(10) // console output: 11 ``` -[Play wrapped on CodeSandbox](https://codesandbox.io/s/realar-component-level-scope-classes-m0i10?file=/src/App.tsx) - -This feature can be useful for removing logic from the body of a component to keep that free of unnecessary code, and therefore cleaner. +[Try on RunKit](https://runkit.com/betula/60b4e0cab769ca0021660348) -**context component level scope** -```javascript -const Counter = () => { - const { value, inc } = useScoped(CounterLogic); - - return ( -

{value}

- ); -} - -export const App = () => ( - - - - - - - -); -``` - -[Play wrapped on CodeSandbox](https://codesandbox.io/s/realar-context-component-level-scope-classes-wivjv?file=/src/App.tsx) ### Signals @@ -179,13 +76,13 @@ If you making an instance of a class with a subscription in the constructor, tho Below other examples ```javascript -const add = signal(); +const add = signal(); const store = value(1); on(add, num => store.val += num); add(15); -console.log(store.val); // 16 +console.log(store.val); // console output: 16 ``` [Edit on RunKit](https://runkit.com/betula/6013af7649e8720019c9cf2a) @@ -194,18 +91,19 @@ An signal is convenient to use as a promise. ```javascript const fire = signal(); -const listen = async () => { +(async () => { for (;;) { - await fire; // await as a usual promise + await fire.promise; // await as a usual promise console.log('Fire'); } -} +})(); -listen(); setInterval(fire, 500); ``` [Edit on RunKit](https://runkit.com/betula/601e3b0056b62d001bfa391b) + + ### Core The abstraction of the core is an implementation of functional reactive programming on javascript and binding that with React. @@ -242,192 +140,149 @@ That component will be updated every time when new sum value is coming. The difference from exists an implementation of functional reactive programming (mobx) in Realar dependency collector provides the possibility to write in selectors and nested writable reactions. -Realar provides big possibility abstractions for reactive flow. We already know about reactive value container, reactive expressions, and subscribe mechanism. But also have synchronization between data, cycled reactions, cached selectors, and transactions. +Realar provides big possibility abstractions for reactive flow. We already know about reactive value container, reactive expressions, and subscribe mechanism. But also have synchronization between data, cycled reactions, cached selectors, transactions and etc. -### Low level usage -```javascript -const count = value(0); -const tick = () => count.val++; -setInterval(tick, 200); +### OOP Usage -const App = () => { - const value = useValue(count); - return ( -

{value}

- ) +Transparent functional reactive programming with classes, decorators and [babel jsx wrapper](https://github.com/betula/babel-plugin-realar) + +```javascript +class Ticker { + @prop count = 0 + tick = () => ++this.count; } + +const ticker = new Ticker(); +setInterval(ticker.tick, 200); + +const App = () => ( +

{ticker.count}

+) ``` -[Try on CodeSandbox](https://codesandbox.io/s/realar-ticker-functional-6s3mx?file=/src/App.tsx) +[Try wrapped version on CodeSandbox](https://codesandbox.io/s/realar-ticker-classes-c9819?file=/src/App.tsx) + +It looks likes very clear and natively, and you can start development knows only two functions. + +`prop`. Reactive value marker. Each reactive value has an immutable state. If the immutable state will update, all React components that depend on It will refresh. + +`shared`. One of the primary reasons for using state manager in your application is a shared state accessing, and using shared logic between scattered React components and any place of your code. ```javascript -import React from "react"; -import { value, useValue } from "realar"; +import React from 'react'; +import { prop, shared } from 'realar'; -const [get, set] = value(0); +class Counter { + @prop value = 0; -const next = () => get() + 1; + inc = () => this.value += 1; + dec = () => this.value -= 1; +} -const inc = () => set(next()); -const dec = () => set(get() - 1); +const sharedCounter = () => shared(Counter); -const Current = () => { - const value = useValue(get); - return

current: {value}

; +const Count = () => { + const { value } = sharedCounter(); + return

{value}

; }; -const Next = () => { - const value = useValue(next); - return

next: {value}

; +const Buttons = () => { + const { inc, dec } = sharedCounter(); + return ( + <> + + + + ); }; const App = () => ( <> - - - - - + + + + ); export default App; ``` -[Try on CodeSandbox](https://codesandbox.io/s/realar-pure-counter-1ue4h?file=/src/App.tsx). - -### API - -**value** - -The first abstraction of Realar is reactive container - `value`. -The `value` is a place where your store some data as an immutable struct. -When you change value (rewrite to a new immutable struct) all who depend on It will be updated synchronously. - -For create new value we need `value` function from `realar`, and initial value that will store in reactive container. -The call of `value` function returns array of two functions. -- The first is value getter. -- The second one is necessary for save new value to reactive container. - -```javascript -const [get, set] = value(0); - -set(get() + 1); - -console.log(get()); // 1 -``` -[Edit on RunKit](https://runkit.com/betula/6013af7649e8720019c9cf2a) - -In that example -- for a first we created `value` container for number with initial zero; -- After that, we got the value, and set to its value plus one; -- Let's print the result to the developer console, that will is one. - -We learned how to create a value, set, and get it. - -**on** -The next basic abstraction is expression. -Expression is a function that read reactive boxes or selectors. It can return value and write reactive values inside. +For best possibilities use [babel jsx wrapper](https://github.com/betula/babel-plugin-realar), your code will be so beautiful to look like. -We can subscribe to change any reactive expression using `on` function _(which also works with signal)_. - -```javascript -const [get, set] = value(0); - -const next = () => get() + 1; - -on(next, (val, prev) => console.log(val, prev)); +But otherwise necessary to wrap all React function components that use reactive values inside to `observe` wrapper. [Try wrapped version on CodeSandbox](https://codesandbox.io/s/realar-counter-k9kmw?file=/src/App.tsx). -set(5); // We will see 6 and 1 in developer console output, It are new and previous value -``` -[Edit on RunKit](https://runkit.com/betula/6013ea214e0cf9001ac18e71) +#### React component access visibility level -In that example expression is `next` function, because It get value and return that plus one. +The basic level of scopes for React developers is a component level scope (_for example `useState`, and other standard React hooks has that level_). -**selector** +Every React component instance has its own local state, which is saved every render for the component as long as the component is mounted. -Necessary for making high-cost calculations and cache them for many times of accessing without changing source dependencies. And for downgrade (selection from) your hierarchical store. +In the Realar ecosystem `useLocal` hook used to make components local stateful logic. ```javascript -const store = value({ - address: { - city: 'NY' - } -}); - -const address = selector(() => store.val.address); - -on(address, ({ city }) => console.log(city)); // Subscribe to address selector +class CounterLogic { + @prop value = 0; + inc = () => this.value += 1 +} -console.log(address.val.city); // Log current value of address selector +const Counter = () => { + const { value, inc } = useLocal(CounterLogic); -store.update(state => ({ - ...state, - user: {} -})); -// Store changed but non reaction from address selector + return ( +

{value}

+ ); +} -store.update(state => ({ - ...state, - address: { - city: 'LA' - } -})); -// We can see reaction on deleveloper console output with new address selector value +export const App = () => ( + <> + + + +); ``` -[Edit on RunKit](https://runkit.com/betula/60338ff8dbe368001a10be8c) +[Play wrapped on CodeSandbox](https://codesandbox.io/s/realar-component-level-scope-classes-m0i10?file=/src/App.tsx) -**cache** +This feature can be useful for removing logic from the body of a component to keep that free of unnecessary code, and therefore cleaner. -`cache` - is the decorator for define `selector` on class getter. +#### React context access visibility level ```javascript -class Todos { - @prop items = []; +const Counter = () => { + const { value, inc } = useScoped(CounterLogic); - @cache get completed() { - return this.items.filter(item => item.completed); - } + return ( +

{value}

+ ); } -``` -**cycle** - -```javascript -const [get, set] = value(0); - -cycle(() => { - console.log(get() + 1); -}); - -set(1); -set(2); - -// In output of developer console will be 1, 2 and 3. +export const App = () => ( + + + + + + + +); ``` -[Edit on RunKit](https://runkit.com/betula/601a733c5bfc4e001a38def8) -- Takes a function as reactive expression. -- After each run: subscribe to all reactive values accessed while running -- Re-run on data changes +[Play wrapped on CodeSandbox](https://codesandbox.io/s/realar-context-component-level-scope-classes-wivjv?file=/src/App.tsx) -**sync** -```javascript -const [getSource, setSource] = value(0); -const [getTarget, setTarget] = value(0); -sync(getSource, setTarget); -// same as sync(() => getSource(), val => setTarget(val)); -setSource(10); -console.log(getTarget()) // 10 -``` -[Edit on RunKit](https://runkit.com/betula/601a73b26adfe70020a0e229) +### API + +- [value](./docs/api.md) +- [selector](./docs/api.md) +- [on](./docs/api.md) +- [cache](./docs/api.md) +- [cycle](./docs/api.md) +- [sync](./docs/api.md) -_Documentation not ready yet for `effect`, `loop`, `pool`, `stoppable`, `initial`, `mock`, `unmock`, `free`, `transaction`, `untrack`, `isolate`, `ready`, `un` functions. It's coming soon._ ### Demos @@ -435,6 +290,7 @@ _Documentation not ready yet for `effect`, `loop`, `pool`, `stoppable`, `initial + [Todos](https://github.com/realar-project/todos) - todomvc implementation. + [Jest](https://github.com/realar-project/jest) - unit test example. + ### Articles + [Multiparadigm state manager for React by ~2 kB.](https://dev.to/betula/multiparadigm-state-manager-for-react-by-2-kb-4kh1) @@ -449,23 +305,9 @@ npm install realar yarn add realar ``` -And update your babel config for support class decorators and using [babel plugin](https://github.com/betula/babel-plugin-realar) for automatic observation arrow function components. +- If you prefer classes with decorators [update your babel config for support](https://babeljs.io/docs/en/babel-plugin-proposal-decorators). +- And configure [babel jsx wrapper](https://github.com/betula/babel-plugin-realar) for automatic observation arrow function components if you want to use it. -```javascript -//.babelrc -{ - "plugins": [ - ["@babel/plugin-proposal-decorators", { "legacy": true }], - ["@babel/plugin-proposal-class-properties", { "loose": true }], - ["realar", { - "include": [ - "src/components/*", - "src/pages/*" - ] - }] - ] -} -``` Enjoy and happy coding! diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..f1e7c1c1 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,132 @@ +### API + +**value** + +The first abstraction of Realar is reactive container - `value`. +The `value` is a place where your store some data as an immutable struct. +When you change value (rewrite to a new immutable struct) all who depend on It will be updated synchronously. + +For create new value we need `value` function from `realar`, and initial value that will store in reactive container. +The call of `value` function returns array of two functions. +- The first is value getter. +- The second one is necessary for save new value to reactive container. + +```javascript +const { get, set } = value(0); + +set(get() + 1); + +console.log(get()); // 1 +``` +[Edit on RunKit](https://runkit.com/betula/6013af7649e8720019c9cf2a) + +In that example +- for a first we created `value` container for number with initial zero; +- After that, we got the value, and set to its value plus one; +- Let's print the result to the developer console, that will is one. + +We learned how to create a value, set, and get it. + +**on** + +The next basic abstraction is expression. +Expression is a function that read reactive boxes or selectors. It can return value and write reactive values inside. + +We can subscribe to change any reactive expression using `on` function _(which also works with signal)_. + +```javascript +const { get, set } = value(0); + +const next = () => get() + 1; + +on(next, (val, prev) => console.log(val, prev)); + +set(5); // We will see 6 and 1 in developer console output, It are new and previous value +``` +[Edit on RunKit](https://runkit.com/betula/6013ea214e0cf9001ac18e71) + +In that example expression is `next` function, because It get value and return that plus one. + +**selector** + +Necessary for making high-cost calculations and cache them for many times of accessing without changing source dependencies. And for downgrade (selection from) your hierarchical store. + +```javascript +const store = value({ + address: { + city: 'NY' + } +}); + +const address = selector(() => store.val.address); + +on(address, ({ city }) => console.log(city)); // Subscribe to address selector + +console.log(address.val.city); // Log current value of address selector + +store.update(state => ({ + ...state, + user: {} +})); +// Store changed but non reaction from address selector + +store.update(state => ({ + ...state, + address: { + city: 'LA' + } +})); +// We can see reaction on deleveloper console output with new address selector value +``` +[Edit on RunKit](https://runkit.com/betula/60338ff8dbe368001a10be8c) + +**cache** + +`cache` - is the decorator for define `selector` on class getter. + +```javascript +class Todos { + @prop items = []; + + @cache get completed() { + return this.items.filter(item => item.completed); + } +} +``` + +**cycle** + +```javascript +const { get, set } = value(0); + +cycle(() => { + console.log(get() + 1); +}); + +set(1); +set(2); + +// In output of developer console will be 1, 2 and 3. +``` +[Edit on RunKit](https://runkit.com/betula/601a733c5bfc4e001a38def8) + +- Takes a function as reactive expression. +- After each run: subscribe to all reactive values accessed while running +- Re-run on data changes + +**sync** + +```javascript +const { getSource, setSource } = value(0); +const { getTarget, setTarget } = value(0); + +sync(getSource, setTarget); +// same as sync(() => getSource(), val => setTarget(val)); + +setSource(10); + +console.log(getTarget()) // 10 +``` +[Edit on RunKit](https://runkit.com/betula/601a73b26adfe70020a0e229) + +_Documentation not ready yet for `pool`, `contextual`, `initial`, `mock`, `unmock`, `free`, `transaction`, `untrack`, `isolate`, `un` functions. It's coming soon._ diff --git a/lerna.json b/lerna.json index bf391909..1a0d6472 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.5.38", + "version": "0.6.0-alpha.0", "packages": [ "." ] diff --git a/package.json b/package.json index 2edb5cff..91043ba3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "realar", - "version": "0.5.38", + "version": "0.6.0-alpha.0", "description": "React state manager", "repository": { "url": "https://github.com/betula/realar" @@ -27,7 +27,7 @@ "clear": "rimraf build" }, "dependencies": { - "reactive-box": "0.6.6" + "reactive-box": "0.7.2" }, "devDependencies": { "@babel/core": "7.12.10", diff --git a/src/index.ts b/src/index.ts index e756617f..0833be09 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,44 +1,437 @@ import React, { Context, FC } from 'react'; -import { expr, box, sel, transaction, untrack } from 'reactive-box'; +import rb_lib from 'reactive-box'; + +/* + TODOs: + + + [] Add store.updater.multiple + [] Complete the draft explanation of the introductory section + + + [] Add second example to documentation + + ``` + + // Second example + const Loader = () => { + const count = value(0); + + return { + start: count.updater(v => v + 1), + stop: count.updater(v => v - 1), + pending: count.select(v => v > 0) + } + } + ``` + + [] maybe make val as readonly property for fix "never" type for prepended values and signals + or I can make readonly val only for prepended values and signals (think about). + + [] typings for builder + [] documentation update + + + Backlog: + [] Try terser for result code size optimization + [] .as.trigger + [] .as.value.trigger + [] .as.signal.trigger + + [] signal.trigger.resolved + [] value.trigger.resolved + + [] test case "should work signal.trigger with configured .pre" + + [] signal.trigger.from + [] value.trigger.from +*/ + +// +// Exports +// export { + // Declarative framework value, selector, - prop, - cache, signal, - ready, + + // Imperative framework on, - effect, - un, - hook, sync, cycle, - loop, - pool, - stoppable, - isolate, + + // Class decorators for TRFP + prop, + cache, + + // Shared technique shared, initial, + free, + mock, + unmock, + + // Unsubscribe scopes control + isolate, + un, + + // Additional api + local, + contextual, + pool, + + // Track and transactions + transaction, + untrack, + + // React bindings observe, useValue, + useValues, useLocal, useScoped, shared as useShared, Scope, - free, - mock, - unmock, - transaction, - untrack, - Ensurable, - Selector, - Value, - Signal, - StopSignal, - ReadySignal, + useJsx, }; + + +// +// Typings. +// + + +// +// Private +// + +// @see https://github.com/Microsoft/TypeScript/issues/27024 +type Equals = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) + ? ([X] extends [Y] + ? ([Y] extends [X] ? true : false) : false) + : false; + +type Re = { get: () => T } | (() => T); + + +// +// Entity types +// + +interface E_UpdaterPartial { + updater: { + (func?: (state: O, upValue: U, upPrev: U) => I): Equals extends true ? Signal : Signal + value(func?: (state: O, upValue: U, upPrev: U) => I): Equals extends true ? Value : Value + } +} + + + +interface Value extends E_UpdaterPartial { + (value: I): void; + + set: (value: I) => void; + get: () => O; + + val: Equals extends true ? O : never; + + // readable section + sync(func: (value: O, prev: O) => void): Value + op(func: () => R): R extends void ? Value : R + to: { + (func: (value: O, prev: O) => void): Value + once(func: (value: O, prev: O) => void): Value + } + filter: { + (func?: (value: O) => any): Value // tracked by default + untrack(func?: (value: O) => any): Value + not: { + (func?: (value: O) => any): Value // tracked by default + untrack(func?: (value: O) => any): Value + } + } + select: { + (func?: (value: O) => R): Selector // tracked by default + untrack(func?: (value: O) => R): Selector + multiple: { + (cfg: any[]): any // TODO: .select.multiple typings + untrack(cfg: any[]): any + } + } + view: { + (func: (value: O) => R): Value // tracked by default + untrack(func: (value: O) => R): Value + } + + flow: any // TODO: .flow typings + join: any // TODO: .join typings + + promise: Promise + + // writtable section + update: { + (func?: (value: O) => I): void // untracked by default + track(func?: (value: O) => I): void + by: { + (re: Re, updater?: (state: O, reValue: T, rePrev: T) => I) + once: { + (re: Re, updater?: (state: O, reValue: T, rePrev: T) => I) + } + } + } + pre: { + (func?: (value: N) => I): Value // tracked by default + untrack(func?: (value: N) => I): Value + filter: { + (func?: (value: O) => any): Value // tracked by default + untrack(func?: (value: O) => any): Value + not: { // tracked by default + (func?: (value: O) => any): Value + untrack(func?: (value: O) => any): Value + } + } + } +} + + + +interface Signal extends E_UpdaterPartial { + (value: I): void; + + set: (value: I) => void; + get: () => O; + + val: Equals extends true ? O : never; + + // readable section + sync(func: (value: O, prev: O) => void): Signal + op(func: () => R): R extends void ? Signal : R + to: { + (func: (value: O, prev: O) => void): Signal + once(func: (value: O, prev: O) => void): Signal + } + filter: { + (func?: (value: O) => any): Signal // untracked by default + track(func?: (value: O) => any): Signal + not: { + (func?: (value: O) => any): Signal // untracked by default + track(func?: (value: O) => any): Signal + } + } + select: { + (func?: (value: O) => R): Selector // tracked by default + untrack(func?: (value: O) => R): Selector + multiple: { + (cfg: any[]): any // TODO: .select.multiple typings + untrack(cfg: any[]): any + } + } + view: { + (func: (value: O) => R): Signal // tracked by default + untrack(func: (value: O) => R): Signal + } + + flow: any // TODO: .flow typings + join: any // TODO: .join typings + + promise: Promise + + // writtable section + update: { + (func?: (value: O) => I): void // untracked by default + track(func?: (value: O) => I): void + by: { + (re: Re, updater?: (state: O, reValue: T, rePrev: T) => I) + once: { + (re: Re, updater?: (state: O, reValue: T, rePrev: T) => I) + } + } + } + pre: { + (func?: (value: N) => I): Signal // tracked by default + untrack(func?: (value: N) => I): Signal + filter: { + (func?: (value: O) => any): Signal // tracked by default + untrack(func?: (value: O) => any): Signal + not: { // tracked by default + (func?: (value: O) => any): Signal + untrack(func?: (value: O) => any): Signal + } + } + } +} + + + + + + + +type Selector = { + get: () => O; + val: O; + select: any; +} + + + + + + + + + +// +// Entry types +// + +type ValueEntry = { + (initial?: T): Value; + trigger: { + (initial?: any): any; + flag: { + (initial?: any): any; + invert: { (initial?: any): any } + } + }; + from: { (get: () => any, set?: (v) => any): any }, + combine: { (cfg: any): any } +} + +type SelectorEntry = { + (fn: () => any): any; +} + +type SignalEntry = { + (initial?: T): Signal; + trigger: { + (initial?: any): any; + flag: { + (initial?: any): any; + invert: { (initial?: any): any } + } + }; + from: { (get: () => any, set?: (v) => any): any }, + combine: { (cfg: any): any } +}; + + + +// +// Realar external api typings +// + +type Local = { + inject(fn: () => void): void; +} +type Contextual = { + stop: () => void; +} +type Isolate = { + (fn): () => void; + unsafe: () => () => () => void; +} +type Transaction = { + (fn: () => T): T; + unsafe: () => () => void; +} +type Untrack = { + (fn: () => T): T; + unsafe: () => () => void; +} + + +// +// Realar external api typings for React +// + +type Observe = { + (FunctionComponent: T): React.MemoExoticComponent; + nomemo: { + (FunctionComponent: T): T; + } +} +type UseScoped = { + (target: (new (init?: any) => M) | ((init?: any) => M)): M; +} +type UseLocal = { + ( + target: (new (...args: T) => M) | ((...args: T) => M), + deps?: T + ): M; +} +type UseValue = { + (target: Re, deps?: any[]): T; +} + +type UseValues_CfgExemplar = { + [key: string]: Re +} +type UseValues_ExpandCfgTargets = { + [P in keyof T]: T[P] extends Re ? Re_T : T[P] +} +type UseValues = { + (targets: T, deps?: any[]): UseValues_ExpandCfgTargets + (targets: [Re,Re], deps?: any[]): [A,B]; + (targets: [Re,Re,Re], deps?: any[]): [A,B,C]; + (targets: [Re,Re,Re,Re], deps?: any[]): [A,B,C,D]; + (targets: [Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E]; + (targets: [Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F]; + (targets: [Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re<$>], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re

,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re<$>,Re<_>], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_]; + (targets: [Re], deps?: any[]): [A]; +} + +type UseJsx = { + (func: FC, deps?: any[]): React.MemoExoticComponent>; +} + + +// +// Realar additions external api typings +// + +type PoolEntry_BodyExemplar = { + (...args: any[]): Promise; +} +type PoolEntry = { + (body: K): Pool +} + +type Pool = K & { + count: any; + threads: any; + pending: any; +}; + + +// +// React optional require +// + let react; let useRef: typeof React.useRef; @@ -48,6 +441,7 @@ let useMemo: typeof React.useMemo; let useContext: typeof React.useContext; let createContext: typeof React.createContext; let createElement: typeof React.createElement; +let memo: typeof React.memo; /* istanbul ignore next */ try { @@ -60,610 +454,853 @@ try { useContext = react.useContext; createContext = react.createContext; createElement = react.createElement; + memo = react.memo; } catch (e) { - useRef = useReducer = useEffect = useMemo = useContext = createContext = createElement = (() => { + useRef = useReducer = useEffect = useMemo = useContext = createContext = createElement = memo = (() => { throw new Error('Missed "react" dependency'); }) as any; } -const key = 'val'; -const shareds = new Map(); - -let initial_data: any; -let context_unsubs: any; -let context_hooks: any; -let shared_unsubs = [] as any; -let is_sync: any; -let is_stop_signal: any; -let is_observe: any; -let scope_context: any; -let stoppable_context: any; - -const def_prop = Object.defineProperty; - -type Ensurable = T | void; - -type Callable = { - name: never; - (data: T): void; -} & (T extends void - ? { - (): void; - } - : {}); - -type Reactionable = { 0: () => T } | [() => T] | (() => T); - -type Selector = { - 0: () => T; - readonly val: T; - get(): T; - free(): void; -} & [() => T] & { - view

(get: (data: T) => P): Selector

; - select

(get: (data: T) => P): Selector

; - select(): Selector; - - watch: { - (listener: (value: T, prev: T) => void): () => void; - once(listener: (value: T, prev: T) => void): () => void; - }; - - flow: { - filter(fn: (data: T) => any): Value>; - }; - }; -type Value = Callable & { - 0: () => K; - 1: (value: T) => void; - val: T & K; - get(): K; +// +// Global Realar definitions +// - update: (fn: (state: K) => T) => void; - sub: { - (reactionable: Reactionable, fn: (data: K, value: S, prev: S) => T): () => void; - once(reactionable: Reactionable, fn: (data: K, value: S, prev: S) => T): () => void; - }; - set(value: T): void; -} & { - wrap: { -

(set: () => T, get: (data: K) => P): Value; - (set: (data: M) => T, get: (data: K) => P): Value; - (set: () => T): Value; - (set: (data: M) => T): Value; - - filter(fn: (data: T) => any): Value; - }; - view

(get: (data: K) => P): Value; - select

(get: (data: K) => P): Selector

; - select(): Selector; +const shareds = new Map(); - watch: { - (listener: (value: K extends Ensurable ? P : K, prev: K) => void): () => void; - once(listener: (value: K extends Ensurable ? P : K, prev: K) => void): () => void; - }; - reset(): void; +let initial_data: any; +let shared_unsubs = [] as any; - flow: { - filter(fn: (data: K) => any): Value>; - }; -} & { - [P in Exclude, number>]: never; - } & - [() => K, (value: T) => void]; - -type Signal< - T, - K = T, - X = {}, - E = { - reset(): void; - update: (fn: (state: K) => T) => void; - sub: { - (reactionable: Reactionable, fn: (data: K, value: S, prev: S) => T): () => void; - once(reactionable: Reactionable, fn: (data: K, value: S, prev: S) => T): () => void; - }; - set(value: T): void; - } -> = Callable & - Pick, 'then' | 'catch' | 'finally'> & { - 0: () => K; - 1: (value: T) => void; - readonly val: K; - get(): K; - } & { - wrap: { -

(set: () => T, get: (data: K) => P): Signal; - (set: (data: M) => T, get: (data: K) => P): Signal; - (set: () => T): Signal; - (set: (data: M) => T): Signal; - - filter(fn: (data: T) => any): Signal; - }; - - view

(get: (data: K) => P): Signal; - select

(get: (data: K) => P): Selector

; - select(): Selector; - - watch: { - (listener: (value: K extends Ensurable ? P : K, prev: K) => void): () => void; - once(listener: (value: K extends Ensurable ? P : K, prev: K) => void): () => void; - }; - - flow: { - filter(fn: (data: K) => any): Signal>; - }; - } & E & - X & - { - [P in Exclude, number>]: never; - } & - [() => K, (value: T) => void]; - -type StopSignal = Signal< - void, - boolean, - { - stop(): void; - set(): void; - sub: { - (reactionable: Reactionable): () => void; - once(reactionable: Reactionable): () => void; - }; - }, - {} ->; -type ReadySignal = Signal< - T, - K, - { - to(value: T): Signal; - } ->; +let context_unsubs: any; +let context_local_injects: any; +let context_contextual_stop: any; + + +// +// Global js sdk specific definitions. +// + +const obj_equals = Object.is; +const obj_def_prop = Object.defineProperty; +const obj_create = Object.create; +const obj_keys = Object.keys; +const obj_is_array = Array.isArray; +const new_symbol = Symbol; +const const_undef = void 0; +const const_string_function = 'function'; + +// +// Reactive box specific definitions. +// + +const rb = rb_lib; +const expr = rb.expr; +const box = rb.box; +const sel = rb.sel; +const flow = rb.flow; + +const internal_flow_stop = flow.stop; +const internal_untrack = rb.untrack; +const internal_transaction = rb.transaction; + +const un_expr = (a,b) => ((a = expr(a,b)), un(a[1]), a); +const un_flow = (a,b,c) => ((a = flow(a,b,c)), un(a[2]), a); + + +// +// Entity builder implementation for value, signal and etc. +// + +const pure_fn = function () {}; +const pure_arrow_fn_returns_arg = (v) => v; +const pure_arrow_fn_returns_not_arg = (v) => !v; +const pure_arrow_fn_returns_undef = (() => {}) as any; + +const key_proto = '__proto__'; +const key_get = 'get'; +const key_set = 'set'; +const key_promise = 'promise'; +const key_promise_internal = new_symbol(); +const key_reset = 'reset'; +const key_initial = new_symbol(); +const key_dirty_handler = new_symbol(); +const key_dirty = 'dirty'; +const key_sync = 'sync'; +const key_ctx = new_symbol(); +const key_by = 'by'; +const key_reinit = 'reinit'; +const key_update = 'update'; +const key_val = 'val'; +const key_once = 'once'; +const key_to = 'to'; +const key_select = 'select'; +const key_view = 'view'; +const key_handler = new_symbol(); +const key_pre = 'pre'; +const key_filter = 'filter'; +const key_not = 'not'; +const key_flow = 'flow'; +const key_reset_promise_by_reset = new_symbol(); +const key_touched_internal = new_symbol(); +const key_trigger = 'trigger'; +const key_flag = 'flag'; +const key_invert = 'invert'; +const key_from = 'from'; +const key_is_signal = new_symbol(); +const key_track = 'track'; +const key_untrack = 'un'+key_track; +const key_multiple = 'multiple'; +const key_combine = 'combine'; +const key_join = 'join'; +const key_value = 'value'; +const key_as = 'as'; +const key_op = 'op'; +const key_inject = 'inject'; +const key_stop = 'stop'; +const key_unsafe = 'unsafe'; +const key_updater = key_update+'r'; + + + +const obj_def_prop_value = (obj, key, value) => ( + obj_def_prop(obj, key, { value }), value +); + +const obj_def_prop_trait = (obj, key, trait) => + obj_def_prop(obj, key, { + get() { + return obj_def_prop_value(this, key, trait.bind(const_undef, this)); + } + }); -type Pool = K & { - count: Selector; - threads: Selector; - pending: Selector; -}; +const obj_def_prop_with_ns = (obj, key, ns) => + obj_def_prop(obj, key, { + get() { + const ret = {}; + ret[key_proto] = ns; + ret[key_ctx] = this; + return obj_def_prop_value(this, key, ret); + } + }); -function value(): Value; -function value(init: T): Value; -function value(init?: any): any { - const [get, set] = box(init) as any; - def_format(set, get, set); - set.reset = () => set(init); - return set; -} - -function selector(body: () => T): Selector { - const [get, free] = sel(body); - const h = [] as any; - def_format(h, get); - h.free = free; - return h; -} - -function signal(): Signal>; -function signal(init: T): Signal; -function signal(init?: any) { - let resolve: any; - const [get, set] = box([init]); - - const fn = function (data: any) { - const ready = resolve; - resolve = def_promisify(fn); - set([data]); - ready(data); - }; +const obj_def_prop_trait_ns = (obj, key, trait) => + obj_def_prop(obj, key, { + get() { + return obj_def_prop_value(this, key, trait.bind(const_undef, this[key_ctx])); + } + }); - resolve = def_promisify(fn); - def_format(fn, () => get()[0], fn, 0, 1); - fn.reset = () => set([init]); - return fn as any; -} - -signal.stop = stop_signal; -signal.ready = ready; -signal.from = signal_from; -signal.combine = signal_combine; - -function ready(): ReadySignal>; -function ready(init: T): ReadySignal; -function ready(init?: any) { - let resolved = 0; - let resolve: any; - const [get, set] = box([init]); - - const fn = function (data: any) { - if (!resolved) { - resolved = 1; - set([data]); - resolve(data); +const obj_def_prop_trait_ns_with_ns = (obj, key, trait, ns, is_trait_op?) => + obj_def_prop(obj, key, { + get() { + const ctx = this[key_ctx]; + const ret = (is_trait_op ? trait(ctx) : trait).bind(const_undef, ctx); + ret[key_proto] = ns; + ret[key_ctx] = ctx; + return obj_def_prop_value(this, key, ret); } - }; + }); - resolve = def_promisify(fn); - def_format(fn, () => get()[0], fn, is_stop_signal, 1, 1); +const obj_def_prop_trait_with_ns = (obj, key, trait, ns, is_trait_op?) => + obj_def_prop(obj, key, { + get() { + const ret = (is_trait_op ? trait(this) : trait).bind(const_undef, this); + ret[key_proto] = ns; + ret[key_ctx] = this; + return obj_def_prop_value(this, key, ret); + } + }); - if (!is_stop_signal) { - fn.reset = () => { - resolve = def_promisify(fn); - resolved = 0; - set([init]); - }; - } +const obj_def_prop_factory = (obj, key, factory) => + obj_def_prop(obj, key, { + get() { + return obj_def_prop_value(this, key, factory(this)); + } + }); - return fn as any; -} - -ready.flag = () => ready(false).to(true); -ready.from = ready_from; -ready.resolved = ready_resolved; - -function ready_from(source: Reactionable): ReadySignal { - const fn = (source as any)[0] || (source as any); - const dest = ready(fn()); - on(source, dest); - return dest as any; -} - -function ready_resolved(): ReadySignal; -function ready_resolved(value: T): ReadySignal; -function ready_resolved(value?: any): any { - const r = ready(value); - r(value); - return r; -} - -function signal_from(source: Reactionable): Signal { - const fn = (source as any)[0] || (source as any); - const dest = signal(fn()); - on(source, dest); - return dest as any; -} - -function signal_combine(): Signal<[]>; -function signal_combine(a: Reactionable): Signal<[A]>; -function signal_combine(a: Reactionable, b: Reactionable): Signal<[A, B]>; -function signal_combine( - a: Reactionable, - b: Reactionable, - c: Reactionable -): Signal<[A, B, C]>; -function signal_combine( - a: Reactionable, - b: Reactionable, - c: Reactionable, - d: Reactionable -): Signal<[A, B, C, D]>; -function signal_combine( - a: Reactionable, - b: Reactionable, - c: Reactionable, - d: Reactionable, - e: Reactionable -): Signal<[A, B, C, D, E]>; -function signal_combine(...sources: any): any { - const get = () => sources.map((src: any) => (src[0] || src)()); - const dest = signal(get()); - on([get], dest); - return dest as any; -} - -function stop_signal(): StopSignal { - is_stop_signal = 1; - try { - const ctx = ready.flag() as any; - return (ctx.stop = ctx); - } finally { - is_stop_signal = 0; - } -} +const obj_def_prop_promise = (obj) => { + return obj_def_prop(obj, key_promise, { + get() { + const ctx = this; + if (!ctx[key_promise_internal]) { + ctx[key_promise_internal] = new Promise((resolve) => + // TODO: should be the highest priority. + expr(ctx[key_get], () => { + if (!ctx[key_handler][key_reset_promise_by_reset]) ctx[key_promise_internal] = 0; + resolve(ctx[key_get]()); + })[0]() + ); + } + return ctx[key_promise_internal]; + } + }); +}; -function def_format( - ctx: any, - get: any, - set?: any, - no_update?: any, - readonly_val?: any, - has_to?: any -) { - if (!Array.isArray(ctx)) { - ctx[Symbol.iterator] = function* () { - yield get; - if (set) yield set; - }; - } - ctx[0] = get; - ctx.get = get; - const val_prop = { get } as any; - if (set) { - ctx[1] = set; - if (!no_update) { - ctx.update = (fn: any) => set(fn(get())); - } - ctx.set = set; - if (!readonly_val) val_prop.set = set; - ctx.sub = (s: any, fn: any) => on(s, (v, v_prev) => set(fn ? fn(get(), v, v_prev) : void 0)); - ctx.sub.once = (s: any, fn: any) => - once(s, (v, v_prev) => set(fn ? fn(get(), v, v_prev) : void 0)); - } - def_prop(ctx, key, val_prop); +const fill_entity = (handler, proto, has_initial?, initial?, _get?, _set?) => { + let set = _set || handler[1]; + let get = _get || handler[0]; + has_initial && (handler[key_initial] = initial); - if (has_to) { - ctx.to = (value: any) => (wrap as any)(ctx, () => value); - } + let ctx; if (set) { - ctx.wrap = (set: any, get: any) => wrap(ctx, set, get); - ctx.wrap.filter = (fn: any) => wrap(ctx, (v: any) => (fn(v) ? v : stoppable().stop())); + ctx = set; + ctx[key_set] = set; + obj_def_prop(ctx, key_val, { get, set }); + } else { + ctx = {}; + obj_def_prop(ctx, key_val, { get }) } - ctx.view = (get: any) => wrap(ctx, 0, get); - ctx.watch = (fn: any) => on(ctx, fn); - ctx.watch.once = (fn: any) => once(ctx, fn); - - ctx.select = (fn: any) => selector(fn ? () => fn(get()) : get); - - ctx.flow = {}; - ctx.flow.filter = (fn: any) => flow_filter(ctx, fn); + ctx[key_handler] = handler; + ctx[key_proto] = proto; + ctx[key_get] = get; + return ctx; } -function def_promisify(ctx: any) { - let resolve; - const promise = new Promise(r => (resolve = r)); - ['then', 'catch', 'finally'].forEach(prop => { - ctx[prop] = (promise as any)[prop].bind(promise); +const reactionable_subscribe = (target, fn, is_once?, is_sync?) => { + target = target[key_get] || sel(target)[0]; + let value: any; + const e = un_expr(target, () => { + const prev = value; + fn(is_once ? target() : (value = e[0]()), prev); }); - return resolve; + value = e[0](); + if (is_sync) untrack(fn.bind(const_undef, value, const_undef)); } -function wrap(target: any, set?: any, get?: any) { - const source_get = target[0]; - const source_set = target[1]; - - let dest: any; - let dest_set: any; +const make_join_entity = (fn_get, join_cfg, is_signal?, set?, is_untrack?) => { + const fns = join_cfg.map(is_signal + ? (fn) => fn[key_get] || fn + : (fn) => sel(fn[key_get] || fn)[0] + ); + const h = [ + () => { + const ret = [fn_get()]; + const finish = is_untrack && internal_untrack(); + try { return ret.concat(fns.map(f => f())) } + finally { finish && finish() } + }, + set && set.bind() + ]; + h[key_is_signal] = is_signal; + return fill_entity(h, set ? proto_entity_writtable : proto_entity_readable) +} - if (set) { - dest = dest_set = function (data?: any) { - const finish = untrack(); - const stack = stoppable_context; - stoppable_context = 1; - - try { - data = set(data); - if (stoppable_context === 1 || !stoppable_context[0]()) source_set(data); - } finally { - stoppable_context = stack; - finish(); - } - }; - } else if (source_set) { - dest = function (data?: any) { - source_set(data); - }; - } else { - dest = []; +const make_updater = (ctx, fn, is_value?) => ( + is_value = ((is_value ? value : signal) as any)(), + trait_ent_update_by(ctx, is_value, fn), + is_value +) + +const make_trait_ent_pure_fn_untrack = (trait_fn) => + (ctx, fn) => trait_fn(ctx, fn && ((a,b) => { + const finish = internal_untrack(); + try { return fn(a,b); } + finally { finish() } + })); + +const make_trait_ent_untrack = (trait_fn) => + (ctx, fn) => trait_fn(ctx, fn && ((a,b) => { + const finish = internal_untrack(); + try { return (fn[key_get] ? fn[key_get]() : fn(a,b)) } + finally { finish() } + })); + +const op_trait_if_signal = (trait_if_not_signal, trait_if_signal) => ( + (ctx) => ctx[key_handler][key_is_signal] ? trait_if_signal : trait_if_not_signal +); + +const make_proto_for_trackable_ns = (trait_track, trait_untrack) => ( + obj_def_prop_trait_ns( + obj_def_prop_trait_ns(obj_create(pure_fn), key_track, trait_track), + key_untrack, trait_untrack) +); + +const prop_factory_dirty_required_initial = (ctx) => { + const h = ctx[key_handler]; + if (!h[key_dirty_handler]) { + const b = box(h[key_initial]); + obj_def_prop(h, key_initial, { get: b[0], set: b[1] }); + + h[key_dirty_handler] = sel( + () => !obj_equals(h[0](), h[key_initial]) + ).slice(0, 1); } + return fill_entity(h[key_dirty_handler], proto_entity_readable); +}; - if (target.then) { - const methods = ['catch', 'finally']; - if (get) { - def_prop(dest, 'then', { - get() { - const promise = target.then(get); - return promise.then.bind(promise); - }, - }); - } else { - methods.push('then'); - } - methods.forEach(prop => { - def_prop(dest, prop, { - get: () => target[prop], - }); - }); - } - if (target.reset) dest.reset = target.reset; - if (target.stop) target.stop = target; - def_format( - dest, - get ? () => get(source_get()) : source_get, - dest_set || source_set, - !target.update, - target.then, - target.to +const trait_ent_update = (ctx, fn) => ctx[key_set](fn ? fn(untrack(ctx[key_get])) : untrack(ctx[key_get])); +const trait_ent_update_untrack = make_trait_ent_pure_fn_untrack(trait_ent_update); +const trait_ent_update_by = (ctx, src, fn) => ( + reactionable_subscribe(src, fn + ? (src_value, src_prev_value) => ctx[key_set](fn(ctx[key_get](), src_value, src_prev_value)) + : (src_value) => ctx[key_set](src_value) + ), + ctx +); +const trait_ent_update_by_once = (ctx, src, fn) => ( + reactionable_subscribe(src, fn + ? (src_value, src_prev_value) => ctx[key_set](fn(ctx[key_get](), src_value, src_prev_value)) + : (src_value) => ctx[key_set](src_value), + 1 + ), + ctx +); +const trait_ent_updater = (ctx, fn) => make_updater(ctx, fn); +const trait_ent_updater_value = (ctx, fn) => make_updater(ctx, fn, 1); +const trait_ent_sync = (ctx, fn) => (reactionable_subscribe(ctx, fn, 0, 1), ctx); +const trait_ent_reset = (ctx) => { + ctx[key_promise_internal] = 0; + ctx[key_handler][1](ctx[key_handler][key_initial]); + ctx[key_handler][key_touched_internal] = 0; +}; +const trait_ent_reset_by = (ctx, src) => ( + reactionable_subscribe(src, trait_ent_reset.bind(const_undef, ctx)), + ctx +); +const trait_ent_reset_by_once = (ctx, src) => ( + reactionable_subscribe(src, trait_ent_reset.bind(const_undef, ctx), 1), + ctx +); +const trait_ent_reinit = (ctx, initial) => { + ctx[key_handler][key_initial] = initial; + trait_ent_reset(ctx); +}; +const trait_ent_reinit_by = (ctx, src) => ( + reactionable_subscribe(src, (src_value) => trait_ent_reinit(ctx, src_value)), + ctx +); +const trait_ent_reinit_by_once = (ctx, src) => ( + reactionable_subscribe(src, (src_value) => trait_ent_reinit(ctx, src_value), 1), + ctx +); +const trait_ent_to = (ctx, fn) => (reactionable_subscribe(ctx, fn), ctx); +const trait_ent_to_once = (ctx, fn) => (reactionable_subscribe(ctx, fn, 1), ctx); +const trait_ent_select = (ctx, fn) => ( + fill_entity(sel(fn ? () => fn(ctx[key_get]()) : ctx[key_get]).slice(0, 1), proto_entity_readable) +); +const trait_ent_select_untrack = make_trait_ent_pure_fn_untrack(trait_ent_select); +const trait_ent_select_multiple = (ctx, cfg) => obj_keys(cfg).reduce((ret, key) => ( + (ret[key] = trait_ent_select(ctx, cfg[key])), ret +), obj_is_array(cfg) ? [] : {}); +const trait_ent_select_multiple_untrack = (ctx, cfg) => obj_keys(cfg).reduce((ret, key) => ( + (ret[key] = trait_ent_select_untrack(ctx, cfg[key])), ret +), obj_is_array(cfg) ? [] : {}); +const trait_ent_view = (ctx, fn) => ( + fill_entity(ctx[key_handler], ctx[key_proto], + 0, 0, + fn ? () => fn(ctx[key_get]()) : ctx[key_get], + ctx[key_set] && ctx[key_set].bind() + ) +); +const trait_ent_view_untrack = make_trait_ent_pure_fn_untrack(trait_ent_view); +const trait_ent_pre = (ctx, fn) => ( + fn + ? fill_entity(ctx[key_handler], ctx[key_proto], + 0, 0, + ctx[key_get], + (v) => ctx[key_set](fn(v)) + ) + : ctx +); +const trait_ent_pre_untrack = make_trait_ent_pure_fn_untrack(trait_ent_pre); +const trait_ent_pre_filter = (ctx, fn) => ( + (fn = fn + ? (fn[key_get] || fn) + : pure_arrow_fn_returns_arg + ), fill_entity(ctx[key_handler], ctx[key_proto], + 0, 0, + ctx[key_get], + (v) => fn(v) && ctx[key_set](v) + ) +); +const trait_ent_pre_filter_untrack = make_trait_ent_untrack(trait_ent_pre_filter); +const trait_ent_pre_filter_not = (ctx, fn) => ( + trait_ent_pre_filter(ctx, fn + ? (fn[key_get] + ? () => !fn[key_get]() + : (v) => !fn(v)) + : pure_arrow_fn_returns_not_arg) +); +const trait_ent_pre_filter_not_untrack = make_trait_ent_untrack(trait_ent_pre_filter_not); + +const trait_ent_flow = (ctx, fn) => { + fn || (fn = pure_arrow_fn_returns_arg); + let started, prev; + const is_signal = ctx[key_handler][key_is_signal]; + const f = un_flow(() => { + const v = ctx[key_get](); + try { return fn(v, prev) } + finally { prev = v } + }, const_undef, is_signal && pure_arrow_fn_returns_undef); + const h = [ + () => ((started || (f[0](), (started = 1))), f[1]()), + ctx[key_set] && ctx[key_set].bind() + ]; + h[key_is_signal] = is_signal; + return fill_entity(h, + h[1] ? proto_entity_writtable : proto_entity_readable ); - - return dest; +}; +const trait_ent_flow_untrack = make_trait_ent_pure_fn_untrack(trait_ent_flow); +const trait_ent_filter = (ctx, fn) => ( + trait_ent_flow(ctx, fn + ? (fn[key_get] && (fn = fn[key_get]), + (v, prev) => ( + fn(v, prev) ? v : internal_flow_stop + )) + : (v) => v || internal_flow_stop + ) +); +const trait_ent_filter_untrack = make_trait_ent_untrack(trait_ent_filter) +const trait_ent_filter_not = (ctx, fn) => ( + trait_ent_filter(ctx, fn + ? (fn[key_get] && (fn = fn[key_get]), (v) => !fn(v)) + : pure_arrow_fn_returns_not_arg) +); +const trait_ent_filter_not_untrack = make_trait_ent_untrack(trait_ent_filter_not) + +const trait_ent_join = (ctx, cfg) => ( + make_join_entity(ctx[key_get], cfg, ctx[key_handler][key_is_signal], ctx[key_set]) +); +const trait_ent_join_untrack = (ctx, cfg) => ( + make_join_entity(ctx[key_get], cfg, ctx[key_handler][key_is_signal], ctx[key_set], 1) +); + +const trait_ent_as_value = (ctx) => ( + value_from(ctx[key_get], ctx[key_set]) +); +const trait_ent_op = (ctx, f) => ( + (f = f(ctx)), (f === const_undef ? ctx : f) +); + + + + +// readable.to:ns +// .to.once +const proto_entity_readable_to_ns = obj_create(pure_fn); +obj_def_prop_trait_ns(proto_entity_readable_to_ns, key_once, trait_ent_to_once); + +// readable.filter:ns (track|untrack) +// .filter.not (track|untrack) +const proto_entity_readable_filter_ns = obj_create( + make_proto_for_trackable_ns(trait_ent_filter, trait_ent_filter_untrack) +); +obj_def_prop_trait_ns_with_ns(proto_entity_readable_filter_ns, key_not, + op_trait_if_signal(trait_ent_filter_not, trait_ent_filter_not_untrack), + make_proto_for_trackable_ns(trait_ent_filter_not, trait_ent_filter_not_untrack), + 1 +); + +// readable.select:ns (track|untrack) +// .select.multiple (track|untrack) +const proto_entity_readable_select_ns = obj_create( + make_proto_for_trackable_ns(trait_ent_select, trait_ent_select_untrack) +); +obj_def_prop_trait_ns_with_ns(proto_entity_readable_select_ns, key_multiple, trait_ent_select_multiple, + make_proto_for_trackable_ns(trait_ent_select_multiple, trait_ent_select_multiple_untrack) +); + +// readable.as:ns +// .as.value +const proto_entity_readable_as_ns = obj_create(pure_fn); +obj_def_prop_trait_ns(proto_entity_readable_as_ns, key_value, trait_ent_as_value); + +// readable +// .sync +// .op +// .to:readable.to:ns +// .to.once +// .filter:readable.filter:ns (track|untrack) +// .filter.not (track|untrack) +// .select:readable.select:ns (track|untrack) +// .select.multiple (track|untrack) +// .flow (track|untrack) +// .view (track|untrack) +// .join (track|untrack) +// .as:readable.as:ns +// .as.value +// .promise +const proto_entity_readable = obj_create(pure_fn); +obj_def_prop_trait(proto_entity_readable, key_sync, trait_ent_sync); +obj_def_prop_trait(proto_entity_readable, key_op, trait_ent_op); +obj_def_prop_trait_with_ns( + proto_entity_readable, + key_to, + trait_ent_to, + proto_entity_readable_to_ns +); +obj_def_prop_trait_with_ns( + proto_entity_readable, + key_filter, + op_trait_if_signal(trait_ent_filter, trait_ent_filter_untrack), + proto_entity_readable_filter_ns, + 1 +); +obj_def_prop_trait_with_ns( + proto_entity_readable, + key_flow, + op_trait_if_signal(trait_ent_flow, trait_ent_flow_untrack), + make_proto_for_trackable_ns(trait_ent_flow, trait_ent_flow_untrack), + 1 +); +obj_def_prop_trait_with_ns( + proto_entity_readable, + key_select, + trait_ent_select, + proto_entity_readable_select_ns, +); +obj_def_prop_trait_with_ns( + proto_entity_readable, + key_view, + trait_ent_view, + make_proto_for_trackable_ns(trait_ent_view, trait_ent_view_untrack), +); +obj_def_prop_trait_with_ns( + proto_entity_readable, + key_join, + trait_ent_join, + make_proto_for_trackable_ns(trait_ent_join, trait_ent_join_untrack), +); +obj_def_prop_with_ns( + proto_entity_readable, + key_as, + proto_entity_readable_as_ns +); +obj_def_prop_promise(proto_entity_readable); + +// writtable.update.by:ns +// .update.by.once +const proto_entity_writtable_update_by_ns = obj_create(pure_fn); +obj_def_prop_trait_ns(proto_entity_writtable_update_by_ns, key_once, trait_ent_update_by_once); + +// writtable.update:ns (track|untrack) +// .update.by:writtable.update.by:ns +const proto_entity_writtable_update_ns = obj_create( + make_proto_for_trackable_ns(trait_ent_update, trait_ent_update_untrack) +); +obj_def_prop_trait_ns_with_ns(proto_entity_writtable_update_ns, key_by, trait_ent_update_by, + proto_entity_writtable_update_by_ns +); + +// writtable.updater:ns +// .updater.value +const proto_entity_writtable_updater_ns = obj_create(pure_fn); +obj_def_prop_trait_ns(proto_entity_writtable_updater_ns, key_value, trait_ent_updater_value); + +// writtable.pre.filter:ns (track|untrack) +// .pre.filter.not (track|untrack) +const proto_entity_writtable_pre_filter_ns = obj_create( + make_proto_for_trackable_ns(trait_ent_pre_filter, trait_ent_pre_filter_untrack) +); +obj_def_prop_trait_ns_with_ns(proto_entity_writtable_pre_filter_ns, key_not, trait_ent_pre_filter_not, + make_proto_for_trackable_ns(trait_ent_pre_filter_not, trait_ent_pre_filter_not_untrack) +); + +// writtable.pre:ns (track|untrack) +// .pre.filter:writtable.pre.filter:ns (track|untrack) +const proto_entity_writtable_pre_ns = obj_create( + make_proto_for_trackable_ns(trait_ent_pre, trait_ent_pre_untrack) +); +obj_def_prop_trait_ns_with_ns( + proto_entity_writtable_pre_ns, + key_filter, + trait_ent_pre_filter, + proto_entity_writtable_pre_filter_ns +); + +// writtable <- readable +// .update:writtable.update:ns (track|untrack) +// .update.by +// .updater:writtable.updater:ns +// .updater.value +// .pre:writtable.pre:ns (track|untrack) +// .pre.filter:writtable.pre.filter:ns (track|untrack) +// .pre.filter.not (track|untrack) +const proto_entity_writtable = obj_create(proto_entity_readable); +obj_def_prop_trait_with_ns( + proto_entity_writtable, + key_update, + trait_ent_update_untrack, + proto_entity_writtable_update_ns +); +obj_def_prop_trait_with_ns( + proto_entity_writtable, + key_updater, + trait_ent_updater, + proto_entity_writtable_updater_ns +); +obj_def_prop_trait_with_ns( + proto_entity_writtable, + key_pre, + trait_ent_pre, + proto_entity_writtable_pre_ns +); + +// writtable_leaf.reset.by:ns +// .reset.by.once +const proto_entity_writtable_leaf_reset_by_ns = obj_create(pure_fn); +obj_def_prop_trait_ns(proto_entity_writtable_leaf_reset_by_ns, key_once, trait_ent_reset_by_once); + +// writtable_leaf.reset:ns +// .reset.by:writtable_leaf.reset.by:ns +const proto_entity_writtable_leaf_reset_ns = obj_create(pure_fn); +obj_def_prop_trait_ns_with_ns(proto_entity_writtable_leaf_reset_ns, key_by, trait_ent_reset_by, + proto_entity_writtable_leaf_reset_by_ns +); + +// writtable_leaf.reinit.by:ns +// .reinit.by.once +const proto_entity_writtable_leaf_reinit_by_ns = obj_create(pure_fn); +obj_def_prop_trait_ns(proto_entity_writtable_leaf_reinit_by_ns, key_once, trait_ent_reinit_by_once); + +// writtable_leaf.reinit:ns +// .reinit.by:writtable_leaf.reinit.by:ns +const proto_entity_writtable_leaf_reinit_ns = obj_create(pure_fn); +obj_def_prop_trait_ns_with_ns(proto_entity_writtable_leaf_reinit_ns, key_by, trait_ent_reinit_by, + proto_entity_writtable_leaf_reinit_by_ns +); + + +// writtable_leaf <- writtable +// .reset:writtable_leaf.reset:ns +// .reset.by +// .reinit:writtable_leaf.reinit:ns +// .reinit.by +// .dirty +const proto_entity_writtable_leaf = obj_create(proto_entity_writtable); +obj_def_prop_trait_with_ns( + proto_entity_writtable_leaf, + key_reset, + trait_ent_reset, + proto_entity_writtable_leaf_reset_ns +); +obj_def_prop_trait_with_ns( + proto_entity_writtable_leaf, + key_reinit, + trait_ent_reinit, + proto_entity_writtable_leaf_reinit_ns +); +obj_def_prop_factory( + proto_entity_writtable_leaf, + key_dirty, + prop_factory_dirty_required_initial +); + + + +const make_trigger = (initial, has_inverted_to?, is_signal?) => { + const handler = box(initial, () => (handler[key_touched_internal] = 1), is_signal && pure_arrow_fn_returns_undef); + const set = has_inverted_to + ? () => { handler[key_touched_internal] || handler[1](!untrack(handler[0])) } + : (v) => { handler[key_touched_internal] || handler[1](v) }; + handler[key_reset_promise_by_reset] = 1; + handler[key_is_signal] = is_signal; + return fill_entity(handler, proto_entity_writtable_leaf, 1, initial, 0, set); } -function flow_filter(target: Reactionable, fn: (data: T) => boolean) { - const f = (target as any).then ? signal() : value(); - on(target, v => { - if (fn(v)) f(v); - }); - return f; -} +const get_getter_to_reactionable_or_custom = (re) => ( + (re && re[key_get]) || (typeof re === const_string_function ? re : () => re) +) -function loop(body: () => Promise) { - let running = 1; - const fn = async () => { - while (running) await body(); - }; - const unsub = () => { - if (running) running = 0; - }; - un(unsub); - fn(); - return unsub; +const make_combine = (cfg, is_signal?) => { + const keys = obj_keys(cfg); + const fns = keys.map(is_signal + ? (key) => get_getter_to_reactionable_or_custom(cfg[key]) + : (key) => sel(get_getter_to_reactionable_or_custom(cfg[key]))[0] + ); + const is_array = obj_is_array(cfg); + const h = [ + () => keys.reduce((ret, key, key_index) => ( + (ret[key] = fns[key_index]()), ret + ), is_array ? [] : {}) + ]; + h[key_is_signal] = is_signal; + return fill_entity(h, proto_entity_readable); } -function stoppable(): StopSignal { - if (!stoppable_context) throw new Error('Parent context not found'); - if (stoppable_context === 1) stoppable_context = stop_signal(); - return stoppable_context; -} -function pool Promise>(body: K): Pool { - const threads = value([]); - const count = threads.select(t => t.length); - const pending = count.select(c => c > 0); - function run(this: any) { - const stop = stop_signal(); - isolate(threads.sub.once(stop, t => t.filter(ctx => ctx !== stop))); - threads.update(t => t.concat(stop as any)); +const selector: SelectorEntry = (fn) => ( + fill_entity(sel(fn).slice(0, 1), proto_entity_readable) +) - const stack = stoppable_context; - stoppable_context = stop; - let ret; - try { - ret = (body as any).apply(this, arguments); - } finally { - stoppable_context = stack; +const value: ValueEntry = ((initial) => ( + fill_entity(box(initial), proto_entity_writtable_leaf, 1, initial) +)) as any; - if (ret && ret.finally) { - ret.finally(stop); - } else { - stop(); - } - } - return ret; - } - run.count = count; - run.threads = threads; - run.pending = pending; +const value_trigger = (initial) => make_trigger(initial); +const value_trigger_flag = (initial) => make_trigger(!!initial, 1); +const value_trigger_flag_invert = (initial) => make_trigger(!initial, 1); +const value_from = (get, set?) => ( + (get = sel(get[key_get] || get).slice(0, 1), + set && ((set = set[key_set] || set), (get[1] = set.bind()))), + fill_entity(get, set ? proto_entity_writtable : proto_entity_readable) +); +const value_combine = (cfg) => make_combine(cfg); - return run as any; -} +value_trigger_flag[key_invert] = value_trigger_flag_invert; +value_trigger[key_flag] = value_trigger_flag; +value[key_trigger] = value_trigger as any; +value[key_from] = value_from; +value[key_combine] = value_combine; -function on( - target: Reactionable>, - listener: (value: T, prev: Ensurable) => void -): () => void; -function on(target: Reactionable, listener: (value: T, prev: T) => void): () => void; -function on(target: any, listener: (value: any, prev?: any) => void): () => void { - const sync_mode = is_sync; - let free: (() => void) | undefined; - is_sync = 0; +const signal: SignalEntry = ((initial) => { + const h = box(initial, 0 as any, pure_arrow_fn_returns_undef); + h[key_is_signal] = 1; + return fill_entity(h, proto_entity_writtable_leaf, 1, initial) +}) as any; - if (target[0]) { - target = target[0]; // box or selector or custom reactive - } else { - [target, free] = sel(target); - } +const signal_trigger = (initial) => make_trigger(initial, 0, 1); +const signal_trigger_flag = (initial) => make_trigger(!!initial, 1, 1); +const signal_trigger_flag_invert = (initial) => make_trigger(!initial, 1, 1); +const signal_from = (get, set?) => ( + (get = [get[key_get] || get], + (get[key_is_signal] = 1), + set && ((set = set[key_set] || set), (get[1] = set.bind()))), + fill_entity(get, set ? proto_entity_writtable : proto_entity_readable) +); +const signal_combine = (cfg) => make_combine(cfg, 1); - let value: any; +signal_trigger_flag[key_invert] = signal_trigger_flag_invert; +signal_trigger[key_flag] = signal_trigger_flag; +signal[key_trigger] = signal_trigger as any; +signal[key_from] = signal_from; +signal[key_combine] = signal_combine; - const [run, stop] = expr(target, () => { - const prev = value; - listener((value = run()), prev); - }); - value = run(); - const unsub = () => { - if (free) free(); - stop(); - }; - un(unsub); - if (sync_mode) listener(value); - return unsub; -} -on.once = once; -function once( - target: Reactionable>, - listener: (value: T, prev: Ensurable) => void -): () => void; -function once(target: Reactionable, listener: (value: T, prev: T) => void): () => void; -function once(target: any, listener: (value: any, prev?: any) => void): () => void { - const unsub = on(target, (value, prev) => { - try { - listener(value, prev); - } finally { - unsub(); - } - }); - return unsub; -} +// +// Realar internal +// -function sync(target: Reactionable, listener: (value: T, prev: T) => void): () => void { - is_sync = 1; - return on(target, listener); +const call_fns_array = (arr) => arr.forEach(fn => fn()); +const throw_local_inject_error = () => { + throw new Error('The local.inject section available only in useLocal'); +} +const internal_isolate = () => { + const stack = context_unsubs; + context_unsubs = []; + return () => { + const unsubs = context_unsubs; + context_unsubs = stack; + return () => unsubs && call_fns_array(unsubs); + }; } -function effect(fn: () => void): () => void; -function effect(fn: () => () => any): () => any; -function effect(fn: any) { - return un(fn()); +// +// Realar exportable api +// + +const transaction = ((fn) => { + const finish = internal_transaction(); + try { return fn() } + finally { finish() } +}) as Transaction; +transaction[key_unsafe] = internal_transaction; + +const untrack = ((fn) => { + const finish = internal_untrack(); + try { return fn() } + finally { finish() } +}) as Untrack; +untrack[key_unsafe] = internal_untrack; + +const isolate = ((fn?: any) => { + let unsubs; + const finish = internal_isolate(); + try { fn() } + finally { unsubs = finish() } + return unsubs; +}) as Isolate; +isolate[key_unsafe] = internal_isolate; + + +const un = (unsub: () => void) => { + unsub && context_unsubs && context_unsubs.push(unsub) } -function un(unsub: () => void): () => void { - if (unsub && context_unsubs) context_unsubs.push(unsub); - return unsub; + +const local_inject = (fn) => { + if (!context_local_injects) throw_local_inject_error(); + fn && context_local_injects.push(fn); } +const local = {} as Local; +local[key_inject] = local_inject; + + +const on_once = (target, fn) => reactionable_subscribe(target, fn, 1); +const on = (target, fn) => reactionable_subscribe(target, fn); -function cycle(body: () => void) { +on[key_once] = on_once; + +const sync = (target, fn) => reactionable_subscribe(target, fn, 0, 1); + +const cycle = (body) => { const iter = () => { - const stack = stoppable_context; - stoppable_context = stop_signal(); - isolate(once(stoppable_context, stop)); + const stack = context_contextual_stop; + context_contextual_stop = e[1]; try { - run(); + e[0](); } finally { - stoppable_context = stack; + context_contextual_stop = stack; } }; - - const [run, stop] = expr(body, iter); + const e = un_expr(body, iter); iter(); - return un(stop); } -function isolate(): () => () => void; -function isolate(fn: () => void): () => void; -function isolate(fn?: any) { - if (fn) { - if (context_unsubs) context_unsubs = context_unsubs.filter((i: any) => i !== fn); - return fn; +const contextual = {} as Contextual; +obj_def_prop(contextual, key_stop, { + get() { + if (!context_contextual_stop) throw new Error('Parent context not found'); + return context_contextual_stop; } - const stack = context_unsubs; - context_unsubs = []; - return () => { - const unsubs = context_unsubs; - context_unsubs = stack; - return () => { - if (unsubs) call_array(unsubs); - }; - }; -} +}); -function initial(data: any): void { - initial_data = data; -} -function mock(target: (new (init?: any) => M) | ((init?: any) => M), mocked: M): M { - shareds.set(target, mocked); - return mocked; + +// +// Shared technique abstraction +// + +const initial = (data: any): void => { + initial_data = data; } -function unmock( - target: (new (init?: any) => any) | ((init?: any) => any), - ...targets: ((new (init?: any) => any) | ((init?: any) => any))[] -) { - targets.concat(target).forEach(target => shareds.delete(target)); +const inst = ( + target: (new (...args: K) => M) | ((...args: K) => M), + args: K, + local_injects_available?: any +): [M, () => void, (() => void)[]] => { + let instance, unsub, local_injects; + const collect = internal_isolate(); + const track = internal_untrack(); + const stack = context_local_injects; + context_local_injects = []; + try { + instance = + target.prototype === const_undef + ? (target as any)(...args) + : new (target as any)(...args); + if (!local_injects_available && context_local_injects.length > 0) throw_local_inject_error(); + } finally { + unsub = collect(); + track(); + local_injects = context_local_injects; + context_local_injects = stack; + } + return [instance, unsub, local_injects]; } -function shared(target: (new (init?: any) => M) | ((init?: any) => M)): M { +const shared = (target: (new (init?: any) => M) | ((init?: any) => M)): M => { let instance = shareds.get(target); if (!instance) { const h = inst(target, [initial_data]); @@ -674,76 +1311,116 @@ function shared(target: (new (init?: any) => M) | ((init?: any) => M)): M { return instance; } -function inst( - target: (new (...args: K) => M) | ((...args: K) => M), - args: K, - hooks_available?: any -): [M, () => void, (() => void)[]] { - let instance, unsub, hooks; - const collect = isolate(); - const track = untrack(); - const stack = context_hooks; - context_hooks = []; +const free = () => { try { - instance = - typeof target.prototype === 'undefined' - ? (target as any)(...args) - : new (target as any)(...args); - if (!hooks_available && context_hooks.length > 0) throw_hook_error(); + call_fns_array(shared_unsubs); } finally { - unsub = collect(); - track(); - hooks = context_hooks; - context_hooks = stack; + shareds.clear(); + initial_data = const_undef; } - return [instance, unsub, hooks]; } -function throw_hook_error() { - throw new Error('Hook section available only at useLocal'); -} +const mock = (target: (new (init?: any) => M) | ((init?: any) => M), mocked: M): M => ( + shareds.set(target, mocked), + mocked +) -function hook(fn: () => void): void { - if (!context_hooks) throw_hook_error(); - fn && context_hooks.push(fn); -} +const unmock = ( + target: (new (init?: any) => any) | ((init?: any) => any), + ...targets: ((new (init?: any) => any) | ((init?: any) => any))[] +) => ( + targets.concat(target).forEach(target => shareds.delete(target)) +) -function call_array(arr: (() => void)[]) { - arr.forEach(fn => fn()); -} -function get_scope_context(): Context { - return scope_context ? scope_context : (scope_context = (createContext as any)()); -} +// +// Decorator functions "prop" and "cache" +// -function useForceUpdate() { - return useReducer(() => [], [])[1] as () => void; -} +const obj_def_box_prop = (o: any, p: string | number | symbol, init?: any): any => ( + (init = box(init && init())), + obj_def_prop(o, p, { get: init[0], set: init[1] }) +) + +const prop = (_t: any, key: any, descriptor?: any): any => ( + (_t = descriptor?.initializer), { + get() { + obj_def_box_prop(this, key, _t); + return this[key]; + }, + set(value: any) { + obj_def_box_prop(this, key, _t); + this[key] = value; + }, + } +) + +const cache = (_proto: any, key: any, descriptor: any): any => ({ + get() { + const [get] = sel(descriptor.get); + obj_def_prop(this, key, { get }); + return this[key]; + } +}) + + + +// +// React bindings global definitions +// -function observe(FunctionComponent: T): T { - return function (this: any) { - const forceUpdate = useForceUpdate(); - const ref = useRef<[T, () => void]>(); - if (!ref.current) ref.current = expr(FunctionComponent, forceUpdate); - useEffect(() => ref.current![1], []); +let context_is_observe: any; +let react_scope_context: any; +let observe_no_memo_flag: any; - const stack = is_observe; - is_observe = 1; +const key_nomemo = 'nomemo'; + +// +// React bindings +// + +const get_scope_context = (): Context => ( + react_scope_context ? react_scope_context : (react_scope_context = (createContext as any)()) +) + +const useForceUpdate = () => ( + useReducer(() => [], [])[1] as () => void +) + +const observe: Observe = ((target) => { + function fn (this: any) { + const force_update = useForceUpdate(); + const ref = useRef(); + if (!ref.current) ref.current = expr(target, force_update); + useEffect(() => ref.current[1], []); + + const stack = context_is_observe; + context_is_observe = 1; try { - return (ref.current[0] as any).apply(this, arguments); + return ref.current[0].apply(this, arguments); } finally { - is_observe = stack; + context_is_observe = stack; } - } as any; -} + } + return observe_no_memo_flag + ? ((observe_no_memo_flag = 0), fn) + : memo(fn) +}) as any; + +const observe_nomemo: Observe['nomemo'] = (target): any => ( + (observe_no_memo_flag = 1), + observe(target) +) + +observe[key_nomemo] = observe_nomemo; const Scope: FC = ({ children }) => { const h = useMemo(() => [new Map(), [], []], []) as any; - useEffect(() => () => call_array(h[1]), []); + useEffect(() => () => call_fns_array(h[1]), []); return createElement(get_scope_context().Provider, { value: h }, children); }; -function useScoped(target: (new (init?: any) => M) | ((init?: any) => M)): M { +const useScoped: UseScoped = (target) => { const context_data = useContext(get_scope_context()); if (!context_data) { throw new Error('"Scope" parent component didn\'t find'); @@ -761,14 +1438,12 @@ function useScoped(target: (new (init?: any) => M) | ((init?: any) => M)): M return instance; } -function useLocal( - target: (new (...args: T) => M) | ((...args: T) => M), - deps = ([] as unknown) as T -): M { +const useLocal: UseLocal = (target, deps: any) => { + deps || (deps = []); const h = useMemo(() => { const i = inst(target, deps, 1); - const call_hooks = () => call_array(i[2]); - return [i[0], () => i[1], call_hooks] as any; + const call_local_injects = () => call_fns_array(i[2]); + return [i[0], () => i[1], call_local_injects] as any; }, deps); h[2](); @@ -776,18 +1451,19 @@ function useLocal( return h[0]; } -function useValue(target: Reactionable, deps: any[] = []): T { - const forceUpdate = is_observe || useForceUpdate(); +const useValue: UseValue = (target, deps) => { + deps || (deps = []); + const force_update = context_is_observe || useForceUpdate(); const h = useMemo(() => { if (!target) return [target, () => {}]; - if ((target as any)[0]) target = (target as any)[0]; // box or selector or custom reactive + if ((target as any)[key_get]) target = (target as any)[key_get]; - if (typeof target === 'function') { - if (is_observe) { + if (typeof target === const_string_function) { + if (context_is_observe) { return [target, 0, 1]; } else { const [run, stop] = expr(target as any, () => { - forceUpdate(); + force_update(); run(); }); run(); @@ -798,44 +1474,61 @@ function useValue(target: Reactionable, deps: any[] = []): T { } }, deps); - is_observe || useEffect(h[1], [h]); + context_is_observe || useEffect(h[1], [h]); return h[2] ? h[0]() : h[0]; } -function free() { - try { - call_array(shared_unsubs); - } finally { - shareds.clear(); - initial_data = void 0; +const useValues: UseValues = (targets, deps):any => { + deps || (deps = []); + const h = useMemo(() => value.combine(targets), deps); + return useValue(h, [h]); +}; + +const useJsx: UseJsx = (target, deps): any => ( + useMemo(() => observe(target as any), deps || []) +); + + + +// +// Pool abstraction +// + +const pool: PoolEntry = (body): any => { + const threads = value([]); + const count = threads.select(t => t.length); + const pending = count.select(c => c > 0); + + function run(this: any) { + const stop = () => threads.update(t => t.filter(ctx => ctx !== stop)); + threads.update(t => t.concat(stop as any)); + + const stack = context_contextual_stop; + context_contextual_stop = stop; + + let ret; + try { + ret = (body as any).apply(this, arguments); + } finally { + context_contextual_stop = stack; + + if (ret && ret.finally) { + ret.finally(stop); + } else { + stop(); + } + } + return ret; } -} + run.count = count; + run.threads = threads; + run.pending = pending; -function box_property(o: any, p: string | number | symbol, init?: any): any { - const b = box(init && init()); - def_prop(o, p, { get: b[0], set: b[1] }); + return run; } -function prop(_proto: any, key: any, descriptor?: any): any { - const initializer = descriptor?.initializer; - return { - get() { - box_property(this, key, initializer); - return this[key]; - }, - set(value: any) { - box_property(this, key, initializer); - this[key] = value; - }, - }; -} -function cache(_proto: any, key: any, descriptor: any): any { - return { - get() { - const [get] = sel(descriptor.get); - def_prop(this, key, { get }); - return this[key]; - }, - }; -} +// +// End of File +// Enjoy and Happy Coding! +// diff --git a/tests/filter.test.ts b/tests/builder-5/filter.test.ts similarity index 67% rename from tests/filter.test.ts rename to tests/builder-5/filter.test.ts index e2ef5f83..3970ace3 100644 --- a/tests/filter.test.ts +++ b/tests/builder-5/filter.test.ts @@ -1,12 +1,12 @@ -import { signal, on } from '../src'; +import { signal, on } from '../../src'; -test('should work basic operations with wrap filter method', async () => { +test('should work basic operations with pre.filter method', async () => { const spy = jest.fn(); const a = signal(0); const b = signal(0); - const c = a.wrap.filter(() => b.val); + const c = a.pre.filter(() => b.val); on(c, spy); @@ -22,13 +22,13 @@ test('should work basic operations with wrap filter method', async () => { expect(spy).toHaveBeenNthCalledWith(2, 3, 2); }); -test('should work basic operations with flow filter method', async () => { +test('should work basic operations with filter method', async () => { const spy = jest.fn(); const a = signal(0); const b = signal(0); - const c = a.flow.filter(() => b.val); + const c = a.filter(() => b.val); on(c, spy); diff --git a/tests/wrap.test.ts b/tests/builder-5/pre.test.ts similarity index 53% rename from tests/wrap.test.ts rename to tests/builder-5/pre.test.ts index e06c0214..22b4adab 100644 --- a/tests/wrap.test.ts +++ b/tests/builder-5/pre.test.ts @@ -1,14 +1,11 @@ -import { signal, stoppable, selector, on } from '../src'; +import { signal, on } from '../../src'; test('should work basic operations with wrap and signal', async () => { const spy = jest.fn(); const s = signal(0); - const s_1 = s.wrap( - (v: string) => parseInt(v + v), - (g: number) => g + 77 - ); - const s_2 = s_1.wrap((v: number) => '' + (v + 1)); + const s_1 = s.pre((v: string) => parseInt(v + v)).view((g: number) => g + 77); + const s_2 = s_1.pre((v: number) => '' + (v + 1)); expect(s_1.val).toBe(77); s_1('1'); @@ -16,27 +13,18 @@ test('should work basic operations with wrap and signal', async () => { s_2(10); expect(s_2.val).toBe(1188); - const [get, set] = s_2; + const {get, set} = s_2; on(s_2, spy); set(10); expect(spy).toBeCalledWith(1188, 1188); expect(get()).toBe(1188); }); -test('should work basic operations with wrap and stoppable', async () => { +test('should work basic operations with wrap and pre.filter', async () => { const spy = jest.fn(); const s = signal(0); - const s_1 = s.wrap((v: number) => { - const stop = stoppable(); - - expect((stop as any).reset).not.toBeDefined(); - - if (v === 10 || v === 15) { - stop(); - } - return v; - }); + const s_1 = s.pre.filter((v) => v !== 10 && v !== 15); on(s_1, spy); diff --git a/tests/reset.test.ts b/tests/builder-5/reset.test.ts similarity index 86% rename from tests/reset.test.ts rename to tests/builder-5/reset.test.ts index 5fd11470..c6b29a8f 100644 --- a/tests/reset.test.ts +++ b/tests/builder-5/reset.test.ts @@ -1,4 +1,4 @@ -import { signal, value } from '../src'; +import { signal, value } from '../../src'; test('should work basic operations with reset for differents', async () => { const s = signal(0); diff --git a/tests/selector.test.ts b/tests/builder-5/selector.test.ts similarity index 73% rename from tests/selector.test.ts rename to tests/builder-5/selector.test.ts index ea072077..fe34318a 100644 --- a/tests/selector.test.ts +++ b/tests/builder-5/selector.test.ts @@ -1,4 +1,4 @@ -import { sync, value, selector } from '../src'; +import { sync, value, selector } from '../../src'; test('should work basic operations with selector', () => { const spy_1 = jest.fn(); @@ -6,8 +6,7 @@ test('should work basic operations with selector', () => { const a = value(7); const s = selector(() => (spy_1(a.val), a.val)); - const [get] = s; - const { free } = s; + const {get} = s; sync(s, v => spy_2(v)); @@ -21,9 +20,4 @@ test('should work basic operations with selector', () => { a.val++; expect(spy_1).toHaveBeenNthCalledWith(2, 8); expect(spy_2).toHaveBeenNthCalledWith(2, 8); - - free(); - a.val++; - expect(spy_1).toBeCalledTimes(2); - expect(spy_2).toBeCalledTimes(2); }); diff --git a/tests/signal.test.ts b/tests/builder-5/signal.test.ts similarity index 79% rename from tests/signal.test.ts rename to tests/builder-5/signal.test.ts index 29b5a3bd..28ae62b2 100644 --- a/tests/signal.test.ts +++ b/tests/builder-5/signal.test.ts @@ -1,4 +1,4 @@ -import { signal, cycle, on, value } from '../src'; +import { signal, cycle, on, value } from '../../src'; test('should work signal different using', () => { const spy = jest.fn(); @@ -6,10 +6,9 @@ test('should work signal different using', () => { const a = signal(10); on(a, spy); - const [get] = a; + const {get} = a; expect(a.val).toBe(10); - expect(a[0]()).toBe(10); expect(get()).toBe(10); expect(spy).toBeCalledTimes(0); @@ -19,7 +18,6 @@ test('should work signal different using', () => { a(a.val + 5); expect(a.val).toBe(15); - expect(a[0]()).toBe(15); expect(get()).toBe(15); expect(a.get()).toBe(15); @@ -32,7 +30,7 @@ test('should work signal in cycle', () => { const a = signal(); cycle(() => { - const data = a[0](); + const data = a.get(); spy(data); }); @@ -50,7 +48,7 @@ test('should work signal as promise', async () => { const a = signal(); const fn = async () => { - spy(await a); + spy(await a.promise); }; fn(); expect(spy).toBeCalledTimes(0); @@ -86,7 +84,7 @@ test('should work signal in on', () => { test('should work signal with transform', () => { const spy = jest.fn(); - const a = signal(0).wrap((s: string) => parseInt(s) + 10); + const a = signal(0).pre((s: string) => parseInt(s) + 10); on(a, v => { spy(v); }); @@ -106,26 +104,25 @@ test('should work signal from', async () => { setTimeout(() => (v.val = 2), 100); expect(s.val).toBe(2); - expect(await s).toBe(4); + expect(await s.promise).toBe(4); }); test('should work signal combine', async () => { const spy = jest.fn(); const v = value(1); - const s = signal.from(v.select(v => v + v)); + const s = signal.from(v.select(v => v + v), (k) => v.update(_v => _v + k)); - const c = signal.combine(v, s); - c.watch(v => spy(v)); + const c = signal.combine([v, s]); + c.to(v => spy(v)); expect(c.val).toEqual([1, 2]); - s(10); - s(10); + s(4); + s(4); v(2); v(2); - expect(spy).toHaveBeenNthCalledWith(1, [1, 10]); - expect(spy).toHaveBeenNthCalledWith(2, [1, 10]); - expect(spy).toHaveBeenNthCalledWith(3, [2, 10]); // Todo: hmm - expect(spy).toHaveBeenNthCalledWith(4, [2, 4]); - expect(spy).toBeCalledTimes(4); + expect(spy).toHaveBeenNthCalledWith(1, [5, 10]); + expect(spy).toHaveBeenNthCalledWith(2, [9, 18]); + expect(spy).toHaveBeenNthCalledWith(3, [2, 4]); + expect(spy).toBeCalledTimes(3); }); diff --git a/tests/watch.test.ts b/tests/builder-5/to.test.ts similarity index 69% rename from tests/watch.test.ts rename to tests/builder-5/to.test.ts index 6e6e3e88..4f34fee3 100644 --- a/tests/watch.test.ts +++ b/tests/builder-5/to.test.ts @@ -1,16 +1,15 @@ -import { signal } from '../src'; +import { signal } from '../../src'; test('should work basic operations with watch and wrapped signal', async () => { const spy = jest.fn(); const s = signal(0); - const s_1 = s.wrap( - (v: string) => parseInt(v + v), - (g: number) => g + 77 - ); - const s_2 = s_1.wrap((v: number) => '' + (v + 1)); + const s_1 = s + .pre((v: string) => parseInt(v + v)) + .view((g: number) => g + 77); + const s_2 = s_1.pre((v: number) => '' + (v + 1)); - s_2.watch(spy); + s_2.to(spy); s_2(10); expect(spy).toBeCalledWith(1188, 77); @@ -21,7 +20,7 @@ test('should work basic operations with watch once and signal', async () => { const spy = jest.fn(); const s = signal(0); - s.watch.once(spy); + s.to.once(spy); expect(spy).toBeCalledTimes(0); s(10); diff --git a/tests/builder-5/trigger.test.ts b/tests/builder-5/trigger.test.ts new file mode 100644 index 00000000..2654285f --- /dev/null +++ b/tests/builder-5/trigger.test.ts @@ -0,0 +1,144 @@ +import { on, signal } from '../../src'; +import { delay } from '../lib'; + +test('should work signal.trigger with one value', async () => { + const spy = jest.fn(); + const a = signal.trigger(0); + on(a, spy); + + expect(a.val).toBe(0); + + const m = async () => { + expect(await a.promise).toBe(1); + }; + const k = async () => { + await delay(10); + a(1); + }; + await Promise.all([m(), k()]); + + a(2); + expect(a.val).toBe(1); + expect(await a.promise).toBe(1); + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(1, 0); +}); + +test('should work signal.trigger with configured .pre', async () => { + // const spy = jest.fn(); + // const a = _signal.trigger(0).pre(() => 1); + // _on(a, spy); + + // expect(a.val).toBe(0); + // a(); + // expect(a.val).toBe(1); + // expect(await a.promise).toBe(1); + + // expect(spy).toBeCalledTimes(1); + // expect(spy).toBeCalledWith(1, 0); +}); + +test('should work signal.trigger reset', async () => { + const spy = jest.fn(); + const a = signal.trigger(0).pre(() => 1); + on(a, spy); + + expect(a.val).toBe(0); + a(); + expect(a.val).toBe(1); + a.reset(); + expect(a.val).toBe(0); + + const m = async () => { + expect(await a.promise).toBe(1); + }; + const k = async () => { + await delay(10); + a(); + }; + await Promise.all([m(), k()]); + + expect(a.val).toBe(1); + + expect(spy).toBeCalledTimes(3); + expect(spy).toHaveBeenNthCalledWith(1, 1, 0); + expect(spy).toHaveBeenNthCalledWith(2, 0, 1); + expect(spy).toHaveBeenNthCalledWith(3, 1, 0); +}); + +test('should work _signal.trigger wrapped', async () => { + const spy = jest.fn(); + const a = signal.trigger(0) + .pre((v: number) => v * 2) + .pre(() => 10); + on(a, spy); + + expect(a.val).toBe(0); + a(); + expect(a.val).toBe(20); + a.reset(); + expect(a.val).toBe(0); + + const m = async () => { + expect(await a.promise).toBe(20); + }; + const k = async () => { + await delay(10); + a(); + }; + await Promise.all([m(), k()]); + + expect(a.val).toBe(20); + + expect(spy).toBeCalledTimes(3); + expect(spy).toHaveBeenNthCalledWith(1, 20, 0); + expect(spy).toHaveBeenNthCalledWith(2, 0, 20); + expect(spy).toHaveBeenNthCalledWith(3, 20, 0); +}); + + +// test('should work ready from value', async () => { +// const spy = jest.fn(); + +// const s = value(1); +// const r = ready.from(s); + +// sync(r, spy); + +// s(1); +// s(2); +// s(3); + +// expect(spy).toBeCalledTimes(2); +// expect(spy).toHaveBeenNthCalledWith(1, 1); +// expect(spy).toHaveBeenNthCalledWith(2, 2, 1); +// }); + +// test('should work ready from signal', async () => { +// const spy = jest.fn(); + +// const s = _signal(1); +// const r = ready.from(s); + +// sync(r, spy); + +// s(1); +// s(3); + +// expect(spy).toBeCalledTimes(2); +// expect(spy).toHaveBeenNthCalledWith(1, 1); +// expect(spy).toHaveBeenNthCalledWith(2, 1, 1); +// }); + +// test('should work ready resolved', async () => { +// const r = ready.resolved(1); +// expect(r.val).toBe(1); +// expect(await r.promise).toBe(1); +// }); + +// test('should work ready with undefined resolved', async () => { +// const r = ready.resolved(); +// expect(r.val).toBe(void 0); +// expect(await r.promise).toBe(void 0); +// }); diff --git a/tests/value.test.ts b/tests/builder-5/value.test.ts similarity index 74% rename from tests/value.test.ts rename to tests/builder-5/value.test.ts index c39e5e5c..0c73777c 100644 --- a/tests/value.test.ts +++ b/tests/builder-5/value.test.ts @@ -1,12 +1,12 @@ -import { sync, value, signal } from '../src'; +import { sync, value, signal } from '../../src'; test('should work basic operations with value', () => { const spy = jest.fn(); const a = value(10); - const [get, set] = a; + const {get, set} = a; sync(() => a.val, spy); - expect(spy).toHaveBeenNthCalledWith(1, 10); + expect(spy).toHaveBeenNthCalledWith(1, 10, void 0); set(get() + 5); expect(spy).toHaveBeenNthCalledWith(2, 15, 10); @@ -14,7 +14,7 @@ test('should work basic operations with value', () => { a.val += 10; expect(spy).toHaveBeenNthCalledWith(3, 25, 15); - a[1](a[0]() + a[0]()); + a.set(a.get() + a.get()); expect(spy).toHaveBeenNthCalledWith(4, 50, 25); a(1); @@ -22,9 +22,6 @@ test('should work basic operations with value', () => { a(1); expect(spy).toHaveBeenCalledTimes(5); - - a.set(a.get() + 7); - expect(spy).toHaveBeenNthCalledWith(6, 8, 1); }); test('should work value update', () => { @@ -37,8 +34,8 @@ test('should work value sub method', () => { const a = value(0); const b = value(0); const r = signal(0); - a.sub(r, (a, r, r_prev) => a * 100 + r * 10 + r_prev); - a.sub( + a.update.by(r, (a, r, r_prev) => a * 100 + r * 10 + r_prev); + a.update.by( () => b.val + 1, (a, r, r_prev) => a * 100 + r * 10 + r_prev ); @@ -54,7 +51,7 @@ test('should work value sub method', () => { test('should work value sub once method', () => { const a = value(1); const r = signal(0); - a.sub.once(r, (a, r, r_prev) => a * 100 + r * 10 + r_prev); + a.update.by.once(r, (a, r, r_prev) => a * 100 + r * 10 + r_prev); r(5); r(6); diff --git a/tests/view.test.ts b/tests/builder-5/view.test.ts similarity index 88% rename from tests/view.test.ts rename to tests/builder-5/view.test.ts index bbf29ad9..a948f1d0 100644 --- a/tests/view.test.ts +++ b/tests/builder-5/view.test.ts @@ -1,4 +1,4 @@ -import { signal, stoppable, selector, on, value, transaction } from '../src'; +import { signal, on, value, transaction } from '../../src'; test('should work basic operations with view methods for value', () => { const spy = jest.fn(); @@ -6,7 +6,7 @@ test('should work basic operations with view methods for value', () => { const v_1 = v.view(v => v + v); on(v_1, spy); - const commit = transaction(); + const commit = transaction.unsafe(); v(1); v_1.update(v => v + v); commit(); diff --git a/tests/builder-6.test.ts b/tests/builder-6.test.ts new file mode 100644 index 00000000..1abc31d4 --- /dev/null +++ b/tests/builder-6.test.ts @@ -0,0 +1,1405 @@ +import { value, selector, transaction, cycle, signal } from '../src'; + +test('should work value with call, get, set, update, sync', () => { + const spy = jest.fn(); + let t, r, k; + const v = value(0); + const get = v.get; + + expect(get()).toBe(0); + t = v.sync; k = t(spy); + + expect(spy).toHaveBeenCalledWith(0, void 0); + spy.mockReset(); + + t = k.update; r = t(v => v + 1); + expect(get()).toBe(1); + expect(spy).toHaveBeenCalledWith(1, 0); + spy.mockReset(); + expect(r).toBeUndefined(); + + t = v.set; t(10); + expect(get()).toBe(10); + expect(spy).toHaveBeenCalledWith(10, 1); + spy.mockReset(); + + r = v(11); + expect(r).toBeUndefined(); + expect(get()).toBe(11); + expect(spy).toHaveBeenNthCalledWith(1, 11, 10); + v.call(null, 12); + expect(get()).toBe(12); + expect(spy).toHaveBeenNthCalledWith(2, 12, 11); + v.apply(null, [7]); + expect(get()).toBe(7); + expect(spy).toHaveBeenNthCalledWith(3, 7, 12); + spy.mockReset(); +}); + +test('should work value with reset', () => { + const spyvalue = jest.fn(); + const v = value(0); + const k = value(0); + const m = value(0); + let t; + + v.sync(spyvalue); + expect(spyvalue).toHaveBeenCalledWith(0, void 0); spyvalue.mockReset(); + + (t = v.reset); t(); + expect(spyvalue).toBeCalledTimes(0); + + v(5); + expect(spyvalue).toHaveBeenCalledWith(5, 0); spyvalue.mockReset(); + expect(v.dirty.val).toBe(true); + + t(); + expect(v.dirty.val).toBe(false); + expect(spyvalue).toHaveBeenCalledWith(0, 5); + v(10); + spyvalue.mockReset(); + + (t = v.reset); (t = t.by); t(k).reset.by(() => m.val); + k(1); + expect(spyvalue).toHaveBeenCalledWith(0, 10); v(10); spyvalue.mockReset(); + m(1); + expect(spyvalue).toHaveBeenCalledWith(0, 10); v(10); spyvalue.mockReset(); +}); + +test('should work value with reinit', () => { + const spyvalue = jest.fn(); + const v = value(0); + const k = value(0); + const m = value(0); + let t; + + v.sync(spyvalue); + expect(spyvalue).toHaveBeenCalledWith(0, void 0); spyvalue.mockReset(); + + (t = v.reinit); t(10); + expect(spyvalue).toHaveBeenCalledWith(10, 0); spyvalue.mockReset(); + expect(v.dirty.val).toBe(false); + expect(v.val).toBe(10); + + v(0); + expect(spyvalue).toHaveBeenCalledWith(0, 10); spyvalue.mockReset(); + expect(v.dirty.val).toBe(true); + + (t = v.reset); t(); + expect(v.dirty.val).toBe(false); + expect(spyvalue).toHaveBeenCalledWith(10, 0); spyvalue.mockReset(); + + (t = v.reinit); (t = t.by); t(k).reinit.by(() => m.val); + k(1); + expect(spyvalue).toHaveBeenCalledWith(1, 10); spyvalue.mockReset(); + expect(v.dirty.val).toBe(false); + + m(5); + expect(spyvalue).toHaveBeenCalledWith(5, 1); spyvalue.mockReset(); + expect(v.dirty.val).toBe(false); +}); + +test('should work value with dirty', () => { + const spy = jest.fn(); + const v = value(0); + const dirty = v.dirty; + + expect(typeof dirty).toBe('object'); + expect(dirty.update).toBeUndefined(); + expect(dirty.set).toBeUndefined(); + expect(dirty.dirty).toBeUndefined(); + expect(dirty.get).not.toBeUndefined(); + + const dirty_sync = dirty.sync; + dirty_sync(spy); + + expect(spy).toHaveBeenCalledWith(false, void 0); spy.mockReset(); + v(5); + v(6); + v(5); + expect(spy).toHaveBeenCalledWith(true, false); + expect(spy).toHaveBeenCalledTimes(1); spy.mockReset(); + v(0); + expect(spy).toHaveBeenCalledWith(false, true); spy.mockReset(); + v(10); + expect(spy).toHaveBeenCalledWith(true, false); spy.mockReset(); + v.reset(); + expect(spy).toHaveBeenCalledWith(false, true); spy.mockReset(); + v(0); + v.reset(); + expect(spy).toHaveBeenCalledTimes(0); + + v(10); + expect(dirty.val).toBe(true); + v.reinit(10); + expect(dirty.val).toBe(false); +}); + +test('should work value with update.by', () => { + const v = value(1); + const k = value(0); + const m = value(0); + const p = value(0); + + v + .update.by(k, (_v, _k, _k_prev) => _v * 100 + _k * 10 + _k_prev) + .update.by(() => m.get() + 1, (_v, _m, _m_prev) => _v * 1000 + _m * 10 + _m_prev) + .update.by(p); + expect(v.get()).toBe(1); + k(1); + expect(v.get()).toBe(110); + k(2); + expect(v.get()).toBe(11021); + m(5); + expect(v.get()).toBe(11021061); + p(10); + expect(v.get()).toBe(10); +}); + +test('should work value with val', () => { + const v = value(1); + expect(v.val).toBe(1); + v.val += 1; + expect(v.val).toBe(2); + expect(v.dirty.val).toBe(true); + + expect(() => { + v.dirty.val = true; + }).toThrow('Cannot set property val of [object Object] which has only a getter'); + + v.reset(); + expect(v.val).toBe(1); + expect(v.dirty.val).toBe(false); +}); + +test('should work value with to, to.once', () => { + const spy_to = jest.fn(); + const spy_to_once = jest.fn(); + const v = value(0); + let t; + (t = v.to); t(spy_to); + (t = t.once); t(spy_to_once); + + expect(spy_to).toHaveBeenCalledTimes(0); + expect(spy_to_once).toHaveBeenCalledTimes(0); + + v(0); + expect(spy_to).toHaveBeenCalledTimes(0); + expect(spy_to_once).toHaveBeenCalledTimes(0); + + v(1); + expect(spy_to).toHaveBeenCalledWith(1, 0); spy_to.mockReset(); + expect(spy_to_once).toHaveBeenCalledWith(1, 0); spy_to_once.mockReset(); + + v(2); + expect(spy_to).toHaveBeenCalledWith(2, 1); spy_to.mockReset(); + expect(spy_to_once).toHaveBeenCalledTimes(0); +}); + +test('should work value with select', () => { + const spy = jest.fn(); + let t, s; + const v = value(5); + const k = value(0); + + (t = v.select); s = t((_v) => Math.abs(_v - k.val)).sync(spy); + + expect(s.val).toBe(5); + expect(spy).toHaveBeenCalledWith(5, void 0); + expect(spy).toHaveBeenCalledTimes(1); spy.mockReset(); + k(10); + expect(spy).toHaveBeenCalledTimes(0); + v(15); + expect(spy).toHaveBeenCalledTimes(0); + k(11); + expect(spy).toHaveBeenCalledWith(4, 5); + expect(spy).toHaveBeenCalledTimes(1); spy.mockReset(); + v(5); + expect(spy).toHaveBeenCalledWith(6, 4); + expect(spy).toHaveBeenCalledTimes(1); spy.mockReset(); + expect(s.get()).toBe(6); +}); + +test('should work value with view', () => { + const spy = jest.fn(); + let t; + const v = value(5); + const w = ((t = v.view), t((_v) => _v + _v)); + expect(w.val).toBe(10); + w.val = 16; + expect(w.val).toBe(32); + w.val = 5; + expect(w.val).toBe(10); + expect(v.val).toBe(5); + + (t = w.sync); t(spy); + expect(spy).toHaveBeenCalledWith(10, void 0); spy.mockReset(); + expect(w.dirty.val).toBe(false); + expect(v.dirty.val).toBe(false); + w(10); + expect(w.val).toBe(20); + + expect(spy).toHaveBeenCalledWith(20, 10); spy.mockReset(); + expect(w.dirty.val).toBe(true); + expect(v.dirty.val).toBe(true); + v(5); + expect(w.val).toBe(10); + + expect(spy).toHaveBeenCalledWith(10, 20); spy.mockReset(); + expect(w.dirty.val).toBe(false); + expect(v.dirty.val).toBe(false); +}); + +test('should work value with nested view', () => { + let t; + const v = value(5); + const w = ((t = v.view), t((_v) => _v + _v)); + const k = w.view((_v) => _v + _v); + + expect(k.val).toBe(20); + k(1); + expect(k.val).toBe(4); + expect(w.val).toBe(2); + expect(v.val).toBe(1); +}); + +test('should work value with pre', () => { + let t; + const v = value(5); + const w = ((t = v.pre), t((_v) => _v + _v)); + const k = w.pre((_v) => _v + 100); + + expect(w.val).toBe(5); + w(5); + expect(w.val).toBe(10); + expect(v.val).toBe(10); + expect(k.val).toBe(10); + k.val = 1; + expect(v.val).toBe(202); + expect(k.val).toBe(202); +}); + +test('should work value with pre.filter, pre.filter.not', () => { + let t; + const v = value(5); + const f = value(0); + + const w = v.pre.filter((_v) => _v !== 10); + const k = ((t = w.pre.filter), (t = t.not), t(f)); + const m = k.pre.filter(); + const n = ((t = k.pre), (t = t.filter.not), t()); + + expect(w.val).toBe(5); + expect(k.val).toBe(5); + expect(m.val).toBe(5); + expect(n.val).toBe(5); + + w(10); + expect(v.val).toBe(5); + + n(0); + expect(v.val).toBe(0); + n.val = 1; + expect(v.val).toBe(0); + expect(m.dirty.val).toBe(true); + m(10); + expect(v.val).toBe(0); + m(11); + expect(v.val).toBe(11); + expect(n.val).toBe(11); + m(10); + expect(k.val).toBe(11); + m(0); + expect(m.val).toBe(11); + f(1); + k(20); + m(30); + n(0); + expect(m.val).toBe(11); + f(0); + n(0); + expect(v.val).toBe(0); +}); + +test('should work value with flow', () => { + let t; + const v = value(5); + const x = value(1); + const w = ((t = v.flow), t((_v) => _v + _v)); + const k = w.flow((_v) => _v + 100); + const p = k.flow((_v, _v_prev) => _v * 100 + (_v_prev || 0) * 10 + x.val); + + expect(w.val).toBe(10); + expect(k.val).toBe(110); + expect(p.val).toBe(11001); + + p.val = 6; + expect(w.val).toBe(12); + expect(k.val).toBe(112); + expect(p.val).toBe(112 * 100 + 110 * 10 + 1); + x(2); + expect(p.val).toBe(112 * 100 + 112 * 10 + 2); +}); + + +test('should work value with .filter, .filter.not', () => { + let t; + const v = value(5); + const f = value(0); + + const w = v.filter((_v) => _v !== 10); + const k = ((t = w.filter), (t = t.not), (t = t(f))); + const m = k.filter(); + const n = ((t = t.filter.not), t()); + + expect(n.dirty).toBeUndefined(); + expect(m.reset).toBeUndefined(); + expect(k.reinit).toBeUndefined(); + + expect(w.val).toBe(5); + expect(k.val).toBe(5); + expect(m.val).toBe(5); + expect(n.val).toBe(void 0); + + m(0); + expect(w.val).toBe(0); + expect(k.val).toBe(0); + expect(m.val).toBe(5); + expect(n.val).toBe(0); + + f(1); + w(8); + expect(v.val).toBe(8); + expect(w.val).toBe(8); + expect(k.val).toBe(0); + expect(m.val).toBe(5); + + f(); + expect(k.val).toBe(8); + expect(m.val).toBe(8); + + n(10); + expect(v.val).toBe(10); + expect(w.val).toBe(8); + + v(11); + expect(w.val).toBe(11); +}); + +test('should work value with readable flow', () => { + const v = value("Hi"); + const f = v.select((_v) => _v[1]).flow((v) => v + v); + + expect(typeof f).toBe("object"); + expect(f.set).toBeUndefined(); + expect(f.update).toBeUndefined(); + expect(f.reset).toBeUndefined(); + expect(f.reinit).toBeUndefined(); + expect(f.dirty).toBeUndefined(); + expect(f.get()).toBe('ii'); + expect(f.val).toBe('ii'); + v("/+"); + expect(f.val).toBe('++'); +}); + +test('should work value with promise for value, select, view, pre, flow, readable flow', async () => { + const v = value(0); + const s = v.select(t => t + 1); + const w = v.view(t => t + 2); + const p = v.pre(t => t - 1); + const f = v.flow(t => t + 5); + const r = s.flow(t => t + 3); + + expect(v.promise).toBe(v.promise); + expect(v.promise).not.toBe(s.promise); + expect(s.promise).toBe(s.promise); + expect(v.promise).not.toBe(w.promise); + expect(w.promise).toBe(w.promise); + expect(v.promise).not.toBe(p.promise); + expect(p.promise).toBe(p.promise); + expect(v.promise).not.toBe(f.promise); + expect(f.promise).toBe(f.promise); + expect(v.promise).not.toBe(r.promise); + expect(r.promise).toBe(r.promise); + + const v_p = v.promise; + + setTimeout(() => v.update((k) => k + 10), 3); + const a1 = await Promise.all([ + v.promise, + s.promise, + w.promise, + p.promise, + f.promise, + r.promise + ]); + expect(a1).toStrictEqual([ + 10, + 11, + 12, + 10, + 15, + 14 + ]); + + expect(v_p).not.toBe(v.promise); + expect(v.promise).toBe(v.promise); + + setTimeout(() => v.update((k) => k + 10), 3); + const a2 = await Promise.all([ + v.promise, + s.promise, + w.promise, + p.promise, + f.promise, + r.promise + ]); + expect(a2).toStrictEqual([ + 20, + 21, + 22, + 20, + 25, + 24 + ]); +}); + +test('should work value.trigger common support and promise', async () => { + let t; + const v = value.trigger(0); + + let promise = v.promise; + expect(v.promise).toBe(promise); + + setTimeout(v.bind(0, 1)); + expect(await v.promise).toBe(1); + expect(v.promise).toBe(promise); + expect(v.val).toBe(1); + + v.val = 2; + expect(v.promise).toBe(promise); + expect(v.val).toBe(1); + + v.reset(); + expect(v.promise).not.toBe(promise); + promise = v.promise; + expect(v.val).toBe(0); + setTimeout(v.bind(0, 5)); + expect(await v.promise).toBe(5); + expect(v.val).toBe(5); + + v(7); + expect(v.promise).toBe(promise); + expect(v.val).toBe(5); + + v.reinit(10); + expect(v.get()).toBe(10); + expect(v.promise).not.toBe(promise); + promise = v.promise; + v.update(v => v + 5); + expect(((t = v.get), t())).toBe(15); + expect(v.promise).toBe(promise); +}); + +test('should work value.trigger with select, update.by, flow, pre, view', () => { + const p = value(''); + const v = value.trigger('e'); + const t = v.pre((k) => 'h' + k).view((k) => k + 'lo').update.by(p); + const s = t.select((v) => v.slice(0, 3)); + const f = t.flow((v) => v.slice(-3) + p.val); + + expect(s.val).toBe('elo'); + expect(f.val).toBe('elo'); + p.update(() => 'el'); + expect(t.val).toBe('hello'); + expect(s.val).toBe('hel'); + expect(f.val).toBe('lloel'); + p('x'); + expect(t.val).toBe('hello'); + expect(s.val).toBe('hel'); + expect(f.val).toBe('llox'); + + t.reset(); + expect(s.val).toBe('elo'); + expect(f.val).toBe('elox'); +}); + +test('should work value.trigger.flag and .trigger.flag.invert', () => { + const f = value.trigger.flag(); + const i = value.trigger.flag.invert(); + + expect(f.val).toBe(false); + expect(i.val).toBe(true); + f(); i(); + expect(f.val).toBe(true); + expect(i.val).toBe(false); + f(); i(); + expect(f.val).toBe(true); + expect(i.val).toBe(false); + f.reset(); i.reset(); + expect(f.val).toBe(false); + expect(i.val).toBe(true); + f(); i(); + expect(f.val).toBe(true); + expect(i.val).toBe(false); +}); + +test('should work selector basic support', () => { + let t; + const spy = jest.fn(); + const a = value(0); + const b = value(1); + const s = selector(() => a.val * 100 + b.val); + + expect(typeof s).toBe('object'); + expect(s.to).not.toBeUndefined(); + expect(s.to.once).not.toBeUndefined(); + expect(s.flow).not.toBeUndefined(); + expect(s.filter).not.toBeUndefined(); + expect(s.filter.not).not.toBeUndefined(); + expect(s.view).not.toBeUndefined(); + expect(s.promise).not.toBeUndefined(); + + expect(s.set).toBeUndefined(); + expect(s.update).toBeUndefined(); + expect(s.pre).toBeUndefined(); + expect(s.reset).toBeUndefined(); + expect(s.reinit).toBeUndefined(); + expect(s.dirty).toBeUndefined(); + + expect(s.get()).toBe(s.val); + expect(s.val).toBe(1); + + (t = s.sync), t(spy); + expect(spy).toBeCalledWith(1, void 0); spy.mockReset(); + + a.val = 1; + expect(spy).toBeCalledWith(101, 1); spy.mockReset(); + b.val = 2; + expect(spy).toBeCalledWith(102, 101); spy.mockReset(); + + transaction(() => { + a.val = 0; + b.val = 102; + }); + expect(spy).toBeCalledTimes(0); + expect(a.val).toBe(0); + expect(b.val).toBe(102); +}); + +test('should work selector with to, filter, view', () => { + let t; + const spy = jest.fn(); + const b = value(1); + const v = value(0); + const s = selector(() => v.val + 1); + const f = s.filter.not(b); + const w = s.view((v) => v + 5); + + (t = s.to), t(spy); + expect(spy).toBeCalledTimes(0); + + v(1); + expect(spy).toBeCalledWith(2, 1); spy.mockReset(); + expect(s.val).toBe(2); + expect(f.val).toBe(void 0); + expect(w.val).toBe(7); + v(2); + expect(spy).toBeCalledWith(3, 2); spy.mockReset(); + expect(s.val).toBe(3); + expect(f.val).toBe(void 0); + expect(w.val).toBe(8); + b(0); + expect(f.val).toBe(3); +}); + +test('should work value.from with one argument', () => { + let t; + const spy = jest.fn(); + const a = value(0); + const v = value.from(() => a.val + 1); + expect(v.val).toBe(1); + (t = v.to), t(spy); + + a.val = 1; + expect(v.val).toBe(2); + expect(spy).toBeCalledWith(2, 1); + + expect(typeof v).toBe('object'); + expect(v.to.once).not.toBeUndefined(); + expect(v.flow).not.toBeUndefined(); + expect(v.filter).not.toBeUndefined(); + expect(v.filter.not).not.toBeUndefined(); + expect(v.view).not.toBeUndefined(); + expect(v.promise).not.toBeUndefined(); + + expect(v.set).toBeUndefined(); + expect(v.update).toBeUndefined(); + expect(v.pre).toBeUndefined(); + expect(v.reset).toBeUndefined(); + expect(v.reinit).toBeUndefined(); + expect(v.dirty).toBeUndefined(); +}); + +test('should work value.from with two arguments', () => { + let t; + const spy = jest.fn(); + const u = value(0); + const a = value(0); + const v = value.from(() => a.val + 1, (v) => a(v + v)); + expect(v.val).toBe(1); + (t = v.to), (t = t(spy)); + (t = t.update), (t = t.by), t(() => u.val); + + a.val = 1; + expect(v.val).toBe(2); + expect(spy).toBeCalledWith(2, 1); spy.mockReset(); + + expect(typeof v).toBe('function'); + expect(v.sync).not.toBeUndefined(); + expect(v.to.once).not.toBeUndefined(); + expect(v.flow).not.toBeUndefined(); + expect(v.filter).not.toBeUndefined(); + expect(v.filter.not).not.toBeUndefined(); + expect(v.view).not.toBeUndefined(); + expect(v.set).not.toBeUndefined(); + expect(v.update).not.toBeUndefined(); + expect(v.update.by).not.toBeUndefined(); + expect(v.pre).not.toBeUndefined(); + expect(v.pre.filter).not.toBeUndefined(); + expect(v.pre.filter.not).not.toBeUndefined(); + expect(v.promise).not.toBeUndefined(); + + expect(v.reset).toBeUndefined(); + expect(v.reinit).toBeUndefined(); + expect(v.dirty).toBeUndefined(); + + u(1); + expect(v.val).toBe(3); + expect(spy).toBeCalledWith(3, 2); spy.mockReset(); +}); + +test('should work value.select.multiple', () => { + let t; + const k = value(0); + const v = value(1); + (t = v.select.multiple), t = t({ + a: (_v) => _v + 1, + b: (_v) => _v + k.val, + }); + const { a, b } = t; + const [ a1, b1 ] = v.select.multiple([ + (_v) => _v + 1, + (_v) => _v + k.val + ]); + + expect(a.val).toBe(2); + expect(b.val).toBe(1); + expect(a1.val).toBe(2); + expect(b1.val).toBe(1); + v(2); + expect(a.val).toBe(3); + expect(b.val).toBe(2); + expect(a1.val).toBe(3); + expect(b1.val).toBe(2); + k(2); + expect(a.val).toBe(3); + expect(b.val).toBe(4); + expect(a1.val).toBe(3); + expect(b1.val).toBe(4); + + [a,b,a1,b1].forEach((v) => { + expect(typeof v).toBe('object'); + expect(v.sync).not.toBeUndefined(); + expect(v.to).not.toBeUndefined(); + expect(v.to.once).not.toBeUndefined(); + expect(v.flow).not.toBeUndefined(); + expect(v.filter).not.toBeUndefined(); + expect(v.filter.not).not.toBeUndefined(); + expect(v.view).not.toBeUndefined(); + expect(v.promise).not.toBeUndefined(); + + expect(v.set).toBeUndefined(); + expect(v.update).toBeUndefined(); + expect(v.pre).toBeUndefined(); + expect(v.reset).toBeUndefined(); + expect(v.reinit).toBeUndefined(); + expect(v.dirty).toBeUndefined(); + }); +}); + +test('should work track-untrack for value with select', () => { + const a = value(0); + + const v = value(0); + const p = v.select((_v) => _v + a.val); + const t = v.select.track((_v) => _v + a.val); + const u = v.select.untrack((_v) => _v + a.val) + + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + v(1); + expect(p.val).toBe(1); + expect(t.val).toBe(1); + expect(u.val).toBe(1); + a(1); + expect(p.val).toBe(2); + expect(t.val).toBe(2); + expect(u.val).toBe(1); +}); + +test('should work track-untrack for value with select.multiple', () => { + const a = value(0); + + const v = value(0); + const p = v.select.multiple({ p: (_v) => _v + a.val }).p; + const t = v.select.multiple.track({t: (_v) => _v + a.val }).t; + const u = v.select.multiple.untrack({u: (_v) => _v + a.val }).u; + + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + v(1); + expect(p.val).toBe(1); + expect(t.val).toBe(1); + expect(u.val).toBe(1); + a(1); + expect(p.val).toBe(2); + expect(t.val).toBe(2); + expect(u.val).toBe(1); +}); + +test('should work track-untrack for value with flow', () => { + const a = value(0); + + const v = value(0); + const p = v.flow((_v) => _v + a.val); + const t = v.flow.track((_v) => _v + a.val); + const u = v.flow.untrack((_v) => _v + a.val) + + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + v(1); + expect(p.val).toBe(1); + expect(t.val).toBe(1); + expect(u.val).toBe(1); + a(1); + expect(p.val).toBe(2); + expect(t.val).toBe(2); + expect(u.val).toBe(1); +}); + +test('should work track-untrack for value with filter', () => { + const a = value(1); + + const v = value(0); + const p = v.filter(a); + const t = v.filter.track(a); + const u = v.filter.untrack(a); + + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + a(0); + v(1); + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + a(1); + expect(p.val).toBe(1); + expect(t.val).toBe(1); + expect(u.val).toBe(0); + v(2); + expect(p.val).toBe(2); + expect(t.val).toBe(2); + expect(u.val).toBe(2); +}); + +test('should work track-untrack for value with filter.not', () => { + const a = value(0); + + const v = value(0); + const p = v.filter.not(a); + const t = v.filter.not.track(a); + const u = v.filter.not.untrack(a); + + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + a(1); + v(1); + expect(p.val).toBe(0); + expect(t.val).toBe(0); + expect(u.val).toBe(0); + a(0); + expect(p.val).toBe(1); + expect(t.val).toBe(1); + expect(u.val).toBe(0); + v(2); + expect(p.val).toBe(2); + expect(t.val).toBe(2); + expect(u.val).toBe(2); +}); + +test('should work track-untrack for value with view', () => { + let t; + const f = value(0); + const v = value(0); + + const a = v.view((_v) => _v + f.val); + const b = ((t = v.view.track), t((_v) => _v + f.val)); + const c = ((t = v.view.untrack), t((_v) => _v + f.val)); + + const spy_a = jest.fn(); + const spy_b = jest.fn(); + const spy_c = jest.fn(); + + cycle(() => spy_a(a.val)); + cycle(() => spy_b(b.val)); + cycle(() => spy_c(c.val)); + + expect(spy_a).toBeCalledTimes(1); + expect(spy_b).toBeCalledTimes(1); + expect(spy_c).toBeCalledTimes(1); + + f.val = 1; + expect(spy_a).toBeCalledTimes(2); + expect(spy_b).toBeCalledTimes(2); + expect(spy_c).toBeCalledTimes(1); + + v(1); + expect(spy_a).toBeCalledTimes(3); + expect(spy_b).toBeCalledTimes(3); + expect(spy_c).toBeCalledTimes(2); +}); + +test('should work track-untrack for value with pre', () => { + let t; + const f = value(0); + const v = value(0); + + const a = v.pre((_v) => _v + f.val); + const b = ((t = v.pre.track), t((_v) => _v + f.val)); + const c = ((t = v.pre.untrack), t((_v) => _v + f.val)); + + const spy_a = jest.fn(); + const spy_b = jest.fn(); + const spy_c = jest.fn(); + + cycle(() => spy_a(a(1))); + cycle(() => spy_b(b(1))); + cycle(() => spy_c(c(1))); + + expect(spy_a).toBeCalledTimes(1); + expect(spy_b).toBeCalledTimes(1); + expect(spy_c).toBeCalledTimes(1); + + f.val = 1; + expect(spy_a).toBeCalledTimes(2); + expect(spy_b).toBeCalledTimes(2); + expect(spy_c).toBeCalledTimes(1); + + v(1); + expect(spy_a).toBeCalledTimes(2); + expect(spy_b).toBeCalledTimes(2); + expect(spy_c).toBeCalledTimes(1); +}); + +test('should work track-untrack for value with pre.filter', () => { + let t; + const f = value(0); + const v = value(0); + + const a = v.pre.filter(f); + const b = ((t = v.pre.filter.track), t(f)); + const c = ((t = v.pre.filter.untrack), t(f)); + + const spy_a = jest.fn(); + const spy_b = jest.fn(); + const spy_c = jest.fn(); + + cycle(() => spy_a(a(1))); + cycle(() => spy_b(b(1))); + cycle(() => spy_c(c(1))); + + expect(spy_a).toBeCalledTimes(1); + expect(spy_b).toBeCalledTimes(1); + expect(spy_c).toBeCalledTimes(1); + + f.val = 1; + expect(spy_a).toBeCalledTimes(2); + expect(spy_b).toBeCalledTimes(2); + expect(spy_c).toBeCalledTimes(1); +}); + +test('should work track-untrack for value with pre.filter.not', () => { + let t; + const f = value(0); + const v = value(0); + + const a = v.pre.filter.not(f); + const b = ((t = v.pre.filter.not.track), t(f)); + const c = ((t = v.pre.filter.not.untrack), t(f)); + + const spy_a = jest.fn(); + const spy_b = jest.fn(); + const spy_c = jest.fn(); + + cycle(() => spy_a(a(1))); + cycle(() => spy_b(b(1))); + cycle(() => spy_c(c(1))); + + expect(spy_a).toBeCalledTimes(1); + expect(spy_b).toBeCalledTimes(1); + expect(spy_c).toBeCalledTimes(1); + + f.val = 1; + expect(spy_a).toBeCalledTimes(2); + expect(spy_b).toBeCalledTimes(2); + expect(spy_c).toBeCalledTimes(1); +}); + +test('should work track-untrack for value with update', () => { + let t; + const f = value(0); + + const a = value(0); + const b = value(0); + const c = value(0); + + const spy_a = jest.fn(); + const spy_b = jest.fn(); + const spy_c = jest.fn(); + + cycle(() => spy_a(((t = a.update), t(v => v + f.val)))); + cycle(() => spy_b(((t = b.update.track), t(v => v + f.val)))); + cycle(() => spy_c(((t = c.update.untrack), t(v => v + f.val)))); + + expect(spy_a).toBeCalledTimes(1); + expect(spy_b).toBeCalledTimes(1); + expect(spy_c).toBeCalledTimes(1); + + f.val = 1; + expect(spy_a).toBeCalledTimes(1); + expect(spy_b).toBeCalledTimes(2); + expect(spy_c).toBeCalledTimes(1); +}); + +test('should work signal basic support', () => { + let t, z; + const spy = jest.fn(); + const v = signal(0); + + (t = v.sync), (t = t(spy)); + expect(spy).toBeCalledWith(0, void 0); spy.mockReset(); + + (z = t.update), z(_v => _v); + expect(spy).toBeCalledWith(0, 0); spy.mockReset(); + (z = t.set), z(0); + expect(spy).toBeCalledWith(0, 0); spy.mockReset(); + expect(t.val).toBe(0); + (z = t.get); + expect(z()).toBe(0); + (z = t.dirty), (z = z.get); + expect(z()).toBe(false); + + const u = ((z = signal().view), (z = z(_u => _u + 1).pre), (z = z(_u => _u * 10).flow), (z = z(_u => _u * 2))); + (t = t.update), (t = t.by), (t = t(u)); + u(1); + expect(t.val).toBe(22); + expect(t.get()).toBe(22); + expect(spy).toBeCalledWith(22, 0); spy.mockReset(); + u(1); + expect(spy).toBeCalledWith(22, 22); spy.mockReset(); + + expect(v.dirty.val).toBe(true); + (z = t.reset), z(); + expect(v.val).toBe(0); + expect(v.dirty.val).toBe(false); + expect(spy).toBeCalledWith(0, 22); spy.mockReset(); + + v.reset(); + expect(spy).toBeCalledWith(0, 0); spy.mockReset(); + (z = t.reinit), z(5); + expect(spy).toBeCalledWith(5, 0); spy.mockReset(); + expect(v.dirty.val).toBe(false); + + u(0); + expect(spy).toBeCalledWith(2, 5); spy.mockReset(); + expect(v.dirty.val).toBe(true); +}); + +test('should work track-untrack for signal with flow', () => { + const spy_p = jest.fn(); + const spy_t = jest.fn(); + const spy_u = jest.fn(); + const a = signal(0); + + const v = signal(0); + v.flow((_v) => _v + a.val).to(spy_p); + v.flow.track((_v) => _v + a.val).to(spy_t); + v.flow.untrack((_v) => _v + a.val).to(spy_u); + + v(0); + expect(spy_p).toBeCalledWith(0, 0); spy_p.mockReset(); + expect(spy_t).toBeCalledWith(0, 0); spy_t.mockReset(); + expect(spy_u).toBeCalledWith(0, 0); spy_u.mockReset(); + a(0); + expect(spy_p).toBeCalledTimes(0); + expect(spy_t).toBeCalledWith(0, 0); spy_t.mockReset(); + expect(spy_u).toBeCalledTimes(0); +}); + +test('should work track-untrack for signal with filter', () => { + const spy_p = jest.fn(); + const spy_t = jest.fn(); + const spy_u = jest.fn(); + const a = signal(1); + + const v = signal(0); + v.filter(a).to(spy_p); + v.filter.track(a).to(spy_t); + v.filter.untrack(a).to(spy_u); + + v(0); + expect(spy_p).toBeCalledWith(0, 0); spy_p.mockReset(); + expect(spy_t).toBeCalledWith(0, 0); spy_t.mockReset(); + expect(spy_u).toBeCalledWith(0, 0); spy_u.mockReset(); + a(1); + expect(spy_p).toBeCalledTimes(0); + expect(spy_t).toBeCalledWith(0, 0); spy_t.mockReset(); + expect(spy_u).toBeCalledTimes(0); +}); + +test('should work track-untrack for signal with filter.not', () => { + const spy_p = jest.fn(); + const spy_t = jest.fn(); + const spy_u = jest.fn(); + const a = signal(0); + + const v = signal(0); + v.filter.not(a).to(spy_p); + v.filter.not.track(a).to(spy_t); + v.filter.not.untrack(a).to(spy_u); + + v(0); + expect(spy_p).toBeCalledWith(0, 0); spy_p.mockReset(); + expect(spy_t).toBeCalledWith(0, 0); spy_t.mockReset(); + expect(spy_u).toBeCalledWith(0, 0); spy_u.mockReset(); + a(0); + expect(spy_p).toBeCalledTimes(0); + expect(spy_t).toBeCalledWith(0, 0); spy_t.mockReset(); + expect(spy_u).toBeCalledTimes(0); +}); + +test('should work signal.from with one argument', () => { + let t; + const spy = jest.fn(); + const a = signal(0); + const v = signal.from(() => a.val + 1); + expect(v.val).toBe(1); + (t = v.to), t(spy); + + a.val = 0; + expect(v.val).toBe(1); + expect(spy).toBeCalledWith(1, 1); + + expect(typeof v).toBe('object'); + expect(v.to.once).not.toBeUndefined(); + expect(v.flow).not.toBeUndefined(); + expect(v.filter).not.toBeUndefined(); + expect(v.filter.not).not.toBeUndefined(); + expect(v.view).not.toBeUndefined(); + expect(v.promise).not.toBeUndefined(); + + expect(v.set).toBeUndefined(); + expect(v.update).toBeUndefined(); + expect(v.pre).toBeUndefined(); + expect(v.reset).toBeUndefined(); + expect(v.reinit).toBeUndefined(); + expect(v.dirty).toBeUndefined(); +}); + +test('should work signal.from with two arguments', () => { + let t; + const spy = jest.fn(); + const u = signal(0); + const a = signal(0); + const v = signal.from(() => a.val + 1, (v) => a(v + v)); + expect(v.val).toBe(1); + (t = v.to), (t = t(spy)); + (t = t.update), (t = t.by), t(u); + + a.val = 0; + expect(v.val).toBe(1); + expect(spy).toBeCalledWith(1, 1); spy.mockReset(); + + expect(typeof v).toBe('function'); + expect(v.sync).not.toBeUndefined(); + expect(v.to.once).not.toBeUndefined(); + expect(v.flow).not.toBeUndefined(); + expect(v.filter).not.toBeUndefined(); + expect(v.filter.not).not.toBeUndefined(); + expect(v.view).not.toBeUndefined(); + expect(v.set).not.toBeUndefined(); + expect(v.update).not.toBeUndefined(); + expect(v.update.by).not.toBeUndefined(); + expect(v.pre).not.toBeUndefined(); + expect(v.pre.filter).not.toBeUndefined(); + expect(v.pre.filter.not).not.toBeUndefined(); + expect(v.promise).not.toBeUndefined(); + + expect(v.reset).toBeUndefined(); + expect(v.reinit).toBeUndefined(); + expect(v.dirty).toBeUndefined(); + + u(0); + expect(v.val).toBe(1); + expect(spy).toBeCalledWith(1, 1); spy.mockReset(); +}); + +test('should work signal.trigger', () => { + const spy_t = jest.fn(); + const spy_f = jest.fn(); + const spy_u = jest.fn(); + + const t = signal.trigger(); + const f = signal.trigger.flag(); + const u = signal.trigger.flag.invert(); + + t.flow().flow().sync(spy_t)(); + f.flow().flow().sync(spy_f)(); + u.flow().flow().sync(spy_u)(); + + expect(spy_t).toHaveBeenNthCalledWith(1, void 0, void 0); + expect(spy_t).toHaveBeenNthCalledWith(2, void 0, void 0); + expect(spy_f).toHaveBeenNthCalledWith(1, false, void 0); + expect(spy_f).toHaveBeenNthCalledWith(2, true, false); + expect(spy_u).toHaveBeenNthCalledWith(1, true, void 0); + expect(spy_u).toHaveBeenNthCalledWith(2, false, true); + + t(); f(); u(); + expect(spy_t).toBeCalledTimes(2); spy_t.mockReset(); + expect(spy_f).toBeCalledTimes(2); spy_f.mockReset(); + expect(spy_u).toBeCalledTimes(2); spy_u.mockReset(); + + expect(t.dirty.val).toBe(false); + expect(f.dirty.val).toBe(true); + expect(u.dirty.val).toBe(true); + + t.reset(); t(); + f.reset(); f(); + u.reset(); u(); + expect(spy_t).toHaveBeenNthCalledWith(1, void 0, void 0); + expect(spy_t).toHaveBeenNthCalledWith(2, void 0, void 0); + expect(spy_f).toHaveBeenNthCalledWith(1, false, true); + expect(spy_f).toHaveBeenNthCalledWith(2, true, false); + expect(spy_u).toHaveBeenNthCalledWith(1, true, false); + expect(spy_u).toHaveBeenNthCalledWith(2, false, true); +}); + +test('should work value.combine', () => { + const spy_v = jest.fn(); + const spy_w = jest.fn(); + const a = signal(0); + + value.combine({ + a: a, + b: a.flow(_a => _a + 1), + c: () => a.val + }).sync(spy_v); + value.combine([ + a, + a.flow(_a => _a + 1), + () => a.val + ]).sync(spy_w); + + expect(spy_v).toBeCalledWith({ a: 0, b: 1, c: 0 }, void 0); spy_v.mockReset(); + expect(spy_w).toBeCalledWith([ 0, 1, 0 ], void 0); spy_w.mockReset(); + a(0); + expect(spy_v).toBeCalledTimes(0); + expect(spy_w).toBeCalledTimes(0); + a(1); + expect(spy_v).toBeCalledWith({ a: 1, b: 2, c: 1 }, { a: 0, b: 1, c: 0 }); + expect(spy_w).toBeCalledWith([ 1, 2, 1 ], [ 0, 1, 0 ]); +}); + +test('should work signal.combine', () => { + const spy_v = jest.fn(); + const spy_w = jest.fn(); + const a = signal(0); + + signal.combine({ + a: a, + b: a.flow(_a => _a + 1), + c: () => a.val + }).sync(spy_v); + signal.combine([ + a, + a.flow(_a => _a + 1), + () => a.val + ]).sync(spy_w); + + expect(spy_v).toBeCalledWith({ a: 0, b: 1, c: 0 }, void 0); spy_v.mockReset(); + expect(spy_w).toBeCalledWith([ 0, 1, 0 ], void 0); spy_w.mockReset(); + a(0); + expect(spy_v).toBeCalledWith({ a: 0, b: 1, c: 0 }, { a: 0, b: 1, c: 0 }); + expect(spy_w).toBeCalledWith([ 0, 1, 0 ], [ 0, 1, 0 ]); + a(1); + expect(spy_v).toBeCalledWith({ a: 1, b: 2, c: 1 }, { a: 0, b: 1, c: 0 }); + expect(spy_w).toBeCalledWith([ 1, 2, 1 ], [ 0, 1, 0 ]); +}); + +test('should work value.join', () => { + const spy = jest.fn(); + const a = signal(0); + const z = value(0); + + z.join([ + a, + a.flow(_a => _a + 1), + () => a.val + ]).sync(spy); + + expect(spy).toBeCalledWith([ 0, 0, 1, 0 ], void 0); spy.mockReset(); + a(0); + expect(spy).toBeCalledTimes(0); + a(1); + expect(spy).toBeCalledWith([ 0, 1, 2, 1 ], [ 0, 0, 1, 0 ]); spy.mockReset(); + z(0); + expect(spy).toBeCalledTimes(0); + z(1); + expect(spy).toBeCalledWith([ 1, 1, 2, 1 ], [ 0, 1, 2, 1 ]); +}); + +test('should work signal.join', () => { + const spy = jest.fn(); + const a = signal(0); + const z = signal(0); + + z.join([ + a, + a.flow(_a => _a + 1), + () => a.val + ]).sync(spy); + + expect(spy).toBeCalledWith([ 0, 0, 1, 0 ], void 0); spy.mockReset(); + a(0); + expect(spy).toBeCalledWith([ 0, 0, 1, 0 ], [ 0, 0, 1, 0 ]); spy.mockReset(); + a(1); + expect(spy).toBeCalledWith([ 0, 1, 2, 1 ], [ 0, 0, 1, 0 ]); spy.mockReset(); + z(0); + expect(spy).toBeCalledWith([ 0, 1, 2, 1 ], [ 0, 1, 2, 1 ]); spy.mockReset(); + z(1); + expect(spy).toBeCalledWith([ 1, 1, 2, 1 ], [ 0, 1, 2, 1 ]); +}); + +test('should work track-untrack for value with join', () => { + const spy_p = jest.fn(); + const spy_t = jest.fn(); + const spy_u = jest.fn(); + const a = signal(0); + + const v = value(0); + v.join([a]).sync(spy_p); + v.join.track([a]).sync(spy_t); + v.join.untrack([a]).sync(spy_u); + + expect(spy_p).toBeCalledWith([0,0], void 0); spy_p.mockReset(); + expect(spy_t).toBeCalledWith([0,0], void 0); spy_t.mockReset(); + expect(spy_u).toBeCalledWith([0,0], void 0); spy_u.mockReset(); + v(1); + expect(spy_p).toBeCalledWith([1,0], [0,0]); spy_p.mockReset(); + expect(spy_t).toBeCalledWith([1,0], [0,0]); spy_t.mockReset(); + expect(spy_u).toBeCalledWith([1,0], [0,0]); spy_u.mockReset(); + a(0); + expect(spy_p).toBeCalledTimes(0); + expect(spy_t).toBeCalledTimes(0); + expect(spy_u).toBeCalledTimes(0); + a(1); + expect(spy_p).toBeCalledWith([1,1], [1,0]); spy_p.mockReset(); + expect(spy_t).toBeCalledWith([1,1], [1,0]); spy_t.mockReset(); + expect(spy_u).toBeCalledTimes(0); +}); + +test('should work track-untrack for signal with join', () => { + const spy_p = jest.fn(); + const spy_t = jest.fn(); + const spy_u = jest.fn(); + const a = signal(0); + + const v = signal(0); + v.join([a]).sync(spy_p); + v.join.track([a]).sync(spy_t); + v.join.untrack([a]).sync(spy_u); + + expect(spy_p).toBeCalledWith([0,0], void 0); spy_p.mockReset(); + expect(spy_t).toBeCalledWith([0,0], void 0); spy_t.mockReset(); + expect(spy_u).toBeCalledWith([0,0], void 0); spy_u.mockReset(); + v(0); + expect(spy_p).toBeCalledWith([0,0], [0,0]); spy_p.mockReset(); + expect(spy_t).toBeCalledWith([0,0], [0,0]); spy_t.mockReset(); + expect(spy_u).toBeCalledWith([0,0], [0,0]); spy_u.mockReset(); + a(0); + expect(spy_p).toBeCalledWith([0,0], [0,0]); spy_p.mockReset(); + expect(spy_t).toBeCalledWith([0,0], [0,0]); spy_t.mockReset(); + expect(spy_u).toBeCalledTimes(0); +}); + +test('should work signal with as.value', () => { + const spy_s = jest.fn(); + const spy_v = jest.fn(); + + const s = signal(0).sync(spy_s).op(s => { + s.as.value().sync(spy_v); + }); + + expect(spy_s).toBeCalledWith(0, void 0); spy_s.mockReset(); + expect(spy_v).toBeCalledWith(0, void 0); spy_v.mockReset(); + s(1); + expect(spy_s).toBeCalledWith(1, 0); spy_s.mockReset(); + expect(spy_v).toBeCalledWith(1, 0); spy_v.mockReset(); + s(1); + expect(spy_s).toBeCalledWith(1, 1); spy_s.mockReset(); + expect(spy_v).toBeCalledTimes(0); +}); + +test('should work value updater', () => { + const v = value(0); + const inc = v.updater(v => v + 1); + const inc_add = v.updater.value((v, n: number) => v + n + 1); + + expect(v.val).toBe(0); + inc(); + expect(v.val).toBe(1); + inc.update(); + expect(v.val).toBe(2); + + inc_add(0); + expect(v.val).toBe(3); + inc_add(1); + expect(v.val).toBe(5); + inc_add(1); + expect(v.val).toBe(5); +}); + +test('should work signal updater', () => { + const v = signal(0); + const inc = v.updater(v => v + 1); + const inc_add = v.updater.value((v, n: number) => v + n + 1); + + expect(v.val).toBe(0); + inc(); + expect(v.val).toBe(1); + inc.update(); + expect(v.val).toBe(2); + + inc_add(0); + expect(v.val).toBe(3); + inc_add(1); + expect(v.val).toBe(5); + inc_add(1); + expect(v.val).toBe(5); +}); diff --git a/tests/stoppable.test.ts b/tests/contextual-stop.test.ts similarity index 75% rename from tests/stoppable.test.ts rename to tests/contextual-stop.test.ts index 8b44d093..855f4fec 100644 --- a/tests/stoppable.test.ts +++ b/tests/contextual-stop.test.ts @@ -1,13 +1,10 @@ -import { cycle, on, stoppable, value } from '../src'; +import { cycle, value, contextual } from '../src'; test('should work stoppable stop method', () => { const spy = jest.fn(); - const a = value(0).wrap((v: number) => { - if (v % 2) stoppable().stop(); - return v; - }); - a.watch(spy); + const a = value(0).pre.filter((v: number) => !(v % 2)); + a.to(spy); a(1); a(2); @@ -22,10 +19,10 @@ test('should work stoppable in cycle not first iteration', () => { const spy = jest.fn(); const a = value(1); - a.watch(spy); + a.to(spy); cycle(() => { a.val += a.val; - if (a.val > 10) stoppable().stop(); + if (a.val > 10) contextual.stop(); }); expect(spy).toHaveBeenNthCalledWith(4, 16, 8); @@ -36,10 +33,10 @@ test('should work stoppable in cycle first iteration', () => { const spy = jest.fn(); const a = value(1); - a.watch(spy); + a.to(spy); cycle(() => { - stoppable().stop(); + contextual.stop(); a.update(v => v + v); }); @@ -49,6 +46,6 @@ test('should work stoppable in cycle first iteration', () => { test('should throw exception if run stoppable outside of stoppable context', () => { expect(() => { - stoppable(); + contextual.stop; }).toThrow('Parent context not found'); }); diff --git a/tests/isolate.test.ts b/tests/isolate.test.ts index 8a31a65c..569e30c3 100644 --- a/tests/isolate.test.ts +++ b/tests/isolate.test.ts @@ -1,4 +1,4 @@ -import { isolate, effect, shared, free, un } from '../src'; +import { isolate, shared, free, un } from '../src'; test('should work basic operations with isolate', async () => { const destr_1 = jest.fn(); @@ -8,10 +8,10 @@ test('should work basic operations with isolate', async () => { let unsub: any; let unsubs: any; const A = () => { - effect(() => () => destr_1()); - const finish = isolate(); - unsub = effect(() => () => destr_2()); - effect(() => () => destr_3()); + un(() => destr_1()); + const finish = isolate.unsafe(); + unsub = isolate(() => un(() => destr_2())); + un(() => destr_3()); unsubs = finish(); un(() => destr_4()); }; @@ -33,10 +33,10 @@ test('should work isolate with argument', async () => { const destr_1 = jest.fn(); let unsub_1: any, unsub_2: any; const A = () => { - effect(() => () => destr_1()); - unsub_1 = isolate(effect(() => () => destr_1())); - effect(() => () => destr_1()); - unsub_2 = isolate(effect(() => () => destr_1())); + un(() => destr_1()); + unsub_1 = isolate(() => un(() => destr_1())); + un(() => destr_1()); + unsub_2 = isolate(() => un(() => destr_1())); }; shared(A); @@ -51,7 +51,7 @@ test('should work isolate with argument', async () => { test('should work isolate with no context', async () => { const destr_1 = jest.fn(); - const unsub_1 = isolate(effect(() => () => destr_1())); + const unsub_1 = isolate(() => un(() => destr_1())); unsub_1(); expect(destr_1).toBeCalledTimes(1); }); diff --git a/tests/hook.test.tsx b/tests/local-inject.test.tsx similarity index 78% rename from tests/hook.test.tsx rename to tests/local-inject.test.tsx index 9fa6b256..160919ed 100644 --- a/tests/hook.test.tsx +++ b/tests/local-inject.test.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { mount } from 'enzyme'; -import { useLocal, hook, useScoped, Scope } from '../src'; +import { useLocal, local, useScoped, Scope } from '../src'; -test('should work hook in useLocal function', () => { +test('should work local.inject in useLocal function', () => { let spy = jest.fn(); const unit = (value: number) => { - hook(() => { + local.inject(() => { const [a, set_a] = useState(0); if (value !== a) set_a(value); spy(a); @@ -36,8 +36,8 @@ test('should work hook in useLocal function', () => { test('should throw exception if not in context', () => { expect(() => { - hook(() => {}); - }).toThrow('Hook section available only at useLocal'); + local.inject(() => {}); + }).toThrow('The local.inject section available only in useLocal'); }); test('should throw exception if not useLocal', () => { @@ -45,7 +45,7 @@ test('should throw exception if not useLocal', () => { console.error = () => {}; expect(() => { const unit = () => { - hook(() => {}); + local.inject(() => {}); }; function A() { useScoped(unit); @@ -56,6 +56,6 @@ test('should throw exception if not useLocal', () => { ); - }).toThrow('Hook section available only at useLocal'); + }).toThrow('The local.inject section available only in useLocal'); console.error = _error; }); diff --git a/tests/loop.test.ts b/tests/loop.test.ts deleted file mode 100644 index 2adc2feb..00000000 --- a/tests/loop.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { loop, signal, on } from '../src'; -import { delay } from './lib'; - -test('should work basic operations with loop', async () => { - const spy = jest.fn(); - const a = signal(); - const b = signal(); - const c = signal(); - - const stop = loop(async () => { - await Promise.all([a, b]); - c(); - }); - on(c, () => spy()); - - await delay(); - expect(spy).not.toBeCalled(); - a(); - await delay(); - expect(spy).not.toBeCalled(); - b(); - await delay(); - expect(spy).toBeCalled(); - - a(); - b(); - await delay(); - expect(spy).toBeCalledTimes(2); - - stop(); - a(); - b(); - await delay(); - expect(spy).toBeCalledTimes(3); - - a(); - b(); - await delay(); - expect(spy).toBeCalledTimes(3); -}); diff --git a/tests/observe.test.tsx b/tests/observe.test.tsx index d790e71b..f28f8c76 100644 --- a/tests/observe.test.tsx +++ b/tests/observe.test.tsx @@ -7,7 +7,7 @@ type ForwardRefButtonProps = { onClick?: () => void; }; const ForwardRefButton = React.forwardRef( - observe((props, ref) => ( + observe.nomemo((props, ref) => ( diff --git a/tests/on.test.ts b/tests/on.test.ts index f9b4564c..2d58c082 100644 --- a/tests/on.test.ts +++ b/tests/on.test.ts @@ -1,4 +1,4 @@ -import { prop, cache, on, value } from '../src'; +import { prop, cache, on, value, isolate } from '../src'; test('should work basic operations with prop, cache and on', () => { const spy = jest.fn(); @@ -21,12 +21,12 @@ test('should cache return value in on', () => { const spy = jest.fn(); const a = value(0); - on(() => Math.floor(a[0]() / 2), spy); + on(() => Math.floor(a.val / 2), spy); - a[1](1); + a.set(1); expect(spy).toBeCalledTimes(0); - a[1](2); + a.set(2); expect(spy).toHaveBeenNthCalledWith(1, 1, 0); }); @@ -38,7 +38,7 @@ test('should work stop on subscription', () => { () => a.val, () => stop() ); - const stop = on(() => a.val, spy); + const stop = isolate(() => on(() => a.val, spy)); on( () => a.val, () => stop() diff --git a/tests/pool.test.ts b/tests/pool.test.ts index e211b25e..d108a0ef 100644 --- a/tests/pool.test.ts +++ b/tests/pool.test.ts @@ -1,4 +1,4 @@ -import { pool, stoppable } from '../src'; +import { pool, contextual } from '../src'; import { delay } from './lib'; test('should work basic operations with pool', async () => { @@ -7,10 +7,9 @@ test('should work basic operations with pool', async () => { const p = pool(async () => { const id = i++; - const stop = stoppable(); - spy(stop.val, id); + spy(id); await delay(10); - spy(stop.val, id); + spy(id); }); expect(p.pending.val).toBe(false); @@ -21,8 +20,8 @@ test('should work basic operations with pool', async () => { expect(p.count.val).toBe(2); expect(spy).toBeCalledTimes(2); - expect(spy).toHaveBeenNthCalledWith(1, false, 0); - expect(spy).toHaveBeenNthCalledWith(2, false, 1); + expect(spy).toHaveBeenNthCalledWith(1, 0); + expect(spy).toHaveBeenNthCalledWith(2, 1); p.threads.val[0](); @@ -33,8 +32,8 @@ test('should work basic operations with pool', async () => { await delay(20); expect(spy).toBeCalledTimes(4); - expect(spy).toHaveBeenNthCalledWith(3, true, 0); - expect(spy).toHaveBeenNthCalledWith(4, false, 1); + expect(spy).toHaveBeenNthCalledWith(3, 0); + expect(spy).toHaveBeenNthCalledWith(4, 1); expect(p.pending.val).toBe(false); expect(p.count.val).toBe(0); @@ -49,7 +48,7 @@ test('should work correct pool with not async function', async () => { }); expect(p.pending.val).toBe(false); - p.pending.watch(spy); + p.pending.to(spy); expect(p()).toBe(10); expect(p.threads.val).toEqual([]); diff --git a/tests/ready.test.ts b/tests/ready.test.ts deleted file mode 100644 index 96f82ced..00000000 --- a/tests/ready.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { on, ready, signal, sync, value } from '../src'; -import { delay } from './lib'; - -test('should work ready with one value', async () => { - const spy = jest.fn(); - const a = ready(0); - on(a, spy); - - expect(a.val).toBe(0); - - const m = async () => { - expect(await a).toBe(1); - }; - const k = async () => { - await delay(10); - a(1); - }; - await Promise.all([m(), k()]); - - a(2); - expect(a.val).toBe(1); - expect(await a).toBe(1); - - expect(spy).toBeCalledTimes(1); - expect(spy).toBeCalledWith(1, 0); -}); - -test('should work ready with two values', async () => { - const spy = jest.fn(); - const a = signal.ready(0).to(1); - on(a, spy); - - expect(a.val).toBe(0); - a(); - expect(a.val).toBe(1); - expect(await a).toBe(1); - - expect(spy).toBeCalledTimes(1); - expect(spy).toBeCalledWith(1, 0); -}); - -test('should work ready reset', async () => { - const spy = jest.fn(); - const a = ready(0).to(1); - on(a, spy); - - expect(a.val).toBe(0); - a(); - expect(a.val).toBe(1); - a.reset(); - expect(a.val).toBe(0); - - const m = async () => { - expect(await a).toBe(1); - }; - const k = async () => { - await delay(10); - a(); - }; - await Promise.all([m(), k()]); - - expect(a.val).toBe(1); - - expect(spy).toBeCalledTimes(3); - expect(spy).toHaveBeenNthCalledWith(1, 1, 0); - expect(spy).toHaveBeenNthCalledWith(2, 0, 1); - expect(spy).toHaveBeenNthCalledWith(3, 1, 0); -}); - -test('should work ready wrapped to', async () => { - const spy = jest.fn(); - const a = ready(0) - .wrap((v: number) => v * 2) - .to(10); - on(a, spy); - - expect(a.val).toBe(0); - a(); - expect(a.val).toBe(20); - a.reset(); - expect(a.val).toBe(0); - - const m = async () => { - expect(await a).toBe(20); - }; - const k = async () => { - await delay(10); - a(); - }; - await Promise.all([m(), k()]); - - expect(a.val).toBe(20); - - expect(spy).toBeCalledTimes(3); - expect(spy).toHaveBeenNthCalledWith(1, 20, 0); - expect(spy).toHaveBeenNthCalledWith(2, 0, 20); - expect(spy).toHaveBeenNthCalledWith(3, 20, 0); -}); - -test('should work ready from value', async () => { - const spy = jest.fn(); - - const s = value(1); - const r = ready.from(s); - - sync(r, spy); - - s(1); - s(2); - s(3); - - expect(spy).toBeCalledTimes(2); - expect(spy).toHaveBeenNthCalledWith(1, 1); - expect(spy).toHaveBeenNthCalledWith(2, 2, 1); -}); - -test('should work ready from signal', async () => { - const spy = jest.fn(); - - const s = signal(1); - const r = ready.from(s); - - sync(r, spy); - - s(1); - s(3); - - expect(spy).toBeCalledTimes(2); - expect(spy).toHaveBeenNthCalledWith(1, 1); - expect(spy).toHaveBeenNthCalledWith(2, 1, 1); -}); - -test('should work ready resolved', async () => { - const r = ready.resolved(1); - expect(r.val).toBe(1); - expect(await r).toBe(1); -}); - -test('should work ready with undefined resolved', async () => { - const r = ready.resolved(); - expect(r.val).toBe(void 0); - expect(await r).toBe(void 0); -}); diff --git a/tests/shared.test.ts b/tests/shared.test.ts index 509f9387..8dec1f66 100644 --- a/tests/shared.test.ts +++ b/tests/shared.test.ts @@ -1,4 +1,4 @@ -import { shared, initial, free, effect, mock, unmock } from '../src'; +import { shared, initial, free, un, mock, unmock } from '../src'; test('should work initial data with shared', () => { const spy = jest.fn(); @@ -7,7 +7,7 @@ test('should work initial data with shared', () => { class A { constructor(data: typeof a) { spy(data); - effect(() => destr); + un(destr); } } shared(A); diff --git a/tests/sync.test.ts b/tests/sync.test.ts index fb02411f..77a33113 100644 --- a/tests/sync.test.ts +++ b/tests/sync.test.ts @@ -11,7 +11,7 @@ test('should work basic operations with prop, cache and sync', () => { const a = new A(); sync(() => a.b, spy); expect(spy).toBeCalledTimes(1); - expect(spy).toHaveBeenNthCalledWith(1, 11); + expect(spy).toHaveBeenNthCalledWith(1, 11, void 0); a.a += 10; expect(spy).toHaveBeenNthCalledWith(2, 21, 11); @@ -22,12 +22,12 @@ test('should cache return value in sync', () => { const spy = jest.fn(); const a = value(0); - sync(() => Math.floor(a[0]() / 2), spy); + sync(() => Math.floor(a.val / 2), spy); expect(spy).toBeCalledTimes(1); - expect(spy).toHaveBeenNthCalledWith(1, 0); + expect(spy).toHaveBeenNthCalledWith(1, 0, void 0); - a[1](1); - a[1](2); + a.set(1); + a.set(2); expect(spy).toHaveBeenNthCalledWith(2, 1, 0); expect(spy).toBeCalledTimes(2); }); diff --git a/tests/use-jsx.test.tsx b/tests/use-jsx.test.tsx new file mode 100644 index 00000000..80fd4ebb --- /dev/null +++ b/tests/use-jsx.test.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { mount } from 'enzyme'; +import { value, useJsx } from '../src'; + +test('should work useJsx with no deps', () => { + const a = value(0); + const b = value(0); + + function A(props) { + const Body = useJsx<{ value: string }>(({ children, value }) => ( + <> + {a.val} + {b.val} + {value} + {children} + + )); + return ( + +

], deps?: any[]): [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P]; + (targets: [Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re,Re