capacitor-mobilecron 0.2.1 → 0.2.2

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.
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import Capacitor
3
+ import UIKit
3
4
 
4
5
  @objc(MobileCronPlugin)
5
6
  public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
@@ -14,29 +15,125 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
14
15
  CAPPluginMethod(name: "pauseAll", returnType: CAPPluginReturnPromise),
15
16
  CAPPluginMethod(name: "resumeAll", returnType: CAPPluginReturnPromise),
16
17
  CAPPluginMethod(name: "setMode", returnType: CAPPluginReturnPromise),
17
- CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise)
18
+ CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "__testNativeEvaluate", returnType: CAPPluginReturnPromise)
18
20
  ]
19
21
 
22
+ private static let storageKey = "mobilecron:state"
23
+
20
24
  private var jobs: [String: [String: Any]] = [:]
21
25
  private var paused = false
22
26
  private(set) var mode = "balanced"
23
27
  var currentMode: String { mode }
24
28
  private var bgManager: BGTaskManager?
25
29
 
30
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
31
+
26
32
  public override func load() {
27
33
  super.load()
34
+ loadState()
35
+ firePendingNativeEvents()
28
36
  let manager = BGTaskManager(plugin: self)
29
37
  manager.registerBGTasks()
30
38
  manager.scheduleRefresh()
31
39
  manager.scheduleProcessing(requiresExternalPower: true)
32
40
  self.bgManager = manager
41
+ NotificationCenter.default.addObserver(
42
+ self,
43
+ selector: #selector(appDidBecomeActive),
44
+ name: UIApplication.didBecomeActiveNotification,
45
+ object: nil
46
+ )
47
+ }
48
+
49
+ @objc private func appDidBecomeActive() {
50
+ firePendingNativeEvents()
33
51
  }
34
52
 
35
53
  func handleBackgroundWake(source: String) {
54
+ firePendingNativeEvents()
36
55
  notifyListeners("statusChanged", data: buildStatus())
37
56
  notifyListeners("nativeWake", data: ["source": source, "paused": paused])
38
57
  }
39
58
 
59
+ // ── Persistence ───────────────────────────────────────────────────────────
60
+
61
+ private func loadState() {
62
+ guard let raw = UserDefaults.standard.string(forKey: Self.storageKey),
63
+ let data = raw.data(using: .utf8),
64
+ let state = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
65
+ return
66
+ }
67
+ paused = (state["paused"] as? Bool) ?? false
68
+ mode = (state["mode"] as? String) ?? "balanced"
69
+ jobs = [:]
70
+ if let jobsArr = state["jobs"] as? [[String: Any]] {
71
+ for job in jobsArr {
72
+ if let id = job["id"] as? String, !id.isEmpty {
73
+ jobs[id] = job
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ private func saveState() {
80
+ var state: [String: Any] = [
81
+ "version": 1,
82
+ "paused": paused,
83
+ "mode": mode,
84
+ "jobs": Array(jobs.values)
85
+ ]
86
+ // Preserve any pendingNativeEvents written by NativeJobEvaluator
87
+ if let raw = UserDefaults.standard.string(forKey: Self.storageKey),
88
+ let data = raw.data(using: .utf8),
89
+ let existing = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
90
+ let pending = existing["pendingNativeEvents"] as? [[String: Any]],
91
+ !pending.isEmpty {
92
+ state["pendingNativeEvents"] = pending
93
+ }
94
+ guard let data = try? JSONSerialization.data(withJSONObject: state),
95
+ let raw = String(data: data, encoding: .utf8) else { return }
96
+ UserDefaults.standard.set(raw, forKey: Self.storageKey)
97
+ }
98
+
99
+ // ── Background wake ───────────────────────────────────────────────────────
100
+
101
+ /// Read pendingNativeEvents from storage, emit each as jobDue, then clear them.
102
+ private func firePendingNativeEvents() {
103
+ guard let raw = UserDefaults.standard.string(forKey: Self.storageKey),
104
+ let data = raw.data(using: .utf8),
105
+ var state = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
106
+ return
107
+ }
108
+
109
+ // Sync native-evaluated job fields (nextDueAt, lastFiredAt, consecutiveSkips) into memory.
110
+ if let jobsArr = state["jobs"] as? [[String: Any]] {
111
+ for nativeJob in jobsArr {
112
+ if let id = nativeJob["id"] as? String, !id.isEmpty, jobs[id] != nil {
113
+ jobs[id] = nativeJob
114
+ }
115
+ }
116
+ }
117
+
118
+ guard let pending = state["pendingNativeEvents"] as? [[String: Any]], !pending.isEmpty else {
119
+ return
120
+ }
121
+
122
+ for evt in pending {
123
+ notifyListeners("jobDue", data: evt)
124
+ }
125
+
126
+ // Clear pendingNativeEvents and write back updated jobs.
127
+ state.removeValue(forKey: "pendingNativeEvents")
128
+ state["jobs"] = Array(jobs.values)
129
+ if let newData = try? JSONSerialization.data(withJSONObject: state),
130
+ let newRaw = String(data: newData, encoding: .utf8) {
131
+ UserDefaults.standard.set(newRaw, forKey: Self.storageKey)
132
+ }
133
+ }
134
+
135
+ // ── Plugin methods ────────────────────────────────────────────────────────
136
+
40
137
  @objc func register(_ call: CAPPluginCall) {
41
138
  guard let name = call.getString("name")?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty else {
42
139
  call.reject("Job name is required")
@@ -58,6 +155,7 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
58
155
  if let data = call.getObject("data") { record["data"] = data }
59
156
 
60
157
  jobs[id] = record
158
+ saveState()
61
159
  notifyListeners("statusChanged", data: buildStatus())
62
160
  call.resolve(["id": id])
63
161
  }
@@ -68,6 +166,7 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
68
166
  return
69
167
  }
70
168
  jobs.removeValue(forKey: id)
169
+ saveState()
71
170
  notifyListeners("statusChanged", data: buildStatus())
72
171
  call.resolve()
73
172
  }
@@ -91,6 +190,7 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
91
190
  if call.options.keys.contains("data") { existing["data"] = call.getObject("data") }
92
191
 
93
192
  jobs[id] = existing
193
+ saveState()
94
194
  notifyListeners("statusChanged", data: buildStatus())
95
195
  call.resolve()
96
196
  }
@@ -124,12 +224,14 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
124
224
 
125
225
  @objc func pauseAll(_ call: CAPPluginCall) {
126
226
  paused = true
227
+ saveState()
127
228
  notifyListeners("statusChanged", data: buildStatus())
128
229
  call.resolve()
129
230
  }
130
231
 
131
232
  @objc func resumeAll(_ call: CAPPluginCall) {
132
233
  paused = false
234
+ saveState()
133
235
  notifyListeners("statusChanged", data: buildStatus())
134
236
  call.resolve()
135
237
  }
@@ -144,6 +246,7 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
144
246
  bgManager.scheduleRefresh()
145
247
  bgManager.scheduleProcessing(requiresExternalPower: mode != "aggressive")
146
248
  }
249
+ saveState()
147
250
  notifyListeners("statusChanged", data: buildStatus())
148
251
  call.resolve()
149
252
  }
@@ -152,6 +255,14 @@ public class MobileCronPlugin: CAPPlugin, CAPBridgedPlugin {
152
255
  call.resolve(buildStatus())
153
256
  }
154
257
 
258
+ /// E2E test hook: directly calls NativeJobEvaluator.evaluate() and fires pending events.
259
+ /// Used to test native background evaluation without BGTaskScheduler.
260
+ @objc func __testNativeEvaluate(_ call: CAPPluginCall) {
261
+ let events = NativeJobEvaluator.evaluate(source: "test_trigger")
262
+ firePendingNativeEvents()
263
+ call.resolve(["firedCount": events.count])
264
+ }
265
+
155
266
  private func buildStatus() -> [String: Any] {
156
267
  let diagnostics = bgManager?.status ?? .init()
157
268
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-mobilecron",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Capacitor scheduling primitive that emits job due events across web, Android, and iOS",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",