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

iten-core

v0.4.2

Published

Ultralight in-memory router for embedded JS apps — framework-agnostic core

Readme

iten-core

Framework-agnostic state machine for typed in-memory routing. Zero dependencies. Pure TypeScript.

Most React apps should start with jotai-iten. Use iten-core when you want the low-level router directly or you are building an adapter for another state library.


Install

npm install iten-core

iten-core does not depend on React, Jotai, TanStack Query, or Zod. Loaders can use any query client-like object with an ensureQueryData method.


Concepts

A route is a discriminated union where name identifies the screen and the remaining fields are that screen's params. Define routes as a runtime object, then derive the TypeScript route map from it.

import { defineRoutes, type InferRoutes, route } from 'iten-core'

const routes = defineRoutes({
  home: route(),
  detail: route<{ id: string }>(),
  settings: route(),
})

type Routes = InferRoutes<typeof routes>

Navigation is an async pipeline:

navigate({ target })
  -> beforeLoad guard
  -> optional loader
  -> commit target route

The current route remains committed until the target route is ready. Loader failures keep the previous route and expose a retryable router error.


Quick Start

import {
  createItenCore,
  createRoute,
  defineRoutes,
  type InferRoutes,
  route,
} from 'iten-core'

const routes = defineRoutes({
  home: route(),
  detail: route<{ id: string }>(),
})

type Routes = InferRoutes<typeof routes>

const toRoute = createRoute(routes)

const core = createItenCore<Routes, unknown>({
  initial: toRoute({ name: 'home' }),
  routeConfig: {
    detail: {
      loader: async ({ params, queryClient }) => {
        await queryClient.ensureQueryData(detailQuery(params.id))
      },
    },
  },
})

const unsubscribe = core.subscribe({
  listener: (state) => {
    console.log(state.route, state.isNavigating)
  },
})

await core.navigate({ target: toRoute({ name: 'detail', params: { id: '42' } }) })

const { route: currentRoute, history, canGoBack } = core.getState()

core.goBack()
unsubscribe()
core.dispose()

Configuration

type CoreRouterConfig<M, Ctx> = {
  initial: RouteUnion<M> | null
  routeConfig?: { [K in keyof M]?: RouteConfig<M, K, Ctx> }
  maxHistoryLength?: number
  queryClient?: QueryClientLike | null
  getCtx?: () => Ctx
}

| Option | Default | Description | |---|---:|---| | initial | Required | Initial committed route. Use null for routers that begin closed, such as modal routers. | | routeConfig | {} | Per-route loaders, guards, lifecycle hooks, and pending timing. | | maxHistoryLength | 50 | Maximum number of previous routes retained for goBack. | | queryClient | null | Passed to loaders. Only ensureQueryData is required. | | getCtx | undefined | Computes navigation context for guards, loaders, and hooks. |


Core API

const core = createItenCore<Routes, Ctx>(config)

| Method | Description | |---|---| | navigate({ target, options }) | Runs guards and loaders, then commits the target route. Returns a promise. | | goBack() | Pops history and commits the previous route without rerunning its loader. | | subscribe(listener) | Subscribes to state changes and returns an unsubscribe function. | | getState() | Returns the current router state snapshot. | | dispose() | Marks the instance disposed, clears listeners, and prevents later async commits. |


Router State

type RouterState<M> = {
  route: RouteUnion<M> | null
  pendingRoute: RouteUnion<M> | null
  isNavigating: boolean
  history: RouteUnion<M>[]
  canGoBack: boolean
  error: RouterError | null
}

| Field | Meaning | |---|---| | route | Last committed route. | | pendingRoute | Target route while a navigation is in flight. | | isNavigating | true while pending state should be visible. | | history | Previous committed routes, newest last. | | canGoBack | Whether goBack() can commit a previous route. | | error | Retryable error from the latest failed navigation, or null. |


Routes

type RouteConfig<M, K, Ctx> = {
  loader?: (input: { params: M[K]; queryClient: QueryClientLike; context: Ctx }) => Promise<void>
  loaderDeps?: (input: { params: M[K] }) => unknown
  staleTime?: number
  beforeLoad?: (input: { params: M[K]; context: Ctx }) => RouteUnion<M> | undefined
  pendingMs?: number
  pendingMinMs?: number
  onEnter?: (input: { params: M[K]; context: Ctx }) => void
  onLeave?: (input: { params: M[K]; context: Ctx }) => void
}

Loaders

Loaders run before the target route commits.

const core = createItenCore<Routes, unknown>({
  initial: toRoute({ name: 'home' }),
  queryClient,
  routeConfig: {
    detail: {
      loader: async ({ params, queryClient }) => {
        await queryClient.ensureQueryData(detailQuery(params.id))
      },
      loaderDeps: ({ params }) => params.id,
      staleTime: 30_000,
    },
  },
})

Behavior:

  • The current route remains active while the loader runs.
  • pendingRoute tracks the target route immediately.
  • isNavigating becomes visible according to pendingMs and pendingMinMs.
  • If the loader throws, the previous route stays committed and error.retry() reruns the pipeline.
  • loaderDeps and staleTime can skip repeat loads for the same dependency key.

Guards

beforeLoad can redirect before the loader runs.

const core = createItenCore<Routes, { userId: string | null }>({
  initial: toRoute({ name: 'home' }),
  getCtx: () => ({ userId: auth.userId }),
  routeConfig: {
    detail: {
      beforeLoad: ({ context }) => {
        if (!context.userId) return toRoute({ name: 'home' })
      },
    },
  },
})

