craft-native 0.0.244 → 0.0.246

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.
@@ -1376,23 +1376,32 @@ final class CraftWebAudio {
1376
1376
  /// JavaScript, before its bundle runs.
1377
1377
  func install(in context: JSContext) {
1378
1378
  script = context
1379
+ // The graph lives on the main thread. A script on its own thread
1380
+ // (CraftNativeScriptThread) reaches it there, waiting for the answer
1381
+ // its synchronous calls return; arguments are read off the JSValues
1382
+ // where the script runs.
1379
1383
  let open: @convention(block) (Int, Double) -> NSDictionary? = { [weak self] id, rate in
1380
- self?.open(id, requestedRate: rate) as NSDictionary?
1384
+ CraftNativeScriptThread.onMain { self?.open(id, requestedRate: rate) as NSDictionary? }
1381
1385
  }
1382
1386
  let send: @convention(block) (JSValue) -> Void = { [weak self] batch in
1383
- self?.receive(batch.toArray() ?? [])
1387
+ let commands = batch.toArray() ?? []
1388
+ CraftNativeScriptThread.onMain { self?.receive(commands) }
1384
1389
  }
1385
1390
  let time: @convention(block) (Int) -> Double = { [weak self] id in
1386
- self?.contexts[id]?.graph.currentTime ?? 0
1391
+ CraftNativeScriptThread.onMain { self?.contexts[id]?.graph.currentTime ?? 0 }
1387
1392
  }
1388
1393
  let read: @convention(block) (Int, Int, String) -> Double = { [weak self] contextId, nodeId, key in
1389
- self?.read(contextId, nodeId, key) ?? .nan
1394
+ CraftNativeScriptThread.onMain { self?.read(contextId, nodeId, key) ?? .nan }
1390
1395
  }
1391
1396
  let state: @convention(block) (Int, String) -> Bool = { [weak self] id, state in
1392
- self?.setState(id, state) ?? false
1397
+ CraftNativeScriptThread.onMain { self?.setState(id, state) ?? false }
1393
1398
  }
1394
1399
  let buffer: @convention(block) (Int, Double, Int, JSValue) -> Bool = { [weak self] id, rate, length, channels in
1395
- self?.storeBuffer(id, rate, length, channels) ?? false
1400
+ // The samples are copied out of the script's typed arrays where
1401
+ // the script runs, then kept beside the graph.
1402
+ guard let data = Self.copyBuffer(rate, length, channels) else { return false }
1403
+ CraftNativeScriptThread.onMain { self?.buffers[id] = data }
1404
+ return true
1396
1405
  }
1397
1406
  context.setObject(open, forKeyedSubscript: "craftNativeAudioOpen" as NSString)
1398
1407
  context.setObject(send, forKeyedSubscript: "craftNativeAudioSend" as NSString)
@@ -1637,10 +1646,10 @@ final class CraftWebAudio {
1637
1646
  }
1638
1647
 
1639
1648
  /// An AudioBuffer's channels, copied straight out of its Float32Arrays.
1640
- private func storeBuffer(_ id: Int, _ rate: Double, _ length: Int, _ channels: JSValue) -> Bool {
1641
- guard length > 0, rate > 0, let context = JSContext.current() else { return false }
1649
+ private static func copyBuffer(_ rate: Double, _ length: Int, _ channels: JSValue) -> CraftAudioBufferData? {
1650
+ guard length > 0, rate > 0, let context = JSContext.current() else { return nil }
1642
1651
  let count = Int(channels.objectForKeyedSubscript("length")?.toInt32() ?? 0)
1643
- guard count > 0 else { return false }
1652
+ guard count > 0 else { return nil }
1644
1653
  let data = CraftAudioBufferData(sampleRate: rate, length: length, channelCount: count)
1645
1654
  let script = context.jsGlobalContextRef
1646
1655
  for index in 0..<count {
@@ -1651,8 +1660,7 @@ final class CraftWebAudio {
1651
1660
  let available = JSObjectGetTypedArrayLength(script, object, nil)
1652
1661
  data.channels[index].update(from: bytes.assumingMemoryBound(to: Float.self), count: min(length, available))
1653
1662
  }
1654
- buffers[id] = data
1655
- return true
1663
+ return data
1656
1664
  }
1657
1665
 
1658
1666
  // MARK: Notices
@@ -1675,8 +1683,8 @@ final class CraftWebAudio {
1675
1683
  }
1676
1684
  }
1677
1685
  }
1678
- guard !ended.isEmpty else { return }
1679
- script?.objectForKeyedSubscript("__craftNativeAudioEvents")?.call(withArguments: [ended])
1686
+ guard !ended.isEmpty, let script else { return }
1687
+ CraftNativeScriptThread.run { script.objectForKeyedSubscript("__craftNativeAudioEvents")?.call(withArguments: [ended]) }
1680
1688
  }
1681
1689
 
1682
1690
  // MARK: Tests
@@ -188,9 +188,12 @@ ${await Bun.file(join(fixtures, "media-cache.swift.fixture")).text()}`);
188
188
  if (!/^emulator-\d+$/.test(serial))
189
189
  throw new Error("Select a disposable Android emulator with ANDROID_SERIAL");
190
190
  const shell = await Bun.file(join(root, "android/templates/CraftNativeShell.kt.template")).text();
191
- const media = shell.match(/internal object CraftNativeMedia \{[\s\S]*?\n\}/)?.[0];
192
- if (!media)
191
+ const pieces = ["internal object CraftNativeMedia {", "internal class CraftResumableDownload(", "internal class CraftNativeMediaWorker("].map((start) => shell.match(new RegExp(`${start.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?\\n\\}`))?.[0]);
192
+ if (pieces.some((piece) => !piece))
193
193
  throw new Error("Android media implementation missing");
194
+ const media = pieces.join(`
195
+
196
+ `);
194
197
  const android = join(scratch, "android");
195
198
  const app = join(android, "app");
196
199
  const sources = join(app, "src/main/java/dev/craft/mediaprobe");
@@ -214,15 +217,18 @@ android { namespace = "${packageName}";
214
217
  targetSdk = 35 };
215
218
  compileOptions { sourceCompatibility = JavaVersion.VERSION_17;
216
219
  targetCompatibility = JavaVersion.VERSION_17 };
217
- kotlinOptions { jvmTarget = "17" } }`);
220
+ kotlinOptions { jvmTarget = "17" } }
221
+ dependencies { implementation("androidx.work:work-runtime-ktx:2.9.1") }`);
218
222
  await Bun.write(join(app, "src/main/AndroidManifest.xml"), `<manifest xmlns:android="http://schemas.android.com/apk/res/android"><uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/><application android:usesCleartextTraffic="true" android:label="HQ Download Probe"/><instrumentation android:name=".DownloadProbe" android:targetPackage="${packageName}"/></manifest>`);
219
223
  await Bun.write(join(sources, "DownloadProbe.kt"), `package ${packageName}
