@hyperframes/studio-server 0.8.76 → 0.8.78
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/dist/{chunk-5PGWTBCL.js → chunk-X3T65KC2.js} +5 -8
- package/dist/chunk-X3T65KC2.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/helpers/mediaProxyPreview.js +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +348 -198
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-DZD3iu_2.d.ts → mediaProxyPreview-BmJ8AmLd.d.ts} +15 -1
- package/package.json +3 -3
- package/dist/chunk-5PGWTBCL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
isAutoProxyEnabled,
|
|
23
23
|
proxyEtagSalt,
|
|
24
24
|
resolvePreviewMediaCodecProbeCache
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-X3T65KC2.js";
|
|
26
26
|
import {
|
|
27
27
|
ProxyCapacityError,
|
|
28
28
|
ProxyTranscodeError,
|
|
@@ -72,9 +72,18 @@ function isInHiddenOrVendorDir(relPath) {
|
|
|
72
72
|
const segments = relPath.split("/");
|
|
73
73
|
return segments.slice(0, -1).some((seg) => seg.startsWith(".") || seg === "node_modules");
|
|
74
74
|
}
|
|
75
|
+
var UNREADABLE_DIR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "ENOENT", "ENOTDIR"]);
|
|
75
76
|
function walkDir(dir, prefix = "") {
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const code = err.code;
|
|
82
|
+
if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
76
85
|
const files = [];
|
|
77
|
-
for (const entry of
|
|
86
|
+
for (const entry of entries) {
|
|
78
87
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
79
88
|
if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;
|
|
80
89
|
if (entry.isDirectory()) {
|
|
@@ -161,6 +170,15 @@ function openNoFollow(filePath, flags) {
|
|
|
161
170
|
return null;
|
|
162
171
|
}
|
|
163
172
|
}
|
|
173
|
+
function isUnchanged(filePath, expected) {
|
|
174
|
+
const fd = openNoFollow(filePath, constants.O_RDONLY);
|
|
175
|
+
if (fd === null) return false;
|
|
176
|
+
try {
|
|
177
|
+
return readFileSync(fd, "utf-8") === expected;
|
|
178
|
+
} finally {
|
|
179
|
+
closeSync(fd);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
164
182
|
function stampFileHfIds(filePath) {
|
|
165
183
|
let fd = openNoFollow(filePath, constants.O_RDWR);
|
|
166
184
|
let writable = true;
|
|
@@ -179,7 +197,7 @@ function stampFileHfIds(filePath) {
|
|
|
179
197
|
const mode = fstatSync(fd).mode;
|
|
180
198
|
closeSync(fd);
|
|
181
199
|
fd = null;
|
|
182
|
-
replaceFileAtomically(filePath, normalized, mode);
|
|
200
|
+
if (isUnchanged(filePath, html)) replaceFileAtomically(filePath, normalized, mode);
|
|
183
201
|
}
|
|
184
202
|
return normalized;
|
|
185
203
|
} catch (err) {
|
|
@@ -776,6 +794,8 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
|
|
|
776
794
|
}));
|
|
777
795
|
const results = [];
|
|
778
796
|
const attempted = [];
|
|
797
|
+
for (const mutation of prepared)
|
|
798
|
+
assertExpectedVersion(mutation.expectedVersion, readFileSync4(mutation.absPath, "utf-8"));
|
|
779
799
|
try {
|
|
780
800
|
for (const mutation of prepared) {
|
|
781
801
|
results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted));
|
|
@@ -785,7 +805,8 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
|
|
|
785
805
|
const rollbackErrors = [];
|
|
786
806
|
for (const mutation of attempted.reverse()) {
|
|
787
807
|
try {
|
|
788
|
-
|
|
808
|
+
if (!mutation.written || readFileSync4(mutation.absPath, "utf-8") === mutation.after)
|
|
809
|
+
writeFile(mutation.absPath, mutation.before, "utf-8");
|
|
789
810
|
clearFileWriteReceipt(mutation.absPath, mutation.version, mutation.writeToken);
|
|
790
811
|
} catch (rollbackError) {
|
|
791
812
|
rollbackErrors.push(rollbackError);
|
|
@@ -818,8 +839,10 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempt
|
|
|
818
839
|
const before = current;
|
|
819
840
|
const version = fileContentVersion(mutation.after);
|
|
820
841
|
const writeToken = createWriteToken(requestToken);
|
|
821
|
-
|
|
842
|
+
const attempt = { ...mutation, before, version, writeToken, written: false };
|
|
843
|
+
attempted.push(attempt);
|
|
822
844
|
writeFile(mutation.absPath, mutation.after, "utf-8");
|
|
845
|
+
attempt.written = true;
|
|
823
846
|
recordFileWriteReceipt(mutation.absPath, {
|
|
824
847
|
path: mutation.sourceFile,
|
|
825
848
|
version,
|
|
@@ -1133,6 +1156,7 @@ function requestSubPath(url, route) {
|
|
|
1133
1156
|
}
|
|
1134
1157
|
|
|
1135
1158
|
// src/routes/files.ts
|
|
1159
|
+
import { insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
|
|
1136
1160
|
async function loadGsapParser() {
|
|
1137
1161
|
return import("@hyperframes/parsers/gsap-parser-recast");
|
|
1138
1162
|
}
|
|
@@ -1679,8 +1703,8 @@ async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
|
1679
1703
|
`window.__timelines["${compId}"] = tl;`,
|
|
1680
1704
|
"</script>"
|
|
1681
1705
|
].join("\n");
|
|
1682
|
-
html = html
|
|
1683
|
-
|
|
1706
|
+
html = insertBeforeCloseTag(html, "body", `${bootstrap}
|
|
1707
|
+
`) ?? `${html}
|
|
1684
1708
|
${bootstrap}`;
|
|
1685
1709
|
block = extractGsapScriptBlock(html);
|
|
1686
1710
|
}
|
|
@@ -3325,6 +3349,7 @@ import { join as join9, resolve as resolve4 } from "path";
|
|
|
3325
3349
|
import { createHash as createHash4 } from "crypto";
|
|
3326
3350
|
import {
|
|
3327
3351
|
injectScriptsIntoHtml,
|
|
3352
|
+
insertBeforeCloseTag as insertBeforeCloseTag2,
|
|
3328
3353
|
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
3329
3354
|
} from "@hyperframes/core/compiler";
|
|
3330
3355
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
@@ -3550,9 +3575,8 @@ function injectProjectSignature(html, signature) {
|
|
|
3550
3575
|
tag
|
|
3551
3576
|
);
|
|
3552
3577
|
}
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
return `${tag}
|
|
3578
|
+
return insertBeforeCloseTag2(html, "head", `${tag}
|
|
3579
|
+
`) ?? `${tag}
|
|
3556
3580
|
${html}`;
|
|
3557
3581
|
}
|
|
3558
3582
|
function readStudioMotionManifestContent(projectDir) {
|
|
@@ -3577,9 +3601,8 @@ function parseStudioMotionManifestContent(content) {
|
|
|
3577
3601
|
}
|
|
3578
3602
|
}
|
|
3579
3603
|
function injectScriptTagIntoHead(html, scriptTag) {
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
return `${scriptTag}
|
|
3604
|
+
return insertBeforeCloseTag2(html, "head", `${scriptTag}
|
|
3605
|
+
`) ?? `${scriptTag}
|
|
3583
3606
|
${html}`;
|
|
3584
3607
|
}
|
|
3585
3608
|
function htmlHasGsap(html) {
|
|
@@ -3749,8 +3772,8 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3749
3772
|
}
|
|
3750
3773
|
if (!bundled.includes("hyperframe.runtime") && !bundled.includes("hyperframes-preview-runtime")) {
|
|
3751
3774
|
const runtimeTag = `<script src="${adapter.runtimeUrl}"></script>`;
|
|
3752
|
-
bundled = bundled
|
|
3753
|
-
|
|
3775
|
+
bundled = insertBeforeCloseTag2(bundled, "body", `${runtimeTag}
|
|
3776
|
+
`) ?? `${bundled}
|
|
3754
3777
|
${runtimeTag}`;
|
|
3755
3778
|
}
|
|
3756
3779
|
const baseHref = `/api/projects/${project.id}/preview/`;
|
|
@@ -5174,157 +5197,12 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5174
5197
|
api.get("/assets/global", (c) => c.json({ assets: readGlobalAssets().map(toPublicAsset) }));
|
|
5175
5198
|
}
|
|
5176
5199
|
|
|
5177
|
-
// src/routes/history.ts
|
|
5178
|
-
var YOU = { kind: "person", name: "You" };
|
|
5179
|
-
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5180
|
-
async function historyOf(adapter, c) {
|
|
5181
|
-
const project = await adapter.resolveProject(c.req.param("id") ?? "");
|
|
5182
|
-
return project ? await adapter.history?.(project) ?? null : null;
|
|
5183
|
-
}
|
|
5184
|
-
async function bodyOf(c) {
|
|
5185
|
-
const body = await c.req.json().catch(() => null);
|
|
5186
|
-
return body && typeof body === "object" ? body : {};
|
|
5187
|
-
}
|
|
5188
|
-
var text = (value) => typeof value === "string" && value ? value : null;
|
|
5189
|
-
function writing(c) {
|
|
5190
|
-
const header = c.req.header("X-Hyperframes-Write-Token");
|
|
5191
|
-
return header ? { writeToken: createWriteToken(header) } : {};
|
|
5192
|
-
}
|
|
5193
|
-
function idleOf(body) {
|
|
5194
|
-
const idleMs = body.idleMs;
|
|
5195
|
-
return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
|
|
5196
|
-
}
|
|
5197
|
-
function versionsOf(value) {
|
|
5198
|
-
if (!value || typeof value !== "object") return void 0;
|
|
5199
|
-
const pairs = Object.entries(value).filter(
|
|
5200
|
-
(pair) => typeof pair[1] === "string"
|
|
5201
|
-
);
|
|
5202
|
-
return pairs.length ? Object.fromEntries(pairs) : void 0;
|
|
5203
|
-
}
|
|
5204
|
-
function nextStep(history, direction) {
|
|
5205
|
-
const target = history.next(direction);
|
|
5206
|
-
if (!target) return null;
|
|
5207
|
-
const paths = target.files.map((file) => file.path);
|
|
5208
|
-
return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
|
|
5209
|
-
}
|
|
5210
|
-
async function withHistory(adapter, c, task) {
|
|
5211
|
-
const history = await historyOf(adapter, c);
|
|
5212
|
-
if (!history) return c.json({ error: "This project has no history here." }, 404);
|
|
5213
|
-
try {
|
|
5214
|
-
return c.json(await task(history, await bodyOf(c)) ?? null);
|
|
5215
|
-
} catch (error) {
|
|
5216
|
-
return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
5217
|
-
}
|
|
5218
|
-
}
|
|
5219
|
-
function registerHistoryRoutes(api, adapter) {
|
|
5220
|
-
const windows = /* @__PURE__ */ new Map();
|
|
5221
|
-
const base = "/projects/:id/history";
|
|
5222
|
-
api.get(
|
|
5223
|
-
base,
|
|
5224
|
-
(c) => withHistory(adapter, c, (history) => {
|
|
5225
|
-
const entries = history.list();
|
|
5226
|
-
return { entries, back: nextStep(history, "back"), forward: nextStep(history, "forward") };
|
|
5227
|
-
})
|
|
5228
|
-
);
|
|
5229
|
-
api.post(
|
|
5230
|
-
`${base}/step`,
|
|
5231
|
-
(c) => withHistory(
|
|
5232
|
-
adapter,
|
|
5233
|
-
c,
|
|
5234
|
-
(history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU, writing(c))
|
|
5235
|
-
)
|
|
5236
|
-
);
|
|
5237
|
-
api.post(
|
|
5238
|
-
`${base}/undo`,
|
|
5239
|
-
(c) => withHistory(adapter, c, (history, body) => {
|
|
5240
|
-
const mode = body.mode === "just-this" || body.mode === "back-to-before" ? body.mode : void 0;
|
|
5241
|
-
return history.undo(text(body.entryId) ?? "", { who: YOU, mode, ...writing(c) });
|
|
5242
|
-
})
|
|
5243
|
-
);
|
|
5244
|
-
api.post(
|
|
5245
|
-
`${base}/restore`,
|
|
5246
|
-
(c) => withHistory(
|
|
5247
|
-
adapter,
|
|
5248
|
-
c,
|
|
5249
|
-
(history, body) => history.restore(text(body.point) ?? "", YOU, writing(c))
|
|
5250
|
-
)
|
|
5251
|
-
);
|
|
5252
|
-
api.get(
|
|
5253
|
-
`${base}/peek/:point`,
|
|
5254
|
-
(c) => withHistory(adapter, c, (history) => ({ files: history.peek(c.req.param("point")) }))
|
|
5255
|
-
);
|
|
5256
|
-
api.post(
|
|
5257
|
-
`${base}/pin`,
|
|
5258
|
-
(c) => withHistory(adapter, c, (history, body) => {
|
|
5259
|
-
history.pin(text(body.entryId) ?? "", body.pinned === true);
|
|
5260
|
-
return { ok: true };
|
|
5261
|
-
})
|
|
5262
|
-
);
|
|
5263
|
-
api.post(
|
|
5264
|
-
`${base}/claim`,
|
|
5265
|
-
(c) => withHistory(adapter, c, async (history, body) => {
|
|
5266
|
-
const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
|
|
5267
|
-
const coalesceKey = text(body.coalesceKey) ?? void 0;
|
|
5268
|
-
const idleMs = idleOf(body);
|
|
5269
|
-
const overwrote = versionsOf(body.overwrote);
|
|
5270
|
-
const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
|
|
5271
|
-
...coalesceKey && { coalesceKey },
|
|
5272
|
-
...idleMs && { idleMs },
|
|
5273
|
-
...overwrote && { overwrote }
|
|
5274
|
-
});
|
|
5275
|
-
return { claimed };
|
|
5276
|
-
})
|
|
5277
|
-
);
|
|
5278
|
-
api.post(
|
|
5279
|
-
`${base}/window`,
|
|
5280
|
-
(c) => withHistory(adapter, c, async (history, body) => {
|
|
5281
|
-
const idleMs = idleOf(body);
|
|
5282
|
-
const window = await history.beginWindow(
|
|
5283
|
-
YOU,
|
|
5284
|
-
text(body.label) ?? "Edited in Studio",
|
|
5285
|
-
idleMs ? { idleMs } : void 0
|
|
5286
|
-
);
|
|
5287
|
-
windows.set(window.id, { history, window });
|
|
5288
|
-
return { windowId: window.id };
|
|
5289
|
-
})
|
|
5290
|
-
);
|
|
5291
|
-
api.post(
|
|
5292
|
-
`${base}/window/:windowId/close`,
|
|
5293
|
-
(c) => withHistory(adapter, c, async (history) => {
|
|
5294
|
-
const id = c.req.param("windowId") ?? "";
|
|
5295
|
-
const held = windows.get(id);
|
|
5296
|
-
if (held?.history !== history) throw new Error("That window is not open in this project.");
|
|
5297
|
-
windows.delete(id);
|
|
5298
|
-
return { entry: await held.window.close() };
|
|
5299
|
-
})
|
|
5300
|
-
);
|
|
5301
|
-
}
|
|
5302
|
-
|
|
5303
|
-
// src/createStudioApi.ts
|
|
5304
|
-
function createStudioApi(adapter) {
|
|
5305
|
-
const api = new Hono();
|
|
5306
|
-
registerProjectRoutes(api, adapter);
|
|
5307
|
-
registerFileRoutes(api, adapter);
|
|
5308
|
-
registerPreviewRoutes(api, adapter);
|
|
5309
|
-
registerLintRoutes(api, adapter);
|
|
5310
|
-
registerRenderRoutes(api, adapter);
|
|
5311
|
-
registerThumbnailRoutes(api, adapter);
|
|
5312
|
-
registerSelectionRoutes(api, adapter);
|
|
5313
|
-
registerMediaRoutes(api, adapter);
|
|
5314
|
-
registerWaveformRoutes(api, adapter);
|
|
5315
|
-
registerFontRoutes(api);
|
|
5316
|
-
registerRegistryRoutes(api, adapter);
|
|
5317
|
-
registerGlobalAssetRoutes(api);
|
|
5318
|
-
registerHistoryRoutes(api, adapter);
|
|
5319
|
-
return api;
|
|
5320
|
-
}
|
|
5321
|
-
|
|
5322
5200
|
// src/history/projectHistory.ts
|
|
5323
|
-
import { randomUUID as
|
|
5201
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5324
5202
|
import { rm as rm2 } from "fs/promises";
|
|
5325
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
5203
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync18 } from "fs";
|
|
5326
5204
|
import { homedir as homedir2 } from "os";
|
|
5327
|
-
import { join as
|
|
5205
|
+
import { join as join19, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
|
|
5328
5206
|
|
|
5329
5207
|
// src/history/blobStore.ts
|
|
5330
5208
|
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
@@ -5428,8 +5306,89 @@ function projectHistoryId(projectDir, historyRoot) {
|
|
|
5428
5306
|
return id;
|
|
5429
5307
|
}
|
|
5430
5308
|
|
|
5309
|
+
// src/history/ownerLock.ts
|
|
5310
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
5311
|
+
import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5312
|
+
import { join as join18 } from "path";
|
|
5313
|
+
var HistoryBusyError = class extends Error {
|
|
5314
|
+
constructor(pid) {
|
|
5315
|
+
super(`This project's history is open in another process (pid ${pid}).`);
|
|
5316
|
+
this.pid = pid;
|
|
5317
|
+
this.name = "HistoryBusyError";
|
|
5318
|
+
}
|
|
5319
|
+
pid;
|
|
5320
|
+
};
|
|
5321
|
+
function alive(pid) {
|
|
5322
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
5323
|
+
try {
|
|
5324
|
+
process.kill(pid, 0);
|
|
5325
|
+
return true;
|
|
5326
|
+
} catch (error) {
|
|
5327
|
+
return error.code === "EPERM";
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
function ownerOf(file) {
|
|
5331
|
+
try {
|
|
5332
|
+
const text2 = readFileSync16(file, "utf-8");
|
|
5333
|
+
return /^\d+$/.test(text2) ? Number(text2) : Number.NaN;
|
|
5334
|
+
} catch (error) {
|
|
5335
|
+
if (error.code === "ENOENT") return null;
|
|
5336
|
+
throw error;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
function claim(file) {
|
|
5340
|
+
const draft = `${file}-${randomUUID7()}.tmp`;
|
|
5341
|
+
writeFileSync8(draft, String(process.pid));
|
|
5342
|
+
try {
|
|
5343
|
+
linkSync(draft, file);
|
|
5344
|
+
return true;
|
|
5345
|
+
} catch (error) {
|
|
5346
|
+
if (error.code !== "EEXIST") throw error;
|
|
5347
|
+
return false;
|
|
5348
|
+
} finally {
|
|
5349
|
+
rmSync5(draft, { force: true });
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
function releaseOwn(file) {
|
|
5353
|
+
if (ownerOf(file) === process.pid) rmSync5(file, { force: true });
|
|
5354
|
+
}
|
|
5355
|
+
function evictDeadOwner(file) {
|
|
5356
|
+
const evictor = `${file}.evict`;
|
|
5357
|
+
if (!claim(evictor)) {
|
|
5358
|
+
const holder = ownerOf(evictor);
|
|
5359
|
+
if (holder !== null && !alive(holder)) rmSync5(evictor, { force: true });
|
|
5360
|
+
return false;
|
|
5361
|
+
}
|
|
5362
|
+
try {
|
|
5363
|
+
const pid = ownerOf(file);
|
|
5364
|
+
if (pid !== null && !alive(pid)) rmSync5(file, { force: true });
|
|
5365
|
+
return true;
|
|
5366
|
+
} finally {
|
|
5367
|
+
releaseOwn(evictor);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
async function takeHistoryOwnership(home, waitMs) {
|
|
5371
|
+
const file = join18(home, "owner.pid");
|
|
5372
|
+
const deadline = Date.now() + waitMs;
|
|
5373
|
+
mkdirSync9(home, { recursive: true });
|
|
5374
|
+
for (; ; ) {
|
|
5375
|
+
if (claim(file)) {
|
|
5376
|
+
let held = true;
|
|
5377
|
+
return () => {
|
|
5378
|
+
if (held) releaseOwn(file);
|
|
5379
|
+
held = false;
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5382
|
+
const pid = ownerOf(file);
|
|
5383
|
+
if (pid === null) continue;
|
|
5384
|
+
if (!alive(pid) && evictDeadOwner(file)) continue;
|
|
5385
|
+
if (Date.now() >= deadline) throw new HistoryBusyError(pid);
|
|
5386
|
+
await new Promise((settle) => setTimeout(settle, 50));
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5431
5390
|
// src/history/historyLog.ts
|
|
5432
|
-
import { closeSync as closeSync5, constants as constants4, mkdirSync as
|
|
5391
|
+
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
|
|
5433
5392
|
import { dirname as dirname7 } from "path";
|
|
5434
5393
|
var START = "start";
|
|
5435
5394
|
function parseRecord(line) {
|
|
@@ -5442,7 +5401,7 @@ function parseRecord(line) {
|
|
|
5442
5401
|
function readLog(file, onUnreadable) {
|
|
5443
5402
|
let text2;
|
|
5444
5403
|
try {
|
|
5445
|
-
text2 =
|
|
5404
|
+
text2 = readFileSync17(file, "utf-8");
|
|
5446
5405
|
} catch {
|
|
5447
5406
|
return null;
|
|
5448
5407
|
}
|
|
@@ -5482,7 +5441,7 @@ function writeLog(file, log) {
|
|
|
5482
5441
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5483
5442
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5484
5443
|
];
|
|
5485
|
-
|
|
5444
|
+
mkdirSync10(dirname7(file), { recursive: true });
|
|
5486
5445
|
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5487
5446
|
`).join(""), 420);
|
|
5488
5447
|
}
|
|
@@ -5540,7 +5499,8 @@ function referencedHashes(log, current) {
|
|
|
5540
5499
|
}
|
|
5541
5500
|
|
|
5542
5501
|
// src/history/projectHistory.ts
|
|
5543
|
-
var DEFAULT_HISTORY_ROOT =
|
|
5502
|
+
var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
|
|
5503
|
+
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5544
5504
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
5545
5505
|
var OUTSIDE_LABEL = "Changed outside the app";
|
|
5546
5506
|
var RACY_MS = 2e3;
|
|
@@ -5554,6 +5514,7 @@ function splitAt(change, at, kept) {
|
|
|
5554
5514
|
{ ...change, before: at }
|
|
5555
5515
|
];
|
|
5556
5516
|
}
|
|
5517
|
+
var takesWrite = (window, at) => window.idleMs === void 0 || at - (window.lastWriteAt ?? at) <= window.idleMs;
|
|
5557
5518
|
function addChange(group, path, before, after) {
|
|
5558
5519
|
const earlier = group.changes.get(path);
|
|
5559
5520
|
const from = earlier ? earlier.before : before;
|
|
@@ -5566,7 +5527,7 @@ var Engine = class {
|
|
|
5566
5527
|
this.projectId = projectId;
|
|
5567
5528
|
this.blobs = blobs;
|
|
5568
5529
|
this.dir = resolve7(options.projectDir);
|
|
5569
|
-
this.home =
|
|
5530
|
+
this.home = join19(options.historyRoot, projectId);
|
|
5570
5531
|
}
|
|
5571
5532
|
options;
|
|
5572
5533
|
projectId;
|
|
@@ -5586,7 +5547,7 @@ var Engine = class {
|
|
|
5586
5547
|
listeners = /* @__PURE__ */ new Set();
|
|
5587
5548
|
tail = Promise.resolve();
|
|
5588
5549
|
get logFile() {
|
|
5589
|
-
return
|
|
5550
|
+
return join19(this.home, "log.jsonl");
|
|
5590
5551
|
}
|
|
5591
5552
|
now() {
|
|
5592
5553
|
return (this.options.now ?? Date.now)();
|
|
@@ -5612,14 +5573,22 @@ var Engine = class {
|
|
|
5612
5573
|
const cached = cache.get(path);
|
|
5613
5574
|
this.tracked.set(path, { hash, stat: cached?.hash === hash ? cached.stat : "" });
|
|
5614
5575
|
}
|
|
5576
|
+
this.reopenClosedWindow();
|
|
5615
5577
|
await this.sweep();
|
|
5578
|
+
for (const window of [...this.windows]) await this.endWindow(window);
|
|
5616
5579
|
await this.commitOutside();
|
|
5617
5580
|
}
|
|
5581
|
+
reopenClosedWindow() {
|
|
5582
|
+
const closed = this.options.closedWindow;
|
|
5583
|
+
if (!closed || this.log.entries.some((entry) => entry.id === closed.id)) return;
|
|
5584
|
+
const { id, who, label, startedAt, lastWriteAt, idleMs } = closed;
|
|
5585
|
+
this.windows.push({ id, who, label, startedAt, lastWriteAt, idleMs, changes: /* @__PURE__ */ new Map() });
|
|
5586
|
+
}
|
|
5618
5587
|
async firstOpen() {
|
|
5619
5588
|
const sweptAt = Date.now();
|
|
5620
5589
|
for (const file of listProjectFiles(this.dir))
|
|
5621
5590
|
this.tracked.set(file.path, {
|
|
5622
|
-
hash: await this.blobs.put(
|
|
5591
|
+
hash: await this.blobs.put(join19(this.dir, file.path)),
|
|
5623
5592
|
stat: statKey(file, sweptAt)
|
|
5624
5593
|
});
|
|
5625
5594
|
this.log.baseline = this.manifest();
|
|
@@ -5631,7 +5600,7 @@ var Engine = class {
|
|
|
5631
5600
|
}
|
|
5632
5601
|
readStatCache() {
|
|
5633
5602
|
try {
|
|
5634
|
-
const files = JSON.parse(
|
|
5603
|
+
const files = JSON.parse(readFileSync18(join19(this.home, "stat.json"), "utf-8"));
|
|
5635
5604
|
return new Map(Object.entries(files));
|
|
5636
5605
|
} catch {
|
|
5637
5606
|
return /* @__PURE__ */ new Map();
|
|
@@ -5639,8 +5608,8 @@ var Engine = class {
|
|
|
5639
5608
|
}
|
|
5640
5609
|
saveStatCache() {
|
|
5641
5610
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5642
|
-
|
|
5643
|
-
replaceFileAtomically(
|
|
5611
|
+
mkdirSync11(this.home, { recursive: true });
|
|
5612
|
+
replaceFileAtomically(join19(this.home, "stat.json"), files, 420);
|
|
5644
5613
|
}
|
|
5645
5614
|
/**
|
|
5646
5615
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -5649,15 +5618,17 @@ var Engine = class {
|
|
|
5649
5618
|
async sweep() {
|
|
5650
5619
|
if (!existsSync12(this.dir)) return;
|
|
5651
5620
|
const sweptAt = Date.now();
|
|
5652
|
-
const
|
|
5621
|
+
const changedAt = (file) => Math.min(sweptAt, Math.max(file.mtimeMs, file.ctimeMs));
|
|
5622
|
+
const seen = listProjectFiles(this.dir).sort((a, b) => changedAt(a) - changedAt(b));
|
|
5653
5623
|
let changed = false;
|
|
5654
5624
|
for (const file of seen)
|
|
5655
|
-
changed = await this.observe(file.path, statKey(file, sweptAt)) || changed;
|
|
5625
|
+
changed = await this.observe(file.path, statKey(file, sweptAt), changedAt(file)) || changed;
|
|
5656
5626
|
const present = new Set(seen.map((file) => file.path));
|
|
5657
5627
|
for (const [path, known] of this.tracked) {
|
|
5658
5628
|
if (present.has(path)) continue;
|
|
5659
5629
|
this.tracked.delete(path);
|
|
5660
|
-
|
|
5630
|
+
const removedAt = sweptAt;
|
|
5631
|
+
await this.record(path, known.hash, null, removedAt);
|
|
5661
5632
|
changed = true;
|
|
5662
5633
|
}
|
|
5663
5634
|
if (changed) this.saveStatCache();
|
|
@@ -5665,24 +5636,27 @@ var Engine = class {
|
|
|
5665
5636
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
5666
5637
|
async storeIfPresent(path) {
|
|
5667
5638
|
try {
|
|
5668
|
-
return await this.blobs.put(
|
|
5639
|
+
return await this.blobs.put(join19(this.dir, path));
|
|
5669
5640
|
} catch (error) {
|
|
5670
5641
|
if (error.code === "ENOENT") return null;
|
|
5671
5642
|
throw error;
|
|
5672
5643
|
}
|
|
5673
5644
|
}
|
|
5674
|
-
async observe(path, stat2) {
|
|
5645
|
+
async observe(path, stat2, at) {
|
|
5675
5646
|
const known = this.tracked.get(path) ?? { hash: null, stat: null };
|
|
5676
5647
|
if (stat2 && known.stat === stat2) return false;
|
|
5677
5648
|
const hash = await this.storeIfPresent(path);
|
|
5678
5649
|
if (hash === null) return false;
|
|
5679
5650
|
this.tracked.set(path, { hash, stat: stat2 });
|
|
5680
|
-
if (known.hash !== hash) this.record(path, known.hash, hash);
|
|
5651
|
+
if (known.hash !== hash) await this.record(path, known.hash, hash, at);
|
|
5681
5652
|
return known.hash !== hash || known.stat !== stat2;
|
|
5682
5653
|
}
|
|
5683
|
-
record(path, before, after) {
|
|
5654
|
+
async record(path, before, after, at) {
|
|
5655
|
+
const endedBeforeThisWrite = this.windows.filter((open) => !takesWrite(open, at));
|
|
5656
|
+
if (endedBeforeThisWrite.length) await this.commitOutside();
|
|
5657
|
+
for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
|
|
5684
5658
|
const window = this.windows.at(-1);
|
|
5685
|
-
if (window) this.touch(window);
|
|
5659
|
+
if (window) this.touch(window, at);
|
|
5686
5660
|
addChange(window ?? this.outsideGroup(), path, before, after);
|
|
5687
5661
|
}
|
|
5688
5662
|
/** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
|
|
@@ -5790,7 +5764,7 @@ var Engine = class {
|
|
|
5790
5764
|
return this.outside;
|
|
5791
5765
|
}
|
|
5792
5766
|
newGroup(who, label) {
|
|
5793
|
-
return { id:
|
|
5767
|
+
return { id: randomUUID8(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
|
|
5794
5768
|
}
|
|
5795
5769
|
async commitOutside() {
|
|
5796
5770
|
clearTimeout(this.quietTimer);
|
|
@@ -5801,9 +5775,11 @@ var Engine = class {
|
|
|
5801
5775
|
}
|
|
5802
5776
|
async commit(group, extra = {}) {
|
|
5803
5777
|
if (!group.changes.size) return null;
|
|
5804
|
-
const { id, who, label, startedAt, changes } = group;
|
|
5778
|
+
const { id, who, label, startedAt, lastWriteAt, changes } = group;
|
|
5805
5779
|
const files = [...changes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
5806
|
-
const
|
|
5780
|
+
const previousEnd = this.log.entries.at(-1)?.endedAt ?? 0;
|
|
5781
|
+
const endedAt = Math.max(lastWriteAt ?? this.now(), previousEnd);
|
|
5782
|
+
const entry = { id, who, label, startedAt, endedAt, files, ...extra };
|
|
5807
5783
|
this.log.entries.push(entry);
|
|
5808
5784
|
try {
|
|
5809
5785
|
saveRecord(this.logFile, this.log, { type: "entry", entry });
|
|
@@ -5854,12 +5830,14 @@ var Engine = class {
|
|
|
5854
5830
|
await this.sweep();
|
|
5855
5831
|
const window = { ...this.newGroup(who, label), idleMs };
|
|
5856
5832
|
this.windows.push(window);
|
|
5857
|
-
this.touch(window);
|
|
5858
|
-
|
|
5833
|
+
this.touch(window, Date.now());
|
|
5834
|
+
const close = () => this.queue(() => this.sweepAndEnd(window));
|
|
5835
|
+
return { id: window.id, startedAt: window.startedAt, close };
|
|
5859
5836
|
});
|
|
5860
5837
|
}
|
|
5861
5838
|
/** A window with no write for its idleMs ends, so a close that never comes cannot hold every later write. */
|
|
5862
|
-
touch(window) {
|
|
5839
|
+
touch(window, at) {
|
|
5840
|
+
window.lastWriteAt = Math.max(window.lastWriteAt ?? at, at);
|
|
5863
5841
|
clearTimeout(window.idleTimer);
|
|
5864
5842
|
if (window.idleMs === void 0 || !Number.isFinite(window.idleMs)) return;
|
|
5865
5843
|
window.idleTimer = setTimeout(
|
|
@@ -5904,7 +5882,7 @@ var Engine = class {
|
|
|
5904
5882
|
}
|
|
5905
5883
|
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
5906
5884
|
async writeProjectFile(path, hash) {
|
|
5907
|
-
const absPath =
|
|
5885
|
+
const absPath = join19(this.dir, path);
|
|
5908
5886
|
const { writeToken } = this;
|
|
5909
5887
|
if (writeToken) {
|
|
5910
5888
|
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
@@ -6033,10 +6011,180 @@ var Engine = class {
|
|
|
6033
6011
|
};
|
|
6034
6012
|
async function openProjectHistory(options) {
|
|
6035
6013
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
6036
|
-
const
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6014
|
+
const release = await takeHistoryOwnership(
|
|
6015
|
+
join19(options.historyRoot, projectId),
|
|
6016
|
+
options.ownerWaitMs ?? 5e3
|
|
6017
|
+
);
|
|
6018
|
+
try {
|
|
6019
|
+
const blobs = await openBlobStore(join19(options.historyRoot, projectId, "blobs"));
|
|
6020
|
+
const engine = new Engine(options, projectId, blobs);
|
|
6021
|
+
await engine.queue(() => engine.start());
|
|
6022
|
+
const api = engine.api();
|
|
6023
|
+
return {
|
|
6024
|
+
...api,
|
|
6025
|
+
close: () => api.close().finally(release)
|
|
6026
|
+
};
|
|
6027
|
+
} catch (error) {
|
|
6028
|
+
release();
|
|
6029
|
+
throw error;
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
// src/routes/history.ts
|
|
6034
|
+
var YOU = { kind: "person", name: "You" };
|
|
6035
|
+
async function historyOf(adapter, c) {
|
|
6036
|
+
const project = await adapter.resolveProject(c.req.param("id") ?? "");
|
|
6037
|
+
return project ? await adapter.history?.(project) ?? null : null;
|
|
6038
|
+
}
|
|
6039
|
+
async function bodyOf(c) {
|
|
6040
|
+
const body = await c.req.json().catch(() => null);
|
|
6041
|
+
return body && typeof body === "object" ? body : {};
|
|
6042
|
+
}
|
|
6043
|
+
var text = (value) => typeof value === "string" && value ? value : null;
|
|
6044
|
+
function writing(c) {
|
|
6045
|
+
const header = c.req.header("X-Hyperframes-Write-Token");
|
|
6046
|
+
return header ? { writeToken: createWriteToken(header) } : {};
|
|
6047
|
+
}
|
|
6048
|
+
function whoOf(body) {
|
|
6049
|
+
const who = body.who;
|
|
6050
|
+
const name = text(who?.name);
|
|
6051
|
+
return who?.kind === "agent" && name ? { kind: "agent", name } : YOU;
|
|
6052
|
+
}
|
|
6053
|
+
function idleOf(body) {
|
|
6054
|
+
const idleMs = body.idleMs;
|
|
6055
|
+
return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
|
|
6056
|
+
}
|
|
6057
|
+
function versionsOf(value) {
|
|
6058
|
+
if (!value || typeof value !== "object") return void 0;
|
|
6059
|
+
const pairs = Object.entries(value).filter(
|
|
6060
|
+
(pair) => typeof pair[1] === "string"
|
|
6061
|
+
);
|
|
6062
|
+
return pairs.length ? Object.fromEntries(pairs) : void 0;
|
|
6063
|
+
}
|
|
6064
|
+
function nextStep(history, direction) {
|
|
6065
|
+
const target = history.next(direction);
|
|
6066
|
+
if (!target) return null;
|
|
6067
|
+
const paths = target.files.map((file) => file.path);
|
|
6068
|
+
return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
|
|
6069
|
+
}
|
|
6070
|
+
async function withHistory(adapter, c, task) {
|
|
6071
|
+
const history = await historyOf(adapter, c);
|
|
6072
|
+
if (!history) return c.json({ error: "This project has no history here." }, 404);
|
|
6073
|
+
try {
|
|
6074
|
+
return c.json(await task(history, await bodyOf(c)) ?? null);
|
|
6075
|
+
} catch (error) {
|
|
6076
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
function registerHistoryRoutes(api, adapter) {
|
|
6080
|
+
const windows = /* @__PURE__ */ new Map();
|
|
6081
|
+
const base = "/projects/:id/history";
|
|
6082
|
+
api.get(
|
|
6083
|
+
base,
|
|
6084
|
+
(c) => withHistory(adapter, c, (history) => {
|
|
6085
|
+
const entries = history.list();
|
|
6086
|
+
return { entries, back: nextStep(history, "back"), forward: nextStep(history, "forward") };
|
|
6087
|
+
})
|
|
6088
|
+
);
|
|
6089
|
+
api.post(
|
|
6090
|
+
`${base}/step`,
|
|
6091
|
+
(c) => withHistory(
|
|
6092
|
+
adapter,
|
|
6093
|
+
c,
|
|
6094
|
+
(history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU, writing(c))
|
|
6095
|
+
)
|
|
6096
|
+
);
|
|
6097
|
+
api.post(
|
|
6098
|
+
`${base}/undo`,
|
|
6099
|
+
(c) => withHistory(adapter, c, (history, body) => {
|
|
6100
|
+
const mode = body.mode === "just-this" || body.mode === "back-to-before" ? body.mode : void 0;
|
|
6101
|
+
return history.undo(text(body.entryId) ?? "", { who: whoOf(body), mode, ...writing(c) });
|
|
6102
|
+
})
|
|
6103
|
+
);
|
|
6104
|
+
api.post(
|
|
6105
|
+
`${base}/restore`,
|
|
6106
|
+
(c) => withHistory(
|
|
6107
|
+
adapter,
|
|
6108
|
+
c,
|
|
6109
|
+
(history, body) => history.restore(text(body.point) ?? "", whoOf(body), writing(c))
|
|
6110
|
+
)
|
|
6111
|
+
);
|
|
6112
|
+
api.get(
|
|
6113
|
+
`${base}/peek/:point`,
|
|
6114
|
+
(c) => withHistory(adapter, c, (history) => ({ files: history.peek(c.req.param("point")) }))
|
|
6115
|
+
);
|
|
6116
|
+
api.post(
|
|
6117
|
+
`${base}/pin`,
|
|
6118
|
+
(c) => withHistory(adapter, c, (history, body) => {
|
|
6119
|
+
history.pin(text(body.entryId) ?? "", body.pinned === true);
|
|
6120
|
+
return { ok: true };
|
|
6121
|
+
})
|
|
6122
|
+
);
|
|
6123
|
+
api.get(`${base}/blob/:hash`, async (c) => {
|
|
6124
|
+
const history = await historyOf(adapter, c);
|
|
6125
|
+
const hash = c.req.param("hash") ?? "";
|
|
6126
|
+
const missing = () => c.json({ error: "That file is not kept in this project's history." }, 404);
|
|
6127
|
+
if (!history || !/^[0-9a-f]{64}$/.test(hash)) return missing();
|
|
6128
|
+
const bytes = await history.readBlob(hash).catch(() => null);
|
|
6129
|
+
return bytes ? c.body(new Uint8Array(bytes)) : missing();
|
|
6130
|
+
});
|
|
6131
|
+
api.post(
|
|
6132
|
+
`${base}/claim`,
|
|
6133
|
+
(c) => withHistory(adapter, c, async (history, body) => {
|
|
6134
|
+
const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
|
|
6135
|
+
const coalesceKey = text(body.coalesceKey) ?? void 0;
|
|
6136
|
+
const idleMs = idleOf(body);
|
|
6137
|
+
const overwrote = versionsOf(body.overwrote);
|
|
6138
|
+
const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
|
|
6139
|
+
...coalesceKey && { coalesceKey },
|
|
6140
|
+
...idleMs && { idleMs },
|
|
6141
|
+
...overwrote && { overwrote }
|
|
6142
|
+
});
|
|
6143
|
+
return { claimed };
|
|
6144
|
+
})
|
|
6145
|
+
);
|
|
6146
|
+
api.post(
|
|
6147
|
+
`${base}/window`,
|
|
6148
|
+
(c) => withHistory(adapter, c, async (history, body) => {
|
|
6149
|
+
const idleMs = idleOf(body);
|
|
6150
|
+
const window = await history.beginWindow(
|
|
6151
|
+
whoOf(body),
|
|
6152
|
+
text(body.label) ?? "Edited in Studio",
|
|
6153
|
+
idleMs ? { idleMs } : void 0
|
|
6154
|
+
);
|
|
6155
|
+
windows.set(window.id, { history, window });
|
|
6156
|
+
return { windowId: window.id, startedAt: window.startedAt };
|
|
6157
|
+
})
|
|
6158
|
+
);
|
|
6159
|
+
api.post(
|
|
6160
|
+
`${base}/window/:windowId/close`,
|
|
6161
|
+
(c) => withHistory(adapter, c, async (history) => {
|
|
6162
|
+
const id = c.req.param("windowId") ?? "";
|
|
6163
|
+
const held = windows.get(id);
|
|
6164
|
+
if (held?.history !== history) throw new Error("That window is not open in this project.");
|
|
6165
|
+
windows.delete(id);
|
|
6166
|
+
return { entry: await held.window.close() };
|
|
6167
|
+
})
|
|
6168
|
+
);
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6171
|
+
// src/createStudioApi.ts
|
|
6172
|
+
function createStudioApi(adapter) {
|
|
6173
|
+
const api = new Hono();
|
|
6174
|
+
registerProjectRoutes(api, adapter);
|
|
6175
|
+
registerFileRoutes(api, adapter);
|
|
6176
|
+
registerPreviewRoutes(api, adapter);
|
|
6177
|
+
registerLintRoutes(api, adapter);
|
|
6178
|
+
registerRenderRoutes(api, adapter);
|
|
6179
|
+
registerThumbnailRoutes(api, adapter);
|
|
6180
|
+
registerSelectionRoutes(api, adapter);
|
|
6181
|
+
registerMediaRoutes(api, adapter);
|
|
6182
|
+
registerWaveformRoutes(api, adapter);
|
|
6183
|
+
registerFontRoutes(api);
|
|
6184
|
+
registerRegistryRoutes(api, adapter);
|
|
6185
|
+
registerGlobalAssetRoutes(api);
|
|
6186
|
+
registerHistoryRoutes(api, adapter);
|
|
6187
|
+
return api;
|
|
6040
6188
|
}
|
|
6041
6189
|
|
|
6042
6190
|
// src/helpers/duplicateElement.ts
|
|
@@ -6092,26 +6240,26 @@ function numericAttribute(element, name) {
|
|
|
6092
6240
|
|
|
6093
6241
|
// src/helpers/previewDocumentStore.ts
|
|
6094
6242
|
import { createHash as createHash7 } from "crypto";
|
|
6095
|
-
import { mkdirSync as
|
|
6096
|
-
import { join as
|
|
6243
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
|
|
6244
|
+
import { join as join20 } from "path";
|
|
6097
6245
|
function createPreviewDocumentStore(dir, salt) {
|
|
6098
|
-
const fileFor = (key) =>
|
|
6246
|
+
const fileFor = (key) => join20(dir, `${createHash7("sha256").update(`${salt}
|
|
6099
6247
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6100
6248
|
return {
|
|
6101
6249
|
read(key) {
|
|
6102
6250
|
try {
|
|
6103
|
-
return
|
|
6251
|
+
return readFileSync19(fileFor(key), "utf-8");
|
|
6104
6252
|
} catch {
|
|
6105
6253
|
return null;
|
|
6106
6254
|
}
|
|
6107
6255
|
},
|
|
6108
6256
|
write(key, html) {
|
|
6109
6257
|
try {
|
|
6110
|
-
|
|
6258
|
+
mkdirSync12(dir, { recursive: true });
|
|
6111
6259
|
const file = fileFor(key);
|
|
6112
6260
|
replaceFileAtomically(file, html, 420);
|
|
6113
6261
|
for (const name of readdirSync7(dir)) {
|
|
6114
|
-
if (
|
|
6262
|
+
if (join20(dir, name) !== file) rmSync6(join20(dir, name), { force: true });
|
|
6115
6263
|
}
|
|
6116
6264
|
} catch {
|
|
6117
6265
|
}
|
|
@@ -6190,6 +6338,8 @@ export {
|
|
|
6190
6338
|
DEFAULT_HISTORY_ROOT,
|
|
6191
6339
|
DELETED_VERSION,
|
|
6192
6340
|
START as HISTORY_START,
|
|
6341
|
+
HistoryBusyError,
|
|
6342
|
+
MAX_WINDOW_IDLE_MS,
|
|
6193
6343
|
MIME_TYPES,
|
|
6194
6344
|
PREVIEW_BUNDLE_OPTIONS,
|
|
6195
6345
|
STUDIO_MANUAL_EDITS_PATH,
|