invalidate asset cache on metadata changes and refresh asset details
This commit is contained in:
parent
a7710be847
commit
d773e8684b
@ -4,12 +4,16 @@ import java.util.*;
|
||||
|
||||
public record PackerAssetCacheEntry(
|
||||
int assetId,
|
||||
String contractFingerprint,
|
||||
List<PackerFileCacheEntry> files) {
|
||||
|
||||
public PackerAssetCacheEntry {
|
||||
if (assetId <= 0) {
|
||||
throw new IllegalArgumentException("assetId must be positive");
|
||||
}
|
||||
contractFingerprint = contractFingerprint == null || contractFingerprint.isBlank()
|
||||
? null
|
||||
: contractFingerprint.trim();
|
||||
files = List.copyOf(Objects.requireNonNull(files, "files"));
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ public final class FileSystemPackerCacheRepository {
|
||||
}
|
||||
validateAssetId(asset.assetId);
|
||||
validateDuplicateRelativePaths(asset.assetId, files);
|
||||
assets.add(new PackerAssetCacheEntry(asset.assetId, files));
|
||||
assets.add(new PackerAssetCacheEntry(asset.assetId, asset.contractFingerprint, files));
|
||||
}
|
||||
}
|
||||
validateDuplicateAssetIds(assets);
|
||||
@ -84,6 +84,7 @@ public final class FileSystemPackerCacheRepository {
|
||||
document.assets = safeState.assets().stream()
|
||||
.map(asset -> new CacheAssetDocument(
|
||||
asset.assetId(),
|
||||
asset.contractFingerprint(),
|
||||
asset.files().stream()
|
||||
.map(file -> new CacheFileDocument(
|
||||
file.relativePath(),
|
||||
@ -150,6 +151,7 @@ public final class FileSystemPackerCacheRepository {
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record CacheAssetDocument(
|
||||
@JsonProperty("asset_id") int assetId,
|
||||
@JsonProperty("contract_fingerprint") String contractFingerprint,
|
||||
@JsonProperty("files") List<CacheFileDocument> files) {
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package p.packer.repositories;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public final class PackerContractFingerprint {
|
||||
private PackerContractFingerprint() {
|
||||
}
|
||||
|
||||
public static String metadataFingerprint(Map<String, String> metadata) {
|
||||
final var normalized = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
|
||||
Objects.requireNonNull(metadata, "metadata").forEach((key, value) -> {
|
||||
if (key == null || key.isBlank()) {
|
||||
return;
|
||||
}
|
||||
normalized.put(key.trim(), value == null ? "" : value);
|
||||
});
|
||||
final String canonical = normalized.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + "=" + entry.getValue())
|
||||
.reduce((left, right) -> left + "\n" + right)
|
||||
.orElse("");
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(canonical.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 contract fingerprint is unavailable", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,9 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
final Optional<PackerRegistryEntry> safeRegistryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
|
||||
final PackerAssetDeclarationParseResult safeParseResult = Objects.requireNonNull(parseResult, "parseResult");
|
||||
final Optional<PackerAssetCacheEntry> safePriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache");
|
||||
final String currentContractFingerprint = safeParseResult.valid()
|
||||
? PackerContractFingerprint.metadataFingerprint(safeParseResult.declaration().outputMetadata())
|
||||
: null;
|
||||
|
||||
if (safeRegistryEntry.isEmpty() || !safeParseResult.valid()) {
|
||||
return new PackerRuntimeAssetMaterialization(
|
||||
@ -42,7 +45,7 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
final PackerWalkResult walkResult = assetWalker.walk(
|
||||
normalizedAssetRoot,
|
||||
safeParseResult.declaration(),
|
||||
safePriorAssetCache);
|
||||
reusablePriorAssetCache(safePriorAssetCache, currentContractFingerprint));
|
||||
final List<PackerRuntimeWalkFile> buildCandidateFiles = walkResult.probeResults().stream()
|
||||
.map(probeResult -> toRuntimeWalkFile(normalizedAssetRoot, probeResult))
|
||||
.sorted(Comparator.comparing(PackerRuntimeWalkFile::relativePath, String.CASE_INSENSITIVE_ORDER))
|
||||
@ -63,6 +66,7 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
walkResult.diagnostics());
|
||||
final PackerAssetCacheEntry assetCacheEntry = new PackerAssetCacheEntry(
|
||||
safeRegistryEntry.get().assetId(),
|
||||
currentContractFingerprint,
|
||||
buildCandidateFiles.stream()
|
||||
.map(file -> new PackerFileCacheEntry(
|
||||
file.relativePath(),
|
||||
@ -75,6 +79,19 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
return new PackerRuntimeAssetMaterialization(runtimeAsset, Optional.of(assetCacheEntry));
|
||||
}
|
||||
|
||||
private Optional<PackerAssetCacheEntry> reusablePriorAssetCache(
|
||||
Optional<PackerAssetCacheEntry> priorAssetCache,
|
||||
String currentContractFingerprint) {
|
||||
if (priorAssetCache.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final String cachedContractFingerprint = priorAssetCache.get().contractFingerprint();
|
||||
if (cachedContractFingerprint == null || !cachedContractFingerprint.equals(currentContractFingerprint)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return priorAssetCache;
|
||||
}
|
||||
|
||||
private List<String> listAvailableFiles(Path assetRoot) {
|
||||
try (var paths = Files.list(assetRoot)
|
||||
.filter(Files::isRegularFile)
|
||||
|
||||
@ -39,7 +39,7 @@ final class FileSystemPackerCacheRepositoryTest {
|
||||
final var state = new PackerWorkspaceCacheState(
|
||||
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
|
||||
List.of(
|
||||
new PackerAssetCacheEntry(3, List.of(
|
||||
new PackerAssetCacheEntry(3, "contract-3", List.of(
|
||||
new PackerFileCacheEntry(
|
||||
"tiles/ui.png",
|
||||
"image/png",
|
||||
@ -54,7 +54,7 @@ final class FileSystemPackerCacheRepositoryTest {
|
||||
84L,
|
||||
"def456",
|
||||
Map.of()))),
|
||||
new PackerAssetCacheEntry(1, List.of(
|
||||
new PackerAssetCacheEntry(1, "contract-1", List.of(
|
||||
new PackerFileCacheEntry(
|
||||
"audio/click.wav",
|
||||
"audio/wav",
|
||||
@ -67,6 +67,7 @@ final class FileSystemPackerCacheRepositoryTest {
|
||||
final var loaded = repository.load(project);
|
||||
|
||||
assertEquals(List.of(1, 3), loaded.assets().stream().map(PackerAssetCacheEntry::assetId).toList());
|
||||
assertEquals("contract-3", loaded.findAsset(3).orElseThrow().contractFingerprint());
|
||||
assertTrue(loaded.findAsset(3).flatMap(asset -> asset.findFile("tiles/ui.png")).isPresent());
|
||||
assertEquals(128L, loaded.findAsset(3).orElseThrow().findFile("tiles/ui.png").orElseThrow().size());
|
||||
}
|
||||
@ -105,8 +106,8 @@ final class FileSystemPackerCacheRepositoryTest {
|
||||
final var state = new PackerWorkspaceCacheState(
|
||||
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
|
||||
List.of(
|
||||
new PackerAssetCacheEntry(7, List.of()),
|
||||
new PackerAssetCacheEntry(9, List.of())));
|
||||
new PackerAssetCacheEntry(7, "contract-7", List.of()),
|
||||
new PackerAssetCacheEntry(9, "contract-9", List.of())));
|
||||
|
||||
assertTrue(state.findAsset(7).isPresent());
|
||||
assertTrue(state.findAsset(8).isEmpty());
|
||||
@ -118,7 +119,7 @@ final class FileSystemPackerCacheRepositoryTest {
|
||||
final var project = project(tempDir.resolve("project"));
|
||||
final var state = new PackerWorkspaceCacheState(
|
||||
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
|
||||
List.of(new PackerAssetCacheEntry(1, List.of(
|
||||
List.of(new PackerAssetCacheEntry(1, "contract-1", List.of(
|
||||
new PackerFileCacheEntry(
|
||||
"tiles/ui.png",
|
||||
"image/png",
|
||||
@ -133,6 +134,7 @@ final class FileSystemPackerCacheRepositoryTest {
|
||||
assertFalse(json.contains("diagnostic"));
|
||||
assertFalse(json.contains("diagnostics"));
|
||||
assertTrue(json.contains("\"asset_id\""));
|
||||
assertTrue(json.contains("\"contract_fingerprint\""));
|
||||
assertTrue(json.contains("\"metadata\""));
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ final class PackerAbstractBankWalkerTest {
|
||||
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||
final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
|
||||
new PackerFileCacheEntry("entry.txt", "text/plain", 1L, lastModified, "cached", Map.of("source", "cache"))));
|
||||
|
||||
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
|
||||
@ -44,7 +44,7 @@ final class PackerAbstractBankWalkerTest {
|
||||
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||
final long currentLastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||
final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
|
||||
new PackerFileCacheEntry(
|
||||
"entry.txt",
|
||||
"text/plain",
|
||||
@ -67,7 +67,7 @@ final class PackerAbstractBankWalkerTest {
|
||||
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||
final String fingerprint = sha256(Files.readAllBytes(filePath));
|
||||
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||
final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
|
||||
new PackerFileCacheEntry(
|
||||
"entry.txt",
|
||||
"text/plain",
|
||||
@ -89,7 +89,7 @@ final class PackerAbstractBankWalkerTest {
|
||||
final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
|
||||
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
|
||||
final var priorCache = new PackerAssetCacheEntry(1, List.of(
|
||||
final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
|
||||
new PackerFileCacheEntry(
|
||||
"entry.txt",
|
||||
"text/plain",
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
package p.packer.repositories;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.packer.models.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class PackerRuntimeAssetMaterializerTest {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void reusesPriorAssetCacheOnlyWhenContractFingerprintMatches() throws Exception {
|
||||
final var walker = new TrackingWalker();
|
||||
final var materializer = new PackerRuntimeAssetMaterializer(walker);
|
||||
final Path assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
Files.writeString(manifestPath, "{}");
|
||||
|
||||
final PackerAssetDeclaration declaration = declaration(Map.of("tile_size", "32x32"));
|
||||
final PackerAssetDeclarationParseResult parseResult = new PackerAssetDeclarationParseResult(declaration, List.of());
|
||||
final String fingerprint = PackerContractFingerprint.metadataFingerprint(declaration.outputMetadata());
|
||||
final Optional<PackerAssetCacheEntry> priorAssetCache = Optional.of(new PackerAssetCacheEntry(1, fingerprint, List.of()));
|
||||
|
||||
materializer.materialize(
|
||||
assetRoot,
|
||||
manifestPath,
|
||||
Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)),
|
||||
parseResult,
|
||||
priorAssetCache);
|
||||
|
||||
assertTrue(walker.lastPriorAssetCache().isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidatesPriorAssetCacheWhenContractFingerprintChanges() throws Exception {
|
||||
final var walker = new TrackingWalker();
|
||||
final var materializer = new PackerRuntimeAssetMaterializer(walker);
|
||||
final Path assetRoot = Files.createDirectories(tempDir.resolve("asset"));
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
Files.writeString(manifestPath, "{}");
|
||||
|
||||
materializer.materialize(
|
||||
assetRoot,
|
||||
manifestPath,
|
||||
Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)),
|
||||
new PackerAssetDeclarationParseResult(declaration(Map.of("tile_size", "32x32")), List.of()),
|
||||
Optional.of(new PackerAssetCacheEntry(
|
||||
1,
|
||||
PackerContractFingerprint.metadataFingerprint(Map.of("tile_size", "16x16")),
|
||||
List.of())));
|
||||
|
||||
assertTrue(walker.lastPriorAssetCache().isEmpty());
|
||||
}
|
||||
|
||||
private static PackerAssetDeclaration declaration(Map<String, String> metadata) {
|
||||
return new PackerAssetDeclaration(
|
||||
1,
|
||||
"uuid",
|
||||
"asset",
|
||||
AssetFamilyCatalog.TILE_BANK,
|
||||
Map.of(),
|
||||
List.of(),
|
||||
OutputFormatCatalog.TILES_INDEXED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
metadata,
|
||||
true);
|
||||
}
|
||||
|
||||
private static final class TrackingWalker extends PackerAssetWalker {
|
||||
private Optional<PackerAssetCacheEntry> lastPriorAssetCache = Optional.empty();
|
||||
|
||||
private TrackingWalker() {
|
||||
super(MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackerWalkResult walk(
|
||||
Path assetRoot,
|
||||
PackerAssetDeclaration declaration,
|
||||
Optional<PackerAssetCacheEntry> priorAssetCache) {
|
||||
lastPriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache");
|
||||
return PackerWalkResult.EMPTY;
|
||||
}
|
||||
|
||||
private Optional<PackerAssetCacheEntry> lastPriorAssetCache() {
|
||||
return lastPriorAssetCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,6 +104,14 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
@Override
|
||||
public void registerEventSubscriptions() {
|
||||
eventBindings.listen(workspaceBus, StudioAssetsRefreshRequestedEvent.class).handle(event -> {
|
||||
if (event.preferredAssetReference() != null) {
|
||||
loadSelection(event.preferredAssetReference());
|
||||
return;
|
||||
}
|
||||
if (event.deepSync() && viewState.selectedAssetReference() != null) {
|
||||
loadSelection(viewState.selectedAssetReference());
|
||||
return;
|
||||
}
|
||||
if (event.preferredAssetReference() == null) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user