implements PR-027 runtime walkresult and cache integration

This commit is contained in:
bQUARKz 2026-03-18 19:56:52 +00:00
parent 1442adc7b3
commit b4733a0e49
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
18 changed files with 512 additions and 52 deletions

View File

@ -53,6 +53,9 @@ public enum OutputFormatCatalog {
return UNKNOWN;
}
final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT);
if ("sound/bank_v1".equals(normalized)) {
return SOUND_V1;
}
for (OutputFormatCatalog candidate : values()) {
if (candidate == UNKNOWN) {
continue;

View File

@ -2,7 +2,9 @@ package p.packer;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.events.PackerEventSink;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.services.*;
@ -29,7 +31,13 @@ public final class Packer implements Closeable {
final var workspaceFoundation = new PackerWorkspaceFoundation(mapper);
final var declarationParser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var runtimeLoader = new PackerRuntimeLoader(workspaceFoundation, declarationParser, assetWalker);
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var runtimeLoader = new PackerRuntimeLoader(
workspaceFoundation,
declarationParser,
cacheRepository,
assetMaterializer);
final var runtimeRegistry = new PackerRuntimeRegistry(runtimeLoader);
final var assetReferenceResolver = new PackerAssetReferenceResolver(workspaceFoundation.lookup());
final var assetDetailsService = new PackerAssetDetailsService(runtimeRegistry, assetReferenceResolver);
@ -37,7 +45,7 @@ public final class Packer implements Closeable {
runtimeRegistry,
assetReferenceResolver,
workspaceFoundation.lookup());
final var runtimePatchService = new PackerRuntimePatchService(declarationParser);
final var runtimePatchService = new PackerRuntimePatchService(declarationParser, assetMaterializer);
final var writeCoordinator = new PackerProjectWriteCoordinator();
return new Packer(new FileSystemPackerWorkspaceService(
mapper,
@ -46,6 +54,7 @@ public final class Packer implements Closeable {
assetActionReadService,
runtimePatchService,
runtimeRegistry,
cacheRepository,
writeCoordinator,
resolvedEventSink), runtimeRegistry, writeCoordinator);
}

View File

@ -1,6 +1,7 @@
package p.packer.models;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@ -8,12 +9,24 @@ public record PackerRuntimeAsset(
Path assetRoot,
Path manifestPath,
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parsedDeclaration) {
PackerAssetDeclarationParseResult parsedDeclaration,
PackerRuntimeWalkProjection walkProjection,
List<PackerDiagnostic> walkDiagnostics) {
public PackerRuntimeAsset(
Path assetRoot,
Path manifestPath,
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parsedDeclaration) {
this(assetRoot, manifestPath, registryEntry, parsedDeclaration, PackerRuntimeWalkProjection.EMPTY, List.of());
}
public PackerRuntimeAsset {
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
manifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize();
registryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
parsedDeclaration = Objects.requireNonNull(parsedDeclaration, "parsedDeclaration");
walkProjection = Objects.requireNonNull(walkProjection, "walkProjection");
walkDiagnostics = List.copyOf(Objects.requireNonNull(walkDiagnostics, "walkDiagnostics"));
}
}

View File

@ -6,7 +6,15 @@ import java.util.Objects;
public record PackerRuntimeSnapshot(
long generation,
PackerRegistryState registry,
List<PackerRuntimeAsset> assets) {
List<PackerRuntimeAsset> assets,
PackerWorkspaceCacheState cacheState) {
public PackerRuntimeSnapshot(
long generation,
PackerRegistryState registry,
List<PackerRuntimeAsset> assets) {
this(generation, registry, assets, PackerWorkspaceCacheState.EMPTY);
}
public PackerRuntimeSnapshot {
if (generation <= 0L) {
@ -14,5 +22,6 @@ public record PackerRuntimeSnapshot(
}
registry = Objects.requireNonNull(registry, "registry");
assets = List.copyOf(Objects.requireNonNull(assets, "assets"));
cacheState = Objects.requireNonNull(cacheState, "cacheState");
}
}

View File

@ -0,0 +1,30 @@
package p.packer.models;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record PackerRuntimeWalkFile(
String relativePath,
String mimeType,
long size,
long lastModified,
String fingerprint,
Map<String, Object> metadata,
List<PackerDiagnostic> diagnostics) {
public PackerRuntimeWalkFile {
relativePath = Objects.requireNonNull(relativePath, "relativePath");
mimeType = mimeType == null || mimeType.isBlank() ? null : mimeType;
if (size < 0L) {
throw new IllegalArgumentException("size must be non-negative");
}
if (lastModified < 0L) {
throw new IllegalArgumentException("lastModified must be non-negative");
}
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint;
metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata")));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -0,0 +1,19 @@
package p.packer.models;
import java.util.List;
import java.util.Objects;
public record PackerRuntimeWalkProjection(
List<String> availableFiles,
List<PackerRuntimeWalkFile> buildCandidateFiles,
long measuredBankSizeBytes) {
public static final PackerRuntimeWalkProjection EMPTY = new PackerRuntimeWalkProjection(List.of(), List.of(), 0L);
public PackerRuntimeWalkProjection {
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
buildCandidateFiles = List.copyOf(Objects.requireNonNull(buildCandidateFiles, "buildCandidateFiles"));
if (measuredBankSizeBytes < 0L) {
throw new IllegalArgumentException("measuredBankSizeBytes must be non-negative");
}
}
}

View File

@ -14,9 +14,6 @@ import p.packer.models.PackerWalkResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@ -132,12 +129,7 @@ public abstract class PackerAbstractBankWalker<R> {
}
protected String computeFingerprint(final PackerFileProbe fileProbe) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(fileProbe.content()));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 fingerprint is unavailable", exception);
}
return PackerFileFingerprint.sha256(fileProbe);
}
private Optional<PackerFileCacheEntry> resolvePriorFileCache(

View File

@ -146,7 +146,7 @@ public class PackerAssetWalker {
return RequirementBuildResult.fail("Missing sample rate for sound bank");
}
final var sampleRate = Integer.parseInt(sampleRateStr);
final var channelsStr = metadata.get("");
final var channelsStr = metadata.get("channels");
if (StringUtils.isBlank(channelsStr)) {
return RequirementBuildResult.fail("Missing channels for sound bank");
}

View File

@ -0,0 +1,21 @@
package p.packer.repositories;
import p.packer.models.PackerFileProbe;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
final class PackerFileFingerprint {
private PackerFileFingerprint() {
}
static String sha256(final PackerFileProbe fileProbe) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(fileProbe.content()));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 fingerprint is unavailable", exception);
}
}
}

View File

@ -0,0 +1,110 @@
package p.packer.repositories;
import p.packer.models.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public final class PackerRuntimeAssetMaterializer {
private final PackerAssetWalker assetWalker;
public PackerRuntimeAssetMaterializer(PackerAssetWalker assetWalker) {
this.assetWalker = Objects.requireNonNull(assetWalker, "assetWalker");
}
public PackerRuntimeAssetMaterialization materialize(
Path assetRoot,
Path manifestPath,
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parseResult,
Optional<PackerAssetCacheEntry> priorAssetCache) {
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
final Path normalizedManifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize();
final Optional<PackerRegistryEntry> safeRegistryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
final PackerAssetDeclarationParseResult safeParseResult = Objects.requireNonNull(parseResult, "parseResult");
final Optional<PackerAssetCacheEntry> safePriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache");
if (safeRegistryEntry.isEmpty() || !safeParseResult.valid()) {
return new PackerRuntimeAssetMaterialization(
new PackerRuntimeAsset(
normalizedAssetRoot,
normalizedManifestPath,
safeRegistryEntry,
safeParseResult,
PackerRuntimeWalkProjection.EMPTY,
List.of()),
Optional.empty());
}
final List<String> availableFiles = listAvailableFiles(normalizedAssetRoot);
final PackerWalkResult walkResult = assetWalker.walk(
normalizedAssetRoot,
safeParseResult.declaration(),
safePriorAssetCache);
final List<PackerRuntimeWalkFile> buildCandidateFiles = walkResult.probeResults().stream()
.map(probeResult -> toRuntimeWalkFile(normalizedAssetRoot, probeResult))
.sorted(Comparator.comparing(PackerRuntimeWalkFile::relativePath, String.CASE_INSENSITIVE_ORDER))
.toList();
final long measuredBankSizeBytes = buildCandidateFiles.stream()
.mapToLong(PackerRuntimeWalkFile::size)
.sum();
final PackerRuntimeWalkProjection walkProjection = new PackerRuntimeWalkProjection(
availableFiles,
buildCandidateFiles,
measuredBankSizeBytes);
final PackerRuntimeAsset runtimeAsset = new PackerRuntimeAsset(
normalizedAssetRoot,
normalizedManifestPath,
safeRegistryEntry,
safeParseResult,
walkProjection,
walkResult.diagnostics());
final PackerAssetCacheEntry assetCacheEntry = new PackerAssetCacheEntry(
safeRegistryEntry.get().assetId(),
buildCandidateFiles.stream()
.map(file -> new PackerFileCacheEntry(
file.relativePath(),
file.mimeType(),
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata()))
.toList());
return new PackerRuntimeAssetMaterialization(runtimeAsset, Optional.of(assetCacheEntry));
}
private List<String> listAvailableFiles(Path assetRoot) {
try (var paths = Files.list(assetRoot)
.filter(Files::isRegularFile)
.filter(path -> !path.getFileName().toString().equalsIgnoreCase("asset.json"))
.map(path -> assetRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'))
.sorted(String.CASE_INSENSITIVE_ORDER)) {
return paths.toList();
} catch (IOException exception) {
return List.of();
}
}
private PackerRuntimeWalkFile toRuntimeWalkFile(Path assetRoot, PackerProbeResult probeResult) {
final PackerFileProbe fileProbe = probeResult.fileProbe();
return new PackerRuntimeWalkFile(
assetRoot.relativize(fileProbe.path().toAbsolutePath().normalize()).toString().replace('\\', '/'),
fileProbe.mimeType(),
fileProbe.size(),
fileProbe.lastModified(),
PackerFileFingerprint.sha256(fileProbe),
probeResult.metadata(),
probeResult.diagnostics());
}
public record PackerRuntimeAssetMaterialization(
PackerRuntimeAsset runtimeAsset,
Optional<PackerAssetCacheEntry> assetCacheEntry) {
public PackerRuntimeAssetMaterialization {
runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset");
assetCacheEntry = Objects.requireNonNull(assetCacheEntry, "assetCacheEntry");
}
}
}

View File

@ -2,8 +2,10 @@ package p.packer.repositories;
import p.packer.messages.InitWorkspaceRequest;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerRuntimeAsset;
import p.packer.models.PackerRuntimeSnapshot;
import p.packer.models.PackerWorkspaceCacheState;
import p.packer.services.PackerAssetDeclarationParser;
import p.packer.services.PackerWorkspaceFoundation;
import p.packer.PackerWorkspacePaths;
@ -18,15 +20,18 @@ import java.util.stream.Collectors;
public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
private final PackerWorkspaceFoundation workspaceFoundation;
private final PackerAssetDeclarationParser parser;
private final PackerAssetWalker assetWalker;
private final FileSystemPackerCacheRepository cacheRepository;
private final PackerRuntimeAssetMaterializer assetMaterializer;
public PackerRuntimeLoader(
final PackerWorkspaceFoundation workspaceFoundation,
final PackerAssetDeclarationParser parser,
final PackerAssetWalker assetWalker) {
final FileSystemPackerCacheRepository cacheRepository,
final PackerRuntimeAssetMaterializer assetMaterializer) {
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
this.parser = Objects.requireNonNull(parser, "parser");
this.assetWalker = Objects.requireNonNull(assetWalker, "assetWalker");
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
this.assetMaterializer = Objects.requireNonNull(assetMaterializer, "assetMaterializer");
}
private boolean isAssetJson(Path path, BasicFileAttributes attrs) {
@ -39,6 +44,7 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(safeProject));
final var registry = workspaceFoundation.loadRegistry(safeProject);
final PackerWorkspaceCacheState priorCacheState = loadPriorCacheState(safeProject);
final var registryByRoot = registry
.assets()
.stream()
@ -47,6 +53,7 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
entry -> entry));
final List<PackerRuntimeAsset> assets = new ArrayList<>();
final List<PackerAssetCacheEntry> refreshedCacheEntries = new ArrayList<>();
final var assetsRoot = PackerWorkspacePaths.assetsRoot(safeProject);
if (Files.isDirectory(assetsRoot)) {
try (final var paths = Files.find(assetsRoot, Integer.MAX_VALUE, this::isAssetJson)) {
@ -58,12 +65,16 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
final var assetRoot = manifestPath.getParent();
final var registryEntry = Optional.ofNullable(registryByRoot.get(assetRoot));
final var parseResult = parser.parse(manifestPath);
if (parseResult.valid()) {
final var walkResult = assetWalker.walk(assetRoot, parseResult.declaration());
}
final var runtimeAsset = new PackerRuntimeAsset(assetRoot, manifestPath, registryEntry, parseResult);
assets.add(runtimeAsset);
final Optional<PackerAssetCacheEntry> priorAssetCache = registryEntry
.flatMap(entry -> priorCacheState.findAsset(entry.assetId()));
final var materialized = assetMaterializer.materialize(
assetRoot,
manifestPath,
registryEntry,
parseResult,
priorAssetCache);
assets.add(materialized.runtimeAsset());
materialized.assetCacheEntry().ifPresent(refreshedCacheEntries::add);
}
} catch (IOException exception) {
throw new p.packer.exceptions.PackerRegistryException(
@ -72,6 +83,20 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
}
}
return new PackerRuntimeSnapshot(generation, registry, assets);
final PackerWorkspaceCacheState refreshedCacheState = new PackerWorkspaceCacheState(
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
refreshedCacheEntries.stream()
.sorted(Comparator.comparingInt(PackerAssetCacheEntry::assetId))
.toList());
cacheRepository.save(safeProject, refreshedCacheState);
return new PackerRuntimeSnapshot(generation, registry, assets, refreshedCacheState);
}
private PackerWorkspaceCacheState loadPriorCacheState(PackerProjectContext project) {
try {
return cacheRepository.load(project);
} catch (p.packer.exceptions.PackerRegistryException exception) {
return PackerWorkspaceCacheState.EMPTY;
}
}
}

