Repository navigation
Expand file tree
/
Copy patheditor.ts
More file actions
82 lines (80 loc) · 2.91 KB
/
Copy patheditor.ts
File metadata and controls
82 lines (80 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import type { KRMObject, LiveResourceStore, Redaction } from "../../packages/krm-stream/src/index.ts";
export type SaveOutcome =
| "unchanged"
| "busy"
| "saved"
| "draft-conflict"
| "version-stale"
| "recovering"
| "unavailable";
/** Copy into the host. request owns session/CSRF/error policy; isLive reads connection state. */
export function conditionalEditor(
store: LiveResourceStore,
uid: string,
url: string,
request: typeof fetch,
isLive: () => boolean,
) {
let saving = false;
let needsRead = false;
async function refresh(): Promise<SaveOutcome> {
needsRead = true;
const reconcile = store.captureReconciliation(uid);
const latest = await request(url, { cache: "no-store" });
if (latest.status === 404) return "unavailable";
if (!latest.ok) throw new Error(`Reconciliation failed: HTTP ${latest.status}`);
const body = (await latest.json()) as {
object: KRMObject;
redactedPaths?: string[];
redacted?: Redaction[];
};
if (!store.ids().includes(uid) || body.object.metadata.uid !== uid) return "unavailable";
const accepted = reconcile(
body.object,
body.redactedPaths !== undefined
? { redactedPaths: body.redactedPaths }
: body.redacted !== undefined
? { redacted: body.redacted }
: undefined,
);
// false has several causes. Do not infer readiness from it or force a refused response.
// A later Save attempts only another guarded read until one is accepted while live.
if (!isLive()) return "recovering";
needsRead = !accepted;
if (store.conflicts(uid).length) return "draft-conflict";
return accepted ? "version-stale" : "recovering";
}
return {
get saving() {
return saving;
},
async save(): Promise<SaveOutcome> {
if (saving) return "busy";
if (!store.ids().includes(uid)) return "unavailable";
if (!isLive()) return "recovering";
if (store.conflicts(uid).length) return "draft-conflict";
saving = true;
try {
if (needsRead) return await refresh();
const intent = store.captureSave(uid); // Patch, UID and base captured before any await.
if (!intent) return "unchanged";
const result = await request(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(intent),
});
// Deleted after the preflight read: the watch will remove it too, so it is not a host error.
if (result.status === 404) return "unavailable";
if (result.status === 409) {
if (!store.ids().includes(uid)) return "unavailable";
return await refresh();
}
if (!result.ok) throw new Error(`Save failed: HTTP ${result.status}`);
// Never adopt a save response or clear drafts: edits made during the write must survive.
return "saved";
} finally {
saving = false;
}
},
};
}