mirror of
https://github.com/navidrome/navidrome.git
synced 2026-03-04 06:35:52 +00:00
feat: add Cache service for in-memory TTL-based caching in plugins
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
66c396413c
commit
f0d6fd4bc8
@ -300,6 +300,96 @@ func main() {}
|
||||
|
||||
To schedule a task from your plugin, use the generated SDK functions (see `plugins/host/go/nd_host_scheduler.go`).
|
||||
|
||||
### Cache
|
||||
|
||||
Allows plugins to store and retrieve data in an in-memory TTL-based cache. This is useful for caching API responses, storing session tokens, or persisting state across plugin invocations.
|
||||
|
||||
**Important:** The cache is in-memory only and will be lost on server restart. Plugins should handle cache misses gracefully.
|
||||
|
||||
#### Using the Cache Host Service
|
||||
|
||||
To use the cache, plugins call these host functions (provided by Navidrome):
|
||||
|
||||
| Host Function | Parameters | Description |
|
||||
|----------------------|--------------------------------|------------------------------------------------|
|
||||
| `cache_setstring` | `key, value, ttl_seconds` | Store a string value |
|
||||
| `cache_getstring` | `key` | Retrieve a string value |
|
||||
| `cache_setint` | `key, value, ttl_seconds` | Store an integer value |
|
||||
| `cache_getint` | `key` | Retrieve an integer value |
|
||||
| `cache_setfloat` | `key, value, ttl_seconds` | Store a float value |
|
||||
| `cache_getfloat` | `key` | Retrieve a float value |
|
||||
| `cache_setbytes` | `key, value, ttl_seconds` | Store a byte slice |
|
||||
| `cache_getbytes` | `key` | Retrieve a byte slice |
|
||||
| `cache_has` | `key` | Check if a key exists |
|
||||
| `cache_remove` | `key` | Delete a cached value |
|
||||
|
||||
**TTL (Time-to-Live):** Pass `0` to use the default TTL of 24 hours, or specify seconds.
|
||||
|
||||
**Key Isolation:** Each plugin's cache keys are automatically namespaced, so different plugins can use the same key names without conflicts.
|
||||
|
||||
#### Get Response Format
|
||||
|
||||
Get operations return a JSON response:
|
||||
|
||||
```json
|
||||
{
|
||||
"value": "...",
|
||||
"exists": true,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
- `value`: The cached value (type matches the operation: string, int64, float64, or base64-encoded bytes)
|
||||
- `exists`: `true` if the key was found and the type matched, `false` otherwise
|
||||
- `error`: Error message if something went wrong
|
||||
|
||||
#### Manifest Permissions
|
||||
|
||||
Plugins using the cache must declare the permission in their manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"cache": {
|
||||
"reason": "Cache API responses to reduce external requests"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Cache Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// Import the generated cache SDK (from plugins/host/go/nd_host_cache.go)
|
||||
|
||||
func fetchWithCache(key string) (string, error) {
|
||||
// Try to get from cache first
|
||||
resp, err := CacheGetString(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.Exists {
|
||||
return resp.Value, nil
|
||||
}
|
||||
|
||||
// Cache miss - fetch from external API
|
||||
value := fetchFromAPI()
|
||||
|
||||
// Cache for 1 hour (3600 seconds)
|
||||
CacheSetString(key, value, 3600)
|
||||
|
||||
return value, nil
|
||||
}
|
||||
```
|
||||
|
||||
To use the cache from your plugin, copy the generated SDK file `plugins/host/go/nd_host_cache.go` to your plugin directory.
|
||||
|
||||
## Developing Plugins
|
||||
|
||||
Plugins can be written in any language that compiles to WebAssembly. We recommend using the [Extism PDK](https://extism.org/docs/category/write-a-plug-in) for your language.
|
||||
|
||||
117
plugins/host/cache.go
Normal file
117
plugins/host/cache.go
Normal file
@ -0,0 +1,117 @@
|
||||
package host
|
||||
|
||||
import "context"
|
||||
|
||||
// CacheService provides in-memory TTL-based caching capabilities for plugins.
|
||||
//
|
||||
// This service allows plugins to store and retrieve typed values (strings, integers,
|
||||
// floats, and byte slices) with configurable time-to-live expiration. Each plugin's
|
||||
// cache keys are automatically namespaced to prevent collisions between plugins.
|
||||
//
|
||||
// The cache is in-memory only and will be lost on server restart. Plugins should
|
||||
// handle cache misses gracefully.
|
||||
//
|
||||
//nd:hostservice name=Cache permission=cache
|
||||
type CacheService interface {
|
||||
// SetString stores a string value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The string value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
//nd:hostfunc
|
||||
SetString(ctx context.Context, key string, value string, ttlSeconds int64) error
|
||||
|
||||
// GetString retrieves a string value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a string, exists will be false.
|
||||
//nd:hostfunc
|
||||
GetString(ctx context.Context, key string) (value string, exists bool, err error)
|
||||
|
||||
// SetInt stores an integer value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The integer value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
//nd:hostfunc
|
||||
SetInt(ctx context.Context, key string, value int64, ttlSeconds int64) error
|
||||
|
||||
// GetInt retrieves an integer value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not an integer, exists will be false.
|
||||
//nd:hostfunc
|
||||
GetInt(ctx context.Context, key string) (value int64, exists bool, err error)
|
||||
|
||||
// SetFloat stores a float value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The float value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
//nd:hostfunc
|
||||
SetFloat(ctx context.Context, key string, value float64, ttlSeconds int64) error
|
||||
|
||||
// GetFloat retrieves a float value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a float, exists will be false.
|
||||
//nd:hostfunc
|
||||
GetFloat(ctx context.Context, key string) (value float64, exists bool, err error)
|
||||
|
||||
// SetBytes stores a byte slice in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The byte slice to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
//nd:hostfunc
|
||||
SetBytes(ctx context.Context, key string, value []byte, ttlSeconds int64) error
|
||||
|
||||
// GetBytes retrieves a byte slice from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a byte slice, exists will be false.
|
||||
//nd:hostfunc
|
||||
GetBytes(ctx context.Context, key string) (value []byte, exists bool, err error)
|
||||
|
||||
// Has checks if a key exists in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns true if the key exists and has not expired.
|
||||
//nd:hostfunc
|
||||
Has(ctx context.Context, key string) (exists bool, err error)
|
||||
|
||||
// Remove deletes a value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
|
||||
//nd:hostfunc
|
||||
Remove(ctx context.Context, key string) error
|
||||
}
|
||||
384
plugins/host/cache_gen.go
Normal file
384
plugins/host/cache_gen.go
Normal file
@ -0,0 +1,384 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
extism "github.com/extism/go-sdk"
|
||||
)
|
||||
|
||||
// CacheGetStringResponse is the response type for Cache.GetString.
|
||||
type CacheGetStringResponse struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetIntResponse is the response type for Cache.GetInt.
|
||||
type CacheGetIntResponse struct {
|
||||
Value int64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetFloatResponse is the response type for Cache.GetFloat.
|
||||
type CacheGetFloatResponse struct {
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetBytesResponse is the response type for Cache.GetBytes.
|
||||
type CacheGetBytesResponse struct {
|
||||
Value []byte `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheHasResponse is the response type for Cache.Has.
|
||||
type CacheHasResponse struct {
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterCacheHostFunctions registers Cache service host functions.
|
||||
// The returned host functions should be added to the plugin's configuration.
|
||||
func RegisterCacheHostFunctions(service CacheService) []extism.HostFunction {
|
||||
return []extism.HostFunction{
|
||||
newCacheSetStringHostFunction(service),
|
||||
newCacheGetStringHostFunction(service),
|
||||
newCacheSetIntHostFunction(service),
|
||||
newCacheGetIntHostFunction(service),
|
||||
newCacheSetFloatHostFunction(service),
|
||||
newCacheGetFloatHostFunction(service),
|
||||
newCacheSetBytesHostFunction(service),
|
||||
newCacheGetBytesHostFunction(service),
|
||||
newCacheHasHostFunction(service),
|
||||
newCacheRemoveHostFunction(service),
|
||||
}
|
||||
}
|
||||
|
||||
func newCacheSetStringHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_setstring",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err := p.ReadString(stack[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ttlSeconds := int64(stack[2])
|
||||
|
||||
// Call the service method
|
||||
err = service.SetString(ctx, key, value, ttlSeconds)
|
||||
if err != nil {
|
||||
// Write error string to plugin memory
|
||||
if ptr, err := p.WriteString(err.Error()); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write empty string to indicate success
|
||||
if ptr, err := p.WriteString(""); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR, extism.ValueTypePTR, extism.ValueTypeI64},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheGetStringHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_getstring",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
value, exists, err := service.GetString(ctx, key)
|
||||
if err != nil {
|
||||
cacheWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
// Write JSON response to plugin memory
|
||||
resp := CacheGetStringResponse{
|
||||
Value: value,
|
||||
Exists: exists,
|
||||
}
|
||||
cacheWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheSetIntHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_setint",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value := int64(stack[1])
|
||||
ttlSeconds := int64(stack[2])
|
||||
|
||||
// Call the service method
|
||||
err = service.SetInt(ctx, key, value, ttlSeconds)
|
||||
if err != nil {
|
||||
// Write error string to plugin memory
|
||||
if ptr, err := p.WriteString(err.Error()); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write empty string to indicate success
|
||||
if ptr, err := p.WriteString(""); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR, extism.ValueTypeI64, extism.ValueTypeI64},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheGetIntHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_getint",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
value, exists, err := service.GetInt(ctx, key)
|
||||
if err != nil {
|
||||
cacheWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
// Write JSON response to plugin memory
|
||||
resp := CacheGetIntResponse{
|
||||
Value: value,
|
||||
Exists: exists,
|
||||
}
|
||||
cacheWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheSetFloatHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_setfloat",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value := extism.DecodeF64(stack[1])
|
||||
ttlSeconds := int64(stack[2])
|
||||
|
||||
// Call the service method
|
||||
err = service.SetFloat(ctx, key, value, ttlSeconds)
|
||||
if err != nil {
|
||||
// Write error string to plugin memory
|
||||
if ptr, err := p.WriteString(err.Error()); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write empty string to indicate success
|
||||
if ptr, err := p.WriteString(""); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR, extism.ValueTypeF64, extism.ValueTypeI64},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheGetFloatHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_getfloat",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
value, exists, err := service.GetFloat(ctx, key)
|
||||
if err != nil {
|
||||
cacheWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
// Write JSON response to plugin memory
|
||||
resp := CacheGetFloatResponse{
|
||||
Value: value,
|
||||
Exists: exists,
|
||||
}
|
||||
cacheWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheSetBytesHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_setbytes",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err := p.ReadBytes(stack[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ttlSeconds := int64(stack[2])
|
||||
|
||||
// Call the service method
|
||||
err = service.SetBytes(ctx, key, value, ttlSeconds)
|
||||
if err != nil {
|
||||
// Write error string to plugin memory
|
||||
if ptr, err := p.WriteString(err.Error()); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write empty string to indicate success
|
||||
if ptr, err := p.WriteString(""); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR, extism.ValueTypePTR, extism.ValueTypeI64},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheGetBytesHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_getbytes",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
value, exists, err := service.GetBytes(ctx, key)
|
||||
if err != nil {
|
||||
cacheWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
// Write JSON response to plugin memory
|
||||
resp := CacheGetBytesResponse{
|
||||
Value: value,
|
||||
Exists: exists,
|
||||
}
|
||||
cacheWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheHasHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_has",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
exists, err := service.Has(ctx, key)
|
||||
if err != nil {
|
||||
cacheWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
// Write JSON response to plugin memory
|
||||
resp := CacheHasResponse{
|
||||
Exists: exists,
|
||||
}
|
||||
cacheWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newCacheRemoveHostFunction(service CacheService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"cache_remove",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read parameters from stack
|
||||
key, err := p.ReadString(stack[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
err = service.Remove(ctx, key)
|
||||
if err != nil {
|
||||
// Write error string to plugin memory
|
||||
if ptr, err := p.WriteString(err.Error()); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write empty string to indicate success
|
||||
if ptr, err := p.WriteString(""); err == nil {
|
||||
stack[0] = ptr
|
||||
}
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
// cacheWriteResponse writes a JSON response to plugin memory.
|
||||
func cacheWriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
cacheWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
respPtr, err := p.WriteBytes(respBytes)
|
||||
if err != nil {
|
||||
stack[0] = 0
|
||||
return
|
||||
}
|
||||
stack[0] = respPtr
|
||||
}
|
||||
|
||||
// cacheWriteError writes an error response to plugin memory.
|
||||
func cacheWriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
|
||||
errResp := struct {
|
||||
Error string `json:"error"`
|
||||
}{Error: err.Error()}
|
||||
respBytes, _ := json.Marshal(errResp)
|
||||
respPtr, _ := p.WriteBytes(respBytes)
|
||||
stack[0] = respPtr
|
||||
}
|
||||
375
plugins/host/go/nd_host_cache.go
Normal file
375
plugins/host/go/nd_host_cache.go
Normal file
@ -0,0 +1,375 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the Cache host service.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// cache_setstring is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setstring
|
||||
func cache_setstring(uint64, uint64, int64) uint64
|
||||
|
||||
// cache_getstring is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getstring
|
||||
func cache_getstring(uint64) uint64
|
||||
|
||||
// cache_setint is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setint
|
||||
func cache_setint(uint64, int64, int64) uint64
|
||||
|
||||
// cache_getint is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getint
|
||||
func cache_getint(uint64) uint64
|
||||
|
||||
// cache_setfloat is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setfloat
|
||||
func cache_setfloat(uint64, float64, int64) uint64
|
||||
|
||||
// cache_getfloat is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getfloat
|
||||
func cache_getfloat(uint64) uint64
|
||||
|
||||
// cache_setbytes is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setbytes
|
||||
func cache_setbytes(uint64, uint64, int64) uint64
|
||||
|
||||
// cache_getbytes is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getbytes
|
||||
func cache_getbytes(uint64) uint64
|
||||
|
||||
// cache_has is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_has
|
||||
func cache_has(uint64) uint64
|
||||
|
||||
// cache_remove is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_remove
|
||||
func cache_remove(uint64) uint64
|
||||
|
||||
// CacheGetStringResponse is the response type for Cache.GetString.
|
||||
type CacheGetStringResponse struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetIntResponse is the response type for Cache.GetInt.
|
||||
type CacheGetIntResponse struct {
|
||||
Value int64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetFloatResponse is the response type for Cache.GetFloat.
|
||||
type CacheGetFloatResponse struct {
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetBytesResponse is the response type for Cache.GetBytes.
|
||||
type CacheGetBytesResponse struct {
|
||||
Value []byte `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheHasResponse is the response type for Cache.Has.
|
||||
type CacheHasResponse struct {
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheSetString calls the cache_setstring host function.
|
||||
// SetString stores a string value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The string value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetString(key string, value string, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
valueMem := pdk.AllocateString(value)
|
||||
defer valueMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setstring(keyMem.Offset(), valueMem.Offset(), ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetString calls the cache_getstring host function.
|
||||
// GetString retrieves a string value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a string, exists will be false.
|
||||
func CacheGetString(key string) (*CacheGetStringResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getstring(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetStringResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheSetInt calls the cache_setint host function.
|
||||
// SetInt stores an integer value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The integer value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetInt(key string, value int64, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setint(keyMem.Offset(), value, ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetInt calls the cache_getint host function.
|
||||
// GetInt retrieves an integer value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not an integer, exists will be false.
|
||||
func CacheGetInt(key string) (*CacheGetIntResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getint(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetIntResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheSetFloat calls the cache_setfloat host function.
|
||||
// SetFloat stores a float value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The float value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetFloat(key string, value float64, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setfloat(keyMem.Offset(), value, ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetFloat calls the cache_getfloat host function.
|
||||
// GetFloat retrieves a float value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a float, exists will be false.
|
||||
func CacheGetFloat(key string) (*CacheGetFloatResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getfloat(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetFloatResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheSetBytes calls the cache_setbytes host function.
|
||||
// SetBytes stores a byte slice in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The byte slice to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetBytes(key string, value []byte, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
valueMem := pdk.AllocateBytes(value)
|
||||
defer valueMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setbytes(keyMem.Offset(), valueMem.Offset(), ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetBytes calls the cache_getbytes host function.
|
||||
// GetBytes retrieves a byte slice from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a byte slice, exists will be false.
|
||||
func CacheGetBytes(key string) (*CacheGetBytesResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getbytes(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetBytesResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheHas calls the cache_has host function.
|
||||
// Has checks if a key exists in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns true if the key exists and has not expired.
|
||||
func CacheHas(key string) (*CacheHasResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_has(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheHasResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheRemove calls the cache_remove host function.
|
||||
// Remove deletes a value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
|
||||
func CacheRemove(key string) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_remove(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -41,9 +41,4 @@ type SchedulerService interface {
|
||||
// Returns an error if the schedule ID is not found or if cancellation fails.
|
||||
//nd:hostfunc
|
||||
CancelSchedule(ctx context.Context, scheduleID string) error
|
||||
|
||||
// Close cleans up any resources used by the SchedulerService.
|
||||
//
|
||||
// This should be called when the plugin is unloaded to ensure proper cleanup.
|
||||
Close() error
|
||||
}
|
||||
|
||||
@ -56,10 +56,4 @@ type WebSocketService interface {
|
||||
// Returns an error if the connection is not found or if closing fails.
|
||||
//nd:hostfunc
|
||||
CloseConnection(ctx context.Context, connectionID string, code int32, reason string) error
|
||||
|
||||
// Close cleans up any resources used by the WebSocketService.
|
||||
//
|
||||
// This should be called when the plugin is unloaded to ensure proper cleanup
|
||||
// of all active WebSocket connections.
|
||||
Close() error
|
||||
}
|
||||
|
||||
153
plugins/host_cache.go
Normal file
153
plugins/host_cache.go
Normal file
@ -0,0 +1,153 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/plugins/host"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCacheTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
// cacheServiceImpl implements the host.CacheService interface.
|
||||
// Each plugin gets its own cache instance for isolation.
|
||||
type cacheServiceImpl struct {
|
||||
pluginName string
|
||||
cache *ttlcache.Cache[string, any]
|
||||
defaultTTL time.Duration
|
||||
}
|
||||
|
||||
// newCacheService creates a new cacheServiceImpl instance with its own cache.
|
||||
func newCacheService(pluginName string) *cacheServiceImpl {
|
||||
cache := ttlcache.New[string, any](
|
||||
ttlcache.WithTTL[string, any](defaultCacheTTL),
|
||||
)
|
||||
// Start the janitor goroutine to clean up expired entries
|
||||
go cache.Start()
|
||||
|
||||
return &cacheServiceImpl{
|
||||
pluginName: pluginName,
|
||||
cache: cache,
|
||||
defaultTTL: defaultCacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// getTTL converts seconds to a duration, using default if 0 or negative
|
||||
func (s *cacheServiceImpl) getTTL(seconds int64) time.Duration {
|
||||
if seconds <= 0 {
|
||||
return s.defaultTTL
|
||||
}
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
// SetString stores a string value in the cache.
|
||||
func (s *cacheServiceImpl) SetString(ctx context.Context, key string, value string, ttlSeconds int64) error {
|
||||
s.cache.Set(key, value, s.getTTL(ttlSeconds))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetString retrieves a string value from the cache.
|
||||
func (s *cacheServiceImpl) GetString(ctx context.Context, key string) (string, bool, error) {
|
||||
item := s.cache.Get(key)
|
||||
if item == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
value, ok := item.Value().(string)
|
||||
if !ok {
|
||||
log.Debug(ctx, "Cache type mismatch", "plugin", s.pluginName, "key", key, "expected", "string")
|
||||
return "", false, nil
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// SetInt stores an integer value in the cache.
|
||||
func (s *cacheServiceImpl) SetInt(ctx context.Context, key string, value int64, ttlSeconds int64) error {
|
||||
s.cache.Set(key, value, s.getTTL(ttlSeconds))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInt retrieves an integer value from the cache.
|
||||
func (s *cacheServiceImpl) GetInt(ctx context.Context, key string) (int64, bool, error) {
|
||||
item := s.cache.Get(key)
|
||||
if item == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
value, ok := item.Value().(int64)
|
||||
if !ok {
|
||||
log.Debug(ctx, "Cache type mismatch", "plugin", s.pluginName, "key", key, "expected", "int64")
|
||||
return 0, false, nil
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// SetFloat stores a float value in the cache.
|
||||
func (s *cacheServiceImpl) SetFloat(ctx context.Context, key string, value float64, ttlSeconds int64) error {
|
||||
s.cache.Set(key, value, s.getTTL(ttlSeconds))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFloat retrieves a float value from the cache.
|
||||
func (s *cacheServiceImpl) GetFloat(ctx context.Context, key string) (float64, bool, error) {
|
||||
item := s.cache.Get(key)
|
||||
if item == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
value, ok := item.Value().(float64)
|
||||
if !ok {
|
||||
log.Debug(ctx, "Cache type mismatch", "plugin", s.pluginName, "key", key, "expected", "float64")
|
||||
return 0, false, nil
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// SetBytes stores a byte slice in the cache.
|
||||
func (s *cacheServiceImpl) SetBytes(ctx context.Context, key string, value []byte, ttlSeconds int64) error {
|
||||
s.cache.Set(key, value, s.getTTL(ttlSeconds))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBytes retrieves a byte slice from the cache.
|
||||
func (s *cacheServiceImpl) GetBytes(ctx context.Context, key string) ([]byte, bool, error) {
|
||||
item := s.cache.Get(key)
|
||||
if item == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
value, ok := item.Value().([]byte)
|
||||
if !ok {
|
||||
log.Debug(ctx, "Cache type mismatch", "plugin", s.pluginName, "key", key, "expected", "[]byte")
|
||||
return nil, false, nil
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// Has checks if a key exists in the cache.
|
||||
func (s *cacheServiceImpl) Has(ctx context.Context, key string) (bool, error) {
|
||||
item := s.cache.Get(key)
|
||||
return item != nil, nil
|
||||
}
|
||||
|
||||
// Remove deletes a value from the cache.
|
||||
func (s *cacheServiceImpl) Remove(ctx context.Context, key string) error {
|
||||
s.cache.Delete(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close stops the cache's janitor goroutine and clears all entries.
|
||||
// This is called when the plugin is unloaded.
|
||||
func (s *cacheServiceImpl) Close() error {
|
||||
s.cache.Stop()
|
||||
s.cache.DeleteAll()
|
||||
log.Debug("Closed plugin cache", "plugin", s.pluginName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure cacheServiceImpl implements host.CacheService
|
||||
var _ host.CacheService = (*cacheServiceImpl)(nil)
|
||||
555
plugins/host_cache_test.go
Normal file
555
plugins/host_cache_test.go
Normal file
@ -0,0 +1,555 @@
|
||||
//go:build !windows
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("CacheService", func() {
|
||||
var service *cacheServiceImpl
|
||||
var ctx context.Context
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
service = newCacheService("test_plugin")
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
if service != nil {
|
||||
service.Close()
|
||||
}
|
||||
})
|
||||
|
||||
Describe("getTTL", func() {
|
||||
It("returns default TTL when seconds is 0", func() {
|
||||
ttl := service.getTTL(0)
|
||||
Expect(ttl).To(Equal(defaultCacheTTL))
|
||||
})
|
||||
|
||||
It("returns default TTL when seconds is negative", func() {
|
||||
ttl := service.getTTL(-10)
|
||||
Expect(ttl).To(Equal(defaultCacheTTL))
|
||||
})
|
||||
|
||||
It("returns correct duration when seconds is positive", func() {
|
||||
ttl := service.getTTL(60)
|
||||
Expect(ttl).To(Equal(time.Minute))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Plugin Isolation", func() {
|
||||
It("isolates keys between plugins", func() {
|
||||
service1 := newCacheService("plugin1")
|
||||
defer service1.Close()
|
||||
service2 := newCacheService("plugin2")
|
||||
defer service2.Close()
|
||||
|
||||
// Both plugins set same key
|
||||
err := service1.SetString(ctx, "shared", "value1", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = service2.SetString(ctx, "shared", "value2", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Each plugin should get their own value
|
||||
val1, exists1, err := service1.GetString(ctx, "shared")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists1).To(BeTrue())
|
||||
Expect(val1).To(Equal("value1"))
|
||||
|
||||
val2, exists2, err := service2.GetString(ctx, "shared")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists2).To(BeTrue())
|
||||
Expect(val2).To(Equal("value2"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("String Operations", func() {
|
||||
It("sets and gets a string value", func() {
|
||||
err := service.SetString(ctx, "string_key", "test_value", 300)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
value, exists, err := service.GetString(ctx, "string_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal("test_value"))
|
||||
})
|
||||
|
||||
It("returns not exists for missing key", func() {
|
||||
value, exists, err := service.GetString(ctx, "missing_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(""))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Integer Operations", func() {
|
||||
It("sets and gets an integer value", func() {
|
||||
err := service.SetInt(ctx, "int_key", 42, 300)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
value, exists, err := service.GetInt(ctx, "int_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal(int64(42)))
|
||||
})
|
||||
|
||||
It("returns not exists for missing key", func() {
|
||||
value, exists, err := service.GetInt(ctx, "missing_int_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(int64(0)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Float Operations", func() {
|
||||
It("sets and gets a float value", func() {
|
||||
err := service.SetFloat(ctx, "float_key", 3.14, 300)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
value, exists, err := service.GetFloat(ctx, "float_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal(3.14))
|
||||
})
|
||||
|
||||
It("returns not exists for missing key", func() {
|
||||
value, exists, err := service.GetFloat(ctx, "missing_float_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(float64(0)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Bytes Operations", func() {
|
||||
It("sets and gets a bytes value", func() {
|
||||
byteData := []byte("hello world")
|
||||
err := service.SetBytes(ctx, "bytes_key", byteData, 300)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
value, exists, err := service.GetBytes(ctx, "bytes_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal(byteData))
|
||||
})
|
||||
|
||||
It("returns not exists for missing key", func() {
|
||||
value, exists, err := service.GetBytes(ctx, "missing_bytes_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Type mismatch handling", func() {
|
||||
It("returns not exists when type doesn't match the getter", func() {
|
||||
// Set string
|
||||
err := service.SetString(ctx, "mixed_key", "string value", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Try to get as int
|
||||
value, exists, err := service.GetInt(ctx, "mixed_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(int64(0)))
|
||||
})
|
||||
|
||||
It("returns not exists when getting string as float", func() {
|
||||
err := service.SetString(ctx, "str_as_float", "not a float", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
value, exists, err := service.GetFloat(ctx, "str_as_float")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(float64(0)))
|
||||
})
|
||||
|
||||
It("returns not exists when getting int as bytes", func() {
|
||||
err := service.SetInt(ctx, "int_as_bytes", 123, 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
value, exists, err := service.GetBytes(ctx, "int_as_bytes")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Has Operation", func() {
|
||||
It("returns true for existing key", func() {
|
||||
err := service.SetString(ctx, "existing_key", "exists", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
exists, err := service.Has(ctx, "existing_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns false for non-existing key", func() {
|
||||
exists, err := service.Has(ctx, "non_existing_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Remove Operation", func() {
|
||||
It("removes a value from the cache", func() {
|
||||
// Set a value
|
||||
err := service.SetString(ctx, "remove_key", "to be removed", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Verify it exists
|
||||
exists, err := service.Has(ctx, "remove_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
|
||||
// Remove it
|
||||
err = service.Remove(ctx, "remove_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Verify it's gone
|
||||
exists, err = service.Has(ctx, "remove_key")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeFalse())
|
||||
})
|
||||
|
||||
It("does not error when removing non-existing key", func() {
|
||||
err := service.Remove(ctx, "never_existed")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("TTL Behavior", func() {
|
||||
It("uses default TTL when 0 is provided", func() {
|
||||
err := service.SetString(ctx, "default_ttl", "value", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Value should exist immediately
|
||||
exists, err := service.Has(ctx, "default_ttl")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
})
|
||||
|
||||
It("uses custom TTL when provided", func() {
|
||||
err := service.SetString(ctx, "custom_ttl", "value", 300)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Value should exist immediately
|
||||
exists, err := service.Has(ctx, "custom_ttl")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(exists).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Close", func() {
|
||||
It("removes all cache entries for the plugin", func() {
|
||||
// Use a dedicated service for this test
|
||||
closeService := newCacheService("close_test_plugin")
|
||||
|
||||
// Set multiple values
|
||||
err := closeService.SetString(ctx, "key1", "value1", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = closeService.SetInt(ctx, "key2", 42, 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = closeService.SetFloat(ctx, "key3", 3.14, 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Verify they exist
|
||||
exists, _ := closeService.Has(ctx, "key1")
|
||||
Expect(exists).To(BeTrue())
|
||||
exists, _ = closeService.Has(ctx, "key2")
|
||||
Expect(exists).To(BeTrue())
|
||||
exists, _ = closeService.Has(ctx, "key3")
|
||||
Expect(exists).To(BeTrue())
|
||||
|
||||
// Close the service
|
||||
err = closeService.Close()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// All entries should be gone
|
||||
exists, _ = closeService.Has(ctx, "key1")
|
||||
Expect(exists).To(BeFalse())
|
||||
exists, _ = closeService.Has(ctx, "key2")
|
||||
Expect(exists).To(BeFalse())
|
||||
exists, _ = closeService.Has(ctx, "key3")
|
||||
Expect(exists).To(BeFalse())
|
||||
})
|
||||
|
||||
It("does not affect other plugins' cache entries", func() {
|
||||
// Create two services for different plugins
|
||||
service1 := newCacheService("plugin_close_test1")
|
||||
service2 := newCacheService("plugin_close_test2")
|
||||
defer service2.Close()
|
||||
|
||||
// Set values for both plugins
|
||||
err := service1.SetString(ctx, "key", "value1", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = service2.SetString(ctx, "key", "value2", 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Close only service1
|
||||
err = service1.Close()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// service1's key should be gone
|
||||
exists, _ := service1.Has(ctx, "key")
|
||||
Expect(exists).To(BeFalse())
|
||||
|
||||
// service2's key should still exist
|
||||
exists, _ = service2.Has(ctx, "key")
|
||||
Expect(exists).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("CacheService Integration", Ordered, func() {
|
||||
var (
|
||||
manager *Manager
|
||||
tmpDir string
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "cache-test-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Copy the fake_cache_plugin
|
||||
srcPath := filepath.Join(testdataDir, "fake_cache_plugin.wasm")
|
||||
destPath := filepath.Join(tmpDir, "fake_cache_plugin.wasm")
|
||||
data, err := os.ReadFile(srcPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = os.WriteFile(destPath, data, 0600)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Setup config
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.Plugins.Enabled = true
|
||||
conf.Server.Plugins.Folder = tmpDir
|
||||
conf.Server.Plugins.AutoReload = false
|
||||
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
|
||||
|
||||
// Create and start manager
|
||||
manager = &Manager{
|
||||
plugins: make(map[string]*plugin),
|
||||
}
|
||||
err = manager.Start(GinkgoT().Context())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
DeferCleanup(func() {
|
||||
_ = manager.Stop()
|
||||
_ = os.RemoveAll(tmpDir)
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Plugin Loading", func() {
|
||||
It("should load plugin with cache permission", func() {
|
||||
manager.mu.RLock()
|
||||
p, ok := manager.plugins["fake_cache_plugin"]
|
||||
manager.mu.RUnlock()
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(p.manifest.Permissions).ToNot(BeNil())
|
||||
Expect(p.manifest.Permissions.Cache).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Cache Operations via Plugin", func() {
|
||||
type testCacheInput struct {
|
||||
Operation string `json:"operation"`
|
||||
Key string `json:"key"`
|
||||
StringVal string `json:"string_val,omitempty"`
|
||||
IntVal int64 `json:"int_val,omitempty"`
|
||||
FloatVal float64 `json:"float_val,omitempty"`
|
||||
BytesVal []byte `json:"bytes_val,omitempty"`
|
||||
TTLSeconds int64 `json:"ttl_seconds,omitempty"`
|
||||
}
|
||||
type testCacheOutput struct {
|
||||
StringVal string `json:"string_val,omitempty"`
|
||||
IntVal int64 `json:"int_val,omitempty"`
|
||||
FloatVal float64 `json:"float_val,omitempty"`
|
||||
BytesVal []byte `json:"bytes_val,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
callTestCache := func(ctx context.Context, input testCacheInput) (*testCacheOutput, error) {
|
||||
manager.mu.RLock()
|
||||
p := manager.plugins["fake_cache_plugin"]
|
||||
manager.mu.RUnlock()
|
||||
|
||||
instance, err := p.instance()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer instance.Close(ctx)
|
||||
|
||||
inputBytes, _ := json.Marshal(input)
|
||||
_, outputBytes, err := instance.Call("nd_test_cache", inputBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var output testCacheOutput
|
||||
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if output.Error != nil {
|
||||
return nil, errors.New(*output.Error)
|
||||
}
|
||||
return &output, nil
|
||||
}
|
||||
|
||||
It("should set and get string value", func() {
|
||||
ctx := GinkgoT().Context()
|
||||
|
||||
// Set string
|
||||
_, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "set_string",
|
||||
Key: "test_string",
|
||||
StringVal: "hello world",
|
||||
TTLSeconds: 300,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Get string
|
||||
output, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "get_string",
|
||||
Key: "test_string",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
Expect(output.StringVal).To(Equal("hello world"))
|
||||
})
|
||||
|
||||
It("should set and get integer value", func() {
|
||||
ctx := GinkgoT().Context()
|
||||
|
||||
// Set int
|
||||
_, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "set_int",
|
||||
Key: "test_int",
|
||||
IntVal: 42,
|
||||
TTLSeconds: 300,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Get int
|
||||
output, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "get_int",
|
||||
Key: "test_int",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
Expect(output.IntVal).To(Equal(int64(42)))
|
||||
})
|
||||
|
||||
It("should set and get float value", func() {
|
||||
ctx := GinkgoT().Context()
|
||||
|
||||
// Set float
|
||||
_, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "set_float",
|
||||
Key: "test_float",
|
||||
FloatVal: 3.14159,
|
||||
TTLSeconds: 300,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Get float
|
||||
output, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "get_float",
|
||||
Key: "test_float",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
Expect(output.FloatVal).To(Equal(3.14159))
|
||||
})
|
||||
|
||||
It("should set and get bytes value", func() {
|
||||
ctx := GinkgoT().Context()
|
||||
testBytes := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
|
||||
// Set bytes
|
||||
_, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "set_bytes",
|
||||
Key: "test_bytes",
|
||||
BytesVal: testBytes,
|
||||
TTLSeconds: 300,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Get bytes
|
||||
output, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "get_bytes",
|
||||
Key: "test_bytes",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
Expect(output.BytesVal).To(Equal(testBytes))
|
||||
})
|
||||
|
||||
It("should check if key exists", func() {
|
||||
ctx := GinkgoT().Context()
|
||||
|
||||
// Set a value
|
||||
_, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "set_string",
|
||||
Key: "exists_test",
|
||||
StringVal: "value",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Check has
|
||||
output, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "has",
|
||||
Key: "exists_test",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
|
||||
// Check non-existent
|
||||
output, err = callTestCache(ctx, testCacheInput{
|
||||
Operation: "has",
|
||||
Key: "nonexistent",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should remove a key", func() {
|
||||
ctx := GinkgoT().Context()
|
||||
|
||||
// Set a value
|
||||
_, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "set_string",
|
||||
Key: "remove_test",
|
||||
StringVal: "value",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Remove it
|
||||
_, err = callTestCache(ctx, testCacheInput{
|
||||
Operation: "remove",
|
||||
Key: "remove_test",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Verify it's gone
|
||||
output, err := callTestCache(ctx, testCacheInput{
|
||||
Operation: "has",
|
||||
Key: "remove_test",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -50,7 +50,7 @@ type schedulerServiceImpl struct {
|
||||
}
|
||||
|
||||
// newSchedulerService creates a new SchedulerService for a plugin.
|
||||
func newSchedulerService(pluginName string, manager *Manager, sched scheduler.Scheduler) host.SchedulerService {
|
||||
func newSchedulerService(pluginName string, manager *Manager, sched scheduler.Scheduler) *schedulerServiceImpl {
|
||||
return &schedulerServiceImpl{
|
||||
pluginName: pluginName,
|
||||
manager: manager,
|
||||
|
||||
@ -59,7 +59,7 @@ type webSocketServiceImpl struct {
|
||||
}
|
||||
|
||||
// newWebSocketService creates a new WebSocketService for a plugin.
|
||||
func newWebSocketService(pluginName string, manager *Manager, allowedHosts []string) host.WebSocketService {
|
||||
func newWebSocketService(pluginName string, manager *Manager, allowedHosts []string) *webSocketServiceImpl {
|
||||
return &webSocketServiceImpl{
|
||||
pluginName: pluginName,
|
||||
manager: manager,
|
||||
|
||||
@ -363,6 +363,7 @@ func (m *Manager) loadPlugin(name, wasmPath string) error {
|
||||
stubHostFunctions = append(stubHostFunctions, host.RegisterSchedulerHostFunctions(nil)...)
|
||||
stubHostFunctions = append(stubHostFunctions, host.RegisterWebSocketHostFunctions(nil)...)
|
||||
stubHostFunctions = append(stubHostFunctions, host.RegisterArtworkHostFunctions(nil)...)
|
||||
stubHostFunctions = append(stubHostFunctions, host.RegisterCacheHostFunctions(nil)...)
|
||||
|
||||
// Create initial compiled plugin with stub host functions
|
||||
compiled, err := extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, stubHostFunctions)
|
||||
@ -440,6 +441,13 @@ func (m *Manager) loadPlugin(name, wasmPath string) error {
|
||||
hostFunctions = append(hostFunctions, host.RegisterArtworkHostFunctions(service)...)
|
||||
}
|
||||
|
||||
// Register Cache host functions if permission is granted
|
||||
if manifest.Permissions != nil && manifest.Permissions.Cache != nil {
|
||||
service := newCacheService(name)
|
||||
closers = append(closers, service)
|
||||
hostFunctions = append(hostFunctions, host.RegisterCacheHostFunctions(service)...)
|
||||
}
|
||||
|
||||
// Check if recompilation is needed (AllowedHosts or host functions)
|
||||
needsRecompile := len(pluginManifest.AllowedHosts) > 0 || len(hostFunctions) > 0
|
||||
|
||||
|
||||
@ -55,6 +55,9 @@
|
||||
},
|
||||
"artwork": {
|
||||
"$ref": "#/$defs/ArtworkPermission"
|
||||
},
|
||||
"cache": {
|
||||
"$ref": "#/$defs/CachePermission"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -69,6 +72,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CachePermission": {
|
||||
"type": "object",
|
||||
"description": "Cache service permissions for storing and retrieving data",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Explanation for why cache access is needed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HTTPPermission": {
|
||||
"type": "object",
|
||||
"description": "HTTP access permissions for a plugin",
|
||||
|
||||
@ -11,6 +11,12 @@ type ArtworkPermission struct {
|
||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Cache service permissions for storing and retrieving data
|
||||
type CachePermission struct {
|
||||
// Explanation for why cache access is needed
|
||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration access permissions for a plugin
|
||||
type ConfigPermission struct {
|
||||
// Explanation for why config access is needed
|
||||
@ -86,6 +92,9 @@ type Permissions struct {
|
||||
// Artwork corresponds to the JSON schema field "artwork".
|
||||
Artwork *ArtworkPermission `json:"artwork,omitempty" yaml:"artwork,omitempty" mapstructure:"artwork,omitempty"`
|
||||
|
||||
// Cache corresponds to the JSON schema field "cache".
|
||||
Cache *CachePermission `json:"cache,omitempty" yaml:"cache,omitempty" mapstructure:"cache,omitempty"`
|
||||
|
||||
// Http corresponds to the JSON schema field "http".
|
||||
Http *HTTPPermission `json:"http,omitempty" yaml:"http,omitempty" mapstructure:"http,omitempty"`
|
||||
|
||||
|
||||
5
plugins/testdata/fake_cache_plugin/go.mod
vendored
Normal file
5
plugins/testdata/fake_cache_plugin/go.mod
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
module fake-cache
|
||||
|
||||
go 1.23
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
||||
2
plugins/testdata/fake_cache_plugin/go.sum
vendored
Normal file
2
plugins/testdata/fake_cache_plugin/go.sum
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||
210
plugins/testdata/fake_cache_plugin/main.go
vendored
Normal file
210
plugins/testdata/fake_cache_plugin/main.go
vendored
Normal file
@ -0,0 +1,210 @@
|
||||
// Fake Cache plugin for Navidrome plugin system integration tests.
|
||||
// Build with: tinygo build -o ../fake_cache_plugin.wasm -target wasip1 -buildmode=c-shared .
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// Manifest types
|
||||
type Manifest struct {
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Permissions *Permissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Cache *CachePermission `json:"cache,omitempty"`
|
||||
}
|
||||
|
||||
type CachePermission struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
//go:wasmexport nd_manifest
|
||||
func ndManifest() int32 {
|
||||
manifest := Manifest{
|
||||
Name: "Fake Cache Plugin",
|
||||
Author: "Navidrome Test",
|
||||
Version: "1.0.0",
|
||||
Description: "A fake cache plugin for integration testing",
|
||||
Permissions: &Permissions{
|
||||
Cache: &CachePermission{
|
||||
Reason: "For testing cache operations",
|
||||
},
|
||||
},
|
||||
}
|
||||
out, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return 1
|
||||
}
|
||||
pdk.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
// TestCacheInput is the input for nd_test_cache callback.
|
||||
type TestCacheInput struct {
|
||||
Operation string `json:"operation"` // "set_string", "get_string", "set_int", "get_int", "set_float", "get_float", "set_bytes", "get_bytes", "has", "remove"
|
||||
Key string `json:"key"` // Cache key
|
||||
StringVal string `json:"string_val"` // For string operations
|
||||
IntVal int64 `json:"int_val"` // For int operations
|
||||
FloatVal float64 `json:"float_val"` // For float operations
|
||||
BytesVal []byte `json:"bytes_val"` // For bytes operations
|
||||
TTLSeconds int64 `json:"ttl_seconds"` // TTL in seconds
|
||||
}
|
||||
|
||||
// TestCacheOutput is the output from nd_test_cache callback.
|
||||
type TestCacheOutput struct {
|
||||
StringVal string `json:"string_val,omitempty"`
|
||||
IntVal int64 `json:"int_val,omitempty"`
|
||||
FloatVal float64 `json:"float_val,omitempty"`
|
||||
BytesVal []byte `json:"bytes_val,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// nd_test_cache is the test callback that tests the cache host functions.
|
||||
//
|
||||
//go:wasmexport nd_test_cache
|
||||
func ndTestCache() int32 {
|
||||
var input TestCacheInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
|
||||
switch input.Operation {
|
||||
case "set_string":
|
||||
err := CacheSetString(input.Key, input.StringVal, input.TTLSeconds)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{})
|
||||
return 0
|
||||
|
||||
case "get_string":
|
||||
resp, err := CacheGetString(input.Key)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
if resp.Error != "" {
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &resp.Error})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{StringVal: resp.Value, Exists: resp.Exists})
|
||||
return 0
|
||||
|
||||
case "set_int":
|
||||
err := CacheSetInt(input.Key, input.IntVal, input.TTLSeconds)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{})
|
||||
return 0
|
||||
|
||||
case "get_int":
|
||||
resp, err := CacheGetInt(input.Key)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
if resp.Error != "" {
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &resp.Error})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{IntVal: resp.Value, Exists: resp.Exists})
|
||||
return 0
|
||||
|
||||
case "set_float":
|
||||
err := CacheSetFloat(input.Key, input.FloatVal, input.TTLSeconds)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{})
|
||||
return 0
|
||||
|
||||
case "get_float":
|
||||
resp, err := CacheGetFloat(input.Key)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
if resp.Error != "" {
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &resp.Error})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{FloatVal: resp.Value, Exists: resp.Exists})
|
||||
return 0
|
||||
|
||||
case "set_bytes":
|
||||
err := CacheSetBytes(input.Key, input.BytesVal, input.TTLSeconds)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{})
|
||||
return 0
|
||||
|
||||
case "get_bytes":
|
||||
resp, err := CacheGetBytes(input.Key)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
if resp.Error != "" {
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &resp.Error})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{BytesVal: resp.Value, Exists: resp.Exists})
|
||||
return 0
|
||||
|
||||
case "has":
|
||||
resp, err := CacheHas(input.Key)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
if resp.Error != "" {
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &resp.Error})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{Exists: resp.Exists})
|
||||
return 0
|
||||
|
||||
case "remove":
|
||||
err := CacheRemove(input.Key)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestCacheOutput{})
|
||||
return 0
|
||||
|
||||
default:
|
||||
errStr := "unknown operation: " + input.Operation
|
||||
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
375
plugins/testdata/fake_cache_plugin/nd_host_cache.go
vendored
Normal file
375
plugins/testdata/fake_cache_plugin/nd_host_cache.go
vendored
Normal file
@ -0,0 +1,375 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the Cache host service.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// cache_setstring is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setstring
|
||||
func cache_setstring(uint64, uint64, int64) uint64
|
||||
|
||||
// cache_getstring is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getstring
|
||||
func cache_getstring(uint64) uint64
|
||||
|
||||
// cache_setint is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setint
|
||||
func cache_setint(uint64, int64, int64) uint64
|
||||
|
||||
// cache_getint is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getint
|
||||
func cache_getint(uint64) uint64
|
||||
|
||||
// cache_setfloat is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setfloat
|
||||
func cache_setfloat(uint64, float64, int64) uint64
|
||||
|
||||
// cache_getfloat is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getfloat
|
||||
func cache_getfloat(uint64) uint64
|
||||
|
||||
// cache_setbytes is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_setbytes
|
||||
func cache_setbytes(uint64, uint64, int64) uint64
|
||||
|
||||
// cache_getbytes is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_getbytes
|
||||
func cache_getbytes(uint64) uint64
|
||||
|
||||
// cache_has is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_has
|
||||
func cache_has(uint64) uint64
|
||||
|
||||
// cache_remove is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user cache_remove
|
||||
func cache_remove(uint64) uint64
|
||||
|
||||
// CacheGetStringResponse is the response type for Cache.GetString.
|
||||
type CacheGetStringResponse struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetIntResponse is the response type for Cache.GetInt.
|
||||
type CacheGetIntResponse struct {
|
||||
Value int64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetFloatResponse is the response type for Cache.GetFloat.
|
||||
type CacheGetFloatResponse struct {
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheGetBytesResponse is the response type for Cache.GetBytes.
|
||||
type CacheGetBytesResponse struct {
|
||||
Value []byte `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheHasResponse is the response type for Cache.Has.
|
||||
type CacheHasResponse struct {
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CacheSetString calls the cache_setstring host function.
|
||||
// SetString stores a string value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The string value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetString(key string, value string, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
valueMem := pdk.AllocateString(value)
|
||||
defer valueMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setstring(keyMem.Offset(), valueMem.Offset(), ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetString calls the cache_getstring host function.
|
||||
// GetString retrieves a string value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a string, exists will be false.
|
||||
func CacheGetString(key string) (*CacheGetStringResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getstring(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetStringResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheSetInt calls the cache_setint host function.
|
||||
// SetInt stores an integer value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The integer value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetInt(key string, value int64, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setint(keyMem.Offset(), value, ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetInt calls the cache_getint host function.
|
||||
// GetInt retrieves an integer value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not an integer, exists will be false.
|
||||
func CacheGetInt(key string) (*CacheGetIntResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getint(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetIntResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheSetFloat calls the cache_setfloat host function.
|
||||
// SetFloat stores a float value in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The float value to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetFloat(key string, value float64, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setfloat(keyMem.Offset(), value, ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetFloat calls the cache_getfloat host function.
|
||||
// GetFloat retrieves a float value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a float, exists will be false.
|
||||
func CacheGetFloat(key string) (*CacheGetFloatResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getfloat(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetFloatResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheSetBytes calls the cache_setbytes host function.
|
||||
// SetBytes stores a byte slice in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
// - value: The byte slice to store
|
||||
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func CacheSetBytes(key string, value []byte, ttlSeconds int64) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
valueMem := pdk.AllocateBytes(value)
|
||||
defer valueMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_setbytes(keyMem.Offset(), valueMem.Offset(), ttlSeconds)
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetBytes calls the cache_getbytes host function.
|
||||
// GetBytes retrieves a byte slice from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key doesn't exist
|
||||
// or the stored value is not a byte slice, exists will be false.
|
||||
func CacheGetBytes(key string) (*CacheGetBytesResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_getbytes(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheGetBytesResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheHas calls the cache_has host function.
|
||||
// Has checks if a key exists in the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns true if the key exists and has not expired.
|
||||
func CacheHas(key string) (*CacheHasResponse, error) {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_has(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response CacheHasResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// CacheRemove calls the cache_remove host function.
|
||||
// Remove deletes a value from the cache.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (will be namespaced with plugin ID)
|
||||
//
|
||||
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
|
||||
func CacheRemove(key string) error {
|
||||
keyMem := pdk.AllocateString(key)
|
||||
defer keyMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := cache_remove(keyMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user