├── src ├── MyCrypto.Backend │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── Models │ │ └── Records.cs │ ├── MyCrypto.Backend.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ ├── Controllers │ │ └── CryptosController.cs │ ├── Repositories │ │ └── FakeRepository.cs │ └── Services │ │ └── FakeStreamData.cs ├── MyCrypto.Playground │ ├── MyCrypto.Playground.csproj │ └── Program.cs └── MyCrypto.Site │ ├── index.html │ └── sse.js ├── LICENSE ├── MyCrypto.sln ├── README.md └── .gitignore /src/MyCrypto.Backend/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCrypto.Backend/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/MyCrypto.Backend/Models/Records.cs: -------------------------------------------------------------------------------- 1 | namespace MyCrypto.Backend.Models; 2 | 3 | public record Crypto(string Name, string Symbol); 4 | public record Data(string Symbol, decimal Price, decimal PriceChange, decimal MarketCap, decimal Volume, DateTime DateTime); -------------------------------------------------------------------------------- /src/MyCrypto.Playground/MyCrypto.Playground.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/MyCrypto.Backend/MyCrypto.Backend.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/MyCrypto.Backend/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "MyCrypto.Backend": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:7115;http://localhost:5115", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/MyCrypto.Site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Consumo de SSE 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Angelo Belchior 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/MyCrypto.Backend/Program.cs: -------------------------------------------------------------------------------- 1 | global using MyCrypto.Backend.Repositories; 2 | global using MyCrypto.Backend.Services; 3 | global using MyCrypto.Backend.Models; 4 | global using System.Text.Json; 5 | using Microsoft.AspNetCore.Server.Kestrel.Core; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | builder.Services.AddControllers(); 10 | builder.Services.AddEndpointsApiExplorer(); 11 | builder.Services.AddSwaggerGen(); 12 | 13 | builder.Services.AddCors(options => 14 | { 15 | options.AddPolicy(name: "ApenasParaTestes", 16 | builder => builder.WithOrigins("http://localhos:51234") 17 | .AllowAnyMethod() 18 | .AllowAnyHeader()); 19 | }); 20 | 21 | builder.Services.Configure(o => o.AllowSynchronousIO = true); 22 | 23 | builder.Services.AddSingleton(); 24 | builder.Services.AddSingleton(); 25 | builder.Services.AddCors(); 26 | 27 | var app = builder.Build(); 28 | 29 | if (app.Environment.IsDevelopment()) 30 | { 31 | app.UseSwagger(); 32 | app.UseSwaggerUI(); 33 | } 34 | 35 | app.UseHttpsRedirection(); 36 | 37 | app.UseCors(x => x 38 | .AllowAnyMethod() 39 | .AllowAnyHeader() 40 | .SetIsOriginAllowed(origin => true) 41 | .AllowCredentials()); 42 | 43 | app.UseAuthorization(); 44 | app.MapControllers(); 45 | app.Run(); -------------------------------------------------------------------------------- /src/MyCrypto.Playground/Program.cs: -------------------------------------------------------------------------------- 1 | global using System.Text.Json; 2 | 3 | await Task.Delay(1000); //Aguarda o Server subir para começar o consumo 4 | 5 | using var client = new HttpClient(); 6 | client.Timeout = TimeSpan.FromSeconds(15); 7 | var url = "https://localhost:7115/cryptos/stream/?filter=*"; 8 | 9 | while (true) 10 | { 11 | try 12 | { 13 | Console.WriteLine("Connecting..."); 14 | var stream = await client.GetStreamAsync(url); 15 | using var reader = new StreamReader(stream); 16 | while (!reader.EndOfStream) 17 | { 18 | var message = await reader.ReadLineAsync(); 19 | if (string.IsNullOrEmpty(message)) continue; 20 | 21 | if (!message.StartsWith("data:")) 22 | { 23 | Console.WriteLine($"Event: {message}"); 24 | continue; 25 | } 26 | 27 | var items = JsonSerializer.Deserialize>(message.Replace("data:", string.Empty)); 28 | if (items is null) continue; 29 | 30 | PrintItems(items); 31 | } 32 | } 33 | catch(Exception ex) 34 | { 35 | Console.WriteLine(ex.Message); 36 | Console.WriteLine("[Error] - Waiting..."); 37 | await Task.Delay(TimeSpan.FromSeconds(5)); 38 | } 39 | } 40 | 41 | void PrintItems(IEnumerable items) 42 | { 43 | foreach (var item in items) 44 | Console.WriteLine(item); 45 | } 46 | 47 | public record Data(string Symbol, decimal Price, decimal PriceChange, decimal MarketCap, decimal Volume, DateTime DateTime); -------------------------------------------------------------------------------- /src/MyCrypto.Backend/Controllers/CryptosController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MyCrypto.Backend.Controllers; 4 | 5 | [ApiController] 6 | [Route("cryptos")] 7 | public class CryptosController : ControllerBase 8 | { 9 | private readonly IRepository _repository; 10 | private readonly IStreamData _streamData; 11 | private readonly ILogger _logger; 12 | public CryptosController(IRepository repository, IStreamData streamData, ILogger logger) 13 | { 14 | _repository = repository; 15 | _streamData = streamData; 16 | _logger = logger; 17 | } 18 | 19 | [HttpGet] 20 | public Task> Get() 21 | => _repository.ListAll(); 22 | 23 | [HttpGet("snapshot")] 24 | public Task> Snapshot() 25 | => _streamData.Snapshot(); 26 | 27 | [HttpGet("stream")] 28 | public async Task Stream(string filter) 29 | { 30 | Response.Headers.Add("Content-Type", "text/event-stream"); 31 | Response.Headers.Add("Cache-Control", "no-cache"); 32 | Response.Headers.Add("Connection", "keep-alive"); 33 | var writer = new StreamWriter(Response.Body); 34 | while (!HttpContext.RequestAborted.IsCancellationRequested) 35 | { 36 | var items = await _streamData.Consume(filter); 37 | var json = JsonSerializer.Serialize(items); 38 | _logger.LogInformation(json); 39 | 40 | await writer.WriteLineAsync("event: crypto"); 41 | await writer.WriteLineAsync($"data: {json}"); 42 | await writer.FlushAsync(); 43 | } 44 | writer.Close(); 45 | writer.Dispose(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/MyCrypto.Backend/Repositories/FakeRepository.cs: -------------------------------------------------------------------------------- 1 | namespace MyCrypto.Backend.Repositories; 2 | 3 | public interface IRepository 4 | { 5 | Task> ListAll(); 6 | } 7 | 8 | internal class FakeRepository : IRepository 9 | { 10 | public Task> ListAll() 11 | => Task.FromResult>(new List 12 | { 13 | new Crypto("Bitcoin", "BTC"), 14 | new Crypto("Ethereum", "ETH"), 15 | new Crypto("BNB", "BNB"), 16 | new Crypto("Tether", "USDT"), 17 | new Crypto("Solana", "SOL"), 18 | new Crypto("XRP", "XRP"), 19 | new Crypto("Cardano", "ADA"), 20 | new Crypto("Avalanche", "AVAX"), 21 | new Crypto("USD Coin", "USDC"), 22 | new Crypto("Chainlink", "LINK"), 23 | new Crypto("Algorand", "ALGO"), 24 | new Crypto("Polygon", "MATIC"), 25 | new Crypto("VeChain", "VET"), 26 | new Crypto("Tron", "TRX"), 27 | new Crypto("ZCash", "ZEC"), 28 | new Crypto("EOS", "EOS"), 29 | new Crypto("Tezos", "XTZ"), 30 | new Crypto("Neo", "NEO"), 31 | new Crypto("Dash", "DASH"), 32 | new Crypto("Stacks", "STX"), 33 | new Crypto("NEM", "NEM"), 34 | new Crypto("Decred", "DCR"), 35 | new Crypto("Storj", "STORJ"), 36 | new Crypto("0x", "ZRX"), 37 | new Crypto("DigiByte", "DGB"), 38 | new Crypto("Polkadot", "DOT"), 39 | new Crypto("Bitcoin Cash", "BCH"), 40 | new Crypto("Litecoin", "LTC"), 41 | new Crypto("Dogecoin", "DOGE"), 42 | new Crypto("Binance Coin", "BNB"), 43 | new Crypto("Polygon", "MATIC"), 44 | }); 45 | } -------------------------------------------------------------------------------- /MyCrypto.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32611.2 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{24053C0E-4DDA-45B9-BE99-CCD2189C7256}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCrypto.Backend", "src\MyCrypto.Backend\MyCrypto.Backend.csproj", "{B0940F58-A715-4256-B46D-0D4B432284C0}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCrypto.Playground", "src\MyCrypto.Playground\MyCrypto.Playground.csproj", "{C10A42D5-9FD6-46A3-9695-BE183AE110E5}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B0940F58-A715-4256-B46D-0D4B432284C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B0940F58-A715-4256-B46D-0D4B432284C0}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B0940F58-A715-4256-B46D-0D4B432284C0}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {B0940F58-A715-4256-B46D-0D4B432284C0}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {C10A42D5-9FD6-46A3-9695-BE183AE110E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {C10A42D5-9FD6-46A3-9695-BE183AE110E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {C10A42D5-9FD6-46A3-9695-BE183AE110E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {C10A42D5-9FD6-46A3-9695-BE183AE110E5}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {B0940F58-A715-4256-B46D-0D4B432284C0} = {24053C0E-4DDA-45B9-BE99-CCD2189C7256} 32 | {C10A42D5-9FD6-46A3-9695-BE183AE110E5} = {24053C0E-4DDA-45B9-BE99-CCD2189C7256} 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {FAFB2C7A-8728-447C-997A-2CBB6646B07F} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/MyCrypto.Backend/Services/FakeStreamData.cs: -------------------------------------------------------------------------------- 1 | namespace MyCrypto.Backend.Services; 2 | 3 | public interface IStreamData 4 | { 5 | Task> Consume(string filter); 6 | Task> Snapshot(); 7 | } 8 | 9 | /// 10 | /// Essa classe simula, de maneira bem irresponsável e simplória, um consumo do Kafka, 11 | /// com valores de variações das cryptos 12 | /// 13 | public class FakeStreamData : IStreamData 14 | { 15 | private bool _cryptosLoaded = false; 16 | private IEnumerable _allCryptos; 17 | private int _totalCryptos; 18 | 19 | private readonly IRepository _repository; 20 | public FakeStreamData(IRepository repository) 21 | { 22 | _repository = repository; 23 | _allCryptos = Enumerable.Empty(); 24 | _totalCryptos = 0; 25 | } 26 | 27 | public async Task> Consume(string filter) 28 | { 29 | //Simulando um tempo de requisição/processamento 30 | var delay = Random.Shared.Next(30, 80); 31 | await Task.Delay(delay); 32 | 33 | //Nesse caso, simula-se que apenas algumas criptos sofreram alterações de valores 34 | var cryptos = await GetRndCryptos(); 35 | var data = new List(); 36 | foreach (var crypto in cryptos) 37 | data.Add(CreateRndData(crypto)); 38 | 39 | //*** 40 | //Simula a opção de escolher algumas cryptos apenas para receber os dados de atualização 41 | if (filter.Equals("*")) return data; 42 | var symbols = filter.Split(",").Select(s => s.Trim()); 43 | return data.Where(i => symbols.Any(symbol => symbol.Equals(i.Symbol, StringComparison.InvariantCultureIgnoreCase))); 44 | //*** 45 | } 46 | 47 | public async Task> Snapshot() 48 | { 49 | await LoadCryptos(); 50 | var data = new List(); 51 | foreach (var crypto in _allCryptos) 52 | data.Add(CreateRndData(crypto)); 53 | 54 | return data; 55 | } 56 | 57 | private static Data CreateRndData(Crypto crypto) 58 | => new(crypto.Symbol, GetRndPrices(), GetRndPrices(true), GetRndPrices(), GetRndPrices(), DateTime.Now); 59 | 60 | /// 61 | /// Simula a criação aleatória de preços. 62 | /// 63 | /// 64 | /// 65 | private static decimal GetRndPrices(bool useRandomSign = false) 66 | { 67 | var x = Random.Shared.NextDouble(); 68 | var y = Random.Shared.NextDouble(); 69 | 70 | var sign = 1; 71 | if (useRandomSign) sign = Random.Shared.Next(0, 2); 72 | if (sign == 0) sign = -1; 73 | 74 | var divider = 100 * sign; 75 | var value = (x / y) * divider; 76 | return Math.Round((decimal)value, 2); 77 | } 78 | 79 | //*** 80 | //Obtém a lista de cryptos e deixa na memória. 81 | //Isso serve para não ficar efetuando consultas desnecessárias todo tempo 82 | //Essa abordagem é apenas para efeito de exemplo 83 | private async Task> GetRndCryptos() 84 | { 85 | await LoadCryptos(); 86 | var count = Random.Shared.Next(1, _totalCryptos + 1); 87 | return _allCryptos.OrderBy(x => Guid.NewGuid()).Take(count); 88 | } 89 | 90 | private async Task LoadCryptos() 91 | { 92 | if (_cryptosLoaded) return; 93 | 94 | _allCryptos = await _repository.ListAll(); 95 | _totalCryptos = _allCryptos.Count(); 96 | _cryptosLoaded = true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introdução 2 | 3 | SSE (Server Sent Events) é uma tecnologia que nos permite "ficar pendurado" em um servidor Http recebendo dados continuamente. 4 | 5 | Diferente do Websocket, o SSE permite apenas o consumo de dados pelo cliente, não sendo possível enviar nenhuma informação para o servidor. 6 | Em resumo, o Websocket tem comunicação bi-direcional (cliente <-> server) e o SSE unidirecional (Server -> Client). 7 | 8 | Esse tipo de tecnologia é muito útil em cenários onde precisamos ter atualizações em tempo real de dados (Que é o caso que esse exemplo se propõe a mostrar). Pelo fato de ser uma via de mão única, acaba sendo mais performático do que o Websocket em muitos casos. 9 | 10 | Mais informações em: 11 | 12 | - https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface 13 | - https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events 14 | 15 | # Sobre o Repositório 16 | 17 | ## MyCrypto-SSE 18 | Exemplo de aplicação que utiliza SSE simulando um consumo em "real time" dos valores de crypto moedas. 19 | 20 | **Reforço aqui que esse projeto serve única e exclusivamente para estudos. Colocar um projeto SSE em produção não é tão trivial, principalmente no que diz respeito a escalabilidade. Produção é vida real, e todos nós sabemos que 'O mundo é diferente da ponte pra cá'...**. 21 | 22 | ### MyCrypto.Backend 23 | 24 | Web API que expõe stream de dados via SSE com informações fakes sobre preços de crypto moedas. 25 | 26 | Note que, todas as informações de valores são criadas aleatoriamente. 27 | 28 | Esse exemplo **simula** de maneira bem simplória um consumo de dados de um serviço de data stream como o Kafka. 29 | 30 | Para esse exemplo, o consumo das informações é indiferente, mas no mundo real, isso impactaria a forma de escalar a sua aplicação. O foco aqui não é entrar nesse mérito. 31 | 32 | Para efeito de testes, o CORS nesse Web Api está sendo ignorado. Nunca faça isso sem entender profundamente como essa feature funciona. 33 | 34 | ### MyCrypto.Playground 35 | 36 | Console Application que consome os dados do Web API. 37 | Serve apenas para demonstrar como consumir dados de SSE usando o HttpClient. 38 | 39 | ### MyCrypto.Site 40 | Exemplo de consumo do Web API usando JS. 41 | É um site simples com apenas dois arquivos, index.html e sse.js. 42 | Não sou (e nem pretendo ser) especialista em JS. 43 | 44 | Utilizei a primeira lib que encontrei no Github para esse fim: https://github.com/mpetazzoni/sse.js. 45 | Funcionou de primeira. Achei bem simples de usar :) 46 | 47 | Para rodar o site eu usei o dotnet-serve, que é uma ferramenta bem legal para subir um servidor HTTP por linha de comando de forma simples e rápida. É o tipo de coisa que facilita muito o dia-a-dia. 48 | 49 | Nesse link https://github.com/natemcmaster/dotnet-serve é possível ter todas as informações para instalação e uso da ferramenta. 50 | 51 | Considere apoiar o desenvolvedor desse projeto ❤️ 52 | 53 | Após instalar o dotnet-serve já é possível rodar o projeto. Para isso execute os seguintes comandos: 54 | 55 | **Para inicar o Backend:** 56 | ``` 57 | cd MyCrypto-SSE/src/MyCrypto.Backend 58 | dotnet run 59 | ``` 60 | 61 | **Para inicar o Playground:** 62 | ``` 63 | cd MyCrypto-SSE/src/MyCrypto.Playground 64 | dotnet run 65 | ``` 66 | 67 | **Para inicar o Site:** 68 | ``` 69 | cd MyCrypto-SSE/src/MyCrypto.Site 70 | dotnet serve -o -S -p 51234 71 | # -o faz com que o navegador seja aberto 72 | # -S roda o site usando https 73 | # -p indica a porta onde a aplicação vai rodar 74 | ``` 75 | 76 | Agradecimentos especiais ao [**Tiago Aguiar**](https://github.com/aguiardev) pela ajuda com a parte de front-end. 💕 77 | -------------------------------------------------------------------------------- /src/MyCrypto.Site/sse.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright (C) 2016 Maxime Petazzoni . 4 | * All rights reserved. 5 | */ 6 | 7 | // Retirado de https://github.com/mpetazzoni/sse.js 8 | 9 | var SSE = function (url, options) { 10 | if (!(this instanceof SSE)) { 11 | return new SSE(url, options); 12 | } 13 | 14 | this.INITIALIZING = -1; 15 | this.CONNECTING = 0; 16 | this.OPEN = 1; 17 | this.CLOSED = 2; 18 | 19 | this.url = url; 20 | 21 | options = options || {}; 22 | this.headers = options.headers || {}; 23 | this.payload = options.payload !== undefined ? options.payload : ''; 24 | this.method = options.method || (this.payload && 'POST' || 'GET'); 25 | this.withCredentials = !!options.withCredentials; 26 | 27 | this.FIELD_SEPARATOR = ':'; 28 | this.listeners = {}; 29 | 30 | this.xhr = null; 31 | this.readyState = this.INITIALIZING; 32 | this.progress = 0; 33 | this.chunk = ''; 34 | 35 | this.addEventListener = function (type, listener) { 36 | if (this.listeners[type] === undefined) { 37 | this.listeners[type] = []; 38 | } 39 | 40 | if (this.listeners[type].indexOf(listener) === -1) { 41 | this.listeners[type].push(listener); 42 | } 43 | }; 44 | 45 | this.removeEventListener = function (type, listener) { 46 | if (this.listeners[type] === undefined) { 47 | return; 48 | } 49 | 50 | var filtered = []; 51 | this.listeners[type].forEach(function (element) { 52 | if (element !== listener) { 53 | filtered.push(element); 54 | } 55 | }); 56 | if (filtered.length === 0) { 57 | delete this.listeners[type]; 58 | } else { 59 | this.listeners[type] = filtered; 60 | } 61 | }; 62 | 63 | this.dispatchEvent = function (e) { 64 | if (!e) { 65 | return true; 66 | } 67 | 68 | e.source = this; 69 | 70 | var onHandler = 'on' + e.type; 71 | if (this.hasOwnProperty(onHandler)) { 72 | this[onHandler].call(this, e); 73 | if (e.defaultPrevented) { 74 | return false; 75 | } 76 | } 77 | 78 | if (this.listeners[e.type]) { 79 | return this.listeners[e.type].every(function (callback) { 80 | callback(e); 81 | return !e.defaultPrevented; 82 | }); 83 | } 84 | 85 | return true; 86 | }; 87 | 88 | this._setReadyState = function (state) { 89 | var event = new CustomEvent('readystatechange'); 90 | event.readyState = state; 91 | this.readyState = state; 92 | this.dispatchEvent(event); 93 | }; 94 | 95 | this._onStreamFailure = function (e) { 96 | this.dispatchEvent(new CustomEvent('error')); 97 | this.close(); 98 | } 99 | 100 | this._onStreamProgress = function (e) { 101 | if (!this.xhr) { 102 | return; 103 | } 104 | 105 | if (this.xhr.status !== 200) { 106 | this._onStreamFailure(e); 107 | return; 108 | } 109 | 110 | if (this.readyState == this.CONNECTING) { 111 | this.dispatchEvent(new CustomEvent('open')); 112 | this._setReadyState(this.OPEN); 113 | } 114 | 115 | var data = this.xhr.responseText.substring(this.progress); 116 | this.progress += data.length; 117 | data.split(/(\r\n|\r|\n){2}/g).forEach(function (part) { 118 | if (part.trim().length === 0) { 119 | this.dispatchEvent(this._parseEventChunk(this.chunk.trim())); 120 | this.chunk = ''; 121 | } else { 122 | this.chunk += part; 123 | } 124 | }.bind(this)); 125 | }; 126 | 127 | this._onStreamLoaded = function (e) { 128 | this._onStreamProgress(e); 129 | 130 | // Parse the last chunk. 131 | this.dispatchEvent(this._parseEventChunk(this.chunk)); 132 | this.chunk = ''; 133 | }; 134 | 135 | /** 136 | * Parse a received SSE event chunk into a constructed event object. 137 | */ 138 | this._parseEventChunk = function (chunk) { 139 | if (!chunk || chunk.length === 0) { 140 | return null; 141 | } 142 | 143 | var e = { 'id': null, 'retry': null, 'data': '', 'event': 'message' }; 144 | chunk.split(/\n|\r\n|\r/).forEach(function (line) { 145 | line = line.trimRight(); 146 | var index = line.indexOf(this.FIELD_SEPARATOR); 147 | if (index <= 0) { 148 | // Line was either empty, or started with a separator and is a comment. 149 | // Either way, ignore. 150 | return; 151 | } 152 | 153 | var field = line.substring(0, index); 154 | if (!(field in e)) { 155 | return; 156 | } 157 | 158 | var value = line.substring(index + 1).trimLeft(); 159 | if (field === 'data') { 160 | e[field] += value; 161 | } else { 162 | e[field] = value; 163 | } 164 | }.bind(this)); 165 | 166 | var event = new CustomEvent(e.event); 167 | event.data = e.data; 168 | event.id = e.id; 169 | return event; 170 | }; 171 | 172 | this._checkStreamClosed = function () { 173 | if (!this.xhr) { 174 | return; 175 | } 176 | 177 | if (this.xhr.readyState === XMLHttpRequest.DONE) { 178 | this._setReadyState(this.CLOSED); 179 | } 180 | }; 181 | 182 | this.stream = function () { 183 | this._setReadyState(this.CONNECTING); 184 | 185 | this.xhr = new XMLHttpRequest(); 186 | this.xhr.addEventListener('progress', this._onStreamProgress.bind(this)); 187 | this.xhr.addEventListener('load', this._onStreamLoaded.bind(this)); 188 | this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this)); 189 | this.xhr.addEventListener('error', this._onStreamFailure.bind(this)); 190 | this.xhr.addEventListener('abort', this._onStreamFailure.bind(this)); 191 | this.xhr.open(this.method, this.url); 192 | for (var header in this.headers) { 193 | this.xhr.setRequestHeader(header, this.headers[header]); 194 | } 195 | this.xhr.withCredentials = this.withCredentials; 196 | this.xhr.send(this.payload); 197 | }; 198 | 199 | this.close = function () { 200 | if (this.readyState === this.CLOSED) { 201 | return; 202 | } 203 | 204 | this.xhr.abort(); 205 | this.xhr = null; 206 | this._setReadyState(this.CLOSED); 207 | }; 208 | }; 209 | 210 | // Export our SSE module for npm.js 211 | if (typeof exports !== 'undefined') { 212 | exports.SSE = SSE; 213 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | --------------------------------------------------------------------------------