@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-BXtfHq7G.js';
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-BXtfHq7G.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-BXtfHq7G.js';
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(base, (c) => withHistory(adapter, c, (history) => ({ entries: history.list() })));
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 window = await history.beginWindow(YOU, text(body.label) ?? "Edited in Studio");
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 constants3, createReadStream as createReadStream2 } from "fs";
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 dirname5, join as join15 } from "path";
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(dirname5(to), { recursive: true });
5365
+ await mkdir(dirname6(to), { recursive: true });
5220
5366
  const temp = `${to}.${randomUUID5()}.tmp`;
5221
5367
  try {
5222
- await copyFile(from, temp, constants3.COPYFILE_FICLONE);
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, constants3.COPYFILE_FICLONE);
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(dirname5(pathOf(hash)), { recursive: true });
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 mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
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 = readFileSync15(join16(projectDir, ID_PATH), "utf-8").trim();
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(readFileSync15(join16(historyDir, "project.json"), "utf-8")).dir ?? null;
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
- mkdirSync7(join16(dir, ".hyperframes"), { recursive: true });
5442
+ mkdirSync8(join16(dir, ".hyperframes"), { recursive: true });
5297
5443
  writeFileSync6(join16(dir, ID_PATH), `${id}
5298
5444
  `);
5299
5445
  }
5300
- mkdirSync7(join16(historyRoot, id), { recursive: true });
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
- const group = window ?? this.outsideGroup();
5542
- const earlier = group.changes.get(path);
5543
- const from = earlier ? earlier.before : before;
5544
- if (from === after) group.changes.delete(path);
5545
- else group.changes.set(path, { path, before: from, after });
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);