@corbado/observe 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/analysis/client.mjs +1851 -0
  2. package/dist/analysis/config-manager.mjs +83 -0
  3. package/dist/analysis/config.mjs +216 -0
  4. package/dist/analysis/device-info-manager.mjs +153 -0
  5. package/dist/analysis/device.mjs +562 -0
  6. package/dist/analysis/index.mjs +118 -0
  7. package/dist/analysis/logger.mjs +50 -0
  8. package/dist/analysis/low-events/capture-sequence.mjs +11 -0
  9. package/dist/analysis/low-events/ceremony-initiator.mjs +26 -0
  10. package/dist/analysis/low-events/form-input-binding.mjs +46 -0
  11. package/dist/analysis/low-events/grouped-input-tracker.mjs +133 -0
  12. package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
  13. package/dist/analysis/low-events/input-paste-observer.mjs +91 -0
  14. package/dist/analysis/low-events/input-tracker.mjs +223 -0
  15. package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
  16. package/dist/analysis/low-events/shared-window-observer.mjs +225 -0
  17. package/dist/analysis/low-events/sink.mjs +0 -0
  18. package/dist/analysis/low-events/viewport-batcher.mjs +42 -0
  19. package/dist/analysis/low-events/window-tracker.mjs +40 -0
  20. package/dist/analysis/queue.mjs +887 -0
  21. package/dist/analysis/readers/device.mjs +73 -0
  22. package/dist/analysis/readers/meta.mjs +18 -0
  23. package/dist/analysis/readers/password-manager-dom.mjs +112 -0
  24. package/dist/analysis/readers/password-manager-extensions.mjs +247 -0
  25. package/dist/analysis/readers/private-mode.mjs +9 -0
  26. package/dist/analysis/storage/index.mjs +197 -0
  27. package/dist/analysis/storage/migration.mjs +56 -0
  28. package/dist/analysis/store/event-store.mjs +114 -0
  29. package/dist/analysis/transport.mjs +0 -0
  30. package/dist/analysis/transports/fetch.mjs +108 -0
  31. package/dist/analysis/types.mjs +25 -0
  32. package/dist/analysis/utils/appconfirmation-operation-full.mjs +19 -0
  33. package/dist/analysis/utils/blocked-user-agents.mjs +37 -0
  34. package/dist/analysis/utils/captcha-operation-full.mjs +17 -0
  35. package/dist/analysis/utils/email-link-continuity.mjs +50 -0
  36. package/dist/analysis/utils/email-link-operation-full.mjs +142 -0
  37. package/dist/analysis/utils/email-otp-operation-full.mjs +39 -0
  38. package/dist/analysis/utils/form-operation-full.mjs +81 -0
  39. package/dist/analysis/utils/key-operation-full.mjs +38 -0
  40. package/dist/analysis/utils/normalize-error.mjs +29 -0
  41. package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +46 -0
  42. package/dist/analysis/utils/operation-full.mjs +235 -0
  43. package/dist/analysis/utils/passkey-deletion-operation-full.mjs +17 -0
  44. package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +32 -0
  45. package/dist/analysis/utils/passkey-login-operation-full.mjs +34 -0
  46. package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
  47. package/dist/analysis/utils/password-login-operation-full.mjs +61 -0
  48. package/dist/analysis/utils/password-manager-sampler.mjs +130 -0
  49. package/dist/analysis/utils/provide-data-operation-full.mjs +28 -0
  50. package/dist/analysis/utils/serialize-raw-error.mjs +205 -0
  51. package/dist/analysis/utils/sms-otp-operation-full.mjs +39 -0
  52. package/dist/analysis/utils/social-operation-login.mjs +17 -0
  53. package/dist/analysis/utils/source-path.mjs +39 -0
  54. package/dist/analysis/utils/sso-operation-login.mjs +37 -0
  55. package/dist/analysis/utils/totp-operation-full.mjs +35 -0
  56. package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
  57. package/dist/analysis/utils/web-lock.mjs +52 -0
  58. package/dist/analysis/utils/webauthn-payloads.mjs +101 -0
  59. package/dist/analysis/version.mjs +7 -0
  60. package/dist/cdn/script.global.js +4 -4
  61. package/dist/index.d.mts +164 -11
  62. package/dist/index.d.ts +164 -11
  63. package/dist/index.js +4 -4
  64. package/dist/index.mjs +4 -4
  65. package/package.json +7 -2
