Skip to content

Commit

Permalink
Docs in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
mweststrate committed Jan 4, 2019
1 parent 8448017 commit 2a861eb
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 18 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ Todo:
* [ ] smart lack of act detection. Only have `act`, no `run`?
* [ ] use yalc? https://www.google.com/url?q=https%3A%2F%2Fgithub.com%2Fwhitecolor%2Fyalc%2F&sa=D&sntz=1&usg=AFQjCNGCTXoCduIMdVHx5xm-uAs_REX3MA
* [ ] rename MDX files to md
* [ ] rview as wrapper
* [ ] deep merge model tree?

Later
* [ ] rval-remote
Expand Down
11 changes: 6 additions & 5 deletions docs_source/0_introduction/10_getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,26 +133,27 @@ Such as updating the UI, logging, making network effects etc.

_“No act of kindness, no matter how small, is ever wasted...” — Aesop_

TODO: run or act?

You might have noticed that in the previous listening or side effects where immediately fired when emitting an update to `myLuckyNumber`.
This is just the default behavior and there are several ways to influence it.
First of all, we can use techniques like debounce to roun our subscriptions less frequently.
But more importantly, we can hold of the reactive system to wait a bit until we've done all our updates,
so that changes will be broadcased as one atomic update.
To express that, there is `act` to group multiple changes into a single **activity**:
To express that, there is `act` to group multiple changes into a single **activity**.
`act` takes accepts a function, and returns a function with the same signature, that, when invoked, will batch the updates.

```javascript
const cancelPrint = sub(isGoodLuckyNumber, isGood => {
console.log(isGood)
})

act(() => {
const assignNumbers = act(() => {
myLuckyNumber(42)
myLuckyNumber(33)
myLuckyNumber(55)
myLuckyNumber(2)
}) // prints only once, at the end of the activity: 'false'
})

assignNumbers()// prints only once, at the end of the activity: 'false'
```

That's all! Note that `act` only batches _synchronosly_ run updates. Passing an `async` function to `act` is in that regard mostly useless.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: Philosophy
order: 5
order: 6
menu: Introduction
route: /introduction/philosophy
---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
name: Using preprocessors
order: 0
menu: Advanced
route: /advanced/preprocessors
order: 6
menu: Introduction
route: /introduction/preprocessors
---


Expand Down Expand Up @@ -53,8 +53,13 @@ profit("7") // Parses the number, and sets profit to number 7

## Type checking

sarcastic
@r-val/types

## Type conversio

## Factories

## Debugging / logging

## Side effects
## Side effects
6 changes: 6 additions & 0 deletions docs_source/1_convenience/50_updaters.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
name: Using updaters
order: 5
menu: Introduction
route: /introduction/updaters
---
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions docs_source/2_advanced/performance.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@


group mutations of collections
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"rollup-plugin-filesize": "^5.0.1",
"rollup-plugin-terser": "^3.0.0",
"rollup-plugin-typescript2": "^0.18.0",
"sarcastic": "^1.5.0",
"tape": "^4.9.2",
"ts-jest": "^23.10.5",
"tslib": "^1.9.3",
Expand Down
157 changes: 154 additions & 3 deletions pkgs/core/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The `initialValue` froms the initial state of the reative value.

