also looking at this
fix(native): interaction, audio, document and checkpoint gaps stranded after #401
#415
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.
| @@ -1385,7 +1385,9 @@ Checkpoint names use the same bounded filename contract on both native hosts: | ||
| 1385 | 1385 | letters, digits, `.`, `_`, and `-`, up to 128 characters, without a leading |
| 1386 | 1386 | dot. Non-JSON values and invalid names are rejected before a write is posted; |
| 1387 | 1387 | the previous confirmed checkpoint remains available after a failed or |
| 1388 | interrupted replacement. | |
| 1388 | interrupted replacement. If a screen closes while a checkpoint is still being | |
| 1389 | written, its promise rejects instead of hanging; the host's later worker | |
| 1390 | completion cannot publish a second result for that request. | |
| 1389 | 1391 | |
| 1390 | 1392 | The `media` capability (iOS) keeps video and audio on the phone for offline, |
| 1391 | 1393 | in Application Support rather than the temporary directory, left out of |
| @@ -717,6 +717,9 @@ describe('Craft Android builder', () => { | ||
| 717 | 717 | expect(native).toContain('showError("Missing native-screen.js. Build with --native-bundle after compiling STX.")') |
| 718 | 718 | expect(native).toContain('screen.capabilityTimeouts.values.forEach(capabilityHandler::removeCallbacks)') |
| 719 | 719 | 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)') | |
| 720 | 723 | expect(native).toContain('screen.close {') |
| 721 | 724 | expect(native).toContain('imageJobs.values.forEach { it.cancel(true) }') |
| 722 | 725 | expect(native).toContain('imageExecutor.shutdownNow()') |
| @@ -4005,6 +4005,8 @@ class MainActivity : AppCompatActivity() { | ||
| 4005 | 4005 | val audio = CraftNativeAudio() |
| 4006 | 4006 | val sounds = CraftNativeSounds(view.context) |
| 4007 | 4007 | val soundRequestCancellers = mutableMapOf<String, () -> Unit>() |
| 4008 | /** Snapshot writes awaiting the writer thread; close rejects them once. */ | |
| 4009 | val pendingSyncResults = mutableSetOf<String>() | |
| 4008 | 4010 | var active = true |
| 4009 | 4011 | var closing = false |
| 4010 | 4012 | private set |
| @@ -4952,13 +4954,20 @@ class MainActivity : AppCompatActivity() { | ||
| 4952 | 4954 | /** A synchronous store written in an isolate (CraftNativeShell.kt's prelude). */ |
| 4953 | 4955 | private fun syncWrite(screen: Screen, payload: JSONObject) { |
| 4954 | 4956 | val id = payload.optString("id").takeIf { it.isNotBlank() } |
| 4957 | val store = payload.optString("store") | |
| 4958 | if (id != null) screen.pendingSyncResults.add(id) | |
| 4955 | 4959 | 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 | } | |
| 4957 | 4963 | } |
| 4958 | 4964 | val key = payload.optString("key") |
| 4959 | 4965 | 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) | |
| 4962 | 4971 | "snapshots" -> if (payload.optBoolean("clear")) { |
| 4963 | 4972 | val keep = payload.optJSONArray("keep") ?: JSONArray() |
| 4964 | 4973 | stores.clearSnapshots((0 until keep.length()).map { keep.optString(it) }.toSet(), reply) |
| @@ -8051,6 +8060,13 @@ class MainActivity : AppCompatActivity() { | ||
| 8051 | 8060 | |
| 8052 | 8061 | private fun closeScreen(screen: Screen, done: () -> Unit = {}) { |
| 8053 | 8062 | 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() | |
| 8054 | 8070 | CraftNativeHub.unregister(screen.capabilityScope) |
| 8055 | 8071 | screen.timers.values.forEach(capabilityHandler::removeCallbacks) |
| 8056 | 8072 | screen.timers.clear() |