@seekprotocol/game 0.1.0 → 0.1.1

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/seek-game.js CHANGED
@@ -1,293 +1,304 @@
1
- // @seekprotocol/game: the Seek GameKit bridge. Generated by packages/game/bundle.mjs; do not edit.
2
-
3
- // src/diagnostics.ts
1
+ // @seekprotocol/game, the GameKit bridge. Copyright (c) 2026 Seek Protocol. MIT License.
4
2
  var LOG_LEVELS = ["debug", "info", "warn", "error"];
5
3
  var isLogLevel = (v) => typeof v === "string" && LOG_LEVELS.includes(v);
6
4
  var HOST_EVENTS = ["no_ready", "hang", "render_gone", "load_failed"];
7
5
  var isHostEvent = (v) => typeof v === "string" && HOST_EVENTS.includes(v);
8
- var HOST_READY_TIMEOUT_MS = 1e4;
9
- var HOST_HANG_MS = 3e4;
6
+ var HOST_READY_TIMEOUT_MS = 10000;
7
+ var HOST_HANG_MS = 30000;
10
8
  var HOST_EVENT_TEXT = {
11
- no_ready: "The game did not call ready() within 10 seconds.",
12
- hang: "The game sent no heartbeat for 30 seconds while it was in the foreground.",
13
- render_gone: "The WebView process running the game died.",
14
- load_failed: "The game page failed to load."
9
+ no_ready: "The game did not call ready() within 10 seconds.",
10
+ hang: "The game sent no heartbeat for 30 seconds while it was in the foreground.",
11
+ render_gone: "The WebView process running the game died.",
12
+ load_failed: "The game page failed to load."
15
13
  };
16
14
  var END_REASONS = ["exit", "pagehide", "closed", "host_exit", ...HOST_EVENTS];
17
15
  var ERROR_SOURCES = ["error", "unhandledrejection", "console", "manual"];
18
16
  var isFatalSource = (s) => s === "error" || s === "unhandledrejection";
19
17
  var DIAG_LIMITS = {
20
- /** Characters of a message (log or error). */
21
- message: 500,
22
- /** Characters of a stack, after scrubbing. */
23
- stack: 4e3,
24
- /** Stack frames kept. */
25
- frames: 20,
26
- /** Characters of a source file path. */
27
- file: 200,
28
- /** UTF-8 bytes of a log's data as JSON. */
29
- data: 2048,
30
- /** Characters of a host event's detail. */
31
- detail: 200,
32
- /** Entries in one batch. */
33
- batchEntries: 50,
34
- /** UTF-8 bytes of one batch as JSON (the proxy reads at most 64 KB). */
35
- batchBytes: 48 * 1024,
36
- /** Error entries per session; the proxy keeps the same limit. */
37
- sessionErrors: 100,
38
- /** Log entries per session. */
39
- sessionLogs: 1e3,
40
- /** One error entry stands for at most this many identical errors. */
41
- repeat: 1e3
18
+ message: 500,
19
+ stack: 4000,
20
+ frames: 20,
21
+ file: 200,
22
+ data: 2048,
23
+ detail: 200,
24
+ batchEntries: 50,
25
+ batchBytes: 48 * 1024,
26
+ sessionErrors: 100,
27
+ sessionLogs: 1000,
28
+ repeat: 1000
42
29
  };
43
30
  var DIAG_BODY_MAX_BYTES = 64 * 1024;
44
- var PERF_RANGES = { fps: [0, 240], jank: [0, 1e5], heap_mb: [0, 65536], ready_ms: [0, 6e5] };
31
+ var PERF_RANGES = { fps: [0, 240], jank: [0, 100000], heap_mb: [0, 65536], ready_ms: [0, 600000] };
45
32
  var DEVICE_CLASSES = ["phone", "tablet", "desktop", "unknown"];
46
33
  var OS_NAMES = ["ios", "android", "web", "unknown"];
47
34
  var OS_VERSION = /^\d{1,3}(?:\.\d{1,3}){0,2}$/;
48
35
  var APP_VERSION = /^(?:\d{1,3}\.\d{1,3}\.\d{1,4}|simulator)$/;
49
36
  function launchContextProblem(c) {
50
- if (!c || typeof c !== "object" || Array.isArray(c)) return "ctx is an object.";
51
- const x = c;
52
- for (const k of Object.keys(x)) if (!["device", "os", "os_version", "app_version"].includes(k)) return `ctx has no field ${k}.`;
53
- if (!DEVICE_CLASSES.includes(x.device)) return "ctx.device is phone, tablet, desktop or unknown.";
54
- if (!OS_NAMES.includes(x.os)) return "ctx.os is ios, android, web or unknown.";
55
- if (x.os_version !== void 0 && (typeof x.os_version !== "string" || !OS_VERSION.test(x.os_version))) return "ctx.os_version is digits and dots.";
56
- if (x.app_version !== void 0 && (typeof x.app_version !== "string" || !APP_VERSION.test(x.app_version))) return "ctx.app_version is x.y.z or simulator.";
57
- return null;
37
+ if (!c || typeof c !== "object" || Array.isArray(c))
38
+ return "ctx is an object.";
39
+ const x = c;
40
+ for (const k of Object.keys(x))
41
+ if (!["device", "os", "os_version", "app_version"].includes(k))
42
+ return `ctx has no field ${k}.`;
43
+ if (!DEVICE_CLASSES.includes(x.device))
44
+ return "ctx.device is phone, tablet, desktop or unknown.";
45
+ if (!OS_NAMES.includes(x.os))
46
+ return "ctx.os is ios, android, web or unknown.";
47
+ if (x.os_version !== void 0 && (typeof x.os_version !== "string" || !OS_VERSION.test(x.os_version)))
48
+ return "ctx.os_version is digits and dots.";
49
+ if (x.app_version !== void 0 && (typeof x.app_version !== "string" || !APP_VERSION.test(x.app_version)))
50
+ return "ctx.app_version is x.y.z or simulator.";
51
+ return null;
58
52
  }
59
53
  function normaliseContext(c) {
60
- const x = c && typeof c === "object" ? c : {};
61
- return {
62
- device: DEVICE_CLASSES.includes(x.device) ? x.device : "unknown",
63
- os: OS_NAMES.includes(x.os) ? x.os : "unknown",
64
- os_version: typeof x.os_version === "string" && OS_VERSION.test(x.os_version) ? x.os_version : null,
65
- app_version: typeof x.app_version === "string" && APP_VERSION.test(x.app_version) ? x.app_version : null
66
- };
54
+ const x = c && typeof c === "object" ? c : {};
55
+ return {
56
+ device: DEVICE_CLASSES.includes(x.device) ? x.device : "unknown",
57
+ os: OS_NAMES.includes(x.os) ? x.os : "unknown",
58
+ os_version: typeof x.os_version === "string" && OS_VERSION.test(x.os_version) ? x.os_version : null,
59
+ app_version: typeof x.app_version === "string" && APP_VERSION.test(x.app_version) ? x.app_version : null
60
+ };
67
61
  }
