mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-23 20:04:05 +02:00
Serve the web client from yaak-web (#582)
This commit is contained in:
@@ -24,9 +24,9 @@ installs the Tauri host exactly as before.
|
||||
|
||||
```
|
||||
tab (index.ts, commands.ts) ──MessagePort──▶ worker.ts ──▶ @yaakapp-internal/web (wasm)
|
||||
│ ◀── model_writes ── crates/yaak-web → yaak-models → SQLite
|
||||
│ ◀── model_writes ── crates/yaak-wasm → yaak-models → SQLite
|
||||
│ └─ pages in IndexedDB
|
||||
└── send.ts ──POST rendered request──▶ yaak-send-proxy (crates-server) ──▶ the internet
|
||||
└── send.ts ──POST rendered request──▶ yaak-web (crates-server) ──▶ the internet
|
||||
◀── NDJSON: events, response, body, cookies ──
|
||||
```
|
||||
|
||||
@@ -35,14 +35,14 @@ tab (index.ts, commands.ts) ──MessagePort──▶ worker.ts ──▶ @yaak
|
||||
| `index.ts` | The `Platform` implementation. |
|
||||
| `commands.ts` | The command table: model commands forward to the worker; the rest is fixed answers and refusals-with-a-reason. |
|
||||
| `connection.ts` | A tab's end of the wire: request/response over a `MessagePort`, event delivery, and the tab's identity (`label`). |
|
||||
| `send.ts` | Sending: the worker renders (`prepare_http_send`), the proxy executes, this file stores what comes back where the desktop stores it. |
|
||||
| `proxy.ts` | The proxy's location and wire shapes, mirrored by hand from `crates-server/yaak-send-proxy/src/wire.rs`. |
|
||||
| `send.ts` | Sending: the worker renders (`prepare_http_send`), the server executes, this file stores what comes back where the desktop stores it. |
|
||||
| `server.ts` | Where the Yaak server is, and the wire shapes it speaks (generated from `crates-server/yaak-web/src/wire.rs`). |
|
||||
| `worker.ts` | The process that owns the database. Loads the wasm, opens the DB once, answers each port, fans `model_writes` out to every port. |
|
||||
| `protocol.ts` | The message shapes both sides import. |
|
||||
| `errors.ts` | `UnsupportedCommandError`, the structured refusal. |
|
||||
| `storage.ts` | `navigator.storage.persist()`. |
|
||||
|
||||
The Rust side is `crates/yaak-web` (`@yaakapp-internal/web`): `boot()`,
|
||||
The Rust side is `crates/yaak-wasm` (`@yaakapp-internal/wasm`): `boot()`,
|
||||
`rpc(cmd, payload, label)` returning `{ result, events }`, blob get/put, and
|
||||
`prepare_http_send(payload)` — the database half of a send (environment chain,
|
||||
inherited headers and auth, request settings, cookie jar, rendering), which is
|
||||
@@ -85,7 +85,7 @@ Behaviours worth knowing before changing anything:
|
||||
| Group | Commands |
|
||||
| --- | --- |
|
||||
| Models | `models_workspace_models`, `models_upsert`, `models_delete`, `models_duplicate`, `models_get_settings`, `models_get_graphql_introspection`, `models_upsert_graphql_introspection`, `models_grpc_events`, `models_websocket_events` |
|
||||
| Sending | `cmd_send_http_request` (through the send proxy; see below) |
|
||||
| Sending | `cmd_send_http_request` (through the Yaak server; see below) |
|
||||
| App | `cmd_metadata`, `cmd_get_workspace_meta`, `cmd_default_headers`, `cmd_get_themes`, `cmd_check_for_updates`, `cmd_dismiss_notification`, `cmd_plugin_init_errors` |
|
||||
| Bodies | `cmd_http_response_body`, `cmd_http_response_body_path`, `cmd_http_request_body`, `cmd_get_http_response_events`, `cmd_get_sse_events` |
|
||||
| Plugin surfaces (empty results) | `cmd_http_request_actions`, `cmd_websocket_request_actions`, `cmd_grpc_request_actions`, `cmd_workspace_actions`, `cmd_folder_actions`, `cmd_template_function_summaries`, `cmd_get_http_authentication_summaries`, `cmd_get_http_authentication_config` |
|
||||
@@ -187,8 +187,8 @@ other's writes for an echo of their own and drop them.
|
||||
|
||||
A page cannot see a response the way a desktop app can — CORS exposes a handful
|
||||
of headers, redirects are followed silently, there is no timeline — so the
|
||||
network half of a send runs on a small stateless proxy,
|
||||
`crates-server/yaak-send-proxy`. This layer stays the only place data lives:
|
||||
network half of a send runs on a small stateless server,
|
||||
`crates-server/yaak-web`. This layer stays the only place data lives:
|
||||
|
||||
1. `send.ts` creates the `http_response` row (state `initialized`), as the
|
||||
desktop does, so anything that goes wrong lands in the response pane.
|
||||
@@ -198,7 +198,7 @@ network half of a send runs on a small stateless proxy,
|
||||
with `yaak_models::render::render_http_request`. Variables (`${[ name ]}`)
|
||||
render here with no plugins involved.
|
||||
3. The rendered request, the settings and the jar's cookies are POSTed to the
|
||||
proxy. It streams back timeline events, the response head, body chunks and a
|
||||
server. It streams back timeline events, the response head, body chunks and a
|
||||
terminal frame carrying the jar as the send left it.
|
||||
4. Each frame is written where the desktop writes it: the response row as it
|
||||
progresses, `http_response_event` rows for the timeline (which is why
|
||||
@@ -210,10 +210,18 @@ authentication is none, or an inline header. Sending a request that needs a
|
||||
template *function* (`${[ timestamp() ]}`) or an authentication plugin (bearer,
|
||||
basic, OAuth, …) is refused before anything leaves the tab, with a message naming
|
||||
what it needs; those light up when plugins run in the browser. Requests with a
|
||||
file body or multipart file fields are refused by the proxy (it has no access to
|
||||
your files, and must not read its own). And a request to `localhost` or a LAN
|
||||
address can't work from a browser: the proxy runs elsewhere and refuses private
|
||||
ranges outright — reaching your own machine's APIs is what the desktop app is for.
|
||||
file body or multipart file fields are refused by the server (it has no access to
|
||||
your files, and must not read its own). And on a public instance a request to
|
||||
`localhost` or a LAN address can't work: the server runs elsewhere and refuses
|
||||
private ranges outright — that is what the desktop app is for. A self-hosted
|
||||
server on your own network can be started with `--allow-private-networks`, which
|
||||
is the one case where those addresses are the user's to reach.
|
||||
|
||||
The proxy URL is `VITE_YAAK_SEND_PROXY_URL` at build time, defaulting to
|
||||
`http://127.0.0.1:9227` (see `proxy.ts`). Run one with `cargo run -p yaak-send-proxy`.
|
||||
**Where the tab sends** (`server.ts`): a production build posts to `/v1/http/send`
|
||||
on its own origin, because the server can serve the app itself
|
||||
(`yaak-web --serve dist/apps/yaak-client`, which is what the
|
||||
`ghcr.io/mountain-loop/yaak-web` image runs) — same origin, so no CORS and
|
||||
nothing to configure. A dev build falls back to `http://127.0.0.1:9227`, since
|
||||
the Vite server is a different origin and serves no `/v1`; run one with
|
||||
`cargo run -p yaak-web`. `VITE_YAAK_WEB_URL` overrides both, for a
|
||||
deployment that keeps the app and the server apart.
|
||||
|
||||
@@ -71,7 +71,7 @@ const HANDLERS: Partial<Record<AppCmd, Handler>> = {
|
||||
|
||||
/* ------------------------------- sending ------------------------------- */
|
||||
|
||||
// The tab renders and stores; a stateless proxy puts the bytes on the wire.
|
||||
// The tab renders and stores; a stateless server puts the bytes on the wire.
|
||||
// See send.ts for the whole shape of it.
|
||||
cmd_send_http_request: (payload, db) => {
|
||||
const requestId = str(payload, "requestId");
|
||||
@@ -249,7 +249,7 @@ const HTTP_AUTHENTICATION_SUMMARIES = [
|
||||
* while the first is a slice away.
|
||||
*/
|
||||
const DECLINED: Partial<Record<AppCmd, [reason: string, capability: CapabilityName | null]>> = {
|
||||
// Saved requests send through the proxy (see send.ts). Ephemeral sends — the
|
||||
// Saved requests send through the server (see send.ts). Ephemeral sends — the
|
||||
// ones nothing stores, used for GraphQL introspection — take the same road but
|
||||
// return the body inline; not wired yet.
|
||||
cmd_send_ephemeral_request: ["Sending unsaved requests isn't available in the browser yet", null],
|
||||
@@ -266,7 +266,7 @@ const DECLINED: Partial<Record<AppCmd, [reason: string, capability: CapabilityNa
|
||||
|
||||
// Anything that needs files the page can't reach.
|
||||
cmd_import_data: ["Importing from a file needs a filesystem, which a browser tab has no", "localFiles"],
|
||||
cmd_import_url: ["Importing from a URL needs the send proxy, which isn't available yet", null],
|
||||
cmd_import_url: ["Importing from a URL needs the Yaak server, which isn't available yet", null],
|
||||
cmd_export_data: ["Exporting to a file isn't available in the browser yet", "localFiles"],
|
||||
cmd_save_response: ["Saving a response to disk isn't available in the browser", "localFiles"],
|
||||
cmd_save_base64_to_binary: ["Saving to disk isn't available in the browser", "localFiles"],
|
||||
|
||||
@@ -163,7 +163,7 @@ export class WorkerConnection {
|
||||
return this.request<T>((id) => ({ type: "rpc", id, cmd, payload, label: this.label }));
|
||||
}
|
||||
|
||||
/** See `prepare_http_send` in crates/yaak-web: the database half of a send. */
|
||||
/** See `prepare_http_send` in crates/yaak-wasm: the database half of a send. */
|
||||
prepareHttpSend<T>(payload: unknown): Promise<T> {
|
||||
return this.request<T>((id) => ({ type: "prepare_http_send", id, payload }));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* the origin, so two tabs stay coherent for the same reason two desktop windows
|
||||
* do: one process holds the data and pushes every write to all of them.
|
||||
*
|
||||
* Sending goes through a small stateless proxy, because a page cannot see a
|
||||
* Sending goes through a small stateless server, because a page cannot see a
|
||||
* response the way a desktop app can (see send.ts). What a page genuinely
|
||||
* cannot do is not faked: there is no file dialog, no second window, no
|
||||
* clipboard read without a prompt. Those report false through `capabilities`
|
||||
@@ -33,7 +33,7 @@ import { requestPersistence } from "./storage";
|
||||
/** What this host can do, reported honestly. */
|
||||
function capabilitiesFor(): PlatformCapabilities {
|
||||
return {
|
||||
// Through the send proxy: the tab renders, the proxy executes, the tab
|
||||
// Through the Yaak server: the tab renders, the server executes, the tab
|
||||
// stores. Requests needing plugin auth or template functions are refused
|
||||
// with the reason until plugins run here.
|
||||
httpSending: true,
|
||||
@@ -42,11 +42,11 @@ function capabilitiesFor(): PlatformCapabilities {
|
||||
git: false,
|
||||
sync: false,
|
||||
// Certificates and proxies are decided by whoever puts the bytes on the
|
||||
// wire, and the send proxy uses its own.
|
||||
// wire, and the Yaak server uses its own.
|
||||
tlsOptions: false,
|
||||
cookieJar: true,
|
||||
localFiles: false,
|
||||
// The proxy streams the engine's events back and the sender stores them.
|
||||
// The server streams the engine's events back and the sender stores them.
|
||||
timeline: true,
|
||||
// Whether the host can put a *second window* on this data on demand — what
|
||||
// `cmd_new_child_window` does for Settings and workspace switching. A tab
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* The wire to the send proxy: where it is, and how to read what comes back.
|
||||
*
|
||||
* The shapes themselves are generated from `crates-server/yaak-send-proxy/src/wire.rs`
|
||||
* into `@yaakapp-internal/send-proxy`, so the two sides cannot drift silently.
|
||||
*/
|
||||
|
||||
import type { Frame } from "@yaakapp-internal/send-proxy";
|
||||
|
||||
/* ------------------------------- location -------------------------------- */
|
||||
|
||||
/**
|
||||
* Where the tab sends. Build-time configuration for now: `VITE_YAAK_SEND_PROXY_URL`
|
||||
* (Vite exposes `VITE_*` to the bundle), defaulting to a proxy on this machine at
|
||||
* its default port. A per-user setting can replace this later without touching
|
||||
* the callers, which only ever ask for the URL.
|
||||
*/
|
||||
export function proxyBaseUrl(): string {
|
||||
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env;
|
||||
const configured = env?.VITE_YAAK_SEND_PROXY_URL?.trim();
|
||||
return (configured || "http://127.0.0.1:9227").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function proxySendUrl(): string {
|
||||
return `${proxyBaseUrl()}/v1/http/send`;
|
||||
}
|
||||
|
||||
let identity: Promise<string> | null = null;
|
||||
|
||||
/**
|
||||
* Who does the sending, for the timeline: `yaak-send-proxy 0.1.0 at http://…`.
|
||||
* Asked of `/v1/health` once per page load; if the proxy can't be reached the
|
||||
* URL alone is the answer, and the send itself will say why shortly after.
|
||||
*/
|
||||
export function proxyIdentity(): Promise<string> {
|
||||
identity ??= fetch(`${proxyBaseUrl()}/v1/health`)
|
||||
.then((res) => res.json() as Promise<{ version?: string }>)
|
||||
.then((health) => `yaak-send-proxy ${health.version ?? ""} at ${proxyBaseUrl()}`.replace(" ", " "))
|
||||
.catch(() => {
|
||||
identity = null; // try again next send
|
||||
return `send proxy at ${proxyBaseUrl()}`;
|
||||
});
|
||||
return identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield frames from an NDJSON stream as they arrive. A partial trailing line is
|
||||
* held until its newline comes; anything left when the stream ends is dropped,
|
||||
* because a frame without its newline is a frame the proxy didn't finish writing.
|
||||
*/
|
||||
export async function* readFrames(stream: ReadableStream<Uint8Array>): AsyncGenerator<Frame> {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let newline = buffer.indexOf("\n");
|
||||
while (newline !== -1) {
|
||||
const line = buffer.slice(0, newline);
|
||||
buffer = buffer.slice(newline + 1);
|
||||
if (line.trim() !== "") yield JSON.parse(line) as Frame;
|
||||
newline = buffer.indexOf("\n");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* A tab can't see a response the way the desktop can — CORS hides most headers,
|
||||
* redirects are followed silently, there is no timeline — so the network half of
|
||||
* a send happens on a small stateless proxy (`crates-server/yaak-send-proxy`).
|
||||
* a send happens on a small stateless server (`crates-server/yaak-web`).
|
||||
* Everything else happens here, against this tab's own database, in the same
|
||||
* order the desktop does it:
|
||||
*
|
||||
@@ -11,13 +11,13 @@
|
||||
* 2. resolve and render the request in the worker (`prepare_http_send`: the
|
||||
* environment chain, inherited headers and auth, request settings, cookie
|
||||
* jar — the desktop's `HttpSendInputs`, in Rust, on the same model layer);
|
||||
* 3. POST the rendered request to the proxy and consume its stream: timeline
|
||||
* 3. POST the rendered request to the server and consume its stream: timeline
|
||||
* events, the response head, body chunks, and a terminal frame;
|
||||
* 4. write what comes back where the desktop writes it — the response row as
|
||||
* it progresses, `http_response_event` rows for the timeline, the body
|
||||
* blob under the response id, the cookie jar with the proxy's changes.
|
||||
* blob under the response id, the cookie jar with the server's changes.
|
||||
*
|
||||
* The proxy keeps nothing. Every byte it sees comes from this tab and every
|
||||
* The server keeps nothing. Every byte it sees comes from this tab and every
|
||||
* byte it returns is stored by this tab.
|
||||
*/
|
||||
|
||||
@@ -31,9 +31,9 @@ import type {
|
||||
HttpResponseEventData,
|
||||
HttpSendSettings,
|
||||
} from "@yaakapp-internal/models";
|
||||
import type { Frame, SendRequest } from "@yaakapp-internal/send-proxy";
|
||||
import type { Frame, SendRequest } from "@yaakapp-internal/web";
|
||||
import type { WorkerConnection } from "./connection";
|
||||
import { proxyIdentity, proxySendUrl, readFrames } from "./proxy";
|
||||
import { serverIdentity, serverSendUrl, readFrames } from "./server";
|
||||
|
||||
/* -------------------------------- shapes --------------------------------- */
|
||||
|
||||
@@ -48,7 +48,7 @@ type ResponseRow = Pick<HttpResponse, "model" | "requestId" | "workspaceId"> &
|
||||
|
||||
type ResponsePatch = Partial<HttpResponse>;
|
||||
|
||||
/** What `prepare_http_send` (crates/yaak-web) hands back. */
|
||||
/** What `prepare_http_send` (crates/yaak-wasm) hands back. */
|
||||
interface PreparedHttpSend {
|
||||
request: HttpRequest;
|
||||
settings: HttpSendSettings;
|
||||
@@ -68,7 +68,7 @@ export async function sendHttpRequest(
|
||||
cookieJarId: string | null,
|
||||
): Promise<ResponseRow> {
|
||||
// The response row exists before anything can go wrong, as on the desktop, so
|
||||
// a failure to render or to reach the proxy lands in the response pane as
|
||||
// a failure to render or to reach the server lands in the response pane as
|
||||
// that response's error rather than as a toast that names no request.
|
||||
const workspaceId = await workspaceIdOfRequest(db, requestId);
|
||||
const response = new ResponseWriter(db, { model: "http_response", requestId, workspaceId });
|
||||
@@ -107,7 +107,7 @@ async function runSend(
|
||||
// request through a proxy shows a different origin to the server than the
|
||||
// user's machine, and this is where that should be visible.
|
||||
const timeline = new TimelineWriter(db, response.id, response.workspaceId);
|
||||
timeline.push([{ type: "info", message: `Executed by ${await proxyIdentity()}` }]);
|
||||
timeline.push([{ type: "info", message: `Executed by ${await serverIdentity()}` }]);
|
||||
timeline.push(prepared.settingEvents);
|
||||
|
||||
const body: SendRequest = {
|
||||
@@ -116,19 +116,19 @@ async function runSend(
|
||||
cookies: prepared.cookieJar?.cookies ?? null,
|
||||
};
|
||||
const startedAt = performance.now();
|
||||
const res = await fetch(proxySendUrl(), {
|
||||
const res = await fetch(serverSendUrl(), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
}).catch((err: unknown) => {
|
||||
if (signal.aborted) throw err;
|
||||
throw new Error(`Couldn't reach the send proxy at ${proxySendUrl()}: ${errorMessage(err)}`);
|
||||
throw new Error(`Couldn't reach the Yaak server at ${serverSendUrl()}: ${errorMessage(err)}`);
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
// A refusal, not a failed send: bad destination, rate limit, a body the
|
||||
// proxy can't build. It comes as JSON with the reason.
|
||||
// server can't build. It comes as JSON with the reason.
|
||||
const text = await res.text();
|
||||
let reason = text;
|
||||
try {
|
||||
@@ -136,9 +136,9 @@ async function runSend(
|
||||
} catch {
|
||||
/* not JSON; the text is the reason */
|
||||
}
|
||||
throw new Error(reason || `The send proxy answered ${res.status}`);
|
||||
throw new Error(reason || `The Yaak server answered ${res.status}`);
|
||||
}
|
||||
if (res.body == null) throw new Error("The send proxy sent no body");
|
||||
if (res.body == null) throw new Error("The Yaak server sent no body");
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let received = 0;
|
||||
@@ -175,12 +175,12 @@ async function runSend(
|
||||
if (terminal != null) break;
|
||||
}
|
||||
|
||||
// Everything the proxy said about the timeline is in the database before the
|
||||
// Everything the server said about the timeline is in the database before the
|
||||
// response is marked closed, so a reader that wakes on "closed" sees all of it.
|
||||
await timeline.flush();
|
||||
|
||||
if (terminal == null) {
|
||||
throw new Error("The send proxy closed the stream without finishing");
|
||||
throw new Error("The Yaak server closed the stream without finishing");
|
||||
}
|
||||
|
||||
// Cookies come back on both outcomes: a hop before the failing one may have
|
||||
@@ -196,7 +196,7 @@ async function runSend(
|
||||
// The body is written under the response id, which is how every reader —
|
||||
// `cmd_http_response_body`, the image viewer, the download button — asks for
|
||||
// it. One write, once the whole body is here: the worker's blob store has no
|
||||
// append, and a body larger than memory is over the proxy's cap anyway.
|
||||
// append, and a body larger than memory is over the server's cap anyway.
|
||||
await db.blobPut(response.id, concat(chunks, received));
|
||||
await response.finish({
|
||||
contentLength: terminal.contentLength,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* The wire to the Yaak server: where it is, and how to read what comes back.
|
||||
*
|
||||
* The shapes themselves are generated from `crates-server/yaak-web/src/wire.rs`
|
||||
* into `@yaakapp-internal/web`, so the two sides cannot drift silently.
|
||||
*/
|
||||
|
||||
import type { Frame } from "@yaakapp-internal/web";
|
||||
|
||||
/* ------------------------------- location -------------------------------- */
|
||||
|
||||
/**
|
||||
* Where the tab sends.
|
||||
*
|
||||
* Empty means "this origin": the server can serve the app itself
|
||||
* (`yaak-web --serve`), and then a send is a request to a path on the
|
||||
* page's own origin — no CORS, and nothing for a self-hoster to configure.
|
||||
*
|
||||
* `VITE_YAAK_WEB_URL` overrides it at build time, for a deployment that
|
||||
* keeps the two apart. The dev server is one of those: it serves the app on its
|
||||
* own origin and knows nothing about `/v1`, so a dev build falls back to a server
|
||||
* running locally (`cargo run -p yaak-web`).
|
||||
*/
|
||||
export function serverBaseUrl(): string {
|
||||
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env;
|
||||
const configured = env?.VITE_YAAK_WEB_URL?.trim();
|
||||
if (configured) return configured.replace(/\/+$/, "");
|
||||
return env?.DEV ? "http://127.0.0.1:9227" : "";
|
||||
}
|
||||
|
||||
export function serverSendUrl(): string {
|
||||
return `${serverBaseUrl()}/v1/http/send`;
|
||||
}
|
||||
|
||||
let identity: Promise<string> | null = null;
|
||||
|
||||
/** The server's location as a person reads it, since "" means "this origin". */
|
||||
function serverLocation(): string {
|
||||
return serverBaseUrl() || globalThis.location?.origin || "this origin";
|
||||
}
|
||||
|
||||
/**
|
||||
* Who does the sending, for the timeline: `yaak-web 0.1.0 at http://…`.
|
||||
* Asked of `/v1/health` once per page load; if the server can't be reached the
|
||||
* URL alone is the answer, and the send itself will say why shortly after.
|
||||
*/
|
||||
export function serverIdentity(): Promise<string> {
|
||||
identity ??= fetch(`${serverBaseUrl()}/v1/health`)
|
||||
.then((res) => res.json() as Promise<{ version?: string }>)
|
||||
.then((health) => `yaak-web ${health.version ?? ""} at ${serverLocation()}`.replace(" ", " "))
|
||||
.catch(() => {
|
||||
identity = null; // try again next send
|
||||
return `Yaak server at ${serverLocation()}`;
|
||||
});
|
||||
return identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield frames from an NDJSON stream as they arrive. A partial trailing line is
|
||||
* held until its newline comes; anything left when the stream ends is dropped,
|
||||
* because a frame without its newline is a frame the server didn't finish writing.
|
||||
*/
|
||||
export async function* readFrames(stream: ReadableStream<Uint8Array>): AsyncGenerator<Frame> {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let newline = buffer.indexOf("\n");
|
||||
while (newline !== -1) {
|
||||
const line = buffer.slice(0, newline);
|
||||
buffer = buffer.slice(newline + 1);
|
||||
if (line.trim() !== "") yield JSON.parse(line) as Frame;
|
||||
newline = buffer.indexOf("\n");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import { DB_LOCK_NAME, type FromWorker, type ToWorker } from "./protocol";
|
||||
* download and compile — and the tab can therefore tell "this worker is dead"
|
||||
* from "this worker is busy" with a short timeout.
|
||||
*/
|
||||
type Engine = typeof import("@yaakapp-internal/web");
|
||||
type Engine = typeof import("@yaakapp-internal/wasm");
|
||||
let engine: Engine | null = null;
|
||||
|
||||
const ports = new Set<MessagePort>();
|
||||
@@ -96,7 +96,7 @@ function bootOnce(): Promise<void> {
|
||||
|
||||
booted = (async () => {
|
||||
await acquireDatabaseLock();
|
||||
const loaded = await import("@yaakapp-internal/web");
|
||||
const loaded = await import("@yaakapp-internal/wasm");
|
||||
await loaded.boot();
|
||||
engine = loaded;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user