`val` returns a function that takes zero or one arguments:
* If the function is invoked without arguments, the current value is returned
* If the function is invoked with one argument, and that value is not a function, the argument becomes the new state of the reactive value.
* If the function is invoked with one argument, and that value is not a function, the argument becomes the new state of the reactive value. (If you want to store a function itself as value, use `val(() => fn)` instead of `val(fn)`)
* If the function is invoked with a function as argument, that function is invoked and should produce the new state, based on the current state. Built-in update functions can be found in the [`@r-val/updaters` package](#/api/updaters).

```javascript
Expand All @@ -47,6 +47,40 @@ For the `preProcessor` argument, see [#/advanced/preprocessors]. `preProcessor`

**Description**

`drv` creates a derived, reactive value. It accepts a single function (which should not have side effects),
and it will automatically subscribe to all reactive values that were accessed when running that function.
The output of the function is cached until one observed reactive values changes.

Generally, it is recommended to use `drv` abunduntaly; every `drv` is a potential caching point, making change propagation trough your system easier.
It will also help you reduce the amount of _state_ you have in your application, and under almost all circumstances it is better
to have a piece of derived information, over some redundant information that is kept up to date through other means (such as `sub`).

The optional `setter` argument can be used to make the derived value writable.
This is just syntactic convenience, as the `drv` itself does not hold state, but it can be used as 'write-through' mechanism.
Setters are automatically wrapped in an `act`.

```javascript
const priceDollar = val(10)
const euroDollarRate = val(1.1487)

const priceEuro = drv(
// derive the price in euro
() => priceDollar() / euroDollarRate(),
// setter: makes priceEuro writeable
(newPriceEur) => {
priceDollar(newPriceEur * euroDollarRate())
}
)

console.log(priceEuro()) // 8.71
priceEuro(10)
console.log(priceDollar()) // 11.49
```

By default, RVal assumes that a `drv` that is not observed itself by anyone (by a `drv`, `sub` or `effect) is not relevant anymore,
and it will automatically suspend the computation, meaning that it will stop observing it's inputs on the next JavaScript tick.
This is done to make sure no memory leaks are introduced. If you want to forcefully cache the `drv` output under all circumstances, use `keepAlive` from `@r-val/utils`.

## `sub`

**Signature**
Expand All @@ -65,9 +99,51 @@ For the `preProcessor` argument, see [#/advanced/preprocessors]. `preProcessor`

**Description**

TODO
`sub` subscribe to a reative value.
It will trigger for each change in the reactive value, but only once per batch (see `act`).
`sub` is the primary means to create side effects that are triggered by reactive values,
such as logging, updating a rendering, or makign network requests.
(In the end, any system needs side effects, to be able to perform output).

`sub` returns a disposer function, that stops future executions of the `listener` once called.

```javascript
import { val, sub, drv } from "@r-val/core"

function createTodo(id, initialTitle) {
const title = val(initialTitle)
const done = val(false)
const toJS = drv(() => ({
id,
title: title(),
done: done()
}))

// set up a subscription to automatically save the
// todo when either title or done changes
const stop = sub(toJS, data => {
window.fetch("http://host/todos/" + id, {
method: "POST",
body: JSON.stringify(data)
}).catch(e => {
console.error(e)
})
})

return {
title, done, toJS, stop
}
}

const myTodo = createTodo(1, "get cofee")
myTodo.title("get coffee") // will be saved automatically
myTodo.stop() // stop future executions of the subscription
```

In cases, it might be tempting to update other reactive values in a subscription,
but in most cases this is an anti pattern and the value-to-be-updated could be expressed as derived value using `drv` instead.

The curried version of the api can be useful to create utilities, for example a `logger` that prints the values of any state transition:
The curried version of the `sub` can be useful to create utilities, for example a `logger` that prints the values of any state transition:

```javascript
const log = sub((current, previous) => {
Expand All @@ -78,14 +154,89 @@ log(someReactiveValue)
log(someOtherReactiveValue)
```

N.B. this utility is actually built-in as `logChanges` in the `@r-val/utils` package.

## `act`

**signature**

`act(function) -> function`

**discription**

`act` takes accepts a function, and returns a function with the same signature, that, when invoked, will batch the updates.
This is also called an _action_.
This means, that if multiple mutations in that same function would normally affect multiple invocations of the same side-effect,
an function wrapped in `act` will trigger the side effects at most once.

Although it is very well possible to create and invoke an action in one go (like: `act(() => { /* someStuff */ })()`),
it is recommended to not do this. Updates that are grouped together are a nice opportunity to extract a single function with limited functionality.

If functions that are wrapped by `act` invoke other functions wrapped by `act`, side effects will only trigger once the outer `act` completes.

That's all! Note that `act` only batches _synchronosly_ run updates. Passing an `async` function to `act` is in that regard mostly useless (only the code until the first `await` will be batched in that case).
Calling individual actions from `async` functions is no problem.

## `effect`

**signature**

`effect(functionToTrack: () -> T, onInvalidate: (didChange() -> boolean, pull() -> T)): disposerFunction`

**description**

`effect` is a core building block to organize side effects.
For example `sub` and `@r-val/react`'s `rview` use `effect` under the hood.
`effect` tracks a function (that might produce a value), and notifies the `onInvalidate` callback when
some of the dependencies of the tracked function _might_ have changed.

`effect` basically produces a state machine with the following states / contract (it initializes at step 7!):

1. The `functionToTrack` is executed, and the effect records which reactive values where used during the executions
2. If one or more of those dependencies might have changed, `onInvalidate` is called, with two parameters; `didChange()` and `pull()`
3. At any point in time, `didChange()` should be called. This will make sure that the dependencies are actually _evaluated_ (the effect might be depending on derived values that have not been evauated so far).
4. If `didChange()` returns `false`, nothing did actually changed, and `onInvalidate` might be invoked again if dependencies change in the future. (Go back to 2.)
5. If `didChange()` returns `true`, `pull()` should be called at any point in time in the future. `pull()` will perform step 1 again (tracking the function), register the dependencies, and returns any value it yields.
6. If the `disposer` returned from `effect` is called at any moment in time, the effect is immediately disposed, cleaning up the dependency tree.
7. Initially, the state machine is kicked of by calling `onInvalidate` once, starting basically at step 3. (Note that `didChange` will always yield `true` the first time)


Note that `onInvalidate` won't be called again, until `didChange()` is called and returned `false`,
or `pull()` is called.

You probably won't use `effect` much in practice (using `sub` is more straightforward),
but `effect` is a good building block to create utility functions that have very specific caching or timing behavior.

For example, an alternative to `sub(source, listener)` that debounces the listener and at max triggers its listener
every x milliseconds could be defined as:

```javascript
function sub(src, listener, delay) {
let lastSeen = undefined

return effect(
() => src(),
(didChange, pull) => {
if (didChange()) {
setTimeout(() => {
const v = pull()
if (v !== lastSeen)
listener(v, lastSeen)
lastSeen = v
}, delay);
}
}
)
}
```

## `isVal`

## `isDrv`


---

## `configure`

**Signature**
Expand Down
8 changes: 4 additions & 4 deletions pkgs/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ export function rval(base?: Val<any, any>): RValInstance {
return new Computed<T>(context, api, derivation, setter).get as any
}

function effect<T>(fn: () => T, onInvalidate: (onChanged: () => boolean, pull: () => T) => void): Thunk {
function effect<T>(fn: () => T, onInvalidate: (didChange: () => boolean, pull: () => T) => void): Thunk {
const computed = isDrv(fn) ? fn[$RVal] : new Computed(context, api, fn)
let scheduled = true
let disposed = false
let lastSeen = undefined
let lastSeen: any = {} // definitely not pointer equal to something else

function didChange() {
if (disposed) return false
Expand Down Expand Up @@ -153,8 +153,8 @@ export function rval(base?: Val<any, any>): RValInstance {
return effect(src, (didChange, pull) => {
if (didChange()) {
const v = pull()
if (!firstRun && v !== lastSeen) listener(v, lastSeen)
if (firstRun && options && options.fireImmediately) listener(v, lastSeen)
if (firstRun ? (options && options.fireImmediately) : v !== lastSeen)
listener(v, lastSeen)
lastSeen = v
firstRun = false
}
Expand Down
13 changes: 12 additions & 1 deletion pkgs/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,16 @@ export function debug(condition? : any, startDebuggerOrMessage?: any) {
// if functioon
// if nothing
// if value
// TODO:
(new Function("debugger"))
}
}

export function logChanges(source: Observable<any>, prefix = "rval: changed") {
return rval(source).sub(source, (current, previous) => {
console.log(`[${prefix}] ${previous} -> ${current}`)
})
}

// export function actNow<R>(fn: () => R): R {
// return act(fn)()
// }
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11826,6 +11826,11 @@ sane@^2.0.0:
optionalDependencies:
fsevents "^1.2.3"

sarcastic@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/sarcastic/-/sarcastic-1.5.0.tgz#1e87e75bb64e856d3f34d1f5a391c3abecf8c7c1"
integrity sha512-j7zmeNbABvOGXzt11WIm7nITPJ0exX/IL0sV9GZHNSsrFEjadfK5Z3GfBEelc6/VoEp17i/JEw3UuzyJNvgZNw==

sax@^1.2.4, sax@~1.2.1, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
Expand Down

0 comments on commit 2a861eb

Please sign in to comment.