@dra2020/baseclient 1.0.77 → 1.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/baseclient.js +8 -8
- package/dist/baseclient.js.map +1 -1
- package/dist/dataflow/dataflow.d.ts +4 -4
- package/docs/dataflow.md +167 -0
- package/lib/dataflow/dataflow.ts +9 -9
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ interface UseItem {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
df: IDataFlow;
|
|
7
7
|
id?: any;
|
|
8
|
-
|
|
8
|
+
wasfresh?: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare class DataFlow {
|
|
11
11
|
usesList: UseItem[];
|
|
@@ -13,9 +13,9 @@ export declare class DataFlow {
|
|
|
13
13
|
dfid(): any;
|
|
14
14
|
compute(): void;
|
|
15
15
|
uses(df: IDataFlow, name?: string): void;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
stale(): boolean;
|
|
17
|
+
wasFresh(name: string): boolean;
|
|
18
|
+
remember(): void;
|
|
19
19
|
ifcompute(): void;
|
|
20
20
|
}
|
|
21
21
|
export declare class DataFlowCallback extends DataFlow {
|
package/docs/dataflow.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# DataFlow
|
|
2
|
+
Library for managing the synchronous execution of a series of data flow computations
|
|
3
|
+
that are recomputed only when inputs explicitly change.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A common design problem in interactive applications is that you have a set of base data
|
|
8
|
+
objects that are changing over time and then a set of derived data objects that are recomputed by
|
|
9
|
+
some functions over those base data objects when they change.
|
|
10
|
+
|
|
11
|
+
In order to optimize interactive performance, you would like to minimize the amount of recomputation
|
|
12
|
+
that needs to happen when base data objects change to only those set of derived objects that are
|
|
13
|
+
actually impacted by the change. In order to ensure correctness, you want to ensure that they do
|
|
14
|
+
indeed get recomputed when the inputs have changed.
|
|
15
|
+
|
|
16
|
+
A common technique is to have the base data objects expose some `stamp` - perhaps simply a monotonically
|
|
17
|
+
increasing integer change stamp, a timestamp, or a content-based hash or even an entire new object that represents
|
|
18
|
+
the state of the world (libraries that implement `immutable` objects tend to work this way).
|
|
19
|
+
Dependent objects then keep track of the value of the `stamp` when they were computed.
|
|
20
|
+
Next time they are asked to compute their value, they can examine this saved stamp against the current
|
|
21
|
+
stamp value and only recompute if there has been a change.
|
|
22
|
+
|
|
23
|
+
Often derived objects are computed off some combination of base objects and so are maintaining and tracking
|
|
24
|
+
multiple stamps. A derived data object might also itself serve as the input to another derived computation.
|
|
25
|
+
|
|
26
|
+
The result is that you have a tree of data flows and would like to prune the computation to only compute
|
|
27
|
+
what is necessary based on the actual base data changes that actually occurred.
|
|
28
|
+
|
|
29
|
+
The `DataFlow` class allows you to manage this set of computations in a structured way that makes the
|
|
30
|
+
dependencies explicit and handles the basic bookkeeping around changing and tracking stamps.
|
|
31
|
+
|
|
32
|
+
A base data object needs to simply match the `IDataFlow` interface to participate in the dataflow computation:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
interface IDataFlow {
|
|
36
|
+
dfid: () => any
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `dfid` function is the `stamp` and should match the data flow semantics: it changes when dependent
|
|
41
|
+
derived objects need to be recomputed and can be tested against a previous stamp using JavaScript
|
|
42
|
+
exact equivalence (===).
|
|
43
|
+
|
|
44
|
+
## Example
|
|
45
|
+
|
|
46
|
+
A derived data object should extend off the `DataFlow` class.
|
|
47
|
+
|
|
48
|
+
That object should describe its dependencies by the `uses` function. Here's a full class that we will walk through.
|
|
49
|
+
|
|
50
|
+
(Note that in the example below we have our class take a `IDataFlow` object in the constructor.
|
|
51
|
+
Normally it would take some object that exposes the data it needs to use for computing its value and
|
|
52
|
+
either matches the `IDataFlow` interface or exposes a sub-object that does and can be passed to the `uses` function call.
|
|
53
|
+
For simplicity I just had it take an `IDataFlow`.)
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
class MyComputation extends DataFlow
|
|
57
|
+
{
|
|
58
|
+
basedata: IDataFlow;
|
|
59
|
+
_myresult: any;
|
|
60
|
+
|
|
61
|
+
constructor(basedata: IDataFlow)
|
|
62
|
+
{
|
|
63
|
+
super();
|
|
64
|
+
this.basedata = basedata;
|
|
65
|
+
this.uses(basedata);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dfid(): any { this.ifcompute(); return this._myresult }
|
|
69
|
+
|
|
70
|
+
myresult(): any { this.ifcompute(); return this._myresult }
|
|
71
|
+
|
|
72
|
+
compute(): void
|
|
73
|
+
{
|
|
74
|
+
// compute _myresult from basedata
|
|
75
|
+
// maybe only change _myresult conditionally - the change in basedata might have been irrelevant
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This class has a number of common characteristics you find in many `DataFlow` subclasses:
|
|
81
|
+
|
|
82
|
+
- It extends from the `DataFlow` class to inherit the basic change tracking mechanism.
|
|
83
|
+
|
|
84
|
+
- It defines its own dfid function so it can act as an internal node in a larger data flow computation.
|
|
85
|
+
|
|
86
|
+
- It defines a `compute` function that does the actual work of computing the derived result. This function might
|
|
87
|
+
examine the basedata object to determine if its value (`_myresult`) actually does change. If it doesn't need to
|
|
88
|
+
change, it would ensure that any downstream data flow nodes are not forced to recompute by leaving `_myresult`
|
|
89
|
+
unchanged.
|
|
90
|
+
|
|
91
|
+
- It uses the helper member function `ifcompute` to determine whether it needs to recompute its result. This function is
|
|
92
|
+
simply:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
ifcompute(): void
|
|
96
|
+
{
|
|
97
|
+
if (this.stale())
|
|
98
|
+
{
|
|
99
|
+
this.remember();
|
|
100
|
+
this.compute();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The function `stale` simply tests whether any nodes this object `uses` have changed in value since the last time
|
|
106
|
+
they were remembered. If the inputs are _fresh_, the function calls `remember` to remember the stamps (dfid) of the
|
|
107
|
+
inputs and calls the (typically overridden) `compute` function to perform the actual computation.
|
|
108
|
+
|
|
109
|
+
## wasFresh
|
|
110
|
+
|
|
111
|
+
The helper function `wasFresh` can be used inside a `compute` implementation to test a specific input for freshness.
|
|
112
|
+
That is, `compute` will be called when _any_ of its inputs are fresh. In some cases, `compute` can be optimized if
|
|
113
|
+
it knows which specific inputs are fresh. In order to use `wasFresh`, provide an extra `name` argument to the `uses` call
|
|
114
|
+
for that input.
|
|
115
|
+
|
|
116
|
+
If you are considering using `wasFresh`, also consider whether you might instead define an additional node in the data flow
|
|
117
|
+
tree that performs this pruning rather than embedding it inside your `compute` implementation.
|
|
118
|
+
|
|
119
|
+
## Common Patterns
|
|
120
|
+
|
|
121
|
+
There are some common patterns that recur when using `DataFlow`.
|
|
122
|
+
|
|
123
|
+
- A base object participates by simply exposing a `dfid` function over a pre-existing stamp that matches the DataFlow
|
|
124
|
+
semantics.
|
|
125
|
+
|
|
126
|
+
- A node acts as a _throttle_ or _gate_ to the computation tree by taking a dependency on an object that is promiscuous
|
|
127
|
+
about announcing changes and verifies that subsequent nodes are actually impacted. Those can then be simpler in
|
|
128
|
+
assuming that they should really recompute rather than mixing special checks into the internals of their `compute`
|
|
129
|
+
implementation. In fact, it is just this process of pulling out special "optimizations" around recomputation into
|
|
130
|
+
an explicit tree of dependencies that makes the `DataFlow` structure an improvement in the design of your application.
|
|
131
|
+
|
|
132
|
+
- An object just performs actions based on whether some input has changed. That is, it doesn't actual produce an
|
|
133
|
+
explicit result. The DataFlow model is a _pull_ model, so recomputation only happens when the result is actually
|
|
134
|
+
requested. In the case of side-effects, a common approach is to simply define a *root* `DataFlow` node and make this
|
|
135
|
+
node dependent on your classes that work through side-effects. At some appropriate point, `root.ifcompute()` is called
|
|
136
|
+
and the dependents are evaluated, triggering any side-effects.
|
|
137
|
+
|
|
138
|
+
- Some class computes multiple outputs. It is a common scenario that a node walking over some structure might actually
|
|
139
|
+
optimize by computing multiple outputs. In this case, you call ifcompute() before either output is requested,
|
|
140
|
+
but secondary calls will not need to do any work. So it would look something like this:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
class TwoOutputs extends DataFlow
|
|
144
|
+
{
|
|
145
|
+
basedata: IDataFlow;
|
|
146
|
+
_myresult1: any;
|
|
147
|
+
_myresult2: any;
|
|
148
|
+
|
|
149
|
+
constructor(basedata: IDataFlow)
|
|
150
|
+
{
|
|
151
|
+
super();
|
|
152
|
+
this.basedata = basedata;
|
|
153
|
+
this.uses(basedata);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Just pick one output that matchs DataFlow semantics, or create an explicit additional stamp
|
|
157
|
+
dfid(): any { this.ifcompute(); return this._myresult1 }
|
|
158
|
+
|
|
159
|
+
myresult1(): any { this.ifcompute(); return this._myresult1 }
|
|
160
|
+
myresult2(): any { this.ifcompute(); return this._myresult2 }
|
|
161
|
+
|
|
162
|
+
compute(): void
|
|
163
|
+
{
|
|
164
|
+
// compute _myresult1 and _myresult2 from basedata
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
package/lib/dataflow/dataflow.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface UseItem
|
|
|
20
20
|
name?: string;
|
|
21
21
|
df: IDataFlow,
|
|
22
22
|
id?: any,
|
|
23
|
-
|
|
23
|
+
wasfresh?: boolean,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export class DataFlow
|
|
@@ -45,32 +45,32 @@ export class DataFlow
|
|
|
45
45
|
this.usesList.push({ name: name, df: df });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
stale(): boolean
|
|
49
49
|
{
|
|
50
50
|
let isstale = false;
|
|
51
51
|
this.usesList.forEach(ui => {
|
|
52
|
-
ui.
|
|
53
|
-
if (ui.
|
|
52
|
+
ui.wasfresh = ui.id !== ui.df.dfid();
|
|
53
|
+
if (ui.wasfresh) isstale = true;
|
|
54
54
|
});
|
|
55
55
|
return isstale;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
wasFresh(name: string): boolean
|
|
59
59
|
{
|
|
60
60
|
let ui: UseItem = this.usesList.find((ui: UseItem) => ui.name === name);
|
|
61
|
-
return ui != null && ui.
|
|
61
|
+
return ui != null && ui.wasfresh;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
remember(): void
|
|
65
65
|
{
|
|
66
66
|
this.usesList.forEach(ui => { ui.id = ui.df.dfid() });
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
ifcompute(): void
|
|
70
70
|
{
|
|
71
|
-
if (this.
|
|
71
|
+
if (this.stale())
|
|
72
72
|
{
|
|
73
|
-
this.
|
|
73
|
+
this.remember();
|
|
74
74
|
this.compute();
|
|
75
75
|
}
|
|
76
76
|
}
|