@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/README.md +5 -8
- package/bridge.d.ts +6 -51
- package/collector.d.ts +2 -20
- package/diagnostics.d.ts +57 -109
- package/index.d.ts +5 -5
- package/index.js +928 -863
- package/package.json +2 -3
- package/perf.d.ts +0 -18
- package/protocol.d.ts +3 -41
- package/seek-game.js +928 -863
package/index.js
CHANGED
|
@@ -1,293 +1,304 @@
|
|
|
1
|
-
// @seekprotocol/game
|
|
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 =
|
|
9
|
-
var HOST_HANG_MS =
|
|
6
|
+
var HOST_READY_TIMEOUT_MS = 10000;
|
|
7
|
+
var HOST_HANG_MS = 30000;
|
|
10
8
|
var HOST_EVENT_TEXT = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
101
|
+
return null;
|
|
108
102
|
}
|
|
109
103
|
function boundData(value, extra = []) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
152
|
+
return (await sha256HexOf(fingerprintInput(kind, message, stack, file, line))).slice(0, 32);
|
|
156
153
|
}
|
|
157
154
|
async function sessionIdOf(appId, runId) {
|
|
158
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
c
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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 =
|
|
659
|
+
var PAUSE_MS = 1000;
|
|
589
660
|
function createPerfSampler(win) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
818
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
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 };
|