diff --git a/src/api/app.zig b/src/api/app.zig index 88a8348..00ceed3 100644 --- a/src/api/app.zig +++ b/src/api/app.zig @@ -59,9 +59,13 @@ pub export fn uhdk_app_run( const application = runtime.fromAppHandle(handle); if (application.has_run) return abi.UNAVAILABLE; profile.thread("UHDK platform"); + const ready = runtime.prepareReady(application) catch return abi.OOM; const status = c.z_native_start(handle, abi.textSpan(application.name)); - if (status != abi.OK) return status; - runtime.simpleEvent(application, c.UHDK_EVENT_READY); + if (status != abi.OK) { + runtime.destroyEvent(ready); + return status; + } + runtime.publishReady(application, ready); application.entry = entry; application.user = user; application.running = true; diff --git a/src/core_tests.zig b/src/core_tests.zig index 0c924f5..040f40a 100644 --- a/src/core_tests.zig +++ b/src/core_tests.zig @@ -164,3 +164,21 @@ test "poll allocation failure keeps queued terminal completion retryable" { try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_poll_events(handle, 0, &batch)); try std.testing.expectEqual(@as(u64, 0), batch.len); } + +test "READY precedes native startup events without being published on failed startup" { + const application = try Application.create(std.testing.allocator, std.testing.io, "startup"); + defer destroyApplication(application); + const failed = try runtime.prepareReady(application); + try std.testing.expect(application.pending_head == null); + runtime.destroyEvent(failed); + const ready = try runtime.prepareReady(application); + runtime.simpleEvent(application, c.UHDK_EVENT_PREFERENCES_CHANGED); + const preferences = application.pending_head.?; + runtime.publishReady(application, ready); + runtime.simpleEvent(application, c.UHDK_EVENT_WAKE); + try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_READY), application.pending_head.?.event.type); + try std.testing.expect(ready.next == preferences); + try std.testing.expect(ready.event.time_ns <= preferences.event.time_ns); + try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_WAKE), preferences.next.?.event.type); + try std.testing.expect(application.pending_tail == preferences.next); +} diff --git a/src/runtime.zig b/src/runtime.zig index 5b94b5e..ad442a5 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -152,6 +152,27 @@ pub fn copyPayload(node: *Event) !void { } } +// Reserve READY before native initialization, but expose it only after startup +// succeeds. Initial preferences/open-file events may arrive during that call. +pub fn prepareReady(application: *Application) !*Event { + const node = try application.gpa.create(Event); + node.* = .{ .gpa = application.gpa, .arena = .init(application.gpa) }; + node.event.type = c.UHDK_EVENT_READY; + node.event.struct_size = @sizeOf(c.z_event); + node.event.time_ns = now(application.io) - application.epoch; + return node; +} + +pub fn publishReady(application: *Application, node: *Event) void { + std.debug.assert(node.event.type == c.UHDK_EVENT_READY); + application.lock(); + defer application.unlock(); + node.next = application.pending_head; + application.pending_head = node; + if (application.pending_tail == null) application.pending_tail = node; + application.notify(); +} + pub fn simpleEvent(application: *Application, typ: u32) void { var event = std.mem.zeroes(c.z_event); event.type = typ;