@mudlet/mudlet-web 0.4.0 → 0.4.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.
@@ -20,9 +20,38 @@
20
20
  * </map>
21
21
  */
22
22
  import type { MudletMap } from 'mudlet-map-binary-reader';
23
+ /**
24
+ * Why a document was refused. Mudlet keeps these three apart
25
+ * (`fileHoldsMapData` in `TMap.cpp`), because they are three different things
26
+ * to tell a player:
27
+ *
28
+ * `not-a-map` somebody else's well-formed document — a game's "no map
29
+ * here" page answered with a 200, or a `<MudletPackage>`.
30
+ * Every element parses and none of it means anything, which
31
+ * is what used to count as importing a map successfully.
32
+ * `import-failed` a map document that would not parse: the player's own map,
33
+ * damaged. Rooted at `<map>`, so the root test passes and the
34
+ * failure is further in.
35
+ * `damaged` not XML at all — no element to read a root from.
36
+ *
37
+ * The root is taken from the first start tag in the text rather than from a
38
+ * parsed document, exactly as Mudlet's streaming reader does, so a file that is
39
+ * rooted at `<map>` but truncated is still recognised as a map that broke.
40
+ */
41
+ export type XmlMapRefusal = 'not-a-map' | 'import-failed' | 'damaged';
42
+ export type XmlMapParse = {
43
+ ok: true;
44
+ map: MudletMap;
45
+ } | {
46
+ ok: false;
47
+ reason: XmlMapRefusal;
48
+ };
23
49
  /**
24
50
  * Parse an XML map document into the `MudletMap` shape `loadFromBinary`
25
51
  * ingests. Returns null when the text isn't well-formed XML or the root
26
52
  * element isn't `<map>` (e.g. an HTML error page served for the map URL).
53
+ * {@link parseXmlMapResult} is the same parse with the refusal reason kept.
27
54
  */
28
55
  export declare function parseXmlMap(xmlText: string): MudletMap | null;
