fast reactive signals jsr.io/@mary/signals
typescript jsr
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: reactive proxy

Mary fd7e5674 96955bdf

+253
+253
store.ts
··· 1 + /** 2 + * @module 3 + * Creates a reactive proxy of an object. 4 + * Loosely based off Svelte's and Solid's reactivity proxy implementation. 5 + */ 6 + 7 + import { batch, eval_listener, type Signal, signal } from './mod.ts'; 8 + 9 + interface ObjectMetadata { 10 + _is_array: boolean; 11 + _self: Signal<any>; 12 + _values: Record<string | symbol, Signal<any> | undefined>; 13 + } 14 + 15 + let uid = 0; 16 + 17 + const METADATA_SYMBOL = /*#__PURE__*/ Symbol('reactive-metadata'); 18 + const RAW_SYMBOL = /*#__PURE__*/ Symbol('reactive-raw'); 19 + 20 + const UNINITIALIZED = /*#__PURE__*/ Symbol('reactive-uninitialized'); 21 + 22 + const deep_proxies = /*#__PURE__*/ new WeakSet<object>(); 23 + 24 + const object_to_shallow = /*#__PURE__*/ new WeakMap<object, any>(); 25 + const object_to_deep = /*#__PURE__*/ new WeakMap<object, any>(); 26 + 27 + const object_proto = /*#__PURE__*/ Object.prototype; 28 + const array_proto = /*#__PURE__*/ Array.prototype; 29 + const get_prototype_of = /*#__PURE__*/ Object.getPrototypeOf; 30 + const get_descriptor = /*#__PURE__*/ Object.getOwnPropertyDescriptor; 31 + const is_extensible = /*#__PURE__*/ Object.isExtensible; 32 + 33 + const proxy_handler: ProxyHandler<any> = { 34 + get(target, prop, receiver) { 35 + if (prop === RAW_SYMBOL) { 36 + return target; 37 + } 38 + 39 + // deno-lint-ignore prefer-const 40 + let metadata: ObjectMetadata = target[METADATA_SYMBOL]; 41 + let s = metadata._values[prop]; 42 + 43 + if (s === undefined && eval_listener && (!(prop in target) || get_descriptor(target, prop)?.writable)) { 44 + s = metadata._values[prop] = signal(target[prop]); 45 + } 46 + 47 + if (s !== undefined) { 48 + const value = s.value; 49 + const is_deep_proxy = deep_proxies.has(receiver); 50 + return value === UNINITIALIZED ? undefined : is_deep_proxy ? reactive(value) : value; 51 + } 52 + 53 + return target[prop]; 54 + }, 55 + 56 + has(target, prop) { 57 + const metadata: ObjectMetadata = target[METADATA_SYMBOL]; 58 + const has = prop in target; 59 + 60 + let s = metadata._values[prop]; 61 + 62 + if (s !== undefined || (eval_listener && (!has || get_descriptor(target, prop)?.writable))) { 63 + if (s === undefined) { 64 + s = metadata._values[prop] = signal(has ? target[prop] : UNINITIALIZED); 65 + } 66 + 67 + if (s.value === UNINITIALIZED) { 68 + return false; 69 + } 70 + } 71 + 72 + return has; 73 + }, 74 + 75 + set(target, prop, value) { 76 + const metadata: ObjectMetadata = target[METADATA_SYMBOL]; 77 + 78 + const is_array = metadata._is_array; 79 + const not_has = !(prop in target); 80 + const s = metadata._values[prop]; 81 + 82 + const unwrapped = unwrap(value); 83 + 84 + batch(() => { 85 + if (s !== undefined) { 86 + s.value = unwrapped; 87 + } 88 + 89 + if (is_array && prop === 'length') { 90 + for (let i = value, ilen = target.length; i < ilen; i++) { 91 + const cs = metadata._values[i]; 92 + 93 + if (cs !== undefined) { 94 + cs.value = UNINITIALIZED; 95 + } 96 + } 97 + } 98 + 99 + target[prop] = unwrapped; 100 + 101 + if (not_has) { 102 + if (is_array) { 103 + const ls = metadata._values.length; 104 + 105 + if (ls !== undefined) { 106 + ls.value = target.length; 107 + } 108 + } 109 + 110 + metadata._self.value = uid++; 111 + } 112 + }); 113 + 114 + return true; 115 + }, 116 + 117 + defineProperty(target, prop, descriptor) { 118 + if ('value' in descriptor) { 119 + const metadata: ObjectMetadata = target[METADATA_SYMBOL]; 120 + const s = metadata._values[prop]; 121 + 122 + if (s !== undefined) { 123 + s.value = unwrap(descriptor.value); 124 + } 125 + } 126 + 127 + return Reflect.defineProperty(target, prop, descriptor); 128 + }, 129 + 130 + deleteProperty(target, prop) { 131 + const metadata: ObjectMetadata = target[METADATA_SYMBOL]; 132 + 133 + const is_array = metadata._is_array; 134 + const s = metadata._values[prop]; 135 + 136 + const result = delete target[prop]; 137 + 138 + batch(() => { 139 + if (is_array && result) { 140 + const ls = metadata._values.length; 141 + 142 + if (ls !== undefined) { 143 + ls.value = target.length - 1; 144 + } 145 + } 146 + 147 + if (s !== undefined) { 148 + s.value = UNINITIALIZED; 149 + } 150 + 151 + if (result) { 152 + metadata._self.value = uid++; 153 + } 154 + }); 155 + 156 + return result; 157 + }, 158 + 159 + getOwnPropertyDescriptor(target, prop) { 160 + const descriptor = Reflect.getOwnPropertyDescriptor(target, prop); 161 + if (descriptor && 'value' in descriptor) { 162 + const metadata: ObjectMetadata = target[METADATA_SYMBOL]; 163 + const s = metadata._values[prop]; 164 + 165 + if (s) { 166 + descriptor.value = s.value; 167 + } 168 + } 169 + 170 + return descriptor; 171 + }, 172 + 173 + ownKeys(target) { 174 + if (eval_listener) { 175 + const metadata: ObjectMetadata = target[METADATA_SYMBOL]; 176 + metadata._self.value; 177 + } 178 + 179 + return Reflect.ownKeys(target); 180 + }, 181 + }; 182 + 183 + function is_wrappable(obj: any): boolean { 184 + const proto = get_prototype_of(obj); 185 + return (proto === null || proto === object_proto || proto === array_proto) && is_extensible(obj); 186 + } 187 + 188 + function initialize(value: any): ObjectMetadata { 189 + return { 190 + _is_array: get_prototype_of(value) === array_proto, 191 + _self: signal(0), 192 + _values: Object.create(null), 193 + }; 194 + } 195 + 196 + /** 197 + * Retrieve the original object from a reactive proxy 198 + * @param value Any value 199 + * @returns If a reactive proxy, the original object, otherwise returned as-is. 200 + */ 201 + export function unwrap<T>(value: T): T { 202 + if (typeof value === 'object' && value !== null) { 203 + // @ts-expect-error: wrong type 204 + return value[RAW_SYMBOL] ?? value; 205 + } 206 + 207 + return value; 208 + } 209 + 210 + /** 211 + * Creates a shallow reactive proxy of an object 212 + * @param value Any value 213 + * @returns If passed an object, a reactive proxy of that object, otherwise returned as-is. 214 + */ 215 + export function shallowReactive<T extends object>(value: T): T { 216 + if (typeof value === 'object' && value !== null && is_wrappable(value)) { 217 + let proxy = object_to_shallow.get(value = unwrap(value)); 218 + 219 + if (proxy === undefined) { 220 + // @ts-expect-error: wrong type 221 + value[METADATA_SYMBOL] ||= initialize(value); 222 + 223 + object_to_shallow.set(value, proxy = new Proxy(value, proxy_handler)); 224 + } 225 + 226 + return proxy; 227 + } 228 + 229 + return value; 230 + } 231 + 232 + /** 233 + * Creates a deep reactive proxy of an object 234 + * @param value Any value 235 + * @returns If passed an object, a reactive proxy of that object, otherwise returned as-is. 236 + */ 237 + export function reactive<T extends object>(value: T): T { 238 + if (typeof value === 'object' && value !== null && is_wrappable(value)) { 239 + let proxy = object_to_deep.get(value = unwrap(value)); 240 + 241 + if (proxy === undefined) { 242 + // @ts-expect-error: wrong type 243 + value[METADATA_SYMBOL] ||= initialize(value); 244 + 245 + object_to_deep.set(value, proxy = new Proxy(value, proxy_handler)); 246 + deep_proxies.add(proxy); 247 + } 248 + 249 + return proxy; 250 + } 251 + 252 + return value; 253 + }