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.
Files changed (2) hide show
  1. package/README.md +321 -81
  2. 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:3000/`.
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
- <span case="idle">Idle</span>
103
- <span case="active">Active</span>
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
- > Note: `when`/`match` bindings are available only in the example dev-server today.
108
- > They are not part of the core runtime yet.
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
- // All updates happen in one frame
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
- ### Resource Management
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
- // Signals
179
- effect(() => {
180
- console.log(data(), loading(), error());
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
- // Cached resource
184
- const cachedData = createCachedResource(
185
- 'user-data',
186
- async (signal) => fetchUser(signal)
187
- );
276
+ **Behavior**
188
277
 
189
- // Auto-refresh
190
- const liveData = createAutoRefreshResource(
191
- fetchData,
192
- 5000 // Refresh every 5 seconds
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
- ### Virtualization (experimental)
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
- // Virtual table
207
- createVirtualTable(
208
- data,
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
- // Infinite scroll
218
- const { items, loading, refresh } = createInfiniteScroll(
219
- (offset, limit) => fetchItems(offset, limit),
220
- (item) => div(item.name),
221
- document.getElementById('scroll-container')
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
- ### DevTools & Warnings (console-only)
226
- ```typescript
227
- // Enable dev tools
228
- initDevTools();
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
- // Inspect signals
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
- // Get active effects
234
- const effects = getActiveEffects();
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
- // Performance monitoring (automatic, console warnings)
237
- monitorPerformance();
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
- > There is no DevTools UI yet. The current tooling is lightweight console diagnostics.
372
+ **What this gives you**
241
373
 
242
- ### Cleanup Utilities
243
- ```typescript
244
- // Event listener with auto-cleanup
245
- useEvent(window, 'resize', handleResize);
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
- // Interval with auto-cleanup
248
- useInterval(() => {
249
- console.log('Tick');
250
- }, 1000);
392
+ Keys are data identity, not fetch parameters.
251
393
 
252
- // Timeout with auto-cleanup
253
- useTimeout(() => {
254
- console.log('Delayed');
255
- }, 2000);
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
- // Fetch with auto-cleanup
258
- const { data, loading, error } = useFetch('/api/data');
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.0.0",
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.js",
11
- "test": "jest",
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.0.0"
33
+ "typescript": "^5.9.3"
31
34
  },
32
35
  "files": [
33
36
  "dist",