dalila 1.3.2 → 1.4.0
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/README.md +87 -611
- package/dist/context/auto-scope.d.ts +65 -0
- package/dist/context/auto-scope.js +28 -0
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/raw.d.ts +1 -1
- package/dist/context/raw.js +1 -1
- package/dist/core/dev.d.ts +5 -0
- package/dist/core/dev.js +7 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/persist.d.ts +63 -0
- package/dist/core/persist.js +371 -0
- package/dist/core/scope.d.ts +17 -0
- package/dist/core/scope.js +29 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/bind.d.ts +59 -0
- package/dist/runtime/bind.js +336 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +9 -0
- package/package.json +18 -1
- package/dist/compiler/dalila-lang.d.ts +0 -85
- package/dist/compiler/dalila-lang.js +0 -442
- package/dist/dom/elements.d.ts +0 -15
- package/dist/dom/elements.js +0 -100
- package/dist/dom/events.d.ts +0 -7
- package/dist/dom/events.js +0 -47
- package/dist/dom/index.d.ts +0 -2
- package/dist/dom/index.js +0 -2
package/README.md
CHANGED
|
@@ -1,691 +1,167 @@
|
|
|
1
|
-
# Dalila
|
|
1
|
+
# Dalila
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**DOM-first reactivity without the re-renders.**
|
|
4
4
|
|
|
5
|
-
Dalila is a
|
|
5
|
+
Dalila is a reactive framework built on signals. No virtual DOM, no JSX required — just HTML with declarative bindings.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
### Core (runtime, stable)
|
|
10
|
-
- 🚀 **Signals-based reactivity** - Automatic dependency tracking
|
|
11
|
-
- 🎯 **DOM-first rendering** - Direct DOM manipulation, no Virtual DOM
|
|
12
|
-
- 🔄 **Scope-based lifecycle** - Automatic cleanup (best effort)
|
|
13
|
-
- 🧿 **DOM lifecycle watchers** — `watch()` + helpers (`useEvent`, `useInterval`, `useTimeout`, `useFetch`) with scope-based cleanup
|
|
14
|
-
- 🛣️ **SPA router** - Basic routing with loaders and AbortSignal
|
|
15
|
-
- 📦 **Context system** - Reactive dependency injection
|
|
16
|
-
- 🔧 **Scheduler & batching** - Group updates into a single frame
|
|
17
|
-
- 📚 **List rendering** - `createList` with keyed diffing for efficient updates
|
|
18
|
-
- 🧱 **Resources** - Async data helpers with AbortSignal and scope cleanup
|
|
19
|
-
|
|
20
|
-
### Experimental
|
|
21
|
-
- 🎨 **Natural HTML bindings** - Only in the example dev-server (not in core)
|
|
22
|
-
- 🔍 **DevTools (console)** - Warnings and FPS monitor only
|
|
23
|
-
- 🧪 **Low-level list API** - `forEach` (advanced control, use when you need fine-grained behavior like reactive index; `createList` is the default)
|
|
24
|
-
|
|
25
|
-
### Planned / Roadmap
|
|
26
|
-
- 🧰 **DevTools UI** - Visual inspection tooling
|
|
27
|
-
- 🧩 **HTML bindings runtime/compiler** - First-class template binding
|
|
28
|
-
- 📊 **Virtualization** - Virtual lists/tables for very large datasets (10k+ items)
|
|
29
|
-
|
|
30
|
-
## 📦 Installation
|
|
7
|
+
## Installation
|
|
31
8
|
|
|
32
9
|
```bash
|
|
33
10
|
npm install dalila
|
|
34
11
|
```
|
|
35
12
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
Dalila examples use HTML bindings and a controller:
|
|
13
|
+
## Quick Start
|
|
39
14
|
|
|
40
15
|
```html
|
|
41
|
-
<div>
|
|
42
|
-
<
|
|
43
|
-
<button on
|
|
16
|
+
<div id="app">
|
|
17
|
+
<p>Count: {count}</p>
|
|
18
|
+
<button d-on-click="increment">+</button>
|
|
44
19
|
</div>
|
|
45
20
|
```
|
|
46
21
|
|
|
47
22
|
```ts
|
|
48
23
|
import { signal } from 'dalila';
|
|
24
|
+
import { bind } from 'dalila/runtime';
|
|
49
25
|
|
|
50
|
-
export function createController() {
|
|
51
|
-
const count = signal(0);
|
|
52
|
-
const increment = () => count.update(c => c + 1);
|
|
53
|
-
|
|
54
|
-
return { count, increment };
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
> Note: HTML bindings in this example are provided by the example dev-server (`npm run serve`),
|
|
59
|
-
> not by the core runtime.
|
|
60
|
-
|
|
61
|
-
## 🧪 Local Demo Server
|
|
62
|
-
|
|
63
|
-
Run a local server with HMR from the repo root:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npm run serve
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Then open `http://localhost:4242/`.
|
|
70
|
-
|
|
71
|
-
## 📚 Core Concepts
|
|
72
|
-
|
|
73
|
-
### Signals
|
|
74
|
-
```typescript
|
|
75
26
|
const count = signal(0);
|
|
76
27
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
count.set(5);
|
|
82
|
-
|
|
83
|
-
// Update with function
|
|
84
|
-
count.update(c => c + 1);
|
|
85
|
-
|
|
86
|
-
// Reactive effects
|
|
87
|
-
effect(() => {
|
|
88
|
-
console.log(`Count changed to: ${count()}`);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Async effects with cleanup
|
|
92
|
-
effectAsync(async (signal) => {
|
|
93
|
-
const response = await fetch('/api/data', { signal });
|
|
94
|
-
const data = await response.json();
|
|
95
|
-
console.log(data);
|
|
96
|
-
});
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Lifecycle / Cleanup (Scopes + DOM)
|
|
100
|
-
|
|
101
|
-
Dalila is DOM-first. That means a lot of your "lifecycle" work is not React-like rendering —
|
|
102
|
-
it's **attaching listeners, timers, and async work to real DOM nodes**.
|
|
103
|
-
|
|
104
|
-
Dalila's rule is simple:
|
|
105
|
-
|
|
106
|
-
- **Inside a scope** → cleanup is automatic (best-effort) on `scope.dispose()`
|
|
107
|
-
- **Outside a scope** → you must call `dispose()` manually unless a primitive explicitly auto-disposes on DOM removal (Dalila warns in dev mode)
|
|
108
|
-
|
|
109
|
-
#### `watch(node, fn)` — DOM lifecycle primitive
|
|
110
|
-
|
|
111
|
-
`watch()` runs a reactive function while a DOM node is connected.
|
|
112
|
-
When the node disconnects, the effect is disposed. If the node reconnects later, it starts again.
|
|
113
|
-
It's the primitive that enables DOM lifecycle without a VDOM.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import { watch, signal } from "dalila";
|
|
117
|
-
|
|
118
|
-
const count = signal(0);
|
|
119
|
-
|
|
120
|
-
const dispose = watch(someNode, () => {
|
|
121
|
-
// Reactive while connected because watch() runs this inside an effect()
|
|
122
|
-
someNode.textContent = String(count());
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// later (optional if inside a scope)
|
|
126
|
-
dispose();
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
#### Lifecycle helpers
|
|
130
|
-
|
|
131
|
-
Built on the same mental model, Dalila provides small helpers that always return an **idempotent** `dispose()`:
|
|
132
|
-
`useEvent`, `useInterval`, `useTimeout`, `useFetch`.
|
|
133
|
-
|
|
134
|
-
**Inside a scope (recommended)**
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
import { createScope, withScope, useEvent, useInterval, useFetch } from "dalila";
|
|
138
|
-
|
|
139
|
-
const scope = createScope();
|
|
140
|
-
|
|
141
|
-
withScope(scope, () => {
|
|
142
|
-
useEvent(button, "click", onClick);
|
|
143
|
-
useInterval(tick, 1000);
|
|
144
|
-
|
|
145
|
-
const user = useFetch("/api/user");
|
|
146
|
-
|
|
147
|
-
// Optional manual cleanup:
|
|
148
|
-
// user.dispose();
|
|
149
|
-
});
|
|
28
|
+
const ctx = {
|
|
29
|
+
count,
|
|
30
|
+
increment: () => count.update(n => n + 1),
|
|
31
|
+
};
|
|
150
32
|
|
|
151
|
-
|
|
33
|
+
bind(document.getElementById('app')!, ctx);
|
|
152
34
|
```
|
|
153
35
|
|
|
154
|
-
|
|
36
|
+
## Docs
|
|
155
37
|
|
|
156
|
-
|
|
38
|
+
### Getting Started
|
|
157
39
|
|
|
158
|
-
|
|
159
|
-
|
|
40
|
+
- [Overview](./docs/index.md) — Philosophy and quick start
|
|
41
|
+
- [Template Spec](./docs/template-spec.md) — Binding syntax reference
|
|
42
|
+
- [Roadmap](./docs/roadmap.md) — Strengths, weaknesses, and goals
|
|
160
43
|
|
|
161
|
-
|
|
44
|
+
### Core
|
|
162
45
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
46
|
+
- [Signals](./docs/core/signals.md) — `signal`, `computed`, `effect`
|
|
47
|
+
- [Scopes](./docs/core/scope.md) — Lifecycle management and cleanup
|
|
48
|
+
- [Persist](./docs/core/persist.md) — Automatic storage sync for signals
|
|
49
|
+
- [Context](./docs/context.md) — Dependency injection
|
|
166
50
|
|
|
167
|
-
|
|
51
|
+
### Runtime
|
|
168
52
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
Scopes make cleanup a **default**, not a convention — so UI changes don't silently leak listeners, timers, or in-flight async work.
|
|
53
|
+
- [Template Binding](./docs/runtime/README.md) — `bind()`, text interpolation, events
|
|
54
|
+
- [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
|
|
172
55
|
|
|
173
|
-
###
|
|
56
|
+
### Rendering
|
|
174
57
|
|
|
175
|
-
|
|
58
|
+
- [when](./docs/core/when.md) — Conditional visibility
|
|
59
|
+
- [match](./docs/core/match.md) — Switch-style rendering
|
|
60
|
+
- [for](./docs/core/for.md) — List rendering with keyed diffing
|
|
176
61
|
|
|
177
|
-
|
|
178
|
-
- **`match`** — value-based branching (`switch / cases`)
|
|
62
|
+
### Data
|
|
179
63
|
|
|
180
|
-
|
|
64
|
+
- [Resources](./docs/core/resource.md) — Async data with loading/error states
|
|
65
|
+
- [Query](./docs/core/query.md) — Cached queries
|
|
66
|
+
- [Mutations](./docs/core/mutation.md) — Write operations
|
|
181
67
|
|
|
182
|
-
|
|
68
|
+
### Utilities
|
|
183
69
|
|
|
184
|
-
|
|
70
|
+
- [Scheduler](./docs/core/scheduler.md) — Batching and coordination
|
|
71
|
+
- [Keys](./docs/core/key.md) — Cache key encoding
|
|
72
|
+
- [Dev Mode](./docs/core/dev.md) — Warnings and helpers
|
|
185
73
|
|
|
186
|
-
|
|
187
|
-
when(
|
|
188
|
-
() => isVisible(),
|
|
189
|
-
() => VisibleView(),
|
|
190
|
-
() => HiddenView()
|
|
191
|
-
);
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
HTML binding example:
|
|
195
|
-
|
|
196
|
-
```html
|
|
197
|
-
<div>
|
|
198
|
-
<button on:click={toggle}>Toggle</button>
|
|
74
|
+
## Features
|
|
199
75
|
|
|
200
|
-
<p when={show}>🐒 Visible branch</p>
|
|
201
|
-
<p when={!show}>🙈 Hidden branch</p>
|
|
202
|
-
</div>
|
|
203
76
|
```
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
#### `match` — value-based branching
|
|
210
|
-
|
|
211
|
-
Use `match` when your UI depends on a state or key, not just true/false.
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
match(
|
|
215
|
-
() => status(),
|
|
216
|
-
{
|
|
217
|
-
loading: Loading,
|
|
218
|
-
error: Error,
|
|
219
|
-
success: Success,
|
|
220
|
-
_: Idle
|
|
221
|
-
}
|
|
222
|
-
);
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
HTML binding example:
|
|
226
|
-
|
|
227
|
-
```html
|
|
228
|
-
<div match={status}>
|
|
229
|
-
<p case="idle">🟦 Idle</p>
|
|
230
|
-
<p case="loading">⏳ Loading...</p>
|
|
231
|
-
<p case="success">✅ Success!</p>
|
|
232
|
-
<p case="error">❌ Error</p>
|
|
233
|
-
<p case="_">🤷 Unknown</p>
|
|
234
|
-
</div>
|
|
77
|
+
dalila → signal, computed, effect, batch, ...
|
|
78
|
+
dalila/runtime → bind() for HTML templates
|
|
79
|
+
dalila/context → createContext, provide, inject
|
|
80
|
+
dalila/router → (in development)
|
|
235
81
|
```
|
|
236
82
|
|
|
237
|
-
|
|
238
|
-
- `_` is the default (fallback) case
|
|
239
|
-
- Swaps cases only when the selected key changes
|
|
240
|
-
- Each case has its own lifecycle (scope cleanup)
|
|
241
|
-
|
|
242
|
-
#### Rule of thumb
|
|
243
|
-
|
|
244
|
-
- `when` → booleans → optional else
|
|
245
|
-
- `match` → values/keys → `_` as fallback
|
|
246
|
-
|
|
247
|
-
These primitives are not abstractions over JSX.
|
|
248
|
-
They are explicit DOM control tools, designed to make branching visible and predictable.
|
|
83
|
+
### Signals
|
|
249
84
|
|
|
250
|
-
### Context (Dependency Injection)
|
|
251
85
|
```ts
|
|
252
|
-
|
|
253
|
-
provide(Theme, signal('light'));
|
|
254
|
-
const theme = inject(Theme);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### SPA Router
|
|
258
|
-
```typescript
|
|
259
|
-
const router = createRouter({
|
|
260
|
-
routes: [
|
|
261
|
-
{
|
|
262
|
-
path: '/',
|
|
263
|
-
view: HomePage,
|
|
264
|
-
loader: async ({ signal }) => {
|
|
265
|
-
const res = await fetch('/api/home', { signal });
|
|
266
|
-
return res.json();
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
{ path: '/users/:id', view: UserPage }
|
|
270
|
-
]
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
router.mount(document.getElementById('app'));
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Batching & Scheduling
|
|
277
|
-
```typescript
|
|
278
|
-
// Batch multiple updates - effects coalesce into a single frame
|
|
279
|
-
batch(() => {
|
|
280
|
-
count.set(1); // ✅ State updates immediately
|
|
281
|
-
theme.set('dark'); // ✅ State updates immediately
|
|
282
|
-
console.log(count()); // Reads new value: 1
|
|
283
|
-
|
|
284
|
-
// Effects are deferred and run once at the end of the batch
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// DOM read/write discipline
|
|
288
|
-
const width = measure(() => element.offsetWidth);
|
|
289
|
-
mutate(() => {
|
|
290
|
-
element.style.width = `${width + 10}px`;
|
|
291
|
-
});
|
|
292
|
-
```
|
|
86
|
+
import { signal, computed, effect } from 'dalila';
|
|
293
87
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
- Effects are **deferred** until the batch completes
|
|
297
|
-
- All deferred effects run once in a single animation frame
|
|
298
|
-
- This allows reading updated values inside the batch while coalescing UI updates
|
|
299
|
-
|
|
300
|
-
### List Rendering with Keys
|
|
301
|
-
|
|
302
|
-
Dalila provides efficient list rendering with keyed diffing, similar to React's `key` prop or Vue's `:key`.
|
|
303
|
-
|
|
304
|
-
**Basic usage:**
|
|
305
|
-
```typescript
|
|
306
|
-
import { signal } from 'dalila';
|
|
307
|
-
import { createList } from 'dalila/core';
|
|
308
|
-
|
|
309
|
-
const todos = signal([
|
|
310
|
-
{ id: 1, text: 'Learn Dalila', done: true },
|
|
311
|
-
{ id: 2, text: 'Build app', done: false }
|
|
312
|
-
]);
|
|
313
|
-
|
|
314
|
-
const listFragment = createList(
|
|
315
|
-
() => todos(),
|
|
316
|
-
(todo) => {
|
|
317
|
-
const li = document.createElement('li');
|
|
318
|
-
li.textContent = todo.text;
|
|
319
|
-
return li;
|
|
320
|
-
},
|
|
321
|
-
(todo) => todo.id.toString() // Key function
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
document.body.append(listFragment);
|
|
325
|
-
```
|
|
88
|
+
const count = signal(0);
|
|
89
|
+
const doubled = computed(() => count() * 2);
|
|
326
90
|
|
|
327
|
-
**Key function best practices:**
|
|
328
|
-
- ✅ Always provide a keyFn for dynamic lists
|
|
329
|
-
- ✅ Use stable, unique identifiers (IDs, not indices)
|
|
330
|
-
- ✅ Avoid using object references as keys
|
|
331
|
-
- ⚠️ Without keyFn, items use index as key (re-renders on reorder)
|
|
332
|
-
|
|
333
|
-
**How it works:**
|
|
334
|
-
- Only re-renders items whose value changed (not items that only moved)
|
|
335
|
-
- Preserves DOM nodes for unchanged items (maintains focus, scroll, etc)
|
|
336
|
-
- Efficient for lists up to ~1000 items (informal guideline, not yet benchmarked)
|
|
337
|
-
- Each item gets its own scope for automatic cleanup
|
|
338
|
-
- Outside a scope, the list auto-disposes when removed from the DOM (or call `fragment.dispose()`)
|
|
339
|
-
> **Note:** In `createList`, `index` is a snapshot (not reactive). Reorder moves nodes without re-render. Use `forEach()` if you need a reactive index.
|
|
340
|
-
|
|
341
|
-
**Performance:**
|
|
342
|
-
```typescript
|
|
343
|
-
// Bad: re-creates all items on every change
|
|
344
91
|
effect(() => {
|
|
345
|
-
|
|
346
|
-
todos().forEach(todo => {
|
|
347
|
-
container.append(createTodoItem(todo));
|
|
348
|
-
});
|
|
92
|
+
console.log('Count is', count());
|
|
349
93
|
});
|
|
350
94
|
|
|
351
|
-
//
|
|
352
|
-
const list = createList(
|
|
353
|
-
() => todos(),
|
|
354
|
-
(todo) => createTodoItem(todo),
|
|
355
|
-
(todo) => todo.id.toString()
|
|
356
|
-
);
|
|
95
|
+
count.set(5); // logs: Count is 5
|
|
357
96
|
```
|
|
358
97
|
|
|
359
|
-
###
|
|
360
|
-
|
|
361
|
-
> **Scope rule (important):**
|
|
362
|
-
> - `q.query()` / `createCachedResource()` cache **only within a scope**.
|
|
363
|
-
> - Outside scope, **no cache** (safer).
|
|
364
|
-
> - For explicit global cache, use `q.queryGlobal()` or `createCachedResource(..., { persist: true })`.
|
|
365
|
-
|
|
366
|
-
Dalila treats async data as **state**, not as lifecycle effects.
|
|
367
|
-
|
|
368
|
-
Instead of hooks or lifecycle-driven fetching, Dalila provides resources that:
|
|
369
|
-
|
|
370
|
-
- Are driven by signals
|
|
371
|
-
- Are abortable by default
|
|
372
|
-
- Clean themselves up with scopes
|
|
373
|
-
- Can be cached, invalidated, and revalidated declaratively
|
|
374
|
-
|
|
375
|
-
There are three layers, from low-level to DX-focused:
|
|
376
|
-
|
|
377
|
-
- `createResource` — primitive (no cache)
|
|
378
|
-
- `createCachedResource` — shared cache + invalidation
|
|
379
|
-
- `QueryClient` — ergonomic DX (queries + mutations)
|
|
380
|
-
|
|
381
|
-
You can stop at any layer.
|
|
382
|
-
|
|
383
|
-
#### 🧱 createResource — the primitive
|
|
384
|
-
|
|
385
|
-
Use `createResource` when you want a single async source tied to reactive dependencies.
|
|
386
|
-
|
|
387
|
-
```ts
|
|
388
|
-
const user = createResource(async (signal) => {
|
|
389
|
-
const res = await fetch(`/api/user/${id()}`, { signal });
|
|
390
|
-
return res.json();
|
|
391
|
-
});
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
**Behavior**
|
|
395
|
-
|
|
396
|
-
- Runs inside effectAsync
|
|
397
|
-
- Tracks any signal reads inside the fetch
|
|
398
|
-
- Aborts the previous request on re-run
|
|
399
|
-
- Aborts automatically on scope disposal
|
|
400
|
-
- Exposes reactive state
|
|
401
|
-
|
|
402
|
-
```ts
|
|
403
|
-
user.data(); // T | null
|
|
404
|
-
user.loading(); // boolean
|
|
405
|
-
user.error(); // Error | null
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
**Manual revalidation:**
|
|
98
|
+
### Template Binding
|
|
409
99
|
|
|
410
100
|
```ts
|
|
411
|
-
|
|
412
|
-
user.refresh({ force }); // abort + refetch
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
**When to use**
|
|
416
|
-
|
|
417
|
-
- Local data
|
|
418
|
-
- One-off fetches
|
|
419
|
-
- Non-shared state
|
|
420
|
-
- Full control
|
|
421
|
-
|
|
422
|
-
If you want sharing, cache, or invalidation, go up one level.
|
|
423
|
-
|
|
424
|
-
#### 🗄️ Cached Resources
|
|
425
|
-
|
|
426
|
-
> **Scoped cache (recommended):**
|
|
427
|
-
```ts
|
|
428
|
-
withScope(createScope(), () => {
|
|
429
|
-
const user = createCachedResource("user:42", fetchUser, { tags: ["users"] });
|
|
430
|
-
});
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
> **Global cache (explicit):**
|
|
434
|
-
```ts
|
|
435
|
-
const user = createCachedResource("user:42", fetchUser, { tags: ["users"], persist: true });
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
Dalila can cache resources by key, without introducing a global singleton or context provider.
|
|
439
|
-
|
|
440
|
-
```ts
|
|
441
|
-
const user = createCachedResource(
|
|
442
|
-
"user:42",
|
|
443
|
-
async (signal) => fetchUser(signal, 42),
|
|
444
|
-
{ tags: ["users"] }
|
|
445
|
-
);
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
**What caching means in Dalila**
|
|
449
|
-
|
|
450
|
-
- One fetch per key (deduped)
|
|
451
|
-
- Shared across scopes (when using `persist: true`)
|
|
452
|
-
- Automatically revalidated on invalidation
|
|
453
|
-
- Still abortable and scope-safe
|
|
454
|
-
|
|
455
|
-
**Invalidation by tag**
|
|
456
|
-
```ts
|
|
457
|
-
invalidateResourceTag("users");
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
All cached resources registered with "users" will:
|
|
461
|
-
- Be marked stale
|
|
462
|
-
- Revalidate in place (best-effort)
|
|
463
|
-
|
|
464
|
-
This is the foundation used by the query layer.
|
|
465
|
-
|
|
466
|
-
#### 🧠 Query Client (DX Layer)
|
|
467
|
-
|
|
468
|
-
The QueryClient builds a React Query–like experience, but stays signal-driven and scope-safe.
|
|
469
|
-
|
|
470
|
-
```ts
|
|
471
|
-
const q = createQueryClient();
|
|
472
|
-
|
|
473
|
-
// Scoped query (recommended)
|
|
474
|
-
const user = q.query({
|
|
475
|
-
key: () => q.key("user", userId()),
|
|
476
|
-
tags: ["users"],
|
|
477
|
-
fetch: (signal, key) => apiGetUser(signal, key[1]),
|
|
478
|
-
staleTime: 10_000,
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Global query (explicit)
|
|
482
|
-
const user = q.queryGlobal({
|
|
483
|
-
key: () => q.key("user", userId()),
|
|
484
|
-
tags: ["users"],
|
|
485
|
-
fetch: (signal, key) => apiGetUser(signal, key[1]),
|
|
486
|
-
staleTime: 10_000,
|
|
487
|
-
});
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
**What this gives you**
|
|
491
|
-
|
|
492
|
-
- Reactive key
|
|
493
|
-
- Automatic caching by encoded key
|
|
494
|
-
- Abort on key change
|
|
495
|
-
- Deduped requests
|
|
496
|
-
- Tag-based invalidation
|
|
497
|
-
- Optional stale revalidation
|
|
498
|
-
- No providers, no hooks
|
|
499
|
-
|
|
500
|
-
```ts
|
|
501
|
-
user.data();
|
|
502
|
-
user.loading();
|
|
503
|
-
user.error();
|
|
504
|
-
user.status(); // "loading" | "error" | "success"
|
|
505
|
-
user.refresh();
|
|
506
|
-
```
|
|
101
|
+
import { bind } from 'dalila/runtime';
|
|
507
102
|
|
|
508
|
-
|
|
103
|
+
// Binds {tokens}, d-on-*, when, match to the DOM
|
|
104
|
+
// Dev-server auto-prevents FOUC (flash of tokens)
|
|
105
|
+
const dispose = bind(rootElement, ctx);
|
|
509
106
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
```ts
|
|
513
|
-
q.key("user", userId());
|
|
107
|
+
// Cleanup when done
|
|
108
|
+
dispose();
|
|
514
109
|
```
|
|
515
110
|
|
|
516
|
-
|
|
517
|
-
- Stable
|
|
518
|
-
- Readonly
|
|
519
|
-
- Encoded safely (no JSON.stringify)
|
|
520
|
-
- If the key changes, the query refetches.
|
|
521
|
-
|
|
522
|
-
#### 🔁 Stale Revalidation (staleTime)
|
|
523
|
-
|
|
524
|
-
Dalila’s staleTime is intentionally simpler than React Query.
|
|
111
|
+
### Scopes
|
|
525
112
|
|
|
526
113
|
```ts
|
|
527
|
-
|
|
528
|
-
```
|
|
114
|
+
import { createScope, withScope, effect } from 'dalila';
|
|
529
115
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
- After a successful fetch
|
|
533
|
-
- Schedule a best-effort revalidate
|
|
534
|
-
- Cleared automatically on scope disposal
|
|
535
|
-
|
|
536
|
-
This avoids background timers leaking or running after unmount.
|
|
537
|
-
|
|
538
|
-
#### ✍️ Mutations
|
|
539
|
-
|
|
540
|
-
Mutations represent intentional writes.
|
|
541
|
-
|
|
542
|
-
They:
|
|
543
|
-
- Are abortable
|
|
544
|
-
- Deduplicate concurrent runs
|
|
545
|
-
- Store last successful result
|
|
546
|
-
- Invalidate queries declaratively
|
|
116
|
+
const scope = createScope();
|
|
547
117
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
mutate: (signal, input) => apiSaveUser(signal, input),
|
|
551
|
-
invalidateTags: ["users"],
|
|
118
|
+
withScope(scope, () => {
|
|
119
|
+
effect(() => { /* auto-cleaned when scope disposes */ });
|
|
552
120
|
});
|
|
553
|
-
```
|
|
554
121
|
|
|
555
|
-
|
|
556
|
-
```ts
|
|
557
|
-
await saveUser.run({ name: "Everton" });
|
|
122
|
+
scope.dispose(); // stops all effects
|
|
558
123
|
```
|
|
559
124
|
|
|
560
|
-
|
|
561
|
-
```ts
|
|
562
|
-
saveUser.data(); // last success
|
|
563
|
-
saveUser.loading();
|
|
564
|
-
saveUser.error();
|
|
565
|
-
```
|
|
125
|
+
### Context
|
|
566
126
|
|
|
567
|
-
**Deduplication & force**
|
|
568
127
|
```ts
|
|
569
|
-
|
|
570
|
-
saveUser.run(input, { force }); // abort + restart
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
**Invalidation**
|
|
574
|
-
|
|
575
|
-
On success, mutations can invalidate:
|
|
576
|
-
- Tags → revalidate all matching queries
|
|
577
|
-
- Keys → revalidate a specific query
|
|
578
|
-
|
|
579
|
-
This keeps writes explicit and reads declarative.
|
|
580
|
-
|
|
581
|
-
#### 🧭 Mental Model
|
|
582
|
-
|
|
583
|
-
Think in layers:
|
|
128
|
+
import { createContext, provide, inject } from 'dalila';
|
|
584
129
|
|
|
585
|
-
|
|
586
|
-
|-------|---------|
|
|
587
|
-
| createResource | Async signal |
|
|
588
|
-
| Cached resource | Shared async state |
|
|
589
|
-
| Query | Read model |
|
|
590
|
-
| Mutation | Write model |
|
|
130
|
+
const ThemeContext = createContext<'light' | 'dark'>('theme');
|
|
591
131
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
#### ✅ Rule of Thumb
|
|
595
|
-
|
|
596
|
-
- Local async state → `createResource`
|
|
597
|
-
- Shared server data → `query()`
|
|
598
|
-
- Global cache → `queryGlobal()` / `persist: true`
|
|
599
|
-
- Writes / side effects → `mutation`
|
|
600
|
-
- UI branching → `when` / `match`
|
|
601
|
-
|
|
602
|
-
Queries and mutations are just signals.
|
|
603
|
-
They compose naturally with `when`, `match`, lists, and effects.
|
|
604
|
-
|
|
605
|
-
#### 🧠 Philosophy
|
|
606
|
-
|
|
607
|
-
Dalila’s data layer is designed to be:
|
|
608
|
-
|
|
609
|
-
- Predictable
|
|
610
|
-
- Abortable
|
|
611
|
-
- Scope-safe
|
|
612
|
-
- Explicit
|
|
613
|
-
- Boring in the right way
|
|
614
|
-
|
|
615
|
-
No magic lifecycles.
|
|
616
|
-
No hidden background work.
|
|
617
|
-
No provider pyramids.
|
|
618
|
-
|
|
619
|
-
## 🏗️ Architecture
|
|
620
|
-
|
|
621
|
-
Dalila is built around these core principles:
|
|
622
|
-
|
|
623
|
-
- **No JSX** - Core runtime doesn't require JSX
|
|
624
|
-
- **No Virtual DOM** - Direct DOM manipulation
|
|
625
|
-
- **No manual memoization** - Signals reduce manual memoization (goal)
|
|
626
|
-
- **Scope-based cleanup** - Automatic resource management (best-effort)
|
|
627
|
-
- **Signal-driven reactivity** - Localized updates where possible
|
|
628
|
-
|
|
629
|
-
## 📊 Performance
|
|
630
|
-
|
|
631
|
-
- **Localized updates**: Signals update only subscribed DOM nodes (goal)
|
|
632
|
-
- **Automatic cleanup**: Scope-based cleanup is best-effort
|
|
633
|
-
- **Bundle size**: Not yet measured/verified
|
|
634
|
-
|
|
635
|
-
## 🤔 Why Dalila vs React?
|
|
636
|
-
|
|
637
|
-
| Feature | React | Dalila |
|
|
638
|
-
|---------|-------|--------|
|
|
639
|
-
| Rendering | Virtual DOM diffing | Direct DOM manipulation |
|
|
640
|
-
| Performance | Manual optimization | Runtime scheduling (best-effort) |
|
|
641
|
-
| State management | Hooks + deps arrays | Signals + automatic tracking |
|
|
642
|
-
| Side effects | `useEffect` + deps | `effect()` + automatic cleanup (best-effort) |
|
|
643
|
-
| Bundle size | ~40KB | Not yet measured |
|
|
644
|
-
|
|
645
|
-
## 📁 Project Structure
|
|
132
|
+
// In parent scope
|
|
133
|
+
provide(ThemeContext, 'dark');
|
|
646
134
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
├── src/
|
|
650
|
-
│ ├── core/ # Signals, effects, scopes
|
|
651
|
-
│ ├── context/ # Dependency injection
|
|
652
|
-
│ ├── router/ # SPA routing
|
|
653
|
-
│ ├── dom/ # DOM utilities
|
|
654
|
-
│ └── index.ts # Main exports
|
|
655
|
-
├── examples/ # Example applications
|
|
656
|
-
└── dist/ # Compiled output
|
|
135
|
+
// In child scope
|
|
136
|
+
const theme = inject(ThemeContext); // 'dark'
|
|
657
137
|
```
|
|
658
138
|
|
|
659
|
-
##
|
|
139
|
+
## Development
|
|
660
140
|
|
|
661
141
|
```bash
|
|
662
|
-
# Install
|
|
142
|
+
# Install
|
|
663
143
|
npm install
|
|
664
144
|
|
|
665
|
-
# Build
|
|
145
|
+
# Build
|
|
666
146
|
npm run build
|
|
667
147
|
|
|
668
|
-
#
|
|
669
|
-
npm run
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
## 📖 Examples
|
|
148
|
+
# Dev server with HMR
|
|
149
|
+
npm run serve
|
|
673
150
|
|
|
674
|
-
|
|
675
|
-
|
|
151
|
+
# Tests
|
|
152
|
+
npm test
|
|
153
|
+
```
|
|
676
154
|
|
|
677
|
-
##
|
|
155
|
+
## Architecture
|
|
678
156
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
157
|
+
```
|
|
158
|
+
src/
|
|
159
|
+
├── core/ → Signals, effects, scopes, scheduler
|
|
160
|
+
├── context/ → Dependency injection
|
|
161
|
+
├── runtime/ → Template binding (bind, autoBind)
|
|
162
|
+
└── router/ → Routing (in development)
|
|
163
|
+
```
|
|
684
164
|
|
|
685
|
-
##
|
|
165
|
+
## License
|
|
686
166
|
|
|
687
167
|
MIT
|
|
688
|
-
|
|
689
|
-
---
|
|
690
|
-
|
|
691
|
-
**Build UI, not workarounds.**
|