docs: reorder & testing
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package push
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Event is the typed contract for client events emitted onto the gRPC
|
||||
// push stream. Implementations carry their own serialiser; push.Service
|
||||
// invokes Marshal at publish time to obtain the bytes that go into
|
||||
// `pushv1.ClientEvent.Payload`.
|
||||
//
|
||||
// Notification dispatcher builds a typed FlatBuffers Event for every
|
||||
// catalog kind through `notification.buildClientPushEvent`, backed by
|
||||
// the per-kind helpers in `pkg/transcoder/notification.go`. JSONEvent
|
||||
// (below) remains the safety net for kinds that arrive without a
|
||||
// catalog schema.
|
||||
type Event interface {
|
||||
// Kind returns the catalog kind of this event (`backend/README.md`
|
||||
// §10). Empty kind is rejected at publish time.
|
||||
Kind() string
|
||||
|
||||
// Marshal returns the bytes that travel inside
|
||||
// `pushv1.ClientEvent.Payload`. Implementations are expected to use
|
||||
// FlatBuffers (preferred) or any deterministic encoding the client
|
||||
// can decode; the push transport treats the result as opaque
|
||||
// payload bytes.
|
||||
Marshal() ([]byte, error)
|
||||
}
|
||||
|
||||
// JSONEvent is the safety-net Event implementation for kinds that
|
||||
// arrive without a catalog FlatBuffers schema. It serialises Payload
|
||||
// via encoding/json so a misconfigured producer cannot silently drop
|
||||
// events while a new kind is being added.
|
||||
//
|
||||
// New kinds must ship with a typed FlatBuffers schema in
|
||||
// `pkg/schema/fbs/notification.fbs` and a matching case in
|
||||
// `notification.buildClientPushEvent`; JSONEvent is not a canonical
|
||||
// shape, only a fallback.
|
||||
type JSONEvent struct {
|
||||
// EventKind is the catalog kind returned by Kind().
|
||||
EventKind string
|
||||
|
||||
// Payload is the JSON-serialisable map written by the producer.
|
||||
Payload map[string]any
|
||||
}
|
||||
|
||||
// Kind returns EventKind verbatim.
|
||||
func (e JSONEvent) Kind() string { return e.EventKind }
|
||||
|
||||
// Marshal returns Payload encoded as JSON. The result is treated as
|
||||
// opaque bytes by the push transport.
|
||||
func (e JSONEvent) Marshal() ([]byte, error) {
|
||||
return json.Marshal(e.Payload)
|
||||
}
|
||||
|
||||
var _ Event = JSONEvent{}
|
||||
@@ -33,7 +33,7 @@ func TestPublishClientEventStampsCursorAndPayload(t *testing.T) {
|
||||
userID := uuid.New()
|
||||
devID := uuid.New()
|
||||
payload := map[string]any{"game_id": "g1", "n": 7.0}
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, &devID, "lobby.invite.received", payload, "route-1", "req-1", "trace-1"))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, &devID, JSONEvent{EventKind: "lobby.invite.received", Payload: payload},"route-1", "req-1", "trace-1"))
|
||||
|
||||
events, stale := svc.ring.since(0, time.Now())
|
||||
require.False(t, stale)
|
||||
@@ -63,7 +63,7 @@ func TestPublishClientEventOmitsDeviceSessionWhenNil(t *testing.T) {
|
||||
t.Cleanup(svc.Close)
|
||||
|
||||
userID := uuid.New()
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "x", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "x"},"", "", ""))
|
||||
|
||||
events, _ := svc.ring.since(0, time.Now())
|
||||
require.Len(t, events, 1)
|
||||
@@ -76,8 +76,8 @@ func TestPublishClientEventRequiresUserAndKind(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
t.Cleanup(svc.Close)
|
||||
|
||||
require.Error(t, svc.PublishClientEvent(context.Background(), uuid.Nil, nil, "k", nil, "", "", ""))
|
||||
require.Error(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, " ", nil, "", "", ""))
|
||||
require.Error(t, svc.PublishClientEvent(context.Background(), uuid.Nil, nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
require.Error(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, JSONEvent{EventKind: " "},"", "", ""))
|
||||
}
|
||||
|
||||
func TestPublishSessionInvalidationStampsCursor(t *testing.T) {
|
||||
@@ -123,7 +123,7 @@ func TestPublishCursorMonotonic(t *testing.T) {
|
||||
|
||||
userID := uuid.New()
|
||||
for range 5 {
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
}
|
||||
events, _ := svc.ring.since(0, time.Now())
|
||||
require.Len(t, events, 5)
|
||||
@@ -137,7 +137,7 @@ func TestPublishOnClosedServiceIsNoop(t *testing.T) {
|
||||
|
||||
svc := newTestService(t)
|
||||
svc.Close()
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
events, _ := svc.ring.since(0, time.Now())
|
||||
assert.Empty(t, events)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ var (
|
||||
)
|
||||
|
||||
type pushClientEventPublisher interface {
|
||||
PublishClientEvent(ctx context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, kind string, payload map[string]any, eventID, requestID, traceID string) error
|
||||
PublishClientEvent(ctx context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, event Event, eventID, requestID, traceID string) error
|
||||
}
|
||||
|
||||
type pushSessionInvalidationEmitter interface {
|
||||
|
||||
+18
-12
@@ -19,7 +19,6 @@ package push
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -131,23 +130,30 @@ func (s *Service) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// PublishClientEvent enqueues a ClientEvent for delivery. payload is
|
||||
// marshalled to JSON; deviceSessionID is optional. eventID, requestID
|
||||
// and traceID are correlation identifiers that gateway forwards
|
||||
// verbatim into the signed client envelope (typically the producing
|
||||
// route id, the originating client request id, and the trace id of the
|
||||
// span that produced the event); empty strings are forwarded
|
||||
// unchanged. The method satisfies notification.PushPublisher.
|
||||
func (s *Service) PublishClientEvent(_ context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, kind string, payload map[string]any, eventID, requestID, traceID string) error {
|
||||
// PublishClientEvent enqueues a ClientEvent for delivery. The typed
|
||||
// `event` carries both the catalog kind and the payload bytes;
|
||||
// push.Service invokes event.Marshal() at publish time so producers
|
||||
// stay decoupled from the wire encoding. deviceSessionID is optional.
|
||||
// eventID, requestID and traceID are correlation identifiers that
|
||||
// gateway forwards verbatim into the signed client envelope (typically
|
||||
// the producing route id, the originating client request id, and the
|
||||
// trace id of the span that produced the event); empty strings are
|
||||
// forwarded unchanged. The method satisfies
|
||||
// notification.PushPublisher.
|
||||
func (s *Service) PublishClientEvent(_ context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, event Event, eventID, requestID, traceID string) error {
|
||||
if event == nil {
|
||||
return errors.New("push.PublishClientEvent: event is required")
|
||||
}
|
||||
if userID == uuid.Nil {
|
||||
return errors.New("push.PublishClientEvent: userID is required")
|
||||
}
|
||||
kind := event.Kind()
|
||||
if strings.TrimSpace(kind) == "" {
|
||||
return errors.New("push.PublishClientEvent: kind is required")
|
||||
return errors.New("push.PublishClientEvent: event kind is required")
|
||||
}
|
||||
encoded, err := json.Marshal(payload)
|
||||
encoded, err := event.Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("push.PublishClientEvent: marshal payload: %w", err)
|
||||
return fmt.Errorf("push.PublishClientEvent: marshal event: %w", err)
|
||||
}
|
||||
ev := &pushv1.PushEvent{
|
||||
Kind: &pushv1.PushEvent_ClientEvent{
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestSubscribePushDeliversLiveEvents(t *testing.T) {
|
||||
require.Eventually(t, func() bool { return svc.SubscriberCount() == 1 }, time.Second, 5*time.Millisecond)
|
||||
|
||||
userID := uuid.New()
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
|
||||
ev, err := recvOne(t, stream, time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -104,7 +104,7 @@ func TestSubscribePushReplaysPastEventsOnReconnect(t *testing.T) {
|
||||
|
||||
userID := uuid.New()
|
||||
for range 3 {
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
}
|
||||
|
||||
client, cleanup := startBufconnServer(t, svc)
|
||||
@@ -129,7 +129,7 @@ func TestSubscribePushSkipsReplayWhenCursorStale(t *testing.T) {
|
||||
|
||||
userID := uuid.New()
|
||||
for range 4 {
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
}
|
||||
// Ring capacity 2 means cursors 1 and 2 are evicted.
|
||||
|
||||
@@ -141,7 +141,7 @@ func TestSubscribePushSkipsReplayWhenCursorStale(t *testing.T) {
|
||||
require.Eventually(t, func() bool { return svc.SubscriberCount() == 1 }, time.Second, 5*time.Millisecond)
|
||||
|
||||
// Stale cursor → no replay; live publish must arrive.
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
ev, err := recvOne(t, stream, time.Second)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, formatCursor(5), ev.Cursor)
|
||||
@@ -173,7 +173,7 @@ func TestSubscribePushReplacesExistingClientID(t *testing.T) {
|
||||
require.Eventually(t, func() bool { return svc.SubscriberCount() == 1 }, time.Second, 5*time.Millisecond)
|
||||
|
||||
// Live publish reaches the replacement.
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, "k", nil, "", "", ""))
|
||||
require.NoError(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, JSONEvent{EventKind: "k"},"", "", ""))
|
||||
ev, err := recvOne(t, stream2, time.Second)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, ev.Cursor)
|
||||
|
||||
Reference in New Issue
Block a user