From 9efd57f64ee740fe36dc5c841b25e69c6293377f Mon Sep 17 00:00:00 2001 From: Lam Tran Date: Sun, 18 Jan 2026 00:03:36 +0700 Subject: [PATCH 1/3] problem: 1 - Three ways to sum to n --- src/problem1/.keep | 0 src/problem1/README.md | 53 ++++++++++++++++++++++++ src/problem1/index.js | 31 ++++++++++++++ src/problem1/test.js | 84 ++++++++++++++++++++++++++++++++++++++ src/problem1/validation.js | 23 +++++++++++ 5 files changed, 191 insertions(+) delete mode 100644 src/problem1/.keep create mode 100644 src/problem1/README.md create mode 100644 src/problem1/index.js create mode 100644 src/problem1/test.js create mode 100644 src/problem1/validation.js diff --git a/src/problem1/.keep b/src/problem1/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/problem1/README.md b/src/problem1/README.md new file mode 100644 index 000000000..1f67c1496 --- /dev/null +++ b/src/problem1/README.md @@ -0,0 +1,53 @@ +# Problem 1: Three ways to sum to n + +## Description +Provide 3 unique implementations of a function that calculates the summation of numbers from 1 to `n`. + +**Input**: `n` - any integer. +**Output**: Summation to `n`. + +## Assumptions +1. The result will always be less than `Number.MAX_SAFE_INTEGER`. +2. Input `n` should be a non-negative integer. + - If `n` is 0, a sum is 0. + - If `n` < 0, the function throws an error (based on standard summation definition 1...n, though mathematical definition might return 0 or be invalid, here strict validation is applied). + +## Implementations + +### 1. `sum_to_n_a`: Arithmetic Series Formula +Uses the mathematical formula for the sum of the first n integers: +\[ S_n = \frac{n(n+1)}{2} \] + +- **Time Complexity**: O(1)—Constant time calculation. +- **Space Complexity**: O(1)—Constant space. +- **Pros**: Fastest and most efficient. +- **Cons**: Theoretical risk of intermediate overflow `n*(n+1)` if `n` is extremely large (close to `MAX_SAFE_INTEGER`). + +### 2. `sum_to_n_b`: Iterative Loop +Uses a `for` loop to accumulate the sum from 1 to `n`. + +- **Time Complexity**: O(n)—Linear time. +- **Space Complexity**: O(1)—Constant space. +- **Pros**: Robust, easy to read, no recursion stack limits. +- **Cons**: Slower than the formula for very large `n`. + +### 3. `sum_to_n_c`: Recursion +Uses the recursive definition: `sum(n) = n + sum(n-1)`. + +- **Time Complexity**: O(n)—Linear time. +- **Space Complexity**: O(n)—Linear space due to the call stack. +- **Pros**: Demonstrates functional programming concepts. +- **Cons**: In JavaScript, this is limited by the maximum call stack size (typically ~10,000 calls). Will throw `RangeError: Maximum call stack size exceeded` for large `n`. + +## Usage +```javascript +import { sum_to_n_a } from './index' + +console.log(sum_to_n_a(5)); // 15 +``` + +## Testing +Run the test suite: +```bash +node src/problem1/test.js +``` diff --git a/src/problem1/index.js b/src/problem1/index.js new file mode 100644 index 000000000..8bcb2b2fd --- /dev/null +++ b/src/problem1/index.js @@ -0,0 +1,31 @@ +import { validateInput } from "./validation" + +// Option A: Arithmetic Series Formula | O(1) time, O(1) space +export const sum_to_n_a = function(n) { + validateInput(n) + + if (n === 0) return 0 + return (n * (n + 1)) / 2 +} + +// Option B: Iterative Loop | O(n) time, O(1) space +export const sum_to_n_b = function(n) { + validateInput(n) + + let sum = 0 + for (let i = 1; i <= n; i++) { + sum += i + } + return sum +} + +// Option C: Recursion | O(n) time, O(n) space (stack) +export const sum_to_n_c = function(n) { + if (n === 0) return 0 + if (n === 1) return 1 + + validateInput(n) + + if (n <= 1) return n + return n + sum_to_n_c(n - 1) +} \ No newline at end of file diff --git a/src/problem1/test.js b/src/problem1/test.js new file mode 100644 index 000000000..cb38fc7f8 --- /dev/null +++ b/src/problem1/test.js @@ -0,0 +1,84 @@ +import assert from "assert" +import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "./index" + +const functions = [ + { name: 'sum_to_n_a', fn: sum_to_n_a }, + { name: 'sum_to_n_b', fn: sum_to_n_b }, + { name: 'sum_to_n_c', fn: sum_to_n_c } +] + +console.log("Starting Tests...\n") + +let passed = 0 +let failed = 0 + +function runTest(testName, action) { + try { + action() + console.log(`✅ [PASS] ${testName}`) + passed++ + } catch (e) { + console.error(`❌ [FAIL] ${testName}`) + console.error(` Error: ${e.message}`) + failed++ + } +} + +functions.forEach(function(impl) { + const fn = impl.fn + const name = impl.name + + console.log(`Testing implementation: ${name}`) + + // Test Case 1: Standard Input + runTest(`${name}(5) should be 15`, function() { + assert.strictEqual(fn(5), 15) + }) + + // Test Case 2: Base Case 1 + runTest(`${name}(1) should be 1`, function() { + assert.strictEqual(fn(1), 1) + }) + + // Test Case 3: Base Case 0 + runTest(`${name}(0) should be 0`, function() { + assert.strictEqual(fn(0), 0) + }) + + // Test Case 4: Large Input (within stack limit for recursion) + runTest(`${name}(100) should be 5050`, function() { + assert.strictEqual(fn(100), 5050) + }) + + // Test Case 5: Negative Input + runTest(`${name}(-1) should throw Error`, function() { + assert.throws(function() { + return fn(-1) + }, /Input must be a non-negative integer/) + }) + + // Test Case 6: Non-integer Input + runTest(`${name}(5.5) should throw Error`, function() { + assert.throws(function() { + return fn(5.5) + }, /Input must be an integer/) + }) + + // Test Case 7: String Input + runTest(`${name}('5') should throw Error`, function() { + assert.throws(function() { + return fn('5') + }, /Input must be a number/) + }) + + console.log('---') +}) + +console.log(`\nTest Summary:`) +console.log(`Total: ${passed + failed}`) +console.log(`Passed: ${passed}`) +console.log(`Failed: ${failed}`) + +if (failed > 0) { + process.exit(1) +} diff --git a/src/problem1/validation.js b/src/problem1/validation.js new file mode 100644 index 000000000..45f7de21c --- /dev/null +++ b/src/problem1/validation.js @@ -0,0 +1,23 @@ +/** + * Validates the input n for the sum_to_n function. + * + * @param {any} n - The input to validate. + * @throws {Error} If the input is invalid. + */ +export const validateInput = function(n) { + if (typeof n !== 'number') { + throw new Error("Input must be a number") + } + + if (!Number.isInteger(n)) { + throw new Error("Input must be an integer") + } + + if (n < 0) { + throw new Error("Input must be a non-negative integer") + } + + if (n > Number.MAX_SAFE_INTEGER) { + throw new Error("Input exceeds MAX_SAFE_INTEGER") + } +} From 14250648ef55e24d692348e3764965792c9397ae Mon Sep 17 00:00:00 2001 From: Lam Tran Date: Tue, 20 Jan 2026 04:33:44 +0700 Subject: [PATCH 2/3] problem: 2 - Fancy Form --- src/problem2/.gitignore | 24 + src/problem2/.husky/pre-commit | 1 + src/problem2/README.md | 73 + src/problem2/biome.json | 54 + src/problem2/components.json | 23 + src/problem2/index.html | 41 +- src/problem2/package.json | 65 + src/problem2/public/mockServiceWorker.js | 336 ++ src/problem2/script.js | 0 src/problem2/src/App.tsx | 20 + src/problem2/src/components/Layout/Footer.tsx | 37 + src/problem2/src/components/Layout/Header.tsx | 65 + src/problem2/src/components/Layout/index.tsx | 16 + src/problem2/src/components/TokenIcon.tsx | 19 + src/problem2/src/components/TokenSelect.tsx | 161 + src/problem2/src/components/ui/button.tsx | 65 + src/problem2/src/components/ui/card.tsx | 83 + src/problem2/src/components/ui/dialog.tsx | 139 + .../src/components/ui/input-group.tsx | 147 + src/problem2/src/components/ui/input.tsx | 19 + src/problem2/src/components/ui/label.tsx | 19 + src/problem2/src/components/ui/popover.tsx | 74 + .../src/components/ui/scroll-area.tsx | 53 + src/problem2/src/components/ui/separator.tsx | 26 + src/problem2/src/components/ui/sonner.tsx | 43 + src/problem2/src/components/ui/textarea.tsx | 18 + src/problem2/src/features/Swap/Card.tsx | 36 + src/problem2/src/features/Swap/Detail.tsx | 111 + src/problem2/src/features/Swap/Form.tsx | 161 + .../src/features/Swap/SwitcherSeparator.tsx | 18 + src/problem2/src/features/Swap/index.tsx | 65 + src/problem2/src/index.css | 175 + src/problem2/src/lib/currency.ts | 7 + src/problem2/src/lib/number.ts | 35 + src/problem2/src/lib/utils.ts | 6 + src/problem2/src/main.tsx | 25 + src/problem2/src/mocks/browser.ts | 4 + src/problem2/src/mocks/handlers.ts | 176 + .../src/services/price/price.query.ts | 28 + .../src/services/price/price.schema.ts | 39 + .../src/services/price/price.service.ts | 18 + src/problem2/src/services/price/price.type.ts | 13 + .../src/services/swap/swap.service.ts | 21 + .../src/services/token/token.query.ts | 41 + .../src/services/token/token.schema.ts | 14 + .../src/services/token/token.service.ts | 12 + src/problem2/src/services/token/token.type.ts | 6 + .../src/services/wallet/wallet.service.ts | 37 + src/problem2/style.css | 8 - src/problem2/tsconfig.app.json | 31 + src/problem2/tsconfig.json | 17 + src/problem2/tsconfig.node.json | 24 + src/problem2/vite.config.ts | 14 + src/problem2/yarn.lock | 4417 +++++++++++++++++ 54 files changed, 7147 insertions(+), 33 deletions(-) create mode 100644 src/problem2/.gitignore create mode 100644 src/problem2/.husky/pre-commit create mode 100644 src/problem2/README.md create mode 100644 src/problem2/biome.json create mode 100644 src/problem2/components.json create mode 100644 src/problem2/package.json create mode 100644 src/problem2/public/mockServiceWorker.js delete mode 100644 src/problem2/script.js create mode 100644 src/problem2/src/App.tsx create mode 100644 src/problem2/src/components/Layout/Footer.tsx create mode 100644 src/problem2/src/components/Layout/Header.tsx create mode 100644 src/problem2/src/components/Layout/index.tsx create mode 100644 src/problem2/src/components/TokenIcon.tsx create mode 100644 src/problem2/src/components/TokenSelect.tsx create mode 100644 src/problem2/src/components/ui/button.tsx create mode 100644 src/problem2/src/components/ui/card.tsx create mode 100644 src/problem2/src/components/ui/dialog.tsx create mode 100644 src/problem2/src/components/ui/input-group.tsx create mode 100644 src/problem2/src/components/ui/input.tsx create mode 100644 src/problem2/src/components/ui/label.tsx create mode 100644 src/problem2/src/components/ui/popover.tsx create mode 100644 src/problem2/src/components/ui/scroll-area.tsx create mode 100644 src/problem2/src/components/ui/separator.tsx create mode 100644 src/problem2/src/components/ui/sonner.tsx create mode 100644 src/problem2/src/components/ui/textarea.tsx create mode 100644 src/problem2/src/features/Swap/Card.tsx create mode 100644 src/problem2/src/features/Swap/Detail.tsx create mode 100644 src/problem2/src/features/Swap/Form.tsx create mode 100644 src/problem2/src/features/Swap/SwitcherSeparator.tsx create mode 100644 src/problem2/src/features/Swap/index.tsx create mode 100644 src/problem2/src/index.css create mode 100644 src/problem2/src/lib/currency.ts create mode 100644 src/problem2/src/lib/number.ts create mode 100644 src/problem2/src/lib/utils.ts create mode 100644 src/problem2/src/main.tsx create mode 100644 src/problem2/src/mocks/browser.ts create mode 100644 src/problem2/src/mocks/handlers.ts create mode 100644 src/problem2/src/services/price/price.query.ts create mode 100644 src/problem2/src/services/price/price.schema.ts create mode 100644 src/problem2/src/services/price/price.service.ts create mode 100644 src/problem2/src/services/price/price.type.ts create mode 100644 src/problem2/src/services/swap/swap.service.ts create mode 100644 src/problem2/src/services/token/token.query.ts create mode 100644 src/problem2/src/services/token/token.schema.ts create mode 100644 src/problem2/src/services/token/token.service.ts create mode 100644 src/problem2/src/services/token/token.type.ts create mode 100644 src/problem2/src/services/wallet/wallet.service.ts delete mode 100644 src/problem2/style.css create mode 100644 src/problem2/tsconfig.app.json create mode 100644 src/problem2/tsconfig.json create mode 100644 src/problem2/tsconfig.node.json create mode 100644 src/problem2/vite.config.ts create mode 100644 src/problem2/yarn.lock diff --git a/src/problem2/.gitignore b/src/problem2/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/src/problem2/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/problem2/.husky/pre-commit b/src/problem2/.husky/pre-commit new file mode 100644 index 000000000..af5adff9d --- /dev/null +++ b/src/problem2/.husky/pre-commit @@ -0,0 +1 @@ +lint-staged \ No newline at end of file diff --git a/src/problem2/README.md b/src/problem2/README.md new file mode 100644 index 000000000..4dcad1f91 --- /dev/null +++ b/src/problem2/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/src/problem2/biome.json b/src/problem2/biome.json new file mode 100644 index 000000000..ebc0767f9 --- /dev/null +++ b/src/problem2/biome.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noUselessFragments": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off" + }, + "style": { + "useConsistentCurlyBraces": { + "level": "warn", + "fix": "safe" + } + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "asNeeded", + "arrowParentheses": "asNeeded" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "recommended": true + } + } + }, + "css": { + "parser": { + "tailwindDirectives": true + } + } +} diff --git a/src/problem2/components.json b/src/problem2/components.json new file mode 100644 index 000000000..e714875b5 --- /dev/null +++ b/src/problem2/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-mira", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "menuColor": "default", + "menuAccent": "subtle", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/src/problem2/index.html b/src/problem2/index.html index 4058a68bf..e22df41a5 100644 --- a/src/problem2/index.html +++ b/src/problem2/index.html @@ -1,27 +1,18 @@ - - - - - Fancy Form - - - - - - - - -
-
Swap
- - - - - - - -
- - + + + + + + Fancy Form + + + + + + + +
+ + diff --git a/src/problem2/package.json b/src/problem2/package.json new file mode 100644 index 000000000..fc7ee15a9 --- /dev/null +++ b/src/problem2/package.json @@ -0,0 +1,65 @@ +{ + "name": "problem2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "prepare": "husky" + }, + "dependencies": { + "@number-flow/react": "^0.5.10", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-form": "^1.27.7", + "@tanstack/react-query": "^5.90.19", + "@tanstack/react-router": "^1.151.3", + "@tanstack/router-devtools": "^1.151.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "decimal.js": "^10.6.0", + "es-toolkit": "^1.44.0", + "ethers": "^6.16.0", + "framer-motion": "^12.26.2", + "lucide-react": "^0.562.0", + "next-themes": "^0.4.6", + "nuqs": "^2.8.6", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "shadcn": "^3.7.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "tiny-invariant": "^1.3.3", + "usehooks-ts": "^3.1.1", + "zod": "^4.3.5", + "zustand": "^5.0.10" + }, + "devDependencies": { + "@biomejs/biome": "2.3.11", + "@faker-js/faker": "^10.2.0", + "@tailwindcss/container-queries": "^0.1.1", + "@tailwindcss/forms": "^0.5.11", + "@types/node": "^25.0.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "husky": "^9.1.7", + "lint-staged": "^16.2.7", + "msw": "^2.12.7", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "vite": "^7.2.4" + }, + "lint-staged": { + "*": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/src/problem2/public/mockServiceWorker.js b/src/problem2/public/mockServiceWorker.js new file mode 100644 index 000000000..5b83e5498 --- /dev/null +++ b/src/problem2/public/mockServiceWorker.js @@ -0,0 +1,336 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = "2.12.7" +const INTEGRITY_CHECKSUM = "4db4a41e972cec1b64cc569c66952d82" +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse") +const activeClientIds = new Set() + +addEventListener("install", () => { + self.skipWaiting() +}) + +addEventListener("activate", event => { + event.waitUntil(self.clients.claim()) +}) + +addEventListener("message", async event => { + const clientId = Reflect.get(event.source || {}, "id") + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: "window", + }) + + switch (event.data) { + case "KEEPALIVE_REQUEST": { + sendToClient(client, { + type: "KEEPALIVE_RESPONSE", + }) + break + } + + case "INTEGRITY_CHECK_REQUEST": { + sendToClient(client, { + type: "INTEGRITY_CHECK_RESPONSE", + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case "MOCK_ACTIVATE": { + activeClientIds.add(clientId) + + sendToClient(client, { + type: "MOCKING_ENABLED", + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case "CLIENT_CLOSED": { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter(client => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener("fetch", event => { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === "navigate") { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId, requestInterceptedAt) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: "RESPONSE", + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === "top-level") { + return client + } + + const allClients = await self.clients.matchAll({ + type: "window", + }) + + return allClients + .filter(client => { + // Get only those clients that are currently visible. + return client.visibilityState === "visible" + }) + .find(client => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get("accept") + if (acceptHeader) { + const values = acceptHeader.split(",").map(value => value.trim()) + const filteredValues = values.filter(value => value !== "msw/passthrough") + + if (filteredValues.length > 0) { + headers.set("accept", filteredValues.join(", ")) + } else { + headers.delete("accept") + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: "REQUEST", + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case "MOCK_RESPONSE": { + return respondWithMock(clientMessage.data) + } + + case "PASSTHROUGH": { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = event => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/src/problem2/script.js b/src/problem2/script.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/problem2/src/App.tsx b/src/problem2/src/App.tsx new file mode 100644 index 000000000..353a41fbb --- /dev/null +++ b/src/problem2/src/App.tsx @@ -0,0 +1,20 @@ +import { Layout } from "@/components/Layout" +import { Footer } from "@/components/Layout/Footer" +import { Header } from "@/components/Layout/Header" +import { Swap } from "@/features/Swap" + +export default function App() { + return ( + +
+ +
+
+ +
+
+ +