npm package: @servlyadmin/runtime-core
Description: Framework-agnostic core renderer for Servly components with prefetching and loading states
Author: Servly
Latest version: 0.2.2

# @servlyadmin/runtime-core

Framework-agnostic core renderer for Servly components. This package provides the foundation for rendering Servly components in any JavaScript environment.

## Installation

```bash
npm install @servlyadmin/runtime-core
# or
yarn add @servlyadmin/runtime-core
# or
pnpm add @servlyadmin/runtime-core
```

## Quick Start

The simplest way to render a component:

```typescript
import { mount } from '@servlyadmin/runtime-core';

// Mount a component - that's it!
const app = await mount({
  componentId: 'my-component-id',
  target: '#app',
  props: { title: 'Hello World' }
});

// Update props
app.update({ props: { title: 'Updated!' } });

// Cleanup
app.destroy();
```

### With Callbacks

```typescript
const app = await mount({
  componentId: 'my-component-id',
  target: '#app',
  props: { title: 'Hello' },
  onReady: (result) => console.log('Mounted!', result.version),
  onError: (err) => console.error('Failed:', err)
});
```

### With Loading & Error States

```typescript
const app = await mount({
  componentId: 'my-component-id',
  target: '#app',
  props: { title: 'Hello' },
  
  // Show loading indicator
  loadingComponent: '<div class="skeleton animate-pulse h-32 bg-gray-200 rounded"></div>',
  
  // Show error message on failure
  errorComponent: (err) => `<div class="text-red-500">Failed to load: ${err.message}</div>`,
  
  // Optional: delay before showing loading (avoids flash for fast loads)
  loadingDelay: 200,
  
  // Optional: minimum time to show loading (avoids flash)
  minLoadingTime: 500,
  
  // Lifecycle callbacks
  onLoadStart: () => console.log('Loading...'),
  onLoadEnd: () => console.log('Done!'),
  onReady: (result) => console.log('Mounted!'),
  onError: (err) => console.error('Failed:', err)
});
```

### With Version & Cache Control

```typescript
const app = await mount({
  componentId: 'my-component-id',
  target: document.getElementById('app'),
  props: { title: 'Hello' },
  version: '^1.0.0',
  fetchOptions: {
    cacheStrategy: 'memory',
    forceRefresh: true
  }
});
```

## Low-Level API

For more control, use `fetchComponent` and `render` directly:

```typescript
import { render, fetchComponent } from '@servlyadmin/runtime-core';

// Fetch a component from the registry
const { data } = await fetchComponent('my-component-id');

// Render to a container
const result = render({
  container: document.getElementById('app'),
  elements: data.layout,
  context: {
    props: { title: 'Hello World' },
    state: {},
    context: {},
  },
});

// Update props
result.update({
  props: { title: 'Updated Title' },
  state: {},
  context: {},
});

// Cleanup
result.destroy();
```

## Registry Configuration

By default, components are fetched from Servly's production registry:
- **Default URL**: `https://core-api.servly.app/v1/views/registry`

You can override this if you have a custom registry:

```typescript
import { setRegistryUrl } from '@servlyadmin/runtime-core';

// Use a custom registry
setRegistryUrl('https://your-api.com/v1/views/registry');
```

## Cache Strategies

The runtime supports three caching strategies to optimize component loading:

| Strategy | Description | Persistence | Best For |
|----------|-------------|-------------|----------|
| `localStorage` | Persists across browser sessions | Yes | Production apps (default) |
| `memory` | In-memory cache, cleared on page refresh | No | Development, SSR |
| `none` | No caching, always fetches fresh | No | Testing, debugging |

**Default**: `localStorage` - Components are cached in the browser's localStorage for fast subsequent loads.

```typescript
// Use default localStorage caching
const { data } = await fetchComponent('my-component');

// Explicitly set cache strategy
const { data } = await fetchComponent('my-component', {
  cacheStrategy: 'memory',  // or 'localStorage' or 'none'
});

// Force refresh (bypass cache)
const { data } = await fetchComponent('my-component', {
  forceRefresh: true,
});
```

## Prefetching

Preload components for faster subsequent rendering:

```typescript
import { prefetch, prefetchAll, prefetchOnHover, prefetchOnVisible, prefetchOnIdle } from '@servlyadmin/runtime-core';

// Prefetch a single component
await prefetch('pricing-card');

// Prefetch multiple components
await prefetchAll(['pricing-card', 'contact-form', 'navbar']);

// Prefetch with versions
await prefetchAll([
  { id: 'pricing-card', version: '^1.0.0' },
  { id: 'contact-form', version: 'latest' }
]);

// Prefetch on hover (great for modals/dialogs)
const cleanup = prefetchOnHover('#open-modal-btn', 'modal-component');
// Later: cleanup() to remove listener

// Prefetch when element becomes visible (Intersection Observer)
const cleanup = prefetchOnVisible('#pricing-section', ['pricing-card', 'feature-list']);

// Prefetch when browser is idle (non-critical components)
prefetchOnIdle(['footer', 'sidebar', 'help-modal']);
```

