99 UNINITIALIZED ,
1010 mutable_source ,
1111 batch_inspect
12- } from '.. /runtime.js' ;
12+ } from './runtime.js' ;
1313import {
1414 array_prototype ,
1515 define_property ,
@@ -20,36 +20,40 @@ import {
2020 is_frozen ,
2121 object_keys ,
2222 object_prototype
23- } from '../utils.js' ;
24-
25- /** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean, i: boolean, p: StateObject } } Metadata */
26- /** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata } } StateObject */
23+ } from './utils.js' ;
2724
2825export const STATE_SYMBOL = Symbol ( '$state' ) ;
2926export const READONLY_SYMBOL = Symbol ( 'readonly' ) ;
27+
3028/**
31- * @template {StateObject} T
29+ * @template T
3230 * @param {T } value
3331 * @param {boolean } [immutable]
34- * @returns {T }
32+ * @returns {import('./types.js').ProxyStateObject<T> | T }
3533 */
3634export function proxy ( value , immutable = true ) {
3735 if ( typeof value === 'object' && value != null && ! is_frozen ( value ) ) {
3836 if ( STATE_SYMBOL in value ) {
39- return /** @type {T } */ ( value [ STATE_SYMBOL ] . p ) ;
37+ return /** @type {import('./types.js').ProxyMetadata<T> } */ ( value [ STATE_SYMBOL ] ) . p ;
4038 }
4139
4240 const prototype = get_prototype_of ( value ) ;
4341
4442 // TODO handle Map and Set as well
4543 if ( prototype === object_prototype || prototype === array_prototype ) {
46- const proxy = new Proxy ( value , handler ) ;
44+ const proxy = new Proxy (
45+ value ,
46+ /** @type {ProxyHandler<import('./types.js').ProxyStateObject<T>> } */ ( state_proxy_handler )
47+ ) ;
4748 define_property ( value , STATE_SYMBOL , {
48- value : init ( value , proxy , immutable ) ,
49+ value : init (
50+ /** @type {import('./types.js').ProxyStateObject<T> } */ ( value ) ,
51+ /** @type {import('./types.js').ProxyStateObject<T> } */ ( proxy ) ,
52+ immutable
53+ ) ,
4954 writable : false
5055 } ) ;
5156
52- // @ts -expect-error not sure how to fix this
5357 return proxy ;
5458 }
5559 }
@@ -58,7 +62,7 @@ export function proxy(value, immutable = true) {
5862}
5963
6064/**
61- * @template {StateObject } T
65+ * @template {import('./types.js').ProxyStateObject } T
6266 * @param {T } value
6367 * @param {Map<T, Record<string | symbol, any>> } already_unwrapped
6468 * @returns {Record<string | symbol, any> }
@@ -104,14 +108,14 @@ function unwrap(value, already_unwrapped = new Map()) {
104108 * @returns {T }
105109 */
106110export function unstate ( value ) {
107- return /** @type {T } */ ( unwrap ( /** @type {StateObject } */ ( value ) ) ) ;
111+ return /** @type {T } */ ( unwrap ( /** @type {import('./types.js').ProxyStateObject } */ ( value ) ) ) ;
108112}
109113
110114/**
111- * @param {StateObject } value
112- * @param {StateObject } proxy
115+ * @param {import('./types.js').ProxyStateObject } value
116+ * @param {import('./types.js').ProxyStateObject } proxy
113117 * @param {boolean } immutable
114- * @returns {Metadata }
118+ * @returns {import('./types.js').ProxyMetadata }
115119 */
116120function init ( value , proxy , immutable ) {
117121 return {
@@ -123,8 +127,8 @@ function init(value, proxy, immutable) {
123127 } ;
124128}
125129
126- /** @type {ProxyHandler<StateObject > } */
127- const handler = {
130+ /** @type {ProxyHandler<import('./types.js').ProxyStateObject > } */
131+ const state_proxy_handler = {
128132 defineProperty ( target , prop , descriptor ) {
129133 if ( descriptor . value ) {
130134 const metadata = target [ STATE_SYMBOL ] ;
@@ -290,9 +294,67 @@ export function observe(object) {
290294}
291295
292296if ( DEV ) {
293- handler . setPrototypeOf = ( ) => {
297+ state_proxy_handler . setPrototypeOf = ( ) => {
294298 throw new Error ( 'Cannot set prototype of $state object' ) ;
295299 } ;
296300}
297301
298- export { readonly } from './readonly.js' ;
302+ /**
303+ * Expects a value that was wrapped with `proxy` and makes it readonly.
304+ *
305+ * @template {Record<string | symbol, any>} T
306+ * @template {import('./types.js').ProxyReadonlyObject<T> | T} U
307+ * @param {U } value
308+ * @returns {Proxy<U> | U }
309+ */
310+ export function readonly ( value ) {
311+ const proxy = value && value [ READONLY_SYMBOL ] ;
312+ if ( proxy ) return proxy ;
313+
314+ if (
315+ typeof value === 'object' &&
316+ value != null &&
317+ ! is_frozen ( value ) &&
318+ STATE_SYMBOL in value && // TODO handle Map and Set as well
319+ ! ( READONLY_SYMBOL in value )
320+ ) {
321+ const proxy = new Proxy (
322+ value ,
323+ /** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject<U>> } */ (
324+ readonly_proxy_handler
325+ )
326+ ) ;
327+ define_property ( value , READONLY_SYMBOL , { value : proxy , writable : false } ) ;
328+ return proxy ;
329+ }
330+
331+ return value ;
332+ }
333+
334+ /**
335+ * @param {any } _
336+ * @param {string } prop
337+ * @returns {never }
338+ */
339+ const readonly_error = ( _ , prop ) => {
340+ throw new Error (
341+ `Non-bound props cannot be mutated — to make the \`${ prop } \` settable, ensure the object it is used within is bound as a prop \`bind:<prop>={...}\`. Fallback values can never be mutated.`
342+ ) ;
343+ } ;
344+
345+ /** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject> } */
346+ const readonly_proxy_handler = {
347+ defineProperty : readonly_error ,
348+ deleteProperty : readonly_error ,
349+ set : readonly_error ,
350+
351+ get ( target , prop , receiver ) {
352+ const value = Reflect . get ( target , prop , receiver ) ;
353+
354+ if ( ! ( prop in target ) ) {
355+ return readonly ( value ) ;
356+ }
357+
358+ return value ;
359+ }
360+ } ;
0 commit comments