@hyperframes/studio-server 0.8.77 → 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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { h as PreviewApiAdapter, P as PreviewDocumentStore, p as injectMediaCodecMap, q as injectMediaCodecMapIntoHtml, r as isAutoProxyEnabled, s as proxyEtagSalt, t as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-BmJ8AmLd.js';
|
|
2
2
|
import './mediaCodecMap.js';
|
|
3
3
|
import '@hyperframes/core';
|
|
4
4
|
import '@hyperframes/parsers';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-
|
|
3
|
-
export { D as DEFAULT_HISTORY_ROOT, a as HISTORY_START, H as HistoryEntry, b as HistoryFileChange, c as HistoryListItem, d as HistoryResult, e as HistoryWho, f as HistoryWindow, L as LintResult, g as
|
|
2
|
+
import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-BmJ8AmLd.js';
|
|
3
|
+
export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, a as HISTORY_START, H as HistoryEntry, b as HistoryFileChange, c as HistoryListItem, d as HistoryResult, e as HistoryWho, f as HistoryWindow, L as LintResult, g as MAX_WINDOW_IDLE_MS, h as PreviewApiAdapter, i as ProjectHistory, j as ProjectHistoryOptions, R as RenderJobState, k as ResolvedProject, l as StudioSelectionResponse, m as StudioSelectionSnapshot, n as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-BmJ8AmLd.js';
|
|
4
4
|
import { SourceMutationTarget } from './helpers/sourceMutation.js';
|
|
5
5
|
export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSourceDocument, patchElementInHtml, removeElementFromHtml, splitElementInHtml } from './helpers/sourceMutation.js';
|
|
6
6
|
export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
|
|
@@ -57,7 +57,8 @@ declare class HistoryBusyError extends Error {
|
|
|
57
57
|
constructor(pid: number);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** Recursively walk a directory and return relative file paths.
|
|
60
|
+
/** Recursively walk a directory and return relative file paths. A subfolder that is unreadable
|
|
61
|
+
* or removed mid-walk is skipped; any other failure, or an unreadable `dir` itself, throws. */
|
|
61
62
|
declare function walkDir(dir: string, prefix?: string): string[];
|
|
62
63
|
|
|
63
64
|
/** Pins hf-ids into every composition in the project. A host runs it once before it serves or
|
package/dist/index.js
CHANGED
|
@@ -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,
|
|
@@ -5174,151 +5197,6 @@ 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
5201
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
5324
5202
|
import { rm as rm2 } from "fs/promises";
|
|
@@ -5622,6 +5500,7 @@ function referencedHashes(log, current) {
|
|
|
5622
5500
|
|
|
5623
5501
|
// src/history/projectHistory.ts
|
|
5624
5502
|
var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
|
|
5503
|
+
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5625
5504
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
5626
5505
|
var OUTSIDE_LABEL = "Changed outside the app";
|
|
5627
5506
|
var RACY_MS = 2e3;
|
|
@@ -5635,6 +5514,7 @@ function splitAt(change, at, kept) {
|
|
|
5635
5514
|
{ ...change, before: at }
|
|
5636
5515
|
];
|
|
5637
5516
|
}
|
|
5517
|
+
var takesWrite = (window, at) => window.idleMs === void 0 || at - (window.lastWriteAt ?? at) <= window.idleMs;
|
|
5638
5518
|
function addChange(group, path, before, after) {
|
|
5639
5519
|
const earlier = group.changes.get(path);
|
|
5640
5520
|
const from = earlier ? earlier.before : before;
|
|
@@ -5693,9 +5573,17 @@ var Engine = class {
|
|
|
5693
5573
|
const cached = cache.get(path);
|
|
5694
5574
|
this.tracked.set(path, { hash, stat: cached?.hash === hash ? cached.stat : "" });
|
|
5695
5575
|
}
|
|
5576
|
+
this.reopenClosedWindow();
|
|
5696
5577
|
await this.sweep();
|
|
5578
|
+
for (const window of [...this.windows]) await this.endWindow(window);
|
|
5697
5579
|
await this.commitOutside();
|
|
5698
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
|
+
}
|
|
5699
5587
|
async firstOpen() {
|
|
5700
5588
|
const sweptAt = Date.now();
|
|
5701
5589
|
for (const file of listProjectFiles(this.dir))
|
|
@@ -5730,15 +5618,17 @@ var Engine = class {
|
|
|
5730
5618
|
async sweep() {
|
|
5731
5619
|
if (!existsSync12(this.dir)) return;
|
|
5732
5620
|
const sweptAt = Date.now();
|
|
5733
|
-
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));
|
|
5734
5623
|
let changed = false;
|
|
5735
5624
|
for (const file of seen)
|
|
5736
|
-
changed = await this.observe(file.path, statKey(file, sweptAt)) || changed;
|
|
5625
|
+
changed = await this.observe(file.path, statKey(file, sweptAt), changedAt(file)) || changed;
|
|
5737
5626
|
const present = new Set(seen.map((file) => file.path));
|
|
5738
5627
|
for (const [path, known] of this.tracked) {
|
|
5739
5628
|
if (present.has(path)) continue;
|
|
5740
5629
|
this.tracked.delete(path);
|
|
5741
|
-
|
|
5630
|
+
const removedAt = sweptAt;
|
|
5631
|
+
await this.record(path, known.hash, null, removedAt);
|
|
5742
5632
|
changed = true;
|
|
5743
5633
|
}
|
|
5744
5634
|
if (changed) this.saveStatCache();
|
|
@@ -5752,18 +5642,21 @@ var Engine = class {
|
|
|
5752
5642
|
throw error;
|
|
5753
5643
|
}
|
|
5754
5644
|
}
|
|
5755
|
-
async observe(path, stat2) {
|
|
5645
|
+
async observe(path, stat2, at) {
|
|
5756
5646
|
const known = this.tracked.get(path) ?? { hash: null, stat: null };
|
|
5757
5647
|
if (stat2 && known.stat === stat2) return false;
|
|
5758
5648
|
const hash = await this.storeIfPresent(path);
|
|
5759
5649
|
if (hash === null) return false;
|
|
5760
5650
|
this.tracked.set(path, { hash, stat: stat2 });
|
|
5761
|
-
if (known.hash !== hash) this.record(path, known.hash, hash);
|
|
5651
|
+
if (known.hash !== hash) await this.record(path, known.hash, hash, at);
|
|
5762
5652
|
return known.hash !== hash || known.stat !== stat2;
|
|
5763
5653
|
}
|
|
5764
|
-
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);
|
|
5765
5658
|
const window = this.windows.at(-1);
|
|
5766
|
-
if (window) this.touch(window);
|
|
5659
|
+
if (window) this.touch(window, at);
|
|
5767
5660
|
addChange(window ?? this.outsideGroup(), path, before, after);
|
|
5768
5661
|
}
|
|
5769
5662
|
/** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
|
|
@@ -5882,9 +5775,11 @@ var Engine = class {
|
|
|
5882
5775
|
}
|
|
5883
5776
|
async commit(group, extra = {}) {
|
|
5884
5777
|
if (!group.changes.size) return null;
|
|
5885
|
-
const { id, who, label, startedAt, changes } = group;
|
|
5778
|
+
const { id, who, label, startedAt, lastWriteAt, changes } = group;
|
|
5886
5779
|
const files = [...changes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
5887
|
-
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 };
|
|
5888
5783
|
this.log.entries.push(entry);
|
|
5889
5784
|
try {
|
|
5890
5785
|
saveRecord(this.logFile, this.log, { type: "entry", entry });
|
|
@@ -5935,12 +5830,14 @@ var Engine = class {
|
|
|
5935
5830
|
await this.sweep();
|
|
5936
5831
|
const window = { ...this.newGroup(who, label), idleMs };
|
|
5937
5832
|
this.windows.push(window);
|
|
5938
|
-
this.touch(window);
|
|
5939
|
-
|
|
5833
|
+
this.touch(window, Date.now());
|
|
5834
|
+
const close = () => this.queue(() => this.sweepAndEnd(window));
|
|
5835
|
+
return { id: window.id, startedAt: window.startedAt, close };
|
|
5940
5836
|
});
|
|
5941
5837
|
}
|
|
5942
5838
|
/** A window with no write for its idleMs ends, so a close that never comes cannot hold every later write. */
|
|
5943
|
-
touch(window) {
|
|
5839
|
+
touch(window, at) {
|
|
5840
|
+
window.lastWriteAt = Math.max(window.lastWriteAt ?? at, at);
|
|
5944
5841
|
clearTimeout(window.idleTimer);
|
|
5945
5842
|
if (window.idleMs === void 0 || !Number.isFinite(window.idleMs)) return;
|
|
5946
5843
|
window.idleTimer = setTimeout(
|
|
@@ -6133,6 +6030,163 @@ async function openProjectHistory(options) {
|
|
|
6133
6030
|
}
|
|
6134
6031
|
}
|
|
6135
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;
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6136
6190
|
// src/helpers/duplicateElement.ts
|
|
6137
6191
|
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
6138
6192
|
function duplicateElementInHtml(source, target, newId, at) {
|
|
@@ -6285,6 +6339,7 @@ export {
|
|
|
6285
6339
|
DELETED_VERSION,
|
|
6286
6340
|
START as HISTORY_START,
|
|
6287
6341
|
HistoryBusyError,
|
|
6342
|
+
MAX_WINDOW_IDLE_MS,
|
|
6288
6343
|
MIME_TYPES,
|
|
6289
6344
|
PREVIEW_BUNDLE_OPTIONS,
|
|
6290
6345
|
STUDIO_MANUAL_EDITS_PATH,
|