docs: Add Elixir SDK README with comprehensive API documentation and implement modular architecture with GenServer support

- Add README.md with installation instructions, architecture overview, and API examples for all SDK modules
- Refactor TelecomSDK main module to use modular API clients (SubscribersAPI, UsageAPI, PaymentAPI, RatingPlanAPI, SystemAPI, GraphQLAPI)
- Add AuthProvider class with API key and JWT token support (generate_jwt_token, validate_jwt_token methods)
- Add HTTPClient base
This commit is contained in:
nutcas3
2026-04-25 04:55:06 +03:00
parent 808afbc2be
commit cfdbe92477
10 changed files with 668 additions and 326 deletions
+59 -326
View File
@@ -1,7 +1,7 @@
defmodule TelecomSDK do
@moduledoc """
Telecom Platform Elixir SDK
Provides GenServer-based async access to the Telecom Platform API with full Elixir pattern matching.
"""
@@ -19,363 +19,96 @@ defmodule TelecomSDK do
end
@doc """
Get subscriber by ID
Generate a JWT token for authentication
"""
def get_subscriber(id) do
GenServer.call(__MODULE__, {:get_subscriber, id})
def generate_jwt_token(user_id, expiry_hours \\ 24, additional_claims \\ %{}) do
GenServer.call(__MODULE__, {:generate_jwt_token, user_id, expiry_hours, additional_claims})
end
@doc """
List subscribers with pagination
Validate a JWT token
"""
def list_subscribers(page \\ 1, page_size \\ 50, status \\ nil) do
GenServer.call(__MODULE__, {:list_subscribers, page, page_size, status})
def validate_jwt_token(token) do
GenServer.call(__MODULE__, {:validate_jwt_token, token})
end
@doc """
Create a new subscriber
Close the SDK and cleanup resources
"""
def create_subscriber(request) do
GenServer.call(__MODULE__, {:create_subscriber, request})
end
@doc """
Update an existing subscriber
"""
def update_subscriber(id, request) do
GenServer.call(__MODULE__, {:update_subscriber, id, request})
end
@doc """
Suspend a subscriber
"""
def suspend_subscriber(id) do
GenServer.call(__MODULE__, {:suspend_subscriber, id})
end
@doc """
Activate a suspended subscriber
"""
def activate_subscriber(id) do
GenServer.call(__MODULE__, {:activate_subscriber, id})
end
@doc """
Terminate a subscriber
"""
def terminate_subscriber(id) do
GenServer.call(__MODULE__, {:terminate_subscriber, id})
end
@doc """
Get usage statistics for a subscriber
"""
def get_usage_stats(subscriber_id, start_date, end_date) do
GenServer.call(__MODULE__, {:get_usage_stats, subscriber_id, start_date, end_date})
end
@doc """
List usage events with filtering
"""
def list_usage_events(opts \\ []) do
GenServer.call(__MODULE__, {:list_usage_events, opts})
end
@doc """
Get real-time usage for a subscriber
"""
def get_real_time_usage(subscriber_id) do
GenServer.call(__MODULE__, {:get_real_time_usage, subscriber_id})
end
@doc """
Create a payment transaction
"""
def create_payment_transaction(request) do
GenServer.call(__MODULE__, {:create_payment_transaction, request})
end
@doc """
Get payment transaction by ID
"""
def get_payment_transaction(transaction_id) do
GenServer.call(__MODULE__, {:get_payment_transaction, transaction_id})
end
@doc """
List payment transactions with filtering
"""
def list_payment_transactions(opts \\ []) do
GenServer.call(__MODULE__, {:list_payment_transactions, opts})
end
@doc """
List all available rating plans
"""
def list_rating_plans do
GenServer.call(__MODULE__, :list_rating_plans)
end
@doc """
Get rating plan by ID
"""
def get_rating_plan(plan_id) do
GenServer.call(__MODULE__, {:get_rating_plan, plan_id})
end
@doc """
Get system statistics
"""
def get_system_stats do
GenServer.call(__MODULE__, :get_system_stats)
end
@doc """
Get system health status
"""
def get_health_status do
GenServer.call(__MODULE__, :get_health_status)
end
@doc """
Execute a GraphQL query
"""
def execute_graphql_query(query, variables \\ %{}) do
GenServer.call(__MODULE__, {:execute_graphql_query, query, variables})
end
@doc """
Connect to WebSocket for real-time updates
"""
def connect_websocket(message_handler \\ nil) do
GenServer.call(__MODULE__, {:connect_websocket, message_handler})
def close do
GenServer.call(__MODULE__, :close)
end
# GenServer Callbacks
@impl true
def init(config) do
Finch.start_link(name: MyFinch)
{:ok, %{config: config, finch: MyFinch}}
end
auth_provider = AuthProvider.new(api_key: config.api_key, jwt_secret: config.jwt_secret)
http_client = HTTPClient.new(config.api_url, auth_provider,
timeout: config.timeout,
max_retries: config.max_retries,
retry_delay: config.retry_delay,
enable_logging: config.enable_logging
)
@impl true
def handle_call({:get_subscriber, id}, _from, state) do
case make_request(:get, "/v1/subscribers/#{id}", nil, state) do
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
# Initialize API modules
subscribers = SubscriberAPI.new(http_client)
usage = UsageAPI.new(http_client)
payments = PaymentAPI.new(http_client)
rating_plans = RatingPlanAPI.new(http_client)
system = SystemAPI.new(http_client)
graphql = GraphQLAPI.new(http_client)
@impl true
def handle_call({:list_subscribers, page, page_size, status}, _from, state) do
params = %{page: page, page_size: page_size}
params = if status, do: Map.put(params, :status, status), else: params
case make_request(:get, "/v1/subscribers", params, state) do
{:ok, response} -> {:reply, {:ok, SubscriberList.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:create_subscriber, request}, _from, state) do
case make_request(:post, "/v1/subscribers", request.to_map(), state) do
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:update_subscriber, id, request}, _from, state) do
case make_request(:put, "/v1/subscribers/#{id}", request.to_map(), state) do
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:suspend_subscriber, id}, _from, state) do
case make_request(:post, "/v1/subscribers/#{id}/suspend", nil, state) do
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:activate_subscriber, id}, _from, state) do
case make_request(:post, "/v1/subscribers/#{id}/activate", nil, state) do
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:terminate_subscriber, id}, _from, state) do
case make_request(:delete, "/v1/subscribers/#{id}", nil, state) do
{:ok, response} -> {:reply, {:ok, response["success"]}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:get_usage_stats, subscriber_id, start_date, end_date}, _from, state) do
params = %{
start_date: DateTime.to_iso8601(start_date),
end_date: DateTime.to_iso8601(end_date)
state = %{
config: config,
auth_provider: auth_provider,
http_client: http_client,
subscribers: subscribers,
usage: usage,
payments: payments,
rating_plans: rating_plans,
system: system,
graphql: graphql
}
case make_request(:get, "/v1/subscribers/#{subscriber_id}/usage", params, state) do
{:ok, response} -> {:reply, {:ok, UsageStats.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
{:ok, state}
end
@impl true
def handle_call({:list_usage_events, opts}, _from, state) do
params = build_params(opts, [:subscriber_id, :usage_type, :start_date, :end_date, :page, :page_size])
case make_request(:get, "/v1/usage/events", params, state) do
{:ok, response} -> {:reply, {:ok, UsageEventList.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
def handle_call({:generate_jwt_token, user_id, expiry_hours, additional_claims}, _from, state) do
auth_provider = AuthProvider.generate_jwt_token(state.auth_provider, user_id, expiry_hours, additional_claims)
{:reply, {:ok, auth_provider.token_cache}, %{state | auth_provider: auth_provider}}
end
@impl true
def handle_call({:get_real_time_usage, subscriber_id}, _from, state) do
case make_request(:get, "/v1/subscribers/#{subscriber_id}/realtime", nil, state) do
{:ok, response} -> {:reply, {:ok, RealTimeUsage.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
def handle_call({:validate_jwt_token, token}, _from, state) do
result = AuthProvider.validate_jwt_token(state.auth_provider, token)
{:reply, {:ok, result}, state}
end
@impl true
def handle_call({:create_payment_transaction, request}, _from, state) do
case make_request(:post, "/v1/payments/transactions", request.to_map(), state) do
{:ok, response} -> {:reply, {:ok, PaymentTransaction.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
def handle_call(:close, _from, state) do
HTTPClient.close(state.http_client)
{:reply, :ok, state}
end
@impl true
def handle_call({:get_payment_transaction, transaction_id}, _from, state) do
case make_request(:get, "/v1/payments/transactions/#{transaction_id}", nil, state) do
{:ok, response} -> {:reply, {:ok, PaymentTransaction.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
# Configuration
@impl true
def handle_call({:list_payment_transactions, opts}, _from, state) do
params = build_params(opts, [:subscriber_id, :status, :page, :page_size])
case make_request(:get, "/v1/payments/transactions", params, state) do
{:ok, response} -> {:reply, {:ok, PaymentTransactionList.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
defstruct [:api_url, :api_key, :jwt_secret, :timeout, :max_retries, :retry_delay, :enable_logging]
@impl true
def handle_call(:list_rating_plans, _from, state) do
case make_request(:get, "/v1/rating-plans", nil, state) do
{:ok, response} ->
plans = Enum.map(response, &RatingPlan.from_map/1)
{:reply, {:ok, plans}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
defmodule Config do
@moduledoc """
Configuration for Telecom SDK
"""
@impl true
def handle_call({:get_rating_plan, plan_id}, _from, state) do
case make_request(:get, "/v1/rating-plans/#{plan_id}", nil, state) do
{:ok, response} -> {:reply, {:ok, RatingPlan.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call(:get_system_stats, _from, state) do
case make_request(:get, "/v1/system/stats", nil, state) do
{:ok, response} -> {:reply, {:ok, SystemStats.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call(:get_health_status, _from, state) do
case make_request(:get, "/v1/health", nil, state) do
{:ok, response} -> {:reply, {:ok, HealthStatus.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:execute_graphql_query, query, variables}, _from, state) do
request = %{query: query, variables: variables}
case make_request(:post, "/graphql", request, state) do
{:ok, response} -> {:reply, {:ok, GraphQLResponse.from_map(response)}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
@impl true
def handle_call({:connect_websocket, message_handler}, _from, state) do
ws_url = String.replace(state.config.api_url, "http://", "ws://") <> "/ws"
case WebSockex.start_link(ws_url, WebSocketHandler, %{message_handler: message_handler}) do
{:ok, pid} -> {:reply, {:ok, pid}, state}
{:error, error} -> {:reply, {:error, error}, state}
end
end
# Private functions
defp make_request(method, path, params, state) do
url = state.config.api_url <> path
headers = [
{"user-agent", "Telecom-Elixir-SDK/1.0.0"},
{"content-type", "application/json"}
defstruct [
api_url: "http://localhost:8000",
api_key: nil,
jwt_secret: nil,
timeout: 30_000,
max_retries: 3,
retry_delay: 1_000,
enable_logging: false
]
headers = if state.config.api_key do
[{"authorization", "Bearer #{state.config.api_key}"} | headers]
else
headers
end
request = Finch.build(method, url, headers, Jason.encode!(params || %{}))
case Finch.request(request, state.finch) do
{:ok, response} ->
case response.status do
200 ->
body = response.body |> Jason.decode!()
{:ok, body}
401 ->
{:error, :authentication_error}
429 ->
{:error, :rate_limit_error}
status when status in 400..499 ->
body = response.body |> Jason.decode!()
{:error, {:api_error, body["error"] || "Bad request"}}
status when status in 500..599 ->
{:error, {:server_error, status}}
_ ->
{:error, :unknown_error}
end
{:error, reason} ->
{:error, {:network_error, reason}}
end
end
defp build_params(opts, keys) do
Enum.reduce(keys, %{}, fn key, acc ->
case Map.get(opts, key) do
nil -> acc
value -> Map.put(acc, key, value)
end
end)
end
end
+104
View File
@@ -0,0 +1,104 @@
defmodule TelecomSDK.AuthProvider do
@moduledoc """
Authentication provider for the Telecom SDK
"""
defstruct [:api_key, :jwt_secret, :token_cache, :token_expiry]
def new(opts \\ []) do
%__MODULE__{
api_key: Keyword.get(opts, :api_key),
jwt_secret: Keyword.get(opts, :jwt_secret),
token_cache: nil,
token_expiry: nil
}
end
def get_headers(%__MODULE__{api_key: api_key, token_cache: token_cache, token_expiry: token_expiry}) do
headers = %{
"Content-Type" => "application/json",
"User-Agent" => "Telecom-Elixir-SDK/1.0.0"
}
headers = if api_key, do: Map.put(headers, "X-API-Key", api_key), else: headers
if token_cache && token_valid?(token_expiry) do
Map.put(headers, "Authorization", "Bearer #{token_cache}")
else
headers
end
end
def generate_jwt_token(%__MODULE__{jwt_secret: jwt_secret} = auth, user_id, expiry_hours \\ 24, additional_claims \\ %{}) do
unless jwt_secret, do: raise(ArgumentError, "JWT secret not configured")
now = System.system_time(:second)
exp = now + (expiry_hours * 3600)
claims = %{
"sub" => user_id,
"exp" => exp,
"iat" => now
}
|> Map.merge(additional_claims)
header = %{"alg" => "HS256", "typ" => "JWT"}
encoded_header = base64_url_encode(header)
encoded_payload = base64_url_encode(claims)
signature = sign("#{encoded_header}.#{encoded_payload}", jwt_secret)
token = "#{encoded_header}.#{encoded_payload}.#{signature}"
%{auth | token_cache: token, token_expiry: exp}
end
def validate_jwt_token(%__MODULE__{jwt_secret: jwt_secret}, token) do
unless jwt_secret, do: raise(ArgumentError, "JWT secret not configured")
parts = String.split(token, ".")
unless length(parts) == 3, do: raise(ArgumentError, "Invalid token format")
[encoded_header, encoded_payload, signature] = parts
expected_signature = sign("#{encoded_header}.#{encoded_payload}", jwt_secret)
unless signature == expected_signature, do: raise(ArgumentError, "Invalid token signature")
case base64_url_decode(encoded_payload) do
{:ok, payload_bytes} ->
case Jason.decode(payload_bytes) do
{:ok, payload} ->
if payload["exp"] && payload["exp"] < System.system_time(:second) do
raise(ArgumentError, "Token has expired")
end
payload
{:error, _} -> raise(ArgumentError, "Failed to decode payload")
end
{:error, _} -> raise(ArgumentError, "Failed to decode payload")
end
end
def clear_token_cache(auth) do
%{auth | token_cache: nil, token_expiry: nil}
end
defp token_valid?(nil), do: false
defp token_valid?(expiry), do: System.system_time(:second) < expiry
defp base64_url_encode(data) do
json = Jason.encode!(data)
json
|> Base.url_encode64(padding: false)
end
defp base64_url_decode(data) do
# Add padding if needed
padded_data = data <> String.duplicate("=", rem(4 - rem(String.length(data), 4), 4))
Base.url_decode64(padded_data)
end
defp sign(data, secret) do
:crypto.mac(:hmac, :sha256, secret, data)
|> Base.url_encode64(padding: false)
end
end
+17
View File
@@ -0,0 +1,17 @@
defmodule TelecomSDK.GraphQLAPI do
@moduledoc """
API for GraphQL queries
"""
defstruct [:client]
def new(client) do
%__MODULE__{client: client}
end
def execute(api, query, variables \\ nil) do
request = %{query: query}
request = if variables, do: Map.put(request, :variables, variables), else: request
TelecomSDK.HTTPClient.post(api.client, "/graphql", request)
end
end
+84
View File
@@ -0,0 +1,84 @@
defmodule TelecomSDK.HTTPClient do
@moduledoc """
HTTP client for making API requests
"""
defstruct [:base_url, :auth_provider, :timeout, :max_retries, :retry_delay, :enable_logging]
def new(base_url, auth_provider, opts \\ []) do
%__MODULE__{
base_url: base_url,
auth_provider: auth_provider,
timeout: Keyword.get(opts, :timeout, 30_000),
max_retries: Keyword.get(opts, :max_retries, 3),
retry_delay: Keyword.get(opts, :retry_delay, 1_000),
enable_logging: Keyword.get(opts, :enable_logging, false)
}
end
def get(client, path, params \\ %{}) do
request(client, "GET", path, nil, params)
end
def post(client, path, body \\ nil) do
request(client, "POST", path, body, nil)
end
def put(client, path, body \\ nil) do
request(client, "PUT", path, body, nil)
end
def delete(client, path) do
request(client, "DELETE", path, nil, nil)
end
defp request(client, method, path, body, params) do
url = client.base_url <> path
headers = TelecomSDK.AuthProvider.get_headers(client.auth_provider)
opts = [
timeout: client.timeout,
recv_timeout: client.timeout
]
opts = if params != %{}, do: Keyword.put(opts, :params, params), else: opts
opts = if body, do: Keyword.put(opts, :body, Jason.encode!(body)), else: opts
last_error = nil
Enum.reduce_while(0..client.max_retries, nil, fn attempt, _acc ->
case do_request(method, url, headers, opts) do
{:ok, response} ->
case handle_response_errors(response) do
:ok -> {:halt, {:ok, Jason.decode!(response.body)}}
{:error, error} -> {:halt, {:error, error}}
end
{:error, error} ->
if attempt < client.max_retries do
Process.sleep(client.retry_delay * round(:math.pow(2, attempt)))
{:cont, error}
else
{:halt, {:error, error}}
end
end
end)
end
defp do_request(method, url, headers, opts) do
case HTTPoison.request(method, url, Keyword.get(opts, :body), headers, opts) do
{:ok, response} -> {:ok, response}
{:error, error} -> {:error, error}
end
end
defp handle_response_errors(%{status_code: status}) when status >= 200 and status < 300, do: :ok
defp handle_response_errors(%{status_code: 401}), do: {:error, :authentication_failed}
defp handle_response_errors(%{status_code: 429}), do: {:error, :rate_limit_exceeded}
defp handle_response_errors(%{status_code: status}) when status >= 400 and status < 500, do: {:error, {:client_error, status}}
defp handle_response_errors(%{status_code: status}) when status >= 500, do: {:error, {:server_error, status}}
def close(_client) do
# HTTPoison doesn't need explicit cleanup
:ok
end
end
@@ -0,0 +1,31 @@
defmodule TelecomSDK.PaymentAPI do
@moduledoc """
API for payment management
"""
defstruct [:client]
def new(client) do
%__MODULE__{client: client}
end
def create_transaction(api, request) do
TelecomSDK.HTTPClient.post(api.client, "/v1/payments/transactions", request)
end
def get_transaction(api, transaction_id) do
TelecomSDK.HTTPClient.get(api.client, "/v1/payments/transactions/#{transaction_id}")
end
def list_transactions(api, opts \\ []) do
params = %{
page: Keyword.get(opts, :page, 1),
page_size: Keyword.get(opts, :page_size, 50)
}
params = if Keyword.get(opts, :subscriber_id), do: Map.put(params, :subscriber_id, Keyword.get(opts, :subscriber_id)), else: params
params = if Keyword.get(opts, :status), do: Map.put(params, :status, Keyword.get(opts, :status)), else: params
TelecomSDK.HTTPClient.get(api.client, "/v1/payments/transactions", params)
end
end
@@ -0,0 +1,19 @@
defmodule TelecomSDK.RatingPlanAPI do
@moduledoc """
API for rating plan management
"""
defstruct [:client]
def new(client) do
%__MODULE__{client: client}
end
def list(api) do
TelecomSDK.HTTPClient.get(api.client, "/v1/rating-plans")
end
def get(api, plan_id) do
TelecomSDK.HTTPClient.get(api.client, "/v1/rating-plans/#{plan_id}")
end
end
@@ -0,0 +1,41 @@
defmodule TelecomSDK.SubscriberAPI do
@moduledoc """
API for subscriber management
"""
defstruct [:client]
def new(client) do
%__MODULE__{client: client}
end
def get(api, id) do
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers/#{id}")
end
def list(api, page \\ 1, page_size \\ 50, status \\ nil) do
params = %{page: page, page_size: page_size}
params = if status, do: Map.put(params, :status, status), else: params
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers", params)
end
def create(api, request) do
TelecomSDK.HTTPClient.post(api.client, "/v1/subscribers", request)
end
def update(api, id, request) do
TelecomSDK.HTTPClient.put(api.client, "/v1/subscribers/#{id}", request)
end
def delete(api, id) do
TelecomSDK.HTTPClient.delete(api.client, "/v1/subscribers/#{id}")
end
def suspend(api, id) do
TelecomSDK.HTTPClient.post(api.client, "/v1/subscribers/#{id}/suspend")
end
def activate(api, id) do
TelecomSDK.HTTPClient.post(api.client, "/v1/subscribers/#{id}/activate")
end
end
+19
View File
@@ -0,0 +1,19 @@
defmodule TelecomSDK.SystemAPI do
@moduledoc """
API for system management
"""
defstruct [:client]
def new(client) do
%__MODULE__{client: client}
end
def get_stats(api) do
TelecomSDK.HTTPClient.get(api.client, "/v1/system/stats")
end
def get_health(api) do
TelecomSDK.HTTPClient.get(api.client, "/v1/health")
end
end
+37
View File
@@ -0,0 +1,37 @@
defmodule TelecomSDK.UsageAPI do
@moduledoc """
API for usage management
"""
defstruct [:client]
def new(client) do
%__MODULE__{client: client}
end
def get_stats(api, subscriber_id, start_date, end_date) do
params = %{
start_date: DateTime.to_iso8601(start_date),
end_date: DateTime.to_iso8601(end_date)
}
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers/#{subscriber_id}/usage", params)
end
def list_events(api, opts \\ []) do
params = %{
page: Keyword.get(opts, :page, 1),
page_size: Keyword.get(opts, :page_size, 50)
}
params = if Keyword.get(opts, :subscriber_id), do: Map.put(params, :subscriber_id, Keyword.get(opts, :subscriber_id)), else: params
params = if Keyword.get(opts, :usage_type), do: Map.put(params, :usage_type, Keyword.get(opts, :usage_type)), else: params
params = if Keyword.get(opts, :start_date), do: Map.put(params, :start_date, DateTime.to_iso8601(Keyword.get(opts, :start_date))), else: params
params = if Keyword.get(opts, :end_date), do: Map.put(params, :end_date, DateTime.to_iso8601(Keyword.get(opts, :end_date))), else: params
TelecomSDK.HTTPClient.get(api.client, "/v1/usage/events", params)
end
def get_realtime(api, subscriber_id) do
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers/#{subscriber_id}/realtime")
end
end