idlize

Форк
0
914 строк · 34.7 Кб
1
/*
2
 * Copyright (c) 2022-2024 Huawei Device Co., Ltd.
3
 * Licensed under the Apache License, Version 2.0 (the "License");
4
 * you may not use this file except in compliance with the License.
5
 * You may obtain a copy of the License at
6
 *
7
 * http://www.apache.org/licenses/LICENSE-2.0
8
 *
9
 * Unless required by applicable law or agreed to in writing, software
10
 * distributed under the License is distributed on an "AS IS" BASIS,
11
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
 * See the License for the specific language governing permissions and
13
 * limitations under the License.
14
 */
15

16
import { KoalaProfiler, KoalaCallsiteKey, KoalaCallsiteKeys } from "@koalaui/common"
17
import { Array_from_set, className, int32, refEqual, uint32 } from "@koalaui/compat"
18
import { Dependencies, Dependency } from "./Dependency"
19
import { Disposable, disposeContent, disposeContentBackward } from "./Disposable"
20
import { Observable, ObservableHandler } from "./Observable"
21
import { IncrementalNode } from "../tree/IncrementalNode"
22
import { ReadonlyTreeNode } from "../tree/ReadonlyTreeNode"
23

24
export const CONTEXT_ROOT_SCOPE = "ohos.koala.context.root.scope"
25
export const CONTEXT_ROOT_NODE = "ohos.koala.context.root.node"
26

27
/**
28
 * Compares two different values and returns true
29
 * if a corresponding state (or parameter) should not be modified.
30
 */
31
export type Equivalent<Value> = (oldV: Value, newV: Value) => boolean
32

33
/**
34
 * Create new instance of state manager.
35
 * @returns an instance of the state manager
36
 */
37
export function createStateManager(): StateManager {
38
    return new StateManagerImpl()
39
}
40

41
/**
42
 * State manager, core of incremental runtime engine.
43
 *
44
 * Internal interface of state manager, please do not use directly in
45
 * applications.
46
 */
47
export interface StateManager extends StateContext {
48
    readonly updateNeeded: boolean
49
    updateSnapshot(): uint32
50
    updatableNode<Node extends IncrementalNode>(node: Node, update: (context: StateContext) => void, cleanup?: () => void): ComputableState<Node>
51
    scheduleCallback(callback: () => void): void
52
    callCallbacks(): void
53
    frozen: boolean
54
    reset(): void
55
}
56

57
/**
58
 * Individual state, wrapping a value of type `Value`.
59
 */
60
export interface State<Value> {
61
    /**
62
     * If state was modified since last UI computations.
63
     */
64
    readonly modified: boolean
65
    /**
66
     * Current value of the state.
67
     * State value doesn't change during memo code execution.
68
     */
69
    readonly value: Value
70
}
71

72
/**
73
 * Individual mutable state, wrapping a value of type `Value`.
74
 */
75
export interface MutableState<Value> extends Disposable, State<Value> {
76
    /**
77
     * Current value of the state as a mutable value.
78
     * You should not change state value from a memo code.
79
     * State value doesn't change during memo code execution.
80
     * In the event handlers and other non-memo code
81
     * a changed value is immediately visible.
82
     */
83
    value: Value
84
}
85

86
/**
87
 * Individual computable state that provides recomputable value of type `Value`.
88
 */
89
export interface ComputableState<Value> extends Disposable, State<Value> {
90
    /**
91
     * If value will be recomputed on access.
92
     */
93
    readonly recomputeNeeded: boolean
94
}
95

96
/**
97
 * Context of a state, keeping track of changes in the given scope.
98
 *
99
 * Internal interface of state manager, please do not use directly in
100
 * applications.
101
 */
102
export interface StateContext {
103
    readonly node: IncrementalNode | undefined // defined for all scopes within the scope that creates a node
104
    attach<Node extends IncrementalNode>(id: KoalaCallsiteKey, create: () => Node, update: () => void, cleanup?: () => void): void
105
    compute<Value>(id: KoalaCallsiteKey, compute: () => Value, cleanup?: (value: Value | undefined) => void, once?: boolean): Value
106
    computableState<Value>(compute: (context: StateContext) => Value, cleanup?: (context: StateContext, value: Value | undefined) => void): ComputableState<Value>
107
    mutableState<Value>(initial: Value, global?: boolean, equivalent?: Equivalent<Value>, tracker?: ValueTracker<Value>): MutableState<Value>
108
    namedState<Value>(name: string, create: () => Value, global?: boolean, equivalent?: Equivalent<Value>, tracker?: ValueTracker<Value>): MutableState<Value>
109
    stateBy<Value>(name: string, global?: boolean): MutableState<Value> | undefined
110
    valueBy<Value>(name: string, global?: boolean): Value
111
    /** @internal */
112
    scope<Value>(id: KoalaCallsiteKey, paramCount?: int32, create?: () => IncrementalNode, compute?: () => Value, cleanup?: (value: Value | undefined) => void, once?: boolean): InternalScope<Value>
113
    controlledScope(id: KoalaCallsiteKey, invalidate: () => void): ControlledScope
114
}
115

