diff --git a/examples/streaming/.gitignore b/examples/streaming/.gitignore new file mode 100644 index 000000000..ad2da72f4 --- /dev/null +++ b/examples/streaming/.gitignore @@ -0,0 +1,11 @@ +/.wasp/ + +# We ignore env files recognized and used by Wasp. +.env.server +.env.client + +# To be extra safe, we by default ignore any files with `.env` extension in them. +# If this is too agressive for you, consider allowing specific files with `!` operator, +# or modify/delete these two lines. +*.env +*.env.* diff --git a/examples/streaming/.wasproot b/examples/streaming/.wasproot new file mode 100644 index 000000000..ca2cfdb48 --- /dev/null +++ b/examples/streaming/.wasproot @@ -0,0 +1 @@ +File marking the root of Wasp project. diff --git a/examples/streaming/main.wasp b/examples/streaming/main.wasp new file mode 100644 index 000000000..a2c87e63c --- /dev/null +++ b/examples/streaming/main.wasp @@ -0,0 +1,21 @@ +app streaming { + wasp: { + version: "^0.11.5" + }, + title: "streaming" +} + +route RootRoute { path: "/", to: MainPage } +page MainPage { + component: import Main from "@client/MainPage.jsx" +} + +api streamingText { + httpRoute: (GET, "/api/streaming-test"), + fn: import { getText } from "@server/streaming.js", +} + +apiNamespace defaultMiddleware { + path: "/api", + middlewareConfigFn: import { getMiddlewareConfig } from "@server/streaming.js", +} \ No newline at end of file diff --git a/examples/streaming/src/.waspignore b/examples/streaming/src/.waspignore new file mode 100644 index 000000000..1c432f30d --- /dev/null +++ b/examples/streaming/src/.waspignore @@ -0,0 +1,3 @@ +# Ignore editor tmp files +**/*~ +**/#*# diff --git a/examples/streaming/src/client/Main.css b/examples/streaming/src/client/Main.css new file mode 100644 index 000000000..b6e7ed3f1 --- /dev/null +++ b/examples/streaming/src/client/Main.css @@ -0,0 +1,89 @@ +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +.container { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +main { + padding: 5rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +main p { + font-size: 1.2rem; +} + +.logo { + margin-bottom: 2rem; +} + +.logo img { + max-height: 200px; +} + +.welcome-title { + font-weight: 500; +} + +.welcome-subtitle { + font-weight: 400; + margin-bottom: 3rem; +} + +.buttons { + display: flex; + flex-direction: row; +} + +.buttons .button:not(:last-child) { + margin-right: 0.5rem; +} + +.button { + border-radius: 3px; + font-size: 1.2rem; + padding: 1rem 2rem; + text-align: center; + font-weight: 700; + text-decoration: none; +} + +.button-filled { + border: 2px solid #bf9900; + background-color: #bf9900; + color: #f4f4f4; +} + +.button-outline { + border: 2px solid #8a9cff; + color: #8a9cff; + background-color: none; +} + +code { + border-radius: 5px; + padding: 0.2rem; + background: #efefef; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} diff --git a/examples/streaming/src/client/MainPage.jsx b/examples/streaming/src/client/MainPage.jsx new file mode 100644 index 000000000..93a197e63 --- /dev/null +++ b/examples/streaming/src/client/MainPage.jsx @@ -0,0 +1,57 @@ +import { useState, useEffect } from "react"; +import config from "@wasp/config"; +import "./Main.css"; + +const MainPage = () => { + const { response } = useTextStream("/api/streaming-test"); + return ( +
+
+

Streaming Demo

+

+ {response} +