View File

@ -13,6 +13,7 @@ import p.packer.messages.assets.*;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerRuntimeRegistry;
import java.io.IOException;
@ -28,6 +29,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
private final PackerAssetActionReadService actionReadService;
private final PackerRuntimePatchService runtimePatchService;
private final PackerRuntimeRegistry runtimeRegistry;
private final FileSystemPackerCacheRepository cacheRepository;
private final PackerProjectWriteCoordinator writeCoordinator;
private final PackerEventSink eventSink;
@ -38,6 +40,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
PackerAssetActionReadService actionReadService,
PackerRuntimePatchService runtimePatchService,
PackerRuntimeRegistry runtimeRegistry,
FileSystemPackerCacheRepository cacheRepository,
PackerProjectWriteCoordinator writeCoordinator,
PackerEventSink eventSink) {
this.mapper = Objects.requireNonNull(mapper, "mapper");
@ -46,6 +49,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService");
this.runtimePatchService = Objects.requireNonNull(runtimePatchService, "runtimePatchService");
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
}
@ -84,6 +88,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
final var registryEntry = registryByRoot.get(assetRoot);
final var parsed = runtimeAsset.parsedDeclaration();
diagnostics.addAll(parsed.diagnostics());
diagnostics.addAll(runtimeAsset.walkDiagnostics());
diagnostics.addAll(identityMismatchDiagnostics(registryEntry, parsed, assetManifestPath));
final var summary = buildSummary(project, assetRoot, registryEntry, parsed);
assets.add(summary);
@ -220,13 +225,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
writeManifest(manifestPath, request, entry.assetUuid());
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
workspaceFoundation.saveRegistry(project, updated);
runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
snapshot,
generation,
updated,
entry,
assetRoot,
manifestPath));
saveRuntimeCache(project, runtime.snapshot());
final CreateAssetResult result = new CreateAssetResult(
PackerOperationStatus.SUCCESS,
"Asset created: " + relativeAssetRoot,
@ -270,12 +276,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
declaration.assetUuid());
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
workspaceFoundation.saveRegistry(project, updated);
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset(
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset(
currentSnapshot,
generation,
updated,
entry,
assetRoot));
saveRuntimeCache(project, runtime.snapshot());
final RegisterAssetResult result = new RegisterAssetResult(
PackerOperationStatus.SUCCESS,
"Asset registered: " + relativeAssetRoot,
@ -316,11 +323,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
if (!updatedRegistry.equals(registry)) {
workspaceFoundation.saveRegistry(project, updatedRegistry);
}
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterDeleteAsset(
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterDeleteAsset(
currentSnapshot,
generation,
updatedRegistry,
assetRoot));
saveRuntimeCache(project, runtime.snapshot());
final DeleteAssetResult result = new DeleteAssetResult(
PackerOperationStatus.SUCCESS,
"Asset deleted: " + relativeAssetRoot,
@ -372,7 +380,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
if (!updatedRegistry.equals(registry)) {
workspaceFoundation.saveRegistry(project, updatedRegistry);
}
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterMoveAsset(
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterMoveAsset(
currentSnapshot,
generation,
updatedRegistry,
@ -380,6 +388,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
sourceRoot,
targetRoot,
targetManifestPath));
saveRuntimeCache(project, runtime.snapshot());
final AssetReference canonicalReference = updatedEntry
.map(packerRegistryEntry -> AssetReference.forAssetId(packerRegistryEntry.assetId()))
.orElseGet(() -> AssetReference.forRelativeAssetRoot(targetRelativeRoot));
@ -558,6 +567,10 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/');
}
private void saveRuntimeCache(PackerProjectContext project, PackerRuntimeSnapshot snapshot) {
cacheRepository.save(project, snapshot.cacheState());
}
private PackerRegistryState removeRegistryEntry(
PackerRegistryState registry,
PackerRegistryEntry entry) {
@ -628,12 +641,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
try {
patchManifestContract(manifest, request);
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
currentSnapshot,
generation,
assetRoot,
manifestPath,
evaluation.resolved().registryEntry()));
saveRuntimeCache(project, runtime.snapshot());
return new UpdateAssetContractResponse(true, null);
} catch (IOException exception) {
return new UpdateAssetContractResponse(false, "Unable to update asset contract: " + exception.getMessage());

View File

@ -42,6 +42,7 @@ public final class PackerAssetDetailsService {
final var manifestPath = runtimeAsset.manifestPath();
final var parsed = runtimeAsset.parsedDeclaration();
diagnostics.addAll(parsed.diagnostics());
diagnostics.addAll(runtimeAsset.walkDiagnostics());
if (!parsed.valid()) {
return failureResult(project, request.assetReference(), resolved, diagnostics);
}

View File

@ -1,15 +1,21 @@
package p.packer.services;
import p.packer.models.*;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
public final class PackerRuntimePatchService {
private final PackerAssetDeclarationParser declarationParser;
private final PackerRuntimeAssetMaterializer assetMaterializer;
public PackerRuntimePatchService(PackerAssetDeclarationParser declarationParser) {
public PackerRuntimePatchService(
PackerAssetDeclarationParser declarationParser,
PackerRuntimeAssetMaterializer assetMaterializer) {
this.declarationParser = Objects.requireNonNull(declarationParser, "declarationParser");
this.assetMaterializer = Objects.requireNonNull(assetMaterializer, "assetMaterializer");
}
public PackerRuntimeSnapshot afterCreateAsset(
@ -23,13 +29,19 @@ public final class PackerRuntimePatchService {
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(snapshot.assets().stream()
.filter(candidate -> !candidate.assetRoot().equals(assetRoot.toAbsolutePath().normalize()))
.toList());
updatedAssets.add(new PackerRuntimeAsset(
final var materialized = assetMaterializer.materialize(
assetRoot,
manifestPath,
Optional.of(entry),
parsed));
parsed,
snapshot.cacheState().findAsset(entry.assetId()));
updatedAssets.add(materialized.runtimeAsset());
updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets);
return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
mergeCacheState(snapshot.cacheState(), updatedRegistry, materialized.assetCacheEntry()));
}
public PackerRuntimeSnapshot afterRegisterAsset(
@ -40,13 +52,17 @@ public final class PackerRuntimePatchService {
Path assetRoot) {
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
boolean patched = false;
Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
for (PackerRuntimeAsset asset : snapshot.assets()) {
if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) {
updatedAssets.add(new PackerRuntimeAsset(
final var materialized = assetMaterializer.materialize(
asset.assetRoot(),
asset.manifestPath(),
Optional.of(entry),
asset.parsedDeclaration()));
asset.parsedDeclaration(),
snapshot.cacheState().findAsset(entry.assetId()));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
patched = true;
} else {
updatedAssets.add(asset);
@ -55,7 +71,11 @@ public final class PackerRuntimePatchService {
if (!patched) {
throw new IllegalStateException("Unable to patch runtime snapshot for unregistered asset: " + assetRoot);
}
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets);
return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
mergeCacheState(snapshot.cacheState(), updatedRegistry, refreshedCacheEntry));
}
public PackerRuntimeSnapshot afterDeleteAsset(
@ -66,7 +86,11 @@ public final class PackerRuntimePatchService {
final List<PackerRuntimeAsset> updatedAssets = snapshot.assets().stream()
.filter(asset -> !asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize()))
.toList();
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets);
return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
pruneCacheState(snapshot.cacheState(), updatedRegistry));
}
public PackerRuntimeSnapshot afterMoveAsset(
@ -80,13 +104,17 @@ public final class PackerRuntimePatchService {
final PackerAssetDeclarationParseResult parsed = declarationParser.parse(targetManifestPath);
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
boolean patched = false;
Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
for (PackerRuntimeAsset asset : snapshot.assets()) {
if (asset.assetRoot().equals(sourceRoot.toAbsolutePath().normalize())) {
updatedAssets.add(new PackerRuntimeAsset(
final var materialized = assetMaterializer.materialize(
targetRoot,
targetManifestPath,
updatedRegistryEntry,
parsed));
parsed,
updatedRegistryEntry.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
patched = true;
} else {
updatedAssets.add(asset);
@ -96,7 +124,11 @@ public final class PackerRuntimePatchService {
throw new IllegalStateException("Unable to patch runtime snapshot for moved asset: " + sourceRoot);
}
updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets);
return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
mergeCacheState(snapshot.cacheState(), updatedRegistry, refreshedCacheEntry));
}
public PackerRuntimeSnapshot afterUpdateAssetContract(
@ -108,26 +140,69 @@ public final class PackerRuntimePatchService {
final PackerAssetDeclarationParseResult parsed = declarationParser.parse(manifestPath);
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
boolean patched = false;
Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
for (PackerRuntimeAsset asset : snapshot.assets()) {
if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) {
updatedAssets.add(new PackerRuntimeAsset(
final var materialized = assetMaterializer.materialize(
asset.assetRoot(),
asset.manifestPath(),
asset.registryEntry(),
parsed));
parsed,
asset.registryEntry().flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
patched = true;
} else {
updatedAssets.add(asset);
}
}
if (!patched) {
updatedAssets.add(new PackerRuntimeAsset(
final var materialized = assetMaterializer.materialize(
assetRoot,
manifestPath,
registryEntry,
parsed));
parsed,
registryEntry.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
}
return new PackerRuntimeSnapshot(generation, snapshot.registry(), updatedAssets);
return new PackerRuntimeSnapshot(
generation,
snapshot.registry(),
updatedAssets,
mergeCacheState(snapshot.cacheState(), snapshot.registry(), refreshedCacheEntry));
}
private PackerWorkspaceCacheState mergeCacheState(
PackerWorkspaceCacheState currentCacheState,
PackerRegistryState registry,
Optional<PackerAssetCacheEntry> refreshedCacheEntry) {
final Map<Integer, PackerAssetCacheEntry> byAssetId = new LinkedHashMap<>();
currentCacheState.assets().forEach(entry -> byAssetId.put(entry.assetId(), entry));
refreshedCacheEntry.ifPresent(entry -> byAssetId.put(entry.assetId(), entry));
return pruneCacheEntries(byAssetId, registry);
}
private PackerWorkspaceCacheState pruneCacheState(
PackerWorkspaceCacheState currentCacheState,
PackerRegistryState registry) {
final Map<Integer, PackerAssetCacheEntry> byAssetId = new LinkedHashMap<>();
currentCacheState.assets().forEach(entry -> byAssetId.put(entry.assetId(), entry));
return pruneCacheEntries(byAssetId, registry);
}
private PackerWorkspaceCacheState pruneCacheEntries(
Map<Integer, PackerAssetCacheEntry> byAssetId,
PackerRegistryState registry) {
final Set<Integer> activeAssetIds = registry.assets().stream()
.map(PackerRegistryEntry::assetId)
.collect(Collectors.toSet());
return new PackerWorkspaceCacheState(
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
byAssetId.values().stream()
.filter(entry -> activeAssetIds.contains(entry.assetId()))
.sorted(Comparator.comparingInt(PackerAssetCacheEntry::assetId))
.toList());
}
}

View File

@ -7,7 +7,9 @@ import p.packer.events.PackerEvent;
import p.packer.events.PackerEventKind;
import p.packer.messages.*;
import p.packer.messages.assets.*;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.repositories.PackerRuntimeSnapshotLoader;
@ -138,7 +140,8 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state());
assertEquals("new_atlas", detailsResult.details().summary().identity().assetName());
assertNotNull(detailsResult.details().summary().identity().assetUuid());
assertTrue(detailsResult.diagnostics().isEmpty());
assertTrue(detailsResult.diagnostics().stream().noneMatch(diagnostic -> diagnostic.blocking()));
assertTrue(detailsResult.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Output metadata for tile bank cannot be empty")));
}
@Test
@ -679,11 +682,14 @@ final class FileSystemPackerWorkspaceServiceTest {
final var mapper = new ObjectMapper();
final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper);
final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var runtimeRegistry = new PackerRuntimeRegistry(loader);
final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup());
final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver);
final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup());
final var runtimePatchService = new p.packer.services.PackerRuntimePatchService(parser);
final var runtimePatchService = new p.packer.services.PackerRuntimePatchService(parser, assetMaterializer);
final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator();
return new FileSystemPackerWorkspaceService(
new ObjectMapper(),
@ -692,6 +698,7 @@ final class FileSystemPackerWorkspaceServiceTest {
actionReadService,
runtimePatchService,
runtimeRegistry,
cacheRepository,
writeCoordinator,
eventSink);
}
@ -701,7 +708,9 @@ final class FileSystemPackerWorkspaceServiceTest {
final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper);
final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
return new CountingLoader(new PackerRuntimeLoader(foundation, parser, assetWalker));
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
return new CountingLoader(new PackerRuntimeLoader(foundation, parser, cacheRepository, assetMaterializer));
}
private static final class CountingLoader implements PackerRuntimeSnapshotLoader {

View File

@ -11,7 +11,9 @@ import p.packer.messages.PackerProjectContext;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.PackerAssetState;
import p.packer.messages.assets.PackerBuildParticipation;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator;
@ -43,7 +45,8 @@ final class PackerAssetDetailsServiceTest {
assertEquals("TILES/indexed_v1", result.details().outputFormat().displayName());
assertEquals(List.of(OutputCodecCatalog.NONE), result.details().availableOutputCodecs());
assertEquals(List.of(), result.details().codecConfigurationFieldsByCodec().get(OutputCodecCatalog.NONE));
assertTrue(result.diagnostics().isEmpty());
assertTrue(result.diagnostics().stream().noneMatch(diagnostic -> diagnostic.blocking()));
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Output metadata for tile bank cannot be empty")));
}
@Test
@ -137,7 +140,13 @@ final class PackerAssetDetailsServiceTest {
final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser, assetWalker));
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(
foundation,
parser,
cacheRepository,
assetMaterializer));
final var resolver = new PackerAssetReferenceResolver(foundation.lookup());
return new PackerAssetDetailsService(runtimeRegistry, resolver);
}