@@ -0,0 +1,223 @@
1
+ // src/low-events/input-tracker.ts
2
+ import { InputChangeRecorder } from "./input-change-recorder.mjs";
3
+ import { InputPasteObserver } from "./input-paste-observer.mjs";
4
+ import { readPasswordManagerDomSignals } from "../readers/password-manager-dom.mjs";
5
+ var MOUNT_PROBE_DELAY_MS = 500;
6
+ var FOCUS_PROBE_DELAY_MS = 250;
7
+ function fieldContentLength(field) {
8
+ if (field.type === "checkbox" || field.type === "radio") return field.checked ? 1 : 0;
9
+ return field.value.length;
10
+ }
11
+ var LowEventInputTracker = class {
12
+ constructor(logger, inputField, sink) {
13
+ this.logger = logger;
14
+ this.inputField = inputField;
15
+ this.sink = sink;
16
+ this.changes = new InputChangeRecorder(
17
+ fieldContentLength(inputField),
18
+ this.pushLow,
19
+ logger,
20
+ sink.reserveCaptureSeq
21
+ );
22
+ this.pasteObserver = new InputPasteObserver(
23
+ [inputField],
24
+ logger,
25
+ (ts, _inserted, captureSeq) => {
26
+ this.changes.flush();
27
+ this.changes.markInputObserved();
28
+ const oldLength = this.changes.previousLength;
29
+ this.changes.previousLength = fieldContentLength(inputField);
30
+ this.pushLow({
31
+ ...captureSeq === void 0 ? {} : { captureSeq },
32
+ lowType: "input-paste",
33
+ ts,
34
+ inputEffect: fieldContentLength(inputField) === 0 ? "clear" : oldLength === 0 ? "fill" : "modify"
35
+ });
36
+ },
37
+ sink.reserveCaptureSeq
38
+ );
39
+ this.boundHandlers = {
40
+ focus: this.wrapHandler("focus", () => this.handleFocus()),
41
+ blur: this.wrapHandler("blur", () => this.handleBlur()),
42
+ pointerdown: this.wrapHandler("pointerdown", () => this.enqueueNonInputEvent("pointerdown")),
43
+ pointerup: this.wrapHandler("pointerup", () => this.enqueueNonInputEvent("pointerup")),
44
+ click: this.wrapHandler("click", () => this.enqueueNonInputEvent("click")),
45
+ input: (event) => {
46
+ try {
47
+ this.handleInput(event);
48
+ } catch (error) {
49
+ this.logger.error("Failed to observe input", error);
50
+ }
51
+ },
52
+ keyup: this.wrapKeyboardHandler("keyup", (event) => this.handleKeyUp(event)),
53
+ pageHide: this.wrapHandler("pagehide", () => this.flushForTeardown())
54
+ };
55
+ this.unregisterInputs = sink.registerInputs?.([inputField], () => this.flushPending());
56
+ this.capturePrefill();
57
+ this.attach();
58
+ }
59
+ unregisterInputs;
60
+ changes;
61
+ pasteObserver;
62
+ emittedSignals = /* @__PURE__ */ new Set();
63
+ firstFocusHandled = false;
64
+ mountProbeTimer = null;
65
+ focusProbeTimer = null;
66
+ destroyed = false;
67
+ boundHandlers;
68
+ destroy() {
69
+ if (this.destroyed) {
70
+ return;
71
+ }
72
+ this.destroyed = true;
73
+ try {
74
+ if (this.mountProbeTimer !== null) {
75
+ clearTimeout(this.mountProbeTimer);
76
+ this.mountProbeTimer = null;
77
+ }
78
+ if (this.focusProbeTimer !== null) {
79
+ clearTimeout(this.focusProbeTimer);
80
+ this.focusProbeTimer = null;
81
+ }
82
+ this.changes.flush();
83
+ this.unregisterInputs?.();
84
+ this.pasteObserver.destroy();
85
+ this.detach();
86
+ } catch (error) {
87
+ this.logger.error("Failed to destroy LowEventInputTracker", error);
88
+ }
89
+ }
90
+ attach() {
91
+ const input = this.inputField;
92
+ input.addEventListener("focus", this.boundHandlers.focus);
93
+ input.addEventListener("blur", this.boundHandlers.blur);
94
+ input.addEventListener("pointerdown", this.boundHandlers.pointerdown);
95
+ input.addEventListener("pointerup", this.boundHandlers.pointerup);
96
+ input.addEventListener("click", this.boundHandlers.click);
97
+ input.addEventListener("input", this.boundHandlers.input);
98
+ input.addEventListener("keyup", this.boundHandlers.keyup);
99
+ if (typeof window !== "undefined") {
100
+ window.addEventListener("pagehide", this.boundHandlers.pageHide);
101
+ }
102
+ if (typeof document !== "undefined") {
103
+ if (document.activeElement === input) {
104
+ this.handleFocus();
105
+ }
106
+ }
107
+ if (typeof window !== "undefined") {
108
+ this.mountProbeTimer = setTimeout(() => this.runEnvProbe(), MOUNT_PROBE_DELAY_MS);
109
+ }
110
+ }
111
+ detach() {
112
+ const input = this.inputField;
113
+ input.removeEventListener("focus", this.boundHandlers.focus);
114
+ input.removeEventListener("blur", this.boundHandlers.blur);
115
+ input.removeEventListener("pointerdown", this.boundHandlers.pointerdown);
116
+ input.removeEventListener("pointerup", this.boundHandlers.pointerup);
117
+ input.removeEventListener("click", this.boundHandlers.click);
118
+ input.removeEventListener("input", this.boundHandlers.input);
119
+ input.removeEventListener("keyup", this.boundHandlers.keyup);
120
+ if (typeof window !== "undefined") {
121
+ window.removeEventListener("pagehide", this.boundHandlers.pageHide);
122
+ }
123
+ }
124
+ pushLow = (low) => {
125
+ this.sink.enqueueLowEvent(low);
126
+ };
127
+ runEnvProbe() {
128
+ try {
129
+ if (typeof __CBO_DATA_PMD__ === "boolean" ? __CBO_DATA_PMD__ : this.sink.passwordManagerDomScan?.() !== false) {
130
+ const matches = readPasswordManagerDomSignals({
131
+ key: "dom:password-managers.dom-signals",
132
+ fields: [
133
+ {
134
+ path: "signals[]",
135
+ exposure: "enum",
136
+ transform: { values: ["pa-1", "pa-2", "pa-3", "pa-4", "pa-5", "pa-6"] }
137
+ }
138
+ ],
139
+ dataConfig: "pmd"
140
+ });
141
+ const now = Date.now();
142
+ for (const eventType of matches) {
143
+ if (this.emittedSignals.has(eventType)) {
144
+ continue;
145
+ }
146
+ this.emittedSignals.add(eventType);
147
+ this.pushLow({ lowType: eventType, ts: now });
148
+ }
149
+ }
150
+ } catch (error) {
151
+ this.logger.error("Failed to run input env probe", error);
152
+ }
153
+ }
154
+ scheduleFocusProbe() {
155
+ if (this.firstFocusHandled) {
156
+ return;
157
+ }
158
+ this.firstFocusHandled = true;
159
+ this.focusProbeTimer = setTimeout(() => this.runEnvProbe(), FOCUS_PROBE_DELAY_MS);
160
+ }
161
+ capturePrefill(newCapture = false) {
162
+ if (this.destroyed) return;
163
+ this.flushPending();
164
+ this.changes.capturePrefill(fieldContentLength(this.inputField), newCapture);
165
+ }
166
+ flushPending() {
167
+ this.pasteObserver.flush();
168
+ this.changes.flush();
169
+ }
170
+ enqueueNonInputEvent(eventType) {
171
+ this.changes.flush();
172
+ this.pushLow({ lowType: eventType, ts: Date.now() });
173
+ }
174
+ handleFocus() {
175
+ this.scheduleFocusProbe();
176
+ this.enqueueNonInputEvent("focus");
177
+ }
178
+ handleBlur() {
179
+ this.pasteObserver.flush();
180
+ this.enqueueNonInputEvent("blur");
181
+ }
182
+ handleInput(event) {
183
+ if (this.pasteObserver.handlesInput(event)) return;
184
+ this.changes.record(
185
+ fieldContentLength(this.inputField),
186
+ Date.now(),
187
+ event.isComposing || event.inputType === "insertCompositionText"
188
+ );
189
+ }
190
+ handleKeyUp(event) {
191
+ if (event.key !== "Escape") {
192
+ return;
193
+ }
194
+ this.enqueueNonInputEvent("keyup-escape");
195
+ }
196
+ flushForTeardown() {
197
+ this.pasteObserver.flush();
198
+ this.changes.flush();
199
+ this.sink.flushKeepalive();
200
+ }
201
+ wrapHandler(name, fn) {
202
+ return () => {
203
+ try {
204
+ fn();
205
+ } catch (error) {
206
+ this.logger.error(`Failed to handle low event "${name}"`, error);
207
+ }
208
+ };
209
+ }
210
+ wrapKeyboardHandler(name, fn) {
211
+ return (event) => {
212
+ try {
213
+ fn(event);
214
+ } catch (error) {
215
+ this.logger.error(`Failed to handle low event "${name}"`, error);
216
+ }
217
+ };
218
+ }
219
+ };
220
+ export {
221
+ LowEventInputTracker,
222
+ fieldContentLength
223
+ };
@@ -0,0 +1,67 @@
1
+ // src/low-events/otp-input-binding.ts
2
+ import { LowEventInputTracker } from "./input-tracker.mjs";
3
+ import { LowEventGroupedInputTracker } from "./grouped-input-tracker.mjs";
4
+ var OTPInputBinding = class {
5
+ constructor(logger, inputSink, groupSink) {
6
+ this.logger = logger;
7
+ this.inputSink = inputSink;
8
+ this.groupSink = groupSink;
9
+ }
10
+ collector;
11
+ fields = [];
12
+ grouped = false;
13
+ destroyed = false;
14
+ prefillClaimed = false;
15
+ setFields(fields, grouped = true) {
16
+ try {
17
+ if (this.destroyed) return;
18
+ const next = [...new Set(fields)];
19
+ if (grouped === this.grouped && next.length === this.fields.length && next.every((field, i) => field === this.fields[i]))
20
+ return;
21
+ this.collector?.destroy();
22
+ this.collector = void 0;
23
+ this.fields = [];
24
+ this.grouped = grouped;
25
+ if (next.length === 0) return;
26
+ const sink = grouped ? this.groupSink : this.inputSink;
27
+ const filteredSink = { ...sink, enqueueLowEvent: (low) => this.enqueue(sink, low) };
28
+ this.collector = grouped ? new LowEventGroupedInputTracker(this.logger, next, filteredSink) : new LowEventInputTracker(this.logger, next[0], filteredSink);
29
+ this.fields = next;
30
+ } catch (error) {
31
+ this.logger.error("Failed to bind OTP inputs", error);
32
+ }
33
+ }
34
+ enqueue(sink, low) {
35
+ if (low.lowType === "input-prefill") {
36
+ if (this.prefillClaimed) return;
37
+ this.prefillClaimed = true;
38
+ } else if ([
39
+ "input",
40
+ "input-idle",
41
+ "input-clear",
42
+ "input-paste",
43
+ "big-input-paste",
44
+ "big-input-add",
45
+ "big-input-rem"
46
+ ].includes(low.lowType)) {
47
+ this.prefillClaimed = true;
48
+ }
49
+ sink.enqueueLowEvent(low);
50
+ }
51
+ capturePrefill(newCapture = false) {
52
+ if (newCapture) this.prefillClaimed = false;
53
+ this.collector?.capturePrefill(newCapture);
54
+ }
55
+ flushPending() {
56
+ this.collector?.flushPending();
57
+ }
58
+ destroy() {
59
+ this.destroyed = true;
60
+ this.collector?.destroy();
61
+ this.collector = void 0;
62
+ this.fields = [];
63
+ }
64
+ };
65
+ export {
66
+ OTPInputBinding
67
+ };
@@ -0,0 +1,225 @@
1
+ // src/low-events/shared-window-observer.ts
2
+ import { reserveCaptureSeq } from "./capture-sequence.mjs";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import { createViewportBatcher } from "./viewport-batcher.mjs";
5
+ var documentPageId;
6
+ var getCapturePageId = () => documentPageId ??= uuidv7();
7
+ var focusedElements = () => {
8
+ const chain = [];
9
+ for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
10
+ chain.push(active);
11
+ }
12
+ return chain;
13
+ };
14
+ var SharedWindowObserver = class {
15
+ constructor(logger, enqueue, flushKeepalive) {
16
+ this.logger = logger;
17
+ this.enqueue = enqueue;
18
+ this.flushKeepalive = flushKeepalive;
19
+ if (typeof window === "undefined" || typeof document === "undefined") return;
20
+ this.listen(window, "blur", () => {
21
+ this.flushInputs();
22
+ this.bracket = this.context();
23
+ this.emit({ lowType: "window-blur", ts: Date.now() }, this.bracket);
24
+ });
25
+ this.listen(window, "focus", () => {
26
+ this.flushInputs();
27
+ this.emit({ lowType: "window-focus", ts: Date.now() }, this.bracket ?? this.context());
28
+ this.bracket = void 0;
29
+ });
30
+ this.listen(document, "visibilitychange", () => {
31
+ this.flushInputs();
32
+ this.emit({
33
+ lowType: `document-visibilitychange-${document.visibilityState === "hidden" ? "hidden" : "visible"}`,
34
+ ts: Date.now()
35
+ });
36
+ });
37
+ this.listen(document, "focusout", (event) => {
38
+ const registration = this.findInput(event.target);
39
+ this.lastBlur = registration && !event.relatedTarget && Date.now() - this.unrelatedPointerAt > 250 ? { registration, ts: Date.now() } : void 0;
40
+ });
41
+ this.listen(document, "focusin", (event) => {
42
+ this.lastBlur = void 0;
43
+ const field = this.inputFocusListeners.size > 0 && event.composedPath().find((target) => this.findInput(target));
44
+ if (field) this.notifyInputFocus(field);
45
+ });
46
+ this.listen(document, "pointerdown", (event) => {
47
+ if (!this.findInput(event.target)) {
48
+ this.lastBlur = void 0;
49
+ this.unrelatedPointerAt = Date.now();
50
+ } else this.unrelatedPointerAt = -Infinity;
51
+ });
52
+ if (window.visualViewport) {
53
+ this.listen(window.visualViewport, "resize", () => this.extendViewport("resize"));
54
+ this.listen(window.visualViewport, "scroll", () => this.extendViewport("scroll"));
55
+ }
56
+ this.listen(window, "pagehide", () => {
57
+ this.flushInputs();
58
+ this.resize.flush();
59
+ this.scroll.flush();
60
+ this.flushKeepalive();
61
+ });
62
+ this.observeNavigation();
63
+ }
64
+ inputs = /* @__PURE__ */ new Set();
65
+ inputFocusListeners = /* @__PURE__ */ new Set();
66
+ ceremonies = /* @__PURE__ */ new Set();
67
+ removers = [];
68
+ lastBlur;
69
+ bracket;
70
+ unrelatedPointerAt = -Infinity;
71
+ destroyed = false;
72
+ viewportContext = {};
73
+ resize = createViewportBatcher(
74
+ (low) => this.emit(low, this.viewportContext.resize),
75
+ "visualviewport-resize",
76
+ reserveCaptureSeq
77
+ );
78
+ scroll = createViewportBatcher(
79
+ (low) => this.emit(low, this.viewportContext.scroll),
80
+ "visualviewport-scroll",
81
+ reserveCaptureSeq
82
+ );
83
+ registerInputs(fields, context, flush) {
84
+ if (this.destroyed) return () => {
85
+ };
86
+ const registration = { fields, context, flush };
87
+ this.inputs.add(registration);
88
+ if (this.inputFocusListeners.size > 0 && typeof document !== "undefined") {
89
+ const focused = focusedElements();
90
+ const field = fields.find((candidate) => focused.includes(candidate));
91
+ if (field) this.notifyInputFocus(field);
92
+ }
93
+ return () => {
94
+ this.inputs.delete(registration);
95
+ if (this.lastBlur?.registration === registration) this.lastBlur = void 0;
96
+ };
97
+ }
98
+ beginCeremony(context) {
99
+ const registration = { context, settled: false };
100
+ if (!this.destroyed) {
101
+ this.flushInputs();
102
+ this.ceremonies.add(registration);
103
+ }
104
+ const destroy = () => {
105
+ if (registration.timer) clearTimeout(registration.timer);
106
+ this.ceremonies.delete(registration);
107
+ if (this.bracket === registration.context) this.bracket = void 0;
108
+ };
109
+ return {
110
+ settle: (tailMs) => {
111
+ registration.settled = true;
112
+ if (registration.timer) clearTimeout(registration.timer);
113
+ registration.timer = setTimeout(destroy, tailMs);
114
+ },
115
+ destroy
116
+ };
117
+ }
118
+ /**
119
+ * Calls `listener` with the field whenever a registered input gains focus, including one already focused when
120
+ * registered.
121
+ */
122
+ onInputFocus(listener) {
123
+ if (this.destroyed) return () => {
124
+ };
125
+ this.inputFocusListeners.add(listener);
126
+ return () => this.inputFocusListeners.delete(listener);
127
+ }
128
+ notifyInputFocus(field) {
129
+ for (const listener of this.inputFocusListeners) {
130
+ try {
131
+ listener(field);
132
+ } catch (error) {
133
+ this.logger.error("Failed to notify registered input focus", error);
134
+ }
135
+ }
136
+ }
137
+ context() {
138
+ const active = [...this.ceremonies].filter((item) => !item.settled);
139
+ if (active.length === 1) return active[0].context;
140
+ if (active.length > 1) return void 0;
141
+ const input = this.findInput(document.activeElement);
142
+ if (input) return input.context();
143
+ const previous = this.lastBlur;
144
+ if (previous && Date.now() - previous.ts <= 250 && (document.activeElement === document.body || !document.activeElement)) {
145
+ return previous.registration.context();
146
+ }
147
+ return void 0;
148
+ }
149
+ findInput(target) {
150
+ return [...this.inputs].find((registration) => registration.fields.some((field) => field === target));
151
+ }
152
+ /** Flush all registered input batches before a high-level lifecycle boundary. */
153
+ flushInputs() {
154
+ for (const input of this.inputs) {
155
+ try {
156
+ input.flush();
157
+ } catch (error) {
158
+ this.logger.error("Failed to flush window input context", error);
159
+ }
160
+ }
161
+ }
162
+ emit(low, context) {
163
+ if (this.destroyed) return;
164
+ const { sessionId, ...capture } = context ?? {};
165
+ this.enqueue(
166
+ {
167
+ ...low,
168
+ ...capture,
169
+ captureSeq: low.captureSeq ?? reserveCaptureSeq(),
170
+ sourceKind: "window",
171
+ captureVersion: 1,
172
+ pageId: getCapturePageId()
173
+ },
174
+ sessionId
175
+ );
176
+ }
177
+ listen(target, name, handler) {
178
+ const safe = (event) => {
179
+ try {
180
+ handler(event);
181
+ } catch (error) {
182
+ this.logger.error(`Failed to observe window ${name}`, error);
183
+ }
184
+ };
185
+ target.addEventListener(name, safe);
186
+ this.removers.push(() => target.removeEventListener(name, safe));
187
+ }
188
+ extendViewport(kind) {
189
+ const context = this.context();
190
+ const previous = this.viewportContext[kind];
191
+ if (previous?.subflowStartEventId !== context?.subflowStartEventId || previous?.fieldRole !== context?.fieldRole || previous?.sessionId !== context?.sessionId)
192
+ this[kind].flush();
193
+ this.viewportContext[kind] = context;
194
+ this[kind].extend();
195
+ }
196
+ observeNavigation() {
197
+ const navigation = window.navigation;
198
+ if (!navigation) return;
199
+ this.listen(navigation, "currententrychange", (event) => {
200
+ const change = event;
201
+ const from = change.from?.index;
202
+ const to = navigation.currentEntry?.index;
203
+ if (change.navigationType === "traverse" && typeof from === "number" && typeof to === "number" && from >= 0 && to >= 0 && to < from) {
204
+ this.flushInputs();
205
+ this.emit({ lowType: "window-navigate-back", ts: Date.now() });
206
+ }
207
+ });
208
+ }
209
+ destroy() {
210
+ if (this.destroyed) return;
211
+ this.flushInputs();
212
+ this.resize.flush();
213
+ this.scroll.flush();
214
+ this.destroyed = true;
215
+ for (const remove of this.removers) remove();
216
+ for (const ceremony of this.ceremonies) if (ceremony.timer) clearTimeout(ceremony.timer);
217
+ this.ceremonies.clear();
218
+ this.inputs.clear();
219
+ this.inputFocusListeners.clear();
220
+ }
221
+ };
222
+ export {
223
+ SharedWindowObserver,
224
+ getCapturePageId
225
+ };
File without changes
@@ -0,0 +1,42 @@
1
+ // src/low-events/viewport-batcher.ts
2
+ var VIEWPORT_BATCH_TIMEOUT_MS = 150;
3
+ var createViewportBatcher = (pushLow, eventType, reserveCaptureSeq) => {
4
+ let active = false;
5
+ let captureSeq;
6
+ let startTimestamp = null;
7
+ let timeout = null;
8
+ const flush = () => {
9
+ if (timeout !== null) {
10
+ window.clearTimeout(timeout);
11
+ timeout = null;
12
+ }
13
+ const ts = startTimestamp;
14
+ active = false;
15
+ startTimestamp = null;
16
+ if (ts === null) {
17
+ return;
18
+ }
19
+ pushLow({
20
+ ...captureSeq === void 0 ? {} : { captureSeq },
21
+ lowType: eventType,
22
+ ts,
23
+ durationMs: Date.now() - ts
24
+ });
25
+ };
26
+ const extend = () => {
27
+ if (!active) {
28
+ const sequence = reserveCaptureSeq?.();
29
+ active = true;
30
+ startTimestamp = Date.now();
31
+ captureSeq = sequence;
32
+ }
33
+ if (timeout !== null) {
34
+ window.clearTimeout(timeout);
35
+ }
36
+ timeout = window.setTimeout(flush, VIEWPORT_BATCH_TIMEOUT_MS);
37
+ };
38
+ return { extend, flush };
39
+ };
40
+ export {
41
+ createViewportBatcher
42
+ };
@@ -0,0 +1,40 @@
1
+ // src/low-events/window-tracker.ts
2
+ var LowEventWindowTracker = class {
3
+ constructor(logger, sink) {
4
+ this.logger = logger;
5
+ this.sink = sink;
6
+ }
7
+ lease;
8
+ destroyed = false;
9
+ arm(initiator, conditional = false) {
10
+ this.disarm();
11
+ if (this.destroyed || conditional) return;
12
+ try {
13
+ this.lease = this.sink.beginCeremony?.(initiator);
14
+ } catch (error) {
15
+ this.logger.error("Failed to register ceremony low-event context", error);
16
+ }
17
+ }
18
+ scheduleDisarm(delayMs) {
19
+ try {
20
+ this.lease?.settle(delayMs);
21
+ } catch (error) {
22
+ this.logger.error("Failed to settle ceremony low-event context", error);
23
+ }
24
+ }
25
+ disarm() {
26
+ try {
27
+ this.lease?.destroy();
28
+ } catch (error) {
29
+ this.logger.error("Failed to release ceremony low-event context", error);
30
+ }
31
+ this.lease = void 0;
32
+ }
33
+ destroy() {
34
+ this.disarm();
35
+ this.destroyed = true;
36
+ }
37
+ };
38
+ export {
39
+ LowEventWindowTracker
40
+ };