-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprotocol.js
More file actions
104 lines (95 loc) · 3.48 KB
/
Copy pathprotocol.js
File metadata and controls
104 lines (95 loc) · 3.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* Shared, typed message protocol for view-transition-debugger.
*
* Four contexts exchange messages:
*
* main world (injected/instrument.js)
* | window.postMessage
* content bridge (content/bridge.js, ISOLATED world)
* | chrome.runtime.sendMessage / port
* devtools page (devtools/devtools.js)
* | port
* panel (panel/panel.js)
*
* IMPORTANT: `injected/instrument.js` is a *classic* script (MV3 content
* scripts, including `world: "MAIN"` ones, cannot be ES modules), so it cannot
* `import` from this file. It therefore repeats the wire constants inline in a
* clearly marked block. `tests/protocol.test.js` parses both files and asserts
* the two copies stay in sync, so drift is a test failure rather than a
* silent production bug.
*/
/** Every envelope carries this so we can ignore unrelated postMessage traffic. */
export const CHANNEL = 'view-transition-debugger';
/** Messages that travel page -> panel. */
export const FromPage = Object.freeze({
HELLO: 'page/hello',
TRANSITION_RECORD: 'page/transition-record',
TRANSITION_STARTED: 'page/transition-started',
STEP_PAUSED: 'page/step-paused',
NAVIGATED: 'page/navigated',
ERROR: 'page/error',
});
/** Messages that travel panel -> page. */
export const ToPage = Object.freeze({
REQUEST_BUFFER: 'panel/request-buffer',
SET_SLOW_MO: 'panel/set-slow-mo',
SET_STEP_MODE: 'panel/set-step-mode',
STEP_CONTINUE: 'panel/step-continue',
REPLAY_LAST: 'panel/replay-last',
CLEAR: 'panel/clear',
});
/** Slow-motion multipliers the panel may request. `0` means "hold paused". */
export const SLOW_MO_FACTORS = Object.freeze([1, 2, 5, 10, 0]);
/**
* Build a wire envelope.
*
* @param {string} type one of FromPage/ToPage
* @param {unknown} [payload]
* @returns {{channel: string, type: string, payload: unknown, sentAt: number}}
*/
export function envelope(type, payload = null) {
if (typeof type !== 'string' || type.length === 0) {
throw new TypeError('envelope(type): type must be a non-empty string');
}
return { channel: CHANNEL, type, payload, sentAt: Date.now() };
}
const KNOWN_TYPES = new Set([...Object.values(FromPage), ...Object.values(ToPage)]);
/**
* Type guard for anything arriving over an untrusted channel (`window.message`
* is shared with the page and with every other extension, so this must be
* strict and must never throw).
*
* @param {unknown} value
* @returns {boolean}
*/
export function isEnvelope(value) {
if (value === null || typeof value !== 'object') return false;
const candidate = /** @type {Record<string, unknown>} */ (value);
if (candidate.channel !== CHANNEL) return false;
if (typeof candidate.type !== 'string') return false;
return KNOWN_TYPES.has(candidate.type);
}
/**
* Narrow an envelope to a direction, so the bridge never reflects a panel
* message back to the panel (which would loop forever).
*
* @param {unknown} value
* @param {'from-page' | 'to-page'} direction
* @returns {boolean}
*/
export function isEnvelopeFrom(value, direction) {
if (!isEnvelope(value)) return false;
const type = /** @type {{type: string}} */ (value).type;
const table = direction === 'from-page' ? FromPage : ToPage;
return Object.values(table).includes(type);
}
/**
* Validate a slow-motion factor coming from the UI before it reaches the page.
*
* @param {unknown} factor
* @returns {number} a safe factor, defaulting to 1
*/
export function normalizeSlowMo(factor) {
const n = Number(factor);
return SLOW_MO_FACTORS.includes(n) ? n : 1;
}