@onekeyfe/react-native-bundle-update 3.0.67 → 3.0.69

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.
@@ -18,8 +18,9 @@ import java.io.FileOutputStream
18
18
  import java.nio.file.Files
19
19
  import java.nio.file.Path
20
20
  import java.nio.file.Paths
21
+ import java.util.concurrent.ConcurrentHashMap
21
22
  import java.util.concurrent.CopyOnWriteArrayList
22
- import java.util.concurrent.atomic.AtomicBoolean
23
+ import java.util.concurrent.TimeUnit
23
24
  import java.util.concurrent.atomic.AtomicInteger
24
25
  import java.util.concurrent.atomic.AtomicLong
25
26
  import java.util.zip.ZipEntry
@@ -1094,8 +1095,27 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1094
1095
 
1095
1096
  private val listeners = CopyOnWriteArrayList<BundleListener>()
1096
1097
  private val nextListenerId = AtomicLong(1)
1097
- private val isDownloading = AtomicBoolean(false)
1098
+
1099
+ // Per-destination single-flight (OCDS §5.8): one in-flight run per final
1100
+ // bundle path, keyed by `filePath`. `putIfAbsent` makes a second
1101
+ // downloadBundle for the SAME dest fail fast ("Already downloading"), while
1102
+ // different dests now run concurrently (the old global boolean serialized
1103
+ // everything). The value is the live CancelHandle so clearDownload/clearBundle
1104
+ // can stop the 8 workers BEFORE deleting the directory (cancel-then-delete),
1105
+ // preventing a worker from re-creating a just-deleted `.segN`. Being an
1106
+ // in-memory map, a crashed run leaves no stale lock — the entry is gone on
1107
+ // relaunch, giving inherent stale-lock recovery (OCDS §5.8).
1108
+ private val activeDownloads =
1109
+ ConcurrentHashMap<String, ConcurrentRangeDownloader.CancelHandle>()
1098
1110
  private val httpClient = OkHttpClient.Builder()
