2

I'm trying to send messages using Server Sent Events to JS clients. The client gets only every 6th or 7th event. What am I doing wrong?

The behavior can be reproduced using a simple standalone sample:

using System;
using System.Threading;

using Funq;
using ServiceStack;

namespace ServerSentEvents
{
    public class AppHost : AppSelfHostBase
    {
        /// <summary>
        /// Default constructor.
        /// Base constructor requires a name and assembly to locate web service classes.
        /// </summary>
        public AppHost()
            : base("ServerSentEvents", typeof(AppHost).Assembly)
        {

        }

        /// <summary>
        /// Application specific configuration
        /// This method should initialize any IoC resources utilized by your web service classes.
        /// </summary>
        /// <param name="container"></param>
        public override void Configure(Container container)
        {
            SetConfig(new HostConfig
            {
#if DEBUG
                DebugMode = true,
                WebHostPhysicalPath = "~/../..".MapServerPath(),
#endif
            });

            container.Register<IServerEvents>(c => new MemoryServerEvents());
            Plugins.Add(new ServerEventsFeature
            {
                OnPublish = (res, msg) =>
                {
                    // Throws an exception
                    //res.Write("\n\n\n\n\n\n\n\n\n\n");  // Force flush: http://stackoverflow.com/questions/25960723/servicestack-sever-sent-events/25983774#25983774
                    //res.Flush();
                }
            });

            container.Register(new FrontendMessages(container.Resolve<IServerEvents>()));
        }
    }

    public class FrontendMessage
    {
        public string Level { get; set; }
        public string Message { get; set; }
    }

    public class FrontendMessages
    {
        private readonly IServerEvents _serverEvents;
        private Timer _timer;

        public FrontendMessages(IServerEvents serverEvents)
        {
            if (serverEvents == null) throw new ArgumentNullException(nameof(serverEvents));
            _serverEvents = serverEvents;

            var ticks = 0;
            _timer = new Timer(_ => Info($"Tick {ticks++}"), null, 500, 500);
        }

        public void Info(string message, params object[] parameters)
        {
            var frontendMessage = new FrontendMessage
            {
                Level = "success",
                Message = message
            };

            Console.WriteLine("Sending message: " + frontendMessage.Message);
            _serverEvents.NotifyChannel("messages", frontendMessage);
        }
    }
}

And the client:

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="js/jquery-1.11.1.min.js"></script>
    <script src="js/ss-utils.js"></script>
</head>
<body>
<script>
    // Handle messages
    var msgSource = new EventSource('event-stream?channel=messages&t=' + new Date().getTime());
    $(msgSource).handleServerEvents({
        handlers: {
            FrontendMessage: function (msg) {
                console.log('Message from server', msg);
            }
        }
    });
</script>
</body>
</html>

Console log looks like:

Message from server Object {Level: "success", Message: "Tick 28"}
Message from server Object {Level: "success", Message: "Tick 35"}
Message from server Object {Level: "success", Message: "Tick 42"}
Message from server Object {Level: "success", Message: "Tick 49"}
Jörg
  • 803
  • 6
  • 7
  • 1
    Have you checked [this question](http://stackoverflow.com/questions/25960723/servicestack-sever-sent-events?lq=1) ? It looks similar – Martin Verjans May 23 '16 at 12:11
  • Yes, as you can see in the sample I included the suggested OnPublish code. I have commented it out as it throws exceptions in this case. It isn't throwing in my real world code thought. But it doesn't work either. – Jörg May 23 '16 at 15:00

1 Answers1

2

The issue is you're trying to send messages to IServerEvents before the ServerEventsFeature has even been registered since you're starting it immediately in AppHost.Configure() instead of when after the AppHost has been initialized. The actual cause of the issue was that IdleTimeout wasn't being properly initialized when the timer was started and caused each Server Events connection to have a lifetime of 00:00:00 which would mean they'd receive a message than automatically be disposed of and auto-reconnect again - the whole process would take about 6-7 ticks :)

ServiceStack Plugins aren't registered when they're added, they get registered together after AppHost.Configure() which gives other plugins a chance to add/remove/inspect other plugins before they're registered. You also don't need to register MemoryServerEvents since that's the default and the recommended way to initialize a timer with an interval is to use timer.Change() in the timer callback.

Given this I would rewrite your AppHost as:

public class AppHost : AppSelfHostBase
{
    public AppHost()
        : base("ServerSentEvents", typeof(AppHost).Assembly) { }

    public override void Configure(Container container)
    {
        SetConfig(new HostConfig {
#if DEBUG
            DebugMode = true,
            WebHostPhysicalPath = "~/../..".MapServerPath(),
#endif
        });

        Plugins.Add(new ServerEventsFeature());
        container.Register(c => new FrontendMessages(c.Resolve<IServerEvents>()));
    }
}

And have your FrontendMessages only start when Start() is called explicitly, i.e:

public class FrontendMessage
{
    public string Level { get; set; }
    public string Message { get; set; }
}

public class FrontendMessages
{
    private readonly IServerEvents _serverEvents;
    private Timer _timer;

    public FrontendMessages(IServerEvents serverEvents)
    {
        if (serverEvents == null) throw new ArgumentNullException(nameof(serverEvents));
        _serverEvents = serverEvents;
    }

    public void Start()
    {
        var ticks = 0;
        _timer = new Timer(_ => {
            Info($"Tick {ticks++}");
            _timer.Change(500, Timeout.Infinite);
        }, null, 500, Timeout.Infinite);
    }

    public void Info(string message, params object[] parameters)
    {
        var frontendMessage = new FrontendMessage {
            Level = "success",
            Message = message
        };

        Console.WriteLine("Sending message: " + frontendMessage.Message);
        _serverEvents.NotifyChannel("messages", frontendMessage);
    }
}

Then only start it after the AppHost has been initialized, i.e:

class Program
{
    static void Main(string[] args)
    {
        var appHost = new AppHost()
            .Init()
            .Start("http://*:2000/"); //Start AppSelfHost

        appHost.Resolve<FrontendMessages>().Start(); //Start timer

        Process.Start("http://localhost:2000/"); //View in Web browser
        Console.ReadLine();  //Prevent Console App from existing
    }
}
Community
  • 1
  • 1
mythz
  • 141,670
  • 29
  • 246
  • 390