## Core Concepts

### Layout Elements

Components are defined as a tree of layout elements:

```typescript
interface LayoutElement {
  i: string;           // Unique identifier
  componentId: string; // Element type (container, text, button, etc.)
  configuration?: {    // Element configuration
    classNames?: string;
    style?: Record<string, any>;
    text?: string;
    // ... other attributes
  };
  children?: string[]; // Child element IDs
  parent?: string;     // Parent element ID
}
```

### Binding Context

Data is passed to components through a binding context:

```typescript
interface BindingContext {
  props: Record<string, any>;   // Component props
  state: Record<string, any>;   // Component state
  context: Record<string, any>; // Additional context
}
```

### Template Bindings

Use `{{path}}` syntax to bind data:

```typescript
const elements = [
  {
    i: 'greeting',
    componentId: 'text',
    configuration: {
      text: 'Hello, {{props.name}}!',
      classNames: '{{props.className}}',
    },
  },
];
```

## API Reference

### mount(options) - Recommended

The simplest way to render a component. Handles fetching, container resolution, and rendering.

```typescript
const app = await mount({
  componentId: string,           // Required: Component ID to fetch
  target: string | HTMLElement,  // Required: CSS selector or element
  props?: Record<string, any>,   // Props to pass (default: {})
  state?: Record<string, any>,   // Initial state (default: {})
  context?: Record<string, any>, // Additional context (default: {})
  version?: string,              // Version specifier (default: 'latest')
  eventHandlers?: Record<string, Record<string, (e: Event) => void>>,
  fetchOptions?: {
    cacheStrategy?: 'localStorage' | 'memory' | 'none',
    forceRefresh?: boolean,
    apiKey?: string,
    retryConfig?: RetryConfig,
  },
  
  // Loading & Error States
  loadingComponent?: string | HTMLElement,  // HTML to show while loading
  errorComponent?: (err: Error) => string | HTMLElement,  // Error display
  loadingDelay?: number,         // Delay before showing loading (ms)
  minLoadingTime?: number,       // Minimum loading display time (ms)
  
  // Lifecycle Callbacks
  onLoadStart?: () => void,      // Called when loading starts
  onLoadEnd?: () => void,        // Called when loading ends
  onReady?: (result: MountResult) => void,
  onError?: (error: Error) => void,
});

// Returns MountResult
interface MountResult {
  update(context: Partial<BindingContext>): void;
  destroy(): void;
  rootElement: HTMLElement | null;
  container: HTMLElement;
  data: ComponentData;
  fromCache: boolean;
  version: string;
}
```

### mountData(options)

Mount with pre-fetched data (useful for SSR or custom data sources):

```typescript
import { mountData } from '@servlyadmin/runtime-core';

const app = mountData({
  data: myComponentData,  // Pre-fetched ComponentData
  target: '#app',
  props: { title: 'Hello' }
});
```

### render(options)

Renders elements to a container.

```typescript
const result = render({
  container: HTMLElement,
  elements: LayoutElement[],
  context: BindingContext,
  eventHandlers?: Record<string, Record<string, (e: Event) => void>>,
});

// Returns
interface RenderResult {
  update(context: BindingContext): void;
  destroy(): void;
  getElement(id: string): HTMLElement | null;
}
```

### fetchComponent(id, options?)

Fetches a component from the registry.

```typescript
const { data, fromCache, version } = await fetchComponent('component-id', {
  version: 'latest',           // Version specifier (default: 'latest')
  cacheStrategy: 'localStorage', // 'localStorage' | 'memory' | 'none' (default: 'localStorage')
  forceRefresh: false,         // Bypass cache (default: false)
  retryConfig: {
    maxRetries: 3,             // Number of retry attempts (default: 3)
    initialDelay: 1000,        // Initial retry delay in ms (default: 1000)
    maxDelay: 10000,           // Maximum retry delay in ms (default: 10000)
    backoffMultiplier: 2,      // Exponential backoff multiplier (default: 2)
  },
});
```

### setRegistryUrl(url)

Configure a custom registry URL.

```typescript
import { setRegistryUrl, DEFAULT_REGISTRY_URL } from '@servlyadmin/runtime-core';

// Use custom registry
setRegistryUrl('https://your-api.com/v1/views/registry');

// Reset to default
setRegistryUrl(DEFAULT_REGISTRY_URL);
```

