@dra2020/baseclient 1.0.75 → 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.
@@ -1,23 +1,33 @@
1
+ export interface IDataFlow {
2
+ dfid: () => any;
3
+ }
1
4
  interface UseItem {
2
- df: DataFlow;
5
+ name?: string;
6
+ df: IDataFlow;
3
7
  id?: any;
8
+ wasfresh?: boolean;
4
9
  }
5
10
  export declare class DataFlow {
6
11
  usesList: UseItem[];
7
12
  constructor();
8
- id(): any;
9
- value(): any;
10
- uses(df: DataFlow): void;
11
- usesStale(): boolean;
12
- usesRemember(): void;
13
- ifcompute(): void;
13
+ dfid(): any;
14
14
  compute(): void;
15
+ uses(df: IDataFlow, name?: string): void;
16
+ stale(): boolean;
17
+ wasFresh(name: string): boolean;
18
+ remember(): void;
19
+ ifcompute(): void;
15
20
  }
16
21
  export declare class DataFlowCallback extends DataFlow {
17
22
  _value: any;
18
23
  _cb: () => any;
19
24
  constructor(cb: () => any);
20
- id(): any;
21
- value(): any;
25
+ dfid(): any;
26
+ }
27
+ export declare class DataFlowStamp extends DataFlow {
28
+ _stamp: number;
29
+ constructor();
30
+ dfid(): any;
31
+ stamp(): void;
22
32
  }
23
33
  export {};
@@ -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
+ ```
@@ -2,21 +2,25 @@
2
2
  // DataFlow: mechanism for setting up a data-flow dependency graph that gets computed on demand.
3
3
  //
4
4
  // Semantics are these:
5
- // 1. The simplest "atomic" DataFlow object just has an id() and a value(). The id() is used to check for exact
6
- // equivalence when determining if any dependents need to be recomputed. id() and value() might be the same
7
- // for something that just creates a new whole object when recomputed. In other cases, id() might represent a
8
- // hash or changestamp/timestamp that is distinct from the value(). The value may or may not be "ready" as well.
9
- // If the value is not "ready", no dependents can be computed.
5
+ // 1. The simplest "atomic" DataFlow object just has an id(). The id() is used to check for exact
6
+ // equivalence when determining if any dependents need to be recomputed.
10
7
  // 2. A DataFlow object can record that it "uses" another DataFlow object. If it does, it can use a set of helper
11
- // routines to track the state of its dependents. When its dependents are all ready(), it remembers their ids
12
- // and can later test if they are stale.
8
+ // routines to track the state of its dependents. When its dependents are "stale" (have changed) the computation
9
+ // needs to be run.
13
10
  //
14
11
  //
15
12
 
13
+ export interface IDataFlow
14
+ {
15
+ dfid: () => any;
16
+ }
17
+
16
18
  interface UseItem
17
19
  {
18
- df: DataFlow,
20
+ name?: string;
21
+ df: IDataFlow,
19
22
  id?: any,
23
+ wasfresh?: boolean,
20
24
  }
21
25
 
22
26
  export class DataFlow
@@ -29,38 +33,47 @@ export class DataFlow
29
33
  }
30
34
 
31
35
  // override in subclass
32
- id(): any { return null }
33
- value(): any { return null }
36
+ dfid(): any { return null }
34
37
 
35
- uses(df: DataFlow): void
38
+ // override in subclass
39
+ compute(): void
36
40
  {
37
- this.usesList.push({ df: df });
38
41
  }
39
42
 
40
- usesStale(): boolean
43
+ uses(df: IDataFlow, name?: string): void
44
+ {
45
+ this.usesList.push({ name: name, df: df });
46
+ }
47
+
48
+ stale(): boolean
41
49
  {
42
50
  let isstale = false;
43
- this.usesList.forEach(ui => { if (ui.id !== ui.df.id()) isstale = true });
51
+ this.usesList.forEach(ui => {
52
+ ui.wasfresh = ui.id !== ui.df.dfid();
53
+ if (ui.wasfresh) isstale = true;
54
+ });
44
55
  return isstale;
45
56
  }
46
57
 
47
- usesRemember(): void
58
+ wasFresh(name: string): boolean
59
+ {
60
+ let ui: UseItem = this.usesList.find((ui: UseItem) => ui.name === name);
61
+ return ui != null && ui.wasfresh;
62
+ }
63
+
64
+ remember(): void
48
65
  {
49
- this.usesList.forEach(ui => { ui.id = ui.df.id() });
66
+ this.usesList.forEach(ui => { ui.id = ui.df.dfid() });
50
67
  }
51
68
 
52
69
  ifcompute(): void
53
70
  {
54
- if (this.usesStale())
71
+ if (this.stale())
55
72
  {
56
- this.usesRemember();
73
+ this.remember();
57
74
  this.compute();
58
75
  }
59
76
  }
60
-
61
- compute(): void
62
- {
63
- }
64
77
  }
65
78
 
66
79
  // Takes callback that, eventually, returns non-null. The return value is both the value and the id.
@@ -76,6 +89,21 @@ export class DataFlowCallback extends DataFlow
76
89
  this._cb = cb;
77
90
  }
78
91
 
79
- id(): any { if (!this._value) this._value = this._cb(); return this._value }
80
- value(): any { return this.id() }
92
+ dfid(): any { if (!this._value) this._value = this._cb(); return this._value }
93
+ }
94
+
95
+ // Simple helper that maintains a simple monotonically increasing stamp
96
+ export class DataFlowStamp extends DataFlow
97
+ {
98
+ _stamp: number;
99
+
100
+ constructor()
101
+ {
102
+ super();
103
+ this._stamp = 0;
104
+ }
105
+
106
+ dfid(): any { return this._stamp }
107
+
108
+ stamp(): void { this._stamp++ }
81
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dra2020/baseclient",
3
- "version": "1.0.75",
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",