+
+
+ ); +}; +export default MainPage; + +function useTextStream(path) { + const [response, setResponse] = useState(""); + useEffect(() => { + const controller = new AbortController(); + fetchStream( + path, + (chunk) => { + setResponse((prev) => prev + chunk); + }, + controller + ); + return () => { + controller.abort(); + }; + }, []); + + return { + response, + }; +} + +async function fetchStream(path, onData, controller) { + const response = await fetch(config.apiUrl + path, { + signal: controller.signal, + }); + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + return; + } + onData(value.toString()); + } +} diff --git a/examples/streaming/src/client/tsconfig.json b/examples/streaming/src/client/tsconfig.json new file mode 100644 index 000000000..d501a4193 --- /dev/null +++ b/examples/streaming/src/client/tsconfig.json @@ -0,0 +1,55 @@ +// =============================== IMPORTANT ================================= +// +// This file is only used for Wasp IDE support. You can change it to configure +// your IDE checks, but none of these options will affect the TypeScript +// compiler. Proper TS compiler configuration in Wasp is coming soon :) +{ + "compilerOptions": { + // JSX support + "jsx": "preserve", + "strict": true, + // Allow default imports. + "esModuleInterop": true, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + // Wasp needs the following settings enable IDE support in your source + // files. Editing them might break features like import autocompletion and + // definition lookup. Don't change them unless you know what you're doing. + // + // The relative path to the generated web app's root directory. This must be + // set to define the "paths" option. + "baseUrl": "../../.wasp/out/web-app/", + "paths": { + // Resolve all "@wasp" imports to the generated source code. + "@wasp/*": [ + "src/*" + ], + // Resolve all non-relative imports to the correct node module. Source: + // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping + "*": [ + // Start by looking for the definiton inside the node modules root + // directory... + "node_modules/*", + // ... If that fails, try to find it inside definitely-typed type + // definitions. + "node_modules/@types/*" + ] + }, + // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots + "typeRoots": [ + "../../.wasp/out/web-app/node_modules/@types" + ], + // Since this TS config is used only for IDE support and not for + // compilation, the following directory doesn't exist. We need to specify + // it to prevent this error: + // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file + "outDir": "phantom" + }, + "exclude": [ + "phantom" + ], +} \ No newline at end of file diff --git a/examples/streaming/src/client/vite-env.d.ts b/examples/streaming/src/client/vite-env.d.ts new file mode 100644 index 000000000..1623b9c79 --- /dev/null +++ b/examples/streaming/src/client/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/streaming/src/client/waspLogo.png b/examples/streaming/src/client/waspLogo.png new file mode 100644 index 000000000..d39a9443a Binary files /dev/null and b/examples/streaming/src/client/waspLogo.png differ diff --git a/examples/streaming/src/server/streaming.ts b/examples/streaming/src/server/streaming.ts new file mode 100644 index 000000000..49834d28f --- /dev/null +++ b/examples/streaming/src/server/streaming.ts @@ -0,0 +1,29 @@ +import { StreamingText } from "@wasp/apis/types"; +import { MiddlewareConfigFn } from "@wasp/middleware"; + +// Custom API endpoint that returns a streaming text. +export const getText: StreamingText = async (req, res, context) => { + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.setHeader("Transfer-Encoding", "chunked"); + + let counter = 1; + res.write("Hm, let me see...\n"); + while (counter <= 10) { + // Send a chunk of data. + if (counter === 10) { + res.write(`and finally about ${counter}.`); + } else { + res.write(`let's talk about number ${counter} and `); + } + counter++; + // Wait for 1 second. + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + // End the response. + res.end(); +}; + +// Returning the default config. +export const getMiddlewareConfig: MiddlewareConfigFn = (config) => { + return config; +}; diff --git a/examples/streaming/src/server/tsconfig.json b/examples/streaming/src/server/tsconfig.json new file mode 100644 index 000000000..70a79b44e --- /dev/null +++ b/examples/streaming/src/server/tsconfig.json @@ -0,0 +1,48 @@ +// =============================== IMPORTANT ================================= +// +// This file is only used for Wasp IDE support. You can change it to configure +// your IDE checks, but none of these options will affect the TypeScript +// compiler. Proper TS compiler configuration in Wasp is coming soon :) +{ + "compilerOptions": { + // Allows default imports. + "esModuleInterop": true, + "allowJs": true, + "strict": true, + // Wasp needs the following settings enable IDE support in your source + // files. Editing them might break features like import autocompletion and + // definition lookup. Don't change them unless you know what you're doing. + // + // The relative path to the generated web app's root directory. This must be + // set to define the "paths" option. + "baseUrl": "../../.wasp/out/server/", + "paths": { + // Resolve all "@wasp" imports to the generated source code. + "@wasp/*": [ + "src/*" + ], + // Resolve all non-relative imports to the correct node module. Source: + // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping + "*": [ + // Start by looking for the definiton inside the node modules root + // directory... + "node_modules/*", + // ... If that fails, try to find it inside definitely-typed type + // definitions. + "node_modules/@types/*" + ] + }, + // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots + "typeRoots": [ + "../../.wasp/out/server/node_modules/@types" + ], + // Since this TS config is used only for IDE support and not for + // compilation, the following directory doesn't exist. We need to specify + // it to prevent this error: + // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file + "outDir": "phantom", + }, + "exclude": [ + "phantom" + ], +} \ No newline at end of file diff --git a/examples/streaming/src/shared/tsconfig.json b/examples/streaming/src/shared/tsconfig.json new file mode 100644 index 000000000..20fcac843 --- /dev/null +++ b/examples/streaming/src/shared/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Enable default imports in TypeScript. + "esModuleInterop": true, + "allowJs": true, + // The following settings enable IDE support in user-provided source files. + // Editing them might break features like import autocompletion and + // definition lookup. Don't change them unless you know what you're doing. + // + // The relative path to the generated web app's root directory. This must be + // set to define the "paths" option. + "baseUrl": "../../.wasp/out/server/", + "paths": { + // Resolve all non-relative imports to the correct node module. Source: + // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping + "*": [ + // Start by looking for the definiton inside the node modules root + // directory... + "node_modules/*", + // ... If that fails, try to find it inside definitely-typed type + // definitions. + "node_modules/@types/*" + ] + }, + // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots + "typeRoots": ["../../.wasp/out/server/node_modules/@types"] + } +}