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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
name: query-core v${{ matrix.query-core }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
query-core: [4, 5]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Enable corepack
run: corepack enable

- name: Install
run: yarn install --immutable

- name: Typecheck
run: yarn typecheck

- name: Build
run: yarn build

# Each matrix leg runs the same suite against one major: the `query-core-v4`
# vitest project resolves `@tanstack/query-core` to the v4 alias dependency.
- name: Test
run: yarn vitest run --project query-core-v${{ matrix.query-core }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ yarn add @bam.tech/tanstack-query-detect-waterfall --dev
# or
bun add @bam.tech/tanstack-query-detect-waterfall --dev
```

Works with both TanStack Query v4 and v5: the detector handles the `loading` (v4) and `pending` (v5) query statuses.
3 changes: 3 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
package-lock.json
.expo
122 changes: 122 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { detectQueryWaterfalls } from "@bam.tech/tanstack-query-detect-waterfall";
import { QueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, StyleSheet, Text, View } from "react-native";

const queryClient = new QueryClient();

/**
* The detector reports through `console.warn`. We tap that channel and parse the
* `from --> to` lines out of its real payload so every detection it makes shows
* up on screen. Nothing is synthesised here.
*/
type Listener = (line: string) => void;
const listeners = new Set<Listener>();

const originalWarn = console.warn.bind(console);
console.warn = (...args: unknown[]) => {
const message = String(args[0] ?? "");
if (message.startsWith("Detected query waterfalls")) {
message
.split("\n")
.filter((line) => line.includes(" --> "))
.forEach((line) => listeners.forEach((listener) => listener(line.trim())));
}
originalWarn(...args);
};

detectQueryWaterfalls(queryClient);

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

let run = 0;

const fetchQuery = (name: string) =>
queryClient.fetchQuery({
queryKey: [name, run],
queryFn: async () => {
await delay(300);
return name;
},
});

export default function App() {
const [waterfalls, setWaterfalls] = useState<string[]>([]);
const [status, setStatus] = useState("idle");

useEffect(() => {
const listener: Listener = (line) => setWaterfalls((previous) => [...previous, line]);
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}, []);

const runSequential = async () => {
run += 1;
setStatus("running 2 sequential queries…");
const user = await fetchQuery("user");
// `posts` only starts once `user` has resolved: a textbook waterfall.
await fetchQuery(`posts-of-${user}`);
setStatus("sequential run done");
};

const runParallel = async () => {
run += 1;
setStatus("running 2 parallel queries…");
await Promise.all([fetchQuery("profile"), fetchQuery("settings")]);
setStatus("parallel run done");
};

return (
<SafeAreaView style={styles.screen}>
<Text style={styles.title}>Waterfall detector E2E</Text>
<Text style={styles.subtitle}>@tanstack/react-query v5 · Expo Go</Text>

<Pressable testID="run-sequential" style={styles.button} onPress={runSequential}>
<Text style={styles.buttonLabel}>Run 2 SEQUENTIAL queries</Text>
</Pressable>
<Pressable testID="run-parallel" style={[styles.button, styles.parallel]} onPress={runParallel}>
<Text style={styles.buttonLabel}>Run 2 PARALLEL queries</Text>
</Pressable>

<Text testID="status" style={styles.status}>
Status: {status}
</Text>
<Text testID="waterfall-count" style={styles.count}>
Detected waterfalls: {waterfalls.length}
</Text>

<ScrollView testID="waterfall-list" style={styles.list}>
{waterfalls.length === 0 ? (
<Text testID="waterfall-empty" style={styles.empty}>
No waterfall detected yet
</Text>
) : (
waterfalls.map((line, index) => (
<View key={`${line}-${index}`} style={styles.item}>
<Text testID={`waterfall-${index}`} style={styles.itemText}>
{line}
</Text>
</View>
))
)}
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: "#0f172a", paddingHorizontal: 20, paddingTop: 60 },
title: { color: "#f8fafc", fontSize: 24, fontWeight: "700" },
subtitle: { color: "#94a3b8", fontSize: 13, marginBottom: 24 },
button: { backgroundColor: "#ef4444", borderRadius: 10, paddingVertical: 16, marginBottom: 12 },
parallel: { backgroundColor: "#22c55e" },
buttonLabel: { color: "#0f172a", fontSize: 16, fontWeight: "700", textAlign: "center" },
status: { color: "#cbd5f5", fontSize: 14, marginTop: 8 },
count: { color: "#f8fafc", fontSize: 18, fontWeight: "700", marginTop: 4, marginBottom: 12 },
list: { flex: 1, backgroundColor: "#1e293b", borderRadius: 10, padding: 12 },
empty: { color: "#64748b", fontStyle: "italic" },
item: { borderBottomColor: "#334155", borderBottomWidth: 1, paddingVertical: 8 },
itemText: { color: "#fca5a5", fontFamily: "Courier", fontSize: 13 },
});
22 changes: 22 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# On-device example

A minimal Expo app that runs the detector against **@tanstack/react-query v5**
on a real device/simulator, and renders every waterfall it reports on screen.
The library is consumed through `file:..`, i.e. the built `dist/` of this repo.

The app taps `console.warn`, keeps the lines the detector actually emits, and
lists them: nothing on screen is synthesised.

## Run it

```bash
yarn build # at the repo root, the example consumes dist/
cd example
npm install
npx expo start --ios # or --android; Expo Go is enough, the library is pure JS
```

- **Run 2 SEQUENTIAL queries** starts `["user", n]`, waits for it to resolve,
then starts `["posts-of-user", n]`. One waterfall appears in the list.
- **Run 2 PARALLEL queries** starts `["profile", n]` and `["settings", n]` at
once. The list does not grow.
12 changes: 12 additions & 0 deletions example/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"expo": {
"name": "waterfall-detector-example",
"slug": "waterfall-detector-example",
"version": "1.0.0",
"orientation": "portrait",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true
}
}
}
8 changes: 8 additions & 0 deletions example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';

import App from './App';

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
31 changes: 31 additions & 0 deletions example/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");

const config = getDefaultConfig(__dirname);

// The library is linked with `file:`, so without this it would resolve
// `@tanstack/query-core` / `lodash` from its OWN node_modules. Force both to the
// app's copies so the detector really runs against the app's react-query v5
// instance.
const forced = {
"@tanstack/query-core": path.resolve(__dirname, "node_modules/@tanstack/query-core"),
lodash: path.resolve(__dirname, "node_modules/lodash"),
};

const defaultResolveRequest = config.resolver.resolveRequest;
config.resolver.resolveRequest = (context, moduleName, platform) => {
for (const [name, dir] of Object.entries(forced)) {
if (moduleName === name || moduleName.startsWith(`${name}/`)) {
return context.resolveRequest(
context,
path.join(dir, moduleName.slice(name.length)),
platform
);
}
}
return (defaultResolveRequest ?? context.resolveRequest)(context, moduleName, platform);
};

config.watchFolders = [path.resolve(__dirname, "..")];

module.exports = config;
25 changes: 25 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "waterfall-detector-example",
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"@bam.tech/tanstack-query-detect-waterfall": "file:..",
"@tanstack/react-query": "^5.103.2",
"expo": "~57.0.24",
"expo-status-bar": "~57.0.1",
"lodash": "^4.18.1",
"react": "19.2.3",
"react-native": "0.86.3"
},
"devDependencies": {
"@types/react": "~19.2.2",
"typescript": "~6.0.3"
},
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"private": true
}
6 changes: 6 additions & 0 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"url": "git@github.com:bamlab/tanstack-query-detect-waterfall.git"
},
"scripts": {
"build": "tsup"
"build": "tsup",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand All @@ -26,11 +28,13 @@
},
"devDependencies": {
"@tanstack/query-core": "~5",
"@tanstack/query-core-v4": "npm:@tanstack/query-core@^4",
"@types/lodash": "~4",
"tsup": "^8.0.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"vitest": "^3"
},
"peerDependencies": {
"@tanstack/query-core": "~4"
"@tanstack/query-core": ">=4 <6"
}
}
Loading
Loading