ReviewOS

also looking at this

craft-native/craft

fix(native): interaction, audio, document and checkpoint gaps stranded after #401

#415
Merged glennmichael123 wants to merge codex/issues-388-400-344 into main
19 files +987 -50

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/guides/native-stx.mdmodified+3-1
Changes to docs/guides/native-stx.md
@@ -1385,7 +1385,9 @@ Checkpoint names use the same bounded filename contract on both native hosts:
13851385letters, digits, `.`, `_`, and `-`, up to 128 characters, without a leading
13861386dot. Non-JSON values and invalid names are rejected before a write is posted;
13871387the previous confirmed checkpoint remains available after a failed or
1388interrupted replacement.
1388interrupted replacement. If a screen closes while a checkpoint is still being
1389written, its promise rejects instead of hanging; the host's later worker
1390completion cannot publish a second result for that request.
13891391
13901392The `media` capability (iOS) keeps video and audio on the phone for offline,
13911393in Application Support rather than the temporary directory, left out of
packages/android/src/index.test.tsmodified+3-0
Changes to packages/android/src/index.test.ts
@@ -717,6 +717,9 @@ describe('Craft Android builder', () => {
717717 expect(native).toContain('showError("Missing native-screen.js. Build with --native-bundle after compiling STX.")')
718718 expect(native).toContain('screen.capabilityTimeouts.values.forEach(capabilityHandler::removeCallbacks)')
719719 expect(native).toContain('screen.pendingCapabilities.forEach(capabilities::cancel)')
720 expect(native).toContain('screen.pendingSyncResults.toList().forEach')
721 expect(native).toContain('screen.pendingSyncResults.clear()')
722 expect(native).toContain('screen.pendingSyncResults.remove(id)')
720723 expect(native).toContain('screen.close {')
721724 expect(native).toContain('imageJobs.values.forEach { it.cancel(true) }')
722725 expect(native).toContain('imageExecutor.shutdownNow()')
packages/android/templates/MainActivityNative.kt.templatemodified+19-3
Changes to packages/android/templates/MainActivityNative.kt.template
@@ -4005,6 +4005,8 @@ class MainActivity : AppCompatActivity() {
40054005 val audio = CraftNativeAudio()
40064006 val sounds = CraftNativeSounds(view.context)
40074007 val soundRequestCancellers = mutableMapOf<String, () -> Unit>()
4008 /** Snapshot writes awaiting the writer thread; close rejects them once. */
4009 val pendingSyncResults = mutableSetOf<String>()
40084010 var active = true
40094011 var closing = false
40104012 private set
@@ -4952,13 +4954,20 @@ class MainActivity : AppCompatActivity() {
49524954 /** A synchronous store written in an isolate (CraftNativeShell.kt's prelude). */
49534955 private fun syncWrite(screen: Screen, payload: JSONObject) {
49544956 val id = payload.optString("id").takeIf { it.isNotBlank() }
4957 val store = payload.optString("store")
4958 if (id != null) screen.pendingSyncResults.add(id)
49554959 val reply: (Boolean) -> Unit = { ok ->
4956 if (id != null && screen.active) send(screen, "SYNC_RESULT", JSONObject().put("id", id).put("ok", ok))
4960 if (id != null && screen.pendingSyncResults.remove(id) && (screen.active || screen.closing)) {
4961 send(screen, "SYNC_RESULT", JSONObject().put("id", id).put("ok", ok))
4962 }
49574963 }
49584964 val key = payload.optString("key")
49594965 val value = if (payload.isNull("value")) null else payload.optString("value")
4960 when (payload.optString("store")) {
4961 "storage" -> if (key.isNotBlank()) stores.setStorage(key, value, from = screen.capabilityScope)
4966 when (store) {
4967 "storage" -> if (key.isNotBlank()) {
4968 stores.setStorage(key, value, from = screen.capabilityScope)
4969 if (id != null) reply(true)
4970 } else if (id != null) reply(false)
49624971 "snapshots" -> if (payload.optBoolean("clear")) {
49634972 val keep = payload.optJSONArray("keep") ?: JSONArray()
49644973 stores.clearSnapshots((0 until keep.length()).map { keep.optString(it) }.toSet(), reply)
@@ -8051,6 +8060,13 @@ class MainActivity : AppCompatActivity() {
80518060
80528061 private fun closeScreen(screen: Screen, done: () -> Unit = {}) {
80538062 if (!screen.active) { screen.close(done); return }
8063 // A snapshot writer may still be replacing a file after the isolate
8064 // starts teardown. Reject its promise before the port is closed; the
8065 // worker's later completion sees the id was already consumed.
8066 screen.pendingSyncResults.toList().forEach { id ->
8067 send(screen, "SYNC_RESULT", JSONObject().put("id", id).put("ok", false))
8068 }
8069 screen.pendingSyncResults.clear()
80548070 CraftNativeHub.unregister(screen.capabilityScope)
80558071 screen.timers.values.forEach(capabilityHandler::removeCallbacks)
80568072 screen.timers.clear()