@ngstato/angular 0.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 ADDED
@@ -0,0 +1,388 @@
1
+ # ngStato
2
+
3
+ > Stato est une librairie de gestion d'état Angular moderne pour remplacer NgRx complètement, avec une API plus simple, Signals-first, sans RxJS obligatoire.
4
+
5
+ ![version](https://img.shields.io/badge/version-0.1.0-blue)
6
+ ![tests](https://img.shields.io/badge/tests-144%20%E2%9C%85-green)
7
+ ![bundle](https://img.shields.io/badge/bundle-~3KB-yellow)
8
+ ![angular](https://img.shields.io/badge/Angular-17%2B-red)
9
+ ![license](https://img.shields.io/badge/license-MIT-lightgrey)
10
+
11
+ ---
12
+
13
+ ## La même action. Deux approches.
14
+
15
+ | ❌ NgRx v20 (officiel, MIT) | ✅ ngStato |
16
+ |---|---|
17
+ | `loadStudents: rxMethod<void>(` | `async loadStudents(state) {` |
18
+ | ` pipe(` | ` state.isLoading = true` |
19
+ | ` tap(() => patchState(store, { isLoading: true })),` | ` state.students = await service.getAll()` |
20
+ | ` switchMap(() =>` | ` state.isLoading = false` |
21
+ | ` from(service.getAll()).pipe(` | `}` |
22
+ | ` tapResponse({` | |
23
+ | ` next: (s) => patchState(store, { students: s, isLoading: false }),` | **1 concept : async/await** |
24
+ | ` error: (e) => patchState(store, { error: e.message, isLoading: false })` | **5 lignes** |
25
+ | ` })` | |
26
+ | ` )` | |
27
+ | ` )` | |
28
+ | ` )` | |
29
+ | `)` | |
30
+ | **9 concepts RxJS/NgRx — 14 lignes** | |
31
+
32
+ ---
33
+
34
+ ## Pourquoi switcher vers ngStato ?
35
+
36
+ **1 concept au lieu de 9 pour écrire une action async**
37
+ NgRx nécessite rxMethod, pipe, tap, switchMap, from, tapResponse, patchState... ngStato nécessite uniquement async/await — natif JavaScript.
38
+
39
+ **2x moins de code pour le même résultat**
40
+ Sur un store CRUD complet (5 actions), NgRx v20 nécessite ~90 lignes. ngStato nécessite ~45 lignes.
41
+
42
+ **DevTools sans extension browser**
43
+ NgRx DevTools nécessite l'extension Chrome Redux DevTools. ngStato intègre ses DevTools directement dans l'app — fonctionnels sur tous les browsers et mobile.
44
+
45
+ **Protection production automatique**
46
+ ngStato utilise `isDevMode()` d'Angular — les DevTools sont physiquement impossibles à activer en prod.
47
+
48
+ **38x plus léger — ~3 KB vs ~50 KB gzip**
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install @ngstato/core @ngstato/angular
56
+ ```
57
+
58
+ ```ts
59
+ // app.config.ts
60
+ import { provideStato } from '@ngstato/angular'
61
+ import { isDevMode } from '@angular/core'
62
+
63
+ export const appConfig: ApplicationConfig = {
64
+ providers: [
65
+ provideRouter(routes),
66
+ provideStato({
67
+ http: { baseUrl: 'https://api.monapp.com', timeout: 8000 },
68
+ devtools: isDevMode()
69
+ })
70
+ ]
71
+ }
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Créer un store
77
+
78
+ ```ts
79
+ // user.store.ts
80
+ import { Injectable, OnDestroy, inject } from '@angular/core'
81
+ import { createStore, http, optimistic, retryable, connectDevTools } from '@ngstato/core'
82
+ import { injectStore } from '@ngstato/angular'
83
+
84
+ function createUserStore() {
85
+ const store = createStore({
86
+
87
+ // State
88
+ users: [] as User[],
89
+ isLoading: false,
90
+ error: null as string | null,
91
+
92
+ // Computed — recalculés automatiquement
93
+ computed: {
94
+ total: (state) => state.users.length,
95
+ admins: (state) => state.users.filter(u => u.role === 'admin')
96
+ },
97
+
98
+ // Actions
99
+ actions: {
100
+
101
+ // Chargement avec retry automatique
102
+ loadUsers: retryable(
103
+ async (state) => {
104
+ state.isLoading = true
105
+ state.users = await http.get('/users')
106
+ state.isLoading = false
107
+ },
108
+ { attempts: 3, backoff: 'exponential', delay: 1000 }
109
+ ),
110
+
111
+ // Suppression avec rollback automatique
112
+ deleteUser: optimistic(
113
+ (state, id: string) => {
114
+ state.users = state.users.filter(u => u.id !== id)
115
+ },
116
+ async (_, id: string) => {
117
+ await http.delete(`/users/${id}`)
118
+ }
119
+ ),
120
+
121
+ // Action simple
122
+ async addUser(state, user: UserCreate) {
123
+ const created = await http.post('/users', user)
124
+ state.users = [...state.users, created]
125
+ }
126
+ },
127
+
128
+ // Hooks lifecycle
129
+ hooks: {
130
+ onError: (err, name) => console.error(`[UserStore] ${name}:`, err.message)
131
+ }
132
+ })
133
+
134
+ connectDevTools(store, 'UserStore') // ← une seule ligne
135
+ return store
136
+ }
137
+
138
+ @Injectable({ providedIn: 'root' })
139
+ export class UserStore implements OnDestroy {
140
+ private _store = createUserStore()
141
+
142
+ get users() { return this._store.users }
143
+ get isLoading() { return this._store.isLoading }
144
+ get total() { return this._store.total }
145
+
146
+ loadUsers = (...a: any[]) => this._store.loadUsers(...a)
147
+ deleteUser = (...a: any[]) => this._store.deleteUser(...a)
148
+ addUser = (...a: any[]) => this._store.addUser(...a)
149
+
150
+ ngOnDestroy() { this._store.__store__.destroy(this._store) }
151
+ }
152
+
153
+ // Dans un composant
154
+ @Component({ ... })
155
+ export class UserListComponent {
156
+ store = injectStore(UserStore) // ou inject(UserStore)
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Helpers
163
+
164
+ | Helper | Description | Équivalent NgRx |
165
+ |--------|-------------|-----------------|
166
+ | `abortable()` | Annule la requête précédente automatiquement | switchMap |
167
+ | `debounced()` | Debounce sans RxJS — défaut 300ms | debounceTime |
168
+ | `throttled()` | Throttle sans RxJS | throttleTime |
169
+ | `retryable()` | Retry avec backoff fixe ou exponentiel | retryWhen |
170
+ | `fromStream()` | Écoute Observable/WebSocket/Firebase/Supabase | rxMethod + Effect |
171
+ | `optimistic()` | Update immédiat + rollback automatique si échec | Manuel en NgRx |
172
+
173
+ ```ts
174
+ import { abortable, debounced, throttled, retryable, fromStream, optimistic } from '@ngstato/core'
175
+
176
+ actions: {
177
+ // Annulation auto — comme switchMap
178
+ search: abortable(async (state, q: string, { signal }) => {
179
+ state.results = await fetch(`/api/search?q=${q}`, { signal }).then(r => r.json())
180
+ }),
181
+
182
+ // Debounce 300ms
183
+ filter: debounced((state, q: string) => { state.query = q }, 300),
184
+
185
+ // Retry x3 avec backoff exponentiel
186
+ load: retryable(async (state) => {
187
+ state.data = await http.get('/data')
188
+ }, { attempts: 3, backoff: 'exponential' }),
189
+
190
+ // Realtime WebSocket
191
+ listen: fromStream(
192
+ () => webSocket('wss://api.monapp.com/ws'),
193
+ (state, msg) => { state.messages = [...state.messages, msg] }
194
+ ),
195
+
196
+ // Optimistic + rollback auto
197
+ delete: optimistic(
198
+ (state, id) => { state.items = state.items.filter(i => i.id !== id) },
199
+ async (_, id) => { await http.delete(`/items/${id}`) }
200
+ )
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Client HTTP
207
+
208
+ ```ts
209
+ import { http } from '@ngstato/core'
210
+
211
+ // Configurer via provideStato() — une seule fois
212
+ provideStato({
213
+ http: {
214
+ baseUrl: 'https://api.monapp.com',
215
+ timeout: 8000,
216
+ headers: { 'X-App-Version': '1.0' },
217
+ auth: () => localStorage.getItem('token')
218
+ }
219
+ })
220
+
221
+ // Utiliser partout dans les actions
222
+ await http.get('/users')
223
+ await http.get('/users', { params: { page: 1, limit: 10 } })
224
+ await http.post('/users', { name: 'Alice' })
225
+ await http.put('/users/1', { name: 'Bob' })
226
+ await http.patch('/users/1', { active: false })
227
+ await http.delete('/users/1')
228
+ ```
229
+
230
+ ---
231
+
232
+ ## DevTools
233
+
234
+ Panel intégré dans l'app — sans extension browser, sans installation supplémentaire.
235
+
236
+ - Panel déplaçable à la souris
237
+ - Redimensionnable — coin bas-droite
238
+ - Minimisable — bouton ▼/▲
239
+ - Historique des actions avec durées et timestamps
240
+ - Diff Avant/Après pour chaque action
241
+ - Onglet State — state actuel complet
242
+ - **Désactivé automatiquement en production via `isDevMode()`**
243
+
244
+ ```ts
245
+ // app.config.ts
246
+ provideStato({ devtools: isDevMode() })
247
+
248
+ // app.component.ts
249
+ import { StatoDevToolsComponent } from '@ngstato/angular'
250
+
251
+ @Component({
252
+ imports: [RouterOutlet, StatoDevToolsComponent],
253
+ template: `<router-outlet /><stato-devtools />`
254
+ })
255
+ export class AppComponent {}
256
+
257
+ // mon-store.ts
258
+ connectDevTools(store, 'MonStore') // une seule ligne
259
+ ```
260
+
261
+ | | NgRx DevTools | ngStato DevTools |
262
+ |---|---|---|
263
+ | Installation | Extension Chrome | Zéro installation |
264
+ | Browser support | Chrome/Firefox | Tous browsers |
265
+ | Mobile | ❌ | ✅ |
266
+ | Désactivé en prod | Manuel | `isDevMode()` auto |
267
+ | State visible en prod | Oui si oubli | Jamais |
268
+
269
+ ---
270
+
271
+ ## Guide de migration NgRx → ngStato
272
+
273
+ La migration est progressive — store par store.
274
+
275
+ ```ts
276
+ // withState → state initial
277
+ // NgRx
278
+ withState({ users: [] as User[], isLoading: false })
279
+ // ngStato
280
+ users: [] as User[], isLoading: false,
281
+
282
+ // withMethods + rxMethod → actions
283
+ // NgRx
284
+ withMethods((store) => ({
285
+ load: rxMethod<void>(pipe(
286
+ tap(() => patchState(store, { isLoading: true })),
287
+ switchMap(() => from(service.get()).pipe(
288
+ tapResponse({
289
+ next: (d) => patchState(store, { data: d, isLoading: false }),
290
+ error: (e) => patchState(store, { error: e.message })
291
+ })
292
+ ))
293
+ ))
294
+ }))
295
+ // ngStato
296
+ actions: {
297
+ async load(state) {
298
+ state.isLoading = true
299
+ state.data = await service.get()
300
+ state.isLoading = false
301
+ }
302
+ }
303
+
304
+ // withComputed → computed
305
+ // NgRx
306
+ withComputed((store) => ({
307
+ total: computed(() => store.users().length)
308
+ }))
309
+ // ngStato
310
+ computed: {
311
+ total: (state) => state.users.length
312
+ }
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Comparaison NgRx SignalStore v20 vs ngStato
318
+
319
+ | Feature | NgRx SignalStore v20 | ngStato v0.1 |
320
+ |---------|---------------------|--------------|
321
+ | withState | ✅ | ✅ |
322
+ | withMethods / actions | ✅ rxMethod requis | ✅ async/await |
323
+ | withComputed | ✅ | ✅ |
324
+ | patchState | ✅ obligatoire | ✅ state.x = y |
325
+ | provideStore | ✅ | ✅ provideStato() |
326
+ | inject() | ✅ | ✅ injectStore() |
327
+ | onInit / onDestroy | ✅ | ✅ |
328
+ | DevTools | ✅ extension Chrome | ✅ panel intégré |
329
+ | DevTools mobile | ❌ | ✅ |
330
+ | Protection prod | ⚠️ logOnly manuel | ✅ isDevMode() auto |
331
+ | RxJS requis | ✅ obligatoire | ❌ optionnel |
332
+ | Bundle size | ~50 KB gzip | ~3 KB gzip |
333
+ | withProps | ✅ | 🔜 v0.2 |
334
+ | withEntities | ✅ | 🔜 v1.0 |
335
+ | signalStoreFeature() | ✅ | 🔜 v0.4 |
336
+ | Schematics CLI | ✅ | 🔜 v1.0 |
337
+ | ESLint plugin | ✅ | 🔜 v1.0 |
338
+
339
+ ---
340
+
341
+ ## Roadmap
342
+
343
+ ### v0.1 ✅ TERMINÉ
344
+ - `createStore()` — state, actions, computed, hooks
345
+ - `StatoHttp` — GET POST PUT PATCH DELETE avec auth, timeout, params
346
+ - `abortable()`, `debounced()`, `throttled()`, `retryable()`, `fromStream()`, `optimistic()`
347
+ - `@ngstato/angular` — Signals natifs, `provideStato()`, `injectStore()`
348
+ - DevTools — panel déplaçable, redimensionnable, minimisable
349
+ - `connectDevTools()` — connexion automatique store → DevTools
350
+ - Protection prod automatique via `isDevMode()`
351
+ - **144 tests — 100% passing**
352
+
353
+ ### v0.2 — Helpers avancés
354
+ - `exclusive()` — = exhaustMap NgRx
355
+ - `queued()` — = concatMap NgRx
356
+ - `store.on()` — réactions inter-stores
357
+ - Testing utilities
358
+ - DevTools time-travel
359
+
360
+ ### v0.3 — Persistance
361
+ - `withPersist()` — localStorage / sessionStorage / IndexedDB
362
+ - `withHistory()` — undo/redo
363
+ - SSR ready
364
+
365
+ ### v1.0 — Production ready
366
+ - `withEntities()`, Schematics CLI, ESLint plugin
367
+ - Documentation VitePress complète
368
+ - Benchmarks comparatifs
369
+
370
+ ---
371
+
372
+ ## Contribuer
373
+
374
+ ```bash
375
+ git clone https://github.com/becher/ngstato
376
+ cd ngstato
377
+ pnpm install # Node >= 18, pnpm >= 8
378
+ pnpm build
379
+ pnpm test # 144 tests
380
+ ```
381
+
382
+ Convention commits : `feat` / `fix` / `docs` / `test` / `refactor` / `chore`
383
+
384
+ ---
385
+
386
+ ## License
387
+
388
+ MIT — Copyright (c) 2025 ngStato
@@ -0,0 +1,50 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { Type, OnInit, OnDestroy } from '@angular/core';
3
+ import { StatoConfig, StatoStoreConfig, ActionLog } from '@ngstato/core';
4
+
5
+ declare function injectStore<T>(store: Type<T>): T;
6
+
7
+ interface StatoAngularConfig {
8
+ http?: StatoConfig;
9
+ devtools?: boolean;
10
+ }
11
+ declare function provideStato(config?: StatoAngularConfig): _angular_core.EnvironmentProviders;
12
+
13
+ declare function createAngularStore<S extends object>(config: S & StatoStoreConfig<S>): any;
14
+
15
+ declare class StatoDevToolsComponent implements OnInit, OnDestroy {
16
+ private config;
17
+ private unsub?;
18
+ isOpen: _angular_core.WritableSignal<boolean>;
19
+ isMinimized: _angular_core.WritableSignal<boolean>;
20
+ activeTab: _angular_core.WritableSignal<"actions" | "state">;
21
+ logs: _angular_core.WritableSignal<ActionLog[]>;
22
+ selectedLog: _angular_core.WritableSignal<ActionLog | null>;
23
+ posX: _angular_core.WritableSignal<number>;
24
+ posY: _angular_core.WritableSignal<number>;
25
+ panelWidth: _angular_core.WritableSignal<number>;
26
+ panelHeight: _angular_core.WritableSignal<number>;
27
+ private isDragging;
28
+ private isResizing;
29
+ private dragOffsetX;
30
+ private dragOffsetY;
31
+ private startW;
32
+ private startH;
33
+ private startX;
34
+ private startY;
35
+ private boundMouseMove;
36
+ private boundMouseUp;
37
+ ngOnInit(): void;
38
+ ngOnDestroy(): void;
39
+ toggle(): void;
40
+ toggleMinimize(): void;
41
+ clear(): void;
42
+ selectLog(log: ActionLog): void;
43
+ formatTime(iso: string): string;
44
+ onDragStart(e: MouseEvent): void;
45
+ onResizeStart(e: MouseEvent): void;
46
+ onMouseMove(e: MouseEvent): void;
47
+ onMouseUp(): void;
48
+ }
49
+
50
+ export { type StatoAngularConfig, StatoDevToolsComponent, createAngularStore, injectStore, provideStato };
@@ -0,0 +1,50 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { Type, OnInit, OnDestroy } from '@angular/core';
3
+ import { StatoConfig, StatoStoreConfig, ActionLog } from '@ngstato/core';
4
+
5
+ declare function injectStore<T>(store: Type<T>): T;
6
+
7
+ interface StatoAngularConfig {
8
+ http?: StatoConfig;
9
+ devtools?: boolean;
10
+ }
11
+ declare function provideStato(config?: StatoAngularConfig): _angular_core.EnvironmentProviders;
12
+
13
+ declare function createAngularStore<S extends object>(config: S & StatoStoreConfig<S>): any;
14
+
15
+ declare class StatoDevToolsComponent implements OnInit, OnDestroy {
16
+ private config;
17
+ private unsub?;
18
+ isOpen: _angular_core.WritableSignal<boolean>;
19
+ isMinimized: _angular_core.WritableSignal<boolean>;
20
+ activeTab: _angular_core.WritableSignal<"actions" | "state">;
21
+ logs: _angular_core.WritableSignal<ActionLog[]>;
22
+ selectedLog: _angular_core.WritableSignal<ActionLog | null>;
23
+ posX: _angular_core.WritableSignal<number>;
24
+ posY: _angular_core.WritableSignal<number>;
25
+ panelWidth: _angular_core.WritableSignal<number>;
26
+ panelHeight: _angular_core.WritableSignal<number>;
27
+ private isDragging;
28
+ private isResizing;
29
+ private dragOffsetX;
30
+ private dragOffsetY;
31
+ private startW;
32
+ private startH;
33
+ private startX;
34
+ private startY;
35
+ private boundMouseMove;
36
+ private boundMouseUp;
37
+ ngOnInit(): void;
38
+ ngOnDestroy(): void;
39
+ toggle(): void;
40
+ toggleMinimize(): void;
41
+ clear(): void;
42
+ selectLog(log: ActionLog): void;
43
+ formatTime(iso: string): string;
44
+ onDragStart(e: MouseEvent): void;
45
+ onResizeStart(e: MouseEvent): void;
46
+ onMouseMove(e: MouseEvent): void;
47
+ onMouseUp(): void;
48
+ }
49
+
50
+ export { type StatoAngularConfig, StatoDevToolsComponent, createAngularStore, injectStore, provideStato };