Skip to main content

Function: withPersistence()

function withPersistence(
service: MediaServiceApi,
storage: MediaSessionStorage,
options?: PersistenceOptions
): PersistedMediaService;

Defined in: packages/media-session/src/persistence.ts:467

Tee the three broadcast channels into storage, so a session survives process death.

A decorator over MediaServiceApi rather than a MediaHandler decorator: the handler is the fan-in side (commands coming back), and what needs saving is the fan-out state. Wrapping the service means an app writes one line at init and every existing broadcast call site persists itself.

const service = withPersistence(await MediaService.init(...), AsyncStorage)
service.setQueue(items) // saved
service.setPlaybackState(state) // saved

Writes happen on discontinuities, plus one checkpoint per 30 s of playback

The three broadcast setters are the primary trigger, and those are already discontinuity-only by the package's central design rule (ARCHITECTURE §7) — nothing here polls a player, and no state is ever read on a tick.

On top of that, PersistenceOptions.autosave (on by default) re-arms a single JS timer while the last broadcast said playing, and each tick re-projects the anchor the app already gave us and writes it. That exists because the discontinuity rule, applied alone, means a track played straight through writes nothing — the position on disk stays at the last play/seek/track change for the length of the track. The cost is one storage setItem per interval and no bridge traffic at all: a tick deliberately does not refresh the native resumption mirror, because that call crosses into native and a per-tick native call is precisely what this package forbids. Broadcasts and PersistedMediaService.save refresh both copies.

Write scheduling

The latest snapshot always wins and writes never overlap:

  • a synchronous storage engine is written through synchronously, inside the broadcast call, so the record is durable before setPlaybackState returns;
  • an asynchronous one gets at most one write in flight; snapshots produced while it is pending collapse into a single follow-up write. Three channel broadcasts in one tick therefore cost one round trip, not three, and an out-of-order setItem completion can never resurrect stale state.

Two copies, one string (Android playback resumption)

Every record is also handed to the native side through MediaServiceApi.setResumptionSnapshot, which keeps it in this package's own SharedPreferences. That copy exists for exactly one caller: the Android media service when the OS creates it into a process with no JavaScript in it, which is the whole premise of playback resumption — it has ~5 s to call startForeground and must already know what to show. The app's storage engine stays the source of truth; the mirror is a cache, written from the same serialized string so the two cannot drift. Inert unless android.playbackResumption is on, and absent on iOS.

What is not persisted

Handlers (they are code), and the stopService lifecycle. stopService() deliberately leaves the record intact: "stop" ends background execution, not the user's place in the album. Call clearPersisted for that.

Parameters

ParameterType
serviceMediaServiceApi
storageMediaSessionStorage
optionsPersistenceOptions

Returns

PersistedMediaService