implements packer PR-09 event lane and studio adapter

This commit is contained in:
bQUARKz 2026-03-11 18:06:47 +00:00
parent 682a0e72b5
commit 1c418a454b
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
16 changed files with 393 additions and 14 deletions

View File

@ -6,6 +6,10 @@ import p.packer.api.building.PackerBuildRequest;
import p.packer.api.building.PackerBuildResult; import p.packer.api.building.PackerBuildResult;
import p.packer.api.building.PackerBuildService; import p.packer.api.building.PackerBuildService;
import p.packer.api.diagnostics.PackerDiagnostic; import p.packer.api.diagnostics.PackerDiagnostic;
import p.packer.api.events.PackerEventKind;
import p.packer.api.events.PackerEventSink;
import p.packer.api.events.PackerProgress;
import p.packer.events.PackerOperationEventEmitter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -27,13 +31,19 @@ public final class FileSystemPackerBuildService implements PackerBuildService {
private static final int PRELUDE_SIZE = 24; private static final int PRELUDE_SIZE = 24;
private final PackerBuildPlanner buildPlanner; private final PackerBuildPlanner buildPlanner;
private final PackerEventSink eventSink;
public FileSystemPackerBuildService() { public FileSystemPackerBuildService() {
this(new PackerBuildPlanner()); this(new PackerBuildPlanner(), PackerEventSink.noop());
} }
public FileSystemPackerBuildService(PackerBuildPlanner buildPlanner) { public FileSystemPackerBuildService(PackerBuildPlanner buildPlanner) {
this(buildPlanner, PackerEventSink.noop());
}
public FileSystemPackerBuildService(PackerBuildPlanner buildPlanner, PackerEventSink eventSink) {
this.buildPlanner = Objects.requireNonNull(buildPlanner, "buildPlanner"); this.buildPlanner = Objects.requireNonNull(buildPlanner, "buildPlanner");
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
} }
@Override @Override
@ -43,14 +53,17 @@ public final class FileSystemPackerBuildService implements PackerBuildService {
@Override @Override
public PackerBuildResult build(PackerBuildRequest request) { public PackerBuildResult build(PackerBuildRequest request) {
final Path buildDirectory = Objects.requireNonNull(request, "request").project().rootPath().resolve("build"); final PackerBuildRequest buildRequest = Objects.requireNonNull(request, "request");
final PackerOperationEventEmitter events = new PackerOperationEventEmitter(buildRequest.project(), eventSink);
final Path buildDirectory = buildRequest.project().rootPath().resolve("build");
final Path assetsArchive = buildDirectory.resolve("assets.pa").toAbsolutePath().normalize(); final Path assetsArchive = buildDirectory.resolve("assets.pa").toAbsolutePath().normalize();
final Path assetTableJson = buildDirectory.resolve("asset_table.json").toAbsolutePath().normalize(); final Path assetTableJson = buildDirectory.resolve("asset_table.json").toAbsolutePath().normalize();
final Path preloadJson = buildDirectory.resolve("preload.json").toAbsolutePath().normalize(); final Path preloadJson = buildDirectory.resolve("preload.json").toAbsolutePath().normalize();
final Path metadataJson = buildDirectory.resolve("asset_table_metadata.json").toAbsolutePath().normalize(); final Path metadataJson = buildDirectory.resolve("asset_table_metadata.json").toAbsolutePath().normalize();
events.emit(PackerEventKind.BUILD_STARTED, "Build started.", new PackerProgress(0.0d, false), List.of());
final PackerBuildPlanResult planResult = buildPlanner.plan(request.project()); final PackerBuildPlanResult planResult = buildPlanner.plan(buildRequest.project());
if (planResult.plan() == null) { if (planResult.plan() == null) {
events.emit(PackerEventKind.BUILD_FINISHED, planResult.summary(), new PackerProgress(1.0d, false), List.of());
return new PackerBuildResult( return new PackerBuildResult(
PackerOperationStatus.FAILED, PackerOperationStatus.FAILED,
planResult.summary(), planResult.summary(),
@ -61,11 +74,18 @@ public final class FileSystemPackerBuildService implements PackerBuildService {
try { try {
Files.createDirectories(buildDirectory); Files.createDirectories(buildDirectory);
final String previousCacheKey = loadPreviousCacheKey(metadataJson);
events.emit(
previousCacheKey != null && previousCacheKey.equals(planResult.plan().cacheKey()) ? PackerEventKind.CACHE_HIT : PackerEventKind.CACHE_MISS,
previousCacheKey != null && previousCacheKey.equals(planResult.plan().cacheKey()) ? "Build cache hit." : "Build cache miss.",
List.of());
final EmittedArchive archive = emitArchive(planResult.plan()); final EmittedArchive archive = emitArchive(planResult.plan());
events.emit(PackerEventKind.PROGRESS_UPDATED, "Build archive prepared.", new PackerProgress(0.5d, false), List.of());
Files.write(assetsArchive, archive.bytes()); Files.write(assetsArchive, archive.bytes());
Files.writeString(assetTableJson, archive.assetTableJson(), StandardCharsets.UTF_8); Files.writeString(assetTableJson, archive.assetTableJson(), StandardCharsets.UTF_8);
Files.writeString(preloadJson, archive.preloadJson(), StandardCharsets.UTF_8); Files.writeString(preloadJson, archive.preloadJson(), StandardCharsets.UTF_8);
Files.writeString(metadataJson, archive.metadataJson(), StandardCharsets.UTF_8); Files.writeString(metadataJson, archive.metadataJson(), StandardCharsets.UTF_8);
events.emit(PackerEventKind.BUILD_FINISHED, "Build finished.", new PackerProgress(1.0d, false), List.of());
return new PackerBuildResult( return new PackerBuildResult(
planResult.status(), planResult.status(),
"Build emitted " + planResult.plan().assets().size() + " assets.", "Build emitted " + planResult.plan().assets().size() + " assets.",
@ -149,6 +169,20 @@ public final class FileSystemPackerBuildService implements PackerBuildService {
return new EmittedArchive(archiveBytes, assetTableJson, preloadJson, metadataJson); return new EmittedArchive(archiveBytes, assetTableJson, preloadJson, metadataJson);
} }
@SuppressWarnings("unchecked")
private String loadPreviousCacheKey(Path metadataJson) {
if (!Files.isRegularFile(metadataJson)) {
return null;
}
try {
return ((Map<String, Object>) new com.fasterxml.jackson.databind.ObjectMapper().readValue(Files.readString(metadataJson), Map.class))
.getOrDefault("cache_key", "")
.toString();
} catch (IOException exception) {
return null;
}
}
private record EmittedArchive( private record EmittedArchive(
byte[] bytes, byte[] bytes,
String assetTableJson, String assetTableJson,

View File

@ -11,9 +11,13 @@ import p.packer.api.doctor.PackerDoctorMode;
import p.packer.api.doctor.PackerDoctorRequest; import p.packer.api.doctor.PackerDoctorRequest;
import p.packer.api.doctor.PackerDoctorResult; import p.packer.api.doctor.PackerDoctorResult;
import p.packer.api.doctor.PackerDoctorService; import p.packer.api.doctor.PackerDoctorService;
import p.packer.api.events.PackerEventKind;
import p.packer.api.events.PackerEventSink;
import p.packer.api.events.PackerProgress;
import p.packer.api.workspace.GetAssetDetailsRequest; import p.packer.api.workspace.GetAssetDetailsRequest;
import p.packer.api.workspace.ListAssetsRequest; import p.packer.api.workspace.ListAssetsRequest;
import p.packer.declarations.PackerAssetDetailsService; import p.packer.declarations.PackerAssetDetailsService;
import p.packer.events.PackerOperationEventEmitter;
import p.packer.workspace.FileSystemPackerWorkspaceService; import p.packer.workspace.FileSystemPackerWorkspaceService;
import java.nio.file.Files; import java.nio.file.Files;
@ -27,16 +31,25 @@ import java.util.Set;
public final class FileSystemPackerDoctorService implements PackerDoctorService { public final class FileSystemPackerDoctorService implements PackerDoctorService {
private final FileSystemPackerWorkspaceService workspaceService; private final FileSystemPackerWorkspaceService workspaceService;
private final PackerAssetDetailsService detailsService; private final PackerAssetDetailsService detailsService;
private final PackerEventSink eventSink;
public FileSystemPackerDoctorService() { public FileSystemPackerDoctorService() {
this(new FileSystemPackerWorkspaceService(), new PackerAssetDetailsService()); this(new FileSystemPackerWorkspaceService(), new PackerAssetDetailsService(), PackerEventSink.noop());
} }
public FileSystemPackerDoctorService( public FileSystemPackerDoctorService(
FileSystemPackerWorkspaceService workspaceService, FileSystemPackerWorkspaceService workspaceService,
PackerAssetDetailsService detailsService) { PackerAssetDetailsService detailsService) {
this(workspaceService, detailsService, PackerEventSink.noop());
}
public FileSystemPackerDoctorService(
FileSystemPackerWorkspaceService workspaceService,
PackerAssetDetailsService detailsService,
PackerEventSink eventSink) {
this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService"); this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService");
this.detailsService = Objects.requireNonNull(detailsService, "detailsService"); this.detailsService = Objects.requireNonNull(detailsService, "detailsService");
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
} }
@Override @Override
@ -48,6 +61,7 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
public PackerDoctorResult doctor(PackerDoctorRequest request) { public PackerDoctorResult doctor(PackerDoctorRequest request) {
final PackerDoctorRequest doctorRequest = Objects.requireNonNull(request, "request"); final PackerDoctorRequest doctorRequest = Objects.requireNonNull(request, "request");
final PackerProjectContext project = doctorRequest.project(); final PackerProjectContext project = doctorRequest.project();
final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
final var snapshot = workspaceService.listAssets(new ListAssetsRequest(project)); final var snapshot = workspaceService.listAssets(new ListAssetsRequest(project));
final List<PackerDiagnostic> diagnostics = new ArrayList<>(); final List<PackerDiagnostic> diagnostics = new ArrayList<>();
final Set<String> safeFixes = new LinkedHashSet<>(); final Set<String> safeFixes = new LinkedHashSet<>();
@ -57,10 +71,13 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
addDiagnostic(diagnostics, seenDiagnostics, diagnostic); addDiagnostic(diagnostics, seenDiagnostics, diagnostic);
} }
final int totalAssets = snapshot.assets().size();
int inspected = 0;
for (var asset : snapshot.assets()) { for (var asset : snapshot.assets()) {
if (!includeAsset(doctorRequest.mode(), asset.state())) { if (!includeAsset(doctorRequest.mode(), asset.state())) {
continue; continue;
} }
inspected += 1;
final String assetReference = asset.identity().assetId() == null final String assetReference = asset.identity().assetId() == null
? relativeAssetRoot(project, asset.identity().assetRoot()) ? relativeAssetRoot(project, asset.identity().assetRoot())
: Integer.toString(asset.identity().assetId()); : Integer.toString(asset.identity().assetId());
@ -102,6 +119,11 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
input, input,
managed)); managed));
})); }));
events.emit(
PackerEventKind.PROGRESS_UPDATED,
"Doctor inspected asset: " + asset.identity().assetName(),
new PackerProgress(totalAssets == 0 ? 1.0d : inspected / (double) totalAssets, false),
List.of(asset.identity().assetName()));
} }
final long blockingCount = diagnostics.stream().filter(PackerDiagnostic::blocking).count(); final long blockingCount = diagnostics.stream().filter(PackerDiagnostic::blocking).count();
@ -113,6 +135,7 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
: diagnostics.isEmpty() : diagnostics.isEmpty()
? "Doctor found no diagnostics." ? "Doctor found no diagnostics."
: "Doctor found " + diagnostics.size() + " diagnostics with no blockers."; : "Doctor found " + diagnostics.size() + " diagnostics with no blockers.";
events.emit(PackerEventKind.DIAGNOSTICS_UPDATED, summary, List.of());
return new PackerDoctorResult(status, summary, diagnostics, List.copyOf(safeFixes)); return new PackerDoctorResult(status, summary, diagnostics, List.copyOf(safeFixes));
} }

View File

@ -0,0 +1,49 @@
package p.packer.events;
import p.packer.api.PackerProjectContext;
import p.packer.api.events.PackerEvent;
import p.packer.api.events.PackerEventKind;
import p.packer.api.events.PackerEventSink;
import p.packer.api.events.PackerProgress;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public final class PackerOperationEventEmitter {
private final PackerProjectContext project;
private final PackerEventSink sink;
private final String operationId;
private long sequence;
public PackerOperationEventEmitter(PackerProjectContext project, PackerEventSink sink) {
this(project, sink, UUID.randomUUID().toString());
}
public PackerOperationEventEmitter(PackerProjectContext project, PackerEventSink sink, String operationId) {
this.project = Objects.requireNonNull(project, "project");
this.sink = Objects.requireNonNull(sink, "sink");
this.operationId = Objects.requireNonNull(operationId, "operationId").trim();
}
public String operationId() {
return operationId;
}
public void emit(PackerEventKind kind, String summary, List<String> affectedAssets) {
emit(kind, summary, null, affectedAssets);
}
public void emit(PackerEventKind kind, String summary, PackerProgress progress, List<String> affectedAssets) {
sink.publish(new PackerEvent(
project.projectId(),
operationId,
sequence++,
Objects.requireNonNull(kind, "kind"),
Instant.now(),
Objects.requireNonNull(summary, "summary"),
progress,
affectedAssets == null ? List.of() : List.copyOf(affectedAssets)));
}
}

View File

@ -81,10 +81,11 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
throw new PackerMutationException("Cannot apply mutation preview with blockers"); throw new PackerMutationException("Cannot apply mutation preview with blockers");
} }
final PackerMutationResult result = writeCoordinator.withWriteLock(project, () -> applyLocked(preview)); final PackerMutationResult result = writeCoordinator.withWriteLock(project, () -> applyLocked(preview));
emit(project, result.operationId(), 1L, PackerEventKind.ACTION_APPLIED, result.summary(), affectedAssets(preview)); emit(project, result.operationId(), 1L, PackerEventKind.ASSET_CHANGED, "Asset state changed.", affectedAssets(preview));
emit(project, result.operationId(), 2L, PackerEventKind.ACTION_APPLIED, result.summary(), affectedAssets(preview));
return result; return result;
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
emit(project, preview.operationId(), 1L, PackerEventKind.ACTION_FAILED, rootCauseMessage(exception), affectedAssets(preview)); emit(project, preview.operationId(), 2L, PackerEventKind.ACTION_FAILED, rootCauseMessage(exception), affectedAssets(preview));
throw exception; throw exception;
} }
} }

