fict 0.1.0 → 0.2.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/src/resource.ts CHANGED
@@ -1,42 +1,177 @@
1
+ /**
2
+ * @fileoverview Async data fetching with caching and Suspense support.
3
+ *
4
+ * The `resource` function creates a reactive data fetcher that:
5
+ * - Automatically cancels in-flight requests when args change
6
+ * - Supports Suspense for loading states
7
+ * - Provides caching with TTL and stale-while-revalidate
8
+ * - Handles errors gracefully
9
+ */
10
+
1
11
  import { createEffect, onCleanup, createSuspenseToken } from '@fictjs/runtime'
2
12
  import { createSignal } from '@fictjs/runtime/advanced'
3
13
 
14
+ /**
15
+ * The result of reading a resource.
16
+ *
17
+ * @typeParam T - The type of data returned by the fetcher
18
+ */
4
19
  export interface ResourceResult<T> {
5
- data: T | undefined
6
- loading: boolean
7
- error: unknown
20
+ /** The fetched data, or undefined if not yet loaded or on error */
21
+ readonly data: T | undefined
22
+ /** Whether the resource is currently loading (initial fetch or refetch) */
23
+ readonly loading: boolean
24
+ /**
25
+ * Any error that occurred during fetching.
26
+ * Type is unknown since errors can be any value in JavaScript.
27
+ */
28
+ readonly error: unknown
29
+ /** Manually trigger a refetch of the resource */
8
30
  refresh: () => void
9
31
  }
10
32
 
33
+ /**
34
+ * Cache configuration options for a resource.
35
+ */
11
36
  export interface ResourceCacheOptions {
37
+ /**
38
+ * Caching mode:
39
+ * - `'memory'`: Cache responses in memory (default)
40
+ * - `'none'`: No caching, always refetch
41
+ * @default 'memory'
42
+ */
12
43
  mode?: 'memory' | 'none'
44
+
45
+ /**
46
+ * Time-to-live in milliseconds before cached data is considered stale.
47
+ * @default Infinity
48
+ */
13
49
  ttlMs?: number
50
+
51
+ /**
52
+ * If true, return stale cached data immediately while refetching in background.
53
+ * @default false
54
+ */
14
55
  staleWhileRevalidate?: boolean
56
+
57
+ /**
58
+ * If true, cache error responses as well.
59
+ * @default false
60
+ */
15
61
  cacheErrors?: boolean
16
62
  }
17
63
 
64
+ /**
65
+ * Configuration options for creating a resource.
66
+ *
67
+ * @typeParam T - The type of data returned by the fetcher
68
+ * @typeParam Args - The type of arguments passed to the fetcher
69
+ */
18
70
  export interface ResourceOptions<T, Args> {
19
- key?: unknown
71
+ /**
72
+ * Custom cache key. Can be a static value or a function that computes
73
+ * the key from the args. If not provided, args are used as the key.
74
+ */
75
+ key?: unknown | ((args: Args) => unknown)
76
+
77
+ /**
78
+ * The fetcher function that performs the async data retrieval.
79
+ * Receives an AbortController signal for cancellation support.
80
+ */
20
81
  fetch: (ctx: { signal: AbortSignal }, args: Args) => Promise<T>
82
+
83
+ /**
84
+ * If true, the resource will throw a Suspense token while loading,
85
+ * enabling React-like Suspense boundaries.
86
+ * @default false
87
+ */
21
88
  suspense?: boolean
89
+
90
+ /**
91
+ * Cache configuration options.
92
+ */
22
93
  cache?: ResourceCacheOptions
94
+
95
+ /**
96
+ * A value or reactive getter that, when changed, resets the resource.
97
+ * Useful for clearing cache when certain conditions change.
98
+ */
23
99
  reset?: unknown | (() => unknown)
24
100
  }
25
101
 
