feat(lua-client): 新增 Lua 客户端 SDK 及示例

This commit is contained in:
2026-07-20 21:18:35 +08:00
parent 881c9319df
commit 295de72308
11 changed files with 1390 additions and 4 deletions
@@ -0,0 +1,41 @@
local json = require("dkjson")
local Cache = {}
Cache.__index = Cache
local CACHE_FILE = "config-cache.json"
function Cache.new(cache_dir)
local self = setmetatable({}, Cache)
self.cache_dir = cache_dir or "."
return self
end
function Cache:write(data)
local path = self.cache_dir .. "/" .. CACHE_FILE
local file = io.open(path, "w")
if not file then
return false, "cannot open cache file: " .. path
end
local str = json.encode(data, { indent = true })
file:write(str)
file:close()
return true
end
function Cache:read()
local path = self.cache_dir .. "/" .. CACHE_FILE
local file = io.open(path, "r")
if not file then
return nil, "cache file not found"
end
local str = file:read("*a")
file:close()
local data, _, err = json.decode(str)
if err then
return nil, "json decode error: " .. err
end
return data
end
return Cache
@@ -0,0 +1,219 @@
local http = require("socket.http")
local ltn12 = require("ltn12")
local json = require("dkjson")
local Config = require("kenaito_conf.config")
local Cache = require("kenaito_conf.cache")
local Listener = require("kenaito_conf.listener")
local Client = {}
Client.__index = Client
function Client.new(opts)
local config = opts.config
if not config then
error("config is required")
end
local self = setmetatable({}, Client)
self.config = config
self.cache = Cache.new(config.cache_dir)
self.listener = Listener.new()
self.configs = {}
self.last_time = 0
self.started = false
self.stop = false
return self
end
function Client:on_change(handler)
self.listener:on_change(handler)
end
function Client:get(key)
return self.configs[key]
end
function Client:get_all()
local result = {}
for k, v in pairs(self.configs) do
result[k] = v
end
return result
end
function Client:start()
if self.started then
return
end
self.started = true
local ok, err = self:_fetch_all()
if not ok then
print("[ConfigClient] fetch all configs error: " .. tostring(err) .. ", trying cache")
local cached, cache_err = self.cache:read()
if cached then
self:_apply_configs(cached)
local count = 0
for _ in pairs(self.configs) do
count = count + 1
end
print("[ConfigClient] loaded " .. count .. " configs from cache")
else
error("fetch configs failed and no cache: " .. tostring(err))
end
end
end
function Client:stop()
self.stop = true
end
function Client:sync_once()
local ok, err = self:_fetch_changed()
if not ok then
print("[ConfigClient] sync error: " .. tostring(err))
end
end
function Client:_http_post(url, payload)
local request_body = json.encode(payload)
local response_body = {}
local res, code, response_headers = http.request{
url = url,
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Content-Length"] = #request_body
},
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body)
}
if res == nil then
return nil, "HTTP request failed: " .. tostring(code)
end
if code ~= 200 then
return nil, "HTTP " .. code
end
local response_str = table.concat(response_body)
local data, _, err = json.decode(response_str)
if err then
return nil, "JSON decode error: " .. err
end
return data
end
function Client:_fetch_all()
local url = self.config.base_url .. "/api/fetchAllConfigs"
local payload = {
appName = self.config.app_name,
env = self.config.env,
version = self.config.version,
ignoreGlobal = self.config.ignore_global,
token = self.config.token
}
local data, err = self:_http_post(url, payload)
if not data then
return false, err
end
if data.code and data.code ~= 0 then
return false, "server error: " .. (data.msg or "unknown")
end
if data["latest.fetch.timestamp"] then
self.last_time = tonumber(data["latest.fetch.timestamp"])
end
self.cache:write(data)
self:_apply_configs(data)
local count = 0
for _ in pairs(self.configs) do
count = count + 1
end
print("[ConfigClient] fetched " .. count .. " configs")
return true
end
function Client:_fetch_changed()
local url = self.config.base_url .. "/api/fetchChangedConfigs"
local payload = {
appName = self.config.app_name,
env = self.config.env,
version = self.config.version,
lastTime = self.last_time,
ignoreGlobal = self.config.ignore_global,
token = self.config.token
}
local result, err = self:_http_post(url, payload)
if not result then
return false, err
end
if result.code and result.code ~= 0 then
return false, "server error code: " .. result.code
end
local changed_data = result.data or {}
if changed_data["latest.fetch.timestamp"] then
self.last_time = tonumber(changed_data["latest.fetch.timestamp"])
changed_data["latest.fetch.timestamp"] = nil
end
-- 处理删除的配置
if changed_data.deletedKeys and changed_data.deletedKeys ~= "" then
local deleted_keys = {}
for key in string.gmatch(changed_data.deletedKeys, "([^,]+)") do
table.insert(deleted_keys, key)
self.configs[key] = nil
end
print("[ConfigClient] " .. #deleted_keys .. " configs deleted: " .. table.concat(deleted_keys, ", "))
changed_data.deletedKeys = nil
end
-- 处理变更的配置
local has_changes = false
local changed_keys = {}
for k, v in pairs(changed_data) do
if type(v) == "string" then
self.configs[k] = v
table.insert(changed_keys, k)
has_changes = true
end
end
if has_changes then
local changed = {}
for _, k in ipairs(changed_keys) do
changed[k] = self.configs[k]
end
self.listener:notify(changed)
print("[ConfigClient] " .. #changed_keys .. " configs changed: " .. table.concat(changed_keys, ", "))
end
return true
end
function Client:_apply_configs(data)
for k, v in pairs(data) do
-- 跳过元数据
if k:sub(1, 8) == "kenaito." or k == "latest.fetch.timestamp" then
-- skip
elseif type(v) == "string" then
self.configs[k] = v
end
end
end
return Client
@@ -0,0 +1,18 @@
local Config = {}
Config.__index = Config
function Config.new(opts)
opts = opts or {}
local self = setmetatable({}, Config)
self.base_url = opts.base_url or "http://localhost:8080"
self.app_name = opts.app_name or ""
self.env = opts.env or "dev"
self.version = opts.version or "latest"
self.token = opts.token or ""
self.sync_interval = opts.sync_interval or 10
self.ignore_global = opts.ignore_global or false
self.cache_dir = opts.cache_dir or "."
return self
end
return Config
@@ -0,0 +1,8 @@
local Client = require("kenaito_conf.client")
local Config = require("kenaito_conf.config")
return {
Client = Client,
Config = Config,
_VERSION = "1.0.3"
}
@@ -0,0 +1,23 @@
local Listener = {}
Listener.__index = Listener
function Listener.new()
local self = setmetatable({}, Listener)
self.handlers = {}
return self
end
function Listener:on_change(handler)
table.insert(self.handlers, handler)
end
function Listener:notify(changed)
for _, handler in ipairs(self.handlers) do
local ok, err = pcall(handler, changed)
if not ok then
print("[ConfigClient] change handler error: " .. tostring(err))
end
end
end
return Listener