From 1c418a454b783b8db5c8ca61b599170676176d13 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Wed, 11 Mar 2026 18:06:47 +0000 Subject: [PATCH] implements packer PR-09 event lane and studio adapter --- .../FileSystemPackerBuildService.java | 42 ++++++++++-- .../doctor/FileSystemPackerDoctorService.java | 25 ++++++- .../events/PackerOperationEventEmitter.java | 49 +++++++++++++ .../FileSystemPackerMutationService.java | 5 +- .../FileSystemPackerWorkspaceService.java | 34 ++++++++-- .../FileSystemPackerBuildServiceTest.java | 18 +++++ .../FileSystemPackerDoctorServiceTest.java | 22 ++++++ .../FileSystemPackerMutationServiceTest.java | 3 +- .../FileSystemPackerWorkspaceServiceTest.java | 20 ++++++ .../shell/StudioActivityEventMapper.java | 11 +++ .../shell/StudioActivityFeedControl.java | 18 +++++ .../events/StudioPackerEventAdapter.java | 39 +++++++++++ .../events/StudioPackerOperationEvent.java | 28 ++++++++ .../workspaces/assets/AssetWorkspace.java | 12 +++- .../shell/StudioActivityEventMapperTest.java | 13 ++++ .../events/StudioPackerEventAdapterTest.java | 68 +++++++++++++++++++ 16 files changed, 393 insertions(+), 14 deletions(-) create mode 100644 prometeu-packer/src/main/java/p/packer/events/PackerOperationEventEmitter.java create mode 100644 prometeu-studio/src/main/java/p/studio/events/StudioPackerEventAdapter.java create mode 100644 prometeu-studio/src/main/java/p/studio/events/StudioPackerOperationEvent.java create mode 100644 prometeu-studio/src/test/java/p/studio/events/StudioPackerEventAdapterTest.java diff --git a/prometeu-packer/src/main/java/p/packer/building/FileSystemPackerBuildService.java b/prometeu-packer/src/main/java/p/packer/building/FileSystemPackerBuildService.java index 4b3d9ba3..ea174684 100644 --- a/prometeu-packer/src/main/java/p/packer/building/FileSystemPackerBuildService.java +++ b/prometeu-packer/src/main/java/p/packer/building/FileSystemPackerBuildService.java @@ -6,6 +6,10 @@ import p.packer.api.building.PackerBuildRequest; import p.packer.api.building.PackerBuildResult; import p.packer.api.building.PackerBuildService; 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.IOException; @@ -27,13 +31,19 @@ public final class FileSystemPackerBuildService implements PackerBuildService { private static final int PRELUDE_SIZE = 24; private final PackerBuildPlanner buildPlanner; + private final PackerEventSink eventSink; public FileSystemPackerBuildService() { - this(new PackerBuildPlanner()); + this(new PackerBuildPlanner(), PackerEventSink.noop()); } public FileSystemPackerBuildService(PackerBuildPlanner buildPlanner) { + this(buildPlanner, PackerEventSink.noop()); + } + + public FileSystemPackerBuildService(PackerBuildPlanner buildPlanner, PackerEventSink eventSink) { this.buildPlanner = Objects.requireNonNull(buildPlanner, "buildPlanner"); + this.eventSink = Objects.requireNonNull(eventSink, "eventSink"); } @Override @@ -43,14 +53,17 @@ public final class FileSystemPackerBuildService implements PackerBuildService { @Override 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 assetTableJson = buildDirectory.resolve("asset_table.json").toAbsolutePath().normalize(); final Path preloadJson = buildDirectory.resolve("preload.json").toAbsolutePath().normalize(); final Path metadataJson = buildDirectory.resolve("asset_table_metadata.json").toAbsolutePath().normalize(); - - final PackerBuildPlanResult planResult = buildPlanner.plan(request.project()); + events.emit(PackerEventKind.BUILD_STARTED, "Build started.", new PackerProgress(0.0d, false), List.of()); + final PackerBuildPlanResult planResult = buildPlanner.plan(buildRequest.project()); if (planResult.plan() == null) { + events.emit(PackerEventKind.BUILD_FINISHED, planResult.summary(), new PackerProgress(1.0d, false), List.of()); return new PackerBuildResult( PackerOperationStatus.FAILED, planResult.summary(), @@ -61,11 +74,18 @@ public final class FileSystemPackerBuildService implements PackerBuildService { try { 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()); + events.emit(PackerEventKind.PROGRESS_UPDATED, "Build archive prepared.", new PackerProgress(0.5d, false), List.of()); Files.write(assetsArchive, archive.bytes()); Files.writeString(assetTableJson, archive.assetTableJson(), StandardCharsets.UTF_8); Files.writeString(preloadJson, archive.preloadJson(), 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( planResult.status(), "Build emitted " + planResult.plan().assets().size() + " assets.", @@ -149,6 +169,20 @@ public final class FileSystemPackerBuildService implements PackerBuildService { return new EmittedArchive(archiveBytes, assetTableJson, preloadJson, metadataJson); } + @SuppressWarnings("unchecked") + private String loadPreviousCacheKey(Path metadataJson) { + if (!Files.isRegularFile(metadataJson)) { + return null; + } + try { + return ((Map) new com.fasterxml.jackson.databind.ObjectMapper().readValue(Files.readString(metadataJson), Map.class)) + .getOrDefault("cache_key", "") + .toString(); + } catch (IOException exception) { + return null; + } + } + private record EmittedArchive( byte[] bytes, String assetTableJson, diff --git a/prometeu-packer/src/main/java/p/packer/doctor/FileSystemPackerDoctorService.java b/prometeu-packer/src/main/java/p/packer/doctor/FileSystemPackerDoctorService.java index 302fa904..ec201002 100644 --- a/prometeu-packer/src/main/java/p/packer/doctor/FileSystemPackerDoctorService.java +++ b/prometeu-packer/src/main/java/p/packer/doctor/FileSystemPackerDoctorService.java @@ -11,9 +11,13 @@ import p.packer.api.doctor.PackerDoctorMode; import p.packer.api.doctor.PackerDoctorRequest; import p.packer.api.doctor.PackerDoctorResult; 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.ListAssetsRequest; import p.packer.declarations.PackerAssetDetailsService; +import p.packer.events.PackerOperationEventEmitter; import p.packer.workspace.FileSystemPackerWorkspaceService; import java.nio.file.Files; @@ -27,16 +31,25 @@ import java.util.Set; public final class FileSystemPackerDoctorService implements PackerDoctorService { private final FileSystemPackerWorkspaceService workspaceService; private final PackerAssetDetailsService detailsService; + private final PackerEventSink eventSink; public FileSystemPackerDoctorService() { - this(new FileSystemPackerWorkspaceService(), new PackerAssetDetailsService()); + this(new FileSystemPackerWorkspaceService(), new PackerAssetDetailsService(), PackerEventSink.noop()); } public FileSystemPackerDoctorService( FileSystemPackerWorkspaceService workspaceService, PackerAssetDetailsService detailsService) { + this(workspaceService, detailsService, PackerEventSink.noop()); + } + + public FileSystemPackerDoctorService( + FileSystemPackerWorkspaceService workspaceService, + PackerAssetDetailsService detailsService, + PackerEventSink eventSink) { this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService"); this.detailsService = Objects.requireNonNull(detailsService, "detailsService"); + this.eventSink = Objects.requireNonNull(eventSink, "eventSink"); } @Override @@ -48,6 +61,7 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService public PackerDoctorResult doctor(PackerDoctorRequest request) { final PackerDoctorRequest doctorRequest = Objects.requireNonNull(request, "request"); final PackerProjectContext project = doctorRequest.project(); + final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink); final var snapshot = workspaceService.listAssets(new ListAssetsRequest(project)); final List diagnostics = new ArrayList<>(); final Set safeFixes = new LinkedHashSet<>(); @@ -57,10 +71,13 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService addDiagnostic(diagnostics, seenDiagnostics, diagnostic); } + final int totalAssets = snapshot.assets().size(); + int inspected = 0; for (var asset : snapshot.assets()) { if (!includeAsset(doctorRequest.mode(), asset.state())) { continue; } + inspected += 1; final String assetReference = asset.identity().assetId() == null ? relativeAssetRoot(project, asset.identity().assetRoot()) : Integer.toString(asset.identity().assetId()); @@ -102,6 +119,11 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService input, 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(); @@ -113,6 +135,7 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService : diagnostics.isEmpty() ? "Doctor found no diagnostics." : "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)); } diff --git a/prometeu-packer/src/main/java/p/packer/events/PackerOperationEventEmitter.java b/prometeu-packer/src/main/java/p/packer/events/PackerOperationEventEmitter.java new file mode 100644 index 00000000..1bd05dac --- /dev/null +++ b/prometeu-packer/src/main/java/p/packer/events/PackerOperationEventEmitter.java @@ -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 affectedAssets) { + emit(kind, summary, null, affectedAssets); + } + + public void emit(PackerEventKind kind, String summary, PackerProgress progress, List 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))); + } +} diff --git a/prometeu-packer/src/main/java/p/packer/mutations/FileSystemPackerMutationService.java b/prometeu-packer/src/main/java/p/packer/mutations/FileSystemPackerMutationService.java index d33e24e7..6a8c2e3a 100644 --- a/prometeu-packer/src/main/java/p/packer/mutations/FileSystemPackerMutationService.java +++ b/prometeu-packer/src/main/java/p/packer/mutations/FileSystemPackerMutationService.java @@ -81,10 +81,11 @@ public final class FileSystemPackerMutationService implements PackerMutationServ throw new PackerMutationException("Cannot apply mutation preview with blockers"); } 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; } 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; } } diff --git a/prometeu-packer/src/main/java/p/packer/workspace/FileSystemPackerWorkspaceService.java b/prometeu-packer/src/main/java/p/packer/workspace/FileSystemPackerWorkspaceService.java index 8d2f0ac6..4bc9e300 100644 --- a/prometeu-packer/src/main/java/p/packer/workspace/FileSystemPackerWorkspaceService.java +++ b/prometeu-packer/src/main/java/p/packer/workspace/FileSystemPackerWorkspaceService.java @@ -9,6 +9,9 @@ import p.packer.api.assets.PackerAssetSummary; import p.packer.api.diagnostics.PackerDiagnostic; import p.packer.api.diagnostics.PackerDiagnosticCategory; 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.GetAssetDetailsResult; import p.packer.api.workspace.InitWorkspaceRequest; @@ -23,6 +26,7 @@ import p.packer.foundation.PackerRegistryEntry; import p.packer.foundation.PackerRegistryState; import p.packer.foundation.PackerWorkspaceFoundation; import p.packer.foundation.PackerWorkspacePaths; +import p.packer.events.PackerOperationEventEmitter; import java.io.IOException; import java.nio.file.Files; @@ -34,17 +38,26 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe private final PackerWorkspaceFoundation workspaceFoundation; private final PackerAssetDeclarationParser parser; private final PackerAssetDetailsService detailsService; + private final PackerEventSink eventSink; public FileSystemPackerWorkspaceService() { - this(new PackerWorkspaceFoundation(), new PackerAssetDeclarationParser()); + this(new PackerWorkspaceFoundation(), new PackerAssetDeclarationParser(), PackerEventSink.noop()); } public FileSystemPackerWorkspaceService( PackerWorkspaceFoundation workspaceFoundation, PackerAssetDeclarationParser parser) { + this(workspaceFoundation, parser, PackerEventSink.noop()); + } + + public FileSystemPackerWorkspaceService( + PackerWorkspaceFoundation workspaceFoundation, + PackerAssetDeclarationParser parser, + PackerEventSink eventSink) { this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation"); this.parser = Objects.requireNonNull(parser, "parser"); this.detailsService = new PackerAssetDetailsService(workspaceFoundation, parser); + this.eventSink = Objects.requireNonNull(eventSink, "eventSink"); } @Override @@ -60,6 +73,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe @Override public ListAssetsResult listAssets(ListAssetsRequest request) { final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); + final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink); final Path assetsRoot = PackerWorkspacePaths.assetsRoot(project); final PackerRegistryState registry = workspaceFoundation.loadRegistry(project); final Map registryByRoot = new HashMap<>(); @@ -74,14 +88,23 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe if (Files.isDirectory(assetsRoot)) { try (Stream paths = Files.find(assetsRoot, Integer.MAX_VALUE, (path, attrs) -> attrs.isRegularFile() && path.getFileName().toString().equals("asset.json"))) { - paths.forEach(assetManifestPath -> { + final List 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(); discoveredRoots.add(assetRoot); final PackerRegistryEntry registryEntry = registryByRoot.get(assetRoot); final PackerAssetDeclarationParseResult parsed = parser.parse(assetManifestPath); 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) { diagnostics.add(new PackerDiagnostic( PackerDiagnosticSeverity.ERROR, @@ -110,6 +133,9 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe final PackerOperationStatus status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking) ? PackerOperationStatus.PARTIAL : PackerOperationStatus.SUCCESS; + if (!diagnostics.isEmpty()) { + events.emit(PackerEventKind.DIAGNOSTICS_UPDATED, "Asset scan diagnostics updated.", List.of()); + } return new ListAssetsResult( status, "Packer asset snapshot ready.", diff --git a/prometeu-packer/src/test/java/p/packer/building/FileSystemPackerBuildServiceTest.java b/prometeu-packer/src/test/java/p/packer/building/FileSystemPackerBuildServiceTest.java index b9808e23..8ee8376d 100644 --- a/prometeu-packer/src/test/java/p/packer/building/FileSystemPackerBuildServiceTest.java +++ b/prometeu-packer/src/test/java/p/packer/building/FileSystemPackerBuildServiceTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.io.TempDir; import p.packer.api.PackerOperationStatus; import p.packer.api.PackerProjectContext; 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.ByteOrder; @@ -14,6 +16,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import static org.junit.jupiter.api.Assertions.*; @@ -72,6 +75,21 @@ final class FileSystemPackerBuildServiceTest { assertTrue(result.companionArtifacts().isEmpty()); } + @Test + void emitsBuildLifecycleAndCacheEvents() throws Exception { + final Path projectRoot = createProject(tempDir.resolve("events")); + final List 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 { final byte[] bytes = Files.readAllBytes(archivePath); final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); diff --git a/prometeu-packer/src/test/java/p/packer/doctor/FileSystemPackerDoctorServiceTest.java b/prometeu-packer/src/test/java/p/packer/doctor/FileSystemPackerDoctorServiceTest.java index cf64c943..f4b57cac 100644 --- a/prometeu-packer/src/test/java/p/packer/doctor/FileSystemPackerDoctorServiceTest.java +++ b/prometeu-packer/src/test/java/p/packer/doctor/FileSystemPackerDoctorServiceTest.java @@ -7,9 +7,13 @@ import p.packer.api.PackerProjectContext; import p.packer.api.diagnostics.PackerDiagnosticCategory; import p.packer.api.doctor.PackerDoctorMode; 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.Path; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import static org.junit.jupiter.api.Assertions.*; @@ -81,6 +85,24 @@ final class FileSystemPackerDoctorServiceTest { assertTrue(result.safeFixes().isEmpty()); } + @Test + void emitsDiagnosticsLifecycleEvents() throws Exception { + final Path projectRoot = createManagedProjectWithMissingInput(); + final List 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 { final Path projectRoot = tempDir.resolve("managed-missing-input"); final Path assetRoot = projectRoot.resolve("assets/ui/atlas"); diff --git a/prometeu-packer/src/test/java/p/packer/mutations/FileSystemPackerMutationServiceTest.java b/prometeu-packer/src/test/java/p/packer/mutations/FileSystemPackerMutationServiceTest.java index 18aaa860..b00fb5b7 100644 --- a/prometeu-packer/src/test/java/p/packer/mutations/FileSystemPackerMutationServiceTest.java +++ b/prometeu-packer/src/test/java/p/packer/mutations/FileSystemPackerMutationServiceTest.java @@ -50,9 +50,10 @@ final class FileSystemPackerMutationServiceTest { assertTrue(Files.isDirectory(preview.targetAssetRoot())); final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json")); 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.get(1).operationId()); + assertEquals(preview.operationId(), events.get(2).operationId()); } @Test diff --git a/prometeu-packer/src/test/java/p/packer/workspace/FileSystemPackerWorkspaceServiceTest.java b/prometeu-packer/src/test/java/p/packer/workspace/FileSystemPackerWorkspaceServiceTest.java index 9ab6ea1b..7ebe65a1 100644 --- a/prometeu-packer/src/test/java/p/packer/workspace/FileSystemPackerWorkspaceServiceTest.java +++ b/prometeu-packer/src/test/java/p/packer/workspace/FileSystemPackerWorkspaceServiceTest.java @@ -5,12 +5,16 @@ import org.junit.jupiter.api.io.TempDir; import p.packer.api.PackerOperationStatus; import p.packer.api.PackerProjectContext; 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.testing.PackerFixtureLocator; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import static org.junit.jupiter.api.Assertions.*; @@ -54,6 +58,22 @@ final class FileSystemPackerWorkspaceServiceTest { assertTrue(result.assets().getFirst().hasDiagnostics()); } + @Test + void emitsDiscoveryAndDiagnosticsEventsDuringScan() throws Exception { + final Path projectRoot = copyFixture("workspaces/read-invalid", tempDir.resolve("events")); + final List 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) { return new PackerProjectContext("main", root); } diff --git a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java index 72fb2f96..1037bdf9 100644 --- a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java +++ b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java @@ -30,7 +30,18 @@ public final class StudioActivityEventMapper { Optional.of(new StudioActivityEntry("Assets", "Action applied: " + applied.action().name().toLowerCase(), StudioActivityEntrySeverity.SUCCESS, false)); case StudioAssetsMutationFailedEvent failed -> 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(); }; } + + 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; + }; + } } diff --git a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java index 6907f01b..29248931 100644 --- a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java +++ b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java @@ -59,6 +59,7 @@ public final class StudioActivityFeedControl extends VBox implements StudioContr subscriptions.add(eventBus.subscribe(StudioAssetsMutationPreviewReadyEvent.class, this::onEvent)); subscriptions.add(eventBus.subscribe(StudioAssetsMutationAppliedEvent.class, this::onEvent)); subscriptions.add(eventBus.subscribe(StudioAssetsMutationFailedEvent.class, this::onEvent)); + subscriptions.add(eventBus.subscribe(StudioPackerOperationEvent.class, this::onPackerOperation)); } @Override @@ -114,6 +115,23 @@ public final class StudioActivityFeedControl extends VBox implements StudioContr 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() { Platform.runLater(() -> { progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE)); diff --git a/prometeu-studio/src/main/java/p/studio/events/StudioPackerEventAdapter.java b/prometeu-studio/src/main/java/p/studio/events/StudioPackerEventAdapter.java new file mode 100644 index 00000000..40453be8 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/events/StudioPackerEventAdapter.java @@ -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; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/events/StudioPackerOperationEvent.java b/prometeu-studio/src/main/java/p/studio/events/StudioPackerOperationEvent.java new file mode 100644 index 00000000..84fd17f7 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/events/StudioPackerOperationEvent.java @@ -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"); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java index e94452f5..1101fdac 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java @@ -15,6 +15,9 @@ import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; import p.studio.workspaces.Workspace; 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.nio.file.Files; @@ -58,7 +61,7 @@ public final class AssetWorkspace implements Workspace { public AssetWorkspace(ProjectReference projectReference) { this( projectReference, - new PackerBackedAssetWorkspaceService(), + null, defaultWorkspaceBus(), null); } @@ -76,8 +79,13 @@ public final class AssetWorkspace implements Workspace { StudioWorkspaceEventBus workspaceBus, AssetWorkspaceMutationService mutationService) { this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); - this.assetWorkspaceService = Objects.requireNonNull(assetWorkspaceService, "assetWorkspaceService"); 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 ? new PackerBackedAssetWorkspaceMutationService(this.workspaceBus) : Objects.requireNonNull(mutationService, "mutationService"); diff --git a/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java b/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java index 7fc9b6a6..35cb5186 100644 --- a/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java +++ b/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java @@ -1,9 +1,11 @@ package p.studio.controls.shell; import org.junit.jupiter.api.Test; +import p.packer.api.events.PackerEventKind; import p.studio.events.StudioAssetsMutationAppliedEvent; import p.studio.events.StudioAssetsMutationFailedEvent; import p.studio.events.StudioAssetsMutationPreviewReadyEvent; +import p.studio.events.StudioPackerOperationEvent; import p.studio.events.StudioAssetsWorkspaceRefreshFailedEvent; import p.studio.events.StudioAssetsWorkspaceRefreshedEvent; import p.studio.events.StudioProjectOpenedEvent; @@ -83,6 +85,17 @@ final class StudioActivityEventMapperTest { 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() { return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main")); } diff --git a/prometeu-studio/src/test/java/p/studio/events/StudioPackerEventAdapterTest.java b/prometeu-studio/src/test/java/p/studio/events/StudioPackerEventAdapterTest.java new file mode 100644 index 00000000..75a75236 --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/events/StudioPackerEventAdapterTest.java @@ -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 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 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")); + } +}