• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-2025 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
16import { int32 } from "./types"
17
18const OBSERVABLE_TARGET = "target"
19
20export function getObservableTarget(proxy: Object): Object {
21    try {
22        return Reflect.get(proxy, OBSERVABLE_TARGET) ?? proxy
23    } catch (error) {
24        return proxy
25    }
26}
27
28/**
29 * Data class decorator that makes all child fields trackable.
30 */
31export function Observed() {
32    throw new Error("TypeScript class decorators are not supported yet")
33}
34
35/** @internal */
36export interface Observable {
37    /** It is called when the observable value is accessed. */
38    onAccess(): void
39    /** It is called when the observable value is modified. */
40    onModify(): void
41}
42
43/** @internal */
44export class ObservableHandler implements Observable {
45    private static handlers: WeakMap<Object, ObservableHandler> | undefined = undefined
46
47    private parents = new Set<ObservableHandler>()
48    private children = new Map<ObservableHandler, number>()
49
50    private readonly observables = new Set<Observable>()
51    private _modified = false
52
53    readonly observed: boolean
54    constructor(parent?: ObservableHandler, observed: boolean = false) {
55        this.observed = observed
56        if (parent) this.addParent(parent)
57    }
58
59    onAccess(): void {
60        if (this.observables.size > 0) {
61            const it = this.observables.keys()
62            while (true) {
63                const result = it.next()
64                if (result.done) break
65                result.value?.onAccess()
66            }
67        }
68    }
69
70    onModify(): void {
71        const set = new Set<ObservableHandler>()
72        this.collect(true, set)
73        set.forEach((handler: ObservableHandler) => {
74            handler._modified = true
75            if (handler.observables.size > 0) {
76                const it = handler.observables.keys()
77                while (true) {
78                    const result = it.next()
79                    if (result.done) break
80                    result.value?.onModify()
81                }
82            }
83        })
84    }
85
86    static dropModified<Value>(value: Value): boolean {
87        const handler = ObservableHandler.findIfObject(value)
88        if (handler === undefined) return false
89        const result = handler._modified
90        handler._modified = false
91        return result
92    }
93
94    /** Adds the specified `observable` to the handler corresponding to the given `value`. */
95    static attach<Value>(value: Value, observable: Observable): void {
96        const handler = ObservableHandler.findIfObject(value)
97        if (handler) handler.observables.add(observable)
98    }
99
100    /** Deletes the specified `observable` from the handler corresponding to the given `value`. */
101    static detach<Value>(value: Value, observable: Observable): void {
102        const handler = ObservableHandler.findIfObject(value)
103        if (handler) handler.observables.delete(observable)
104    }
105
106    /** @returns the handler corresponding to the given `value` if it was installed */
107    private static findIfObject<Value>(value: Value): ObservableHandler | undefined {
108        const handlers = ObservableHandler.handlers
109        return handlers !== undefined && value instanceof Object ? handlers.get(getObservableTarget(value as Object)) : undefined
110    }
111
112    /**
113     * @param value - any non-null object including arrays
114     * @returns an observable handler or `undefined` if it is not installed
115     */
116    static find(value: Object): ObservableHandler | undefined {
117        const handlers = ObservableHandler.handlers
118        return handlers ? handlers.get(getObservableTarget(value)) : undefined
119    }
120
121    /**
122     * @param value - any non-null object including arrays
123     * @param observable - a handler to install on this object
124     * @throws an error if observable handler cannot be installed
125     */
126    static installOn(value: Object, observable?: ObservableHandler): void {
127        let handlers = ObservableHandler.handlers
128        if (handlers === undefined) {
129            handlers = new WeakMap<Object, ObservableHandler>()
130            ObservableHandler.handlers = handlers
131        }
132        observable
133            ? handlers.set(getObservableTarget(value), observable)
134            : handlers.delete(getObservableTarget(value))
135    }
136
137    addParent(parent: ObservableHandler) {
138        const count = parent.children.get(this) ?? 0
139        parent.children.set(this, count + 1)
140        this.parents.add(parent)
141    }
142
143    removeParent(parent: ObservableHandler) {
144        const count = parent.children.get(this) ?? 0
145        if (count > 1) {
146            parent.children.set(this, count - 1)
147        }
148        else if (count == 1) {
149            parent.children.delete(this)
150            this.parents.delete(parent)
151        }
152    }
153
154    removeChild<Value>(value: Value) {
155        const child = ObservableHandler.findIfObject(value)
156        if (child) child.removeParent(this)
157    }
158
159    private collect(all: boolean, guards: Set<ObservableHandler>) {
160        if (guards.has(this)) return guards // already collected
161        guards.add(this) // handler is already guarded
162        this.parents.forEach((handler: ObservableHandler) => { handler.collect(all, guards) })
163        if (all) this.children.forEach((_count: number, handler: ObservableHandler) => { handler.collect(all, guards) })
164        return guards
165    }
166
167    static contains(observable: ObservableHandler, guards?: Set<ObservableHandler>) {
168        if (observable.observed) return true
169        if (guards === undefined) guards = new Set<ObservableHandler>() // create if needed
170        else if (guards!.has(observable)) return false // already checked
171        guards.add(observable) // handler is already guarded
172        for (const it of observable.parents.keys()) {
173            if (ObservableHandler.contains(it, guards)) return true
174        }
175        return false
176    }
177}
178
179/** @internal */
180export function observableProxyArray<Value>(...value: Value[]): Array<Value> {
181    return observableProxy(Array.of<Value>(...value))
182}
183
184const PROXY_DISABLED = true // because of ArkTS Reflection performance
185
186/** @internal */
187export function observableProxy<Value>(value: Value, parent?: ObservableHandler, observed?: boolean, strict: boolean = true): Value {
188    if (PROXY_DISABLED) return value
189    if (value instanceof ObservableHandler) return value as Value // do not proxy a marker itself
190    if (value == null || !(value instanceof Object)) return value as Value // only non-null object can be observable
191    const observable = ObservableHandler.find(value as Object)
192    if (observable) {
193        if (parent) {
194            if (strict) observable.addParent(parent)
195            if (observed === undefined) observed = ObservableHandler.contains(parent)
196        }
197        if (observed) {
198            if (value instanceof Array) {
199                for (let index = 0; index < value.length; index++) {
200                    value[index] = observableProxy(value[index], observable, observed, false)
201                }
202            } else {
203                // TODO: proxy fields of the given object
204            }
205        }
206        return value as Value
207    }
208    if (value instanceof Array) {
209        const handler = new ObservableHandler(parent)
210        const array = proxyChildrenOnly(value, handler, observed)
211        ObservableHandler.installOn(array, handler)
212        return createProxyArray(array) as Value
213    }
214    // TODO: proxy the given object
215    return value as Value
216}
217
218function createProxyArray<T>(array: Array<T>): Array<T> {
219    return Proxy.create(array, new CustomArrayProxyHandler<T>())
220}
221
222function proxyChildrenOnly<T>(array: Array<T>, parent: ObservableHandler, observed?: boolean): Array<T> {
223    if (observed === undefined) observed = ObservableHandler.contains(parent)
224    return array.map((it: T) => observableProxy(it, parent, observed))
225}
226
227class CustomArrayProxyHandler<T> extends DefaultArrayProxyHandler<T> {
228    override get(target: Array<T>, index: int32): T {
229        const observable = ObservableHandler.find(target)
230        if (observable) observable.onAccess()
231        return super.get(target, index)
232    }
233
234    override set(target: Array<T>, index: int32, value: T): boolean {
235        const observable = ObservableHandler.find(target)
236        if (observable) {
237            observable.onModify()
238            observable.removeChild(super.get(target, index))
239            value = observableProxy(value, observable, ObservableHandler.contains(observable))
240        }
241        return super.set(target, index, value)
242    }
243
244    override get(target: Array<T>, name: string): NullishType {
245        const observable = ObservableHandler.find(target)
246        if (observable) observable.onAccess()
247        return super.get(target, name)
248    }
249
250    override set(target: Array<T>, name: string, value: NullishType): boolean {
251        const observable = ObservableHandler.find(target)
252        if (observable) {
253            observable.onModify()
254            observable.removeChild(super.get(target, name))
255            value = observableProxy(value, observable, ObservableHandler.contains(observable))
256        }
257        return super.set(target, name, value)
258    }
259
260    override invoke(target: Array<T>, method: Method, args: NullishType[]): NullishType {
261        const observable = ObservableHandler.find(target)
262        if (observable) {
263            const name = method.getName()
264            if (name == "copyWithin" || name == "reverse" || name == "sort") {
265                observable.onModify()
266                return super.invoke(target, method, args)
267            }
268            if (name == "fill") {
269                observable.onModify()
270                if (args.length > 0) {
271                    args[0] = observableProxy(args[0], observable)
272                }
273                return super.invoke(target, method, args)
274            }
275            if (name == "pop" || name == "shift") {
276                observable.onModify()
277                const result = super.invoke(target, method, args)
278                observable.removeChild(result)
279                return result
280            }
281            if (name == "push" || name == "unshift") {
282                observable.onModify()
283                if (args.length > 0) {
284                    const items = args[0]
285                    if (items instanceof Array) {
286                        args[0] = proxyChildrenOnly(items, observable)
287                    }
288                }
289                return super.invoke(target, method, args)
290            }
291            if (name == "splice") {
292                observable.onModify()
293                if (args.length > 2) {
294                    const items = args[2]
295                    if (items instanceof Array) {
296                        args[2] = proxyChildrenOnly(items, observable)
297                    }
298                }
299                const result = super.invoke(target, method, args)
300                if (result instanceof Array) {
301                    for (let i = 0; i < result.length; i++) {
302                        observable.removeChild(result[i])
303                    }
304                }
305                return result
306            }
307            observable.onAccess()
308        }
309        return super.invoke(target, method, args)
310    }
311}
312