docs: reorder & testing

This commit is contained in:
Ilia Denisov
2026-05-07 00:58:53 +03:00
committed by GitHub
parent f446c6a2ac
commit 604fe40bcf
148 changed files with 9150 additions and 2757 deletions
+54
View File
@@ -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{}
+7 -7
View File
@@ -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
View File
@@ -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{
+5 -5
View File
@@ -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)