-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathobs.mjs
380 lines (305 loc) · 9.56 KB
/
obs.mjs
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import * as l from './lang.mjs'
import * as o from './obj.mjs'
export function deinit(val) {if (isDe(val)) val.deinit()}
// TODO move to `lang.mjs`.
export function isDe(val) {return l.isComp(val) && l.hasMeth(val, `deinit`)}
export function reqDe(val) {return l.req(val, isDe)}
export function isObs(val) {return isDe(val) && isTrig(val) && l.hasMeth(val, `sub`) && l.hasMeth(val, `unsub`)}
export function reqObs(val) {return l.req(val, isObs)}
export function isTrig(val) {return l.isComp(val) && l.hasMeth(val, `trig`)}
export function reqTrig(val) {return l.req(val, isTrig)}
export function isSub(val) {return l.isFun(val) || isTrig(val)}
export function reqSub(val) {return l.req(val, isSub)}
export function isSubber(val) {return l.isFun(val) || (l.isComp(val) && l.hasMeth(val, `subTo`))}
export function reqSubber(val) {return l.req(val, isSubber)}
export function isRunTrig(val) {return l.isComp(val) && l.hasMeth(val, `run`) && isTrig(val)}
export function reqRunTrig(val) {return l.req(val, isRunTrig)}
export function ph(val) {return l.isComp(val) && keyPh in val ? val[keyPh] : undefined}
export function self(val) {return l.isComp(val) && keySelf in val ? val[keySelf] : val}
export const keyPh = Symbol.for(`ph`)
export const keySelf = Symbol.for(`self`)
export function de(val) {return new Proxy(val, DeinitPh.main)}
export function obs(val) {return new Proxy(val, new ObsPh())}
export function deObs(val) {return new Proxy(val, new DeObsPh())}
export class StaticProxied extends l.Emp {
constructor() {
super()
return new Proxy(this, this.ph)
}
}
export class Proxied extends l.Emp {
constructor() {
super()
return new Proxy(this, new this.Ph())
}
}
export class De extends StaticProxied {get ph() {return DeinitPh.main}}
export class Obs extends Proxied {get Ph() {return ObsPh}}
export class DeObs extends Proxied {get Ph() {return DeObsPh}}
export const dyn = new class Dyn extends o.Dyn {
sub(obs) {
const val = this.$
if (l.isFun(val)) val(obs)
else if (isSubber(val)) val.subTo(obs)
}
inert(fun, ...val) {
const prev = this.swap()
try {return fun(...val)}
finally {this.swap(prev)}
}
}()
/*
Extremely simple scheduler for our observables. Provides reentrant pause/resume
and batch flushing. Note that even in the "unpaused" state, the scheduler
doesn't immediately run its entries. Our observables simply bypass it when
unpaused. Also note that this is fully synchronous. Compare the timed scheduler
in `sched.mjs`.
*/
export class Sched extends o.MixMain(Set) {
constructor(val) {super(val).p = 0}
isPaused() {return this.p > 0}
pause() {return this.p++, this}
resume() {
if (!this.p) return this
this.p--
if (!this.p) this.run()
return this
}
run() {
for (const val of this) {
this.delete(val)
val.trig()
}
return this
}
paused(fun, ...val) {
this.pause()
try {return fun(...val)}
finally {this.resume()}
}
/*
Caution: this doesn't reset the `.p` counter because non-buggy callers must
always use try/finally for pause/unpause. If some code is calling `.deinit`
while the scheduler is paused, it should unpause the scheduler afterwards.
Resetting the counter here would be a surprise.
*/
deinit() {this.clear()}
}
/*
Extremely simple implementation of an observable in a "traditional" sense.
Name short for "imperative observable". Maintains and triggers subscribers.
Satisfies the `isObs` interface. All functionality is imperative, not automatic.
Not to be confused with `Obs`, which is an "automatically" observable object
wrapped into a proxy that secretly uses `ImpObs`.
Implicit observation and automatic triggers are provided by other classes using
this as an inner component. See `Rec` and `ObsPh`.
*/
export class ImpObs extends Set {
sub(val) {this.add(reqSub(val))}
unsub(val) {this.delete(val)}
trig() {
const sch = Sched.main
if (sch.isPaused()) sch.add(this)
else for (const val of this) subTrig(val)
}
deinit() {for (const val of this) this.unsub(val)}
}
/*
Name is short for "reactive" or "recurring", because it's both. Base class for
implementing automatic subscriptions. Invoking `.run` sets up context via `dyn`
and calls `.onRun`. During the call, observables may find the `Rec` instance in
`dyn` and register themselves for future triggers. The link is two-way;
observables must refer to `Rec` to trigger it, and `Rec` must refer to
observables to unsubscribe when deinited. Rerunning `.run` clears previous
observables.
This is half of our "invisible magic" for automatic subscriptions. The other
half is proxy handlers such as `ObsPh`, which trap property access such as
`someObs.someField` and secretly use `dyn` to find the current "subber" such as
`Rec` and establish subscriptions.
`Rec` itself has a nop run. See subclasses.
*/
export class Rec extends Set {
constructor() {
super()
this.new = new Set()
this.act = false
}
onRun() {}
/*
Language observation. The `try` pyramid demonstrates that `try`/`finally` is
inferior to `defer` as seen in Go and Swift, which would simplify our code
to the following:
const subber = dyn.swap(this)
defer dyn.swap(subber)
this.act = true
defer this.act = false
this.new.clear()
defer this.delOld()
sch.pause()
defer sch.resume()
return this.onRun()
*/
run() {
if (this.act) throw Error(`unexpected overlapping rec.run`)
const sch = Sched.main
const subber = dyn.swap(this)
try {
this.act = true
try {
this.new.clear()
try {
sch.pause()
try {return this.onRun()}
finally {sch.resume()}
}
finally {this.delOld()}
}
finally {this.act = false}
}
finally {dyn.swap(subber)}
}
trig() {}
subTo(obs) {
reqObs(obs)
if (this.new.has(obs)) return
this.new.add(obs)
this.add(obs)
obs.sub(this)
}
del(obs) {this.delete(obs), obs.unsub(this)}
delOld() {for (const val of this) if (!this.new.has(val)) this.del(val)}
deinit() {for (const val of this) this.del(val)}
}
export class Moebius extends Rec {
constructor(ref) {super().ref = reqRunTrig(ref)}
onRun() {return this.ref.run()}
trig() {if (!this.act) this.ref.trig()}
}
export class Loop extends Rec {
constructor(ref) {super().ref = reqSub(ref)}
onRun() {subTrig(this.ref)}
trig() {if (!this.act) this.run()}
}
// Short for "proxy handler". Base for other handlers.
export class Ph extends l.Emp {
/*
Hack/workaround for Chrome. At the time of writing, in recent versions of
Chrome, the engine invokes only "own" methods of proxy handlers, ignoring
methods on the prototype. TODO remove if they fix this.
*/
constructor() {
super()
/* eslint-disable no-self-assign */
this.has = this.has
this.get = this.get
this.set = this.set
this.deleteProperty = this.deleteProperty
/* eslint-enable no-self-assign */
}
/* Standard traps */
has(tar, key) {
return (
key === keyPh ||
key === keySelf ||
key === `deinit` ||
key in tar
)
}
get(tar, key) {
if (key === keyPh) return this
if (key === keySelf) return tar
if (key === `deinit`) return this.proxyDeinit
return this.getIn(tar, key)
}
set(tar, key, val) {
this.didSet(tar, key, val)
return true
}
deleteProperty(tar, key) {
this.didDel(tar, key)
return true
}
/* Extensions */
// Allows accidental `ph(ph(val))` to work.
get [keyPh]() {return this}
getIn(tar, key) {return tar[key]}
didSet(tar, key, val) {
const had = hasPub(tar, key)
const prev = tar[key]
tar[key] = val
if (l.eq(prev, val)) return false
if (had) this.drop(prev)
return true
}
didDel(ref, key) {
if (!l.hasOwn(ref, key)) return false
const had = hasPub(ref, key)
const val = ref[key]
delete ref[key]
if (had) this.drop(val)
return true
}
drop() {}
/*
This method is returned by the "get" trap and invoked on the proxy, not the
proxy handler. It assumes that `this` is the proxy. Placed on the handler's
prototype to make it possible to override in subclasses. The base
implementation simply tries to invoke the same method on the target.
*/
proxyDeinit() {deinit(self(this))}
}
export class DeinitPh extends o.MixMain(Ph) {
drop(val) {deinit(val)}
proxyDeinit() {
const val = self(this)
deinitAll(val)
deinit(val)
}
}
export class ObsPh extends Ph {
constructor() {super().obs = new this.ImpObs()}
set(tar, key, val) {
if (this.didSet(tar, key, val)) this.obs.trig()
return true
}
deleteProperty(tar, key) {
if (this.didDel(tar, key)) this.obs.trig()
return true
}
getIn(tar, key) {
if (!hasPriv(tar, key)) dyn.sub(this.obs)
return tar[key]
}
/*
See comments on `Ph.prototype.proxyDeinit`. "this" is the proxy.
`ph(this)` gets the `ObsPh` instance to deinit the observable.
`self(this)` gets the target to deinit it, if appropriate.
*/
proxyDeinit() {
ph(this).deinit()
deinit(self(this))
}
deinit() {this.obs.deinit()}
get ImpObs() {return ImpObs}
}
export class DeObsPh extends ObsPh {
drop(val) {DeinitPh.prototype.drop.call(this, val)}
proxyDeinit() {
ph(this).deinit()
DeinitPh.prototype.proxyDeinit.call(this)
}
}
/* Internal */
export function deinitAll(val) {
for (const key of l.structKeys(val)) deinit(val[key])
}
function subTrig(val) {
if (l.isFun(val)) val()
else val.trig()
}
function hasPriv(tar, key) {
return l.isStr(key) && !l.hasOwnEnum(tar, key) && key in tar
}
function hasPub(tar, key) {
return l.isStr(key) && l.hasOwnEnum(tar, key)
}