@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-native-patterns
|
|
3
|
+
description: React Native patterns for navigation, native modules, animations, platform-specific code, and OTA updates.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Native Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply these patterns when building cross-platform mobile applications with React Native. These cover the core challenges: navigation architecture, bridging to native APIs, performant animations, writing platform-specific code, and shipping updates without app store delays. Use these from project setup to avoid costly refactors later.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Navigation with React Navigation
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/navigation/RootNavigator.tsx
|
|
17
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
18
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
19
|
+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|
20
|
+
|
|
21
|
+
type RootStackParamList = {
|
|
22
|
+
Auth: undefined;
|
|
23
|
+
Main: undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type MainTabParamList = {
|
|
27
|
+
Home: undefined;
|
|
28
|
+
Profile: { userId: string };
|
|
29
|
+
Settings: undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
33
|
+
const Tab = createBottomTabNavigator<MainTabParamList>();
|
|
34
|
+
|
|
35
|
+
function MainTabs() {
|
|
36
|
+
return (
|
|
37
|
+
<Tab.Navigator screenOptions={{ headerShown: false }}>
|
|
38
|
+
<Tab.Screen name="Home" component={HomeScreen} />
|
|
39
|
+
<Tab.Screen name="Profile" component={ProfileScreen} initialParams={{ userId: 'me' }} />
|
|
40
|
+
<Tab.Screen name="Settings" component={SettingsScreen} />
|
|
41
|
+
</Tab.Navigator>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function RootNavigator() {
|
|
46
|
+
const { isAuthenticated } = useAuth();
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<NavigationContainer>
|
|
50
|
+
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
|
51
|
+
{isAuthenticated ? (
|
|
52
|
+
<Stack.Screen name="Main" component={MainTabs} />
|
|
53
|
+
) : (
|
|
54
|
+
<Stack.Screen name="Auth" component={AuthScreen} />
|
|
55
|
+
)}
|
|
56
|
+
</Stack.Navigator>
|
|
57
|
+
</NavigationContainer>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Type-Safe Navigation Hooks
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// src/navigation/hooks.ts
|
|
66
|
+
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
67
|
+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
68
|
+
import type { RouteProp } from '@react-navigation/native';
|
|
69
|
+
|
|
70
|
+
export function useAppNavigation() {
|
|
71
|
+
return useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useProfileRoute() {
|
|
75
|
+
return useRoute<RouteProp<MainTabParamList, 'Profile'>>();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Usage in component:
|
|
79
|
+
function ProfileScreen() {
|
|
80
|
+
const { params } = useProfileRoute();
|
|
81
|
+
const navigation = useAppNavigation();
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Button title="Go Home" onPress={() => navigation.navigate('Main')} />
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Native Module Bridge (Turbo Modules)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// src/native/HapticModule.ts
|
|
93
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
94
|
+
import type { TurboModule } from 'react-native';
|
|
95
|
+
|
|
96
|
+
export interface Spec extends TurboModule {
|
|
97
|
+
trigger(type: string): void;
|
|
98
|
+
impactFeedback(style: 'light' | 'medium' | 'heavy'): void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('HapticModule');
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```swift
|
|
105
|
+
// ios/HapticModule.swift
|
|
106
|
+
@objc(HapticModule)
|
|
107
|
+
class HapticModule: NSObject {
|
|
108
|
+
@objc func trigger(_ type: String) {
|
|
109
|
+
let generator = UINotificationFeedbackGenerator()
|
|
110
|
+
switch type {
|
|
111
|
+
case "success": generator.notificationOccurred(.success)
|
|
112
|
+
case "warning": generator.notificationOccurred(.warning)
|
|
113
|
+
case "error": generator.notificationOccurred(.error)
|
|
114
|
+
default: break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@objc func impactFeedback(_ style: String) {
|
|
119
|
+
let s: UIImpactFeedbackGenerator.FeedbackStyle = style == "heavy" ? .heavy : style == "medium" ? .medium : .light
|
|
120
|
+
UIImpactFeedbackGenerator(style: s).impactOccurred()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@objc static func requiresMainQueueSetup() -> Bool { true }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Reanimated Animations
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// src/components/AnimatedCard.tsx
|
|
131
|
+
import Animated, {
|
|
132
|
+
useSharedValue,
|
|
133
|
+
useAnimatedStyle,
|
|
134
|
+
withSpring,
|
|
135
|
+
withTiming,
|
|
136
|
+
interpolate,
|
|
137
|
+
} from 'react-native-reanimated';
|
|
138
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
139
|
+
|
|
140
|
+
export function AnimatedCard({ children }: { children: React.ReactNode }) {
|
|
141
|
+
const translateX = useSharedValue(0);
|
|
142
|
+
const scale = useSharedValue(1);
|
|
143
|
+
|
|
144
|
+
const panGesture = Gesture.Pan()
|
|
145
|
+
.onUpdate((e) => {
|
|
146
|
+
translateX.value = e.translationX;
|
|
147
|
+
})
|
|
148
|
+
.onEnd(() => {
|
|
149
|
+
if (Math.abs(translateX.value) > 150) {
|
|
150
|
+
translateX.value = withTiming(Math.sign(translateX.value) * 500);
|
|
151
|
+
} else {
|
|
152
|
+
translateX.value = withSpring(0);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const tapGesture = Gesture.Tap()
|
|
157
|
+
.onBegin(() => { scale.value = withSpring(0.95); })
|
|
158
|
+
.onFinalize(() => { scale.value = withSpring(1); });
|
|
159
|
+
|
|
160
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
161
|
+
transform: [
|
|
162
|
+
{ translateX: translateX.value },
|
|
163
|
+
{ scale: scale.value },
|
|
164
|
+
{ rotateZ: `${interpolate(translateX.value, [-200, 0, 200], [-15, 0, 15])}deg` },
|
|
165
|
+
],
|
|
166
|
+
opacity: interpolate(Math.abs(translateX.value), [0, 200], [1, 0.5]),
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const composed = Gesture.Simultaneous(panGesture, tapGesture);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<GestureDetector gesture={composed}>
|
|
173
|
+
<Animated.View style={[styles.card, animatedStyle]}>{children}</Animated.View>
|
|
174
|
+
</GestureDetector>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Platform-Specific Code
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// src/components/Shadow.tsx
|
|
183
|
+
import { Platform, StyleSheet, View, ViewProps } from 'react-native';
|
|
184
|
+
|
|
185
|
+
const styles = StyleSheet.create({
|
|
186
|
+
shadow: Platform.select({
|
|
187
|
+
ios: {
|
|
188
|
+
shadowColor: '#000',
|
|
189
|
+
shadowOffset: { width: 0, height: 2 },
|
|
190
|
+
shadowOpacity: 0.1,
|
|
191
|
+
shadowRadius: 8,
|
|
192
|
+
},
|
|
193
|
+
android: {
|
|
194
|
+
elevation: 4,
|
|
195
|
+
},
|
|
196
|
+
default: {},
|
|
197
|
+
})!,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// File-based platform splitting:
|
|
201
|
+
// src/components/BiometricAuth.ios.ts — Face ID implementation
|
|
202
|
+
// src/components/BiometricAuth.android.ts — Fingerprint implementation
|
|
203
|
+
// src/components/BiometricAuth.ts — exports from platform file
|
|
204
|
+
|
|
205
|
+
export function Shadow({ style, ...props }: ViewProps) {
|
|
206
|
+
return <View style={[styles.shadow, style]} {...props} />;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### OTA Updates with EAS Update
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// src/updates/useAppUpdate.ts
|
|
214
|
+
import * as Updates from 'expo-updates';
|
|
215
|
+
import { useEffect, useState } from 'react';
|
|
216
|
+
import { AppState } from 'react-native';
|
|
217
|
+
|
|
218
|
+
export function useAppUpdate() {
|
|
219
|
+
const [updateAvailable, setUpdateAvailable] = useState(false);
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const subscription = AppState.addEventListener('change', async (state) => {
|
|
223
|
+
if (state !== 'active' || __DEV__) return;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const update = await Updates.checkForUpdateAsync();
|
|
227
|
+
if (update.isAvailable) {
|
|
228
|
+
await Updates.fetchUpdateAsync();
|
|
229
|
+
setUpdateAvailable(true);
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.warn('Update check failed:', e);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return () => subscription.remove();
|
|
237
|
+
}, []);
|
|
238
|
+
|
|
239
|
+
const applyUpdate = async () => {
|
|
240
|
+
await Updates.reloadAsync();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return { updateAvailable, applyUpdate };
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Performance: FlatList Optimization
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// src/components/OptimizedList.tsx
|
|
251
|
+
import { FlatList, View, Text } from 'react-native';
|
|
252
|
+
import { useCallback, memo } from 'react';
|
|
253
|
+
|
|
254
|
+
interface Item { id: string; title: string; subtitle: string }
|
|
255
|
+
|
|
256
|
+
const ListItem = memo(({ item }: { item: Item }) => (
|
|
257
|
+
<View style={{ padding: 16 }}>
|
|
258
|
+
<Text style={{ fontSize: 16, fontWeight: '600' }}>{item.title}</Text>
|
|
259
|
+
<Text style={{ fontSize: 14, color: '#666' }}>{item.subtitle}</Text>
|
|
260
|
+
</View>
|
|
261
|
+
));
|
|
262
|
+
|
|
263
|
+
export function OptimizedList({ data }: { data: Item[] }) {
|
|
264
|
+
const renderItem = useCallback(({ item }: { item: Item }) => <ListItem item={item} />, []);
|
|
265
|
+
const keyExtractor = useCallback((item: Item) => item.id, []);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<FlatList
|
|
269
|
+
data={data}
|
|
270
|
+
renderItem={renderItem}
|
|
271
|
+
keyExtractor={keyExtractor}
|
|
272
|
+
getItemLayout={(_, index) => ({ length: 64, offset: 64 * index, index })}
|
|
273
|
+
maxToRenderPerBatch={20}
|
|
274
|
+
windowSize={5}
|
|
275
|
+
removeClippedSubviews
|
|
276
|
+
initialNumToRender={15}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Examples
|
|
283
|
+
|
|
284
|
+
| Pattern | Problem | Solution |
|
|
285
|
+
|---------|---------|----------|
|
|
286
|
+
| Conditional navigator | Auth vs main flow | Render different Stack.Screens based on auth state |
|
|
287
|
+
| Turbo Module | Need native haptics/biometrics | Define spec interface, implement per platform |
|
|
288
|
+
| Reanimated shared value | Smooth gesture animations | `useSharedValue` + `useAnimatedStyle` on UI thread |
|
|
289
|
+
| Platform.select | Different shadow APIs | iOS shadowColor vs Android elevation |
|
|
290
|
+
| EAS Update | Ship fixes without app store | `expo-updates` check on app foreground |
|
|
291
|
+
|
|
292
|
+
## Checklist
|
|
293
|
+
- [ ] Navigation typed with ParamList generics for compile-time route safety
|
|
294
|
+
- [ ] Native modules use Turbo Module spec for New Architecture compatibility
|
|
295
|
+
- [ ] Animations run on UI thread via Reanimated worklets (no `runOnJS` in hot paths)
|
|
296
|
+
- [ ] Platform-specific files use `.ios.ts` / `.android.ts` suffixes where divergence is large
|
|
297
|
+
- [ ] FlatList uses `getItemLayout`, `memo`, and `removeClippedSubviews` for large lists
|
|
298
|
+
- [ ] OTA updates checked on app foreground with user-visible apply prompt
|
|
299
|
+
- [ ] Hermes engine enabled for both iOS and Android builds
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: real-time-patterns
|
|
3
|
+
description: Real-time patterns for SSE, WebSockets, reconnection with backoff, presence channels, and Redis scaling.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Real-Time Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use real-time patterns when users need instant updates: chat messages, notifications, live dashboards, collaborative editing, or auction bidding. Choose SSE for server-to-client streaming, WebSockets for bidirectional communication, and long polling as a universal fallback. This skill covers implementation, reconnection, presence, and multi-server scaling.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Server-Sent Events (SSE) -- Simplest Server Push
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// Server -- one-direction, built-in auto-reconnection
|
|
17
|
+
app.get("/events/:userId", (req, res) => {
|
|
18
|
+
res.writeHead(200, {
|
|
19
|
+
"Content-Type": "text/event-stream",
|
|
20
|
+
"Cache-Control": "no-cache",
|
|
21
|
+
"Connection": "keep-alive",
|
|
22
|
+
"X-Accel-Buffering": "no",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const userId = req.params.userId;
|
|
26
|
+
const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), 30_000);
|
|
27
|
+
|
|
28
|
+
const handler = (event: AppEvent) => {
|
|
29
|
+
res.write(`event: ${event.type}\n`);
|
|
30
|
+
res.write(`data: ${JSON.stringify(event.payload)}\n`);
|
|
31
|
+
res.write(`id: ${event.id}\n\n`);
|
|
32
|
+
};
|
|
33
|
+
eventBus.subscribe(userId, handler);
|
|
34
|
+
|
|
35
|
+
req.on("close", () => {
|
|
36
|
+
clearInterval(heartbeat);
|
|
37
|
+
eventBus.unsubscribe(userId, handler);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Client -- browser auto-reconnects with Last-Event-ID header
|
|
42
|
+
const source = new EventSource("/events/user-123");
|
|
43
|
+
source.addEventListener("notification", (e) => {
|
|
44
|
+
showNotification(JSON.parse(e.data));
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### WebSockets -- Bidirectional Communication
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
52
|
+
|
|
53
|
+
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
54
|
+
const rooms = new Map<string, Set<WebSocket>>();
|
|
55
|
+
|
|
56
|
+
wss.on("connection", (ws, req) => {
|
|
57
|
+
const userId = authenticateFromCookie(req);
|
|
58
|
+
if (!userId) { ws.close(4001, "Unauthorized"); return; }
|
|
59
|
+
|
|
60
|
+
ws.on("message", (raw) => {
|
|
61
|
+
const msg = JSON.parse(raw.toString());
|
|
62
|
+
switch (msg.type) {
|
|
63
|
+
case "join": joinRoom(msg.room, ws); break;
|
|
64
|
+
case "message":
|
|
65
|
+
broadcastToRoom(msg.room, {
|
|
66
|
+
type: "message", from: userId, text: msg.text, timestamp: Date.now(),
|
|
67
|
+
}, ws);
|
|
68
|
+
break;
|
|
69
|
+
case "ping": ws.send(JSON.stringify({ type: "pong" })); break;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ws.on("close", () => removeFromAllRooms(ws));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function broadcastToRoom(room: string, data: object, exclude?: WebSocket) {
|
|
77
|
+
const members = rooms.get(room);
|
|
78
|
+
if (!members) return;
|
|
79
|
+
const payload = JSON.stringify(data);
|
|
80
|
+
for (const client of members) {
|
|
81
|
+
if (client !== exclude && client.readyState === WebSocket.OPEN) {
|
|
82
|
+
client.send(payload);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Client Reconnection with Backoff
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
class ReconnectingSocket {
|
|
92
|
+
private ws: WebSocket | null = null;
|
|
93
|
+
private attempt = 0;
|
|
94
|
+
private maxDelay = 30_000;
|
|
95
|
+
|
|
96
|
+
constructor(private url: string, private onMessage: (data: any) => void) {
|
|
97
|
+
this.connect();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private connect() {
|
|
101
|
+
this.ws = new WebSocket(this.url);
|
|
102
|
+
this.ws.onopen = () => { this.attempt = 0; };
|
|
103
|
+
this.ws.onmessage = (e) => this.onMessage(JSON.parse(e.data));
|
|
104
|
+
this.ws.onclose = (e) => {
|
|
105
|
+
if (e.code !== 1000) this.scheduleReconnect();
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private scheduleReconnect() {
|
|
110
|
+
const delay = Math.min(1000 * Math.pow(2, this.attempt), this.maxDelay);
|
|
111
|
+
const jitter = delay * 0.3 * Math.random();
|
|
112
|
+
this.attempt++;
|
|
113
|
+
setTimeout(() => this.connect(), delay + jitter);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
send(data: object) {
|
|
117
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
118
|
+
this.ws.send(JSON.stringify(data));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Presence Channels -- Who Is Online
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const presence = new Map<string, { userId: string; lastSeen: number }>();
|
|
128
|
+
|
|
129
|
+
function updatePresence(userId: string) {
|
|
130
|
+
presence.set(userId, { userId, lastSeen: Date.now() });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getOnlineUsers(staleMs = 60_000): string[] {
|
|
134
|
+
const cutoff = Date.now() - staleMs;
|
|
135
|
+
const online: string[] = [];
|
|
136
|
+
for (const [userId, info] of presence) {
|
|
137
|
+
if (info.lastSeen > cutoff) online.push(userId);
|
|
138
|
+
else presence.delete(userId);
|
|
139
|
+
}
|
|
140
|
+
return online;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Multi-Server Scaling with Redis
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { createClient } from "redis";
|
|
148
|
+
|
|
149
|
+
const pub = createClient();
|
|
150
|
+
const sub = createClient();
|
|
151
|
+
await pub.connect();
|
|
152
|
+
await sub.connect();
|
|
153
|
+
|
|
154
|
+
function broadcastToCluster(channel: string, data: object) {
|
|
155
|
+
pub.publish(`ws:${channel}`, JSON.stringify(data));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await sub.pSubscribe("ws:*", (message, channel) => {
|
|
159
|
+
const room = channel.replace("ws:", "");
|
|
160
|
+
broadcastToRoom(room, JSON.parse(message));
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Examples
|
|
165
|
+
|
|
166
|
+
| Transport | Direction | Auto-Reconnect | Use Case |
|
|
167
|
+
|-----------|-----------|----------------|----------|
|
|
168
|
+
| SSE | Server to client | Yes (built-in) | Notifications, live feeds |
|
|
169
|
+
| WebSocket | Bidirectional | Manual (implement) | Chat, gaming, collaboration |
|
|
170
|
+
| Long polling | Simulated push | N/A (client loops) | Legacy browser fallback |
|
|
171
|
+
| Socket.io | Bidirectional | Yes (built-in) | WebSocket + fallback + rooms |
|
|
172
|
+
|
|
173
|
+
## Checklist
|
|
174
|
+
- [ ] Transport chosen based on requirements (SSE for push-only, WS for bidirectional)
|
|
175
|
+
- [ ] Authentication checked on connection, not just on first message
|
|
176
|
+
- [ ] Heartbeat/keepalive prevents proxy and load balancer timeouts
|
|
177
|
+
- [ ] Client reconnects with exponential backoff and jitter
|
|
178
|
+
- [ ] Last-Event-ID or cursor used to resume without data loss
|
|
179
|
+
- [ ] Presence tracking uses heartbeat + stale cleanup
|
|
180
|
+
- [ ] Multi-server scaling uses Redis pub/sub or a message broker
|
|
181
|
+
- [ ] Connection count monitored with alerting configured
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: redis-patterns
|
|
3
|
+
description: Redis patterns for caching, pub/sub, streams, sorted sets, Lua scripts, and cluster configuration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Redis Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use Redis when you need sub-millisecond reads, ephemeral data structures, or coordination between services. Common use cases: caching expensive queries, rate limiting API endpoints, real-time leaderboards, job queues, session storage, and distributed locking. Choose the right data structure for each problem.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Cache-Aside with TTL
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
async function getUser(userId: string): Promise<User> {
|
|
17
|
+
const cacheKey = `user:${userId}`;
|
|
18
|
+
const cached = await redis.get(cacheKey);
|
|
19
|
+
if (cached) return JSON.parse(cached);
|
|
20
|
+
|
|
21
|
+
const user = await db.users.findUnique({ where: { id: userId } });
|
|
22
|
+
if (user) {
|
|
23
|
+
await redis.set(cacheKey, JSON.stringify(user), "EX", 3600);
|
|
24
|
+
}
|
|
25
|
+
return user;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Invalidate on write
|
|
29
|
+
async function updateUser(userId: string, data: Partial<User>): Promise<User> {
|
|
30
|
+
const user = await db.users.update({ where: { id: userId }, data });
|
|
31
|
+
await redis.del(`user:${userId}`);
|
|
32
|
+
return user;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Keep values under 100KB. Use Hash for structured data when you need partial reads.
|
|
37
|
+
|
|
38
|
+
### Rate Limiting -- Sliding Window
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
async function isRateLimited(
|
|
42
|
+
key: string, limit: number, windowSec: number
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const windowStart = now - windowSec * 1000;
|
|
46
|
+
|
|
47
|
+
const pipeline = redis.pipeline();
|
|
48
|
+
pipeline.zremrangebyscore(key, 0, windowStart);
|
|
49
|
+
pipeline.zadd(key, now, `${now}-${Math.random()}`);
|
|
50
|
+
pipeline.zcard(key);
|
|
51
|
+
pipeline.expire(key, windowSec);
|
|
52
|
+
|
|
53
|
+
const results = await pipeline.exec();
|
|
54
|
+
const count = results![2][1] as number;
|
|
55
|
+
return count > limit;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Usage: 100 requests per minute per user
|
|
59
|
+
const limited = await isRateLimited(`rate:${userId}`, 100, 60);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Distributed Lock -- Redlock Pattern
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
async function acquireLock(resource: string, ttlMs: number): Promise<string | null> {
|
|
66
|
+
const token = crypto.randomUUID();
|
|
67
|
+
const acquired = await redis.set(`lock:${resource}`, token, "PX", ttlMs, "NX");
|
|
68
|
+
return acquired ? token : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function releaseLock(resource: string, token: string): Promise<boolean> {
|
|
72
|
+
const script = `
|
|
73
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
74
|
+
return redis.call("del", KEYS[1])
|
|
75
|
+
else
|
|
76
|
+
return 0
|
|
77
|
+
end
|
|
78
|
+
`;
|
|
79
|
+
const result = await redis.eval(script, 1, `lock:${resource}`, token);
|
|
80
|
+
return result === 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Usage
|
|
84
|
+
const token = await acquireLock("order-123", 30000);
|
|
85
|
+
if (token) {
|
|
86
|
+
try {
|
|
87
|
+
await processOrder("order-123");
|
|
88
|
+
} finally {
|
|
89
|
+
await releaseLock("order-123", token);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Pub/Sub -- Event Broadcasting
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Publisher
|
|
98
|
+
await redis.publish("orders:created", JSON.stringify({ orderId, userId }));
|
|
99
|
+
|
|
100
|
+
// Subscriber (must use a separate connection)
|
|
101
|
+
const sub = redis.duplicate();
|
|
102
|
+
await sub.subscribe("orders:created");
|
|
103
|
+
sub.on("message", (channel, message) => {
|
|
104
|
+
const event = JSON.parse(message);
|
|
105
|
+
notifyDashboard(event);
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Pub/Sub is fire-and-forget. If subscribers are down, messages are lost. Use Streams for durable messaging.
|
|
110
|
+
|
|
111
|
+
### Streams -- Durable Event Log
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Produce
|
|
115
|
+
await redis.xadd("events:orders", "*", "type", "created", "orderId", orderId);
|
|
116
|
+
|
|
117
|
+
// Consumer group -- each message delivered to one consumer
|
|
118
|
+
await redis.xgroup("CREATE", "events:orders", "workers", "0", "MKSTREAM").catch(() => {});
|
|
119
|
+
|
|
120
|
+
const messages = await redis.xreadgroup(
|
|
121
|
+
"GROUP", "workers", "worker-1",
|
|
122
|
+
"COUNT", 10, "BLOCK", 5000,
|
|
123
|
+
"STREAMS", "events:orders", ">"
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Acknowledge after processing
|
|
127
|
+
if (messages) {
|
|
128
|
+
for (const [, msgs] of messages) {
|
|
129
|
+
for (const [id] of msgs) {
|
|
130
|
+
await redis.xack("events:orders", "workers", id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Sorted Sets -- Leaderboards and Rankings
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Add/update score
|
|
140
|
+
await redis.zadd("leaderboard:weekly", score, `user:${userId}`);
|
|
141
|
+
|
|
142
|
+
// Get top 10 with scores
|
|
143
|
+
const top10 = await redis.zrevrange("leaderboard:weekly", 0, 9, "WITHSCORES");
|
|
144
|
+
|
|
145
|
+
// Get a user's rank (0-indexed)
|
|
146
|
+
const rank = await redis.zrevrank("leaderboard:weekly", `user:${userId}`);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Lua Scripts -- Atomic Multi-Step Operations
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Atomic increment with ceiling
|
|
153
|
+
const script = `
|
|
154
|
+
local current = tonumber(redis.call("GET", KEYS[1]) or "0")
|
|
155
|
+
local ceiling = tonumber(ARGV[1])
|
|
156
|
+
if current >= ceiling then
|
|
157
|
+
return -1
|
|
158
|
+
end
|
|
159
|
+
return redis.call("INCR", KEYS[1])
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
const result = await redis.eval(script, 1, "counter:signups", "1000");
|
|
163
|
+
if (result === -1) {
|
|
164
|
+
throw new Error("Signup limit reached");
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Examples
|
|
169
|
+
|
|
170
|
+
| Pattern | Data Structure | Use Case |
|
|
171
|
+
|---------|---------------|----------|
|
|
172
|
+
| Cache-aside | String / Hash | DB query results, API responses |
|
|
173
|
+
| Rate limiter | Sorted Set | API throttling per IP/user |
|
|
174
|
+
| Distributed lock | String + NX + Lua | Singleton job execution |
|
|
175
|
+
| Pub/Sub | Channels | Real-time notifications |
|
|
176
|
+
| Streams | Stream | Durable event processing |
|
|
177
|
+
| Leaderboard | Sorted Set | Rankings, top-N queries |
|
|
178
|
+
| Session store | Hash + TTL | User sessions |
|
|
179
|
+
|
|
180
|
+
## Checklist
|
|
181
|
+
- [ ] Every cache key has a TTL -- no unbounded growth
|
|
182
|
+
- [ ] Cache keys use consistent namespace: `entity:id:field`
|
|
183
|
+
- [ ] Cache invalidation happens in same transaction as write
|
|
184
|
+
- [ ] Rate limiter uses sorted set sliding window
|
|
185
|
+
- [ ] Distributed locks use NX + PX + unique token + Lua release
|
|
186
|
+
- [ ] Pub/Sub subscribers run on dedicated Redis connection
|
|
187
|
+
- [ ] Streams use consumer groups with XACK
|
|
188
|
+
- [ ] Memory policy set (`maxmemory-policy allkeys-lru` for caches)
|