From 531bf4285dbcea88cda5f9cd8a89b87a0931d867 Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Wed, 15 Jan 2025 15:27:27 +0300 Subject: [PATCH 1/9] extract global resolving during state creation Signed-off-by: Sergey Malenkov --- incremental/runtime/src/states/State.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index 8112d182d..a90b610a5 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -501,11 +501,17 @@ class StateManagerImpl implements StateManager { this.callbacks.callCallbacks() } + private isGlobal(global?: boolean): boolean { + if (global == true) return true // allow to create global state everywhere + const remember = this.current?.once // true: remember // false: memo // undefined: global + if (remember == false) throw new Error("unnamed local state created in memo-context without remember") + if (global === undefined) return remember != true // create local state within remember only + if (remember === undefined) throw new Error("unnamed local state created in global context") + return false + } + mutableState(initial: Value, global?: boolean, equivalent?: Equivalent, tracker?: ValueTracker): MutableState { - if (global != true && this.current?.once == false) throw new Error("unnamed local state created in memo-context without remember") - if (global === undefined) global = this.current?.once != true - else if (!global && !this.current) throw new Error("unnamed local state created in global context") - return new StateImpl(this, initial, global, equivalent, tracker) + return new StateImpl(this, initial, this.isGlobal(global), equivalent, tracker) } get node(): IncrementalNode | undefined { @@ -588,7 +594,7 @@ class StateManagerImpl implements StateManager { const state = scope!.getNamedState(name) if (state) return state } - return (global ?? true) == false ? undefined : this.getNamedState(name) + return (global == false) ? undefined : this.getNamedState(name) } valueBy(name: string, global?: boolean): Value { -- Gitee From b96d499e9feb42cba8e3c626fdfcf82cd4e516ef Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Fri, 24 Jan 2025 14:41:31 +0300 Subject: [PATCH 2/9] replace computable property setProhibited to method checkSetProhibited Signed-off-by: Sergey Malenkov --- incremental/runtime/src/states/State.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index a90b610a5..59f9910bc 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -231,7 +231,7 @@ class StateImpl implements Observable, ManagedState, MutableState } set value(value: Value) { - if (this.setProhibited) throw new Error("prohibited to modify a state when updating a call tree") + this.checkSetProhibited() const tracker = this.tracker if (tracker) value = tracker.onUpdate(value) const manager = this.manager @@ -258,12 +258,12 @@ class StateImpl implements Observable, ManagedState, MutableState } } - private get setProhibited(): boolean { - if (this.dependencies?.empty != false) return false // no dependencies + private checkSetProhibited() { + if (this.dependencies?.empty != false) return // no dependencies const scope = this.manager?.current - if (scope === undefined) return false // outside the incremental update - if (scope?.node === undefined && scope?.parent === undefined) return false // during animation - return true + if (scope === undefined) return // outside the incremental update + if (scope?.node === undefined && scope?.parent === undefined) return // during animation + throw new Error("prohibited to modify a state when updating a call tree") } private current(changes?: Changes): Value { @@ -1000,3 +1000,7 @@ class ControlledScopeImpl implements Dependency, ControlledScope { this.old = undefined } } + +function asArray(initial?: ReadonlyArray): Array { + return initial === undefined ? new Array() : Array.from(initial) +} -- Gitee From 1cf3848eddf2c12b75838ede6b730aa17305e6bc Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Fri, 24 Jan 2025 15:22:55 +0300 Subject: [PATCH 3/9] collect array changes in journal Signed-off-by: Sergey Malenkov --- incremental/runtime/src/states/Journal.ts | 29 ++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/incremental/runtime/src/states/Journal.ts b/incremental/runtime/src/states/Journal.ts index 5cb94280b..dc1186694 100644 --- a/incremental/runtime/src/states/Journal.ts +++ b/incremental/runtime/src/states/Journal.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -77,6 +77,23 @@ export class Journal implements Changes { clear(): void { this.current.value = new Chunk() } + + /** + * Returns the cached array to store changes. + * The copy of the given array is created on first change. + * @param state - the corresponding array state + * @param array - current snapshot of the array state + * @returns the cached array registered for the given state + * @experimental + */ + getCachedArray(state: Object, array: Array): Array { + const chunk = this.current.value + const change = chunk.get>(state) + if (change) return change.value + const copy = Array.from(find>(state, chunk.previous.value)?.value ?? array) + chunk.map.set(state, new AtomicRef>(copy)) + return copy + } } class ChunkChanges implements Changes { @@ -98,6 +115,12 @@ class ChunkChanges implements Changes { class Chunk { readonly previous = new AtomicRef(undefined) readonly map = new Map() + + get(state: Object): AtomicRef | undefined { + const change = this.map.get(state) + if (change) return change as AtomicRef + return undefined + } } /** @@ -109,8 +132,8 @@ class Chunk { */ function find(state: Object, chunk?: Chunk): AtomicRef | undefined { while (chunk) { - const change = chunk?.map.get(state) - if (change) return change as AtomicRef + const change = chunk?.get(state) + if (change) return change chunk = chunk?.previous.value } return undefined -- Gitee From c56ba65acda3208e576eba60c1dbe9058600148b Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Fri, 24 Jan 2025 16:11:37 +0300 Subject: [PATCH 4/9] introduce a dedicated mutable state for arrays Signed-off-by: Sergey Malenkov --- incremental/runtime/src/index.ts | 5 +++- incremental/runtime/src/memo/remember.ts | 16 +++++++++++-- .../runtime/src/states/GlobalStateManager.ts | 17 ++++++++++++-- incremental/runtime/src/states/State.ts | 23 +++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/incremental/runtime/src/index.ts b/incremental/runtime/src/index.ts index 67697b5ef..dc93c3198 100644 --- a/incremental/runtime/src/index.ts +++ b/incremental/runtime/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -101,6 +101,7 @@ export { rememberComputableValue, rememberControlledScope, rememberDisposable, + rememberMutableArrayState, rememberMutableAsyncState, rememberMutableState, } from "./memo/remember" @@ -126,6 +127,7 @@ export { export { GlobalStateManager, callScheduledCallbacks, + mutableArrayState, mutableState, scheduleCallback, updateStateManager, @@ -136,6 +138,7 @@ export { ComputableState, ControlledScope, Equivalent, + MutableArrayState, MutableState, State, StateContext, diff --git a/incremental/runtime/src/memo/remember.ts b/incremental/runtime/src/memo/remember.ts index bde746672..3a3ba251c 100644 --- a/incremental/runtime/src/memo/remember.ts +++ b/incremental/runtime/src/memo/remember.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,7 +16,7 @@ import { functionOverValue } from "@koalaui/common" import { __context, __id } from "../internals" import { scheduleCallback } from "../states/GlobalStateManager" -import { ControlledScope, MutableState } from "../states/State" +import { ControlledScope, MutableArrayState, MutableState } from "../states/State" /** * It calculates the value of the given lambda and caches its result. @@ -127,6 +127,18 @@ export function rememberMutableState(initial: (() => Value) | Value): Mut ) } +/** + * Creates remembered array state which can be updated from anywhere, + * and if changed - all dependent memo functions recache automatically. + * @param initial - initial array supplier used on the state creation + * @returns an array state remembered for the current code position + * @memo:intrinsic + */ +export function rememberMutableArrayState(initial?: () => ReadonlyArray): MutableArrayState { + const scope = __context().scope>(__id(), 0, undefined, undefined, undefined, true) // do not recalculate if used states were updated + return scope.unchanged ? scope.cached : scope.recache(__context().mutableArrayState(initial?.())) +} + /** * @param promise - result of asynchronous function * @param state - state to receive computed value on success or `undefined` value on error diff --git a/incremental/runtime/src/states/GlobalStateManager.ts b/incremental/runtime/src/states/GlobalStateManager.ts index 1aa454207..1bb39e57f 100644 --- a/incremental/runtime/src/states/GlobalStateManager.ts +++ b/incremental/runtime/src/states/GlobalStateManager.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,7 +13,7 @@ * limitations under the License. */ -import { Equivalent, MutableState, StateManager, ValueTracker, createStateManager } from "./State" +import { Equivalent, MutableArrayState, MutableState, StateManager, ValueTracker, createStateManager } from "./State" /** * This class provides an access to the global state manager of the application. @@ -87,3 +87,16 @@ export function scheduleCallback(callback?: () => void, manager: StateManager = export function mutableState(value: T, equivalent?: Equivalent, tracker?: ValueTracker): MutableState { return GlobalStateManager.instance.mutableState(value, undefined, equivalent, tracker) } + +/** + * Creates new mutable array state in the global state manager. + * This state is valid until it is manually detached from the manager. + * It will be detached automatically if it is in the {@link remember}. + * Note that thoughtless state disposing can lead to memory leaks. + * @param array - initial array to initialize the created state + * @param equivalent - optional value comparator for a state + * @returns new mutable array state trackable by memo-functions + */ +export function mutableArrayState(array?: ReadonlyArray, equivalent?: Equivalent): MutableArrayState { + return GlobalStateManager.instance.mutableArrayState(array, undefined, equivalent) +} diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index 59f9910bc..d2eb7e5a7 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -83,6 +83,24 @@ export interface MutableState extends Disposable, State { value: Value } +/** + * Individual mutable state, wrapping an array of elements with the specified type. + */ +export interface MutableArrayState extends State> { + get(index: int32): Item + set(index: int32, item: Item): void + copyWithin(target: number, start: number, end?: number): Array + fill(value: Item, start?: number, end?: number): Array + pop(): Item | undefined + push(...items: Item[]): number + reverse(): Array + shift(): Item | undefined + sort(comparator?: (a: Item, b: Item) => number): Array + //splice(start: number, deleteCount?: number): Array + //splice(start: number, deleteCount: number, ...items: Item[]): Array + unshift(...items: Item[]): number +} + /** * Individual computable state that provides recomputable value of type `Value`. */ @@ -105,6 +123,7 @@ export interface StateContext { compute(id: KoalaCallsiteKey, compute: () => Value, cleanup?: (value: Value | undefined) => void, once?: boolean): Value computableState(compute: (context: StateContext) => Value, cleanup?: (context: StateContext, value: Value | undefined) => void): ComputableState mutableState(initial: Value, global?: boolean, equivalent?: Equivalent, tracker?: ValueTracker): MutableState + mutableArrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): MutableArrayState namedState(name: string, create: () => Value, global?: boolean, equivalent?: Equivalent, tracker?: ValueTracker): MutableState stateBy(name: string, global?: boolean): MutableState | undefined valueBy(name: string, global?: boolean): Value @@ -514,6 +533,10 @@ class StateManagerImpl implements StateManager { return new StateImpl(this, initial, this.isGlobal(global), equivalent, tracker) } + mutableArrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): MutableArrayState { + throw new Error("mutableArrayState is not supported for now") + } + get node(): IncrementalNode | undefined { return this.current?.nodeRef } -- Gitee From 93ca7ab2c590c588beff5711e9912c0fe8f6c84f Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Fri, 24 Jan 2025 18:24:26 +0300 Subject: [PATCH 5/9] basic implementation for array states Signed-off-by: Sergey Malenkov --- incremental/runtime/src/states/State.ts | 101 +++++++++++++++++++++--- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index d2eb7e5a7..c8702ae32 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -96,8 +96,7 @@ export interface MutableArrayState extends State> { reverse(): Array shift(): Item | undefined sort(comparator?: (a: Item, b: Item) => number): Array - //splice(start: number, deleteCount?: number): Array - //splice(start: number, deleteCount: number, ...items: Item[]): Array + //splice(start: number, deleteCount?: number, ...items: Item[]): Array unshift(...items: Item[]): number } @@ -204,13 +203,13 @@ interface ManagedScope extends Disposable, Dependency, ReadonlyTreeNode { } class StateImpl implements Observable, ManagedState, MutableState { - private manager: StateManagerImpl | undefined = undefined + protected manager: StateManagerImpl | undefined = undefined private dependencies: Dependencies | undefined = undefined - private snapshot: Value - private myModified = false - private myUpdated = true + protected snapshot: Value + protected myModified = false + protected myUpdated = true private readonly myGlobal: boolean - private equivalent: Equivalent | undefined = undefined + protected equivalent: Equivalent | undefined = undefined private tracker: ValueTracker | undefined = undefined private name: string | undefined = undefined @@ -277,7 +276,7 @@ class StateImpl implements Observable, ManagedState, MutableState } } - private checkSetProhibited() { + checkSetProhibited() { if (this.dependencies?.empty != false) return // no dependencies const scope = this.manager?.current if (scope === undefined) return // outside the incremental update @@ -301,7 +300,7 @@ class StateImpl implements Observable, ManagedState, MutableState this.dependencies?.updateDependencies(this.myModified) } - private applyStateSnapshot(newValue: Value) { + protected applyStateSnapshot(newValue: Value) { const oldValue = this.snapshot const isModified = ObservableHandler.dropModified(oldValue) if (!refEqual(oldValue, newValue)) { @@ -338,6 +337,84 @@ class StateImpl implements Observable, ManagedState, MutableState } } +class ArrayStateImpl extends StateImpl> implements MutableArrayState { + constructor(manager: StateManagerImpl, initial: Array, global: boolean, equivalent?: Equivalent) { + super(manager, initial, global, (oldArray: Array, newArray: Array): boolean => { + let i = oldArray.length + if (i != newArray.length) return false + while (0 < i--) { + if (isModified(oldArray[i], newArray[i], equivalent)) return false + } + return true + }) + } + + protected override applyStateSnapshot(newValue: Array) { + const modified = isModified>(this.snapshot, newValue, this.equivalent) + if (modified) this.snapshot = newValue + this.myModified = modified + } + + get(index: int32): Item { + return this.value[index] + } + + set(index: int32, item: Item): void { + this.mutable[index] = item + } + + copyWithin(target: number, start: number, end?: number): Array { + return this.mutable.copyWithin(target, start, end) + } + + fill(value: Item, start?: number, end?: number): Array { + return this.mutable.fill(value, start, end) + } + + pop(): Item | undefined { + return this.mutable.pop() + } + + push(...items: Item[]): number { + return this.mutable.push(...items) + } + + reverse(): Array { + return this.mutable.reverse() + } + + shift(): Item | undefined { + return this.mutable.shift() + } + + sort(comparator?: (a: Item, b: Item) => number): Array { + return this.mutable.sort(comparator) + } + + /* + splice(start: number, deleteCount: number, ...items: Item[]): Array { + return this.mutable.splice(start, deleteCount, ...items) + } + */ + + unshift(...items: Item[]): number { + return this.mutable.unshift(...items) + } + + private get mutable(): Array { + super.checkSetProhibited() + const manager = this.manager + if (manager) { + manager.updateNeeded = true + this.myUpdated = false + return manager.journal.getCachedArray(this, this.snapshot) + } else { + this.myModified = true + return this.snapshot + } + } +} + class ParameterImpl implements MutableState { private manager: StateManagerImpl | undefined = undefined private dependencies: Dependencies | undefined = undefined @@ -534,7 +611,7 @@ class StateManagerImpl implements StateManager { } mutableArrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): MutableArrayState { - throw new Error("mutableArrayState is not supported for now") + return new ArrayStateImpl(this, initial === undefined ? new Array() : Array.from(initial), this.isGlobal(global), equivalent) } get node(): IncrementalNode | undefined { @@ -1024,6 +1101,6 @@ class ControlledScopeImpl implements Dependency, ControlledScope { } } -function asArray(initial?: ReadonlyArray): Array { - return initial === undefined ? new Array() : Array.from(initial) +function isModified(oldV: Value, newV: Value, equivalent?: Equivalent): boolean { + return !refEqual(oldV, newV) && (equivalent?.(oldV, newV) != true) } -- Gitee From d54e657f74f06a53abc8fb28a43ce5e547d027b7 Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Fri, 24 Jan 2025 18:25:54 +0300 Subject: [PATCH 6/9] implement splice that is compatible with TS and ArkTS Signed-off-by: Sergey Malenkov --- incremental/runtime/src/states/State.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index c8702ae32..141ed2f45 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -96,7 +96,7 @@ export interface MutableArrayState extends State> { reverse(): Array shift(): Item | undefined sort(comparator?: (a: Item, b: Item) => number): Array - //splice(start: number, deleteCount?: number, ...items: Item[]): Array + splice(start: number, deleteCount: number | undefined, ...items: Item[]): Array unshift(...items: Item[]): number } @@ -391,11 +391,10 @@ class ArrayStateImpl extends StateImpl> implements MutableArra return this.mutable.sort(comparator) } - /* - splice(start: number, deleteCount: number, ...items: Item[]): Array { - return this.mutable.splice(start, deleteCount, ...items) + splice(start: number, deleteCount: number | undefined, ...items: Item[]): Array { + const array = this.mutable + return array.splice(start, deleteCount ?? array.length, ...items) } - */ unshift(...items: Item[]): number { return this.mutable.unshift(...items) -- Gitee From b1bf2638409cade98bf119f3dc188267f27396ba Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Mon, 27 Jan 2025 16:15:35 +0300 Subject: [PATCH 7/9] add length property and at getter Signed-off-by: Sergey Malenkov --- incremental/runtime/src/states/State.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index 141ed2f45..991240fbf 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -87,8 +87,10 @@ export interface MutableState extends Disposable, State { * Individual mutable state, wrapping an array of elements with the specified type. */ export interface MutableArrayState extends State> { - get(index: int32): Item - set(index: int32, item: Item): void + length: number + at(index: number): Item + get(index: number): Item + set(index: number, item: Item): void copyWithin(target: number, start: number, end?: number): Array fill(value: Item, start?: number, end?: number): Array pop(): Item | undefined @@ -355,11 +357,24 @@ class ArrayStateImpl extends StateImpl> implements MutableArra this.myModified = modified } - get(index: int32): Item { + get length(): number { + return this.value.length + } + + set length(value: number) { + this.mutable.length = value + } + + at(index: number): Item { + const array = this.value + return array[index < 0 ? array.length + index : index] + } + + get(index: number): Item { return this.value[index] } - set(index: int32, item: Item): void { + set(index: number, item: Item): void { this.mutable[index] = item } -- Gitee From d48cca782c9186c6128a080aa8819dadfc0357d3 Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Mon, 27 Jan 2025 16:16:39 +0300 Subject: [PATCH 8/9] add basic tests for TS Signed-off-by: Sergey Malenkov --- incremental/runtime/test/states/State.test.ts | 441 ++++++++++++++++++ 1 file changed, 441 insertions(+) diff --git a/incremental/runtime/test/states/State.test.ts b/incremental/runtime/test/states/State.test.ts index bb67757ba..072a0e2e3 100644 --- a/incremental/runtime/test/states/State.test.ts +++ b/incremental/runtime/test/states/State.test.ts @@ -1373,3 +1373,444 @@ suite("ValueTracker", () => { assertModifiedState(state, 999) }) }) + +suite("ArrayState", () => { + test("managed array state supports #at getter", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["one", "two", "three"]) + assert.equal(array.length, 3) + assert.equal(array.at(0), "one") + assert.equal(array.at(1), "two") + assert.equal(array.at(2), "three") + assert.equal(array.at(-1), "three") + assert.equal(array.at(-2), "two") + assert.equal(array.at(-3), "one") + }) + test("computable state depends on managed array state", () => { + const manager = createStateManager() + const state = manager.mutableState(1) + const array = manager.mutableArrayState(["item"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + const prefix = state.value + ": " + return prefix + context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= 1: item =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= 1: item =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + state.value = 2 + array.set(0, "value") + assert.equal(testUpdate(false, manager), 2) + assert.equal(result.value, "<= 2: value =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + state.value = 3 + array.set(0, "value") + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= 3: value =>") + assertStringsAndCleanup(computing, "outer ; center") + // compute state only when snapshot updated + state.value = 4 + array.length = 0 + assert.equal(testUpdate(false, manager), 2) + assert.equal(result.value, "<= 4: =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= 4: =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #copyWithin", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["one", "two", "three"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one two three =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + array.copyWithin(0, 1, 2) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= two two three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.copyWithin(2, 0, 1) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= two two two =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.copyWithin(1, 2, 0) + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= two two two =>") + assert.isEmpty(computing) + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= two two two =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #fill", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["one", "two", "three"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one two three =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + array.fill("X", 1, 2) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= one X three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.fill("X") + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= X X X =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.fill("X", 1) + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= X X X =>") + assert.isEmpty(computing) + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= X X X =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #pop", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["first", "last"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= first last =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= first last =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + assert.equal(array.pop(), "last") + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= first =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + assert.equal(array.pop(), "first") + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + assert.isUndefined(array.pop()) + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #push", () => { + const manager = createStateManager() + const array = manager.mutableArrayState() + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + assert.equal(array.push(), 0) + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + assert.equal(array.push("one"), 1) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + assert.equal(array.push("two", "three"), 3) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one two three =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #reverse", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["one", "two", "three"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one two three =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + array.reverse() + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= three two one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= three two one =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #shift", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["first", "last"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= first last =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= first last =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + assert.equal(array.shift(), "first") + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= last =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + assert.equal(array.shift(), "last") + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + assert.isUndefined(array.shift()) + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #sort", () => { + const manager = createStateManager() + const array = manager.mutableArrayState(["one", "two", "three"]) + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one two three =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + array.sort() + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= one three two =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.sort() + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one three two =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + array.sort((s1, s2) => s1.length < s2.length ? -1 : s1.length > s2.length ? 1 : s1.localeCompare(s2)) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= one two three =>") + assert.isEmpty(computing) + }) + test("computable state depends on managed array state #unshift", () => { + const manager = createStateManager() + const array = manager.mutableArrayState() + // create computable state that tracks computing inner scopes + const computing: string[] = [] + const result = manager.computableState(context => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + assert.equal(array.unshift(), 0) + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= =>") + assert.isEmpty(computing) + // compute state only when snapshot updated + assert.equal(array.unshift("one"), 1) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + assert.equal(array.unshift("two", "three"), 3) + assert.equal(testUpdate(false, manager), 1) + assert.equal(result.value, "<= two three one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + assert.equal(testUpdate(false, manager), 0) + assert.equal(result.value, "<= two three one =>") + assert.isEmpty(computing) + }) +}) -- Gitee From 1bc937ce4489604297a567c1edd449fe52963d90 Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Thu, 30 Jan 2025 12:43:39 +0300 Subject: [PATCH 9/9] rename MutableArrayState to ArrayState Signed-off-by: Sergey Malenkov --- incremental/runtime/src/index.ts | 6 ++--- incremental/runtime/src/memo/remember.ts | 8 +++---- .../runtime/src/states/GlobalStateManager.ts | 6 ++--- incremental/runtime/src/states/State.ts | 8 +++---- incremental/runtime/test/states/State.test.ts | 22 +++++++++---------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/incremental/runtime/src/index.ts b/incremental/runtime/src/index.ts index dc93c3198..6c584c0f6 100644 --- a/incremental/runtime/src/index.ts +++ b/incremental/runtime/src/index.ts @@ -97,11 +97,11 @@ export { memoLifecycle, once, remember, + rememberArrayState, rememberComputableState, rememberComputableValue, rememberControlledScope, rememberDisposable, - rememberMutableArrayState, rememberMutableAsyncState, rememberMutableState, } from "./memo/remember" @@ -126,19 +126,19 @@ export { } from "./states/Disposable" export { GlobalStateManager, + arrayState, callScheduledCallbacks, - mutableArrayState, mutableState, scheduleCallback, updateStateManager, } from "./states/GlobalStateManager" export { + ArrayState, CONTEXT_ROOT_NODE, CONTEXT_ROOT_SCOPE, ComputableState, ControlledScope, Equivalent, - MutableArrayState, MutableState, State, StateContext, diff --git a/incremental/runtime/src/memo/remember.ts b/incremental/runtime/src/memo/remember.ts index 3a3ba251c..090c1a8b9 100644 --- a/incremental/runtime/src/memo/remember.ts +++ b/incremental/runtime/src/memo/remember.ts @@ -16,7 +16,7 @@ import { functionOverValue } from "@koalaui/common" import { __context, __id } from "../internals" import { scheduleCallback } from "../states/GlobalStateManager" -import { ControlledScope, MutableArrayState, MutableState } from "../states/State" +import { ArrayState, ControlledScope, MutableState } from "../states/State" /** * It calculates the value of the given lambda and caches its result. @@ -134,9 +134,9 @@ export function rememberMutableState(initial: (() => Value) | Value): Mut * @returns an array state remembered for the current code position * @memo:intrinsic */ -export function rememberMutableArrayState(initial?: () => ReadonlyArray): MutableArrayState { - const scope = __context().scope>(__id(), 0, undefined, undefined, undefined, true) // do not recalculate if used states were updated - return scope.unchanged ? scope.cached : scope.recache(__context().mutableArrayState(initial?.())) +export function rememberArrayState(initial?: () => ReadonlyArray): ArrayState { + const scope = __context().scope>(__id(), 0, undefined, undefined, undefined, true) // do not recalculate if used states were updated + return scope.unchanged ? scope.cached : scope.recache(__context().arrayState(initial?.())) } /** diff --git a/incremental/runtime/src/states/GlobalStateManager.ts b/incremental/runtime/src/states/GlobalStateManager.ts index 1bb39e57f..0371bac5c 100644 --- a/incremental/runtime/src/states/GlobalStateManager.ts +++ b/incremental/runtime/src/states/GlobalStateManager.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import { Equivalent, MutableArrayState, MutableState, StateManager, ValueTracker, createStateManager } from "./State" +import { ArrayState, Equivalent, MutableState, StateManager, ValueTracker, createStateManager } from "./State" /** * This class provides an access to the global state manager of the application. @@ -97,6 +97,6 @@ export function mutableState(value: T, equivalent?: Equivalent, tracker?: * @param equivalent - optional value comparator for a state * @returns new mutable array state trackable by memo-functions */ -export function mutableArrayState(array?: ReadonlyArray, equivalent?: Equivalent): MutableArrayState { - return GlobalStateManager.instance.mutableArrayState(array, undefined, equivalent) +export function arrayState(array?: ReadonlyArray, equivalent?: Equivalent): ArrayState { + return GlobalStateManager.instance.arrayState(array, undefined, equivalent) } diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index 991240fbf..5923d582b 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -86,7 +86,7 @@ export interface MutableState extends Disposable, State { /** * Individual mutable state, wrapping an array of elements with the specified type. */ -export interface MutableArrayState extends State> { +export interface ArrayState extends State> { length: number at(index: number): Item get(index: number): Item @@ -124,7 +124,7 @@ export interface StateContext { compute(id: KoalaCallsiteKey, compute: () => Value, cleanup?: (value: Value | undefined) => void, once?: boolean): Value computableState(compute: (context: StateContext) => Value, cleanup?: (context: StateContext, value: Value | undefined) => void): ComputableState mutableState(initial: Value, global?: boolean, equivalent?: Equivalent, tracker?: ValueTracker): MutableState - mutableArrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): MutableArrayState + arrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): ArrayState namedState(name: string, create: () => Value, global?: boolean, equivalent?: Equivalent, tracker?: ValueTracker): MutableState stateBy(name: string, global?: boolean): MutableState | undefined valueBy(name: string, global?: boolean): Value @@ -339,7 +339,7 @@ class StateImpl implements Observable, ManagedState, MutableState } } -class ArrayStateImpl extends StateImpl> implements MutableArrayState { +class ArrayStateImpl extends StateImpl> implements ArrayState { constructor(manager: StateManagerImpl, initial: Array, global: boolean, equivalent?: Equivalent) { super(manager, initial, global, (oldArray: Array, newArray: Array): boolean => { let i = oldArray.length @@ -624,7 +624,7 @@ class StateManagerImpl implements StateManager { return new StateImpl(this, initial, this.isGlobal(global), equivalent, tracker) } - mutableArrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): MutableArrayState { + arrayState(initial?: ReadonlyArray, global?: boolean, equivalent?: Equivalent): ArrayState { return new ArrayStateImpl(this, initial === undefined ? new Array() : Array.from(initial), this.isGlobal(global), equivalent) } diff --git a/incremental/runtime/test/states/State.test.ts b/incremental/runtime/test/states/State.test.ts index 072a0e2e3..65c7d833b 100644 --- a/incremental/runtime/test/states/State.test.ts +++ b/incremental/runtime/test/states/State.test.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -1377,7 +1377,7 @@ suite("ValueTracker", () => { suite("ArrayState", () => { test("managed array state supports #at getter", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["one", "two", "three"]) + const array = manager.arrayState(["one", "two", "three"]) assert.equal(array.length, 3) assert.equal(array.at(0), "one") assert.equal(array.at(1), "two") @@ -1389,7 +1389,7 @@ suite("ArrayState", () => { test("computable state depends on managed array state", () => { const manager = createStateManager() const state = manager.mutableState(1) - const array = manager.mutableArrayState(["item"]) + const array = manager.arrayState(["item"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1441,7 +1441,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #copyWithin", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["one", "two", "three"]) + const array = manager.arrayState(["one", "two", "three"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1489,7 +1489,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #fill", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["one", "two", "three"]) + const array = manager.arrayState(["one", "two", "three"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1537,7 +1537,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #pop", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["first", "last"]) + const array = manager.arrayState(["first", "last"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1585,7 +1585,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #push", () => { const manager = createStateManager() - const array = manager.mutableArrayState() + const array = manager.arrayState() // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1633,7 +1633,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #reverse", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["one", "two", "three"]) + const array = manager.arrayState(["one", "two", "three"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1671,7 +1671,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #shift", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["first", "last"]) + const array = manager.arrayState(["first", "last"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1719,7 +1719,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #sort", () => { const manager = createStateManager() - const array = manager.mutableArrayState(["one", "two", "three"]) + const array = manager.arrayState(["one", "two", "three"]) // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { @@ -1767,7 +1767,7 @@ suite("ArrayState", () => { }) test("computable state depends on managed array state #unshift", () => { const manager = createStateManager() - const array = manager.mutableArrayState() + const array = manager.arrayState() // create computable state that tracks computing inner scopes const computing: string[] = [] const result = manager.computableState(context => { -- Gitee