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.
- package/dist/context/auto-scope.d.ts +65 -49
- package/dist/context/auto-scope.js +208 -88
- package/dist/context/context.d.ts +106 -2
- package/dist/context/context.js +245 -28
- package/dist/context/index.d.ts +2 -3
- package/dist/context/index.js +2 -3
- package/dist/context/raw.d.ts +1 -1
- package/dist/context/raw.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/match.js +9 -3
- package/dist/core/query.js +46 -13
- package/dist/core/resource.d.ts +110 -0
- package/dist/core/resource.js +280 -39
- package/dist/core/scheduler.d.ts +31 -0
- package/dist/core/scheduler.js +26 -13
- package/dist/core/scope.d.ts +17 -1
- package/dist/core/scope.js +62 -7
- package/dist/core/signal.d.ts +22 -0
- package/dist/core/signal.js +86 -0
- package/dist/core/when.js +9 -3
- package/package.json +1 -1
|
@@ -1,72 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-scoping Context API
|
|
2
|
+
* Auto-scoping Context API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
14
|
*/
|
|
7
15
|
import { type Scope } from '../core/scope.js';
|
|
8
|
-
import { createContext, type ContextToken } from './context.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;
|
|
9
19
|
/**
|
|
10
|
-
* Provide with auto-scope
|
|
11
|
-
*
|
|
12
|
-
* Se chamado fora de scope, cria um global scope automaticamente
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* // ✅ ANTES (verboso)
|
|
17
|
-
* const scope = createScope();
|
|
18
|
-
* withScope(scope, () => {
|
|
19
|
-
* provide(Theme, 'dark');
|
|
20
|
-
* });
|
|
20
|
+
* Provide with auto-scope.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
25
|
*/
|
|
26
26
|
export declare function provide<T>(token: ContextToken<T>, value: T): void;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Provide explicitly in the global root scope.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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).
|
|
32
43
|
*/
|
|
33
44
|
export declare function inject<T>(token: ContextToken<T>): T;
|
|
34
45
|
/**
|
|
35
|
-
*
|
|
46
|
+
* Try to inject a context value with auto-scope.
|
|
36
47
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
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 }`.
|
|
45
64
|
*
|
|
46
|
-
*
|
|
47
|
-
* scope
|
|
48
|
-
*
|
|
49
|
-
* createApp();
|
|
50
|
-
* });
|
|
51
|
-
* ```
|
|
65
|
+
* Semantics:
|
|
66
|
+
* - If `fn` throws, the scope is disposed and the error is rethrown.
|
|
67
|
+
* - Caller owns disposal.
|
|
52
68
|
*/
|
|
53
69
|
export declare function scope<T>(fn: () => T): {
|
|
54
70
|
result: T;
|
|
55
71
|
dispose: () => void;
|
|
56
72
|
};
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
74
|
+
* Provider helper that bundles:
|
|
75
|
+
* - a dedicated provider scope
|
|
76
|
+
* - a value created by `setup()`
|
|
77
|
+
* - registration via `provide()`
|
|
59
78
|
*
|
|
60
|
-
*
|
|
61
|
-
* ```typescript
|
|
62
|
-
* const ThemeProvider = createProvider(ThemeContext, () => {
|
|
63
|
-
* const theme = signal('dark');
|
|
64
|
-
* return { theme };
|
|
65
|
-
* });
|
|
66
|
-
*
|
|
67
|
-
* const { value, dispose } = ThemeProvider.create();
|
|
68
|
-
* // use value.theme()
|
|
69
|
-
* ```
|
|
79
|
+
* Useful for "feature modules" that want to expose a typed dependency with explicit lifetime.
|
|
70
80
|
*/
|
|
71
81
|
export declare function createProvider<T>(token: ContextToken<T>, setup: () => T): {
|
|
72
82
|
create: () => {
|
|
@@ -76,11 +86,17 @@ export declare function createProvider<T>(token: ContextToken<T>, setup: () => T
|
|
|
76
86
|
use: () => T;
|
|
77
87
|
};
|
|
78
88
|
/**
|
|
79
|
-
*
|
|
89
|
+
* Returns the global root scope (if it exists).
|
|
90
|
+
* Intended for debugging/advanced usage only.
|
|
80
91
|
*/
|
|
81
92
|
export declare function getGlobalScope(): Scope | null;
|
|
82
93
|
/**
|
|
83
|
-
*
|
|
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.
|
|
84
100
|
*/
|
|
85
101
|
export declare function resetGlobalScope(): void;
|
|
86
102
|
export { createContext };
|
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-scoping Context API
|
|
2
|
+
* Auto-scoping Context API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
14
|
*/
|
|
7
|
-
import { getCurrentScope, createScope, withScope } from '../core/scope.js';
|
|
15
|
+
import { getCurrentScope, createScope, isScopeDisposed, withScope } from '../core/scope.js';
|
|
8
16
|
import { isInDevMode } from '../core/dev.js';
|
|
9
|
-
import { createContext, provide as rawProvide,
|
|
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;
|
|
10
29
|
/**
|
|
11
|
-
* Global root scope (lazy initialized)
|
|
12
|
-
* Usado quando provide/inject são chamados sem scope
|
|
30
|
+
* Global root scope (lazy initialized).
|
|
13
31
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - inject() outside a scope never creates global state; it only reads an existing global root.
|
|
32
|
+
* Used only when `provide()` is called outside a scope.
|
|
33
|
+
* `inject()` is safe-by-default and never creates this scope.
|
|
17
34
|
*/
|
|
18
|
-
|
|
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. */
|
|
19
42
|
let warnedGlobalProvide = false;
|
|
43
|
+
/** Browser-only unload handler for global scope cleanup. */
|
|
20
44
|
let beforeUnloadHandler = null;
|
|
45
|
+
let autoScopePolicy = "throw";
|
|
46
|
+
export function setAutoScopePolicy(policy) {
|
|
47
|
+
autoScopePolicy = policy;
|
|
48
|
+
}
|
|
21
49
|
function warnGlobalProvideOnce() {
|
|
22
50
|
if (!isInDevMode())
|
|
23
51
|
return;
|
|
@@ -25,7 +53,8 @@ function warnGlobalProvideOnce() {
|
|
|
25
53
|
return;
|
|
26
54
|
warnedGlobalProvide = true;
|
|
27
55
|
console.warn('[Dalila] provide() called outside a scope. Using a global root scope (auto-scope). ' +
|
|
28
|
-
'Prefer scope(() => { ... }) for
|
|
56
|
+
'Prefer scope(() => { ... }) or provideGlobal() for explicit globals. ' +
|
|
57
|
+
'You can also setAutoScopePolicy("throw") to prevent accidental globals.');
|
|
29
58
|
}
|
|
30
59
|
function detachBeforeUnloadListener() {
|
|
31
60
|
if (typeof window === 'undefined')
|
|
@@ -37,21 +66,36 @@ function detachBeforeUnloadListener() {
|
|
|
37
66
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
38
67
|
beforeUnloadHandler = null;
|
|
39
68
|
}
|
|
40
|
-
|
|
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.
|
|
41
78
|
function getOrCreateGlobalScope() {
|
|
79
|
+
let globalRootScope = getGlobalRootScope();
|
|
80
|
+
if (globalRootScope && isScopeDisposed(globalRootScope)) {
|
|
81
|
+
detachBeforeUnloadListener();
|
|
82
|
+
setGlobalRootScope(null);
|
|
83
|
+
warnedGlobalProvide = false;
|
|
84
|
+
globalRootScope = null;
|
|
85
|
+
}
|
|
42
86
|
if (!globalRootScope) {
|
|
43
|
-
globalRootScope = createScope();
|
|
87
|
+
globalRootScope = createScope(null);
|
|
88
|
+
setGlobalRootScope(globalRootScope);
|
|
44
89
|
if (typeof window !== 'undefined' && window.addEventListener && !beforeUnloadHandler) {
|
|
45
|
-
// Cleanup on page unload (browser only)
|
|
46
90
|
beforeUnloadHandler = () => {
|
|
47
91
|
detachBeforeUnloadListener();
|
|
48
92
|
try {
|
|
49
|
-
|
|
93
|
+
getGlobalRootScope()?.dispose();
|
|
50
94
|
}
|
|
51
95
|
catch {
|
|
52
|
-
//
|
|
96
|
+
// Do not throw during unload.
|
|
53
97
|
}
|
|
54
|
-
|
|
98
|
+
setGlobalRootScope(null);
|
|
55
99
|
warnedGlobalProvide = false;
|
|
56
100
|
};
|
|
57
101
|
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
@@ -60,31 +104,25 @@ function getOrCreateGlobalScope() {
|
|
|
60
104
|
return globalRootScope;
|
|
61
105
|
}
|
|
62
106
|
/**
|
|
63
|
-
* Provide with auto-scope
|
|
64
|
-
*
|
|
65
|
-
* Se chamado fora de scope, cria um global scope automaticamente
|
|
107
|
+
* Provide with auto-scope.
|
|
66
108
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* const scope = createScope();
|
|
71
|
-
* withScope(scope, () => {
|
|
72
|
-
* provide(Theme, 'dark');
|
|
73
|
-
* });
|
|
74
|
-
*
|
|
75
|
-
* // ✅ DEPOIS (simples)
|
|
76
|
-
* provide(Theme, 'dark'); // auto-scope!
|
|
77
|
-
* ```
|
|
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).
|
|
78
112
|
*/
|
|
79
113
|
export function provide(token, value) {
|
|
80
114
|
const currentScope = getCurrentScope();
|
|
81
115
|
if (currentScope) {
|
|
82
|
-
// Inside a scope - use normal provide
|
|
83
116
|
rawProvide(token, value);
|
|
84
117
|
}
|
|
85
118
|
else {
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
126
|
const globalScope = getOrCreateGlobalScope();
|
|
89
127
|
withScope(globalScope, () => {
|
|
90
128
|
rawProvide(token, value);
|
|
@@ -92,55 +130,121 @@ export function provide(token, value) {
|
|
|
92
130
|
}
|
|
93
131
|
}
|
|
94
132
|
/**
|
|
95
|
-
*
|
|
133
|
+
* Provide explicitly in the global root scope.
|
|
96
134
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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).
|
|
99
153
|
*/
|
|
100
154
|
export function inject(token) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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;
|
|
111
166
|
}
|
|
112
|
-
// Outside scope - try global scope
|
|
113
167
|
if (!globalRootScope) {
|
|
168
|
+
// Check for default value before throwing
|
|
169
|
+
if (token.defaultValue !== undefined) {
|
|
170
|
+
return token.defaultValue;
|
|
171
|
+
}
|
|
114
172
|
throw createInjectOutsideScopeError(token, 'No global scope exists yet.');
|
|
115
173
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
174
|
+
return withScope(globalRootScope, () => {
|
|
175
|
+
const result = rawTryInject(token);
|
|
176
|
+
if (result.found)
|
|
177
|
+
return result.value;
|
|
120
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);
|
|
121
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));
|
|
122
207
|
}
|
|
123
208
|
/**
|
|
124
|
-
*
|
|
209
|
+
* Inject explicitly from the global root scope.
|
|
125
210
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 }`.
|
|
134
240
|
*
|
|
135
|
-
*
|
|
136
|
-
* scope
|
|
137
|
-
*
|
|
138
|
-
* createApp();
|
|
139
|
-
* });
|
|
140
|
-
* ```
|
|
241
|
+
* Semantics:
|
|
242
|
+
* - If `fn` throws, the scope is disposed and the error is rethrown.
|
|
243
|
+
* - Caller owns disposal.
|
|
141
244
|
*/
|
|
142
245
|
export function scope(fn) {
|
|
143
|
-
const
|
|
246
|
+
const parent = getCurrentScope();
|
|
247
|
+
const newScope = createScope(parent ?? null);
|
|
144
248
|
try {
|
|
145
249
|
const result = withScope(newScope, fn);
|
|
146
250
|
return {
|
|
@@ -154,23 +258,18 @@ export function scope(fn) {
|
|
|
154
258
|
}
|
|
155
259
|
}
|
|
156
260
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* const ThemeProvider = createProvider(ThemeContext, () => {
|
|
162
|
-
* const theme = signal('dark');
|
|
163
|
-
* return { theme };
|
|
164
|
-
* });
|
|
261
|
+
* Provider helper that bundles:
|
|
262
|
+
* - a dedicated provider scope
|
|
263
|
+
* - a value created by `setup()`
|
|
264
|
+
* - registration via `provide()`
|
|
165
265
|
*
|
|
166
|
-
*
|
|
167
|
-
* // use value.theme()
|
|
168
|
-
* ```
|
|
266
|
+
* Useful for "feature modules" that want to expose a typed dependency with explicit lifetime.
|
|
169
267
|
*/
|
|
170
268
|
export function createProvider(token, setup) {
|
|
171
269
|
return {
|
|
172
270
|
create() {
|
|
173
|
-
const
|
|
271
|
+
const parent = getCurrentScope();
|
|
272
|
+
const providerScope = createScope(parent ?? null);
|
|
174
273
|
const value = withScope(providerScope, () => {
|
|
175
274
|
const result = setup();
|
|
176
275
|
provide(token, result);
|
|
@@ -187,7 +286,7 @@ export function createProvider(token, setup) {
|
|
|
187
286
|
};
|
|
188
287
|
}
|
|
189
288
|
/**
|
|
190
|
-
*
|
|
289
|
+
* Builds a descriptive error message for missing contexts inside a scope hierarchy.
|
|
191
290
|
*/
|
|
192
291
|
function createContextNotFoundError(token) {
|
|
193
292
|
const contextName = token.name || 'unnamed';
|
|
@@ -198,8 +297,19 @@ function createContextNotFoundError(token) {
|
|
|
198
297
|
message += ` 3. The scope where provide() was called has already been disposed\n\n`;
|
|
199
298
|
message += `How to fix:\n`;
|
|
200
299
|
message += ` • Make sure provide() is called in a parent scope\n`;
|
|
201
|
-
message += ` • Use scope(() => { provide(...); inject(...); }) to ensure same hierarchy\n`;
|
|
300
|
+
message += ` • Use scope(() => { provide(...); inject(...); }) to ensure the same hierarchy\n`;
|
|
202
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
|
+
}
|
|
203
313
|
message += `Learn more: https://github.com/evertondsvieira/dalila/blob/main/docs/context.md`;
|
|
204
314
|
return message;
|
|
205
315
|
}
|
|
@@ -213,21 +323,31 @@ function createInjectOutsideScopeError(token, extra) {
|
|
|
213
323
|
`Learn more: https://github.com/evertondsvieira/dalila/blob/main/docs/context.md`);
|
|
214
324
|
}
|
|
215
325
|
/**
|
|
216
|
-
*
|
|
326
|
+
* Returns the global root scope (if it exists).
|
|
327
|
+
* Intended for debugging/advanced usage only.
|
|
217
328
|
*/
|
|
218
329
|
export function getGlobalScope() {
|
|
219
|
-
return
|
|
330
|
+
return getGlobalRootScope();
|
|
220
331
|
}
|
|
221
332
|
/**
|
|
222
|
-
*
|
|
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.
|
|
223
341
|
*/
|
|
224
342
|
export function resetGlobalScope() {
|
|
225
343
|
detachBeforeUnloadListener();
|
|
344
|
+
const globalRootScope = getGlobalRootScope();
|
|
226
345
|
if (globalRootScope) {
|
|
227
346
|
globalRootScope.dispose();
|
|
228
|
-
|
|
347
|
+
setGlobalRootScope(null);
|
|
229
348
|
}
|
|
230
349
|
warnedGlobalProvide = false;
|
|
350
|
+
autoScopePolicy = "throw";
|
|
231
351
|
}
|
|
232
|
-
// Re-export createContext for convenience
|
|
352
|
+
// Re-export createContext for convenience.
|
|
233
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
|
|
29
|
+
readonly defaultValue?: T;
|
|
30
|
+
readonly [ContextBrand]: T;
|
|
4
31
|
}
|
|
5
|
-
|
|
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 {};
|