116
/**
117
 * The interface allows to track the values assigned to a state.
118
 */
119
export interface ValueTracker<Value> {
120
    /**
121
     * Tracks state creation.
122
     * @param value - an initial state value
123
     * @returns the same value or a modified one
124
     */
125
    onCreate(value: Value): Value
126
    /**
127
     * Tracks state updates.
128
     * @param value - a value to set to state
129
     * @returns the same value or a modified one
130
     */
131
    onUpdate(value: Value): Value
132
}
133

134
/** @internal */
135
export interface InternalScope<Value> {
136
    /** @returns true if internal value can be returned as is */
137
    readonly unchanged: boolean
138
    /** @returns internal value if it is already computed */
139
    readonly cached: Value
140
    /** @returns internal value updated after the computation */
141
    recache(newValue?: Value): Value
142
    /** @returns internal state for parameter */
143
    param<V>(index: int32, value: V, equivalent?: Equivalent<V>, name?: string, contextLocal?: boolean): State<V>
144
}
145

146
/**
147
 * The interface represents a user-controlled scope,
148
 * that can be used outside of the incremental update.
149
 * @internal
150
 */
151
export interface ControlledScope {
152
    /** must be called to enter the controlled scope */
153
    enter(): void
154
    /** must be called to leave the controlled scope */
155
    leave(): void
156
}
157

158
// IMPLEMENTATION DETAILS: DO NOT USE IT DIRECTLY
159

160
interface ManagedState extends Disposable {
161
    /**
162
     * `true` - global state is added to the manager and is valid until it is disposed,
163
     * `false` - local state is added to the current scope and is valid as long as this scope is valid.
164
     */
165
    readonly global: boolean
166
    readonly modified: boolean
167
    updateStateSnapshot(): void
168
}
169

170
interface ManagedScope extends Disposable, Dependency, ReadonlyTreeNode {
171
    readonly manager: StateManagerImpl | undefined
172
    readonly dependencies: Dependencies | undefined
173
    readonly id: KoalaCallsiteKey
174
    readonly node: IncrementalNode | undefined
175
    readonly nodeRef: IncrementalNode | undefined
176
    readonly once: boolean
177
    readonly modified: boolean
178
    readonly parent: ManagedScope | undefined
179
    next: ManagedScope | undefined
180
    recomputeNeeded: boolean
181
    addCreatedState(state: Disposable): void
182
    getNamedState<Value>(name: string): MutableState<Value> | undefined
183
    setNamedState(name: string, state: Disposable): void
184
    getChildScope<Value>(id: KoalaCallsiteKey, paramCount: int32, create?: () => IncrementalNode, compute?: () => Value, cleanup?: (value: Value | undefined) => void, once?: boolean): ScopeImpl<Value>
185
    increment(count: uint32, skip: boolean): void
186
}
187

188
class StateImpl<Value> implements Observable, ManagedState, MutableState<Value> {
189
    manager: StateManagerImpl | undefined = undefined
190
    private dependencies: Dependencies | undefined = undefined
191
    private current: Value
192
    private snapshot: Value
193
    private myModified = false
194
    private myUpdated = true
195
    readonly global: boolean
196
    private equivalent: Equivalent<Value> | undefined = undefined
197
    private tracker: ValueTracker<Value> | undefined = undefined
198
    private name: string | undefined = undefined
199

200
    /**
201
     * @param manager - current state manager to register with
202
     * @param initial - initial state value
203
     * @param global - type of the state
204
     * @param name - name defined for named states only
205
     * @see StateManagerImpl.namedState
206
     */
207
    constructor(manager: StateManagerImpl, initial: Value, global: boolean, equivalent?: Equivalent<Value>, tracker?: ValueTracker<Value>, name?: string) {
208
        if (tracker !== undefined) initial = tracker.onCreate(initial)
209
        this.global = global
210
        this.equivalent = equivalent
211
        this.tracker = tracker
212
        this.name = name
213
        this.manager = manager
214
        this.dependencies = new Dependencies()
215
        this.current = initial
216
        this.snapshot = initial
217
        ObservableHandler.attach(initial, this)
218
        manager.addCreatedState(this)
219
    }
220

221
    get modified(): boolean {
222
        this.onAccess()
223
        return this.myModified
224
    }
225

226
    get value(): Value {
227
        this.onAccess()
228
        return this.manager?.frozen == true ? this.snapshot : this.current
229
    }
230

231
    set value(value: Value) {
232
        if (this.setProhibited) throw new Error("prohibited to modify a state when updating a call tree")
233
        if (this.tracker !== undefined) value = this.tracker!.onUpdate(value)
234
        this.current = value
235
        this.onModify()
236
    }
237

238
    onAccess(): void {
239
        this.dependencies?.register(this.manager?.dependency)
240
    }
241

242
    onModify(): void {
243
        this.myUpdated = false
244
        if (this.manager === undefined) {
245
            this.updateStateSnapshot()
246
        } else {
247
            this.manager!.updateNeeded = true
248
        }
249
    }
250

251
    private get setProhibited(): boolean {
252
        if (this.dependencies?.empty != false) return false // no dependencies
253
        const scope = this.manager?.current
254
        if (scope === undefined) return false // outside the incremental update
255
        if (scope?.node === undefined && scope?.parent === undefined) return false // during animation
256
        return true
257
    }
258

259
    updateStateSnapshot(): void {
260
        const isModified = ObservableHandler.dropModified(this.snapshot)
261
        // optimization: ignore comparison if the state is already updated
262
        if (this.myUpdated) {
263
            this.myModified = false
264
        }
265
        else {
266
            this.myUpdated = true
267
            if (isDifferent(this.current, this.snapshot, this.equivalent)) {
268
                ObservableHandler.detach(this.snapshot, this)
269
                ObservableHandler.attach(this.current, this)
270
                this.snapshot = this.current
271
                this.myModified = true
272
            } else {
273
                this.myModified = isModified
274
            }
275
        }
276
        this.dependencies?.updateDependencies(this.myModified)
277
    }
278

279
    get disposed(): boolean {
280
        return this.manager === undefined
281
    }
282

283
    dispose(): void {
284
        const manager = this.manager
285
        if (manager === undefined) return // already disposed
286
        manager.checkForStateDisposing()
287
        this.manager = undefined
288
        this.tracker = undefined
289
        this.dependencies = undefined
290
        manager.removeCreatedState(this, this.name)
291
    }
292

293
    toString(): string {
294
        let str = this.global ? "GlobalState" : "LocalState"
295
        if (this.name !== undefined) str += "(" + this.name + ")"
296
        if (this.disposed) str += ",disposed"
297
        if (this.myModified) str += ",modified"
298
        return this.manager?.frozen == true
299
            ? (str + ",frozen=" + this.snapshot)
300
            : (str + "=" + this.current)
301
    }
302
}
303

304
class ParameterImpl<Value> implements MutableState<Value> {
305
    private manager: StateManagerImpl | undefined = undefined
306
    private dependencies: Dependencies | undefined = undefined
307
    private name: string | undefined = undefined
308
    private _value: Value
309
    private _modified = false
310

311
    /**
312
     * @param manager - current state manager to register with
313
     * @param value - initial state value
314
     * @param name - name defined for named states only
315
     */
316
    constructor(manager: StateManagerImpl, value: Value, name?: string) {
317
        this.manager = manager
318
        this.dependencies = new Dependencies()
319
        this.name = name
320
        this._value = value
321
    }
322

323
    get modified(): boolean {
324
        this.dependencies?.register(this.manager?.dependency)
325
        return this._modified
326
    }
327

328
    get value(): Value {
329
        this.dependencies?.register(this.manager?.dependency)
330
        return this._value
331
    }
332

333
    set value(value: Value) {
334
        this.update(value)
335
    }
336

337
    update(value: Value, equivalent?: Equivalent<Value>): void {
338
        const isModified = ObservableHandler.dropModified(this._value)
339
        if (isDifferent(this._value, value, equivalent)) {
340
            this._value = value
341
            this._modified = true
342
        } else {
343
            this._modified = isModified
344
        }
345
        this.dependencies?.updateDependencies(this._modified)
346
    }
347

348
    get disposed(): boolean {
349
        return this.manager === undefined
350
    }
351

352
    dispose(): void {
353
        const manager = this.manager
354
        if (manager === undefined) return // already disposed
355
        manager.checkForStateDisposing()
356
        this.manager = undefined
357
        this.dependencies = undefined
358
    }
359

360
    toString(): string {
361
        let str = "Parameter"
362
        if (this.name !== undefined) str += "(" + this.name + ")"
363
        if (this.disposed) str += ",disposed"
364
        if (this._modified) str += ",modified"
365
        return str + "=" + this._value
366
    }
367
}
368

