Use SSE to send zaps to overlay
This commit is contained in:
parent
aa584d2cb5
commit
4eaf0e1003
|
@ -1,2 +1,4 @@
|
||||||
.env
|
.env
|
||||||
zaply
|
zaply
|
||||||
|
*_templ.go
|
||||||
|
__livereload
|
|
@ -0,0 +1,8 @@
|
||||||
|
.PHONY: dev
|
||||||
|
|
||||||
|
dev:
|
||||||
|
bash livereload.sh
|
||||||
|
|
||||||
|
build:
|
||||||
|
templ generate
|
||||||
|
go build -o zaply main.go
|
|
@ -16,6 +16,7 @@ var (
|
||||||
PhoenixdLimitedAccessToken string
|
PhoenixdLimitedAccessToken string
|
||||||
CommitLongSha string
|
CommitLongSha string
|
||||||
CommitShortSha string
|
CommitShortSha string
|
||||||
|
Env string
|
||||||
)
|
)
|
||||||
|
|
||||||
func Load(filenames ...string) error {
|
func Load(filenames ...string) error {
|
||||||
|
@ -26,6 +27,7 @@ func Load(filenames ...string) error {
|
||||||
flag.StringVar(&PublicUrl, "PUBLIC_URL", "", "Base URL")
|
flag.StringVar(&PublicUrl, "PUBLIC_URL", "", "Base URL")
|
||||||
flag.StringVar(&PhoenixdURL, "PHOENIXD_URL", "", "Phoenixd URL")
|
flag.StringVar(&PhoenixdURL, "PHOENIXD_URL", "", "Phoenixd URL")
|
||||||
flag.StringVar(&PhoenixdLimitedAccessToken, "PHOENIXD_LIMITED_ACCESS_TOKEN", "", "Phoenixd limited access token")
|
flag.StringVar(&PhoenixdLimitedAccessToken, "PHOENIXD_LIMITED_ACCESS_TOKEN", "", "Phoenixd limited access token")
|
||||||
|
flag.StringVar(&Env, "ENV", "development", "Build environment")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/ekzyis/zaply
|
||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/a-h/templ v0.2.793 // indirect
|
||||||
github.com/btcsuite/btcutil v1.0.2 // indirect
|
github.com/btcsuite/btcutil v1.0.2 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
|
||||||
|
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
|
|
|
@ -7,14 +7,16 @@ type PaymentRequest string
|
||||||
type Lightning interface {
|
type Lightning interface {
|
||||||
CreateInvoice(msats int64, description string) (PaymentRequest, error)
|
CreateInvoice(msats int64, description string) (PaymentRequest, error)
|
||||||
GetInvoice(paymentHash string) (*Invoice, error)
|
GetInvoice(paymentHash string) (*Invoice, error)
|
||||||
|
|
||||||
|
IncomingPayments() chan *Invoice
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invoice struct {
|
type Invoice struct {
|
||||||
PaymentHash string
|
PaymentHash string `json:"paymentHash"`
|
||||||
Preimage string
|
Preimage string `json:"preimage"`
|
||||||
Msats int64
|
Msats int64 `json:"msats"`
|
||||||
Description string
|
Description string `json:"description"`
|
||||||
PaymentRequest string
|
PaymentRequest string `json:"paymentRequest"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
ConfirmedAt time.Time
|
ConfirmedAt time.Time `json:"confirmedAt"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,14 @@ type Phoenixd struct {
|
||||||
accessToken string
|
accessToken string
|
||||||
limitedAccessToken string
|
limitedAccessToken string
|
||||||
webhookUrl string
|
webhookUrl string
|
||||||
|
|
||||||
|
paymentsChan chan *lightning.Invoice
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPhoenixd(opts ...func(*Phoenixd) *Phoenixd) *Phoenixd {
|
func NewPhoenixd(opts ...func(*Phoenixd) *Phoenixd) *Phoenixd {
|
||||||
ln := &Phoenixd{}
|
ln := &Phoenixd{
|
||||||
|
paymentsChan: make(chan *lightning.Invoice),
|
||||||
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(ln)
|
opt(ln)
|
||||||
}
|
}
|
||||||
|
@ -153,18 +157,21 @@ func (p *Phoenixd) GetInvoice(paymentHash string) (*lightning.Invoice, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Phoenixd) WebhookHandler(c echo.Context) error {
|
func (p *Phoenixd) IncomingPayments() chan *lightning.Invoice {
|
||||||
go func() {
|
return p.paymentsChan
|
||||||
var webhook struct {
|
}
|
||||||
Type string `json:"type"`
|
|
||||||
AmountSat int64 `json:"amountSat"`
|
|
||||||
PaymentHash string `json:"paymentHash"`
|
|
||||||
}
|
|
||||||
if err := c.Bind(&webhook); err != nil {
|
|
||||||
c.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func (p *Phoenixd) WebhookHandler(c echo.Context) error {
|
||||||
|
var webhook struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
AmountSat int64 `json:"amountSat"`
|
||||||
|
PaymentHash string `json:"paymentHash"`
|
||||||
|
}
|
||||||
|
if err := c.Bind(&webhook); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
inv, err := p.GetInvoice(webhook.PaymentHash)
|
inv, err := p.GetInvoice(webhook.PaymentHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger().Error(err)
|
c.Logger().Error(err)
|
||||||
|
@ -176,6 +183,8 @@ func (p *Phoenixd) WebhookHandler(c echo.Context) error {
|
||||||
inv.PaymentHash, inv.Msats, inv.Description,
|
inv.PaymentHash, inv.Msats, inv.Description,
|
||||||
inv.CreatedAt.Format(time.RFC3339), inv.ConfirmedAt.Format(time.RFC3339),
|
inv.CreatedAt.Format(time.RFC3339), inv.ConfirmedAt.Format(time.RFC3339),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
p.paymentsChan <- inv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return c.NoContent(http.StatusOK)
|
return c.NoContent(http.StatusOK)
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
PID=$(pidof zaply)
|
||||||
|
DIRS="env/ lightning/ lnurl/ pages/ server/"
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo ":: remote port forwarding for zap-dev.ekzy.is ::"
|
||||||
|
ssh -fnNR 5555:localhost:4444 zap-dev.ekzy.is
|
||||||
|
echo
|
||||||
|
|
||||||
|
function restart_server() {
|
||||||
|
set +e
|
||||||
|
[[ -z "$PID" ]] || kill -15 $PID
|
||||||
|
ENV=development make build -B
|
||||||
|
set -e
|
||||||
|
./zaply 2>&1 &
|
||||||
|
PID=$(pidof zaply)
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart() {
|
||||||
|
restart_server
|
||||||
|
# give server time start listening for connections
|
||||||
|
sleep 1
|
||||||
|
date +%s.%N > public/__livereload
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
rm -f public/__livereload
|
||||||
|
[[ -z "$PID" ]] || kill -15 $PID
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
restart
|
||||||
|
|
||||||
|
while inotifywait -r -e modify $DIRS; do
|
||||||
|
restart
|
||||||
|
done
|
|
@ -0,0 +1,32 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
templ Overlay() {
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>zaply</title>
|
||||||
|
<script src={ GetBaseUrl(ctx) + "/js/htmx.min.js" } integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
||||||
|
<script src={ GetBaseUrl(ctx) + "/js/htmx-sse.js" } crossorigin="anonymous"></script>
|
||||||
|
if GetEnv(ctx) == "development" {
|
||||||
|
<script src={ GetBaseUrl(ctx) + "/js/livereload.js" }></script>
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
const sse = new EventSource("/overlay/sse");
|
||||||
|
sse.onmessage = (event) => {
|
||||||
|
// console.log("event", event)
|
||||||
|
};
|
||||||
|
sse.addEventListener("zap", event => {
|
||||||
|
let inv
|
||||||
|
try {
|
||||||
|
inv = JSON.parse(event.data)
|
||||||
|
} catch(err) {
|
||||||
|
console.error("error parsing zap event", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("zap received:", inv.paymentHash, inv.msats, inv.description)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/ekzyis/zaply/env"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var baseUrlContextKey = "baseUrl"
|
||||||
|
var envContextKey = "env"
|
||||||
|
|
||||||
|
func GetBaseUrl(ctx context.Context) string {
|
||||||
|
if u, ok := ctx.Value(baseUrlContextKey).(string); ok {
|
||||||
|
return strings.TrimRight(u, "/")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnv(ctx context.Context) string {
|
||||||
|
if u, ok := ctx.Value(envContextKey).(string); ok {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
return "development"
|
||||||
|
}
|
||||||
|
|
||||||
|
func OverlayHandler(c echo.Context) error {
|
||||||
|
return render(c, http.StatusOK, Overlay())
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(ctx echo.Context, statusCode int, t templ.Component) error {
|
||||||
|
buf := templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(buf)
|
||||||
|
|
||||||
|
renderContext := context.WithValue(ctx.Request().Context(), baseUrlContextKey, env.PublicUrl)
|
||||||
|
renderContext = context.WithValue(renderContext, envContextKey, env.Env)
|
||||||
|
|
||||||
|
if err := t.Render(renderContext, buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.HTML(statusCode, buf.String())
|
||||||
|
}
|
|
@ -0,0 +1,290 @@
|
||||||
|
/*
|
||||||
|
Server Sent Events Extension
|
||||||
|
============================
|
||||||
|
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
/** @type {import("../htmx").HtmxInternalApi} */
|
||||||
|
var api
|
||||||
|
|
||||||
|
htmx.defineExtension('sse', {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init saves the provided reference to the internal HTMX API.
|
||||||
|
*
|
||||||
|
* @param {import("../htmx").HtmxInternalApi} api
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
init: function(apiRef) {
|
||||||
|
// store a reference to the internal API.
|
||||||
|
api = apiRef
|
||||||
|
|
||||||
|
// set a function in the public API for creating new EventSource objects
|
||||||
|
if (htmx.createEventSource == undefined) {
|
||||||
|
htmx.createEventSource = createEventSource
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getSelectors: function() {
|
||||||
|
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEvent handles all events passed to this extension.
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Event} evt
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
onEvent: function(name, evt) {
|
||||||
|
var parent = evt.target || evt.detail.elt
|
||||||
|
switch (name) {
|
||||||
|
case 'htmx:beforeCleanupElement':
|
||||||
|
var internalData = api.getInternalData(parent)
|
||||||
|
// Try to remove remove an EventSource when elements are removed
|
||||||
|
var source = internalData.sseEventSource
|
||||||
|
if (source) {
|
||||||
|
api.triggerEvent(parent, 'htmx:sseClose', {
|
||||||
|
source,
|
||||||
|
type: 'nodeReplaced',
|
||||||
|
})
|
||||||
|
internalData.sseEventSource.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
// Try to create EventSources when elements are processed
|
||||||
|
case 'htmx:afterProcessNode':
|
||||||
|
ensureEventSourceOnElement(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/// ////////////////////////////////////////////
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
/// ////////////////////////////////////////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createEventSource is the default method for creating new EventSource objects.
|
||||||
|
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @returns EventSource
|
||||||
|
*/
|
||||||
|
function createEventSource(url) {
|
||||||
|
return new EventSource(url, { withCredentials: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* registerSSE looks for attributes that can contain sse events, right
|
||||||
|
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||||
|
* the closest event source
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} elt
|
||||||
|
*/
|
||||||
|
function registerSSE(elt) {
|
||||||
|
// Add message handlers for every `sse-swap` attribute
|
||||||
|
if (api.getAttributeValue(elt, 'sse-swap')) {
|
||||||
|
// Find closest existing event source
|
||||||
|
var sourceElement = api.getClosestMatch(elt, hasEventSource)
|
||||||
|
if (sourceElement == null) {
|
||||||
|
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||||
|
return null // no eventsource in parentage, orphaned element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set internalData and source
|
||||||
|
var internalData = api.getInternalData(sourceElement)
|
||||||
|
var source = internalData.sseEventSource
|
||||||
|
|
||||||
|
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
|
||||||
|
var sseEventNames = sseSwapAttr.split(',')
|
||||||
|
|
||||||
|
for (var i = 0; i < sseEventNames.length; i++) {
|
||||||
|
const sseEventName = sseEventNames[i].trim()
|
||||||
|
const listener = function(event) {
|
||||||
|
// If the source is missing then close SSE
|
||||||
|
if (maybeCloseSSESource(sourceElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the body no longer contains the element, remove the listener
|
||||||
|
if (!api.bodyContains(elt)) {
|
||||||
|
source.removeEventListener(sseEventName, listener)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// swap the response into the DOM and trigger a notification
|
||||||
|
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
swap(elt, event.data)
|
||||||
|
api.triggerEvent(elt, 'htmx:sseMessage', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the new listener
|
||||||
|
api.getInternalData(elt).sseEventListener = listener
|
||||||
|
source.addEventListener(sseEventName, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
||||||
|
if (api.getAttributeValue(elt, 'hx-trigger')) {
|
||||||
|
// Find closest existing event source
|
||||||
|
var sourceElement = api.getClosestMatch(elt, hasEventSource)
|
||||||
|
if (sourceElement == null) {
|
||||||
|
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||||
|
return null // no eventsource in parentage, orphaned element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set internalData and source
|
||||||
|
var internalData = api.getInternalData(sourceElement)
|
||||||
|
var source = internalData.sseEventSource
|
||||||
|
|
||||||
|
var triggerSpecs = api.getTriggerSpecs(elt)
|
||||||
|
triggerSpecs.forEach(function(ts) {
|
||||||
|
if (ts.trigger.slice(0, 4) !== 'sse:') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener = function (event) {
|
||||||
|
if (maybeCloseSSESource(sourceElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!api.bodyContains(elt)) {
|
||||||
|
source.removeEventListener(ts.trigger.slice(4), listener)
|
||||||
|
}
|
||||||
|
// Trigger events to be handled by the rest of htmx
|
||||||
|
htmx.trigger(elt, ts.trigger, event)
|
||||||
|
htmx.trigger(elt, 'htmx:sseMessage', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the new listener
|
||||||
|
api.getInternalData(elt).sseEventListener = listener
|
||||||
|
source.addEventListener(ts.trigger.slice(4), listener)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||||
|
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||||
|
* is created and stored in the element's internalData.
|
||||||
|
* @param {HTMLElement} elt
|
||||||
|
* @param {number} retryCount
|
||||||
|
* @returns {EventSource | null}
|
||||||
|
*/
|
||||||
|
function ensureEventSourceOnElement(elt, retryCount) {
|
||||||
|
if (elt == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle extension source creation attribute
|
||||||
|
if (api.getAttributeValue(elt, 'sse-connect')) {
|
||||||
|
var sseURL = api.getAttributeValue(elt, 'sse-connect')
|
||||||
|
if (sseURL == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureEventSource(elt, sseURL, retryCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSSE(elt)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureEventSource(elt, url, retryCount) {
|
||||||
|
var source = htmx.createEventSource(url)
|
||||||
|
|
||||||
|
source.onerror = function(err) {
|
||||||
|
// Log an error event
|
||||||
|
api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source })
|
||||||
|
|
||||||
|
// If parent no longer exists in the document, then clean up this EventSource
|
||||||
|
if (maybeCloseSSESource(elt)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try to reconnect the EventSource
|
||||||
|
if (source.readyState === EventSource.CLOSED) {
|
||||||
|
retryCount = retryCount || 0
|
||||||
|
retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
|
||||||
|
var timeout = retryCount * 500
|
||||||
|
window.setTimeout(function() {
|
||||||
|
ensureEventSourceOnElement(elt, retryCount)
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.onopen = function(evt) {
|
||||||
|
api.triggerEvent(elt, 'htmx:sseOpen', { source })
|
||||||
|
|
||||||
|
if (retryCount && retryCount > 0) {
|
||||||
|
const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]")
|
||||||
|
for (let i = 0; i < childrenToFix.length; i++) {
|
||||||
|
registerSSE(childrenToFix[i])
|
||||||
|
}
|
||||||
|
// We want to increase the reconnection delay for consecutive failed attempts only
|
||||||
|
retryCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.getInternalData(elt).sseEventSource = source
|
||||||
|
|
||||||
|
|
||||||
|
var closeAttribute = api.getAttributeValue(elt, "sse-close");
|
||||||
|
if (closeAttribute) {
|
||||||
|
// close eventsource when this message is received
|
||||||
|
source.addEventListener(closeAttribute, function() {
|
||||||
|
api.triggerEvent(elt, 'htmx:sseClose', {
|
||||||
|
source,
|
||||||
|
type: 'message',
|
||||||
|
})
|
||||||
|
source.close()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* maybeCloseSSESource confirms that the parent element still exists.
|
||||||
|
* If not, then any associated SSE source is closed and the function returns true.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} elt
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
function maybeCloseSSESource(elt) {
|
||||||
|
if (!api.bodyContains(elt)) {
|
||||||
|
var source = api.getInternalData(elt).sseEventSource
|
||||||
|
if (source != undefined) {
|
||||||
|
api.triggerEvent(elt, 'htmx:sseClose', {
|
||||||
|
source,
|
||||||
|
type: 'nodeMissing',
|
||||||
|
})
|
||||||
|
source.close()
|
||||||
|
// source = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} elt
|
||||||
|
* @param {string} content
|
||||||
|
*/
|
||||||
|
function swap(elt, content) {
|
||||||
|
api.withExtensions(elt, function(extension) {
|
||||||
|
content = extension.transformResponse(content, null, elt)
|
||||||
|
})
|
||||||
|
|
||||||
|
var swapSpec = api.getSwapSpecification(elt)
|
||||||
|
var target = api.getTarget(elt)
|
||||||
|
api.swap(target, content, swapSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hasEventSource(node) {
|
||||||
|
return api.getInternalData(node).sseEventSource != null
|
||||||
|
}
|
||||||
|
})()
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,18 @@
|
||||||
|
async function buildTime() {
|
||||||
|
const r = await fetch("/__livereload", { cache: "no-cache"})
|
||||||
|
return r.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function liveReload() {
|
||||||
|
console.log("running in development mode")
|
||||||
|
|
||||||
|
let t_old = await buildTime()
|
||||||
|
setInterval(async () => {
|
||||||
|
let t_new = await buildTime()
|
||||||
|
if (t_old !== t_new) {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
liveReload().catch(console.error)
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/ekzyis/zaply/env"
|
"github.com/ekzyis/zaply/env"
|
||||||
"github.com/ekzyis/zaply/lightning/phoenixd"
|
"github.com/ekzyis/zaply/lightning/phoenixd"
|
||||||
"github.com/ekzyis/zaply/lnurl"
|
"github.com/ekzyis/zaply/lnurl"
|
||||||
|
"github.com/ekzyis/zaply/pages"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
|
@ -41,6 +42,11 @@ func NewServer() *Server {
|
||||||
|
|
||||||
lnurl.Router(s.Echo, p)
|
lnurl.Router(s.Echo, p)
|
||||||
|
|
||||||
|
s.Static("/", "public/")
|
||||||
|
|
||||||
|
s.GET("/overlay", pages.OverlayHandler)
|
||||||
|
s.GET("/overlay/sse", sseHandler(p.IncomingPayments()))
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ekzyis/zaply/lightning"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Event []byte
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ev *Event) MarshalTo(w io.Writer) error {
|
||||||
|
if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range bytes.Split(ev.Data, []byte("\n")) {
|
||||||
|
if _, err := fmt.Fprintf(w, "data: %s\n", line); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Fprint(w, "\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sseHandler(invSrc chan *lightning.Invoice) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
w := c.Response()
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
// disable nginx buffering
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.Request().Context().Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
event := Event{
|
||||||
|
Event: []byte("message"),
|
||||||
|
Data: []byte("keepalive"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := event.MarshalTo(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case inv := <-invSrc:
|
||||||
|
data, err := json.Marshal(inv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
event := Event{
|
||||||
|
Event: []byte("zap"),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("sending zap event: %s", inv.PaymentHash)
|
||||||
|
|
||||||
|
if err := event.MarshalTo(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue