@hyperframes/studio-server 0.7.103 → 0.7.105

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/index.d.ts CHANGED
@@ -34,8 +34,8 @@ interface FileWriteReceipt {
34
34
  }
35
35
  /** Strong content version used as both the JSON version and HTTP ETag. */
36
36
  declare function fileContentVersion(content: string): string;
37
- /** Attach one API write's identity to the corresponding filesystem-watch echo. */
38
- declare function consumeFileWriteReceipt(absPath: string): FileWriteReceipt | null;
37
+ /** Attach one API write's identity to the watcher echo for its exact bytes. */
38
+ declare function consumeFileWriteReceipt(absPath: string, expectedVersion: string): FileWriteReceipt | null;
39
39
 
40
40
  /**
41
41
  * Build a standalone HTML page for a sub-composition.
package/dist/index.js CHANGED
@@ -624,12 +624,13 @@ function recordFileWriteReceipt(absPath, receipt) {
624
624
  current.push({ ...receipt, recordedAt: now });
625
625
  receipts.set(absPath, current);
626
626
  }
627
- function consumeFileWriteReceipt(absPath) {
627
+ function consumeFileWriteReceipt(absPath, expectedVersion) {
628
628
  const now = Date.now();
629
629
  const current = (receipts.get(absPath) ?? []).filter(
630
630
  (entry) => now - entry.recordedAt < RECEIPT_TTL_MS
631
631
  );
632
- const receipt = current.shift() ?? null;
632
+ const receiptIndex = current.findIndex((entry) => entry.version === expectedVersion);
633
+ const receipt = receiptIndex === -1 ? null : current.splice(receiptIndex, 1)[0] ?? null;
633
634
  if (current.length > 0) receipts.set(absPath, current);
634
635
  else receipts.delete(absPath);
635
636
  if (!receipt) return null;
@@ -1077,19 +1078,44 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
1077
1078
  }
1078
1079
  return { durable: true, files };
1079
1080
  }
1081
+ function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
1082
+ const result = commitElementPatchBatches(projectDir, batches);
1083
+ if ("error" in result || !result.durable) return result;
1084
+ for (const file of result.files) {
1085
+ if (!file.changed) continue;
1086
+ const absPath = resolveWithinProject(projectDir, file.sourceFile);
1087
+ if (!absPath) throw new Error(`Committed element patch escaped project: ${file.sourceFile}`);
1088
+ recordMutationReceipt(c, file.sourceFile, absPath, file.after);
1089
+ }
1090
+ return result;
1091
+ }
1092
+ function recordMutationReceipt(c, filePath, absPath, html) {
1093
+ const version = fileContentVersion(html);
1094
+ const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
1095
+ recordFileWriteReceipt(absPath, { path: filePath, version, writeToken });
1096
+ return { version, writeToken };
1097
+ }
1098
+ function writeFileWithReceipt(c, filePath, absPath, html) {
1099
+ writeFileSync4(absPath, html, "utf-8");
1100
+ return recordMutationReceipt(c, filePath, absPath, html);
1101
+ }
1102
+ function writeMutationResult(c, projectDir, filePath, absPath, html) {
1103
+ const backup = snapshotBeforeWrite(projectDir, absPath);
1104
+ if (backup.error) console.warn(`Failed to create backup for ${filePath}: ${backup.error}`);
1105
+ const { version } = writeFileWithReceipt(c, filePath, absPath, html);
1106
+ return { backupPath: backupPathForResponse(projectDir, backup.backupPath), version };
1107
+ }
1080
1108
  function writeIfChanged(c, projectDir, filePath, absPath, original, next) {
1081
1109
  if (next === original) {
1082
1110
  return c.json({ ok: true, changed: false, content: original, path: filePath });
1083
1111
  }
1084
- const backup = snapshotBeforeWrite(projectDir, absPath);
1085
- if (backup.error) console.warn(`Failed to create backup for ${filePath}: ${backup.error}`);
1086
- writeFileSync4(absPath, next, "utf-8");
1112
+ const { backupPath } = writeMutationResult(c, projectDir, filePath, absPath, next);
1087
1113
  return c.json({
1088
1114
  ok: true,
1089
1115
  changed: true,
1090
1116
  content: next,
1091
1117
  path: filePath,
1092
- backupPath: backupPathForResponse(projectDir, backup.backupPath)
1118
+ backupPath
1093
1119
  });
1094
1120
  }
1095
1121
  function rejectUnsafeMutationValues(c, unsafeFields) {
@@ -1488,10 +1514,13 @@ async function applyGsapMutations(c, res, mutations) {
1488
1514
  return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
1489
1515
  }
1490
1516
  if (changed) {
1491
- const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
1492
- if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
1493
- backupPath = backupPathForResponse(res.project.dir, backup.backupPath);
1494
- writeFileSync4(res.absPath, newHtml, "utf-8");
1517
+ backupPath = writeMutationResult(
1518
+ c,
1519
+ res.project.dir,
1520
+ res.filePath,
1521
+ res.absPath,
1522
+ newHtml
1523
+ ).backupPath;
1495
1524
  }
1496
1525
  const responsePayload = {
1497
1526
  ok: true,
@@ -2448,10 +2477,12 @@ function registerFileRoutes(api, adapter) {
2448
2477
  }
2449
2478
  const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2450
2479
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2451
- writeFileSync4(ctx.absPath, insertion.html, "utf-8");
2452
- const version = fileContentVersion(insertion.html);
2453
- const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
2454
- recordFileWriteReceipt(ctx.absPath, { path: ctx.filePath, version, writeToken });
2480
+ const { version, writeToken } = writeFileWithReceipt(
2481
+ c,
2482
+ ctx.filePath,
2483
+ ctx.absPath,
2484
+ insertion.html
2485
+ );
2455
2486
  c.header("ETag", version);
2456
2487
  return c.json({
2457
2488
  ok: true,
@@ -2649,10 +2680,13 @@ function registerFileRoutes(api, adapter) {
2649
2680
  version: version2
2650
2681
  });
2651
2682
  }
2652
- const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2653
- if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
2654
- writeFileSync4(ctx.absPath, result.html, "utf-8");
2655
- const version = fileContentVersion(result.html);
2683
+ const { version, backupPath } = writeMutationResult(
2684
+ c,
2685
+ ctx.project.dir,
2686
+ ctx.filePath,
2687
+ ctx.absPath,
2688
+ result.html
2689
+ );
2656
2690
  c.header("ETag", version);
2657
2691
  return c.json({
2658
2692
  ok: true,
@@ -2661,7 +2695,7 @@ function registerFileRoutes(api, adapter) {
2661
2695
  newId: result.newId,
2662
2696
  path: ctx.filePath,
2663
2697
  version,
2664
- backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
2698
+ backupPath
2665
2699
  });
2666
2700
  });
2667
2701
  api.post("/projects/:id/file-mutations/patch-element/*", async (c) => {
@@ -2696,16 +2730,20 @@ function registerFileRoutes(api, adapter) {
2696
2730
  path: ctx.filePath
2697
2731
  });
2698
2732
  }
2699
- const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2700
- if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
2701
- writeFileSync4(ctx.absPath, patched, "utf-8");
2733
+ const { backupPath } = writeMutationResult(
2734
+ c,
2735
+ ctx.project.dir,
2736
+ ctx.filePath,
2737
+ ctx.absPath,
2738
+ patched
2739
+ );
2702
2740
  return c.json({
2703
2741
  ok: true,
2704
2742
  changed: true,
2705
2743
  matched,
2706
2744
  content: patched,
2707
2745
  path: ctx.filePath,
2708
- backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
2746
+ backupPath
2709
2747
  });
2710
2748
  });
2711
2749
  api.post("/projects/:id/file-mutations/patch-element-batches", async (c) => {
@@ -2717,7 +2755,7 @@ function registerFileRoutes(api, adapter) {
2717
2755
  }
2718
2756
  const unsafeFields = findUnsafeElementPatchBatchValues(body.batches);
2719
2757
  if (unsafeFields.length > 0) return rejectUnsafeMutationValues(c, unsafeFields);
2720
- const result = commitElementPatchBatches(project.dir, body.batches);
2758
+ const result = commitElementPatchBatchesWithReceipts(c, project.dir, body.batches);
2721
2759
  if ("error" in result) {
2722
2760
  return elementPatchBatchCommitErrorResponse(c, result.error, result.sourceFile);
2723
2761
  }
@@ -2735,7 +2773,7 @@ function registerFileRoutes(api, adapter) {
2735
2773
  if (unsafeFields.length > 0) {
2736
2774
  return rejectUnsafeMutationValues(c, unsafeFields);
2737
2775
  }
2738
- const result = commitElementPatchBatches(ctx.project.dir, [batch]);
2776
+ const result = commitElementPatchBatchesWithReceipts(c, ctx.project.dir, [batch]);
2739
2777
  if ("error" in result) {
2740
2778
  return elementPatchBatchCommitErrorResponse(c, result.error, result.sourceFile);
2741
2779
  }
@@ -2791,16 +2829,20 @@ function registerFileRoutes(api, adapter) {
2791
2829
  result.error === "grouped elements must share a single parent" ? 422 : 400
2792
2830
  );
2793
2831
  }
2794
- const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2795
- if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
2796
- writeFileSync4(ctx.absPath, result.html, "utf-8");
2832
+ const { backupPath } = writeMutationResult(
2833
+ c,
2834
+ ctx.project.dir,
2835
+ ctx.filePath,
2836
+ ctx.absPath,
2837
+ result.html
2838
+ );
2797
2839
  return c.json({
2798
2840
  ok: true,
2799
2841
  changed: true,
2800
2842
  groupId: result.groupId,
2801
2843
  content: result.html,
2802
2844
  path: ctx.filePath,
2803
- backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
2845
+ backupPath
2804
2846
  });
2805
2847
  });
2806
2848
  api.post("/projects/:id/file-mutations/unwrap-elements/*", async (c) => {