diff --git a/src/Integrations/Divi/Design/SnapshotStore.php b/src/Integrations/Divi/Design/SnapshotStore.php
index 5ddff26..3e5a5ca 100644
--- a/src/Integrations/Divi/Design/SnapshotStore.php
+++ b/src/Integrations/Divi/Design/SnapshotStore.php
@@ -11,8 +11,9 @@ defined('ABSPATH') || exit;
/**
* Snapshots of the design system (spec §5, G6): one option per area (lit_mcp_snapshots_, never autoloaded),
- * newest first, at most LIMIT entries – the oldest falls out. An entry keeps the area's raw inventory as Divi stores
- * it plus who made it with which tool and the change log entry. The only way back for colors, variables and presets:
+ * newest first, at most LIMIT entries and TOTAL_BYTES together – the oldest fall out, the newest always stays.
+ * An entry keeps the area's raw inventory as Divi stores it plus who made it with which tool and the change log
+ * entry. The only way back for colors, variables and presets:
* Divi 5 keeps no server-side history (formats §7.6). Not final so abilities can be tested with a mock.
*/
class SnapshotStore
@@ -24,6 +25,9 @@ class SnapshotStore
/** Spec §5: a larger snapshot refuses the change. */
public const MAX_BYTES = 2000000;
+ /** Final review I-4: all entries of one area together (sum of "size"). */
+ public const TOTAL_BYTES = 8000000;
+
private const PREFIX = 'lit_mcp_snapshots_';
public static function option(string $area): string
@@ -63,13 +67,30 @@ class SnapshotStore
'size' => $size,
'data' => $data,
]);
- if (! update_option(self::option($area), array_slice($entries, 0, self::LIMIT), false)) {
+ if (! update_option(self::option($area), self::trimmed($entries), false)) {
throw new RuntimeException(sprintf('snapshot %s could not be stored', $area));
}
return $id;
}
+ /**
+ * The newest LIMIT entries, minus the oldest while all together exceed TOTAL_BYTES; the newest always stays.
+ *
+ * @param list $entries Newest first.
+ * @return list
+ */
+ private static function trimmed(array $entries): array
+ {
+ $entries = array_slice($entries, 0, self::LIMIT);
+ $total = array_sum(array_column($entries, 'size'));
+ while (count($entries) > 1 && $total > self::TOTAL_BYTES) {
+ $total -= array_pop($entries)['size'];
+ }
+
+ return $entries;
+ }
+
/** Notes the change log entry of the change the snapshot belongs to (the log entry is written after it). */
public function attachLog(string $area, string $id, ?int $logId): void
{
diff --git a/tests/Unit/Integrations/Divi/Design/SnapshotStoreTest.php b/tests/Unit/Integrations/Divi/Design/SnapshotStoreTest.php
index 1d30d46..136470f 100644
--- a/tests/Unit/Integrations/Divi/Design/SnapshotStoreTest.php
+++ b/tests/Unit/Integrations/Divi/Design/SnapshotStoreTest.php
@@ -64,6 +64,32 @@ final class SnapshotStoreTest extends TestCase
self::assertNull($store->get('presets', $ids[1]));
}
+ /** Final review I-4: the option as a whole stays below TOTAL_BYTES; the oldest entries fall out first. */
+ public function testKeepsTheOptionBelowTheTotalSize(): void
+ {
+ $store = new SnapshotStore();
+ $big = ['x' => str_repeat('a', 1900000)];
+ $ids = [];
+ for ($i = 0; $i < 5; ++$i) {
+ $ids[] = $store->save('presets', $big + ['n' => $i], 'lit/divi-presets-set');
+ }
+
+ $list = $store->list('presets');
+ self::assertSame([$ids[4], $ids[3], $ids[2], $ids[1]], array_column($list, 'id'));
+ self::assertLessThanOrEqual(SnapshotStore::TOTAL_BYTES, array_sum(array_column($list, 'size')));
+ }
+
+ public function testOldEntriesOverTheTotalSizeFallOutButTheNewestStays(): void
+ {
+ $store = new SnapshotStore();
+ $store->save('presets', ['x' => str_repeat('a', 1900000)], 'lit/divi-presets-set');
+ $this->options['lit_mcp_snapshots_presets'][0]['size'] = SnapshotStore::TOTAL_BYTES + 1;
+
+ $newest = $store->save('presets', ['n' => 1], 'lit/divi-presets-set');
+
+ self::assertSame([$newest], array_column($store->list('presets'), 'id'));
+ }
+
public function testRefusesSnapshotsAboveTheLimit(): void
{
$this->expectException(SnapshotTooLarge::class);