@omniaura/scenario-sim 0.1.0
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 +118 -0
- package/dist/browser.d.ts +81 -0
- package/dist/browser.js +303 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-44ZHRYG6.js +959 -0
- package/dist/chunk-44ZHRYG6.js.map +1 -0
- package/dist/chunk-7FWVWBX6.js +380 -0
- package/dist/chunk-7FWVWBX6.js.map +1 -0
- package/dist/chunk-7HBAY3QY.js +133 -0
- package/dist/chunk-7HBAY3QY.js.map +1 -0
- package/dist/chunk-KU4W4SKO.js +1236 -0
- package/dist/chunk-KU4W4SKO.js.map +1 -0
- package/dist/cli.js +131 -0
- package/dist/cli.js.map +1 -0
- package/dist/engine-2w32ngB2.d.ts +897 -0
- package/dist/engine-HE7MEQHD.js +15 -0
- package/dist/engine-HE7MEQHD.js.map +1 -0
- package/dist/examples.d.ts +38 -0
- package/dist/examples.js +233 -0
- package/dist/examples.js.map +1 -0
- package/dist/index.d.ts +322 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/pulse.d.ts +40 -0
- package/dist/pulse.js +80 -0
- package/dist/pulse.js.map +1 -0
- package/dist/scenario--CvzkzX_.d.ts +637 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -0
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +48 -0
- package/dist/vite.js.map +1 -0
- package/package.json +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @omniaura/scenario-sim
|
|
2
|
+
|
|
3
|
+
> A mock backend that behaves like a backend: stateful, seeded, reproducible, streaming — and controllable by humans and agents alike.
|
|
4
|
+
|
|
5
|
+
Most mock layers return fixtures. `scenario-sim` runs **scenarios**: a seeded world with CRUD state, a virtual clock, response sequences and faults, SSE and WebSocket routes whose events come from state mutations, and a control plane to select/reset/step/inspect everything. The core is runtime-neutral (Web `Request`/`Response`) so the same scenario runs as a Bun/Node server, inside the Vite dev server, or entirely inside the browser.
|
|
6
|
+
|
|
7
|
+
## Define a scenario
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineScenario, crud, route, json, problem, sequence, malformed, ws, sse } from "@omniaura/scenario-sim";
|
|
11
|
+
|
|
12
|
+
export const happy = defineScenario({
|
|
13
|
+
name: "notes-happy",
|
|
14
|
+
seed: "notes-happy", // same seed → same ids, same fixtures, same jitter
|
|
15
|
+
clock: { mode: "realtime" }, // or "manual": nothing time-based happens until stepped
|
|
16
|
+
faults: { latencyMs: 0 },
|
|
17
|
+
setup({ state, rng, clock, streams }) {
|
|
18
|
+
for (let i = 0; i < 8; i++) state.insert("notes", { id: rng.id("note"), title: rng.pick(WORDS), done: false, createdAt: clock.iso() });
|
|
19
|
+
// Mutations → stream events. Routes mutate state; streams subscribe to it.
|
|
20
|
+
state.on((e) => e.collection === "notes" && streams.publish("notes", { type: `notes.${e.kind}`, id: e.id, note: e.record ?? null }));
|
|
21
|
+
},
|
|
22
|
+
routes: [
|
|
23
|
+
...crud("/api/notes", "notes", { create: (body, ctx) => ({ id: ctx.rng.id("note"), title: String(body.title), done: false, createdAt: ctx.clock.iso() }) }),
|
|
24
|
+
route.get("/api/flaky", (ctx) => (ctx.calls % 3 === 0 ? problem(503, "flaky upstream") : json({ ok: true }))),
|
|
25
|
+
route.get("/api/drift", sequence([() => malformed("invalid-json"), () => malformed("schema-drift"), () => json({ ok: true })])),
|
|
26
|
+
],
|
|
27
|
+
streams: [
|
|
28
|
+
sse("/api/notes/events", (ctx, stream) => stream.subscribe("notes")), // resumes from Last-Event-ID
|
|
29
|
+
ws("/api/chat/ws", {
|
|
30
|
+
protocols: ["chat-v1"],
|
|
31
|
+
onOpen: (ctx, socket) => socket.send({ type: "ready" }),
|
|
32
|
+
onMessage(ctx, socket, raw) {
|
|
33
|
+
const frame = JSON.parse(String(raw));
|
|
34
|
+
if (frame.type === "subscribe") {
|
|
35
|
+
const r = socket.subscribe(frame.topic, { since: frame.sinceEventID }); // replay > sinceEventID, then live
|
|
36
|
+
socket.send({ type: "subscribed", replayed: r.replayed, last: r.last });
|
|
37
|
+
if (r.missed) socket.send({ type: "resume.miss" });
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
actions: { burst: ({ state, rng, args }) => { /* insert N notes */ } }, // POST /__sim/action {name:"burst"}
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Everything in a scenario is deterministic given the seed: `rng.id()`, `rng.pick()`, jitter, virtual timestamps (`clock.iso()` starts at 2026-01-01T00:00:00Z). Streams carry a per-topic monotonic `eventID` with a bounded replay log; `subscribe(topic, { since })` replays what a reconnecting client missed and reports `missed` when the resume point fell out of the window (so you can send the protocol's "resync" frame).
|
|
47
|
+
|
|
48
|
+
`crud(path, collection, …)` gives you `GET /path`, `POST /path` → 201, `GET/PATCH/PUT/DELETE /path/:id` with 404/422 handling and store events for every mutation.
|
|
49
|
+
|
|
50
|
+
## Run it
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// as a server
|
|
54
|
+
import { Simulator } from "@omniaura/scenario-sim";
|
|
55
|
+
import { serveSimulator } from "@omniaura/scenario-sim/server";
|
|
56
|
+
const sim = new Simulator({ scenarios: [happy, slow, flaky] });
|
|
57
|
+
await serveSimulator(sim, { port: 4100 }); // http://127.0.0.1:4100/__sim/status
|
|
58
|
+
|
|
59
|
+
// inside Vite (same origin as the app, WebSocket upgrades included)
|
|
60
|
+
import scenarioSim from "@omniaura/scenario-sim/vite";
|
|
61
|
+
plugins: [scenarioSim({ scenarios: () => import("./scenarios"), match: (p) => p.startsWith("/api/") })]
|
|
62
|
+
|
|
63
|
+
// entirely in the browser (static builds, demo islands, Playwright against `vite preview`)
|
|
64
|
+
import { installBrowserSimulator } from "@omniaura/scenario-sim/browser";
|
|
65
|
+
const { sim } = installBrowserSimulator({ scenarios, run: "tab-1" }); // patches fetch/WebSocket/EventSource for matching URLs
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
scenario-sim serve ./scenarios.ts --port 4100 --scenario notes-happy
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Control plane (HTTP, CLI, JS, pulse panel — the same operations)
|
|
73
|
+
|
|
74
|
+
| Operation | HTTP | CLI | pulse command |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| list scenarios | `GET /__sim/scenarios` | `scenario-sim scenarios` | `scenario.list` |
|
|
77
|
+
| status (scenario, seed, clock, state counts, streams, faults, routes) | `GET /__sim/status?run=` | `status` | `scenario.status` |
|
|
78
|
+
| select scenario (fresh state) | `POST /__sim/select {scenario, seed?, run?}` | `select name= seed=` | `scenario.select` |
|
|
79
|
+
| reset (rebuild from seed) | `POST /__sim/reset {run?, seed?}` | `reset` | `scenario.reset` |
|
|
80
|
+
| step the virtual clock | `POST /__sim/step {ms}` | `step ms=1000` | `scenario.step` |
|
|
81
|
+
| clock mode/speed | `POST /__sim/clock {mode, speed}` | `clock mode=manual` | `scenario.clock` |
|
|
82
|
+
| inspect state | `GET /__sim/state?collection=` | `state collection=notes` | `scenario.state` |
|
|
83
|
+
| mutation log | `GET /__sim/events?since=` | `events` | `scenario.events` |
|
|
84
|
+
| run log | `GET /__sim/log` | `log` | `scenario.log` |
|
|
85
|
+
| open streams + topics | `GET /__sim/streams` | `streams` | `scenario.streams` |
|
|
86
|
+
| disconnect / hard-drop streams | `POST /__sim/streams/disconnect {id\|topic\|path\|all, drop}` | `disconnect all=true drop=true` | `scenario.disconnect` |
|
|
87
|
+
| pause / resume delivery | `POST /__sim/streams/pause {id, paused}` | `pause id=ws_1` | — |
|
|
88
|
+
| publish an event by hand | `POST /__sim/publish {topic, data}` | `publish topic= data=` | `scenario.publish` |
|
|
89
|
+
| overrides (force any endpoint) | `GET/POST/DELETE /__sim/overrides` | `override matcher= status= times=` | `scenario.override`, `.override.clear`, `.overrides` |
|
|
90
|
+
| faults | `POST /__sim/faults {latencyMs, jitterMs, failMode, streamLatencyMs}` | `faults latencyMs=800` | `scenario.faults` |
|
|
91
|
+
| scenario action | `POST /__sim/action {name, args}` | `action name=burst args='{"count":5}'` | `scenario.action` |
|
|
92
|
+
| runs (isolation) | `GET /__sim/runs`, `DELETE /__sim/runs?run=` | `runs` | `scenario.runs` |
|
|
93
|
+
|
|
94
|
+
**Runs** isolate state: pick one per request with `X-Sim-Run` (or the `sim_run` cookie / `?__run=`). Each run has its own store, clock, RNG, streams and faults, so two tabs or two agents never see each other's mutations. A scenario for a *new* run comes from `X-Sim-Scenario` / cookie / `?scenario=` / the default.
|
|
95
|
+
|
|
96
|
+
**Overrides** win over routing and even over unrouted paths: `{matcher: "/api/notes", method: "GET", status: 503, times: 1}` fails the next list once, then the endpoint recovers. `malformed: "invalid-json" | "wrong-content-type" | "truncated" | "empty-200" | "html-500" | "schema-drift"` exercises client validation paths.
|
|
97
|
+
|
|
98
|
+
**Fail modes**: `off`, `data` (5xx everything except `shellPaths`, so the app shell still boots), `all`. Per request, `X-Sim-Latency: <ms>` and `X-Sim-Fail: off|data|all` headers layer on top of the run's faults (the same contract as header-driven console mocks, so a tab or a curl can opt into its own faults).
|
|
99
|
+
|
|
100
|
+
Attach to solid-pulse so the panel's Scenarios tab and `solid-pulse scenario.*` drive it:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { attachScenarioCommands } from "@omniaura/scenario-sim/pulse";
|
|
104
|
+
attachScenarioCommands(pulse, { kind: "remote", controlUrl: "/__sim" }); // Vite/server
|
|
105
|
+
attachScenarioCommands(pulse, { kind: "local", sim, run: "tab-1" }); // in-browser
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Shipped example world
|
|
109
|
+
|
|
110
|
+
`@omniaura/scenario-sim/examples` exports a generic notes + chat world (no product data): `notes-happy`, `notes-empty`, `notes-slow`, `notes-flaky`, `notes-malformed`, `chat-stream-drop` (the socket is hard-dropped halfway through every reply; clients must reconnect with `sinceEventID`), `notes-manual-clock` (deterministic streaming for tests). Its WebSocket protocol (`chat-v1`: subscribe/send → ready/subscribed/message/stream.start/chat.content/stream.done/resume.miss) is shaped like real chat backends so reconnect logic gets a workout. The package tests are the reference for CRUD → stream events, resume, drop, isolation and manual-clock determinism.
|
|
111
|
+
|
|
112
|
+
## Notes on transports
|
|
113
|
+
|
|
114
|
+
- SSE is served as a `Response` with a `ReadableStream`, so it works on every adapter; `Last-Event-ID` (or `?lastEventId=` / `?sinceEventID=`) resumes. Both `GET` and `POST`-opened SSE routes are supported (`method: "POST"`).
|
|
115
|
+
- WebSocket upgrades are performed by the adapter (`ws` on Node/Bun, a fake `WebSocket` class in the browser); scenario code only sees `SimSocket`. Browsers drop `Upgrade`/`Sec-WebSocket-Protocol` from `Request` headers, so adapters also send `x-sim-upgrade` / `x-sim-websocket-protocol`, which the engine honours.
|
|
116
|
+
- `drop()` cuts a connection without a close frame (the client sees 1006), unlike `close()`. Stream delivery latency is separate from HTTP latency (`streamLatencyMs`).
|
|
117
|
+
|
|
118
|
+
MIT © omniaura
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { SimulatorOptions, Simulator } from './index.js';
|
|
2
|
+
import './scenario--CvzkzX_.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @omniaura/scenario-sim/browser — run the simulator *inside the page*: no
|
|
6
|
+
* server, no CORS, works in a static build (demo islands, Playwright against
|
|
7
|
+
* `vite preview`). Patches `fetch`, `WebSocket` and `EventSource` so calls
|
|
8
|
+
* matching `match` are answered by the Simulator; everything else passes
|
|
9
|
+
* through. Install this BEFORE @omniaura/solid-pulse so pulse still sees the
|
|
10
|
+
* app's calls.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface BrowserSimulatorOptions extends SimulatorOptions {
|
|
14
|
+
/** Which URLs the simulator answers (default: same-origin `/api/` + control path). */
|
|
15
|
+
match?: (url: URL) => boolean;
|
|
16
|
+
/** Base used to resolve relative URLs (default location.origin). */
|
|
17
|
+
origin?: string;
|
|
18
|
+
/** Fixed run id for this page (default "default"; use per-tab ids for isolation). */
|
|
19
|
+
run?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A WebSocket the page can use that terminates inside the Simulator. */
|
|
22
|
+
declare class SimWebSocket extends EventTarget {
|
|
23
|
+
private sim;
|
|
24
|
+
private runId;
|
|
25
|
+
static readonly CONNECTING = 0;
|
|
26
|
+
static readonly OPEN = 1;
|
|
27
|
+
static readonly CLOSING = 2;
|
|
28
|
+
static readonly CLOSED = 3;
|
|
29
|
+
readonly CONNECTING = 0;
|
|
30
|
+
readonly OPEN = 1;
|
|
31
|
+
readonly CLOSING = 2;
|
|
32
|
+
readonly CLOSED = 3;
|
|
33
|
+
readyState: number;
|
|
34
|
+
protocol: string;
|
|
35
|
+
extensions: string;
|
|
36
|
+
binaryType: BinaryType;
|
|
37
|
+
bufferedAmount: number;
|
|
38
|
+
readonly url: string;
|
|
39
|
+
onopen: ((ev: Event) => unknown) | null;
|
|
40
|
+
onmessage: ((ev: MessageEvent) => unknown) | null;
|
|
41
|
+
onclose: ((ev: CloseEvent) => unknown) | null;
|
|
42
|
+
onerror: ((ev: Event) => unknown) | null;
|
|
43
|
+
private inbound;
|
|
44
|
+
private clientClose;
|
|
45
|
+
constructor(url: string | URL, protocols: string | string[] | undefined, sim: Simulator, runId: string);
|
|
46
|
+
private connect;
|
|
47
|
+
private dispatch;
|
|
48
|
+
private finish;
|
|
49
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
50
|
+
close(code?: number, reason?: string): void;
|
|
51
|
+
}
|
|
52
|
+
/** EventSource over the simulator's SSE routes (same semantics: GET, Last-Event-ID on reconnect). */
|
|
53
|
+
declare class SimEventSource extends EventTarget {
|
|
54
|
+
private sim;
|
|
55
|
+
private runId;
|
|
56
|
+
static readonly CONNECTING = 0;
|
|
57
|
+
static readonly OPEN = 1;
|
|
58
|
+
static readonly CLOSED = 2;
|
|
59
|
+
readyState: number;
|
|
60
|
+
readonly url: string;
|
|
61
|
+
readonly withCredentials: boolean;
|
|
62
|
+
onopen: ((ev: Event) => unknown) | null;
|
|
63
|
+
onmessage: ((ev: MessageEvent) => unknown) | null;
|
|
64
|
+
onerror: ((ev: Event) => unknown) | null;
|
|
65
|
+
private lastEventId;
|
|
66
|
+
private reader;
|
|
67
|
+
private retryMs;
|
|
68
|
+
constructor(url: string | URL, init: EventSourceInit | undefined, sim: Simulator, runId: string);
|
|
69
|
+
private dispatch;
|
|
70
|
+
private open;
|
|
71
|
+
private frame;
|
|
72
|
+
close(): void;
|
|
73
|
+
}
|
|
74
|
+
declare function installBrowserSimulator(options: BrowserSimulatorOptions): {
|
|
75
|
+
sim: Simulator;
|
|
76
|
+
runId: string;
|
|
77
|
+
controlUrl: string;
|
|
78
|
+
restore(): void;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { type BrowserSimulatorOptions, SimEventSource, SimWebSocket, installBrowserSimulator };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Simulator,
|
|
3
|
+
isUpgraded,
|
|
4
|
+
upgradedResponse
|
|
5
|
+
} from "./chunk-7FWVWBX6.js";
|
|
6
|
+
import "./chunk-44ZHRYG6.js";
|
|
7
|
+
|
|
8
|
+
// src/browser.ts
|
|
9
|
+
function frameType(data) {
|
|
10
|
+
if (typeof data !== "string" || data.length > 65536 || data[0] !== "{") return null;
|
|
11
|
+
const m = /"type"\s*:\s*"([^"]{1,80})"/.exec(data);
|
|
12
|
+
return m ? m[1] : null;
|
|
13
|
+
}
|
|
14
|
+
function makeCloseEvent(code, reason, wasClean) {
|
|
15
|
+
let ev;
|
|
16
|
+
try {
|
|
17
|
+
ev = new CloseEvent("close", { code, reason, wasClean });
|
|
18
|
+
} catch {
|
|
19
|
+
ev = new Event("close");
|
|
20
|
+
}
|
|
21
|
+
if (ev.code !== code) {
|
|
22
|
+
Object.defineProperties(ev, {
|
|
23
|
+
code: { value: code, enumerable: true },
|
|
24
|
+
reason: { value: reason, enumerable: true },
|
|
25
|
+
wasClean: { value: wasClean, enumerable: true }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return ev;
|
|
29
|
+
}
|
|
30
|
+
var SimWebSocket = class extends EventTarget {
|
|
31
|
+
constructor(url, protocols, sim, runId) {
|
|
32
|
+
super();
|
|
33
|
+
this.sim = sim;
|
|
34
|
+
this.runId = runId;
|
|
35
|
+
this.url = String(url);
|
|
36
|
+
const requested = protocols ? [].concat(protocols) : [];
|
|
37
|
+
queueMicrotask(() => void this.connect(requested));
|
|
38
|
+
}
|
|
39
|
+
sim;
|
|
40
|
+
runId;
|
|
41
|
+
static CONNECTING = 0;
|
|
42
|
+
static OPEN = 1;
|
|
43
|
+
static CLOSING = 2;
|
|
44
|
+
static CLOSED = 3;
|
|
45
|
+
CONNECTING = 0;
|
|
46
|
+
OPEN = 1;
|
|
47
|
+
CLOSING = 2;
|
|
48
|
+
CLOSED = 3;
|
|
49
|
+
readyState = 0;
|
|
50
|
+
protocol = "";
|
|
51
|
+
extensions = "";
|
|
52
|
+
binaryType = "blob";
|
|
53
|
+
bufferedAmount = 0;
|
|
54
|
+
url;
|
|
55
|
+
onopen = null;
|
|
56
|
+
onmessage = null;
|
|
57
|
+
onclose = null;
|
|
58
|
+
onerror = null;
|
|
59
|
+
inbound = null;
|
|
60
|
+
clientClose = null;
|
|
61
|
+
async connect(requested) {
|
|
62
|
+
const httpUrl = this.url.replace(/^ws/, "http");
|
|
63
|
+
const headers = { "x-sim-upgrade": "websocket", "x-sim-run": this.runId };
|
|
64
|
+
if (requested.length) headers["x-sim-websocket-protocol"] = requested.join(", ");
|
|
65
|
+
const request = new Request(httpUrl, { headers });
|
|
66
|
+
const upgrade = (route, ctx, run, protocol) => {
|
|
67
|
+
const transport = {
|
|
68
|
+
send: (data) => {
|
|
69
|
+
if (this.readyState !== 1) return;
|
|
70
|
+
this.dispatch(new MessageEvent("message", { data }));
|
|
71
|
+
},
|
|
72
|
+
close: (code = 1e3, reason = "") => this.finish(code, reason, true),
|
|
73
|
+
drop: () => this.finish(1006, "", false)
|
|
74
|
+
};
|
|
75
|
+
this.protocol = protocol ?? "";
|
|
76
|
+
this.readyState = 1;
|
|
77
|
+
const sock = run.streams.openSocket(route, ctx, transport, protocol);
|
|
78
|
+
this.inbound = (data) => run.streams.receive(route, ctx, sock, data);
|
|
79
|
+
this.clientClose = (code, reason) => run.streams.clientClosed(route, ctx, sock, code, reason);
|
|
80
|
+
this.dispatch(new Event("open"));
|
|
81
|
+
return upgradedResponse();
|
|
82
|
+
};
|
|
83
|
+
const response = await this.sim.handle(request, { upgrade });
|
|
84
|
+
if (!isUpgraded(response)) {
|
|
85
|
+
this.dispatch(new Event("error"));
|
|
86
|
+
this.finish(1006, `upgrade failed: ${response.status}`, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
dispatch(ev) {
|
|
90
|
+
const handler = this[`on${ev.type}`];
|
|
91
|
+
handler?.call(this, ev);
|
|
92
|
+
this.dispatchEvent(ev);
|
|
93
|
+
}
|
|
94
|
+
finish(code, reason, wasClean) {
|
|
95
|
+
if (this.readyState === 3) return;
|
|
96
|
+
this.readyState = 3;
|
|
97
|
+
this.dispatch(makeCloseEvent(code, reason, wasClean));
|
|
98
|
+
}
|
|
99
|
+
send(data) {
|
|
100
|
+
if (this.readyState !== 1) throw new DOMException("WebSocket is not open", "InvalidStateError");
|
|
101
|
+
if (typeof data === "string") this.inbound?.(data);
|
|
102
|
+
else if (data instanceof ArrayBuffer) this.inbound?.(data);
|
|
103
|
+
else if (ArrayBuffer.isView(data)) this.inbound?.(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
104
|
+
else void data.arrayBuffer().then((b) => this.inbound?.(b));
|
|
105
|
+
}
|
|
106
|
+
close(code = 1e3, reason = "") {
|
|
107
|
+
if (this.readyState >= 2) return;
|
|
108
|
+
this.readyState = 2;
|
|
109
|
+
this.clientClose?.(code, reason);
|
|
110
|
+
this.finish(code, reason, true);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var SimEventSource = class extends EventTarget {
|
|
114
|
+
constructor(url, init, sim, runId) {
|
|
115
|
+
super();
|
|
116
|
+
this.sim = sim;
|
|
117
|
+
this.runId = runId;
|
|
118
|
+
this.url = String(url);
|
|
119
|
+
this.withCredentials = init?.withCredentials ?? false;
|
|
120
|
+
void this.open();
|
|
121
|
+
}
|
|
122
|
+
sim;
|
|
123
|
+
runId;
|
|
124
|
+
static CONNECTING = 0;
|
|
125
|
+
static OPEN = 1;
|
|
126
|
+
static CLOSED = 2;
|
|
127
|
+
readyState = 0;
|
|
128
|
+
url;
|
|
129
|
+
withCredentials;
|
|
130
|
+
onopen = null;
|
|
131
|
+
onmessage = null;
|
|
132
|
+
onerror = null;
|
|
133
|
+
lastEventId = "";
|
|
134
|
+
reader = null;
|
|
135
|
+
retryMs = 1e3;
|
|
136
|
+
dispatch(ev) {
|
|
137
|
+
const handler = this[`on${ev.type}`];
|
|
138
|
+
handler?.call(this, ev);
|
|
139
|
+
this.dispatchEvent(ev);
|
|
140
|
+
}
|
|
141
|
+
async open() {
|
|
142
|
+
if (this.readyState === 2) return;
|
|
143
|
+
const headers = { accept: "text/event-stream", "x-sim-run": this.runId };
|
|
144
|
+
if (this.lastEventId) headers["last-event-id"] = this.lastEventId;
|
|
145
|
+
const response = await this.sim.handle(new Request(this.url, { headers }));
|
|
146
|
+
if (this.readyState === 2) return;
|
|
147
|
+
if (!response.ok || !response.body) {
|
|
148
|
+
this.dispatch(new Event("error"));
|
|
149
|
+
this.readyState = 2;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.readyState = 1;
|
|
153
|
+
this.dispatch(new Event("open"));
|
|
154
|
+
this.reader = response.body.getReader();
|
|
155
|
+
const dec = new TextDecoder();
|
|
156
|
+
let buf = "";
|
|
157
|
+
try {
|
|
158
|
+
for (; ; ) {
|
|
159
|
+
const { value, done } = await this.reader.read();
|
|
160
|
+
if (done) break;
|
|
161
|
+
buf += dec.decode(value, { stream: true });
|
|
162
|
+
let idx;
|
|
163
|
+
while ((idx = buf.search(/\r?\n\r?\n/)) >= 0) {
|
|
164
|
+
const frame = buf.slice(0, idx);
|
|
165
|
+
buf = buf.slice(idx).replace(/^\r?\n\r?\n/, "");
|
|
166
|
+
this.frame(frame);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
if (this.readyState === 2) return;
|
|
172
|
+
this.readyState = 0;
|
|
173
|
+
this.dispatch(new Event("error"));
|
|
174
|
+
setTimeout(() => void this.open(), this.retryMs);
|
|
175
|
+
}
|
|
176
|
+
frame(frame) {
|
|
177
|
+
let event = "message";
|
|
178
|
+
const data = [];
|
|
179
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
180
|
+
if (!line || line.startsWith(":")) continue;
|
|
181
|
+
const i = line.indexOf(":");
|
|
182
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
183
|
+
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
184
|
+
if (field === "event") event = value;
|
|
185
|
+
else if (field === "data") data.push(value);
|
|
186
|
+
else if (field === "id") this.lastEventId = value;
|
|
187
|
+
else if (field === "retry" && /^\d+$/.test(value)) this.retryMs = Number(value);
|
|
188
|
+
}
|
|
189
|
+
if (data.length === 0) return;
|
|
190
|
+
this.dispatch(new MessageEvent(event, { data: data.join("\n"), lastEventId: this.lastEventId }));
|
|
191
|
+
}
|
|
192
|
+
close() {
|
|
193
|
+
this.readyState = 2;
|
|
194
|
+
void this.reader?.cancel().catch(() => {
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
function pulseReporter(alreadyWrapped) {
|
|
199
|
+
const pulse = () => globalThis.__SOLID_PULSE__;
|
|
200
|
+
const on = () => alreadyWrapped && pulse()?.controller.isOn("network") === true;
|
|
201
|
+
let nextId = 1e5;
|
|
202
|
+
return {
|
|
203
|
+
id: () => nextId++,
|
|
204
|
+
emit(kind, data) {
|
|
205
|
+
if (on()) pulse().bus.emit(kind, { ...data, via: "scenario-sim" });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function installBrowserSimulator(options) {
|
|
210
|
+
const sim = new Simulator(options);
|
|
211
|
+
const origin = options.origin ?? location.origin;
|
|
212
|
+
const controlPath = sim.controlPath;
|
|
213
|
+
const runId = options.run ?? sim.defaultRun;
|
|
214
|
+
const match = options.match ?? ((u) => u.origin === origin && (u.pathname.startsWith("/api/") || u.pathname === controlPath || u.pathname.startsWith(`${controlPath}/`)));
|
|
215
|
+
const g = globalThis;
|
|
216
|
+
const nativeFetch = g.fetch;
|
|
217
|
+
const NativeWebSocket = g.WebSocket;
|
|
218
|
+
const NativeEventSource = g.EventSource;
|
|
219
|
+
const report = pulseReporter(NativeWebSocket.__solidPulse === true || nativeFetch.__solidPulse === true);
|
|
220
|
+
g.fetch = function simFetch(input, init) {
|
|
221
|
+
const src = typeof Request !== "undefined" && input instanceof Request ? input : null;
|
|
222
|
+
const raw = src ? src.url : input instanceof URL ? input.href : String(input);
|
|
223
|
+
let url;
|
|
224
|
+
try {
|
|
225
|
+
url = new URL(raw, origin);
|
|
226
|
+
} catch {
|
|
227
|
+
return nativeFetch.call(this, input, init);
|
|
228
|
+
}
|
|
229
|
+
if (!match(url)) return nativeFetch.call(this, input, init);
|
|
230
|
+
const method = (init?.method ?? src?.method ?? "GET").toUpperCase();
|
|
231
|
+
const headers = new Headers(init?.headers ?? src?.headers ?? void 0);
|
|
232
|
+
if (!headers.has("x-sim-run")) headers.set("x-sim-run", runId);
|
|
233
|
+
const bodyless = method === "GET" || method === "HEAD";
|
|
234
|
+
const bodyP = bodyless ? Promise.resolve(void 0) : init?.body !== void 0 ? Promise.resolve(init.body) : src ? src.clone().text() : Promise.resolve(void 0);
|
|
235
|
+
const id = report.id();
|
|
236
|
+
const started = performance.now();
|
|
237
|
+
report.emit("net.fetch.start", { id, method, url: url.pathname + url.search });
|
|
238
|
+
return bodyP.then((body) => sim.handle(new Request(url, { method, headers, body: body ?? void 0, signal: init?.signal ?? src?.signal ?? void 0 }))).then((res) => {
|
|
239
|
+
report.emit("net.fetch.end", { id, method, url: url.pathname + url.search, status: res.status, ok: res.ok, ms: Math.round((performance.now() - started) * 100) / 100, contentType: res.headers.get("content-type") ?? "", sse: (res.headers.get("content-type") ?? "").includes("text/event-stream") });
|
|
240
|
+
return res;
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
g.WebSocket = class extends SimWebSocket {
|
|
244
|
+
constructor(url, protocols) {
|
|
245
|
+
const u = new URL(String(url), origin.replace(/^http/, "ws"));
|
|
246
|
+
if (!match(new URL(u.href.replace(/^ws/, "http")))) {
|
|
247
|
+
return new NativeWebSocket(url, protocols);
|
|
248
|
+
}
|
|
249
|
+
super(u.href, protocols, sim, runId);
|
|
250
|
+
const id = report.id();
|
|
251
|
+
const safeUrl = u.pathname + u.search.replace(/([?&](?:ticket|token)=)[^&]*/gi, "$1[redacted]");
|
|
252
|
+
const openedAt = performance.now();
|
|
253
|
+
let inbound = 0;
|
|
254
|
+
let outbound = 0;
|
|
255
|
+
report.emit("net.ws.open", { id, url: safeUrl, protocols: protocols ? [].concat(protocols) : [], state: "connecting" });
|
|
256
|
+
this.addEventListener("open", () => report.emit("net.ws.open", { id, url: safeUrl, protocol: this.protocol, state: "open", ms: Math.round(performance.now() - openedAt) }));
|
|
257
|
+
this.addEventListener("message", (ev) => {
|
|
258
|
+
inbound++;
|
|
259
|
+
const data = ev.data;
|
|
260
|
+
report.emit("net.ws.message", { id, url: safeUrl, dir: "in", n: inbound, bytes: typeof data === "string" ? data.length : null, type: frameType(data) });
|
|
261
|
+
});
|
|
262
|
+
this.addEventListener("close", (ev) => {
|
|
263
|
+
const e = ev;
|
|
264
|
+
report.emit("net.ws.close", { id, url: safeUrl, code: e.code, reason: e.reason, wasClean: e.wasClean, inbound, outbound, ms: Math.round(performance.now() - openedAt) });
|
|
265
|
+
});
|
|
266
|
+
const origSend = this.send.bind(this);
|
|
267
|
+
this.send = (data) => {
|
|
268
|
+
outbound++;
|
|
269
|
+
report.emit("net.ws.message", { id, url: safeUrl, dir: "out", n: outbound, bytes: typeof data === "string" ? data.length : null, type: frameType(data) });
|
|
270
|
+
return origSend(data);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const ES = NativeEventSource;
|
|
275
|
+
g.EventSource = class extends SimEventSource {
|
|
276
|
+
constructor(url, init) {
|
|
277
|
+
const u = new URL(String(url), origin);
|
|
278
|
+
if (!match(u)) {
|
|
279
|
+
if (!ES) throw new Error(`EventSource is unavailable in this environment and ${u.href} is not simulated`);
|
|
280
|
+
return new ES(url, init);
|
|
281
|
+
}
|
|
282
|
+
super(u.href, init, sim, runId);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
sim,
|
|
287
|
+
runId,
|
|
288
|
+
controlUrl: `${origin}${controlPath}`,
|
|
289
|
+
restore() {
|
|
290
|
+
g.fetch = nativeFetch;
|
|
291
|
+
g.WebSocket = NativeWebSocket;
|
|
292
|
+
if (NativeEventSource) g.EventSource = NativeEventSource;
|
|
293
|
+
else delete g.EventSource;
|
|
294
|
+
sim.dispose();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
SimEventSource,
|
|
300
|
+
SimWebSocket,
|
|
301
|
+
installBrowserSimulator
|
|
302
|
+
};
|
|
303
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * @omniaura/scenario-sim/browser — run the simulator *inside the page*: no\n * server, no CORS, works in a static build (demo islands, Playwright against\n * `vite preview`). Patches `fetch`, `WebSocket` and `EventSource` so calls\n * matching `match` are answered by the Simulator; everything else passes\n * through. Install this BEFORE @omniaura/solid-pulse so pulse still sees the\n * app's calls.\n */\n\nimport { isUpgraded, Simulator, upgradedResponse, type SimulatorOptions, type UpgradeHook } from \"./core/engine.js\";\nimport type { SocketTransport } from \"./core/streams.js\";\n\nexport interface BrowserSimulatorOptions extends SimulatorOptions {\n /** Which URLs the simulator answers (default: same-origin `/api/` + control path). */\n match?: (url: URL) => boolean;\n /** Base used to resolve relative URLs (default location.origin). */\n origin?: string;\n /** Fixed run id for this page (default \"default\"; use per-tab ids for isolation). */\n run?: string;\n}\n\nfunction frameType(data: unknown): string | null {\n if (typeof data !== \"string\" || data.length > 65536 || data[0] !== \"{\") return null;\n const m = /\"type\"\\s*:\\s*\"([^\"]{1,80})\"/.exec(data);\n return m ? m[1]! : null;\n}\n\n/** CloseEvent with code/reason even where the host's CloseEvent ignores its init dict. */\nfunction makeCloseEvent(code: number, reason: string, wasClean: boolean): CloseEvent {\n let ev: CloseEvent;\n try {\n ev = new CloseEvent(\"close\", { code, reason, wasClean });\n } catch {\n ev = new Event(\"close\") as CloseEvent;\n }\n if (ev.code !== code) {\n Object.defineProperties(ev, {\n code: { value: code, enumerable: true },\n reason: { value: reason, enumerable: true },\n wasClean: { value: wasClean, enumerable: true },\n });\n }\n return ev;\n}\n\n/** A WebSocket the page can use that terminates inside the Simulator. */\nclass SimWebSocket extends EventTarget {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n readonly CONNECTING = 0;\n readonly OPEN = 1;\n readonly CLOSING = 2;\n readonly CLOSED = 3;\n readyState = 0;\n protocol = \"\";\n extensions = \"\";\n binaryType: BinaryType = \"blob\";\n bufferedAmount = 0;\n readonly url: string;\n onopen: ((ev: Event) => unknown) | null = null;\n onmessage: ((ev: MessageEvent) => unknown) | null = null;\n onclose: ((ev: CloseEvent) => unknown) | null = null;\n onerror: ((ev: Event) => unknown) | null = null;\n private inbound: ((data: string | ArrayBuffer) => void) | null = null;\n private clientClose: ((code: number, reason: string) => void) | null = null;\n\n constructor(url: string | URL, protocols: string | string[] | undefined, private sim: Simulator, private runId: string) {\n super();\n this.url = String(url);\n const requested = protocols ? ([] as string[]).concat(protocols) : [];\n queueMicrotask(() => void this.connect(requested));\n }\n\n private async connect(requested: string[]) {\n const httpUrl = this.url.replace(/^ws/, \"http\");\n // Browsers drop `Upgrade`/`Sec-*` request headers; the engine also reads the x-sim-* mirrors.\n const headers: Record<string, string> = { \"x-sim-upgrade\": \"websocket\", \"x-sim-run\": this.runId };\n if (requested.length) headers[\"x-sim-websocket-protocol\"] = requested.join(\", \");\n const request = new Request(httpUrl, { headers });\n const upgrade: UpgradeHook = (route, ctx, run, protocol) => {\n const transport: SocketTransport = {\n send: (data) => {\n if (this.readyState !== 1) return;\n this.dispatch(new MessageEvent(\"message\", { data }));\n },\n close: (code = 1000, reason = \"\") => this.finish(code, reason, true),\n drop: () => this.finish(1006, \"\", false),\n };\n this.protocol = protocol ?? \"\";\n this.readyState = 1;\n const sock = run.streams.openSocket(route, ctx, transport, protocol);\n this.inbound = (data) => run.streams.receive(route, ctx, sock, data);\n this.clientClose = (code, reason) => run.streams.clientClosed(route, ctx, sock, code, reason);\n this.dispatch(new Event(\"open\"));\n return upgradedResponse();\n };\n const response = await this.sim.handle(request, { upgrade });\n if (!isUpgraded(response)) {\n this.dispatch(new Event(\"error\"));\n this.finish(1006, `upgrade failed: ${response.status}`, false);\n }\n }\n\n private dispatch(ev: Event) {\n const handler = (this as unknown as Record<string, ((e: Event) => unknown) | null>)[`on${ev.type}`];\n handler?.call(this, ev);\n this.dispatchEvent(ev);\n }\n\n private finish(code: number, reason: string, wasClean: boolean) {\n if (this.readyState === 3) return;\n this.readyState = 3;\n this.dispatch(makeCloseEvent(code, reason, wasClean));\n }\n\n send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {\n if (this.readyState !== 1) throw new DOMException(\"WebSocket is not open\", \"InvalidStateError\");\n if (typeof data === \"string\") this.inbound?.(data);\n else if (data instanceof ArrayBuffer) this.inbound?.(data);\n else if (ArrayBuffer.isView(data)) this.inbound?.(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer);\n else void (data as Blob).arrayBuffer().then((b) => this.inbound?.(b));\n }\n\n close(code = 1000, reason = \"\") {\n if (this.readyState >= 2) return;\n this.readyState = 2;\n this.clientClose?.(code, reason);\n this.finish(code, reason, true);\n }\n}\n\n/** EventSource over the simulator's SSE routes (same semantics: GET, Last-Event-ID on reconnect). */\nclass SimEventSource extends EventTarget {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSED = 2;\n readyState = 0;\n readonly url: string;\n readonly withCredentials: boolean;\n onopen: ((ev: Event) => unknown) | null = null;\n onmessage: ((ev: MessageEvent) => unknown) | null = null;\n onerror: ((ev: Event) => unknown) | null = null;\n private lastEventId = \"\";\n private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n private retryMs = 1000;\n\n constructor(url: string | URL, init: EventSourceInit | undefined, private sim: Simulator, private runId: string) {\n super();\n this.url = String(url);\n this.withCredentials = init?.withCredentials ?? false;\n void this.open();\n }\n\n private dispatch(ev: Event) {\n const handler = (this as unknown as Record<string, ((e: Event) => unknown) | null>)[`on${ev.type}`];\n handler?.call(this, ev);\n this.dispatchEvent(ev);\n }\n\n private async open() {\n if (this.readyState === 2) return;\n const headers: Record<string, string> = { accept: \"text/event-stream\", \"x-sim-run\": this.runId };\n if (this.lastEventId) headers[\"last-event-id\"] = this.lastEventId;\n const response = await this.sim.handle(new Request(this.url, { headers }));\n if (this.readyState === 2) return;\n if (!response.ok || !response.body) {\n this.dispatch(new Event(\"error\"));\n this.readyState = 2;\n return;\n }\n this.readyState = 1;\n this.dispatch(new Event(\"open\"));\n this.reader = response.body.getReader();\n const dec = new TextDecoder();\n let buf = \"\";\n try {\n for (;;) {\n const { value, done } = await this.reader.read();\n if (done) break;\n buf += dec.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.search(/\\r?\\n\\r?\\n/)) >= 0) {\n const frame = buf.slice(0, idx);\n buf = buf.slice(idx).replace(/^\\r?\\n\\r?\\n/, \"\");\n this.frame(frame);\n }\n }\n } catch {\n // dropped\n }\n if (this.readyState === 2) return;\n // Auto-reconnect like a real EventSource.\n this.readyState = 0;\n this.dispatch(new Event(\"error\"));\n setTimeout(() => void this.open(), this.retryMs);\n }\n\n private frame(frame: string) {\n let event = \"message\";\n const data: string[] = [];\n for (const line of frame.split(/\\r?\\n/)) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n else if (field === \"id\") this.lastEventId = value;\n else if (field === \"retry\" && /^\\d+$/.test(value)) this.retryMs = Number(value);\n }\n if (data.length === 0) return;\n this.dispatch(new MessageEvent(event, { data: data.join(\"\\n\"), lastEventId: this.lastEventId }));\n }\n\n close() {\n this.readyState = 2;\n void this.reader?.cancel().catch(() => {});\n }\n}\n\n/**\n * When @omniaura/solid-pulse instrumented the globals BEFORE this shim was\n * installed, calls that we answer never reach pulse's wrappers, so we report\n * them to pulse's bus ourselves (same event kinds). When pulse installs after\n * us it wraps this shim and reports on its own — then we stay quiet.\n */\ninterface PulseBusLike {\n bus: { emit(kind: string, data: Record<string, unknown>, extra?: Record<string, unknown>): unknown };\n controller: { isOn(feature: string): boolean };\n}\nfunction pulseReporter(alreadyWrapped: boolean) {\n const pulse = () => (globalThis as unknown as { __SOLID_PULSE__?: PulseBusLike }).__SOLID_PULSE__;\n const on = () => alreadyWrapped && pulse()?.controller.isOn(\"network\") === true;\n let nextId = 100_000;\n return {\n id: () => nextId++,\n emit(kind: string, data: Record<string, unknown>) {\n if (on()) pulse()!.bus.emit(kind, { ...data, via: \"scenario-sim\" });\n },\n };\n}\n\nexport function installBrowserSimulator(options: BrowserSimulatorOptions) {\n const sim = new Simulator(options);\n const origin = options.origin ?? location.origin;\n const controlPath = sim.controlPath;\n const runId = options.run ?? sim.defaultRun;\n const match = options.match ?? ((u: URL) => u.origin === origin && (u.pathname.startsWith(\"/api/\") || u.pathname === controlPath || u.pathname.startsWith(`${controlPath}/`)));\n const g = globalThis as unknown as { fetch: typeof fetch; WebSocket: typeof WebSocket; EventSource?: typeof EventSource };\n const nativeFetch = g.fetch;\n const NativeWebSocket = g.WebSocket;\n const NativeEventSource = g.EventSource;\n const report = pulseReporter((NativeWebSocket as unknown as { __solidPulse?: boolean }).__solidPulse === true || (nativeFetch as unknown as { __solidPulse?: boolean }).__solidPulse === true);\n\n g.fetch = function simFetch(this: unknown, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const src = typeof Request !== \"undefined\" && input instanceof Request ? input : null;\n const raw = src ? src.url : input instanceof URL ? input.href : String(input);\n let url: URL;\n try {\n url = new URL(raw, origin);\n } catch {\n return nativeFetch.call(this, input, init);\n }\n if (!match(url)) return nativeFetch.call(this, input, init);\n // Normalise into a fresh Request instead of constructing Request-from-Request:\n // some DOM implementations tee/await the source body and never settle.\n const method = (init?.method ?? src?.method ?? \"GET\").toUpperCase();\n const headers = new Headers(init?.headers ?? src?.headers ?? undefined);\n if (!headers.has(\"x-sim-run\")) headers.set(\"x-sim-run\", runId);\n const bodyless = method === \"GET\" || method === \"HEAD\";\n const bodyP: Promise<BodyInit | null | undefined> = bodyless ? Promise.resolve(undefined) : init?.body !== undefined ? Promise.resolve(init.body) : src ? src.clone().text() : Promise.resolve(undefined);\n const id = report.id();\n const started = performance.now();\n report.emit(\"net.fetch.start\", { id, method, url: url.pathname + url.search });\n return bodyP\n .then((body) => sim.handle(new Request(url, { method, headers, body: body ?? undefined, signal: init?.signal ?? src?.signal ?? undefined })))\n .then((res) => {\n report.emit(\"net.fetch.end\", { id, method, url: url.pathname + url.search, status: res.status, ok: res.ok, ms: Math.round((performance.now() - started) * 100) / 100, contentType: res.headers.get(\"content-type\") ?? \"\", sse: (res.headers.get(\"content-type\") ?? \"\").includes(\"text/event-stream\") });\n return res;\n });\n } as typeof fetch;\n\n g.WebSocket = class extends SimWebSocket {\n constructor(url: string | URL, protocols?: string | string[]) {\n const u = new URL(String(url), origin.replace(/^http/, \"ws\"));\n if (!match(new URL(u.href.replace(/^ws/, \"http\")))) {\n // Not ours: hand back a real socket.\n return new NativeWebSocket(url, protocols) as unknown as SimWebSocket;\n }\n super(u.href, protocols, sim, runId);\n const id = report.id();\n const safeUrl = u.pathname + u.search.replace(/([?&](?:ticket|token)=)[^&]*/gi, \"$1[redacted]\");\n const openedAt = performance.now();\n let inbound = 0;\n let outbound = 0;\n report.emit(\"net.ws.open\", { id, url: safeUrl, protocols: protocols ? ([] as string[]).concat(protocols) : [], state: \"connecting\" });\n this.addEventListener(\"open\", () => report.emit(\"net.ws.open\", { id, url: safeUrl, protocol: this.protocol, state: \"open\", ms: Math.round(performance.now() - openedAt) }));\n this.addEventListener(\"message\", (ev) => {\n inbound++;\n const data = (ev as MessageEvent).data as unknown;\n report.emit(\"net.ws.message\", { id, url: safeUrl, dir: \"in\", n: inbound, bytes: typeof data === \"string\" ? data.length : null, type: frameType(data) });\n });\n this.addEventListener(\"close\", (ev) => {\n const e = ev as CloseEvent;\n report.emit(\"net.ws.close\", { id, url: safeUrl, code: e.code, reason: e.reason, wasClean: e.wasClean, inbound, outbound, ms: Math.round(performance.now() - openedAt) });\n });\n const origSend = this.send.bind(this);\n this.send = (data: string | ArrayBufferLike | Blob | ArrayBufferView) => {\n outbound++;\n report.emit(\"net.ws.message\", { id, url: safeUrl, dir: \"out\", n: outbound, bytes: typeof data === \"string\" ? data.length : null, type: frameType(data) });\n return origSend(data);\n };\n }\n } as unknown as typeof WebSocket;\n\n // Installed even when the host has no EventSource (bun/happy-dom, some\n // workers): simulator routes work everywhere; foreign URLs need the native one.\n const ES = NativeEventSource;\n g.EventSource = class extends SimEventSource {\n constructor(url: string | URL, init?: EventSourceInit) {\n const u = new URL(String(url), origin);\n if (!match(u)) {\n if (!ES) throw new Error(`EventSource is unavailable in this environment and ${u.href} is not simulated`);\n return new ES(url, init) as unknown as SimEventSource;\n }\n super(u.href, init, sim, runId);\n }\n } as unknown as typeof EventSource;\n\n return {\n sim,\n runId,\n controlUrl: `${origin}${controlPath}`,\n restore() {\n g.fetch = nativeFetch;\n g.WebSocket = NativeWebSocket;\n if (NativeEventSource) g.EventSource = NativeEventSource;\n else delete (g as { EventSource?: unknown }).EventSource;\n sim.dispose();\n },\n };\n}\n\nexport { SimWebSocket, SimEventSource };\n"],"mappings":"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","names":[]}
|