View File

@ -9,6 +9,9 @@ import p.packer.api.assets.PackerAssetSummary;
import p.packer.api.diagnostics.PackerDiagnostic; import p.packer.api.diagnostics.PackerDiagnostic;
import p.packer.api.diagnostics.PackerDiagnosticCategory; import p.packer.api.diagnostics.PackerDiagnosticCategory;
import p.packer.api.diagnostics.PackerDiagnosticSeverity; import p.packer.api.diagnostics.PackerDiagnosticSeverity;
import p.packer.api.events.PackerEventKind;
import p.packer.api.events.PackerEventSink;
import p.packer.api.events.PackerProgress;
import p.packer.api.workspace.GetAssetDetailsRequest; import p.packer.api.workspace.GetAssetDetailsRequest;
import p.packer.api.workspace.GetAssetDetailsResult; import p.packer.api.workspace.GetAssetDetailsResult;
import p.packer.api.workspace.InitWorkspaceRequest; import p.packer.api.workspace.InitWorkspaceRequest;
@ -23,6 +26,7 @@ import p.packer.foundation.PackerRegistryEntry;
import p.packer.foundation.PackerRegistryState; import p.packer.foundation.PackerRegistryState;
import p.packer.foundation.PackerWorkspaceFoundation; import p.packer.foundation.PackerWorkspaceFoundation;
import p.packer.foundation.PackerWorkspacePaths; import p.packer.foundation.PackerWorkspacePaths;
import p.packer.events.PackerOperationEventEmitter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -34,17 +38,26 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
private final PackerWorkspaceFoundation workspaceFoundation; private final PackerWorkspaceFoundation workspaceFoundation;
private final PackerAssetDeclarationParser parser; private final PackerAssetDeclarationParser parser;
private final PackerAssetDetailsService detailsService; private final PackerAssetDetailsService detailsService;
private final PackerEventSink eventSink;
public FileSystemPackerWorkspaceService() { public FileSystemPackerWorkspaceService() {
this(new PackerWorkspaceFoundation(), new PackerAssetDeclarationParser()); this(new PackerWorkspaceFoundation(), new PackerAssetDeclarationParser(), PackerEventSink.noop());
} }
public FileSystemPackerWorkspaceService( public FileSystemPackerWorkspaceService(
PackerWorkspaceFoundation workspaceFoundation, PackerWorkspaceFoundation workspaceFoundation,
PackerAssetDeclarationParser parser) { PackerAssetDeclarationParser parser) {
this(workspaceFoundation, parser, PackerEventSink.noop());
}
public FileSystemPackerWorkspaceService(
PackerWorkspaceFoundation workspaceFoundation,
PackerAssetDeclarationParser parser,
PackerEventSink eventSink) {
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation"); this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
this.parser = Objects.requireNonNull(parser, "parser"); this.parser = Objects.requireNonNull(parser, "parser");
this.detailsService = new PackerAssetDetailsService(workspaceFoundation, parser); this.detailsService = new PackerAssetDetailsService(workspaceFoundation, parser);
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
} }
@Override @Override
@ -60,6 +73,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
@Override @Override
public ListAssetsResult listAssets(ListAssetsRequest request) { public ListAssetsResult listAssets(ListAssetsRequest request) {
final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
final Path assetsRoot = PackerWorkspacePaths.assetsRoot(project); final Path assetsRoot = PackerWorkspacePaths.assetsRoot(project);
final PackerRegistryState registry = workspaceFoundation.loadRegistry(project); final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
final Map<Path, PackerRegistryEntry> registryByRoot = new HashMap<>(); final Map<Path, PackerRegistryEntry> registryByRoot = new HashMap<>();
@ -74,14 +88,23 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
if (Files.isDirectory(assetsRoot)) { if (Files.isDirectory(assetsRoot)) {
try (Stream<Path> paths = Files.find(assetsRoot, Integer.MAX_VALUE, (path, attrs) -> try (Stream<Path> paths = Files.find(assetsRoot, Integer.MAX_VALUE, (path, attrs) ->
attrs.isRegularFile() && path.getFileName().toString().equals("asset.json"))) { attrs.isRegularFile() && path.getFileName().toString().equals("asset.json"))) {
paths.forEach(assetManifestPath -> { final List<Path> manifests = paths.toList();
final int total = manifests.size();
for (int index = 0; index < manifests.size(); index += 1) {
final Path assetManifestPath = manifests.get(index);
final Path assetRoot = assetManifestPath.getParent().toAbsolutePath().normalize(); final Path assetRoot = assetManifestPath.getParent().toAbsolutePath().normalize();
discoveredRoots.add(assetRoot); discoveredRoots.add(assetRoot);
final PackerRegistryEntry registryEntry = registryByRoot.get(assetRoot); final PackerRegistryEntry registryEntry = registryByRoot.get(assetRoot);
final PackerAssetDeclarationParseResult parsed = parser.parse(assetManifestPath); final PackerAssetDeclarationParseResult parsed = parser.parse(assetManifestPath);
diagnostics.addAll(parsed.diagnostics()); diagnostics.addAll(parsed.diagnostics());
assets.add(buildSummary(assetRoot, registryEntry, parsed)); final PackerAssetSummary summary = buildSummary(assetRoot, registryEntry, parsed);
}); assets.add(summary);
events.emit(
PackerEventKind.ASSET_DISCOVERED,
"Discovered asset: " + summary.identity().assetName(),
new PackerProgress(total == 0 ? 1.0d : (index + 1) / (double) total, false),
List.of(summary.identity().assetName()));
}
} catch (IOException exception) { } catch (IOException exception) {
diagnostics.add(new PackerDiagnostic( diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR, PackerDiagnosticSeverity.ERROR,
@ -110,6 +133,9 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
final PackerOperationStatus status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking) final PackerOperationStatus status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking)
? PackerOperationStatus.PARTIAL ? PackerOperationStatus.PARTIAL
: PackerOperationStatus.SUCCESS; : PackerOperationStatus.SUCCESS;
if (!diagnostics.isEmpty()) {
events.emit(PackerEventKind.DIAGNOSTICS_UPDATED, "Asset scan diagnostics updated.", List.of());
}
return new ListAssetsResult( return new ListAssetsResult(
status, status,
"Packer asset snapshot ready.", "Packer asset snapshot ready.",

View File

@ -6,6 +6,8 @@ import org.junit.jupiter.api.io.TempDir;
import p.packer.api.PackerOperationStatus; import p.packer.api.PackerOperationStatus;
import p.packer.api.PackerProjectContext; import p.packer.api.PackerProjectContext;
import p.packer.api.building.PackerBuildRequest; import p.packer.api.building.PackerBuildRequest;
import p.packer.api.events.PackerEvent;
import p.packer.api.events.PackerEventKind;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
@ -14,6 +16,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -72,6 +75,21 @@ final class FileSystemPackerBuildServiceTest {
assertTrue(result.companionArtifacts().isEmpty()); assertTrue(result.companionArtifacts().isEmpty());
} }
@Test
void emitsBuildLifecycleAndCacheEvents() throws Exception {
final Path projectRoot = createProject(tempDir.resolve("events"));
final List<PackerEvent> events = new CopyOnWriteArrayList<>();
final FileSystemPackerBuildService service = new FileSystemPackerBuildService(new PackerBuildPlanner(), events::add);
service.build(new PackerBuildRequest(new PackerProjectContext("events", projectRoot), false));
service.build(new PackerBuildRequest(new PackerProjectContext("events", projectRoot), false));
assertEquals(PackerEventKind.BUILD_STARTED, events.getFirst().kind());
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.CACHE_MISS));
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.CACHE_HIT));
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.BUILD_FINISHED));
}
private ParsedArchive parseArchive(Path archivePath) throws Exception { private ParsedArchive parseArchive(Path archivePath) throws Exception {
final byte[] bytes = Files.readAllBytes(archivePath); final byte[] bytes = Files.readAllBytes(archivePath);
final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);

View File

@ -7,9 +7,13 @@ import p.packer.api.PackerProjectContext;
import p.packer.api.diagnostics.PackerDiagnosticCategory; import p.packer.api.diagnostics.PackerDiagnosticCategory;
import p.packer.api.doctor.PackerDoctorMode; import p.packer.api.doctor.PackerDoctorMode;
import p.packer.api.doctor.PackerDoctorRequest; import p.packer.api.doctor.PackerDoctorRequest;
import p.packer.api.events.PackerEvent;
import p.packer.api.events.PackerEventKind;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -81,6 +85,24 @@ final class FileSystemPackerDoctorServiceTest {
assertTrue(result.safeFixes().isEmpty()); assertTrue(result.safeFixes().isEmpty());
} }
@Test
void emitsDiagnosticsLifecycleEvents() throws Exception {
final Path projectRoot = createManagedProjectWithMissingInput();
final List<PackerEvent> events = new CopyOnWriteArrayList<>();
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService(
new p.packer.workspace.FileSystemPackerWorkspaceService(),
new p.packer.declarations.PackerAssetDetailsService(),
events::add);
service.doctor(new PackerDoctorRequest(
new PackerProjectContext("main", projectRoot),
PackerDoctorMode.MANAGED_WORLD,
false));
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.PROGRESS_UPDATED));
assertEquals(PackerEventKind.DIAGNOSTICS_UPDATED, events.getLast().kind());
}
private Path createManagedProjectWithMissingInput() throws Exception { private Path createManagedProjectWithMissingInput() throws Exception {
final Path projectRoot = tempDir.resolve("managed-missing-input"); final Path projectRoot = tempDir.resolve("managed-missing-input");
final Path assetRoot = projectRoot.resolve("assets/ui/atlas"); final Path assetRoot = projectRoot.resolve("assets/ui/atlas");

View File

@ -50,9 +50,10 @@ final class FileSystemPackerMutationServiceTest {
assertTrue(Files.isDirectory(preview.targetAssetRoot())); assertTrue(Files.isDirectory(preview.targetAssetRoot()));
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json")); final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
assertFalse(registryJson.contains("\"root\" : \"ui/atlas\"")); assertFalse(registryJson.contains("\"root\" : \"ui/atlas\""));
assertEquals(List.of(PackerEventKind.PREVIEW_READY, PackerEventKind.ACTION_APPLIED), events.stream().map(PackerEvent::kind).toList()); assertEquals(List.of(PackerEventKind.PREVIEW_READY, PackerEventKind.ASSET_CHANGED, PackerEventKind.ACTION_APPLIED), events.stream().map(PackerEvent::kind).toList());
assertEquals(preview.operationId(), events.getFirst().operationId()); assertEquals(preview.operationId(), events.getFirst().operationId());
assertEquals(preview.operationId(), events.get(1).operationId()); assertEquals(preview.operationId(), events.get(1).operationId());
assertEquals(preview.operationId(), events.get(2).operationId());
} }
@Test @Test

View File

@ -5,12 +5,16 @@ import org.junit.jupiter.api.io.TempDir;
import p.packer.api.PackerOperationStatus; import p.packer.api.PackerOperationStatus;
import p.packer.api.PackerProjectContext; import p.packer.api.PackerProjectContext;
import p.packer.api.assets.PackerAssetState; import p.packer.api.assets.PackerAssetState;
import p.packer.api.events.PackerEvent;
import p.packer.api.events.PackerEventKind;
import p.packer.api.workspace.ListAssetsRequest; import p.packer.api.workspace.ListAssetsRequest;
import p.packer.testing.PackerFixtureLocator; import p.packer.testing.PackerFixtureLocator;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -54,6 +58,22 @@ final class FileSystemPackerWorkspaceServiceTest {
assertTrue(result.assets().getFirst().hasDiagnostics()); assertTrue(result.assets().getFirst().hasDiagnostics());
} }
@Test
void emitsDiscoveryAndDiagnosticsEventsDuringScan() throws Exception {
final Path projectRoot = copyFixture("workspaces/read-invalid", tempDir.resolve("events"));
final List<PackerEvent> events = new CopyOnWriteArrayList<>();
final FileSystemPackerWorkspaceService service = new FileSystemPackerWorkspaceService(
new p.packer.foundation.PackerWorkspaceFoundation(),
new p.packer.declarations.PackerAssetDeclarationParser(),
events::add);
service.listAssets(new ListAssetsRequest(project(projectRoot)));
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ASSET_DISCOVERED));
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.DIAGNOSTICS_UPDATED));
assertTrue(events.stream().allMatch(event -> event.sequence() >= 0L));
}
private PackerProjectContext project(Path root) { private PackerProjectContext project(Path root) {
return new PackerProjectContext("main", root); return new PackerProjectContext("main", root);
} }

