@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.
- package/AGENTS-TEMPLATE.md +173 -0
- package/README.md +867 -0
- package/agents/ti-researcher.md +108 -0
- package/bin/titools.js +53 -0
- package/lib/commands/agents.js +126 -0
- package/lib/commands/install.js +188 -0
- package/lib/commands/uninstall.js +215 -0
- package/lib/commands/update.js +159 -0
- package/lib/config.js +119 -0
- package/lib/downloader.js +153 -0
- package/lib/installer.js +253 -0
- package/lib/platform.js +108 -0
- package/lib/symlink.js +142 -0
- package/lib/utils.js +270 -0
- package/package.json +67 -0
- package/skills/alloy-expert/SKILL.md +247 -0
- package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
- package/skills/alloy-expert/references/alloy-structure.md +381 -0
- package/skills/alloy-expert/references/anti-patterns.md +133 -0
- package/skills/alloy-expert/references/code-conventions.md +469 -0
- package/skills/alloy-expert/references/contracts.md +280 -0
- package/skills/alloy-expert/references/controller-patterns.md +520 -0
- package/skills/alloy-expert/references/error-handling.md +484 -0
- package/skills/alloy-expert/references/examples.md +735 -0
- package/skills/alloy-expert/references/migration-patterns.md +298 -0
- package/skills/alloy-expert/references/patterns.md +448 -0
- package/skills/alloy-expert/references/performance-patterns.md +855 -0
- package/skills/alloy-expert/references/security-patterns.md +847 -0
- package/skills/alloy-expert/references/state-management.md +779 -0
- package/skills/alloy-expert/references/testing.md +872 -0
- package/skills/alloy-guides/SKILL.md +214 -0
- package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
- package/skills/alloy-guides/references/CONCEPTS.md +191 -0
- package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
- package/skills/alloy-guides/references/MODELS.md +1028 -0
- package/skills/alloy-guides/references/PURGETSS.md +56 -0
- package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
- package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
- package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
- package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
- package/skills/alloy-guides/references/WIDGETS.md +176 -0
- package/skills/alloy-howtos/SKILL.md +203 -0
- package/skills/alloy-howtos/references/best_practices.md +138 -0
- package/skills/alloy-howtos/references/cli_reference.md +253 -0
- package/skills/alloy-howtos/references/config_files.md +87 -0
- package/skills/alloy-howtos/references/custom_tags.md +147 -0
- package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
- package/skills/alloy-howtos/references/samples.md +167 -0
- package/skills/purgetss/SKILL.md +442 -0
- package/skills/purgetss/assets/purgetss.config.cjs +17 -0
- package/skills/purgetss/references/EXAMPLES.md +247 -0
- package/skills/purgetss/references/animation-system.md +1294 -0
- package/skills/purgetss/references/apply-directive.md +375 -0
- package/skills/purgetss/references/arbitrary-values.md +612 -0
- package/skills/purgetss/references/class-index.md +1350 -0
- package/skills/purgetss/references/cli-commands.md +948 -0
- package/skills/purgetss/references/configurable-properties.md +654 -0
- package/skills/purgetss/references/custom-rules.md +161 -0
- package/skills/purgetss/references/customization-deep-dive.md +722 -0
- package/skills/purgetss/references/dynamic-component-creation.md +489 -0
- package/skills/purgetss/references/grid-layout.md +455 -0
- package/skills/purgetss/references/icon-fonts.md +609 -0
- package/skills/purgetss/references/installation-setup.md +366 -0
- package/skills/purgetss/references/opacity-modifier.md +291 -0
- package/skills/purgetss/references/platform-modifiers.md +479 -0
- package/skills/purgetss/references/smart-mappings.md +42 -0
- package/skills/purgetss/references/titanium-resets.md +359 -0
- package/skills/purgetss/references/ui-ux-design.md +1526 -0
- package/skills/ti-guides/SKILL.md +94 -0
- package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
- package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
- package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
- package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
- package/skills/ti-guides/references/android-manifest.md +97 -0
- package/skills/ti-guides/references/app-distribution.md +258 -0
- package/skills/ti-guides/references/application-frameworks.md +377 -0
- package/skills/ti-guides/references/cli-reference.md +402 -0
- package/skills/ti-guides/references/coding-best-practices.md +102 -0
- package/skills/ti-guides/references/commonjs-advanced.md +134 -0
- package/skills/ti-guides/references/hello-world.md +100 -0
- package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
- package/skills/ti-guides/references/javascript-primer.md +411 -0
- package/skills/ti-guides/references/reserved-words.md +36 -0
- package/skills/ti-guides/references/resources.md +183 -0
- package/skills/ti-guides/references/style-and-conventions.md +48 -0
- package/skills/ti-guides/references/tiapp-config.md +609 -0
- package/skills/ti-howtos/SKILL.md +174 -0
- package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
- package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
- package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
- package/skills/ti-howtos/references/cross-platform-development.md +348 -0
- package/skills/ti-howtos/references/debugging-profiling.md +543 -0
- package/skills/ti-howtos/references/extending-titanium.md +723 -0
- package/skills/ti-howtos/references/google-maps-v2.md +169 -0
- package/skills/ti-howtos/references/ios-map-kit.md +143 -0
- package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
- package/skills/ti-howtos/references/local-data-sources.md +301 -0
- package/skills/ti-howtos/references/location-and-maps.md +252 -0
- package/skills/ti-howtos/references/media-apis.md +210 -0
- package/skills/ti-howtos/references/notification-services.md +599 -0
- package/skills/ti-howtos/references/remote-data-sources.md +349 -0
- package/skills/ti-howtos/references/tutorials.md +502 -0
- package/skills/ti-howtos/references/using-modules.md +237 -0
- package/skills/ti-howtos/references/web-content-integration.md +307 -0
- package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
- package/skills/ti-ui/SKILL.md +179 -0
- package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
- package/skills/ti-ui/references/animation-and-matrices.md +599 -0
- package/skills/ti-ui/references/application-structures.md +655 -0
- package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
- package/skills/ti-ui/references/event-handling.md +393 -0
- package/skills/ti-ui/references/gestures.md +473 -0
- package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
- package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
- package/skills/ti-ui/references/listviews-and-performance.md +619 -0
- package/skills/ti-ui/references/orientation.md +362 -0
- package/skills/ti-ui/references/platform-ui-android.md +635 -0
- package/skills/ti-ui/references/platform-ui-ios.md +469 -0
- package/skills/ti-ui/references/scrolling-views.md +252 -0
- 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
|
+
```
|