@erdemtuna/doc-review 0.7.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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Document serialization, shared by the in-page SDK and Node tests.
3
+ *
4
+ * Everything doc-review adds to a live page — the overlay, highlight marks, the
5
+ * injected style/script, contenteditable — must vanish on the way back to disk,
6
+ * so the saved file renders exactly as it does standalone.
7
+ */
8
+
9
+ export const UI_ATTR = "data-eh-ui";
10
+ export const MARK_ATTR = "data-eh-mark";
11
+
12
+ /** Keep a hydrated app editable even if its framework removes the attribute. */
13
+ export function keepBodyEditable(body) {
14
+ const enforce = () => {
15
+ if (body.getAttribute("contenteditable") !== "true") {
16
+ body.setAttribute("contenteditable", "true");
17
+ }
18
+ };
19
+ enforce();
20
+ const Observer = body.ownerDocument.defaultView.MutationObserver;
21
+ const observer = new Observer(enforce);
22
+ observer.observe(body, { attributes: true, attributeFilter: ["contenteditable"] });
23
+ return observer;
24
+ }
25
+
26
+ /** Serialize a document to the HTML that should live on disk. */
27
+ export function serializeDocument(doc) {
28
+ const clone = doc.documentElement.cloneNode(true);
29
+ clone.querySelectorAll(`[${UI_ATTR}]`).forEach((n) => n.remove());
30
+ clone.querySelectorAll("script[data-eh-sdk], style[data-eh-sdk]").forEach((n) => n.remove());
31
+ clone.querySelectorAll(`mark[${MARK_ATTR}]`).forEach((mark) => {
32
+ const parent = mark.parentNode;
33
+ if (!parent) return;
34
+ while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
35
+ parent.removeChild(mark);
36
+ });
37
+ clone.querySelectorAll("[data-eh-el]").forEach((n) => n.removeAttribute("data-eh-el"));
38
+ const body = clone.querySelector("body");
39
+ if (body) {
40
+ body.removeAttribute("contenteditable");
41
+ body.removeAttribute("spellcheck");
42
+ }
43
+ clone.normalize();
44
+ const doctype = doc.doctype ? `<!DOCTYPE ${doc.doctype.name}>\n` : "";
45
+ return `${doctype}${clone.outerHTML}\n`;
46
+ }
@@ -0,0 +1,26 @@
1
+ import { start } from "./server.js";
2
+
3
+ const port = Number(process.env.DOC_REVIEW_PORT || 0);
4
+ let review;
5
+ try {
6
+ review = await start(port);
7
+ } catch (err) {
8
+ if (err.code !== "SERVER_LOCKED") {
9
+ console.error(`doc-review server could not start: ${err.message}`);
10
+ process.exitCode = 1;
11
+ }
12
+ }
13
+
14
+ // The detached server exits on its own once idle (see IDLE_SHUTDOWN_MS).
15
+ async function shutdown() {
16
+ if (!review) return;
17
+ try {
18
+ await review.dispose();
19
+ } catch (err) {
20
+ console.error(`doc-review server shutdown failed: ${err.message}`);
21
+ process.exitCode = 1;
22
+ }
23
+ }
24
+
25
+ process.once("SIGTERM", shutdown);
26
+ process.once("SIGINT", shutdown);
@@ -0,0 +1,101 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { ensureStateDir, serverLockPath, serverPath } from "./paths.js";
4
+
5
+ const sameOwner = (left, right) =>
6
+ !!left &&
7
+ !!right &&
8
+ Number(left.pid) === Number(right.pid) &&
9
+ String(left.instance_id || "") === String(right.instance_id || "");
10
+
11
+ export function readServerLock() {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(serverLockPath(), "utf8"));
14
+ } catch (err) {
15
+ if (err.code === "ENOENT") return null;
16
+ throw err;
17
+ }
18
+ }
19
+
20
+ export function isProcessAlive(pid) {
21
+ if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
22
+ try {
23
+ process.kill(Number(pid), 0);
24
+ return true;
25
+ } catch (err) {
26
+ if (err.code === "EPERM") return true;
27
+ if (err.code === "ESRCH") return false;
28
+ throw err;
29
+ }
30
+ }
31
+
32
+ export function acquireServerLock({ pid = process.pid, instanceId = crypto.randomBytes(16).toString("hex") } = {}) {
33
+ ensureStateDir();
34
+ const owner = { pid, instance_id: instanceId };
35
+ const lockFile = serverLockPath();
36
+
37
+ for (;;) {
38
+ try {
39
+ fs.writeFileSync(lockFile, JSON.stringify(owner), { flag: "wx", mode: 0o600 });
40
+ return owner;
41
+ } catch (err) {
42
+ if (err.code !== "EEXIST") throw err;
43
+ }
44
+
45
+ const existing = readServerLock();
46
+ if (!existing || !existing.instance_id || !Number.isInteger(Number(existing.pid))) {
47
+ const invalid = new Error("The doc-review server lock is malformed and cannot be reclaimed safely.");
48
+ invalid.code = "SERVER_LOCK_INVALID";
49
+ throw invalid;
50
+ }
51
+ if (isProcessAlive(Number(existing.pid))) {
52
+ const locked = new Error(`Doc Review state is already owned by server PID ${existing.pid}.`);
53
+ locked.code = "SERVER_LOCKED";
54
+ locked.owner = existing;
55
+ throw locked;
56
+ }
57
+
58
+ const quarantine = `${lockFile}.${existing.instance_id}.${crypto.randomBytes(6).toString("hex")}.stale`;
59
+ try {
60
+ fs.renameSync(lockFile, quarantine);
61
+ } catch (err) {
62
+ if (err.code === "ENOENT") continue;
63
+ throw err;
64
+ }
65
+ const moved = JSON.parse(fs.readFileSync(quarantine, "utf8"));
66
+ if (!sameOwner(moved, existing)) {
67
+ throw new Error("The doc-review server lock changed while reclaiming it.");
68
+ }
69
+ fs.unlinkSync(quarantine);
70
+ }
71
+ }
72
+
73
+ export function releaseServerLock(owner) {
74
+ const existing = readServerLock();
75
+ if (!sameOwner(existing, owner)) return false;
76
+ try {
77
+ fs.unlinkSync(serverLockPath());
78
+ return true;
79
+ } catch (err) {
80
+ if (err.code === "ENOENT") return false;
81
+ throw err;
82
+ }
83
+ }
84
+
85
+ export function removeOwnedServerRecord(owner) {
86
+ let record;
87
+ try {
88
+ record = JSON.parse(fs.readFileSync(serverPath(), "utf8"));
89
+ } catch (err) {
90
+ if (err.code === "ENOENT") return false;
91
+ throw err;
92
+ }
93
+ if (!sameOwner(record, owner)) return false;
94
+ try {
95
+ fs.unlinkSync(serverPath());
96
+ return true;
97
+ } catch (err) {
98
+ if (err.code === "ENOENT") return false;
99
+ throw err;
100
+ }
101
+ }