| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A minimal, type-safe library for managing stateful resources in JavaScript and TypeScript applications through declarative system composition with automatic dependency resolution and lifecycle management.
Inspired by Clojure's Integrant.
Building applications with multiple stateful resources creates coordination challenges:
Traditional approaches lead to common issues:
Braided provides a data-driven approach to system composition:
npm install braidedimport { defineResource, startSystem, haltSystem } from "braided";
// Define a database resource
const database = defineResource({
start: async () => {
const conn = await connectToDatabase();
return {
query: (sql: string) => conn.execute(sql),
close: () => conn.disconnect(),
};
},
halt: async (db) => {
await db.close();
},
});
// Define an API that depends on the database
const api = defineResource({
dependencies: ["database"],
start: ({ database }) => {
return {
getUsers: () => database.query("SELECT * FROM users"),
getOrders: () => database.query("SELECT * FROM orders"),
};
},
halt: () => {
console.log("API shutting down");
},
});
// Compose your system
const system = {
database,
api,
};
// Start everything in dependency order
const { system: running, errors } = await startSystem(system);
// Use your resources
const users = running.api.getUsers();
// Shutdown in reverse order
await haltSystem(system, running);A resource is anything with a lifecycle or anything that needs to be managed and distributed to other parts of the system: database connections, HTTP servers, WebSocket/WebRTC connections, timers, caches, recovery mechanisms, observers, etc. Anything that you can't dispose of immediately after spawning, that is a stateful thing, which is what you need to manage carefully.
const resource = defineResource({
// Optional: declare dependencies
dependencies: ["otherResource"],
// Optional: validate dependencies before starting
assert: ({ otherResource }: { otherResource: TOtherResource }) => {
if (!isValid(otherResource)) {
throw new Error("Invalid dependency");
}
},
// Required: start the resource
start: (deps: { otherResource: TOtherResource }) => {
// Return your resource instance
return createMyResource(deps);
},
// Required: stop the resource
halt: (instance) => {
// Clean up
instance.cleanup();
},
});Define your system as a plain object mapping resource IDs to resource configs:
const system = {
config: configResource,
database: databaseResource,
cache: cacheResource,
api: apiResource,
websocket: websocketResource,
};The library uses topological sorting to determine the correct startup order based on declared dependencies.
// Start system (dependencies first)
const { system: running, errors } = await startSystem(config);
// Check for startup errors
if (errors.size > 0) {
console.error("Some resources failed:", errors);
}
// Use your system
running.api.handleRequest();
// Halt system (reverse order)
const { errors: haltErrors } = await haltSystem(config, running);import { defineResource, startSystem, haltSystem } from "braided";
// Configuration resource (no dependencies)
const config = defineResource({
start: () => ({
port: process.env.PORT || 3000,
dbUrl: process.env.DATABASE_URL,
}),
halt: () => {},
});
// Database resource (depends on config)
const database = defineResource({
dependencies: ["config"],
start: async ({ config }) => {
const pool = await createPool(config.dbUrl);
return {
query: (sql) => pool.query(sql),
close: () => pool.end(),
};
},
halt: async (db) => await db.close(),
});
// WebSocket server (depends on config and database)
const websocket = defineResource({
dependencies: ["config", "database"],
start: ({ config, database }) => {
const wss = new WebSocketServer({ port: config.port + 1 });
wss.on("connection", (ws) => {
ws.on("message", async (msg) => {
const result = await database.query("INSERT INTO messages VALUES (?)", [
msg,
]);
ws.send(JSON.stringify(result));
});
});
return wss;
},
halt: (wss) => {
wss.close();
},
});
// HTTP API (depends on config, database, and websocket)
const api = defineResource({
dependencies: ["config", "database", "websocket"],
start: ({ config, database, websocket }) => {
const app = express();
app.get("/messages", async (req, res) => {
const messages = await database.query("SELECT * FROM messages");
res.json(messages);
});
const server = app.listen(config.port);
return server;
},
halt: (server) => {
server.close();
},
});
// Define the system
const system = { config, database, websocket, api };
// Start everything
const { system: running } = await startSystem(system);
// Graceful shutdown
process.on("SIGTERM", async () => {
await haltSystem(system, running);
process.exit(0);
});Resources are plain functions, making testing straightforward:
import { describe, test, expect } from "vitest";
import { startSystem, haltSystem } from "braided";
describe("API Tests", () => {
test("handles requests with mocked database", async () => {
// Create a mock database
const mockDb = defineResource({
start: () => ({
query: () => Promise.resolve([{ id: 1, name: "Test" }]),
}),
halt: () => {},
});
// Use the real API with mock database
const testSystem = {
database: mockDb,
api: apiResource, // your real api resource
};
const { system } = await startSystem(testSystem);
// Test your API
const result = await system.api.getUsers();
expect(result).toEqual([{ id: 1, name: "Test" }]);
await haltSystem(testSystem, system);
});
});Braided supports graceful degradation when you opt in by marking dependencies as optional.
const resilientApi = defineResource({
dependencies: { optional: ["cache"] },
start: ({ cache }) => {
if (!cache) {
console.warn("Cache unavailable, running without cache");
return createApiWithoutCache();
}
return createApiWithCache(cache);
},
halt: (api) => api.close(),
});Braided handles complex dependency patterns including diamond dependencies:
// config
// / \
// cache db
// \ /
// apiRun multiple isolated system instances simultaneously, compose variations of resources:
const server1 = await startSystem(serverConfig);
const server2 = await startSystem(serverConfig);Every call to startSystem returns a topology object containing the complete dependency structure:
const { system, errors, topology } = await startSystem(config);
// Print human-readable topology
console.log(formatTopology(topology));
// System Topology (5 resources, max depth: 3)
//
// Layer 0:
// config (no dependencies) → [database, cache]
//
// Layer 1:
// database ← [config] → [api]
// cache ← [config] → [api]
// ...
// Generate Mermaid diagram for markdown
console.log(toMermaid(topology));
// graph TD
// config --> database
// config --> cache
// database --> api
// cache --> api
// api --> httpServer
// Export as JSON for custom visualizations
const json = toJSON(topology);
fs.writeFileSync("topology.json", JSON.stringify(json, null, 2));
// Generate GraphViz DOT format
const dot = toDot(topology);
fs.writeFileSync("system.dot", dot);
// Render with: dot -Tpng system.dot -o system.pngTopology Structure:
{
layers: [
{
depth: 0,
resources: [
{ id: 'config', dependencies: [], dependents: ['database', 'cache'] }
]
},
// ... more layers
],
graph: { config: [], database: ['config'], ... },
dependents: { config: ['database', 'cache'], ... },
depths: { config: 0, database: 1, ... },
totalResources: 5,
maxDepth: 3,
startupOrder: ['config', 'database', 'cache', 'api', 'httpServer'],
shutdownOrder: ['httpServer', 'api', 'cache', 'database', 'config']
}Use Cases:
Helper to define a resource with full type inference.
Parameters:
Returns: ResourceConfig<TStart>
Start all resources in dependency order.
Parameters:
Returns: Promise<{ system: StartedSystem<TConfig>, errors: Map<string, Error>, topology: SystemTopology }>
Halt all resources in reverse dependency order.
Parameters:
Returns: Promise<{ errors: Map<string, Error> }>
How does the library determine startup and shutdown order?
The library uses topological sorting to analyze the dependency graph. Resources start in dependency order (dependencies first) and stop in reverse order (dependents first).
What happens if a resource fails to start?
The system continues starting other resources. Dependent resources receive undefined for failed dependencies. Errors are collected in the returned errors map. Use the assert function to validate dependencies and fail fast if needed.
What happens if a resource fails to stop?
The system continues halting other resources. Errors are collected and returned in the errors map from haltSystem.
Does it detect circular dependencies?
Yes. Circular dependencies are detected during topological sorting and throw an error immediately, preventing the system from starting.
ISC
Braided React - React adapter for managing Braided systems independent of the React component lifecycle.
Issues and pull requests are welcome. This library has been used in production systems managing WebRTC connections, WebSocket servers, database pools, caches, and background workers.
| Back | FazBrowse Home | New Git URL |