@effing/effie-preview 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { EffieSources } from '@effing/effie';
2
+ import { WarmupEventMap } from '@effing/ffs/sse';
2
3
 
3
4
  /**
4
5
  * Create a source resolver function that handles #reference lookups
@@ -41,55 +42,24 @@ type EffieValidationIssue = {
41
42
  */
42
43
  declare function parseEffieValidationIssues(issues: unknown): EffieValidationIssue[] | undefined;
43
44
 
44
- /** Progress event when a source is processed */
45
- type EffieWarmupProgressEvent = {
46
- url: string;
47
- status: "hit" | "cached" | "error";
48
- cached: number;
49
- failed: number;
50
- total: number;
51
- ms?: number;
52
- error?: string;
53
- };
54
- /** Downloading event during source fetch */
55
- type EffieWarmupDownloadingEvent = {
56
- url: string;
57
- status: "started" | "downloading";
58
- bytesReceived: number;
59
- };
60
- /** Union of all SSE event types */
45
+ /**
46
+ * Union of all warmup SSE event types, derived from the server-side
47
+ * `WarmupEventMap` so the client stays in sync automatically.
48
+ * Each variant is `{ type: K; data: WarmupEventMap[K] }`.
49
+ */
61
50
  type EffieWarmupEvent = {
62
- type: "start";
63
- total: number;
64
- } | {
65
- type: "progress";
66
- data: EffieWarmupProgressEvent;
67
- } | {
68
- type: "downloading";
69
- data: EffieWarmupDownloadingEvent;
70
- } | {
71
- type: "keepalive";
72
- cached: number;
73
- failed: number;
74
- total: number;
75
- } | {
76
- type: "summary";
77
- cached: number;
78
- failed: number;
79
- total: number;
80
- } | {
81
- type: "complete";
82
- status: "ready";
83
- } | {
84
- type: "error";
85
- message: string;
86
- };
51
+ [K in keyof WarmupEventMap & string]: {
52
+ type: K;
53
+ data: WarmupEventMap[K];
54
+ };
55
+ }[keyof WarmupEventMap & string];
87
56
  /** Current warmup state */
