@erdemtuna/doc-review 0.8.0 → 0.9.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.
- package/README.md +155 -19
- package/package.json +4 -2
- package/src/SKILL.md +56 -7
- package/src/atomic-write.js +53 -0
- package/src/chrome-client.js +844 -103
- package/src/chrome.css +253 -8
- package/src/chrome.html +78 -11
- package/src/cli.js +52 -130
- package/src/comment-target.js +87 -0
- package/src/comparison-view.js +337 -0
- package/src/document-execution.js +103 -0
- package/src/document-trust.js +2 -0
- package/src/edit-limits.js +23 -0
- package/src/execution-client.js +63 -0
- package/src/frame-policy.js +38 -0
- package/src/history-client.js +104 -0
- package/src/history-coordinator.js +186 -0
- package/src/history-policy.js +43 -0
- package/src/history-server.js +463 -0
- package/src/paths.js +1 -1
- package/src/poll-transport.js +222 -0
- package/src/review-mode.js +1 -1
- package/src/revision-diff.js +440 -0
- package/src/revision-schema.js +219 -0
- package/src/revision-store.js +177 -0
- package/src/sdk.js +419 -55
- package/src/semantic-snapshot.js +230 -0
- package/src/server.js +240 -53
- package/src/setup-guidance.js +128 -0
- package/src/setup.js +41 -15
- package/src/state.js +257 -35
- package/src/view-identity.js +99 -0
package/README.md
CHANGED
|
@@ -37,6 +37,20 @@ no longer need them.
|
|
|
37
37
|
|
|
38
38
|
## How to use /doc-review
|
|
39
39
|
|
|
40
|
+
Review is opt-in: explicitly invoke `/doc-review` or ask to open an interactive
|
|
41
|
+
browser review. Writing, updating, or asking for a general review of content does
|
|
42
|
+
not automatically open the browser or start polling. An explicitly started review
|
|
43
|
+
continues until you end it or switch tasks.
|
|
44
|
+
|
|
45
|
+
After updating installed skill instructions, start a fresh agent session or reload
|
|
46
|
+
skills (in Copilot CLI, `/skills reload`). Setup copies the executing package's
|
|
47
|
+
skill template; rerunning setup from an older package can restore older behavior.
|
|
48
|
+
Setup updates its own `AGENTS.md` guidance inside `<!-- BEGIN doc-review -->` and
|
|
49
|
+
`<!-- END doc-review -->` markers. Exact, known older generated blocks are migrated.
|
|
50
|
+
Custom guidance is preserved with a migration message rather than overwritten;
|
|
51
|
+
update it yourself if it requests automatic review. Invalid or duplicate markers
|
|
52
|
+
must be corrected before setup can proceed.
|
|
53
|
+
|
|
40
54
|

|
|
41
55
|
|
|
42
56
|
Open an HTML or Markdown file:
|
|
@@ -51,7 +65,7 @@ Review a page running on localhost:
|
|
|
51
65
|
/doc-review (localhost URL)
|
|
52
66
|
```
|
|
53
67
|
|
|
54
|
-
Doc Review opens in **View**
|
|
68
|
+
Doc Review opens in **Review**, with **View** selected, so you can read, use page controls, and comment. Switch to **Edit** to change content directly. Plain HTML saves edits to the file; scripted HTML, Markdown, and localhost pages send edits to your agent. Commenting stays available in both modes.
|
|
55
69
|
|
|
56
70
|
Select text, or hover or focus an element, then use the nearby comment icon. `Ctrl+Alt+M` (`Cmd+Option+M` on macOS) opens a comment for the current selection. Press Enter to submit or Shift+Enter for a new line. On desktop the composer stays beside its target, or pins to the effective top or bottom scrolling edge when that target leaves view. **Back to selection** reveals the target without changing the draft. The full-width sheet is used only at the narrow responsive breakpoint.
|
|
57
71
|
|
|
@@ -59,23 +73,42 @@ Select text, or hover or focus an element, then use the nearby comment icon. `Ct
|
|
|
59
73
|
|
|
60
74
|
Aligned cards show **Edit**, **Close**, and **More**; drawer cards show **Jump to**, **Edit**, and **More**. Delete lives only in **More** and requires an inline confirmation. Quote hints preserve both the beginning and ending of long selections. Editing has explicit **Save** and **Cancel** controls: Enter saves, Shift+Enter adds a line, Escape cancels, and moving focus never autosaves. A draft follows its comment between aligned and drawer cards and keeps its caret through target movement.
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
### Files and page interactions
|
|
77
|
+
|
|
78
|
+
Self-contained HTML runs its inline scripts and event handlers automatically.
|
|
79
|
+
There is no approval switch or renewed approval after an agent updates the file.
|
|
80
|
+
Plain HTML, including ordinary JSON data blocks and escaped code examples, keeps
|
|
81
|
+
direct autosave and Revert. Scripted HTML is **feedback-only**: your edits are sent
|
|
82
|
+
to the agent to apply to the original source, rather than writing script-generated
|
|
83
|
+
DOM into the file. The renderer re-evaluates that distinction when the source changes.
|
|
84
|
+
Markdown and localhost also remain feedback-only.
|
|
85
|
+
|
|
86
|
+
If a scripted page is broken, **More > Reload without scripts** provides a
|
|
87
|
+
temporary recovery mode. **Use page interactions** restores automatic behavior.
|
|
88
|
+
That preference belongs to this page in this review session, not to a saved
|
|
89
|
+
approval record. Disabling scripts does not make a scripted file's preview
|
|
90
|
+
edits writable. A changed preference takes effect when the frame is replaced;
|
|
91
|
+
reload handling preserves comment drafts and reports genuine source conflicts.
|
|
92
|
+
|
|
93
|
+
The supported interactive-file mode is for self-contained documents. Separate
|
|
94
|
+
script files, application imports, workers, and embedded applications are not
|
|
95
|
+
made compatible by this change. Use the existing localhost review route for
|
|
96
|
+
application workflows. Dependency limitations appear in contextual details,
|
|
97
|
+
not as a permission task or text inserted into the authored document.
|
|
98
|
+
|
|
99
|
+
Files retain an opaque iframe origin. The parent owns API authentication and
|
|
100
|
+
checks the frame identity for messages and writes. Authored code shares the
|
|
101
|
+
document with the SDK, so frame correlation is not proof of human authorship.
|
|
102
|
+
Relative assets stay scoped to the reviewed file. Localhost reviews retain their
|
|
103
|
+
existing nonopaque artifact origin, popup, and download behavior; they are fetched
|
|
104
|
+
and rewritten by the review server, not guaranteed to reproduce the application's
|
|
105
|
+
original cookies, storage, or origin-sensitive requests.
|
|
106
|
+
|
|
107
|
+
**Compatibility:** the old per-version `/trust` API is retired and returns 410.
|
|
108
|
+
Existing decision files are left unused. The CLI does not reuse background
|
|
109
|
+
servers with the previous protocol. Writable HTML save and revert requests must
|
|
110
|
+
identify their current served frame and source hash so delayed edits cannot
|
|
111
|
+
overwrite a newer file.
|
|
79
112
|
|
|
80
113
|
Agent polling returns an immutable `batch_id`. After applying the batch, the
|
|
81
114
|
agent acknowledges that exact receipt and keeps waiting:
|
|
@@ -87,6 +120,109 @@ npx -y @erdemtuna/doc-review poll path/to/file.html --ack b_0123456789abcdef --t
|
|
|
87
120
|
A stale or repeated batch ID is harmless: it never clears newer feedback. The
|
|
88
121
|
complete acknowledgement command is included in each response's `next_step`.
|
|
89
122
|
|
|
123
|
+
### Review and Changes
|
|
124
|
+
|
|
125
|
+
**Review** keeps the document interactive. **Changes** shows a selected review
|
|
126
|
+
round, with round selection and comparison navigation instead of live-page controls.
|
|
127
|
+
The default comparison is the content captured when feedback was sent against
|
|
128
|
+
the result captured after the agent acknowledged that exact batch. Previous
|
|
129
|
+
rounds retain their own fixed endpoints rather than changing on every reload.
|
|
130
|
+
|
|
131
|
+
Content comparisons use a continuous aligned before/after reading surface with
|
|
132
|
+
expandable unchanged context. Source comparisons use line-numbered source hunks.
|
|
133
|
+
At narrow widths, a unified view retains each change's before/after attribution.
|
|
134
|
+
Content comparisons cover text, structure, formatting, links, and image
|
|
135
|
+
references. File-backed reviews also retain source for a source comparison.
|
|
136
|
+
Deleted passages remain readable even when there is no current element to jump
|
|
137
|
+
to, and uncertain matches do not pretend to identify an exact current target.
|
|
138
|
+
These are changes observed during a round, not proof of agent authorship or that
|
|
139
|
+
every comment was resolved.
|
|
140
|
+
|
|
141
|
+
History works in the normal browser for HTML, Markdown, and localhost pages.
|
|
142
|
+
It does not use screenshots, a browser extension, or automatic Git commits.
|
|
143
|
+
Live history captures the reviewed page's available content, not every possible
|
|
144
|
+
application state. Changes to image bytes at an unchanged URL, canvas pixels,
|
|
145
|
+
external stylesheet appearance, and inaccessible embedded content are outside
|
|
146
|
+
the comparison guarantee.
|
|
147
|
+
|
|
148
|
+
Send waits for edit persistence and writable HTML saves, then makes a short
|
|
149
|
+
best-effort baseline capture. Missing comparison data does not require a second
|
|
150
|
+
decision or prevent feedback from being sent. Available snapshots are retained;
|
|
151
|
+
inactive pages can have incomplete Content coverage. A last-visited live page is
|
|
152
|
+
never described as a fresh Send-time snapshot. Actual save or delivery failures
|
|
153
|
+
are reported without discarding your feedback.
|
|
154
|
+
|
|
155
|
+
When an active tab has reliable authored tab/panel identifiers, Content capture
|
|
156
|
+
records that visible view. A result on a different tab remains pending and asks
|
|
157
|
+
you to return to the original tab. Returning retries capture without clicking
|
|
158
|
+
through other tabs automatically. Unknown views and older snapshots are labelled
|
|
159
|
+
as visible-content comparisons whose matching view is unverified. This does not
|
|
160
|
+
capture all hidden panels or every application state. File Source comparison
|
|
161
|
+
still covers the whole file independently.
|
|
162
|
+
|
|
163
|
+
After acknowledgement, file-source results are captured independently of the
|
|
164
|
+
browser. Content results are captured when the appropriate reviewed page is
|
|
165
|
+
ready. These observations can have different timestamps; neither timestamp
|
|
166
|
+
claims that the document was captured at the instant of acknowledgement.
|
|
167
|
+
Background capture does not disable document interaction and retries transient
|
|
168
|
+
failures a bounded number of times. An unchanged file does not need a new reload
|
|
169
|
+
solely to capture an acknowledged result. Available Source remains readable
|
|
170
|
+
while Content is pending. Contextual retry or return-to-view actions handle
|
|
171
|
+
remaining failures; a continuously changing page may still have no stable Content
|
|
172
|
+
snapshot. Permanent finalization is optional in capture details, not a prerequisite
|
|
173
|
+
for reading the comparison. Failed or limited captures are not reported as zero
|
|
174
|
+
changes, and handled feedback remains archived even when history is unavailable.
|
|
175
|
+
|
|
176
|
+
Snapshots stay in the local Doc Review state directory. The latest five
|
|
177
|
+
completed rounds are retained automatically, while active rounds, pending
|
|
178
|
+
captures, and unsent feedback protect their referenced revisions. History starts
|
|
179
|
+
with this capability; earlier documents cannot be reconstructed from old
|
|
180
|
+
feedback receipts. Review snapshots can contain document content, so treat the
|
|
181
|
+
state directory as private.
|
|
182
|
+
Unreferenced snapshot files are collected at startup and during periodic
|
|
183
|
+
maintenance, with a one-hour grace period protecting interrupted publications.
|
|
184
|
+
Capture and comparison limits fail explicitly rather than publishing truncated
|
|
185
|
+
content as a complete comparison.
|
|
186
|
+
|
|
187
|
+
Default capture limits are 8 MiB of file source and 4 MiB of normalized content,
|
|
188
|
+
with at most 2,000 semantic blocks and 1,000,000 text characters. A single block
|
|
189
|
+
is limited to 100,000 characters. Comparisons have independent size, token, and
|
|
190
|
+
edit-work limits, including a 1,000,000-character budget and a 250 ms processing
|
|
191
|
+
budget. A stored snapshot can therefore exceed comparison limits; the UI reports
|
|
192
|
+
that limitation rather than claiming there were no changes.
|
|
193
|
+
|
|
194
|
+
### Feedback reliability and limits
|
|
195
|
+
|
|
196
|
+
`poll` without `--timeout` waits for at most 12 hours. Explicit timeouts include
|
|
197
|
+
server discovery, reconnection, and retry backoff, not just time spent connected.
|
|
198
|
+
Recoverable connection drops are retried; invalid responses, authorization errors,
|
|
199
|
+
and incompatible servers fail visibly. A timeout does not discard feedback: use
|
|
200
|
+
`status` to check it, then start another poll if the review is still wanted.
|
|
201
|
+
|
|
202
|
+
Each edit text or HTML field is limited to 200,000 Unicode code points. Oversized
|
|
203
|
+
edits carry `truncated: true` and `truncated_fields` naming incomplete fields.
|
|
204
|
+
The agent must not use partial text or HTML as a complete replacement or invent
|
|
205
|
+
the rest. It must obtain the full edit from an authoritative source or ask you
|
|
206
|
+
for it before acknowledging the batch.
|
|
207
|
+
|
|
208
|
+
External writes to an HTML or Markdown source refresh its rendered baseline without
|
|
209
|
+
clearing unsent feedback. That preserves your edits; it does not automatically
|
|
210
|
+
resolve conflicts with the changed source. HTML autosaves keep their existing
|
|
211
|
+
behavior.
|
|
212
|
+
|
|
213
|
+
### Upgrading from servers using protocol 14 or earlier
|
|
214
|
+
|
|
215
|
+
The updated CLI requires server protocol 15. An older server that is still
|
|
216
|
+
running is not silently reused or forcibly replaced. End active reviews and
|
|
217
|
+
shut down that specific old server (or let it exit when idle) before restarting
|
|
218
|
+
Doc Review. Keep the `.doc-review` state directory: pending batches and their
|
|
219
|
+
exact receipt IDs survive a controlled restart.
|
|
220
|
+
|
|
221
|
+
After installing the updated package, rerun `doc-review setup --global` for
|
|
222
|
+
personal skills and `doc-review setup` in projects with generated guidance.
|
|
223
|
+
Reload skills or start a fresh agent session afterward. A newer CLI paired with
|
|
224
|
+
older instructions is not a complete upgrade.
|
|
225
|
+
|
|
90
226
|
## What this skill lets you do
|
|
91
227
|
|
|
92
228
|
- **Edit text directly and tweak basic formatting** (e.g., bold, italic).
|
|
@@ -113,7 +249,7 @@ Doc Review works well for editing AI-generated plans, updating landing pages, re
|
|
|
113
249
|
- [`SKILL.md`](src/SKILL.md) teaches Claude Code, Codex, and other agents how to use Doc Review.
|
|
114
250
|
|
|
115
251
|
Everything runs on your computer. Doc Review doesn’t require an account, cloud service, database, or API key.
|
|
116
|
-
Comment geometry remains local, transient presentation data and is never written to review state or sent to the agent. After the agent acknowledges the exact delivered `batch_id`, the comments carried by that batch
|
|
252
|
+
Comment geometry remains local, transient presentation data and is never written to review state or sent to the agent. After the agent acknowledges the exact delivered `batch_id`, the comments carried by that batch leave the active feedback inventory; their round archive remains, and newer comments and corrections stay active.
|
|
117
253
|
|
|
118
254
|
## Upstream project
|
|
119
255
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erdemtuna/doc-review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Review and edit agent-generated files and localhost pages in the browser, then send the whole batch back to your agent.",
|
|
5
5
|
"author": "Peter Yang",
|
|
6
6
|
"homepage": "https://github.com/erdemtuna/doc-review#readme",
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"lucide-static": "^1.34.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"
|
|
56
|
+
"diff": "^9.0.0",
|
|
57
|
+
"marked": "^18.0.7",
|
|
58
|
+
"parse5": "^8.0.1"
|
|
57
59
|
}
|
|
58
60
|
}
|
package/src/SKILL.md
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doc-review
|
|
3
|
-
description: Open an HTML file, Markdown file, or localhost page
|
|
3
|
+
description: Open an HTML file, Markdown file, or localhost page for View-first interactive browser feedback. Use only when the user explicitly invokes /doc-review or requests an interactive browser review. Do not invoke merely because you write, update, discuss, or review a document or web page.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# doc-review
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
## Activation
|
|
9
|
+
|
|
10
|
+
Start only when the user explicitly invokes `/doc-review` or asks to open an
|
|
11
|
+
interactive browser review. A generic request to review, proofread, analyze, write,
|
|
12
|
+
or update content is not permission to open this workflow. Another skill's
|
|
13
|
+
automatic review step is not user permission.
|
|
14
|
+
|
|
15
|
+
Without that request, respond normally without opening a review or polling for
|
|
16
|
+
feedback. Once the user starts a review, continue its feedback loop until they
|
|
17
|
+
end it; stop if they cancel or switch to a different task.
|
|
18
|
+
|
|
19
|
+
## Review behavior
|
|
20
|
+
|
|
21
|
+
The user reviews your HTML, Markdown, or localhost page in a real browser. It starts in View.
|
|
22
|
+
Native page controls work; supported self-contained HTML scripts run automatically.
|
|
23
|
+
They can switch to Edit, comment explicitly in either mode,
|
|
10
24
|
and send the whole batch at once.
|
|
11
25
|
|
|
26
|
+
Plain HTML retains direct autosave. Scripted HTML edits are feedback-only:
|
|
27
|
+
apply them to the original source, never serialize the script-modified runtime
|
|
28
|
+
page over the file. Source changes do not require renewed approval.
|
|
29
|
+
Reload without scripts is a temporary recovery option, not permission to write
|
|
30
|
+
a scripted preview over its source. Reloads can wait for unresolved editing work.
|
|
31
|
+
Applications requiring external script dependencies should use localhost review;
|
|
32
|
+
automatic inline execution does not add support for application imports or workers.
|
|
33
|
+
|
|
12
34
|
Markdown files open rendered. Their quotes and edits reference the rendered text,
|
|
13
35
|
and the file itself is never touched — apply every change to the Markdown source,
|
|
14
36
|
keeping its formatting syntax.
|
|
@@ -36,7 +58,8 @@ carried; newer comments and corrections survive.
|
|
|
36
58
|
|
|
37
59
|
## The loop
|
|
38
60
|
|
|
39
|
-
1.
|
|
61
|
+
1. After the explicit review request, use the requested file or localhost route.
|
|
62
|
+
Create or update content, or start a local page, only as needed for that request.
|
|
40
63
|
2. Open it for the user:
|
|
41
64
|
|
|
42
65
|
```sh
|
|
@@ -56,6 +79,12 @@ carried; newer comments and corrections survive.
|
|
|
56
79
|
npx -y @erdemtuna/doc-review poll path/to/file.html --timeout 600
|
|
57
80
|
```
|
|
58
81
|
|
|
82
|
+
Without `--timeout`, the CLI stops after 12 hours. An explicit timeout covers
|
|
83
|
+
the entire operation, including server discovery, reconnects, and backoff.
|
|
84
|
+
Recoverable connection drops retry within that deadline; terminal errors
|
|
85
|
+
require action rather than another automatic poll. The 600-second command
|
|
86
|
+
above deliberately uses a shorter deadline.
|
|
87
|
+
|
|
59
88
|
Keep this command in the foreground. Do not end your turn while it is waiting.
|
|
60
89
|
If your shell returns a process or session handle, keep waiting on that handle
|
|
61
90
|
until the command exits. If it prints `{"status":"timeout"}`, no feedback has
|
|
@@ -76,6 +105,17 @@ carried; newer comments and corrections survive.
|
|
|
76
105
|
The response's `next_step` contains the complete acknowledgement command.
|
|
77
106
|
Never acknowledge a different or guessed ID.
|
|
78
107
|
|
|
108
|
+
Acknowledgement means that you handled the feedback; it is separate from
|
|
109
|
+
browser result-capture readiness. Do not wait for history capture before
|
|
110
|
+
acknowledging completed work, and do not acknowledge merely because a page
|
|
111
|
+
looks stable. The browser captures round results automatically when possible.
|
|
112
|
+
Missing comparison data does not prevent the user from sending feedback.
|
|
113
|
+
Captured differences are observations, not proof that you alone authored them.
|
|
114
|
+
Content results may wait for the same identifiable tab as the baseline.
|
|
115
|
+
The user sees a return-to-tab prompt; do not automate tab clicking to force
|
|
116
|
+
capture. Source comparison remains independent. Unknown view identity is
|
|
117
|
+
labelled unverified, not asserted as a whole-application comparison.
|
|
118
|
+
|
|
79
119
|
Repeat 3–4 until the user says they are done.
|
|
80
120
|
|
|
81
121
|
Not sure whether feedback is already waiting — say, at the start of a new turn?
|
|
@@ -119,8 +159,13 @@ One batch covers every page the user visited, grouped by file or localhost URL.
|
|
|
119
159
|
|
|
120
160
|
## Rules
|
|
121
161
|
|
|
162
|
+
- Edits marked **`truncated`** contain incomplete fields listed in
|
|
163
|
+
`truncated_fields`. Each text/HTML field is limited to 200,000 Unicode code
|
|
164
|
+
points. Never apply a truncated field as a complete replacement or guess the
|
|
165
|
+
missing content. Obtain the complete edit from an authoritative source, or ask
|
|
166
|
+
the user for it. Do not acknowledge the batch until all feedback is handled.
|
|
122
167
|
- **`edits` are changes the user already made.** `after` is their exact wording —
|
|
123
|
-
carry it across verbatim and never revert it. If the HTML was generated from
|
|
168
|
+
unless marked truncated, carry it across verbatim and never revert it. If the HTML was generated from
|
|
124
169
|
something else (MDX, Markdown, a template), apply `after` to the **source** too,
|
|
125
170
|
or their fix disappears on the next build.
|
|
126
171
|
- When `before_html`/`after_html` are present, the user changed formatting, not
|
|
@@ -148,8 +193,12 @@ One batch covers every page the user visited, grouped by file or localhost URL.
|
|
|
148
193
|
- Find each comment by its `quote`; that exact string is in the file.
|
|
149
194
|
- `kind: "element"` points at a whole block, so `quote` is its label, not body text.
|
|
150
195
|
- Fix every page in `pages`, not just the first.
|
|
151
|
-
- **Do not write a reply.** There is no chat. The user sees
|
|
152
|
-
|
|
196
|
+
- **Do not write a reply.** There is no chat. The user sees the updated page and
|
|
197
|
+
can choose Changes for a fixed Send-to-result comparison. Reload may wait
|
|
198
|
+
for the user to handle unsaved review work. Live result capture may require
|
|
199
|
+
a contextual retry; never describe unavailable history as no changes.
|
|
200
|
+
- Review history uses local content/source snapshots, not screenshots or Git
|
|
201
|
+
commits. Do not create commits or alter repository state to populate history.
|
|
153
202
|
|
|
154
203
|
## Better edit labels (optional)
|
|
155
204
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
const RETRY_DELAYS_MS = [10, 20, 40, 80, 160];
|
|
5
|
+
const TRANSIENT_RENAME_ERRORS = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
6
|
+
const waitState = new Int32Array(new SharedArrayBuffer(4));
|
|
7
|
+
|
|
8
|
+
// Keep Store transactions synchronous; rename backoff blocks for at most 310ms.
|
|
9
|
+
const sleepSync = (ms) => Atomics.wait(waitState, 0, 0, ms);
|
|
10
|
+
|
|
11
|
+
export function createAtomicWriter({ fileSystem = fs, sleep = sleepSync, report = console.error } = {}) {
|
|
12
|
+
return function atomicWrite(file, data) {
|
|
13
|
+
const tmp = `${file}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.doc-review.tmp`;
|
|
14
|
+
let fd;
|
|
15
|
+
let created = false;
|
|
16
|
+
try {
|
|
17
|
+
fd = fileSystem.openSync(tmp, "wx");
|
|
18
|
+
created = true;
|
|
19
|
+
fileSystem.writeFileSync(fd, data);
|
|
20
|
+
fileSystem.closeSync(fd);
|
|
21
|
+
fd = undefined;
|
|
22
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
23
|
+
try {
|
|
24
|
+
fileSystem.renameSync(tmp, file);
|
|
25
|
+
return;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (!TRANSIENT_RENAME_ERRORS.has(err.code) || attempt === RETRY_DELAYS_MS.length) throw err;
|
|
28
|
+
sleep(RETRY_DELAYS_MS[attempt]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (fd !== undefined) {
|
|
33
|
+
try {
|
|
34
|
+
fileSystem.closeSync(fd);
|
|
35
|
+
} catch (cleanupError) {
|
|
36
|
+
report(`Could not close atomic-write temporary file ${tmp}: ${cleanupError.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (created) {
|
|
40
|
+
try {
|
|
41
|
+
fileSystem.unlinkSync(tmp);
|
|
42
|
+
} catch (cleanupError) {
|
|
43
|
+
if (cleanupError.code !== "ENOENT") {
|
|
44
|
+
report(`Could not remove atomic-write temporary file ${tmp}: ${cleanupError.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const atomicWrite = createAtomicWriter();
|