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