├── src ├── main.ts ├── style.css └── app.tsx ├── vite.config.ts ├── index.html ├── .gitignore ├── package.json ├── tsconfig.json ├── .github └── workflows │ └── pages.yml └── renderer └── __reactive_renderer.ts /src/main.ts: -------------------------------------------------------------------------------- 1 | import {main} from "./app.tsx"; 2 | main(); -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | esbuild: { 6 | // jsxInject: `import {__jsx} from '@renderer/renderer';`, 7 | }, 8 | resolve: { 9 | alias: { 10 | 'renderer': path.resolve(__dirname, './renderer/') 11 | } 12 | } 13 | }) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | New Reactive 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ex", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^20.3.2", 13 | "tsc-alias": "^1.8.6", 14 | "typescript": "^5.0.2", 15 | "vite": "^4.3.9" 16 | }, 17 | "dependencies": { 18 | "highlight.js": "^11.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react", 16 | "jsxFactory": "__jsx", 17 | "composite": true, 18 | "declaration": true, 19 | "declarationMap": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "renderer": ["renderer"] 23 | }, 24 | /* Linting */ 25 | // "strict": false, 26 | "alwaysStrict": false, 27 | "strictPropertyInitialization": false, 28 | "noUnusedLocals": true, 29 | "noUnusedParameters": true, 30 | "noFallthroughCasesInSwitch": true, 31 | }, 32 | "include": ["src", "**/*.d.ts", "renderer"], 33 | 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: 'pages' 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Set up Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | cache: 'npm' 38 | - name: Install dependencies 39 | run: npm install 40 | - name: Build 41 | run: npm run build 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v3 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v1 46 | with: 47 | # Upload dist repository 48 | path: './dist' 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v1 52 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | font-family: "Twilio Sans Mono", monospace; 5 | padding: 20px 20px; 6 | } 7 | 8 | code { 9 | white-space: pre; 10 | display: block; 11 | margin-bottom: 20px; 12 | background-color: rgb(25, 25, 30); 13 | padding: 20px; 14 | border: 3px solid rgb(105,105,105); 15 | max-width: 100%; 16 | overflow: hidden; 17 | overflow-x: auto; 18 | } 19 | 20 | code>h3 { 21 | color: #AAA; 22 | margin: 0px; 23 | margin-bottom: 10px; 24 | } 25 | 26 | .tests { 27 | border: solid 3px white; 28 | padding: 15px; 29 | } 30 | 31 | .tests>div { 32 | padding-left: 20px; 33 | padding-right: 20px; 34 | padding-bottom: 10px; 35 | } 36 | 37 | h2 { 38 | padding-left: 15px; 39 | padding-top: 20px; 40 | border-top: solid 1px rgb(64,64,64); 41 | } 42 | 43 | h2>a { 44 | color: white; 45 | text-decoration: none; 46 | } 47 | h2>a:hover { 48 | text-decoration: underline; 49 | } 50 | .tests>div>a.output { 51 | margin-top: 10px; 52 | background-color: rgb(15,15,15); 53 | padding: 10px 20px; 54 | width: 100%; 55 | box-sizing: border-box; 56 | display: block; 57 | border: 3px solid rgb(185,185,185); 58 | } 59 | 60 | h1 { 61 | text-align: center; 62 | } 63 | 64 | button { 65 | background: white; 66 | border: none; 67 | outline: none; 68 | font-family: "Twilio Sans Mono", monospace; 69 | font-weight: bold; 70 | cursor: pointer; 71 | font-size: 16px; 72 | margin-left: 10px; 73 | border: 1px solid black; 74 | padding: 5px 10px; 75 | } 76 | 77 | button:hover { 78 | font-style: italic; 79 | transition: 100ms; 80 | border: 1px solid white; 81 | } 82 | 83 | button:hover:active { 84 | transform: scale(1.05); 85 | transition: 100ms; 86 | } 87 | 88 | .output, .output * { 89 | background-color: inherit; 90 | animation: mymove 0.5s ease ; 91 | } 92 | 93 | @keyframes mymove { 94 | from {background-color: rgba(255,255,255,0.5);} 95 | to {background-color: inherit;} 96 | } -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { $, $_, __jsx } from "renderer/__reactive_renderer"; 2 | import "./style.css"; 3 | import hljs from "highlight.js"; 4 | import "highlight.js/styles/github-dark.css"; 5 | function render_code(lang: string, el: string) { 6 | // return hljs.highlight(el, {language: lang}); 7 | const container: HTMLElement = ; 8 | container.innerHTML = hljs.highlight(el.trim(), { language: lang }).value; 9 | container.prepend(

// {lang}

); 10 | return container; 11 | } 12 | 13 | export function main() { 14 | $.worldEmoji = "🌏"; 15 | const planets = ["🌕", "🪐", "☀️", "☄️", "🌏"]; 16 | setInterval(() => { 17 | $.worldEmoji = planets[0]; 18 | planets.push(planets.shift()); 19 | }, 2000); 20 | 21 | $.fireEmoji = "🔥"; 22 | $.reflected = $_(() => $.worldEmoji + "... copycat"); 23 | $.arr = [ 24 | fireEmoji: {$.fireEmoji}, 25 | | , 26 | worldEmoji: {$.worldEmoji}, 27 | ]; 28 | $.arr2 = [ 29 | 1,hey,2,3,4,5,6, hey 30 | ] 31 | 32 | app = ( 33 |
34 |

Reactivity Demo

35 |

36 | You shouldn't need to learn a whole framework to have HTML that is 37 | synced with your code. 38 |

39 |

40 | This library attempts to match the syntax and experience of writing 41 | plain vanilla JS. No need for functions to set values, access them like 42 | you would any other variable :) 43 |

44 |

45 | # Basics 46 |

47 |
48 |

Create and change a reactive value using the “$” syntax.

49 | {render_code("html", ``)} 50 | {render_code( 51 | "jsx", 52 | ` 53 | $.worldEmoji = "🌏"; 54 | const planets = ["🌕", "🪐", "☀️", "☄️", "🌏"]; 55 | setInterval(() => { 56 | $.worldEmoji = planets[0]; 57 | planets.push(planets.shift()); 58 | }, 2000); 59 | 60 | app = $.worldEmoji 61 | ` 62 | )} 63 |

Output

64 | {$.worldEmoji} 65 |
66 |

67 | # Re-Evaluated Expressions 68 |

69 |
70 |

71 | It's easy to have complex expressions that will be re-evaluated 72 | whenever one of the used values is updated. 73 |

74 |

75 | (Note: Currently this may only work for variables that are accessed on the initial evaluation, so they may not work if they are behind an if statement.) 76 |

77 |

You can work around this by simply referencing any variables at the start of the function to make sure they are included.

78 | {render_code( 79 | "jsx", 80 | `app = $_(() => ($.worldEmoji == "🌏" ? "Yes!" : "nope :("))` 81 | )} 82 |

Output

83 | 84 | Does {$.worldEmoji} equal 🌏?{" "} 85 | {$_(() => ($.worldEmoji == "🌏" ? "Yes!" : "nope :("))} 86 | 87 |
88 |

89 | # Reflected values 90 |

91 |
92 |

93 | Variables can reference other variables and then be set to another 94 | value without affecting the original. This is achieved by simply using 95 | the same function used to create re-evaluated expressions. 96 |

97 | {render_code( 98 | "jsx", 99 | ` 100 | $.reflected = $_(() => $.worldEmoji + "... copycat"); 101 | 102 | app =
103 | 106 | {$.reflected} 107 |
108 | ` 109 | )} 110 | 117 |

Output

118 | {$.reflected} 119 |
120 |

121 | # Undefined values 122 |

123 |
124 |

125 | If a reactive variable does not exist when you try to access it, it 126 | will actually create a placeholder variable in case it gets set in the 127 | future. 128 |

129 | {render_code( 130 | "jsx", 131 | ` 132 | app =
133 | 136 | {$.test} 137 |
138 | ` 139 | )} 140 | 147 |

Output

148 | {$.test} 149 |
150 |

151 | # Arrays 152 |

153 |
154 |

And of course, arrays of JSX function how you'd expect.

155 | {render_code( 156 | "jsx", 157 | ` 158 | $.arr = [ 159 | fireEmoji: {$.fireEmoji} | , 160 | worldEmoji: {$.worldEmoji}, 161 | ]; 162 | 163 | app = {$.arr} 164 | ` 165 | )} 166 |

It even supports mutations!

167 | {render_code("jsx", ` 168 | 169 | 170 | `)} 171 | 172 |

Output

173 | {/* This breaks as JSX is a reference to an element, ig would need to detect if a Node already exists in the document and if so, duplicate it, but this could de-couple it from the data? */} 174 | {$.arr2[2]} 175 | {$.arr2} 176 |
177 |

178 | # Issues 179 |

180 | 192 |
193 | ); 194 | $.arr2.push(
who
) 195 | } 196 | -------------------------------------------------------------------------------- /renderer/__reactive_renderer.ts: -------------------------------------------------------------------------------- 1 | let queue: Array<__reactive_object> = []; 2 | const _d: { 3 | [key: string | symbol]: __reactive_object; 4 | } = {}; 5 | let count: number = 0; 6 | declare global { 7 | namespace JSX { 8 | interface IntrinsicElements { 9 | [elemName: string]: any; 10 | } 11 | } 12 | let $: typeof _d; 13 | let app: Node | null; 14 | interface Window { 15 | $: typeof _d; 16 | } 17 | } 18 | 19 | function find_key_by_id(id) { 20 | const [k, _] = Object.entries(_d).find(([_, v]) => v.__reactive_id == id); 21 | return k; 22 | } 23 | 24 | function set_node(new_node: AppNode) { 25 | Object.defineProperties(globalThis, { 26 | app: { 27 | get: function () { 28 | return new_node.firstChild; 29 | }, 30 | set: function (value) { 31 | new_node.set_child(value); 32 | }, 33 | enumerable: true, 34 | configurable: true, 35 | }, 36 | }); 37 | } 38 | 39 | const custom_classes = {}; 40 | 41 | type __reactive_object = T & { 42 | __reactive_id: number; 43 | }; 44 | 45 | // Creates a custom class that allows mutations to be detected. 46 | // Cached so it should only generate once for each type 47 | function __reactive_factory(construct) { 48 | if (construct in custom_classes) { 49 | return custom_classes[construct]; 50 | } 51 | class newClass extends construct { 52 | constructor(...args) { 53 | args ? super(...args) : super(); 54 | } 55 | } 56 | 57 | for (const key of Object.getOwnPropertyNames(construct.prototype).filter( 58 | (a) => a !== "valueOf" 59 | )) { 60 | if (typeof newClass.prototype[key] == "function") { 61 | newClass.prototype[key] = function (...args) { 62 | // TODO: Detect mutations here 63 | // Not sure if this clone is sufficient but it is good enough for now 64 | // Structuredclone isn't usable with objects that may contain htmlelements afaik 65 | const temp = Object.assign({}, this); 66 | const work = construct.prototype[key].call(this, ...args); 67 | if (check_mutation(temp, this)) { 68 | // This shoullddd work? 69 | reactive_update(this.__reactive_id); 70 | } 71 | return work; 72 | }; 73 | } 74 | } 75 | 76 | custom_classes[construct] = newClass; 77 | return newClass; 78 | } 79 | 80 | function check_mutation(x, y) { 81 | const xk = Object.keys(x).sort(); 82 | const yk = Object.keys(y).sort(); 83 | if (xk.length !== yk.length) { 84 | return true; 85 | } else { 86 | const areEqual = xk.every((key, index) => { 87 | const xv = x[key]; 88 | const yv = y[yk[index]]; 89 | return xv === yv; 90 | }); 91 | if (areEqual) { 92 | return false; 93 | } else { 94 | return true; 95 | } 96 | } 97 | } 98 | 99 | class AppNode extends HTMLElement { 100 | child: Node = this.appendChild(document.createTextNode("")); 101 | constructor() { 102 | super(); 103 | if ("app" in window) { 104 | this.set_child(app); 105 | } 106 | set_node(this); 107 | } 108 | 109 | set_child(value: Node) { 110 | if (this.child) { 111 | this.removeChild(this.child); 112 | } 113 | this.child = this.appendChild(value); 114 | } 115 | } 116 | 117 | customElements.define("reactive-app", AppNode); 118 | 119 | function __jsx_append( 120 | parent: HTMLElement, 121 | child: Node | __reactive_object<__reactive_func | Object> | string 122 | ) { 123 | if (Array.isArray(child)) { 124 | child.forEach((nestedChild) => __jsx_append(parent, nestedChild)); 125 | } else { 126 | if (child != undefined) { 127 | let out: Node; 128 | if (!(child instanceof Node)) { 129 | if (child instanceof __reactive_func) { 130 | out = document.createTextNode(child.run_function()); 131 | queue = []; 132 | } else { 133 | out = document.createTextNode(String(child.valueOf())); 134 | } 135 | } else { 136 | if (child.isConnected) { 137 | out = child.cloneNode(true); 138 | } else { 139 | out = child; 140 | } 141 | } 142 | parent.appendChild(out); 143 | } 144 | } 145 | } 146 | 147 | export function __jsx( 148 | tag: string, 149 | props: { 150 | [key: string]: string | EventListener | __reactive_object<__reactive_func>; 151 | }, 152 | ...children: (Node | __reactive_object<__reactive_func | Object> | string)[] 153 | ) { 154 | const element = document.createElement(tag); 155 | if(queue.length > 0 && props) { 156 | element.setAttribute( 157 | "__reactive_props_recipe", 158 | JSON.stringify( 159 | Object.entries(props).map(([name,value]) => { 160 | if (typeof value == "object" && "__reactive_id" in value) { 161 | return { name: name, __reactive_id: value.__reactive_id }; 162 | } 163 | return {name: name, value: value}; 164 | }) 165 | ) 166 | ); 167 | } 168 | 169 | Object.entries(props || {}).forEach(([name, value]) => { 170 | if (name.startsWith("on") && name.toLowerCase() in window) { 171 | element.addEventListener( 172 | name.toLowerCase().slice(2), 173 | value as EventListener | EventListenerObject 174 | ); 175 | } else if (typeof value == "object" && "__reactive_id" in value) { 176 | element.setAttribute(name, value.run_function()); 177 | } else { 178 | element.setAttribute(name, value.toString()); 179 | } 180 | }); 181 | 182 | if (queue.length > 0) { 183 | const deps = []; 184 | queue.forEach((a) => { 185 | if (a instanceof __reactive_func) { 186 | deps.push(...a.ids); 187 | } 188 | deps.push(a.__reactive_id); 189 | }); 190 | element.setAttribute( 191 | "__reactive_deps", 192 | deps.filter((v, i, a) => a.indexOf(v) == i).join(" ") 193 | ); 194 | element.setAttribute( 195 | "__reactive_recipe", 196 | JSON.stringify( 197 | children.map((a) => { 198 | if (typeof a == "object" && "__reactive_id" in a) { 199 | return { __reactive_id: a.__reactive_id }; 200 | } 201 | return a; 202 | }) 203 | ) 204 | ); 205 | queue = []; 206 | } 207 | 208 | children.forEach((child) => { 209 | __jsx_append(element, child); 210 | }); 211 | return element; 212 | } 213 | 214 | // Wrapper for an expression that can be re-evaluated dynamically based on the reactives called during its initial evaluation 215 | class __reactive_func { 216 | function: Function; 217 | ids: number[]; 218 | constructor(func: Function) { 219 | this.function = func; 220 | // Need to call the generator to add dependencies into the queue 221 | func(); 222 | this.ids = queue.map((a) => a.__reactive_id); 223 | } 224 | 225 | run_function() { 226 | let x = this.function(); 227 | return x; 228 | } 229 | } 230 | 231 | // TODO: can probably remove this and implement everything in the reactive factory 232 | function objectify( 233 | input: T, 234 | existing_id: number | null = null 235 | ): __reactive_object { 236 | let temp: Object; 237 | switch (typeof input) { 238 | case "object": 239 | if (input == null) { 240 | throw "Null cannot be rendered."; 241 | } 242 | // This may not work with custom classes but we'll have to see 243 | const newClass = __reactive_factory(input.constructor); 244 | if (Array.isArray(input)) { 245 | temp = new newClass(...input); 246 | break; 247 | } 248 | temp = new newClass(input); 249 | break; 250 | case "string": 251 | temp = new (__reactive_factory(String))(input); 252 | break; 253 | case "number": 254 | temp = new (__reactive_factory(Number))(input); 255 | break; 256 | case "boolean": 257 | temp = new (__reactive_factory(Boolean))(input); 258 | break; 259 | case "undefined": 260 | throw "Undefined cannot be rendered."; 261 | case "symbol": 262 | throw "Symbols cannot be rendered."; 263 | case "function": 264 | temp = new __reactive_func(input); 265 | break; 266 | case "bigint": 267 | throw "Bigints cannot be rendered"; 268 | default: 269 | throw "Unhandled input type."; 270 | } 271 | const out = temp as __reactive_object; 272 | if (!out.__reactive_id) { 273 | const id = existing_id ? existing_id : (count += 1); 274 | out.__reactive_id = id; 275 | } 276 | return out; 277 | } 278 | 279 | // Sends event to update any dependent DOM nodes 280 | function reactive_update(id) { 281 | document.querySelectorAll(`[__reactive_deps~="${id}"]`).forEach((a: HTMLElement) => { 282 | const recipe: Array = JSON.parse( 283 | a.getAttribute("__reactive_recipe") 284 | ) || []; 285 | 286 | const props: Array<{name: string, value: any} | {name:string, __reactive_id: number}> = JSON.parse( 287 | a.getAttribute("__reactive_props_recipe") 288 | ); 289 | const prop_data = (props || []).reduce((x, y)=>{ 290 | x[y.name] = ("value" in y) ? y.value : _d[find_key_by_id(y.__reactive_id)]; 291 | return x; 292 | }, {}); 293 | 294 | a.replaceWith( 295 | __jsx( 296 | a.tagName, 297 | prop_data, 298 | ...recipe.map((b) => { 299 | if (typeof b == "object") { 300 | const k = find_key_by_id(b.__reactive_id); 301 | queue.push(_d[k]); 302 | return _d[k]; 303 | } 304 | return b; 305 | }) 306 | ) 307 | ); 308 | }); 309 | } 310 | 311 | function create_var(p: string | symbol, val: any) { 312 | let out = val; 313 | if (!val.__reactive_id) { 314 | out = objectify(val); 315 | } 316 | const scope = get_scope(); 317 | const scope_id = scope.from_level(0); 318 | // console.log(scope_id); 319 | _d[p] = out; 320 | if (val.__reactive_id) { 321 | // This is for reflected values 322 | const k = find_key_by_id(val.__reactive_id); 323 | if (k.startsWith("__anonymous_")) { 324 | _d[p].__reactive_id = _d[k].__reactive_id; 325 | delete _d[k]; 326 | } 327 | } 328 | queue = []; 329 | } 330 | // Used to access stored reactive variables with $.[prop_name] 331 | export const $: typeof _d = (window.$ = new Proxy(_d, { 332 | get(target: typeof _d, p) { 333 | // TODO: Somehow clean up the _d store once the variables are no longer in scope 334 | if (!(p in target)) { 335 | create_var(p, "undefined"); 336 | } 337 | queue.push(target[p]); 338 | return target[p]; 339 | }, 340 | set( 341 | target: typeof _d, 342 | p: string | symbol, 343 | newValue: 344 | | Object 345 | | __reactive_func 346 | | __reactive_object 347 | ): boolean { 348 | queue = []; 349 | if (!(p in target)) { 350 | create_var(p, newValue); 351 | return true; 352 | } 353 | target[p] = objectify(newValue, target[p].__reactive_id); 354 | reactive_update(target[p].__reactive_id); 355 | return true; 356 | }, 357 | })); 358 | 359 | // Creates anonymous object 360 | export function $_(input: T) { 361 | let out = objectify(input); 362 | _d[`__anonymous_${out.__reactive_id}`] = out; 363 | queue.push(_d[`__anonymous_${out.__reactive_id}`]); 364 | return out; 365 | } 366 | class __reactive_scope extends Array { 367 | constructor(x: Array>) { 368 | if (typeof x == "number") { 369 | super(x); 370 | } else { 371 | super(x.length); 372 | for (const [k, v] of x.entries()) { 373 | this[k] = v; 374 | } 375 | } 376 | } 377 | 378 | from_level(level: number) { 379 | const levels = this.slice(level); 380 | let identifier = ""; 381 | levels.forEach((a) => (identifier += `@${new URL(a[1]).pathname}#${a[0]}`)); 382 | return identifier; 383 | } 384 | } 385 | 386 | function get_scope() { 387 | const stack: Array = Error().stack?.split("\n"); 388 | let split = stack.map((a) => { 389 | return /at (.*) \((.*)\)/g.exec(a)?.slice(1); 390 | }); 391 | split = split.filter( 392 | (a) => 393 | a !== null && 394 | a !== undefined && 395 | !a[1].includes("__reactive_renderer") && 396 | a[1] !== "" && 397 | !a[0].includes("__reactive_func") 398 | ); 399 | return new __reactive_scope(split); 400 | } 401 | --------------------------------------------------------------------------------