@textjs/app 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame player: the entire browser bundle of a server-rendered console
|
|
3
|
+
* route (`export const runtime = "server"`). No React, no Yoga, no route
|
|
4
|
+
* modules: it applies cell deltas streamed from the server to a `CellBuffer`,
|
|
5
|
+
* logs them with the same `CssConsoleDriver` (and the same ~15fps pacing) as
|
|
6
|
+
* the client renderer, and forwards keyboard, paste, wheel, focus, resize and
|
|
7
|
+
* Back / Forward to the server.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CellBuffer,
|
|
12
|
+
CssConsoleDriver,
|
|
13
|
+
InputEngine,
|
|
14
|
+
ViewportController,
|
|
15
|
+
type ConsoleTarget,
|
|
16
|
+
type InputEvent,
|
|
17
|
+
} from "@textjs/core";
|
|
18
|
+
import { isAppPath, LIVE_PATH, LIVE_SESSION_HEADER, LiveFrameDecoder, type LiveInput, type LiveServerMessage } from "./live-protocol";
|
|
19
|
+
|
|
20
|
+
export interface FramePlayerConfig {
|
|
21
|
+
/** Mount point (`""` or `"/console"`). */
|
|
22
|
+
basePath: string;
|
|
23
|
+
viewport?: { width?: number; height?: number; responsive?: boolean };
|
|
24
|
+
/** Shown in the page's status line. */
|
|
25
|
+
title?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FramePlayerCoreOptions {
|
|
29
|
+
console?: ConsoleTarget;
|
|
30
|
+
/** Push earlier frames above the console top (default: on for the global console). */
|
|
31
|
+
curtain?: boolean;
|
|
32
|
+
/** A prerendered shell already cleared the console and painted (skip the first clear). */
|
|
33
|
+
shell?: boolean;
|
|
34
|
+
/** Minimum ms between frames (default 66; 110 after a frame that cleared). */
|
|
35
|
+
frameInterval?: number;
|
|
36
|
+
/** Paint scheduler (default: timers + `requestAnimationFrame`). `null` paints only on `paint()`. */
|
|
37
|
+
schedule?: ((cb: () => void, delay: number) => void) | null;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
/**
|
|
40
|
+
* When the shell's frame was logged (`performance.now()`): the first server
|
|
41
|
+
* frame then respects the post-clear pacing window the shell's clear opened.
|
|
42
|
+
*/
|
|
43
|
+
shellPaintedAt?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_FRAME_INTERVAL = 66;
|
|
47
|
+
const POST_CLEAR_INTERVAL = 110;
|
|
48
|
+
|
|
49
|
+
/** Decode + paint, independent of the network (tests drive it directly). */
|
|
50
|
+
export class FramePlayerCore {
|
|
51
|
+
readonly buffer = new CellBuffer(0, 0);
|
|
52
|
+
readonly driver: CssConsoleDriver;
|
|
53
|
+
private readonly decoder = new LiveFrameDecoder();
|
|
54
|
+
private readonly interval: number;
|
|
55
|
+
private readonly postClear: number;
|
|
56
|
+
private readonly schedule: ((cb: () => void, delay: number) => void) | null;
|
|
57
|
+
private readonly now: () => number;
|
|
58
|
+
private scheduled = false;
|
|
59
|
+
private lastEmit = -Infinity;
|
|
60
|
+
private lastCleared = false;
|
|
61
|
+
private hidden = false;
|
|
62
|
+
private pendingWhileHidden = false;
|
|
63
|
+
frames = 0;
|
|
64
|
+
bytes = 0;
|
|
65
|
+
|
|
66
|
+
constructor(options: FramePlayerCoreOptions = {}) {
|
|
67
|
+
this.driver = new CssConsoleDriver({ console: options.console, curtain: options.curtain, initialClear: options.shell !== true });
|
|
68
|
+
this.interval = options.frameInterval ?? DEFAULT_FRAME_INTERVAL;
|
|
69
|
+
this.postClear = options.frameInterval ?? POST_CLEAR_INTERVAL;
|
|
70
|
+
this.now = options.now ?? (() => performance.now());
|
|
71
|
+
this.schedule = options.schedule === undefined ? defaultSchedule : options.schedule;
|
|
72
|
+
if (options.shell === true && options.shellPaintedAt !== undefined) {
|
|
73
|
+
this.lastEmit = options.shellPaintedAt;
|
|
74
|
+
this.lastCleared = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Stop / restart painting (`text.pause()`: select and copy freely). */
|
|
79
|
+
paused = false;
|
|
80
|
+
|
|
81
|
+
/** Apply a server frame. Returns `false` when the delta didn't match the buffer (a reconnect will resync). */
|
|
82
|
+
applyFrame(msg: Extract<LiveServerMessage, { t: "f" }>, size = 0): boolean {
|
|
83
|
+
this.bytes += size;
|
|
84
|
+
const ok = this.decoder.apply(this.buffer, msg);
|
|
85
|
+
if (ok) this.requestPaint();
|
|
86
|
+
return ok;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setHidden(hidden: boolean): void {
|
|
90
|
+
if (hidden === this.hidden) return;
|
|
91
|
+
this.hidden = hidden;
|
|
92
|
+
if (!hidden && this.pendingWhileHidden) {
|
|
93
|
+
this.pendingWhileHidden = false;
|
|
94
|
+
this.requestPaint();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
requestPaint(): void {
|
|
99
|
+
if (this.hidden || this.paused) {
|
|
100
|
+
this.pendingWhileHidden = true;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (this.scheduled || this.schedule === null) return;
|
|
104
|
+
this.scheduled = true;
|
|
105
|
+
const wait = this.lastEmit + (this.lastCleared ? this.postClear : this.interval) - this.now();
|
|
106
|
+
this.schedule(() => {
|
|
107
|
+
this.scheduled = false;
|
|
108
|
+
this.paint();
|
|
109
|
+
}, wait > 1 ? wait : 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Emit a frame now if any cell changed (one `console.log`, zero calls otherwise). */
|
|
113
|
+
paint(): boolean {
|
|
114
|
+
if (this.buffer.width === 0 || this.buffer.height === 0) return false;
|
|
115
|
+
const r = this.driver.render(this.buffer);
|
|
116
|
+
if (r.skipped) return false;
|
|
117
|
+
this.lastEmit = this.now();
|
|
118
|
+
this.lastCleared = r.cleared === true;
|
|
119
|
+
this.frames++;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function defaultSchedule(cb: () => void, delay: number): void {
|
|
125
|
+
if (delay > 0 || typeof requestAnimationFrame !== "function") setTimeout(cb, delay);
|
|
126
|
+
else requestAnimationFrame(cb);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface FramePlayer {
|
|
130
|
+
readonly core: FramePlayerCore;
|
|
131
|
+
/** Close the stream and stop forwarding input. */
|
|
132
|
+
dispose(): void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
declare global {
|
|
136
|
+
interface Window {
|
|
137
|
+
/** Set by a prerendered shell once it painted (`1`, or its `performance.now()` timestamp). */
|
|
138
|
+
__TEXTJS_SHELL__?: number;
|
|
139
|
+
__TEXTJS_SHELL_AT__?: number;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readConfig(doc: Document): FramePlayerConfig {
|
|
144
|
+
const el = doc.getElementById("__textjs-live");
|
|
145
|
+
try {
|
|
146
|
+
return { basePath: "", ...(el?.textContent ? (JSON.parse(el.textContent) as Partial<FramePlayerConfig>) : {}) };
|
|
147
|
+
} catch {
|
|
148
|
+
return { basePath: "" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const STUB_GUARD_KEY = "textjs:server-route";
|
|
153
|
+
const LOAD_GUARD_KEY = "textjs:live-load";
|
|
154
|
+
|
|
155
|
+
/** Same-origin app path or `null` (never follow `//host`, `/\\host`, `javascript:` or other origins). */
|
|
156
|
+
export function safeHref(href: unknown, base = ""): string | null {
|
|
157
|
+
if (typeof href !== "string" || !isAppPath(href)) return null;
|
|
158
|
+
return base && href !== base && !href.startsWith(`${base}/`) && !href.startsWith(`${base}?`) ? null : href;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Read Server-Sent Events from a `fetch()` body (the session id travels in a header, never the URL). */
|
|
162
|
+
async function readEvents(body: ReadableStream<Uint8Array>, onData: (data: string) => void): Promise<void> {
|
|
163
|
+
const reader = body.getReader();
|
|
164
|
+
const dec = new TextDecoder();
|
|
165
|
+
let buf = "";
|
|
166
|
+
for (;;) {
|
|
167
|
+
const { value, done } = await reader.read();
|
|
168
|
+
if (done) return;
|
|
169
|
+
buf += dec.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
170
|
+
let i: number;
|
|
171
|
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
172
|
+
const block = buf.slice(0, i);
|
|
173
|
+
buf = buf.slice(i + 2);
|
|
174
|
+
const data = block
|
|
175
|
+
.split("\n")
|
|
176
|
+
.filter((l) => l.startsWith("data:"))
|
|
177
|
+
.map((l) => l.slice(l.startsWith("data: ") ? 6 : 5))
|
|
178
|
+
.join("\n");
|
|
179
|
+
if (data) onData(data);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Boot the player on a server-rendered route's page. */
|
|
185
|
+
export function bootFramePlayer(config: FramePlayerConfig = readConfig(document)): FramePlayer {
|
|
186
|
+
const base = config.basePath;
|
|
187
|
+
const endpoint = `${base}${LIVE_PATH}`;
|
|
188
|
+
try {
|
|
189
|
+
// We reached the server route: the client stub's reload guard has done its job.
|
|
190
|
+
sessionStorage.removeItem(STUB_GUARD_KEY);
|
|
191
|
+
} catch {
|
|
192
|
+
// storage blocked
|
|
193
|
+
}
|
|
194
|
+
const core = new FramePlayerCore({ shell: window.__TEXTJS_SHELL__ !== undefined, shellPaintedAt: window.__TEXTJS_SHELL_AT__ });
|
|
195
|
+
const vp = config.viewport ?? {};
|
|
196
|
+
const viewport = new ViewportController({ width: vp.width ?? 120, height: vp.height ?? 36, responsive: vp.responsive ?? true });
|
|
197
|
+
const engine = new InputEngine();
|
|
198
|
+
const status = document.querySelector<HTMLElement>("[data-textjs-status]");
|
|
199
|
+
const setStatus = (s: string): void => {
|
|
200
|
+
if (status) status.textContent = s;
|
|
201
|
+
document.documentElement.dataset.textjsLive = s;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let sid: string | null = null;
|
|
205
|
+
let stream: AbortController | null = null;
|
|
206
|
+
let retry = 500;
|
|
207
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
208
|
+
let disposed = false;
|
|
209
|
+
let queue: LiveInput[] = [];
|
|
210
|
+
let inFlight = false;
|
|
211
|
+
|
|
212
|
+
// Our position in the browser's history, so Back / Forward map onto the server's entries.
|
|
213
|
+
const stateIndex = (): number => {
|
|
214
|
+
const st = history.state as { textjsIdx?: unknown } | null;
|
|
215
|
+
return typeof st?.textjsIdx === "number" ? st.textjsIdx : 0;
|
|
216
|
+
};
|
|
217
|
+
let index = stateIndex();
|
|
218
|
+
if ((history.state as { textjsIdx?: unknown } | null)?.textjsIdx === undefined) history.replaceState({ ...(history.state ?? {}), textjsIdx: index }, "");
|
|
219
|
+
let expectedPops = 0;
|
|
220
|
+
|
|
221
|
+
const appPath = (): string => {
|
|
222
|
+
const p = location.pathname;
|
|
223
|
+
const stripped = base && (p === base || p.startsWith(`${base}/`)) ? p.slice(base.length) || "/" : p;
|
|
224
|
+
return stripped + location.search;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const flushInput = (): void => {
|
|
228
|
+
if (inFlight || queue.length === 0 || sid === null || disposed) return;
|
|
229
|
+
const batch = queue.splice(0, 64);
|
|
230
|
+
inFlight = true;
|
|
231
|
+
fetch(endpoint, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "content-type": "application/json", [LIVE_SESSION_HEADER]: sid },
|
|
234
|
+
body: JSON.stringify(batch),
|
|
235
|
+
credentials: "same-origin",
|
|
236
|
+
})
|
|
237
|
+
.then((res) => {
|
|
238
|
+
// The session expired (grace period, server restart): start a new one.
|
|
239
|
+
if (res.status === 410) {
|
|
240
|
+
sid = null;
|
|
241
|
+
reconnect();
|
|
242
|
+
} else if (res.status === 429) queue.unshift(...batch);
|
|
243
|
+
})
|
|
244
|
+
.catch(() => undefined)
|
|
245
|
+
.finally(() => {
|
|
246
|
+
inFlight = false;
|
|
247
|
+
if (queue.length > 0) setTimeout(flushInput, 0);
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const send = (event: LiveInput): void => {
|
|
252
|
+
const last = queue[queue.length - 1];
|
|
253
|
+
if (event.type === "wheel" && last?.type === "wheel") {
|
|
254
|
+
last.deltaX += event.deltaX;
|
|
255
|
+
last.deltaY += event.deltaY;
|
|
256
|
+
last.rows += event.rows;
|
|
257
|
+
last.cols += event.cols;
|
|
258
|
+
} else if (event.type === "resize" && last?.type === "resize") queue[queue.length - 1] = event;
|
|
259
|
+
else queue.push(event);
|
|
260
|
+
queueMicrotask(flushInput);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** Load `href` as a document, at most once per URL every few seconds (middleware might keep bouncing the stream). */
|
|
264
|
+
const load = (href: string): void => {
|
|
265
|
+
const target = new URL(href, location.href).href;
|
|
266
|
+
try {
|
|
267
|
+
const last = sessionStorage.getItem(LOAD_GUARD_KEY);
|
|
268
|
+
const at = last ? Number(last.slice(0, last.indexOf(" "))) : 0;
|
|
269
|
+
if (last && last.slice(last.indexOf(" ") + 1) === target && target === location.href && Date.now() - at < 3000) {
|
|
270
|
+
disposed = true;
|
|
271
|
+
stream?.abort();
|
|
272
|
+
setStatus("unavailable");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
sessionStorage.setItem(LOAD_GUARD_KEY, `${Date.now()} ${target}`);
|
|
276
|
+
} catch {
|
|
277
|
+
// storage blocked
|
|
278
|
+
}
|
|
279
|
+
disposed = true;
|
|
280
|
+
stream?.abort();
|
|
281
|
+
location.assign(href);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const onMessage = (data: string): void => {
|
|
285
|
+
let msg: LiveServerMessage;
|
|
286
|
+
try {
|
|
287
|
+
msg = JSON.parse(data) as LiveServerMessage;
|
|
288
|
+
} catch {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
switch (msg.t) {
|
|
292
|
+
case "f":
|
|
293
|
+
if (!core.applyFrame(msg, data.length)) reconnect();
|
|
294
|
+
break;
|
|
295
|
+
case "hello":
|
|
296
|
+
sid = msg.sid;
|
|
297
|
+
retry = 500;
|
|
298
|
+
setStatus("live");
|
|
299
|
+
// The stream URL may be stale after a reconnect; resend what the server can't see.
|
|
300
|
+
send({ type: "resize", cols: viewport.cols, rows: viewport.rows });
|
|
301
|
+
send({ type: "focus", focused: engine.focused && document.visibilityState !== "hidden" });
|
|
302
|
+
break;
|
|
303
|
+
case "url": {
|
|
304
|
+
const href = safeHref(msg.href, base);
|
|
305
|
+
if (href === null || href === location.pathname + location.search + location.hash) break;
|
|
306
|
+
if (msg.replace) history.replaceState({ textjsIdx: index }, "", href);
|
|
307
|
+
else history.pushState({ textjsIdx: ++index }, "", href);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "go":
|
|
311
|
+
if (Number.isInteger(msg.delta) && msg.delta !== 0) {
|
|
312
|
+
expectedPops++;
|
|
313
|
+
history.go(msg.delta);
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
case "title":
|
|
317
|
+
document.title = msg.title;
|
|
318
|
+
break;
|
|
319
|
+
case "load": {
|
|
320
|
+
const href = safeHref(msg.href, base);
|
|
321
|
+
if (href !== null) load(href);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case "error":
|
|
325
|
+
console.error(`▼ Text.js (server): ${msg.message}`);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const connect = (): void => {
|
|
331
|
+
if (disposed) return;
|
|
332
|
+
const q = new URLSearchParams({
|
|
333
|
+
path: appPath(),
|
|
334
|
+
cols: String(viewport.cols),
|
|
335
|
+
rows: String(viewport.rows),
|
|
336
|
+
focused: engine.focused ? "1" : "0",
|
|
337
|
+
});
|
|
338
|
+
setStatus(sid ? "reconnecting" : "connecting");
|
|
339
|
+
const ctrl = new AbortController();
|
|
340
|
+
stream = ctrl;
|
|
341
|
+
const headers: Record<string, string> = { accept: "text/event-stream" };
|
|
342
|
+
if (sid) headers[LIVE_SESSION_HEADER] = sid;
|
|
343
|
+
fetch(`${endpoint}?${q}`, { headers, credentials: "same-origin", cache: "no-store", signal: ctrl.signal })
|
|
344
|
+
.then(async (res) => {
|
|
345
|
+
if (!res.ok || !res.body) throw new Error(`live stream: ${res.status}`);
|
|
346
|
+
await readEvents(res.body, onMessage);
|
|
347
|
+
throw new Error("live stream ended");
|
|
348
|
+
})
|
|
349
|
+
.catch(() => {
|
|
350
|
+
if (stream === ctrl && !disposed) scheduleReconnect();
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const scheduleReconnect = (): void => {
|
|
355
|
+
if (disposed || retryTimer !== null) return;
|
|
356
|
+
setStatus("reconnecting");
|
|
357
|
+
retryTimer = setTimeout(() => {
|
|
358
|
+
retryTimer = null;
|
|
359
|
+
connect();
|
|
360
|
+
}, retry);
|
|
361
|
+
retry = Math.min(retry * 2, 10_000);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/** Drop the stream and resume the session (or open a new one) with a fresh keyframe. */
|
|
365
|
+
const reconnect = (): void => {
|
|
366
|
+
const s = stream;
|
|
367
|
+
stream = null;
|
|
368
|
+
s?.abort();
|
|
369
|
+
scheduleReconnect();
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const offInput = engine.onAny((event: InputEvent) => {
|
|
373
|
+
if (event.type === "focus") send({ type: "focus", focused: event.focused && document.visibilityState !== "hidden" });
|
|
374
|
+
else send(event as LiveInput);
|
|
375
|
+
});
|
|
376
|
+
engine.attach();
|
|
377
|
+
const offViewport = viewport.subscribe((size) => send({ type: "resize", cols: size.cols, rows: size.rows }));
|
|
378
|
+
const onPop = (): void => {
|
|
379
|
+
const next = stateIndex();
|
|
380
|
+
const delta = next - index;
|
|
381
|
+
index = next;
|
|
382
|
+
// The server asked for this move (`router.back()`): it is already there.
|
|
383
|
+
if (expectedPops > 0) {
|
|
384
|
+
expectedPops--;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
send(delta !== 0 ? { type: "nav", href: appPath(), delta } : { type: "nav", href: appPath() });
|
|
388
|
+
};
|
|
389
|
+
const onVisibility = (): void => {
|
|
390
|
+
const hidden = document.visibilityState === "hidden";
|
|
391
|
+
core.setHidden(hidden);
|
|
392
|
+
send({ type: "focus", focused: !hidden && document.hasFocus() });
|
|
393
|
+
};
|
|
394
|
+
window.addEventListener("popstate", onPop);
|
|
395
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
396
|
+
core.setHidden(document.visibilityState === "hidden");
|
|
397
|
+
connect();
|
|
398
|
+
|
|
399
|
+
const player: FramePlayer = {
|
|
400
|
+
core,
|
|
401
|
+
dispose() {
|
|
402
|
+
disposed = true;
|
|
403
|
+
stream?.abort();
|
|
404
|
+
if (retryTimer !== null) clearTimeout(retryTimer);
|
|
405
|
+
offInput();
|
|
406
|
+
engine.detach();
|
|
407
|
+
offViewport();
|
|
408
|
+
viewport.dispose();
|
|
409
|
+
window.removeEventListener("popstate", onPop);
|
|
410
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
const HELP = `▼ Text.js console API (server-rendered route)
|
|
414
|
+
text.resize(cols, rows) pin the console grid
|
|
415
|
+
text.responsive(on) follow the docked DevTools size
|
|
416
|
+
text.pause() / resume() freeze output (select & copy freely)
|
|
417
|
+
text.stats() frames, bytes, grid size
|
|
418
|
+
text.copyText() copy the current frame as plain text
|
|
419
|
+
text.help() this message`;
|
|
420
|
+
const stats = () => ({ cols: core.buffer.width, rows: core.buffer.height, frames: core.frames, bytes: core.bytes, paused: core.paused, runtime: "server" as const });
|
|
421
|
+
(window as unknown as { text?: unknown }).text = {
|
|
422
|
+
stats,
|
|
423
|
+
copyText: () => {
|
|
424
|
+
const text = core.buffer.toPlainString(true).replace(/\n+$/, "");
|
|
425
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) navigator.clipboard.writeText(text).catch(() => undefined);
|
|
426
|
+
return text;
|
|
427
|
+
},
|
|
428
|
+
resize: (cols: number, rows: number) => {
|
|
429
|
+
viewport.setSize(cols, rows);
|
|
430
|
+
return stats();
|
|
431
|
+
},
|
|
432
|
+
responsive: (on = true) => {
|
|
433
|
+
viewport.setResponsive(on);
|
|
434
|
+
return stats();
|
|
435
|
+
},
|
|
436
|
+
pause: () => {
|
|
437
|
+
core.paused = true;
|
|
438
|
+
},
|
|
439
|
+
resume: () => {
|
|
440
|
+
core.paused = false;
|
|
441
|
+
core.requestPaint();
|
|
442
|
+
},
|
|
443
|
+
help: () => {
|
|
444
|
+
console.log(HELP);
|
|
445
|
+
return HELP;
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
return player;
|
|
449
|
+
}
|