npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@klyftig/alarm-core

v0.4.0

Published

Klyftig shared alarm engine for Expo / React Native: reliable killed-state alarms (notifee on Android, AlarmKit + notification fallback on iOS), mission-gated dismissal model, snooze/escalation, permissions & OEM guidance, per-fire + missed-alarm telemetr

Readme

@klyftig/alarm-core

The Klyftig shared alarm engine for Expo / React Native. Extracted from Sleepwise (2026-09-15) so that Sleepwise and the habit app run the same engine: one place to fix reliability bugs, one device test protocol.

What it does:

  • Schedules alarms that fire from a killed state, after reboot and over the lock screen — notifee on Android (exact TIMESTAMP trigger, allowWhileIdle, full-screen intent), AlarmKit on iOS 26+ with a time-sensitive-notification fallback below that (one path per alarm, chosen by iosCoordinator).
  • Rings through silent mode via @klyftig/alarm-audio (Android STREAM_ALARM foreground service with volume ramp / extra-loud / vibration; iOS AVAudioSession .playback).
  • Snooze (snoozeTracker), skip-next-once, "prevent last-minute edits" (editLock), Emergency Escape pledges, missed-alarm detection, permission / OEM-battery guidance (permissions, oemGuidance), a boot-reschedule headless task, and the built-in alarm tone catalog.
  • The mission data model (MissionType, MissionConfig, AlarmMission) — the screens live in @klyftig/missions. pushup (0.4.0, { repCount }, default 10, premium, escapable) is a type whose screen is opt-in: @klyftig/missions/pushup (camera + on-device pose model).

It deliberately has no UI, no analytics and no storage of its own. The app supplies those through the hooks below.

Install

npm i @klyftig/alarm-core @klyftig/alarm-audio @klyftig/alarm-kit @klyftig/android-perms \
      @notifee/react-native expo-notifications expo-asset expo-document-picker expo-file-system \
      @react-native-async-storage/async-storage

app.config.js:

plugins: [
  '@klyftig/alarm-core',        // Android perms + lock-screen flags + boot receiver; iOS AlarmKit usage + audio background mode
  ['expo-notifications', { sounds: ['./node_modules/@klyftig/alarm-core/assets/sounds/alarms/classic.wav'] }],
]

The iOS notification fallback uses classic.wav as its bundled sound (IOS_ALARM_NOTIFICATION_SOUND), which is why expo-notifications must copy it.

Custom entry file (index.js; set "main": "index.js" in package.json):

import 'expo-router/entry';
import { registerBootTask, registerNotifeeBackground } from '@klyftig/alarm-core';
import { loadAlarms } from './src/storage/alarmStorage';

registerBootTask(loadAlarms);          // re-arms alarms after reboot / app update
registerNotifeeBackground([]);         // notifee needs a background handler; pass your own handlers

Wire analytics once:

configureAlarmCore({
  onMissedAlarm: (m) => analytics.capture('alarm_missed_suspected', { alarm_id: m.alarmId, expected_at: m.expectedAt, delay_ms: m.delayMs }),
});

Requires a dev client (native modules) — never Expo Go. Both Klyftig apps build locally (expo prebuild → Xcode / Gradle).

Using the engine

import { alarmEngine, makeDefaultAlarmInput, nextFireTime, type Alarm } from '@klyftig/alarm-core';

await alarmEngine.setupChannels();                 // idempotent, on every launch
const occurrence = await alarmEngine.scheduleAlarm(alarm);   // cancels + re-arms; null if disabled / no future occurrence
await alarmEngine.cancelAlarm(alarm.id);
await alarmEngine.snoozeAlarm(alarm, 9);
await alarmEngine.fireNow(alarm, 'Smart Wake • 07:00');      // immediate fire on the alarm's own id
await alarmEngine.scheduleWakeUpCheck(alarm, 3);             // one-off re-fire N minutes later

Alarm is the product-agnostic shape. Apps extend it (Sleepwise adds smartWake / wakeUpCheck) — the engine only reads the core fields, so the extended object passes straight through. normalizeAlarm is generic and keeps the extra fields.

Detecting a fired alarm on Android: use notifee.getDisplayedNotifications() (the full-screen-intent launch leaves getInitialNotification() empty) plus a foreground poll — see Sleepwise useAlarmFireHandler.

Telemetry hooks (0.2)

The engine emits three signals through configureAlarmCore (one wiring site per app; the config merges, a hook given twice is replaced):

configureAlarmCore({
  onMissedAlarm:    (m) => …, // alarm_missed_suspected { alarmId, expectedAt, delayMs } — from reportMissedAlarms() on launch
  onAlarmFired:     (e) => …, // alarm_fired { alarmId, kind, firedAt, scheduledFor, delayMs } — from recordFired()
  onAlarmDismissed: (e) => …, // alarm_dismissed { alarmId, kind, missionAttempts, snoozeCount, msToDismiss, … } — from recordDismissed()
});

The dismissal screen drives them:

