chore: 初始化 Cloudreve 源码仓库
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
package eventhub
|
||||
|
||||
import "errors"
|
||||
|
||||
type (
|
||||
Event struct {
|
||||
Type EventType `json:"type"`
|
||||
FileID string `json:"file_id"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
EventType string
|
||||
)
|
||||
|
||||
const (
|
||||
EventTypeCreate = "create"
|
||||
EventTypeModify = "modify"
|
||||
EventTypeRename = "rename"
|
||||
EventTypeDelete = "delete"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrEventHubClosed is returned when operations are attempted on a closed EventHub.
|
||||
ErrEventHubClosed = errors.New("event hub is closed")
|
||||
)
|
||||
|
||||
// eventState tracks the accumulated state for each file
|
||||
type eventState struct {
|
||||
baseType EventType // The base event type (Create, Delete, or first event type)
|
||||
originalSrc string // Original source path (for Create or first Rename)
|
||||
currentDst string // Current destination path
|
||||
}
|
||||
|
||||
/*
|
||||
Modify + Modify → keep only the last Modify;
|
||||
Create + Modify → fold into a single Create with final metadata/content.
|
||||
Create + Rename(a→b) → Create at b.
|
||||
Create + Delete → drop both (ephemeral object never needs to reach clients).
|
||||
Modify + Delete → Delete (intermediate Modify is irrelevant to final state).
|
||||
Rename(a→b) + Rename(b→c) → Rename(a→c).
|
||||
Rename(a→b) + Modify → emit Rename(a→b) then a single Modify at b (or fold Modify into Create if the chain starts with Create).
|
||||
Rename(a→b) + Delete → emit only Delete(object_id);
|
||||
Rename(a→b) + Rename(b→a) with no intervening Modify → drop both (rename there-and-back is a no-op).
|
||||
Delete + Create might be a valid case, e.g. user restore same file from trash bin.
|
||||
*/
|
||||
// DebounceEvents takes time-ordered events and returns debounced/merged events.
|
||||
func DebounceEvents(in []*Event) []*Event {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
states := make(map[string]*eventState) // keyed by FileID
|
||||
order := make([]string, 0) // to preserve order of first appearance
|
||||
|
||||
for _, e := range in {
|
||||
state, exists := states[e.FileID]
|
||||
|
||||
if !exists {
|
||||
// First event for this file
|
||||
order = append(order, e.FileID)
|
||||
states[e.FileID] = &eventState{
|
||||
baseType: e.Type,
|
||||
originalSrc: e.From,
|
||||
currentDst: e.To,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case EventTypeCreate:
|
||||
// Delete + Create → keep as Create (e.g. restore from trash)
|
||||
if state.baseType == EventTypeDelete {
|
||||
state.baseType = EventTypeCreate
|
||||
state.originalSrc = e.From
|
||||
state.currentDst = ""
|
||||
}
|
||||
|
||||
case EventTypeModify:
|
||||
switch state.baseType {
|
||||
case EventTypeCreate:
|
||||
// Create + Modify → fold into Create (no change needed, Create already implies content)
|
||||
case EventTypeModify:
|
||||
// Modify + Modify → keep only last Modify (state already correct)
|
||||
case EventTypeRename:
|
||||
// Rename + Modify → fold into first Rename
|
||||
case EventTypeDelete:
|
||||
// Delete + Modify → should not happen, but ignore Modify
|
||||
}
|
||||
|
||||
case EventTypeRename:
|
||||
switch state.baseType {
|
||||
case EventTypeCreate:
|
||||
// Create + Rename(a→b) → Create at b
|
||||
state.originalSrc = e.To
|
||||
state.currentDst = ""
|
||||
case EventTypeModify:
|
||||
// Modify + Rename → emit Rename only
|
||||
state.baseType = EventTypeRename
|
||||
state.currentDst = e.To
|
||||
state.originalSrc = e.From
|
||||
|
||||
case EventTypeRename:
|
||||
// Rename(a→b) + Rename(b→c) → Rename(a→c)
|
||||
// Check for no-op: Rename(a→b) + Rename(b→a) → drop both
|
||||
if state.originalSrc == e.To {
|
||||
// Rename there-and-back, drop both
|
||||
delete(states, e.FileID)
|
||||
// Remove from order
|
||||
for i, id := range order {
|
||||
if id == e.FileID {
|
||||
order = append(order[:i], order[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.currentDst = e.To
|
||||
}
|
||||
case EventTypeDelete:
|
||||
// Delete + Rename → should not happen, ignore
|
||||
}
|
||||
|
||||
case EventTypeDelete:
|
||||
switch state.baseType {
|
||||
case EventTypeCreate:
|
||||
// Create + Delete → drop both (ephemeral object)
|
||||
delete(states, e.FileID)
|
||||
// Remove from order
|
||||
for i, id := range order {
|
||||
if id == e.FileID {
|
||||
order = append(order[:i], order[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
case EventTypeModify:
|
||||
// Modify + Delete → Delete
|
||||
state.baseType = EventTypeDelete
|
||||
state.originalSrc = e.From
|
||||
state.currentDst = ""
|
||||
case EventTypeRename:
|
||||
// Rename + Delete → Delete only
|
||||
state.baseType = EventTypeDelete
|
||||
state.originalSrc = e.From
|
||||
state.currentDst = ""
|
||||
case EventTypeDelete:
|
||||
// Delete + Delete → keep Delete (should not happen normally)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build output events in order
|
||||
result := make([]*Event, 0, len(order))
|
||||
for _, fileID := range order {
|
||||
state, exists := states[fileID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
switch state.baseType {
|
||||
case EventTypeCreate:
|
||||
result = append(result, &Event{
|
||||
Type: EventTypeCreate,
|
||||
FileID: fileID,
|
||||
From: state.originalSrc,
|
||||
})
|
||||
case EventTypeModify:
|
||||
result = append(result, &Event{
|
||||
Type: EventTypeModify,
|
||||
FileID: fileID,
|
||||
From: state.originalSrc,
|
||||
})
|
||||
case EventTypeRename:
|
||||
// If hasModify and base was originally Modify (converted to Rename),
|
||||
// we need to emit Modify first at original location
|
||||
// But in our current logic, Modify+Rename sets hasModify=true
|
||||
// We emit Rename, then Modify if needed
|
||||
result = append(result, &Event{
|
||||
Type: EventTypeRename,
|
||||
FileID: fileID,
|
||||
From: state.originalSrc,
|
||||
To: state.currentDst,
|
||||
})
|
||||
case EventTypeDelete:
|
||||
result = append(result, &Event{
|
||||
Type: EventTypeDelete,
|
||||
FileID: fileID,
|
||||
From: state.originalSrc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package eventhub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
)
|
||||
|
||||
type (
|
||||
EventHub interface {
|
||||
// Subscribe to a topic and return a channel to receive events.
|
||||
// If a subscriber with the same ID already exists and is offline,
|
||||
// it will be reactivated and any buffered events will be flushed.
|
||||
Subscribe(ctx context.Context, topic int, id string) (chan *Event, bool, error)
|
||||
// Unsubscribe marks the subscriber as offline instead of removing it.
|
||||
// Buffered events will be kept for when the subscriber reconnects.
|
||||
// Subscribers that remain offline for more than 14 days will be permanently removed.
|
||||
Unsubscribe(ctx context.Context, topic int, id string)
|
||||
// Get subscribers of a topic.
|
||||
GetSubscribers(ctx context.Context, topic int) []Subscriber
|
||||
// Close shuts down the event hub and disconnects all subscribers.
|
||||
Close()
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
bufSize = 16
|
||||
cleanupPeriod = 1 * time.Hour
|
||||
)
|
||||
|
||||
type eventHub struct {
|
||||
mu sync.RWMutex
|
||||
topics map[int]map[string]*subscriber
|
||||
userClient inventory.UserClient
|
||||
fsEventClient inventory.FsEventClient
|
||||
settings setting.Provider
|
||||
closed bool
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewEventHub(userClient inventory.UserClient, fsEventClient inventory.FsEventClient, settings setting.Provider) EventHub {
|
||||
e := &eventHub{
|
||||
topics: make(map[int]map[string]*subscriber),
|
||||
userClient: userClient,
|
||||
fsEventClient: fsEventClient,
|
||||
settings: settings,
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Remove all existing FsEvents
|
||||
fsEventClient.DeleteAll(context.Background())
|
||||
|
||||
// Start background cleanup goroutine
|
||||
e.wg.Add(1)
|
||||
go e.cleanupLoop()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// cleanupLoop periodically removes subscribers that have been offline for too long.
|
||||
func (e *eventHub) cleanupLoop() {
|
||||
defer e.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(cleanupPeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-e.closeCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
e.cleanupExpiredSubscribers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupExpiredSubscribers removes subscribers that have been offline for more than 14 days.
|
||||
func (e *eventHub) cleanupExpiredSubscribers() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return
|
||||
}
|
||||
|
||||
for topic, subs := range e.topics {
|
||||
for id, sub := range subs {
|
||||
if sub.shouldExpire() {
|
||||
sub.close()
|
||||
delete(subs, id)
|
||||
}
|
||||
}
|
||||
if len(subs) == 0 {
|
||||
delete(e.topics, topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *eventHub) GetSubscribers(ctx context.Context, topic int) []Subscriber {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
subs := make([]Subscriber, 0, len(e.topics[topic]))
|
||||
for _, v := range e.topics[topic] {
|
||||
subs = append(subs, v)
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
func (e *eventHub) Subscribe(ctx context.Context, topic int, id string) (chan *Event, bool, error) {
|
||||
l := logging.FromContext(ctx)
|
||||
l.Info("Subscribing to event hub for topic %d with id %s", topic, id)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil, false, ErrEventHubClosed
|
||||
}
|
||||
|
||||
subs, ok := e.topics[topic]
|
||||
if !ok {
|
||||
subs = make(map[string]*subscriber)
|
||||
e.topics[topic] = subs
|
||||
}
|
||||
|
||||
// Check if subscriber already exists
|
||||
if existingSub, ok := subs[id]; ok {
|
||||
if existingSub.isClosed() {
|
||||
// Subscriber was closed, create a new one
|
||||
delete(subs, id)
|
||||
} else {
|
||||
// Reactivate the offline subscriber
|
||||
l.Info("Reactivating offline subscriber %s for topic %d", id, topic)
|
||||
existingSub.setOnline(ctx)
|
||||
return existingSub.ch, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
sub, err := newSubscriber(ctx, id, e.userClient, e.fsEventClient, e.settings.EventHubMaxOfflineDuration(ctx), e.settings.EventHubDebounceDelay(ctx))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
e.topics[topic][id] = sub
|
||||
return sub.ch, false, nil
|
||||
}
|
||||
|
||||
func (e *eventHub) Unsubscribe(ctx context.Context, topic int, id string) {
|
||||
l := logging.FromContext(ctx)
|
||||
l.Info("Marking subscriber offline for topic %d with id %s", topic, id)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return
|
||||
}
|
||||
|
||||
subs, ok := e.topics[topic]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if sub, ok := subs[id]; ok {
|
||||
// Stop debounce timer but keep events in buffer
|
||||
sub.Stop()
|
||||
// Mark as offline instead of deleting
|
||||
sub.setOffline()
|
||||
}
|
||||
}
|
||||
|
||||
// Close shuts down the event hub and disconnects all subscribers.
|
||||
func (e *eventHub) Close() {
|
||||
e.mu.Lock()
|
||||
|
||||
if e.closed {
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
e.closed = true
|
||||
close(e.closeCh)
|
||||
|
||||
// Close all subscribers
|
||||
for _, subs := range e.topics {
|
||||
for _, sub := range subs {
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
e.topics = nil
|
||||
|
||||
e.mu.Unlock()
|
||||
|
||||
// Wait for cleanup goroutine to finish
|
||||
e.wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package eventhub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Subscriber interface {
|
||||
ID() string
|
||||
Ch() chan *Event
|
||||
Publish(evt Event)
|
||||
Stop()
|
||||
Buffer() []*Event
|
||||
// Owner returns the owner of the subscriber.
|
||||
Owner() (*ent.User, error)
|
||||
// Online returns whether the subscriber is online.
|
||||
Online() bool
|
||||
// OfflineSince returns when the subscriber went offline.
|
||||
// Returns zero time if the subscriber is online.
|
||||
OfflineSince() time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
userCacheTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
type subscriber struct {
|
||||
mu sync.Mutex
|
||||
userClient inventory.UserClient
|
||||
fsEventClient inventory.FsEventClient
|
||||
|
||||
id string
|
||||
uid int
|
||||
ch chan *Event
|
||||
|
||||
// Online status
|
||||
online bool
|
||||
offlineSince time.Time
|
||||
|
||||
// Debounce buffer for pending events
|
||||
buffer []*Event
|
||||
timer *time.Timer
|
||||
offlineMaxAge time.Duration
|
||||
debounceDelay time.Duration
|
||||
|
||||
// Owner info
|
||||
ownerCached *ent.User
|
||||
cachedAt time.Time
|
||||
|
||||
// Close signal
|
||||
closed bool
|
||||
closedCh chan struct{}
|
||||
}
|
||||
|
||||
func newSubscriber(ctx context.Context, id string, userClient inventory.UserClient, fsEventClient inventory.FsEventClient, maxAge, debounceDelay time.Duration) (*subscriber, error) {
|
||||
user := inventory.UserFromContext(ctx)
|
||||
if user == nil || inventory.IsAnonymousUser(user) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return &subscriber{
|
||||
id: id,
|
||||
ch: make(chan *Event, bufSize),
|
||||
userClient: userClient,
|
||||
fsEventClient: fsEventClient,
|
||||
|
||||
ownerCached: user,
|
||||
uid: user.ID,
|
||||
cachedAt: time.Now(),
|
||||
online: true,
|
||||
closedCh: make(chan struct{}),
|
||||
offlineMaxAge: maxAge,
|
||||
debounceDelay: debounceDelay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *subscriber) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
func (s *subscriber) Ch() chan *Event {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *subscriber) Online() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.online
|
||||
}
|
||||
|
||||
func (s *subscriber) OfflineSince() time.Time {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.offlineSince
|
||||
}
|
||||
|
||||
func (s *subscriber) Owner() (*ent.User, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.ownerLocked()
|
||||
}
|
||||
|
||||
func (s *subscriber) ownerLocked() (*ent.User, error) {
|
||||
if time.Since(s.cachedAt) > userCacheTTL || s.ownerCached == nil {
|
||||
user, err := s.userClient.GetLoginUserByID(context.Background(), s.uid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get login user: %w", err)
|
||||
}
|
||||
|
||||
s.ownerCached = user
|
||||
s.cachedAt = time.Now()
|
||||
}
|
||||
|
||||
return s.ownerCached, nil
|
||||
}
|
||||
|
||||
// Publish adds an event to the buffer and starts/resets the debounce timer.
|
||||
// Events will be flushed to the channel after the debounce delay.
|
||||
// If the subscriber is offline, events are kept in the buffer only.
|
||||
func (s *subscriber) Publish(evt Event) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
|
||||
s.publishLocked(evt)
|
||||
}
|
||||
|
||||
// publishLocked adds an event to the buffer and manages the debounce timer.
|
||||
// Caller must hold s.mu.
|
||||
func (s *subscriber) publishLocked(evt Event) {
|
||||
// Add event to buffer
|
||||
s.buffer = append(s.buffer, &evt)
|
||||
|
||||
// Reset or start the debounce timer
|
||||
if s.timer != nil {
|
||||
s.timer.Stop()
|
||||
}
|
||||
s.timer = time.AfterFunc(s.debounceDelay, s.flush)
|
||||
}
|
||||
|
||||
// flush sends all buffered events to the channel.
|
||||
// Called by the debounce timer.
|
||||
func (s *subscriber) flush() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.flushLocked(context.Background())
|
||||
}
|
||||
|
||||
// flushLocked sends all buffered events to the channel.
|
||||
// Caller must hold s.mu.
|
||||
func (s *subscriber) flushLocked(ctx context.Context) {
|
||||
if len(s.buffer) == 0 || s.closed {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.online {
|
||||
owner, err := s.ownerLocked()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = s.fsEventClient.Create(ctx, owner.ID, uuid.FromStringOrNil(s.id), lo.Map(s.buffer, func(item *Event, index int) string {
|
||||
res, _ := json.Marshal(item)
|
||||
return string(res)
|
||||
})...)
|
||||
} else {
|
||||
// TODO: implement event merging logic here
|
||||
// For now, send all buffered events individually
|
||||
debouncedEvents := DebounceEvents(s.buffer)
|
||||
for _, evt := range debouncedEvents {
|
||||
select {
|
||||
case s.ch <- evt:
|
||||
default:
|
||||
// Non-blocking send; drop if subscriber is slow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the buffer
|
||||
s.buffer = nil
|
||||
s.timer = nil
|
||||
}
|
||||
|
||||
// Stop cancels any pending debounce timer and flushes remaining events.
|
||||
// Should be called before closing the subscriber.
|
||||
func (s *subscriber) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.timer != nil {
|
||||
s.timer.Stop()
|
||||
s.timer = nil
|
||||
}
|
||||
|
||||
// Flush any remaining events before stopping
|
||||
s.flushLocked(context.Background())
|
||||
}
|
||||
|
||||
// setOnline marks the subscriber as online and flushes any buffered events.
|
||||
func (s *subscriber) setOnline(ctx context.Context) {
|
||||
l := logging.FromContext(ctx)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
|
||||
s.online = true
|
||||
s.ownerCached = nil
|
||||
s.offlineSince = time.Time{}
|
||||
|
||||
// Retrieve events from inventory
|
||||
events, err := s.fsEventClient.TakeBySubscriber(ctx, uuid.FromStringOrNil(s.id), s.uid)
|
||||
if err != nil {
|
||||
l.Error("Failed to get events from inventory: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Append events to buffer
|
||||
for _, event := range events {
|
||||
var eventParsed Event
|
||||
err := json.Unmarshal([]byte(event.Event), &eventParsed)
|
||||
if err != nil {
|
||||
l.Error("Failed to unmarshal event: %s", err)
|
||||
continue
|
||||
}
|
||||
s.buffer = append(s.buffer, &eventParsed)
|
||||
}
|
||||
|
||||
// Flush buffered events if any
|
||||
if len(s.buffer) > 0 {
|
||||
if s.timer != nil {
|
||||
s.timer.Stop()
|
||||
}
|
||||
s.timer = time.AfterFunc(s.debounceDelay, s.flush)
|
||||
}
|
||||
}
|
||||
|
||||
// setOffline marks the subscriber as offline.
|
||||
func (s *subscriber) setOffline() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
|
||||
s.online = false
|
||||
s.offlineSince = time.Now()
|
||||
|
||||
// Stop the timer, events will be kept in buffer
|
||||
if s.timer != nil {
|
||||
s.timer.Stop()
|
||||
s.timer = nil
|
||||
}
|
||||
|
||||
// flush the buffer
|
||||
s.flushLocked(context.Background())
|
||||
}
|
||||
|
||||
// close permanently closes the subscriber.
|
||||
func (s *subscriber) close() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
|
||||
s.closed = true
|
||||
if s.timer != nil {
|
||||
s.timer.Stop()
|
||||
s.timer = nil
|
||||
}
|
||||
|
||||
// Delete the FsEvent
|
||||
s.fsEventClient.DeleteBySubscriber(context.Background(), uuid.FromStringOrNil(s.id))
|
||||
|
||||
// Signal close and close the channel
|
||||
close(s.closedCh)
|
||||
close(s.ch)
|
||||
s.buffer = nil
|
||||
}
|
||||
|
||||
// isClosed returns whether the subscriber is closed.
|
||||
func (s *subscriber) isClosed() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.closed
|
||||
}
|
||||
|
||||
// shouldExpire returns whether the subscriber should be expired (offline for too long).
|
||||
func (s *subscriber) shouldExpire() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return !s.online && !s.offlineSince.IsZero() && time.Since(s.offlineSince) > s.offlineMaxAge
|
||||
}
|
||||
|
||||
// Buffer returns a copy of the current buffered events.
|
||||
// Useful for debugging or implementing custom merging logic.
|
||||
func (s *subscriber) Buffer() []*Event {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if len(s.buffer) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return a copy to avoid data races
|
||||
buf := make([]*Event, len(s.buffer))
|
||||
copy(buf, s.buffer)
|
||||
return buf
|
||||
}
|
||||
Reference in New Issue
Block a user