@capgo/capacitor-updater 8.41.13 → 8.42.0

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.
@@ -84,7 +84,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
84
84
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
85
85
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
86
86
 
87
- private final String pluginVersion = "8.41.13";
87
+ private final String pluginVersion = "8.42.0";
88
88
  private static final String DELAY_CONDITION_PREFERENCES = "";
89
89
 
90
90
  private SharedPreferences.Editor editor;
@@ -35,8 +35,11 @@ import java.util.Objects;
35
35
  import java.util.Set;
36
36
  import java.util.concurrent.CompletableFuture;
37
37
  import java.util.concurrent.ConcurrentHashMap;
38
+ import java.util.concurrent.CopyOnWriteArrayList;
38
39
  import java.util.concurrent.ExecutorService;
39
40
  import java.util.concurrent.Executors;
41
+ import java.util.concurrent.ScheduledExecutorService;
42
+ import java.util.concurrent.ScheduledFuture;
40
43
  import java.util.concurrent.TimeUnit;
41
44
  import java.util.zip.ZipEntry;
42
45
  import java.util.zip.ZipInputStream;
@@ -91,6 +94,12 @@ public class CapgoUpdater {
91
94
  // Flag to track if we've already sent the rate limit statistic - prevents infinite loop
92
95
  private static volatile boolean rateLimitStatisticSent = false;
93
96
 
97
+ // Stats batching - queue events and send max once per second
98
+ private final List<JSONObject> statsQueue = new CopyOnWriteArrayList<>();
99
+ private final ScheduledExecutorService statsScheduler = Executors.newSingleThreadScheduledExecutor();
100
+ private ScheduledFuture<?> statsFlushTask = null;
101
+ private static final long STATS_FLUSH_INTERVAL_MS = 1000;
102
+
94
103
  private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
95
104
  private final ExecutorService io = Executors.newSingleThreadExecutor();
96
105
 
@@ -1450,30 +1459,74 @@ public class CapgoUpdater {
1450
1459
  if (statsUrl == null || statsUrl.isEmpty()) {
1451
1460
  return;
1452
1461
  }
1462
+
1453
1463
  JSONObject json;
1454
1464
  try {
1455
1465
  json = this.createInfoObject();
1456
1466
  json.put("version_name", versionName);
1457
1467
  json.put("old_version_name", oldVersionName);
1458
1468
  json.put("action", action);
1469
+ json.put("timestamp", System.currentTimeMillis());
1459
1470
  } catch (JSONException e) {
1460
1471
  logger.error("Error preparing stats");
1461
1472
  logger.debug("JSONException: " + e.getMessage());
1462
1473
  return;
1463
1474
  }
1464
1475
 
1476
+ statsQueue.add(json);
1477
+ ensureStatsTimerStarted();
1478
+ }
1479
+
1480
+ private synchronized void ensureStatsTimerStarted() {
1481
+ if (statsFlushTask == null || statsFlushTask.isCancelled() || statsFlushTask.isDone()) {
1482
+ statsFlushTask = statsScheduler.scheduleAtFixedRate(
1483
+ this::flushStatsQueue,
1484
+ STATS_FLUSH_INTERVAL_MS,
1485
+ STATS_FLUSH_INTERVAL_MS,
1486
+ TimeUnit.MILLISECONDS
1487
+ );
1488
+ }
1489
+ }
1490
+
1491
+ private void flushStatsQueue() {
1492
+ if (statsQueue.isEmpty()) {
1493
+ return;
1494
+ }
1495
+
1496
+ String statsUrl = this.statsUrl;
1497
+ if (statsUrl == null || statsUrl.isEmpty()) {
1498
+ statsQueue.clear();
1499
+ return;
1500
+ }
1501
+
1502
+ // Copy and clear the queue atomically using synchronized block
1503
+ List<JSONObject> eventsToSend;
1504
+ synchronized (statsQueue) {
1505
+ if (statsQueue.isEmpty()) {
1506
+ return;
1507
+ }
1508
+ eventsToSend = new ArrayList<>(statsQueue);
1509
+ statsQueue.clear();
1510
+ }
1511
+
1512
+ JSONArray jsonArray = new JSONArray();
1513
+ for (JSONObject event : eventsToSend) {
1514
+ jsonArray.put(event);
1515
+ }
1516
+
1465
1517
  Request request = new Request.Builder()
1466
1518
  .url(statsUrl)
1467
- .post(RequestBody.create(json.toString(), MediaType.get("application/json")))
1519
+ .post(RequestBody.create(jsonArray.toString(), MediaType.get("application/json")))
1468
1520
  .build();
1469
1521
 
1522
+ final int eventCount = eventsToSend.size();
1470
1523
  DownloadService.sharedClient
1471
1524
  .newCall(request)
1472
1525
  .enqueue(
1473
1526
  new okhttp3.Callback() {
1474
1527
  @Override
1475
1528
  public void onFailure(@NonNull Call call, @NonNull IOException e) {
1476
- logger.error("Failed to send stats");
1529
+ logger.error("Failed to send stats batch");
1477
1530
  logger.debug("Error: " + e.getMessage());
1478
1531
  }
1479
1532
 
@@ -1486,10 +1539,10 @@ public class CapgoUpdater {
1486
1539
  }
1487
1540
 
1488
1541
  if (response.isSuccessful()) {
1489
- logger.info("Stats sent successfully");
1490
- logger.debug("Action: " + action + ", Version: " + versionName);
1542
+ logger.info("Stats batch sent successfully");
1543
+ logger.debug("Sent " + eventCount + " events");
1491
1544
  } else {
1492
- logger.error("Error sending stats");
1545
+ logger.error("Error sending stats batch");
1493
1546
  logger.debug("Response code: " + response.code());
1494
1547
  }
1495
1548
  }
@@ -1623,4 +1676,30 @@ public class CapgoUpdater {
1623
1676
  this.editor.commit();
1624
1677
  return true;
1625
1678
  }
1679
+
1680
+ /**
1681
+ * Shuts down the stats scheduler and flushes any pending stats.
1682
+ * Should be called when the plugin is destroyed to prevent resource leaks.
1683
+ */
1684
+ public void shutdown() {
1685
+ // Cancel the scheduled task
1686
+ if (statsFlushTask != null) {
1687
+ statsFlushTask.cancel(false);
1688
+ statsFlushTask = null;
1689
+ }
1690
+
1691
+ // Flush any remaining stats before shutdown
1692
+ flushStatsQueue();
1693
+
1694
+ // Shutdown the scheduler
1695
+ statsScheduler.shutdown();
1696
+ try {
1697
+ if (!statsScheduler.awaitTermination(2, TimeUnit.SECONDS)) {
1698
+ statsScheduler.shutdownNow();
1699
+ }
1700
+ } catch (InterruptedException e) {
1701
+ statsScheduler.shutdownNow();
1702
+ Thread.currentThread().interrupt();
1703
+ }
1704
+ }
1626
1705
  }
@@ -71,7 +71,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
71
71
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
72
72
  ]
73
73
  public var implementation = CapgoUpdater()
74
- private let pluginVersion: String = "8.41.13"
74
+ private let pluginVersion: String = "8.42.0"
75
75
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
76
76
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
77
77
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -48,6 +48,12 @@ import UIKit
48
48
  // Flag to track if we've already sent the rate limit statistic - prevents infinite loop
49
49
  private static var rateLimitStatisticSent = false
50
50
 
51
+ // Stats batching - queue events and send max once per second
52
+ private var statsQueue: [StatsEvent] = []
53
+ private let statsQueueLock = NSLock()
54
+ private var statsFlushTimer: Timer?
55
+ private static let statsFlushInterval: TimeInterval = 1.0
56
+
51
57
  private var userAgent: String {
52
58
  let safePluginVersion = pluginVersion.isEmpty ? "unknown" : pluginVersion
53
59
  let safeAppId = appId.isEmpty ? "unknown" : appId
@@ -70,6 +76,15 @@ import UIKit
70
76
  self.logger = logger
71
77
  }
72
78
 
79
+ deinit {
80
+ // Invalidate the stats timer to prevent memory leaks
81
+ statsFlushTimer?.invalidate()
82
+ statsFlushTimer = nil
83
+
84
+ // Flush any remaining stats before deallocation
85
+ flushStatsQueue()
86
+ }
87
+
73
88
  private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
74
89
  return (percent * (max - min)) / 100 + min
75
90
  }