View File

@ -0,0 +1,80 @@
package p.packer.services;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.messages.PackerProjectContext;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.testing.PackerFixtureLocator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
final class PackerRuntimePatchServiceTest {
@TempDir
Path tempDir;
@Test
void afterUpdateAssetContractRefreshesOneAssetProjectionAndCacheState() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed"));
writeTilePng(projectRoot.resolve("assets/ui/atlas/tile.png"));
final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var loader = new PackerRuntimeLoader(foundation, parser, cacheRepository, assetMaterializer);
final var patchService = new PackerRuntimePatchService(parser, assetMaterializer);
final var project = new PackerProjectContext("main", projectRoot);
final var initialSnapshot = loader.load(project, 1L);
final var registryEntry = initialSnapshot.registry().assets().getFirst();
final var updatedSnapshot = patchService.afterUpdateAssetContract(
initialSnapshot,
2L,
projectRoot.resolve("assets/ui/atlas"),
projectRoot.resolve("assets/ui/atlas/asset.json"),
Optional.of(registryEntry));
assertTrue(updatedSnapshot.cacheState().findAsset(registryEntry.assetId()).isPresent());
assertEquals(1, updatedSnapshot.assets().getFirst().walkProjection().buildCandidateFiles().size());
assertEquals("tile.png", updatedSnapshot.assets().getFirst().walkProjection().buildCandidateFiles().getFirst().relativePath());
}
private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
final Path sourceRoot = PackerFixtureLocator.fixtureRoot(relativePath);
try (var stream = Files.walk(sourceRoot)) {
for (Path source : stream.sorted(Comparator.naturalOrder()).toList()) {
final Path target = targetRoot.resolve(sourceRoot.relativize(source).toString());
if (Files.isDirectory(source)) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
Files.copy(source, target);
}
}
}
return targetRoot;
}
private void writeTilePng(Path path) throws Exception {
Files.createDirectories(path.getParent());
final BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < 16; y += 1) {
for (int x = 0; x < 16; x += 1) {
image.setRGB(x, y, 0xFFFF0000);
}
}
ImageIO.write(image, "png", path.toFile());
}
}

