-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathloop.tsx
456 lines (391 loc) · 11.9 KB
/
loop.tsx
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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
import * as React from 'react'
const { useState, useEffect, useContext } = React
import { ReactComponentLike } from 'prop-types'
import createEvent, { Event, Emitter } from './event'
import { useMemo } from 'react'
export const Context = React.createContext<CellContext>(null)
export class Pattern {
constructor(public readonly evaluator, public readonly props) {}
get key(): string {
const props = this.props || {}
const propStr = Object.entries(props).map(
([k, v]) => tag(k) + ':' + tag(v)
).join(', ')
const value = `${tag(this.evaluator)}(${propStr})`
Object.defineProperty(this, 'key', { value })
return value
}
withProps(newProps: any): Pattern {
return new Pattern(this.evaluator, {...this.props, ...newProps})
}
}
export function withProps<T>(pattern: T, newProps: any) {
return (pattern as any).withProps(newProps) as T
}
export function Seed<T=any>(evaluator: Evaluator, props: any): T {
return new Pattern(evaluator, props) as any
}
export function ReadObject<T extends object=object>(object: T, cell?: Cell): T {
if (!cell) return Seed(ReadObject, object)
const out: T = {} as T
let ready = true
const keys = Object.keys(object)
let i = keys.length; while (i --> 0) {
const k = keys[i]
out[k] = cell.read(object[k])
const c = cell.context(object[k])
if (!c.isConstant && c.lastEvaluatedAt === -1) {
ready = false
}
}
if (!ready) return null
return out
}
interface CellContext {
(pattern: any, evaluator?: Evaluator): Cell
onDidEvaluate: Event<Set<Cell>>
invalidate(cell: Cell): void
invalidateAll(cells?: Iterable<Cell>): void
run(): void
readonly cells: Map<string, Cell>
readonly tick: number
queueForDeath(cell: Cell): void
}
export default function Run({
loop, children
}: { loop: CellContext, children?: any }) {
return <Context.Provider value={loop}>
{children}
</Context.Provider>
}
type EvalProps = {
id?: string
children: Evaluator
}
const isKey = (key: any) => key instanceof Pattern || typeof key === 'string'
let nextEvalId = 0
export function Eval(props: EvalProps) {
const { children } = props
const id = useMemo(() => props.id || `anonymous/${nextEvalId++}`, [props.id])
const loop = useContext(Context)
const cell = loop(id, children)
if (cell.evaluator !== children) {
loop(id, children).evaluator = children
loop(id, children).invalidate()
}
return null
}
type Cells = Map<string | symbol, Cell>
export function createLoop(): CellContext {
const cells: Cells = new Map
const dirty = new Set<Cell>()
let didEvaluate: Emitter<Set<Cell>> | null = null
const onDidEvaluate = createEvent(emit => { didEvaluate = emit })
const dying: { cell: Cell, tick: number, }[] = []
const get: any = (pattern: any, evaluator?: Evaluator) => {
const key = tag(pattern)
if (!cells.has(key)) {
let cell: Cell
if (isKey(pattern)) {
cell = new Cell(get, pattern, evaluator)
cells.set(key, cell)
invalidate(cell)
} else {
cell = new Cell(get, pattern, NilEvaluator)
cell.value = pattern
cell.isConstant = true
cells.set(key, cell)
}
return cell
}
return cells.get(key)
}
get.tick = 0
function invalidate(cell: Cell) {
dirty.add(cell)
}
function invalidateAll(cellsToInvalidate: Iterable<Cell> = cells.values()) {
for (const c of cellsToInvalidate) dirty.add(c)
}
function queueForDeath(cell: Cell) {
dying.push({ cell, tick: get.tick })
}
function run(now=performance.now()) {
++get.tick;
const deferred = new Set<Cell>()
while (dirty.size) {
const cells = new Set<Cell>(dirty.values())
// console.log('%c Beginning evaluation of %s cells', 'color: red', cells.size)
if (!cells.size) return
dirty.clear()
try {
for (const cell of cells) {
if (cell.lastEvaluatedAt === now) {
deferred.add(cell)
continue
}
cell.lastEvaluatedAt = now
++cell.evaluationCount
currentCell = cell
cell.evaluate()
currentCell = null
}
} catch (error) {
console.error(new EvaluationError(error, currentCell))
console.error(error)
currentCell = null
}
// console.log('%c did evaluate %s cells', 'color: red', cells.size)
didEvaluate(cells)
}
if (deferred.size) {
// console.log('Deferred processing of', deferred.size, 'cells')
deferred.forEach(cell => dirty.add(cell))
}
const killLine = get.tick - 10
while (dying.length && dying[0].tick < killLine) {
const { cell } = dying.shift()
if (Object.keys(cell.outputs).length > 0) continue
if (cell.wasForgottenAt > killLine) continue
cell.kill()
cells.delete(cell.key)
dirty.delete(cell)
}
}
return Object.assign(get, {
onDidEvaluate,
cells, dirty, invalidate, invalidateAll, run,
queueForDeath
})
}
class EvaluationError extends Error {
constructor(public error: Error, public cell: Cell) {
super(`Evaluation error: ${error.message} in cell ${String(cell.key)}`)
}
}
export const useRead = (input: React.ReactElement) => {
const $ = useContext(Context)
const [value, setValue] = useState($ && $(input))
useEffect(() => {
const cell = $ && $(input)
return $ && $.onDidEvaluate(changes =>
changes.has(cell) && setValue(cell))
}, [input])
return value
}
type Evaluator = (inputs: any, cell: Cell) => any
type Disposer<T> = (value: T) => void
type Writer<T> = (value: T) => void
type Effector<T> = (write: Writer<T>, current: T) => Disposer<T> | void
class Effect<T=any> {
public value: T
public _dispose: Disposer<T> | void = null
public _deps: any[] = null
constructor(
public key: string,
public cell: Cell
) {}
write = (value: T) => {
this.value = value
this.cell.invalidate()
}
update(effector: Effector<T>, deps?: any[]) {
if (this.needsUpdate(deps)) {
this.dispose()
this._dispose = effector(this.write, this.value)
}
this._deps = deps
}
needsUpdate(params?: any[]) {
const { _deps } = this
if (!params) return true
if (params && !_deps) return true
if (_deps.length !== params.length) return true
let i = params.length; while (i --> 0) {
if (_deps[i] !== params[i]) return true
}
return false
}
dispose() {
if (this._dispose) {
this._dispose(this.value)
this._dispose = null
}
}
}
export const NilEvaluator = (_args, cell) => cell.value
const getEvaluator = (pattern: any): Evaluator =>
(pattern && pattern.evaluator) || NilEvaluator
let currentCell = null
export const $ = (pattern: any) => currentCell.read(pattern)
export const $Child = (pattern: any) => currentCell.readChild(pattern)
export class Cell {
constructor(public readonly context: CellContext,
public readonly pattern: Pattern,
public evaluator: Evaluator = getEvaluator(pattern)) { }
public readonly key: string = tag(this.pattern)
public value: any = null
public effects: { [key: string]: Effect } = {}
public lastEvaluatedAt = -1
public evaluationCount = 0
public wasForgottenAt = Infinity
public isConstant: boolean = false
public addOutput(cell: Cell) {
this.outputs[cell.key] = cell
this.wasForgottenAt = Infinity
}
public removeOutput(cell: Cell) {
delete this.outputs[cell.key]
if (Object.keys(this.outputs).length === 0) {
this.wasForgottenAt = this.context.tick
this.context.queueForDeath(this)
}
}
public outputs: { [key: string]: Cell } = {}
public inputs: Cell[] = []
public read<T=any>(pattern: any): T {
return this.get(pattern).value
}
public readChild(pattern: any): any {
return this.child(pattern).value
}
public get(pattern: any): Cell {
const target = this.context(pattern)
target.addOutput(this)
this.inputs.push(target)
return target
}
public child(pattern: any): Cell {
if (pattern instanceof Pattern) {
return this.get(pattern.withProps({ __parentKey: this.key }))
}
console.error('cell.child called on non-pattern at cell', this.key)
return this.get(pattern)
}
public effect<T>(key: string, effector: Effector<T>, deps?: any[]): T {
const { effects } = this
const existing = effects[key]
if (!existing) {
effects[key] = new Effect(key, this)
}
const effect = effects[key]
effect.update(effector, deps)
return effect.value
}
public invalidate() {
this.context.invalidate(this)
}
public write = (value: any) => {
this.value = value
this.context.invalidateAll(Object.values(this.outputs))
}
public evaluate() {
this.inputs.forEach(input => input.removeOutput(this))
this.inputs.splice(0, this.inputs.length)
this.value = this.evaluator(this.pattern.props, this)
this.context.invalidateAll(Object.values(this.outputs))
}
public isDead: boolean = false
public kill() {
// console.log('Killing cell', this.key, 'at tick', this.context.tick, ' wasForgottenAt=', this.wasForgottenAt)
this.isDead = true
Object.values(this.effects)
.forEach(effect => effect.dispose())
this.inputs.forEach(i => i.removeOutput(this))
this.effects = {}
}
}
console.log('Context:', Context)
export const isContext = (c: any): c is React.Context<any> =>
c.$$typeof === (Context as any).$$typeof
const asKey = (key: any) =>
typeof key === 'string'
? key
:
key instanceof Pattern
?
key.key
:
asKeyPart(key)
const tagMap = new WeakMap<any, string>()
export const tag = (key: any): string => {
if (typeof key === 'string') return JSON.stringify(key)
if (typeof key === 'number' || typeof key === 'boolean') return String(key)
if (key instanceof Pattern) return key.key
if (key === null) return 'null'
if (key === undefined) return 'undefined'
if (typeof key === 'symbol') {
if (!symMap.has(key))
symMap.set(key, `[${nextId++}/${String(key)}]`)
return symMap.get(key)
}
if (tagMap.has(key)) return tagMap.get(key)
const keyString =
Object.getPrototypeOf(key) === Object.prototype
? serializePlainObject(key)
:
Array.isArray(key)
? serializeArray(key)
:
`<${nextId++}/${typeof key}(${key.displayName || key.name || (key.constructor && key.constructor.name)})>`
tagMap.set(key, keyString)
return keyString
}
function serializePlainObject(o: object) {
const entries = Object.entries(o).map(
([k, v]) => tag(k) + ':' + tag(v)
).join(', ')
return `{${entries}}`
}
function serializeArray(a: any[]) {
return `[${a.map(tag).join(', ')}]`
}
const asKeyPart = (part: any) =>
typeof part === 'symbol' || typeof part === 'function'
? repr(part)
:
isContext(part)
? repr(part, 'Context')
:
React.isValidElement(part)
? repr((part.type as any).evaluator || part.type) + asKey(part.props)
:
(part && typeof part === 'object')
? '(' +
Object.keys(part).map(
k => `${JSON.stringify(k)}: ${asKeyPart(part[k])}`
).join(', ') +
')'
:
JSON.stringify(part)
const REPR = Symbol('Cached JSON representation of constant values')
const symMap = new Map<symbol, string>()
let nextId = 0
const repr = (key: any, kind='Function') => {
if (typeof key === 'string') return JSON.stringify(key)
if (typeof key === 'symbol') {
if (!symMap.has(key))
symMap.set(key, `[${nextId++}/${String(key)}]`)
return symMap.get(key)
}
if (key[REPR]) return key[REPR]
return key[REPR] = `[${nextId++}/${kind}(${key.displayName || key.name})]`
}
const toJson = (val: any) =>
React.isValidElement(val)
? {
type: toTypeJson(val.type),
props: toPropsJson(val.props)
}
: val
const toTypeJson = (type: string | ReactComponentLike) =>
typeof type === 'function'
? (type as any).displayName || type.name
: type
const toPropsJson = (props: any) => {
const json = {}
Object.keys(props).forEach(key => {
json[key] = toJson(props[key])
})
return json
}