dalila 1.3.0 → 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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Auto-scoping Context API.
3
+ *
4
+ * Goal:
5
+ * - Remove the need for explicit `withScope()` in common app code.
6
+ *
7
+ * Auto-scope rules:
8
+ * - `provide()` outside a scope creates a global root scope (warns once in dev).
9
+ * - `inject()` outside a scope never creates global state; it only reads an existing global root.
10
+ *
11
+ * Rationale:
12
+ * - Apps often want "global-ish" DI without Provider pyramids.
13
+ * - Still keep things lifecycle-safe: the global scope can be disposed on page unload.
14
+ */
15
+ import { type Scope } from '../core/scope.js';
16
+ import { createContext, type ContextToken, type TryInjectResult } from './context.js';
17
+ export type AutoScopePolicy = "warn" | "throw" | "silent";
18
+ export declare function setAutoScopePolicy(policy: AutoScopePolicy): void;
19
+ /**
20
+ * Provide with auto-scope.
21
+ *
22
+ * Semantics:
23
+ * - Inside a scope: behaves like the raw `provide()`.
24
+ * - Outside a scope: creates/uses the global root scope (warns once in dev).
25
+ */
26
+ export declare function provide<T>(token: ContextToken<T>, value: T): void;
27
+ /**
28
+ * Provide explicitly in the global root scope.
29
+ *
30
+ * Semantics:
31
+ * - Always uses the detached global scope (creates if needed).
32
+ * - Never warns.
33
+ */
34
+ export declare function provideGlobal<T>(token: ContextToken<T>, value: T): void;
35
+ /**
36
+ * Inject with auto-scope.
37
+ *
38
+ * Semantics:
39
+ * - Inside a scope: behaves like raw `inject()`, but throws a more descriptive error.
40
+ * - Outside a scope:
41
+ * - If a global scope exists: reads from it (safe-by-default).
42
+ * - If no global scope exists: throws with guidance (does NOT create global state).
43
+ */
44
+ export declare function inject<T>(token: ContextToken<T>): T;
45
+ /**
46
+ * Try to inject a context value with auto-scope.
47
+ *
48
+ * Semantics:
49
+ * - Returns { found: true, value } when the token is found.
50
+ * - Returns { found: false, value: undefined } when not found.
51
+ * - Works both inside and outside scopes (reads global if exists).
52
+ */
53
+ export declare function tryInject<T>(token: ContextToken<T>): TryInjectResult<T>;
54
+ /**
55
+ * Inject explicitly from the global root scope.
56
+ *
57
+ * Semantics:
58
+ * - Reads only the global root scope.
59
+ * - Throws if no global scope exists yet.
60
+ */
61
+ export declare function injectGlobal<T>(token: ContextToken<T>): T;
62
+ /**
63
+ * Convenience helper: create a scope, run `fn` inside it, and return `{ result, dispose }`.
64
+ *
65
+ * Semantics:
66
+ * - If `fn` throws, the scope is disposed and the error is rethrown.
67
+ * - Caller owns disposal.
68
+ */
69
+ export declare function scope<T>(fn: () => T): {
70
+ result: T;
71
+ dispose: () => void;
72
+ };
73
+ /**
74
+ * Provider helper that bundles:
75
+ * - a dedicated provider scope
76
+ * - a value created by `setup()`
77
+ * - registration via `provide()`
78
+ *
79
+ * Useful for "feature modules" that want to expose a typed dependency with explicit lifetime.
80
+ */
81
+ export declare function createProvider<T>(token: ContextToken<T>, setup: () => T): {
82
+ create: () => {
83
+ value: T;
84
+ dispose: () => void;
85
+ };
86
+ use: () => T;
87
+ };
88
+ /**
89
+ * Returns the global root scope (if it exists).
90
+ * Intended for debugging/advanced usage only.
91
+ */
92
+ export declare function getGlobalScope(): Scope | null;
93
+ /**
94
+ * Returns true if a global root scope exists.
95
+ */
96
+ export declare function hasGlobalScope(): boolean;
97
+ /**
98
+ * Resets the global root scope.
99
+ * Intended for tests to ensure isolation between runs.
100
+ */
101
+ export declare function resetGlobalScope(): void;
102
+ export { createContext };
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Auto-scoping Context API.
3
+ *
4
+ * Goal:
5
+ * - Remove the need for explicit `withScope()` in common app code.
6
+ *
7
+ * Auto-scope rules:
8
+ * - `provide()` outside a scope creates a global root scope (warns once in dev).
9
+ * - `inject()` outside a scope never creates global state; it only reads an existing global root.
10
+ *
11
+ * Rationale:
12
+ * - Apps often want "global-ish" DI without Provider pyramids.
13
+ * - Still keep things lifecycle-safe: the global scope can be disposed on page unload.
14
+ */
15
+ import { getCurrentScope, createScope, isScopeDisposed, withScope } from '../core/scope.js';
16
+ import { isInDevMode } from '../core/dev.js';
17
+ import { createContext, provide as rawProvide, tryInject as rawTryInject, debugListAvailableContexts, } from './context.js';
18
+ /**
19
+ * Symbol key for storing the global root scope.
20
+ * Using Symbol.for() allows multiple instances of Dalila to share the same global scope,
21
+ * preventing conflicts when multiple libs use Dalila in the same app.
22
+ */
23
+ const DALILA_GLOBAL_SCOPE_KEY = Symbol.for('dalila:global-scope');
24
+ /**
25
+ * Global storage for the root scope using the symbol key.
26
+ * This allows sharing across multiple Dalila instances.
27
+ */
28
+ const globalStorage = globalThis;
29
+ /**
30
+ * Global root scope (lazy initialized).
31
+ *
32
+ * Used only when `provide()` is called outside a scope.
33
+ * `inject()` is safe-by-default and never creates this scope.
34
+ */
35
+ function getGlobalRootScope() {
36
+ return globalStorage[DALILA_GLOBAL_SCOPE_KEY] ?? null;
37
+ }
38
+ function setGlobalRootScope(scope) {
39
+ globalStorage[DALILA_GLOBAL_SCOPE_KEY] = scope;
40
+ }
41
+ /** Dev warning guard so we only warn once per page lifecycle. */
42
+ let warnedGlobalProvide = false;
43
+ /** Browser-only unload handler for global scope cleanup. */
44
+ let beforeUnloadHandler = null;
45
+ let autoScopePolicy = "throw";
46
+ export function setAutoScopePolicy(policy) {
47
+ autoScopePolicy = policy;
48
+ }
49
+ function warnGlobalProvideOnce() {
50
+ if (!isInDevMode())
51
+ return;
52
+ if (warnedGlobalProvide)
53
+ return;
54
+ warnedGlobalProvide = true;
55
+ console.warn('[Dalila] provide() called outside a scope. Using a global root scope (auto-scope). ' +
56
+ 'Prefer scope(() => { ... }) or provideGlobal() for explicit globals. ' +
57
+ 'You can also setAutoScopePolicy("throw") to prevent accidental globals.');
58
+ }
59
+ function detachBeforeUnloadListener() {
60
+ if (typeof window === 'undefined')
61
+ return;
62
+ if (!beforeUnloadHandler)
63
+ return;
64
+ if (!window.removeEventListener)
65
+ return;
66
+ window.removeEventListener('beforeunload', beforeUnloadHandler);
67
+ beforeUnloadHandler = null;
68
+ }
69
+ /**
70
+ * Returns the global root scope, creating it if needed.
71
+ *
72
+ * Notes:
73
+ * - Only `provide()` is allowed to create the global scope.
74
+ * - On browsers, we dispose the global scope on `beforeunload` to release resources.
75
+ * - We intentionally swallow errors during unload to avoid noisy teardown failures.
76
+ */
77
+ // Note: provideGlobal() can also create this scope explicitly.
78
+ function getOrCreateGlobalScope() {
79
+ let globalRootScope = getGlobalRootScope();
80
+ if (globalRootScope && isScopeDisposed(globalRootScope)) {
81
+ detachBeforeUnloadListener();
82
+ setGlobalRootScope(null);
83
+ warnedGlobalProvide = false;
84
+ globalRootScope = null;
85
+ }
86
+ if (!globalRootScope) {
87
+ globalRootScope = createScope(null);
88
+ setGlobalRootScope(globalRootScope);
89
+ if (typeof window !== 'undefined' && window.addEventListener && !beforeUnloadHandler) {
90
+ beforeUnloadHandler = () => {
91
+ detachBeforeUnloadListener();
92
+ try {
93
+ getGlobalRootScope()?.dispose();
94
+ }
95
+ catch {
96
+ // Do not throw during unload.
97
+ }
98
+ setGlobalRootScope(null);
99
+ warnedGlobalProvide = false;
100
+ };
101
+ window.addEventListener('beforeunload', beforeUnloadHandler);
102
+ }
103
+ }
104
+ return globalRootScope;
105
+ }
106
+ /**
107
+ * Provide with auto-scope.
108
+ *
109
+ * Semantics:
110
+ * - Inside a scope: behaves like the raw `provide()`.
111
+ * - Outside a scope: creates/uses the global root scope (warns once in dev).
112
+ */
113
+ export function provide(token, value) {
114
+ const currentScope = getCurrentScope();
115
+ if (currentScope) {
116
+ rawProvide(token, value);
117
+ }
118
+ else {
119
+ if (autoScopePolicy === "throw") {
120
+ throw new Error("[Dalila] provide() called outside a scope. " +
121
+ "Use scope(() => provide(...)) or provideGlobal() instead.");
122
+ }
123
+ if (autoScopePolicy === "warn") {
124
+ warnGlobalProvideOnce();
125
+ }
126
+ const globalScope = getOrCreateGlobalScope();
127
+ withScope(globalScope, () => {
128
+ rawProvide(token, value);
129
+ });
130
+ }
131
+ }
132
+ /**
133
+ * Provide explicitly in the global root scope.
134
+ *
135
+ * Semantics:
136
+ * - Always uses the detached global scope (creates if needed).
137
+ * - Never warns.
138
+ */
139
+ export function provideGlobal(token, value) {
140
+ const globalScope = getOrCreateGlobalScope();
141
+ withScope(globalScope, () => {
142
+ rawProvide(token, value);
143
+ });
144
+ }
145
+ /**
146
+ * Inject with auto-scope.
147
+ *
148
+ * Semantics:
149
+ * - Inside a scope: behaves like raw `inject()`, but throws a more descriptive error.
150
+ * - Outside a scope:
151
+ * - If a global scope exists: reads from it (safe-by-default).
152
+ * - If no global scope exists: throws with guidance (does NOT create global state).
153
+ */
154
+ export function inject(token) {
155
+ if (getCurrentScope()) {
156
+ const result = rawTryInject(token);
157
+ if (result.found)
158
+ return result.value;
159
+ throw new Error(createContextNotFoundError(token));
160
+ }
161
+ let globalRootScope = getGlobalRootScope();
162
+ if (globalRootScope && isScopeDisposed(globalRootScope)) {
163
+ setGlobalRootScope(null);
164
+ warnedGlobalProvide = false;
165
+ globalRootScope = null;
166
+ }
167
+ if (!globalRootScope) {
168
+ // Check for default value before throwing
169
+ if (token.defaultValue !== undefined) {
170
+ return token.defaultValue;
171
+ }
172
+ throw createInjectOutsideScopeError(token, 'No global scope exists yet.');
173
+ }
174
+ return withScope(globalRootScope, () => {
175
+ const result = rawTryInject(token);
176
+ if (result.found)
177
+ return result.value;
178
+ throw createInjectOutsideScopeError(token);
179
+ });
180
+ }
181
+ /**
182
+ * Try to inject a context value with auto-scope.
183
+ *
184
+ * Semantics:
185
+ * - Returns { found: true, value } when the token is found.
186
+ * - Returns { found: false, value: undefined } when not found.
187
+ * - Works both inside and outside scopes (reads global if exists).
188
+ */
189
+ export function tryInject(token) {
190
+ if (getCurrentScope()) {
191
+ return rawTryInject(token);
192
+ }
193
+ let globalRootScope = getGlobalRootScope();
194
+ if (globalRootScope && isScopeDisposed(globalRootScope)) {
195
+ setGlobalRootScope(null);
196
+ warnedGlobalProvide = false;
197
+ globalRootScope = null;
198
+ }
199
+ if (!globalRootScope) {
200
+ // Check for default value
201
+ if (token.defaultValue !== undefined) {
202
+ return { found: true, value: token.defaultValue };
203
+ }
204
+ return { found: false, value: undefined };
205
+ }
206
+ return withScope(globalRootScope, () => rawTryInject(token));
207
+ }
208
+ /**
209
+ * Inject explicitly from the global root scope.
210
+ *
211
+ * Semantics:
212
+ * - Reads only the global root scope.
213
+ * - Throws if no global scope exists yet.
214
+ */
215
+ export function injectGlobal(token) {
216
+ let globalRootScope = getGlobalRootScope();
217
+ if (globalRootScope && isScopeDisposed(globalRootScope)) {
218
+ setGlobalRootScope(null);
219
+ warnedGlobalProvide = false;
220
+ globalRootScope = null;
221
+ }
222
+ if (!globalRootScope) {
223
+ // Check for default value before throwing
224
+ if (token.defaultValue !== undefined) {
225
+ return token.defaultValue;
226
+ }
227
+ throw new Error("[Dalila] injectGlobal() called but no global scope exists yet. " +
228
+ "Use provideGlobal() first.");
229
+ }
230
+ return withScope(globalRootScope, () => {
231
+ const result = rawTryInject(token);
232
+ if (result.found)
233
+ return result.value;
234
+ throw new Error("[Dalila] injectGlobal() token not found in global scope. " +
235
+ "Provide it via provideGlobal() or call inject() inside the correct scope.");
236
+ });
237
+ }
238
+ /**
239
+ * Convenience helper: create a scope, run `fn` inside it, and return `{ result, dispose }`.
240
+ *
241
+ * Semantics:
242
+ * - If `fn` throws, the scope is disposed and the error is rethrown.
243
+ * - Caller owns disposal.
244
+ */
245
+ export function scope(fn) {
246
+ const parent = getCurrentScope();
247
+ const newScope = createScope(parent ?? null);
248
+ try {
249
+ const result = withScope(newScope, fn);
250
+ return {
251
+ result,
252
+ dispose: () => newScope.dispose(),
253
+ };
254
+ }
255
+ catch (err) {
256
+ newScope.dispose();
257
+ throw err;
258
+ }
259
+ }
260
+ /**
261
+ * Provider helper that bundles:
262
+ * - a dedicated provider scope
263
+ * - a value created by `setup()`
264
+ * - registration via `provide()`
265
+ *
266
+ * Useful for "feature modules" that want to expose a typed dependency with explicit lifetime.
267
+ */
268
+ export function createProvider(token, setup) {
269
+ return {
270
+ create() {
271
+ const parent = getCurrentScope();
272
+ const providerScope = createScope(parent ?? null);
273
+ const value = withScope(providerScope, () => {
274
+ const result = setup();
275
+ provide(token, result);
276
+ return result;
277
+ });
278
+ return {
279
+ value,
280
+ dispose: () => providerScope.dispose(),
281
+ };
282
+ },
283
+ use() {
284
+ return inject(token);
285
+ },
286
+ };
287
+ }
288
+ /**
289
+ * Builds a descriptive error message for missing contexts inside a scope hierarchy.
290
+ */
291
+ function createContextNotFoundError(token) {
292
+ const contextName = token.name || 'unnamed';
293
+ let message = `[Dalila] Context '${contextName}' not found in scope hierarchy.\n\n`;
294
+ message += `Possible causes:\n`;
295
+ message += ` 1. You forgot to call provide(token, value) in an ancestor scope\n`;
296
+ message += ` 2. You're calling inject() in a child scope, but provide() was in a sibling scope\n`;
297
+ message += ` 3. The scope where provide() was called has already been disposed\n\n`;
298
+ message += `How to fix:\n`;
299
+ message += ` • Make sure provide() is called in a parent scope\n`;
300
+ message += ` • Use scope(() => { provide(...); inject(...); }) to ensure the same hierarchy\n`;
301
+ message += ` • Check that the scope hasn't been disposed\n\n`;
302
+ if (isInDevMode()) {
303
+ const levels = debugListAvailableContexts(8);
304
+ if (levels.length > 0) {
305
+ message += `Available contexts by depth:\n`;
306
+ for (const level of levels) {
307
+ const names = level.tokens.map((t) => t.name).join(', ') || '(none)';
308
+ message += ` depth ${level.depth}: ${names}\n`;
309
+ }
310
+ message += `\n`;
311
+ }
312
+ }
313
+ message += `Learn more: https://github.com/evertondsvieira/dalila/blob/main/docs/context.md`;
314
+ return message;
315
+ }
316
+ function createInjectOutsideScopeError(token, extra) {
317
+ const name = token.name || 'unnamed';
318
+ return new Error(`[Dalila] Context '${name}' not found.\n\n` +
319
+ (extra ? `${extra}\n` : '') +
320
+ `You called inject() outside a scope. Either:\n` +
321
+ ` 1. Call provide(token, value) first, or\n` +
322
+ ` 2. Wrap your code in scope(() => { ... })\n\n` +
323
+ `Learn more: https://github.com/evertondsvieira/dalila/blob/main/docs/context.md`);
324
+ }
325
+ /**
326
+ * Returns the global root scope (if it exists).
327
+ * Intended for debugging/advanced usage only.
328
+ */
329
+ export function getGlobalScope() {
330
+ return getGlobalRootScope();
331
+ }
332
+ /**
333
+ * Returns true if a global root scope exists.
334
+ */
335
+ export function hasGlobalScope() {
336
+ return getGlobalRootScope() != null;
337
+ }
338
+ /**
339
+ * Resets the global root scope.
340
+ * Intended for tests to ensure isolation between runs.
341
+ */
342
+ export function resetGlobalScope() {
343
+ detachBeforeUnloadListener();
344
+ const globalRootScope = getGlobalRootScope();
345
+ if (globalRootScope) {
346
+ globalRootScope.dispose();
347
+ setGlobalRootScope(null);
348
+ }
349
+ warnedGlobalProvide = false;
350
+ autoScopePolicy = "throw";
351
+ }
352
+ // Re-export createContext for convenience.
353
+ export { createContext };
@@ -1,7 +1,111 @@
1
+ import { type Scope } from "../core/scope.js";
2
+ /**
3
+ * Context (Dependency Injection) — scope-based and hierarchical.
4
+ *
5
+ * Mental model:
6
+ * - A context value lives in the current Scope.
7
+ * - Child scopes can read values provided by ancestors (via Scope.parent chain).
8
+ * - Values are cleaned up automatically when the owning scope is disposed.
9
+ *
10
+ * Constraints (raw API):
11
+ * - `provide()` MUST be called inside a scope.
12
+ * - `inject()` MUST be called inside a scope.
13
+ *
14
+ * (Auto-scope behavior belongs in `dalila/context` wrapper — not here.)
15
+ */
16
+ /**
17
+ * Branded type marker for compile-time type safety.
18
+ * Using a unique symbol ensures tokens are not interchangeable even if they have the same T.
19
+ */
20
+ declare const ContextBrand: unique symbol;
21
+ /**
22
+ * ContextToken:
23
+ * - `name` is optional (used for debugging/errors)
24
+ * - `defaultValue` is optional (returned when no provider is found)
25
+ * - Branded with a unique symbol for type safety
26
+ */
1
27
  export interface ContextToken<T> {
2
28
  readonly name?: string;
3
- readonly _brand: T;
29
+ readonly defaultValue?: T;
30
+ readonly [ContextBrand]: T;
4
31
  }
