dalila 1.3.1 → 1.3.2

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.
@@ -1,66 +1,283 @@
1
- import { getCurrentScope } from '../core/scope.js';
2
- export function createContext(name) {
3
- return { name };
1
+ import { getCurrentScope } from "../core/scope.js";
2
+ import { isInDevMode } from "../core/dev.js";
3
+ /**
4
+ * Create a new context token.
5
+ *
6
+ * Notes:
7
+ * - Tokens are identity-based: the same token instance is the key.
8
+ * - `name` is for developer-facing errors and debugging only.
9
+ * - `defaultValue` is returned by inject/tryInject when no provider is found.
10
+ */
11
+ export function createContext(name, defaultValue) {
12
+ return { name, defaultValue };
4
13
  }
14
+ let globalRevision = 0;
15
+ let warnedDeepHierarchy = false;
16
+ /**
17
+ * Configurable deep hierarchy warning threshold.
18
+ */
19
+ let deepHierarchyWarnDepth = 50;
20
+ /**
21
+ * Configure the depth at which context lookup warns about deep hierarchies.
22
+ * Set to Infinity to disable the warning.
23
+ */
24
+ export function setDeepHierarchyWarnDepth(depth) {
25
+ deepHierarchyWarnDepth = depth;
26
+ }
27
+ function bumpRevision() {
28
+ globalRevision++;
29
+ }
30
+ function maybeWarnDeepHierarchy(depth) {
31
+ if (!isInDevMode())
32
+ return;
33
+ if (warnedDeepHierarchy)
34
+ return;
35
+ if (depth < deepHierarchyWarnDepth)
36
+ return;
37
+ warnedDeepHierarchy = true;
38
+ console.warn(`[Dalila] Context lookup traversed ${depth} parent scopes. ` +
39
+ `Consider flattening scope hierarchy or caching static contexts.`);
40
+ }
41
+ /**
42
+ * Per-scope registry that stores context values and links to a parent registry.
43
+ *
44
+ * Lookup:
45
+ * - `get()` checks current registry first.
46
+ * - If not found, it delegates to the parent registry (if any).
47
+ */
5
48
  class ContextRegistry {
6
- constructor(parent) {
49
+ constructor(ownerScope, parent) {
7
50
  this.contexts = new Map();
51
+ this.resolveCache = new Map();
52
+ this.ownerScope = ownerScope;
8
53
  this.parent = parent;
9
54
  }
10
55
  set(token, value) {
11
56
  this.contexts.set(token, { token, value });
57
+ this.resolveCache.delete(token);
12
58
  }
13
59
  get(token) {
14
- const value = this.contexts.get(token);
15
- if (value !== undefined) {
16
- return value.value;
60
+ const res = this.resolve(token);
61
+ return res ? res.value : undefined;
62
+ }
63
+ resolve(token) {
64
+ const cached = this.resolveCache.get(token);
65
+ if (cached && cached.rev === globalRevision)
66
+ return cached.res;
67
+ const hit = this.contexts.get(token);
68
+ if (hit !== undefined) {
69
+ const res = { value: hit.value, ownerScope: this.ownerScope, depth: 0 };
70
+ this.resolveCache.set(token, { rev: globalRevision, res });
71
+ return res;
17
72
  }
18
73
  if (this.parent) {
19
- return this.parent.get(token);
74
+ const parentRes = this.parent.resolve(token);
75
+ if (parentRes) {
76
+ const res = {
77
+ value: parentRes.value,
78
+ ownerScope: parentRes.ownerScope,
79
+ depth: parentRes.depth + 1,
80
+ };
81
+ this.resolveCache.set(token, { rev: globalRevision, res });
82
+ return res;
83
+ }
20
84
  }
85
+ this.resolveCache.set(token, { rev: globalRevision, res: undefined });
21
86
  return undefined;
22
87
  }
88
+ listTokens(maxPerLevel) {
89
+ const tokens = [];
90
+ for (const entry of this.contexts.values()) {
91
+ tokens.push(entry.token);
92
+ if (maxPerLevel != null && tokens.length >= maxPerLevel)
93
+ break;
94
+ }
95
+ return tokens;
96
+ }
97
+ /**
98
+ * Release references eagerly.
99
+ *
100
+ * Safety:
101
+ * - Scopes in Dalila cascade: disposing a parent scope disposes children,
102
+ * so no child scope should outlive a parent registry.
103
+ */
23
104
  clear() {
105
+ bumpRevision();
24
106
  this.contexts.clear();
25
107
  this.parent = undefined;
108
+ this.resolveCache.clear();
26
109
  }
27
110
  }
111
+ /**
112
+ * Registry storage keyed by Scope.
113
+ *
114
+ * Why WeakMap:
115
+ * - Avoid keeping scopes alive through registry bookkeeping.
116
+ */
28
117
  const scopeRegistries = new WeakMap();
118
+ /**
119
+ * Get (or lazily create) the registry for a given scope.
120
+ *
121
+ * Key detail:
122
+ * - Parent linkage is derived from `scope.parent` (captured at createScope time),
123
+ * not from `getCurrentScope()`. This makes the hierarchy stable and explicit.
124
+ *
125
+ * Cleanup:
126
+ * - When the scope is disposed, we clear the registry and remove it from the WeakMap.
127
+ */
29
128
  function getScopeRegistry(scope) {
30
129
  let registry = scopeRegistries.get(scope);
31
- if (!registry) {
32
- // Use the scope's parent reference (not getCurrentScope)
33
- const parentScope = scope.parent;
34
- const parentRegistry = parentScope ? getScopeRegistry(parentScope) : undefined;
35
- registry = new ContextRegistry(parentRegistry);
36
- scopeRegistries.set(scope, registry);
37
- const registryRef = registry;
38
- scope.onCleanup(() => {
39
- registryRef.clear();
40
- scopeRegistries.delete(scope);
41
- });
42
- }
130
+ if (registry)
131
+ return registry;
132
+ const parentScope = scope.parent;
133
+ const parentRegistry = parentScope ? getScopeRegistry(parentScope) : undefined;
134
+ registry = new ContextRegistry(scope, parentRegistry);
135
+ scopeRegistries.set(scope, registry);
136
+ const registryRef = registry;
137
+ scope.onCleanup(() => {
138
+ registryRef.clear();
139
+ scopeRegistries.delete(scope);
140
+ });
43
141
  return registry;
44
142
  }
143
+ /**
144
+ * Provide a context value in the current scope.
145
+ *
146
+ * Rules:
147
+ * - Must be called inside a scope.
148
+ * - Overrides any existing value for the same token in this scope.
149
+ */
45
150
  export function provide(token, value) {
46
151
  const scope = getCurrentScope();
47
152
  if (!scope) {
48
- throw new Error('[Dalila] provide() must be called within a scope');
153
+ throw new Error("[Dalila] provide() must be called within a scope. " +
154
+ "Use withScope(createScope(), () => provide(...)) or the auto-scope API.");
49
155
  }
50
156
  const registry = getScopeRegistry(scope);
157
+ bumpRevision();
51
158
  registry.set(token, value);
52
159
  }
160
+ /**
161
+ * Inject a context value from the current scope hierarchy.
162
+ *
163
+ * Rules:
164
+ * - Must be called inside a scope.
165
+ * - Walks up the parent chain until it finds the token.
166
+ * - If not found and token has a defaultValue, returns that.
167
+ * - Throws a descriptive error if not found and no default.
168
+ */
53
169
  export function inject(token) {
54
170
  const scope = getCurrentScope();
55
171
  if (!scope) {
56
- throw new Error('[Dalila] inject() must be called within a scope');
172
+ throw new Error("[Dalila] inject() must be called within a scope. " +
173
+ "Wrap your code in withScope(...) or use the auto-scope API.");
174
+ }
175
+ const registry = getScopeRegistry(scope);
176
+ const res = registry.resolve(token);
177
+ if (res) {
178
+ maybeWarnDeepHierarchy(res.depth);
179
+ return res.value;
180
+ }
181
+ // Check for default value
182
+ if (token.defaultValue !== undefined) {
183
+ return token.defaultValue;
184
+ }
185
+ const name = token.name || "unnamed";
186
+ let message = `[Dalila] Context "${name}" not found in scope hierarchy.`;
187
+ if (isInDevMode()) {
188
+ const levels = debugListAvailableContexts(8);
189
+ if (levels.length > 0) {
190
+ message += `\n\nAvailable contexts by depth:\n`;
191
+ for (const level of levels) {
192
+ const names = level.tokens.map((t) => t.name).join(", ") || "(none)";
193
+ message += ` depth ${level.depth}: ${names}\n`;
194
+ }
195
+ }
196
+ }
197
+ throw new Error(message);
198
+ }
199
+ /**
200
+ * Inject a context value from the current scope hierarchy if present.
201
+ *
202
+ * Rules:
203
+ * - Must be called inside a scope.
204
+ * - Returns { found: true, value } when found.
205
+ * - Returns { found: false, value: undefined } when not found (or uses defaultValue if available).
206
+ */
207
+ export function tryInject(token) {
208
+ const scope = getCurrentScope();
209
+ if (!scope) {
210
+ throw new Error("[Dalila] tryInject() must be called within a scope. " +
211
+ "Wrap your code in withScope(...) or use the auto-scope API.");
212
+ }
213
+ const registry = getScopeRegistry(scope);
214
+ const res = registry.resolve(token);
215
+ if (res) {
216
+ maybeWarnDeepHierarchy(res.depth);
217
+ return { found: true, value: res.value };
218
+ }
219
+ // Check for default value
220
+ if (token.defaultValue !== undefined) {
221
+ return { found: true, value: token.defaultValue };
222
+ }
223
+ return { found: false, value: undefined };
224
+ }
225
+ /**
226
+ * Inject a context value and return metadata about where it was resolved.
227
+ */
228
+ export function injectMeta(token) {
229
+ const scope = getCurrentScope();
230
+ if (!scope) {
231
+ throw new Error("[Dalila] injectMeta() must be called within a scope. " +
232
+ "Wrap your code in withScope(...) or use the auto-scope API.");
57
233
  }
58
- // The registry.get() method already walks up the parent chain,
59
- // so we only need to get the registry for the current scope
60
234
  const registry = getScopeRegistry(scope);
61
- const value = registry.get(token);
62
- if (value !== undefined) {
63
- return value;
235
+ const res = registry.resolve(token);
236
+ if (!res) {
237
+ const name = token.name || "unnamed";
238
+ let message = `[Dalila] Context "${name}" not found in scope hierarchy.`;
239
+ if (isInDevMode()) {
240
+ const levels = debugListAvailableContexts(8);
241
+ if (levels.length > 0) {
242
+ message += `\n\nAvailable contexts by depth:\n`;
243
+ for (const level of levels) {
244
+ const names = level.tokens.map((t) => t.name).join(", ") || "(none)";
245
+ message += ` depth ${level.depth}: ${names}\n`;
246
+ }
247
+ }
248
+ }
249
+ throw new Error(message);
64
250
  }
65
- throw new Error(`[Dalila] Context ${token.name || 'unnamed'} not found`);
251
+ maybeWarnDeepHierarchy(res.depth);
252
+ return { value: res.value, ownerScope: res.ownerScope, depth: res.depth };
253
+ }
254
+ /**
255
+ * Debug helper: list available context tokens per depth.
256
+ */
257
+ export function debugListAvailableContexts(maxPerLevel) {
258
+ const scope = getCurrentScope();
259
+ if (!scope)
260
+ return [];
261
+ const levels = [];
262
+ let depth = 0;
263
+ let current = scope;
264
+ while (current) {
265
+ const registry = scopeRegistries.get(current);
266
+ const tokens = registry
267
+ ? registry.listTokens(maxPerLevel).map((token) => ({
268
+ name: token.name || "unnamed",
269
+ token,
270
+ }))
271
+ : [];
272
+ levels.push({ depth, tokens });
273
+ current = current.parent;
274
+ depth++;
275
+ }
276
+ return levels;
277
+ }
278
+ /**
279
+ * Alias for debugListAvailableContexts (public helper name).
280
+ */
281
+ export function listAvailableContexts(maxPerLevel) {
282
+ return debugListAvailableContexts(maxPerLevel);
66
283
  }
@@ -1,3 +1,2 @@
1
- export { createContext, type ContextToken } from "./context.js";
2
- export { provide, inject, scope, createProvider, getGlobalScope, resetGlobalScope, } from "./auto-scope.js";
3
- export { provide as provideGlobalSafe, inject as injectGlobalSafe } from "./auto-scope.js";
1
+ export { createContext, setDeepHierarchyWarnDepth, listAvailableContexts, type ContextToken, type TryInjectResult, } from "./context.js";
2
+ export { provide, inject, tryInject, scope, createProvider, provideGlobal, injectGlobal, setAutoScopePolicy, hasGlobalScope, getGlobalScope, resetGlobalScope, } from "./auto-scope.js";
@@ -1,3 +1,2 @@
1
- export { createContext } from "./context.js";
2
- export { provide, inject, scope, createProvider, getGlobalScope, resetGlobalScope, } from "./auto-scope.js";
3
- export { provide as provideGlobalSafe, inject as injectGlobalSafe } from "./auto-scope.js";
1
+ export { createContext, setDeepHierarchyWarnDepth, listAvailableContexts, } from "./context.js";
2
+ export { provide, inject, tryInject, scope, createProvider, provideGlobal, injectGlobal, setAutoScopePolicy, hasGlobalScope, getGlobalScope, resetGlobalScope, } from "./auto-scope.js";
@@ -1,2 +1,2 @@
1
1
  export { createContext, type ContextToken } from "./context.js";
2
- export { provide, inject } from "./context.js";
2
+ export { provide, inject, tryInject, injectMeta, debugListAvailableContexts, listAvailableContexts, } from "./context.js";
@@ -1,2 +1,2 @@
1
1
  export { createContext } from "./context.js";
2
- export { provide, inject } from "./context.js";
2
+ export { provide, inject, tryInject, injectMeta, debugListAvailableContexts, listAvailableContexts, } from "./context.js";
@@ -11,4 +11,4 @@ export * from "./resource.js";
11
11
  export * from "./query.js";
12
12
  export * from "./mutation.js";
13
13
  export * from "./store.js";
14
- export { batch, measure, mutate } from "./scheduler.js";
14
+ export { batch, measure, mutate, configureScheduler, getSchedulerConfig } from "./scheduler.js";
@@ -11,4 +11,4 @@ export * from "./resource.js";
11
11
  export * from "./query.js";
12
12
  export * from "./mutation.js";
13
13
  export * from "./store.js";
14
- export { batch, measure, mutate } from "./scheduler.js";
14
+ export { batch, measure, mutate, configureScheduler, getSchedulerConfig } from "./scheduler.js";
@@ -1,5 +1,5 @@
1
1
  import { effect } from "./signal.js";
2
- import { createScope, getCurrentScope, withScope } from "./scope.js";
2
+ import { createScope, getCurrentScope, isScopeDisposed, withScope } from "./scope.js";
3
3
  import { scheduleMicrotask } from "./scheduler.js";
4
4
  /**
5
5
  * Multi-branch conditional DOM primitive with per-case lifetime.
@@ -35,6 +35,13 @@ export function match(value, cases) {
35
35
  /** Microtask coalescing: allow only one pending swap per tick. */
36
36
  let swapScheduled = false;
37
37
  let pendingKey = undefined;
38
+ /** Parent scope captured at creation time (if any). */
39
+ const parentScope = getCurrentScope();
40
+ const resolveParentScope = () => {
41
+ if (!parentScope)
42
+ return null;
43
+ return isScopeDisposed(parentScope) ? null : parentScope;
44
+ };
38
45
  /**
39
46
  * Guard to prevent "orphan" microtasks from touching DOM after this match()
40
47
  * is disposed by a parent scope.
@@ -89,7 +96,7 @@ export function match(value, cases) {
89
96
  */
90
97
  const swap = (key) => {
91
98
  clear();
92
- const nextScope = createScope();
99
+ const nextScope = createScope(resolveParentScope());
93
100
  try {
94
101
  const fn = cases[key];
95
102
  if (!fn) {
@@ -158,7 +165,6 @@ export function match(value, cases) {
158
165
  * - dispose current case scope,
159
166
  * - remove mounted nodes.
160
167
  */
161
- const parentScope = getCurrentScope();
162
168
  if (parentScope) {
163
169
  parentScope.onCleanup(() => {
164
170
  disposed = true;
@@ -1,8 +1,9 @@
1
1
  import { computed, effect } from "./signal.js";
2
- import { getCurrentScope } from "./scope.js";
2
+ import { getCurrentScope, createScope, withScope } from "./scope.js";
3
3
  import { key as keyBuilder, encodeKey } from "./key.js";
4
4
  import { createCachedResource, invalidateResourceCache, invalidateResourceTag, invalidateResourceTags, } from "./resource.js";
5
5
  import { createMutation } from "./mutation.js";
6
+ import { isInDevMode } from "./dev.js";
6
7
  /**
7
8
  * Query client (React Query-like API on top of Dalila resources).
8
9
  *
@@ -25,9 +26,31 @@ import { createMutation } from "./mutation.js";
25
26
  export function createQueryClient() {
26
27
  function makeQuery(cfg, behavior) {
27
28
  const scope = getCurrentScope();
29
+ const parentScope = scope;
28
30
  const staleTime = cfg.staleTime ?? 0;
29
31
  let staleTimer = null;
30
32
  let cleanupRegistered = false;
33
+ let keyScope = null;
34
+ let keyScopeCk = null;
35
+ if (isInDevMode() && !parentScope && behavior.persist === false) {
36
+ console.warn(`[Dalila] q.query() called outside a scope. ` +
37
+ `It will not cache and may leak. Use within a scope or q.queryGlobal().`);
38
+ }
39
+ function ensureKeyScope(ck) {
40
+ if (!parentScope)
41
+ return null;
42
+ if (keyScope && keyScopeCk === ck)
43
+ return keyScope;
44
+ // cancel any pending stale timer from the previous key
45
+ if (staleTimer != null) {
46
+ clearTimeout(staleTimer);
47
+ staleTimer = null;
48
+ }
49
+ keyScope?.dispose();
50
+ keyScopeCk = ck;
51
+ keyScope = createScope(parentScope);
52
+ return keyScope;
53
+ }
31
54
  /**
32
55
  * Schedules a stale-time revalidation after success.
33
56
  *
@@ -41,8 +64,17 @@ export function createQueryClient() {
41
64
  const scheduleStaleRevalidate = (r, expectedCk) => {
42
65
  if (staleTime <= 0)
43
66
  return;
67
+ if (!scope) {
68
+ if (isInDevMode()) {
69
+ console.warn(`[Dalila] staleTime requires a scope for cleanup. ` +
70
+ `Run the query inside a scope or disable staleTime.`);
71
+ }
72
+ return;
73
+ }
74
+ if (encodeKey(cfg.key()) !== expectedCk)
75
+ return;
44
76
  // Register cleanup once (if we have a scope).
45
- if (!cleanupRegistered && scope) {
77
+ if (!cleanupRegistered) {
46
78
  cleanupRegistered = true;
47
79
  scope.onCleanup(() => {
48
80
  if (staleTimer != null)
@@ -72,6 +104,7 @@ export function createQueryClient() {
72
104
  const k = cfg.key();
73
105
  const ck = encodeKey(k);
74
106
  let r;
107
+ const ks = ensureKeyScope(ck);
75
108
  const opts = {
76
109
  onError: cfg.onError,
77
110
  onSuccess: (data) => {
@@ -79,11 +112,17 @@ export function createQueryClient() {
79
112
  scheduleStaleRevalidate(r, ck);
80
113
  },
81
114
  persist: behavior.persist,
115
+ warnPersistWithoutTtl: behavior.warnPersistWithoutTtl,
116
+ fetchScope: ks ?? undefined,
82
117
  };
83
118
  if (cfg.initialValue !== undefined)
84
119
  opts.initialValue = cfg.initialValue;
85
120
  // Keyed cache entry (scope-safe unless persist is enabled).
86
- r = createCachedResource(ck, (sig) => cfg.fetch(sig, k), { ...opts, tags: cfg.tags });
121
+ const make = () => createCachedResource(ck, async (sig) => {
122
+ await Promise.resolve(); // break reactive tracking
123
+ return cfg.fetch(sig, k);
124
+ }, { ...opts, tags: cfg.tags });
125
+ r = ks ? withScope(ks, make) : make();
87
126
  return r;
88
127
  });
89
128
  /** Convenience derived status from the underlying resource. */
@@ -98,17 +137,11 @@ export function createQueryClient() {
98
137
  /** Expose the current encoded key as a computed signal. */
99
138
  const cacheKeySig = computed(() => encodeKey(cfg.key()));
100
139
  /**
101
- * Eager kick:
102
- * - computed() is lazy
103
- * - calling resource() starts the fetch immediately (resource auto-fetches on creation)
140
+ * Kick once so the initial query starts immediately.
141
+ * Then keep it reactive so key changes recreate the resource
142
+ * even if nobody reads data() / loading() / error().
104
143
  */
105
144
  resource();
106
- /**
107
- * Key-change driver:
108
- * - computed() only re-evaluates on read
109
- * - this effect reads resource() so key changes will recreate the resource
110
- * even if the consumer never reads data() / loading() / error()
111
- */
112
145
  effect(() => {
113
146
  resource();
114
147
  });
@@ -125,7 +158,7 @@ export function createQueryClient() {
125
158
  return makeQuery(cfg, { persist: false });
126
159
  }
127
160
  function queryGlobal(cfg) {
128
- return makeQuery(cfg, { persist: true });
161
+ return makeQuery(cfg, { persist: true, warnPersistWithoutTtl: false });
129
162
  }
130
163
  function mutation(cfg) {
131
164
  return createMutation(cfg);
@@ -1,4 +1,5 @@
1
1
  import { type Signal } from "./signal.js";
2
+ import { type Scope } from "./scope.js";
2
3
  /**
3
4
  * ResourceOptions:
4
5
  * - initialValue: optional initial data (null is allowed by design)
@@ -96,6 +97,40 @@ export type DepSource<D> = (() => D) | ReadonlyArray<Signal<any>> | {
96
97
  * Guard: early-return resolves only if `!loading()`.
97
98
  */
98
99
  export declare function createDependentResource<T, D>(fetchFn: (signal: AbortSignal, deps: D) => Promise<T>, deps: DepSource<D>, options?: ResourceOptions<T>): ResourceState<T>;
100
+ /**
101
+ * CacheEntry:
102
+ * A cached resource plus book-keeping for memory safety and invalidation.
103
+ *
104
+ * Fields:
105
+ * - createdAt/ttlMs: TTL-based expiry.
106
+ * - tags: tag index for invalidation.
107
+ * - stale: marker used by invalidation flows.
108
+ * - refCount: number of active scopes referencing this entry.
109
+ * - persist: if true, entry may live outside scopes (global cache).
110
+ * - cacheScope: dedicated scope that owns the underlying resource lifetime.
111
+ *
112
+ * Why cacheScope?
113
+ * - If a cached resource were created in the caller scope, it could be disposed
114
+ * prematurely when that scope ends, even though other scopes still reference it.
115
+ * - We isolate each cache entry in its own scope so the cache controls disposal.
116
+ */
117
+ type CacheEntry = {
118
+ resource: ResourceState<any>;
119
+ createdAt: number;
120
+ ttlMs?: number;
121
+ tags: Set<string>;
122
+ stale: boolean;
123
+ /** Active scoped users. */
124
+ refCount: number;
125
+ /** Explicit "keep global" flag. */
126
+ persist: boolean;
127
+ /**
128
+ * Dedicated scope for this cache entry.
129
+ * The resource is created inside this scope, isolating it from caller scopes.
130
+ * When the entry is removed from cache, this scope is disposed.
131
+ */
132
+ cacheScope: Scope;
133
+ };
99
134
  export interface CachedResourceOptions<T> extends ResourceOptions<T> {
100
135
  ttlMs?: number;
101
136
  tags?: readonly string[];
@@ -109,6 +144,16 @@ export interface CachedResourceOptions<T> extends ResourceOptions<T> {
109
144
  * Leave default as true to teach DX.
110
145
  */
111
146
  warnIfNoScope?: boolean;
147
+ /**
148
+ * Optional dev warning when persist is true without ttlMs.
149
+ * Leave default as true to teach DX.
150
+ */
151
+ warnPersistWithoutTtl?: boolean;
152
+ /**
153
+ * Optional scope to run fetchFn inside (for context lookup).
154
+ * Note: only the sync portion before the first await runs inside this scope.
155
+ */
156
+ fetchScope?: Scope | null;
112
157
  }
113
158
  /**
114
159
  * Global cache configuration for memory safety.
@@ -186,4 +231,69 @@ export declare function getResourceCacheKeysByTag(tag: string): string[];
186
231
  * - Outside a scope, it warns and returns a normal resource (no interval).
187
232
  */
188
233
  export declare function createAutoRefreshResource<T>(fetchFn: (signal: AbortSignal) => Promise<T>, refreshInterval: number, options?: ResourceOptions<T>): ResourceState<T>;
234
+ /**
235
+ * Isolated cache instance for SSR and testing.
236
+ *
237
+ * Use this when you need complete cache isolation:
238
+ * - SSR: each request gets its own cache
239
+ * - Testing: each test gets fresh state
240
+ *
241
+ * Example:
242
+ * ```ts
243
+ * const { createCachedResource, clearCache, invalidateKey, getCache } = createIsolatedCache();
244
+ *
245
+ * // Use the isolated createCachedResource instead of the global one
246
+ * const resource = createCachedResource('key', fetchFn);
247
+ *
248
+ * // Clean up when done
249
+ * clearCache();
250
+ * ```
251
+ */
252
+ export interface IsolatedCache {
253
+ /**
254
+ * Create a cached resource using this isolated cache.
255
+ */
256
+ createCachedResource: <T>(key: string, fetchFn: (signal: AbortSignal) => Promise<T>, options?: CachedResourceOptions<T>) => ResourceState<T>;
257
+ /**
258
+ * Clear all entries in this isolated cache.
259
+ */
260
+ clearCache: (key?: string) => void;
261
+ /**
262
+ * Invalidate a specific key in this isolated cache.
263
+ */
264
+ invalidateKey: (key: string, opts?: {
265
+ revalidate?: boolean;
266
+ force?: boolean;
267
+ }) => void;
268
+ /**
269
+ * Invalidate all entries with a specific tag.
270
+ */
271
+ invalidateTag: (tag: string, opts?: {
272
+ revalidate?: boolean;
273
+ force?: boolean;
274
+ }) => void;
275
+ /**
276
+ * Invalidate all entries with any of the specified tags.
277
+ */
278
+ invalidateTags: (tags: readonly string[], opts?: {
279
+ revalidate?: boolean;
280
+ force?: boolean;
281
+ }) => void;
282
+ /**
283
+ * Get the underlying cache Map (for debugging/inspection).
284
+ */
285
+ getCache: () => Map<string, CacheEntry>;
286
+ /**
287
+ * Get cache keys by tag.
288
+ */
289
+ getKeysByTag: (tag: string) => string[];
290
+ /**
291
+ * Configure cache limits for this instance.
292
+ */
293
+ configure: (config: Partial<{
294
+ maxEntries: number;
295
+ warnOnEviction: boolean;
296
+ }>) => void;
297
+ }
298
+ export declare function createIsolatedCache(): IsolatedCache;
189
299
  export {};