Skip to content

Instantly share code, notes, and snippets.

@Blacksmoke16
Last active February 23, 2024 04:08
Show Gist options
  • Save Blacksmoke16/7411a47516be94a9080ffab95af0b8aa to your computer and use it in GitHub Desktop.
Save Blacksmoke16/7411a47516be94a9080ffab95af0b8aa to your computer and use it in GitHub Desktop.
Crystal Client code for publishing to a Mercure Hub
MERCURE_URL=http://localhost:8000/.well-known/mercure
MERCURE_JWT_SECRET=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM
---
version: '3.8'
services:
nginx:
image: nginx
volumes:
- ./public:/usr/share/nginx/html:ro
environment:
FOO: BAR
ports:
- '8080:80'
mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
SERVER_NAME: ':80' # Disable HTTPS for local dev
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://localhost:8080
anonymous
MERCURE_CORS_ALLOWED_ORIGINS: '*'
command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.dev # Enable dev mode
ports:
- '8000:80'
volumes:
- mercure_data:/data
- mercure_config:/config
volumes:
mercure_data:
mercure_config:
require "./update"
module TokenProviderInterface
abstract def jwt : String
end
@[ADI::Register(_jwt: ENV["MERCURE_JWT_SECRET"])]
record StaticTokenProvider, jwt : String do
include TokenProviderInterface
end
module TokenFactoryInterface
abstract def create(
subscribe : Array(String)? = [] of String,
publish : Array(String)? = [] of String,
additional_claims : Hash(String, String) = {} of String => String
) : String
end
module HubInterface
abstract def url : String
abstract def public_url : String
abstract def token_provider : TokenProviderInterface
abstract def token_factory : TokenFactoryInterface?
abstract def publish(update : Update) : String
end
@[ADI::Register(_url: ENV["MERCURE_URL"])]
class Hub
include HubInterface
getter url : String
getter token_provider : TokenProviderInterface
getter token_factory : TokenFactoryInterface?
@public_url : String?
def initialize(
@url : String,
@token_provider : TokenProviderInterface,
@public_url : String? = nil,
@token_factory : TokenFactoryInterface? = nil
)
end
def public_url : String
@public_url || @url
end
def publish(update : Update) : String
HTTP::Client.post(
@url,
headers: HTTP::Headers{
"Authorization" => "Bearer #{@token_provider.jwt}",
},
form: URI::Params.build do |form|
self.encode form, update
end
).body
end
private def encode(form : URI::Params::Builder, update : Update) : Nil
form.add "topic", update.topics
form.add "data", update.data
if update.private?
form.add "private", "on"
end
if id = update.id
form.add "id", id
end
if type = update.type
form.add "type", type
end
if retry = update.retry
form.add "retry", retry.to_s
end
end
end
<!DOCTYPE html>
<html>
<body>
<script type="application/javascript">
const url = new URL('http://localhost:8000/.well-known/mercure');
url.searchParams.append('topic', 'https://example.com/my-private-topic');
const eventSource = new EventSource(url, { withCredentials: true });
console.log('listening...');
eventSource.onmessage = e => console.log(e);
</script>
</body>
</html>
require "athena"
require "./hub"
@[ADI::Register]
class BookController < ATH::Controller
def initialize(@hub : HubInterface); end
@[ARTA::Get("/book/{id}")]
def book(id : Int32) : String
update = Update.new(
"https://example.com/my-private-topic",
{status: "Secretly checked out #{id}"}.to_json,
private: true
)
@hub.publish update
end
end
ATH.run
name: athena-mercure
version: 0.1.0
description: |
Proof of concept Athena Mercure integration
dependencies:
athena:
github: athena-framework/framework
version: ~> 0.18
struct Update
getter topics : Array(String)
getter data : String
getter? private : Bool
getter id : String?
getter type : String?
getter retry : Int32?
def initialize(
topics : String | Array(String),
@data : String = "",
@private : Bool = false,
@id : String? = nil,
@type : String? = nil,
@retry : Int32? = nil
)
@topics = topics.is_a?(String) ? [topics] : topics
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment