@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.
@@ -5,7 +5,7 @@ interface UseItem {
5
5
  name?: string;
6
6
  df: IDataFlow;
7
7
  id?: any;
8
- wasstale?: boolean;
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
- usesStale(): boolean;
17
- wasStale(name: string): boolean;
18
- usesRemember(): void;
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 {
@@ -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
+ ```
@@ -20,7 +20,7 @@ interface UseItem
20
20
  name?: string;
21
21
  df: IDataFlow,
22
22
  id?: any,
23
- wasstale?: boolean,
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
- usesStale(): boolean
48
+ stale(): boolean
49
49
  {
50
50
  let isstale = false;
51
51
  this.usesList.forEach(ui => {
52
- ui.wasstale = ui.id !== ui.df.dfid();
53
- if (ui.wasstale) isstale = true;
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
- wasStale(name: string): boolean
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.wasstale;
61
+ return ui != null && ui.wasfresh;
62
62
  }
63
63
 
64
- usesRemember(): void
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.usesStale())
71
+ if (this.stale())
72
72
  {
73
- this.usesRemember();
73
+ this.remember();
74
74
  this.compute();
75
75
  }
76
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dra2020/baseclient",
3
- "version": "1.0.77",
3
+ "version": "1.0.78",
4
4
  "description": "Utility functions for Javascript projects.",
5
5
  "main": "dist/baseclient.js",
6
6
  "types": "./dist/all/all.d.ts",