The Signal K server Resource API provides a common set operations for clients to interact with routes, waypoints, charts, etc but it does NOT provide the ability to persist or retrieve resources to / from storage.
This functionality needs to be provided by one or more server plugins that interface with the Resource API to facilitate the storage and retrieval of resource data.
These plugins are called Provider Plugins.
Resource API architecture:
This de-coupling of request handling and data storage provides the flexibility to persist resource data in a variety of different storage types as well as Internet based services.
Signal K server comes with the resources-provider-plugin pre-installed which persists resource data to the local file system.
The Resources API handles all client requests received via the /signalk/v2/api/resources path, before passing on the request to registered provider plugin(s).
The Resources API performs the following operations when a request is received:
POST and PUT requests for Standard (Signal K defined) resource types are checked for validity of the submitted:
resource idresource data against the OpenAPI definition.Only after successful completion of all these operations is the request passed on to the registered provider plugin(s).
A resource provider plugin is a Signal K server plugin that implements the ResourceProvider interface which:
Multiple providers can be registered for a resource type (e.g. 2 x chart providers)
Note: The Resource Provider is responsible for implementing the methods and returning data in the required format!
To register a plugin as a provider for one or more resource types with the SignalK server, it must call the server's registerResourceProvider function for each resource type being serviced during plugin startup.
Example: Plugin registering as a routes & waypoints provider.
import { ResourceProvider } from '@signalk/server-api'
module.exports = function (app) {
const plugin = {
id: 'mypluginid',
name: 'My Resource Provider plugin'
}
const routesProvider: ResourceProvider = {
type: 'routes',
methods: {
listResources: (params) => {
fetchRoutes(params)
...
},
getResource: (id, property?) => {
getRoute(id, property)
...
},
setResource: (id, value )=> {
saveRoute(id, value)
...
},
deleteResource: (id) => {
deleteRoute(id, value)
...
}
}
}
const waypointsProvider: ResourceProvider = {
type: 'waypoints',
methods: {
listResources: (params) => {
fetchWaypoints(params)
...
},
getResource: (id, property?) => {
getWaypoint(id, property)
...
},
setResource: (id, value )=> {
saveWaypoint(id, value)
...
},
deleteResource: (id) => {
deleteWaypoint(id, value)
...
}
}
}
plugin.start = function(options) {
...
try {
app.registerResourceProvider(routesProvider)
app.registerResourceProvider(waypointsProvider)
}
catch (error) {
// handle error
}
}
return plugin
}
A Resource Provider plugin must implement ALL methods in ResourceProviderMethods to service the requests passed from the server.
Each method should return a Promise on success and throw on error, if a request is not serviced or is not implemented.
Example:
// SignalK server plugin
module.exports = function (app) {
const plugin = {
id: 'mypluginid',
name: 'My Resource Providerplugin',
start: options => {
...
app.registerResourceProvider({
type: 'waypoints',
methods: {
listResources: (params) => {
return new Promise( (resolve, reject) => {
...
if (ok) {
resolve(resource_list)
} else {
reject( new Error('Error fetching resources!'))
}
})
},
getResource: (id, property?) => {
return new Promise( (resolve, reject) => {
...
if (ok) {
resolve(resource_list)
} else {
reject( new Error('Error fetching resource with supplied id!'))
}
})
},
setResource: (id, value )=> {
throw( new Error('Not implemented!'))
},
deleteResource: (id) => {
throw( new Error('Not implemented!'))
}
}
})
}
}
}
While the built-in Resources API automatically emits deltas for standard operations (POST, PUT, DELETE), custom provider endpoints must manually emit deltas when resources are modified through custom endpoints to keep clients synchronized in real-time.
Emit delta notifications after:
Resource deltas use the standard Signal K delta format with the resource path. Target version 2 data structure.
app.handleMessage(
'my-provider-plugin-id',
{
updates: [
{
values: [
{
path: 'resources.<resourceType>.<resourceId>',
value: resourceData // or null for deletions
}
]
}
]
},
2 // Signal K v2 - resources should not be in full model cache
)
This example shows a chart provider plugin that emits deltas for all operations:
module.exports = function (app) {
let chartCache = {}
const plugin = {
id: 'my-charts-provider',
name: 'My Charts Provider',
start: (options) => {
// Register as resource provider
app.registerResourceProvider({
type: 'charts',
methods: {
listResources: () => Promise.resolve(chartCache),
getResource: (id) => {
if (chartCache[id]) {
return Promise.resolve(chartCache[id])
}
throw new Error('Chart not found')
},
setResource: (id, value) => {
throw new Error('Not implemented')
},
deleteResource: (id) => {
throw new Error('Not implemented')
}
}
})
// Register custom endpoints
registerCustomEndpoints()
// Initial load
refreshCharts()
},
registerWithRouter: (router) => {
router.post('/charts/upload', async (req, res) => {
try {
const chartId = await saveUploadedChart(req)
await refreshCharts()
if (chartCache[chartId]) {
emitChartDelta(chartId, chartCache[chartId])
}
res.json({ success: true, id: chartId })
} catch (error) {
res.status(500).json({ error: error.message })
}
})
// Delete endpoint
router.delete('/charts/:id', async (req, res) => {
try {
await deleteChartFromDisk(req.params.id)
await refreshCharts()
emitChartDelta(req.params.id, null)
res.send('Chart deleted successfully')
} catch (error) {
res.status(500).send(error.message)
}
})
}
}
const emitChartDelta = (chartId, chartValue) => {
try {
app.handleMessage(
plugin.id,
{
updates: [
{
values: [
{
path: `resources.charts.${chartId}`,
value: chartValue
}
]
}
]
},
2 // Signal K v2 - resources should not be in full model cache
)
app.debug(`Delta emitted for chart: ${chartId}`)
} catch (error) {
app.error(`Failed to emit delta: ${error.message}`)
}
}
const refreshCharts = async () => {
try {
const charts = await loadChartsFromDisk()
chartCache = charts
app.debug(`Charts refreshed: ${Object.keys(chartCache).length} charts`)
} catch (error) {
app.error(`Failed to refresh charts: ${error.message}`)
}
}
return plugin
}
Clients can subscribe to resource changes via WebSocket:
{
"context": "resources.*",
"subscribe": [
{
"path": "charts.*",
"policy": "instant"
}
]
}
When resources change, clients receive delta messages:
{
"context": "resources",
"updates": [{
"values": [{
"path": "charts.myChart",
"value": {
"name": "My Chart",
"description": "Chart description",
...
}
}]
}]
}
For deletions, value is null:
{
"context": "resources",
"updates": [
{
"values": [
{
"path": "charts.myChart",
"value": null
}
]
}
]
}
The signalk-charts-provider-simple plugin provides a complete working example of this pattern.