@zag-js/store 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/store",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "The reactive store package for zag machines",
5
5
  "keywords": [
6
6
  "js",
@@ -14,7 +14,8 @@
14
14
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/utilities/store",
15
15
  "sideEffects": false,
16
16
  "files": [
17
- "dist/**/*"
17
+ "dist",
18
+ "src"
18
19
  ],
19
20
  "publishConfig": {
20
21
  "access": "public"
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { proxy, ref, snapshot, subscribe, type Snapshot } from "./proxy"
2
+ export { proxyWithComputed } from "./proxy-computed"
3
+ export { subscribeKey } from "./subscribe-key"
@@ -0,0 +1,33 @@
1
+ import { proxy, snapshot, Snapshot } from "./proxy"
2
+
3
+ export function proxyWithComputed<T extends object, U extends object>(
4
+ initialObject: T,
5
+ computedFns: {
6
+ [K in keyof U]:
7
+ | ((snap: Snapshot<T>) => U[K])
8
+ | {
9
+ get: (snap: Snapshot<T>) => U[K]
10
+ set?: (state: T, newValue: U[K]) => void
11
+ }
12
+ },
13
+ ) {
14
+ const keys = Object.keys(computedFns) as (keyof U)[]
15
+ keys.forEach((key) => {
16
+ if (Object.getOwnPropertyDescriptor(initialObject, key)) {
17
+ throw new Error("object property already defined")
18
+ }
19
+ const computedFn = computedFns[key]
20
+ const { get, set } = (typeof computedFn === "function" ? { get: computedFn } : computedFn) as {
21
+ get: (snap: Snapshot<T>) => U[typeof key]
22
+ set?: (state: T, newValue: U[typeof key]) => void
23
+ }
24
+ const desc: PropertyDescriptor = {}
25
+ desc.get = () => get(snapshot(proxyObject))
26
+ if (set) {
27
+ desc.set = (newValue) => set(proxyObject, newValue)
28
+ }
29
+ Object.defineProperty(initialObject, key, desc)
30
+ })
31
+ const proxyObject = proxy(initialObject) as T & U
32
+ return proxyObject
33
+ }
package/src/proxy.ts ADDED
@@ -0,0 +1,346 @@
1
+ // Credits: https://github.com/pmndrs/valtio
2
+
3
+ import { getUntracked, markToTrack } from "proxy-compare"
4
+
5
+ const isDev = process.env.NODE_ENV !== "production"
6
+ const isObject = (x: unknown): x is object => typeof x === "object" && x !== null
7
+
8
+ type AsRef = { $$valtioRef: true }
9
+
10
+ type ProxyObject = object
11
+
12
+ type Path = (string | symbol)[]
13
+ type Op =
14
+ | [op: "set", path: Path, value: unknown, prevValue: unknown]
15
+ | [op: "delete", path: Path, prevValue: unknown]
16
+ | [op: "resolve", path: Path, value: unknown]
17
+ | [op: "reject", path: Path, error: unknown]
18
+ type Listener = (op: Op, nextVersion: number) => void
19
+
20
+ type AnyFunction = (...args: any[]) => any
21
+
22
+ export type Snapshot<T> = T extends AnyFunction
23
+ ? T
24
+ : T extends AsRef
25
+ ? T
26
+ : T extends Promise<any>
27
+ ? Awaited<T>
28
+ : {
29
+ readonly [K in keyof T]: Snapshot<T[K]>
30
+ }
31
+
32
+ type HandlePromise = <P extends Promise<any>>(promise: P) => Awaited<P>
33
+
34
+ type CreateSnapshot = <T extends object>(target: T, version: number, handlePromise?: HandlePromise) => T
35
+
36
+ type RemoveListener = () => void
37
+ type AddListener = (listener: Listener) => RemoveListener
38
+
39
+ type ProxyState = readonly [
40
+ target: object,
41
+ ensureVersion: (nextCheckVersion?: number) => number,
42
+ createSnapshot: CreateSnapshot,
43
+ addListener: AddListener,
44
+ ]
45
+
46
+ // shared state
47
+ const proxyStateMap = new WeakMap<ProxyObject, ProxyState>()
48
+ const refSet = new WeakSet()
49
+
50
+ const buildProxyFunction = (
51
+ objectIs = Object.is,
52
+
53
+ newProxy = <T extends object>(target: T, handler: ProxyHandler<T>): T => new Proxy(target, handler),
54
+
55
+ canProxy = (x: unknown) =>
56
+ isObject(x) &&
57
+ !refSet.has(x) &&
58
+ (Array.isArray(x) || !(Symbol.iterator in x)) &&
59
+ !(x instanceof WeakMap) &&
60
+ !(x instanceof WeakSet) &&
61
+ !(x instanceof Error) &&
62
+ !(x instanceof Number) &&
63
+ !(x instanceof Date) &&
64
+ !(x instanceof String) &&
65
+ !(x instanceof RegExp) &&
66
+ !(x instanceof ArrayBuffer),
67
+
68
+ defaultHandlePromise = <P extends Promise<any>>(
69
+ promise: P & {
70
+ status?: "pending" | "fulfilled" | "rejected"
71
+ value?: Awaited<P>
72
+ reason?: unknown
73
+ },
74
+ ) => {
75
+ switch (promise.status) {
76
+ case "fulfilled":
77
+ return promise.value as Awaited<P>
78
+ case "rejected":
79
+ throw promise.reason
80
+ default:
81
+ throw promise
82
+ }
83
+ },
84
+
85
+ snapCache = new WeakMap<object, [version: number, snap: unknown]>(),
86
+
87
+ createSnapshot: CreateSnapshot = <T extends object>(
88
+ target: T,
89
+ version: number,
90
+ handlePromise: HandlePromise = defaultHandlePromise,
91
+ ): T => {
92
+ const cache = snapCache.get(target)
93
+ if (cache?.[0] === version) {
94
+ return cache[1] as T
95
+ }
96
+ const snap: any = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target))
97
+ markToTrack(snap, true) // mark to track
98
+ snapCache.set(target, [version, snap])
99
+ Reflect.ownKeys(target).forEach((key) => {
100
+ const value = Reflect.get(target, key)
101
+ if (refSet.has(value as object)) {
102
+ markToTrack(value as object, false) // mark not to track
103
+ snap[key] = value
104
+ } else if (value instanceof Promise) {
105
+ Object.defineProperty(snap, key, {
106
+ get() {
107
+ return handlePromise(value)
108
+ },
109
+ })
110
+ } else if (proxyStateMap.has(value as object)) {
111
+ snap[key] = snapshot(value as object, handlePromise)
112
+ } else {
113
+ snap[key] = value
114
+ }
115
+ })
116
+ return Object.freeze(snap)
117
+ },
118
+
119
+ proxyCache = new WeakMap<object, ProxyObject>(),
120
+
121
+ versionHolder = [1, 1] as [number, number],
122
+
123
+ proxyFunction = <T extends object>(initialObject: T): T => {
124
+ if (!isObject(initialObject)) {
125
+ throw new Error("object required")
126
+ }
127
+ const found = proxyCache.get(initialObject) as T | undefined
128
+ if (found) {
129
+ return found
130
+ }
131
+ let version = versionHolder[0]
132
+ const listeners = new Set<Listener>()
133
+ const notifyUpdate = (op: Op, nextVersion = ++versionHolder[0]) => {
134
+ if (version !== nextVersion) {
135
+ version = nextVersion
136
+ listeners.forEach((listener) => listener(op, nextVersion))
137
+ }
138
+ }
139
+ let checkVersion = versionHolder[1]
140
+ const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => {
141
+ if (checkVersion !== nextCheckVersion && !listeners.size) {
142
+ checkVersion = nextCheckVersion
143
+ propProxyStates.forEach(([propProxyState]) => {
144
+ const propVersion = propProxyState[1](nextCheckVersion)
145
+ if (propVersion > version) {
146
+ version = propVersion
147
+ }
148
+ })
149
+ }
150
+ return version
151
+ }
152
+ const createPropListener =
153
+ (prop: string | symbol): Listener =>
154
+ (op, nextVersion) => {
155
+ const newOp: Op = [...op]
156
+ newOp[1] = [prop, ...(newOp[1] as Path)]
157
+ notifyUpdate(newOp, nextVersion)
158
+ }
159
+ const propProxyStates = new Map<string | symbol, readonly [ProxyState, RemoveListener?]>()
160
+ const addPropListener = (prop: string | symbol, propProxyState: ProxyState) => {
161
+ if (isDev && propProxyStates.has(prop)) {
162
+ throw new Error("prop listener already exists")
163
+ }
164
+ if (listeners.size) {
165
+ const remove = propProxyState[3](createPropListener(prop))
166
+ propProxyStates.set(prop, [propProxyState, remove])
167
+ } else {
168
+ propProxyStates.set(prop, [propProxyState])
169
+ }
170
+ }
171
+ const removePropListener = (prop: string | symbol) => {
172
+ const entry = propProxyStates.get(prop)
173
+ if (entry) {
174
+ propProxyStates.delete(prop)
175
+ entry[1]?.()
176
+ }
177
+ }
178
+ const addListener = (listener: Listener) => {
179
+ listeners.add(listener)
180
+ if (listeners.size === 1) {
181
+ propProxyStates.forEach(([propProxyState, prevRemove], prop) => {
182
+ if (isDev && prevRemove) {
183
+ throw new Error("remove already exists")
184
+ }
185
+ const remove = propProxyState[3](createPropListener(prop))
186
+ propProxyStates.set(prop, [propProxyState, remove])
187
+ })
188
+ }
189
+ const removeListener = () => {
190
+ listeners.delete(listener)
191
+ if (listeners.size === 0) {
192
+ propProxyStates.forEach(([propProxyState, remove], prop) => {
193
+ if (remove) {
194
+ remove()
195
+ propProxyStates.set(prop, [propProxyState])
196
+ }
197
+ })
198
+ }
199
+ }
200
+ return removeListener
201
+ }
202
+ const baseObject = Array.isArray(initialObject) ? [] : Object.create(Object.getPrototypeOf(initialObject))
203
+ const handler: ProxyHandler<T> = {
204
+ deleteProperty(target: T, prop: string | symbol) {
205
+ const prevValue = Reflect.get(target, prop)
206
+ removePropListener(prop)
207
+ const deleted = Reflect.deleteProperty(target, prop)
208
+ if (deleted) {
209
+ notifyUpdate(["delete", [prop], prevValue])
210
+ }
211
+ return deleted
212
+ },
213
+ set(target: T, prop: string | symbol, value: any, receiver: object) {
214
+ const hasPrevValue = Reflect.has(target, prop)
215
+ const prevValue = Reflect.get(target, prop, receiver)
216
+ if (
217
+ hasPrevValue &&
218
+ (objectIs(prevValue, value) || (proxyCache.has(value) && objectIs(prevValue, proxyCache.get(value))))
219
+ ) {
220
+ return true
221
+ }
222
+ removePropListener(prop)
223
+ if (isObject(value)) {
224
+ value = getUntracked(value) || value
225
+ }
226
+ let nextValue = value
227
+ if (Object.getOwnPropertyDescriptor(target, prop)?.set) {
228
+ // do nothing
229
+ } else if (value instanceof Promise) {
230
+ value
231
+ .then((v) => {
232
+ value.status = "fulfilled"
233
+ value.value = v
234
+ notifyUpdate(["resolve", [prop], v])
235
+ })
236
+ .catch((e) => {
237
+ value.status = "rejected"
238
+ value.reason = e
239
+ notifyUpdate(["reject", [prop], e])
240
+ })
241
+ } else {
242
+ if (!proxyStateMap.has(value) && canProxy(value)) {
243
+ nextValue = proxy(value)
244
+ }
245
+ const childProxyState = !refSet.has(nextValue) && proxyStateMap.get(nextValue)
246
+ if (childProxyState) {
247
+ addPropListener(prop, childProxyState)
248
+ }
249
+ }
250
+ Reflect.set(target, prop, nextValue, receiver)
251
+ notifyUpdate(["set", [prop], value, prevValue])
252
+ return true
253
+ },
254
+ }
255
+ const proxyObject = newProxy(baseObject, handler)
256
+ proxyCache.set(initialObject, proxyObject)
257
+ const proxyState: ProxyState = [baseObject, ensureVersion, createSnapshot, addListener]
258
+ proxyStateMap.set(proxyObject, proxyState)
259
+ Reflect.ownKeys(initialObject).forEach((key) => {
260
+ const desc = Object.getOwnPropertyDescriptor(initialObject, key) as PropertyDescriptor
261
+ if (desc.get || desc.set) {
262
+ Object.defineProperty(baseObject, key, desc)
263
+ } else {
264
+ proxyObject[key as keyof T] = initialObject[key as keyof T]
265
+ }
266
+ })
267
+ return proxyObject
268
+ },
269
+ ) =>
270
+ [
271
+ // public functions
272
+ proxyFunction,
273
+ // shared state
274
+ proxyStateMap,
275
+ refSet,
276
+ // internal things
277
+ objectIs,
278
+ newProxy,
279
+ canProxy,
280
+ defaultHandlePromise,
281
+ snapCache,
282
+ createSnapshot,
283
+ proxyCache,
284
+ versionHolder,
285
+ ] as const
286
+
287
+ const [proxyFunction] = buildProxyFunction()
288
+
289
+ export function proxy<T extends object>(initialObject: T = {} as T): T {
290
+ return proxyFunction(initialObject)
291
+ }
292
+
293
+ export function getVersion(proxyObject: unknown): number | undefined {
294
+ const proxyState = proxyStateMap.get(proxyObject as object)
295
+ return proxyState?.[1]()
296
+ }
297
+
298
+ export function subscribe<T extends object>(
299
+ proxyObject: T,
300
+ callback: (ops: Op[]) => void,
301
+ notifyInSync?: boolean,
302
+ ): () => void {
303
+ const proxyState = proxyStateMap.get(proxyObject as object)
304
+ if (isDev && !proxyState) {
305
+ console.warn("Please use proxy object")
306
+ }
307
+ let promise: Promise<void> | undefined
308
+ const ops: Op[] = []
309
+ const addListener = (proxyState as ProxyState)[3]
310
+ let isListenerActive = false
311
+ const listener: Listener = (op) => {
312
+ ops.push(op)
313
+ if (notifyInSync) {
314
+ callback(ops.splice(0))
315
+ return
316
+ }
317
+ if (!promise) {
318
+ promise = Promise.resolve().then(() => {
319
+ promise = undefined
320
+ if (isListenerActive) {
321
+ callback(ops.splice(0))
322
+ }
323
+ })
324
+ }
325
+ }
326
+ const removeListener = addListener(listener)
327
+ isListenerActive = true
328
+ return () => {
329
+ isListenerActive = false
330
+ removeListener()
331
+ }
332
+ }
333
+
334
+ export function snapshot<T extends object>(proxyObject: T, handlePromise?: HandlePromise): Snapshot<T> {
335
+ const proxyState = proxyStateMap.get(proxyObject as object)
336
+ if (isDev && !proxyState) {
337
+ console.warn("Please use proxy object")
338
+ }
339
+ const [target, ensureVersion, createSnapshot] = proxyState as ProxyState
340
+ return createSnapshot(target, ensureVersion(), handlePromise) as Snapshot<T>
341
+ }
342
+
343
+ export function ref<T extends object>(obj: T): T & AsRef {
344
+ refSet.add(obj)
345
+ return obj as T & AsRef
346
+ }
@@ -0,0 +1,23 @@
1
+ import { snapshot, subscribe } from "./proxy"
2
+
3
+ export type CompareFn<T = any> = (prev: T, next: T) => boolean
4
+
5
+ const defaultCompareFn: CompareFn = (prev, next) => Object.is(prev, next)
6
+
7
+ export function subscribeKey<T extends object, K extends keyof T>(
8
+ obj: T,
9
+ key: K,
10
+ fn: (value: T[K]) => void,
11
+ sync?: boolean,
12
+ compareFn?: (prev: T[K], next: T[K]) => boolean,
13
+ ) {
14
+ let prev: any = Reflect.get(snapshot(obj), key)
15
+ const isEqual = compareFn || defaultCompareFn
16
+ function onSnapshotChange() {
17
+ const snap = snapshot(obj) as T
18
+ if (isEqual(prev, snap[key])) return
19
+ fn(snap[key])
20
+ prev = Reflect.get(snap, key)
21
+ }
22
+ return subscribe(obj, onSnapshotChange, sync)
23
+ }