// 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/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 } // 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 } } } // Registry exposes the long-lived stores the services own so the caller (main) // can manage their lifecycle — for example start the notify/datastore janitors // that reclaim expired entries. type Registry struct { Notify *notify.Store Data *datastore.Store } // 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) return &Registry{ Notify: registerNotify(hub, o.notifyTrigger), Data: registerDatastore(hub), } } // ---- 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