@hyperframes/studio-server 0.8.74 → 0.8.75
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 { P as PreviewApiAdapter, m as injectMediaCodecMap, n as injectMediaCodecMapIntoHtml, p as isAutoProxyEnabled, q as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-
|
|
1
|
+
export { P as PreviewApiAdapter, m as injectMediaCodecMap, n as injectMediaCodecMapIntoHtml, p as isAutoProxyEnabled, q as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-FxDAeURd.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, M as MediaProcessingJobState } from './mediaProxyPreview-
|
|
3
|
-
export { 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, P as PreviewApiAdapter, g as ProjectHistory, h as ProjectHistoryOptions, R as RenderJobState, i as ResolvedProject, j as StudioSelectionResponse, k as StudioSelectionSnapshot, l as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-
|
|
2
|
+
import { S as StudioApiAdapter, M as MediaProcessingJobState } from './mediaProxyPreview-FxDAeURd.js';
|
|
3
|
+
export { 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, P as PreviewApiAdapter, g as ProjectHistory, h as ProjectHistoryOptions, R as RenderJobState, i as ResolvedProject, j as StudioSelectionResponse, k as StudioSelectionSnapshot, l as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-FxDAeURd.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';
|
package/dist/index.js
CHANGED
|
@@ -5106,8 +5106,120 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5106
5106
|
api.get("/assets/global", (c) => c.json({ assets: readGlobalAssets().map(toPublicAsset) }));
|
|
5107
5107
|
}
|
|
5108
5108
|
|
|
5109
|
+
// src/history/historyLog.ts
|
|
5110
|
+
import { closeSync as closeSync5, constants as constants3, mkdirSync as mkdirSync7, openSync as openSync5, readFileSync as readFileSync15, writeSync as writeSync2 } from "fs";
|
|
5111
|
+
import { dirname as dirname5 } from "path";
|
|
5112
|
+
var START = "start";
|
|
5113
|
+
function parseRecord(line) {
|
|
5114
|
+
try {
|
|
5115
|
+
return JSON.parse(line);
|
|
5116
|
+
} catch {
|
|
5117
|
+
return null;
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
function readLog(file, onUnreadable) {
|
|
5121
|
+
let text2;
|
|
5122
|
+
try {
|
|
5123
|
+
text2 = readFileSync15(file, "utf-8");
|
|
5124
|
+
} catch {
|
|
5125
|
+
return null;
|
|
5126
|
+
}
|
|
5127
|
+
const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
|
|
5128
|
+
const lines = text2.split("\n");
|
|
5129
|
+
lines.forEach((line, index) => {
|
|
5130
|
+
const record = line ? parseRecord(line) : null;
|
|
5131
|
+
if (record) applyRecord(log, record);
|
|
5132
|
+
else if (line && index < lines.length - 1) onUnreadable(index + 1);
|
|
5133
|
+
});
|
|
5134
|
+
return log;
|
|
5135
|
+
}
|
|
5136
|
+
function applyRecord(log, record) {
|
|
5137
|
+
if (record.type === "baseline") log.baseline = new Map(Object.entries(record.files));
|
|
5138
|
+
else if (record.type === "entry") log.entries.push(record.entry);
|
|
5139
|
+
else if (record.pinned) log.pins.add(record.id);
|
|
5140
|
+
else log.pins.delete(record.id);
|
|
5141
|
+
}
|
|
5142
|
+
function saveRecord(file, log, record) {
|
|
5143
|
+
let fd;
|
|
5144
|
+
try {
|
|
5145
|
+
fd = openSync5(file, constants3.O_WRONLY | constants3.O_APPEND);
|
|
5146
|
+
} catch (error) {
|
|
5147
|
+
if (error.code !== "ENOENT") throw error;
|
|
5148
|
+
return writeLog(file, log);
|
|
5149
|
+
}
|
|
5150
|
+
try {
|
|
5151
|
+
writeSync2(fd, `${JSON.stringify(record)}
|
|
5152
|
+
`);
|
|
5153
|
+
} finally {
|
|
5154
|
+
closeSync5(fd);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
function writeLog(file, log) {
|
|
5158
|
+
const records = [
|
|
5159
|
+
{ type: "baseline", files: Object.fromEntries(log.baseline) },
|
|
5160
|
+
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5161
|
+
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5162
|
+
];
|
|
5163
|
+
mkdirSync7(dirname5(file), { recursive: true });
|
|
5164
|
+
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5165
|
+
`).join(""), 420);
|
|
5166
|
+
}
|
|
5167
|
+
function applyEntry(manifest, entry) {
|
|
5168
|
+
for (const file of entry.files)
|
|
5169
|
+
if (file.after === null) manifest.delete(file.path);
|
|
5170
|
+
else manifest.set(file.path, file.after);
|
|
5171
|
+
}
|
|
5172
|
+
function manifestAt(log, point) {
|
|
5173
|
+
const manifest = new Map(log.baseline);
|
|
5174
|
+
if (point === START) return manifest;
|
|
5175
|
+
for (const entry of log.entries) {
|
|
5176
|
+
applyEntry(manifest, entry);
|
|
5177
|
+
if (entry.id === point) return manifest;
|
|
5178
|
+
}
|
|
5179
|
+
return null;
|
|
5180
|
+
}
|
|
5181
|
+
function undoneIds(entries) {
|
|
5182
|
+
const undone = /* @__PURE__ */ new Set();
|
|
5183
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5184
|
+
const entry = entries[i];
|
|
5185
|
+
if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
|
|
5186
|
+
}
|
|
5187
|
+
return undone;
|
|
5188
|
+
}
|
|
5189
|
+
function stepTarget(entries, direction) {
|
|
5190
|
+
const undone = undoneIds(entries);
|
|
5191
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
5192
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5193
|
+
const entry = entries[i];
|
|
5194
|
+
const isChange = !entry.undoes;
|
|
5195
|
+
if (direction === "back" && isChange && !undone.has(entry.id)) return entry;
|
|
5196
|
+
if (direction !== "forward") continue;
|
|
5197
|
+
if (isChange) return void 0;
|
|
5198
|
+
const target = byId.get(entry.undoes);
|
|
5199
|
+
if (!undone.has(entry.id) && target && !target.undoes) return entry;
|
|
5200
|
+
}
|
|
5201
|
+
return void 0;
|
|
5202
|
+
}
|
|
5203
|
+
function foldOldest(log) {
|
|
5204
|
+
const oldest = log.entries[0];
|
|
5205
|
+
if (!oldest || log.pins.has(oldest.id)) return false;
|
|
5206
|
+
applyEntry(log.baseline, oldest);
|
|
5207
|
+
log.entries.shift();
|
|
5208
|
+
return true;
|
|
5209
|
+
}
|
|
5210
|
+
function referencedHashes(log, current) {
|
|
5211
|
+
const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
|
|
5212
|
+
for (const entry of log.entries)
|
|
5213
|
+
for (const file of entry.files) {
|
|
5214
|
+
if (file.before) keep.add(file.before);
|
|
5215
|
+
if (file.after) keep.add(file.after);
|
|
5216
|
+
}
|
|
5217
|
+
return keep;
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5109
5220
|
// src/routes/history.ts
|
|
5110
5221
|
var YOU = { kind: "person", name: "You" };
|
|
5222
|
+
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5111
5223
|
async function historyOf(adapter, c) {
|
|
5112
5224
|
const project = await adapter.resolveProject(c.req.param("id") ?? "");
|
|
5113
5225
|
return project ? await adapter.history?.(project) ?? null : null;
|
|
@@ -5117,6 +5229,16 @@ async function bodyOf(c) {
|
|
|
5117
5229
|
return body && typeof body === "object" ? body : {};
|
|
5118
5230
|
}
|
|
5119
5231
|
var text = (value) => typeof value === "string" && value ? value : null;
|
|
5232
|
+
function idleOf(body) {
|
|
5233
|
+
const idleMs = body.idleMs;
|
|
5234
|
+
return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
|
|
5235
|
+
}
|
|
5236
|
+
function nextStep(entries, direction) {
|
|
5237
|
+
const target = stepTarget(entries, direction);
|
|
5238
|
+
if (!target) return null;
|
|
5239
|
+
const paths = target.files.map((file) => file.path);
|
|
5240
|
+
return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
|
|
5241
|
+
}
|
|
5120
5242
|
async function withHistory(adapter, c, task) {
|
|
5121
5243
|
const history = await historyOf(adapter, c);
|
|
5122
5244
|
if (!history) return c.json({ error: "This project has no history here." }, 404);
|
|
@@ -5129,7 +5251,13 @@ async function withHistory(adapter, c, task) {
|
|
|
5129
5251
|
function registerHistoryRoutes(api, adapter) {
|
|
5130
5252
|
const windows = /* @__PURE__ */ new Map();
|
|
5131
5253
|
const base = "/projects/:id/history";
|
|
5132
|
-
api.get(
|
|
5254
|
+
api.get(
|
|
5255
|
+
base,
|
|
5256
|
+
(c) => withHistory(adapter, c, (history) => {
|
|
5257
|
+
const entries = history.list();
|
|
5258
|
+
return { entries, back: nextStep(entries, "back"), forward: nextStep(entries, "forward") };
|
|
5259
|
+
})
|
|
5260
|
+
);
|
|
5133
5261
|
api.post(
|
|
5134
5262
|
`${base}/step`,
|
|
5135
5263
|
(c) => withHistory(
|
|
@@ -5160,10 +5288,28 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
5160
5288
|
return { ok: true };
|
|
5161
5289
|
})
|
|
5162
5290
|
);
|
|
5291
|
+
api.post(
|
|
5292
|
+
`${base}/claim`,
|
|
5293
|
+
(c) => withHistory(adapter, c, async (history, body) => {
|
|
5294
|
+
const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
|
|
5295
|
+
const coalesceKey = text(body.coalesceKey) ?? void 0;
|
|
5296
|
+
const idleMs = idleOf(body);
|
|
5297
|
+
const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
|
|
5298
|
+
...coalesceKey && { coalesceKey },
|
|
5299
|
+
...idleMs && { idleMs }
|
|
5300
|
+
});
|
|
5301
|
+
return { claimed };
|
|
5302
|
+
})
|
|
5303
|
+
);
|
|
5163
5304
|
api.post(
|
|
5164
5305
|
`${base}/window`,
|
|
5165
5306
|
(c) => withHistory(adapter, c, async (history, body) => {
|
|
5166
|
-
const
|
|
5307
|
+
const idleMs = idleOf(body);
|
|
5308
|
+
const window = await history.beginWindow(
|
|
5309
|
+
YOU,
|
|
5310
|
+
text(body.label) ?? "Edited in Studio",
|
|
5311
|
+
idleMs ? { idleMs } : void 0
|
|
5312
|
+
);
|
|
5167
5313
|
windows.set(window.id, { history, window });
|
|
5168
5314
|
return { windowId: window.id };
|
|
5169
5315
|
})
|
|
@@ -5203,23 +5349,23 @@ function createStudioApi(adapter) {
|
|
|
5203
5349
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
5204
5350
|
import { rm as rm2 } from "fs/promises";
|
|
5205
5351
|
import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync17 } from "fs";
|
|
5206
|
-
import { join as join17, resolve as resolve7 } from "path";
|
|
5352
|
+
import { join as join17, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
|
|
5207
5353
|
|
|
5208
5354
|
// src/history/blobStore.ts
|
|
5209
5355
|
import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
|
|
5210
|
-
import { constants as
|
|
5356
|
+
import { constants as constants4, createReadStream as createReadStream2 } from "fs";
|
|
5211
5357
|
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat } from "fs/promises";
|
|
5212
|
-
import { dirname as
|
|
5358
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
5213
5359
|
async function hashFile(path) {
|
|
5214
5360
|
const hash = createHash5("sha256");
|
|
5215
5361
|
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
5216
5362
|
return hash.digest("hex");
|
|
5217
5363
|
}
|
|
5218
5364
|
async function cloneOrCopy(from, to) {
|
|
5219
|
-
await mkdir(
|
|
5365
|
+
await mkdir(dirname6(to), { recursive: true });
|
|
5220
5366
|
const temp = `${to}.${randomUUID5()}.tmp`;
|
|
5221
5367
|
try {
|
|
5222
|
-
await copyFile(from, temp,
|
|
5368
|
+
await copyFile(from, temp, constants4.COPYFILE_FICLONE);
|
|
5223
5369
|
await rename(temp, to);
|
|
5224
5370
|
} catch (error) {
|
|
5225
5371
|
await rm(temp, { force: true });
|
|
@@ -5238,13 +5384,13 @@ async function openBlobStore(dir) {
|
|
|
5238
5384
|
async put(absPath) {
|
|
5239
5385
|
const temp = join15(dir, `incoming-${randomUUID5()}`);
|
|
5240
5386
|
await mkdir(dir, { recursive: true });
|
|
5241
|
-
await copyFile(absPath, temp,
|
|
5387
|
+
await copyFile(absPath, temp, constants4.COPYFILE_FICLONE);
|
|
5242
5388
|
const hash = await hashFile(temp);
|
|
5243
5389
|
if (sizes.has(hash)) {
|
|
5244
5390
|
await rm(temp, { force: true });
|
|
5245
5391
|
return hash;
|
|
5246
5392
|
}
|
|
5247
|
-
await mkdir(
|
|
5393
|
+
await mkdir(dirname6(pathOf(hash)), { recursive: true });
|
|
5248
5394
|
const size = (await stat(temp)).size;
|
|
5249
5395
|
await rename(temp, pathOf(hash));
|
|
5250
5396
|
sizes.set(hash, size);
|
|
@@ -5268,13 +5414,13 @@ async function openBlobStore(dir) {
|
|
|
5268
5414
|
|
|
5269
5415
|
// src/history/historyId.ts
|
|
5270
5416
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5271
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
5417
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
|
|
5272
5418
|
import { join as join16, resolve as resolve6 } from "path";
|
|
5273
5419
|
var ID_PATH = join16(".hyperframes", "history-id");
|
|
5274
5420
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5275
5421
|
function readId(projectDir) {
|
|
5276
5422
|
try {
|
|
5277
|
-
const id =
|
|
5423
|
+
const id = readFileSync16(join16(projectDir, ID_PATH), "utf-8").trim();
|
|
5278
5424
|
return ID_SHAPE.test(id) ? id : null;
|
|
5279
5425
|
} catch {
|
|
5280
5426
|
return null;
|
|
@@ -5282,7 +5428,7 @@ function readId(projectDir) {
|
|
|
5282
5428
|
}
|
|
5283
5429
|
function recordedDir(historyDir) {
|
|
5284
5430
|
try {
|
|
5285
|
-
return JSON.parse(
|
|
5431
|
+
return JSON.parse(readFileSync16(join16(historyDir, "project.json"), "utf-8")).dir ?? null;
|
|
5286
5432
|
} catch {
|
|
5287
5433
|
return null;
|
|
5288
5434
|
}
|
|
@@ -5293,131 +5439,26 @@ function projectHistoryId(projectDir, historyRoot) {
|
|
|
5293
5439
|
const was = id && recordedDir(join16(historyRoot, id));
|
|
5294
5440
|
if (!id || was && was !== dir && existsSync11(was) && readId(was) === id) {
|
|
5295
5441
|
id = randomUUID6();
|
|
5296
|
-
|
|
5442
|
+
mkdirSync8(join16(dir, ".hyperframes"), { recursive: true });
|
|
5297
5443
|
writeFileSync6(join16(dir, ID_PATH), `${id}
|
|
5298
5444
|
`);
|
|
5299
5445
|
}
|
|
5300
|
-
|
|
5446
|
+
mkdirSync8(join16(historyRoot, id), { recursive: true });
|
|
5301
5447
|
writeFileSync6(join16(historyRoot, id, "project.json"), JSON.stringify({ dir }));
|
|
5302
5448
|
return id;
|
|
5303
5449
|
}
|
|
5304
5450
|
|
|
5305
|
-
// src/history/historyLog.ts
|
|
5306
|
-
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync8, openSync as openSync5, readFileSync as readFileSync16, writeSync as writeSync2 } from "fs";
|
|
5307
|
-
import { dirname as dirname6 } from "path";
|
|
5308
|
-
var START = "start";
|
|
5309
|
-
function parseRecord(line) {
|
|
5310
|
-
try {
|
|
5311
|
-
return JSON.parse(line);
|
|
5312
|
-
} catch {
|
|
5313
|
-
return null;
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
function readLog(file, onUnreadable) {
|
|
5317
|
-
let text2;
|
|
5318
|
-
try {
|
|
5319
|
-
text2 = readFileSync16(file, "utf-8");
|
|
5320
|
-
} catch {
|
|
5321
|
-
return null;
|
|
5322
|
-
}
|
|
5323
|
-
const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
|
|
5324
|
-
const lines = text2.split("\n");
|
|
5325
|
-
lines.forEach((line, index) => {
|
|
5326
|
-
const record = line ? parseRecord(line) : null;
|
|
5327
|
-
if (record) applyRecord(log, record);
|
|
5328
|
-
else if (line && index < lines.length - 1) onUnreadable(index + 1);
|
|
5329
|
-
});
|
|
5330
|
-
return log;
|
|
5331
|
-
}
|
|
5332
|
-
function applyRecord(log, record) {
|
|
5333
|
-
if (record.type === "baseline") log.baseline = new Map(Object.entries(record.files));
|
|
5334
|
-
else if (record.type === "entry") log.entries.push(record.entry);
|
|
5335
|
-
else if (record.pinned) log.pins.add(record.id);
|
|
5336
|
-
else log.pins.delete(record.id);
|
|
5337
|
-
}
|
|
5338
|
-
function saveRecord(file, log, record) {
|
|
5339
|
-
let fd;
|
|
5340
|
-
try {
|
|
5341
|
-
fd = openSync5(file, constants4.O_WRONLY | constants4.O_APPEND);
|
|
5342
|
-
} catch (error) {
|
|
5343
|
-
if (error.code !== "ENOENT") throw error;
|
|
5344
|
-
return writeLog(file, log);
|
|
5345
|
-
}
|
|
5346
|
-
try {
|
|
5347
|
-
writeSync2(fd, `${JSON.stringify(record)}
|
|
5348
|
-
`);
|
|
5349
|
-
} finally {
|
|
5350
|
-
closeSync5(fd);
|
|
5351
|
-
}
|
|
5352
|
-
}
|
|
5353
|
-
function writeLog(file, log) {
|
|
5354
|
-
const records = [
|
|
5355
|
-
{ type: "baseline", files: Object.fromEntries(log.baseline) },
|
|
5356
|
-
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5357
|
-
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5358
|
-
];
|
|
5359
|
-
mkdirSync8(dirname6(file), { recursive: true });
|
|
5360
|
-
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5361
|
-
`).join(""), 420);
|
|
5362
|
-
}
|
|
5363
|
-
function applyEntry(manifest, entry) {
|
|
5364
|
-
for (const file of entry.files)
|
|
5365
|
-
if (file.after === null) manifest.delete(file.path);
|
|
5366
|
-
else manifest.set(file.path, file.after);
|
|
5367
|
-
}
|
|
5368
|
-
function manifestAt(log, point) {
|
|
5369
|
-
const manifest = new Map(log.baseline);
|
|
5370
|
-
if (point === START) return manifest;
|
|
5371
|
-
for (const entry of log.entries) {
|
|
5372
|
-
applyEntry(manifest, entry);
|
|
5373
|
-
if (entry.id === point) return manifest;
|
|
5374
|
-
}
|
|
5375
|
-
return null;
|
|
5376
|
-
}
|
|
5377
|
-
function undoneIds(entries) {
|
|
5378
|
-
const undone = /* @__PURE__ */ new Set();
|
|
5379
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5380
|
-
const entry = entries[i];
|
|
5381
|
-
if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
|
|
5382
|
-
}
|
|
5383
|
-
return undone;
|
|
5384
|
-
}
|
|
5385
|
-
function stepTarget(entries, direction) {
|
|
5386
|
-
const undone = undoneIds(entries);
|
|
5387
|
-
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
5388
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5389
|
-
const entry = entries[i];
|
|
5390
|
-
const isChange = !entry.undoes;
|
|
5391
|
-
if (direction === "back" && isChange && !undone.has(entry.id)) return entry;
|
|
5392
|
-
if (direction !== "forward") continue;
|
|
5393
|
-
if (isChange) return void 0;
|
|
5394
|
-
const target = byId.get(entry.undoes);
|
|
5395
|
-
if (!undone.has(entry.id) && target && !target.undoes) return entry;
|
|
5396
|
-
}
|
|
5397
|
-
return void 0;
|
|
5398
|
-
}
|
|
5399
|
-
function foldOldest(log) {
|
|
5400
|
-
const oldest = log.entries[0];
|
|
5401
|
-
if (!oldest || log.pins.has(oldest.id)) return false;
|
|
5402
|
-
applyEntry(log.baseline, oldest);
|
|
5403
|
-
log.entries.shift();
|
|
5404
|
-
return true;
|
|
5405
|
-
}
|
|
5406
|
-
function referencedHashes(log, current) {
|
|
5407
|
-
const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
|
|
5408
|
-
for (const entry of log.entries)
|
|
5409
|
-
for (const file of entry.files) {
|
|
5410
|
-
if (file.before) keep.add(file.before);
|
|
5411
|
-
if (file.after) keep.add(file.after);
|
|
5412
|
-
}
|
|
5413
|
-
return keep;
|
|
5414
|
-
}
|
|
5415
|
-
|
|
5416
5451
|
// src/history/projectHistory.ts
|
|
5417
5452
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
5418
5453
|
var OUTSIDE_LABEL = "Changed outside the app";
|
|
5419
5454
|
var RACY_MS = 2e3;
|
|
5420
5455
|
var statKey = (file, sweptAt) => sweptAt - Math.max(file.mtimeMs, file.ctimeMs) < RACY_MS ? "" : `${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
|
|
5456
|
+
function addChange(group, path, before, after) {
|
|
5457
|
+
const earlier = group.changes.get(path);
|
|
5458
|
+
const from = earlier ? earlier.before : before;
|
|
5459
|
+
if (from === after) group.changes.delete(path);
|
|
5460
|
+
else group.changes.set(path, { path, before: from, after });
|
|
5461
|
+
}
|
|
5421
5462
|
var Engine = class {
|
|
5422
5463
|
constructor(options, projectId, blobs) {
|
|
5423
5464
|
this.options = options;
|
|
@@ -5435,6 +5476,8 @@ var Engine = class {
|
|
|
5435
5476
|
tracked = /* @__PURE__ */ new Map();
|
|
5436
5477
|
windows = [];
|
|
5437
5478
|
outside = null;
|
|
5479
|
+
/** A coalescing claim, open until another key, its idle timer, or an operation commits it. */
|
|
5480
|
+
claimed = null;
|
|
5438
5481
|
quietTimer;
|
|
5439
5482
|
maxTimer;
|
|
5440
5483
|
notedTimer = null;
|
|
@@ -5538,11 +5581,53 @@ var Engine = class {
|
|
|
5538
5581
|
record(path, before, after) {
|
|
5539
5582
|
const window = this.windows.at(-1);
|
|
5540
5583
|
if (window) this.touch(window);
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5584
|
+
addChange(window ?? this.outsideGroup(), path, before, after);
|
|
5585
|
+
}
|
|
5586
|
+
/**
|
|
5587
|
+
* ponytail: a claim takes every uncommitted outside change to its paths, so another writer's write to the same file
|
|
5588
|
+
* between Studio's write and its claim (milliseconds) folds into Studio's entry. Per-write tokens would split them.
|
|
5589
|
+
* A write filed to another writer's open window stays that window's.
|
|
5590
|
+
*/
|
|
5591
|
+
async claimNow(who, label, paths, { coalesceKey, idleMs }) {
|
|
5592
|
+
await this.sweep();
|
|
5593
|
+
const taken = this.takeOutside(paths);
|
|
5594
|
+
if (!taken.length) return null;
|
|
5595
|
+
const group = await this.claimGroup(who, label, coalesceKey);
|
|
5596
|
+
for (const change of taken) addChange(group, change.path, change.before, change.after);
|
|
5597
|
+
if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
|
|
5598
|
+
const entry = await this.commit(group);
|
|
5599
|
+
return entry && { id: entry.id };
|
|
5600
|
+
}
|
|
5601
|
+
/** The held claim when the key matches; otherwise it is committed and a new group starts. */
|
|
5602
|
+
async claimGroup(who, label, key) {
|
|
5603
|
+
if (key && this.claimed?.key === key) return this.claimed.group;
|
|
5604
|
+
await this.commitClaim();
|
|
5605
|
+
return this.newGroup(who, label);
|
|
5606
|
+
}
|
|
5607
|
+
/** Removes and returns the uncommitted outside changes to `paths` (project-relative or absolute). */
|
|
5608
|
+
takeOutside(paths) {
|
|
5609
|
+
const outside = this.outside;
|
|
5610
|
+
if (!outside) return [];
|
|
5611
|
+
const wanted = new Set(
|
|
5612
|
+
paths.map((path) => relative4(this.dir, resolve7(this.dir, path)).split(sep3).join("/"))
|
|
5613
|
+
);
|
|
5614
|
+
const taken = [...outside.changes.values()].filter((change) => wanted.has(change.path));
|
|
5615
|
+
for (const change of taken) outside.changes.delete(change.path);
|
|
5616
|
+
return taken;
|
|
5617
|
+
}
|
|
5618
|
+
holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
|
|
5619
|
+
clearTimeout(this.claimed?.timer);
|
|
5620
|
+
const timer = setTimeout(() => this.background(() => this.commitClaim()), idleMs);
|
|
5621
|
+
timer.unref?.();
|
|
5622
|
+
this.claimed = { group, key, timer };
|
|
5623
|
+
return group.changes.size ? { id: group.id } : null;
|
|
5624
|
+
}
|
|
5625
|
+
async commitClaim() {
|
|
5626
|
+
const held = this.claimed;
|
|
5627
|
+
this.claimed = null;
|
|
5628
|
+
if (!held) return;
|
|
5629
|
+
clearTimeout(held.timer);
|
|
5630
|
+
await this.commit(held.group);
|
|
5546
5631
|
}
|
|
5547
5632
|
outsideGroup() {
|
|
5548
5633
|
const { quietMs = 2e3, maxGroupMs = 3e4 } = this.options;
|
|
@@ -5604,6 +5689,7 @@ var Engine = class {
|
|
|
5604
5689
|
/** Before an operation: every write so far is filed, and the outside group is closed so it sorts first. */
|
|
5605
5690
|
async settle() {
|
|
5606
5691
|
await this.sweep();
|
|
5692
|
+
await this.commitClaim();
|
|
5607
5693
|
await this.commitOutside();
|
|
5608
5694
|
}
|
|
5609
5695
|
/** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
|
|
@@ -5652,6 +5738,7 @@ var Engine = class {
|
|
|
5652
5738
|
/** Every pending write, open window and outside group, committed: for flush and close. */
|
|
5653
5739
|
async settleAll() {
|
|
5654
5740
|
await this.sweep();
|
|
5741
|
+
await this.commitClaim();
|
|
5655
5742
|
for (const window of [...this.windows]) await this.endWindow(window);
|
|
5656
5743
|
await this.commitOutside();
|
|
5657
5744
|
}
|
|
@@ -5723,6 +5810,7 @@ var Engine = class {
|
|
|
5723
5810
|
return {
|
|
5724
5811
|
projectId: this.projectId,
|
|
5725
5812
|
beginWindow: (who, label, options = {}) => this.beginWindow(who, label, options.idleMs ?? this.options.maxGroupMs ?? 3e4),
|
|
5813
|
+
claim: (who, label, paths, options = {}) => this.queue(() => this.claimNow(who, label, paths, options)),
|
|
5726
5814
|
noteChange: (path) => this.noteChange(path),
|
|
5727
5815
|
list: () => {
|
|
5728
5816
|
const undone = undoneIds(this.log.entries);
|