feat(lua-client): 新增 Lua 客户端 SDK 及示例
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user