369
function isDifferent<Value>(value1: Value, value2: Value, equivalent?: Equivalent<Value>): boolean {
370
    return !refEqual(value1, value2) && (equivalent?.(value1, value2) != true)
371
}
372

373
class StateManagerImpl implements StateManager {
374
    private stateCreating: string | undefined = undefined
375
    private readonly statesNamed = new Map<string, Disposable>()
376
    private readonly statesCreated = new Set<ManagedState>()
377
    private readonly dirtyScopes = new Set<ManagedScope>()
378
    current: ManagedScope | undefined = undefined
379
    external: Dependency | undefined = undefined
380
    updateNeeded = false
381
    frozen = false
382
    private readonly callbacks = new Array<() => void>()
383

384
    constructor() {
385
    }
386

387
    reset(): void {
388
        if (this.statesNamed.size > 0) {
389
            disposeContent(this.statesNamed.values())
390
            this.statesNamed.clear()
391
        }
392
        if (this.statesCreated.size > 0) {
393
            disposeContent(this.statesCreated.keys())
394
            this.statesCreated.clear()
395
        }
396
        this.dirtyScopes.clear()
397
        this.callbacks.splice(0, this.callbacks.length)
398
        this.updateNeeded = false
399
        this.frozen = false
400
    }
401

402
    toString(): string {
403
        const scope = this.current
404
        return scope !== undefined ? scope.toHierarchy() : ""
405
    }
406

407
    updateSnapshot(): uint32 {
408
        KoalaProfiler.counters?.updateSnapshotEnter()
409
        this.checkForStateComputing()
410
        // optimization: all states are valid and not modified
411
        if (!this.updateNeeded) return 0
412
        let modified: uint32 = 0
413
        // try to update snapshot for every state, except for parameter states
414
        const created = this.statesCreated.size as int32 // amount of created states to update
415
        if (created > 0) {
416
            const it = this.statesCreated.keys()
417
            while (true) {
418
                const result = it.next()
419
                if (result.done) break
420
                result.value?.updateStateSnapshot()
421
                if (result.value?.modified == true) modified++
422
            }
423
        }
424
        KoalaProfiler.counters?.updateSnapshot(modified, created)
425
        // recompute dirty scopes only
426
        while (this.dirtyScopes.size > 0) {
427
            const scopes = Array_from_set(this.dirtyScopes)
428
            this.dirtyScopes.clear()
429
            const length = scopes.length
430
            for (let i = 0; i < length; i++) {
431
                if (scopes[i].modified) modified++
432
            }
433
        }
434
        KoalaProfiler.counters?.updateSnapshot(modified)
435
        this.updateNeeded = modified > 0 // reset modified on next update
436
        KoalaProfiler.counters?.updateSnapshotExit()
437
        return modified
438
    }
439

440
    updatableNode<Node extends IncrementalNode>(node: Node, update: (context: StateContext) => void, cleanup?: () => void): ComputableState<Node> {
441
        this.checkForStateComputing()
442
        const scope = new ScopeImpl<Node>(KoalaCallsiteKeys.empty, 0, () => {
443
            update(this)
444
            return node
445
        }, cleanup)
446
        scope.manager = this
447
        scope.node = node
448
        scope.nodeRef = node
449
        scope.dependencies = new Dependencies()
450
        scope.setNamedState(CONTEXT_ROOT_SCOPE, new StateImpl<ScopeImpl<Node>>(this, scope, false))
451
        scope.setNamedState(CONTEXT_ROOT_NODE, new StateImpl<Node>(this, node, false))
452
        return scope
453
    }
454

455
    computableState<Value>(compute: (context: StateContext) => Value, cleanup?: (context: StateContext, value: Value | undefined) => void): ComputableState<Value> {
456
        if (this.current?.once == false) throw new Error("computable state created in memo-context without remember")
457
        this.checkForStateCreating()
458
        const scope = new ScopeImpl<Value>(KoalaCallsiteKeys.empty, 0, () => compute(this), cleanup ? () => cleanup(this, undefined) : undefined)
459
        scope.manager = this
460
        scope.dependencies = new Dependencies()
461
        this.current?.addCreatedState(scope)
462
        return scope
463
    }
464

465
    scheduleCallback(callback: () => void): void {
466
        this.callbacks.push(callback)
467
    }
468

469
    callCallbacks(): void {
470
        const length = this.callbacks.length
471
        if (length > 0) {
472
            const callbacks = this.callbacks.splice(0, length)
473
            for (let i = 0; i < length; i++) callbacks[i]()
474
        }
475
    }
476

477
    mutableState<Value>(initial: Value, global?: boolean, equivalent?: Equivalent<Value>, tracker?: ValueTracker<Value>): StateImpl<Value> {
478
        if (!global && this.current?.once == false) throw new Error("unnamed local state created in memo-context without remember")
479
        if (global === undefined) global = this.current?.once != true
480
        else if (!global && !this.current) throw new Error("unnamed local state created in global context")
481
        return new StateImpl<Value>(this, initial, global, equivalent, tracker)
482
    }
483

484
    get node(): IncrementalNode | undefined {
485
        return this.current?.nodeRef
486
    }
487

488
    /**
489
     * Returns the current context scope if it can be invalidated later.
490
     * This method must have maximal performance,
491
     * as it is called for each access to a state value.
492
     * An externally controlled scope takes precedence over the current scope,
493
     * except for computable scopes used for animation.
494
     */
495
    get dependency(): Dependency | undefined {
496
        if (this.stateCreating === undefined) {
497
            const scope = this.current
498
            if (scope?.once == false && (scope?.nodeRef === undefined || this.external === undefined)) {
499
                return scope
500
            }
501
        }
502
        return this.external
503
    }
504

505
    scope<Value>(id: KoalaCallsiteKey, paramCount: int32 = 0, create?: () => IncrementalNode, compute?: () => Value, cleanup?: (value: Value | undefined) => void, once?: boolean): InternalScope<Value> {
506
        const counters = KoalaProfiler.counters
507
        if (counters !== undefined) {
508
            create ? counters.build() : counters.compute()
509
        }
510
        const scope = this.current
511
        if (scope !== undefined) return scope.getChildScope<Value>(id, paramCount, create, compute, cleanup, once)
512
        throw new Error("prohibited to create scope(" + KoalaCallsiteKeys.asString(id) + ") for the top level")
513
    }
514

515
    controlledScope(id: KoalaCallsiteKey, invalidate: () => void): ControlledScope {
516
        const scope = this.scope<ControlledScopeImpl>(id, 0, undefined, undefined, ControlledScopeImpl.cleanup, true)
517
        return scope.unchanged ? scope.cached : scope.recache(new ControlledScopeImpl(this, invalidate))
518
    }
519

520
    attach<Node extends IncrementalNode>(id: KoalaCallsiteKey, create: () => Node, update: () => void, cleanup?: () => void): void {
521
        const scope = this.scope<void>(id, 0, create, undefined, cleanup, undefined)
522
        scope.unchanged ? scope.cached : scope.recache(update())
523
    }
524

525
    compute<Value>(id: KoalaCallsiteKey, compute: () => Value, cleanup?: (value: Value | undefined) => void, once: boolean = false): Value {
526
        const scope = this.scope<Value>(id, 0, undefined, undefined, cleanup, once)
527
        return scope.unchanged ? scope.cached : scope.recache(compute())
528
    }
529

530
    /**
531
     * @param name - unique state name for this context
532
     * @param create - the factory to create the initial state value
533
     * @returns
534
     */
535
    namedState<Value>(name: string, create: () => Value, global?: boolean, equivalent?: Equivalent<Value>, tracker?: ValueTracker<Value>): MutableState<Value> {
536
        const scope = this.current
537
        if (global === undefined) global = scope === undefined
538
        let state = global ? this.getNamedState<Value>(name) : scope?.getNamedState<Value>(name)
539
        if (state !== undefined) return state // named state is already exist
540
        this.checkForStateCreating()
541
        this.stateCreating = name
542
        let initial = create()
543
        this.stateCreating = undefined
544
        state = new StateImpl<Value>(this, initial, global, equivalent, tracker, name)
545
        if (global) this.statesNamed.set(name, state)
546
        else if (scope !== undefined) scope.setNamedState(name, state)
547
        else throw new Error("local state '" + name + "' created in global context")
548
        return state
549
    }
550

551
    stateBy<Value>(name: string, global?: boolean): MutableState<Value> | undefined {
552
        if (global == true) return this.getNamedState<Value>(name)
553
        for (let scope = this.current; scope !== undefined; scope = scope!.parent) {
554
            const state = scope!.getNamedState<Value>(name)
555
            if (state !== undefined) return state
556
        }
557
        return global == false ? undefined : this.getNamedState<Value>(name)
558
    }
559

560
    valueBy<Value>(name: string, global?: boolean): Value {
561
        const state = this.stateBy<Value>(name, global)
562
        if (state !== undefined) return state.value
563
        const scope = this.current
564
        throw new Error(scope !== undefined
565
            ? ("state(" + name + ") is not defined in scope(" + KoalaCallsiteKeys.asString(scope.id) + ")")
566
            : ("global state(" + name + ") is not defined"))
567
    }
568

569
    addDirtyScope(state: ManagedScope): void {
570
        this.dirtyScopes.add(state)
571
    }
572

573
    addCreatedState(state: ManagedState): void {
574
        this.statesCreated.add(state)
575
        if (!state.global) this.current?.addCreatedState(state)
576
    }
577

578
    removeCreatedState(state: ManagedState, name?: string): void {
579
        if (state.global && name !== undefined) this.statesNamed.delete(name)
580
        this.statesCreated.delete(state)
581
    }
582

583
    getNamedState<T>(name: string): StateImpl<T> | undefined {
584
        const state = this.statesNamed.get(name)
585
        return state instanceof StateImpl ? state as StateImpl<T> : undefined
586
    }
587

588
    checkForStateDisposing(): void {
589
        this.current?.manager !== undefined
590
            ? this.checkForStateComputing()
591
            : this.checkForStateCreating()
592
    }
593

594
    checkForStateCreating(): void {
595
        const name = this.stateCreating
596
        if (name === undefined) return
597
        const scope = this.current
598
        throw new Error(scope !== undefined
599
            ? ("prohibited when creating state(" + name + ") in scope(" + KoalaCallsiteKeys.asString(scope.id) + ")")
600
            : ("prohibited when creating global state(" + name + ")"))
601
    }
602

603
    private checkForStateComputing(): void {
604
        this.checkForStateCreating()
605
        const scope = this.current
606
        if (scope !== undefined) throw new Error("prohibited when computing scope(" + KoalaCallsiteKeys.asString(scope.id) + ")")
607
    }
608
}
609