@@ -1675,21 +1690,70 @@ import UIKit
1675
1690
  guard !statsUrl.isEmpty else {
1676
1691
  return
1677
1692
  }
1678
- operationQueue.maxConcurrentOperationCount = 1
1679
1693
 
1680
- let versionName = versionName ?? getCurrentBundle().getVersionName()
1694
+ let resolvedVersionName = versionName ?? getCurrentBundle().getVersionName()
1695
+ let info = createInfoObject()
1696
+
1697
+ let event = StatsEvent(
1698
+ platform: info.platform,
1699
+ device_id: info.device_id,
1700
+ app_id: info.app_id,
1701
+ custom_id: info.custom_id,
1702
+ version_build: info.version_build,
1703
+ version_code: info.version_code,
1704
+ version_os: info.version_os,
1705
+ version_name: resolvedVersionName,
1706
+ old_version_name: oldVersionName ?? "",
1707
+ plugin_version: info.plugin_version,
1708
+ is_emulator: info.is_emulator,
1709
+ is_prod: info.is_prod,
1710
+ action: action,
1711
+ channel: info.channel,
1712
+ defaultChannel: info.defaultChannel,
1713
+ key_id: info.key_id,
1714
+ timestamp: Int64(Date().timeIntervalSince1970 * 1000)
1715
+ )
1681
1716
 
