node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,631 @@
package plugin
import (
"encoding/json"
"errors"
"seanime/internal/database/models"
"seanime/internal/extension"
goja_util "seanime/internal/util/goja"
"seanime/internal/util/result"
"strings"
"github.com/dop251/goja"
"github.com/rs/zerolog"
"gorm.io/gorm"
)
// Storage is used to store data for an extension.
// A new instance is created for each extension.
type Storage struct {
ctx *AppContextImpl
ext *extension.Extension
logger *zerolog.Logger
runtime *goja.Runtime
pluginDataCache *result.Map[string, *models.PluginData] // Cache to avoid repeated database calls
keyDataCache *result.Map[string, interface{}] // Cache to avoid repeated database calls
keySubscribers *result.Map[string, []chan interface{}] // Subscribers for key changes
scheduler *goja_util.Scheduler
}
var (
ErrDatabaseNotInitialized = errors.New("database is not initialized")
)
// BindStorage binds the storage API to the Goja runtime.
// Permissions need to be checked by the caller.
// Permissions needed: storage
func (a *AppContextImpl) BindStorage(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Storage {
storageLogger := logger.With().Str("id", ext.ID).Logger()
storage := &Storage{
ctx: a,
ext: ext,
logger: &storageLogger,
runtime: vm,
pluginDataCache: result.NewResultMap[string, *models.PluginData](),
keyDataCache: result.NewResultMap[string, interface{}](),
keySubscribers: result.NewResultMap[string, []chan interface{}](),
scheduler: scheduler,
}
storageObj := vm.NewObject()
_ = storageObj.Set("get", storage.Get)
_ = storageObj.Set("set", storage.Set)
_ = storageObj.Set("remove", storage.Delete)
_ = storageObj.Set("drop", storage.Drop)
_ = storageObj.Set("clear", storage.Clear)
_ = storageObj.Set("keys", storage.Keys)
_ = storageObj.Set("has", storage.Has)
_ = storageObj.Set("watch", storage.Watch)
_ = vm.Set("$storage", storageObj)
return storage
}
// Stop closes all subscriber channels.
func (s *Storage) Stop() {
s.keySubscribers.Range(func(key string, subscribers []chan interface{}) bool {
for _, ch := range subscribers {
close(ch)
}
return true
})
s.keySubscribers.Clear()
}
// getDB returns the database instance or an error if not initialized
func (s *Storage) getDB() (*gorm.DB, error) {
db, ok := s.ctx.database.Get()
if !ok {
return nil, ErrDatabaseNotInitialized
}
return db.Gorm(), nil
}
// getPluginData retrieves the plugin data from the database
// If createIfNotExists is true, it will create an empty record if none exists
func (s *Storage) getPluginData(createIfNotExists bool) (*models.PluginData, error) {
// Check cache first
if cachedData, ok := s.pluginDataCache.Get(s.ext.ID); ok {
return cachedData, nil
}
db, err := s.getDB()
if err != nil {
return nil, err
}
var pluginData models.PluginData
if err := db.Where("plugin_id = ?", s.ext.ID).First(&pluginData).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) && createIfNotExists {
// Create empty data structure
baseData := make(map[string]interface{})
baseDataMarshaled, err := json.Marshal(baseData)
if err != nil {
return nil, err
}
newPluginData := &models.PluginData{
PluginID: s.ext.ID,
Data: baseDataMarshaled,
}
if err := db.Create(newPluginData).Error; err != nil {
return nil, err
}
// Cache the new plugin data
s.pluginDataCache.Set(s.ext.ID, newPluginData)
return newPluginData, nil
}
return nil, err
}
// Cache the plugin data
s.pluginDataCache.Set(s.ext.ID, &pluginData)
return &pluginData, nil
}
// getDataMap unmarshals the plugin data into a map
func (s *Storage) getDataMap(pluginData *models.PluginData) (map[string]interface{}, error) {
var data map[string]interface{}
if err := json.Unmarshal(pluginData.Data, &data); err != nil {
return make(map[string]interface{}), err
}
return data, nil
}
// saveDataMap marshals and saves the data map to the database
func (s *Storage) saveDataMap(pluginData *models.PluginData, data map[string]interface{}) error {
marshaled, err := json.Marshal(data)
if err != nil {
return err
}
pluginData.Data = marshaled
db, err := s.getDB()
if err != nil {
return err
}
err = db.Save(pluginData).Error
if err != nil {
return err
}
// Update the cache
s.pluginDataCache.Set(s.ext.ID, pluginData)
s.keyDataCache.Clear()
return nil
}
// getNestedValue retrieves a value from a nested map using dot notation
func getNestedValue(data map[string]interface{}, path string) interface{} {
if !strings.Contains(path, ".") {
return data[path]
}
parts := strings.Split(path, ".")
current := data
// Navigate through all parts except the last one
for i := 0; i < len(parts)-1; i++ {
part := parts[i]
next, ok := current[part]
if !ok {
return nil
}
// Try to convert to map for next level
nextMap, ok := next.(map[string]interface{})
if !ok {
// Try to convert from unmarshaled JSON
jsonMap, ok := next.(map[string]interface{})
if !ok {
return nil
}
nextMap = jsonMap
}
current = nextMap
}
// Return the value at the final part
return current[parts[len(parts)-1]]
}
// setNestedValue sets a value in a nested map using dot notation
// It creates intermediate maps as needed
func setNestedValue(data map[string]interface{}, path string, value interface{}) {
if !strings.Contains(path, ".") {
data[path] = value
return
}
parts := strings.Split(path, ".")
current := data
// Navigate and create intermediate maps as needed
for i := 0; i < len(parts)-1; i++ {
part := parts[i]
next, ok := current[part]
if !ok {
// Create new map if key doesn't exist
next = make(map[string]interface{})
current[part] = next
}
// Try to convert to map for next level
nextMap, ok := next.(map[string]interface{})
if !ok {
// Try to convert from unmarshaled JSON
jsonMap, ok := next.(map[string]interface{})
if !ok {
// Replace with a new map if not convertible
nextMap = make(map[string]interface{})
current[part] = nextMap
} else {
nextMap = jsonMap
current[part] = nextMap
}
}
current = nextMap
}
// Set the value at the final part
current[parts[len(parts)-1]] = value
}
// deleteNestedValue deletes a value from a nested map using dot notation
// Returns true if the key was found and deleted
func deleteNestedValue(data map[string]interface{}, path string) bool {
if !strings.Contains(path, ".") {
_, exists := data[path]
if exists {
delete(data, path)
return true
}
return false
}
parts := strings.Split(path, ".")
current := data
// Navigate through all parts except the last one
for i := 0; i < len(parts)-1; i++ {
part := parts[i]
next, ok := current[part]
if !ok {
return false
}
// Try to convert to map for next level
nextMap, ok := next.(map[string]interface{})
if !ok {
// Try to convert from unmarshaled JSON
jsonMap, ok := next.(map[string]interface{})
if !ok {
return false
}
nextMap = jsonMap
}
current = nextMap
}
// Delete the value at the final part
lastPart := parts[len(parts)-1]
_, exists := current[lastPart]
if exists {
delete(current, lastPart)
return true
}
return false
}
// hasNestedKey checks if a nested key exists using dot notation
func hasNestedKey(data map[string]interface{}, path string) bool {
if !strings.Contains(path, ".") {
_, exists := data[path]
return exists
}
parts := strings.Split(path, ".")
current := data
// Navigate through all parts except the last one
for i := 0; i < len(parts)-1; i++ {
part := parts[i]
next, ok := current[part]
if !ok {
return false
}
// Try to convert to map for next level
nextMap, ok := next.(map[string]interface{})
if !ok {
// Try to convert from unmarshaled JSON
jsonMap, ok := next.(map[string]interface{})
if !ok {
return false
}
nextMap = jsonMap
}
current = nextMap
}
// Check if the final key exists
_, exists := current[parts[len(parts)-1]]
return exists
}
// getAllKeys recursively gets all keys from a nested map using dot notation
func getAllKeys(data map[string]interface{}, prefix string) []string {
keys := make([]string, 0)
for key, value := range data {
fullKey := key
if prefix != "" {
fullKey = prefix + "." + key
}
keys = append(keys, fullKey)
// If value is a map, recursively get its keys
if nestedMap, ok := value.(map[string]interface{}); ok {
nestedKeys := getAllKeys(nestedMap, fullKey)
keys = append(keys, nestedKeys...)
}
}
return keys
}
// notifyKeyAndParents sends notifications to subscribers of the given key and its parent keys
// If the value is nil, it indicates the key was deleted
func (s *Storage) notifyKeyAndParents(key string, value interface{}, data map[string]interface{}) {
// Notify direct subscribers of this key
if subscribers, ok := s.keySubscribers.Get(key); ok {
for _, ch := range subscribers {
// Non-blocking send to avoid deadlocks
select {
case ch <- value:
default:
// Channel is full or closed, skip
}
}
}
// Also notify parent key subscribers if this is a nested key
if strings.Contains(key, ".") {
parts := strings.Split(key, ".")
for i := 1; i < len(parts); i++ {
parentKey := strings.Join(parts[:i], ".")
if subscribers, ok := s.keySubscribers.Get(parentKey); ok {
// Get the current parent value
parentValue := getNestedValue(data, parentKey)
for _, ch := range subscribers {
// Non-blocking send to avoid deadlocks
select {
case ch <- parentValue:
default:
// Channel is full or closed, skip
}
}
}
}
}
}
func (s *Storage) Watch(key string, callback goja.Callable) goja.Value {
s.logger.Trace().Msgf("plugin: Watching key %s", key)
// Create a channel to receive updates
updateCh := make(chan interface{}, 100)
// Add this channel to the subscribers for this key
subscribers := []chan interface{}{}
if existingSubscribers, ok := s.keySubscribers.Get(key); ok {
subscribers = existingSubscribers
}
subscribers = append(subscribers, updateCh)
s.keySubscribers.Set(key, subscribers)
// Start a goroutine to listen for updates
go func() {
for value := range updateCh {
// Call the callback with the new value
s.scheduler.ScheduleAsync(func() error {
_, err := callback(goja.Undefined(), s.runtime.ToValue(value))
if err != nil {
s.logger.Error().Err(err).Msgf("plugin: Error calling watch callback for key %s", key)
}
return nil
})
}
}()
// Check if the key currently exists and immediately send its value
// This allows watchers to get the current value right away
currentValue, _ := s.Get(key)
if currentValue != nil {
// Use non-blocking send
select {
case updateCh <- currentValue:
default:
// Channel is full, skip
}
}
// Return a function that can be used to cancel the watch
cancelFn := func() {
close(updateCh)
// Remove this specific channel from subscribers
if existingSubscribers, ok := s.keySubscribers.Get(key); ok {
newSubscribers := make([]chan interface{}, 0, len(existingSubscribers)-1)
for _, ch := range existingSubscribers {
if ch != updateCh {
newSubscribers = append(newSubscribers, ch)
}
}
if len(newSubscribers) > 0 {
s.keySubscribers.Set(key, newSubscribers)
} else {
s.keySubscribers.Delete(key)
}
}
}
return s.runtime.ToValue(cancelFn)
}
func (s *Storage) Delete(key string) error {
s.logger.Trace().Msgf("plugin: Deleting key %s", key)
// Remove from key cache
s.keyDataCache.Delete(key)
pluginData, err := s.getPluginData(false)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
data, err := s.getDataMap(pluginData)
if err != nil {
return err
}
// Notify subscribers that the key was deleted
s.notifyKeyAndParents(key, nil, data)
if deleteNestedValue(data, key) {
return s.saveDataMap(pluginData, data)
}
return nil
}
func (s *Storage) Drop() error {
s.logger.Trace().Msg("plugin: Dropping storage")
// // Close all subscriber channels
// s.keySubscribers.Range(func(key string, subscribers []chan interface{}) bool {
// for _, ch := range subscribers {
// close(ch)
// }
// return true
// })
// s.keySubscribers.Clear()
// Clear caches
s.pluginDataCache.Clear()
s.keyDataCache.Clear()
db, err := s.getDB()
if err != nil {
return err
}
return db.Where("plugin_id = ?", s.ext.ID).Delete(&models.PluginData{}).Error
}
func (s *Storage) Clear() error {
s.logger.Trace().Msg("plugin: Clearing storage")
// Clear key cache
s.keyDataCache.Clear()
pluginData, err := s.getPluginData(true)
if err != nil {
return err
}
// Get all keys before clearing
data, err := s.getDataMap(pluginData)
if err != nil {
return err
}
// Get all keys to notify subscribers
keys := getAllKeys(data, "")
// Create empty data map
cleanData := make(map[string]interface{})
// Save the empty data first
if err := s.saveDataMap(pluginData, cleanData); err != nil {
return err
}
// Notify all subscribers that their keys were cleared
for _, key := range keys {
s.notifyKeyAndParents(key, nil, cleanData)
}
return nil
}
func (s *Storage) Keys() ([]string, error) {
pluginData, err := s.getPluginData(false)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return []string{}, nil
}
return nil, err
}
data, err := s.getDataMap(pluginData)
if err != nil {
return nil, err
}
return getAllKeys(data, ""), nil
}
func (s *Storage) Has(key string) (bool, error) {
// Check key cache first
if s.keyDataCache.Has(key) {
return true, nil
}
pluginData, err := s.getPluginData(false)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
data, err := s.getDataMap(pluginData)
if err != nil {
return false, err
}
exists := hasNestedKey(data, key)
// If key exists, we can also cache its value for future Get calls
if exists {
value := getNestedValue(data, key)
if value != nil {
s.keyDataCache.Set(key, value)
}
}
return exists, nil
}
func (s *Storage) Get(key string) (interface{}, error) {
// Check key cache first
if cachedValue, ok := s.keyDataCache.Get(key); ok {
return cachedValue, nil
}
pluginData, err := s.getPluginData(true)
if err != nil {
return nil, err
}
data, err := s.getDataMap(pluginData)
if err != nil {
return nil, err
}
value := getNestedValue(data, key)
// Cache the value
if value != nil {
s.keyDataCache.Set(key, value)
}
return value, nil
}
func (s *Storage) Set(key string, value interface{}) error {
s.logger.Trace().Msgf("plugin: Setting key %s", key)
pluginData, err := s.getPluginData(true)
if err != nil {
return err
}
data, err := s.getDataMap(pluginData)
if err != nil {
data = make(map[string]interface{})
}
setNestedValue(data, key, value)
// Update key cache
s.keyDataCache.Set(key, value)
// Notify subscribers
s.notifyKeyAndParents(key, value, data)
return s.saveDataMap(pluginData, data)
}