Architecture

The "depends" project uses a directed graph model to manage entities as nodes with dependencies, ensuring no cycles exist. Each node has a state indicating its health, such as "green" (healthy), "yellow" (warning), or "red" (critical). This architecture leverages a SQLite database in Write-Ahead Logging (WAL) mode for concurrent reads and writes, storing relationships and enabling dynamic updates. Namespaces provide isolated environments, while authentication via Bearer tokens ensures secure access. For setup details, see Installation or Deployment.

Core Model

The core model represents entities as nodes in a directed graph, where nodes depend on others via edges. This structure manages dependencies without cycles, with the system checking for cycles during configuration import. Nodes are defined in configuration files and stored in the database with properties such as label, dependencies, default state, and metadata.

Nodes

Nodes are the fundamental entities in the graph, each with a unique ID. Properties include:

Example configuration:

namespace: my-project

nodes:
  database:
    label: Database
    depends_on:
      - api-server
  api-server:
    label: API Server
    default_state: yellow
    meta:
      version: 1.0

In the database, nodes are stored in a table with these properties, along with fields for state management and timestamps. Nodes can be managed through API endpoints, such as:

States

States indicate a node's health, with possible values of "green", "yellow", or "red", following a priority order where "red" is the most severe. A node's effective state is the worst state among itself and its transitive dependencies, computed using a breadth-first search (BFS) traversal.

The computation is handled as follows:

export function computeEffectiveState(
  db: Database,
  nsId: number,
  nodeId: string
): string {
  const node = db
    .query("SELECT state, ttl, last_state_write FROM nodes WHERE ns_id = ? AND id = ?")
    .get(nsId, nodeId) as { state: string; ttl: number | null; last_state_write: string | null } | null;

  if (!node) throw new Error(`Node not found: ${nodeId}`);

  let worst = resolveNodeState(node);
  const visited = new Set<string>();
  const queue = [nodeId];

  while (queue.length > 0) {
    const current = queue.shift()!;
    if (visited.has(current)) continue;
    visited.add(current);

    const deps = db
      .query("SELECT to_node FROM edges WHERE ns_id = ? AND from_node = ?")
      .all(nsId, current) as { to_node: string }[];

    for (const dep of deps) {
      const depNode = db
        .query("SELECT state, ttl, last_state_write FROM nodes WHERE ns_id = ? AND id = ?")
        .get(nsId, dep.to_node) as { state: string; ttl: number | null; last_state_write: string | null } | null;

      if (depNode) {
        worst = worstState(worst, resolveNodeState(depNode));
        if (!visited.has(dep.to_node)) {
          queue.push(dep.to_node);
        }
      }
    }
  }

  return worst;
}

States can be updated via:

API Overview

The API is the primary interface for interacting with the "depends" system, handling operations related to namespaces, nodes, graphs, events, and notifications. It uses RESTful principles, JSON for data exchange, and Bearer tokens for authentication. The verifyToken function ensures users can only access their own data.

Authentication

Tokens are verified against the database to confirm namespace ownership. Include the token in the Authorization header as Bearer .

Example:

export async function verifyToken(
  db: Database,
  namespace: string,
  token: string,
  isLocal: boolean = false
): Promise<AuthResult | null> {
  // Verification logic as described
}

Endpoints

Endpoints are organized by resource type for modularity.

Namespaces

export function handleDeleteNamespace(
  db: Database,
  nsId: number
): Response {
  db.query("DELETE FROM namespaces WHERE ns_id = ?").run(nsId);
  return new Response(null, { status: 204 });
}

Nodes

As detailed in the Core Model section.

Events

Graph

Notifications

Example configuration for a notification rule:

notifications:
  alert-webhook:
    watch: api-server
    on: red
    url: https://example.com/webhook
    secret: mysecret

Usage

For advanced topics, refer to Migration Guide or Log.

Querying the Graph

Graph queries retrieve related nodes, such as upstream and downstream dependencies, using BFS for efficiency. This design minimizes memory usage by querying the database on-the-fly.

The getUpstreamNodes function performs this search:

export function getUpstreamNodes(
  db: Database,
  nsId: number,
  nodeId: string
): string[] {
  const upstream: string[] = [];
  const visited = new Set<string>();
  const queue = [nodeId];

  while (queue.length > 0) {
    const current = queue.shift()!;
    if (visited.has(current)) continue;
    visited.add(current);
    const deps = db
      .query("SELECT to_node FROM edges WHERE ns_id = ? AND from_node = ?")
      .all(nsId, current) as { to_node: string }[];
    for (const dep of deps) {
      if (!visited.has(dep.to_node)) {
        upstream.push(dep.to_node);
        queue.push(dep.to_node);
      }
    }
  }
  return upstream;
}

Design decisions, such as using BFS, ensure performance for large graphs and maintain a Directed Acyclic Graph (DAG). Effective state computation integrates with state updates, triggering notifications as needed.

Database

The database schema includes tables for tokens, namespaces, nodes, edges, notification rules, and events, with indexes for query optimization.

Example schema:

CREATE TABLE IF NOT EXISTS nodes (
  ns_id       INTEGER NOT NULL REFERENCES namespaces(ns_id) ON DELETE CASCADE,
  id          TEXT NOT NULL,
  state       TEXT NOT NULL DEFAULT 'yellow' CHECK (state IN ('green', 'yellow', 'red')),
  -- Other fields...
  PRIMARY KEY (ns_id, id)
);

CREATE TABLE IF NOT EXISTS edges (

ns_id INTEGER NOT NULL,

from_node TEXT NOT NULL,

to_node TEXT NOT NULL,

PRIMARY KEY (ns_id, from_node, to_node),

FOREIGN KEY (ns_id, from_node) REFERENCES nodes(ns_id, id) ON DELETE CASCADE,

FOREIGN KEY (ns_id, to_node) REFERENCES nodes(ns_id, id) ON DELETE CASCADE

);

Database interactions occur through API endpoints, such as those for namespaces and events.

Usage

The database stores and queries graphs dynamically. For example, updating a node's state triggers recomputation of effective states and notifications:

export async function handlePutState(
  db: Database,
  nsId: number,
  namespace: string,
  nodeId: string,
  state: string,
  req: Request,
  legendumToken: string | null
): Promise<Response> {
  // Update logic and notification dispatch
}

For testing, use an in-memory database option.

Recent changes