Basic Usage
Create a Sentinel, read its current state, subscribe to invalidations, and release both resources when the owner ends.
import { createViewport } from '@vielzeug/sentinel';
function observeViewport(): () => void {
const viewport = createViewport();
const render = () => {
const { dpr, height, width } = viewport.getSnapshot();
console.log(`${width}×${height} at ${dpr}dpr`);
};
render();
const unsubscribe = viewport.subscribe(render);
return () => {
unsubscribe();
viewport.dispose();
};
}
const stopObserving = observeViewport();
// Call stopObserving() when the owning view unmounts.subscribe() notifies you that the snapshot changed; call getSnapshot() inside the listener to read it. Disposing a Sentinel stops its browser observer, removes its listeners, and aborts disposalSignal.
Observe Window State
Use createViewport() for viewport dimensions and device pixel ratio.
import { createViewport } from '@vielzeug/sentinel';
const viewport = createViewport();
console.log(viewport.getSnapshot().width);
console.log(viewport.getSnapshot().height);
console.log(viewport.getSnapshot().dpr);Use createNetwork() for online status and the optional Network Information API snapshot.
import { createNetwork } from '@vielzeug/sentinel';
const network = createNetwork();
console.log(network.getSnapshot().online);
console.log(network.getSnapshot().connection);connection is null when navigator.connection is unavailable.
Observe Media Queries
Use createMediaQuery() to react to a browser media query.
import { createMediaQuery, SentinelUnavailableError } from '@vielzeug/sentinel';
function observeReducedMotion(): () => void {
try {
const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');
const applyPreference = () => {
document.documentElement.classList.toggle('reduce-motion', reducedMotion.getSnapshot().matches);
};
applyPreference();
const unsubscribe = reducedMotion.subscribe(applyPreference);
return () => {
unsubscribe();
reducedMotion.dispose();
};
} catch (error) {
if (!(error instanceof SentinelUnavailableError)) throw error;
return () => {};
}
}
const stopObserving = observeReducedMotion();
// Call stopObserving() when the owning view unmounts.createMediaQuery() throws SentinelUnavailableError when matchMedia is unavailable.
Observe Elements
Element Size
Use createElementSize() after the target element exists.
import { createElementSize } from '@vielzeug/sentinel';
const panel = document.querySelector<HTMLElement>('[data-panel]');
if (!panel) throw new Error('Panel not found');
const size = createElementSize(panel);
const unsubscribe = size.subscribe(() => {
const current = size.getSnapshot();
if (current) panel.dataset.width = String(current.width);
});The initial state is null until ResizeObserver reports its first measurement.
Intersection
Use createIntersection() to observe visibility relative to the viewport or a custom root.
import { createIntersection } from '@vielzeug/sentinel';
const target = document.querySelector<HTMLElement>('[data-lazy-section]');
if (!target) throw new Error('Section not found');
const intersection = createIntersection(target, {
rootMargin: '100px',
threshold: [0, 0.5, 1],
});
const unsubscribe = intersection.subscribe(() => {
target.hidden = !intersection.getSnapshot()?.isIntersecting;
});The initial state is null until IntersectionObserver reports its first entry.
Control Ownership
Call dispose() to stop observation. Disposal is idempotent.
const viewport = createViewport();
viewport.dispose();
viewport.dispose();Pass an AbortSignal when several Sentinels share one lifetime.
const controller = new AbortController();
const viewport = createViewport({ signal: controller.signal });
const network = createNetwork({ signal: controller.signal });
controller.abort();Sentinel follows the standard external-store shape, so reactive libraries can bridge it without a Sentinel-specific adapter.
Prevent Screen Sleep
Use createWakeLock() to keep the screen awake during active use: for example, during a game, presentation, or long-running task.
import { createWakeLock } from '@vielzeug/sentinel';
const wakeLock = createWakeLock();
wakeLock.request();
const unsubscribe = wakeLock.subscribe(() => {
console.log('Wake lock active:', wakeLock.getSnapshot().active);
});
// Later: release the lock and dispose
wakeLock.release();
unsubscribe();
wakeLock.dispose();The lock auto-releases when the tab is hidden (the browser does this). The Sentinel re-acquires it automatically when the tab becomes visible again if request() was called and release() was not.
Check getSnapshot().supported before acquiring if you need to show different UI when the Wake Lock API is unavailable.
const wakeLock = createWakeLock();
if (wakeLock.getSnapshot().supported) {
wakeLock.request();
}Track Fullscreen State
Use createFullscreen() to observe and drive document fullscreen state: for example, immersing a board or map view:
import { createFullscreen } from '@vielzeug/sentinel';
const fullscreen = createFullscreen();
const unsubscribe = fullscreen.subscribe(() => {
fullscreenButton.pressed = fullscreen.getSnapshot().active;
});
fullscreenButton.addEventListener('click', () => fullscreen.toggle());request(), exit(), and toggle() degrade silently when the Fullscreen API is unavailable or the browser refuses (user activation is required in some browsers). Disposing the sentinel exits fullscreen, so leaving the owning view releases it.
Handle Unavailable APIs
createMediaQuery(), createElementSize(), and createIntersection() report unavailable platform APIs with SentinelUnavailableError.
import { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';
try {
const size = createElementSize(document.body);
size.dispose();
} catch (error) {
if (error instanceof SentinelUnavailableError) {
console.warn(error.message);
} else {
throw error;
}
}Invoke all factories only in a browser client lifecycle. Package imports are safe during SSR, but factories require browser or DOM APIs.
Framework Integration
Sentinel's callback-safe external-store methods integrate directly with framework subscription APIs. Create browser-bound Sentinels in a client owner and dispose them when that owner ends.
import { type Sentinel, type ViewportState } from '@vielzeug/sentinel';
import { useSyncExternalStore } from 'react';
type ViewportSizeProps = {
readonly viewport: Sentinel<ViewportState>;
};
export function ViewportSize({ viewport }: ViewportSizeProps) {
const viewportState = useSyncExternalStore(viewport.subscribe, viewport.getSnapshot);
return <output>{`${viewportState.width}×${viewportState.height}`}</output>;
}<script setup lang="ts">
import { createViewport, type Sentinel, type ViewportState } from '@vielzeug/sentinel';
import { onMounted, onUnmounted, ref } from 'vue';
const viewportState = ref<ViewportState | null>(null);
let viewport: Sentinel<ViewportState> | undefined;
let unsubscribe: (() => void) | undefined;
onMounted(() => {
viewport = createViewport();
const update = () => {
viewportState.value = viewport?.getSnapshot() ?? null;
};
update();
unsubscribe = viewport.subscribe(update);
});
onUnmounted(() => {
unsubscribe?.();
viewport?.dispose();
});
</script>
<template>
<output>
{{ viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…' }}
</output>
</template><script lang="ts">
import { createViewport, type ViewportState } from '@vielzeug/sentinel';
import { onMount } from 'svelte';
let viewportState: ViewportState | null = null;
onMount(() => {
const viewport = createViewport();
const update = () => {
viewportState = viewport.getSnapshot();
};
update();
const unsubscribe = viewport.subscribe(update);
return () => {
unsubscribe();
viewport.dispose();
};
});
</script>
<output>
{viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…'}
</output>Working with Other Vielzeug Libraries
Sentinel + Ripple
Bridge Sentinel stores with fromSubscribable() before deriving reactive values. Dispose the watcher separately; disposing each Sentinel also disposes its bridge through disposalSignal.
import { computed, fromSubscribable, watch } from '@vielzeug/ripple';
import { createMediaQuery, createViewport } from '@vielzeug/sentinel';
const viewport = createViewport();
const mobileQuery = createMediaQuery('(max-width: 768px)');
const viewportState = fromSubscribable(viewport, { signal: viewport.disposalSignal });
const mobileState = fromSubscribable(mobileQuery, { signal: mobileQuery.disposalSignal });
const compact = computed(() => mobileState.value.matches || viewportState.value.width < 400);
const compactWatcher = watch(compact, (value) => console.log('Compact layout:', value), { immediate: true });
compactWatcher.dispose();
mobileQuery.dispose();
viewport.dispose();Sentinel + Ore
Create DOM-dependent Sentinels in onMounted() and register both subscription and Sentinel cleanup with the component.
import { define, html, onCleanup, onMounted, ref } from '@vielzeug/ore';
import { createElementSize } from '@vielzeug/sentinel';
define('measured-panel', {
setup() {
const panel = ref<HTMLElement>();
onMounted(() => {
const element = panel.value;
if (!element) return;
const size = createElementSize(element);
const update = () => {
element.dataset.width = String(size.getSnapshot()?.width ?? 0);
};
const unsubscribe = size.subscribe(update);
onCleanup(() => {
unsubscribe();
size.dispose();
});
});
return html`<section ref=${panel}>Measured panel</section>`;
},
});Best Practices
- Create DOM-dependent Sentinels only after their target elements exist.
- Read the latest snapshot with
getSnapshot()inside subscription listeners. - Unsubscribe Ripple listeners when their owner ends.
- Dispose every Sentinel to release browser observers and event listeners.
- Share an
AbortSignalwhen multiple Sentinels have the same lifetime. - Guard APIs that can throw
SentinelUnavailableError. - Treat
NetworkState.connectionas optional browser enhancement data. - Invoke factories only in browser client lifecycles.