You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+3-20Lines changed: 3 additions & 20 deletions
Original file line number
Diff line number
Diff line change
@@ -5,25 +5,8 @@
5
5
</picture>
6
6
</div>
7
7
8
-
**Signia** is a small, fast, and scaleable signals library for JavaScript and TypeScript.
8
+
**Signia** is a minimal, fast, and [scalable](https://signia.tldraw.dev/docs/scalability) signals library for TypeScript.
9
9
10
-
It uses an epochal pull-based (i.e. lazy) reactivity model that provides a lot of leverage to keep performance high and spaghetti low as the size and complexity of an application grows.
10
+
It uses a new clock-based lazy reactivity system that allows signals to scale with complex data-intensive applications.
11
11
12
-
Check the [docs](https://tldraw.github.io/signia)
13
-
14
-
## What are signals?
15
-
16
-
## How is Signia different?
17
-
18
-
The key difference is scalability. Signia uses a unique reactivity model which allows signals to emit both ordinary values, and 'deltas' between successive values over time.
19
-
20
-
Imagine something like the following:
21
-
22
-
```ts
23
-
const todos =atom([{ title: 'buy milk', completed: false}, ...])
Every time you add a new todo item, `incompleteTodos` will be recomputed from scratch, running the filter predicate on all todo items regardless of whether they have changed.
28
-
29
-
With Signia, you can get only the changes since last time, and do with them what you like.
Copy file name to clipboardExpand all lines: docs/docs/_intro.mdx
+2-2Lines changed: 2 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -9,8 +9,8 @@ import useBaseUrl from '@docusaurus/useBaseUrl'
9
9
}}
10
10
/>
11
11
12
-
**Signia** is a minimal, fast, and scalable signals library for TypeScript. It is framework-agnostic, and has official React bindings.
12
+
**Signia** is a minimal, fast, and [scalable](/docs/scalability) signals library for TypeScript. It is framework-agnostic, and has official React bindings.
13
13
14
-
It uses a new epochal pull-based reactivity system that allows signals to scale with complex data-intensive applications.
14
+
It uses a new clock-based lazy reactivity system that allows signals to scale with complex data-intensive applications.
15
15
16
16
Signia was originally created for [tldraw](https://beta.tldraw.com), to meet performance demands that other reactive signals libraries could not.
Copy file name to clipboardExpand all lines: docs/docs/incremental.mdx
+92-24Lines changed: 92 additions & 24 deletions
Original file line number
Diff line number
Diff line change
@@ -4,24 +4,44 @@ sidebar_position: 2
4
4
5
5
# Incrementally computed signals
6
6
7
-
One of the things that sets Signia apart is that it supports incremental recomputation of derived values.
8
-
This means that working with large derived collections can be extremely efficient, while still benefitting from the lazy evaluation and always-on-caching that Signia provides.
7
+
One of Signia's superpowers is its support for incremental recomputation of derived values.
8
+
This makes it possible to work with large derived collections extremely efficiently, while still benefitting from the lazy evaluation and always-oncaching that Signia provides.
9
9
10
-
This is achieved using an epochal reactivity system.
10
+
This is achieved using a clock-based reactivity system.
11
11
12
-
## Epochs
12
+
## Clocks and Epochs
13
13
14
-
Signia has a global 'epoch' value which is an integer that gets incremented every time any atom is updated.
14
+
Signia has a global logical **clock**. This is an integer that gets incremented every time any atom is updated.
15
15
16
-
When a derived value is computed, its computing function is passed two arguments: the previous value, and the global epoch when the previous value was computed.
16
+
An **epoch**is one specific value of the global clock. It is a virtual point in time.
17
17
18
-
This 'last computed' epoch can be used in conjunction with the `Signal.getDiffSince(epoch)` method to retrieve a list of changes, or 'diffs', since the last time the computed value was derived.
18
+
You can access the epoch upon which a signal's value last changed using the `lastChangedEpoch` property:
19
+
20
+
```ts
21
+
const firstName =atom('firstName', 'Brian')
22
+
const startEpoch =firstName.lastChangedEpoch
23
+
firstName.set('Steve')
24
+
const endEpoch =firstName.lastChangedEpoch
25
+
console.log(endEpoch-startEpoch) // 1
26
+
```
27
+
28
+
When a derived value is computed, its computing function is passed two arguments:
29
+
30
+
-`previousValue` the last value returned by the computing function
31
+
-`lastComputedEpoch` the value of the global clock when the previous value was last _computed_.
32
+
33
+
:::note
34
+
Beware that 'last computed' is not the same as 'last changed', since a value can be recomputed and end up the same as it was before.
35
+
:::
36
+
37
+
`lastComputedEpoch` can be used in conjunction with the `Signal.getDiffSince` method to retrieve a list of changes, or **diffs**, since the last time the computing function was invoked.
19
38
20
39
## Diffs don't come free
21
40
22
-
JavaScript's data types don't have built-in support for diffs, so it necessary to implement this functionality manually.
41
+
JavaScript's datatypes don't have built-in support for diffs, so you need to implement this functionality manually.
23
42
24
-
For this tutorial, let's use [`immer`](https://immerjs.github.io/immer/), which is a library for working with immutable data. It also has the ability to extract diffs while making changes
43
+
For this tutorial, let's use [`immer`](https://immerjs.github.io/immer/), which is a library for working with immutable data.
44
+
It has the ability to extract diffs while making changes using its `produceWithPatches` function.
25
45
26
46
Here is an example of an Atom wrapper which uses `immer` to capture diffs:
27
47
@@ -36,7 +56,7 @@ class ImmerAtom<T> {
36
56
readonly atom:Atom<T, Patch[]>
37
57
constructor(name:string, initialValue:T) {
38
58
this.atom=atom(name, initialValue, {
39
-
// In order to save diffs, we need to provide a historyLength argument
59
+
// In order to store diffs, we need to provide the `historyLength` argument
40
60
// to the atom constructor. Otherwise it will not allocate a history buffer.
41
61
historyLength: 10,
42
62
})
@@ -51,7 +71,7 @@ class ImmerAtom<T> {
51
71
52
72
## Using diffs in `computed`
53
73
54
-
Now that we have a way to capture diffs, we can use them in our `computed` functions.
74
+
We can use the diffs emitted by our ImmerAtom in our `computed` functions.
55
75
56
76
Let's define an incremental version of Array.map:
57
77
@@ -118,7 +138,7 @@ function map<T, U>(source: ImmerAtom<T[]>, fn: (value: T) => U): Computed<U[], P
118
138
119
139
You're probably thinking: "that's a whole lot of code just to map over an array!"
120
140
121
-
Alas, such is the nature of the beast. Incremental logic is _much_ trickier to write than non-incremental logic, but the payoff is worth it.
141
+
Alas, incremental logic is _much_ trickier to write than non-incremental logic. But often the payoff is worth it.
In this example, if the mapping function reads any other external signals, then those dependencies will not necessarily be captured on incremental runs. e.g. if popping an item off the list.
176
-
This will prevent updates to external signals from propagating correctly through the mapped list.
177
-
There are a couple of ways around this:
178
-
179
-
- Maintaining an array of computed signals, one for each item in the root list.
180
-
- Adding an explicit dependencies array to the `map` function.
181
-
182
-
These are left as an exercise for the reader :) Feel free to reach out in our [Discord](https://discord.gg/3GJTuqay) if you need help.
183
-
:::
184
-
185
193
## The `historyLength` option
186
194
187
195
The `historyLength` option is used to tell Signia how many diffs to store. Each time a value changes, a new diff is stored.
Most complex software systems do _something_ along these lines by necessity, usually ad-hoc. The nice thing about integrating it into Signia is that it's now a first-class citizen and it works seamlessly with other signals. There's no need to worry about cache invalidation or update ordering, everything just works.
219
279
220
280
At [tldraw](https://tldraw.com) we use incrementally computed signals for a handful of our core data structures, and it's been a huge win for performance. We're able to keep our canvas snappy and responsive even when we have thousands of shapes.
221
281
222
282
We also have a rudimentary reactive database based on `signia` which makes heavy use of incrementally computed signals for building reactive queries and indexes.
283
+
284
+
:::info Get involved!
285
+
286
+
This is an extremely new kind of tool with lots of sharp edges! There are probably lots of ways to improve it and address common problems.
287
+
288
+
We'd be happy to hear your feedback and suggestions on [GitHub](https://github.com/tldraw/signia/discussions) or in the [Discord channel](https://discord.gg/3GJTuqay).
0 commit comments