-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
239 lines (229 loc) · 7.59 KB
/
index.js
File metadata and controls
239 lines (229 loc) · 7.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
console.info("Loading kobs")
// const log = console.log
const log = () => {}
const last = require("lodash/last")
const difference = function difference(setA, setB) {
setB.forEach((elm) => setA.delete(elm))
return setA
}
let autorunsNeedingRun = new Set()
let maybeUnobserved = new Set()
let currentlyRunningStack = []
let transactionLevel = 0
exports.observable = (arg, name) => {
if (typeof arg === "function") {
const obs = new exports.Computed(arg, null, null, name)
return () => obs.get()
} else {
const obs = new exports.Obs(arg, null, null, name)
return function (arg) {
return arguments.length ? obs.set(arg) : obs.get()
}
}
}
exports.Obs = function (initValue, onUnobserved, onObserved, name) {
this.name = name
this.value = initValue
this.observers = new Set()
this.onUnobserved = onUnobserved
}
Object.assign(exports.Obs.prototype, {
set: function (newValue) {
//TODO: mettre en commun avec le code de "transaction"
transactionLevel++
this.value = newValue
log("source changed", this.name)
this.observers.forEach((observer) => observer.markStale())
transactionLevel--
if (transactionLevel === 0) {
autorunsNeedingRun.forEach((autorun) =>
autorun.compute(autorun.unobserve)
)
autorunsNeedingRun = new Set()
maybeUnobserved.forEach((obs) => obs.notifyIfUnobserved())
maybeUnobserved = new Set()
}
},
get: function () {
const currentlyRunning = last(currentlyRunningStack)
if (currentlyRunning && !currentlyRunning.deps.has(this)) {
currentlyRunning.deps.add(this)
this.observers.add(currentlyRunning)
log("adding deps from source", this.name, "to", currentlyRunning.name)
}
return this.value
},
removeObserver: function (observer) {
log("removing deps from source", this.name, "to", observer.name)
this.observers.delete(observer)
log("observers of", this.name, this.observers.size, this.observers)
if (this.observers.size === 0) maybeUnobserved.add(this)
},
notifyIfUnobserved: function () {
// after running all autorun, if this.observers is still empty, we can notify that we are unobserved
if (this.onUnobserved && this.observers.size === 0) {
log("notify unobserved", this.name)
this.onUnobserved()
}
},
})
exports.Computed = function (fn, onUnobserved, onObserved, name) {
this.name = name
this.stale = true
this.observers = new Set()
this.deps = new Set()
this.fn = fn
}
Object.assign(exports.Computed.prototype, {
markStale: function () {
if (this.stale) return // pas besoin de recommencer si on a déjà été marqué stale
this.stale = true
log(this.name, "computed marked stale")
this.observers.forEach((observer) => observer.markStale())
},
compute: function () {
log("computing", this.name)
this.oldDeps = this.deps
this.deps = new Set()
currentlyRunningStack.push(this)
this.value = this.fn()
currentlyRunningStack.pop()
this.stale = false
difference(this.oldDeps, this.deps).forEach((dep) =>
dep.removeObserver(this)
)
},
removeObserver: function (observer) {
log("removing deps from computed", this.name, "to", observer.name)
this.observers.delete(observer)
if (this.observers.size === 0) {
this.deps.forEach((dep) => dep.removeObserver(this))
this.stale = true // déclenche le recomputing lors de la prochaine observation
}
},
get: function () {
const currentlyRunning = last(currentlyRunningStack)
if (currentlyRunning && !currentlyRunning.deps.has(this)) {
currentlyRunning.deps.add(this)
this.observers.add(currentlyRunning)
log("adding deps from computed", this.name, "to", currentlyRunning.name)
}
if (this.stale) this.compute()
return this.value
},
})
const autorun = (exports.autorun = (fn, name, after) => {
const me = {
name,
deps: new Set(),
markStale: () => {
log("autorun marked stale", name)
autorunsNeedingRun.add(me)
},
}
me.unobserve = () => {
log("cancelling", name)
if (last(currentlyRunningStack) === me) {
me.canceledDuringRun = true
log("canceling during run", name)
} else {
me.deps.forEach((dep) => dep.removeObserver(me))
autorunsNeedingRun.delete(me)
me.deps = new Set()
maybeUnobserved.forEach((obs) => obs.notifyIfUnobserved())
maybeUnobserved = new Set()
log("cancelled", name)
}
}
me.compute = (cancel) => {
log("computing autorun", name)
me.oldDeps = me.deps
me.deps = new Set()
currentlyRunningStack.push(me)
fn(cancel)
currentlyRunningStack.pop()
if (me.canceledDuringRun) {
// si on a été annulé en cours de run, il faut vider la liste des dépendances qui se sont auto enregistrées
me.deps.forEach((dep) => dep.removeObserver(me))
autorunsNeedingRun.delete(me)
me.deps = new Set()
me.canceledDuringRun = false
log("autorun cancelled during run", name)
}
difference(me.oldDeps, me.deps).forEach((dep) => dep.removeObserver(me))
if (after) after() // éxécuté en synchrone mais sans tracking des dépendances
}
me.compute(me.unobserve)
return me.unobserve
})
exports.transaction = (fn) => {
transactionLevel++
fn()
transactionLevel--
if (transactionLevel === 0) {
autorunsNeedingRun.forEach((autorun) => autorun.compute(autorun.unobserve))
autorunsNeedingRun = new Set()
maybeUnobserved.forEach((obs) => obs.notifyIfUnobserved())
maybeUnobserved = new Set()
}
}
// permet d'éxécuter une fonction en continu dès que la condition est vraie
// si la fonction retourne un promise, la prochaine itération attend qu'il soit terminé (résolu ou rejeté)
exports.repeatWhen = (canRun, cb, name) => {
let cancel
let repeat = true
const autorunFn = (internalCancel) => {
const canRunValue = canRun()
if (!canRunValue) return
log("repeatWhen condition truthy", { name, canRunValue })
internalCancel() // on se désabonne avant d'exécuter le callback pour être sûr de ne déclencher qu'un run à la fois
// peut-être pas très logique de renvoyer la valeur du canRun dans le cb mais c'est pratique quand on veut exécuter le cb avec le résultat (truthy) du canRun
// on execute le cb en asynchrone au cas où il ferait une mutation pour ne pas déclencher en boucle
Promise.resolve()
.then(() => cb(canRunValue))
.then(() => {
if (!repeat) return log("repeatWhen canceled", name)
cancel = autorun(autorunFn, "repeatWhenAgain") // puis on se réabonne après le run (en asynchrone), pour déclencher le suivant
})
}
cancel = autorun(autorunFn, "repeatWhenInit")
return () => {
cancel && cancel()
repeat = false
}
}
exports.observeSync = function (obs, cb) {
let oldVal, newVal
return autorun(
() => {
newVal = obs()
},
null,
() => {
cb(newVal, oldVal)
oldVal = newVal
}
)
}
// asynchronous version of observe
exports.observe = function (obs, cb) {
return () =>
autorun(function () {
const v = obs()
setTimeout(() => cb(v))
})
}
// persiste la valeur dans le local storage
// initValue n'est utilisée que si l'observbale n'a jamais été persisté
exports.persistedObservable = function (name, initValue) {
const json = window.localStorage.getItem(name)
const value = json ? JSON.parse(json) : initValue
const obs = new exports.Obs(value, null, null, name)
const setAndPersist = (newValue) => {
window.localStorage.setItem(name, JSON.stringify(newValue))
obs.set(newValue)
}
return function (arg) {
return arguments.length ? setAndPersist(arg) : obs.get()
}
}