const kind = fireKindOf(notificationId);            // 'alarm' | 'snooze' | 'wakecheck'
const fired = await recordFired(alarm.id, kind);    // clears the missed-fire expectation (base fires only), opens the cycle
…
await recordDismissed(alarm.id, 'mission', { missionAttempts }); // BEFORE resetSnoozeCount — it reads the tracker
resetSnoozeCount(alarm.id);

recordFired matches the stored expectation only when it is not further than FIRE_EARLY_TOLERANCE_MS in the future, so a store that re-armed the NEXT occurrence before the screen mounted never has that expectation wiped. A snooze / wake-check re-fire continues the cycle opened by the first ring, so msToDismiss measures from the first ring.

Notification copy (0.3)

The engine writes a few strings itself: the title of an unlabelled alarm, the ringing / snoozed / wake-up-check bodies and the two Android channel names. They default to English; apps pass their own through the same config:

configureAlarmCore({
  strings: {
    untitled: t('alarm:untitled'),                          // title when the alarm has no label
    alarmBody: (time) => t('alarm:body', { time }),          // time = formatAlarmTime(alarm.time)
    snoozedBody: (minutes) => t('alarm:snoozed', { count: minutes }),
    wakeUpCheckBody: t('alarm:wakeUpCheck'),
    alarmChannelName: t('alarm:channel'),
    smartWakeChannelName: t('alarm:smartWakeChannel'),
  },
});

Unset keys stay English. The copy is baked into a notification when it is scheduled, so call configureAlarmCore again after a language switch and re-arm every alarm; channel names follow on the next setupChannels(). AlarmKit's own Stop / Open buttons (iOS 26+) are still English — they live in @klyftig/alarm-kit's Swift. oemGuide() steps are English and name Sleepwise.

Time and timezone changes (0.2)

A notifee trigger is an absolute epoch-ms computed for a wall-clock time in the zone current at scheduling. The config plugin's receiver therefore also listens for ACTION_TIMEZONE_CHANGED and ACTION_TIME_CHANGED (android.intent.action.TIME_SET) and runs the same headless AlarmBootReschedule task as after a reboot, so the closed-app case is covered on Android. For the open-app case on both platforms (and for iOS, which has no receiver) call reconcileScheduled(alarms) on launch and on every return to the foreground: it compares listScheduled() with each enabled alarm's nextFireTime and re-arms only what drifted (findDrift is the pure part). An app that already re-arms every alarm on foreground does not need it.

Hard-won device gotchas (read before touching the Android path)

  • POST_NOTIFICATIONS must be granted or the whole alarm is silently suppressed — ask for it first in onboarding.
  • Alarm audio must play on STREAM_ALARM; notifee's notification sound is muted on vibrate. Never route the ring through expo-audio.
  • DND bypass needs notification-policy access (@klyftig/android-perms isNotificationPolicyAccessGranted), asked separately.
  • Android 14+ grants USE_FULL_SCREEN_INTENT automatically only to alarm/calling-category apps. Probe canUseFullScreenIntent() and deep-link to the grant screen when false (openPermissionScreen.fullScreenIntent).
  • OxygenOS force-kills the process when the user swipes the ringing notification away. @klyftig/alarm-audio's service uses stopWithTask=false
    • START_REDELIVER_INTENT so audio self-heals in ~5 s; a determined swipe-escape is not solvable in-app. Don't rabbit-hole.
  • notifee refuses a trigger whose timestamp is not strictly in the future, so an immediate fire must go through displayNotification — that is what fireNow does on Android (0.2.1; before that it silently never rang).
  • Under --configure-on-demand, notifee's local maven repo may not be registered before :app resolves its classpath; the config plugin adds it to the root build.gradle so app.notifee:core always resolves.

Validated on a OnePlus CPH2769 (Android 16): fires from killed state, after reboot, over the lock screen, on silent. The iOS path has never run on a physical iPhone (simulator build only) — see TEST_PROTOCOL.md.

Known debt

  • Kotlin/Swift module identifiers still carry the Sleepwise… names (expo.modules.sleepwisealarmaudio, SleepwiseAlarmKit, …). Renaming is churn with no user value; do it only with a coordinated release of all four packages.
  • Smart-Wake checkpoint id helpers (smartWakeCheckId, SMARTWAKE_CHECK_CHANNEL_ID) are generic silent-checkpoint primitives with a product-flavoured name.
  • The timezone / clock-change receiver and the foreground reconcile (0.2) have not been exercised on a device yet — see TEST_PROTOCOL.md.

Development

npm install          # devDependencies; the three native packages are local tarballs until published
npm test             # jest-expo, 133 tests
npm run typecheck
npm run pack-local   # → ~/Code/.local-packages/klyftig-alarm-core-<v>.tgz for consumers

Publish like @klyftig/mobile-ui: bump version → npm publish → bump in the app → rm -rf node_modules/@klyftig/* node_modules/.cache && npm install && npx expo start --clear. Metro reads the TypeScript source directly; there is no build step.