1682
- var parameters = createInfoObject()
1683
- parameters.action = action
1684
- parameters.version_name = versionName
1685
- parameters.old_version_name = oldVersionName ?? ""
1717
+ statsQueueLock.lock()
1718
+ statsQueue.append(event)
1719
+ statsQueueLock.unlock()
1720
+
1721
+ ensureStatsTimerStarted()
1722
+ }
1723
+
1724
+ private func ensureStatsTimerStarted() {
1725
+ DispatchQueue.main.async { [weak self] in
1726
+ guard let self = self else { return }
1727
+ if self.statsFlushTimer == nil || !self.statsFlushTimer!.isValid {
1728
+ // Use closure-based timer to avoid strong reference cycle
1729
+ self.statsFlushTimer = Timer.scheduledTimer(
1730
+ withTimeInterval: CapgoUpdater.statsFlushInterval,
1731
+ repeats: true
1732
+ ) { [weak self] _ in
1733
+ self?.flushStatsQueue()
1734
+ }
1735
+ }
1736
+ }
1737
+ }
1738
+
1739
+ private func flushStatsQueue() {
1740
+ statsQueueLock.lock()
1741
+ guard !statsQueue.isEmpty else {
1742
+ statsQueueLock.unlock()
1743
+ return
1744
+ }
1745
+ let eventsToSend = statsQueue
1746
+ statsQueue.removeAll()
1747
+ statsQueueLock.unlock()
1748
+
1749
+ operationQueue.maxConcurrentOperationCount = 1
1686
1750
 
1687
1751
  let operation = BlockOperation {
1688
1752
  let semaphore = DispatchSemaphore(value: 0)
1689
1753
  self.alamofireSession.request(
1690
1754
  self.statsUrl,
1691
1755
  method: .post,
1692
- parameters: parameters,
1756
+ parameters: eventsToSend,
1693
1757
  encoder: JSONParameterEncoder.default,
1694
1758
  requestModifier: { $0.timeoutInterval = self.timeout }
1695
1759
  ).responseData { response in
@@ -1701,10 +1765,10 @@ import UIKit
1701
1765
 
1702
1766
  switch response.result {
1703
1767
  case .success:
1704
- self.logger.info("Stats sent successfully")
1705
- self.logger.debug("Action: \(action), Version: \(versionName)")
1768
+ self.logger.info("Stats batch sent successfully")
1769
+ self.logger.debug("Sent \(eventsToSend.count) events")
1706
1770
  case let .failure(error):
1707
- self.logger.error("Error sending stats")
1771
+ self.logger.error("Error sending stats batch")
1708
1772
  self.logger.debug("Response: \(response.value?.debugDescription ?? "nil"), Error: \(error.localizedDescription)")
1709
1773
  }
1710
1774
  semaphore.signal()
@@ -1712,7 +1776,6 @@ import UIKit
1712
1776
  semaphore.wait()
1713
1777
  }
1714
1778
  operationQueue.addOperation(operation)
1715
-
1716
1779
  }
1717
1780
 
1718
1781
  public func getBundleInfo(id: String?) -> BundleInfo {
@@ -135,6 +135,28 @@ struct InfoObject: Codable {
135
135
  }
136
136
  // swiftlint:enable identifier_name
137
137
 
138
+ // swiftlint:disable identifier_name
139
+ struct StatsEvent: Codable {
140
+ let platform: String?
141
+ let device_id: String?
142
+ let app_id: String?
143
+ let custom_id: String?
144
+ let version_build: String?
145
+ let version_code: String?
146
+ let version_os: String?
147
+ let version_name: String?
148
+ let old_version_name: String?
149
+ let plugin_version: String?
150
+ let is_emulator: Bool?
151
+ let is_prod: Bool?
152
+ let action: String?
153
+ let channel: String?
154
+ let defaultChannel: String?
155
+ let key_id: String?
156
+ let timestamp: Int64
157
+ }
158
+ // swiftlint:enable identifier_name
159
+
138
160
  // swiftlint:disable identifier_name
139
161
  public struct ManifestEntry: Codable {
140
162
  let file_name: String?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.41.13",
3
+ "version": "8.42.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",