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:
Deluan 2025-12-25 16:25:19 -05:00
parent d1225b7828
commit 9da40af6fb
10 changed files with 941 additions and 4 deletions

View 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)
}

View 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`.

View File

@ -0,0 +1,5 @@
module crypto-ticker
go 1.22.1
require github.com/extism/go-pdk v1.1.0

View 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=

View 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", &currentFloat)
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() {}

View 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
}

View 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"`
}

View File

@ -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
}

View 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.

View File

@ -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.