mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-03 06:15:22 +00:00
feat: add support for experimental WebAssembly threads
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
a2ace6e84e
commit
c6fe02a49c
@ -132,6 +132,27 @@ Every plugin must include a `manifest.json` file. Example:
|
||||
|
||||
**Required fields:** `name`, `author`, `version`
|
||||
|
||||
#### Experimental Features
|
||||
|
||||
Plugins can opt-in to experimental WebAssembly features that may change or be removed in future versions. Currently supported:
|
||||
|
||||
- **`threads`** – Enables WebAssembly threads support (for plugins compiled with multi-threading)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Threaded Plugin",
|
||||
"author": "Author Name",
|
||||
"version": "1.0.0",
|
||||
"experimental": {
|
||||
"threads": {
|
||||
"reason": "Required for concurrent audio processing"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** Experimental features may have compatibility or performance implications. Use only when necessary.
|
||||
|
||||
---
|
||||
|
||||
## Capabilities
|
||||
|
||||
@ -12,6 +12,8 @@ import (
|
||||
"github.com/navidrome/navidrome/plugins/host"
|
||||
"github.com/navidrome/navidrome/scheduler"
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@ -268,11 +270,19 @@ func (m *Manager) loadPluginWithConfig(name, ndpPath, configJSON string) error {
|
||||
}
|
||||
|
||||
// Compile the plugin with all host functions
|
||||
runtimeConfig := wazero.NewRuntimeConfig().
|
||||
WithCompilationCache(m.cache).
|
||||
WithCloseOnContextDone(true)
|
||||
|
||||
// Enable experimental threads if requested in manifest
|
||||
if pkg.Manifest.HasExperimentalThreads() {
|
||||
runtimeConfig = runtimeConfig.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads)
|
||||
log.Debug(m.ctx, "Enabling experimental threads support", "plugin", name)
|
||||
}
|
||||
|
||||
extismConfig := extism.PluginConfig{
|
||||
EnableWasi: true,
|
||||
RuntimeConfig: wazero.NewRuntimeConfig().
|
||||
WithCompilationCache(m.cache).
|
||||
WithCloseOnContextDone(true),
|
||||
EnableWasi: true,
|
||||
RuntimeConfig: runtimeConfig,
|
||||
}
|
||||
compiled, err := extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, hostFunctions)
|
||||
if err != nil {
|
||||
|
||||
@ -33,9 +33,33 @@
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/Permissions"
|
||||
},
|
||||
"experimental": {
|
||||
"$ref": "#/$defs/Experimental"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Experimental": {
|
||||
"type": "object",
|
||||
"description": "Experimental features that may change or be removed in future versions",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"threads": {
|
||||
"$ref": "#/$defs/ThreadsFeature"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ThreadsFeature": {
|
||||
"type": "object",
|
||||
"description": "Enable experimental WebAssembly threads support",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Explanation for why threads support is needed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permissions": {
|
||||
"type": "object",
|
||||
"description": "Permissions required by the plugin",
|
||||
|
||||
@ -11,5 +11,7 @@ func (m *Manifest) AllowedHosts() []string {
|
||||
return m.Permissions.Http.AllowedHosts
|
||||
}
|
||||
|
||||
// TODO: ConfigPermission is defined in the schema but not currently enforced.
|
||||
// Plugins always receive their config section. Implement permission checking or remove from schema.
|
||||
// HasExperimentalThreads returns true if the manifest requests experimental threads support.
|
||||
func (m *Manifest) HasExperimentalThreads() bool {
|
||||
return m.Experimental != nil && m.Experimental.Threads != nil
|
||||
}
|
||||
|
||||
@ -23,6 +23,12 @@ type ConfigPermission struct {
|
||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Experimental features that may change or be removed in future versions
|
||||
type Experimental struct {
|
||||
// Threads corresponds to the JSON schema field "threads".
|
||||
Threads *ThreadsFeature `json:"threads,omitempty" yaml:"threads,omitempty" mapstructure:"threads,omitempty"`
|
||||
}
|
||||
|
||||
// HTTP access permissions for a plugin
|
||||
type HTTPPermission struct {
|
||||
// List of allowed host patterns for HTTP requests (e.g., 'api.example.com',
|
||||
@ -78,6 +84,9 @@ type Manifest struct {
|
||||
// A brief description of what the plugin does
|
||||
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
|
||||
|
||||
// Experimental corresponds to the JSON schema field "experimental".
|
||||
Experimental *Experimental `json:"experimental,omitempty" yaml:"experimental,omitempty" mapstructure:"experimental,omitempty"`
|
||||
|
||||
// The display name of the plugin
|
||||
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||
|
||||
@ -187,6 +196,12 @@ func (j *SubsonicAPIPermission) UnmarshalJSON(value []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable experimental WebAssembly threads support
|
||||
type ThreadsFeature struct {
|
||||
// Explanation for why threads support is needed
|
||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocket service permissions for establishing WebSocket connections
|
||||
type WebSocketPermission struct {
|
||||
// List of allowed host patterns for WebSocket connections (e.g.,
|
||||
|
||||
@ -145,4 +145,75 @@ var _ = Describe("Manifest", func() {
|
||||
Expect(hosts).To(Equal([]string{"api.example.com", "*.spotify.com"}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("HasExperimentalThreads", func() {
|
||||
It("returns false when no experimental section", func() {
|
||||
m := &Manifest{}
|
||||
Expect(m.HasExperimentalThreads()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("returns false when experimental section has no threads", func() {
|
||||
m := &Manifest{
|
||||
Experimental: &Experimental{},
|
||||
}
|
||||
Expect(m.HasExperimentalThreads()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("returns true when threads feature is present", func() {
|
||||
m := &Manifest{
|
||||
Experimental: &Experimental{
|
||||
Threads: &ThreadsFeature{},
|
||||
},
|
||||
}
|
||||
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns true when threads feature has a reason", func() {
|
||||
reason := "Required for concurrent processing"
|
||||
m := &Manifest{
|
||||
Experimental: &Experimental{
|
||||
Threads: &ThreadsFeature{
|
||||
Reason: &reason,
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("parses experimental.threads from JSON", func() {
|
||||
data := []byte(`{
|
||||
"name": "Threaded Plugin",
|
||||
"author": "Test Author",
|
||||
"version": "1.0.0",
|
||||
"experimental": {
|
||||
"threads": {
|
||||
"reason": "To use multi-threaded WASM module"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
var m Manifest
|
||||
err := json.Unmarshal(data, &m)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||
Expect(m.Experimental.Threads.Reason).ToNot(BeNil())
|
||||
Expect(*m.Experimental.Threads.Reason).To(Equal("To use multi-threaded WASM module"))
|
||||
})
|
||||
|
||||
It("parses experimental.threads without reason from JSON", func() {
|
||||
data := []byte(`{
|
||||
"name": "Threaded Plugin",
|
||||
"author": "Test Author",
|
||||
"version": "1.0.0",
|
||||
"experimental": {
|
||||
"threads": {}
|
||||
}
|
||||
}`)
|
||||
|
||||
var m Manifest
|
||||
err := json.Unmarshal(data, &m)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user