diff --git a/modules/sessiontag/sessiontag.go b/modules/sessiontag/sessiontag.go new file mode 100644 index 0000000000..4aaa3262cb --- /dev/null +++ b/modules/sessiontag/sessiontag.go @@ -0,0 +1,38 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Package sessiontag carries a per-page-load identifier from the +// originating HTTP request down to the service- and model-layer SSE +// publishers. The publishers echo the tag back inside event payloads so +// the originating browser tab can suppress its own event after it has +// already applied the optimistic update locally. +// +// It is deliberately tiny and dependency-free so any feature that emits +// Server-Sent Events (project boards, milestones, ...) can share one +// context key without importing one another. +package sessiontag + +import "context" + +// sessionTagCtxKey is the context key under which the X-Session-Tag +// value from the originating HTTP request is stashed. +type sessionTagCtxKey struct{} + +// WithSessionTag returns ctx decorated with the provided session tag. +// Web/API middleware reads the X-Session-Tag header and calls this so +// service- and model-layer publishers can pull the tag back out. +func WithSessionTag(ctx context.Context, tag string) context.Context { + if tag == "" { + return ctx + } + return context.WithValue(ctx, sessionTagCtxKey{}, tag) +} + +// SessionTagFromContext returns the session tag previously stored via +// WithSessionTag, or "" when none was set. +func SessionTagFromContext(ctx context.Context) string { + if v, ok := ctx.Value(sessionTagCtxKey{}).(string); ok { + return v + } + return "" +} diff --git a/routers/common/session_tag.go b/routers/common/session_tag.go index bd91fc2421..fbbdde2065 100644 --- a/routers/common/session_tag.go +++ b/routers/common/session_tag.go @@ -6,7 +6,7 @@ package common import ( "net/http" - "code.gitea.io/gitea/services/project_events" + "code.gitea.io/gitea/modules/sessiontag" ) // SessionTagHeader is the HTTP header browser tabs use to broadcast a @@ -17,7 +17,7 @@ const SessionTagHeader = "X-Session-Tag" // SessionTagMiddleware decorates each incoming request's context with // the X-Session-Tag header value when present. Service- and model- -// layer publishers read the value via project_events.SessionTagFromContext. +// layer publishers read the value via sessiontag.SessionTagFromContext. // // Empty / missing headers are a no-op. func SessionTagMiddleware() func(http.Handler) http.Handler { @@ -25,7 +25,7 @@ func SessionTagMiddleware() func(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tag := r.Header.Get(SessionTagHeader) if tag != "" { - ctx := project_events.WithSessionTag(r.Context(), tag) + ctx := sessiontag.WithSessionTag(r.Context(), tag) r = r.WithContext(ctx) } next.ServeHTTP(w, r) diff --git a/services/project_events/events.go b/services/project_events/events.go index e168f6dcd1..936f32e47e 100644 --- a/services/project_events/events.go +++ b/services/project_events/events.go @@ -25,31 +25,20 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/sessiontag" ) -// sessionTagCtxKey is the context key under which the X-Session-Tag value -// from the originating HTTP request is stashed. Publishers read it via -// SessionTagFromContext to attach to outgoing events so the originating -// browser tab can suppress its own echo. -type sessionTagCtxKey struct{} - -// WithSessionTag returns ctx decorated with the provided session tag. -// Web/API middleware reads the X-Session-Tag header and calls this so -// service- and model-layer publishers can pull the tag back out. +// WithSessionTag re-exports modules/sessiontag.WithSessionTag so existing +// callers of project_events keep working after the context-key helper was +// extracted into its own dependency-free package (shared with +// milestone_events and any future SSE feature). func WithSessionTag(ctx context.Context, tag string) context.Context { - if tag == "" { - return ctx - } - return context.WithValue(ctx, sessionTagCtxKey{}, tag) + return sessiontag.WithSessionTag(ctx, tag) } -// SessionTagFromContext returns the session tag previously stored via -// WithSessionTag, or "" when none was set. +// SessionTagFromContext re-exports modules/sessiontag.SessionTagFromContext. func SessionTagFromContext(ctx context.Context) string { - if v, ok := ctx.Value(sessionTagCtxKey{}).(string); ok { - return v - } - return "" + return sessiontag.SessionTagFromContext(ctx) } // Event payload structs ------------------------------------------------------