craft-native 0.0.251 → 0.0.262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/src/index.d.ts +4 -2
- package/dist/android/src/index.js +69 -5
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftHealthServices.kt.template +132 -0
- package/dist/android/templates/CraftHealthServicesService.kt.template +217 -0
- package/dist/android/templates/CraftHealthServicesStub.kt.template +16 -0
- package/dist/android/templates/CraftNativeHealth.kt.template +115 -0
- package/dist/android/templates/CraftNativeHealthStub.kt.template +1 -0
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +0 -1
- package/dist/android/templates/CraftNativeServices.kt.template +31 -8
- package/dist/android/templates/CraftNativeShell.kt.template +73 -23
- package/dist/android/templates/MainActivityNative.kt.template +217 -28
- package/dist/android/templates/build.gradle.kts.app.template +5 -0
- package/dist/api/mobile.d.ts +9 -1
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +19 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +36 -4
- package/dist/ios/templates/CraftHealth.swift +38 -0
- package/dist/ios/templates/CraftHybrid.swift +38 -8
- package/dist/ios/templates/CraftNativeActions.swift +1 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +34 -20
- package/dist/ios/templates/CraftNativeScreen.swift +308 -28
- package/dist/ios/templates/testing/DriverTests.swift.template +21 -1
- package/dist/mobile-media-testing.d.ts +2 -0
- package/dist/mobile-testing.js +19 -10
- package/dist/mobile.js +1 -1
- package/dist/testing/media-cache/media-cache.kt +47 -6
- package/package.json +1 -1
package/dist/mobile-testing.js
CHANGED
|
@@ -37,6 +37,16 @@ ${stderr}`.trim() };
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/mobile-media-testing.ts
|
|
40
|
+
function androidMediaProbeSource(shell) {
|
|
41
|
+
return ["internal object CraftNativeMedia", "internal class CraftResumableDownload", "internal class CraftNativeMediaWorker"].map((declaration) => {
|
|
42
|
+
const source = shell.match(new RegExp(`${declaration}\\b[\\s\\S]*?\\n\\}`))?.[0];
|
|
43
|
+
if (!source)
|
|
44
|
+
throw new Error(`Android media implementation missing: ${declaration}`);
|
|
45
|
+
return source;
|
|
46
|
+
}).join(`
|
|
47
|
+
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
40
50
|
async function verifyMobileMediaCache(options) {
|
|
41
51
|
if (options.platform !== "ios" && options.platform !== "android")
|
|
42
52
|
throw new Error("Choose ios or android");
|
|
@@ -129,7 +139,7 @@ Connection: close\r
|
|
|
129
139
|
}
|
|
130
140
|
const empty = path.endsWith("/empty.mp4");
|
|
131
141
|
const partial = path.endsWith("/partial.mp4");
|
|
132
|
-
const cut = (path.endsWith("/broken.mp4") || path.endsWith("/restart.mp4")) && n === 1;
|
|
142
|
+
const cut = (path.endsWith("/broken.mp4") || path.endsWith("/retry.mp4") || path.endsWith("/restart.mp4")) && n === 1;
|
|
133
143
|
const slow = path.endsWith("/slow.mp4") || path.endsWith("/restart.mp4") && n === 1;
|
|
134
144
|
socket.write(`HTTP/1.1 ${partial ? "206 Partial Content" : "200 OK"}\r
|
|
135
145
|
Content-Type: video/mp4\r
|
|
@@ -250,12 +260,7 @@ ${await Bun.file(join(fixtures, "media-cache.swift.fixture")).text()}`);
|
|
|
250
260
|
if (!/^emulator-\d+$/.test(serial))
|
|
251
261
|
throw new Error("Select a disposable Android emulator with ANDROID_SERIAL");
|
|
252
262
|
const shell = await Bun.file(join(root, "android/templates/CraftNativeShell.kt.template")).text();
|
|
253
|
-
const
|
|
254
|
-
if (pieces.some((piece) => !piece))
|
|
255
|
-
throw new Error("Android media implementation missing");
|
|
256
|
-
const media = pieces.join(`
|
|
257
|
-
|
|
258
|
-
`);
|
|
263
|
+
const media = androidMediaProbeSource(shell);
|
|
259
264
|
const android = join(scratch, "android");
|
|
260
265
|
const app = join(android, "app");
|
|
261
266
|
const sources = join(app, "src/main/java/dev/craft/mediaprobe");
|
|
@@ -267,9 +272,9 @@ include(":app")`);
|
|
|
267
272
|
await Bun.write(join(android, "build.gradle.kts"), 'plugins { id("com.android.application") version "8.9.1" apply false; id("org.jetbrains.kotlin.android") version "2.0.21" apply false }');
|
|
268
273
|
await Bun.write(join(android, "local.properties"), `sdk.dir=${sdk}
|
|
269
274
|
`);
|
|
270
|
-
await Bun.write(join(android, "gradle.properties"), `
|
|
275
|
+
await Bun.write(join(android, "gradle.properties"), `android.useAndroidX=true
|
|
276
|
+
org.gradle.jvmargs=-Xmx768m
|
|
271
277
|
org.gradle.workers.max=2
|
|
272
|
-
android.useAndroidX=true
|
|
273
278
|
`);
|
|
274
279
|
await Bun.write(join(app, "build.gradle.kts"), `plugins { id("com.android.application");
|
|
275
280
|
id("org.jetbrains.kotlin.android") }
|
|
@@ -291,8 +296,8 @@ import android.os.Handler
|
|
|
291
296
|
import android.os.Looper
|
|
292
297
|
import org.json.JSONObject
|
|
293
298
|
import java.io.File
|
|
294
|
-
import java.util.concurrent.ConcurrentHashMap
|
|
295
299
|
import java.util.concurrent.Executors
|
|
300
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
296
301
|
internal data class CraftNativeCapabilityResult(val data: Any? = null, val code: String? = null, val message: String? = null)
|
|
297
302
|
${media}
|
|
298
303
|
${await Bun.file(join(fixtures, "media-cache.kt")).text()}`);
|
|
@@ -327,6 +332,10 @@ ${await Bun.file(join(fixtures, "media-cache.kt")).text()}`);
|
|
|
327
332
|
if (counts.get("/android/restart.mp4") !== 1)
|
|
328
333
|
throw new Error("Android process did not start interrupted download");
|
|
329
334
|
await probe("recover");
|
|
335
|
+
if (counts.get("/android/retry.mp4") !== 2)
|
|
336
|
+
throw new Error("Android explicit retry did not fetch once more immediately");
|
|
337
|
+
if (counts.get("/android/settled.mp4") !== 1)
|
|
338
|
+
throw new Error("Android explicit keep did not replace the settled running attempt exactly once");
|
|
330
339
|
if (!resumed.has("/android/restart.mp4"))
|
|
331
340
|
throw new Error("Android did not resume the interrupted download from its part file");
|
|
332
341
|
if (counts.get("/android/slow.mp4") !== 1)
|
package/dist/mobile.js
CHANGED
|
@@ -1725,7 +1725,7 @@ var health = {
|
|
|
1725
1725
|
return () => {};
|
|
1726
1726
|
const listener = (event) => {
|
|
1727
1727
|
const detail = event.detail;
|
|
1728
|
-
if (detail && typeof detail.id === "string" && (detail.event === "state" || detail.event === "error"))
|
|
1728
|
+
if (detail && typeof detail.id === "string" && (detail.event === "state" || detail.event === "metrics" || detail.event === "error"))
|
|
1729
1729
|
callback(detail);
|
|
1730
1730
|
};
|
|
1731
1731
|
window.addEventListener("craftHealthWorkout", listener);
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Holds a completed attempt in RUNNING, before WorkManager commits its result. */
|
|
2
|
+
class SettledMediaAttemptWorker(context: Context, parameters: androidx.work.WorkerParameters) : androidx.work.Worker(context, parameters) {
|
|
3
|
+
private val released = java.util.concurrent.CountDownLatch(1)
|
|
4
|
+
override fun doWork(): Result {
|
|
5
|
+
setProgressAsync(androidx.work.workDataOf("craft.media.attemptSettled" to true)).get()
|
|
6
|
+
released.await(30, java.util.concurrent.TimeUnit.SECONDS)
|
|
7
|
+
return Result.retry()
|
|
8
|
+
}
|
|
9
|
+
override fun onStopped() { released.countDown() }
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
class DownloadProbe : android.app.Instrumentation() {
|
|
2
13
|
override fun onCreate(arguments: android.os.Bundle?) {
|
|
3
14
|
super.onCreate(arguments)
|
|
@@ -23,6 +34,13 @@ class DownloadProbe : android.app.Instrumentation() {
|
|
|
23
34
|
check(done.await(25, java.util.concurrent.TimeUnit.SECONDS), "operation settled")
|
|
24
35
|
return ok
|
|
25
36
|
}
|
|
37
|
+
private fun awaitCached(address: String) {
|
|
38
|
+
// The worker authors a 30-second first retry. Allow that backoff plus
|
|
39
|
+
// the existing 25-second transfer budget, without issuing another keep.
|
|
40
|
+
val until = System.currentTimeMillis() + 60000
|
|
41
|
+
while (CraftNativeMedia.local(targetContext, address) == null && System.currentTimeMillis() < until) Thread.sleep(25)
|
|
42
|
+
check(CraftNativeMedia.local(targetContext, address) != null, "the durable worker recovers the complete file without another keep")
|
|
43
|
+
}
|
|
26
44
|
override fun onStart() {
|
|
27
45
|
val result = android.os.Bundle()
|
|
28
46
|
try {
|
|
@@ -80,10 +98,11 @@ class DownloadProbe : android.app.Instrumentation() {
|
|
|
80
98
|
val until = System.currentTimeMillis() + 25000
|
|
81
99
|
while (!partial() && System.currentTimeMillis() < until) Thread.sleep(25)
|
|
82
100
|
check(partial(), "download is in progress before process death")
|
|
101
|
+
check(CraftNativeMedia.local(targetContext, restart) == null, "interrupted process never published a partial video")
|
|
83
102
|
android.os.Process.killProcess(android.os.Process.myPid())
|
|
84
103
|
}
|
|
85
104
|
"recover" -> {
|
|
86
|
-
|
|
105
|
+
awaitCached(restart)
|
|
87
106
|
check(keep(restart), "retry after process death downloads a complete video")
|
|
88
107
|
val media = android.media.MediaMetadataRetriever()
|
|
89
108
|
try {
|
|
@@ -98,12 +117,34 @@ class DownloadProbe : android.app.Instrumentation() {
|
|
|
98
117
|
check(!keep(broken), "truncated transfer fails")
|
|
99
118
|
check(CraftNativeMedia.local(targetContext, broken) == null, "truncated transfer is not playable cache")
|
|
100
119
|
val root = CraftNativeMedia.file(targetContext, broken).parentFile!!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
check(keep(broken), "same URL retries successfully
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
check(CraftNativeMedia.part(targetContext, broken).length() > 0, "a truncated transfer retains bytes for resumption")
|
|
121
|
+
awaitCached(broken)
|
|
122
|
+
check(keep(broken), "same URL retries successfully")
|
|
123
|
+
val retry = "$base/retry.mp4"
|
|
124
|
+
check(!keep(retry), "a separate interrupted transfer reports its failure")
|
|
125
|
+
check(keep(retry), "an explicit retry runs immediately, without waiting for backoff")
|
|
126
|
+
check(root.listFiles()?.none { it.name.endsWith(".part") } != false, "finished retries leave no staging files")
|
|
106
127
|
val work = androidx.work.WorkManager.getInstance(targetContext)
|
|
128
|
+
val settled = "$base/settled.mp4"
|
|
129
|
+
val settledName = "craft-media:${CraftNativeMedia.digest(settled)}:any"
|
|
130
|
+
val settledRequest = androidx.work.OneTimeWorkRequestBuilder<SettledMediaAttemptWorker>()
|
|
131
|
+
.addTag("craft-media").addTag("craft-media:${CraftNativeMedia.digest(settled)}")
|
|
132
|
+
.addTag("craft-media:generation:${CraftNativeMedia.generation(targetContext)}").build()
|
|
133
|
+
work.enqueueUniqueWork(settledName, androidx.work.ExistingWorkPolicy.KEEP, settledRequest).result.get()
|
|
134
|
+
val settledDeadline = System.currentTimeMillis() + 10000
|
|
135
|
+
var settledInfo = work.getWorkInfoById(settledRequest.id).get()!!
|
|
136
|
+
while ((settledInfo.state != androidx.work.WorkInfo.State.RUNNING || !settledInfo.progress.getBoolean("craft.media.attemptSettled", false)) && System.currentTimeMillis() < settledDeadline) {
|
|
137
|
+
Thread.sleep(25)
|
|
138
|
+
settledInfo = work.getWorkInfoById(settledRequest.id).get()!!
|
|
139
|
+
}
|
|
140
|
+
check(settledInfo.state == androidx.work.WorkInfo.State.RUNNING && settledInfo.progress.getBoolean("craft.media.attemptSettled", false), "the settled attempt still has WorkManager's running identity")
|
|
141
|
+
check(keep(settled), "an explicit keep replaces a settled running attempt immediately")
|
|
142
|
+
// REPLACE can prune the old WorkSpec. Inspect the current
|
|
143
|
+
// unique chain rather than requiring the removed row.
|
|
144
|
+
val replacement = work.getWorkInfosForUniqueWork(settledName).get()
|
|
145
|
+
check(replacement.size == 1 && replacement.single().id != settledRequest.id && replacement.single().state != androidx.work.WorkInfo.State.CANCELLED, "the settled work identity was replaced, not joined")
|
|
146
|
+
|
|
147
|
+
val queued = "$base/queued.mp4"
|
|
107
148
|
val queuedName = "craft-media:${CraftNativeMedia.digest(queued)}:any"
|
|
108
149
|
val queuedRequest = androidx.work.OneTimeWorkRequestBuilder<CraftNativeMediaWorker>()
|
|
109
150
|
.setInitialDelay(10, java.util.concurrent.TimeUnit.SECONDS)
|