View File

@ -3,15 +3,21 @@ package p.packer.services;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.PackerProjectContext;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@ -66,6 +72,24 @@ final class PackerRuntimeRegistryTest {
assertEquals(2, refreshed.snapshot().assets().size());
}
@Test
void runtimeSnapshotRetainsWalkProjectionAndPersistsCacheForRegisteredAssets() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("cache"));
writeTilePng(projectRoot.resolve("assets/ui/atlas/tile.png"));
final PackerRuntimeRegistry registry = runtimeRegistry();
final PackerProjectContext project = project(projectRoot);
final PackerProjectRuntime runtime = registry.getOrLoad(project);
assertTrue(Files.isRegularFile(PackerWorkspacePaths.cachePath(project)));
assertTrue(runtime.snapshot().cacheState().findAsset(1).isPresent());
final var runtimeAsset = runtime.snapshot().assets().getFirst();
assertEquals(List.of("tile.png"), runtimeAsset.walkProjection().availableFiles());
assertEquals(1, runtimeAsset.walkProjection().buildCandidateFiles().size());
assertEquals("tile.png", runtimeAsset.walkProjection().buildCandidateFiles().getFirst().relativePath());
assertTrue(runtimeAsset.walkProjection().measuredBankSizeBytes() > 0L);
}
@Test
void disposeMarksRuntimeInactiveAndRemovesItFromRegistry() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("dispose"));
@ -85,7 +109,13 @@ final class PackerRuntimeRegistryTest {
final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
return new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser, assetWalker));
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
return new PackerRuntimeRegistry(new PackerRuntimeLoader(
foundation,
parser,
cacheRepository,
assetMaterializer));
}
private PackerProjectContext project(Path root) {
@ -107,4 +137,15 @@ final class PackerRuntimeRegistryTest {
}
return targetRoot;
}
private void writeTilePng(Path path) throws Exception {
Files.createDirectories(path.getParent());
final BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < 16; y += 1) {
for (int x = 0; x < 16; x += 1) {
image.setRGB(x, y, 0xFFFF0000);
}
}
ImageIO.write(image, "png", path.toFile());
}
}