@jay-framework/reactive 0.5.0
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/dist/index.d.ts +43 -0
- package/dist/index.js +209 -0
- package/dist/tracing.js +197 -0
- package/package.json +35 -0
- package/readme.md +284 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
declare enum MeasureOfChange {
|
|
2
|
+
NO_CHANGE = 0,
|
|
3
|
+
PARTIAL = 1,
|
|
4
|
+
FULL = 2
|
|
5
|
+
}
|
|
6
|
+
type Next<T> = (t: T) => T;
|
|
7
|
+
type Setter<T> = (t: T | Next<T>) => T;
|
|
8
|
+
type Getter<T> = () => T;
|
|
9
|
+
type Reaction = (measureOfChange: MeasureOfChange) => void;
|
|
10
|
+
type ValueOrGetter<T> = T | Getter<T>;
|
|
11
|
+
declare const GetterMark: unique symbol;
|
|
12
|
+
declare const SetterMark: unique symbol;
|
|
13
|
+
declare class Reactive {
|
|
14
|
+
private batchedReactionsToRun;
|
|
15
|
+
private isAutoBatchScheduled;
|
|
16
|
+
protected reactionIndex: number;
|
|
17
|
+
private reactions;
|
|
18
|
+
private reactionDependencies;
|
|
19
|
+
private dirty;
|
|
20
|
+
private dirtyResolve;
|
|
21
|
+
private timeout;
|
|
22
|
+
protected inBatchReactions: boolean;
|
|
23
|
+
private inFlush;
|
|
24
|
+
private reactionGlobalKey;
|
|
25
|
+
private reactivesToFlush;
|
|
26
|
+
private disabled;
|
|
27
|
+
private allowedPairedReactives;
|
|
28
|
+
createSignal<T>(value: ValueOrGetter<T>, measureOfChange?: MeasureOfChange): [get: Getter<T>, set: Setter<T>];
|
|
29
|
+
protected triggerReaction(index: number, measureOfChange: MeasureOfChange, paired: boolean): void;
|
|
30
|
+
enablePairing(sourceReactive: Reactive): void;
|
|
31
|
+
createReaction(func: Reaction): void;
|
|
32
|
+
batchReactions<T>(func: () => T): T;
|
|
33
|
+
private ScheduleAutoBatchRuns;
|
|
34
|
+
toBeClean(): Promise<void>;
|
|
35
|
+
private runReaction;
|
|
36
|
+
flush(): void;
|
|
37
|
+
enable(): void;
|
|
38
|
+
disable(): void;
|
|
39
|
+
}
|
|
40
|
+
declare function setMkReactive(mkReactive: (...reactiveNames: (string | number)[]) => Reactive): void;
|
|
41
|
+
declare function mkReactive(...reactiveNames: (string | number)[]): Reactive;
|
|
42
|
+
|
|
43
|
+
export { type Getter, GetterMark, MeasureOfChange, type Next, type Reaction, Reactive, type Setter, SetterMark, type ValueOrGetter, mkReactive, setMkReactive };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => {
|
|
4
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
var MeasureOfChange = /* @__PURE__ */ ((MeasureOfChange2) => {
|
|
8
|
+
MeasureOfChange2[MeasureOfChange2["NO_CHANGE"] = 0] = "NO_CHANGE";
|
|
9
|
+
MeasureOfChange2[MeasureOfChange2["PARTIAL"] = 1] = "PARTIAL";
|
|
10
|
+
MeasureOfChange2[MeasureOfChange2["FULL"] = 2] = "FULL";
|
|
11
|
+
return MeasureOfChange2;
|
|
12
|
+
})(MeasureOfChange || {});
|
|
13
|
+
const GetterMark = Symbol.for("getterMark");
|
|
14
|
+
const SetterMark = Symbol.for("setterMark");
|
|
15
|
+
const runningReactions = [];
|
|
16
|
+
function pushRunningReaction(reactiveGlobalKey) {
|
|
17
|
+
runningReactions.push(reactiveGlobalKey);
|
|
18
|
+
}
|
|
19
|
+
function popRunningReaction() {
|
|
20
|
+
runningReactions.pop();
|
|
21
|
+
}
|
|
22
|
+
class Reactive {
|
|
23
|
+
constructor() {
|
|
24
|
+
__publicField(this, "batchedReactionsToRun", []);
|
|
25
|
+
__publicField(this, "isAutoBatchScheduled", false);
|
|
26
|
+
__publicField(this, "reactionIndex", 0);
|
|
27
|
+
__publicField(this, "reactions", []);
|
|
28
|
+
__publicField(this, "reactionDependencies", []);
|
|
29
|
+
__publicField(this, "dirty", Promise.resolve());
|
|
30
|
+
__publicField(this, "dirtyResolve");
|
|
31
|
+
__publicField(this, "timeout");
|
|
32
|
+
__publicField(this, "inBatchReactions");
|
|
33
|
+
__publicField(this, "inFlush");
|
|
34
|
+
__publicField(this, "reactionGlobalKey", []);
|
|
35
|
+
__publicField(this, "reactivesToFlush", /* @__PURE__ */ new Set());
|
|
36
|
+
__publicField(this, "disabled", false);
|
|
37
|
+
__publicField(this, "allowedPairedReactives", /* @__PURE__ */ new WeakSet());
|
|
38
|
+
}
|
|
39
|
+
createSignal(value, measureOfChange = 2) {
|
|
40
|
+
let current;
|
|
41
|
+
const reactionsToRerun = [];
|
|
42
|
+
let pairedReactionsToRun = /* @__PURE__ */ new Set();
|
|
43
|
+
const triggerReactions = () => {
|
|
44
|
+
for (let index = 0; index < reactionsToRerun.length; index++) {
|
|
45
|
+
if (reactionsToRerun[index]) {
|
|
46
|
+
this.triggerReaction(index, measureOfChange, false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
pairedReactionsToRun.forEach(([reactive, index]) => {
|
|
50
|
+
if (reactive.allowedPairedReactives.has(this)) {
|
|
51
|
+
reactive.triggerReaction(index, measureOfChange, true);
|
|
52
|
+
this.reactivesToFlush.add(reactive);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const setter = (value2) => {
|
|
57
|
+
let materializedValue = typeof value2 === "function" ? value2(current) : value2;
|
|
58
|
+
let isModified = materializedValue !== current;
|
|
59
|
+
current = materializedValue;
|
|
60
|
+
if (isModified) {
|
|
61
|
+
triggerReactions();
|
|
62
|
+
}
|
|
63
|
+
return current;
|
|
64
|
+
};
|
|
65
|
+
const resetDependency = (reactionGlobalKey) => {
|
|
66
|
+
reactionsToRerun[reactionGlobalKey[1]] = false;
|
|
67
|
+
};
|
|
68
|
+
const resetPairedDependency = (reactionGlobalKey) => {
|
|
69
|
+
pairedReactionsToRun.delete(reactionGlobalKey);
|
|
70
|
+
};
|
|
71
|
+
const getter = () => {
|
|
72
|
+
const runningReactionsLength = runningReactions.length;
|
|
73
|
+
for (let index = runningReactionsLength - 1; index > -1; index--) {
|
|
74
|
+
const [reactive, reactionIndex] = runningReactions[index];
|
|
75
|
+
if (reactive === this) {
|
|
76
|
+
reactionsToRerun[reactionIndex] = true;
|
|
77
|
+
this.reactionDependencies[reactionIndex].add(resetDependency);
|
|
78
|
+
break;
|
|
79
|
+
} else {
|
|
80
|
+
pairedReactionsToRun.add(runningReactions[index]);
|
|
81
|
+
reactive.reactionDependencies[reactionIndex].add(resetPairedDependency);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
};
|
|
86
|
+
if (typeof value === "function") {
|
|
87
|
+
this.createReaction(() => {
|
|
88
|
+
let newValue = value();
|
|
89
|
+
setter(newValue);
|
|
90
|
+
});
|
|
91
|
+
} else
|
|
92
|
+
setter(value);
|
|
93
|
+
getter[GetterMark] = true;
|
|
94
|
+
setter[SetterMark] = true;
|
|
95
|
+
return [getter, setter];
|
|
96
|
+
}
|
|
97
|
+
triggerReaction(index, measureOfChange, paired) {
|
|
98
|
+
if (!this.inBatchReactions && !paired)
|
|
99
|
+
this.ScheduleAutoBatchRuns();
|
|
100
|
+
this.batchedReactionsToRun[index] = Math.max(
|
|
101
|
+
measureOfChange,
|
|
102
|
+
this.batchedReactionsToRun[index] || 0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
enablePairing(sourceReactive) {
|
|
106
|
+
this.allowedPairedReactives.add(sourceReactive);
|
|
107
|
+
}
|
|
108
|
+
createReaction(func) {
|
|
109
|
+
let reactionIndex = this.reactionIndex++;
|
|
110
|
+
this.reactions[reactionIndex] = func;
|
|
111
|
+
this.reactionGlobalKey[reactionIndex] = [this, reactionIndex];
|
|
112
|
+
this.reactionDependencies[reactionIndex] = /* @__PURE__ */ new Set();
|
|
113
|
+
this.runReaction(
|
|
114
|
+
reactionIndex,
|
|
115
|
+
2
|
|
116
|
+
/* FULL */
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
batchReactions(func) {
|
|
120
|
+
if (this.inBatchReactions || this.inFlush)
|
|
121
|
+
return func();
|
|
122
|
+
this.inBatchReactions = true;
|
|
123
|
+
[this.dirty, this.dirtyResolve] = mkResolvablePromise();
|
|
124
|
+
try {
|
|
125
|
+
return func();
|
|
126
|
+
} finally {
|
|
127
|
+
this.flush();
|
|
128
|
+
this.inBatchReactions = false;
|
|
129
|
+
this.dirtyResolve();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
ScheduleAutoBatchRuns() {
|
|
133
|
+
if (!this.isAutoBatchScheduled) {
|
|
134
|
+
this.isAutoBatchScheduled = true;
|
|
135
|
+
[this.dirty, this.dirtyResolve] = mkResolvablePromise();
|
|
136
|
+
this.timeout = setTimeout(() => {
|
|
137
|
+
this.timeout = void 0;
|
|
138
|
+
this.flush();
|
|
139
|
+
}, 0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
toBeClean() {
|
|
143
|
+
return this.dirty;
|
|
144
|
+
}
|
|
145
|
+
runReaction(reactionIndex, measureOfChange) {
|
|
146
|
+
this.reactionDependencies[reactionIndex].forEach(
|
|
147
|
+
(resetDependency) => resetDependency(this.reactionGlobalKey[reactionIndex])
|
|
148
|
+
);
|
|
149
|
+
this.reactionDependencies[reactionIndex].clear();
|
|
150
|
+
pushRunningReaction(this.reactionGlobalKey[reactionIndex]);
|
|
151
|
+
try {
|
|
152
|
+
this.reactions[reactionIndex](measureOfChange);
|
|
153
|
+
} finally {
|
|
154
|
+
popRunningReaction();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
flush() {
|
|
158
|
+
if (this.inFlush || this.disabled)
|
|
159
|
+
return;
|
|
160
|
+
this.inFlush = true;
|
|
161
|
+
try {
|
|
162
|
+
for (let index = 0; index < this.batchedReactionsToRun.length; index++)
|
|
163
|
+
if (this.batchedReactionsToRun[index])
|
|
164
|
+
this.runReaction(index, this.batchedReactionsToRun[index]);
|
|
165
|
+
if (this.isAutoBatchScheduled) {
|
|
166
|
+
this.isAutoBatchScheduled = false;
|
|
167
|
+
if (this.timeout)
|
|
168
|
+
clearTimeout(this.timeout);
|
|
169
|
+
this.timeout = void 0;
|
|
170
|
+
}
|
|
171
|
+
this.batchedReactionsToRun = [];
|
|
172
|
+
this.dirtyResolve && this.dirtyResolve();
|
|
173
|
+
} finally {
|
|
174
|
+
this.inFlush = false;
|
|
175
|
+
this.reactivesToFlush.forEach((reactive) => {
|
|
176
|
+
if (!reactive.inBatchReactions)
|
|
177
|
+
reactive.flush();
|
|
178
|
+
});
|
|
179
|
+
this.reactivesToFlush.clear();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
enable() {
|
|
183
|
+
this.disabled = false;
|
|
184
|
+
this.flush();
|
|
185
|
+
}
|
|
186
|
+
disable() {
|
|
187
|
+
this.disabled = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function mkResolvablePromise() {
|
|
191
|
+
let resolve;
|
|
192
|
+
let promise = new Promise((res) => resolve = res);
|
|
193
|
+
return [promise, resolve];
|
|
194
|
+
}
|
|
195
|
+
let _mkReactive = (...reactiveNames) => new Reactive();
|
|
196
|
+
function setMkReactive(mkReactive2) {
|
|
197
|
+
_mkReactive = mkReactive2;
|
|
198
|
+
}
|
|
199
|
+
function mkReactive(...reactiveNames) {
|
|
200
|
+
return _mkReactive(...reactiveNames);
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
GetterMark,
|
|
204
|
+
MeasureOfChange,
|
|
205
|
+
Reactive,
|
|
206
|
+
SetterMark,
|
|
207
|
+
mkReactive,
|
|
208
|
+
setMkReactive
|
|
209
|
+
};
|
package/dist/tracing.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Reactive, MeasureOfChange, setMkReactive } from "./index.js";
|
|
2
|
+
class ReactiveTracer {
|
|
3
|
+
constructor(flushToConsole = false) {
|
|
4
|
+
this.flushToConsole = flushToConsole;
|
|
5
|
+
this.log = [];
|
|
6
|
+
this.getStates = [];
|
|
7
|
+
this.setStates = [];
|
|
8
|
+
this.scheduledReactions = [];
|
|
9
|
+
this.inReaction = -1;
|
|
10
|
+
this.batches = [];
|
|
11
|
+
this.reactionLogPosition = [];
|
|
12
|
+
this.ident = "";
|
|
13
|
+
this.settingSignalFromBatch = "";
|
|
14
|
+
}
|
|
15
|
+
logGetState(name) {
|
|
16
|
+
if (this.inReaction > -1)
|
|
17
|
+
this.getStates[this.inReaction].add(name);
|
|
18
|
+
}
|
|
19
|
+
logSetState(name) {
|
|
20
|
+
if (this.inReaction > -1)
|
|
21
|
+
this.setStates[this.inReaction].add(name);
|
|
22
|
+
else {
|
|
23
|
+
this.scheduledReactions[this.inReaction] = /* @__PURE__ */ new Set();
|
|
24
|
+
this.settingSignalFromBatch = name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
logAfterSetState() {
|
|
28
|
+
if (this.inReaction === -1) {
|
|
29
|
+
const scheduledReactions = [...this.scheduledReactions[this.inReaction]].sort().join(",");
|
|
30
|
+
this.doLog(
|
|
31
|
+
`${this.batches.join(", ")} - batch: -> (${this.settingSignalFromBatch}) --> (${scheduledReactions})`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
beforeReaction() {
|
|
36
|
+
this.inReaction++;
|
|
37
|
+
this.getStates[this.inReaction] = /* @__PURE__ */ new Set();
|
|
38
|
+
this.setStates[this.inReaction] = /* @__PURE__ */ new Set();
|
|
39
|
+
this.scheduledReactions[this.inReaction] = /* @__PURE__ */ new Set();
|
|
40
|
+
this.reactionLogPosition.push(this.log.length);
|
|
41
|
+
this.ident += " ";
|
|
42
|
+
}
|
|
43
|
+
completeReaction(name, reactionName) {
|
|
44
|
+
this.ident = this.ident.slice(0, -2);
|
|
45
|
+
const reactionLogPosition = this.reactionLogPosition.pop();
|
|
46
|
+
const signalGetters = [...this.getStates[this.inReaction]].sort().join(",");
|
|
47
|
+
const signalSetters = [...this.setStates[this.inReaction]].sort().join(",");
|
|
48
|
+
const scheduledReactions = [...this.scheduledReactions[this.inReaction]].sort().join(",");
|
|
49
|
+
const logMessage = `${this.ident}${name} - ${reactionName}: (${signalGetters}) -> (${signalSetters}) --> (${scheduledReactions})`;
|
|
50
|
+
this.log.splice(reactionLogPosition, 0, logMessage);
|
|
51
|
+
this.inReaction--;
|
|
52
|
+
if (this.inReaction === -1)
|
|
53
|
+
this.flushLog();
|
|
54
|
+
}
|
|
55
|
+
beforeBatch(name) {
|
|
56
|
+
this.batches.push(name);
|
|
57
|
+
}
|
|
58
|
+
completeBatch() {
|
|
59
|
+
this.batches.pop();
|
|
60
|
+
}
|
|
61
|
+
flush(name) {
|
|
62
|
+
this.doLog(`${name} - flush!!!`);
|
|
63
|
+
this.ident += " ";
|
|
64
|
+
}
|
|
65
|
+
flushEnd(name) {
|
|
66
|
+
this.ident = this.ident.slice(0, -2);
|
|
67
|
+
this.doLog(`${name} - flush end`);
|
|
68
|
+
}
|
|
69
|
+
logToBeClean(name) {
|
|
70
|
+
this.doLog(`${name} - await toBeClean!!!`);
|
|
71
|
+
}
|
|
72
|
+
triggerReaction(name, index, scheduleAutoBatchRuns) {
|
|
73
|
+
this.scheduledReactions[this.inReaction].add(
|
|
74
|
+
`${name} - ${formatReactionName(index)}${scheduleAutoBatchRuns ? " async" : ""}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
createSignal(name, stateName) {
|
|
78
|
+
this.doLog(`${name} - createSignal ${stateName}`);
|
|
79
|
+
}
|
|
80
|
+
doLog(message) {
|
|
81
|
+
this.log.push(`${this.ident}${message}`);
|
|
82
|
+
if (this.inReaction === -1)
|
|
83
|
+
this.flushLog();
|
|
84
|
+
}
|
|
85
|
+
flushLog() {
|
|
86
|
+
if (this.flushToConsole) {
|
|
87
|
+
this.log.forEach((entry) => console.debug(entry));
|
|
88
|
+
this.log = [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const romanNumerals = {
|
|
93
|
+
M: 1e3,
|
|
94
|
+
CM: 900,
|
|
95
|
+
D: 500,
|
|
96
|
+
CD: 400,
|
|
97
|
+
C: 100,
|
|
98
|
+
XC: 90,
|
|
99
|
+
L: 50,
|
|
100
|
+
XL: 40,
|
|
101
|
+
X: 10,
|
|
102
|
+
IX: 9,
|
|
103
|
+
V: 5,
|
|
104
|
+
IV: 4,
|
|
105
|
+
I: 1
|
|
106
|
+
};
|
|
107
|
+
function toRoman(num) {
|
|
108
|
+
let roman = "";
|
|
109
|
+
for (let i in romanNumerals) {
|
|
110
|
+
while (num >= romanNumerals[i]) {
|
|
111
|
+
roman += i;
|
|
112
|
+
num -= romanNumerals[i];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return roman;
|
|
116
|
+
}
|
|
117
|
+
function formatReactionName(num) {
|
|
118
|
+
return toRoman(num + 1);
|
|
119
|
+
}
|
|
120
|
+
class ReactiveWithTracking extends Reactive {
|
|
121
|
+
constructor(name, reactiveTracer) {
|
|
122
|
+
super();
|
|
123
|
+
this.name = name;
|
|
124
|
+
this.reactiveTracer = reactiveTracer;
|
|
125
|
+
this.stateIndex = 1;
|
|
126
|
+
}
|
|
127
|
+
createSignal(value, measureOfChange = MeasureOfChange.FULL) {
|
|
128
|
+
const stateName = this.name + this.stateIndex++;
|
|
129
|
+
this.reactiveTracer.createSignal(this.name, stateName);
|
|
130
|
+
const [getter, setter] = super.createSignal(value, measureOfChange);
|
|
131
|
+
const loggedSetter = (value2) => {
|
|
132
|
+
this.reactiveTracer.logSetState(stateName);
|
|
133
|
+
const ret = setter(value2);
|
|
134
|
+
this.reactiveTracer.logAfterSetState();
|
|
135
|
+
return ret;
|
|
136
|
+
};
|
|
137
|
+
const loggedGetter = () => {
|
|
138
|
+
this.reactiveTracer.logGetState(stateName);
|
|
139
|
+
return getter();
|
|
140
|
+
};
|
|
141
|
+
return [loggedGetter, loggedSetter];
|
|
142
|
+
}
|
|
143
|
+
createReaction(func) {
|
|
144
|
+
const reactionName = formatReactionName(this.reactionIndex);
|
|
145
|
+
super.createReaction((measureOfChange) => {
|
|
146
|
+
this.reactiveTracer.beforeReaction();
|
|
147
|
+
try {
|
|
148
|
+
func(measureOfChange);
|
|
149
|
+
} finally {
|
|
150
|
+
this.reactiveTracer.completeReaction(this.name, reactionName);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
triggerReaction(index, measureOfChange, paired) {
|
|
155
|
+
this.reactiveTracer.triggerReaction(this.name, index, !this.inBatchReactions && !paired);
|
|
156
|
+
super.triggerReaction(index, measureOfChange, paired);
|
|
157
|
+
}
|
|
158
|
+
batchReactions(func) {
|
|
159
|
+
this.reactiveTracer.beforeBatch(this.name);
|
|
160
|
+
try {
|
|
161
|
+
return super.batchReactions(func);
|
|
162
|
+
} finally {
|
|
163
|
+
this.reactiveTracer.completeBatch();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
flush() {
|
|
167
|
+
this.reactiveTracer.flush(this.name);
|
|
168
|
+
super.flush();
|
|
169
|
+
this.reactiveTracer.flushEnd(this.name);
|
|
170
|
+
}
|
|
171
|
+
toBeClean() {
|
|
172
|
+
this.reactiveTracer.logToBeClean(this.name);
|
|
173
|
+
return super.toBeClean();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const globalReactiveTracer = new ReactiveTracer(true);
|
|
177
|
+
let runningNumber = 1;
|
|
178
|
+
function numberToAlphaNumeric(num) {
|
|
179
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
180
|
+
let result = "";
|
|
181
|
+
while (num > 0) {
|
|
182
|
+
num--;
|
|
183
|
+
result = alphabet[num % 26] + result;
|
|
184
|
+
num = Math.floor(num / 26);
|
|
185
|
+
}
|
|
186
|
+
return result || "A";
|
|
187
|
+
}
|
|
188
|
+
setMkReactive((...reactiveNames) => {
|
|
189
|
+
return new ReactiveWithTracking(
|
|
190
|
+
[numberToAlphaNumeric(runningNumber++), ...reactiveNames].join("-"),
|
|
191
|
+
globalReactiveTracer
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
export {
|
|
195
|
+
ReactiveTracer,
|
|
196
|
+
ReactiveWithTracking
|
|
197
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jay-framework/reactive",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./tracing": "./dist/tracing.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"readme.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "npm run build:js && npm run build:types",
|
|
17
|
+
"build:watch": "npm run build:js -- --watch & npm run build:types -- --watch",
|
|
18
|
+
"build:js": "vite build",
|
|
19
|
+
"build:types": "tsup lib/index.ts --dts-only --format esm",
|
|
20
|
+
"build:check-types": "tsc",
|
|
21
|
+
"clean": "rimraf dist",
|
|
22
|
+
"confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@jay-framework/dev-environment": "workspace:^",
|
|
28
|
+
"@types/node": "^20.11.5",
|
|
29
|
+
"rimraf": "^5.0.5",
|
|
30
|
+
"tsup": "^8.0.1",
|
|
31
|
+
"typescript": "^5.3.3",
|
|
32
|
+
"vite": "^5.0.11",
|
|
33
|
+
"vitest": "^1.2.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Jay Reactive Module
|
|
2
|
+
|
|
3
|
+
The Reactive module is a minimal reactive core implementation that handles storing data,
|
|
4
|
+
reacting to data change and detecting if data has actually changed.
|
|
5
|
+
|
|
6
|
+
Reactive will strive to run reactions as a batch and will do so **sync** when using `batchReactions` or
|
|
7
|
+
**async** if `batchReactions` was not used. When there are pending reactions to be run async, `toBeClean`
|
|
8
|
+
can be used to wait for the reactions to run using `await reactive.toBeClean()`.
|
|
9
|
+
|
|
10
|
+
The package one class - the `Reactive` which is a simple reactive core, at which reactions are dependent on signals.
|
|
11
|
+
When a signal is updated, any of the dependent reactions are re-run.
|
|
12
|
+
|
|
13
|
+
The reactions auto track which signals they depend on. On each run of a reaction,
|
|
14
|
+
it will recalculate dependencies to ensure it only depends on signal values that are actually in use.
|
|
15
|
+
A direct impact is that conditions based on signals are supported in reactions, and the reaction rerun will take
|
|
16
|
+
into account the conditions.
|
|
17
|
+
|
|
18
|
+
Reactive can also pair, creating dependencies between multiple Reactive instances. See the section below on Reactive Pairing
|
|
19
|
+
|
|
20
|
+
## Notes:
|
|
21
|
+
|
|
22
|
+
- `Reactive` is intended to be an internal core implementation for state management and not a user facing API.
|
|
23
|
+
- `Reactive` is used by `@jay-framework/component` as state management for components, at which each component has it's own independent
|
|
24
|
+
instance of `Reactive`.
|
|
25
|
+
- `@jay-framework/component` also defines reactive context which is also using an independent `Reactive` instance.
|
|
26
|
+
- one `Reactive` can depend on a signal from another `Reactive` creating `Reactive` pairing discussed below.
|
|
27
|
+
- `@jay-framework/reactive` is inspired by [solid.js](https://www.solidjs.com/) state management (amazing framework, BTW).
|
|
28
|
+
- `Reactive.enable` and `Reactive.disable` are used by `@jay-framework/component` to disable and enable reactive as a component unmounts and mounts.
|
|
29
|
+
|
|
30
|
+
## createSignal
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
type Next<T> = (t: T) => T;
|
|
34
|
+
type Setter<T> = (t: T | Next<T>) => T;
|
|
35
|
+
type Getter<T> = () => T;
|
|
36
|
+
type ValueOrGetter<T> = T | Getter<T>;
|
|
37
|
+
declare function createSignal<T>(
|
|
38
|
+
value: ValueOrGetter<T>,
|
|
39
|
+
measureOfChange: MeasureOfChange = MeasureOfChange.FULL,
|
|
40
|
+
): [get: Getter<T>, set: Setter<T>];
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Creates a signal getter / setter pair such that when setting signal value, any dependent reaction is rerun.
|
|
44
|
+
The reactions run on `setTimeout(...,0)`, or at the end of a batch when using `batchReactions`.
|
|
45
|
+
|
|
46
|
+
The getter always returns the signal value
|
|
47
|
+
The setter accepts a new value or a function to compute the next value, as well as a `MeasureOfChange`.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const [getter, setter] = reactive.createSignal(12);
|
|
51
|
+
|
|
52
|
+
getter(); // returns 12
|
|
53
|
+
setter(13);
|
|
54
|
+
setter((x) => x + 1);
|
|
55
|
+
|
|
56
|
+
const [getter2, setter2] = reactive.createSignal(() => `signal value is ${getter()}`);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### createSignal parameters
|
|
60
|
+
|
|
61
|
+
- `value: ValueOrGetter<T>` - an initial value for the signal, or a getter function to track using `createReaction`.
|
|
62
|
+
- `measureOfChange: MeasureOfChange = MeasureOfChange.FULL` - an indicator of how large a change is signal is considered
|
|
63
|
+
within reactions that depend on this signal (when a reaction is run, it also gets the `max(...measureOfChange)`
|
|
64
|
+
of all signals that have changed and it depends on).
|
|
65
|
+
|
|
66
|
+
### getter
|
|
67
|
+
|
|
68
|
+
the first function returned by `createSignal` is the `getter` function which returns the current value of the signal.
|
|
69
|
+
|
|
70
|
+
### setter
|
|
71
|
+
|
|
72
|
+
The second function returned is `setter` which accepts one parameter - a new value for the signal,
|
|
73
|
+
or a function to update the signal value. Note that a change is defined by strict equality - using the `===` and `!==` operators.
|
|
74
|
+
|
|
75
|
+
## createReaction
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
export type Reaction = (measureOfChange: MeasureOfChange) => void;
|
|
79
|
+
reactive.createReaction(func: Reaction);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Creates a reaction that re-runs when signals it depends on changes.
|
|
83
|
+
It will re-run on `setTimeout(..., 0)`, or at the end of a batch when using `batchReactions`.
|
|
84
|
+
The `Reaction` accepts a `MeasureOfChange` computed as the `max(...measureOfChange)`
|
|
85
|
+
of all signals that have changed and the reaction depends on.
|
|
86
|
+
|
|
87
|
+
The `Reaction` function is running once as part of the call to `createReaction` used to figure out what
|
|
88
|
+
initial dependencies to track. On each run of the `Reaction` function dependencies are recomputed and the
|
|
89
|
+
function will only rerun with relevant dependencies are updated.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
reactive.createReaction(() => {
|
|
93
|
+
console.log(signalGetter());
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Note that only dependencies (signal getters) that are actually in use are set as dependencies.
|
|
98
|
+
In the following case, the reaction will track signals `a` and `b`, but will not track signal `c` (by design).
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const [a, setA] = reactive.createSignal(true);
|
|
102
|
+
const [b, setB] = reactive.createSignal('abc');
|
|
103
|
+
const [c, setC] = reactive.createSignal('def');
|
|
104
|
+
|
|
105
|
+
reactive.createReaction(() => {
|
|
106
|
+
if (a()) b();
|
|
107
|
+
else c();
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Once `a` or `b` update, the reaction will rerun.
|
|
112
|
+
|
|
113
|
+
If `a` is set to false, the reaction will now depend on `a` and `c`.
|
|
114
|
+
|
|
115
|
+
## batchReactions
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
reactive.batchReactions(func: () => void);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Batch reaction enables to update multiple signals while computing reactions only once. It is important for
|
|
122
|
+
performance optimizations, to enable rendering DOM updates once when a component updates multiple signals. It
|
|
123
|
+
is built for the component API to optimize rendering.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
let reactive = new Reactive((reactive) => {
|
|
127
|
+
const [a, setA] = reactive.createSignal(false);
|
|
128
|
+
const [b, setB] = reactive.createSignal('abc');
|
|
129
|
+
const [c, setC] = reactive.createSignal('def');
|
|
130
|
+
reactive.createReaction(() => {
|
|
131
|
+
console.log(a(), b(), c());
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// will print the console log false abc def
|
|
135
|
+
|
|
136
|
+
reactive.batchReactions(() => {
|
|
137
|
+
setA(true);
|
|
138
|
+
setB('abcde');
|
|
139
|
+
setC('fghij');
|
|
140
|
+
});
|
|
141
|
+
// will print the console log true abcde fghij
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## toBeClean
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
reactive.toBeClean(): Promise<void>;
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
returns a promise that is resolved when pending reactions have run. If there are no pending reactions, the promise
|
|
151
|
+
will resolve immediately.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
setA(12);
|
|
155
|
+
setB('Joe');
|
|
156
|
+
// waits for reaction to run
|
|
157
|
+
await reactive.toBeClean();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## flush
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
reactive.flush(): void;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
In the case of not using batch reactions, reactive will auto batch the reactions and run them async.
|
|
167
|
+
`flush` can be used to force the reactions to run sync.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
setA(12);
|
|
171
|
+
setB('Joe');
|
|
172
|
+
// forces reactions to run synchronosly
|
|
173
|
+
reactive.flush();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## enable & disable
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
reactive.enable();
|
|
180
|
+
reactive.disable();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Enables and disables the reactive.
|
|
184
|
+
A Disabled reactive will not run reactions.
|
|
185
|
+
|
|
186
|
+
- When calling enable, the reactive will also flush any pending reactions.
|
|
187
|
+
- Reactive are created, by default, enabled.
|
|
188
|
+
|
|
189
|
+
## MeasureOfChange
|
|
190
|
+
|
|
191
|
+
Measure of Change is an optional value passed when creating signals, which is then used to tune how reactions run.
|
|
192
|
+
The `MeasureOfChange` is defined as an ordered enum, at which case the reaction always gets the max `MeasureOfChange`
|
|
193
|
+
from signals that are updated.
|
|
194
|
+
|
|
195
|
+
It is defined as
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
export enum MeasureOfChange {
|
|
199
|
+
NO_CHANGE,
|
|
200
|
+
PARTIAL,
|
|
201
|
+
FULL,
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
At which
|
|
206
|
+
|
|
207
|
+
- `NO_CHANGE` - allows to update a signal without triggering reactions
|
|
208
|
+
- `PARTIAL` - triggers reactions with the `PARTIAL` measure of change, unless other signals are updated with a higher measure of change
|
|
209
|
+
- `FULL` - triggers reactions with the `FULL` measure of change
|
|
210
|
+
|
|
211
|
+
see the `@jay-framework/component` library, the `createDerivedArray` function for an example use case.
|
|
212
|
+
|
|
213
|
+
## enablePairing
|
|
214
|
+
|
|
215
|
+
Reactive Pairing is useful when an application has multiple reactive instances who need to sync flush between them.
|
|
216
|
+
For instance, with Jay, a context is one reactive and component is another instance of a reactive.
|
|
217
|
+
|
|
218
|
+
Pairing is created explicitly using the `enablePairing` API,
|
|
219
|
+
then by reading a signal value of reactive `A` from a reaction of reactive `B`.
|
|
220
|
+
When paired, once reactive `A` flushes, it will also trigger a flush of reactive `B` after `A` flush completes,
|
|
221
|
+
which will re-run the reaction in `B` that have read a signal value of `A`.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
reactive.enablePairing(anotherReactive);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- `reactive` the reactive from which `anotherReactive` signal values are read. In Jay, a component. The `B` above.
|
|
228
|
+
- `anotherReactive` the reactive from which signal values are read. In Jay, a context. The `A` above.
|
|
229
|
+
|
|
230
|
+
example:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
B.enablePairing(A);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
# Reactive Tracing
|
|
237
|
+
|
|
238
|
+
The reactive library includes the facility to trace how Reactive signals and reactions are running.
|
|
239
|
+
|
|
240
|
+
To enable reactive tracing, import the `@jay-framework/reactive/tracing` module before starting jay.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import '@jay-framework/reactive/tracing';
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Reactive tracing outputs tracing similar to the following:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
// on counter example creation
|
|
250
|
+
A - createSignal A1
|
|
251
|
+
A - createSignal A2
|
|
252
|
+
A - createSignal A3
|
|
253
|
+
A - I: (A3) -> () --> ()
|
|
254
|
+
A - II: () -> () --> ()
|
|
255
|
+
A - flush!!!
|
|
256
|
+
A - flush end
|
|
257
|
+
A - batch: -> (A1) --> ()
|
|
258
|
+
A - flush!!!
|
|
259
|
+
A - flush end
|
|
260
|
+
A - batch: -> (A3) --> (A - I)
|
|
261
|
+
A - flush!!!
|
|
262
|
+
A - I: (A3) -> () --> ()
|
|
263
|
+
A - flush end
|
|
264
|
+
|
|
265
|
+
// on counter click on a button
|
|
266
|
+
A - flush!!!
|
|
267
|
+
A - flush end
|
|
268
|
+
A - batch: -> (A3) --> (A - I)
|
|
269
|
+
A - flush!!!
|
|
270
|
+
A - I: (A3) -> () --> ()
|
|
271
|
+
A - flush end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The trace should be read as:
|
|
275
|
+
|
|
276
|
+
- `A` - each Reactive gets a letter as a name, like `A`, `B`, `C`, etc.
|
|
277
|
+
- `A - createSignal A1` - creating the first signal.
|
|
278
|
+
- Signals are named after the reactive name + a serial number, like `A1`, `A2`, `A3`, `B1`, etc.
|
|
279
|
+
- `A - I: (A3) -> () --> ()` - running a reaction, including on reaction creation.
|
|
280
|
+
- Reactions are named after the reaction name + serial roman number, like `A - I`, `A - II`, `A - III`, etc.
|
|
281
|
+
- The first `()` are the signals read (using getters) in the reaction.
|
|
282
|
+
- The second `()` are signals written (using setters) in the reaction.
|
|
283
|
+
- The third `()` are reactions to run once this reaction is running.
|
|
284
|
+
- `A - batch: -> (A3) --> (A - I)` - a batch reaction setting the `A3` signal and scheduling the `A - I` reaction to run.
|