Files
cc-platform-core/apis/protocol.lua
2026-03-26 15:00:49 -04:00

119 lines
3.6 KiB
Lua

--[[
platform.protocol — Canonical message envelope for cc-platform-core
Provides a standard message format that is backward-compatible with
existing modem messages. All messages retain the `type` field as the
primary discriminator. New metadata fields (_v, _src, _ts, _id) are
additive — legacy consumers ignore them.
Usage:
local Protocol = require('platform.protocol')
Protocol.setServiceId('inventory-manager')
-- Create a well-formed message
local msg = Protocol.message('state', { items = {...}, total = 42 })
-- => { type='state', items={...}, total=42, _v=1, _src='inventory-manager', _ts=... }
-- Parse incoming message (works with both legacy and versioned)
local parsed = Protocol.parse(msg)
-- => { type='state', version=1, source='inventory-manager', timestamp=..., data=msg }
-- Quick type check
if Protocol.is(msg, 'status') then ... end
]]
local os = _G.os
local Protocol = {
VERSION = 1,
serviceId = nil,
}
--- Set the service identity for this process.
-- All messages created after this call will include the service ID.
-- @param id string Service identifier (e.g. 'remoteturtle', 'inventory-manager')
function Protocol.setServiceId(id)
Protocol.serviceId = id
end
--- Create a well-formed platform message.
-- Merges data fields into a new table and adds protocol metadata.
-- The `type` field is always set from the first argument.
-- @param msgType string Message type discriminator
-- @param data table|nil Service-specific payload fields (merged into message)
-- @param opts table|nil Options: { src=string, id=string, noMeta=bool }
-- @return table The constructed message
function Protocol.message(msgType, data, opts)
opts = opts or {}
local msg = {}
-- Merge data fields into the message (flat, not nested)
if data then
for k, v in pairs(data) do
msg[k] = v
end
end
-- Primary discriminator
msg.type = msgType
-- Protocol metadata (additive — legacy consumers ignore these)
if not opts.noMeta then
msg._v = Protocol.VERSION
msg._src = opts.src or Protocol.serviceId
msg._ts = os.epoch and os.epoch('utc') or math.floor(os.clock() * 1000)
if opts.id then
msg._id = opts.id
end
end
return msg
end
--- Parse a received message, normalizing both legacy and versioned formats.
-- Returns nil if the input is not a valid message table.
-- @param msg table Raw message from modem or WebSocket
-- @return table|nil Normalized message descriptor
function Protocol.parse(msg)
if type(msg) ~= 'table' then
return nil
end
if not msg.type then
return nil
end
return {
type = msg.type,
version = msg._v or 0,
source = msg._src,
timestamp = msg._ts,
correlationId = msg._id,
data = msg, -- full original message for backward-compat field access
}
end
--- Check whether a message matches a given type.
-- Works on both raw messages and parsed descriptors.
-- @param msg table Message or parsed descriptor
-- @param msgType string Expected type
-- @return boolean
function Protocol.is(msg, msgType)
if type(msg) ~= 'table' then
return false
end
return msg.type == msgType
end
--- Generate a correlation ID for request/response patterns.
-- Uses a combination of computer ID, clock, and random to ensure uniqueness.
-- @return string Unique correlation ID
function Protocol.correlationId()
local id = os.getComputerID and os.getComputerID() or 0
local clock = os.epoch and os.epoch('utc') or os.clock()
local rand = math.random(0, 0xFFFF)
return string.format('%d-%d-%04x', id, clock, rand)
end
return Protocol