610
class ScopeImpl<Value> implements ManagedScope, InternalScope<Value>, ComputableState<Value> {
611
    recomputeNeeded = true
612
    manager: StateManagerImpl | undefined = undefined
613
    dependencies: Dependencies | undefined = undefined
614

615
    private myCompute: (() => Value) | undefined = undefined
616
    private myCleanup: ((value: Value | undefined) => void) | undefined = undefined
617
    private myValue: Value | undefined = undefined
618
    private myModified = false
619
    private myComputed = false
620

621
    private params: Array<Disposable | undefined> | undefined = undefined
622
    private statesNamed: Map<string, Disposable> | undefined = undefined
623
    private statesCreated: Array<Disposable> | undefined = undefined
624

625
    private scopeInternal: ManagedScope | undefined = undefined
626
    private incremental: ManagedScope | undefined = undefined
627
    private child: ManagedScope | undefined = undefined
628

629
    parent: ManagedScope | undefined = undefined
630
    next: ManagedScope | undefined = undefined
631

632
    readonly id: KoalaCallsiteKey
633
    once: boolean = false
634
    node: IncrementalNode | undefined = undefined
635
    nodeRef: IncrementalNode | undefined = undefined
636
    nodeCount: uint32 = 0
637

638
    constructor(id: KoalaCallsiteKey, paramCount: int32, compute?: () => Value, cleanup?: (value: Value | undefined) => void) {
639
        this.id = id // special type to distinguish scopes
640
        this.params = paramCount > 0 ? new Array<Disposable>(paramCount) : undefined
641
        this.myCompute = compute
642
        this.myCleanup = cleanup
643
    }
644

645
    addCreatedState(state: Disposable): void {
646
        if (this.statesCreated === undefined) this.statesCreated = new Array<Disposable>()
647
        this.statesCreated!.push(state)
648
    }
649

650
    setNamedState(name: string, state: Disposable): void {
651
        if (this.statesNamed === undefined) this.statesNamed = new Map<string, Disposable>()
652
        this.statesNamed!.set(name, state)
653
    }
654

655
    getNamedState<T>(name: string): MutableState<T> | undefined {
656
        return this.statesNamed !== undefined ? this.statesNamed!.get(name) as MutableState<T> : undefined
657
    }
658

659
    getChildScope<Value>(id: KoalaCallsiteKey, paramCount: int32, create?: () => IncrementalNode, compute?: () => Value, cleanup?: (value: Value | undefined) => void, once: boolean = false): ScopeImpl<Value> {
660
        const manager = this.manager
661
        if (manager === undefined) throw new Error("prohibited to create scope(" + KoalaCallsiteKeys.asString(id) + ") within the disposed scope(" + KoalaCallsiteKeys.asString(this.id) + ")")
662
        manager.checkForStateCreating()
663
        const inc = this.incremental
664
        const next = inc ? inc.next : this.child
665
        for (let child = next; child !== undefined; child = child!.next) {
666
            if (child!.id == id) {
667
                this.detachChildScopes(child!)
668
                this.incremental = child
669
                return child as ScopeImpl<Value>
670
            }
671
        }
672
        if (!once && this.once) throw new Error("prohibited to create scope(" + KoalaCallsiteKeys.asString(id) + ") within the remember scope(" + KoalaCallsiteKeys.asString(this.id) + ")")
673
        const scope = new ScopeImpl<Value>(id, paramCount, compute, cleanup)
674
        scope.manager = manager
675
        if (create) {
676
            // create node within a scope
677
            scope.once = true
678
            manager.current = scope
679
            if (this.nodeRef === undefined) throw new Error("prohibited to add nodes into computable state")
680
            scope.node = create()
681
            manager.current = this
682
        }
683
        scope.nodeRef = scope.node ?? this.nodeRef
684
        scope.once = once == true
685
        scope.parent = this
686
        scope.next = next
687
        if (inc !== undefined) {
688
            inc.next = scope
689
        } else {
690
            this.child = scope
691
        }
692
        this.incremental = scope
693
        return scope
694
    }
695

696
    private detachChildScopes(last?: ManagedScope): void {
697
        const inc = this.incremental
698
        let child = inc ? inc.next : this.child
699
        if (child === last) return
700
        if (inc) {
701
            inc.next = last
702
        } else {
703
            this.child = last
704
        }
705
        const manager = this.manager
706
        if (manager === undefined) throw new Error("unexpected")
707
        const scope = manager.current
708
        manager.current = undefined // allow to dispose children during recomputation
709
        while (child != last) {
710
            if (child === undefined) throw new Error("unexpected")
711
            child.dispose()
712
            child = child.next
713
        }
714
        manager.current = scope
715
    }
716

717
    increment(count: uint32, skip: boolean): void {
718
        if (count > 0) {
719
            this.nodeCount += count
720
            if (skip) this.nodeRef!.incrementalUpdateSkip(count)
721
        }
722
    }
723

724
    get value(): Value {
725
        if (this.unchanged) return this.cached
726
        let value = this.myValue
727
        try {
728
            const compute = this.myCompute
729
            if (compute === undefined) throw new Error("Wrong use of Internal API")
730
            value = compute()
731
        } finally {
732
            this.recache(value)
733
        }
734
        return value!
735
    }
736

737
    get unchanged(): boolean {
738
        if (this.recomputeNeeded) {
739
            this.incremental = undefined
740
            this.nodeCount = 0
741
            if (this.manager !== undefined) {
742
                this.scopeInternal = this.manager!.current
743
                this.manager!.current = this
744
            }
745
            return false
746
        } else {
747
            this.parent?.increment(this.node ? 1 : this.nodeCount, true)
748
            return true
749
        }
750
    }
751

752
    recache(newValue?: Value): Value {
753
        if (this.manager !== undefined) this.manager!.current = this.scopeInternal
754
        const oldValue = this.myValue
755
        this.myValue = newValue
756
        this.myModified = this.myComputed && !refEqual(newValue, oldValue)
757
        this.myComputed = true
758
        this.recomputeNeeded = false
759
        this.detachChildScopes()
760
        this.parent?.increment(this.node ? 1 : this.nodeCount, false)
761
        this.node?.incrementalUpdateDone(this.parent?.nodeRef)
762
        return this.cached
763
    }
764

765
    get cached(): Value {
766
        this.dependencies?.register(this.manager?.dependency)
767
        this.dependencies?.updateDependencies(this.myModified)
768
        return this.myValue as Value
769
    }
770

771
    param<V>(index: int32, value: V, equivalent?: Equivalent<V>, name?: string, contextLocal: boolean = false): State<V> {
772
        const manager = this.manager
773
        const params = this.params
774
        if (manager === undefined || params === undefined) throw new Error("Wrong use of Internal API")
775
        let state = params[index] as (ParameterImpl<V> | undefined)
776
        if (state !== undefined) {
777
            if (contextLocal && name && state != this.getNamedState(name)) throw new Error("name was unexpectedly changed to " + name)
778
            state!.update(value, equivalent)
779
        } else {
780
            params[index] = state = new ParameterImpl<V>(manager, value, name)
781
            if (contextLocal && name) this.setNamedState(name, state)
782
        }
783
        return state!
784
    }
785

786
    get modified(): boolean {
787
        if (this.recomputeNeeded) this.value
788
        else this.dependencies?.register(this.manager?.dependency)
789
        return this.myModified
790
    }
791

792
    get obsolete(): boolean {
793
        return this.manager === undefined
794
    }
795

796
    invalidate(): void {
797
        const current = this.manager?.current // parameters can update snapshot during recomposition
798
        let scope: ManagedScope = this
799
        while (true) { // fix optimization: !scope.myRecomputeNeeded
800
            if (scope === current) break // parameters should not invalidate whole hierarchy
801
            if (!scope.recomputeNeeded) KoalaProfiler.counters?.invalidation()
802
            scope.recomputeNeeded = true
803
            const parent = scope.parent
804
            if (parent !== undefined) {
805
                // TODO/DEBUG: investigate a case when invalid node has valid parent
806
                // Button.IsHovered does not work properly with the optimization above
807
                // if (this.myRecomputeNeeded && !parent.myRecomputeNeeded) console.log("parent of invalid scope is valid unexpectedly")
808
                scope = parent
809
            } else {
810
                // mark top-level computable state as dirty if it has dependencies.
811
                // they will be recomputed during the snapshot updating.
812
                // we do not recompute other computable states and updatable nodes.
813
                if (scope.dependencies?.empty == false) {
814
                    this.manager?.addDirtyScope(scope)
815
                }
816
                break
817
            }
818
        }
819
    }
820

821
    get disposed(): boolean {
822
        return this.manager === undefined
823
    }
824

825
    dispose() {
826
        const manager = this.manager
827
        if (manager === undefined) return // already disposed
828
        manager.checkForStateDisposing()
829
        let error: Error | undefined = undefined
830
        this.manager = undefined
831
        this.dependencies = undefined
832
        const scope = manager.current
833
        manager.current = this
834
        try {
835
            this.myCleanup?.(this.myValue)
836
        } catch (cause) {
837
            error = cause as Error
838
        }
839
        for (let child = this.child; child !== undefined; child = child!.next) {
840
            child!.dispose()
841
        }
842
        this.child = undefined
843
        this.parent = undefined
844
        this.node?.dispose()
845
        this.node = undefined
846
        this.nodeRef = undefined
847
        this.scopeInternal = undefined
848
        if (this.statesCreated !== undefined) {
849
            disposeContentBackward<Disposable>(this.statesCreated!)
850
            this.statesCreated = undefined
851
        }
852
        if (this.params !== undefined) {
853
            disposeContentBackward<Disposable>(this.params!)
854
            this.params = undefined
855
        }
856
        manager.current = scope
857
        this.myModified = false
858
        if (error !== undefined) throw error
859
    }
860

861
    toString(): string {
862
        let str: string = KoalaCallsiteKeys.asString(this.id)
863
        if (this.once) str += " remember..."
864
        if (this.node !== undefined) str += " " + className(this.node)
865
        if (this === this.manager?.current) str += " (*)"
866
        return str
867
    }
868

869
    toHierarchy(): string {
870
        let str = ""
871
        for (let node = this.parent; node !== undefined; node = node!.parent) str += "  "
872
        str += this.toString()
873
        for (let node = this.child; node !== undefined; node = node!.next) str += "\n" + node!.toHierarchy()
874
        return str
875
    }
876
}
877

878
class ControlledScopeImpl implements Dependency, ControlledScope {
879
    private manager: StateManagerImpl | undefined
880
    private old: Dependency | undefined = undefined
881
    private readonly _invalidate: () => void
882

883
    constructor(manager: StateManagerImpl, invalidate: () => void) {
884
        this.manager = manager
885
        this._invalidate = invalidate
886
    }
887

888
    invalidate(): void {
889
        this._invalidate()
890
    }
891

892
    static cleanup(scope?: ControlledScopeImpl): void {
893
        if (scope !== undefined) scope.manager = undefined
894
    }
895

896
    get obsolete(): boolean {
897
        return this.manager === undefined
898
    }
899

900
    enter(): void {
901
        const manager = this.manager
902
        if (manager === undefined) throw new Error("ControlledScope is already disposed")
903
        this.old = manager.external
904
        manager.external = this
905
    }
906

907
    leave(): void {
908
        const manager = this.manager
909
        if (manager === undefined) throw new Error("ControlledScope is already disposed")
910
        if (manager.external !== this) throw new Error("ControlledScope is not valid")
911
        manager.external = this.old
912
        this.old = undefined
913
    }
914
}
915

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.