View File

@ -30,7 +30,18 @@ public final class StudioActivityEventMapper {
Optional.of(new StudioActivityEntry("Assets", "Action applied: " + applied.action().name().toLowerCase(), StudioActivityEntrySeverity.SUCCESS, false)); Optional.of(new StudioActivityEntry("Assets", "Action applied: " + applied.action().name().toLowerCase(), StudioActivityEntrySeverity.SUCCESS, false));
case StudioAssetsMutationFailedEvent failed -> case StudioAssetsMutationFailedEvent failed ->
Optional.of(new StudioActivityEntry("Assets", failed.message(), StudioActivityEntrySeverity.ERROR, true)); Optional.of(new StudioActivityEntry("Assets", failed.message(), StudioActivityEntrySeverity.ERROR, true));
case StudioPackerOperationEvent packerEvent ->
Optional.of(new StudioActivityEntry("Assets", packerEvent.summary(), severity(packerEvent), packerEvent.kind() == p.packer.api.events.PackerEventKind.ACTION_FAILED));
default -> Optional.empty(); default -> Optional.empty();
}; };
} }
private static StudioActivityEntrySeverity severity(StudioPackerOperationEvent event) {
return switch (event.kind()) {
case BUILD_FINISHED, CACHE_HIT -> StudioActivityEntrySeverity.SUCCESS;
case DIAGNOSTICS_UPDATED, CACHE_MISS, BUILD_STARTED, ASSET_DISCOVERED, ASSET_CHANGED, PROGRESS_UPDATED -> StudioActivityEntrySeverity.INFO;
case ACTION_FAILED -> StudioActivityEntrySeverity.ERROR;
default -> StudioActivityEntrySeverity.INFO;
};
}
} }

