MWSE/internal/services/services.go

141 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package services ports the message handlers and lifecycle hooks that lived in
// Source/Services/* of the Node.js engine: YourID, Session, Auth, Room,
// IPPressure and DataTransfer. Each is registered onto a *ws.Hub, which owns the
// router and the connect/disconnect event bus.
//
// Where the original handlers contained outright bugs (a Set used as if it were an
// Array, comparing a Client object to a string id, validating against an
// undefined schema variable, ...) this port implements the *intended* behaviour
// and records the deviation in decisions.md. The on-the-wire message shapes the
// SDK depends on are preserved.
package services
import (
"crypto/sha256"
"encoding/hex"
"git.saqut.com/saqut/mwse/internal/bridge"
"git.saqut.com/saqut/mwse/internal/datastore"
"git.saqut.com/saqut/mwse/internal/notify"
"git.saqut.com/saqut/mwse/internal/protocol"
"git.saqut.com/saqut/mwse/internal/ws"
)
// NotifyTrigger is invoked when a suit notification receives a reply, so MWSE can
// push the result to a 3rd-party application server (#44) instead of that server
// polling for it. The default is a no-op; a real HTTP-backed implementation lives
// in the bridge wiring (#46).
type NotifyTrigger interface {
NotifyReplied(n notify.Notification)
}
type noopTrigger struct{}
func (noopTrigger) NotifyReplied(notify.Notification) {}
// options collects the externally-wired integrations a deployment may supply.
type options struct {
notifyTrigger NotifyTrigger
bridgeInbox *bridge.Inbox // nil = bridge/send not registered
}
// Option configures Register.
type Option func(*options)
// WithNotifyTrigger sets the 3rd-party trigger fired on suit replies (#44).
func WithNotifyTrigger(t NotifyTrigger) Option {
return func(o *options) {
if t != nil {
o.notifyTrigger = t
}
}
}
// WithBridgeInbox enables the bridge/send handler (#46). Clients can then
// route messages into the inbox, which the application server drains via
// POST /api/bridge/inbox.
func WithBridgeInbox(inbox *bridge.Inbox) Option {
return func(o *options) {
if inbox != nil {
o.bridgeInbox = inbox
}
}
}
// Registry exposes the long-lived stores and optional subsystems so the caller
// (main) can manage their lifecycle.
type Registry struct {
Notify *notify.Store
Data *datastore.Store
// Bridge is the inbox registered via WithBridgeInbox; nil when bridge is not
// configured. The HTTP layer drains it via POST /api/bridge/inbox.
Bridge *bridge.Inbox
}
// Register wires every service onto the hub and returns a Registry of the stores
// that need lifecycle management. Call once during startup, before the server
// begins accepting connections. The order mirrors the Node require() order so
// connect-time side effects (id message, private room, session defaults) happen
// in the same sequence; the data-sync services (#43#45) are appended after.
func Register(hub *ws.Hub, opts ...Option) *Registry {
o := options{notifyTrigger: noopTrigger{}}
for _, apply := range opts {
apply(&o)
}
registerYourID(hub)
registerAuth(hub)
registerRoom(hub)
registerDataTransfer(hub)
registerIPPressure(hub, nil)
registerSession(hub)
reg := &Registry{
Notify: registerNotify(hub, o.notifyTrigger),
Data: registerDatastore(hub),
}
if o.bridgeInbox != nil {
registerBridge(hub, o.bridgeInbox)
reg.Bridge = o.bridgeInbox
}
return reg
}
// ---- small response helpers ---------------------------------------------
// success is the ubiquitous {status:"success"} reply.
func success() map[string]any { return map[string]any{"status": "success"} }
// fail is the ubiquitous {status:"fail", message:...} reply.
func fail(message string) map[string]any {
return map[string]any{"status": "fail", "message": message}
}
// toMap coerces an arbitrary decoded JSON value to an object, returning an empty
// map when it is not one (e.g. a missing "filter" field).
func toMap(v any) map[string]any {
if m, ok := v.(map[string]any); ok {
return m
}
return map[string]any{}
}
// sha256hex hashes credentials the same way the original Room service did.
func sha256hex(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])
}
// ids extracts the client ids from a slice of clients.
func ids(clients []*ws.Client) []string {
out := make([]string, 0, len(clients))
for _, c := range clients {
out = append(out, c.ID)
}
return out
}
// handler is a convenience alias matching ws.Handler's shape, used to keep the
// per-service files concise.
type handler = func(c *ws.Client, m protocol.Message) any