Redirect loops are capped and surfaced as retryable router errors instead of recurring indefinitely.

Timing

Use pendingMs and pendingMinMs to avoid loading-state flicker.

routeConfig: {
  detail: {
    pendingMs: 200,
    pendingMinMs: 100,
    loader: async ({ params, queryClient }) => {
      await queryClient.ensureQueryData(detailQuery(params.id))
    },
  },
}

pendingMs delays visible pending state. pendingMinMs keeps it visible long enough once shown.

Lifecycle Hooks

onEnter runs for the target route before it commits after a successful loader navigation. onLeave is available on route config and currently runs for skip-loader commits such as goBack().

routeConfig: {
  detail: {
    onEnter: ({ params }) => analytics.track('detail_opened', { id: params.id }),
    onLeave: ({ params }) => analytics.track('detail_closed', { id: params.id }),
  },
}

Type Utilities and Guards

These utilities are also available from iten-core/utils for utility-only imports.

import {
  createRoute,
  defineRouteConfig,
  defineRoutes,
  isRoute,
  isRouteName,
  matchRoute,
  type InferRoutes,
  route,
} from 'iten-core/utils'

createRoute

Creates routes with route-specific params.

const routes = defineRoutes({
  home: route(),
  detail: route<{ id: string }>(),
})

type Routes = InferRoutes<typeof routes>

const toRoute = createRoute(routes)

toRoute({ name: 'home' })
toRoute({ name: 'detail', params: { id: '42' } })

// Type error: detail requires id
toRoute({ name: 'detail' })

defineRouteConfig

Preserves route-specific inference when authoring route config separately.

const routeConfig = defineRouteConfig<Routes, unknown>({
  detail: {
    loader: async ({ params }) => {
      params.id // string
    },
  },
})

isRoute and isRouteName

Use guards at runtime boundaries such as host messages, persisted state, or URL sync.

const candidate = { value: message, name: 'detail' as const }

if (isRoute<Routes, 'detail'>(candidate)) {
  candidate.value.id // string
}

const names = ['home', 'detail', 'settings'] as const

const candidateName = { names, value: maybeName }

if (isRouteName(candidateName)) {
  candidateName.value // 'home' | 'detail' | 'settings'
}

matchRoute

Exhaustive branching over a route union.

const title = matchRoute<Routes, string>({
  route: currentRoute,
  matcher: {
    home: () => 'Home',
    detail: ({ params }) => `Detail ${params.id}`,
    settings: () => 'Settings',
  },
})

Building an Adapter

Adapters should keep iten-core as the source of truth and mirror state into their host state library.

const core = createItenCore<M, Ctx>(config)

let snapshot = core.getState()

const unsubscribe = core.subscribe({
  listener: (next) => {
    snapshot = next
    notifySubscribers()
  },
})

Recommended adapter shape:

  • Create one core instance per router instance.
  • Expose framework-native hooks, stores, atoms, or signals that derive from core.getState().
  • Delegate navigation to core.navigate and core.goBack.
  • Dispose the core when the adapter instance is no longer reachable.
  • Re-export iten-core/utils so users can share route factories and guards without importing component code.
  • Use iten-core/url for optional URL synchronization instead of mixing URL concerns into adapter state.

jotai-iten is the reference adapter.


URL Sync

iten-core is in-memory by default. Import iten-core/url only when a surface needs URL synchronization. The adapter uses explicit parse and format functions instead of path-pattern route definitions.

import { createUrlSync } from 'iten-core/url'

Hydrate from the current URL explicitly:

const urlSync = createUrlSync<Routes>({
  router: core,
  parse: ({ url }) => {
    const id = url.searchParams.get('id')
    return id ? toRoute({ name: 'detail', params: { id } }) : toRoute({ name: 'home' })
  },
  format: ({ route, url }) => {
    const next = new URL(url)
    next.searchParams.set('route', String(route.name))
    if (route.name === 'detail') {
      next.searchParams.set('id', route.id)
    } else {
      next.searchParams.delete('id')
    }
    return next
  },
})

await urlSync.hydrate()
const stopUrlSync = urlSync.start({ mode: 'replace' })

Use custom getUrl, writeUrl, and subscribeUrl functions for non-browser hosts, tests, iframes, extension panels, or host-owned navigation.


Zod and Runtime Validation

Zod is useful for validating external input before it reaches the router. It is intentionally not a dependency of iten-core because route params are already typed inside trusted TypeScript code and many apps do not need runtime schemas in the navigation hot path.

const detailMessage = z.object({
  name: z.literal('detail'),
  id: z.string(),
})

const parsed = detailMessage.safeParse(hostMessage)
if (parsed.success) {
  await core.navigate({ target: parsed.data })
}

Troubleshooting

My no-param route type does not work

Use route() for no-param routes, not route<Record<string, never>>().

const routes = defineRoutes({
  home: route(),
})

My loader does not run again

Check loaderDeps and staleTime. If the same dependency key is still fresh, the loader is skipped.

I only need type utilities

Use the utility subpath:

import { createRoute, matchRoute } from 'iten-core/utils'

Should this be a URL router?

Use a URL router when paths, search params, SSR, nested route trees, or route files are core product concerns. Use iten-core when routing is local state and you want a small typed state machine.


License

MIT