220
224
  import android.content.Context
225
+ import android.content.SharedPreferences
221
226
  import android.net.Uri
222
227
  import android.os.Handler
223
228
  import android.os.Looper
224
229
  import org.json.JSONObject
225
230
  import java.io.File
231
+ import java.util.concurrent.ConcurrentHashMap
226
232
  import java.util.concurrent.Executors
227
233
  internal data class CraftNativeCapabilityResult(val data: Any? = null, val code: String? = null, val message: String? = null)
228
234
  ${media}
package/dist/mobile.js CHANGED
@@ -355,6 +355,32 @@ var secureStorage = {
355
355
  Object.keys(localStorage).filter((k) => k.startsWith("secure_")).forEach((k) => localStorage.removeItem(k));
356
356
  }
357
357
  };
358
+ var SESSION_EVENT = "craftsession";
359
+ var SESSION_MESSAGE = "SESSION_CHANGED";
360
+ var DEFAULT_SESSION_TOKEN_KEY = "auth.token";
361
+ var sessionToken = (value) => typeof value === "string" && value !== "" ? value : null;
362
+ var session = {
363
+ token() {
364
+ const host = getCraftRoot()?.session;
365
+ if (typeof host?.token !== "function")
366
+ return null;
367
+ try {
368
+ return sessionToken(host.token());
369
+ } catch {
370
+ return null;
371
+ }
372
+ },
373
+ onChange(callback) {
374
+ if (typeof callback !== "function")
375
+ throw new TypeError("session.onChange needs a function");
376
+ const host = getCraftRoot()?.session;
377
+ if (typeof host?.onChange === "function") {
378
+ const stop = host.onChange((token, previous) => callback(sessionToken(token), sessionToken(previous)));
379
+ return typeof stop === "function" ? stop : () => {};
380
+ }
381
+ return onCraftEvent(SESSION_EVENT, (detail) => callback(sessionToken(detail.token), sessionToken(detail.previous)));
382
+ }
383
+ };
358
384
  function storageHost(method) {
359
385
  const host = getCraftRoot()?.storage;
360
386
  if (typeof host?.[method] !== "function")
@@ -1643,11 +1669,11 @@ var health = {
1643
1669
  throw new Error("Reading sleep intervals is unavailable");
1644
1670
  return craft.health.getSleepIntervals(options);
1645
1671
  },
1646
- async saveMindfulSession(session) {
1672
+ async saveMindfulSession(session2) {
1647
1673
  const craft = getCraftRoot();
1648
1674
  if (!craft?.health?.saveMindfulSession)
1649
1675
  throw new Error("Saving mindful sessions is unavailable");
1650
- return craft.health.saveMindfulSession(session);
1676
+ return craft.health.saveMindfulSession(session2);
1651
1677
  },
1652
1678
  async getMindfulSessions(options = {}) {
1653
1679
  const craft = getCraftRoot();
@@ -1802,6 +1828,7 @@ export {
1802
1828
  speech,
1803
1829
  snapshots,
1804
1830
  share,
1831
+ session,
1805
1832
  secureStorage,
1806
1833
  scheduledWorkoutsUnsupported,
1807
1834
  scheduledWorkouts,
@@ -1843,5 +1870,8 @@ export {
1843
1870
  appearance,
1844
1871
  appReview,
1845
1872
  _webFallbackDeviceId,
1846
- _resetWebFallbackDeviceIdForTests
1873
+ _resetWebFallbackDeviceIdForTests,
1874
+ SESSION_MESSAGE,
1875
+ SESSION_EVENT,
1876
+ DEFAULT_SESSION_TOKEN_KEY
1847
1877
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.244",
3
+ "version": "0.0.246",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",