View File

@ -59,6 +59,7 @@ public final class StudioActivityFeedControl extends VBox implements StudioContr
subscriptions.add(eventBus.subscribe(StudioAssetsMutationPreviewReadyEvent.class, this::onEvent)); subscriptions.add(eventBus.subscribe(StudioAssetsMutationPreviewReadyEvent.class, this::onEvent));
subscriptions.add(eventBus.subscribe(StudioAssetsMutationAppliedEvent.class, this::onEvent)); subscriptions.add(eventBus.subscribe(StudioAssetsMutationAppliedEvent.class, this::onEvent));
subscriptions.add(eventBus.subscribe(StudioAssetsMutationFailedEvent.class, this::onEvent)); subscriptions.add(eventBus.subscribe(StudioAssetsMutationFailedEvent.class, this::onEvent));
subscriptions.add(eventBus.subscribe(StudioPackerOperationEvent.class, this::onPackerOperation));
} }
@Override @Override
@ -114,6 +115,23 @@ public final class StudioActivityFeedControl extends VBox implements StudioContr
clearProgress(); clearProgress();
} }
private void onPackerOperation(StudioPackerOperationEvent event) {
onEvent(event);
if (event.progress() != null) {
Platform.runLater(() -> {
progressLabel.setText(event.summary());
progressBar.setVisible(true);
progressBar.setManaged(true);
progressBar.setProgress(event.indeterminate() ? ProgressBar.INDETERMINATE_PROGRESS : event.progress());
});
if (event.kind() == p.packer.api.events.PackerEventKind.BUILD_FINISHED) {
clearProgress();
}
} else if (event.kind() == p.packer.api.events.PackerEventKind.BUILD_FINISHED) {
clearProgress();
}
}
private void clearProgress() { private void clearProgress() {
Platform.runLater(() -> { Platform.runLater(() -> {
progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE)); progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE));

View File

@ -0,0 +1,39 @@
package p.studio.events;
import p.packer.api.events.PackerEvent;
import p.packer.api.events.PackerEventKind;
import p.packer.api.events.PackerEventSink;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public final class StudioPackerEventAdapter implements PackerEventSink {
private final StudioWorkspaceEventBus eventBus;
private final ProjectReference projectReference;
public StudioPackerEventAdapter(StudioWorkspaceEventBus eventBus, ProjectReference projectReference) {
this.eventBus = Objects.requireNonNull(eventBus, "eventBus");
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
}
@Override
public void publish(PackerEvent event) {
final PackerEvent packerEvent = Objects.requireNonNull(event, "event");
if (ignore(packerEvent.kind())) {
return;
}
eventBus.publish(new StudioPackerOperationEvent(
projectReference,
packerEvent.operationId(),
packerEvent.kind(),
packerEvent.summary(),
packerEvent.progress() == null ? null : packerEvent.progress().value(),
packerEvent.progress() == null || packerEvent.progress().indeterminate()));
}
private boolean ignore(PackerEventKind kind) {
return kind == PackerEventKind.PREVIEW_READY
|| kind == PackerEventKind.ACTION_APPLIED
|| kind == PackerEventKind.ACTION_FAILED;
}
}

View File

@ -0,0 +1,28 @@
package p.studio.events;
import p.packer.api.events.PackerEventKind;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioPackerOperationEvent(
ProjectReference project,
String operationId,
PackerEventKind kind,
String summary,
Double progress,
boolean indeterminate) implements StudioEvent {
public StudioPackerOperationEvent {
Objects.requireNonNull(project, "project");
operationId = Objects.requireNonNull(operationId, "operationId").trim();
Objects.requireNonNull(kind, "kind");
summary = Objects.requireNonNull(summary, "summary").trim();
if (operationId.isBlank() || summary.isBlank()) {
throw new IllegalArgumentException("operationId and summary must not be blank");
}
if (progress != null && !indeterminate && (progress < 0.0d || progress > 1.0d)) {
throw new IllegalArgumentException("progress must be between 0.0 and 1.0 when determinate");
}
}
}

View File

@ -15,6 +15,9 @@ import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n; import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.Workspace; import p.studio.workspaces.Workspace;
import p.studio.workspaces.WorkspaceId; import p.studio.workspaces.WorkspaceId;
import p.packer.declarations.PackerAssetDeclarationParser;
import p.packer.foundation.PackerWorkspaceFoundation;
import p.packer.workspace.FileSystemPackerWorkspaceService;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -58,7 +61,7 @@ public final class AssetWorkspace implements Workspace {
public AssetWorkspace(ProjectReference projectReference) { public AssetWorkspace(ProjectReference projectReference) {
this( this(
projectReference, projectReference,
new PackerBackedAssetWorkspaceService(), null,
defaultWorkspaceBus(), defaultWorkspaceBus(),
null); null);
} }
@ -76,8 +79,13 @@ public final class AssetWorkspace implements Workspace {
StudioWorkspaceEventBus workspaceBus, StudioWorkspaceEventBus workspaceBus,
AssetWorkspaceMutationService mutationService) { AssetWorkspaceMutationService mutationService) {
this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
this.assetWorkspaceService = Objects.requireNonNull(assetWorkspaceService, "assetWorkspaceService");
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus"); this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
this.assetWorkspaceService = assetWorkspaceService == null
? new PackerBackedAssetWorkspaceService(new FileSystemPackerWorkspaceService(
new PackerWorkspaceFoundation(),
new PackerAssetDeclarationParser(),
new StudioPackerEventAdapter(this.workspaceBus, this.projectReference)))
: Objects.requireNonNull(assetWorkspaceService, "assetWorkspaceService");
this.mutationService = mutationService == null this.mutationService = mutationService == null
? new PackerBackedAssetWorkspaceMutationService(this.workspaceBus) ? new PackerBackedAssetWorkspaceMutationService(this.workspaceBus)
: Objects.requireNonNull(mutationService, "mutationService"); : Objects.requireNonNull(mutationService, "mutationService");

View File

@ -1,9 +1,11 @@
package p.studio.controls.shell; package p.studio.controls.shell;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import p.packer.api.events.PackerEventKind;
import p.studio.events.StudioAssetsMutationAppliedEvent; import p.studio.events.StudioAssetsMutationAppliedEvent;
import p.studio.events.StudioAssetsMutationFailedEvent; import p.studio.events.StudioAssetsMutationFailedEvent;
import p.studio.events.StudioAssetsMutationPreviewReadyEvent; import p.studio.events.StudioAssetsMutationPreviewReadyEvent;
import p.studio.events.StudioPackerOperationEvent;
import p.studio.events.StudioAssetsWorkspaceRefreshFailedEvent; import p.studio.events.StudioAssetsWorkspaceRefreshFailedEvent;
import p.studio.events.StudioAssetsWorkspaceRefreshedEvent; import p.studio.events.StudioAssetsWorkspaceRefreshedEvent;
import p.studio.events.StudioProjectOpenedEvent; import p.studio.events.StudioProjectOpenedEvent;
@ -83,6 +85,17 @@ final class StudioActivityEventMapperTest {
assertEquals("Apply failed", entry.message()); assertEquals("Apply failed", entry.message());
} }
@Test
void mapsPackerBuildEventToActivityEntry() {
final StudioActivityEntry entry = StudioActivityEventMapper
.map(new StudioPackerOperationEvent(project(), "op-1", PackerEventKind.BUILD_FINISHED, "Build finished.", 1.0d, false))
.orElseThrow();
assertEquals("Assets", entry.source());
assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity());
assertEquals("Build finished.", entry.message());
}
private ProjectReference project() { private ProjectReference project() {
return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main")); return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main"));
} }