68
62
  var SCRUBS = [
69
- // A URL keeps its path but loses its query and fragment: those carry ids, tokens and positions.
70
- [/\b((?:https?|wss?):\/\/[^\s?#"'<>`]*)[?#][^\s"'<>`)]*/gi, "$1?[query]"],
71
- // Bearer and basic credentials.
72
- [/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [token]"],
73
- // JWTs and JWS: three base64url parts, the first starting like a JSON header.
74
- [/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g, "[token]"],
75
- // Emails.
76
- [/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}/g, "[email]"],
77
- // key=value and "key": "value" where the key names a secret.
78
- [/((?<![A-Za-z])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|passwd|pwd|auth|authorization|session|cookie|sig|signature)["']?\s*[:=]\s*["']?)(?!(?:bearer|basic)\b|\[)[^\s"'&,;}]{4,}/gi, "$1[secret]"],
79
- // A latitude, longitude pair: two decimals with at least three places, in range.
80
- [/(?<![\d.])-?(?:[1-8]?\d(?:\.\d{3,})|90\.0{3,})\s*[,;/ ]\s*-?(?:1[0-7]\d|0?\d?\d)\.\d{3,}(?![\d.])/g, "[location]"],
81
- // Long opaque strings: 24 or more base64url or hex characters with letters and digits mixed in.
82
- [/(?<![A-Za-z0-9_-])(?=[A-Za-z0-9_-]{24,})(?=[A-Za-z_-]*\d)(?=[0-9_-]*[A-Za-z])[A-Za-z0-9_-]{24,}(?![A-Za-z0-9_-])/g, "[key]"],
83
- // Long digit runs (phone, card and account numbers, timestamps), with spaces or dashes inside.
84
- [/(?<![\d.])\+?\d(?:[\s-]?\d){6,}(?![\d.])/g, "[number]"]
63
+ [/\b((?:https?|wss?):\/\/[^\s?#"'<>`]*)[?#][^\s"'<>`)]*/gi, "$1?[query]"],
64
+ [/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [token]"],
65
+ [/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g, "[token]"],
66
+ [/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}/g, "[email]"],
67
+ [/((?<![A-Za-z])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|passwd|pwd|auth|authorization|session|cookie|sig|signature)["']?\s*[:=]\s*["']?)(?!(?:bearer|basic)\b|\[)[^\s"'&,;}]{4,}/gi, "$1[secret]"],
68
+ [/(?<![\d.])-?(?:[1-8]?\d(?:\.\d{3,})|90\.0{3,})\s*[,;/ ]\s*-?(?:1[0-7]\d|0?\d?\d)\.\d{3,}(?![\d.])/g, "[location]"],
69
+ [/(?<![A-Za-z0-9_-])(?=[A-Za-z0-9_-]{24,})(?=[A-Za-z_-]*\d)(?=[0-9_-]*[A-Za-z])[A-Za-z0-9_-]{24,}(?![A-Za-z0-9_-])/g, "[key]"],
70
+ [/(?<![\d.])\+?\d(?:[\s-]?\d){6,}(?![\d.])/g, "[number]"]
85
71
  ];
86
72
  function scrubText(s, extra = []) {
87
- let out = s;
88
- for (const secret of extra) if (secret && secret.length >= 8) out = out.split(secret).join("[token]");
89
- for (const [re, to] of SCRUBS) out = out.replace(re, to);
90
- return out;
73
+ let out = s;
74
+ for (const secret of extra)
75
+ if (secret && secret.length >= 8)
76
+ out = out.split(secret).join("[token]");
77
+ for (const [re, to] of SCRUBS)
78
+ out = out.replace(re, to);
79
+ return out;
91
80
  }
92
81
  var SECRET_KEY = /^(?:.*(?:token|secret|password|passwd|pwd|auth|cookie|session|signature|apikey|api_key|email|phone|wallet|address)|lat|lng|lon|long|latitude|longitude|coords?|position|location|ip)$/i;
93
82
  function scrubData(value, extra = [], depth = 0) {
94
- if (depth > 6) return "[deep]";
95
- if (value === null || typeof value === "boolean") return value;
96
- if (typeof value === "number") return Number.isFinite(value) ? value : null;
97
- if (typeof value === "string") return scrubText(value.length > DIAG_LIMITS.message ? value.slice(0, DIAG_LIMITS.message) : value, extra);
98
- if (Array.isArray(value)) return value.slice(0, 50).map((v) => scrubData(v, extra, depth + 1));
99
- if (typeof value === "object") {
100
- const out = {};
101
- for (const [k, v] of Object.entries(value).slice(0, 50)) {
102
- const key = scrubText(k.slice(0, 64), extra);
103
- out[key] = SECRET_KEY.test(k) ? "[removed]" : scrubData(v, extra, depth + 1);
83
+ if (depth > 6)
84
+ return "[deep]";
85
+ if (value === null || typeof value === "boolean")
86
+ return value;
87
+ if (typeof value === "number")
88
+ return Number.isFinite(value) ? value : null;
89
+ if (typeof value === "string")
90
+ return scrubText(value.length > DIAG_LIMITS.message ? value.slice(0, DIAG_LIMITS.message) : value, extra);
91
+ if (Array.isArray(value))
92
+ return value.slice(0, 50).map((v) => scrubData(v, extra, depth + 1));
93
+ if (typeof value === "object") {
94
+ const out = {};
95
+ for (const [k, v] of Object.entries(value).slice(0, 50)) {
96
+ const key = scrubText(k.slice(0, 64), extra);
97
+ out[key] = SECRET_KEY.test(k) ? "[removed]" : scrubData(v, extra, depth + 1);
98
+ }
99
+ return out;
104
100
  }
105
- return out;
106
- }
107
- return null;
101
+ return null;
108
102
  }
109
103
  function boundData(value, extra = []) {
110
- if (value === void 0) return void 0;
111
- const clean = scrubData(value, extra);
112
- let bytes = Infinity;
113
- try {
114
- bytes = new TextEncoder().encode(JSON.stringify(clean)).byteLength;
115
- } catch {
116
- return { truncated: true };
117
- }
118
- return bytes > DIAG_LIMITS.data ? { truncated: true, bytes } : clean;
104
+ if (value === void 0)
105
+ return void 0;
106
+ const clean = scrubData(value, extra);
107
+ let bytes = Infinity;
108
+ try {
109
+ bytes = new TextEncoder().encode(JSON.stringify(clean)).byteLength;
110
+ }
111
+ catch {
112
+ return { truncated: true };
113
+ }
114
+ return bytes > DIAG_LIMITS.data ? { truncated: true, bytes } : clean;
119
115
  }
120
116
  var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
121
117
  function shortenGameUrls(s) {
122
- return s.replace(
123
- /https:\/\/[a-z0-9][a-z0-9-]{1,28}[a-z0-9]-[0-9a-f]{16}\.play\.seekprotocol\.ai(?:\/@[0-9a-f]{64})?(\/[^\s?#"'()<>`]*)([?#][^\s"'()<>`]*?)?(?=:\d+:\d+|[\s"'()<>`]|$)/g,
124
- (_m, path, query) => `${path}${query ? "?[query]" : ""}`
125
- );
118
+ return s.replace(/https:\/\/[a-z0-9][a-z0-9-]{1,28}[a-z0-9]-[0-9a-f]{16}\.play\.seekprotocol\.ai(?:\/@[0-9a-f]{64})?(\/[^\s?#"'()<>`]*)([?#][^\s"'()<>`]*?)?(?=:\d+:\d+|[\s"'()<>`]|$)/g, (_m, path, query) => `${path}${query ? "?[query]" : ""}`);
126
119
  }
127
120
  var CHROME_FRAME = /^\s*at\s+(?:(.*?)\s+\()?(.+?):(\d+):(\d+)\)?\s*$/;
128
121
  var GECKO_FRAME = /^\s*(.*?)@(.+?):(\d+):(\d+)\s*$/;
129
122
  function parseStack(stack) {
130
- if (!stack) return [];
131
- const frames = [];
132
- for (const raw of stack.split("\n")) {
133
- const m = CHROME_FRAME.exec(raw) ?? GECKO_FRAME.exec(raw);
134
- if (!m) continue;
135
- frames.push({ fn: (m[1] ?? "").trim(), file: m[2].trim(), line: Number(m[3]), col: Number(m[4]) });
136
- if (frames.length >= DIAG_LIMITS.frames) break;
137
- }
138
- return frames;
123
+ if (!stack)
124
+ return [];
125
+ const frames = [];
126
+ for (const raw of stack.split("\n")) {
127
+ const m = CHROME_FRAME.exec(raw) ?? GECKO_FRAME.exec(raw);
128
+ if (!m)
129
+ continue;
130
+ frames.push({ fn: (m[1] ?? "").trim(), file: m[2].trim(), line: Number(m[3]), col: Number(m[4]) });
131
+ if (frames.length >= DIAG_LIMITS.frames)
132
+ break;
133
+ }
134
+ return frames;
139
135
  }
140
136
  function normaliseMessage(message) {
141
- return message.replace(/\[(?:email|token|key|secret|location|number|query)\]/g, "<x>").replace(/(["'`])(?:(?!\1).){0,200}\1/g, "<s>").replace(/\b0x[0-9a-f]+\b/gi, "<n>").replace(/\b[0-9a-f]{8,}\b/gi, "<n>").replace(/-?\d+(?:\.\d+)?/g, "<n>").replace(/\s+/g, " ").trim().toLowerCase().slice(0, 300);
137
+ return message.replace(/\[(?:email|token|key|secret|location|number|query)\]/g, "<x>").replace(/(["'`])(?:(?!\1).){0,200}\1/g, "<s>").replace(/\b0x[0-9a-f]+\b/gi, "<n>").replace(/\b[0-9a-f]{8,}\b/gi, "<n>").replace(/-?\d+(?:\.\d+)?/g, "<n>").replace(/\s+/g, " ").trim().toLowerCase().slice(0, 300);
142
138
  }
143
139
  var FINGERPRINT_FRAMES = 3;
144
140
  var frameKey = (f) => `${shortenGameUrls(f.file).replace(/[?#].*$/, "")}:${f.fn || `line ${f.line ?? "?"}`}`;
145
141
  var hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
146
142
  async function sha256HexOf(s) {
147
- return hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)));
143
+ return hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)));
148
144
  }
149
145
  function fingerprintInput(kind, message, stack, file, line) {
150
- const frames = parseStack(stack).slice(0, FINGERPRINT_FRAMES).map(frameKey);
151
- if (!frames.length && file) frames.push(`${shortenGameUrls(file)}:line ${line ?? "?"}`);
152
- return [kind, normaliseMessage(message), ...frames].join("\n");
146
+ const frames = parseStack(stack).slice(0, FINGERPRINT_FRAMES).map(frameKey);
147
+ if (!frames.length && file)
148
+ frames.push(`${shortenGameUrls(file)}:line ${line ?? "?"}`);
149
+ return [kind, normaliseMessage(message), ...frames].join("\n");
153
150
  }
154
151
  async function fingerprint(kind, message, stack, file, line) {
155
- return (await sha256HexOf(fingerprintInput(kind, message, stack, file, line))).slice(0, 32);
152
+ return (await sha256HexOf(fingerprintInput(kind, message, stack, file, line))).slice(0, 32);
156
153
  }
157
154
  async function sessionIdOf(appId, runId) {
158
- return (await sha256HexOf(`seek-diag-session:v1:${appId}:${runId.toLowerCase()}`)).slice(0, 32);
155
+ return (await sha256HexOf(`seek-diag-session:v1:${appId}:${runId.toLowerCase()}`)).slice(0, 32);
159
156
  }
160
157
  var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
161
158
  var txt = (v, max) => typeof v === "string" && v.length > 0 ? clip(v, max) : null;
162
159
  var int = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? v : void 0;
163
160
  function parseDiagnosticsBatch(body, opts = { from: "game" }) {
164
- if (!isObj(body) || !Array.isArray(body.entries)) return null;
165
- const secrets = opts.secrets ?? [];
166
- const out = { entries: [], invalid: 0, hostOnly: 0, dropped: int(body.dropped, 0, 1e6) ?? 0 };
167
- const list = body.entries.slice(0, DIAG_LIMITS.batchEntries);
168
- out.invalid += body.entries.length - list.length;
169
- for (const e of list) {
170
- const entry = cleanDiagnosticEntry(e, secrets);
171
- if (!entry) out.invalid++;
172
- else if (entry.kind === "host" && opts.from !== "host") out.hostOnly++;
173
- else out.entries.push(entry);
174
- }
175
- return out;
161
+ if (!isObj(body) || !Array.isArray(body.entries))
162
+ return null;
163
+ const secrets = opts.secrets ?? [];
164
+ const out = { entries: [], invalid: 0, hostOnly: 0, dropped: int(body.dropped, 0, 1000000) ?? 0 };
165
+ const list = body.entries.slice(0, DIAG_LIMITS.batchEntries);
166
+ out.invalid += body.entries.length - list.length;
167
+ for (const e of list) {
168
+ const entry = cleanDiagnosticEntry(e, secrets);
169
+ if (!entry)
170
+ out.invalid++;
171
+ else if (entry.kind === "host" && opts.from !== "host")
172
+ out.hostOnly++;
173
+ else
174
+ out.entries.push(entry);
175
+ }
176
+ return out;
176
177
  }
177
178
  function cleanDiagnosticEntry(e, secrets = []) {
178
- if (!isObj(e)) return null;
179
- const at = typeof e.at === "number" && Number.isFinite(e.at) ? Math.round(e.at) : NaN;
180
- if (!Number.isFinite(at)) return null;
181
- const scrub = (s, max) => clip(scrubText(shortenGameUrls(s), secrets), max);
182
- switch (e.kind) {
183
- case "heartbeat": {
184
- const fps = int(e.fps, ...PERF_RANGES.fps);
185
- const jank = int(e.jank, ...PERF_RANGES.jank);
186
- const heap = int(e.heap_mb, ...PERF_RANGES.heap_mb);
187
- return { kind: "heartbeat", at, ...fps !== void 0 ? { fps } : {}, ...jank !== void 0 ? { jank } : {}, ...heap !== void 0 ? { heap_mb: heap } : {} };
188
- }
189
- case "start": {
190
- const ready = int(e.ready_ms, ...PERF_RANGES.ready_ms);
191
- return { kind: "start", at, ...ready !== void 0 ? { ready_ms: ready } : {} };
192
- }
193
- case "end":
194
- return END_REASONS.includes(e.reason) ? { kind: "end", at, reason: e.reason } : null;
195
- case "host": {
196
- if (!isHostEvent(e.event)) return null;
197
- const detail = txt(e.detail, DIAG_LIMITS.detail);
198
- return { kind: "host", at, event: e.event, ...detail ? { detail: scrub(detail, DIAG_LIMITS.detail) } : {} };
199
- }
200
- case "log": {
201
- const message = txt(e.message, DIAG_LIMITS.message * 2);
202
- if (!isLogLevel(e.level) || message === null) return null;
203
- const data = e.data === void 0 ? void 0 : boundData(e.data, secrets);
204
- return {
205
- kind: "log",
206
- at,
207
- level: e.level,
208
- message: scrub(message, DIAG_LIMITS.message),
209
- ...data !== void 0 ? { data } : {},
210
- source: e.source === "console" ? "console" : "log"
211
- };
212
- }
213
- case "error": {
214
- const message = txt(e.message, DIAG_LIMITS.message * 2) ?? "Unknown error";
215
- if (!ERROR_SOURCES.includes(e.source)) return null;
216
- const stack = txt(e.stack, DIAG_LIMITS.stack * 2);
217
- const file = txt(e.file, DIAG_LIMITS.file * 2);
218
- const line = int(e.line, 0, 1e7);
219
- const col = int(e.col, 0, 1e7);
220
- const count = int(e.count, 1, DIAG_LIMITS.repeat);
221
- return {
222
- kind: "error",
223
- at,
224
- source: e.source,
225
- message: scrub(message, DIAG_LIMITS.message),
226
- ...stack ? { stack: scrub(stack, DIAG_LIMITS.stack) } : {},
227
- ...file ? { file: scrub(file, DIAG_LIMITS.file) } : {},
228
- ...line !== void 0 ? { line } : {},
229
- ...col !== void 0 ? { col } : {},
230
- ...count !== void 0 && count > 1 ? { count } : {}
231
- };
179
+ if (!isObj(e))
180
+ return null;
181
+ const at = typeof e.at === "number" && Number.isFinite(e.at) ? Math.round(e.at) : NaN;
182
+ if (!Number.isFinite(at))
183
+ return null;
184
+ const scrub = (s, max) => clip(scrubText(shortenGameUrls(s), secrets), max);
185
+ switch (e.kind) {
186
+ case "heartbeat": {
187
+ const fps = int(e.fps, ...PERF_RANGES.fps);
188
+ const jank = int(e.jank, ...PERF_RANGES.jank);
189
+ const heap = int(e.heap_mb, ...PERF_RANGES.heap_mb);
190
+ return { kind: "heartbeat", at, ...fps !== void 0 ? { fps } : {}, ...jank !== void 0 ? { jank } : {}, ...heap !== void 0 ? { heap_mb: heap } : {} };
191
+ }
192
+ case "start": {
193
+ const ready = int(e.ready_ms, ...PERF_RANGES.ready_ms);
194
+ return { kind: "start", at, ...ready !== void 0 ? { ready_ms: ready } : {} };
195
+ }
196
+ case "end":
197
+ return END_REASONS.includes(e.reason) ? { kind: "end", at, reason: e.reason } : null;
198
+ case "host": {
199
+ if (!isHostEvent(e.event))
200
+ return null;
201
+ const detail = txt(e.detail, DIAG_LIMITS.detail);
202
+ return { kind: "host", at, event: e.event, ...detail ? { detail: scrub(detail, DIAG_LIMITS.detail) } : {} };
203
+ }
204
+ case "log": {
205
+ const message = txt(e.message, DIAG_LIMITS.message * 2);
206
+ if (!isLogLevel(e.level) || message === null)
207
+ return null;
208
+ const data = e.data === void 0 ? void 0 : boundData(e.data, secrets);
209
+ return {
210
+ kind: "log",
211
+ at,
212
+ level: e.level,
213
+ message: scrub(message, DIAG_LIMITS.message),
214
+ ...data !== void 0 ? { data } : {},
215
+ source: e.source === "console" ? "console" : "log"
216
+ };
217
+ }
218
+ case "error": {
219
+ const message = txt(e.message, DIAG_LIMITS.message * 2) ?? "Unknown error";
220
+ if (!ERROR_SOURCES.includes(e.source))
221
+ return null;
222
+ const stack = txt(e.stack, DIAG_LIMITS.stack * 2);
223
+ const file = txt(e.file, DIAG_LIMITS.file * 2);
224
+ const line = int(e.line, 0, 10000000);
225
+ const col = int(e.col, 0, 10000000);
226
+ const count = int(e.count, 1, DIAG_LIMITS.repeat);
227
+ return {
228
+ kind: "error",
229
+ at,
230
+ source: e.source,
231
+ message: scrub(message, DIAG_LIMITS.message),
232
+ ...stack ? { stack: scrub(stack, DIAG_LIMITS.stack) } : {},
233
+ ...file ? { file: scrub(file, DIAG_LIMITS.file) } : {},
234
+ ...line !== void 0 ? { line } : {},
235
+ ...col !== void 0 ? { col } : {},
236
+ ...count !== void 0 && count > 1 ? { count } : {}
237
+ };
238
+ }
239
+ default:
240
+ return null;
232
241
  }
233
- default:
234
- return null;
235
- }
236
242
  }
237
243
  var isCrashEntry = (e) => e.kind === "host" || e.kind === "error" && isFatalSource(e.source);
238
244
  function createHostWatchdog(opts) {
239
- const now = opts.now ?? (() => Date.now());
240
- const setT = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
241
- const clearT = opts.clearTimer ?? ((t) => clearTimeout(t));
242
- const readyMs = opts.readyTimeoutMs ?? HOST_READY_TIMEOUT_MS;
243
- const hangMs = opts.hangMs ?? HOST_HANG_MS;
244
- let timer = null;
245
- let isReady = false;
246
- let paused = false;
247
- let done = false;
248
- const arm = (ms, event) => {
249
- if (timer !== null) clearT(timer);
250
- timer = done || paused ? null : setT(() => report(event), ms);
251
- };
252
- const report = (event, detail) => {
253
- if (done) return;
254
- done = true;
255
- if (timer !== null) clearT(timer);
256
- timer = null;
257
- opts.onCrash({ kind: "host", at: now(), event, ...detail ? { detail: clip(scrubText(detail), DIAG_LIMITS.detail) } : {} });
258
- };
259
- arm(readyMs, "no_ready");
260
- return {
261
- ready() {
262
- if (!isReady && !done) {
263
- isReady = true;
264
- arm(hangMs, "hang");
265
- }
266
- },
267
- beat() {
268
- if (isReady && !done) arm(hangMs, "hang");
269
- },
270
- pause() {
271
- paused = true;
272
- if (timer !== null) clearT(timer);
273
- timer = null;
274
- },
275
- resume() {
276
- paused = false;
277
- if (!done) arm(isReady ? hangMs : readyMs, isReady ? "hang" : "no_ready");
278
- },
279
- fail(event, detail) {
280
- report(event, detail);
281
- },
282
- stop() {
283
- done = true;
284
- if (timer !== null) clearT(timer);
285
- timer = null;
286
- }
287
- };
245
+ const now = opts.now ?? (() => Date.now());
246
+ const setT = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
247
+ const clearT = opts.clearTimer ?? ((t) => clearTimeout(t));
248
+ const readyMs = opts.readyTimeoutMs ?? HOST_READY_TIMEOUT_MS;
249
+ const hangMs = opts.hangMs ?? HOST_HANG_MS;
250
+ let timer = null;
251
+ let isReady = false;
252
+ let paused = false;
253
+ let done = false;
254
+ const arm = (ms, event) => {
255
+ if (timer !== null)
256
+ clearT(timer);
257
+ timer = done || paused ? null : setT(() => report(event), ms);
258
+ };
259
+ const report = (event, detail) => {
260
+ if (done)
261
+ return;
262
+ done = true;
263
+ if (timer !== null)
264
+ clearT(timer);
265
+ timer = null;
266
+ opts.onCrash({ kind: "host", at: now(), event, ...detail ? { detail: clip(scrubText(detail), DIAG_LIMITS.detail) } : {} });
267
+ };
268
+ arm(readyMs, "no_ready");
269
+ return {
270
+ ready() {
271
+ if (!isReady && !done) {
272
+ isReady = true;
273
+ arm(hangMs, "hang");
274
+ }
275
+ },
276
+ beat() {
277
+ if (isReady && !done)
278
+ arm(hangMs, "hang");
279
+ },
280
+ pause() {
281
+ paused = true;
282
+ if (timer !== null)
283
+ clearT(timer);
284
+ timer = null;
285
+ },
286
+ resume() {
287
+ paused = false;
288
+ if (!done)
289
+ arm(isReady ? hangMs : readyMs, isReady ? "hang" : "no_ready");
290
+ },
291
+ fail(event, detail) {
292
+ report(event, detail);
293
+ },
294
+ stop() {
295
+ done = true;
296
+ if (timer !== null)
297
+ clearT(timer);
298
+ timer = null;
299
+ }
300
+ };
288
301
  }
289
-
290
- // src/protocol.ts
291
302
  var PROTOCOL = "seek-game";
292
303
  var PROTOCOL_VERSION = 1;
293
304
  var BRIDGE_VERSION = "0.1.0";
@@ -303,680 +314,734 @@ var PROXY_ORIGIN = "https://api.play.seekprotocol.ai";
303
314
  var DEFAULT_HOST_ORIGINS = ["https://storeconnect.seekprotocol.ai"];
304
315
  var utf8Bytes = (s) => new TextEncoder().encode(s).byteLength;
305
316
  function jsonBytes(value) {
306
- try {
307
- const s = JSON.stringify(value);
308
- return s === void 0 ? Infinity : utf8Bytes(s);
309
- } catch {
310
- return Infinity;
311
- }
317
+ try {
318
+ const s = JSON.stringify(value);
319
+ return s === void 0 ? Infinity : utf8Bytes(s);
320
+ }
321
+ catch {
322
+ return Infinity;
323
+ }
312
324
  }
313
325
  function isJsonValue(v, depth = 0) {
314
- if (depth > 32) return false;
315
- if (v === null || typeof v === "string" || typeof v === "boolean") return true;
316
- if (typeof v === "number") return Number.isFinite(v);
317
- if (Array.isArray(v)) return v.every((x) => isJsonValue(x, depth + 1));
318
- if (typeof v === "object") {
319
- const proto = Object.getPrototypeOf(v);
320
- return (proto === Object.prototype || proto === null) && Object.values(v).every((x) => isJsonValue(x, depth + 1));
321
- }
322
- return false;
326
+ if (depth > 32)
327
+ return false;
328
+ if (v === null || typeof v === "string" || typeof v === "boolean")
329
+ return true;
330
+ if (typeof v === "number")
331
+ return Number.isFinite(v);
332
+ if (Array.isArray(v))
333
+ return v.every((x) => isJsonValue(x, depth + 1));
334
+ if (typeof v === "object") {
335
+ const proto = Object.getPrototypeOf(v);
336
+ return (proto === Object.prototype || proto === null) && Object.values(v).every((x) => isJsonValue(x, depth + 1));
337
+ }
338
+ return false;
323
339
  }
324
340
  var storageKeyProblem = (key) => typeof key === "string" && STORAGE_KEY.test(key) ? null : "A storage key is 1 to 64 letters, digits, dots, hyphens and underscores.";
325
341
  function trackProblem(event, props) {
326
- if (typeof event !== "string" || !TRACK_EVENT.test(event)) return "An event name is 1 to 32 lowercase letters, digits and underscores, starting with a letter.";
327
- if (props === void 0) return null;
328
- if (!props || typeof props !== "object" || Array.isArray(props)) return "Props are an object.";
329
- const entries = Object.entries(props);
330
- if (entries.length > TRACK_MAX_PROPS) return `At most ${TRACK_MAX_PROPS} props.`;
331
- for (const [k, v] of entries) {
332
- if (!TRACK_PROP_KEY.test(k)) return `Prop "${k}": names are lowercase letters, digits and underscores.`;
333
- if (typeof v === "string") {
334
- if (v.length > TRACK_MAX_STRING) return `Prop "${k}": text is at most ${TRACK_MAX_STRING} characters.`;
335
- } else if (typeof v === "number") {
336
- if (!Number.isFinite(v)) return `Prop "${k}": numbers are finite.`;
337
- } else if (typeof v !== "boolean") {
338
- return `Prop "${k}": a prop is text, a number or true/false.`;
342
+ if (typeof event !== "string" || !TRACK_EVENT.test(event))
343
+ return "An event name is 1 to 32 lowercase letters, digits and underscores, starting with a letter.";
344
+ if (props === void 0)
345
+ return null;
346
+ if (!props || typeof props !== "object" || Array.isArray(props))
347
+ return "Props are an object.";
348
+ const entries = Object.entries(props);
349
+ if (entries.length > TRACK_MAX_PROPS)
350
+ return `At most ${TRACK_MAX_PROPS} props.`;
351
+ for (const [k, v] of entries) {
352
+ if (!TRACK_PROP_KEY.test(k))
353
+ return `Prop "${k}": names are lowercase letters, digits and underscores.`;
354
+ if (typeof v === "string") {
355
+ if (v.length > TRACK_MAX_STRING)
356
+ return `Prop "${k}": text is at most ${TRACK_MAX_STRING} characters.`;
357
+ }
358
+ else if (typeof v === "number") {
359
+ if (!Number.isFinite(v))
360
+ return `Prop "${k}": numbers are finite.`;
361
+ }
362
+ else if (typeof v !== "boolean") {
363
+ return `Prop "${k}": a prop is text, a number or true/false.`;
364
+ }
339
365
  }
340
- }
341
- return null;
366
+ return null;
342
367
  }
343
368
  function finishProblem(r) {
344
- const x = r;
345
- if (!x || typeof x !== "object") return "finish takes { score, seconds }.";
346
- if (typeof x.score !== "number" || !Number.isFinite(x.score) || x.score < 0 || x.score > 1e9) return "score is a number from 0 to 1,000,000,000.";
347
- if (typeof x.seconds !== "number" || !Number.isFinite(x.seconds) || x.seconds < 0 || x.seconds > 86400) return "seconds is a number from 0 to 86,400.";
348
- return null;
369
+ const x = r;
370
+ if (!x || typeof x !== "object")
371
+ return "finish takes { score, seconds }.";
372
+ if (typeof x.score !== "number" || !Number.isFinite(x.score) || x.score < 0 || x.score > 1000000000)
373
+ return "score is a number from 0 to 1,000,000,000.";
374
+ if (typeof x.seconds !== "number" || !Number.isFinite(x.seconds) || x.seconds < 0 || x.seconds > 86400)
375
+ return "seconds is a number from 0 to 86,400.";
376
+ return null;
349
377
  }
350
378
  var isObj2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
351
379
  function parseHostMessage(data) {
352
- let m = data;
353
- if (typeof data === "string") {
354
- if (data.length > MAX_MESSAGE_BYTES || utf8Bytes(data) > MAX_MESSAGE_BYTES) return null;
355
- try {
356
- m = JSON.parse(data);
357
- } catch {
358
- return null;
380
+ let m = data;
381
+ if (typeof data === "string") {
382
+ if (data.length > MAX_MESSAGE_BYTES || utf8Bytes(data) > MAX_MESSAGE_BYTES)
383
+ return null;
384
+ try {
385
+ m = JSON.parse(data);
386
+ }
387
+ catch {
388
+ return null;
389
+ }
359
390
  }
360
- } else if (jsonBytes(data) > MAX_MESSAGE_BYTES) {
361
- return null;
362
- }
363
- if (!isObj2(m) || m.seek !== PROTOCOL || m.v !== PROTOCOL_VERSION) return null;
364
- if (typeof m.event === "string") {
365
- return m.event === "pause" || m.event === "resume" || m.event === "exit" ? { seek: PROTOCOL, v: PROTOCOL_VERSION, event: m.event } : null;
366
- }
367
- if (typeof m.re !== "string" || m.re.length > 64 || typeof m.ok !== "boolean") return null;
368
- if (m.ok) return { seek: PROTOCOL, v: PROTOCOL_VERSION, re: m.re, ok: true, payload: m.payload };
369
- const e = isObj2(m.error) ? m.error : {};
370
- return {
371
- seek: PROTOCOL,
372
- v: PROTOCOL_VERSION,
373
- re: m.re,
374
- ok: false,
375
- error: { code: typeof e.code === "string" ? e.code.slice(0, 64) : "host_error", message: typeof e.message === "string" ? e.message.slice(0, 500) : "The host refused." }
376
- };
391
+ else if (jsonBytes(data) > MAX_MESSAGE_BYTES) {
392
+ return null;
393
+ }
394
+ if (!isObj2(m) || m.seek !== PROTOCOL || m.v !== PROTOCOL_VERSION)
395
+ return null;
396
+ if (typeof m.event === "string") {
397
+ return m.event === "pause" || m.event === "resume" || m.event === "exit" ? { seek: PROTOCOL, v: PROTOCOL_VERSION, event: m.event } : null;
398
+ }
399
+ if (typeof m.re !== "string" || m.re.length > 64 || typeof m.ok !== "boolean")
400
+ return null;
401
+ if (m.ok)
402
+ return { seek: PROTOCOL, v: PROTOCOL_VERSION, re: m.re, ok: true, payload: m.payload };
403
+ const e = isObj2(m.error) ? m.error : {};
404
+ return {
405
+ seek: PROTOCOL,
406
+ v: PROTOCOL_VERSION,
407
+ re: m.re,
408
+ ok: false,
409
+ error: { code: typeof e.code === "string" ? e.code.slice(0, 64) : "host_error", message: typeof e.message === "string" ? e.message.slice(0, 500) : "The host refused." }
410
+ };
377
411
  }
378
412
  function parseGameRequest(data) {
379
- let m = data;
380
- if (typeof data === "string") {
381
- if (utf8Bytes(data) > MAX_MESSAGE_BYTES) return null;
382
- try {
383
- m = JSON.parse(data);
384
- } catch {
385
- return null;
413
+ let m = data;
414
+ if (typeof data === "string") {
415
+ if (utf8Bytes(data) > MAX_MESSAGE_BYTES)
416
+ return null;
417
+ try {
418
+ m = JSON.parse(data);
419
+ }
420
+ catch {
421
+ return null;
422
+ }
386
423
  }
387
- } else if (jsonBytes(data) > MAX_MESSAGE_BYTES) {
388
- return null;
389
- }
390
- if (!isObj2(m) || m.seek !== PROTOCOL || m.v !== PROTOCOL_VERSION || typeof m.id !== "string" || !m.id || m.id.length > 64) return null;
391
- const types = ["ready", "finish", "storage.get", "storage.set", "storage.remove", "track", "heartbeat", "diagnostics"];
392
- if (!types.includes(m.type)) return null;
393
- const p = m.payload;
394
- switch (m.type) {
395
- case "finish":
396
- if (finishProblem(p)) return null;
397
- break;
398
- case "storage.get":
399
- case "storage.remove":
400
- if (!isObj2(p) || storageKeyProblem(p.key)) return null;
401
- break;
402
- case "storage.set":
403
- if (!isObj2(p) || storageKeyProblem(p.key) || !isJsonValue(p.value)) return null;
404
- break;
405
- case "track":
406
- if (!isObj2(p) || trackProblem(p.event, p.props)) return null;
407
- break;
408
- case "heartbeat":
409
- if (!isObj2(p) || p.ended !== void 0 && typeof p.ended !== "boolean") return null;
410
- break;
411
- case "diagnostics": {
412
- const batch = parseDiagnosticsBatch(p, { from: "game" });
413
- if (!batch || batch.hostOnly) return null;
414
- return { seek: PROTOCOL, v: PROTOCOL_VERSION, id: m.id, type: "diagnostics", payload: { entries: batch.entries, ...batch.dropped ? { dropped: batch.dropped } : {} } };
424
+ else if (jsonBytes(data) > MAX_MESSAGE_BYTES) {
425
+ return null;
426
+ }
427
+ if (!isObj2(m) || m.seek !== PROTOCOL || m.v !== PROTOCOL_VERSION || typeof m.id !== "string" || !m.id || m.id.length > 64)
428
+ return null;
429
+ const types = ["ready", "finish", "storage.get", "storage.set", "storage.remove", "track", "heartbeat", "diagnostics"];
430
+ if (!types.includes(m.type))
431
+ return null;
432
+ const p = m.payload;
433
+ switch (m.type) {
434
+ case "finish":
435
+ if (finishProblem(p))
436
+ return null;
437
+ break;
438
+ case "storage.get":
439
+ case "storage.remove":
440
+ if (!isObj2(p) || storageKeyProblem(p.key))
441
+ return null;
442
+ break;
443
+ case "storage.set":
444
+ if (!isObj2(p) || storageKeyProblem(p.key) || !isJsonValue(p.value))
445
+ return null;
446
+ break;
447
+ case "track":
448
+ if (!isObj2(p) || trackProblem(p.event, p.props))
449
+ return null;
450
+ break;
451
+ case "heartbeat":
452
+ if (!isObj2(p) || p.ended !== void 0 && typeof p.ended !== "boolean")
453
+ return null;
454
+ break;
455
+ case "diagnostics": {
456
+ const batch = parseDiagnosticsBatch(p, { from: "game" });
457
+ if (!batch || batch.hostOnly)
458
+ return null;
459
+ return { seek: PROTOCOL, v: PROTOCOL_VERSION, id: m.id, type: "diagnostics", payload: { entries: batch.entries, ...batch.dropped ? { dropped: batch.dropped } : {} } };
460
+ }
461
+ case "ready":
462
+ break;
415
463
  }
416
- case "ready":
417
- break;
418
- }
419
- return { seek: PROTOCOL, v: PROTOCOL_VERSION, id: m.id, type: m.type, payload: p };
464
+ return { seek: PROTOCOL, v: PROTOCOL_VERSION, id: m.id, type: m.type, payload: p };
420
465
  }
421
466
  var fromB64url = (s) => {
422
- const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
423
- return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)));
467
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
468
+ return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)));
424
469
  };
425
470
  function decodeLaunchToken(token) {
426
- if (typeof token !== "string" || token.length > MAX_TOKEN_LENGTH) return null;
427
- const parts = token.split(".");
428
- if (parts.length !== 3 || !parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p))) return null;
429
- let c;
430
- try {
431
- c = JSON.parse(fromB64url(parts[1]));
432
- } catch {
433
- return null;
434
- }
435
- if (!isObj2(c)) return null;
436
- const { run, seed, lang, sub, game, version, exp } = c;
437
- if (typeof run !== "string" || typeof sub !== "string" || typeof game !== "string" || typeof version !== "string" || typeof lang !== "string") return null;
438
- if (typeof seed !== "number" || !Number.isSafeInteger(seed) || typeof exp !== "number") return null;
439
- return { runId: run, seed, lang, sub, game, version, expiresAt: exp * 1e3 };
471
+ if (typeof token !== "string" || token.length > MAX_TOKEN_LENGTH)
472
+ return null;
473
+ const parts = token.split(".");
474
+ if (parts.length !== 3 || !parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p)))
475
+ return null;
476
+ let c;
477
+ try {
478
+ c = JSON.parse(fromB64url(parts[1]));
479
+ }
480
+ catch {
481
+ return null;
482
+ }
483
+ if (!isObj2(c))
484
+ return null;
485
+ const { run, seed, lang, sub, game, version, exp } = c;
486
+ if (typeof run !== "string" || typeof sub !== "string" || typeof game !== "string" || typeof version !== "string" || typeof lang !== "string")
487
+ return null;
488
+ if (typeof seed !== "number" || !Number.isSafeInteger(seed) || typeof exp !== "number")
489
+ return null;
490
+ return { runId: run, seed, lang, sub, game, version, expiresAt: exp * 1000 };
440
491
  }
441
-
442
- // src/collector.ts
443
492
  var PRE_READY_ENTRIES = 20;
444
493
  function createCollector(opts) {
445
- const now = opts.now ?? (() => Date.now());
446
- const timers = opts.timers ?? {
447
- setInterval: (fn, ms) => setInterval(fn, ms),
448
- clearInterval: (t) => clearInterval(t)
449
- };
450
- const flushMs = Math.max(1e3, opts.flushMs ?? 5e3);
451
- const heartbeatMs = Math.min(25e3, Math.max(5e3, opts.heartbeatMs ?? 1e4));
452
- const rate = opts.rate ?? { perSecond: 5, burst: 30 };
453
- const secrets = () => opts.secrets?.() ?? [];
454
- let session = null;
455
- let ended = false;
456
- let queue = [];
457
- let dropped = 0;
458
- let droppedTotal = 0;
459
- let sent = 0;
460
- let batches = 0;
461
- let seq = 0;
462
- let errors = 0;
463
- let logs = 0;
464
- let tokens = rate.burst;
465
- let lastRefill = now();
466
- let flushTimer = null;
467
- let beatTimer = null;
468
- const drop = () => {
469
- dropped++;
470
- droppedTotal++;
471
- return false;
472
- };
473
- const allow = () => {
474
- const t = now();
475
- tokens = Math.min(rate.burst, tokens + (t - lastRefill) / 1e3 * rate.perSecond);
476
- lastRefill = t;
477
- if (tokens < 1) return false;
478
- tokens -= 1;
479
- return true;
480
- };
481
- const push = (raw, limited = true) => {
482
- if (ended) return false;
483
- if (limited && !allow()) return drop();
484
- const entry = cleanDiagnosticEntry(raw, secrets());
485
- if (!entry) return drop();
486
- if (!session && queue.length >= PRE_READY_ENTRIES) return drop();
487
- queue.push(entry);
488
- if (session && queue.length >= DIAG_LIMITS.batchEntries) void flush();
489
- return true;
490
- };
491
- const bytes = (b) => new TextEncoder().encode(JSON.stringify(b)).byteLength;
492
- async function flush(final = false) {
493
- if (!session || !queue.length && !dropped) return;
494
- const pending = queue;
495
- queue = [];
496
- while (pending.length || dropped) {
497
- const batch = { session, seq: ++seq, entries: [], ...dropped ? { dropped } : {} };
498
- dropped = 0;
499
- while (pending.length && batch.entries.length < DIAG_LIMITS.batchEntries) {
500
- batch.entries.push(pending[0]);
501
- if (batch.entries.length > 1 && bytes(batch) > DIAG_LIMITS.batchBytes) {
502
- batch.entries.pop();
503
- break;
494
+ const now = opts.now ?? (() => Date.now());
495
+ const timers = opts.timers ?? {
496
+ setInterval: (fn, ms) => setInterval(fn, ms),
497
+ clearInterval: (t) => clearInterval(t)
498
+ };
499
+ const flushMs = Math.max(1000, opts.flushMs ?? 5000);
500
+ const heartbeatMs = Math.min(25000, Math.max(5000, opts.heartbeatMs ?? 10000));
501
+ const rate = opts.rate ?? { perSecond: 5, burst: 30 };
502
+ const secrets = () => opts.secrets?.() ?? [];
503
+ let session = null;
504
+ let ended = false;
505
+ let queue = [];
506
+ let dropped = 0;
507
+ let droppedTotal = 0;
508
+ let sent = 0;
509
+ let batches = 0;
510
+ let seq = 0;
511
+ let errors = 0;
512
+ let logs = 0;
513
+ let tokens = rate.burst;
514
+ let lastRefill = now();
515
+ let flushTimer = null;
516
+ let beatTimer = null;
517
+ const drop = () => {
518
+ dropped++;
519
+ droppedTotal++;
520
+ return false;
521
+ };
522
+ const allow = () => {
523
+ const t = now();
524
+ tokens = Math.min(rate.burst, tokens + (t - lastRefill) / 1000 * rate.perSecond);
525
+ lastRefill = t;
526
+ if (tokens < 1)
527
+ return false;
528
+ tokens -= 1;
529
+ return true;
530
+ };
531
+ const push = (raw, limited = true) => {
532
+ if (ended)
533
+ return false;
534
+ if (limited && !allow())
535
+ return drop();
536
+ const entry = cleanDiagnosticEntry(raw, secrets());
537
+ if (!entry)
538
+ return drop();
539
+ if (!session && queue.length >= PRE_READY_ENTRIES)
540
+ return drop();
541
+ queue.push(entry);
542
+ if (session && queue.length >= DIAG_LIMITS.batchEntries)
543
+ void flush();
544
+ return true;
545
+ };
546
+ const bytes = (b) => new TextEncoder().encode(JSON.stringify(b)).byteLength;
547
+ async function flush(final = false) {
548
+ if (!session || !queue.length && !dropped)
549
+ return;
550
+ const pending = queue;
551
+ queue = [];
552
+ while (pending.length || dropped) {
553
+ const batch = { session, seq: ++seq, entries: [], ...dropped ? { dropped } : {} };
554
+ dropped = 0;
555
+ while (pending.length && batch.entries.length < DIAG_LIMITS.batchEntries) {
556
+ batch.entries.push(pending[0]);
557
+ if (batch.entries.length > 1 && bytes(batch) > DIAG_LIMITS.batchBytes) {
558
+ batch.entries.pop();
559
+ break;
560
+ }
561
+ pending.shift();
562
+ }
563
+ batches++;
564
+ sent += batch.entries.length;
565
+ try {
566
+ await opts.send(batch, final && !pending.length);
567
+ }
568
+ catch {
569
+ }
504
570
  }
505
- pending.shift();
506
- }
507
- batches++;
508
- sent += batch.entries.length;
509
- try {
510
- await opts.send(batch, final && !pending.length);
511
- } catch {
512
- }
513
571
  }
514
- }
515
- const collector = {
516
- get session() {
517
- return session;
518
- },
519
- get ended() {
520
- return ended;
521
- },
522
- start(id, readyMs) {
523
- if (session || ended) return;
524
- session = id;
525
- const start = cleanDiagnosticEntry({ kind: "start", at: now(), ...readyMs !== void 0 ? { ready_ms: readyMs } : {} });
526
- queue.unshift(start ?? { kind: "start", at: now() });
527
- void flush();
528
- flushTimer = timers.setInterval(() => void flush(), flushMs);
529
- beatTimer = timers.setInterval(() => {
530
- if (ended) return;
531
- let sample = {};
532
- try {
533
- sample = opts.sample?.() ?? {};
534
- } catch {
535
- }
536
- queue.push(cleanDiagnosticEntry({ ...sample, kind: "heartbeat", at: now() }) ?? { kind: "heartbeat", at: now() });
537
- try {
538
- opts.beat?.(false);
539
- } catch {
540
- }
541
- }, heartbeatMs);
542
- try {
543
- opts.beat?.(false);
544
- } catch {
545
- }
546
- },
547
- error(e) {
548
- if (ended) return false;
549
- if (errors >= DIAG_LIMITS.sessionErrors) return drop();
550
- const entry = cleanDiagnosticEntry({ kind: "error", at: now(), ...e }, secrets());
551
- if (!entry || entry.kind !== "error") return drop();
552
- const same = queue.find((q) => q.kind === "error" && q.source === entry.source && q.message === entry.message && q.stack === entry.stack);
553
- if (same && (same.count ?? 1) < DIAG_LIMITS.repeat) {
554
- same.count = (same.count ?? 1) + 1;
555
- return true;
556
- }
557
- if (!push(entry)) return false;
558
- errors++;
559
- return true;
560
- },
561
- log(level, message, data, source = "log") {
562
- if (logs >= DIAG_LIMITS.sessionLogs) return drop();
563
- if (!push({ kind: "log", at: now(), level, message: typeof message === "string" && message ? message : String(message || "(empty)"), ...data !== void 0 ? { data } : {}, source })) return false;
564
- logs++;
565
- return true;
566
- },
567
- async end(reason) {
568
- if (ended) return;
569
- if (session) queue.push({ kind: "end", at: now(), reason });
570
- ended = true;
571
- if (flushTimer !== null) timers.clearInterval(flushTimer);
572
- if (beatTimer !== null) timers.clearInterval(beatTimer);
573
- flushTimer = beatTimer = null;
574
- try {
575
- opts.beat?.(true);
576
- } catch {
577
- }
578
- await flush(true);
579
- },
580
- flush,
581
- stats: () => ({ sent, dropped: droppedTotal, errors, logs, batches })
582
- };
583
- return collector;
572
+ const collector = {
573
+ get session() {
574
+ return session;
575
+ },
576
+ get ended() {
577
+ return ended;
578
+ },
579
+ start(id, readyMs) {
580
+ if (session || ended)
581
+ return;
582
+ session = id;
583
+ const start = cleanDiagnosticEntry({ kind: "start", at: now(), ...readyMs !== void 0 ? { ready_ms: readyMs } : {} });
584
+ queue.unshift(start ?? { kind: "start", at: now() });
585
+ void flush();
586
+ flushTimer = timers.setInterval(() => void flush(), flushMs);
587
+ beatTimer = timers.setInterval(() => {
588
+ if (ended)
589
+ return;
590
+ let sample = {};
591
+ try {
592
+ sample = opts.sample?.() ?? {};
593
+ }
594
+ catch {
595
+ }
596
+ queue.push(cleanDiagnosticEntry({ ...sample, kind: "heartbeat", at: now() }) ?? { kind: "heartbeat", at: now() });
597
+ try {
598
+ opts.beat?.(false);
599
+ }
600
+ catch {
601
+ }
602
+ }, heartbeatMs);
603
+ try {
604
+ opts.beat?.(false);
605
+ }
606
+ catch {
607
+ }
608
+ },
609
+ error(e) {
610
+ if (ended)
611
+ return false;
612
+ if (errors >= DIAG_LIMITS.sessionErrors)
613
+ return drop();
614
+ const entry = cleanDiagnosticEntry({ kind: "error", at: now(), ...e }, secrets());
615
+ if (!entry || entry.kind !== "error")
616
+ return drop();
617
+ const same = queue.find((q) => q.kind === "error" && q.source === entry.source && q.message === entry.message && q.stack === entry.stack);
618
+ if (same && (same.count ?? 1) < DIAG_LIMITS.repeat) {
619
+ same.count = (same.count ?? 1) + 1;
620
+ return true;
621
+ }
622
+ if (!push(entry))
623
+ return false;
624
+ errors++;
625
+ return true;
626
+ },
627
+ log(level, message, data, source = "log") {
628
+ if (logs >= DIAG_LIMITS.sessionLogs)
629
+ return drop();
630
+ if (!push({ kind: "log", at: now(), level, message: typeof message === "string" && message ? message : String(message || "(empty)"), ...data !== void 0 ? { data } : {}, source }))
631
+ return false;
632
+ logs++;
633
+ return true;
634
+ },
635
+ async end(reason) {
636
+ if (ended)
637
+ return;
638
+ if (session)
639
+ queue.push({ kind: "end", at: now(), reason });
640
+ ended = true;
641
+ if (flushTimer !== null)
642
+ timers.clearInterval(flushTimer);
643
+ if (beatTimer !== null)
644
+ timers.clearInterval(beatTimer);
645
+ flushTimer = beatTimer = null;
646
+ try {
647
+ opts.beat?.(true);
648
+ }
649
+ catch {
650
+ }
651
+ await flush(true);
652
+ },
653
+ flush,
654
+ stats: () => ({ sent, dropped: droppedTotal, errors, logs, batches })
655
+ };
656
+ return collector;
584
657
  }
585
-
586
- // src/perf.ts
587
658
  var JANK_FRAME_MS = 50;
588
- var PAUSE_MS = 1e3;
659
+ var PAUSE_MS = 1000;
589
660
  function createPerfSampler(win) {
590
- const raf = win.requestAnimationFrame?.bind(win);
591
- const perf = win.performance;
592
- let frames = 0;
593
- let jank = 0;
594
- let visibleMs = 0;
595
- let last = null;
596
- let id = null;
597
- let stopped = false;
598
- const tick = (t) => {
599
- if (stopped) return;
600
- if (last !== null && !win.document?.hidden) {
601
- const gap = t - last;
602
- if (gap < PAUSE_MS) {
603
- frames++;
604
- visibleMs += gap;
605
- if (gap > JANK_FRAME_MS) jank++;
606
- }
607
- }
608
- last = t;
609
- id = raf(tick);
610
- };
611
- if (raf) id = raf(tick);
612
- return {
613
- sample() {
614
- const out = {};
615
- if (visibleMs >= 500) {
616
- out.fps = Math.min(240, Math.round(frames * 1e3 / visibleMs));
617
- out.jank = jank;
618
- }
619
- const heap = perf?.memory?.usedJSHeapSize;
620
- if (typeof heap === "number" && Number.isFinite(heap) && heap > 0) out.heap_mb = Math.round(heap / (1024 * 1024));
621
- frames = 0;
622
- jank = 0;
623
- visibleMs = 0;
624
- return out;
625
- },
626
- sinceLoad() {
627
- const now = perf?.now();
628
- return typeof now === "number" && Number.isFinite(now) ? Math.round(now) : void 0;
629
- },
630
- stop() {
631
- stopped = true;
632
- if (id !== null) win.cancelAnimationFrame?.(id);
633
- }
634
- };
661
+ const raf = win.requestAnimationFrame?.bind(win);
662
+ const perf = win.performance;
663
+ let frames = 0;
664
+ let jank = 0;
665
+ let visibleMs = 0;
666
+ let last = null;
667
+ let id = null;
668
+ let stopped = false;
669
+ const tick = (t) => {
670
+ if (stopped)
671
+ return;
672
+ if (last !== null && !win.document?.hidden) {
673
+ const gap = t - last;
674
+ if (gap < PAUSE_MS) {
675
+ frames++;
676
+ visibleMs += gap;
677
+ if (gap > JANK_FRAME_MS)
678
+ jank++;
679
+ }
680
+ }
681
+ last = t;
682
+ id = raf(tick);
683
+ };
684
+ if (raf)
685
+ id = raf(tick);
686
+ return {
687
+ sample() {
688
+ const out = {};
689
+ if (visibleMs >= 500) {
690
+ out.fps = Math.min(240, Math.round(frames * 1000 / visibleMs));
691
+ out.jank = jank;
692
+ }
693
+ const heap = perf?.memory?.usedJSHeapSize;
694
+ if (typeof heap === "number" && Number.isFinite(heap) && heap > 0)
695
+ out.heap_mb = Math.round(heap / (1024 * 1024));
696
+ frames = 0;
697
+ jank = 0;
698
+ visibleMs = 0;
699
+ return out;
700
+ },
701
+ sinceLoad() {
702
+ const now = perf?.now();
703
+ return typeof now === "number" && Number.isFinite(now) ? Math.round(now) : void 0;
704
+ },
705
+ stop() {
706
+ stopped = true;
707
+ if (id !== null)
708
+ win.cancelAnimationFrame?.(id);
709
+ }
710
+ };
635
711
  }
636
-
637
- // src/bridge.ts
638
712
  var BridgeError = class extends Error {
639
- constructor(code, message) {
640
- super(message);
641
- this.code = code;
642
- this.name = "BridgeError";
643
- }
713
+ constructor(code, message) {
714
+ super(message);
715
+ this.code = code;
716
+ this.name = "BridgeError";
717
+ }
644
718
  };
645
719
  var TRACK_PER_LAUNCH = 200;
646
720
  function createBridge(opts = {}) {
647
- const win = opts.window ?? globalThis;
648
- const hostOrigins = [...opts.hostOrigins ?? DEFAULT_HOST_ORIGINS];
649
- const doFetch = opts.fetch ?? ((...a) => fetch(...a));
650
- const timeoutMs = opts.timeoutMs ?? 1e4;
651
- const native = typeof win.ReactNativeWebView?.postMessage === "function";
652
- const parent = !native && win.parent && win.parent !== win ? win.parent : null;
653
- let seq = 0;
654
- let lockedOrigin = null;
655
- let readyPromise = null;
656
- let launchInfo = null;
657
- let token = "";
658
- let transport = "proxy";
659
- let finished = false;
660
- let tracked = 0;
661
- let closed = false;
662
- const pending = /* @__PURE__ */ new Map();
663
- const handlers = { pause: /* @__PURE__ */ new Set(), resume: /* @__PURE__ */ new Set(), exit: /* @__PURE__ */ new Set() };
664
- const vopts = opts.vitals === false ? null : opts.vitals ?? {};
665
- let vitals = null;
666
- const trusted = (e) => {
667
- if (native) return e.source === null || e.source === void 0;
668
- if (!parent || e.source !== parent) return false;
669
- return lockedOrigin ? e.origin === lockedOrigin : hostOrigins.includes(e.origin);
670
- };
671
- const onMessage = (e) => {
672
- if (closed || !trusted(e)) return;
673
- const m = parseHostMessage(e.data);
674
- if (!m) return;
675
- if ("event" in m) {
676
- if (m.event === "exit") void vitals?.end("host_exit");
677
- for (const fn of handlers[m.event]) {
678
- try {
679
- fn();
680
- } catch {
721
+ const win = opts.window ?? globalThis;
722
+ const hostOrigins = [...opts.hostOrigins ?? DEFAULT_HOST_ORIGINS];
723
+ const doFetch = opts.fetch ?? ((...a) => fetch(...a));
724
+ const timeoutMs = opts.timeoutMs ?? 10000;
725
+ const native = typeof win.ReactNativeWebView?.postMessage === "function";
726
+ const parent = !native && win.parent && win.parent !== win ? win.parent : null;
727
+ let seq = 0;
728
+ let lockedOrigin = null;
729
+ let readyPromise = null;
730
+ let launchInfo = null;
731
+ let token = "";
732
+ let transport = "proxy";
733
+ let finished = false;
734
+ let tracked = 0;
735
+ let closed = false;
736
+ const pending = new Map();
737
+ const handlers = { pause: new Set(), resume: new Set(), exit: new Set() };
738
+ const vopts = opts.vitals === false ? null : opts.vitals ?? {};
739
+ let vitals = null;
740
+ const trusted = (e) => {
741
+ if (native)
742
+ return e.source === null || e.source === void 0;
743
+ if (!parent || e.source !== parent)
744
+ return false;
745
+ return lockedOrigin ? e.origin === lockedOrigin : hostOrigins.includes(e.origin);
746
+ };
747
+ const onMessage = (e) => {
748
+ if (closed || !trusted(e))
749
+ return;
750
+ const m = parseHostMessage(e.data);
751
+ if (!m)
752
+ return;
753
+ if ("event" in m) {
754
+ if (m.event === "exit")
755
+ void vitals?.end("host_exit");
756
+ for (const fn of handlers[m.event]) {
757
+ try {
758
+ fn();
759
+ }
760
+ catch {
761
+ }
762
+ }
763
+ return;
681
764
  }
682
- }
683
- return;
684
- }
685
- const p = pending.get(m.re);
686
- if (!p) return;
687
- if (!native && !lockedOrigin) lockedOrigin = e.origin;
688
- pending.delete(m.re);
689
- clearTimeout(p.timer);
690
- if (m.ok) p.resolve(m.payload);
691
- else p.reject(new BridgeError(m.error.code, m.error.message));
692
- };
693
- win.addEventListener("message", onMessage);
694
- if (native) win.document?.addEventListener("message", onMessage);
695
- function post(type, payload, wait = true, ms = timeoutMs) {
696
- if (closed) return Promise.reject(new BridgeError("closed", "The bridge is closed."));
697
- if (!native && !parent) return Promise.reject(new BridgeError("no_host", "Not running inside Seek or the simulator."));
698
- const msg = { seek: PROTOCOL, v: PROTOCOL_VERSION, id: `g${++seq}-${Math.random().toString(36).slice(2, 10)}`, type, payload };
699
- const json = JSON.stringify(msg);
700
- if (utf8Bytes(json) > MAX_MESSAGE_BYTES) return Promise.reject(new BridgeError("too_big", `A message is at most ${MAX_MESSAGE_BYTES} bytes.`));
701
- const sent = new Promise((resolve, reject) => {
702
- if (!wait) return resolve(void 0);
703
- const timer = setTimeout(() => {
704
- pending.delete(msg.id);
705
- reject(new BridgeError("timeout", `The host did not answer ${type}.`));
706
- }, ms);
707
- pending.set(msg.id, { resolve, reject, timer });
708
- });
709
- if (native) win.ReactNativeWebView.postMessage(json);
710
- else if (lockedOrigin) parent.postMessage(msg, lockedOrigin);
711
- else for (const origin of hostOrigins) parent.postMessage(msg, origin);
712
- return sent;
713
- }
714
- const needLaunch = () => {
715
- if (!launchInfo) throw new BridgeError("not_ready", "Call ready() first.");
716
- return launchInfo;
717
- };
718
- async function proxy(method, path, body) {
719
- let res;
720
- try {
721
- res = await doFetch(`${PROXY_ORIGIN}${path}`, {
722
- method,
723
- headers: { authorization: `Bearer ${token}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
724
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
725
- credentials: "omit",
726
- mode: "cors",
727
- ...method === "POST" ? { keepalive: true } : {}
728
- });
729
- } catch {
730
- throw new BridgeError("offline", "Could not reach Seek.");
731
- }
732
- let data = {};
733
- try {
734
- data = await res.json();
735
- } catch {
765
+ const p = pending.get(m.re);
766
+ if (!p)
767
+ return;
768
+ if (!native && !lockedOrigin)
769
+ lockedOrigin = e.origin;
770
+ pending.delete(m.re);
771
+ clearTimeout(p.timer);
772
+ if (m.ok)
773
+ p.resolve(m.payload);
774
+ else
775
+ p.reject(new BridgeError(m.error.code, m.error.message));
776
+ };
777
+ win.addEventListener("message", onMessage);
778
+ if (native)
779
+ win.document?.addEventListener("message", onMessage);
780
+ function post(type, payload, wait = true, ms = timeoutMs) {
781
+ if (closed)
782
+ return Promise.reject(new BridgeError("closed", "The bridge is closed."));
783
+ if (!native && !parent)
784
+ return Promise.reject(new BridgeError("no_host", "Not running inside Seek or the simulator."));
785
+ const msg = { seek: PROTOCOL, v: PROTOCOL_VERSION, id: `g${++seq}-${Math.random().toString(36).slice(2, 10)}`, type, payload };
786
+ const json = JSON.stringify(msg);
787
+ if (utf8Bytes(json) > MAX_MESSAGE_BYTES)
788
+ return Promise.reject(new BridgeError("too_big", `A message is at most ${MAX_MESSAGE_BYTES} bytes.`));
789
+ const sent = new Promise((resolve, reject) => {
790
+ if (!wait)
791
+ return resolve(void 0);
792
+ const timer = setTimeout(() => {
793
+ pending.delete(msg.id);
794
+ reject(new BridgeError("timeout", `The host did not answer ${type}.`));
795
+ }, ms);
796
+ pending.set(msg.id, { resolve, reject, timer });
797
+ });
798
+ if (native)
799
+ win.ReactNativeWebView.postMessage(json);
800
+ else if (lockedOrigin)
801
+ parent.postMessage(msg, lockedOrigin);
802
+ else
803
+ for (const origin of hostOrigins)
804
+ parent.postMessage(msg, origin);
805
+ return sent;
736
806
  }
737
- if (!res.ok) throw new BridgeError(typeof data.error === "string" ? data.error : `http_${res.status}`, typeof data.message === "string" ? data.message : "Seek refused.");
738
- return data;
739
- }
740
- const sendBatch = (batch) => transport === "host" ? post("diagnostics", batch, false) : proxy("POST", "/v1/diagnostics", batch);
741
- const perf = vopts && vopts.performance !== false ? createPerfSampler(win) : null;
742
- if (vopts) {
743
- vitals = createCollector({
744
- ...perf ? { sample: () => perf.sample() } : {},
745
- send: (batch) => sendBatch(batch),
746
- beat: (ended) => {
747
- if (!closed) post("heartbeat", ended ? { ended: true } : {}, false).catch(() => void 0);
748
- },
749
- secrets: () => token ? [token] : [],
750
- ...vopts.now ? { now: vopts.now } : {},
751
- ...vopts.timers ? { timers: vopts.timers } : {},
752
- ...vopts.heartbeatSeconds ? { heartbeatMs: vopts.heartbeatSeconds * 1e3 } : {},
753
- ...vopts.flushSeconds ? { flushMs: vopts.flushSeconds * 1e3 } : {}
754
- });
755
- }
756
- const errorOf = (v) => {
757
- if (v && typeof v === "object" && typeof v.message === "string") {
758
- const e = v;
759
- return { message: `${e.name && e.name !== "Error" ? `${e.name}: ` : ""}${e.message || "Error"}`, ...typeof e.stack === "string" ? { stack: e.stack } : {} };
807
+ const needLaunch = () => {
808
+ if (!launchInfo)
809
+ throw new BridgeError("not_ready", "Call ready() first.");
810
+ return launchInfo;
811
+ };
812
+ async function proxy(method, path, body) {
813
+ let res;
814
+ try {
815
+ res = await doFetch(`${PROXY_ORIGIN}${path}`, {
816
+ method,
817
+ headers: { authorization: `Bearer ${token}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
818
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
819
+ credentials: "omit",
820
+ mode: "cors",
821
+ ...method === "POST" ? { keepalive: true } : {}
822
+ });
823
+ }
824
+ catch {
825
+ throw new BridgeError("offline", "Could not reach Seek.");
826
+ }
827
+ let data = {};
828
+ try {
829
+ data = await res.json();
830
+ }
831
+ catch {
832
+ }
833
+ if (!res.ok)
834
+ throw new BridgeError(typeof data.error === "string" ? data.error : `http_${res.status}`, typeof data.message === "string" ? data.message : "Seek refused.");
835
+ return data;
760
836
  }
761
- if (typeof v === "string") return { message: v || "Error" };
762
- try {
763
- return { message: JSON.stringify(v)?.slice(0, 500) ?? String(v) };
764
- } catch {
765
- return { message: String(v) };
837
+ const sendBatch = (batch) => transport === "host" ? post("diagnostics", batch, false) : proxy("POST", "/v1/diagnostics", batch);
838
+ const perf = vopts && vopts.performance !== false ? createPerfSampler(win) : null;
839
+ if (vopts) {
840
+ vitals = createCollector({
841
+ ...perf ? { sample: () => perf.sample() } : {},
842
+ send: (batch) => sendBatch(batch),
843
+ beat: (ended) => {
844
+ if (!closed)
845
+ post("heartbeat", ended ? { ended: true } : {}, false).catch(() => void 0);
846
+ },
847
+ secrets: () => token ? [token] : [],
848
+ ...vopts.now ? { now: vopts.now } : {},
849
+ ...vopts.timers ? { timers: vopts.timers } : {},
850
+ ...vopts.heartbeatSeconds ? { heartbeatMs: vopts.heartbeatSeconds * 1000 } : {},
851
+ ...vopts.flushSeconds ? { flushMs: vopts.flushSeconds * 1000 } : {}
852
+ });
766
853
  }
767
- };
768
- const onError = (e) => {
769
- if (!vitals || !e || "data" in e || typeof e.message !== "string") return;
770
- const err = e.error ? errorOf(e.error) : { message: e.message || "Script error" };
771
- vitals.error({
772
- source: "error",
773
- message: err.message || e.message,
774
- ...err.stack ? { stack: err.stack } : {},
775
- ...typeof e.filename === "string" && e.filename ? { file: e.filename } : {},
776
- ...Number.isInteger(e.lineno) ? { line: e.lineno } : {},
777
- ...Number.isInteger(e.colno) ? { col: e.colno } : {}
778
- });
779
- };
780
- const onRejection = (e) => {
781
- if (!vitals || !e || "data" in e || !("reason" in e)) return;
782
- const err = errorOf(e.reason);
783
- vitals.error({ source: "unhandledrejection", message: `Unhandled rejection: ${err.message}`, ...err.stack ? { stack: err.stack } : {} });
784
- };
785
- const onPageHide = (e) => {
786
- if (!vitals || !e || "data" in e || e.type !== "pagehide") return;
787
- void vitals.end("pagehide");
788
- };
789
- if (vitals && vopts?.errors !== false) {
790
- win.addEventListener("error", onError);
791
- win.addEventListener("unhandledrejection", onRejection);
792
- }
793
- if (vitals) win.addEventListener("pagehide", onPageHide);
794
- const restoreConsole = [];
795
- if (vitals && vopts?.console) {
796
- const target = vopts.consoleObject ?? globalThis.console;
797
- const wanted = new Set(vopts.console === true ? LOG_LEVELS : vopts.console.filter(isLogLevel));
798
- const levelOf = { debug: "debug", log: "info", info: "info", warn: "warn", error: "error" };
799
- let inside = false;
800
- for (const method of Object.keys(levelOf)) {
801
- const original = target?.[method];
802
- if (!target || typeof original !== "function" || !wanted.has(levelOf[method])) continue;
803
- target[method] = (...args) => {
804
- original.apply(target, args);
805
- if (inside) return;
806
- inside = true;
854
+ const errorOf = (v) => {
855
+ if (v && typeof v === "object" && typeof v.message === "string") {
856
+ const e = v;
857
+ return { message: `${e.name && e.name !== "Error" ? `${e.name}: ` : ""}${e.message || "Error"}`, ...typeof e.stack === "string" ? { stack: e.stack } : {} };
858
+ }
859
+ if (typeof v === "string")
860
+ return { message: v || "Error" };
807
861
  try {
808
- const parts = args.map((a) => typeof a === "string" ? a : errorOf(a).message);
809
- const err = args.find((a) => a instanceof Error);
810
- if (method === "error" && err) vitals.error({ source: "console", ...errorOf(err), message: parts.join(" ") });
811
- else vitals.log(levelOf[method], parts.join(" "), void 0, "console");
812
- } catch {
813
- } finally {
814
- inside = false;
862
+ return { message: JSON.stringify(v)?.slice(0, 500) ?? String(v) };
863
+ }
864
+ catch {
865
+ return { message: String(v) };
815
866
  }
816
- };
817
- restoreConsole.push(() => {
818
- target[method] = original;
819
- });
867
+ };
868
+ const onError = (e) => {
869
+ if (!vitals || !e || "data" in e || typeof e.message !== "string")
870
+ return;
871
+ const err = e.error ? errorOf(e.error) : { message: e.message || "Script error" };
872
+ vitals.error({
873
+ source: "error",
874
+ message: err.message || e.message,
875
+ ...err.stack ? { stack: err.stack } : {},
876
+ ...typeof e.filename === "string" && e.filename ? { file: e.filename } : {},
877
+ ...Number.isInteger(e.lineno) ? { line: e.lineno } : {},
878
+ ...Number.isInteger(e.colno) ? { col: e.colno } : {}
879
+ });
880
+ };
881
+ const onRejection = (e) => {
882
+ if (!vitals || !e || "data" in e || !("reason" in e))
883
+ return;
884
+ const err = errorOf(e.reason);
885
+ vitals.error({ source: "unhandledrejection", message: `Unhandled rejection: ${err.message}`, ...err.stack ? { stack: err.stack } : {} });
886
+ };
887
+ const onPageHide = (e) => {
888
+ if (!vitals || !e || "data" in e || e.type !== "pagehide")
889
+ return;
890
+ void vitals.end("pagehide");
891
+ };
892
+ if (vitals && vopts?.errors !== false) {
893
+ win.addEventListener("error", onError);
894
+ win.addEventListener("unhandledrejection", onRejection);
820
895
  }
821
- }
822
- const bridge = {
823
- ready() {
824
- if (readyPromise) return readyPromise;
825
- readyPromise = post("ready", { bridge: BRIDGE_VERSION, protocol: PROTOCOL_VERSION }).then((payload) => {
826
- const p = payload;
827
- const info = decodeLaunchToken(p?.token);
828
- if (!info) throw new BridgeError("bad_launch", "The host sent no valid launch token.");
829
- token = p.token;
830
- transport = p.transport === "host" ? "host" : "proxy";
831
- launchInfo = info;
832
- if (vitals && !vitals.session) {
833
- const readyMs = perf?.sinceLoad();
834
- void sessionIdOf(info.game, info.runId).then((id) => vitals?.start(id, readyMs)).catch(() => void 0);
896
+ if (vitals)
897
+ win.addEventListener("pagehide", onPageHide);
898
+ const restoreConsole = [];
899
+ if (vitals && vopts?.console) {
900
+ const target = vopts.consoleObject ?? globalThis.console;
901
+ const wanted = new Set(vopts.console === true ? LOG_LEVELS : vopts.console.filter(isLogLevel));
902
+ const levelOf = { debug: "debug", log: "info", info: "info", warn: "warn", error: "error" };
903
+ let inside = false;
904
+ for (const method of Object.keys(levelOf)) {
905
+ const original = target?.[method];
906
+ if (!target || typeof original !== "function" || !wanted.has(levelOf[method]))
907
+ continue;
908
+ target[method] = (...args) => {
909
+ original.apply(target, args);
910
+ if (inside)
911
+ return;
912
+ inside = true;
913
+ try {
914
+ const parts = args.map((a) => typeof a === "string" ? a : errorOf(a).message);
915
+ const err = args.find((a) => a instanceof Error);
916
+ if (method === "error" && err)
917
+ vitals.error({ source: "console", ...errorOf(err), message: parts.join(" ") });
918
+ else
919
+ vitals.log(levelOf[method], parts.join(" "), void 0, "console");
920
+ }
921
+ catch {
922
+ }
923
+ finally {
924
+ inside = false;
925
+ }
926
+ };
927
+ restoreConsole.push(() => {
928
+ target[method] = original;
929
+ });
835
930
  }
836
- return info;
837
- });
838
- readyPromise.catch(() => {
839
- readyPromise = null;
840
- });
841
- return readyPromise;
842
- },
843
- launch: () => ({ ...needLaunch() }),
844
- async finish(result) {
845
- needLaunch();
846
- const problem = finishProblem(result);
847
- if (problem) throw new BridgeError("bad_finish", problem);
848
- if (finished) throw new BridgeError("already_finished", "A run is finished once.");
849
- finished = true;
850
- const answer = await post("finish", { score: result.score, seconds: result.seconds }, true, Math.max(timeoutMs, 3e4));
851
- return { accepted: answer?.accepted === true };
852
- },
853
- storage: {
854
- async get(key) {
855
- needLaunch();
856
- const problem = storageKeyProblem(key);
857
- if (problem) throw new BridgeError("bad_key", problem);
858
- const data = transport === "host" ? await post("storage.get", { key }) : await proxy("GET", `/v1/storage/${encodeURIComponent(key)}`);
859
- return data?.value ?? null;
860
- },
861
- async set(key, value) {
862
- needLaunch();
863
- const problem = storageKeyProblem(key);
864
- if (problem) throw new BridgeError("bad_key", problem);
865
- if (!isJsonValue(value)) throw new BridgeError("bad_value", "Storage holds plain JSON: objects, arrays, text, finite numbers, true/false and null.");
866
- if (jsonBytes(value) > STORAGE_MAX_BYTES) throw new BridgeError("storage_full", `A game keeps at most ${STORAGE_MAX_BYTES} bytes per player.`);
867
- if (transport === "host") await post("storage.set", { key, value });
868
- else await proxy("PUT", `/v1/storage/${encodeURIComponent(key)}`, { value });
869
- },
870
- async remove(key) {
871
- needLaunch();
872
- const problem = storageKeyProblem(key);
873
- if (problem) throw new BridgeError("bad_key", problem);
874
- if (transport === "host") await post("storage.remove", { key });
875
- else await proxy("DELETE", `/v1/storage/${encodeURIComponent(key)}`);
876
- }
877
- },
878
- track(event, props) {
879
- if (!launchInfo || closed || tracked >= TRACK_PER_LAUNCH || trackProblem(event, props)) return false;
880
- tracked++;
881
- const body = { event, ...props ? { props } : {} };
882
- const sent = transport === "host" ? post("track", body, false) : proxy("POST", "/v1/track", body);
883
- sent.catch(() => {
884
- });
885
- return true;
886
- },
887
- log(level, message, data) {
888
- if (!vitals || !isLogLevel(level)) return false;
889
- return vitals.log(level, typeof message === "string" ? message : String(message), data);
890
- },
891
- async exit() {
892
- await vitals?.end("exit");
893
- },
894
- sessionId: () => vitals?.session ?? null,
895
- on(event, fn) {
896
- handlers[event].add(fn);
897
- return () => handlers[event].delete(fn);
898
- },
899
- close() {
900
- if (closed) return;
901
- void vitals?.end("closed");
902
- perf?.stop();
903
- closed = true;
904
- win.removeEventListener("message", onMessage);
905
- win.removeEventListener("error", onError);
906
- win.removeEventListener("unhandledrejection", onRejection);
907
- win.removeEventListener("pagehide", onPageHide);
908
- for (const restore of restoreConsole) restore();
909
- if (native) win.document?.removeEventListener("message", onMessage);
910
- for (const [id, p] of pending) {
911
- clearTimeout(p.timer);
912
- p.reject(new BridgeError("closed", "The bridge is closed."));
913
- pending.delete(id);
914
- }
915
931
  }
916
- };
917
- return bridge;
932
+ const bridge = {
933
+ ready() {
934
+ if (readyPromise)
935
+ return readyPromise;
936
+ readyPromise = post("ready", { bridge: BRIDGE_VERSION, protocol: PROTOCOL_VERSION }).then((payload) => {
937
+ const p = payload;
938
+ const info = decodeLaunchToken(p?.token);
939
+ if (!info)
940
+ throw new BridgeError("bad_launch", "The host sent no valid launch token.");
941
+ token = p.token;
942
+ transport = p.transport === "host" ? "host" : "proxy";
943
+ launchInfo = info;
944
+ if (vitals && !vitals.session) {
945
+ const readyMs = perf?.sinceLoad();
946
+ void sessionIdOf(info.game, info.runId).then((id) => vitals?.start(id, readyMs)).catch(() => void 0);
947
+ }
948
+ return info;
949
+ });
950
+ readyPromise.catch(() => {
951
+ readyPromise = null;
952
+ });
953
+ return readyPromise;
954
+ },
955
+ launch: () => ({ ...needLaunch() }),
956
+ async finish(result) {
957
+ needLaunch();
958
+ const problem = finishProblem(result);
959
+ if (problem)
960
+ throw new BridgeError("bad_finish", problem);
961
+ if (finished)
962
+ throw new BridgeError("already_finished", "A run is finished once.");
963
+ finished = true;
964
+ const answer = await post("finish", { score: result.score, seconds: result.seconds }, true, Math.max(timeoutMs, 30000));
965
+ return { accepted: answer?.accepted === true };
966
+ },
967
+ storage: {
968
+ async get(key) {
969
+ needLaunch();
970
+ const problem = storageKeyProblem(key);
971
+ if (problem)
972
+ throw new BridgeError("bad_key", problem);
973
+ const data = transport === "host" ? await post("storage.get", { key }) : await proxy("GET", `/v1/storage/${encodeURIComponent(key)}`);
974
+ return data?.value ?? null;
975
+ },
976
+ async set(key, value) {
977
+ needLaunch();
978
+ const problem = storageKeyProblem(key);
979
+ if (problem)
980
+ throw new BridgeError("bad_key", problem);
981
+ if (!isJsonValue(value))
982
+ throw new BridgeError("bad_value", "Storage holds plain JSON: objects, arrays, text, finite numbers, true/false and null.");
983
+ if (jsonBytes(value) > STORAGE_MAX_BYTES)
984
+ throw new BridgeError("storage_full", `A game keeps at most ${STORAGE_MAX_BYTES} bytes per player.`);
985
+ if (transport === "host")
986
+ await post("storage.set", { key, value });
987
+ else
988
+ await proxy("PUT", `/v1/storage/${encodeURIComponent(key)}`, { value });
989
+ },
990
+ async remove(key) {
991
+ needLaunch();
992
+ const problem = storageKeyProblem(key);
993
+ if (problem)
994
+ throw new BridgeError("bad_key", problem);
995
+ if (transport === "host")
996
+ await post("storage.remove", { key });
997
+ else
998
+ await proxy("DELETE", `/v1/storage/${encodeURIComponent(key)}`);
999
+ }
1000
+ },
1001
+ track(event, props) {
1002
+ if (!launchInfo || closed || tracked >= TRACK_PER_LAUNCH || trackProblem(event, props))
1003
+ return false;
1004
+ tracked++;
1005
+ const body = { event, ...props ? { props } : {} };
1006
+ const sent = transport === "host" ? post("track", body, false) : proxy("POST", "/v1/track", body);
1007
+ sent.catch(() => {
1008
+ });
1009
+ return true;
1010
+ },
1011
+ log(level, message, data) {
1012
+ if (!vitals || !isLogLevel(level))
1013
+ return false;
1014
+ return vitals.log(level, typeof message === "string" ? message : String(message), data);
1015
+ },
1016
+ async exit() {
1017
+ await vitals?.end("exit");
1018
+ },
1019
+ sessionId: () => vitals?.session ?? null,
1020
+ on(event, fn) {
1021
+ handlers[event].add(fn);
1022
+ return () => handlers[event].delete(fn);
1023
+ },
1024
+ close() {
1025
+ if (closed)
1026
+ return;
1027
+ void vitals?.end("closed");
1028
+ perf?.stop();
1029
+ closed = true;
1030
+ win.removeEventListener("message", onMessage);
1031
+ win.removeEventListener("error", onError);
1032
+ win.removeEventListener("unhandledrejection", onRejection);
1033
+ win.removeEventListener("pagehide", onPageHide);
1034
+ for (const restore of restoreConsole)
1035
+ restore();
1036
+ if (native)
1037
+ win.document?.removeEventListener("message", onMessage);
1038
+ for (const [id, p] of pending) {
1039
+ clearTimeout(p.timer);
1040
+ p.reject(new BridgeError("closed", "The bridge is closed."));
1041
+ pending.delete(id);
1042
+ }
1043
+ }
1044
+ };
1045
+ return bridge;
918
1046
  }
919
- export {
920
- BRIDGE_VERSION,
921
- BridgeError,
922
- DEFAULT_HOST_ORIGINS,
923
- DEVICE_CLASSES,
924
- DIAG_BODY_MAX_BYTES,
925
- DIAG_LIMITS,
926
- END_REASONS,
927
- ERROR_SOURCES,
928
- FINGERPRINT_FRAMES,
929
- HOST_EVENTS,
930
- HOST_EVENT_TEXT,
931
- HOST_HANG_MS,
932
- HOST_READY_TIMEOUT_MS,
933
- JANK_FRAME_MS,
934
- LOG_LEVELS,
935
- MAX_MESSAGE_BYTES,
936
- MAX_TOKEN_LENGTH,
937
- OS_NAMES,
938
- PERF_RANGES,
939
- PRE_READY_ENTRIES,
940
- PROTOCOL,
941
- PROTOCOL_VERSION,
942
- PROXY_ORIGIN,
943
- STORAGE_KEY,
944
- STORAGE_MAX_BYTES,
945
- TRACK_EVENT,
946
- TRACK_MAX_PROPS,
947
- TRACK_MAX_STRING,
948
- TRACK_PER_LAUNCH,
949
- TRACK_PROP_KEY,
950
- boundData,
951
- cleanDiagnosticEntry,
952
- createBridge,
953
- createCollector,
954
- createHostWatchdog,
955
- createPerfSampler,
956
- decodeLaunchToken,
957
- fingerprint,
958
- fingerprintInput,
959
- finishProblem,
960
- frameKey,
961
- isCrashEntry,
962
- isFatalSource,
963
- isHostEvent,
964
- isJsonValue,
965
- isLogLevel,
966
- jsonBytes,
967
- launchContextProblem,
968
- normaliseContext,
969
- normaliseMessage,
970
- parseDiagnosticsBatch,
971
- parseGameRequest,
972
- parseHostMessage,
973
- parseStack,
974
- scrubData,
975
- scrubText,
976
- sessionIdOf,
977
- sha256HexOf,
978
- shortenGameUrls,
979
- storageKeyProblem,
980
- trackProblem,
981
- utf8Bytes
982
- };
1047
+ export { BRIDGE_VERSION, BridgeError, DEFAULT_HOST_ORIGINS, DEVICE_CLASSES, DIAG_BODY_MAX_BYTES, DIAG_LIMITS, END_REASONS, ERROR_SOURCES, FINGERPRINT_FRAMES, HOST_EVENTS, HOST_EVENT_TEXT, HOST_HANG_MS, HOST_READY_TIMEOUT_MS, JANK_FRAME_MS, LOG_LEVELS, MAX_MESSAGE_BYTES, MAX_TOKEN_LENGTH, OS_NAMES, PERF_RANGES, PRE_READY_ENTRIES, PROTOCOL, PROTOCOL_VERSION, PROXY_ORIGIN, STORAGE_KEY, STORAGE_MAX_BYTES, TRACK_EVENT, TRACK_MAX_PROPS, TRACK_MAX_STRING, TRACK_PER_LAUNCH, TRACK_PROP_KEY, boundData, cleanDiagnosticEntry, createBridge, createCollector, createHostWatchdog, createPerfSampler, decodeLaunchToken, fingerprint, fingerprintInput, finishProblem, frameKey, isCrashEntry, isFatalSource, isHostEvent, isJsonValue, isLogLevel, jsonBytes, launchContextProblem, normaliseContext, normaliseMessage, parseDiagnosticsBatch, parseGameRequest, parseHostMessage, parseStack, scrubData, scrubText, sessionIdOf, sha256HexOf, shortenGameUrls, storageKeyProblem, trackProblem, utf8Bytes };