| 1 | // a pretend media server. it serves requests on its own, and the prompt is |
| 2 | // there to poke at it meanwhile: try `server.`, `server.bs`, |
| 3 | // `await delay(500)`, `/encode 3`, and `/deploy`. |
| 4 | const console = log.scoped("http"); |
| 5 | const server = { |
| 6 | requests: 0, |
| 7 | bytesSent: 0, |
| 8 | visitors: new Map<string, number>(), |
| 9 | startedAt: new Date(), |
| 10 | }; |
| 11 | |
| 12 | new repl.Repl({ |
| 13 | language: repl.javascript({ globals: { server, delay: async.delay } }), |
| 14 | commands: { |
| 15 | // "/encode 3" encodes three videos at once, each a bar under the prompt |
| 16 | async encode(args, node) { |
| 17 | const files = Array.from({ length: Number(args) || 2 }, (_, i) => `video-${i + 1}.mov`); |
| 18 | await Promise.all(files.map((file) => encode(node, file))); |
| 19 | }, |
| 20 | // a blocking command has the terminal to itself until it is done |
| 21 | deploy: { |
| 22 | block: true, |
| 23 | complete: () => ({ from: 0, options: ["production", "staging", "preview"] }), |
| 24 | async execute(target, node) { |
| 25 | for (const step of ["build", "upload", "restart"]) { |
| 26 | using task = node.start(step); |
| 27 | for (const part of ["pages", "styles", "scripts"]) { |
| 28 | using _ = task.start(`${step} ${part}`); |
| 29 | await async.delay(250 + Math.random() * 250); |
| 30 | task.log.info(`${part} ok`); |
| 31 | } |
| 32 | } |
| 33 | node.log.info(`deployed to ${target || "production"}`); |
| 34 | }, |
| 35 | }, |
| 36 | }, |
| 37 | prompt: "$ ", |
| 38 | style: "lines", |
| 39 | title: "media server", |
| 40 | placeholder: "javascript, or / for a command", |
| 41 | history: ".clover/repl-history", |
| 42 | }); |
| 43 | |
| 44 | async function encode(command: progress.Node, file: string) { |
| 45 | const size = (40 + Math.random() * 80) * 1e6; |
| 46 | using node = command.start(file, { total: size, units: "bytes" }); |
| 47 | while (node.value < size) { |
| 48 | await async.delay(40 + Math.random() * 80); |
| 49 | node.value = Math.min(size, node.value + size * Math.random() / 25); |
| 50 | } |
| 51 | command.log.info(`encoded ${file}`); |
| 52 | } |
| 53 | |
| 54 | // requests come in whether or not anyone is typing. `progress.start` is the |
| 55 | // global progress display, which is drawn under the prompt as well. |
| 56 | (async () => { |
| 57 | for (const path of cycle(["/", "/videos/7", "/blog/feed.xml", "/videos/7/stream", "/q+a"])) { |
| 58 | await async.delay(1200 + Math.random() * 1800); |
| 59 | const visitor = `10.0.0.${1 + Math.floor(Math.random() * 4)}`; |
| 60 | server.visitors.set(visitor, (server.visitors.get(visitor) ?? 0) + 1); |
| 61 | server.requests += 1; |
| 62 | server.bytesSent += Math.floor(Math.random() * 2e6); |
| 63 | |
| 64 | const started = performance.now(); |
| 65 | { |
| 66 | using node = progress.start(`GET ${path}`, { total: 12, showTotal: path.endsWith("stream") }); |
| 67 | while (node.value < 11) { |
| 68 | await async.delay(30 + Math.random() * 60); |
| 69 | node.value += 1; |
| 70 | } |
| 71 | } |
| 72 | console.info(`GET ${path} 200 in ${Math.round(performance.now() - started)}ms`); |
| 73 | } |
| 74 | })(); |
| 75 | |
| 76 | function* cycle<T>(items: T[]) { |
| 77 | while (true) yield* items; |
| 78 | } |
| 79 | |
| 80 | import * as async from "../async.ts"; |
| 81 | import * as log from "../log.ts"; |
| 82 | import * as progress from "../progress.ts"; |
| 83 | import * as repl from "../repl.ts"; |