56
+ /** {@link parseXmlMap}, reporting *why* a document was refused. */
57
+ export declare function parseXmlMapResult(xmlText: string): XmlMapParse;
@@ -11,6 +11,12 @@ export declare class PingTracker {
11
11
  private lastSentAt;
12
12
  private lastDuration;
13
13
  private supported;
14
+ /** The responsiveness beat, live only while a ping is outstanding. */
15
+ private beatTimer;
16
+ private lastBeatAt;
17
+ /** Longest interval the event loop went undelivered during the current
18
+ * wait — see {@link MAX_BEAT_GAP_MS}. */
19
+ private worstBeatGapMs;
14
20
  private readonly unsubs;
15
21
  constructor(sendPingCommand: (latencyMs: number) => void, onPing: (duration: number | null) => void, source: PingEventSource);
16
22
  destroy(): void;
@@ -24,5 +30,12 @@ export declare class PingTracker {
24
30
  * `Core.Ping` makes servers that unconditionally JSON-parse the part after
25
31
  * the module name choke on an empty string. */
26
32
  private sendPing;
33
+ /** Begin witnessing the event loop for the wait that starts now. */
34
+ private startBeat;
35
+ private recordBeatGap;
36
+ /** Stop the beat and answer the worst gap it saw, counting the stretch
37
+ * between the final beat and now — a stall that began after the last beat
38
+ * and ran until the reply is still a stall the reading spans. */
39
+ private stopBeat;
27
40
  private handlePingResponse;
28
41
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Transcoding between the wire and JS strings, for the protocol handlers that
3
+ * carry UTF-8 text inside a telnet subnegotiation (GMCP, MSDP, MSSP).
4
+ *
5
+ * Everything upstream of these speaks *byte-strings*: `bytesToLatin1` in
6
+ * `MudClient.ts` maps each socket byte to one char, and the client's `sendBytes`
7
+ * reverses it with `charCodeAt(i) & 0xff`. Subnegotiations are extracted from that byte-string
8
+ * before the session codec ever runs (`stripTelnetSequences` precedes
9
+ * `codec.decode`), which is what makes these three protocols independent of the
10
+ * session's text encoding — and what obliges each of them to do its own
11
+ * transcoding here.
12
+ */
13
+ export interface DecodedByteString {
14
+ text: string;
15
+ /** The bytes weren't valid UTF-8, so `text` contains U+FFFD where they were.
16
+ * Callers that can report it should; nothing downstream is able to notice,
17
+ * since the substitution is indistinguishable from a legitimate U+FFFD. */
18
+ malformed: boolean;
19
+ }
20
+ /** Decode a Latin-1 byte-string (one char per byte, as produced upstream) as
21
+ * UTF-8. Decoding is lenient by design — a replacement character in one field
22
+ * beats dropping the whole message — but a strict pass runs first so the
23
+ * caller can tell the two apart and say so. */
24
+ export declare const fromByteString: (s: string) => DecodedByteString;
25
+ /** UTF-8-encode into a Latin-1 byte-string, for `MudClient.sendBytes`. Inverse
26
+ * of {@link fromByteString} for well-formed input only — both directions are
27
+ * non-fatal, so invalid UTF-8 inbound and lone surrogates outbound each
28
+ * collapse to U+FFFD rather than round-tripping. Plain ASCII is unchanged. */
29
+ export declare const toByteString: (s: string) => string;
30
+ /** Render a byte-string as hex, for diagnostics that would otherwise print the
31
+ * post-decode text — where every malformed sequence has already collapsed to
32
+ * an indistinguishable U+FFFD. Truncated: the head is where the fault is. */
33
+ export declare const toHex: (s: string) => string;
@@ -3,9 +3,6 @@ export interface GmcpEnvelope {
3
3
  value: unknown;
4
4
  }
5
5
  export type TelnetOptionHandler = (data: string) => string;
6
- /** UTF-8-encode into a Latin-1 byte-string, for MudClient.sendBytes (which
7
- * writes `charCodeAt(i) & 0xff` per char). The inverse of fromByteString. */
8
- export declare const toByteString: (s: string) => string;
9
6
  export declare const createTelnetOptionParser: (onSubnegotiation: (data: string) => void, opts?: {
10
7
  /** When true, an inbound IAC GA / IAC EOR is replaced by a newline
11
8
  * rather than stripped — Mudlet's `mFORCE_GA_OFF` behaviour
@@ -21,10 +18,18 @@ export declare const encodeGmcp: (path: string, payload: unknown) => string;
21
18
  /** Encode a GMCP frame from a single pre-formatted body (e.g. `"Module.Sub args"`).
22
19
  * Mudlet's `sendGMCP` semantics — the caller controls the body between IAC SB
23
20
  * GMCP and IAC SE — except that the body is transcoded to UTF-8 rather than to
24
- * the session's outgoing encoding, so it can't be used to place arbitrary raw
25
- * bytes on the wire. UTF-8 is what the GMCP spec asks for, and it's also what
26
- * keeps a 0xFF byte (which would need IAC-escaping inside a subnegotiation)
27
- * out of the body in the first place. */
21
+ * the session's outgoing encoding, so a JS caller can't use it to place arbitrary
22
+ * raw bytes on the wire: `U+00FF` goes out as `c3 bf`, not as `ff`. UTF-8 is what
23
+ * the GMCP spec asks for, and it's also what keeps a 0xFF byte (which would need
24
+ * IAC-escaping inside a subnegotiation) out of the body in the first place.
25
+ *
26
+ * From Lua the same limit holds, but it isn't this call that imposes it — the
27
+ * bytes are gone a layer earlier. wasmoon marshals Lua→JS strings through
28
+ * `UTF8ToString`, which UTF-8-*decodes* them, so `"\1\200"` reaches `sendGMCP`
29
+ * as `U+0001 U+0200` and `"\1\255"` as a lone surrogate. Re-encoding here
30
+ * restores the caller's original Lua bytes rather than destroying them.
31
+ * Carrying genuinely arbitrary bytes across that bridge needs numbers, or the
32
+ * `%XX` armoring the VFS io path uses. */
28
33
  export declare const encodeGmcpRaw: (message: string) => string;
29
34
  export interface GmcpStreamOptions {
30
35
  onEnvelope: (payload: GmcpEnvelope) => void;
@@ -2,10 +2,11 @@ export * from "./constants";
2
2
  export * from "./gmcp";
3
3
  export * from "./msdp";
4
4
  export * from "./mssp";
5
+ export { toByteString } from "./byteString";
5
6
  export { MccpHandler } from "./mccp";
6
7
  export { EchoHandler } from "./echo";
7
8
  export { MspParser, type MspCommand, type MspKind } from "./msp";
8
9
  export { MxpParser, splitMxpResultLines, type MxpLink, type MxpLineResult } from "./mxp";
9
- export { parseMnesRequest, encodeMnesIs, selectMnesVars, buildNewEnvironVars, CLIENT_NAME, CLIENT_VERSION, TERMINAL_TYPE, type MnesVar, type MnesRequest, type NewEnvironState } from "./mnes";
10
+ export { parseMnesRequest, encodeMnesIs, selectMnesVars, MNES_UNMAINTAINED, buildNewEnvironVars, CLIENT_NAME, CLIENT_VERSION, TERMINAL_TYPE, type MnesVar, type MnesRequest, type NewEnvironState } from "./mnes";
10
11
  export { encodeNaws } from "./naws";
11
12
  export { SessionCodec, CharsetHandler, normalizeCharsetName, pickCharsetFromRequest, SUPPORTED_SERVER_ENCODINGS, DEFAULT_SERVER_ENCODING, canonicalServerEncoding, canEncodeForServer, type CharsetHandlerHooks } from "./charset";
@@ -1,9 +1,16 @@
1
1
  import { CLIENT_NAME, CLIENT_VERSION, TERMINAL_TYPE } from "../../version";
2
2
  /** A single MNES variable the client reports back, e.g. `{ name: "CHARSET",
3
- * value: "UTF-8" }`. */
3
+ * value: "UTF-8" }`.
4
+ *
5
+ * A null `value` means **undefined** rather than empty. RFC 1572 draws the
6
+ * distinction structurally: a name followed by VALUE is defined (and, with
7
+ * nothing after the VALUE, defined-but-empty), while a name followed by the
8
+ * next marker or IAC with no VALUE at all is undefined. That is how a variable
9
+ * the client deliberately does not supply is answered — see
10
+ * {@link MNES_UNMAINTAINED}. */
4
11
  export interface MnesVar {
5
12
  name: string;
6
- value: string;
13
+ value: string | null;
7
14
  }
8
15
  /** Result of parsing an `IAC SB NEW-ENVIRON SEND … IAC SE` request body. */
9
16
  export interface MnesRequest {
@@ -72,12 +79,28 @@ export { CLIENT_NAME, CLIENT_VERSION, TERMINAL_TYPE };
72
79
  * the result with VAR (MNES) or USERVAR (NEW-ENVIRON) via encodeMnesIs.
73
80
  */
74
81
  export declare function buildNewEnvironVars(state: NewEnvironState, extended: boolean): MnesVar[];
82
+ /** MNES names the standard defines that mudix deliberately does not supply.
83
+ * They are still *known*, so a server that asks for one is told it is
84
+ * undefined rather than left with silence — Mudlet's `isMNESVariable` lists
85
+ * IPADDRESS alongside the five it reports, and answers it with a name and no
86
+ * VALUE. (A browser tab has no way to learn its own address, and the client is
87
+ * not the right party to guess: the server already sees the peer address.) */
88
+ export declare const MNES_UNMAINTAINED: ReadonlyArray<string>;
75
89
  /**
76
- * Pick which of the client's `available` MNES variables to report in response
77
- * to a parsed request. A bare SEND (no specific names) returns everything;
78
- * otherwise the requested names are returned in request order, filtered to the
79
- * ones we actually know. If the server asked only for names we don't report,
80
- * we fall back to sending everything rather than an empty reply — the server
81
- * still learns who we are.
90
+ * Pick which of the client's `available` variables to report in response to a
91
+ * parsed request, mirroring Mudlet's `sendIsMNESValues` /
92
+ * `sendIsNewEnvironValues`:
93
+ *
94
+ * - A bare SEND (no names) asks for everything, and gets it.
95
+ * - Named variables come back in request order, each either with its value or
96
+ * — for a name in `undefinedNames` — as undefined (no VALUE).
97
+ * - A name that is neither is left out entirely.
98
+ *
99
+ * A request naming only names we do not report therefore selects **nothing**.
100
+ * It used to fall back to sending everything on the reasoning that the server
101
+ * still learns who we are, but that answers a question nobody asked: a server
102
+ * probing for one specific variable gets an unsolicited dump of the whole set,
103
+ * which is not what Mudlet does and not what the request meant. The caller
104
+ * decides what an empty selection is framed as.
82
105
  */
83
- export declare function selectMnesVars(request: MnesRequest, available: ReadonlyArray<MnesVar>): MnesVar[];
106
+ export declare function selectMnesVars(request: MnesRequest, available: ReadonlyArray<MnesVar>, undefinedNames?: ReadonlyArray<string>): MnesVar[];
@@ -1,4 +1,4 @@
1
- import type { MediaCaptionInfo } from './closedCaption';
1
+ import type { MediaCaptionInfo, MediaKind } from './closedCaption';
2
2
  type LoaderFn = (path: string) => Promise<ArrayBuffer | null>;
3
3
  /** Which mute gate a playback belongs to. `api` = triggered by a Lua script
4
4
  * (playSoundFile / playMusicFile); `game` = triggered by the server (MSP, and
@@ -54,13 +54,25 @@ export declare class SoundManager {
54
54
  * tearing them down.
55
55
  */
56
56
  private muted;
57
+ /**
58
+ * Raised when a tracked source starts playing. ScriptingEngine wires this
59
+ * to Mudlet's `sysMediaStarted(file, path, mediaType, key, tag)` — the same
60
+ * five arguments TMedia.cpp appends when a player reaches PlayingState.
61
+ *
62
+ * Fired after `source.start()` has actually been accepted, so a decode
63
+ * failure or a rejected start never announces a playback that isn't
64
+ * happening. Mudlet withholds it for preload-volume loads; mudix has no
65
+ * preload volume (`preload()` only warms the decode cache and never builds
66
+ * a source), so there is nothing to withhold it for.
67
+ */
68
+ onMediaStarted?: (file: string, path: string, mediaType: MediaKind, key: string, tag: string) => void;
57
69
  /**
58
70
  * Raised when a tracked source ends — whether it played out naturally or
59
71
  * was stopped. ScriptingEngine wires this to raise Mudlet's
60
- * `sysMediaFinished(name, path)`. `stopAll()` nulls each source's onended
61
- * before stopping, so engine teardown never fires it.
72
+ * `sysMediaFinished(file, path, mediaType, key, tag)`. `stopAll()` nulls
73
+ * each source's onended before stopping, so engine teardown never fires it.
62
74
  */
63
- onMediaFinished?: (name: string, path: string) => void;
75
+ onMediaFinished?: (file: string, path: string, mediaType: MediaKind, key: string, tag: string) => void;
64
76
  /**
65
77
  * Raised when a tracked source starts ('plays') or ends ('stops'), so the
66
78
  * engine can print a closed caption (Mudlet's enableClosedCaption). Fires
@@ -33,9 +33,14 @@ export declare class VideoManager {
33
33
  * still reports what the script asked for.
34
34
  */
35
35
  private muted;
36
+ /** Fires once the element has actually begun playing — mirrors Mudlet's
37
+ * sysMediaStarted. Deliberately after `el.play()` resolves rather than
38
+ * beside the caption below it: autoplay policy can reject the play, and an
39
+ * event announcing a playback that never started is worse than none. */
40
+ onStarted: ((file: string, path: string) => void) | null;
36
41
  /** Fires when a video ends naturally or is stopped — mirrors Mudlet's
37
42
  * sysMediaFinished. */
38
- onEnded: ((name: string, path: string) => void) | null;
43
+ onEnded: ((file: string, path: string) => void) | null;
39
44
  /** Raised when a video starts ('plays') or finishes ('stops') so the engine
40
45
  * can print a closed caption (Mudlet's enableClosedCaption). */
41
46
  onMediaCaption: ((info: MediaCaptionInfo) => void) | null;
@@ -0,0 +1 @@
1
+ export declare function githubRawUrl(url: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mudlet/mudlet-web",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Mudlet Web — Mudlet in the browser, usable standalone or as a library for branded builds",
6
6
  "license": "GPL-2.0-or-later",