Skip to content

First Contract Integration

This tutorial walks through a first integration of @ddgutierrezc/legato-contract in a plain TypeScript app. You will model Track, QueueSnapshot, and PlaybackSnapshot, then read event payload shapes without importing any Capacitor package.

  • Node.js 18+ and npm.
  • A TypeScript project (or a new empty folder).
  • Basic familiarity with TypeScript interfaces and unions.
  1. Install the contract package.

    Terminal window
    npm install @ddgutierrezc/legato-contract
  2. Import contract types and constants.

    import {
    LEGATO_EVENT_NAMES,
    type LegatoEventPayload,
    type PlaybackSnapshot,
    type QueueSnapshot,
    type Track,
    } from '@ddgutierrezc/legato-contract';

Start with a minimal track, then enrich it as your integration grows.

import type { Track } from '@ddgutierrezc/legato-contract';
const introTrack: Track = {
id: 'ep-001',
url: 'https://cdn.example.com/audio/episode-001.mp3',
};
import type { Track } from '@ddgutierrezc/legato-contract';
const introTrack: Track = {
id: 'ep-001',
url: 'https://cdn.example.com/audio/episode-001.mp3',
title: 'Episode 001',
artist: 'Legato Team',
album: 'Season 1',
artwork: 'https://cdn.example.com/artwork/ep-001.jpg',
duration: 180,
type: 'progressive',
headers: {
Authorization: 'Bearer static-token-for-demo',
'X-App-Client': 'docs-tutorial',
},
};

What this gives you immediately:

  • type only accepts 'file' | 'progressive' | 'hls' | 'dash'.
  • headers is a string map (Record<string, string>) scoped per track.

Start with one item, then move to a realistic multi-item queue.

import type { QueueSnapshot } from '@ddgutierrezc/legato-contract';
const queue: QueueSnapshot = {
items: [introTrack],
currentIndex: 0,
};
const queue: QueueSnapshot = {
items: [
introTrack,
{
id: 'ep-002',
url: 'https://cdn.example.com/audio/episode-002.m3u8',
title: 'Episode 002',
type: 'hls',
},
],
currentIndex: 1,
};

QueueSnapshot is the serializable queue projection used by snapshots and event payloads.

import type { PlaybackSnapshot } from '@ddgutierrezc/legato-contract';
const playback: PlaybackSnapshot = {
state: 'playing',
currentTrack: introTrack,
currentIndex: 0,
position: 12.4,
duration: 180,
bufferedPosition: 30,
queue,
};

Model the no-active-item state too:

const idlePlayback: PlaybackSnapshot = {
state: 'idle',
currentTrack: null,
currentIndex: null,
position: 0,
duration: null,
queue: {
items: [],
currentIndex: null,
},
};

Important contract semantics visible in this shape:

  • duration can be number | null.
  • currentTrack and currentIndex can be null when no active item exists.
  • queue is always present and typed as QueueSnapshot.

4) Read event payload shapes without Capacitor

Section titled “4) Read event payload shapes without Capacitor”

You can consume contract event names and payload maps directly in shared code:

import {
LEGATO_EVENT_NAMES,
type LegatoEventName,
type LegatoEventPayload,
} from '@ddgutierrezc/legato-contract';
function handleLegatoEvent<E extends LegatoEventName>(
eventName: E,
payload: LegatoEventPayload<E>,
) {
if (eventName === 'playback-progress') {
payload.position;
payload.duration;
payload.bufferedPosition;
}
if (eventName === 'remote-seek') {
payload.position;
}
}
const allNames = LEGATO_EVENT_NAMES;

This pattern keeps your domain and UI code runtime-neutral while still fully typed.

  • Treating type as a playback guarantee. It declares media semantics; runtime capability still decides what operations are available.
  • Treating headers as dynamic auth orchestration. The contract defines static per-track headers, not token refresh or DRM/license flows.
  • Forgetting nullable branches. duration, currentTrack, and currentIndex are intentionally nullable and must be handled.
  • Mixing runtime behavior into shared contract-only modules. Keep this layer transport-neutral.

Your integration is correct when:

  • Track, QueueSnapshot, and PlaybackSnapshot objects type-check.
  • Invalid type values (for example 'stream') fail at compile time.
  • Event payload access narrows by event name (for example, remote-seek exposes position).

Move to runtime integration when you need:

  • Real playback commands (setup, add, play, seekTo) against native adapters.
  • Runtime event lifecycle management (listener registration + cleanup).
  • Capability-gated UI decisions (getCapabilities() for seek/skip availability).
  • Native setup diagnostics and configuration checks.