dalila 1.0.0 β 1.1.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 +321 -81
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Run a local server with HMR from the repo root:
|
|
|
65
65
|
npm run serve
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Then open `http://localhost:
|
|
68
|
+
Then open `http://localhost:4242/`.
|
|
69
69
|
|
|
70
70
|
## π Core Concepts
|
|
71
71
|
|
|
@@ -96,16 +96,81 @@ effectAsync(async (signal) => {
|
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
### Conditional Rendering
|
|
99
|
+
|
|
100
|
+
Dalila provides two primitives for branching UI:
|
|
101
|
+
|
|
102
|
+
- **`when`** β boolean conditions (`if / else`)
|
|
103
|
+
- **`match`** β value-based branching (`switch / cases`)
|
|
104
|
+
|
|
105
|
+
They are intentionally separate to keep UI logic explicit and predictable.
|
|
106
|
+
|
|
107
|
+
#### `when` β boolean conditions
|
|
108
|
+
|
|
109
|
+
Use `when` when your UI depends on a true/false condition.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
when(
|
|
113
|
+
() => isVisible(),
|
|
114
|
+
() => VisibleView(),
|
|
115
|
+
() => HiddenView()
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
HTML binding example:
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<div>
|
|
123
|
+
<button on:click={toggle}>Toggle</button>
|
|
124
|
+
|
|
125
|
+
<p when={show}>π Visible branch</p>
|
|
126
|
+
<p when={!show}>π Hidden branch</p>
|
|
127
|
+
</div>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- Tracks signals used inside the condition
|
|
131
|
+
- Optional else branch runs when the condition is false
|
|
132
|
+
- Each branch has its own lifecycle (scope cleanup)
|
|
133
|
+
|
|
134
|
+
#### `match` β value-based branching
|
|
135
|
+
|
|
136
|
+
Use `match` when your UI depends on a state or key, not just true/false.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
match(
|
|
140
|
+
() => status(),
|
|
141
|
+
{
|
|
142
|
+
loading: Loading,
|
|
143
|
+
error: Error,
|
|
144
|
+
success: Success,
|
|
145
|
+
_: Idle
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
HTML binding example:
|
|
151
|
+
|
|
99
152
|
```html
|
|
100
|
-
<p when={showTips}>This shows when true.</p>
|
|
101
153
|
<div match={status}>
|
|
102
|
-
<
|
|
103
|
-
<
|
|
154
|
+
<p case="idle">π¦ Idle</p>
|
|
155
|
+
<p case="loading">β³ Loading...</p>
|
|
156
|
+
<p case="success">β
Success!</p>
|
|
157
|
+
<p case="error">β Error</p>
|
|
158
|
+
<p case="_">π€· Unknown</p>
|
|
104
159
|
</div>
|
|
105
160
|
```
|
|
106
161
|
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
- Each case maps a value to a render function
|
|
163
|
+
- `_` is the default (fallback) case
|
|
164
|
+
- Swaps cases only when the selected key changes
|
|
165
|
+
- Each case has its own lifecycle (scope cleanup)
|
|
166
|
+
|
|
167
|
+
#### Rule of thumb
|
|
168
|
+
|
|
169
|
+
- `when` β booleans β optional else
|
|
170
|
+
- `match` β values/keys β `_` as fallback
|
|
171
|
+
|
|
172
|
+
These primitives are not abstractions over JSX.
|
|
173
|
+
They are explicit DOM control tools, designed to make branching visible and predictable.
|
|
109
174
|
|
|
110
175
|
### Context (Dependency Injection)
|
|
111
176
|
```ts
|
|
@@ -135,11 +200,13 @@ router.mount(document.getElementById('app'));
|
|
|
135
200
|
|
|
136
201
|
### Batching & Scheduling
|
|
137
202
|
```typescript
|
|
138
|
-
// Batch multiple updates into single frame
|
|
203
|
+
// Batch multiple updates - effects coalesce into a single frame
|
|
139
204
|
batch(() => {
|
|
140
|
-
count.set(1);
|
|
141
|
-
theme.set('dark');
|
|
142
|
-
//
|
|
205
|
+
count.set(1); // β
State updates immediately
|
|
206
|
+
theme.set('dark'); // β
State updates immediately
|
|
207
|
+
console.log(count()); // Reads new value: 1
|
|
208
|
+
|
|
209
|
+
// Effects are deferred and run once at the end of the batch
|
|
143
210
|
});
|
|
144
211
|
|
|
145
212
|
// DOM read/write discipline
|
|
@@ -149,6 +216,12 @@ mutate(() => {
|
|
|
149
216
|
});
|
|
150
217
|
```
|
|
151
218
|
|
|
219
|
+
**Batching semantics:**
|
|
220
|
+
- `signal.set()` updates the value **immediately** (synchronous)
|
|
221
|
+
- Effects are **deferred** until the batch completes
|
|
222
|
+
- All deferred effects run once in a single animation frame
|
|
223
|
+
- This allows reading updated values inside the batch while coalescing UI updates
|
|
224
|
+
|
|
152
225
|
### List Rendering with Keys
|
|
153
226
|
```typescript
|
|
154
227
|
// Primary API (stable)
|
|
@@ -165,99 +238,266 @@ forEach(
|
|
|
165
238
|
);
|
|
166
239
|
```
|
|
167
240
|
|
|
168
|
-
###
|
|
169
|
-
```typescript
|
|
170
|
-
// Declarative data fetching
|
|
171
|
-
const { data, loading, error, refresh } = createResource(
|
|
172
|
-
async (signal) => {
|
|
173
|
-
const res = await fetch('/api/data', { signal });
|
|
174
|
-
return res.json();
|
|
175
|
-
}
|
|
176
|
-
);
|
|
241
|
+
### Data Fetching & Server State
|
|
177
242
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
243
|
+
> **Scope rule (important):**
|
|
244
|
+
> - `q.query()` / `createCachedResource()` cache **only within a scope**.
|
|
245
|
+
> - Outside scope, **no cache** (safer).
|
|
246
|
+
> - For explicit global cache, use `q.queryGlobal()` or `createCachedResource(..., { persist: true })`.
|
|
247
|
+
|
|
248
|
+
Dalila treats async data as **state**, not as lifecycle effects.
|
|
249
|
+
|
|
250
|
+
Instead of hooks or lifecycle-driven fetching, Dalila provides resources that:
|
|
251
|
+
|
|
252
|
+
- Are driven by signals
|
|
253
|
+
- Are abortable by default
|
|
254
|
+
- Clean themselves up with scopes
|
|
255
|
+
- Can be cached, invalidated, and revalidated declaratively
|
|
256
|
+
|
|
257
|
+
There are three layers, from low-level to DX-focused:
|
|
258
|
+
|
|
259
|
+
- `createResource` β primitive (no cache)
|
|
260
|
+
- `createCachedResource` β shared cache + invalidation
|
|
261
|
+
- `QueryClient` β ergonomic DX (queries + mutations)
|
|
262
|
+
|
|
263
|
+
You can stop at any layer.
|
|
264
|
+
|
|
265
|
+
#### π§± createResource β the primitive
|
|
266
|
+
|
|
267
|
+
Use `createResource` when you want a single async source tied to reactive dependencies.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const user = createResource(async (signal) => {
|
|
271
|
+
const res = await fetch(`/api/user/${id()}`, { signal });
|
|
272
|
+
return res.json();
|
|
181
273
|
});
|
|
274
|
+
```
|
|
182
275
|
|
|
183
|
-
|
|
184
|
-
const cachedData = createCachedResource(
|
|
185
|
-
'user-data',
|
|
186
|
-
async (signal) => fetchUser(signal)
|
|
187
|
-
);
|
|
276
|
+
**Behavior**
|
|
188
277
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
278
|
+
- Runs inside effectAsync
|
|
279
|
+
- Tracks any signal reads inside the fetch
|
|
280
|
+
- Aborts the previous request on re-run
|
|
281
|
+
- Aborts automatically on scope disposal
|
|
282
|
+
- Exposes reactive state
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
user.data(); // T | null
|
|
286
|
+
user.loading(); // boolean
|
|
287
|
+
user.error(); // Error | null
|
|
194
288
|
```
|
|
195
289
|
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// Virtual list
|
|
199
|
-
createVirtualList(
|
|
200
|
-
items,
|
|
201
|
-
50, // Item height
|
|
202
|
-
(item) => div(item.name),
|
|
203
|
-
{ container: document.getElementById('list') }
|
|
204
|
-
);
|
|
290
|
+
**Manual revalidation:**
|
|
205
291
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{ key: 'id', header: 'ID', width: '100px' },
|
|
211
|
-
{ key: 'name', header: 'Name', width: '200px' }
|
|
212
|
-
],
|
|
213
|
-
(item, column) => div(item[column.key]),
|
|
214
|
-
document.getElementById('table')
|
|
215
|
-
);
|
|
292
|
+
```ts
|
|
293
|
+
user.refresh(); // deduped
|
|
294
|
+
user.refresh({ force }); // abort + refetch
|
|
295
|
+
```
|
|
216
296
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
**When to use**
|
|
298
|
+
|
|
299
|
+
- Local data
|
|
300
|
+
- One-off fetches
|
|
301
|
+
- Non-shared state
|
|
302
|
+
- Full control
|
|
303
|
+
|
|
304
|
+
If you want sharing, cache, or invalidation, go up one level.
|
|
305
|
+
|
|
306
|
+
#### ποΈ Cached Resources
|
|
307
|
+
|
|
308
|
+
> **Scoped cache (recommended):**
|
|
309
|
+
```ts
|
|
310
|
+
withScope(createScope(), () => {
|
|
311
|
+
const user = createCachedResource("user:42", fetchUser, { tags: ["users"] });
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
> **Global cache (explicit):**
|
|
316
|
+
```ts
|
|
317
|
+
const user = createCachedResource("user:42", fetchUser, { tags: ["users"], persist: true });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Dalila can cache resources by key, without introducing a global singleton or context provider.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const user = createCachedResource(
|
|
324
|
+
"user:42",
|
|
325
|
+
async (signal) => fetchUser(signal, 42),
|
|
326
|
+
{ tags: ["users"] }
|
|
222
327
|
);
|
|
223
328
|
```
|
|
224
329
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
330
|
+
**What caching means in Dalila**
|
|
331
|
+
|
|
332
|
+
- One fetch per key (deduped)
|
|
333
|
+
- Shared across scopes (when using `persist: true`)
|
|
334
|
+
- Automatically revalidated on invalidation
|
|
335
|
+
- Still abortable and scope-safe
|
|
336
|
+
|
|
337
|
+
**Invalidation by tag**
|
|
338
|
+
```ts
|
|
339
|
+
invalidateResourceTag("users");
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
All cached resources registered with "users" will:
|
|
343
|
+
- Be marked stale
|
|
344
|
+
- Revalidate in place (best-effort)
|
|
345
|
+
|
|
346
|
+
This is the foundation used by the query layer.
|
|
347
|
+
|
|
348
|
+
#### π§ Query Client (DX Layer)
|
|
229
349
|
|
|
230
|
-
|
|
231
|
-
const { value, subscribers } = inspectSignal(count);
|
|
350
|
+
The QueryClient builds a React Queryβlike experience, but stays signal-driven and scope-safe.
|
|
232
351
|
|
|
233
|
-
|
|
234
|
-
const
|
|
352
|
+
```ts
|
|
353
|
+
const q = createQueryClient();
|
|
354
|
+
|
|
355
|
+
// Scoped query (recommended)
|
|
356
|
+
const user = q.query({
|
|
357
|
+
key: () => q.key("user", userId()),
|
|
358
|
+
tags: ["users"],
|
|
359
|
+
fetch: (signal, key) => apiGetUser(signal, key[1]),
|
|
360
|
+
staleTime: 10_000,
|
|
361
|
+
});
|
|
235
362
|
|
|
236
|
-
//
|
|
237
|
-
|
|
363
|
+
// Global query (explicit)
|
|
364
|
+
const user = q.queryGlobal({
|
|
365
|
+
key: () => q.key("user", userId()),
|
|
366
|
+
tags: ["users"],
|
|
367
|
+
fetch: (signal, key) => apiGetUser(signal, key[1]),
|
|
368
|
+
staleTime: 10_000,
|
|
369
|
+
});
|
|
238
370
|
```
|
|
239
371
|
|
|
240
|
-
|
|
372
|
+
**What this gives you**
|
|
241
373
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
374
|
+
- Reactive key
|
|
375
|
+
- Automatic caching by encoded key
|
|
376
|
+
- Abort on key change
|
|
377
|
+
- Deduped requests
|
|
378
|
+
- Tag-based invalidation
|
|
379
|
+
- Optional stale revalidation
|
|
380
|
+
- No providers, no hooks
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
user.data();
|
|
384
|
+
user.loading();
|
|
385
|
+
user.error();
|
|
386
|
+
user.status(); // "loading" | "error" | "success"
|
|
387
|
+
user.refresh();
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### π Query Keys
|
|
246
391
|
|
|
247
|
-
|
|
248
|
-
useInterval(() => {
|
|
249
|
-
console.log('Tick');
|
|
250
|
-
}, 1000);
|
|
392
|
+
Keys are data identity, not fetch parameters.
|
|
251
393
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
394
|
+
```ts
|
|
395
|
+
q.key("user", userId());
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- Typed
|
|
399
|
+
- Stable
|
|
400
|
+
- Readonly
|
|
401
|
+
- Encoded safely (no JSON.stringify)
|
|
402
|
+
- If the key changes, the query refetches.
|
|
403
|
+
|
|
404
|
+
#### π Stale Revalidation (staleTime)
|
|
256
405
|
|
|
257
|
-
|
|
258
|
-
|
|
406
|
+
Dalilaβs staleTime is intentionally simpler than React Query.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
staleTime: 10_000
|
|
259
410
|
```
|
|
260
411
|
|
|
412
|
+
**Meaning:**
|
|
413
|
+
|
|
414
|
+
- After a successful fetch
|
|
415
|
+
- Schedule a best-effort revalidate
|
|
416
|
+
- Cleared automatically on scope disposal
|
|
417
|
+
|
|
418
|
+
This avoids background timers leaking or running after unmount.
|
|
419
|
+
|
|
420
|
+
#### βοΈ Mutations
|
|
421
|
+
|
|
422
|
+
Mutations represent intentional writes.
|
|
423
|
+
|
|
424
|
+
They:
|
|
425
|
+
- Are abortable
|
|
426
|
+
- Deduplicate concurrent runs
|
|
427
|
+
- Store last successful result
|
|
428
|
+
- Invalidate queries declaratively
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const saveUser = q.mutation({
|
|
432
|
+
mutate: (signal, input) => apiSaveUser(signal, input),
|
|
433
|
+
invalidateTags: ["users"],
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Running a mutation**
|
|
438
|
+
```ts
|
|
439
|
+
await saveUser.run({ name: "Everton" });
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Reactive state:**
|
|
443
|
+
```ts
|
|
444
|
+
saveUser.data(); // last success
|
|
445
|
+
saveUser.loading();
|
|
446
|
+
saveUser.error();
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Deduplication & force**
|
|
450
|
+
```ts
|
|
451
|
+
saveUser.run(input); // deduped
|
|
452
|
+
saveUser.run(input, { force }); // abort + restart
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Invalidation**
|
|
456
|
+
|
|
457
|
+
On success, mutations can invalidate:
|
|
458
|
+
- Tags β revalidate all matching queries
|
|
459
|
+
- Keys β revalidate a specific query
|
|
460
|
+
|
|
461
|
+
This keeps writes explicit and reads declarative.
|
|
462
|
+
|
|
463
|
+
#### π§ Mental Model
|
|
464
|
+
|
|
465
|
+
Think in layers:
|
|
466
|
+
|
|
467
|
+
| Layer | Purpose |
|
|
468
|
+
|-------|---------|
|
|
469
|
+
| createResource | Async signal |
|
|
470
|
+
| Cached resource | Shared async state |
|
|
471
|
+
| Query | Read model |
|
|
472
|
+
| Mutation | Write model |
|
|
473
|
+
|
|
474
|
+
Dalila does not blur these layers.
|
|
475
|
+
|
|
476
|
+
#### β
Rule of Thumb
|
|
477
|
+
|
|
478
|
+
- Local async state β `createResource`
|
|
479
|
+
- Shared server data β `query()`
|
|
480
|
+
- Global cache β `queryGlobal()` / `persist: true`
|
|
481
|
+
- Writes / side effects β `mutation`
|
|
482
|
+
- UI branching β `when` / `match`
|
|
483
|
+
|
|
484
|
+
Queries and mutations are just signals.
|
|
485
|
+
They compose naturally with `when`, `match`, lists, and effects.
|
|
486
|
+
|
|
487
|
+
#### π§ Philosophy
|
|
488
|
+
|
|
489
|
+
Dalilaβs data layer is designed to be:
|
|
490
|
+
|
|
491
|
+
- Predictable
|
|
492
|
+
- Abortable
|
|
493
|
+
- Scope-safe
|
|
494
|
+
- Explicit
|
|
495
|
+
- Boring in the right way
|
|
496
|
+
|
|
497
|
+
No magic lifecycles.
|
|
498
|
+
No hidden background work.
|
|
499
|
+
No provider pyramids.
|
|
500
|
+
|
|
261
501
|
## ποΈ Architecture
|
|
262
502
|
|
|
263
503
|
Dalila is built around these core principles:
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dalila",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "DOM-first reactive framework based on signals",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"build": "tsc",
|
|
9
10
|
"dev": "tsc --watch",
|
|
10
|
-
"serve": "node scripts/dev-server.
|
|
11
|
-
"test": "
|
|
11
|
+
"serve": "node scripts/dev-server.cjs",
|
|
12
|
+
"test": "npm run build && node --test",
|
|
13
|
+
"test:e2e": "npm run build && playwright test",
|
|
12
14
|
"test:watch": "jest --watch",
|
|
13
15
|
"clean": "rm -rf dist"
|
|
14
16
|
},
|
|
@@ -23,11 +25,12 @@
|
|
|
23
25
|
"author": "Everton Da Silva Vieira",
|
|
24
26
|
"license": "MIT",
|
|
25
27
|
"devDependencies": {
|
|
28
|
+
"@playwright/test": "^1.57.0",
|
|
26
29
|
"@types/jest": "^29.5.0",
|
|
27
30
|
"@types/node": "^20.0.0",
|
|
28
31
|
"jest": "^29.5.0",
|
|
29
32
|
"jest-environment-jsdom": "^29.5.0",
|
|
30
|
-
"typescript": "^5.
|
|
33
|
+
"typescript": "^5.9.3"
|
|
31
34
|
},
|
|
32
35
|
"files": [
|
|
33
36
|
"dist",
|