Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class DataService: ObservableObject {

private let pollingInterval: TimeInterval = 10.0
private var pollingTask: Task<Void, Never>?
private var hasCompletedInitialFetch = false

func startPolling(accountName: String) {
stopPolling()
Expand All @@ -30,9 +31,15 @@ class DataService: ObservableObject {
}

func fetchProjectsAndData(accountName: String) async {
isLoadingData = true
if !hasCompletedInitialFetch {
isLoadingData = true
}
dataError = nil
defer { isLoadingData = false }

defer {
isLoadingData = false
hasCompletedInitialFetch = true
}

do {
let response: HomeScreenDataResponse = try await APIClient.shared.request(
Expand Down Expand Up @@ -72,5 +79,6 @@ class DataService: ObservableObject {
projects = []
snacks = []
dataError = nil
hasCompletedInitialFetch = false
}
}
1 change: 1 addition & 0 deletions docs/constants/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export const eas = [
makePage('eas-update/download-updates.mdx'),
makePage('eas-update/rollouts.mdx'),
makePage('eas-update/rollbacks.mdx'),
makePage('eas-update/bundle-diffing.mdx'),
makePage('eas-update/optimize-assets.mdx'),
makePage('eas-update/deployment-patterns.mdx'),
]),
Expand Down
59 changes: 59 additions & 0 deletions docs/pages/eas-update/bundle-diffing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Bundle diffing for EAS Update
sidebar_title: Serve bundle diffs
description: Enable your project to accept bundle diffs when available.
---

import { ContentSpotlight } from '~/ui/components/ContentSpotlight';

> **important** Bundle diffing is in **beta** and may have limitations. See [Current limitations](#current-limitations) for details.

Enable bundle diffing to let EAS Update deliver a **bundle patch** when possible. When you publish a new update, EAS Update can generate a smaller file containing only the differences between the bundle currently running on the device and the new bundle. This often reduces update download size significantly.

## Prerequisites

Your app must use **Expo SDK 55 or later**.

## Enable bundle diffing

In your project's [app config](/workflow/configuration/), set `updates.enableBsdiffPatchSupport` to `true`:

```json app.json
{
"expo": {
"updates": {
"enableBsdiffPatchSupport": true
}
}
}
```

## Verify bundle diffs are being served

### Expo website

You can confirm that bundle diffs are being served from the [Update Details](https://expo.dev/accounts/[account]/projects/[project]/updates) page. Open the Update Group you published, then select the platform you want to inspect.

<ContentSpotlight
alt="Bundle diffing downloads"
src="/static/images/eas-update/bundle-diffing.png"
/>

### Updates API

You can confirm that bundle diffs are being served by inspecting update logs with `Updates.readLogEntriesAsync()`. If your app received a patch, you will see an entry indicating it was successfully applied (for example, "patch successfully applied").

## Patch generation and serving

EAS Update uses the [bsdiff algorithm](https://en.wikipedia.org/wiki/Bsdiff) to generate bundle patches.

A patch is served only when:

- **It's meaningfully smaller than the full bundle.** If it isn't, EAS Update serves the full bundle instead.
- **It can be computed efficiently.** If generating the patch is too resource intensive, EAS Update serves the full bundle instead.

## Current limitations

- **Embedded bundles aren't eligible.** The embedded bundle is never used as a base for patching. Devices must already be running a published update to receive a patch.
- **Patches aren't guaranteed for every possible update pair immediately.** When an update is published, EAS Update precomputes a patch only against the second-newest update on the channel. If a device requests the new update while running a different published update, it will initially receive the full bundle. A patch for that specific base update is then generated on demand and served to future similar requests.
- **Patches are generated shortly after publishing.** It can take a few minutes between publishing an update and the patch being ready. During that window, devices may receive the full bundle.
2 changes: 1 addition & 1 deletion docs/public/static/data/unversioned/expo-server.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/public/static/data/v55.0.0/expo-server.json

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 🐛 Bug fixes

- Fix RSC support in development ([#42617](https://github.com/expo/expo/pull/42617) by [@hassankhan](https://github.com/hassankhan))

### 💡 Others

- Bump `@expo/xcpretty` ([#42485](https://github.com/expo/expo/pull/42485) by [@kitten](https://github.com/kitten))
Expand Down
13 changes: 10 additions & 3 deletions packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,14 +1358,15 @@ export class MetroBundlerDevServer extends BundlerDevServer {
routerOptions,
});
this.rscRenderer = rscMiddleware;
middleware.use(rscMiddleware.middleware);
this.onReloadRscEvent = rscMiddleware.onReloadRscEvent;
}

// Append support for redirecting unhandled requests to the index.html page on web.
if (this.isTargetingWeb()) {
if (!useServerRendering) {
// This MUST run last since it's the fallback.
// Use `createRouteHandlerMiddleware()` when either of the following is true:
// - Server rendering is enabled (server/static output)
// - RSC is enabled. Even in `single` output mode, RSC needs the route handler
if (!useServerRendering && !isReactServerComponentsEnabled) {
middleware.use(
new HistoryFallbackMiddleware(manifestMiddleware.getHandler().internal).getHandler()
);
Expand Down Expand Up @@ -1407,6 +1408,12 @@ export class MetroBundlerDevServer extends BundlerDevServer {
// Non-RSC apps will bundle the static HTML for a given pathname and respond with it.
return this.getStaticPageAsync(pathname, route, request);
},
rsc: isReactServerComponentsEnabled
? {
path: '/_flight',
handler: this.rscRenderer!.handler,
}
: undefined,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { toPosixPath } from '../../../utils/filePath';
import { memoize } from '../../../utils/fn';
import { getIpAddress } from '../../../utils/ip';
import { streamToStringAsync } from '../../../utils/stream';
import { createBuiltinAPIRequestHandler } from '../middleware/createBuiltinAPIRequestHandler';
import {
createBundleUrlSearchParams,
type ExpoMetroOptions,
Expand All @@ -42,6 +41,7 @@ type SSRLoadModuleFunc = <T extends Record<string, any>>(

const getMetroServerRootMemo = memoize(getMetroServerRoot);

// TODO(@hassankhan): Rename this to `createRscRenderer()`
export function createServerComponentsMiddleware(
projectRoot: string,
{
Expand Down Expand Up @@ -571,6 +571,9 @@ export function createServerComponentsMiddleware(
getExpoRouterClientReferencesAsync,
exportServerActionsAsync,

// Expose the RSC handler directly for use with `createRouteHandlerMiddleware()`
handler: rscMiddleware,

async exportRoutesAsync(
{
platform,
Expand Down Expand Up @@ -628,13 +631,6 @@ export function createServerComponentsMiddleware(
);
},

middleware: createBuiltinAPIRequestHandler(
// Match `/_flight/[platform]/[...path]`
(req) => {
return getFullUrl(req.url).pathname.startsWith(rscPathPrefix);
},
rscMiddleware
),
onReloadRscEvent: (platform: string) => {
// NOTE: We cannot clear the renderer context because it would break the mounted context state.

Expand All @@ -644,14 +640,6 @@ export function createServerComponentsMiddleware(
};
}

const getFullUrl = (url: string) => {
try {
return new URL(url);
} catch {
return new URL(url, 'http://localhost:0');
}
};

export const fileURLToFilePath = (fileURL: string) => {
try {
return url.fileURLToPath(fileURL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export function createRouteHandlerMiddleware(
) => Promise<Response | undefined>;
config: ProjectConfig;
headers: Record<string, string | string[]>;
rsc?: {
path: string;
handler: {
GET: (req: Request) => Promise<Response>;
POST: (req: Request) => Promise<Response>;
};
};
} & import('@expo/router-server/build/routes-manifest').Options
) {
if (!resolveFrom.silent(projectRoot, 'expo-router')) {
Expand All @@ -57,6 +64,22 @@ export function createRouteHandlerMiddleware(
const manifest = await fetchManifest(projectRoot, options);
debug('manifest', manifest);

// TODO(@hassankhan): Invert the conditionals for an early return if no manifest if found

if (
manifest &&
options.rsc &&
!manifest.apiRoutes.find((route) => route.page.startsWith(options.rsc!.path))
) {
// Insert the route before any catch-all routes that might match the RSC path.
manifest.apiRoutes.unshift({
file: require.resolve('@expo/cli/static/template/[...rsc]+api.ts'),
page: `${options.rsc.path}/[...rsc]`,
namedRegex: new RegExp(`^${options.rsc.path}(?:/(?<rsc>.+?))?(?:/)?$`),
routeKeys: { rsc: 'rsc' },
});
}

const { exp } = options.config;

if (manifest && exp.extra?.router?.unstable_useServerDataLoaders === true) {
Expand Down Expand Up @@ -159,6 +182,12 @@ export function createRouteHandlerMiddleware(
});
},
async getApiRoute(route) {
// We check if RSC is enabled before the warning check, as `web.output` could be set to
// `single`
if (options.rsc && route.page.startsWith(options.rsc.path)) {
return options.rsc.handler;
}

const { exp } = options.config;
if (exp.web?.output !== 'server') {
warnInvalidWebOutput();
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions packages/expo-dev-menu/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### 🐛 Bug fixes

- [Android] Fix `null cannot be cast to non-null type expo.modules.devmenu.DevMenuFragment`. ([#42660](https://github.com/expo/expo/pull/42660) by [@lukmccall](https://github.com/lukmccall))
- [iOS] Fix null current bridge in standalone mode ([#42666](https://github.com/expo/expo/pull/42666) by [@gabrieldonadel](https://github.com/gabrieldonadel))

### 💡 Others

Expand Down
12 changes: 7 additions & 5 deletions packages/expo-dev-menu/ios/DevMenuManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,7 @@ open class DevMenuManager: NSObject {
fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
}

@objc
public func updateFABVisibility() {
public func updateFABVisibility(menuDismissing: Bool = false) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }

Expand All @@ -552,13 +551,16 @@ open class DevMenuManager: NSObject {
}
}

let shouldShow = DevMenuPreferences.showFloatingActionButton && !self.isVisible && self.currentBridge != nil && !self.isNavigatingHome
let shouldShow = DevMenuPreferences.showFloatingActionButton
&& (menuDismissing || !self.isVisible)
&& self.currentBridge != nil
&& !self.isNavigatingHome
&& DevMenuPreferences.isOnboardingFinished
self.fabWindow?.setVisible(shouldShow, animated: true)
}
}
#else
@objc
public func updateFABVisibility() {
public func updateFABVisibility(menuDismissing: Bool = false) {
// FAB not available on macOS/tvOS
}
#endif
Expand Down
3 changes: 2 additions & 1 deletion packages/expo-dev-menu/ios/DevMenuWindow-default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
self.backgroundColor = .clear
}

DevMenuManager.shared.updateFABVisibility(menuDismissing: true)

devMenuViewController.dismiss(animated: true) {
self.isDismissing = false
self.isHidden = true
self.backgroundColor = UIColor(white: 0, alpha: 0.4)
DevMenuManager.shared.updateFABVisibility()
completion?()
}
}
Expand Down
Loading
Loading