88
57
  type EffieWarmupState = {
89
58
  status: "idle" | "connecting" | "warming" | "ready" | "error";
90
59
  total: number;
91
60
  cached: number;
92
61
  failed: number;
62
+ skipped: number;
93
63
  downloading: Map<string, {
94
64
  url: string;
95
65
  bytesReceived: number;
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { EffieWebUrl, EffieBackground, EffieSources, EffieLayer, EffieSegment } from '@effing/effie';
3
3
  import { EffieSourceResolver, EffieValidationIssue, EffieWarmupState } from '../index.js';
4
+ import '@effing/ffs/sse';
4
5
 
5
6
  type EffieCoverPreviewProps = {
6
7
  /** Cover image URL */
@@ -6,46 +6,29 @@ import { AnniePlayer } from "@effing/annie-player/react";
6
6
  function connectEffieWarmupStream(streamUrl, onEvent) {
7
7
  const eventSource = new EventSource(streamUrl);
8
8
  eventSource.addEventListener("start", (e) => {
9
- const data = JSON.parse(e.data);
10
- onEvent({ type: "start", total: data.total });
9
+ onEvent({ type: "start", data: JSON.parse(e.data) });
11
10
  });
12
11
  eventSource.addEventListener("progress", (e) => {
13
- const data = JSON.parse(
14
- e.data
15
- );
16
- onEvent({ type: "progress", data });
12
+ onEvent({ type: "progress", data: JSON.parse(e.data) });
17
13
  });
18
14
  eventSource.addEventListener("downloading", (e) => {
19
- const data = JSON.parse(
20
- e.data
21
- );
22
- onEvent({ type: "downloading", data });
23
- });
24
- eventSource.addEventListener("keepalive", (e) => {
25
- const data = JSON.parse(e.data);
26
15
  onEvent({
27
- type: "keepalive",
28
- cached: data.cached,
29
- failed: data.failed,
30
- total: data.total
16
+ type: "downloading",
17
+ data: JSON.parse(e.data)
31
18
  });
32
19
  });
20
+ eventSource.addEventListener("keepalive", (e) => {
21
+ onEvent({ type: "keepalive", data: JSON.parse(e.data) });
22
+ });
33
23
  eventSource.addEventListener("summary", (e) => {
34
- const data = JSON.parse(e.data);
35
- onEvent({
36
- type: "summary",
37
- cached: data.cached,
38
- failed: data.failed,
39
- total: data.total
40
- });
24
+ onEvent({ type: "summary", data: JSON.parse(e.data) });
41
25
  });
42
26
  eventSource.addEventListener("complete", (e) => {
43
- const data = JSON.parse(e.data);
44
- onEvent({ type: "complete", status: data.status });
27
+ onEvent({ type: "complete", data: JSON.parse(e.data) });
45
28
  eventSource.close();
46
29
  });
47
30
  eventSource.addEventListener("error", () => {
48
- onEvent({ type: "error", message: "Connection lost" });
31
+ onEvent({ type: "error", data: { message: "Connection lost" } });
49
32
  eventSource.close();
50
33
  });
51
34
  return () => eventSource.close();
@@ -541,6 +524,7 @@ function useEffieWarmup(streamUrl) {
541
524
  total: 0,
542
525
  cached: 0,
543
526
  failed: 0,
527
+ skipped: 0,
544
528
  downloading: /* @__PURE__ */ new Map()
545
529
  });
546
530
  const cleanupRef = useRef(null);
@@ -553,6 +537,7 @@ function useEffieWarmup(streamUrl) {
553
537
  total: 0,
554
538
  cached: 0,
555
539
  failed: 0,
540
+ skipped: 0,
556
541
  downloading: /* @__PURE__ */ new Map(),
557
542
  startTime: Date.now()
558
543
  });
@@ -560,7 +545,7 @@ function useEffieWarmup(streamUrl) {
560
545
  setState((prev) => {
561
546
  switch (event.type) {
562
547
  case "start":
563
- return { ...prev, status: "warming", total: event.total };
548
+ return { ...prev, status: "warming", total: event.data.total };
564
549
  case "progress": {
565
550
  const newDownloading = new Map(prev.downloading);
566
551
  newDownloading.delete(hashUrlToId(event.data.url));
@@ -568,6 +553,7 @@ function useEffieWarmup(streamUrl) {
568
553
  ...prev,
569
554
  cached: event.data.cached,
570
555
  failed: event.data.failed,
556
+ skipped: event.data.skipped,
571
557
  downloading: newDownloading
572
558
  };
573
559
  }
@@ -580,21 +566,17 @@ function useEffieWarmup(streamUrl) {
580
566
  return { ...prev, downloading: newDownloading };
581
567
  }
582
568
  case "keepalive":
583
- return {
584
- ...prev,
585
- cached: event.cached,
586
- failed: event.failed
587
- };
588
569
  case "summary":
589
570
  return {
590
571
  ...prev,
591
- cached: event.cached,
592
- failed: event.failed
572
+ cached: event.data.cached,
573
+ failed: event.data.failed,
574
+ skipped: event.data.skipped
593
575
  };
594
576
  case "complete":
595
577
  return { ...prev, status: "ready", endTime: Date.now() };
596
578
  case "error":
597
- return { ...prev, status: "error", error: event.message };
579
+ return { ...prev, status: "error", error: event.data.message };
598
580
  default:
599
581
  return prev;
600
582
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.tsx","../../src/warmup.ts","../../src/utils.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type {\n EffieBackground,\n EffieSegment,\n EffieLayer,\n EffieSources,\n EffieWebUrl,\n} from \"@effing/effie\";\nimport { AnniePlayer } from \"@effing/annie-player/react\";\nimport type { EffieSourceResolver, EffieValidationIssue } from \"../core\";\nimport { connectEffieWarmupStream, type EffieWarmupState } from \"../warmup\";\nimport { hashUrlToId } from \"../utils\";\n\n// ============ Cover Preview ============\n\nexport type EffieCoverPreviewProps = {\n /** Cover image URL */\n cover: EffieWebUrl;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** Optional video URL to show instead of cover image (e.g., after rendering) */\n video?: string | null;\n /** Callback when video starts playing */\n onPlay?: () => void;\n /** Callback when video is fully buffered (entire video downloaded) */\n onFullyBuffered?: () => void;\n /** Class name for the img/video element */\n className?: string;\n /** Style for the img/video element */\n style?: React.CSSProperties;\n};\n\n/**\n * Displays the effie cover image, or a video if provided.\n * Simple component with built-in inline styles.\n */\nexport function EffieCoverPreview({\n cover,\n resolution,\n video,\n onPlay,\n onFullyBuffered,\n className,\n style,\n}: EffieCoverPreviewProps) {\n const fullyBufferedRef = useRef(false);\n const lastVideoRef = useRef<string | null>(null);\n\n // Reset the fully buffered flag when video URL changes\n if (video !== lastVideoRef.current) {\n lastVideoRef.current = video ?? null;\n fullyBufferedRef.current = false;\n }\n\n const handleProgress = (e: React.SyntheticEvent<HTMLVideoElement>) => {\n if (fullyBufferedRef.current || !onFullyBuffered) return;\n\n const vid = e.currentTarget;\n if (vid.buffered.length > 0 && vid.duration > 0) {\n const bufferedEnd = vid.buffered.end(vid.buffered.length - 1);\n if (bufferedEnd >= vid.duration) {\n fullyBufferedRef.current = true;\n onFullyBuffered();\n }\n }\n };\n\n if (video) {\n return (\n <video\n src={video}\n poster={cover}\n crossOrigin=\"anonymous\"\n className={className}\n style={{ ...style, height: resolution.height }}\n controls\n autoPlay\n onPlay={onPlay}\n onProgress={handleProgress}\n />\n );\n }\n\n return (\n <img\n src={cover}\n alt=\"Cover\"\n className={className}\n style={{ ...style, height: resolution.height }}\n />\n );\n}\n\n// ============ Background Preview - Compound Components ============\n\ntype EffieBackgroundPreviewRootProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieBackgroundPreviewRoot({\n className,\n style,\n children,\n}: EffieBackgroundPreviewRootProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieBackgroundPreviewMediaProps = {\n /** Background configuration from effie JSON */\n background: EffieBackground<EffieSources>;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Class name for the media element */\n className?: string;\n /** Style for the media element */\n style?: React.CSSProperties;\n};\n\nfunction EffieBackgroundPreviewMedia({\n background,\n resolveSource,\n className,\n style,\n}: EffieBackgroundPreviewMediaProps) {\n if (background.type === \"color\") {\n return (\n <div\n className={className}\n style={{ ...style, backgroundColor: background.color }}\n />\n );\n }\n\n if (background.type === \"image\") {\n return (\n <img\n src={resolveSource(background.source)}\n alt=\"Background\"\n className={className}\n style={{ objectFit: \"cover\", ...style }}\n />\n );\n }\n\n if (background.type === \"video\") {\n return (\n <video\n src={resolveSource(background.source)}\n className={className}\n style={{ objectFit: \"cover\", ...style }}\n autoPlay\n loop\n muted\n />\n );\n }\n\n return null;\n}\n\ntype EffieBackgroundPreviewInfoProps = {\n /** Background configuration from effie JSON */\n background: EffieBackground<EffieSources>;\n /** Class name for the info section */\n className?: string;\n /** Style for the info section */\n style?: React.CSSProperties;\n};\n\nfunction EffieBackgroundPreviewInfo({\n background,\n className,\n style,\n}: EffieBackgroundPreviewInfoProps) {\n return (\n <div className={className} style={style}>\n <strong>Type:</strong> {background.type}\n {background.type === \"color\" && (\n <div>\n <strong>Color:</strong> {background.color}\n </div>\n )}\n </div>\n );\n}\n\n// Simple pre-composed Background Preview\n\nexport type EffieBackgroundPreviewProps = {\n /** Background configuration from effie JSON */\n background: EffieBackground<EffieSources>;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\nfunction EffieBackgroundPreviewSimple({\n background,\n resolveSource,\n resolution,\n className,\n style,\n}: EffieBackgroundPreviewProps) {\n return (\n <EffieBackgroundPreviewRoot\n className={className}\n style={{\n display: \"flex\",\n gap: \"1rem\",\n ...style,\n }}\n >\n <EffieBackgroundPreviewMedia\n background={background}\n resolveSource={resolveSource}\n style={{\n border: \"1px solid #ddd\",\n width: resolution.width,\n height: resolution.height,\n }}\n />\n <EffieBackgroundPreviewInfo\n background={background}\n style={{\n fontSize: \"0.85rem\",\n color: \"#666\",\n }}\n />\n </EffieBackgroundPreviewRoot>\n );\n}\n\n/**\n * Displays a preview of the effie background (color, image, or video).\n *\n * Usage:\n * - Simple: `<EffieBackgroundPreview background={...} resolveSource={...} />`\n * - Compound: `<EffieBackgroundPreview.Root>...</EffieBackgroundPreview.Root>`\n */\nexport const EffieBackgroundPreview = Object.assign(\n EffieBackgroundPreviewSimple,\n {\n Root: EffieBackgroundPreviewRoot,\n Media: EffieBackgroundPreviewMedia,\n Info: EffieBackgroundPreviewInfo,\n },\n);\n\n// ============ Layer Preview - Compound Components ============\n\ntype EffieLayerPreviewRootProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieLayerPreviewRoot({\n className,\n style,\n children,\n}: EffieLayerPreviewRootProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieLayerPreviewMediaProps = {\n /** Layer configuration from effie JSON */\n layer: EffieLayer<EffieSources>;\n /** Layer index (0-based) for alt text */\n index: number;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** Class name for the media element */\n className?: string;\n /** Style for the media element */\n style?: React.CSSProperties;\n};\n\nfunction EffieLayerPreviewMedia({\n layer,\n index,\n resolveSource,\n resolution,\n className,\n style,\n}: EffieLayerPreviewMediaProps) {\n if (layer.type === \"animation\") {\n return (\n <AnniePlayer\n src={resolveSource(layer.source)}\n height={resolution.height}\n defaultWidth={resolution.width}\n autoLoad={false}\n autoPlay={true}\n className={className}\n style={style}\n />\n );\n }\n\n return (\n <img\n src={resolveSource(layer.source)}\n alt={`Layer ${index + 1}`}\n className={className}\n style={{ ...style, height: resolution.height }}\n />\n );\n}\n\ntype EffieLayerPreviewInfoProps = {\n /** Layer configuration from effie JSON */\n layer: EffieLayer<EffieSources>;\n /** Layer index (0-based) */\n index: number;\n /** Class name for the info section */\n className?: string;\n /** Style for the info section */\n style?: React.CSSProperties;\n};\n\nfunction EffieLayerPreviewInfo({\n layer,\n index,\n className,\n style,\n}: EffieLayerPreviewInfoProps) {\n return (\n <div className={className} style={style}>\n <strong>Layer {index + 1}</strong> ({layer.type})\n {layer.delay !== undefined && layer.delay > 0 && (\n <div>Delay: {layer.delay}s</div>\n )}\n {layer.from !== undefined && <div>From: {layer.from}s</div>}\n {layer.until !== undefined && <div>Until: {layer.until}s</div>}\n {layer.effects && layer.effects.length > 0 && (\n <EffieJsonBlock label=\"Effects\" data={layer.effects} />\n )}\n {layer.motion && <EffieJsonBlock label=\"Motion\" data={layer.motion} />}\n </div>\n );\n}\n\n// Simple pre-composed Layer Preview\n\nexport type EffieLayerPreviewProps = {\n /** Layer configuration from effie JSON */\n layer: EffieLayer<EffieSources>;\n /** Layer index (0-based) */\n index: number;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** How to stack info relative to media: \"horizontal\" (default, info beside) or \"vertical\" (info below) */\n stacking?: \"vertical\" | \"horizontal\";\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\nfunction EffieLayerPreviewSimple({\n layer,\n index,\n resolveSource,\n resolution,\n stacking = \"horizontal\",\n className,\n style,\n}: EffieLayerPreviewProps) {\n return (\n <EffieLayerPreviewRoot\n className={className}\n style={{\n display: \"flex\",\n flexDirection: stacking === \"vertical\" ? \"column\" : \"row\",\n gap: \"1rem\",\n ...style,\n }}\n >\n <EffieLayerPreviewMedia\n layer={layer}\n index={index}\n resolveSource={resolveSource}\n resolution={resolution}\n style={{\n border: \"1px solid #ddd\",\n }}\n />\n <EffieLayerPreviewInfo\n layer={layer}\n index={index}\n style={{\n fontSize: \"0.85rem\",\n color: \"#666\",\n }}\n />\n </EffieLayerPreviewRoot>\n );\n}\n\n/**\n * Displays a preview of a single layer, including its metadata.\n * Uses AnniePlayer for animation layers.\n *\n * Usage:\n * - Simple: `<EffieLayerPreview layer={...} index={...} resolveSource={...} />`\n * - Compound: `<EffieLayerPreview.Root>...</EffieLayerPreview.Root>`\n */\nexport const EffieLayerPreview = Object.assign(EffieLayerPreviewSimple, {\n Root: EffieLayerPreviewRoot,\n Media: EffieLayerPreviewMedia,\n Info: EffieLayerPreviewInfo,\n});\n\n// ============ Segment Preview - Compound Components ============\n\ntype EffieSegmentPreviewRootProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieSegmentPreviewRoot({\n className,\n style,\n children,\n}: EffieSegmentPreviewRootProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieSegmentPreviewHeaderProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieSegmentPreviewHeader({\n className,\n style,\n children,\n}: EffieSegmentPreviewHeaderProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieSegmentPreviewLayersProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieSegmentPreviewLayers({\n className,\n style,\n children,\n}: EffieSegmentPreviewLayersProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\n// Simple pre-composed Segment Preview\n\nexport type EffieSegmentPreviewProps = {\n /** Segment configuration from effie JSON */\n segment: EffieSegment<EffieSources>;\n /** Segment index (0-based) */\n index: number;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** How to stack layers: \"vertical\" (default) or \"horizontal\" */\n stacking?: \"vertical\" | \"horizontal\";\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\nfunction EffieSegmentPreviewSimple({\n segment,\n index,\n resolveSource,\n resolution,\n stacking = \"vertical\",\n className,\n style,\n}: EffieSegmentPreviewProps) {\n const layerStacking = stacking === \"horizontal\" ? \"vertical\" : \"horizontal\";\n\n return (\n <EffieSegmentPreviewRoot\n className={className}\n style={{\n padding: \"1rem\",\n border: \"1px solid #ddd\",\n borderRadius: 8,\n backgroundColor: \"#fafafa\",\n ...style,\n }}\n >\n <EffieSegmentPreviewHeader\n style={{\n fontWeight: 600,\n marginBottom: \"1rem\",\n }}\n >\n <strong>Segment {index + 1}</strong>\n {\" — \"}\n {segment.duration}s\n {segment.transition && (\n <span>\n {\" \"}\n (transition: {segment.transition.type},{\" \"}\n {segment.transition.duration}\n s)\n </span>\n )}\n </EffieSegmentPreviewHeader>\n\n <EffieSegmentPreviewLayers\n style={{\n display: \"flex\",\n flexDirection: stacking === \"horizontal\" ? \"row\" : \"column\",\n flexWrap: stacking === \"horizontal\" ? \"wrap\" : undefined,\n gap: \"1rem\",\n }}\n >\n {segment.layers.map((layer, j) => (\n <EffieLayerPreviewSimple\n key={j}\n layer={layer}\n index={j}\n resolveSource={resolveSource}\n resolution={resolution}\n stacking={layerStacking}\n />\n ))}\n </EffieSegmentPreviewLayers>\n </EffieSegmentPreviewRoot>\n );\n}\n\n/**\n * Displays a preview of a segment with all its layers.\n *\n * Usage:\n * - Simple: `<EffieSegmentPreview segment={...} index={...} resolveSource={...} />`\n * - Compound: `<EffieSegmentPreview.Root>...</EffieSegmentPreview.Root>`\n */\nexport const EffieSegmentPreview = Object.assign(EffieSegmentPreviewSimple, {\n Root: EffieSegmentPreviewRoot,\n Header: EffieSegmentPreviewHeader,\n Layers: EffieSegmentPreviewLayers,\n});\n\n// ============ Validation Errors ============\n\nexport type EffieValidationErrorsProps = {\n /** Error message (e.g., \"Invalid effie data\") */\n error: string;\n /** List of validation issues */\n issues?: EffieValidationIssue[];\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\n/**\n * Displays validation errors with detailed issue breakdown.\n * Simple component with built-in inline styles.\n */\nexport function EffieValidationErrors({\n error,\n issues,\n className,\n style,\n}: EffieValidationErrorsProps) {\n return (\n <div\n className={className}\n style={{\n padding: \"1rem\",\n backgroundColor: \"#fff5f5\",\n border: \"1px solid #ffcccc\",\n borderRadius: 8,\n ...style,\n }}\n >\n <div\n style={{\n color: \"red\",\n fontWeight: 600,\n }}\n >\n Error: {error}\n </div>\n {issues && issues.length > 0 && (\n <ul\n style={{\n margin: \"0.5rem 0\",\n paddingLeft: \"1.5rem\",\n }}\n >\n {issues.map((issue, i) => (\n <li\n key={i}\n style={{\n marginBottom: \"0.25rem\",\n }}\n >\n <code>{issue.path || \"(root)\"}</code>: {issue.message}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\n// ============ Internal Components ============\n\ntype EffieJsonBlockProps = {\n label: string;\n data: unknown;\n};\n\nfunction EffieJsonBlock({ label, data }: EffieJsonBlockProps) {\n return (\n <div>\n <div>{label}:</div>\n <pre\n style={{\n margin: \"4px 0\",\n fontSize: \"0.75rem\",\n backgroundColor: \"#f0f0f0\",\n padding: \"4px 8px\",\n borderRadius: 4,\n overflow: \"auto\",\n }}\n >\n {JSON.stringify(data, null, 2)}\n </pre>\n </div>\n );\n}\n\n// ============ Warmup Hook ============\n\nexport type UseEffieWarmupResult = {\n state: EffieWarmupState;\n isReady: boolean;\n isWarming: boolean;\n hasError: boolean;\n};\n\n/**\n * Hook to connect to a warmup SSE stream and track progress.\n *\n * @param streamUrl - The full SSE stream URL returned by the FFS server (null if FFS not configured)\n */\nexport function useEffieWarmup(streamUrl: string | null): UseEffieWarmupResult {\n const [state, setState] = useState<EffieWarmupState>({\n status: \"idle\",\n total: 0,\n cached: 0,\n failed: 0,\n downloading: new Map(),\n });\n\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n if (!streamUrl) {\n return;\n }\n\n setState({\n status: \"connecting\",\n total: 0,\n cached: 0,\n failed: 0,\n downloading: new Map(),\n startTime: Date.now(),\n });\n\n // streamUrl is now a full URL returned by the FFS server\n cleanupRef.current = connectEffieWarmupStream(streamUrl, (event) => {\n setState((prev) => {\n switch (event.type) {\n case \"start\":\n return { ...prev, status: \"warming\", total: event.total };\n\n case \"progress\": {\n const newDownloading = new Map(prev.downloading);\n newDownloading.delete(hashUrlToId(event.data.url));\n return {\n ...prev,\n cached: event.data.cached,\n failed: event.data.failed,\n downloading: newDownloading,\n };\n }\n\n case \"downloading\": {\n const newDownloading = new Map(prev.downloading);\n newDownloading.set(hashUrlToId(event.data.url), {\n url: event.data.url,\n bytesReceived: event.data.bytesReceived,\n });\n return { ...prev, downloading: newDownloading };\n }\n\n case \"keepalive\":\n return {\n ...prev,\n cached: event.cached,\n failed: event.failed,\n };\n\n case \"summary\":\n return {\n ...prev,\n cached: event.cached,\n failed: event.failed,\n };\n\n case \"complete\":\n return { ...prev, status: \"ready\", endTime: Date.now() };\n\n case \"error\":\n return { ...prev, status: \"error\", error: event.message };\n\n default:\n return prev;\n }\n });\n });\n\n return () => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n };\n }, [streamUrl]);\n\n return {\n state,\n isReady: state.status === \"ready\",\n isWarming: state.status === \"warming\" || state.status === \"connecting\",\n hasError: state.status === \"error\",\n };\n}\n","// ============ Types ============\n\n/** Progress event when a source is processed */\nexport type EffieWarmupProgressEvent = {\n url: string;\n status: \"hit\" | \"cached\" | \"error\";\n cached: number;\n failed: number;\n total: number;\n ms?: number;\n error?: string;\n};\n\n/** Downloading event during source fetch */\nexport type EffieWarmupDownloadingEvent = {\n url: string;\n status: \"started\" | \"downloading\";\n bytesReceived: number;\n};\n\n/** Union of all SSE event types */\nexport type EffieWarmupEvent =\n | { type: \"start\"; total: number }\n | { type: \"progress\"; data: EffieWarmupProgressEvent }\n | { type: \"downloading\"; data: EffieWarmupDownloadingEvent }\n | { type: \"keepalive\"; cached: number; failed: number; total: number }\n | { type: \"summary\"; cached: number; failed: number; total: number }\n | { type: \"complete\"; status: \"ready\" }\n | { type: \"error\"; message: string };\n\n/** Current warmup state */\nexport type EffieWarmupState = {\n status: \"idle\" | \"connecting\" | \"warming\" | \"ready\" | \"error\";\n total: number;\n cached: number;\n failed: number;\n downloading: Map<string, { url: string; bytesReceived: number }>;\n error?: string;\n startTime?: number;\n endTime?: number;\n};\n\n// ============ SSE Connection ============\n\n/**\n * Connects to an SSE warmup stream and calls onEvent for each event.\n * Returns a cleanup function to close the connection.\n *\n * @param streamUrl - Full URL to the warmup SSE endpoint\n * @param onEvent - Callback for each SSE event\n */\nexport function connectEffieWarmupStream(\n streamUrl: string,\n onEvent: (event: EffieWarmupEvent) => void,\n): () => void {\n const eventSource = new EventSource(streamUrl);\n\n eventSource.addEventListener(\"start\", (e) => {\n const data = JSON.parse((e as MessageEvent).data);\n onEvent({ type: \"start\", total: data.total });\n });\n\n eventSource.addEventListener(\"progress\", (e) => {\n const data = JSON.parse(\n (e as MessageEvent).data,\n ) as EffieWarmupProgressEvent;\n onEvent({ type: \"progress\", data });\n });\n\n eventSource.addEventListener(\"downloading\", (e) => {\n const data = JSON.parse(\n (e as MessageEvent).data,\n ) as EffieWarmupDownloadingEvent;\n onEvent({ type: \"downloading\", data });\n });\n\n eventSource.addEventListener(\"keepalive\", (e) => {\n const data = JSON.parse((e as MessageEvent).data);\n onEvent({\n type: \"keepalive\",\n cached: data.cached,\n failed: data.failed,\n total: data.total,\n });\n });\n\n eventSource.addEventListener(\"summary\", (e) => {\n const data = JSON.parse((e as MessageEvent).data);\n onEvent({\n type: \"summary\",\n cached: data.cached,\n failed: data.failed,\n total: data.total,\n });\n });\n\n eventSource.addEventListener(\"complete\", (e) => {\n const data = JSON.parse((e as MessageEvent).data);\n onEvent({ type: \"complete\", status: data.status });\n eventSource.close();\n });\n\n eventSource.addEventListener(\"error\", () => {\n onEvent({ type: \"error\", message: \"Connection lost\" });\n eventSource.close();\n });\n\n return () => eventSource.close();\n}\n","/**\n * Stable, sync ID for a URL for use as a Map key.\n * (We don't want to use the full URL as a Map key because it can be huge.)\n */\nexport function hashUrlToId(url: string): string {\n // FNV-1a 32-bit\n let hash = 0x811c9dc5;\n for (let i = 0; i < url.length; i++) {\n hash ^= url.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n // Convert to unsigned + compact string\n return (hash >>> 0).toString(36);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/index.tsx","../../src/warmup.ts","../../src/utils.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type {\n EffieBackground,\n EffieSegment,\n EffieLayer,\n EffieSources,\n EffieWebUrl,\n} from \"@effing/effie\";\nimport { AnniePlayer } from \"@effing/annie-player/react\";\nimport type { EffieSourceResolver, EffieValidationIssue } from \"../core\";\nimport { connectEffieWarmupStream, type EffieWarmupState } from \"../warmup\";\nimport { hashUrlToId } from \"../utils\";\n\n// ============ Cover Preview ============\n\nexport type EffieCoverPreviewProps = {\n /** Cover image URL */\n cover: EffieWebUrl;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** Optional video URL to show instead of cover image (e.g., after rendering) */\n video?: string | null;\n /** Callback when video starts playing */\n onPlay?: () => void;\n /** Callback when video is fully buffered (entire video downloaded) */\n onFullyBuffered?: () => void;\n /** Class name for the img/video element */\n className?: string;\n /** Style for the img/video element */\n style?: React.CSSProperties;\n};\n\n/**\n * Displays the effie cover image, or a video if provided.\n * Simple component with built-in inline styles.\n */\nexport function EffieCoverPreview({\n cover,\n resolution,\n video,\n onPlay,\n onFullyBuffered,\n className,\n style,\n}: EffieCoverPreviewProps) {\n const fullyBufferedRef = useRef(false);\n const lastVideoRef = useRef<string | null>(null);\n\n // Reset the fully buffered flag when video URL changes\n if (video !== lastVideoRef.current) {\n lastVideoRef.current = video ?? null;\n fullyBufferedRef.current = false;\n }\n\n const handleProgress = (e: React.SyntheticEvent<HTMLVideoElement>) => {\n if (fullyBufferedRef.current || !onFullyBuffered) return;\n\n const vid = e.currentTarget;\n if (vid.buffered.length > 0 && vid.duration > 0) {\n const bufferedEnd = vid.buffered.end(vid.buffered.length - 1);\n if (bufferedEnd >= vid.duration) {\n fullyBufferedRef.current = true;\n onFullyBuffered();\n }\n }\n };\n\n if (video) {\n return (\n <video\n src={video}\n poster={cover}\n crossOrigin=\"anonymous\"\n className={className}\n style={{ ...style, height: resolution.height }}\n controls\n autoPlay\n onPlay={onPlay}\n onProgress={handleProgress}\n />\n );\n }\n\n return (\n <img\n src={cover}\n alt=\"Cover\"\n className={className}\n style={{ ...style, height: resolution.height }}\n />\n );\n}\n\n// ============ Background Preview - Compound Components ============\n\ntype EffieBackgroundPreviewRootProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieBackgroundPreviewRoot({\n className,\n style,\n children,\n}: EffieBackgroundPreviewRootProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieBackgroundPreviewMediaProps = {\n /** Background configuration from effie JSON */\n background: EffieBackground<EffieSources>;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Class name for the media element */\n className?: string;\n /** Style for the media element */\n style?: React.CSSProperties;\n};\n\nfunction EffieBackgroundPreviewMedia({\n background,\n resolveSource,\n className,\n style,\n}: EffieBackgroundPreviewMediaProps) {\n if (background.type === \"color\") {\n return (\n <div\n className={className}\n style={{ ...style, backgroundColor: background.color }}\n />\n );\n }\n\n if (background.type === \"image\") {\n return (\n <img\n src={resolveSource(background.source)}\n alt=\"Background\"\n className={className}\n style={{ objectFit: \"cover\", ...style }}\n />\n );\n }\n\n if (background.type === \"video\") {\n return (\n <video\n src={resolveSource(background.source)}\n className={className}\n style={{ objectFit: \"cover\", ...style }}\n autoPlay\n loop\n muted\n />\n );\n }\n\n return null;\n}\n\ntype EffieBackgroundPreviewInfoProps = {\n /** Background configuration from effie JSON */\n background: EffieBackground<EffieSources>;\n /** Class name for the info section */\n className?: string;\n /** Style for the info section */\n style?: React.CSSProperties;\n};\n\nfunction EffieBackgroundPreviewInfo({\n background,\n className,\n style,\n}: EffieBackgroundPreviewInfoProps) {\n return (\n <div className={className} style={style}>\n <strong>Type:</strong> {background.type}\n {background.type === \"color\" && (\n <div>\n <strong>Color:</strong> {background.color}\n </div>\n )}\n </div>\n );\n}\n\n// Simple pre-composed Background Preview\n\nexport type EffieBackgroundPreviewProps = {\n /** Background configuration from effie JSON */\n background: EffieBackground<EffieSources>;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\nfunction EffieBackgroundPreviewSimple({\n background,\n resolveSource,\n resolution,\n className,\n style,\n}: EffieBackgroundPreviewProps) {\n return (\n <EffieBackgroundPreviewRoot\n className={className}\n style={{\n display: \"flex\",\n gap: \"1rem\",\n ...style,\n }}\n >\n <EffieBackgroundPreviewMedia\n background={background}\n resolveSource={resolveSource}\n style={{\n border: \"1px solid #ddd\",\n width: resolution.width,\n height: resolution.height,\n }}\n />\n <EffieBackgroundPreviewInfo\n background={background}\n style={{\n fontSize: \"0.85rem\",\n color: \"#666\",\n }}\n />\n </EffieBackgroundPreviewRoot>\n );\n}\n\n/**\n * Displays a preview of the effie background (color, image, or video).\n *\n * Usage:\n * - Simple: `<EffieBackgroundPreview background={...} resolveSource={...} />`\n * - Compound: `<EffieBackgroundPreview.Root>...</EffieBackgroundPreview.Root>`\n */\nexport const EffieBackgroundPreview = Object.assign(\n EffieBackgroundPreviewSimple,\n {\n Root: EffieBackgroundPreviewRoot,\n Media: EffieBackgroundPreviewMedia,\n Info: EffieBackgroundPreviewInfo,\n },\n);\n\n// ============ Layer Preview - Compound Components ============\n\ntype EffieLayerPreviewRootProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieLayerPreviewRoot({\n className,\n style,\n children,\n}: EffieLayerPreviewRootProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieLayerPreviewMediaProps = {\n /** Layer configuration from effie JSON */\n layer: EffieLayer<EffieSources>;\n /** Layer index (0-based) for alt text */\n index: number;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** Class name for the media element */\n className?: string;\n /** Style for the media element */\n style?: React.CSSProperties;\n};\n\nfunction EffieLayerPreviewMedia({\n layer,\n index,\n resolveSource,\n resolution,\n className,\n style,\n}: EffieLayerPreviewMediaProps) {\n if (layer.type === \"animation\") {\n return (\n <AnniePlayer\n src={resolveSource(layer.source)}\n height={resolution.height}\n defaultWidth={resolution.width}\n autoLoad={false}\n autoPlay={true}\n className={className}\n style={style}\n />\n );\n }\n\n return (\n <img\n src={resolveSource(layer.source)}\n alt={`Layer ${index + 1}`}\n className={className}\n style={{ ...style, height: resolution.height }}\n />\n );\n}\n\ntype EffieLayerPreviewInfoProps = {\n /** Layer configuration from effie JSON */\n layer: EffieLayer<EffieSources>;\n /** Layer index (0-based) */\n index: number;\n /** Class name for the info section */\n className?: string;\n /** Style for the info section */\n style?: React.CSSProperties;\n};\n\nfunction EffieLayerPreviewInfo({\n layer,\n index,\n className,\n style,\n}: EffieLayerPreviewInfoProps) {\n return (\n <div className={className} style={style}>\n <strong>Layer {index + 1}</strong> ({layer.type})\n {layer.delay !== undefined && layer.delay > 0 && (\n <div>Delay: {layer.delay}s</div>\n )}\n {layer.from !== undefined && <div>From: {layer.from}s</div>}\n {layer.until !== undefined && <div>Until: {layer.until}s</div>}\n {layer.effects && layer.effects.length > 0 && (\n <EffieJsonBlock label=\"Effects\" data={layer.effects} />\n )}\n {layer.motion && <EffieJsonBlock label=\"Motion\" data={layer.motion} />}\n </div>\n );\n}\n\n// Simple pre-composed Layer Preview\n\nexport type EffieLayerPreviewProps = {\n /** Layer configuration from effie JSON */\n layer: EffieLayer<EffieSources>;\n /** Layer index (0-based) */\n index: number;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** How to stack info relative to media: \"horizontal\" (default, info beside) or \"vertical\" (info below) */\n stacking?: \"vertical\" | \"horizontal\";\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\nfunction EffieLayerPreviewSimple({\n layer,\n index,\n resolveSource,\n resolution,\n stacking = \"horizontal\",\n className,\n style,\n}: EffieLayerPreviewProps) {\n return (\n <EffieLayerPreviewRoot\n className={className}\n style={{\n display: \"flex\",\n flexDirection: stacking === \"vertical\" ? \"column\" : \"row\",\n gap: \"1rem\",\n ...style,\n }}\n >\n <EffieLayerPreviewMedia\n layer={layer}\n index={index}\n resolveSource={resolveSource}\n resolution={resolution}\n style={{\n border: \"1px solid #ddd\",\n }}\n />\n <EffieLayerPreviewInfo\n layer={layer}\n index={index}\n style={{\n fontSize: \"0.85rem\",\n color: \"#666\",\n }}\n />\n </EffieLayerPreviewRoot>\n );\n}\n\n/**\n * Displays a preview of a single layer, including its metadata.\n * Uses AnniePlayer for animation layers.\n *\n * Usage:\n * - Simple: `<EffieLayerPreview layer={...} index={...} resolveSource={...} />`\n * - Compound: `<EffieLayerPreview.Root>...</EffieLayerPreview.Root>`\n */\nexport const EffieLayerPreview = Object.assign(EffieLayerPreviewSimple, {\n Root: EffieLayerPreviewRoot,\n Media: EffieLayerPreviewMedia,\n Info: EffieLayerPreviewInfo,\n});\n\n// ============ Segment Preview - Compound Components ============\n\ntype EffieSegmentPreviewRootProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieSegmentPreviewRoot({\n className,\n style,\n children,\n}: EffieSegmentPreviewRootProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieSegmentPreviewHeaderProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieSegmentPreviewHeader({\n className,\n style,\n children,\n}: EffieSegmentPreviewHeaderProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\ntype EffieSegmentPreviewLayersProps = {\n className?: string;\n style?: React.CSSProperties;\n children: React.ReactNode;\n};\n\nfunction EffieSegmentPreviewLayers({\n className,\n style,\n children,\n}: EffieSegmentPreviewLayersProps) {\n return (\n <div className={className} style={style}>\n {children}\n </div>\n );\n}\n\n// Simple pre-composed Segment Preview\n\nexport type EffieSegmentPreviewProps = {\n /** Segment configuration from effie JSON */\n segment: EffieSegment<EffieSources>;\n /** Segment index (0-based) */\n index: number;\n /** Function to resolve source references */\n resolveSource: EffieSourceResolver;\n /** Resolution for preview */\n resolution: { width: number; height: number };\n /** How to stack layers: \"vertical\" (default) or \"horizontal\" */\n stacking?: \"vertical\" | \"horizontal\";\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\nfunction EffieSegmentPreviewSimple({\n segment,\n index,\n resolveSource,\n resolution,\n stacking = \"vertical\",\n className,\n style,\n}: EffieSegmentPreviewProps) {\n const layerStacking = stacking === \"horizontal\" ? \"vertical\" : \"horizontal\";\n\n return (\n <EffieSegmentPreviewRoot\n className={className}\n style={{\n padding: \"1rem\",\n border: \"1px solid #ddd\",\n borderRadius: 8,\n backgroundColor: \"#fafafa\",\n ...style,\n }}\n >\n <EffieSegmentPreviewHeader\n style={{\n fontWeight: 600,\n marginBottom: \"1rem\",\n }}\n >\n <strong>Segment {index + 1}</strong>\n {\" — \"}\n {segment.duration}s\n {segment.transition && (\n <span>\n {\" \"}\n (transition: {segment.transition.type},{\" \"}\n {segment.transition.duration}\n s)\n </span>\n )}\n </EffieSegmentPreviewHeader>\n\n <EffieSegmentPreviewLayers\n style={{\n display: \"flex\",\n flexDirection: stacking === \"horizontal\" ? \"row\" : \"column\",\n flexWrap: stacking === \"horizontal\" ? \"wrap\" : undefined,\n gap: \"1rem\",\n }}\n >\n {segment.layers.map((layer, j) => (\n <EffieLayerPreviewSimple\n key={j}\n layer={layer}\n index={j}\n resolveSource={resolveSource}\n resolution={resolution}\n stacking={layerStacking}\n />\n ))}\n </EffieSegmentPreviewLayers>\n </EffieSegmentPreviewRoot>\n );\n}\n\n/**\n * Displays a preview of a segment with all its layers.\n *\n * Usage:\n * - Simple: `<EffieSegmentPreview segment={...} index={...} resolveSource={...} />`\n * - Compound: `<EffieSegmentPreview.Root>...</EffieSegmentPreview.Root>`\n */\nexport const EffieSegmentPreview = Object.assign(EffieSegmentPreviewSimple, {\n Root: EffieSegmentPreviewRoot,\n Header: EffieSegmentPreviewHeader,\n Layers: EffieSegmentPreviewLayers,\n});\n\n// ============ Validation Errors ============\n\nexport type EffieValidationErrorsProps = {\n /** Error message (e.g., \"Invalid effie data\") */\n error: string;\n /** List of validation issues */\n issues?: EffieValidationIssue[];\n /** Class name for the container */\n className?: string;\n /** Style for the container */\n style?: React.CSSProperties;\n};\n\n/**\n * Displays validation errors with detailed issue breakdown.\n * Simple component with built-in inline styles.\n */\nexport function EffieValidationErrors({\n error,\n issues,\n className,\n style,\n}: EffieValidationErrorsProps) {\n return (\n <div\n className={className}\n style={{\n padding: \"1rem\",\n backgroundColor: \"#fff5f5\",\n border: \"1px solid #ffcccc\",\n borderRadius: 8,\n ...style,\n }}\n >\n <div\n style={{\n color: \"red\",\n fontWeight: 600,\n }}\n >\n Error: {error}\n </div>\n {issues && issues.length > 0 && (\n <ul\n style={{\n margin: \"0.5rem 0\",\n paddingLeft: \"1.5rem\",\n }}\n >\n {issues.map((issue, i) => (\n <li\n key={i}\n style={{\n marginBottom: \"0.25rem\",\n }}\n >\n <code>{issue.path || \"(root)\"}</code>: {issue.message}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\n// ============ Internal Components ============\n\ntype EffieJsonBlockProps = {\n label: string;\n data: unknown;\n};\n\nfunction EffieJsonBlock({ label, data }: EffieJsonBlockProps) {\n return (\n <div>\n <div>{label}:</div>\n <pre\n style={{\n margin: \"4px 0\",\n fontSize: \"0.75rem\",\n backgroundColor: \"#f0f0f0\",\n padding: \"4px 8px\",\n borderRadius: 4,\n overflow: \"auto\",\n }}\n >\n {JSON.stringify(data, null, 2)}\n </pre>\n </div>\n );\n}\n\n// ============ Warmup Hook ============\n\nexport type UseEffieWarmupResult = {\n state: EffieWarmupState;\n isReady: boolean;\n isWarming: boolean;\n hasError: boolean;\n};\n\n/**\n * Hook to connect to a warmup SSE stream and track progress.\n *\n * @param streamUrl - The full SSE stream URL returned by the FFS server (null if FFS not configured)\n */\nexport function useEffieWarmup(streamUrl: string | null): UseEffieWarmupResult {\n const [state, setState] = useState<EffieWarmupState>({\n status: \"idle\",\n total: 0,\n cached: 0,\n failed: 0,\n skipped: 0,\n downloading: new Map(),\n });\n\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n if (!streamUrl) {\n return;\n }\n\n setState({\n status: \"connecting\",\n total: 0,\n cached: 0,\n failed: 0,\n skipped: 0,\n downloading: new Map(),\n startTime: Date.now(),\n });\n\n // streamUrl is now a full URL returned by the FFS server\n cleanupRef.current = connectEffieWarmupStream(streamUrl, (event) => {\n setState((prev) => {\n switch (event.type) {\n case \"start\":\n return { ...prev, status: \"warming\", total: event.data.total };\n\n case \"progress\": {\n const newDownloading = new Map(prev.downloading);\n newDownloading.delete(hashUrlToId(event.data.url));\n return {\n ...prev,\n cached: event.data.cached,\n failed: event.data.failed,\n skipped: event.data.skipped,\n downloading: newDownloading,\n };\n }\n\n case \"downloading\": {\n const newDownloading = new Map(prev.downloading);\n newDownloading.set(hashUrlToId(event.data.url), {\n url: event.data.url,\n bytesReceived: event.data.bytesReceived,\n });\n return { ...prev, downloading: newDownloading };\n }\n\n case \"keepalive\":\n case \"summary\":\n return {\n ...prev,\n cached: event.data.cached,\n failed: event.data.failed,\n skipped: event.data.skipped,\n };\n\n case \"complete\":\n return { ...prev, status: \"ready\", endTime: Date.now() };\n\n case \"error\":\n return { ...prev, status: \"error\", error: event.data.message };\n\n default:\n return prev;\n }\n });\n });\n\n return () => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n };\n }, [streamUrl]);\n\n return {\n state,\n isReady: state.status === \"ready\",\n isWarming: state.status === \"warming\" || state.status === \"connecting\",\n hasError: state.status === \"error\",\n };\n}\n","import type { WarmupEventMap } from \"@effing/ffs/sse\";\n\n// ============ Types ============\n\n/**\n * Union of all warmup SSE event types, derived from the server-side\n * `WarmupEventMap` so the client stays in sync automatically.\n * Each variant is `{ type: K; data: WarmupEventMap[K] }`.\n */\nexport type EffieWarmupEvent = {\n [K in keyof WarmupEventMap & string]: { type: K; data: WarmupEventMap[K] };\n}[keyof WarmupEventMap & string];\n\n/** Current warmup state */\nexport type EffieWarmupState = {\n status: \"idle\" | \"connecting\" | \"warming\" | \"ready\" | \"error\";\n total: number;\n cached: number;\n failed: number;\n skipped: number;\n downloading: Map<string, { url: string; bytesReceived: number }>;\n error?: string;\n startTime?: number;\n endTime?: number;\n};\n\n// ============ SSE Connection ============\n\n/**\n * Connects to an SSE warmup stream and calls onEvent for each event.\n * Returns a cleanup function to close the connection.\n *\n * @param streamUrl - Full URL to the warmup SSE endpoint\n * @param onEvent - Callback for each SSE event\n */\nexport function connectEffieWarmupStream(\n streamUrl: string,\n onEvent: (event: EffieWarmupEvent) => void,\n): () => void {\n const eventSource = new EventSource(streamUrl);\n\n eventSource.addEventListener(\"start\", (e) => {\n onEvent({ type: \"start\", data: JSON.parse((e as MessageEvent).data) });\n });\n\n eventSource.addEventListener(\"progress\", (e) => {\n onEvent({ type: \"progress\", data: JSON.parse((e as MessageEvent).data) });\n });\n\n eventSource.addEventListener(\"downloading\", (e) => {\n onEvent({\n type: \"downloading\",\n data: JSON.parse((e as MessageEvent).data),\n });\n });\n\n eventSource.addEventListener(\"keepalive\", (e) => {\n onEvent({ type: \"keepalive\", data: JSON.parse((e as MessageEvent).data) });\n });\n\n eventSource.addEventListener(\"summary\", (e) => {\n onEvent({ type: \"summary\", data: JSON.parse((e as MessageEvent).data) });\n });\n\n eventSource.addEventListener(\"complete\", (e) => {\n onEvent({ type: \"complete\", data: JSON.parse((e as MessageEvent).data) });\n eventSource.close();\n });\n\n eventSource.addEventListener(\"error\", () => {\n onEvent({ type: \"error\", data: { message: \"Connection lost\" } });\n eventSource.close();\n });\n\n return () => eventSource.close();\n}\n","/**\n * Stable, sync ID for a URL for use as a Map key.\n * (We don't want to use the full URL as a Map key because it can be huge.)\n */\nexport function hashUrlToId(url: string): string {\n // FNV-1a 32-bit\n let hash = 0x811c9dc5;\n for (let i = 0; i < url.length; i++) {\n hash ^= url.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n // Convert to unsigned + compact string\n return (hash >>> 0).toString(36);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effing/effie-preview",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "Preview components for Effie video compositions",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,14 +17,15 @@
17
17
  "dist"
18
18
  ],
19
19
  "dependencies": {
20
- "@effing/annie-player": "0.7.3",
21
- "@effing/effie": "0.7.3"
20
+ "@effing/annie-player": "0.9.0",
21
+ "@effing/effie": "0.9.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/react": "^19.0.0",
25
25
  "tsup": "^8.0.0",
26
26
  "typescript": "^5.9.3",
27
- "vitest": "^3.2.4"
27
+ "vitest": "^3.2.4",
28
+ "@effing/ffs": "0.9.0"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "react": "^18.0.0 || ^19.0.0"