• 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
16const OBSERVABLE_TARGET = "__proxy_observable_target__"
17
18export function getObservableTarget(proxy: Object): Object {
19    return getPropertyValue(OBSERVABLE_TARGET, proxy) ?? proxy
20}
21
22function getPropertyValue(name: string, object: any): any {
23    return object[name]
24}
25
26/**
27 * Data class decorator that makes all child fields trackable.
28 */
29export function Observed(constructorFunction: Function) {
30    constructorFunction.prototype[OBSERVED] = true
31}
32
33const OBSERVED = "__ObservedByArkUI__"
34function isObserved(value: any): boolean {
35    return value[OBSERVED] === true
36}
37
38/** @internal */
39export interface Observable {
40    /** It is called when the observable value is accessed. */
41    onAccess(): void
42    /** It is called when the observable value is modified. */
43    onModify(): void
44}
45
46/** @internal */
47export class ObservableHandler implements Observable {
48    private static handlers: WeakMap<Object, ObservableHandler> | undefined = undefined
49
50    private parents = new Set<ObservableHandler>()
51    private children = new Map<ObservableHandler, number>()
52
53    private readonly observables = new Set<Observable>()
54    private _modified = false
55
56    readonly observed: boolean
57    constructor(parent?: ObservableHandler, observed: boolean = false) {
58        this.observed = observed
59        if (parent) this.addParent(parent)
60    }
61
62    onAccess(): void {
63        if (this.observables.size > 0) {
64            const it = this.observables.keys()
65            while (true) {
66                const result = it.next()
67                if (result.done) break
68                result.value?.onAccess()
69            }
70        }
71    }
72
73    onModify(): void {
74        const set = new Set<ObservableHandler>()
75        this.collect(true, set)
76        set.forEach((handler: ObservableHandler) => {
77            handler._modified = true
78            if (handler.observables.size > 0) {
79                const it = handler.observables.keys()
80                while (true) {
81                    const result = it.next()
82                    if (result.done) break
83                    result.value?.onModify()
84                }
85            }
86        })
87    }
88
89    static dropModified<Value>(value: Value): boolean {
90        const handler = ObservableHandler.findIfObject(value)
91        if (handler === undefined) return false
92        const result = handler._modified
93        handler._modified = false
94        return result
95    }
96
97    /** Adds the specified `observable` to the handler corresponding to the given `value`. */
98    static attach<Value>(value: Value, observable: Observable): void {
99        const handler = ObservableHandler.findIfObject(value)
100        if (handler) handler.observables.add(observable)
101    }
102
103    /** Deletes the specified `observable` from the handler corresponding to the given `value`. */
104    static detach<Value>(value: Value, observable: Observable): void {
105        const handler = ObservableHandler.findIfObject(value)
106        if (handler) handler.observables.delete(observable)
107    }
108
109    /** @returns the handler corresponding to the given `value` if it was installed */
110    private static findIfObject<Value>(value: Value): ObservableHandler | undefined {
111        const handlers = ObservableHandler.handlers
112        return handlers !== undefined && value instanceof Object ? handlers.get(getObservableTarget(value as Object)) : undefined
113    }
114
115    /**
116     * @param value - any non-null object including arrays
117     * @returns an observable handler or `undefined` if it is not installed
118     */
119    static find(value: Object): ObservableHandler | undefined {
120        const handlers = ObservableHandler.handlers
121        return handlers ? handlers.get(getObservableTarget(value)) : undefined
122    }
123
124    /**
125     * @param value - any non-null object including arrays
126     * @param observable - a handler to install on this object
127     * @throws an error if observable handler cannot be installed
128     */
129    static installOn(value: Object, observable?: ObservableHandler): void {
130        let handlers = ObservableHandler.handlers
131        if (handlers === undefined) {
132            handlers = new WeakMap<Object, ObservableHandler>()
133            ObservableHandler.handlers = handlers
134        }
135        observable
136            ? handlers.set(getObservableTarget(value), observable)
137            : handlers.delete(getObservableTarget(value))
138    }
139
140    addParent(parent: ObservableHandler) {
141        const count = parent.children.get(this) ?? 0
142        parent.children.set(this, count + 1)
143        this.parents.add(parent)
144    }
145
146    removeParent(parent: ObservableHandler) {
147        const count = parent.children.get(this) ?? 0
148        if (count > 1) {
149            parent.children.set(this, count - 1)
150        }
151        else if (count == 1) {
152            parent.children.delete(this)
153            this.parents.delete(parent)
154        }
155    }
156
157    removeChild<Value>(value: Value) {
158        const child = ObservableHandler.findIfObject(value)
159        if (child) child.removeParent(this)
160    }
161
162    private collect(all: boolean, guards = new Set<ObservableHandler>()) {
163        if (guards.has(this)) return guards // already collected
164        guards.add(this) // handler is already guarded
165        this.parents.forEach(handler => handler.collect(all, guards))
166        if (all) this.children.forEach((_count, handler) => handler.collect(all, guards))
167        return guards
168    }
169
170    static contains(observable: ObservableHandler, guards?: Set<ObservableHandler>) {
171        if (observable.observed) return true
172        if (guards === undefined) guards = new Set<ObservableHandler>() // create if needed
173        else if (guards.has(observable)) return false // already checked
174        guards.add(observable) // handler is already guarded
175        for (const it of observable.parents.keys()) {
176            if (ObservableHandler.contains(it, guards)) return true
177        }
178        return false
179    }
180}
181
182/** @internal */
183export function observableProxyArray<Value>(...value: Value[]): Array<Value> {
184    return observableProxy(value)
185}
186
187/** @internal */
188export function observableProxy<Value>(value: Value, parent?: ObservableHandler, observed?: boolean, strict = true): Value {
189    if (value instanceof ObservableHandler) return value // do not proxy a marker itself
190    if (value === null || !(value instanceof Object)) return value // only non-null object can be observable
191    const observable = ObservableHandler.find(value)
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 (Array.isArray(value)) {
199                for (let index = 0; index < value.length; index++) {
200                    value[index] = observableProxy(value[index], observable, observed, false)
201                }
202            } else {
203                proxyFields(value, false, observable)
204            }
205        }
206        return value
207    }
208    if (Array.isArray(value)) {
209        const handler = new ObservableHandler(parent)
210        const array = proxyChildrenOnly(value, handler, observed)
211        copyWithinObservable(array)
212        fillObservable(array)
213        popObservable(array)
214        pushObservable(array)
215        reverseObservable(array)
216        shiftObservable(array)
217        sortObservable(array)
218        spliceObservable(array)
219        unshiftObservable(array)
220        return proxyObject(array, handler)
221    }
222    if (value instanceof Date) {
223        const valueAsAny = (value as any)
224        const handler = new ObservableHandler(parent)
225        const setMethods = new Set([
226            "setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds",
227            "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate",
228            "setUTCHours", "setUTCMinutes", "setUTCSeconds", "setUTCMilliseconds"
229        ])
230        setMethods.forEach((method: string) => {
231            const originalMethod = method + 'Original'
232            if (valueAsAny[originalMethod] !== undefined) {
233                return
234            }
235            valueAsAny[originalMethod] = valueAsAny[method]
236            valueAsAny[method] = function (...args: any[]) {
237                ObservableHandler.find(this)?.onModify()
238                return this[originalMethod](...args)
239            }
240        })
241        return proxyObject(value, handler)
242    }
243    // TODO: support set/map
244    const handler = new ObservableHandler(parent, isObserved(value))
245    if (handler.observed || observed) proxyFields(value, true, handler)
246    return proxyObject(value, handler)
247}
248
249function proxyObject(value: any, observable: ObservableHandler) {
250    ObservableHandler.installOn(value, observable)
251    return new Proxy(value, {
252        get(target, property, receiver) {
253            if (property == OBSERVABLE_TARGET) return target
254            const value: any = Reflect.get(target, property, receiver)
255            ObservableHandler.find(target)?.onAccess()
256            return typeof value == "function"
257                ? value.bind(target)
258                : value
259        },
260        set(target, property, value, receiver) {
261            const old = Reflect.get(target, property, receiver)
262            if (value === old) return true
263            const observable = ObservableHandler.find(target)
264            if (observable) {
265                observable.onModify()
266                observable.removeChild(old)
267                const observed = ObservableHandler.contains(observable)
268                if (observed || Array.isArray(target)) {
269                    value = observableProxy(value, observable, observed)
270                }
271            }
272            return Reflect.set(target, property, value, receiver)
273        },
274        deleteProperty(target, property) {
275            ObservableHandler.find(target)?.onModify()
276            delete target[property]
277            return true
278        },
279    })
280}
281
282function proxyFields(value: any, strict: boolean, parent?: ObservableHandler) {
283    for (const name of Object.getOwnPropertyNames(value)) {
284        const descriptor = Object.getOwnPropertyDescriptor(value, name)
285        if (descriptor?.writable) value[name] = observableProxy(value[name], parent, true, strict)
286    }
287}
288
289function proxyChildrenOnly(array: any[], parent: ObservableHandler, observed?: boolean): any[] {
290    if (observed === undefined) observed = ObservableHandler.contains(parent)
291    return array.map(it => observableProxy(it, parent, observed))
292}
293
294function copyWithinObservable(array: any) {
295    if (array.copyWithinOriginal === undefined) {
296        array.copyWithinOriginal = array.copyWithin
297        array.copyWithin = function (this, target: number, start: number, end?: number) {
298            const observable = ObservableHandler.find(this)
299            observable?.onModify()
300            return this.copyWithinOriginal(target, start, end)
301        }
302    }
303}
304
305function fillObservable(array: any) {
306    if (array.fillOriginal === undefined) {
307        array.fillOriginal = array.fill
308        array.fill = function (this, value: any, start?: number, end?: number) {
309            const observable = ObservableHandler.find(this)
310            observable?.onModify()
311            if (observable) value = observableProxy(value, observable)
312            return this.fillOriginal(value, start, end)
313        }
314    }
315}
316
317function popObservable(array: any) {
318    if (array.popOriginal === undefined) {
319        array.popOriginal = array.pop
320        array.pop = function (...args: any[]) {
321            const observable = ObservableHandler.find(this)
322            observable?.onModify()
323            const result = this.popOriginal(...args)
324            if (observable) observable.removeChild(result)
325            return result
326        }
327    }
328}
329
330function pushObservable(array: any) {
331    if (array.pushOriginal === undefined) {
332        array.pushOriginal = array.push
333        array.push = function (this, ...args: any[]) {
334            const observable = ObservableHandler.find(this)
335            observable?.onModify()
336            if (observable) args = proxyChildrenOnly(args, observable)
337            return this.pushOriginal(...args)
338        }
339    }
340}
341
342function reverseObservable(array: any) {
343    if (array.reverseOriginal === undefined) {
344        array.reverseOriginal = array.reverse
345        array.reverse = function (this) {
346            const observable = ObservableHandler.find(this)
347            observable?.onModify()
348            return this.reverseOriginal()
349        }
350    }
351}
352
353function shiftObservable(array: any) {
354    if (array.shiftOriginal === undefined) {
355        array.shiftOriginal = array.shift
356        array.shift = function (this, ...args: any[]) {
357            const observable = ObservableHandler.find(this)
358            observable?.onModify()
359            const result = this.shiftOriginal(...args)
360            if (observable) observable.removeChild(result)
361            return result
362        }
363    }
364}
365
366function sortObservable(array: any) {
367    if (array.sortOriginal === undefined) {
368        array.sortOriginal = array.sort
369        array.sort = function (this, compareFn?: (a: any, b: any) => number) {
370            const observable = ObservableHandler.find(this)
371            observable?.onModify()
372            return this.sortOriginal(compareFn)
373        }
374    }
375}
376
377function spliceObservable(array: any) {
378    if (array.spliceOriginal === undefined) {
379        array.spliceOriginal = array.splice
380        array.splice = function (this, start: number, deleteCount: number, ...items: any[]) {
381            const observable = ObservableHandler.find(this)
382            observable?.onModify()
383            if (observable) items = proxyChildrenOnly(items, observable)
384            if (deleteCount === undefined) deleteCount = array.length
385            const result = this.spliceOriginal(start, deleteCount, ...items)
386            if (observable && Array.isArray(result)) {
387                result.forEach(it => observable.removeChild(it))
388            }
389            return result
390        }
391    }
392}
393
394function unshiftObservable(array: any) {
395    if (array.unshiftOriginal === undefined) {
396        array.unshiftOriginal = array.unshift
397        array.unshift = function (this, ...items: any[]) {
398            const observable = ObservableHandler.find(this)
399            observable?.onModify()
400            if (observable) items = proxyChildrenOnly(items, observable)
401            return this.unshiftOriginal(...items)
402        }
403    }
404}
405