1111
+ // OCDS §5.4 timeouts. `readTimeout` is the primary inter-byte stall
1112
+ // window: OkHttp raises SocketTimeoutException when no bytes arrive within
1113
+ // it, which the segment loop classifies transient and retries. We do NOT
1114
+ // set callTimeout — it bounds the WHOLE call and would cut a legitimately
1115
+ // slow large segment mid-stream; the overall run deadline is owned by the
1116
+ // shared-JS retry budget (ServiceAppUpdate), not this socket layer.
1117
+ .connectTimeout(30, TimeUnit.SECONDS)
1118
+ .readTimeout(60, TimeUnit.SECONDS)
1099
1119
  .addNetworkInterceptor { chain ->
1100
1120
  val req = chain.request()
1101
1121
  if (!req.url.isHttps) {
@@ -1191,10 +1211,13 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1191
1211
 
1192
1212
  override fun downloadBundle(params: BundleDownloadParams): Promise<BundleDownloadResult> {
1193
1213
  return Promise.async {
1194
- if (isDownloading.getAndSet(true)) {
1195
- OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading")
1196
- throw Exception("Already downloading")
1197
- }
1214
+ // Per-dest single-flight is acquired below once `filePath` is known.
1215
+ // Input validation (version/HTTPS) runs FIRST and is intentionally
1216
+ // NOT gated by the lock — a malformed request must fail the same way
1217
+ // whether or not a download is in flight, and must never register a
1218
+ // lock entry it then has to unwind.
1219
+ var acquiredKey: String? = null
1220
+ var cancelHandle: ConcurrentRangeDownloader.CancelHandle? = null
1198
1221
 
1199
1222
  try {
1200
1223
  val context = getContext()
@@ -1224,6 +1247,19 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1224
1247
  // "exists at filePath -> already valid" cache check above.
1225
1248
  val partialFilePath = "$filePath.partial"
1226
1249
 
1250
+ // OCDS §5.8 per-destination single-flight. Register a CancelHandle for
1251
+ // THIS dest; a second downloadBundle for the same `filePath` fails fast
1252
+ // ("Already downloading"). Different dests are allowed to run
1253
+ // concurrently. The handle is passed to the concurrent downloader so
1254
+ // clearDownload/clearBundle can stop its workers before deleting files.
1255
+ val handle = ConcurrentRangeDownloader.CancelHandle()
1256
+ if (activeDownloads.putIfAbsent(filePath, handle) != null) {
1257
+ OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading $filePath")
1258
+ throw Exception("Already downloading")
1259
+ }
1260
+ acquiredKey = filePath
1261
+ cancelHandle = handle
1262
+
1227
1263
  val result = BundleDownloadResult(
1228
1264
  downloadedFile = filePath,
1229
1265
  downloadUrl = downloadUrl,
@@ -1247,12 +1283,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1247
1283
  if (partialFile.exists()) partialFile.delete()
1248
1284
  File("$partialFilePath.progress").delete()
1249
1285
  for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1250
- // Keep isDownloading held across the skip delay below. Clearing
1251
- // it before the sleep opens a ~1s window where a second
1252
- // downloadBundle could pass the getAndSet guard and run
1253
- // concurrently. Reset only after the delay completes.
1286
+ // Keep the per-dest lock held across the skip delay below.
1287
+ // The `finally` removes the lock entry AFTER this return runs,
1288
+ // so the lock spans the whole sleep closing the ~1s window
1289
+ // where a second downloadBundle for this dest could otherwise
1290
+ // pass the single-flight guard and run concurrently.
1254
1291
  Thread.sleep(1000)
1255
- isDownloading.set(false)
1256
1292
  sendEvent("update/complete")
1257
1293
  return@async result
1258
1294
  } else {
@@ -1284,7 +1320,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1284
1320
  val concurrentOutcome = ConcurrentRangeDownloader(
1285
1321
  httpClient = httpClient,
1286
1322
  log = { msg -> OneKeyLog.info("BundleUpdate", msg) },
1287
- ).download(downloadUrl, partialFilePath) { transferred, total ->
1323
+ ).download(downloadUrl, partialFilePath, cancelHandle) { transferred, total ->
1288
1324
  if (total > 0) {
1289
1325
  val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
1290
1326
  val prev = concurrentProgress.get()
@@ -1338,7 +1374,9 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1338
1374
  if (downloadedFile.exists()) downloadedFile.delete()
1339
1375
  if (partialFile.renameTo(downloadedFile) && verifyBundleSHA256(filePath, sha256)) {
1340
1376
  OneKeyLog.info("BundleUpdate", "downloadBundle: recovered crashed-before-rename bundle, skipping download")
1341
- isDownloading.set(false)
1377
+ // The `finally` removes the per-dest lock after this
1378
+ // return; the lock therefore spans the skip delay,
1379
+ // same as the existing-file cache-hit path above.
1342
1380
  Thread.sleep(1000)
1343
1381
  sendEvent("update/complete")
1344
1382
  return@async result
@@ -1545,7 +1583,26 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1545
1583
  // sanitizeErrorMessageForEvent.
1546
1584
  OneKeyLog.error("BundleUpdate", "downloadBundle: failed: ${e.javaClass.simpleName}: ${e.message}")
1547
1585
  val sanitized = sanitizeErrorMessageForEvent(e)
1548
- sendEvent("update/error", message = sanitized)
1586
+ // An INTENTIONAL cancel (clearDownload/clearBundle flipped THIS
1587
+ // run's CancelHandle, which aborts the workers → they throw
1588
+ // IOException("aborted")) is NOT a download failure — the caller
1589
+ // deliberately tore the run down. Before the cancel-then-delete
1590
+ // path existed, clearing never aborted an in-flight run, so no
1591
+ // `update/error` was emitted; preserve that. Detect it off the
1592
+ // handle's `aborted` flag rather than the message, because
1593
+ // sanitizeErrorMessageForEvent collapses "aborted" to the generic
1594
+ // "IO_IOException" tag and would be indistinguishable from a real
1595
+ // I/O failure.
1596
+ val intentionallyCancelled = cancelHandle?.aborted?.get() == true
1597
+ // A duplicate-dest single-flight rejection ("Already downloading")
1598
+ // is NOT a failure of any in-flight download — it means another
1599
+ // run already owns this dest. The original global-boolean guard
1600
+ // threw this BEFORE the try, so no `update/error` was emitted;
1601
+ // preserve that so listeners don't observe a spurious error for the
1602
+ // rejected caller while the real download is still progressing.
1603
+ if (sanitized != "Already downloading" && !intentionallyCancelled) {
1604
+ sendEvent("update/error", message = sanitized)
1605
+ }
1549
1606
  // Rethrow with the same sanitized message so the Promise
1550
1607
  // rejection surfacing to JS carries no /data/user/<u>/<pkg>/
1551
1608
  // paths. Without this rewrap, FileNotFoundException etc.
@@ -1555,7 +1612,16 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1555
1612
  // native crash reporter) still sees the full chain.
1556
1613
  throw Exception(sanitized, e)
1557
1614
  } finally {
1558
- isDownloading.set(false)
1615
+ // Release THIS run's per-dest single-flight lock. Keyed remove
1616
+ // (only if we actually acquired it — input-validation failures
1617
+ // throw before acquisition, leaving acquiredKey null). The
1618
+ // value-checked remove avoids deleting an entry a later run for
1619
+ // the same dest may have re-registered after a concurrent cancel.
1620
+ val key = acquiredKey
1621
+ val handle = cancelHandle
1622
+ if (key != null && handle != null) {
1623
+ activeDownloads.remove(key, handle)
1624
+ }
1559
1625
  }
1560
1626
  }
1561
1627
  }
@@ -1784,10 +1850,28 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1784
1850
  }
1785
1851
  }
1786
1852
 
1853
+ // OCDS §5.8 cancel-then-delete. Stop every in-flight concurrent run's worker
1854
+ // pool (flip its abort flag + shutdownNow) BEFORE the caller deletes the
1855
+ // download directory. Without this, a live segment worker could re-create a
1856
+ // `.segN` we just deleted (CRD streams into `.segN` via append). Removing each
1857
+ // entry here also makes the dest immediately re-acquirable. The in-flight
1858
+ // download's own `finally` does a value-checked remove, so racing it is safe.
1859
+ private fun cancelAllActiveDownloads() {
1860
+ val keys = activeDownloads.keys.toList()
1861
+ for (key in keys) {
1862
+ activeDownloads.remove(key)?.let {
1863
+ OneKeyLog.info("BundleUpdate", "cancelling in-flight download: $key")
1864
+ it.cancel()
1865
+ }
1866
+ }
1867
+ }
1868
+
1787
1869
  override fun clearDownload(): Promise<Unit> {
1788
1870
  return Promise.async {
1789
1871
  OneKeyLog.info("BundleUpdate", "clearDownload: clearing download directory...")
1790
1872
  val context = getContext()
1873
+ // Cancel-then-delete: stop live workers before removing files.
1874
+ cancelAllActiveDownloads()
1791
1875
  val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
1792
1876
  if (downloadDir.exists()) {
1793
1877
  BundleUpdateStoreAndroid.deleteDir(downloadDir)
@@ -1795,7 +1879,6 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1795
1879
  } else {
1796
1880
  OneKeyLog.info("BundleUpdate", "clearDownload: download directory does not exist, skipping")
1797
1881
  }
1798
- isDownloading.set(false)
1799
1882
  OneKeyLog.info("BundleUpdate", "clearDownload: completed")
1800
1883
  }
1801
1884
  }
@@ -1805,6 +1888,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1805
1888
  return Promise.async {
1806
1889
  OneKeyLog.info("BundleUpdate", "clearBundle: clearing download and bundle directories...")
1807
1890
  val context = getContext()
1891
+ // Cancel-then-delete: stop live workers before removing files.
1892
+ cancelAllActiveDownloads()
1808
1893
  // Clear download directory
1809
1894
  val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
1810
1895
  if (downloadDir.exists()) {
@@ -1817,7 +1902,6 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1817
1902
  BundleUpdateStoreAndroid.deleteDir(bundleDir)
1818
1903
  OneKeyLog.info("BundleUpdate", "clearBundle: bundle directory deleted")
1819
1904
  }
1820
- isDownloading.set(false)
1821
1905
  OneKeyLog.info("BundleUpdate", "clearBundle: completed")
1822
1906
  }
1823
1907
  }
@@ -985,6 +985,11 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
985
985
  /// Read by the background-snapshot handler so it knows where to drop the
986
986
  /// `.resume` sidecar.
987
987
  private var activeDownloadFilePath: String?
988
+ /// OCDS §5.7: the highest progress percent already reported to JS for the
989
+ /// in-flight download. Used to floor single-stream progress so the bar never
990
+ /// moves backward at the concurrent→single-stream seam. Guarded by
991
+ /// `stateQueue`. Reset to 0 only on a genuine restart (permanent fallback).
992
+ private var lastReportedProgress: Int = 0
988
993
  private var didEnterBackgroundObserver: NSObjectProtocol?
989
994
 
990
995
  override init() {
@@ -1228,7 +1233,13 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
1228
1233
  // below; the range downloader keeps its `.segN` files for the next
1229
1234
  // attempt (its background session resumes them by taskId).
1230
1235
  self.sendEvent(type: "update/start")
1231
- self.stateQueue.sync { self.activeDownloadFilePath = filePath }
1236
+ // §5.7: a fresh downloadBundle starts the progress floor at 0; the
1237
+ // concurrent path raises it as bytes land (below), and a transient
1238
+ // single-stream fallback keeps it.
1239
+ self.stateQueue.sync {
1240
+ self.activeDownloadFilePath = filePath
1241
+ self.lastReportedProgress = 0
1242
+ }
1232
1243
 
1233
1244
  // Stable, unique task id for this bundle. Same value across retry
1234
1245
  // attempts so the range downloader can resume its `.segN` files, and
@@ -1241,31 +1252,88 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
1241
1252
  // "fallback" are surfaced by this method's own sendEvent calls and
1242
1253
  // the download outcome below, so we don't double-emit them here.
1243
1254
  let progressListenerId = RangeDownloader.shared.addListener { [weak self] event in
1244
- guard event.channel.stringValue == DownloadChannel.bundle.stringValue,
1255
+ guard let self = self,
1256
+ event.channel.stringValue == DownloadChannel.bundle.stringValue,
1245
1257
  event.taskId == rangeTaskId,
1246
1258
  event.type == "progress" else { return }
1247
- self?.sendEvent(type: "update/downloading", progress: Int(event.progress))
1248
- }
1249
-
1250
- let (rangeOutcome, _, rangeReason) = await RangeDownloader.shared.download(
1251
- channel: .bundle,
1252
- taskId: rangeTaskId,
1253
- urlString: downloadUrl,
1254
- filePath: filePath,
1255
- // SHA256 is verified by this module right after assembly (below),
1256
- // so we don't double-hash inside the range downloader.
1257
- expectedSha256: nil,
1258
- segmentCount: nil,
1259
- minConcurrentBytes: nil
1260
- )
1259
+ // §5.7: record the concurrent floor so a later single-stream
1260
+ // transient fallback resumes the bar from here, not from 0.
1261
+ let floored: Int = self.stateQueue.sync {
1262
+ let clamped = max(self.lastReportedProgress, Int(event.progress))
1263
+ self.lastReportedProgress = clamped
1264
+ return clamped
1265
+ }
1266
+ self.sendEvent(type: "update/downloading", progress: floored)
1267
+ }
1268
+
1269
+ // A `.fallback` outcome covers two very different situations whose
1270
+ // recovery must NOT be the same:
1271
+ // • Permanent — Range unsupported / file too small / server 200 /
1272
+ // SHA mismatch. The downloader has already cleaned its `.segN`
1273
+ // segments, so none survive; single-stream is the only way.
1274
+ // • Transient — a network drop or app suspend interrupted a segment
1275
+ // mid-flight ("segment N missing/truncated"). The downloader
1276
+ // RETAINS the segments that DID complete; the next concurrent
1277
+ // attempt resumes only the missing one(s).
1278
+ // This method used to `discardArtifacts` on EVERY fallback and restart
1279
+ // single-stream from byte 0, so any background/lock/network blip threw
1280
+ // away tens of MB and the user saw progress reset to 0. We now retry
1281
+ // the concurrent path while resumable segments survive, and fall back
1282
+ // to single-stream only as a last resort — WITHOUT deleting those
1283
+ // segments, so a later attempt can still resume them.
1284
+ let maxConcurrentAttempts = 3
1285
+ var concurrentAttempt = 0
1286
+ // OCDS §4: consume the EXPLICIT typed class from the in-process core
1287
+ // (`RangeDownloadClass`) instead of inferring transient-vs-permanent
1288
+ // from whether `.segN` happens to survive on disk. The wire enum stays
1289
+ // `completed | fallback` until nitrogen regen; we call the in-process
1290
+ // `download(...)` directly, so we get the precise class here.
1291
+ var rangeClass: RangeDownloadClass = .fallbackPermanent
1292
+ var rangeReason: String?
1293
+ while true {
1294
+ concurrentAttempt += 1
1295
+ (rangeClass, _, rangeReason, _) = await RangeDownloader.shared.download(
1296
+ channel: .bundle,
1297
+ taskId: rangeTaskId,
1298
+ urlString: downloadUrl,
1299
+ filePath: filePath,
1300
+ // SHA256 is verified by this module right after assembly (below),
1301
+ // so we don't double-hash inside the range downloader.
1302
+ expectedSha256: nil,
1303
+ segmentCount: nil,
1304
+ minConcurrentBytes: nil
1305
+ )
1306
+ if rangeClass == .completed {
1307
+ break // completed
1308
+ }
1309
+ // §4: a Transient class is resumable — retry the concurrent path.
1310
+ // The core keeps `.segN`; the next call resumes only what's missing.
1311
+ if rangeClass == .fallbackTransient,
1312
+ concurrentAttempt < maxConcurrentAttempts {
1313
+ OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent transient fallback (\(rangeReason ?? "")), resuming surviving segments (attempt \(concurrentAttempt)/\(maxConcurrentAttempts))")
1314
+ // Brief backoff so a flapping network / suspend settles before
1315
+ // we re-probe and resume the missing segment(s).
1316
+ try? await Task.sleep(nanoseconds: 1_500_000_000)
1317
+ continue
1318
+ }
1319
+ break // permanent, or transient retries exhausted
1320
+ }
1261
1321
  RangeDownloader.shared.removeListener(progressListenerId)
1262
1322
 
1263
- if rangeOutcome.stringValue == RangeDownloadOutcome.fallback.stringValue {
1264
- // Concurrent path unavailable (Range unsupported / file too small /
1265
- // server returned 200 / transient error). Hand off to the
1266
- // single-stream path, leaving a clean slot.
1267
- OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent fallback (\(rangeReason ?? "")), using single-stream")
1268
- RangeDownloader.shared.discardArtifacts(filePath: filePath)
1323
+ if rangeClass != .completed {
1324
+ if rangeClass == .fallbackTransient {
1325
+ // §4 Transient that outlived our concurrent retries: KEEP the
1326
+ // segments so the next downloadBundle call's concurrent path can
1327
+ // resume them. Single-stream is just this call's safety net, and
1328
+ // its progress is floored (below) so the bar never drops to 0.
1329
+ OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent transient fallback persists (\(rangeReason ?? "")), using single-stream (segments preserved for resume)")
1330
+ } else {
1331
+ // §4 Permanent (segments already cleaned by the downloader):
1332
+ // clear any residue and hand off to single-stream from a clean
1333
+ // slot; this is a genuine restart, so the progress floor resets.
1334
+ OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent permanent fallback (\(rangeReason ?? "")), using single-stream")
1335
+ RangeDownloader.shared.discardArtifacts(filePath: filePath)
1336
+ }
1269
1337
  // fall through to the single-stream path below
1270
1338
  } else {
1271
1339
  OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent finished, verifying SHA256...")
@@ -1312,8 +1380,24 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
1312
1380
  throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download delegate not initialized"])
1313
1381
  }
1314
1382
  delegate.reset()
1383
+ // OCDS §5.7: reported progress is monotonic non-decreasing within a
1384
+ // run, and resets only on a GENUINE restart. The single-stream
1385
+ // delegate emits its own 0→100, which would jump the bar backward at
1386
+ // the concurrent→single-stream seam. Floor it: a transient fallback
1387
+ // KEEPS the floor (the concurrent path already reported ~N%), a
1388
+ // permanent fallback is a genuine restart so the floor resets to 0.
1389
+ let progressFloor = (rangeClass == .fallbackTransient)
1390
+ ? self.stateQueue.sync { self.lastReportedProgress }
1391
+ : 0
1392
+ self.stateQueue.sync { self.lastReportedProgress = progressFloor }
1315
1393
  delegate.onProgress = { [weak self] progress in
1316
- self?.sendEvent(type: "update/downloading", progress: progress)
1394
+ guard let self = self else { return }
1395
+ let floored: Int = self.stateQueue.sync {
1396
+ let clamped = max(self.lastReportedProgress, progress)
1397
+ self.lastReportedProgress = clamped
1398
+ return clamped
1399
+ }
1400
+ self.sendEvent(type: "update/downloading", progress: floored)
1317
1401
  }
1318
1402
 
1319
1403
  // Anchor for the background-snapshot handler. Set BEFORE
@@ -1421,6 +1505,11 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
1421
1505
  throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle SHA256 verification failed: \(reason)"])
1422
1506
  }
1423
1507
 
1508
+ // A verified single-stream file is authoritative — drop any
1509
+ // concurrent `.segN` segments we intentionally kept across a
1510
+ // transient fallback so they don't linger as orphans.
1511
+ RangeDownloader.shared.discardArtifacts(filePath: filePath)
1512
+
1424
1513
  self.sendEvent(type: "update/complete")
1425
1514
  OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
1426
1515
  return result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-bundle-update",
3
- "version": "3.0.67",
3
+ "version": "3.0.69",
4
4
  "description": "react-native-bundle-update",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",