5
- export declare function createContext<T>(name?: string): ContextToken<T>;
32
+ /**
33
+ * Create a new context token.
34
+ *
35
+ * Notes:
36
+ * - Tokens are identity-based: the same token instance is the key.
37
+ * - `name` is for developer-facing errors and debugging only.
38
+ * - `defaultValue` is returned by inject/tryInject when no provider is found.
39
+ */
40
+ export declare function createContext<T>(name?: string, defaultValue?: T): ContextToken<T>;
41
+ /**
42
+ * Configure the depth at which context lookup warns about deep hierarchies.
43
+ * Set to Infinity to disable the warning.
44
+ */
45
+ export declare function setDeepHierarchyWarnDepth(depth: number): void;
46
+ /**
47
+ * Provide a context value in the current scope.
48
+ *
49
+ * Rules:
50
+ * - Must be called inside a scope.
51
+ * - Overrides any existing value for the same token in this scope.
52
+ */
6
53
  export declare function provide<T>(token: ContextToken<T>, value: T): void;
54
+ /**
55
+ * Inject a context value from the current scope hierarchy.
56
+ *
57
+ * Rules:
58
+ * - Must be called inside a scope.
59
+ * - Walks up the parent chain until it finds the token.
60
+ * - If not found and token has a defaultValue, returns that.
61
+ * - Throws a descriptive error if not found and no default.
62
+ */
7
63
  export declare function inject<T>(token: ContextToken<T>): T;
