mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
feat: add Crypto Ticker example plugin for real-time cryptocurrency price updates via Coinbase WebSocket API
Also add the lifecycle capability Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
d1225b7828
commit
9da40af6fb
51
plugins/capability_lifecycle.go
Normal file
51
plugins/capability_lifecycle.go
Normal file
@ -0,0 +1,51 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
// CapabilityLifecycle indicates the plugin has lifecycle callback functions.
|
||||
// Detected when the plugin exports the nd_on_init function.
|
||||
const CapabilityLifecycle Capability = "Lifecycle"
|
||||
|
||||
const FuncOnInit = "nd_on_init"
|
||||
|
||||
func init() {
|
||||
registerCapability(
|
||||
CapabilityLifecycle,
|
||||
FuncOnInit,
|
||||
)
|
||||
}
|
||||
|
||||
// onInitInput is the input for nd_on_init (currently empty, reserved for future use)
|
||||
type onInitInput struct{}
|
||||
|
||||
// onInitOutput is the output from nd_on_init
|
||||
type onInitOutput struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// callPluginInit calls the plugin's nd_on_init function if it has the Lifecycle capability.
|
||||
// This is called after the plugin is fully loaded with all services registered.
|
||||
func callPluginInit(ctx context.Context, instance *pluginInstance) {
|
||||
if !hasCapability(instance.capabilities, CapabilityLifecycle) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Calling plugin init function", "plugin", instance.name)
|
||||
|
||||
result, err := callPluginFunction[onInitInput, onInitOutput](ctx, instance, FuncOnInit, onInitInput{})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Plugin init function failed", "plugin", instance.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Error != "" {
|
||||
log.Error(ctx, "Plugin init function returned error", "plugin", instance.name, "error", result.Error)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Plugin init function completed", "plugin", instance.name)
|
||||
}
|
||||
82
plugins/examples/crypto-ticker/README.md
Normal file
82
plugins/examples/crypto-ticker/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Crypto Ticker Plugin
|
||||
|
||||
This is a WebSocket-based WASM plugin for Navidrome that displays real-time cryptocurrency prices from Coinbase.
|
||||
|
||||
## Features
|
||||
|
||||
- Connects to Coinbase WebSocket API to receive real-time ticker updates
|
||||
- Configurable to track multiple cryptocurrency pairs
|
||||
- Implements WebSocket callback handlers for message processing
|
||||
- Automatically reconnects on connection loss using the scheduler service
|
||||
- Displays price, best bid, best ask, and 24-hour percentage change
|
||||
|
||||
## Configuration
|
||||
|
||||
In your `navidrome.toml` file, add:
|
||||
|
||||
```toml
|
||||
[PluginConfig.crypto-ticker]
|
||||
tickers = "BTC,ETH,SOL,MATIC"
|
||||
```
|
||||
|
||||
- `tickers` is a comma-separated list of cryptocurrency symbols
|
||||
- The plugin will append `-USD` to any symbol without a trading pair specified
|
||||
- Default: `BTC,ETH` if not configured
|
||||
|
||||
## How it Works
|
||||
|
||||
1. On plugin initialization, connects to Coinbase's WebSocket API
|
||||
2. Subscribes to ticker updates for the configured cryptocurrencies
|
||||
3. Incoming ticker data is processed via `nd_websocket_on_text_message` callback
|
||||
4. On connection loss, schedules a reconnection attempt via the scheduler service
|
||||
5. Reconnection is attempted until successful
|
||||
|
||||
## Building
|
||||
|
||||
This plugin was scaffolded using XTP CLI:
|
||||
|
||||
```bash
|
||||
xtp plugin init --schema-file ../schemas/websocket_callback.yaml --template go --path ./crypto-ticker --name crypto-ticker
|
||||
```
|
||||
|
||||
To build the plugin to WASM:
|
||||
|
||||
```bash
|
||||
# Using TinyGo (recommended - smaller binary)
|
||||
tinygo build -o crypto-ticker.wasm -target wasip1 -buildmode=c-shared .
|
||||
|
||||
# Or using standard Go
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o crypto-ticker.wasm .
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Copy the resulting `crypto-ticker.wasm` to your Navidrome plugins folder.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
[Crypto] Crypto Ticker Plugin initializing...
|
||||
[Crypto] Configured tickers: [BTC-USD ETH-USD]
|
||||
[Crypto] Connected to Coinbase WebSocket API (connection: crypto-ticker-conn)
|
||||
[Crypto] Subscription message sent to Coinbase WebSocket API
|
||||
[Crypto] Received subscriptions message
|
||||
[Crypto] 💰 BTC-USD: $98765.43 (24h: +2.35%) Bid: $98764.00 Ask: $98766.00
|
||||
[Crypto] 💰 ETH-USD: $3456.78 (24h: -0.54%) Bid: $3455.90 Ask: $3457.80
|
||||
```
|
||||
|
||||
## Permissions Required
|
||||
|
||||
- **config**: Read ticker symbols configuration
|
||||
- **websocket**: Connect to `ws-feed.exchange.coinbase.com`
|
||||
- **scheduler**: Schedule reconnection attempts
|
||||
|
||||
## Files
|
||||
|
||||
- `main.go` - Main plugin implementation
|
||||
- `pdk.gen.go` - Generated WebSocket callback types (from XTP)
|
||||
- `nd_host.go` - Host function wrappers for WebSocket and Scheduler services
|
||||
|
||||
---
|
||||
|
||||
For more details, see the source code in `main.go`.
|
||||
5
plugins/examples/crypto-ticker/go.mod
Executable file
5
plugins/examples/crypto-ticker/go.mod
Executable file
@ -0,0 +1,5 @@
|
||||
module crypto-ticker
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require github.com/extism/go-pdk v1.1.0
|
||||
2
plugins/examples/crypto-ticker/go.sum
Normal file
2
plugins/examples/crypto-ticker/go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY=
|
||||
github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||
355
plugins/examples/crypto-ticker/main.go
Executable file
355
plugins/examples/crypto-ticker/main.go
Executable file
@ -0,0 +1,355 @@
|
||||
// Crypto Ticker Plugin - Demonstrates WebSocket host service capabilities.
|
||||
//
|
||||
// This plugin connects to Coinbase's WebSocket API to receive real-time
|
||||
// cryptocurrency price updates and logs them to the Navidrome console.
|
||||
//
|
||||
// Note: run `go doc -all` in this package to see all of the types and functions available.
|
||||
// ./pdk.gen.go contains the domain types from the host where your plugin will run.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
const (
|
||||
// Coinbase WebSocket API endpoint
|
||||
coinbaseWSEndpoint = "wss://ws-feed.exchange.coinbase.com"
|
||||
|
||||
// Connection ID for our WebSocket connection
|
||||
connectionID = "crypto-ticker-conn"
|
||||
|
||||
// ID for the reconnection schedule
|
||||
reconnectScheduleID = "crypto-ticker-reconnect"
|
||||
)
|
||||
|
||||
// Manifest types
|
||||
type Manifest struct {
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Website string `json:"website,omitempty"`
|
||||
Permissions *Permissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Config *ConfigPermission `json:"config,omitempty"`
|
||||
WebSocket *WebSocketPermission `json:"websocket,omitempty"`
|
||||
Scheduler *SchedulerPermission `json:"scheduler,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigPermission struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type WebSocketPermission struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
AllowedHosts []string `json:"allowedHosts,omitempty"`
|
||||
}
|
||||
|
||||
type SchedulerPermission struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Coinbase subscription message structure
|
||||
type CoinbaseSubscription struct {
|
||||
Type string `json:"type"`
|
||||
ProductIDs []string `json:"product_ids"`
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
// Coinbase ticker message structure
|
||||
type CoinbaseTicker struct {
|
||||
Type string `json:"type"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
ProductID string `json:"product_id"`
|
||||
Price string `json:"price"`
|
||||
Open24h string `json:"open_24h"`
|
||||
Volume24h string `json:"volume_24h"`
|
||||
Low24h string `json:"low_24h"`
|
||||
High24h string `json:"high_24h"`
|
||||
BestBid string `json:"best_bid"`
|
||||
BestAsk string `json:"best_ask"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// nd_manifest is required by Navidrome to identify the plugin.
|
||||
//
|
||||
//export nd_manifest
|
||||
func ndManifest() int32 {
|
||||
manifest := Manifest{
|
||||
Name: "Crypto Ticker",
|
||||
Author: "Navidrome",
|
||||
Version: "1.0.0",
|
||||
Description: "Real-time cryptocurrency price ticker using Coinbase WebSocket API",
|
||||
Website: "https://github.com/navidrome/navidrome/tree/master/plugins/examples/crypto-ticker",
|
||||
Permissions: &Permissions{
|
||||
Config: &ConfigPermission{
|
||||
Reason: "To read ticker symbols configuration",
|
||||
},
|
||||
WebSocket: &WebSocketPermission{
|
||||
Reason: "To connect to Coinbase WebSocket API for real-time prices",
|
||||
AllowedHosts: []string{"ws-feed.exchange.coinbase.com"},
|
||||
},
|
||||
Scheduler: &SchedulerPermission{
|
||||
Reason: "To schedule reconnection attempts on connection loss",
|
||||
},
|
||||
},
|
||||
}
|
||||
out, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return 1
|
||||
}
|
||||
pdk.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
// OnInitInput is the input for nd_on_init (currently empty, reserved for future use)
|
||||
type OnInitInput struct{}
|
||||
|
||||
// OnInitOutput is the output from nd_on_init
|
||||
type OnInitOutput struct {
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// nd_on_init is called when the plugin is loaded.
|
||||
// We use this to establish the initial WebSocket connection.
|
||||
//
|
||||
//export nd_on_init
|
||||
func ndOnInit() int32 {
|
||||
// Read input (currently empty)
|
||||
var input OnInitInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogInfo, "Crypto Ticker Plugin initializing...")
|
||||
|
||||
// Get ticker configuration
|
||||
tickerConfig, ok := pdk.GetConfig("tickers")
|
||||
if !ok || tickerConfig == "" {
|
||||
tickerConfig = "BTC,ETH" // Default tickers
|
||||
}
|
||||
|
||||
tickers := parseTickerSymbols(tickerConfig)
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Configured tickers: %v", tickers))
|
||||
|
||||
// Connect to WebSocket
|
||||
err := connectAndSubscribe(tickers)
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to connect: %v", err))
|
||||
// Don't fail init - let reconnect logic handle it
|
||||
}
|
||||
|
||||
// Return success output
|
||||
if err := pdk.OutputJSON(OnInitOutput{}); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseTickerSymbols parses a comma-separated list of ticker symbols
|
||||
func parseTickerSymbols(tickerConfig string) []string {
|
||||
parts := strings.Split(tickerConfig, ",")
|
||||
tickers := make([]string, 0, len(parts))
|
||||
for _, ticker := range parts {
|
||||
ticker = strings.TrimSpace(ticker)
|
||||
if ticker == "" {
|
||||
continue
|
||||
}
|
||||
// Add -USD suffix if not present
|
||||
if !strings.Contains(ticker, "-") {
|
||||
ticker = ticker + "-USD"
|
||||
}
|
||||
tickers = append(tickers, ticker)
|
||||
}
|
||||
return tickers
|
||||
}
|
||||
|
||||
// connectAndSubscribe connects to Coinbase WebSocket and subscribes to tickers
|
||||
func connectAndSubscribe(tickers []string) error {
|
||||
// Connect to WebSocket using host function
|
||||
connID, err := WebSocketConnect(coinbaseWSEndpoint, nil, connectionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebSocket connection error: %v", err)
|
||||
}
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Connected to Coinbase WebSocket API (connection: %s)", connID))
|
||||
|
||||
// Subscribe to ticker channel
|
||||
subscription := CoinbaseSubscription{
|
||||
Type: "subscribe",
|
||||
ProductIDs: tickers,
|
||||
Channels: []string{"ticker"},
|
||||
}
|
||||
|
||||
subscriptionJSON, err := json.Marshal(subscription)
|
||||
if err != nil {
|
||||
return fmt.Errorf("JSON marshal error: %v", err)
|
||||
}
|
||||
|
||||
// Send subscription message
|
||||
err = WebSocketSendText(connectionID, string(subscriptionJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebSocket send error: %v", err)
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogInfo, "Subscription message sent to Coinbase WebSocket API")
|
||||
return nil
|
||||
}
|
||||
|
||||
// NdWebsocketOnTextMessage is called when a text message is received
|
||||
func NdWebsocketOnTextMessage(input OnTextMessageInput) (OnTextMessageOutput, error) {
|
||||
// Only process messages from our connection
|
||||
if input.ConnectionId != connectionID {
|
||||
return OnTextMessageOutput{}, nil
|
||||
}
|
||||
|
||||
// Try to parse as a ticker message
|
||||
var ticker CoinbaseTicker
|
||||
err := json.Unmarshal([]byte(input.Message), &ticker)
|
||||
if err != nil {
|
||||
// Not a valid JSON message, ignore
|
||||
return OnTextMessageOutput{}, nil
|
||||
}
|
||||
|
||||
// Only process ticker messages
|
||||
if ticker.Type != "ticker" {
|
||||
// Could be subscription confirmation or heartbeat
|
||||
if ticker.Type != "" {
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received %s message", ticker.Type))
|
||||
}
|
||||
return OnTextMessageOutput{}, nil
|
||||
}
|
||||
|
||||
// Calculate 24h change percentage
|
||||
change := calculatePercentChange(ticker.Open24h, ticker.Price)
|
||||
|
||||
// Log ticker information
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("💰 %s: $%s (24h: %s%%) Bid: $%s Ask: $%s",
|
||||
ticker.ProductID,
|
||||
ticker.Price,
|
||||
change,
|
||||
ticker.BestBid,
|
||||
ticker.BestAsk,
|
||||
))
|
||||
|
||||
return OnTextMessageOutput{}, nil
|
||||
}
|
||||
|
||||
// NdWebsocketOnBinaryMessage is called when a binary message is received
|
||||
func NdWebsocketOnBinaryMessage(input OnBinaryMessageInput) (OnBinaryMessageOutput, error) {
|
||||
// Coinbase doesn't send binary messages, but we implement the handler anyway
|
||||
pdk.Log(pdk.LogWarn, fmt.Sprintf("Received unexpected binary message on connection %s", input.ConnectionId))
|
||||
return OnBinaryMessageOutput{}, nil
|
||||
}
|
||||
|
||||
// NdWebsocketOnError is called when an error occurs on the WebSocket connection
|
||||
func NdWebsocketOnError(input OnErrorInput) (OnErrorOutput, error) {
|
||||
pdk.Log(pdk.LogError, fmt.Sprintf("WebSocket error on connection %s: %s", input.ConnectionId, input.Error))
|
||||
return OnErrorOutput{}, nil
|
||||
}
|
||||
|
||||
// NdWebsocketOnClose is called when the WebSocket connection is closed
|
||||
func NdWebsocketOnClose(input OnCloseInput) (OnCloseOutput, error) {
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection %s closed (code: %d, reason: %s)",
|
||||
input.ConnectionId, input.Code, input.Reason))
|
||||
|
||||
// Only attempt reconnect for our connection
|
||||
if input.ConnectionId == connectionID {
|
||||
pdk.Log(pdk.LogInfo, "Scheduling reconnection attempt in 5 seconds...")
|
||||
|
||||
// Schedule a one-time reconnection attempt
|
||||
_, err := SchedulerScheduleOneTime(5, "reconnect", reconnectScheduleID)
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to schedule reconnection: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return OnCloseOutput{}, nil
|
||||
}
|
||||
|
||||
// Scheduler callback input/output types
|
||||
type SchedulerCallbackInput struct {
|
||||
ScheduleId string `json:"schedule_id"`
|
||||
Payload string `json:"payload"`
|
||||
IsRecurring bool `json:"is_recurring"`
|
||||
}
|
||||
|
||||
type SchedulerCallbackOutput struct {
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// nd_scheduler_callback is called when a scheduled task fires
|
||||
//
|
||||
//export nd_scheduler_callback
|
||||
func ndSchedulerCallback() int32 {
|
||||
var input SchedulerCallbackInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
// Only handle our reconnection schedule
|
||||
if input.ScheduleId != reconnectScheduleID {
|
||||
return 0
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogInfo, "Attempting to reconnect to Coinbase WebSocket API...")
|
||||
|
||||
// Get ticker configuration
|
||||
tickerConfig, ok := pdk.GetConfig("tickers")
|
||||
if !ok || tickerConfig == "" {
|
||||
tickerConfig = "BTC,ETH"
|
||||
}
|
||||
|
||||
tickers := parseTickerSymbols(tickerConfig)
|
||||
|
||||
// Try to connect and subscribe
|
||||
err := connectAndSubscribe(tickers)
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogError, fmt.Sprintf("Reconnection failed: %v - will retry in 10 seconds", err))
|
||||
|
||||
// Schedule another attempt
|
||||
_, err = SchedulerScheduleOneTime(10, "reconnect", reconnectScheduleID)
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to schedule retry: %v", err))
|
||||
}
|
||||
} else {
|
||||
pdk.Log(pdk.LogInfo, "Successfully reconnected!")
|
||||
}
|
||||
|
||||
output := SchedulerCallbackOutput{}
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// calculatePercentChange calculates the percentage change between open and current price
|
||||
func calculatePercentChange(open, current string) string {
|
||||
var openFloat, currentFloat float64
|
||||
_, err := fmt.Sscanf(open, "%f", &openFloat)
|
||||
if err != nil || openFloat == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
_, err = fmt.Sscanf(current, "%f", ¤tFloat)
|
||||
if err != nil {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
change := ((currentFloat - openFloat) / openFloat) * 100
|
||||
if change >= 0 {
|
||||
return fmt.Sprintf("+%.2f", change)
|
||||
}
|
||||
return fmt.Sprintf("%.2f", change)
|
||||
}
|
||||
|
||||
func main() {}
|
||||
188
plugins/examples/crypto-ticker/nd_host.go
Normal file
188
plugins/examples/crypto-ticker/nd_host.go
Normal file
@ -0,0 +1,188 @@
|
||||
// Host function wrappers for Navidrome plugin services.
|
||||
// These allow the plugin to call host functions provided by Navidrome.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// WebSocket host functions
|
||||
|
||||
//go:wasmimport extism:host/user websocket_connect
|
||||
func websocket_connect(uint64) uint64
|
||||
|
||||
//go:wasmimport extism:host/user websocket_sendtext
|
||||
func websocket_sendtext(connectionID uint64, message uint64) uint64
|
||||
|
||||
//go:wasmimport extism:host/user websocket_closeconnection
|
||||
func websocket_closeconnection(connectionID uint64, code int32, reason uint64) uint64
|
||||
|
||||
// WebSocketConnectRequest is the request type for WebSocket.Connect
|
||||
type WebSocketConnectRequest struct {
|
||||
Url string `json:"url"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
ConnectionID string `json:"connectionID,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocketConnectResponse is the response type for WebSocket.Connect
|
||||
type WebSocketConnectResponse struct {
|
||||
NewConnectionID string `json:"newConnectionID,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocketConnect establishes a WebSocket connection to the specified URL.
|
||||
func WebSocketConnect(url string, headers map[string]string, connectionID string) (string, error) {
|
||||
req := WebSocketConnectRequest{
|
||||
Url: url,
|
||||
Headers: headers,
|
||||
ConnectionID: connectionID,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
responsePtr := websocket_connect(reqMem.Offset())
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
var resp WebSocketConnectResponse
|
||||
if err := json.Unmarshal(responseBytes, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
return "", errors.New(resp.Error)
|
||||
}
|
||||
|
||||
return resp.NewConnectionID, nil
|
||||
}
|
||||
|
||||
// WebSocketSendText sends a text message over an established WebSocket connection.
|
||||
func WebSocketSendText(connectionID, message string) error {
|
||||
connMem := pdk.AllocateString(connectionID)
|
||||
defer connMem.Free()
|
||||
msgMem := pdk.AllocateString(message)
|
||||
defer msgMem.Free()
|
||||
|
||||
responsePtr := websocket_sendtext(connMem.Offset(), msgMem.Offset())
|
||||
if responsePtr != 0 {
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebSocketCloseConnection gracefully closes a WebSocket connection.
|
||||
func WebSocketCloseConnection(connectionID string, code int32, reason string) error {
|
||||
connMem := pdk.AllocateString(connectionID)
|
||||
defer connMem.Free()
|
||||
reasonMem := pdk.AllocateString(reason)
|
||||
defer reasonMem.Free()
|
||||
|
||||
responsePtr := websocket_closeconnection(connMem.Offset(), code, reasonMem.Offset())
|
||||
if responsePtr != 0 {
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scheduler host functions
|
||||
|
||||
//go:wasmimport extism:host/user scheduler_scheduleonetime
|
||||
func scheduler_scheduleonetime(delaySeconds int32, payload uint64, scheduleID uint64) uint64
|
||||
|
||||
//go:wasmimport extism:host/user scheduler_schedulerecurring
|
||||
func scheduler_schedulerecurring(cronExpression uint64, payload uint64, scheduleID uint64) uint64
|
||||
|
||||
//go:wasmimport extism:host/user scheduler_cancelschedule
|
||||
func scheduler_cancelschedule(scheduleID uint64) uint64
|
||||
|
||||
// SchedulerScheduleOneTimeResponse is the response type for Scheduler.ScheduleOneTime
|
||||
type SchedulerScheduleOneTimeResponse struct {
|
||||
NewScheduleID string `json:"newScheduleID,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulerScheduleRecurringResponse is the response type for Scheduler.ScheduleRecurring
|
||||
type SchedulerScheduleRecurringResponse struct {
|
||||
NewScheduleID string `json:"newScheduleID,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulerScheduleOneTime schedules a one-time task to run after delaySeconds.
|
||||
func SchedulerScheduleOneTime(delaySeconds int32, payload, scheduleID string) (string, error) {
|
||||
payloadMem := pdk.AllocateString(payload)
|
||||
defer payloadMem.Free()
|
||||
scheduleIDMem := pdk.AllocateString(scheduleID)
|
||||
defer scheduleIDMem.Free()
|
||||
|
||||
responsePtr := scheduler_scheduleonetime(delaySeconds, payloadMem.Offset(), scheduleIDMem.Offset())
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
var resp SchedulerScheduleOneTimeResponse
|
||||
if err := json.Unmarshal(responseBytes, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
return "", errors.New(resp.Error)
|
||||
}
|
||||
|
||||
return resp.NewScheduleID, nil
|
||||
}
|
||||
|
||||
// SchedulerScheduleRecurring schedules a recurring task using a cron expression.
|
||||
func SchedulerScheduleRecurring(cronExpression, payload, scheduleID string) (string, error) {
|
||||
cronMem := pdk.AllocateString(cronExpression)
|
||||
defer cronMem.Free()
|
||||
payloadMem := pdk.AllocateString(payload)
|
||||
defer payloadMem.Free()
|
||||
scheduleIDMem := pdk.AllocateString(scheduleID)
|
||||
defer scheduleIDMem.Free()
|
||||
|
||||
responsePtr := scheduler_schedulerecurring(cronMem.Offset(), payloadMem.Offset(), scheduleIDMem.Offset())
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
var resp SchedulerScheduleRecurringResponse
|
||||
if err := json.Unmarshal(responseBytes, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
return "", errors.New(resp.Error)
|
||||
}
|
||||
|
||||
return resp.NewScheduleID, nil
|
||||
}
|
||||
|
||||
// SchedulerCancelSchedule cancels a scheduled task.
|
||||
func SchedulerCancelSchedule(scheduleID string) error {
|
||||
scheduleIDMem := pdk.AllocateString(scheduleID)
|
||||
defer scheduleIDMem.Free()
|
||||
|
||||
responsePtr := scheduler_cancelschedule(scheduleIDMem.Offset())
|
||||
if responsePtr != 0 {
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
errStr := string(responseMem.ReadBytes())
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
185
plugins/examples/crypto-ticker/pdk.gen.go
Executable file
185
plugins/examples/crypto-ticker/pdk.gen.go
Executable file
@ -0,0 +1,185 @@
|
||||
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
|
||||
package main
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
//export nd_websocket_on_binary_message
|
||||
func _NdWebsocketOnBinaryMessage() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: getting JSON input")
|
||||
var input OnBinaryMessageInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: calling implementation function")
|
||||
output, err := NdWebsocketOnBinaryMessage(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_websocket_on_close
|
||||
func _NdWebsocketOnClose() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: getting JSON input")
|
||||
var input OnCloseInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: calling implementation function")
|
||||
output, err := NdWebsocketOnClose(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_websocket_on_error
|
||||
func _NdWebsocketOnError() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: getting JSON input")
|
||||
var input OnErrorInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: calling implementation function")
|
||||
output, err := NdWebsocketOnError(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_websocket_on_text_message
|
||||
func _NdWebsocketOnTextMessage() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: getting JSON input")
|
||||
var input OnTextMessageInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: calling implementation function")
|
||||
output, err := NdWebsocketOnTextMessage(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Input provided when a binary message is received
|
||||
type OnBinaryMessageInput struct {
|
||||
// The unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionId string `json:"connection_id"`
|
||||
// The binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Output from the binary message handler
|
||||
type OnBinaryMessageOutput struct {
|
||||
// Error message if the callback failed. Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Input provided when a WebSocket connection is closed
|
||||
type OnCloseInput struct {
|
||||
// The WebSocket close status code (e.g., 1000 for normal closure,
|
||||
// 1001 for going away, 1006 for abnormal closure).
|
||||
Code int32 `json:"code"`
|
||||
// The unique identifier for the WebSocket connection that was closed.
|
||||
ConnectionId string `json:"connection_id"`
|
||||
// The human-readable reason for the connection closure, if provided.
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// Output from the close handler
|
||||
type OnCloseOutput struct {
|
||||
// Error message if the callback failed. Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Input provided when an error occurs on a WebSocket connection
|
||||
type OnErrorInput struct {
|
||||
// The unique identifier for the WebSocket connection where the error occurred.
|
||||
ConnectionId string `json:"connection_id"`
|
||||
// The error message describing what went wrong.
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Output from the error handler
|
||||
type OnErrorOutput struct {
|
||||
// Error message if the callback failed. Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Input provided when a text message is received
|
||||
type OnTextMessageInput struct {
|
||||
// The unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionId string `json:"connection_id"`
|
||||
// The text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Output from the text message handler
|
||||
type OnTextMessageOutput struct {
|
||||
// Error message if the callback failed. Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
@ -451,6 +451,10 @@ func (m *Manager) loadPlugin(name, wasmPath string) error {
|
||||
closers: closers,
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Call plugin init function if the plugin has the Lifecycle capability
|
||||
callPluginInit(m.ctx, m.plugins[name])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
41
plugins/schemas/lifecycle.yaml
Normal file
41
plugins/schemas/lifecycle.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
version: v1-draft
|
||||
|
||||
exports:
|
||||
nd_on_init:
|
||||
description: |
|
||||
Called after a plugin is fully loaded with all services registered.
|
||||
Plugins can use this function to perform one-time initialization tasks,
|
||||
such as establishing connections, starting background processes, or
|
||||
validating configuration.
|
||||
|
||||
This function is called once when the plugin is loaded, and is NOT
|
||||
called when the plugin is hot-reloaded. Plugins should not assume
|
||||
this function will be called on every startup.
|
||||
|
||||
The input is currently empty (reserved for future use). The output
|
||||
can contain an error string if initialization failed, which will be
|
||||
logged but will not prevent the plugin from being loaded.
|
||||
input:
|
||||
$ref: "#/components/schemas/OnInitInput"
|
||||
contentType: application/json
|
||||
output:
|
||||
$ref: "#/components/schemas/OnInitOutput"
|
||||
contentType: application/json
|
||||
|
||||
components:
|
||||
schemas:
|
||||
OnInitInput:
|
||||
description: Input provided to the init callback (currently empty, reserved for future use)
|
||||
type: object
|
||||
properties: {}
|
||||
|
||||
OnInitOutput:
|
||||
description: Output from the init callback
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |
|
||||
Error message if initialization failed. Empty or null indicates success.
|
||||
The error is logged but does not prevent the plugin from being loaded.
|
||||
@ -68,7 +68,13 @@ components:
|
||||
|
||||
OnTextMessageOutput:
|
||||
description: Output from the text message handler
|
||||
properties: {}
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |
|
||||
Error message if the callback failed. Empty or null indicates success.
|
||||
|
||||
OnBinaryMessageInput:
|
||||
description: Input provided when a binary message is received
|
||||
@ -88,7 +94,13 @@ components:
|
||||
|
||||
OnBinaryMessageOutput:
|
||||
description: Output from the binary message handler
|
||||
properties: {}
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |
|
||||
Error message if the callback failed. Empty or null indicates success.
|
||||
|
||||
OnErrorInput:
|
||||
description: Input provided when an error occurs on a WebSocket connection
|
||||
@ -107,7 +119,13 @@ components:
|
||||
|
||||
OnErrorOutput:
|
||||
description: Output from the error handler
|
||||
properties: {}
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |
|
||||
Error message if the callback failed. Empty or null indicates success.
|
||||
|
||||
OnCloseInput:
|
||||
description: Input provided when a WebSocket connection is closed
|
||||
@ -133,4 +151,10 @@ components:
|
||||
|
||||
OnCloseOutput:
|
||||
description: Output from the close handler
|
||||
properties: {}
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |
|
||||
Error message if the callback failed. Empty or null indicates success.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user