@otakit/capacitor-updater 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -7,7 +7,8 @@ Capacitor OTA updater plugin for OtaKit.
7
7
  - checks the manifest endpoint for a newer bundle
8
8
  - downloads and verifies OTA bundles
9
9
  - stages updates safely
10
- - activates them on the next launch or immediately during startup
10
+ - activates them on the next launch, next resume, or immediately
11
+ - checks for updates on cold start and app resume (configurable interval)
11
12
  - also supports fully manual update prompts when the app wants control
12
13
  - requires `notifyAppReady()` as the success handshake
13
14
  - rolls back automatically if the new bundle does not prove healthy
@@ -45,6 +46,7 @@ plugins: {
45
46
  appReadyTimeout: 10000,
46
47
  // Optional:
47
48
  // channel: "staging",
49
+ // updateMode: "next-resume",
48
50
  // updateMode: "manual",
49
51
  // updateMode: "immediate",
50
52
  }
@@ -74,14 +76,32 @@ In the hosted path, managed signing keys are already built in.
74
76
 
75
77
  ## Update modes
76
78
 
79
+ All automatic modes check for updates on **cold start** (always) and **app
80
+ resume**.
81
+
82
+ ### Production modes
83
+
84
+ - `next-launch` (default)
85
+ check and download in the background on cold start and resume.
86
+ activate the staged bundle only on the next cold start.
87
+ zero disruption during a session — the user never sees a surprise reload.
88
+
89
+ - `next-resume` (more eager)
90
+ check and download in the background on cold start and resume.
91
+ activate the staged bundle on the next resume or cold start.
92
+
93
+ ### Manual mode
94
+
77
95
  - `manual`
78
- no automatic startup check and no automatic staged activation
79
- - `next-launch`
80
- automatic startup check, download in the background, activate on the next cold launch
96
+ no automatic checks, no automatic staged activation.
97
+ the app integration drives everything via `check()`, `download()`, `apply()`, or `update()`.
98
+
99
+ ### Dev/debug mode
100
+
81
101
  - `immediate`
82
- automatic startup check, then download and activate during startup
102
+ checks, downloads, and activates in one shot as soon as possible on cold start and resume (the user may briefly see the previous version before a reload).
103
+ primarily for development and testing — not recommended for production.
83
104
 
84
- The default is `next-launch`.
85
105
 
86
106
  ## Runtime model
87
107
 
