Skip to content

Commit d558ae6

Browse files
ryan-karnRyan Karn
andauthored
feat: add iOS origin sharing, idleTTL prop, and origin-pooling demo (#30, #28) (#34)
- Add iOS support for shared-origin factory pooling (same-origin sandboxes reuse a single RCTReactNativeFactory) - Add idleTTL prop to defer ReactHost/factory cleanup after last surface unmounts, enabling warm starts for same-origin remounts - Add origin-pooling demo app demonstrating both features on iOS and Android Ref: #28 Ref: #30 Co-authored-by: Ryan Karn <rkarn@amazon.com>
1 parent 3f6b250 commit d558ae6

66 files changed

Lines changed: 2403 additions & 24 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/origin-pooling/App.tsx

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/**
2+
* Origin Pooling Demo
3+
*
4+
* Dynamically add/remove sandboxes under two shared origins (alpha, beta)
5+
* plus an isolated (no-origin) option. Same-origin sandboxes share a
6+
* ReactHost / Hermes VM; removing the last one triggers the idle TTL.
7+
*
8+
* Messaging is handled inside the sandbox widget via globalThis.postMessage.
9+
* The host only logs messages received via onMessage.
10+
*/
11+
import SandboxReactNativeView from '@callstack/react-native-sandbox'
12+
import React, {useCallback, useRef, useState} from 'react'
13+
import {
14+
Button,
15+
Platform,
16+
SafeAreaView,
17+
ScrollView,
18+
StatusBar,
19+
StyleSheet,
20+
Text,
21+
View,
22+
} from 'react-native'
23+
24+
type SandboxEntry = {key: string; label: string; origin: string}
25+
type LogEntry = {source: string; text: string; ts: number}
26+
27+
let nextId = 0
28+
29+
const ORIGIN_ALPHA = 'alpha'
30+
const ORIGIN_BETA = 'beta'
31+
const COLOR_ALPHA = '#8232ff'
32+
const COLOR_BETA = '#e67e22'
33+
const COLOR_ISOLATED = '#6c757d'
34+
35+
/** Alpha uses a function-based TTL (4 seconds) */
36+
const ALPHA_TTL = () => 4000
37+
/** Beta and isolated use a static TTL (2 seconds) */
38+
const DEFAULT_TTL = 2000
39+
40+
export default function App() {
41+
const [sandboxes, setSandboxes] = useState<SandboxEntry[]>([])
42+
const [log, setLog] = useState<LogEntry[]>([])
43+
const logScrollRef = useRef<ScrollView>(null)
44+
45+
const addLog = useCallback((source: string, text: string) => {
46+
setLog(prev => [...prev.slice(-49), {source, text, ts: Date.now()}])
47+
}, [])
48+
49+
const addSandbox = useCallback((origin: string) => {
50+
const id = String(++nextId)
51+
setSandboxes(prev => [...prev, {key: id, label: `#${id}`, origin}])
52+
}, [])
53+
54+
const removeSandbox = useCallback((key: string) => {
55+
setSandboxes(prev => prev.filter(s => s.key !== key))
56+
}, [])
57+
58+
const clearLog = useCallback(() => setLog([]), [])
59+
60+
const alphas = sandboxes.filter(s => s.origin === ORIGIN_ALPHA)
61+
const betas = sandboxes.filter(s => s.origin === ORIGIN_BETA)
62+
const isolated = sandboxes.filter(s => s.origin === '')
63+
64+
return (
65+
<SafeAreaView style={styles.safe}>
66+
<Text style={styles.heading}>Origin Pooling Demo</Text>
67+
<Text style={styles.subtitle}>
68+
Same-origin sandboxes share a VM. Alpha: function-based TTL (4s). Beta:
69+
static TTL (2s).
70+
</Text>
71+
72+
<View style={styles.controls}>
73+
<Button
74+
title="+ Alpha"
75+
color={COLOR_ALPHA}
76+
onPress={() => addSandbox(ORIGIN_ALPHA)}
77+
/>
78+
<Button
79+
title="+ Beta"
80+
color={COLOR_BETA}
81+
onPress={() => addSandbox(ORIGIN_BETA)}
82+
/>
83+
<Button
84+
title="+ Isolated"
85+
color={COLOR_ISOLATED}
86+
onPress={() => addSandbox('')}
87+
/>
88+
<Button title="Clear Log" onPress={clearLog} />
89+
</View>
90+
91+
{/* Alpha sandboxes */}
92+
<Text style={[styles.groupLabel, {color: COLOR_ALPHA}]}>
93+
{'origin="alpha"'} ({alphas.length})
94+
</Text>
95+
<ScrollView
96+
horizontal
97+
style={styles.cardRow}
98+
contentContainerStyle={styles.cardRowContent}>
99+
{alphas.map(sb => (
100+
<SandboxCard
101+
key={sb.key}
102+
entry={sb}
103+
color={COLOR_ALPHA}
104+
idleTTL={ALPHA_TTL}
105+
onRemove={() => removeSandbox(sb.key)}
106+
onMessage={data =>
107+
addLog(`alpha ${sb.label}`, JSON.stringify(data))
108+
}
109+
onError={err =>
110+
addLog(`alpha ${sb.label}`, `ERROR: ${err.name}${err.message}`)
111+
}
112+
/>
113+
))}
114+
{alphas.length === 0 && (
115+
<Text style={styles.empty}>No alpha sandboxes yet.</Text>
116+
)}
117+
</ScrollView>
118+
119+
{/* Beta sandboxes */}
120+
<Text style={[styles.groupLabel, {color: COLOR_BETA}]}>
121+
{'origin="beta"'} ({betas.length})
122+
</Text>
123+
<ScrollView
124+
horizontal
125+
style={styles.cardRow}
126+
contentContainerStyle={styles.cardRowContent}>
127+
{betas.map(sb => (
128+
<SandboxCard
129+
key={sb.key}
130+
entry={sb}
131+
color={COLOR_BETA}
132+
idleTTL={DEFAULT_TTL}
133+
onRemove={() => removeSandbox(sb.key)}
134+
onMessage={data => addLog(`beta ${sb.label}`, JSON.stringify(data))}
135+
onError={err =>
136+
addLog(`beta ${sb.label}`, `ERROR: ${err.name}${err.message}`)
137+
}
138+
/>
139+
))}
140+
{betas.length === 0 && (
141+
<Text style={styles.empty}>No beta sandboxes yet.</Text>
142+
)}
143+
</ScrollView>
144+
145+
{/* Isolated sandboxes */}
146+
<Text style={[styles.groupLabel, {color: COLOR_ISOLATED}]}>
147+
no origin / isolated ({isolated.length})
148+
</Text>
149+
<ScrollView
150+
horizontal
151+
style={styles.cardRow}
152+
contentContainerStyle={styles.cardRowContent}>
153+
{isolated.map(sb => (
154+
<SandboxCard
155+
key={sb.key}
156+
entry={sb}
157+
color={COLOR_ISOLATED}
158+
idleTTL={DEFAULT_TTL}
159+
onRemove={() => removeSandbox(sb.key)}
160+
onMessage={data =>
161+
addLog(`isolated ${sb.label}`, JSON.stringify(data))
162+
}
163+
onError={err =>
164+
addLog(
165+
`isolated ${sb.label}`,
166+
`ERROR: ${err.name}${err.message}`
167+
)
168+
}
169+
/>
170+
))}
171+
{isolated.length === 0 && (
172+
<Text style={styles.empty}>No isolated sandboxes yet.</Text>
173+
)}
174+
</ScrollView>
175+
176+
{/* Event log */}
177+
<Text style={styles.logTitle}>Event Log</Text>
178+
<ScrollView
179+
ref={logScrollRef}
180+
style={styles.logScroll}
181+
onContentSizeChange={() => logScrollRef.current?.scrollToEnd()}>
182+
{log.map((e, i) => (
183+
<Text key={i} style={styles.logLine}>
184+
<Text style={styles.logSource}>[{e.source}]</Text> {e.text}
185+
</Text>
186+
))}
187+
</ScrollView>
188+
</SafeAreaView>
189+
)
190+
}
191+
192+
type SandboxCardProps = {
193+
entry: SandboxEntry
194+
color: string
195+
idleTTL: number | (() => number)
196+
onRemove: () => void
197+
onMessage: (data: unknown) => void
198+
onError: (err: {name: string; message: string}) => void
199+
}
200+
201+
function SandboxCard({
202+
entry,
203+
color,
204+
idleTTL,
205+
onRemove,
206+
onMessage,
207+
onError,
208+
}: SandboxCardProps) {
209+
return (
210+
<View style={[styles.card, {borderColor: color}]}>
211+
<View style={[styles.cardHeader, {backgroundColor: color}]}>
212+
<Text style={styles.cardLabel}>
213+
{entry.origin || 'isolated'} {entry.label}
214+
</Text>
215+
<Text style={styles.cardRemove} onPress={onRemove}>
216+
217+
</Text>
218+
</View>
219+
<SandboxReactNativeView
220+
origin={entry.origin || undefined}
221+
allowedOrigins={[ORIGIN_ALPHA, ORIGIN_BETA]}
222+
idleTTL={idleTTL}
223+
componentName="SandboxApp"
224+
jsBundleSource="sandbox"
225+
onMessage={onMessage}
226+
onError={onError}
227+
style={styles.sandboxView}
228+
/>
229+
</View>
230+
)
231+
}
232+
233+
const styles = StyleSheet.create({
234+
safe: {
235+
flex: 1,
236+
backgroundColor: '#f5f5f5',
237+
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
238+
},
239+
heading: {
240+
fontSize: 20,
241+
fontWeight: '700',
242+
textAlign: 'center',
243+
marginTop: 8,
244+
marginBottom: 2,
245+
},
246+
subtitle: {
247+
fontSize: 11,
248+
color: '#6c757d',
249+
textAlign: 'center',
250+
marginBottom: 6,
251+
paddingHorizontal: 16,
252+
},
253+
controls: {
254+
flexDirection: 'row',
255+
justifyContent: 'space-evenly',
256+
paddingHorizontal: 8,
257+
paddingBottom: 4,
258+
},
259+
groupLabel: {
260+
fontSize: 12,
261+
fontWeight: '600',
262+
paddingHorizontal: 12,
263+
paddingTop: 2,
264+
},
265+
cardRow: {height: 150, flexGrow: 0},
266+
cardRowContent: {paddingHorizontal: 8, gap: 8},
267+
card: {
268+
width: 200,
269+
borderWidth: 2,
270+
borderRadius: 8,
271+
overflow: 'hidden',
272+
},
273+
cardLabel: {
274+
color: '#fff',
275+
fontSize: 11,
276+
fontWeight: '600',
277+
textAlign: 'center',
278+
paddingVertical: 2,
279+
flex: 1,
280+
},
281+
cardHeader: {
282+
flexDirection: 'row',
283+
alignItems: 'center',
284+
},
285+
cardRemove: {
286+
color: '#fff',
287+
fontSize: 14,
288+
fontWeight: '700',
289+
paddingHorizontal: 8,
290+
paddingVertical: 2,
291+
},
292+
sandboxView: {flex: 1},
293+
empty: {
294+
color: '#999',
295+
fontStyle: 'italic',
296+
alignSelf: 'center',
297+
paddingTop: 60,
298+
},
299+
logTitle: {
300+
fontSize: 14,
301+
fontWeight: '600',
302+
paddingHorizontal: 12,
303+
paddingTop: 4,
304+
},
305+
logScroll: {flex: 1, paddingHorizontal: 12, paddingTop: 4},
306+
logLine: {fontSize: 11, fontFamily: 'monospace', marginBottom: 2},
307+
logSource: {fontWeight: '700', color: '#8232ff'},
308+
})

apps/origin-pooling/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Origin Pooling Demo
2+
3+
Validates features of sandboxes sharing the same origin.
4+
5+
## Origin-based Pooling
6+
7+
Sandboxes with the same `origin` prop share a single ReactHost / Hermes VM.
8+
9+
- **Alpha** sandboxes share origin `alpha` → same VM
10+
- **Beta** sandboxes share origin `beta` → same VM
11+
- **Isolated** sandboxes get their own VM every time (no origin)
12+
13+
Use the `+ Alpha`, `+ Beta`, and `+ Isolated` buttons to dynamically add
14+
sandboxes. Each card has a **Ping** button and a **** button
15+
to remove itself.
16+
17+
## Lazy Kill
18+
19+
When the last sandbox for an origin unmounts, the underlying ReactHost is
20+
**not** destroyed immediately — it lingers for 2 seconds (`idleTTL={2000}`).
21+
If a new sandbox with the same origin mounts within that window, it reuses
22+
the warm host (no cold start). Compare the `render` time of a cold start
23+
vs a warm re-mount.
24+
25+
## Running
26+
27+
```bash
28+
# From repo root
29+
yarn
30+
cd apps/origin-pooling
31+
npx react-native run-ios
32+
# or
33+
npx react-native run-android
34+
```

0 commit comments

Comments
 (0)