### StateManager

Manages component state with subscriptions.

```typescript
import { StateManager } from '@servlyadmin/runtime-core';

const stateManager = new StateManager({ count: 0 });

// Get/set state
stateManager.set('count', 1);
stateManager.get('count'); // 1
stateManager.set('user.name', 'John');

// Subscribe to changes
const unsubscribe = stateManager.subscribe((event) => {
  console.log('State changed:', event.path, event.value);
});

// Cleanup
stateManager.clear();
```

### EventSystem

Handles events with plugin-based actions.

```typescript
import { EventSystem, getEventSystem } from '@servlyadmin/runtime-core';

const eventSystem = getEventSystem();

// Register custom plugin
eventSystem.registerPlugin('my-action', async (action, context) => {
  console.log('Action executed with:', action.config);
});
```

#### Built-in Plugins

- `executeCode` - Execute arbitrary JavaScript code
- `state-setState` - Update state values
- `navigateTo` - Navigate to URL
- `localStorage-set/get/remove` - LocalStorage operations
- `sessionStorage-set/get` - SessionStorage operations
- `alert` - Show alert dialog
- `console-log` - Log to console
- `clipboard-copy` - Copy text to clipboard
- `scrollTo` - Scroll to element
- `focus/blur` - Focus/blur elements
- `addClass/removeClass/toggleClass` - CSS class manipulation
- `setAttribute/removeAttribute` - Attribute manipulation
- `dispatchEvent` - Dispatch custom events
- `delay` - Add delay between actions

### Cache Management

```typescript
import { 
  clearAllCaches, 
  clearMemoryCache, 
  clearLocalStorageCache,
  getMemoryCacheSize 
} from '@servlyadmin/runtime-core';

// Clear all caches
clearAllCaches();

// Clear specific cache
clearMemoryCache();
clearLocalStorageCache();

// Get cache size
const size = getMemoryCacheSize();
```

### Bindings

Template resolution utilities.

```typescript
import { resolveTemplate, hasTemplateSyntax } from '@servlyadmin/runtime-core';

const context = {
  props: { name: 'World', count: 42 },
  state: {},
  context: {},
};

// Resolve single template
resolveTemplate('Hello, {{props.name}}!', context); // "Hello, World!"

// Check if string has template syntax
hasTemplateSyntax('{{props.name}}'); // true
hasTemplateSyntax('static text');    // false
```

### Prefetch API

Preload components for faster subsequent rendering.

```typescript
import { 
  prefetch, 
  prefetchAll, 
  prefetchOnHover, 
  prefetchOnVisible, 
  prefetchOnIdle 
} from '@servlyadmin/runtime-core';

// Prefetch single component
await prefetch('pricing-card');
await prefetch('pricing-card', '^1.0.0');  // With version

// Prefetch multiple components
const { success, failed } = await prefetchAll([
  'pricing-card',
  'contact-form',
  { id: 'navbar', version: '^2.0.0' }
]);

// Prefetch on hover (returns cleanup function)
const cleanup = prefetchOnHover(
  '#open-modal-btn',  // Target element
  'modal-component',  // Component ID
  'latest',           // Version
  { delay: 100 }      // Options: delay before prefetch
);
// Later: cleanup()

// Prefetch when element becomes visible
const cleanup = prefetchOnVisible(
  '#pricing-section',                    // Target element
  ['pricing-card', 'feature-list'],      // Components to prefetch
  { rootMargin: '100px', threshold: 0 }  // IntersectionObserver options
);

// Prefetch during browser idle time
prefetchOnIdle(
  ['footer', 'sidebar', 'help-modal'],
  { timeout: 5000 }  // Max wait time before forcing prefetch
);
```

## Slots

Components can define slots for content injection:

```typescript
const elements = [
  {
    i: 'card',
    componentId: 'container',
    configuration: { classNames: 'card' },
    children: ['header-slot', 'content-slot'],
  },
  {
    i: 'header-slot',
    componentId: 'slot',
    configuration: {
      slotName: 'header',
    },
    parent: 'card',
  },
  {
    i: 'content-slot',
    componentId: 'slot',
    configuration: {
      slotName: 'default',
    },
    parent: 'card',
  },
];
```

Framework wrappers handle slot content injection automatically.

## TypeScript Support

Full TypeScript support with exported types:

```typescript
import type {
  LayoutElement,
  BindingContext,
  RenderResult,
  RenderOptions,
  ComponentData,
  CacheStrategy,
  RetryConfig,
  FetchOptions,
  FetchResult,
} from '@servlyadmin/runtime-core';
```

## Browser Support

- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+

## License

MIT