@maccesar/titools 2.0.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 (120) hide show
  1. package/AGENTS-TEMPLATE.md +173 -0
  2. package/README.md +867 -0
  3. package/agents/ti-researcher.md +108 -0
  4. package/bin/titools.js +53 -0
  5. package/lib/commands/agents.js +126 -0
  6. package/lib/commands/install.js +188 -0
  7. package/lib/commands/uninstall.js +215 -0
  8. package/lib/commands/update.js +159 -0
  9. package/lib/config.js +119 -0
  10. package/lib/downloader.js +153 -0
  11. package/lib/installer.js +253 -0
  12. package/lib/platform.js +108 -0
  13. package/lib/symlink.js +142 -0
  14. package/lib/utils.js +270 -0
  15. package/package.json +67 -0
  16. package/skills/alloy-expert/SKILL.md +247 -0
  17. package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
  18. package/skills/alloy-expert/references/alloy-structure.md +381 -0
  19. package/skills/alloy-expert/references/anti-patterns.md +133 -0
  20. package/skills/alloy-expert/references/code-conventions.md +469 -0
  21. package/skills/alloy-expert/references/contracts.md +280 -0
  22. package/skills/alloy-expert/references/controller-patterns.md +520 -0
  23. package/skills/alloy-expert/references/error-handling.md +484 -0
  24. package/skills/alloy-expert/references/examples.md +735 -0
  25. package/skills/alloy-expert/references/migration-patterns.md +298 -0
  26. package/skills/alloy-expert/references/patterns.md +448 -0
  27. package/skills/alloy-expert/references/performance-patterns.md +855 -0
  28. package/skills/alloy-expert/references/security-patterns.md +847 -0
  29. package/skills/alloy-expert/references/state-management.md +779 -0
  30. package/skills/alloy-expert/references/testing.md +872 -0
  31. package/skills/alloy-guides/SKILL.md +214 -0
  32. package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
  33. package/skills/alloy-guides/references/CONCEPTS.md +191 -0
  34. package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
  35. package/skills/alloy-guides/references/MODELS.md +1028 -0
  36. package/skills/alloy-guides/references/PURGETSS.md +56 -0
  37. package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
  38. package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
  39. package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
  40. package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
  41. package/skills/alloy-guides/references/WIDGETS.md +176 -0
  42. package/skills/alloy-howtos/SKILL.md +203 -0
  43. package/skills/alloy-howtos/references/best_practices.md +138 -0
  44. package/skills/alloy-howtos/references/cli_reference.md +253 -0
  45. package/skills/alloy-howtos/references/config_files.md +87 -0
  46. package/skills/alloy-howtos/references/custom_tags.md +147 -0
  47. package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
  48. package/skills/alloy-howtos/references/samples.md +167 -0
  49. package/skills/purgetss/SKILL.md +442 -0
  50. package/skills/purgetss/assets/purgetss.config.cjs +17 -0
  51. package/skills/purgetss/references/EXAMPLES.md +247 -0
  52. package/skills/purgetss/references/animation-system.md +1294 -0
  53. package/skills/purgetss/references/apply-directive.md +375 -0
  54. package/skills/purgetss/references/arbitrary-values.md +612 -0
  55. package/skills/purgetss/references/class-index.md +1350 -0
  56. package/skills/purgetss/references/cli-commands.md +948 -0
  57. package/skills/purgetss/references/configurable-properties.md +654 -0
  58. package/skills/purgetss/references/custom-rules.md +161 -0
  59. package/skills/purgetss/references/customization-deep-dive.md +722 -0
  60. package/skills/purgetss/references/dynamic-component-creation.md +489 -0
  61. package/skills/purgetss/references/grid-layout.md +455 -0
  62. package/skills/purgetss/references/icon-fonts.md +609 -0
  63. package/skills/purgetss/references/installation-setup.md +366 -0
  64. package/skills/purgetss/references/opacity-modifier.md +291 -0
  65. package/skills/purgetss/references/platform-modifiers.md +479 -0
  66. package/skills/purgetss/references/smart-mappings.md +42 -0
  67. package/skills/purgetss/references/titanium-resets.md +359 -0
  68. package/skills/purgetss/references/ui-ux-design.md +1526 -0
  69. package/skills/ti-guides/SKILL.md +94 -0
  70. package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
  71. package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
  72. package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
  73. package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
  74. package/skills/ti-guides/references/android-manifest.md +97 -0
  75. package/skills/ti-guides/references/app-distribution.md +258 -0
  76. package/skills/ti-guides/references/application-frameworks.md +377 -0
  77. package/skills/ti-guides/references/cli-reference.md +402 -0
  78. package/skills/ti-guides/references/coding-best-practices.md +102 -0
  79. package/skills/ti-guides/references/commonjs-advanced.md +134 -0
  80. package/skills/ti-guides/references/hello-world.md +100 -0
  81. package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
  82. package/skills/ti-guides/references/javascript-primer.md +411 -0
  83. package/skills/ti-guides/references/reserved-words.md +36 -0
  84. package/skills/ti-guides/references/resources.md +183 -0
  85. package/skills/ti-guides/references/style-and-conventions.md +48 -0
  86. package/skills/ti-guides/references/tiapp-config.md +609 -0
  87. package/skills/ti-howtos/SKILL.md +174 -0
  88. package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
  89. package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
  90. package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
  91. package/skills/ti-howtos/references/cross-platform-development.md +348 -0
  92. package/skills/ti-howtos/references/debugging-profiling.md +543 -0
  93. package/skills/ti-howtos/references/extending-titanium.md +723 -0
  94. package/skills/ti-howtos/references/google-maps-v2.md +169 -0
  95. package/skills/ti-howtos/references/ios-map-kit.md +143 -0
  96. package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
  97. package/skills/ti-howtos/references/local-data-sources.md +301 -0
  98. package/skills/ti-howtos/references/location-and-maps.md +252 -0
  99. package/skills/ti-howtos/references/media-apis.md +210 -0
  100. package/skills/ti-howtos/references/notification-services.md +599 -0
  101. package/skills/ti-howtos/references/remote-data-sources.md +349 -0
  102. package/skills/ti-howtos/references/tutorials.md +502 -0
  103. package/skills/ti-howtos/references/using-modules.md +237 -0
  104. package/skills/ti-howtos/references/web-content-integration.md +307 -0
  105. package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
  106. package/skills/ti-ui/SKILL.md +179 -0
  107. package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
  108. package/skills/ti-ui/references/animation-and-matrices.md +599 -0
  109. package/skills/ti-ui/references/application-structures.md +655 -0
  110. package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
  111. package/skills/ti-ui/references/event-handling.md +393 -0
  112. package/skills/ti-ui/references/gestures.md +473 -0
  113. package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
  114. package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
  115. package/skills/ti-ui/references/listviews-and-performance.md +619 -0
  116. package/skills/ti-ui/references/orientation.md +362 -0
  117. package/skills/ti-ui/references/platform-ui-android.md +635 -0
  118. package/skills/ti-ui/references/platform-ui-ios.md +469 -0
  119. package/skills/ti-ui/references/scrolling-views.md +252 -0
  120. package/skills/ti-ui/references/tableviews.md +568 -0
