@anyreplay/electron 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/bridge-BQKD7rrG.d.cts +53 -0
- package/dist/bridge-BQKD7rrG.d.ts +53 -0
- package/dist/chunk-5TQ2HVGX.js +18 -0
- package/dist/chunk-5TQ2HVGX.js.map +1 -0
- package/dist/index.cjs +177 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/main.cjs +143 -0
- package/dist/main.cjs.map +1 -0
- package/dist/main.d.cts +66 -0
- package/dist/main.d.ts +66 -0
- package/dist/main.js +105 -0
- package/dist/main.js.map +1 -0
- package/dist/preload.cjs +86 -0
- package/dist/preload.cjs.map +1 -0
- package/dist/preload.d.cts +22 -0
- package/dist/preload.d.ts +22 -0
- package/dist/preload.js +51 -0
- package/dist/preload.js.map +1 -0
- package/package.json +74 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_KEY,
|
|
3
|
+
CHANNELS
|
|
4
|
+
} from "./chunk-5TQ2HVGX.js";
|
|
5
|
+
|
|
6
|
+
// src/version.ts
|
|
7
|
+
var SDK_NAME = "@anyreplay/electron";
|
|
8
|
+
var SDK_VERSION = true ? "0.2.1" : "0.0.0-dev";
|
|
9
|
+
var PLATFORM = "electron";
|
|
10
|
+
|
|
11
|
+
// src/web-family.ts
|
|
12
|
+
import {
|
|
13
|
+
createRecorder
|
|
14
|
+
} from "@anyreplay/browser";
|
|
15
|
+
var MAX_QUEUED = 64;
|
|
16
|
+
function deferredRecorder(start, onStarted) {
|
|
17
|
+
let real = null;
|
|
18
|
+
let stopped = false;
|
|
19
|
+
const queue = [];
|
|
20
|
+
const run = (call) => {
|
|
21
|
+
if (real) {
|
|
22
|
+
call(real);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (stopped || queue.length >= MAX_QUEUED) return;
|
|
26
|
+
queue.push(call);
|
|
27
|
+
};
|
|
28
|
+
const ready = start().then((setup) => {
|
|
29
|
+
if (!setup || stopped) return;
|
|
30
|
+
real = createRecorder(setup.options, setup.shell);
|
|
31
|
+
for (const call of queue.splice(0)) call(real);
|
|
32
|
+
onStarted?.(real);
|
|
33
|
+
}).catch((error) => {
|
|
34
|
+
if (typeof console !== "undefined") console.warn("[anyreplay]", error);
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
ready,
|
|
38
|
+
status: () => real ? real.status() : stopped ? "stopped" : "idle",
|
|
39
|
+
sessionId: () => real?.sessionId() ?? null,
|
|
40
|
+
consent: (granted) => run((h) => h.consent(granted)),
|
|
41
|
+
identify: (traits) => run((h) => h.identify(traits)),
|
|
42
|
+
track: (name, properties) => run((h) => h.track(name, properties)),
|
|
43
|
+
trackError: (error) => run((h) => h.trackError(error)),
|
|
44
|
+
stop: () => {
|
|
45
|
+
if (real) real.stop();
|
|
46
|
+
else {
|
|
47
|
+
stopped = true;
|
|
48
|
+
queue.length = 0;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
flush: async () => {
|
|
52
|
+
await ready;
|
|
53
|
+
await real?.flush();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
var APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;
|
|
58
|
+
var clipVersion = (version) => {
|
|
59
|
+
const trimmed = version?.trim();
|
|
60
|
+
return trimmed ? trimmed.slice(0, 40) : void 0;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/index.ts
|
|
64
|
+
var current = null;
|
|
65
|
+
function init(options) {
|
|
66
|
+
if (current) return current;
|
|
67
|
+
const bridge = findBridge();
|
|
68
|
+
const { flushOnBlur = true, ...rest } = options;
|
|
69
|
+
const handle = deferredRecorder(
|
|
70
|
+
async () => {
|
|
71
|
+
const info = bridge ? await bridge.appInfo() : {};
|
|
72
|
+
return electronSetup(rest, info);
|
|
73
|
+
},
|
|
74
|
+
(recorder) => wire(recorder, bridge, flushOnBlur)
|
|
75
|
+
);
|
|
76
|
+
current = handle;
|
|
77
|
+
return handle;
|
|
78
|
+
}
|
|
79
|
+
function electronSetup(options, info) {
|
|
80
|
+
const appId = options.appId?.trim() || info.appId;
|
|
81
|
+
if (!appId && typeof console !== "undefined") {
|
|
82
|
+
console.warn("[anyreplay] no appId: call setupAnyReplay({ appId }) in the main process and exposeAnyReplay() in the preload. A project that lists allowed apps refuses sessions without one.");
|
|
83
|
+
}
|
|
84
|
+
if (appId && !APP_ID_PATTERN.test(appId)) {
|
|
85
|
+
throw new Error(`anyreplay: appId ${JSON.stringify(appId)} should be an app identifier such as com.example.app`);
|
|
86
|
+
}
|
|
87
|
+
const { appVersion: ownVersion, ...browserOptions } = options;
|
|
88
|
+
const appVersion = clipVersion(ownVersion ?? info.appVersion);
|
|
89
|
+
return {
|
|
90
|
+
options: {
|
|
91
|
+
// Inline unless the app says otherwise: a window loaded from the app's
|
|
92
|
+
// own files has addresses no reviewer can load (contract §9.1).
|
|
93
|
+
assets: "inline",
|
|
94
|
+
...browserOptions,
|
|
95
|
+
...appId ? { appId } : {}
|
|
96
|
+
},
|
|
97
|
+
shell: {
|
|
98
|
+
platform: PLATFORM,
|
|
99
|
+
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
|
100
|
+
...appVersion ? { appVersion } : {}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function wire(recorder, bridge, flushOnBlur) {
|
|
105
|
+
bridge?.onFlushRequest(() => recorder.flush());
|
|
106
|
+
bridge?.onMainError((error) => recorder.trackError(mainError(error)));
|
|
107
|
+
if (flushOnBlur && typeof window !== "undefined") {
|
|
108
|
+
window.addEventListener("blur", () => {
|
|
109
|
+
void recorder.flush();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function mainError(error) {
|
|
114
|
+
const value = new Error(typeof error?.message === "string" ? error.message : "error");
|
|
115
|
+
value.name = typeof error?.name === "string" && error.name ? error.name : "Error";
|
|
116
|
+
value.stack = typeof error?.stack === "string" ? error.stack : "";
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function findBridge() {
|
|
120
|
+
const candidate = globalThis[BRIDGE_KEY];
|
|
121
|
+
return candidate && typeof candidate.appInfo === "function" ? candidate : null;
|
|
122
|
+
}
|
|
123
|
+
function __resetForTests() {
|
|
124
|
+
current?.stop();
|
|
125
|
+
current = null;
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
BRIDGE_KEY,
|
|
129
|
+
CHANNELS,
|
|
130
|
+
SDK_NAME,
|
|
131
|
+
SDK_VERSION,
|
|
132
|
+
__resetForTests,
|
|
133
|
+
electronSetup,
|
|
134
|
+
init,
|
|
135
|
+
mainError
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/web-family.ts","../src/index.ts"],"sourcesContent":["/**\n * Which SDK recorded a session (docs/SDK-CONTRACT.md §2.4).\n *\n * The version is written in at build time by tsup (and by Vitest for the\n * tests) from package.json, as in the browser SDK; source used without that\n * step reports `0.0.0-dev`.\n */\ndeclare const __ANYREPLAY_SDK_VERSION__: string | undefined;\n\nexport const SDK_NAME = '@anyreplay/electron';\n\nexport const SDK_VERSION: string =\n typeof __ANYREPLAY_SDK_VERSION__ === 'string' ? __ANYREPLAY_SDK_VERSION__ : '0.0.0-dev';\n\nexport const PLATFORM = 'electron';\n","import {\n createRecorder, type AnyReplayOptions, type RecorderHandle, type RecorderStatus, type ShellInfo,\n} from '@anyreplay/browser';\n\n/**\n * What a desktop shell hands the browser SDK: its options, and who is\n * recording (docs/SDK-CONTRACT.md §1, §4) — the platform and SDK the session\n * reports and the app's version. `createRecorder(options, shell)` is how a\n * wrapper names itself without a public option a website could set.\n */\nexport interface RecorderSetup {\n options: AnyReplayOptions;\n shell: ShellInfo;\n}\n\n/**\n * A recorder handle that exists before the recorder does.\n *\n * A shell learns its app id and version asynchronously (an IPC round trip in\n * Electron, a Tauri command), but `init` has to hand something back at once so\n * an app can call `track` on its next line. Calls made in between are queued\n * in order and replayed on the real handle; `flush` waits for it.\n */\nexport interface DeferredHandle extends RecorderHandle {\n /** Resolves once the recorder has started (or failed to). Never rejects. */\n ready: Promise<void>;\n}\n\ntype Queued = (handle: RecorderHandle) => void;\n\n/** At most this many calls wait for the app info; enough for a boot sequence. */\nconst MAX_QUEUED = 64;\n\nexport function deferredRecorder(\n start: () => Promise<RecorderSetup | null>,\n onStarted?: (handle: RecorderHandle) => void,\n): DeferredHandle {\n let real: RecorderHandle | null = null;\n let stopped = false;\n const queue: Queued[] = [];\n\n const run = (call: Queued): void => {\n if (real) {\n call(real);\n return;\n }\n if (stopped || queue.length >= MAX_QUEUED) return;\n queue.push(call);\n };\n\n const ready = start()\n .then((setup) => {\n if (!setup || stopped) return;\n real = createRecorder(setup.options, setup.shell);\n for (const call of queue.splice(0)) call(real);\n onStarted?.(real);\n })\n .catch((error: unknown) => {\n // Nothing this library does may surface as an error in the app.\n if (typeof console !== 'undefined') console.warn('[anyreplay]', error);\n });\n\n return {\n ready,\n status: (): RecorderStatus => (real ? real.status() : stopped ? 'stopped' : 'idle'),\n sessionId: () => real?.sessionId() ?? null,\n consent: (granted) => run((h) => h.consent(granted)),\n identify: (traits) => run((h) => h.identify(traits)),\n track: (name, properties) => run((h) => h.track(name, properties)),\n trackError: (error) => run((h) => h.trackError(error)),\n stop: () => {\n if (real) real.stop();\n else { stopped = true; queue.length = 0; }\n },\n flush: async () => {\n await ready;\n await real?.flush();\n },\n };\n}\n\n/** The browser SDK's app-id rule (`APP_ID_PATTERN`), so a bad id is caught here with a better message. */\nexport const APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;\n\n/** `meta.appVersion` is at most 40 characters (§2.5). */\nexport const clipVersion = (version: string | undefined): string | undefined => {\n const trimmed = version?.trim();\n return trimmed ? trimmed.slice(0, 40) : undefined;\n};\n","import type { AnyReplayOptions, RecorderHandle } from '@anyreplay/browser';\nimport { BRIDGE_KEY, type AnyReplayBridge, type AppInfo, type MainErrorMessage } from './bridge.js';\nimport { PLATFORM, SDK_NAME, SDK_VERSION } from './version.js';\nimport { APP_ID_PATTERN, clipVersion, deferredRecorder, type DeferredHandle, type RecorderSetup } from './web-family.js';\n\nexport { BRIDGE_KEY, CHANNELS, type AnyReplayBridge, type AppInfo, type MainErrorMessage } from './bridge.js';\nexport { SDK_NAME, SDK_VERSION } from './version.js';\nexport type { RecorderSetup } from './web-family.js';\n\nexport interface AnyReplayElectronOptions extends AnyReplayOptions {\n /**\n * The app id. Normally left out: the main process supplies it\n * (`setupAnyReplay({ appId })`). Set here, it wins.\n */\n appId?: string;\n /** The app version. Normally left out: the main process supplies `app.getVersion()`. */\n appVersion?: string;\n /**\n * Send what has been recorded whenever the window loses focus. On by\n * default: a desktop app is rarely closed the way a tab is, and the main\n * process can be killed without `before-quit`.\n */\n flushOnBlur?: boolean;\n}\n\nexport type AnyReplayElectron = DeferredHandle;\n\nlet current: AnyReplayElectron | null = null;\n\n/**\n * Starts recording this window. Call it once in the renderer, as early as you\n * can:\n *\n * ```ts\n * import { init } from '@anyreplay/electron';\n * const replay = init({ projectKey: 'ar_pk_live_…' });\n * replay.track('document_opened');\n * ```\n *\n * The app id and version come from the main process through the preload\n * bridge (`exposeAnyReplay()` and `setupAnyReplay()`), so recording starts a\n * moment after this returns; calls made in between are queued. A second call\n * returns the first handle.\n *\n * Each window is its own session: the session id lives in the window's\n * `sessionStorage`, the visitor id in `localStorage`, which Electron keeps\n * across launches and shares between windows of the same session partition.\n */\nexport function init(options: AnyReplayElectronOptions): AnyReplayElectron {\n if (current) return current;\n const bridge = findBridge();\n const { flushOnBlur = true, ...rest } = options;\n\n const handle = deferredRecorder(\n async () => {\n const info: AppInfo = bridge ? await bridge.appInfo() : {};\n return electronSetup(rest, info);\n },\n (recorder) => wire(recorder, bridge, flushOnBlur),\n );\n current = handle;\n return handle;\n}\n\n/**\n * The browser SDK's options for this window — the installer's, with the app\n * id filled in — and the shell it runs in, which only the wrapper knows.\n */\nexport function electronSetup(options: AnyReplayElectronOptions, info: AppInfo): RecorderSetup {\n const appId = options.appId?.trim() || info.appId;\n if (!appId && typeof console !== 'undefined') {\n console.warn('[anyreplay] no appId: call setupAnyReplay({ appId }) in the main process and exposeAnyReplay() in the preload. A project that lists allowed apps refuses sessions without one.');\n }\n if (appId && !APP_ID_PATTERN.test(appId)) {\n // Thrown by the browser SDK too; said here in the shell's own terms.\n throw new Error(`anyreplay: appId ${JSON.stringify(appId)} should be an app identifier such as com.example.app`);\n }\n const { appVersion: ownVersion, ...browserOptions } = options;\n const appVersion = clipVersion(ownVersion ?? info.appVersion);\n return {\n options: {\n // Inline unless the app says otherwise: a window loaded from the app's\n // own files has addresses no reviewer can load (contract §9.1).\n assets: 'inline',\n ...browserOptions,\n ...(appId ? { appId } : {}),\n },\n shell: {\n platform: PLATFORM,\n sdk: { name: SDK_NAME, version: SDK_VERSION },\n ...(appVersion ? { appVersion } : {}),\n },\n };\n}\n\nfunction wire(recorder: RecorderHandle, bridge: AnyReplayBridge | null, flushOnBlur: boolean): void {\n bridge?.onFlushRequest(() => recorder.flush());\n // An error like the window's own: the same `anyreplay.error` event, the same\n // redaction and ceiling, and the session's `hasError` set (contract §8.3).\n bridge?.onMainError((error) => recorder.trackError(mainError(error)));\n if (flushOnBlur && typeof window !== 'undefined') {\n window.addEventListener('blur', () => { void recorder.flush(); });\n }\n}\n\n/**\n * A main-process error as an `Error` again — it crossed IPC as plain fields —\n * for the browser SDK to redact and clip like any other.\n */\nexport function mainError(error: MainErrorMessage | undefined): Error {\n const value = new Error(typeof error?.message === 'string' ? error.message : 'error');\n value.name = typeof error?.name === 'string' && error.name ? error.name : 'Error';\n value.stack = typeof error?.stack === 'string' ? error.stack : '';\n return value;\n}\n\nfunction findBridge(): AnyReplayBridge | null {\n const candidate = (globalThis as Record<string, unknown>)[BRIDGE_KEY] as AnyReplayBridge | undefined;\n return candidate && typeof candidate.appInfo === 'function' ? candidate : null;\n}\n\n/** For tests: forget the handle `init` returned. */\nexport function __resetForTests(): void {\n current?.stop();\n current = null;\n}\n"],"mappings":"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","names":[]}
|
package/dist/main.cjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/main.ts
|
|
21
|
+
var main_exports = {};
|
|
22
|
+
__export(main_exports, {
|
|
23
|
+
describeError: () => describeError,
|
|
24
|
+
flushAll: () => flushAll,
|
|
25
|
+
resolveAppId: () => resolveAppId,
|
|
26
|
+
setupAnyReplay: () => setupAnyReplay
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(main_exports);
|
|
29
|
+
var import_electron = require("electron");
|
|
30
|
+
|
|
31
|
+
// src/bridge.ts
|
|
32
|
+
var CHANNELS = {
|
|
33
|
+
/** renderer → main (invoke): `AppInfo`. */
|
|
34
|
+
appInfo: "anyreplay:app-info",
|
|
35
|
+
/** main → renderer: flush now, then answer on `flushed` with the same id. */
|
|
36
|
+
flush: "anyreplay:flush",
|
|
37
|
+
/** renderer → main: a flush asked for by `flush` has finished. */
|
|
38
|
+
flushed: "anyreplay:flushed",
|
|
39
|
+
/** main → renderer: an uncaught exception in the main process. */
|
|
40
|
+
mainError: "anyreplay:main-error"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/main.ts
|
|
44
|
+
var DEFAULT_QUIT_FLUSH_TIMEOUT_MS = 1500;
|
|
45
|
+
var APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;
|
|
46
|
+
function resolveAppId(explicit, name) {
|
|
47
|
+
const given = explicit?.trim();
|
|
48
|
+
if (given) return APP_ID_PATTERN.test(given) ? given : void 0;
|
|
49
|
+
const raw = name?.trim() ?? "";
|
|
50
|
+
if (APP_ID_PATTERN.test(raw)) return raw;
|
|
51
|
+
const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+|-+$/g, "");
|
|
52
|
+
return APP_ID_PATTERN.test(slug) ? slug : void 0;
|
|
53
|
+
}
|
|
54
|
+
function setupAnyReplay(options = {}) {
|
|
55
|
+
const appId = resolveAppId(options.appId, import_electron.app.getName());
|
|
56
|
+
if (options.appId !== void 0 && !appId) {
|
|
57
|
+
console.warn(`[anyreplay] appId ${JSON.stringify(options.appId)} is not an app identifier such as com.example.app; sessions are sent without one`);
|
|
58
|
+
}
|
|
59
|
+
const info = {
|
|
60
|
+
...appId ? { appId } : {},
|
|
61
|
+
appVersion: import_electron.app.getVersion()
|
|
62
|
+
};
|
|
63
|
+
const cleanup = [];
|
|
64
|
+
import_electron.ipcMain.handle(CHANNELS.appInfo, () => info);
|
|
65
|
+
cleanup.push(() => import_electron.ipcMain.removeHandler(CHANNELS.appInfo));
|
|
66
|
+
const timeout = options.quitFlushTimeoutMs ?? DEFAULT_QUIT_FLUSH_TIMEOUT_MS;
|
|
67
|
+
if (timeout > 0) {
|
|
68
|
+
let flushed = false;
|
|
69
|
+
let flushing = false;
|
|
70
|
+
const onBeforeQuit = (event) => {
|
|
71
|
+
if (flushed) return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
if (flushing) return;
|
|
74
|
+
flushing = true;
|
|
75
|
+
void flushAll(import_electron.BrowserWindow.getAllWindows().map((window) => window.webContents), timeout).finally(() => {
|
|
76
|
+
flushed = true;
|
|
77
|
+
import_electron.app.quit();
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
import_electron.app.on("before-quit", onBeforeQuit);
|
|
81
|
+
cleanup.push(() => import_electron.app.removeListener("before-quit", onBeforeQuit));
|
|
82
|
+
}
|
|
83
|
+
if (options.forwardMainErrors !== false) {
|
|
84
|
+
const onError = (error) => {
|
|
85
|
+
try {
|
|
86
|
+
const target = import_electron.BrowserWindow.getFocusedWindow() ?? import_electron.BrowserWindow.getAllWindows()[0];
|
|
87
|
+
if (!target || target.isDestroyed()) return;
|
|
88
|
+
target.webContents.send(CHANNELS.mainError, describeError(error));
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
process.on("uncaughtExceptionMonitor", onError);
|
|
93
|
+
cleanup.push(() => process.removeListener("uncaughtExceptionMonitor", onError));
|
|
94
|
+
}
|
|
95
|
+
return { dispose: () => {
|
|
96
|
+
for (const undo of cleanup.splice(0).reverse()) undo();
|
|
97
|
+
} };
|
|
98
|
+
}
|
|
99
|
+
function describeError(error) {
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
return { name: error.name, message: error.message, ...error.stack ? { stack: error.stack } : {} };
|
|
102
|
+
}
|
|
103
|
+
return { message: String(error) };
|
|
104
|
+
}
|
|
105
|
+
function flushAll(targets, timeoutMs) {
|
|
106
|
+
const live = targets.filter((contents) => !contents.isDestroyed());
|
|
107
|
+
if (live.length === 0) return Promise.resolve();
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
110
|
+
const waiting = new Set(live.map((contents) => contents.id));
|
|
111
|
+
let done = false;
|
|
112
|
+
const finish = () => {
|
|
113
|
+
if (done) return;
|
|
114
|
+
done = true;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
import_electron.ipcMain.removeListener(CHANNELS.flushed, onFlushed);
|
|
117
|
+
resolve();
|
|
118
|
+
};
|
|
119
|
+
const onFlushed = (event, answered) => {
|
|
120
|
+
if (answered !== id) return;
|
|
121
|
+
waiting.delete(event.sender.id);
|
|
122
|
+
if (waiting.size === 0) finish();
|
|
123
|
+
};
|
|
124
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
125
|
+
import_electron.ipcMain.on(CHANNELS.flushed, onFlushed);
|
|
126
|
+
for (const contents of live) {
|
|
127
|
+
try {
|
|
128
|
+
contents.send(CHANNELS.flush, id);
|
|
129
|
+
} catch {
|
|
130
|
+
waiting.delete(contents.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (waiting.size === 0) finish();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
137
|
+
0 && (module.exports = {
|
|
138
|
+
describeError,
|
|
139
|
+
flushAll,
|
|
140
|
+
resolveAppId,
|
|
141
|
+
setupAnyReplay
|
|
142
|
+
});
|
|
143
|
+
//# sourceMappingURL=main.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/main.ts","../src/bridge.ts"],"sourcesContent":["import { app, BrowserWindow, ipcMain, type WebContents } from 'electron';\nimport { CHANNELS, type AppInfo, type MainErrorMessage } from './bridge.js';\n\nexport interface AnyReplayMainOptions {\n /**\n * The app id sessions are reported under (§2.2 of the SDK contract) — the\n * id you list under the project's allowed apps. Use your bundle id, the\n * `appId` from electron-builder or Forge's `appBundleId`\n * (`com.example.notes`).\n *\n * Electron has no API for the bundle id, so without this the id is made from\n * `app.getName()`: kept as it is when it is already a valid id, otherwise\n * lower-cased with every run of other characters turned into `-`\n * (`\"Acme Notes\"` → `acme-notes`). Pass it explicitly in anything you ship.\n */\n appId?: string;\n /**\n * How long quitting may wait for the windows to send what they have\n * recorded, in milliseconds. Default 1500; `0` turns the flush off.\n */\n quitFlushTimeoutMs?: number;\n /**\n * Forward uncaught exceptions in the main process into the focused window's\n * recording as an error, as if the window had caught it (`trackError`): it\n * shows in the replay's errors and marks the session as having one. On by\n * default.\n *\n * Observed with `uncaughtExceptionMonitor`, so it changes nothing about how\n * your app handles (or does not handle) the error. The message and stack are\n * redacted in the window before they are recorded, by the same rules as the\n * page's own errors.\n */\n forwardMainErrors?: boolean;\n}\n\nexport interface AnyReplayMainHandle {\n /** Removes every listener and handler this installed. */\n dispose: () => void;\n}\n\nconst DEFAULT_QUIT_FLUSH_TIMEOUT_MS = 1500;\nconst APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;\n\n/** The app id from an explicit option or, failing that, from the app's name. */\nexport function resolveAppId(explicit: string | undefined, name: string | undefined): string | undefined {\n const given = explicit?.trim();\n if (given) return APP_ID_PATTERN.test(given) ? given : undefined;\n const raw = name?.trim() ?? '';\n if (APP_ID_PATTERN.test(raw)) return raw;\n const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[^a-z0-9]+|-+$/g, '');\n return APP_ID_PATTERN.test(slug) ? slug : undefined;\n}\n\n/**\n * Wires the main process to the recorder in your windows. Call it once, early\n * in your main process, before the first window loads:\n *\n * ```ts\n * // main.ts\n * import { setupAnyReplay } from '@anyreplay/electron/main';\n * setupAnyReplay({ appId: 'com.example.notes' });\n * ```\n *\n * It answers the windows' request for the app id and version, holds quitting\n * back (briefly) until every window has sent what it recorded, and forwards\n * uncaught main-process exceptions into the focused window's session.\n */\nexport function setupAnyReplay(options: AnyReplayMainOptions = {}): AnyReplayMainHandle {\n const appId = resolveAppId(options.appId, app.getName());\n if (options.appId !== undefined && !appId) {\n console.warn(`[anyreplay] appId ${JSON.stringify(options.appId)} is not an app identifier such as com.example.app; sessions are sent without one`);\n }\n const info: AppInfo = {\n ...(appId ? { appId } : {}),\n appVersion: app.getVersion(),\n };\n const cleanup: (() => void)[] = [];\n\n ipcMain.handle(CHANNELS.appInfo, () => info);\n cleanup.push(() => ipcMain.removeHandler(CHANNELS.appInfo));\n\n const timeout = options.quitFlushTimeoutMs ?? DEFAULT_QUIT_FLUSH_TIMEOUT_MS;\n if (timeout > 0) {\n let flushed = false;\n let flushing = false;\n const onBeforeQuit = (event: Electron.Event): void => {\n if (flushed) return;\n event.preventDefault();\n if (flushing) return;\n flushing = true;\n void flushAll(BrowserWindow.getAllWindows().map((window) => window.webContents), timeout).finally(() => {\n flushed = true;\n app.quit();\n });\n };\n app.on('before-quit', onBeforeQuit);\n cleanup.push(() => app.removeListener('before-quit', onBeforeQuit));\n }\n\n if (options.forwardMainErrors !== false) {\n const onError = (error: Error): void => {\n try {\n const target = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];\n if (!target || target.isDestroyed()) return;\n target.webContents.send(CHANNELS.mainError, describeError(error));\n } catch {\n /* the app is already failing; this must not add to it */\n }\n };\n process.on('uncaughtExceptionMonitor', onError);\n cleanup.push(() => process.removeListener('uncaughtExceptionMonitor', onError));\n }\n\n return { dispose: () => { for (const undo of cleanup.splice(0).reverse()) undo(); } };\n}\n\n/** What crosses to the window: plain strings, since an Error does not survive structured cloning whole. */\nexport function describeError(error: unknown): MainErrorMessage {\n if (error instanceof Error) {\n return { name: error.name, message: error.message, ...(error.stack ? { stack: error.stack } : {}) };\n }\n return { message: String(error) };\n}\n\n/**\n * Asks every page to flush and waits for all of them, or for the timeout.\n *\n * A page with no recorder (or no preload) never answers; the timeout is what\n * keeps that from holding quitting up for longer than it was allowed to.\n */\nexport function flushAll(targets: readonly WebContents[], timeoutMs: number): Promise<void> {\n const live = targets.filter((contents) => !contents.isDestroyed());\n if (live.length === 0) return Promise.resolve();\n\n return new Promise<void>((resolve) => {\n const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n const waiting = new Set(live.map((contents) => contents.id));\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n ipcMain.removeListener(CHANNELS.flushed, onFlushed);\n resolve();\n };\n const onFlushed = (event: Electron.IpcMainEvent, answered: unknown): void => {\n if (answered !== id) return;\n waiting.delete(event.sender.id);\n if (waiting.size === 0) finish();\n };\n const timer = setTimeout(finish, timeoutMs);\n ipcMain.on(CHANNELS.flushed, onFlushed);\n for (const contents of live) {\n try {\n contents.send(CHANNELS.flush, id);\n } catch {\n waiting.delete(contents.id);\n }\n }\n if (waiting.size === 0) finish();\n });\n}\n","/**\n * The contract between the three halves of the Electron SDK.\n *\n * The renderer records; the preload script is the only code that can talk to\n * the main process; the main process knows the app's id and version and sees\n * the app quit. They share nothing but these names, so each entry point can be\n * imported on its own without pulling in the others (or the recorder, which\n * has no business in the main process).\n */\n\n/** The global the preload script exposes through `contextBridge`. */\nexport const BRIDGE_KEY = 'anyreplayElectron';\n\n/** IPC channels. All of them, so a reviewer can see exactly what crosses. */\nexport const CHANNELS = {\n /** renderer → main (invoke): `AppInfo`. */\n appInfo: 'anyreplay:app-info',\n /** main → renderer: flush now, then answer on `flushed` with the same id. */\n flush: 'anyreplay:flush',\n /** renderer → main: a flush asked for by `flush` has finished. */\n flushed: 'anyreplay:flushed',\n /** main → renderer: an uncaught exception in the main process. */\n mainError: 'anyreplay:main-error',\n} as const;\n\n/** What the main process says about the app. */\nexport interface AppInfo {\n /** The app id (§2.2 of the SDK contract), already checked against the id pattern. Absent when none could be found. */\n appId?: string;\n /** `app.getVersion()`. */\n appVersion?: string;\n}\n\n/** An uncaught exception in the main process, as sent to a window. Raw: the renderer redacts it. */\nexport interface MainErrorMessage {\n name?: string;\n message: string;\n stack?: string;\n}\n\n/**\n * What the preload script exposes on `window.anyreplayElectron`.\n *\n * Deliberately narrow: three fixed channels, no generic `send` or `invoke`, so\n * exposing it gives page script no more reach into the main process than\n * asking for the app's version and answering a flush.\n */\nexport interface AnyReplayBridge {\n /** The bridge's own version, so a newer renderer can tell an older preload apart. */\n readonly version: 1;\n appInfo: () => Promise<AppInfo>;\n /** The renderer's flush; called when the main process is about to quit. Replaces any earlier one. */\n onFlushRequest: (flush: () => Promise<void> | void) => void;\n /** Called with each main-process error forwarded to this window. Replaces any earlier listener. */\n onMainError: (listener: (error: MainErrorMessage) => void) => void;\n}\n"],"mappings":"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","names":[]}
|
package/dist/main.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { WebContents } from 'electron';
|
|
2
|
+
import { M as MainErrorMessage } from './bridge-BQKD7rrG.cjs';
|
|
3
|
+
|
|
4
|
+
interface AnyReplayMainOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The app id sessions are reported under (§2.2 of the SDK contract) — the
|
|
7
|
+
* id you list under the project's allowed apps. Use your bundle id, the
|
|
8
|
+
* `appId` from electron-builder or Forge's `appBundleId`
|
|
9
|
+
* (`com.example.notes`).
|
|
10
|
+
*
|
|
11
|
+
* Electron has no API for the bundle id, so without this the id is made from
|
|
12
|
+
* `app.getName()`: kept as it is when it is already a valid id, otherwise
|
|
13
|
+
* lower-cased with every run of other characters turned into `-`
|
|
14
|
+
* (`"Acme Notes"` → `acme-notes`). Pass it explicitly in anything you ship.
|
|
15
|
+
*/
|
|
16
|
+
appId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* How long quitting may wait for the windows to send what they have
|
|
19
|
+
* recorded, in milliseconds. Default 1500; `0` turns the flush off.
|
|
20
|
+
*/
|
|
21
|
+
quitFlushTimeoutMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Forward uncaught exceptions in the main process into the focused window's
|
|
24
|
+
* recording as an error, as if the window had caught it (`trackError`): it
|
|
25
|
+
* shows in the replay's errors and marks the session as having one. On by
|
|
26
|
+
* default.
|
|
27
|
+
*
|
|
28
|
+
* Observed with `uncaughtExceptionMonitor`, so it changes nothing about how
|
|
29
|
+
* your app handles (or does not handle) the error. The message and stack are
|
|
30
|
+
* redacted in the window before they are recorded, by the same rules as the
|
|
31
|
+
* page's own errors.
|
|
32
|
+
*/
|
|
33
|
+
forwardMainErrors?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface AnyReplayMainHandle {
|
|
36
|
+
/** Removes every listener and handler this installed. */
|
|
37
|
+
dispose: () => void;
|
|
38
|
+
}
|
|
39
|
+
/** The app id from an explicit option or, failing that, from the app's name. */
|
|
40
|
+
declare function resolveAppId(explicit: string | undefined, name: string | undefined): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Wires the main process to the recorder in your windows. Call it once, early
|
|
43
|
+
* in your main process, before the first window loads:
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* // main.ts
|
|
47
|
+
* import { setupAnyReplay } from '@anyreplay/electron/main';
|
|
48
|
+
* setupAnyReplay({ appId: 'com.example.notes' });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* It answers the windows' request for the app id and version, holds quitting
|
|
52
|
+
* back (briefly) until every window has sent what it recorded, and forwards
|
|
53
|
+
* uncaught main-process exceptions into the focused window's session.
|
|
54
|
+
*/
|
|
55
|
+
declare function setupAnyReplay(options?: AnyReplayMainOptions): AnyReplayMainHandle;
|
|
56
|
+
/** What crosses to the window: plain strings, since an Error does not survive structured cloning whole. */
|
|
57
|
+
declare function describeError(error: unknown): MainErrorMessage;
|
|
58
|
+
/**
|
|
59
|
+
* Asks every page to flush and waits for all of them, or for the timeout.
|
|
60
|
+
*
|
|
61
|
+
* A page with no recorder (or no preload) never answers; the timeout is what
|
|
62
|
+
* keeps that from holding quitting up for longer than it was allowed to.
|
|
63
|
+
*/
|
|
64
|
+
declare function flushAll(targets: readonly WebContents[], timeoutMs: number): Promise<void>;
|
|
65
|
+
|
|
66
|
+
export { type AnyReplayMainHandle, type AnyReplayMainOptions, describeError, flushAll, resolveAppId, setupAnyReplay };
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { WebContents } from 'electron';
|
|
2
|
+
import { M as MainErrorMessage } from './bridge-BQKD7rrG.js';
|
|
3
|
+
|
|
4
|
+
interface AnyReplayMainOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The app id sessions are reported under (§2.2 of the SDK contract) — the
|
|
7
|
+
* id you list under the project's allowed apps. Use your bundle id, the
|
|
8
|
+
* `appId` from electron-builder or Forge's `appBundleId`
|
|
9
|
+
* (`com.example.notes`).
|
|
10
|
+
*
|
|
11
|
+
* Electron has no API for the bundle id, so without this the id is made from
|
|
12
|
+
* `app.getName()`: kept as it is when it is already a valid id, otherwise
|
|
13
|
+
* lower-cased with every run of other characters turned into `-`
|
|
14
|
+
* (`"Acme Notes"` → `acme-notes`). Pass it explicitly in anything you ship.
|
|
15
|
+
*/
|
|
16
|
+
appId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* How long quitting may wait for the windows to send what they have
|
|
19
|
+
* recorded, in milliseconds. Default 1500; `0` turns the flush off.
|
|
20
|
+
*/
|
|
21
|
+
quitFlushTimeoutMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Forward uncaught exceptions in the main process into the focused window's
|
|
24
|
+
* recording as an error, as if the window had caught it (`trackError`): it
|
|
25
|
+
* shows in the replay's errors and marks the session as having one. On by
|
|
26
|
+
* default.
|
|
27
|
+
*
|
|
28
|
+
* Observed with `uncaughtExceptionMonitor`, so it changes nothing about how
|
|
29
|
+
* your app handles (or does not handle) the error. The message and stack are
|
|
30
|
+
* redacted in the window before they are recorded, by the same rules as the
|
|
31
|
+
* page's own errors.
|
|
32
|
+
*/
|
|
33
|
+
forwardMainErrors?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface AnyReplayMainHandle {
|
|
36
|
+
/** Removes every listener and handler this installed. */
|
|
37
|
+
dispose: () => void;
|
|
38
|
+
}
|
|
39
|
+
/** The app id from an explicit option or, failing that, from the app's name. */
|
|
40
|
+
declare function resolveAppId(explicit: string | undefined, name: string | undefined): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Wires the main process to the recorder in your windows. Call it once, early
|
|
43
|
+
* in your main process, before the first window loads:
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* // main.ts
|
|
47
|
+
* import { setupAnyReplay } from '@anyreplay/electron/main';
|
|
48
|
+
* setupAnyReplay({ appId: 'com.example.notes' });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* It answers the windows' request for the app id and version, holds quitting
|
|
52
|
+
* back (briefly) until every window has sent what it recorded, and forwards
|
|
53
|
+
* uncaught main-process exceptions into the focused window's session.
|
|
54
|
+
*/
|
|
55
|
+
declare function setupAnyReplay(options?: AnyReplayMainOptions): AnyReplayMainHandle;
|
|
56
|
+
/** What crosses to the window: plain strings, since an Error does not survive structured cloning whole. */
|
|
57
|
+
declare function describeError(error: unknown): MainErrorMessage;
|
|
58
|
+
/**
|
|
59
|
+
* Asks every page to flush and waits for all of them, or for the timeout.
|
|
60
|
+
*
|
|
61
|
+
* A page with no recorder (or no preload) never answers; the timeout is what
|
|
62
|
+
* keeps that from holding quitting up for longer than it was allowed to.
|
|
63
|
+
*/
|
|
64
|
+
declare function flushAll(targets: readonly WebContents[], timeoutMs: number): Promise<void>;
|
|
65
|
+
|
|
66
|
+
export { type AnyReplayMainHandle, type AnyReplayMainOptions, describeError, flushAll, resolveAppId, setupAnyReplay };
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHANNELS
|
|
3
|
+
} from "./chunk-5TQ2HVGX.js";
|
|
4
|
+
|
|
5
|
+
// src/main.ts
|
|
6
|
+
import { app, BrowserWindow, ipcMain } from "electron";
|
|
7
|
+
var DEFAULT_QUIT_FLUSH_TIMEOUT_MS = 1500;
|
|
8
|
+
var APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;
|
|
9
|
+
function resolveAppId(explicit, name) {
|
|
10
|
+
const given = explicit?.trim();
|
|
11
|
+
if (given) return APP_ID_PATTERN.test(given) ? given : void 0;
|
|
12
|
+
const raw = name?.trim() ?? "";
|
|
13
|
+
if (APP_ID_PATTERN.test(raw)) return raw;
|
|
14
|
+
const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+|-+$/g, "");
|
|
15
|
+
return APP_ID_PATTERN.test(slug) ? slug : void 0;
|
|
16
|
+
}
|
|
17
|
+
function setupAnyReplay(options = {}) {
|
|
18
|
+
const appId = resolveAppId(options.appId, app.getName());
|
|
19
|
+
if (options.appId !== void 0 && !appId) {
|
|
20
|
+
console.warn(`[anyreplay] appId ${JSON.stringify(options.appId)} is not an app identifier such as com.example.app; sessions are sent without one`);
|
|
21
|
+
}
|
|
22
|
+
const info = {
|
|
23
|
+
...appId ? { appId } : {},
|
|
24
|
+
appVersion: app.getVersion()
|
|
25
|
+
};
|
|
26
|
+
const cleanup = [];
|
|
27
|
+
ipcMain.handle(CHANNELS.appInfo, () => info);
|
|
28
|
+
cleanup.push(() => ipcMain.removeHandler(CHANNELS.appInfo));
|
|
29
|
+
const timeout = options.quitFlushTimeoutMs ?? DEFAULT_QUIT_FLUSH_TIMEOUT_MS;
|
|
30
|
+
if (timeout > 0) {
|
|
31
|
+
let flushed = false;
|
|
32
|
+
let flushing = false;
|
|
33
|
+
const onBeforeQuit = (event) => {
|
|
34
|
+
if (flushed) return;
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
if (flushing) return;
|
|
37
|
+
flushing = true;
|
|
38
|
+
void flushAll(BrowserWindow.getAllWindows().map((window) => window.webContents), timeout).finally(() => {
|
|
39
|
+
flushed = true;
|
|
40
|
+
app.quit();
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
app.on("before-quit", onBeforeQuit);
|
|
44
|
+
cleanup.push(() => app.removeListener("before-quit", onBeforeQuit));
|
|
45
|
+
}
|
|
46
|
+
if (options.forwardMainErrors !== false) {
|
|
47
|
+
const onError = (error) => {
|
|
48
|
+
try {
|
|
49
|
+
const target = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
|
|
50
|
+
if (!target || target.isDestroyed()) return;
|
|
51
|
+
target.webContents.send(CHANNELS.mainError, describeError(error));
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
process.on("uncaughtExceptionMonitor", onError);
|
|
56
|
+
cleanup.push(() => process.removeListener("uncaughtExceptionMonitor", onError));
|
|
57
|
+
}
|
|
58
|
+
return { dispose: () => {
|
|
59
|
+
for (const undo of cleanup.splice(0).reverse()) undo();
|
|
60
|
+
} };
|
|
61
|
+
}
|
|
62
|
+
function describeError(error) {
|
|
63
|
+
if (error instanceof Error) {
|
|
64
|
+
return { name: error.name, message: error.message, ...error.stack ? { stack: error.stack } : {} };
|
|
65
|
+
}
|
|
66
|
+
return { message: String(error) };
|
|
67
|
+
}
|
|
68
|
+
function flushAll(targets, timeoutMs) {
|
|
69
|
+
const live = targets.filter((contents) => !contents.isDestroyed());
|
|
70
|
+
if (live.length === 0) return Promise.resolve();
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
73
|
+
const waiting = new Set(live.map((contents) => contents.id));
|
|
74
|
+
let done = false;
|
|
75
|
+
const finish = () => {
|
|
76
|
+
if (done) return;
|
|
77
|
+
done = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
ipcMain.removeListener(CHANNELS.flushed, onFlushed);
|
|
80
|
+
resolve();
|
|
81
|
+
};
|
|
82
|
+
const onFlushed = (event, answered) => {
|
|
83
|
+
if (answered !== id) return;
|
|
84
|
+
waiting.delete(event.sender.id);
|
|
85
|
+
if (waiting.size === 0) finish();
|
|
86
|
+
};
|
|
87
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
88
|
+
ipcMain.on(CHANNELS.flushed, onFlushed);
|
|
89
|
+
for (const contents of live) {
|
|
90
|
+
try {
|
|
91
|
+
contents.send(CHANNELS.flush, id);
|
|
92
|
+
} catch {
|
|
93
|
+
waiting.delete(contents.id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (waiting.size === 0) finish();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
describeError,
|
|
101
|
+
flushAll,
|
|
102
|
+
resolveAppId,
|
|
103
|
+
setupAnyReplay
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/main.ts"],"sourcesContent":["import { app, BrowserWindow, ipcMain, type WebContents } from 'electron';\nimport { CHANNELS, type AppInfo, type MainErrorMessage } from './bridge.js';\n\nexport interface AnyReplayMainOptions {\n /**\n * The app id sessions are reported under (§2.2 of the SDK contract) — the\n * id you list under the project's allowed apps. Use your bundle id, the\n * `appId` from electron-builder or Forge's `appBundleId`\n * (`com.example.notes`).\n *\n * Electron has no API for the bundle id, so without this the id is made from\n * `app.getName()`: kept as it is when it is already a valid id, otherwise\n * lower-cased with every run of other characters turned into `-`\n * (`\"Acme Notes\"` → `acme-notes`). Pass it explicitly in anything you ship.\n */\n appId?: string;\n /**\n * How long quitting may wait for the windows to send what they have\n * recorded, in milliseconds. Default 1500; `0` turns the flush off.\n */\n quitFlushTimeoutMs?: number;\n /**\n * Forward uncaught exceptions in the main process into the focused window's\n * recording as an error, as if the window had caught it (`trackError`): it\n * shows in the replay's errors and marks the session as having one. On by\n * default.\n *\n * Observed with `uncaughtExceptionMonitor`, so it changes nothing about how\n * your app handles (or does not handle) the error. The message and stack are\n * redacted in the window before they are recorded, by the same rules as the\n * page's own errors.\n */\n forwardMainErrors?: boolean;\n}\n\nexport interface AnyReplayMainHandle {\n /** Removes every listener and handler this installed. */\n dispose: () => void;\n}\n\nconst DEFAULT_QUIT_FLUSH_TIMEOUT_MS = 1500;\nconst APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;\n\n/** The app id from an explicit option or, failing that, from the app's name. */\nexport function resolveAppId(explicit: string | undefined, name: string | undefined): string | undefined {\n const given = explicit?.trim();\n if (given) return APP_ID_PATTERN.test(given) ? given : undefined;\n const raw = name?.trim() ?? '';\n if (APP_ID_PATTERN.test(raw)) return raw;\n const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[^a-z0-9]+|-+$/g, '');\n return APP_ID_PATTERN.test(slug) ? slug : undefined;\n}\n\n/**\n * Wires the main process to the recorder in your windows. Call it once, early\n * in your main process, before the first window loads:\n *\n * ```ts\n * // main.ts\n * import { setupAnyReplay } from '@anyreplay/electron/main';\n * setupAnyReplay({ appId: 'com.example.notes' });\n * ```\n *\n * It answers the windows' request for the app id and version, holds quitting\n * back (briefly) until every window has sent what it recorded, and forwards\n * uncaught main-process exceptions into the focused window's session.\n */\nexport function setupAnyReplay(options: AnyReplayMainOptions = {}): AnyReplayMainHandle {\n const appId = resolveAppId(options.appId, app.getName());\n if (options.appId !== undefined && !appId) {\n console.warn(`[anyreplay] appId ${JSON.stringify(options.appId)} is not an app identifier such as com.example.app; sessions are sent without one`);\n }\n const info: AppInfo = {\n ...(appId ? { appId } : {}),\n appVersion: app.getVersion(),\n };\n const cleanup: (() => void)[] = [];\n\n ipcMain.handle(CHANNELS.appInfo, () => info);\n cleanup.push(() => ipcMain.removeHandler(CHANNELS.appInfo));\n\n const timeout = options.quitFlushTimeoutMs ?? DEFAULT_QUIT_FLUSH_TIMEOUT_MS;\n if (timeout > 0) {\n let flushed = false;\n let flushing = false;\n const onBeforeQuit = (event: Electron.Event): void => {\n if (flushed) return;\n event.preventDefault();\n if (flushing) return;\n flushing = true;\n void flushAll(BrowserWindow.getAllWindows().map((window) => window.webContents), timeout).finally(() => {\n flushed = true;\n app.quit();\n });\n };\n app.on('before-quit', onBeforeQuit);\n cleanup.push(() => app.removeListener('before-quit', onBeforeQuit));\n }\n\n if (options.forwardMainErrors !== false) {\n const onError = (error: Error): void => {\n try {\n const target = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];\n if (!target || target.isDestroyed()) return;\n target.webContents.send(CHANNELS.mainError, describeError(error));\n } catch {\n /* the app is already failing; this must not add to it */\n }\n };\n process.on('uncaughtExceptionMonitor', onError);\n cleanup.push(() => process.removeListener('uncaughtExceptionMonitor', onError));\n }\n\n return { dispose: () => { for (const undo of cleanup.splice(0).reverse()) undo(); } };\n}\n\n/** What crosses to the window: plain strings, since an Error does not survive structured cloning whole. */\nexport function describeError(error: unknown): MainErrorMessage {\n if (error instanceof Error) {\n return { name: error.name, message: error.message, ...(error.stack ? { stack: error.stack } : {}) };\n }\n return { message: String(error) };\n}\n\n/**\n * Asks every page to flush and waits for all of them, or for the timeout.\n *\n * A page with no recorder (or no preload) never answers; the timeout is what\n * keeps that from holding quitting up for longer than it was allowed to.\n */\nexport function flushAll(targets: readonly WebContents[], timeoutMs: number): Promise<void> {\n const live = targets.filter((contents) => !contents.isDestroyed());\n if (live.length === 0) return Promise.resolve();\n\n return new Promise<void>((resolve) => {\n const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n const waiting = new Set(live.map((contents) => contents.id));\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n ipcMain.removeListener(CHANNELS.flushed, onFlushed);\n resolve();\n };\n const onFlushed = (event: Electron.IpcMainEvent, answered: unknown): void => {\n if (answered !== id) return;\n waiting.delete(event.sender.id);\n if (waiting.size === 0) finish();\n };\n const timer = setTimeout(finish, timeoutMs);\n ipcMain.on(CHANNELS.flushed, onFlushed);\n for (const contents of live) {\n try {\n contents.send(CHANNELS.flush, id);\n } catch {\n waiting.delete(contents.id);\n }\n }\n if (waiting.size === 0) finish();\n });\n}\n"],"mappings":"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","names":[]}
|