diff --git a/CHANGELOG.md b/CHANGELOG.md index efe46082..a0087787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ - (@todo docs) Changed the After* hooks to be called right before writing the user response, allowing users to return response errors from the after hooks. +- Fixed realtime delete event to be called after the record was deleted from the DB (_including transactions and cascade delete operations_). + +- Added `subscriptions.Client.Unset()` helper to remove a single cached item from the client store. + ## v0.16.4-WIP diff --git a/apis/realtime.go b/apis/realtime.go index 6725552f..699b5209 100644 --- a/apis/realtime.go +++ b/apis/realtime.go @@ -267,7 +267,7 @@ func (api *realtimeApi) bindEvents() { api.app.OnModelAfterCreate().PreAdd(func(e *core.ModelEvent) error { if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastRecord("create", record); err != nil && api.app.IsDebug() { + if err := api.broadcastRecord("create", record, false); err != nil && api.app.IsDebug() { log.Println(err) } } @@ -276,7 +276,7 @@ func (api *realtimeApi) bindEvents() { api.app.OnModelAfterUpdate().PreAdd(func(e *core.ModelEvent) error { if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastRecord("update", record); err != nil && api.app.IsDebug() { + if err := api.broadcastRecord("update", record, false); err != nil && api.app.IsDebug() { log.Println(err) } } @@ -285,7 +285,16 @@ func (api *realtimeApi) bindEvents() { api.app.OnModelBeforeDelete().Add(func(e *core.ModelEvent) error { if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastRecord("delete", record); err != nil && api.app.IsDebug() { + if err := api.broadcastRecord("delete", record, true); err != nil && api.app.IsDebug() { + log.Println(err) + } + } + return nil + }) + + api.app.OnModelAfterDelete().Add(func(e *core.ModelEvent) error { + if record := api.resolveRecord(e.Model); record != nil { + if err := api.broadcastDryCachedRecord("delete", record); err != nil && api.app.IsDebug() { log.Println(err) } } @@ -367,7 +376,7 @@ type recordData struct { Record *models.Record `json:"record"` } -func (api *realtimeApi) broadcastRecord(action string, record *models.Record) error { +func (api *realtimeApi) broadcastRecord(action string, record *models.Record, dryCache bool) error { collection := record.Collection() if collection == nil { return errors.New("Record collection not set.") @@ -399,9 +408,6 @@ func (api *realtimeApi) broadcastRecord(action string, record *models.Record) er dataBytes, err := json.Marshal(data) if err != nil { - if api.app.IsDebug() { - log.Println(err) - } return err } @@ -438,17 +444,46 @@ func (api *realtimeApi) broadcastRecord(action string, record *models.Record) er } } - routine.FireAndForget(func() { - if !client.IsDiscarded() { - client.Channel() <- msg - } - }) + if dryCache { + client.Set(action+"/"+data.Record.Id, msg) + } else { + routine.FireAndForget(func() { + if !client.IsDiscarded() { + client.Channel() <- msg + } + }) + } } } return nil } +// broadcastDryCachedRecord broadcasts record if it is cached in the client context. +func (api *realtimeApi) broadcastDryCachedRecord(action string, record *models.Record) error { + clients := api.app.SubscriptionsBroker().Clients() + + for _, client := range clients { + key := action + "/" + record.Id + + msg, ok := client.Get(key).(subscriptions.Message) + if !ok { + continue + } + + client.Unset(key) + + client := client + + routine.FireAndForget(func() { + if !client.IsDiscarded() { + client.Channel() <- msg + } + }) + } + return nil +} + type getter interface { Get(string) any } diff --git a/tools/subscriptions/client.go b/tools/subscriptions/client.go index 49026a2d..b50fd28f 100644 --- a/tools/subscriptions/client.go +++ b/tools/subscriptions/client.go @@ -35,6 +35,9 @@ type Client interface { // Set stores any value to the client's context. Set(key string, value any) + // Unset removes a single value from the client's context. + Unset(key string) + // Get retrieves the key value from the client's context. Get(key string) any @@ -157,6 +160,14 @@ func (c *DefaultClient) Set(key string, value any) { c.store[key] = value } +// Unset implements the [Client.Unset] interface method. +func (c *DefaultClient) Unset(key string) { + c.mux.Lock() + defer c.mux.Unlock() + + delete(c.store, key) +} + // Discard implements the [Client.Discard] interface method. func (c *DefaultClient) Discard() { c.mux.Lock()