MSR Modlet Development
The MSR (Multi-Session Replay) modlet provides a comprehensive frontend interface for replaying database changes over time. This guide focuses on developing with and extending the MSR frontend components within a web-base project.
Prerequisites
Before developing with the MSR modlet, ensure you have:
- Completed the web-base quickstart
- Installed the MSR modlet using
npx cli install msr - Deployed MSR backend services via dev-containers
- Basic understanding of SvelteKit and Svelte 5
Architecture Overview
The MSR modlet follows the AGIL Ops Hub modlet pattern and integrates seamlessly with web-base projects:
src/lib/aoh/msr/ # MSR modlet root (installed via CLI)
├── msr.store.svelte.ts # Main reactive store using Svelte 5 runes
├── msr.worker.ts # Web Worker for background processing
├── msr.types.d.ts # TypeScript type definitions
├── modlet.config.ts # Modlet configuration for CLI tool
├── api/
│ ├── client.ts # API client implementation
│ └── types.ts # API response types
├── components/
│ ├── MultiSessionReplay/ # Main replay component
│ ├── ReplayController/ # Playback controls
│ ├── ReplayInitCard/ # Initialization interface
│ ├── TimelineSlider/ # Timeline scrubbing control
│ └── DataTable/ # Session management table
└── pages/
└── sessions/ # Session management page
Integration with Web-Base
The MSR modlet integrates with your web-base project through:
- Navigation: Configured in
modlet.config.ts(managed by modlet developer) - Routes: Auto-installed to
/aoh/msrpaths for reference/debugging - Components: Available for use in your application pages
- Store: Global reactive state management
Development Setup
1. Post-Installation Setup
After running npx cli install msr, verify the installation:
# Check that MSR files are installed
ls src/lib/aoh/msr/
# Start development server
npm run dev
# Navigate to sample MSR interface
open http://localhost:5173/aoh/msr
2. Environment Configuration
Ensure your .env file includes the required MSR variables:
# MSR Backend Configuration
MSR_URL=http://msr.127.0.0.1.nip.io
# Authorization Service (required for MSR)
IAMS_AAS_URL=http://iams-aas.127.0.0.1.nip.io
3. Backend Services
Start required backend services:
# From dev-containers directory
podman compose --env-file .env -f msr/compose.yml up -d
Core Development Concepts
Replay Store
The replayStore is the central reactive state manager using Svelte 5 runes:
import { replayStore } from "$lib/aoh/msr/msr.store.svelte";
// Reactive state access
const status = replayStore.status; // Current replay status
const entities = replayStore.entities; // SvelteMap of all entities
const playbackTime = replayStore.playbackTime; // Current timestamp
Entity Management
Entities are stored with composite keys and can be accessed efficiently:
// Get entities from a specific table
const geoEntities = $derived(replayStore.getTableEntitiesMap("gis.geo_entity"));
// Get a specific entity
const entity = replayStore.getEntity("gis.geo_entity", "entity-123");
// Get all available tables
const tables = replayStore.getAvailableTables();
Component Development
Create replay-aware components that react to state changes:
<!-- MyReplayComponent.svelte -->
<script>
import { replayStore } from '$lib/aoh/msr/msr.store.svelte';
// Reactive access to entities from specific table
const airplaneEntities = $derived(replayStore.getTableEntitiesMap('gis.geo_entity'));
// Watch for status changes
const isPlaying = $derived(replayStore.status === 'playing');
// Current playback timestamp
const currentTime = $derived(replayStore.playbackTime);
</script>
<!-- Render entities dynamically -->
{#each [...airplaneEntities.values()] as entity}
<div class="bg-white border rounded-lg p-4 shadow-sm" data-id={entity.id}>
{JSON.stringify(entity.state)}
</div>
{/each}
<!-- Show playback status -->
{#if isPlaying}
<div class="bg-green-100 text-green-800 px-3 py-2 rounded-md">
Playing: {currentTime?.toISOString()}
</div>
{/if}
Type Safety
Define entity state types for better development experience:
// Define entity state types
interface AirplaneEntity {
id: string;
lat: number;
lon: number;
altitude: number;
heading: number;
callsign: string;
}
// Type-safe entity access
const entity = replayStore.getEntity("gis.geo_entity", "plane-123");
if (entity) {
const airplaneData = entity.state as AirplaneEntity;
console.log(`${airplaneData.callsign} at ${airplaneData.lat}, ${airplaneData.lon}`);
}
Extending the MSR Modlet
Custom Entity Renderers
Create specialized renderers for different entity types:
<!-- AirplaneRenderer.svelte -->
<script>
export let entity;
const airplane = entity.state;
</script>
<div
class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs font-bold relative"
style="transform: rotate({airplane.heading}deg)"
>
✈️
<div class="absolute -bottom-6 left-1/2 transform -translate-x-1/2 text-black text-xs whitespace-nowrap">
{airplane.callsign}
</div>
<div class="absolute -bottom-9 left-1/2 transform -translate-x-1/2 text-gray-600 text-xs">
{airplane.altitude}ft
</div>
</div>
Custom Replay Controls
Build application-specific controls:
<!-- CustomReplayControls.svelte -->
<script>
import { replayStore } from '$lib/aoh/msr/msr.store.svelte';
function handlePlay() {
replayStore.play();
}
function handlePause() {
replayStore.pause();
}
function handleSpeedChange(speed) {
replayStore.setSpeed(speed);
}
</script>
<div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
<button
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
onclick={handlePlay}
>
Play
</button>
<button
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
onclick={handlePause}
>
Pause
</button>
<select
class="px-3 py-2 border rounded"
onchange={(e) => handleSpeedChange(Number(e.target.value))}
>
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="2">2x</option>
<option value="4">4x</option>
</select>
</div>
Performance Optimization
Configure performance options for your specific use case:
const customPerformanceOptions = {
pollWindowMs: 1000, // Faster polling for high-frequency data
frameRate: 60, // Higher frame rate for smooth animation
maxBufferSize: 2000000, // Larger buffer for long sessions
minPollingInterval: 50, // Reduced minimum interval
};
replayStore.initiateReplay(startTime, customPerformanceOptions);
Integration with Other Modlets
GIS Integration Example
Integrate MSR with the GIS modlet for geospatial replay:
<!-- MapWithReplay.svelte -->
<script>
import { replayStore } from '$lib/aoh/msr/msr.store.svelte';
import Map from '$lib/aoh/gis/components/Map/index.svelte';
import CesiumMapEngineProvider from '$lib/aoh/gis/components/engines/CesiumMapEngineProvider/index.svelte';
import MapEntityLayerProvider from '$lib/aoh/gis/components/MapEntityLayerProvider/index.svelte';
import MapEntityProvider from '$lib/aoh/gis/components/MapEntityProvider/index.svelte';
// Get geo entities from replay store
const geoEntities = $derived(replayStore.getTableEntitiesMap('gis.geo_entity'));
// Convert to GIS format
const mapEntities = $derived([...geoEntities.values()].map(entity => ({
id: entity.id,
geojson: JSON.parse(entity.state.geojson),
// Add other properties as needed
})));
</script>
<CesiumMapEngineProvider>
<Map {...mapProps}>
<MapEntityLayerProvider layer_name="Replay Entities" is_visible>
<MapEntityProvider kind="replay">
{#snippet children(entity)}
<!-- Render replayed entities on map -->
<div class="bg-blue-500 text-white px-2 py-1 rounded text-xs">
{entity.id}
</div>
{/snippet}
</MapEntityProvider>
</MapEntityLayerProvider>
</Map>
</CesiumMapEngineProvider>
Testing
Component Testing
import { describe, it, expect } from "vitest";
import { render } from "@testing-library/svelte";
import { replayStore } from "$lib/aoh/msr/msr.store.svelte";
import MyReplayComponent from "./MyReplayComponent.svelte";
describe("MyReplayComponent", () => {
it("renders entity data correctly", () => {
// Mock replay store state
replayStore.entities.set("gis.geo_entity:test-1", {
id: "test-1",
tableName: "gis.geo_entity",
state: { lat: 1.3521, lon: 103.8198, callsign: "TEST123" },
});
const { getByText } = render(MyReplayComponent);
expect(getByText("TEST123")).toBeInTheDocument();
});
});
Store Testing
import { describe, it, expect } from "vitest";
import { replayStore } from "$lib/aoh/msr/msr.store.svelte";
describe("replayStore", () => {
it("manages entity state correctly", () => {
// Set playback range
const startTime = new Date("2024-01-01T00:00:00Z");
const endTime = new Date("2024-01-01T01:00:00Z");
replayStore.setPlaybackRange(startTime, endTime);
expect(replayStore.replayStartTime).toBe(startTime);
expect(replayStore.replayEndTime).toBe(endTime);
expect(replayStore.status).toBe("selecting");
});
});
Best Practices
1. Reactive Patterns
// ✅ Use $derived for computed values
const filteredEntities = $derived([...entities.values()].filter((e) => e.tableName === "gis.geo_entity"));
// ❌ Avoid manual updates
let filteredEntities = [];
entities.forEach((e) => {
/* manual filtering */
});
2. Performance Considerations
// ✅ Use efficient entity access methods
const entity = replayStore.getEntity("gis.geo_entity", "entity-123");
// ❌ Avoid linear search through all entities
const entity = [...entities.values()].find((e) => e.id === "entity-123");
3. Error Handling
<script>
const errorMessage = $derived(replayStore.errorMessage);
const status = $derived(replayStore.status);
</script>
{#if status === 'error'}
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
Error: {errorMessage}
<button
class="ml-2 px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700"
onclick={() => replayStore.reset()}
>
Retry
</button>
</div>
{/if}
4. Component Lifecycle
Most MSR components don't require manual cleanup since:
- The
replayStoremanages its own lifecycle automatically - Svelte handles reactive subscription cleanup
- DOM event listeners using
on:directives are auto-removed
For the rare cases where manual cleanup is needed:
import { onDestroy } from "svelte";
onDestroy(() => {
// Clear component-specific timers
if (localTimer) {
clearTimeout(localTimer);
}
// Remove manually added DOM event listeners
document.removeEventListener("keydown", handleKeydown);
});
Available Components
MultiSessionReplay Component
The primary component for integrating replay functionality into your application. It provides a complete replay experience including lobby, initialization, controls, and timeline.
Import:
import { MultiSessionReplay } from '$lib/aoh/msr/components/MultiSessionReplay';
Props:
|| Prop | Type | Required | Default | Description |
|| ----------------------- | ------------------------- | -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|| lobbyTitle | string | Yes | - | Title displayed in the lobby screen when component first loads |
|| lobbyDescription | string | Yes | - | Description text shown in the lobby to explain the replay feature |
|| lobbyButtonText | string | Yes | - | Text for the button that starts the replay session selection |
|| playbackWindowMinutes | number | No | 120 | Duration in minutes for each replay session. Determines how long from the selected start time the replay will cover |
|| startBufferMinutes | number | No | playbackWindowMinutes | Safety buffer (minutes) applied to earliest selectable time. Prevents selecting times too close to data cleanup boundaries |
|| endBufferMinutes | number | No | playbackWindowMinutes | Safety buffer (minutes) applied to latest selectable time. Prevents selecting times where CDC data may not yet exist |
|| performanceOptions | MsrPerformanceOptions | No | See below | Fine-tune replay performance for specific hardware and use cases |
|| blurTargetElement | HTMLElement | No | undefined | Optional DOM element to apply blur effect when MSR dialogs are open (useful for backdrop effects) |
|| errorHandling | ErrorHandlingConfig | No | Default Sonner toast config | Custom error handling configuration. Allows overriding default error display behavior with custom handlers |
|| stateFilterConfig | MsrStateFilterConfig | No | undefined | Filters applied to initial state load to exclude stale data from specific tables |
|| playbackFilterConfig | MsrPlaybackFilterConfig | No | undefined | Filters applied during event polling to exclude stale events from specific tables |
Performance Options:
interface MsrPerformanceOptions {
pollWindowMs?: number; // Time window for fetching CDC data (default: 3000ms)
frameRate?: number; // Rendering rate in FPS (default: 30)
maxBufferSize?: number; // Max buffered changes (default: 1,000,000)
minPollingInterval?: number; // Min interval between requests (default: 100ms)
}
Error Handling Configuration:
interface ErrorHandlingConfig {
onError?: ErrorHandler; // Custom error handler
defaultOptions?: ErrorHandlerOptions; // Global error handling options
}
interface ErrorHandlerOptions {
showToast?: boolean; // Show error toast notification (default: true)
logError?: boolean; // Log error to console (default: true)
throwError?: boolean; // Re-throw error for upstream handling (default: false)
}
Usage Examples:
Basic integration:
<script>
import { MultiSessionReplay } from '$lib/aoh/msr/components/MultiSessionReplay';
</script>
<MultiSessionReplay
lobbyTitle="Historical Data Replay"
lobbyDescription="Review past system states and analyze changes over time"
lobbyButtonText="Start Replay Session"
/>
Advanced configuration with custom performance:
<script>
import { MultiSessionReplay } from '$lib/aoh/msr/components/MultiSessionReplay';
const performanceOptions = {
pollWindowMs: 1000, // 1-second polling windows
frameRate: 60, // Smooth 60 FPS playback
maxBufferSize: 2000000, // 2M event buffer
minPollingInterval: 50 // 50ms minimum between requests
};
const errorHandling = {
onError: async (error, context, options) => {
// Custom error tracking
await sendToErrorTracking(error, context);
},
defaultOptions: {
showToast: true,
logError: true,
throwError: false
}
};
</script>
<MultiSessionReplay
lobbyTitle="High-Frequency Replay"
lobbyDescription="Optimized for real-time analysis"
lobbyButtonText="Begin Analysis"
playbackWindowMinutes={480}
startBufferMinutes={60}
endBufferMinutes={30}
{performanceOptions}
{errorHandling}
/>
Component Behavior:
- Lobby State: Shows initial lobby screen with description and start button
- Selection State: Opens date/time picker for users to select replay start time
- Initialization: Fetches initial state from backend at selected timestamp
- Playback: Streams CDC events and updates entity state in real-time
- Controls: Provides play/pause, speed control, timeline scrubbing
Safety Buffers:
The startBufferMinutes and endBufferMinutes props prevent users from selecting problematic time ranges:
- Start buffer: Prevents selection too close to the
MAX_PLAYBACK_RANGEcutoff where snapshots may be incomplete - End buffer: Prevents selection too close to current time where CDC events may not have been captured yet
- Both default to
playbackWindowMinutesfor consistent safety margins
Filtering (New):
The MultiSessionReplay component supports filtering initial state and playback events to improve performance and focus on relevant data.
stateFilterConfig(MsrStateFilterConfig): Filters applied when loading the initial state at the selected timestampplaybackFilterConfig(MsrPlaybackFilterConfig): Filters applied during event polling while playback runs
Filtering configuration consists of:
tables: string[] — the list of table names to filtergetMinTimestamp(context): callback — compute the minimum timestamp; events older than this will be excluded for the specified tables
Usage examples (dayjs):
<script>
import { MultiSessionReplay } from '$lib/aoh/msr/components/MultiSessionReplay';
import dayjs from 'dayjs';
const stateFilterConfig = {
tables: ['patients', 'medications'],
getMinTimestamp: ({ targetTimestamp }) => {
return dayjs(targetTimestamp).subtract(7, 'day').toDate();
}
};
const playbackFilterConfig = {
tables: ['audit_logs', 'activity_logs'],
getMinTimestamp: ({ targetTimestamp }) => {
return dayjs(targetTimestamp).subtract(1, 'day').toDate();
}
};
</script>
<MultiSessionReplay
lobbyTitle="Replay System Events"
lobbyDescription="View historical system states"
lobbyButtonText="Start Replay"
{stateFilterConfig}
{playbackFilterConfig}
/>
See the dedicated Filtering guide for more patterns and performance considerations.
Core Components
ReplayController: Standalone playback controls (play, pause, speed, restart)ReplayInitCard: Timestamp selection and initialization interfaceTimelineSlider: Timeline scrubbing component for seekingDataTable: Generic data table component for session management
UI Components
The MSR modlet includes its own UI component library based on shadcn/ui:
- Form Controls: Button, Input, Select, Checkbox, etc.
- Layout: Card, Dialog, Sheet, Tabs, etc.
- Data Display: Table, Pagination, etc.
- Feedback: Alert, Toast (Sonner), etc.
These components are available for use in your custom implementations and follow the same design system as other AGIL Ops Hub modlets.
Sample Pages Reference
The installed modlet includes sample pages for development reference:
/aoh/msr: Complete replay interface demonstrating component integration/aoh/msr/sessions: Session management interface with data table
These sample pages serve as implementation examples and debugging tools. Study their source code to understand best practices for component usage and integration patterns.