-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathcontainers.svelte.ts
More file actions
129 lines (117 loc) · 4.12 KB
/
containers.svelte.ts
File metadata and controls
129 lines (117 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { SvelteSet, createSubscriber } from 'svelte/reactivity'
type VoidFn = () => void
type Subscriber = (update: VoidFn) => void | VoidFn
export type Box<T> = { current: T }
export class ReactiveValue<T> implements Box<T> {
#fn
#subscribe
constructor(fn: () => T, onSubscribe: Subscriber) {
this.#fn = fn
this.#subscribe = createSubscriber((update) => onSubscribe(update))
}
get current() {
this.#subscribe()
return this.#fn()
}
}
/**
* Makes all of the top-level keys of an object into $state.raw fields whose initial values
* are the same as in the original object. Does not mutate the original object. Provides an `update`
* function that _can_ (but does not have to be) be used to replace all of the object's top-level keys
* with the values of the new object, while maintaining the original root object's reference.
*/
export function createRawRef<T extends {} | Array<unknown>>(
init: T,
): [T, (newValue: T) => void] {
const refObj = (Array.isArray(init) ? [] : {}) as T
const hiddenKeys = new SvelteSet<PropertyKey>()
const out = new Proxy(refObj, {
set(target, prop, value, receiver) {
hiddenKeys.delete(prop)
if (prop in target) {
return Reflect.set(target, prop, value, receiver)
}
let state = $state.raw(value)
Object.defineProperty(target, prop, {
configurable: true,
enumerable: true,
get: () => {
// If this is a lazy value, we need to call it.
// We can't do something like typeof state === 'function'
// because the value could actually be a function that we don't want to call.
return state && isBranded(state) ? state() : state
},
set: (v) => {
state = v
},
})
return true
},
has: (target, prop) => {
if (hiddenKeys.has(prop)) {
return false
}
return prop in target
},
ownKeys(target) {
return Reflect.ownKeys(target).filter((key) => !hiddenKeys.has(key))
},
getOwnPropertyDescriptor(target, prop) {
if (hiddenKeys.has(prop)) {
return undefined
}
return Reflect.getOwnPropertyDescriptor(target, prop)
},
deleteProperty(target, prop) {
if (prop in target) {
// @ts-expect-error
// We need to set the value to undefined to signal to the listeners that the value has changed.
// If we just deleted it, the reactivity system wouldn't have any idea that the value was gone.
target[prop] = undefined
hiddenKeys.add(prop)
if (Array.isArray(target)) {
target.length--
}
return true
}
return false
},
})
/**
* Replaces the proxy-backed top-level keys in place while preserving the original reference.
*/
function update(newValue: T) {
const existingKeys = Object.keys(out)
const newKeys = Object.keys(newValue)
const keysToRemove = existingKeys.filter((key) => !newKeys.includes(key))
const keysToDelete = Array.isArray(out)
? [...keysToRemove].reverse()
: keysToRemove
for (const key of keysToDelete) {
// @ts-expect-error
delete out[key]
}
for (const key of newKeys) {
// @ts-expect-error
// This craziness is required because Tanstack Query defines getters for all of the keys on the object.
// These getters track property access, so if we access all of them here, we'll end up tracking everything.
// So we wrap the property access in a special function that we can identify later to lazily access the value.
// (See above)
out[key] = brand(() => newValue[key])
}
}
// we can't pass `init` directly into the proxy because it'll never set the state fields
// (because (prop in target) will always be true)
update(init)
return [out, update]
}
const lazyBrand = Symbol('LazyValue')
type Branded<T extends () => unknown> = T & { [lazyBrand]: true }
function brand<T extends () => unknown>(fn: T): Branded<T> {
// @ts-expect-error
fn[lazyBrand] = true
return fn as Branded<T>
}
function isBranded<T extends () => unknown>(fn: T): fn is Branded<T> {
return Boolean((fn as Branded<T>)[lazyBrand])
}