@@ -0,0 +1,779 @@
1
+ # State Management Patterns
2
+
3
+ ## The Problem
4
+
5
+ As apps grow, state gets scattered:
6
+ - `Alloy.Collections` scattered across controllers
7
+ - `Ti.App.Properties` for persistence
8
+ - Global variables in `alloy.js`
9
+ - Service-level caches
10
+
11
+ This leads to:
12
+ - Duplicated state sources
13
+ - Inconsistent UI updates
14
+ - Difficult debugging
15
+
16
+ ## Solution: Centralized State Store
17
+
18
+ Create a single source of truth using Backbone.Events:
19
+
20
+ ```javascript
21
+ // lib/services/stateStore.js
22
+ const Backbone = require('alloy/backbone')
23
+
24
+ class StateStore {
25
+ constructor() {
26
+ // Internal state
27
+ this.state = {
28
+ user: null,
29
+ authToken: null,
30
+ preferences: {},
31
+ ui: {
32
+ isLoading: false,
33
+ currentRoute: null
34
+ }
35
+ }
36
+
37
+ // Event bus for reactivity
38
+ this.events = _.clone(Backbone.Events)
39
+ }
40
+
41
+ // Get current state (immutable)
42
+ getState() {
43
+ return { ...this.state }
44
+ }
45
+
46
+ // Update state and notify listeners
47
+ setState(updates) {
48
+ const oldState = this.getState()
49
+ this.state = { ...this.state, ...updates }
50
+
51
+ // Emit change event
52
+ this.events.trigger('change', this.state, oldState)
53
+ }
54
+
55
+ // Listen to specific state changes
56
+ onChange(callback) {
57
+ this.events.on('change', callback)
58
+ }
59
+
60
+ // Remove listener
61
+ offChange(callback) {
62
+ this.events.off('change', callback)
63
+ }
64
+
65
+ // Listen to specific property
66
+ onPropertyChange(property, callback) {
67
+ this.events.on('change', (newState, oldState) => {
68
+ if (newState[property] !== oldState[property]) {
69
+ callback(newState[property], oldState[property])
70
+ }
71
+ })
72
+ }
73
+ }
74
+
75
+ // Singleton instance
76
+ exports.appStore = new StateStore()
77
+ ```
78
+
79
+ ## State Store Usage
80
+
81
+ ### Initialization in alloy.js
82
+
83
+ ```javascript
84
+ // alloy.js
85
+ const { appStore } = require('lib/services/stateStore')
86
+ const { TokenStorage } = require('lib/services/tokenStorage')
87
+
88
+ // Initialize store from persisted data
89
+ function initAppStore() {
90
+ const token = TokenStorage.get('authToken')
91
+ const userJson = Ti.App.Properties.getString('user')
92
+
93
+ if (token && userJson) {
94
+ appStore.setState({
95
+ authToken: token,
96
+ user: JSON.parse(userJson)
97
+ })
98
+ }
99
+ }
100
+
101
+ initAppStore()
102
+ ```
103
+
104
+ ### Using State in Controllers
105
+
106
+ ```javascript
107
+ // controllers/home/index.js
108
+ const { appStore } = require('lib/services/stateStore')
109
+
110
+ function init() {
111
+ // Get current state
112
+ const state = appStore.getState()
113
+
114
+ if (state.user) {
115
+ updateUIForUser(state.user)
116
+ }
117
+
118
+ // Listen for state changes
119
+ appStore.onChange((newState) => {
120
+ if (newState.user !== state.user) {
121
+ updateUIForUser(newState.user)
122
+ }
123
+ })
124
+
125
+ // Listen to specific property
126
+ appStore.onPropertyChange('ui', (ui) => {
127
+ if (ui.isLoading) {
128
+ $.loadingIndicator.show()
129
+ } else {
130
+ $.loadingIndicator.hide()
131
+ }
132
+ })
133
+ }
134
+
135
+ function cleanup() {
136
+ // Always remove listeners
137
+ appStore.offChange()
138
+ $.destroy()
139
+ }
140
+ ```
141
+
142
+ ### Updating State from Services
143
+
144
+ ```javascript
145
+ // lib/services/authService.js
146
+ const { appStore } = require('lib/services/stateStore')
147
+ const { TokenStorage } = require('lib/services/tokenStorage')
148
+ const api = require('lib/api/authApi')
149
+
150
+ exports.login = async function(email, password) {
151
+ appStore.setState({
152
+ 'ui.isLoading': true
153
+ })
154
+
155
+ try {
156
+ const response = await api.login(email, password)
157
+
158
+ // Update store
159
+ appStore.setState({
160
+ user: response.user,
161
+ authToken: response.token,
162
+ 'ui.isLoading': false
163
+ })
164
+
165
+ // Persist
166
+ TokenStorage.save(response.token)
167
+ Ti.App.Properties.setString('user', JSON.stringify(response.user))
168
+
169
+ return response.user
170
+
171
+ } catch (error) {
172
+ appStore.setState({
173
+ 'ui.isLoading': false
174
+ })
175
+ throw error
176
+ }
177
+ }
178
+
179
+ exports.logout = async function() {
180
+ // Clear store
181
+ appStore.setState({
182
+ user: null,
183
+ authToken: null
184
+ })
185
+
186
+ // Clear persistence
187
+ TokenStorage.clear()
188
+ Ti.App.Properties.removeProperty('user')
189
+ }
190
+ ```
191
+
192
+ ## Service Layer State Caching
193
+
194
+ Services can maintain their own cached state:
195
+
196
+ ```javascript
197
+ // lib/services/userService.js
198
+ const { appStore } = require('lib/services/stateStore')
199
+
200
+ class UserService {
201
+ constructor() {
202
+ this.cache = new Map()
203
+ }
204
+
205
+ async getProfile(userId) {
206
+ // Check cache first
207
+ if (this.cache.has(userId)) {
208
+ return this.cache.get(userId)
209
+ }
210
+
211
+ // Fetch from API
212
+ const profile = await api.getUserProfile(userId)
213
+
214
+ // Cache it
215
+ this.cache.set(userId, profile)
216
+
217
+ return profile
218
+ }
219
+
220
+ invalidateCache(userId) {
221
+ this.cache.delete(userId)
222
+ }
223
+
224
+ clearAllCache() {
225
+ this.cache.clear()
226
+ }
227
+ }
228
+
229
+ exports.userService = new UserService()
230
+ ```
231
+
232
+ ## Collections vs State Store
233
+
234
+ **Use Alloy.Collections when:**
235
+ - Data is list-based (users, items, messages)
236
+ - You need data binding in views
237
+ - Data comes from API or SQLite
238
+
239
+ **Use State Store when:**
240
+ - Data is app-wide state (user, auth, settings)
241
+ - You need reactive updates across controllers
242
+ - Data is not list-based
243
+
244
+ You can use both together:
245
+
246
+ ```javascript
247
+ // Good: Collections for data, Store for UI state
248
+ appStore.setState({ 'ui.isLoading': true })
249
+
250
+ Alloy.Collections.users.fetch({
251
+ success: () => {
252
+ appStore.setState({ 'ui.isLoading': false })
253
+ }
254
+ })
255
+ ```
256
+
257
+ ## State Synchronization Pattern
258
+
259
+ When state needs to sync with server:
260
+
261
+ ```javascript
262
+ // lib/services/syncService.js
263
+ const { appStore } = require('lib/services/stateStore')
264
+
265
+ exports.syncWithServer = function() {
266
+ const localState = appStore.getState()
267
+
268
+ return api.syncState(localState)
269
+ .then((serverState) => {
270
+ // Merge server state with local
271
+ appStore.setState(serverState)
272
+ })
273
+ }
274
+
275
+ // Auto-sync on app resume
276
+ Ti.App.addEventListener('resume', () => {
277
+ syncWithServer().catch(console.error)
278
+ })
279
+ ```
280
+
281
+ ## Anti-Patterns
282
+
283
+ | Anti-Pattern | Why It's Bad | Solution |
284
+ | ------------------------------------- | ---------------------------- | ---------------------------------------- |
285
+ | `Ti.App.fireEvent` for state | No cleanup, memory leaks | Use StateStore with `offChange` |
286
+ | Direct collection mutation | Bypasses reactivity | Use collection methods (`add`, `remove`) |
287
+ | State in multiple places | Inconsistency bugs | Single source of truth |
288
+ | Global variables (`Alloy.Globals`) | No reactivity, hard to track | Use StateStore |
289
+ | Controller-to-controller direct calls | Tight coupling | Use StateStore or events |
290
+
291
+ ## Persistence Strategies
292
+
293
+ ### Ti.App.Properties (Simple Key-Value)
294
+
295
+ Best for: User preferences, flags, simple settings.
296
+
297
+ ```javascript
298
+ // lib/services/preferences.js
299
+ exports.Preferences = {
300
+ // Simple getters/setters with defaults
301
+ get(key, defaultValue = null) {
302
+ const value = Ti.App.Properties.getString(key, null)
303
+ return value ? JSON.parse(value) : defaultValue
304
+ },
305
+
306
+ set(key, value) {
307
+ Ti.App.Properties.setString(key, JSON.stringify(value))
308
+ },
309
+
310
+ remove(key) {
311
+ Ti.App.Properties.removeProperty(key)
312
+ },
313
+
314
+ // Typed helpers
315
+ getBool(key, defaultValue = false) {
316
+ return Ti.App.Properties.getBool(key, defaultValue)
317
+ },
318
+
319
+ setBool(key, value) {
320
+ Ti.App.Properties.setBool(key, value)
321
+ },
322
+
323
+ getInt(key, defaultValue = 0) {
324
+ return Ti.App.Properties.getInt(key, defaultValue)
325
+ },
326
+
327
+ setInt(key, value) {
328
+ Ti.App.Properties.setInt(key, value)
329
+ }
330
+ }
331
+
332
+ // Usage
333
+ Preferences.set('userPrefs', { theme: 'dark', notifications: true })
334
+ const prefs = Preferences.get('userPrefs', { theme: 'light' })
335
+ ```
336
+
337
+ **Limitations:**
338
+ - Not suitable for large data (>1MB)
339
+ - No querying capability
340
+ - Synchronous I/O can block UI
341
+
342
+ ### SQLite (Structured Data)
343
+
344
+ Best for: Lists, searchable data, offline-first apps.
345
+
346
+ ```javascript
347
+ // lib/services/database.js
348
+ const DB_NAME = 'myapp.db'
349
+ const DB_VERSION = 1
350
+
351
+ exports.Database = {
352
+ _db: null,
353
+
354
+ open() {
355
+ if (this._db) return this._db
356
+
357
+ this._db = Ti.Database.open(DB_NAME)
358
+ this._migrate()
359
+
360
+ return this._db
361
+ },
362
+
363
+ close() {
364
+ if (this._db) {
365
+ this._db.close()
366
+ this._db = null
367
+ }
368
+ },
369
+
370
+ _migrate() {
371
+ const currentVersion = Preferences.getInt('dbVersion', 0)
372
+
373
+ if (currentVersion < 1) {
374
+ this._db.execute(`
375
+ CREATE TABLE IF NOT EXISTS users (
376
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
377
+ name TEXT NOT NULL,
378
+ email TEXT UNIQUE,
379
+ created_at INTEGER DEFAULT (strftime('%s', 'now'))
380
+ )
381
+ `)
382
+ this._db.execute(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`)
383
+ }
384
+
385
+ // Add more migrations as needed
386
+ // if (currentVersion < 2) { ... }
387
+
388
+ Preferences.setInt('dbVersion', DB_VERSION)
389
+ },
390
+
391
+ // CRUD operations
392
+ query(sql, params = []) {
393
+ const db = this.open()
394
+ const rows = db.execute(sql, ...params)
395
+ const results = []
396
+
397
+ while (rows.isValidRow()) {
398
+ const row = {}
399
+ for (let i = 0; i < rows.fieldCount; i++) {
400
+ row[rows.fieldName(i)] = rows.field(i)
401
+ }
402
+ results.push(row)
403
+ rows.next()
404
+ }
405
+
406
+ rows.close()
407
+ return results
408
+ },
409
+
410
+ execute(sql, params = []) {
411
+ const db = this.open()
412
+ db.execute(sql, ...params)
413
+ return db.rowsAffected
414
+ }
415
+ }
416
+ ```
417
+
418
+ ### Hybrid Strategy (Recommended)
419
+
420
+ ```javascript
421
+ // lib/services/stateStore.js
422
+ const { Preferences } = require('./preferences')
423
+ const { Database } = require('./database')
424
+
425
+ exports.appStore = {
426
+ _state: {},
427
+ _subscribers: [],
428
+
429
+ // Initialize from persisted data
430
+ async init() {
431
+ // Load preferences (fast)
432
+ this._state.preferences = Preferences.get('appPreferences', {
433
+ theme: 'system',
434
+ language: 'en'
435
+ })
436
+
437
+ // Load auth state from secure storage
438
+ this._state.auth = {
439
+ token: await TokenStorage.get(),
440
+ user: Preferences.get('currentUser', null)
441
+ }
442
+
443
+ // Heavy data loaded on-demand from SQLite
444
+ this._state.lists = {
445
+ loaded: false,
446
+ items: []
447
+ }
448
+ },
449
+
450
+ // Persist specific state slices
451
+ persist(slice) {
452
+ switch (slice) {
453
+ case 'preferences':
454
+ Preferences.set('appPreferences', this._state.preferences)
455
+ break
456
+ case 'user':
457
+ Preferences.set('currentUser', this._state.auth.user)
458
+ break
459
+ case 'lists':
460
+ // Lists go to SQLite
461
+ this._state.lists.items.forEach(item => {
462
+ Database.execute(
463
+ 'INSERT OR REPLACE INTO items (id, name, data) VALUES (?, ?, ?)',
464
+ [item.id, item.name, JSON.stringify(item)]
465
+ )
466
+ })
467
+ break
468
+ }
469
+ }
470
+ }
471
+ ```
472
+
473
+ ### Choosing a Strategy
474
+
475
+ | Data Type | Strategy | Reason |
476
+ | ------------------ | ------------------- | ---------------------- |
477
+ | User preferences | Ti.App.Properties | Simple key-value, fast |
478
+ | Auth tokens | Keychain/KeyStore | Security |
479
+ | User profile | Properties + Secure | Mixed sensitivity |
480
+ | Lists (100+ items) | SQLite | Query, pagination |
481
+ | Offline queue | SQLite | Durability, FIFO |
482
+ | Cache | In-memory + SQLite | Speed + persistence |
483
+
484
+ ## State Middleware
485
+
486
+ ### Logger Middleware
487
+
488
+ ```javascript
489
+ // lib/services/stateStore.js
490
+ class StateStore {
491
+ constructor() {
492
+ this._state = {}
493
+ this._middleware = []
494
+ this._subscribers = []
495
+ }
496
+
497
+ use(middleware) {
498
+ this._middleware.push(middleware)
499
+ return this
500
+ }
501
+
502
+ setState(updates) {
503
+ const oldState = { ...this._state }
504
+ const action = { type: 'SET_STATE', payload: updates }
505
+
506
+ // Run middleware chain
507
+ let finalUpdates = updates
508
+
509
+ for (const middleware of this._middleware) {
510
+ const result = middleware(oldState, action, finalUpdates)
511
+ if (result === false) {
512
+ // Middleware can block state changes
513
+ return
514
+ }
515
+ if (result && typeof result === 'object') {
516
+ finalUpdates = result
517
+ }
518
+ }
519
+
520
+ // Apply state change
521
+ this._state = { ...this._state, ...finalUpdates }
522
+
523
+ // Notify subscribers
524
+ this._subscribers.forEach(sub => sub(this._state, oldState))
525
+ }
526
+ }
527
+
528
+ // Logger middleware
529
+ const loggerMiddleware = (oldState, action, updates) => {
530
+ if (Alloy.CFG.debug) {
531
+ console.log('[State]', action.type, {
532
+ updates,
533
+ oldState,
534
+ timestamp: new Date().toISOString()
535
+ })
536
+ }
537
+ return updates
538
+ }
539
+
540
+ // Validation middleware
541
+ const validationMiddleware = (oldState, action, updates) => {
542
+ // Validate state shape
543
+ if (updates.user && !updates.user.id) {
544
+ console.warn('[State] Invalid user object - missing id')
545
+ return false // Block update
546
+ }
547
+ return updates
548
+ }
549
+
550
+ // Persistence middleware
551
+ const persistMiddleware = (oldState, action, updates) => {
552
+ // Auto-persist certain keys
553
+ const persistKeys = ['preferences', 'user']
554
+
555
+ Object.keys(updates).forEach(key => {
556
+ if (persistKeys.includes(key)) {
557
+ Preferences.set(key, updates[key])
558
+ }
559
+ })
560
+
561
+ return updates
562
+ }
563
+
564
+ // Setup
565
+ exports.appStore = new StateStore()
566
+ appStore
567
+ .use(loggerMiddleware)
568
+ .use(validationMiddleware)
569
+ .use(persistMiddleware)
570
+ ```
571
+
572
+ ### Action-Based State Updates
573
+
574
+ ```javascript
575
+ // lib/services/stateStore.js
576
+ const reducers = {
577
+ 'user/login': (state, payload) => ({
578
+ ...state,
579
+ auth: {
580
+ isAuthenticated: true,
581
+ user: payload.user,
582
+ token: payload.token
583
+ }
584
+ }),
585
+
586
+ 'user/logout': (state) => ({
587
+ ...state,
588
+ auth: {
589
+ isAuthenticated: false,
590
+ user: null,
591
+ token: null
592
+ }
593
+ }),
594
+
595
+ 'cart/add': (state, payload) => ({
596
+ ...state,
597
+ cart: {
598
+ items: [...state.cart.items, payload.item],
599
+ total: state.cart.total + payload.item.price
600
+ }
601
+ }),
602
+
603
+ 'cart/remove': (state, payload) => {
604
+ const item = state.cart.items.find(i => i.id === payload.itemId)
605
+ return {
606
+ ...state,
607
+ cart: {
608
+ items: state.cart.items.filter(i => i.id !== payload.itemId),
609
+ total: state.cart.total - (item?.price || 0)
610
+ }
611
+ }
612
+ }
613
+ }
614
+
615
+ class StateStore {
616
+ dispatch(action, payload) {
617
+ const reducer = reducers[action]
618
+
619
+ if (!reducer) {
620
+ console.warn(`[State] Unknown action: ${action}`)
621
+ return
622
+ }
623
+
624
+ const oldState = { ...this._state }
625
+ this._state = reducer(this._state, payload)
626
+
627
+ // Run middleware
628
+ for (const middleware of this._middleware) {
629
+ middleware(oldState, { type: action, payload }, this._state)
630
+ }
631
+
632
+ // Notify subscribers
633
+ this._subscribers.forEach(sub => sub(this._state, oldState))
634
+ }
635
+ }
636
+
637
+ // Usage
638
+ appStore.dispatch('user/login', { user: userData, token: authToken })
639
+ appStore.dispatch('cart/add', { item: product })
640
+ ```
641
+
642
+ ## State Debugging
643
+
644
+ ### Debug Helper
645
+
646
+ ```javascript
647
+ // lib/services/stateDebug.js
648
+ exports.StateDebug = {
649
+ _history: [],
650
+ _maxHistory: 50,
651
+
652
+ // Record state change
653
+ record(action, oldState, newState) {
654
+ if (!Alloy.CFG.debug) return
655
+
656
+ this._history.push({
657
+ timestamp: Date.now(),
658
+ action,
659
+ oldState: JSON.parse(JSON.stringify(oldState)),
660
+ newState: JSON.parse(JSON.stringify(newState)),
661
+ diff: this._computeDiff(oldState, newState)
662
+ })
663
+
664
+ // Trim history
665
+ if (this._history.length > this._maxHistory) {
666
+ this._history.shift()
667
+ }
668
+ },
669
+
670
+ // Get state history
671
+ getHistory() {
672
+ return [...this._history]
673
+ },
674
+
675
+ // Time travel - get state at specific point
676
+ getStateAt(index) {
677
+ if (index < 0 || index >= this._history.length) return null
678
+ return this._history[index].newState
679
+ },
680
+
681
+ // Compute diff between states
682
+ _computeDiff(oldState, newState) {
683
+ const diff = { added: {}, removed: {}, changed: {} }
684
+
685
+ // Find changed and added
686
+ Object.keys(newState).forEach(key => {
687
+ if (!(key in oldState)) {
688
+ diff.added[key] = newState[key]
689
+ } else if (JSON.stringify(oldState[key]) !== JSON.stringify(newState[key])) {
690
+ diff.changed[key] = { old: oldState[key], new: newState[key] }
691
+ }
692
+ })
693
+
694
+ // Find removed
695
+ Object.keys(oldState).forEach(key => {
696
+ if (!(key in newState)) {
697
+ diff.removed[key] = oldState[key]
698
+ }
699
+ })
700
+
701
+ return diff
702
+ },
703
+
704
+ // Print current state (for debugging)
705
+ logState() {
706
+ console.log('[State Debug] Current state:', JSON.stringify(appStore.getState(), null, 2))
707
+ },
708
+
709
+ // Print history
710
+ logHistory() {
711
+ this._history.forEach((entry, i) => {
712
+ console.log(`[${i}] ${entry.action}:`, entry.diff)
713
+ })
714
+ }
715
+ }
716
+
717
+ // Integrate with store middleware
718
+ const debugMiddleware = (oldState, action, newState) => {
719
+ StateDebug.record(action.type, oldState, newState)
720
+ return newState
721
+ }
722
+
723
+ appStore.use(debugMiddleware)
724
+ ```
725
+
726
+ ### Development Panel
727
+
728
+ ```javascript
729
+ // controllers/debug/stateViewer.js (Development only)
730
+ const { StateDebug } = require('lib/services/stateDebug')
731
+ const { appStore } = require('lib/services/stateStore')
732
+
733
+ function init() {
734
+ if (!Alloy.CFG.debug) {
735
+ $.getView().close()
736
+ return
737
+ }
738
+
739
+ refreshView()
740
+
741
+ // Subscribe to state changes
742
+ appStore.onChange(refreshView)
743
+ }
744
+
745
+ function refreshView() {
746
+ const state = appStore.getState()
747
+
748
+ // Show state tree
749
+ $.stateTree.text = JSON.stringify(state, null, 2)
750
+
751
+ // Show history
752
+ const history = StateDebug.getHistory()
753
+ $.historyList.sections[0].items = history.map((entry, i) => ({
754
+ template: 'historyItem',
755
+ index: { text: String(i) },
756
+ action: { text: entry.action },
757
+ time: { text: new Date(entry.timestamp).toLocaleTimeString() }
758
+ }))
759
+ }
760
+
761
+ function onHistoryItemClick(e) {
762
+ const index = e.itemIndex
763
+ const entry = StateDebug.getHistory()[index]
764
+
765
+ // Show diff
766
+ Ti.UI.createAlertDialog({
767
+ title: entry.action,
768
+ message: JSON.stringify(entry.diff, null, 2),
769
+ buttonNames: ['OK']
770
+ }).show()
771
+ }
772
+
773
+ function cleanup() {
774
+ appStore.offChange(refreshView)
775
+ $.destroy()
776
+ }
777
+
778
+ $.cleanup = cleanup
779
+ ```