@@ -127,7 +147,8 @@ await OtaKit.notifyAppReady();
127
147
  ```
128
148
 
129
149
  The plugin handles checking, downloading, activation, and rollback based on
130
- `updateMode`.
150
+ `updateMode`. It checks on cold start and every time the app comes back from
151
+ the background (throttled by `checkInterval`).
131
152
 
132
153
  For most apps, this is the entire runtime integration.
133
154
 
@@ -154,6 +175,13 @@ await OtaKit.apply();
154
175
  Use the split flow when the app wants to download in the background and switch
155
176
  only after explicit user confirmation.
156
177
 
178
+ ## Throttle
179
+
180
+ All server checks are rate-limited by `checkInterval` (default 10 min).
181
+ This applies to automatic resume checks and to manual `check()` / `download()`
182
+ calls. Within the interval, calls return the staged bundle if one exists, or
183
+ null.
184
+
157
185
  ## Retention and deletion
158
186
 
159
187
  - the builtin, current, fallback, and staged bundles are protected
@@ -48,6 +48,10 @@ final class BundleStore {
48
48
  return builtinVersion;
49
49
  }
50
50
 
51
+ SharedPreferences getPrefs() {
52
+ return prefs;
53
+ }
54
+
51
55
  BundleInfo builtinBundle() {
52
56
  return new BundleInfo(
53
57
  "builtin",
@@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
21
21
  import java.security.MessageDigest;
22
22
  import java.util.concurrent.ExecutorService;
23
23
  import java.util.concurrent.Executors;
24
+ import java.util.concurrent.atomic.AtomicBoolean;
24
25
 
25
26
  @CapacitorPlugin(name = "OtaKit")
26
27
  public class UpdaterPlugin extends Plugin {
@@ -39,11 +40,17 @@ public class UpdaterPlugin extends Plugin {
39
40
  private String appId;
40
41
  private String channel;
41
42
  private java.util.List<ManifestVerifier.KeyEntry> manifestKeys = new java.util.ArrayList<>();
43
+ private long checkIntervalMs = 600_000;
44
+ private final AtomicBoolean checkInProgress = new AtomicBoolean(false);
42
45
  private static final String DEFAULT_UPDATE_URL = "https://www.otakit.app/api/v1";
43
46
  private static final String API_PATH_SUFFIX = "/api/v1";
44
47
  private static final String UPDATE_MODE_MANUAL = "manual";
45
48
  private static final String UPDATE_MODE_NEXT_LAUNCH = "next-launch";
49
+ private static final String UPDATE_MODE_NEXT_RESUME = "next-resume";
46
50
  private static final String UPDATE_MODE_IMMEDIATE = "immediate";
51
+ private static final String KEY_LAST_CHECK_TIMESTAMP = "last_check_timestamp";
52
+ private static final String TRIGGER_LAUNCH = "launch";
53
+ private static final String TRIGGER_RESUME = "resume";
47
54
 
48
55
  @Override
49
56
  public void load() {
@@ -112,6 +119,7 @@ public class UpdaterPlugin extends Plugin {
112
119
  }
113
120
 
114
121
  this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
122
+ this.checkIntervalMs = Math.max(600_000, getConfig().getInt("checkInterval", 600_000));
115
123
 
116
124
  BundleInfo current = store.getCurrentBundle();
117
125
  if (current.status == BundleStatus.TRIAL) {
@@ -135,26 +143,84 @@ public class UpdaterPlugin extends Plugin {
135
143
  }
136
144
 
137
145
  if (isAutomaticUpdateMode()) {
138
- startAutoUpdate();
146
+ runAutomaticUpdate(TRIGGER_LAUNCH);
139
147
  }
140
148
  }
141
149
 
142
- private void startAutoUpdate() {
143
- executor.execute(() -> {
144
- try {
145
- BundleInfo downloaded = performCheckAndDownload(null, true);
146
- if (UPDATE_MODE_IMMEDIATE.equals(updateMode) && downloaded != null) {
150
+ @Override
151
+ protected void handleOnResume() {
152
+ super.handleOnResume();
153
+ if (isAutomaticUpdateMode()) {
154
+ runAutomaticUpdate(TRIGGER_RESUME);
155
+ }
156
+ }
157
+
158
+ private void runAutomaticUpdate(String trigger) {
159
+ // Resume-only guards
160
+ if (TRIGGER_RESUME.equals(trigger)) {
161
+ if (UPDATE_MODE_MANUAL.equals(updateMode)) return;
162
+
163
+ // next-resume and immediate: activate staged bundle on resume without server check
164
+ if ((UPDATE_MODE_NEXT_RESUME.equals(updateMode) || UPDATE_MODE_IMMEDIATE.equals(updateMode))) {
165
+ String stagedId = store.getStagedBundleId();
166
+ if (stagedId != null && store.getBundle(stagedId) != null) {
147
167
  activateStagedBundleForReload();
148
168
  reloadWebView();
169
+ return;
149
170
  }
150
- } catch (Exception e) {
151
- if (UPDATE_MODE_IMMEDIATE.equals(updateMode)) {
152
- android.util.Log.w("UpdateKit", "immediate startup update failed", e);
171
+ }
172
+
173
+ if (shouldThrottleCheck()) return;
174
+ }
175
+
176
+ // Acquire in-flight guard (submit-time)
177
+ if (!checkInProgress.compareAndSet(false, true)) return;
178
+
179
+ if (UPDATE_MODE_IMMEDIATE.equals(updateMode)) {
180
+ // Immediate: check+download, activate if found
181
+ executor.execute(() -> {
182
+ try {
183
+ BundleInfo result = performCheckAndDownload(null, true);
184
+ recordCheckTimestamp();
185
+ if (result != null) {
186
+ activateStagedBundleForReload();
187
+ reloadWebView();
188
+ }
189
+ } catch (Exception e) {
190
+ android.util.Log.w("OtaKit", "immediate update failed (" + trigger + ")", e);
191
+ } finally {
192
+ checkInProgress.set(false);
153
193
  }
194
+ });
195
+ return;
196
+ }
197
+
198
+ // next-launch / next-resume: background check+download (fire-and-forget)
199
+ executor.execute(() -> {
200
+ try {
201
+ performCheckAndDownload(null, true);
202
+ recordCheckTimestamp();
203
+ } catch (Exception ignored) {
204
+ // Check failed — timestamp not recorded, will retry on next trigger
205
+ } finally {
206
+ checkInProgress.set(false);
154
207
  }
155
208
  });
156
209
  }
157
210
 
211
+ private boolean shouldThrottleCheck() {
212
+ long lastCheck = store.getPrefs().getLong(KEY_LAST_CHECK_TIMESTAMP, 0);
213
+ if (lastCheck <= 0) return false;
214
+ long elapsed = System.currentTimeMillis() - lastCheck;
215
+ return elapsed < checkIntervalMs;
216
+ }
217
+
218
+ private void recordCheckTimestamp() {
219
+ store.getPrefs().edit()
220
+ .putLong(KEY_LAST_CHECK_TIMESTAMP, System.currentTimeMillis())
221
+ .apply();
222
+ }
223
+
158
224
  private boolean shouldActivateStagedOnLaunch() {
159
225
  return !UPDATE_MODE_MANUAL.equals(updateMode);
160
226
  }
@@ -175,12 +241,15 @@ public class UpdaterPlugin extends Plugin {
175
241
  if (UPDATE_MODE_MANUAL.equals(raw)) {
176
242
  return UPDATE_MODE_MANUAL;
177
243
  }
244
+ if (UPDATE_MODE_NEXT_RESUME.equals(raw)) {
245
+ return UPDATE_MODE_NEXT_RESUME;
246
+ }
178
247
  if (UPDATE_MODE_IMMEDIATE.equals(raw)) {
179
248
  return UPDATE_MODE_IMMEDIATE;
180
249
  }
181
250
 
182
251
  android.util.Log.w(
183
- "UpdateKit",
252
+ "OtaKit",
184
253
  "Unknown updateMode '" + raw + "', defaulting to 'next-launch'"
185
254
  );
186
255
  return UPDATE_MODE_NEXT_LAUNCH;
@@ -209,10 +278,34 @@ public class UpdaterPlugin extends Plugin {
209
278
 
210
279
  @PluginMethod
211
280
  public void check(PluginCall call) {
281
+ // Respect throttle — return staged bundle as a LatestVersion if available, else null
282
+ if (shouldThrottleCheck() || !checkInProgress.compareAndSet(false, true)) {
283
+ String stagedId = store.getStagedBundleId();
284
+ if (stagedId != null) {
285
+ BundleInfo staged = store.getBundle(stagedId);
286
+ if (staged != null) {
287
+ JSObject result = new JSObject();
288
+ result.put("version", staged.version);
289
+ result.put("url", "");
290
+ result.put("sha256", staged.sha256 != null ? staged.sha256 : "");
291
+ result.put("size", 0);
292
+ result.put("downloaded", true);
293
+ if (staged.releaseId != null) {
294
+ result.put("releaseId", staged.releaseId);
295
+ }
296
+ call.resolve(result);
297
+ return;
298
+ }
299
+ }
300
+ call.resolve((JSObject) null);
301
+ return;
302
+ }
212
303
  String targetChannel = resolveTargetChannel(null);
213
304
  executor.execute(() -> {
214
305
  try {
215
306
  ManifestClient.LatestManifest latest = fetchLatest(targetChannel);
307
+ // check() does NOT record timestamp — only download() and automatic paths do.
308
+ // This keeps check() -> download() working without the throttle blocking download().
216
309
  if (latest == null) {
217
310
  call.resolve((JSObject) null);
218
311
  } else {
@@ -221,15 +314,31 @@ public class UpdaterPlugin extends Plugin {
221
314
  }
222
315
  } catch (Exception e) {
223
316
  call.reject("check failed: " + e.getMessage());
317
+ } finally {
318
+ checkInProgress.set(false);
224
319
  }
225
320
  });
226
321
  }
227
322
 
228
323
  @PluginMethod
229
324
  public void download(PluginCall call) {
325
+ // Respect throttle — return staged info if available, else null
326
+ if (shouldThrottleCheck() || !checkInProgress.compareAndSet(false, true)) {
327
+ String stagedId = store.getStagedBundleId();
328
+ if (stagedId != null) {
329
+ BundleInfo staged = store.getBundle(stagedId);
330
+ if (staged != null) {
331
+ call.resolve(staged.toJSObject());
332
+ return;
333
+ }
334
+ }
335
+ call.resolve((JSObject) null);
336
+ return;
337
+ }
230
338
  executor.execute(() -> {
231
339
  try {
232
340
  BundleInfo bundle = performCheckAndDownload(null, true);
341
+ recordCheckTimestamp();
233
342
  if (bundle == null) {
234
343
  call.resolve((JSObject) null);
235
344
  } else {
@@ -237,6 +346,8 @@ public class UpdaterPlugin extends Plugin {
237
346
  }
238
347
  } catch (Exception e) {
239
348
  call.reject("download failed: " + e.getMessage());
349
+ } finally {
350
+ checkInProgress.set(false);
240
351
  }
241
352
  });
242
353
  }
@@ -57,7 +57,7 @@ export interface OtaKitState {
57
57
  export interface OtaKitDebugState extends OtaKitState {
58
58
  fallback: BundleInfo;
59
59
  }
60
- export type OtaKitUpdateMode = 'manual' | 'next-launch' | 'immediate';
60
+ export type OtaKitUpdateMode = 'manual' | 'next-launch' | 'next-resume' | 'immediate';
61
61
  export interface OtaKitManifestKey {
62
62
  kid: string;
63
63
  key: string;
@@ -72,6 +72,12 @@ export interface OtaKitConfig {
72
72
  channel?: string;
73
73
  /** Overall update behavior. Defaults to next-launch. */
74
74
  updateMode?: OtaKitUpdateMode;
75
+ /**
76
+ * Minimum milliseconds between automatic update checks.
77
+ * Applies to both resume-triggered and manual check()/download() calls.
78
+ * Debug APIs bypass this throttle. Defaults to 600000 (10 min).
79
+ */
80
+ checkInterval?: number;
75
81
  /** Milliseconds to wait for notifyAppReady(). Defaults to 10000. */
76
82
  appReadyTimeout?: number;
77
83
  /** Custom API base URL for self-hosted or custom servers. */
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
@@ -7,9 +7,15 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
7
7
  private enum UpdateMode: String {
8
8
  case manual
9
9
  case nextLaunch = "next-launch"
10
+ case nextResume = "next-resume"
10
11
  case immediate
11
12
  }
12
13
 
14
+ private enum Trigger {
15
+ case launch
16
+ case resume
17
+ }
18
+
13
19
  public let identifier = "UpdaterPlugin"
14
20
  public let jsName = "OtaKit"
15
21
  public let pluginMethods: [CAPPluginMethod] = [
@@ -39,8 +45,13 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
39
45
  private var channel: String?
40
46
  private var manifestKeys: [(kid: String, key: Data)] = []
41
47
  private var trialTimeoutWorkItem: DispatchWorkItem?
48
+ private var checkIntervalMs: Int = 600_000
49
+ private let isCheckInProgress = NSLock()
50
+ private var checkInProgress = false
51
+ private var foregroundObserver: NSObjectProtocol?
42
52
  private static let defaultUpdateURL = "https://www.otakit.app/api/v1"
43
53
  private static let apiPathSuffix = "/api/v1"
54
+ private static let lastCheckTimestampKey = "otakit_last_check_timestamp"
44
55
 
45
56
  public override func load() {
46
57
  let envUpdateUrl = ProcessInfo.processInfo.environment["OTAKIT_SERVER_URL"]
@@ -55,6 +66,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
55
66
  updateMode = resolveUpdateMode(configured: updateModeRaw)
56
67
  downloader = Downloader(allowInsecureUrls: allowInsecureUrls)
57
68
  appReadyTimeoutMs = max(1000, getConfig().getInt("appReadyTimeout", 10_000))
69
+ checkIntervalMs = max(600_000, getConfig().getInt("checkInterval", 600_000))
58
70
 
59
71
  let rawKeysValue = getConfig().getArray("manifestKeys")
60
72
  if let rawKeys = rawKeysValue as? [[String: String]] {
@@ -99,31 +111,124 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
99
111
  }
100
112
 
101
113
  if isAutomaticUpdateMode() {
102
- startAutoUpdate()
114
+ runAutomaticUpdate(trigger: .launch)
115
+ }
116
+
117
+ if isAutomaticUpdateMode() {
118
+ foregroundObserver = NotificationCenter.default.addObserver(
119
+ forName: UIApplication.willEnterForegroundNotification,
120
+ object: nil,
121
+ queue: .main
122
+ ) { [weak self] _ in
123
+ self?.handleAppWillEnterForeground()
124
+ }
103
125
  }
104
126
  }
105
127
 
106
- private func startAutoUpdate() {
107
- switch updateMode {
108
- case .manual:
109
- return
110
- case .nextLaunch:
128
+ deinit {
129
+ if let observer = foregroundObserver {
130
+ NotificationCenter.default.removeObserver(observer)
131
+ }
132
+ }
133
+
134
+ private func handleAppWillEnterForeground() {
135
+ runAutomaticUpdate(trigger: .resume)
136
+ }
137
+
138
+ private func runAutomaticUpdate(trigger: Trigger) {
139
+ // Resume-only guards
140
+ if trigger == .resume {
141
+ if updateMode == .manual { return }
142
+
143
+ // next-resume and immediate: activate staged bundle on resume without server check
144
+ if (updateMode == .nextResume || updateMode == .immediate),
145
+ let stagedId = store.getStagedBundleId(),
146
+ store.getBundle(id: stagedId) != nil {
147
+ activateStagedBundleForReload()
148
+ reloadWebView()
149
+ return
150
+ }
151
+
152
+ if shouldThrottleCheck() { return }
153
+ }
154
+
155
+ // Acquire in-flight guard (submit-time)
156
+ guard claimCheckInProgress() else { return }
157
+
158
+ // Immediate mode on cold start: block until check+download+activate completes
159
+ if updateMode == .immediate && trigger == .launch {
111
160
  Task {
112
- _ = try? await performCheckAndDownload(channel: nil, emitEvents: true)
161
+ defer { releaseCheckInProgress() }
162
+ do {
163
+ let result = try await performCheckAndDownload(channel: nil, emitEvents: true)
164
+ recordCheckTimestamp()
165
+ if result != nil {
166
+ activateStagedBundleForReload()
167
+ reloadWebView()
168
+ }
169
+ } catch {
170
+ print("[OtaKit] immediate startup update failed: \(error.localizedDescription)")
171
+ }
113
172
  }
114
- case .immediate:
173
+ return
174
+ }
175
+
176
+ // Immediate mode on resume: background check+download, activate when done
177
+ if updateMode == .immediate && trigger == .resume {
115
178
  Task {
179
+ defer { releaseCheckInProgress() }
116
180
  do {
117
- let downloaded = try await performCheckAndDownload(channel: nil, emitEvents: true)
118
- if downloaded != nil {
181
+ let result = try await performCheckAndDownload(channel: nil, emitEvents: true)
182
+ recordCheckTimestamp()
183
+ if result != nil {
119
184
  activateStagedBundleForReload()
120
185
  reloadWebView()
121
186
  }
122
187
  } catch {
123
- print("[UpdateKit] immediate startup update failed: \(error.localizedDescription)")
188
+ print("[OtaKit] immediate resume update failed: \(error.localizedDescription)")
124
189
  }
125
190
  }
191
+ return
126
192
  }
193
+
194
+ // next-launch / next-resume: background check+download (fire-and-forget)
195
+ Task {
196
+ defer { releaseCheckInProgress() }
197
+ do {
198
+ _ = try await performCheckAndDownload(channel: nil, emitEvents: true)
199
+ recordCheckTimestamp()
200
+ } catch {
201
+ // Check failed — timestamp not recorded, will retry on next trigger
202
+ }
203
+ }
204
+ }
205
+
206
+ private func shouldThrottleCheck() -> Bool {
207
+ let lastCheck = UserDefaults.standard.double(forKey: UpdaterPlugin.lastCheckTimestampKey)
208
+ guard lastCheck > 0 else { return false }
209
+ let elapsed = Date().timeIntervalSince1970 * 1000 - lastCheck
210
+ return elapsed < Double(checkIntervalMs)
211
+ }
212
+
213
+ private func recordCheckTimestamp() {
214
+ UserDefaults.standard.set(
215
+ Date().timeIntervalSince1970 * 1000,
216
+ forKey: UpdaterPlugin.lastCheckTimestampKey
217
+ )
218
+ }
219
+
220
+ private func claimCheckInProgress() -> Bool {
221
+ isCheckInProgress.lock()
222
+ defer { isCheckInProgress.unlock() }
223
+ if checkInProgress { return false }
224
+ checkInProgress = true
225
+ return true
226
+ }
227
+
228
+ private func releaseCheckInProgress() {
229
+ isCheckInProgress.lock()
230
+ defer { isCheckInProgress.unlock() }
231
+ checkInProgress = false
127
232
  }
128
233
 
129
234
  private func shouldActivateStagedOnLaunch() -> Bool {
@@ -141,10 +246,12 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
141
246
  return .nextLaunch
142
247
  case UpdateMode.manual.rawValue:
143
248
  return .manual
249
+ case UpdateMode.nextResume.rawValue:
250
+ return .nextResume
144
251
  case UpdateMode.immediate.rawValue:
145
252
  return .immediate
146
253
  default:
147
- print("[UpdateKit] Unknown updateMode '\(raw)', defaulting to 'next-launch'")
254
+ print("[OtaKit] Unknown updateMode '\(raw)', defaulting to 'next-launch'")
148
255
  return .nextLaunch
149
256
  }
150
257
  }
@@ -189,10 +296,33 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
189
296
  }
190
297
 
191
298
  @objc func check(_ call: CAPPluginCall) {
299
+ // Respect throttle — return staged bundle as a LatestVersion if available, else null
300
+ if shouldThrottleCheck() || !claimCheckInProgress() {
301
+ if let stagedId = store.getStagedBundleId(),
302
+ let staged = store.getBundle(id: stagedId) {
303
+ var payload: [String: Any] = [
304
+ "version": staged.version,
305
+ "url": "",
306
+ "sha256": staged.sha256 ?? "",
307
+ "size": 0,
308
+ "downloaded": true,
309
+ ]
310
+ if let releaseId = staged.releaseId {
311
+ payload["releaseId"] = releaseId
312
+ }
313
+ call.resolve(payload)
314
+ } else {
315
+ call.resolve()
316
+ }
317
+ return
318
+ }
192
319
  let targetChannel = resolveTargetChannel(nil)
193
320
  Task {
321
+ defer { releaseCheckInProgress() }
194
322
  do {
195
323
  let latest = try await fetchLatest(channel: targetChannel)
324
+ // check() does NOT record timestamp — only download() and automatic paths do.
325
+ // This keeps check() -> download() working without the throttle blocking download().
196
326
  if let latest {
197
327
  let staged = findMatchingStagedBundle(latest: latest, targetChannel: targetChannel)
198
328
  call.resolve(manifestToDictionary(latest, downloaded: staged != nil))
@@ -225,9 +355,21 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
225
355
  }
226
356
 
227
357
  @objc func download(_ call: CAPPluginCall) {
358
+ // Respect throttle — return staged info if available, else null
359
+ if shouldThrottleCheck() || !claimCheckInProgress() {
360
+ if let stagedId = store.getStagedBundleId(),
361
+ let staged = store.getBundle(id: stagedId) {
362
+ call.resolve(staged.toDictionary())
363
+ } else {
364
+ call.resolve()
365
+ }
366
+ return
367
+ }
228
368
  Task {
369
+ defer { releaseCheckInProgress() }
229
370
  do {
230
371
  let bundle = try await performCheckAndDownload(channel: nil, emitEvents: true)
372
+ recordCheckTimestamp()
231
373
  if let bundle {
232
374
  call.resolve(bundle.toDictionary())
233
375
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otakit/capacitor-updater",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Capacitor plugin for OTA updates",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",