View File

@ -0,0 +1,68 @@
package p.studio.events;
import org.junit.jupiter.api.Test;
import p.packer.api.events.PackerEvent;
import p.packer.api.events.PackerEventKind;
import p.packer.api.events.PackerProgress;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.WorkspaceId;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class StudioPackerEventAdapterTest {
@Test
void forwardsBuildEventsIntoStudioOperationEvents() {
final StudioEventBus globalBus = new StudioEventBus();
final List<StudioPackerOperationEvent> events = new ArrayList<>();
globalBus.subscribe(StudioPackerOperationEvent.class, events::add);
final StudioPackerEventAdapter adapter = new StudioPackerEventAdapter(
new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus),
project());
adapter.publish(new PackerEvent(
"main",
"op-1",
0L,
PackerEventKind.BUILD_STARTED,
Instant.now(),
"Build started.",
new PackerProgress(0.25d, false),
List.of("ui_atlas")));
assertEquals(1, events.size());
assertEquals(PackerEventKind.BUILD_STARTED, events.getFirst().kind());
assertEquals(0.25d, events.getFirst().progress());
}
@Test
void ignoresMutationLifecycleEventsAlreadyHandledByTypedStudioEvents() {
final StudioEventBus globalBus = new StudioEventBus();
final List<StudioPackerOperationEvent> events = new ArrayList<>();
globalBus.subscribe(StudioPackerOperationEvent.class, events::add);
final StudioPackerEventAdapter adapter = new StudioPackerEventAdapter(
new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus),
project());
adapter.publish(new PackerEvent(
"main",
"op-1",
0L,
PackerEventKind.PREVIEW_READY,
Instant.now(),
"Preview ready.",
null,
List.of("ui_atlas")));
assertTrue(events.isEmpty());
}
private ProjectReference project() {
return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main"));
}
}