fix(dev): pending inspector stable times and refreshPending without nested busy
Expose wall-clock fire targets from the iOS NotificationInspector (scheduled_time userInfo and predictive_<epochMs> ids) so the debug panel is not misleading when nextTriggerDate resamples for interval triggers. Extend TS types and show the scheduled target in the UI, with a note when iOS nextTriggerDate diverges. Make refreshPending a plain fetch so mock refresh, wakeup ping, flood test, and clear notifications can refresh the pending list while an outer withBusy guard is already active.
This commit is contained in:
@@ -16,6 +16,29 @@ public class NotificationInspectorPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
]
|
||||
}
|
||||
|
||||
/// Stable wall-clock target: plugin `userInfo["scheduled_time"]`, or epoch ms in `predictive_<ms>` identifiers.
|
||||
/// (Apple documents `UNTimeIntervalNotificationTrigger.nextTriggerDate()` as resampling ~now+interval when queried.)
|
||||
private func wallClockMillis(from request: UNNotificationRequest) -> (ms: Int64, source: String)? {
|
||||
let info = request.content.userInfo
|
||||
if let v = info["scheduled_time"] as? Int64 {
|
||||
return (v, "userInfo.scheduled_time")
|
||||
}
|
||||
if let n = info["scheduled_time"] as? NSNumber {
|
||||
return (n.int64Value, "userInfo.scheduled_time")
|
||||
}
|
||||
if let i = info["scheduled_time"] as? Int {
|
||||
return (Int64(i), "userInfo.scheduled_time")
|
||||
}
|
||||
let prefix = "predictive_"
|
||||
if request.identifier.hasPrefix(prefix) {
|
||||
let suffix = String(request.identifier.dropFirst(prefix.count))
|
||||
if let ms = Int64(suffix) {
|
||||
return (ms, "identifier(predictive_)")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc public func getPendingNotifications(_ call: CAPPluginCall) {
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
let pending: [[String: Any]] = requests.map { req in
|
||||
@@ -43,6 +66,13 @@ public class NotificationInspectorPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
]
|
||||
obj["nextTriggerDate"] = nextTriggerMs ?? NSNull()
|
||||
obj["triggerType"] = triggerType ?? NSNull()
|
||||
if let wall = self.wallClockMillis(from: req) {
|
||||
obj["wallClockMillis"] = NSNumber(value: wall.ms)
|
||||
obj["wallClockSource"] = wall.source
|
||||
} else {
|
||||
obj["wallClockMillis"] = NSNull()
|
||||
obj["wallClockSource"] = NSNull()
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
|
||||
@@ -91,13 +91,37 @@
|
||||
<li
|
||||
v-for="p in pending"
|
||||
:key="p.identifier"
|
||||
class="px-3 py-2 text-sm flex gap-3 items-center"
|
||||
class="px-3 py-2 text-sm flex gap-3 items-start"
|
||||
>
|
||||
<code class="truncate">{{ p.identifier }}</code>
|
||||
<span class="ms-auto text-xs text-slate-500">
|
||||
{{
|
||||
p.nextTriggerDate ? new Date(p.nextTriggerDate).toISOString() : ""
|
||||
}}
|
||||
<code class="truncate min-w-0">{{ p.identifier }}</code>
|
||||
<span
|
||||
class="ms-auto text-xs text-right text-slate-600 max-w-[58%] shrink-0"
|
||||
>
|
||||
<template v-if="p.wallClockMillis != null">
|
||||
<span class="block font-medium">{{
|
||||
formatIsoMs(p.wallClockMillis)
|
||||
}}</span>
|
||||
<span class="block text-[10px] text-slate-400"
|
||||
>Scheduled target ({{ p.wallClockSource }})</span
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
p.nextTriggerDate != null &&
|
||||
Math.abs(p.nextTriggerDate - p.wallClockMillis) > 5000
|
||||
"
|
||||
class="block text-[10px] text-amber-800 mt-0.5"
|
||||
>iOS nextTriggerDate (resamples on each fetch for interval
|
||||
triggers): {{ formatIsoMs(p.nextTriggerDate) }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="block">{{
|
||||
formatIsoMs(p.nextTriggerDate ?? null)
|
||||
}}</span>
|
||||
<span class="block text-[10px] text-slate-400"
|
||||
>iOS nextTriggerDate</span
|
||||
>
|
||||
</template>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -147,6 +171,8 @@ type PendingInfo = {
|
||||
identifier: string;
|
||||
nextTriggerDate?: number | null;
|
||||
triggerType?: string | null;
|
||||
wallClockMillis?: number | null;
|
||||
wallClockSource?: string | null;
|
||||
};
|
||||
|
||||
const presets = [
|
||||
@@ -168,6 +194,13 @@ const intervalLabel = computed(() => {
|
||||
return preset?.label ?? `${intervalMs.value}ms`;
|
||||
});
|
||||
|
||||
function formatIsoMs(ms: number | null | undefined): string {
|
||||
if (ms == null || !Number.isFinite(ms)) {
|
||||
return "";
|
||||
}
|
||||
return new Date(ms).toISOString();
|
||||
}
|
||||
|
||||
async function withBusy(fn: () => Promise<void>): Promise<void> {
|
||||
if (busy.value) return;
|
||||
busy.value = true;
|
||||
@@ -179,11 +212,9 @@ async function withBusy(fn: () => Promise<void>): Promise<void> {
|
||||
}
|
||||
|
||||
async function refreshPending(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
const result = await NotificationDebugService.getPendingNotifications();
|
||||
pending.value = result.pending;
|
||||
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
|
||||
});
|
||||
const result = await NotificationDebugService.getPendingNotifications();
|
||||
pending.value = result.pending;
|
||||
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
|
||||
}
|
||||
|
||||
async function onMockRefresh(): Promise<void> {
|
||||
|
||||
@@ -4,6 +4,9 @@ export type PendingNotificationInfo = {
|
||||
identifier: string;
|
||||
nextTriggerDate?: number | null;
|
||||
triggerType?: string | null;
|
||||
/** Epoch ms for intended fire time when known (userInfo or predictive_ id); stable across Refresh. */
|
||||
wallClockMillis?: number | null;
|
||||
wallClockSource?: string | null;
|
||||
};
|
||||
|
||||
export interface NotificationInspectorPlugin {
|
||||
|
||||
@@ -23,6 +23,8 @@ type PendingNotificationInfo = {
|
||||
identifier: string;
|
||||
nextTriggerDate?: number | null;
|
||||
triggerType?: string | null;
|
||||
wallClockMillis?: number | null;
|
||||
wallClockSource?: string | null;
|
||||
};
|
||||
|
||||
export type PendingNotificationsResult = {
|
||||
|
||||
Reference in New Issue
Block a user