102
+ /**
103
+ * Return type of the resource factory.
104
+ *
105
+ * @typeParam T - The type of data returned by the fetcher
106
+ * @typeParam Args - The type of arguments passed to the fetcher
107
+ */
108
+ export interface Resource<T, Args> {
109
+ /**
110
+ * Read the resource data, triggering a fetch if needed.
111
+ * Can accept static args or a reactive getter.
112
+ *
113
+ * @param argsAccessor - Arguments or a getter returning arguments
114
+ */
115
+ read(argsAccessor: (() => Args) | Args): ResourceResult<T>
116
+
117
+ /**
118
+ * Invalidate cached data, causing the next read to refetch.
119
+ *
120
+ * @param key - Optional specific key to invalidate. If omitted, invalidates all.
121
+ */
122
+ invalidate(key?: unknown): void
123
+
124
+ /**
125
+ * Prefetch data without reading it. Useful for eager loading.
126
+ *
127
+ * @param args - Arguments to pass to the fetcher
128
+ * @param keyOverride - Optional cache key override
129
+ */
130
+ prefetch(args: Args, keyOverride?: unknown): void
131
+ }
132
+
133
+ /**
134
+ * Resource status values for tracking fetch lifecycle.
135
+ * @internal
136
+ */
137
+ export type ResourceStatus = 'idle' | 'pending' | 'success' | 'error'
138
+
139
+ /**
140
+ * Internal cache entry for a resource.
141
+ * Tracks the reactive state and metadata for a single cached fetch.
142
+ *
143
+ * @typeParam T - The type of data returned by the fetcher
144
+ * @typeParam Args - The type of arguments passed to the fetcher
145
+ * @internal
146
+ */
26
147
  interface ResourceEntry<T, Args> {
148
+ /** Reactive signal for the fetched data */
27
149
  data: ReturnType<typeof createSignal<T | undefined>>
150
+ /** Reactive signal for loading state */
28
151
  loading: ReturnType<typeof createSignal<boolean>>
152
+ /** Reactive signal for error state */
29
153
  error: ReturnType<typeof createSignal<unknown>>
154
+ /** Version counter for invalidation */
30
155
  version: ReturnType<typeof createSignal<number>>
156
+ /** Suspense token when using suspense mode */
31
157
  pendingToken: ReturnType<typeof createSuspenseToken> | null
158
+ /** Last used arguments for change detection */
32
159
  lastArgs: Args | undefined
160
+ /** Last seen version for change detection */
33
161
  lastVersion: number
162
+ /** Last reset token value for change detection */
34
163
  lastReset: unknown
164
+ /** Whether we have a valid cached value */
35
165
  hasValue: boolean
36
- status: 'idle' | 'pending' | 'success' | 'error'
166
+ /** Current fetch status */
167
+ status: ResourceStatus
168
+ /** Generation counter to handle race conditions */
37
169
  generation: number
170
+ /** Timestamp when the cached value expires */
38
171
  expiresAt: number | undefined
172
+ /** Currently in-flight fetch promise */
39
173
  inFlight: Promise<void> | undefined
174
+ /** AbortController for cancelling in-flight requests */
40
175
  controller: AbortController | undefined
41
176
  }
42
177
 
@@ -48,15 +183,52 @@ const defaultCacheOptions: Required<ResourceCacheOptions> = {
48
183
  }
49
184
 
50
185
  /**
51
- * Creates a resource factory that can be read with arguments.
186
+ * Create a reactive async data resource.
187
+ *
188
+ * Resources handle async data fetching with automatic caching, cancellation,
189
+ * and optional Suspense integration.
190
+ *
191
+ * @param optionsOrFetcher - A fetcher function or full configuration object
192
+ * @returns A resource factory with read, invalidate, and prefetch methods
193
+ *
194
+ * @example
195
+ * ```tsx
196
+ * import { resource } from 'fict'
197
+ *
198
+ * // Simple fetcher
199
+ * const userResource = resource(
200
+ * ({ signal }, userId: string) =>
201
+ * fetch(`/api/users/${userId}`, { signal }).then(r => r.json())
202
+ * )
203
+ *
204
+ * // With full options
205
+ * const postsResource = resource({
206
+ * fetch: ({ signal }, userId: string) =>
207
+ * fetch(`/api/users/${userId}/posts`, { signal }).then(r => r.json()),
208
+ * suspense: true,
209
+ * cache: {
210
+ * ttlMs: 60_000,
211
+ * staleWhileRevalidate: true,
212
+ * },
213
+ * })
214
+ *
215
+ * // Usage in component
216
+ * function UserProfile({ userId }: { userId: string }) {
217
+ * const { data, loading, error, refresh } = userResource.read(() => userId)
218
+ *
219
+ * if (loading) return <Spinner />
220
+ * if (error) return <ErrorMessage error={error} />
221
+ * return <div>{data.name}</div>
222
+ * }
223
+ * ```
52
224
  *
53
- * @param optionsOrFetcher - Configuration object or fetcher function
225
+ * @public
54
226
  */
55
227
  export function resource<T, Args = void>(
56
228
  optionsOrFetcher:
57
229
  | ((ctx: { signal: AbortSignal }, args: Args) => Promise<T>)
58
230
  | ResourceOptions<T, Args>,
59
- ) {
231
+ ): Resource<T, Args> {
60
232
  const fetcher = typeof optionsOrFetcher === 'function' ? optionsOrFetcher : optionsOrFetcher.fetch
61
233
  const useSuspense = typeof optionsOrFetcher === 'object' && !!optionsOrFetcher.suspense
62
234
  const cacheOptions: ResourceCacheOptions =
package/src/slim.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @fileoverview Fict Slim entrypoint
3
+ *
4
+ * Exposes compiler macros only. Intended for users who want the smallest
5
+ * runtime surface and rely on the compiler to erase macro calls.
6
+ *
7
+ * @public
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Compiler macro for reactive state.
13
+ * This is transformed at compile time and should never be called at runtime.
14
+ */
15
+ export function $state<T>(_initialValue: T): T {
16
+ throw new Error('$state() is a compiler macro and should be transformed at compile time')
17
+ }
18
+
19
+ /**
20
+ * Compiler macro for reactive effects.
21
+ * This is transformed at compile time and should never be called at runtime.
22
+ */
23
+ export function $effect(_fn: () => void | (() => void)): void {
24
+ throw new Error('$effect() is a compiler macro and should be transformed at compile time')
25
+ }
package/src/store.ts CHANGED
@@ -1,16 +1,46 @@
1
+ /**
2
+ * @fileoverview Deep reactive store implementation for Fict.
3
+ *
4
+ * $store creates a deeply reactive proxy that tracks property access at the path level.
5
+ * Unlike $state (which is shallow), $store allows direct mutation of nested properties.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const user = $store({ name: 'Alice', address: { city: 'London' } })
10
+ * user.address.city = 'Paris' // Fine-grained reactive update
11
+ * ```
12
+ */
13
+
1
14
  import { createSignal, type Signal } from '@fictjs/runtime/advanced'
2
15
 
16
+ /** Function type for bound methods */
3
17
  type AnyFn = (...args: unknown[]) => unknown
18
+
19
+ /** Cache entry for bound methods to preserve identity */
4
20
  interface BoundMethodEntry {
5
21
  ref: AnyFn
6
22
  bound: AnyFn
7
23
  }
8
24
 
25
+ /** Type for objects with indexable properties */
26
+ type IndexableObject = Record<string | symbol, unknown>
27
+
28
+ /** Cache of proxied objects to avoid duplicate proxies */
9
29
  const PROXY_CACHE = new WeakMap<object, unknown>()
30
+
31
+ /** Cache of signals per object property */
10
32
  const SIGNAL_CACHE = new WeakMap<object, Record<string | symbol, Signal<unknown>>>()
33
+
34
+ /** Cache of bound methods to preserve function identity across reads */
11
35
  const BOUND_METHOD_CACHE = new WeakMap<object, Map<string | symbol, BoundMethodEntry>>()
36
+
37
+ /** Special key for tracking iteration (Object.keys, for-in, etc.) */
12
38
  const ITERATE_KEY = Symbol('iterate')
13
39
 
40
+ /**
41
+ * Get or create a signal for a specific property on a target object.
42
+ * @internal
43
+ */
14
44
  function getSignal(target: object, prop: string | symbol): Signal<unknown> {
15
45
  let signals = SIGNAL_CACHE.get(target)
16
46
  if (!signals) {
@@ -18,13 +48,17 @@ function getSignal(target: object, prop: string | symbol): Signal<unknown> {
18
48
  SIGNAL_CACHE.set(target, signals)
19
49
  }
20
50
  if (!signals[prop]) {
21
- const initial = prop === ITERATE_KEY ? 0 : (target as Record<string | symbol, unknown>)[prop]
51
+ const initial = prop === ITERATE_KEY ? 0 : (target as IndexableObject)[prop]
22
52
  signals[prop] = createSignal(initial)
23
53
  }
24
54
  return signals[prop]
25
55
  }
26
56
 
27
- function triggerIteration(target: object) {
57
+ /**
58
+ * Trigger iteration signal to notify consumers that keys have changed.
59
+ * @internal
60
+ */
61
+ function triggerIteration(target: object): void {
28
62
  const signals = SIGNAL_CACHE.get(target)
29
63
  if (signals && signals[ITERATE_KEY]) {
30
64
  const current = signals[ITERATE_KEY]() as number
@@ -32,6 +66,35 @@ function triggerIteration(target: object) {
32
66
  }
33
67
  }
34
68
 
69
+ /**
70
+ * Create a deep reactive store using Proxy.
71
+ *
72
+ * Unlike `$state` (which is shallow and compiler-transformed), `$store` provides:
73
+ * - **Deep reactivity**: Nested objects are automatically wrapped in proxies
74
+ * - **Direct mutation**: Modify properties directly without spread operators
75
+ * - **Path-level tracking**: Only components reading changed paths re-render
76
+ *
77
+ * @param initialValue - The initial object to make reactive
78
+ * @returns A reactive proxy of the object
79
+ *
80
+ * @example
81
+ * ```tsx
82
+ * import { $store } from 'fict'
83
+ *
84
+ * const form = $store({
85
+ * user: { name: '', email: '' },
86
+ * settings: { theme: 'light' }
87
+ * })
88
+ *
89
+ * // Direct mutation works
90
+ * form.user.name = 'Alice'
91
+ *
92
+ * // In JSX - only updates when form.user.name changes
93
+ * <input value={form.user.name} />
94
+ * ```
95
+ *
96
+ * @public
97
+ */
35
98
  export function $store<T extends object>(initialValue: T): T {
36
99
  if (typeof initialValue !== 'object' || initialValue === null) {
37
100
  return initialValue
@@ -82,6 +145,7 @@ export function $store<T extends object>(initialValue: T): T {
82
145
  },
83
146
 
84
147
  set(target, prop, newValue, receiver) {
148
+ const oldLength = Array.isArray(target) && prop === 'length' ? target.length : undefined
85
149
  const oldValue = Reflect.get(target, prop, receiver)
86
150
  const hadKey = Object.prototype.hasOwnProperty.call(target, prop)
87
151
 
@@ -114,13 +178,24 @@ export function $store<T extends object>(initialValue: T): T {
114
178
  if (Array.isArray(target) && prop !== 'length') {
115
179
  const signals = SIGNAL_CACHE.get(target)
116
180
  if (signals && signals.length) {
117
- signals.length((target as unknown as { length: number }).length)
181
+ signals.length(target.length)
118
182
  }
119
183
  }
120
184
 
121
185
  // If it's an array and length changed implicitly, we might need to handle it.
122
- // But usually 'length' is set explicitly or handled by the runtime.
123
186
  if (Array.isArray(target) && prop === 'length') {
187
+ const nextLength = target.length
188
+ if (typeof oldLength === 'number' && nextLength < oldLength) {
189
+ const signals = SIGNAL_CACHE.get(target)
190
+ if (signals) {
191
+ for (let i = nextLength; i < oldLength; i += 1) {
192
+ const key = String(i)
193
+ if (signals[key]) {
194
+ signals[key](undefined)
195
+ }
196
+ }
197
+ }
198
+ }
124
199
  triggerIteration(target)
125
200
  }
126
201
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/store.ts"],"names":["signals"],"mappings":";;;AAQA,IAAM,WAAA,uBAAkB,OAAA,EAAyB;AACjD,IAAM,YAAA,uBAAmB,OAAA,EAA0D;AACnF,IAAM,kBAAA,uBAAyB,OAAA,EAAwD;AACvF,IAAM,WAAA,GAAc,OAAO,SAAS,CAAA;AAEpC,SAAS,SAAA,CAAU,QAAgB,IAAA,EAAwC;AACzE,EAAA,IAAI,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AACrC,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAA,GAAU,EAAC;AACX,IAAA,YAAA,CAAa,GAAA,CAAI,QAAQ,OAAO,CAAA;AAAA,EAClC;AACA,EAAA,IAAI,CAAC,OAAA,CAAQ,IAAI,CAAA,EAAG;AAClB,IAAA,MAAM,OAAA,GAAU,IAAA,KAAS,WAAA,GAAc,CAAA,GAAK,OAA4C,IAAI,CAAA;AAC5F,IAAA,OAAA,CAAQ,IAAI,CAAA,GAAI,YAAA,CAAa,OAAO,CAAA;AAAA,EACtC;AACA,EAAA,OAAO,QAAQ,IAAI,CAAA;AACrB;AAEA,SAAS,iBAAiB,MAAA,EAAgB;AACxC,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AACvC,EAAA,IAAI,OAAA,IAAW,OAAA,CAAQ,WAAW,CAAA,EAAG;AACnC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,WAAW,CAAA,EAAE;AACrC,IAAA,OAAA,CAAQ,WAAW,CAAA,CAAE,OAAA,GAAU,CAAC,CAAA;AAAA,EAClC;AACF;AAEO,SAAS,OAAyB,YAAA,EAAoB;AAC3D,EAAA,IAAI,OAAO,YAAA,KAAiB,QAAA,IAAY,YAAA,KAAiB,IAAA,EAAM;AAC7D,IAAA,OAAO,YAAA;AAAA,EACT;AAEA,EAAA,IAAI,WAAA,CAAY,GAAA,CAAI,YAAY,CAAA,EAAG;AACjC,IAAA,OAAO,WAAA,CAAY,IAAI,YAAY,CAAA;AAAA,EACrC;AAEA,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAM,YAAA,EAAc;AAAA,IACpC,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU;AAG1B,MAAA,MAAM,MAAA,GAAS,SAAA,CAAU,MAAA,EAAQ,IAAI,CAAA;AACrC,MAAA,MAAM,eAAe,MAAA,EAAO;AAE5B,MAAA,MAAM,eAAe,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,YAAY,KAAK,CAAA;AAChE,MAAA,IAAI,iBAAiB,YAAA,EAAc;AAIjC,QAAA,MAAA,CAAO,YAAY,CAAA;AAAA,MACrB;AAEA,MAAA,IAAI,OAAO,iBAAiB,UAAA,EAAY;AACtC,QAAA,IAAI,YAAA,GAAe,kBAAA,CAAmB,GAAA,CAAI,MAAM,CAAA;AAChD,QAAA,IAAI,CAAC,YAAA,EAAc;AACjB,UAAA,YAAA,uBAAmB,GAAA,EAAI;AACvB,UAAA,kBAAA,CAAmB,GAAA,CAAI,QAAQ,YAAY,CAAA;AAAA,QAC7C;AACA,QAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACpC,QAAA,IAAI,MAAA,IAAU,MAAA,CAAO,GAAA,KAAQ,YAAA,EAAc;AACzC,UAAA,OAAO,MAAA,CAAO,KAAA;AAAA,QAChB;AAEA,QAAA,MAAM,KAAA,GAAS,YAAA,CAAuB,IAAA,CAAK,QAAA,IAAY,KAAK,CAAA;AAC5D,QAAA,YAAA,CAAa,IAAI,IAAA,EAAM,EAAE,GAAA,EAAK,YAAA,EAAuB,OAAO,CAAA;AAC5D,QAAA,OAAO,KAAA;AAAA,MACT;AAGA,MAAA,IAAI,OAAO,YAAA,KAAiB,QAAA,IAAY,YAAA,KAAiB,IAAA,EAAM;AAC7D,QAAA,OAAO,OAAO,YAAuC,CAAA;AAAA,MACvD;AAGA,MAAA,OAAO,YAAA;AAAA,IACT,CAAA;AAAA,IAEA,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,QAAA,EAAU;AACpC,MAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,MAAM,QAAQ,CAAA;AACnD,MAAA,MAAM,SAAS,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,QAAQ,IAAI,CAAA;AAGhE,MAAA,IAAI,QAAA,KAAa,YAAY,MAAA,EAAQ;AACnC,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,MAAM,SAAS,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,UAAU,QAAQ,CAAA;AAG3D,MAAA,MAAM,YAAA,GAAe,kBAAA,CAAmB,GAAA,CAAI,MAAM,CAAA;AAClD,MAAA,IAAI,YAAA,IAAgB,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,EAAG;AAC1C,QAAA,YAAA,CAAa,OAAO,IAAI,CAAA;AAAA,MAC1B;AAGA,MAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AACvC,MAAA,IAAI,OAAA,IAAW,OAAA,CAAQ,IAAI,CAAA,EAAG;AAC5B,QAAA,OAAA,CAAQ,IAAI,EAAE,QAAQ,CAAA;AAAA,MACxB;AAGA,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,gBAAA,CAAiB,MAAM,CAAA;AAAA,MACzB;AAIA,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,IAAK,SAAS,QAAA,EAAU;AAC9C,QAAA,MAAMA,QAAAA,GAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AACvC,QAAA,IAAIA,QAAAA,IAAWA,SAAQ,MAAA,EAAQ;AAC7B,UAAAA,QAAAA,CAAQ,MAAA,CAAQ,MAAA,CAAyC,MAAM,CAAA;AAAA,QACjE;AAAA,MACF;AAIA,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,IAAK,SAAS,QAAA,EAAU;AAC9C,QAAA,gBAAA,CAAiB,MAAM,CAAA;AAAA,MACzB;AAEA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IAEA,cAAA,CAAe,QAAQ,IAAA,EAAM;AAC3B,MAAA,MAAM,SAAS,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,QAAQ,IAAI,CAAA;AAChE,MAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,cAAA,CAAe,MAAA,EAAQ,IAAI,CAAA;AAElD,MAAA,IAAI,UAAU,MAAA,EAAQ;AACpB,QAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AACvC,QAAA,IAAI,OAAA,IAAW,OAAA,CAAQ,IAAI,CAAA,EAAG;AAC5B,UAAA,OAAA,CAAQ,IAAI,EAAE,MAAS,CAAA;AAAA,QACzB;AAGA,QAAA,MAAM,YAAA,GAAe,kBAAA,CAAmB,GAAA,CAAI,MAAM,CAAA;AAClD,QAAA,IAAI,YAAA,IAAgB,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,EAAG;AAC1C,UAAA,YAAA,CAAa,OAAO,IAAI,CAAA;AAAA,QAC1B;AAEA,QAAA,gBAAA,CAAiB,MAAM,CAAA;AAAA,MACzB;AAEA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IAEA,QAAQ,MAAA,EAAQ;AACd,MAAA,SAAA,CAAU,MAAA,EAAQ,WAAW,CAAA,EAAE;AAC/B,MAAA,OAAO,OAAA,CAAQ,QAAQ,MAAM,CAAA;AAAA,IAC/B,CAAA;AAAA,IAEA,GAAA,CAAI,QAAQ,IAAA,EAAM;AAChB,MAAA,SAAA,CAAU,MAAA,EAAQ,IAAI,CAAA,EAAE;AACxB,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAAA,IACjC;AAAA,GACD,CAAA;AAED,EAAA,WAAA,CAAY,GAAA,CAAI,cAAc,KAAK,CAAA;AACnC,EAAA,OAAO,KAAA;AACT","file":"chunk-5AWNWCDT.js","sourcesContent":["import { createSignal, type Signal } from '@fictjs/runtime/advanced'\n\ntype AnyFn = (...args: unknown[]) => unknown\ninterface BoundMethodEntry {\n ref: AnyFn\n bound: AnyFn\n}\n\nconst PROXY_CACHE = new WeakMap<object, unknown>()\nconst SIGNAL_CACHE = new WeakMap<object, Record<string | symbol, Signal<unknown>>>()\nconst BOUND_METHOD_CACHE = new WeakMap<object, Map<string | symbol, BoundMethodEntry>>()\nconst ITERATE_KEY = Symbol('iterate')\n\nfunction getSignal(target: object, prop: string | symbol): Signal<unknown> {\n let signals = SIGNAL_CACHE.get(target)\n if (!signals) {\n signals = {}\n SIGNAL_CACHE.set(target, signals)\n }\n if (!signals[prop]) {\n const initial = prop === ITERATE_KEY ? 0 : (target as Record<string | symbol, unknown>)[prop]\n signals[prop] = createSignal(initial)\n }\n return signals[prop]\n}\n\nfunction triggerIteration(target: object) {\n const signals = SIGNAL_CACHE.get(target)\n if (signals && signals[ITERATE_KEY]) {\n const current = signals[ITERATE_KEY]() as number\n signals[ITERATE_KEY](current + 1)\n }\n}\n\nexport function $store<T extends object>(initialValue: T): T {\n if (typeof initialValue !== 'object' || initialValue === null) {\n return initialValue\n }\n\n if (PROXY_CACHE.has(initialValue)) {\n return PROXY_CACHE.get(initialValue) as T\n }\n\n const proxy = new Proxy(initialValue, {\n get(target, prop, receiver) {\n // Always touch the signal so reference changes to this property are tracked,\n // even if the value is an object we proxy further.\n const signal = getSignal(target, prop)\n const trackedValue = signal()\n\n const currentValue = Reflect.get(target, prop, receiver ?? proxy)\n if (currentValue !== trackedValue) {\n // If the value has changed (e.g. via direct mutation of the underlying object not via proxy),\n // we update the signal to keep it in sync.\n // Note: This is a bit of a heuristic. Ideally all mutations go through proxy.\n signal(currentValue)\n }\n\n if (typeof currentValue === 'function') {\n let boundMethods = BOUND_METHOD_CACHE.get(target)\n if (!boundMethods) {\n boundMethods = new Map()\n BOUND_METHOD_CACHE.set(target, boundMethods)\n }\n const cached = boundMethods.get(prop)\n if (cached && cached.ref === currentValue) {\n return cached.bound\n }\n\n const bound = (currentValue as AnyFn).bind(receiver ?? proxy)\n boundMethods.set(prop, { ref: currentValue as AnyFn, bound })\n return bound\n }\n\n // If the value is an object/array, we recursively wrap it in a store\n if (typeof currentValue === 'object' && currentValue !== null) {\n return $store(currentValue as Record<string, unknown>)\n }\n\n // For primitives (and functions), we return the signal value (which tracks the read)\n return currentValue\n },\n\n set(target, prop, newValue, receiver) {\n const oldValue = Reflect.get(target, prop, receiver)\n const hadKey = Object.prototype.hasOwnProperty.call(target, prop)\n\n // If value hasn't changed, do nothing\n if (oldValue === newValue && hadKey) {\n return true\n }\n\n const result = Reflect.set(target, prop, newValue, receiver)\n\n // IMPORTANT: Clear bound method cache BEFORE updating the signal\n const boundMethods = BOUND_METHOD_CACHE.get(target)\n if (boundMethods && boundMethods.has(prop)) {\n boundMethods.delete(prop)\n }\n\n // Update the signal if it exists\n const signals = SIGNAL_CACHE.get(target)\n if (signals && signals[prop]) {\n signals[prop](newValue)\n }\n\n // If new property, trigger iteration update\n if (!hadKey) {\n triggerIteration(target)\n }\n\n // Ensure array length subscribers are notified even if the native push/pop\n // doesn't trigger a separate set trap for \"length\" (defensive).\n if (Array.isArray(target) && prop !== 'length') {\n const signals = SIGNAL_CACHE.get(target)\n if (signals && signals.length) {\n signals.length((target as unknown as { length: number }).length)\n }\n }\n\n // If it's an array and length changed implicitly, we might need to handle it.\n // But usually 'length' is set explicitly or handled by the runtime.\n if (Array.isArray(target) && prop === 'length') {\n triggerIteration(target)\n }\n\n return result\n },\n\n deleteProperty(target, prop) {\n const hadKey = Object.prototype.hasOwnProperty.call(target, prop)\n const result = Reflect.deleteProperty(target, prop)\n\n if (result && hadKey) {\n const signals = SIGNAL_CACHE.get(target)\n if (signals && signals[prop]) {\n signals[prop](undefined)\n }\n\n // Clear bound method cache\n const boundMethods = BOUND_METHOD_CACHE.get(target)\n if (boundMethods && boundMethods.has(prop)) {\n boundMethods.delete(prop)\n }\n\n triggerIteration(target)\n }\n\n return result\n },\n\n ownKeys(target) {\n getSignal(target, ITERATE_KEY)()\n return Reflect.ownKeys(target)\n },\n\n has(target, prop) {\n getSignal(target, prop)()\n return Reflect.has(target, prop)\n },\n })\n\n PROXY_CACHE.set(initialValue, proxy)\n return proxy\n}\n"]}
@@ -1,3 +0,0 @@
1
- declare function $store<T extends object>(initialValue: T): T;
2
-
3
- export { $store as $ };
@@ -1,3 +0,0 @@
1
- declare function $store<T extends object>(initialValue: T): T;
2
-
3
- export { $store as $ };