141 lines
4.5 KiB
Go
141 lines
4.5 KiB
Go
// 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
|