64
+ /**
65
+ * Result of tryInject - distinguishes between "not found" and "found with undefined value".
66
+ */
67
+ export type TryInjectResult<T> = {
68
+ found: true;
69
+ value: T;
70
+ } | {
71
+ found: false;
72
+ value: undefined;
73
+ };
74
+ /**
75
+ * Inject a context value from the current scope hierarchy if present.
76
+ *
77
+ * Rules:
78
+ * - Must be called inside a scope.
79
+ * - Returns { found: true, value } when found.
80
+ * - Returns { found: false, value: undefined } when not found (or uses defaultValue if available).
81
+ */
82
+ export declare function tryInject<T>(token: ContextToken<T>): TryInjectResult<T>;
83
+ /**
84
+ * Inject a context value and return metadata about where it was resolved.
85
+ */
86
+ export declare function injectMeta<T>(token: ContextToken<T>): {
87
+ value: T;
88
+ ownerScope: Scope;
89
+ depth: number;
90
+ };
91
+ /**
92
+ * Debug helper: list available context tokens per depth.
93
+ */
94
+ export declare function debugListAvailableContexts(maxPerLevel?: number): Array<{
95
+ depth: number;
96
+ tokens: {
97
+ name: string;
98
+ token: ContextToken<any>;
99
+ }[];
100
+ }>;
101
+ /**
102
+ * Alias for debugListAvailableContexts (public helper name).
103
+ */
104
+ export declare function listAvailableContexts(maxPerLevel?: number): Array<{
105
+ depth: number;
106
+ tokens: {
107
+ name: string;
108
+ token: ContextToken<any>;
109
+ }[];
110
+ }>;
111
+ export {};