diff --git a/.gitignore b/.gitignore index a14702c..bb3a6ba 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md index beff57c..a0f3c19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ Duron is a modern, type-safe background job processing system built with TypeScr - **Type-Safe Actions** - Define actions with Zod schemas for input/output validation - **Step-Based Execution** - Break down complex workflows into manageable, retryable steps +- **Nested Steps** - Steps can create child steps with proper parent-child tracking and abort signal propagation - **Intelligent Retry Logic** - Configurable exponential backoff with per-action and per-step options - **Flexible Sync Patterns** - Pull, push, hybrid, or manual job fetching - **Advanced Concurrency Control** - Per-action, per-group, and dynamic concurrency limits @@ -19,6 +20,7 @@ Duron is a modern, type-safe background job processing system built with TypeScr - **Database Adapters** - PostgreSQL (production) and PGLite (development/testing) - **REST API Server** - Built-in Elysia-based API with advanced filtering and pagination - **Dashboard UI** - Beautiful React dashboard for real-time job monitoring +- **Telemetry & Observability** - Built-in support for metrics, tracing, and custom observability with pluggable adapters ## Runtime Environment @@ -85,6 +87,10 @@ The main library providing: - **Adapters**: - `duron/adapters/postgres` - PostgreSQL adapter for production - `duron/adapters/pglite` - PGLite adapter for development/testing +- **Telemetry** - Configured via `telemetry` option on client: + - `telemetry: { local: true }` - Store spans in the database + - `telemetry: { traceExporter }` - Export to OpenTelemetry backends + - No config = telemetry disabled (default) **Key Dependencies:** - `zod` - Schema validation @@ -93,6 +99,7 @@ The main library providing: - `pino` - Logging - `fastq` - Queue implementation - `jose` - JWT handling +- `@opentelemetry/api` - OpenTelemetry integration (optional) ### `duron-dashboard` (React Dashboard) @@ -198,6 +205,49 @@ const sendEmail = defineAction()({ }) ``` +### Nested Steps + +Steps can create child steps using the `step()` method available in the step handler context. Child steps share abort signals with their parent and are tracked with `parentStepId` in the database. + +```typescript +const processOrder = defineAction()({ + name: 'process-order', + input: z.object({ orderId: z.string() }), + output: z.object({ success: z.boolean() }), + handler: async (ctx) => { + const result = await ctx.step('process', async ({ step, signal, stepId }) => { + // stepId is available for the current step + console.log('Processing step:', stepId) + + // Create child steps - they inherit the parent's abort signal + const validation = await step('validate', async ({ parentStepId }) => { + // parentStepId links back to the 'process' step + return { valid: true } + }) + + // Child steps can also be nested further + const payment = await step('charge', async ({ step: nestedStep }) => { + const auth = await nestedStep('authorize', async () => { + return { authCode: '123' } + }) + return { charged: true, authCode: auth.authCode } + }) + + return { success: validation.valid && payment.charged } + }) + + return result + }, +}) +``` + +**Important:** All child steps MUST be awaited before the parent step returns. If a parent step completes with unawaited children, Duron will: +1. Abort all pending child steps +2. Wait for them to settle +3. Throw an `UnhandledChildStepsError` + +This prevents orphaned processes and ensures proper async patterns. + ### Creating a Client ```typescript @@ -228,6 +278,75 @@ const jobId = await client.runAction('send-email', { const job = await client.waitForJob(jobId) ``` +### Telemetry & Observability + +Duron provides built-in OpenTelemetry support for tracing: + +```typescript +import { duron } from 'duron' +import { postgresAdapter } from 'duron/adapters/postgres' + +const client = duron({ + database: postgresAdapter({ + connection: process.env.DATABASE_URL, + }), + // Enable local telemetry - stores spans in the database + telemetry: { local: true }, + actions: { sendEmail }, +}) +``` + +**Telemetry Configuration Options:** + +- `local: true | { flushDelayMs?: number }` - Store spans in the Duron database +- `traceExporter: SpanExporter` - Export to OpenTelemetry-compatible backends (Jaeger, OTLP, etc.) +- `spanProcessors: SpanProcessor[]` - Add custom span processors +- `serviceName: string` - Service name for OpenTelemetry resource (default: `'duron'`) + +**Recording Custom Metrics:** + +The `telemetry` context is available in action and step handlers for recording custom metrics: + +```typescript +const processAI = defineAction()({ + name: 'process-ai', + handler: async (ctx) => { + const startTime = Date.now() + + // Record job-level metrics + ctx.telemetry.recordMetric('ai.request.start', 1) + const span = ctx.telemetry.getActiveSpan() + span?.setAttribute('model', 'gpt-4') + span?.addEvent('processing.started') + + const result = await ctx.step('call-api', async ({ telemetry }) => { + const response = await callAI(ctx.input) + + // Record step-level metrics + telemetry.recordMetric('ai.tokens.input', response.inputTokens) + telemetry.recordMetric('ai.tokens.output', response.outputTokens) + telemetry.recordMetric('ai.latency.ms', Date.now() - startTime) + telemetry.getActiveSpan()?.addEvent('api.call.complete', { status: 'success' }) + + return response + }) + + return result + }, +}) +``` + +**Accessing Metrics via API:** + +When using `telemetry: { local: true }`, spans are stored in the database and accessible via the REST API: + +``` +GET /api/jobs/:id/spans +GET /api/steps/:id/spans +``` + +The dashboard also shows metrics when local telemetry is enabled. + ### Creating a Server with Dashboard ```typescript @@ -340,6 +459,8 @@ Uses Bun's bundler mode with: | `packages/duron/src/server.ts` | REST API server | | `packages/duron/src/adapters/adapter.ts` | Base adapter class | | `packages/duron/src/adapters/postgres/` | PostgreSQL adapter | +| `packages/duron/src/step-manager.ts` | Step execution and nested step handling | +| `packages/duron/src/telemetry/` | Telemetry adapters (local, opentelemetry, noop) | | `packages/duron-dashboard/src/DuronDashboard.tsx` | Dashboard root | | `packages/duron-dashboard/src/views/` | Dashboard pages | | `packages/examples/basic/start.ts` | Basic example | @@ -368,15 +489,22 @@ Uses Bun's bundler mode with: ## Error Handling - Use `NonRetriableError` for errors that should not be retried +- Use `UnhandledChildStepsError` is thrown when parent steps complete with unawaited children - Steps have built-in retry logic with exponential backoff - Jobs have timeout/expiration settings ```typescript -import { NonRetriableError } from 'duron' +import { NonRetriableError, UnhandledChildStepsError } from 'duron' +// For errors that should not be retried if (!apiKey) { throw new NonRetriableError('API key is required') } + +// UnhandledChildStepsError is thrown automatically when: +// - A parent step returns before all child steps are awaited +// - The parent step's callback completes but children are still pending +// This error is non-retriable and will fail the entire job ``` ## Environment Variables @@ -395,3 +523,5 @@ if (!apiKey) { 4. Follow existing code patterns 5. Use TypeScript strict mode 6. Document public APIs with JSDoc + +Always use Context7 MCP when I need library/API documentation, code generation, setup or configuration steps without me having to explicitly ask. diff --git a/README.md b/README.md index a2088d5..39462e9 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ The complete documentation includes detailed guides on actions, adapters, client

Job List View

Duron Dashboard - Job List - -

Job Details - Completed

- Duron Dashboard - Completed Job +
+
+

Timeline

+ Duron Dashboard - Timeline
## 🚀 Quick Start @@ -55,17 +56,17 @@ yarn add duron postgres drizzle-orm@beta Actions are defined with Zod schemas and have access to a context object that includes input, variables, and a step function. First define your variables type, then use `defineAction` with the variables type as a generic parameter: ```typescript -import { defineAction } from 'duron' -import { z } from 'zod' +import { defineAction } from "duron"; +import { z } from "zod"; // Define your variables type const variables = { apiKey: process.env.EMAIL_API_KEY, -} +}; // Define the action with variables type as generic const sendEmail = defineAction()({ - name: 'send-email', + name: "send-email", input: z.object({ email: z.string().email(), subject: z.string(), @@ -76,51 +77,51 @@ const sendEmail = defineAction()({ }), handler: async (ctx) => { // Access input data - const { email, subject, body } = ctx.input + const { email, subject, body } = ctx.input; // Access shared variables (type-safe!) - const apiKey = ctx.var.apiKey + const apiKey = ctx.var.apiKey; // Execute steps with retry logic // Steps are retryable units of work that can be monitored and cancelled - const result = await ctx.step('send-email', async ({ signal }) => { + const result = await ctx.step("send-email", async ({ signal }) => { // Your email sending logic here // The signal can be used to handle cancellation - const response = await fetch('https://api.email.com/send', { - method: 'POST', + const response = await fetch("https://api.email.com/send", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ email, subject, body }), signal, // Pass signal to enable cancellation - }) + }); if (!response.ok) { - throw new Error('Failed to send email') + throw new Error("Failed to send email"); } - return await response.json() - }) + return await response.json(); + }); - return { success: result.success ?? true } + return { success: result.success ?? true }; }, -}) +}); ``` ### Creating a Client with PostgreSQL ```typescript -import { duron } from 'duron' -import { postgresAdapter } from 'duron/adapters/postgres' +import { duron } from "duron"; +import { postgresAdapter } from "duron/adapters/postgres"; // Define variables (same type used in defineAction) const variables = { apiKey: process.env.EMAIL_API_KEY, -} +}; const client = duron({ - id: 'my-app', + id: "my-app", database: postgresAdapter({ connection: process.env.DATABASE_URL, // PostgreSQL connection string }), @@ -128,17 +129,17 @@ const client = duron({ sendEmail, }, variables, // Pass the variables object - logger: 'info', -}) + logger: "info", +}); -await client.start() +await client.start(); // Run an action -const jobId = await client.runAction('sendEmail', { - email: 'user@example.com', - subject: 'Hello', - body: 'Welcome!', -}) +const jobId = await client.runAction("sendEmail", { + email: "user@example.com", + subject: "Hello", + body: "Welcome!", +}); ``` ### Starting a Server with Dashboard @@ -146,14 +147,14 @@ const jobId = await client.runAction('sendEmail', { The server includes authentication and can serve the dashboard as standalone HTML: ```typescript -import { createServer } from 'duron/server' -import { getHTML } from 'duron-dashboard/get-html' -import { Elysia } from 'elysia' +import { createServer } from "duron/server"; +import { getHTML } from "duron-dashboard/get-html"; +import { Elysia } from "elysia"; // Define variables (same type used in defineAction) const variables = { apiKey: process.env.EMAIL_API_KEY, -} +}; // Create the Duron client const client = duron({ @@ -164,35 +165,35 @@ const client = duron({ sendEmail, }, variables, // Pass the variables object -}) +}); // Create the API server with authentication const app = createServer({ client, - prefix: '/api', + prefix: "/api", login: { onLogin: async ({ email, password }) => { // Implement your authentication logic // Return true if credentials are valid // In production, validate against your user database - return email === 'admin@example.com' && password === 'secure-password' + return email === "admin@example.com" && password === "secure-password"; }, - jwtSecret: process.env.JWT_SECRET || 'your-secret-key', - expirationTime: '24h', // Optional, defaults to '1h' - refreshTokenExpirationTime: '7d', // Optional, defaults to '7d' + jwtSecret: process.env.JWT_SECRET || "your-secret-key", + expirationTime: "24h", // Optional, defaults to '1h' + refreshTokenExpirationTime: "7d", // Optional, defaults to '7d' }, -}) +}); // Serve the dashboard -app.get('/', async () => { - const html = await getHTML({ url: 'http://localhost:3000/api' }) +app.get("/", async () => { + const html = await getHTML({ url: "http://localhost:3000/api" }); return new Response(html, { - headers: { 'Content-Type': 'text/html' }, - }) -}) + headers: { "Content-Type": "text/html" }, + }); +}); // Start the server -app.listen(3000) +app.listen(3000); ``` #### Dashboard Authentication Setup diff --git a/bun.lock b/bun.lock index 1ed9a91..d0fb9c7 100644 --- a/bun.lock +++ b/bun.lock @@ -44,11 +44,19 @@ }, "packages/duron": { "name": "duron", - "version": "0.2.2", + "version": "0.3.0-beta.18", "dependencies": { - "elysia": "^1.4.16", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.4.0", + "@opentelemetry/resources": "^2.4.0", + "@opentelemetry/sdk-node": "^0.210.0", + "@opentelemetry/sdk-trace-base": "^2.4.0", + "@opentelemetry/sdk-trace-node": "^2.4.0", + "@opentelemetry/semantic-conventions": "^1.39.0", + "elysia": "^1.4.22", "fastq": "^1.19.1", "jose": "^6.1.2", + "p-all": "^5.0.1", "p-retry": "^7.1.0", "p-timeout": "^7.0.1", "pino": "^10.1.0", @@ -59,15 +67,15 @@ "@electric-sql/pglite": "^0.3.14", "@types/bun": "latest", "@types/node": "^24.0.15", - "drizzle-kit": "^1.0.0-beta.2-b782ae1", - "drizzle-orm": "^1.0.0-beta.2-b782ae1", + "drizzle-kit": "^1.0.0-beta.11-05230d9", + "drizzle-orm": "^1.0.0-beta.11-05230d9", "postgres": "^3.4.7", "typescript": "^5.6.3", }, }, "packages/duron-dashboard": { "name": "duron-dashboard", - "version": "0.1.1", + "version": "0.3.0-beta.13", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -76,6 +84,7 @@ "@maskito/core": "^4.0.1", "@maskito/kit": "^4.0.1", "@maskito/react": "^4.0.1", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -90,13 +99,13 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.10", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", - "@uiw/react-json-view": "2.0.0-alpha.37", + "@tanstack/react-virtual": "^3.13.18", "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "jsonata": "^2.1.0", "lucide-react": "^0.555.0", "motion": "^12.23.24", "nanoid": "^5.1.6", @@ -105,7 +114,9 @@ "react": "^19", "react-day-picker": "^9.11.2", "react-dom": "^19", + "react-resizable-panels": "^4.4.1", "tailwind-merge": "^3.4.0", + "usehooks-ts": "^3.1.1", "zod": "^4.1.12", }, "devDependencies": { @@ -135,8 +146,9 @@ "packages/shared-actions": { "name": "shared-actions", "dependencies": { - "@ai-sdk/openai": "^2.0.73", - "ai": "^5.0.102", + "@ai-sdk/openai": "^3.0.12", + "@opentelemetry/api": "^1.9.0", + "ai": "^6.0.42", "zod": "^4.1.12", }, "devDependencies": { @@ -148,13 +160,13 @@ }, }, "packages": { - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oVAG6q72KsjKlrYdLhWjRO7rcqAR8CjokAbYuyVZoCO4Uh2PH/VzZoxZav71w2ipwlXhHCNaInGYWNs889MMDA=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "3.0.4", "@ai-sdk/provider-utils": "4.0.8", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mCz50GlBPyBV96Wcll1Mpaz56MVFuHL+bwRWGkIsCJwKAKIfdgUZecFzS3gckpHGaqP5+BYnmyJocIMzUhTQ2A=="], - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.75", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ThDHg1+Jes7S0AOXa01EyLBSzZiZwzB5do9vAlufNkoiRHGTH1BmoShrCyci/TUsg4ky1HwbK4hPK+Z0isiE6g=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.12", "", { "dependencies": { "@ai-sdk/provider": "3.0.4", "@ai-sdk/provider-utils": "4.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zqLWEKuaKnjXhu7xCw1jgz/+yTbd3F7EtgU4T2Q8BAo8OJC5wZv14l+kwM7Jai7M1/2Y2T/zBkrfiIu+7NsvfQ=="], - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.4", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5KXyBOSEX+l67elrEa+wqo/LSsSTtrPj9Uoh3zMbe/ceQX4ucHI3b9nUEfNkGF3Ry1svv90widAt+aiKdIJasQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.8", "", { "dependencies": { "@ai-sdk/provider": "3.0.4", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ns9gN7MmpI8vTRandzgz+KK/zNMLzhrriiKECMt4euLtQFSBgNfydtagPOX4j4pS1/3KvHF6RivhT3gNQgBZsg=="], "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], @@ -244,8 +256,6 @@ "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -352,6 +362,10 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -364,6 +378,8 @@ "@js-joda/core": ["@js-joda/core@5.6.5", "", {}, "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@maskito/core": ["@maskito/core@4.0.1", "", {}, "sha512-0ewYXnr8X7as/n57duk1DDqjAKF0m32XbFiAhKdd9OX83eMBzO2+4xgUFxh6WZJ18oSw6y2nWjCTDFAFc5JbhA=="], @@ -374,6 +390,10 @@ "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], "@oozcitak/infra": ["@oozcitak/infra@2.0.2", "", { "dependencies": { "@oozcitak/util": "^10.0.0" } }, "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA=="], @@ -384,6 +404,62 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.210.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg=="], + + "@opentelemetry/configuration": ["@opentelemetry/configuration@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "yaml": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-tM0ROS/hZM72kB55cSjDcghVcUXBJdGkGzpkhD7M1B/gpcvZPSGfjFgKN3dgmxNgF76NxtbUwv3ik0wS+Kz52g=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.4.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.4.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw=="], + + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.210.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-grpc-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/sdk-logs": "0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/sdk-logs": "0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Q8/SEQtgrErbVVRg9M9iaG8m5wdPNdU0UOF7U43sAhwfmPG92ZOk/aenKhg0DXSNJHhkCDNCgS1kSoErAB3z0A=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-logs": "0.210.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y/yPc+gDhsWB7AsNzQWxblw4ULbvhCycMaQ2aAn+HSAVbgbMiZa0SbclPVHSnpnNzKSLVavFjweAr0pQA1KKLg=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.210.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.4.0", "@opentelemetry/exporter-metrics-otlp-http": "0.210.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-grpc-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-metrics": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pWZ/Tjrqev9rdkqe8F6A9FGddLZrjl6iRAU5LBvvRL6I3PSgG8z1xM0cESAy1jzAF4wGohnAh8rB7hHzpUOYEA=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-metrics": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JpLThG8Hh8A/Jzdzw9i4Ftu+EzvLaX/LouN+mOOHmadL0iror0Qsi3QWzucXeiUsDDsiYgjfKyi09e6sltytgA=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/exporter-metrics-otlp-http": "0.210.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-metrics": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-metrics": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8i+7d70Hho6pcheTtbqIuS+bo+AIX/oNUTMwIEZoehUE4ZdbGmeVaE+hJS2LAErFeFaU71w164lAgYyMUEQ8zw=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.210.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-grpc-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1GPLOyxIfUX24WM8Oea+vx9d9TlewposUnsQXTjusxVMQ/dWvt5JIDJyTsfNDS412XRUOORgF97PwsfDY5QKGA=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-qpiXY0TUEFjBBp9b1na9LfuVQw6W8LH+te7uv+CC+0Up78ZDtZZwOjK2M7CL7Nspnw+yS4JdgEA7oxsBu0Ctsg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-transformer": "0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.210.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-logs": "0.210.0", "@opentelemetry/sdk-metrics": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ=="], + + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-t6muBL/3AMD++1EMF658C/KIpj3gfmTmftX3mEQql4KIxNGFvacCmmTtrQt9IZAJmQRfjQRCkv+vsGbQugeJIw=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw=="], + + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/configuration": "0.210.0", "@opentelemetry/context-async-hooks": "2.4.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.210.0", "@opentelemetry/exporter-logs-otlp-http": "0.210.0", "@opentelemetry/exporter-logs-otlp-proto": "0.210.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.210.0", "@opentelemetry/exporter-metrics-otlp-http": "0.210.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.210.0", "@opentelemetry/exporter-prometheus": "0.210.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.210.0", "@opentelemetry/exporter-trace-otlp-http": "0.210.0", "@opentelemetry/exporter-trace-otlp-proto": "0.210.0", "@opentelemetry/exporter-zipkin": "2.4.0", "@opentelemetry/instrumentation": "0.210.0", "@opentelemetry/propagator-b3": "2.4.0", "@opentelemetry/propagator-jaeger": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-logs": "0.210.0", "@opentelemetry/sdk-metrics": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0", "@opentelemetry/sdk-trace-node": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.4.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.4.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + "@orama/orama": ["@orama/orama@3.1.16", "", {}, "sha512-scSmQBD8eANlMUOglxHrN1JdSW8tDghsPuS83otqealBiIeMukCQMOf/wc0JJjDXomqwNdEQFLXLGHrU6PGxuA=="], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], @@ -410,6 +486,26 @@ "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -650,7 +746,7 @@ "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A=="], "@tanstack/router-core": ["@tanstack/router-core@1.139.12", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-HCDi4fpnAFeDDogT0C61yd2nJn0FrIyFDhyHG3xJji8emdn8Ni4rfyrN4Av46xKkXTPUGdbsqih45+uuNtunew=="], @@ -678,7 +774,7 @@ "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="], "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.139.0", "", {}, "sha512-9PImF1d1tovTUIpjFVa0W7Fwj/MHif7BaaczgJJfbv3sDt1Gh+oW9W9uCw9M3ndEJynnp5ZD/TTs0RGubH5ssg=="], @@ -696,7 +792,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], @@ -722,15 +818,15 @@ "@types/readable-stream": ["@types/readable-stream@4.0.22", "", { "dependencies": { "@types/node": "*" } }, "sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], - "@uiw/react-json-view": ["@uiw/react-json-view@2.0.0-alpha.37", "", { "peerDependencies": { "@babel/runtime": ">=7.10.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-y9G0Kz4O4gkTRWh0xoBj2G+QRsC4DQ18XZAF5Q5AdtSTZCM6HfifjiNgQBJIsg0nDndw3432wgpyW9ROV+1FVQ=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="], @@ -740,11 +836,13 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@5.0.105", "", { "dependencies": { "@ai-sdk/gateway": "2.0.17", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-waQZAvv44KYzys6S3l25ti2jcSuJnkyWFTliSKy3swASL6w6ttPxJTm80d+v9sLWoIxrqE3OwhTJbweNp065fg=="], + "ai": ["ai@6.0.42", "", { "dependencies": { "@ai-sdk/gateway": "3.0.17", "@ai-sdk/provider": "3.0.4", "@ai-sdk/provider-utils": "4.0.8", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o+MVN7HBE4HEnhtN7nBt9WO1iISI6svyWNoOuY6WiXCdHuZfSGN4MUQ3QwjWz1Ue5gtBEcvwX5XFhgAwlPAxxw=="], "ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], @@ -808,7 +906,7 @@ "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -838,12 +936,16 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], "clipboardy": ["clipboardy@3.0.0", "", { "dependencies": { "arch": "^2.2.0", "execa": "^5.1.1", "is-wsl": "^2.2.0" } }, "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], @@ -920,13 +1022,15 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], "drange": ["drange@1.1.1", "", {}, "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA=="], - "drizzle-kit": ["drizzle-kit@1.0.0-beta.2-b782ae1", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "esbuild-register": "^3.6.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-ui8nESGtCC82HgsLlsPEvvzyAwwr9EDOhb4DqoY//0n1nJ99YDLhnyS8sJw7c23uCaDZtMQLmU6lIHjzdn8hyQ=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.11-05230d9", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-dIv1Ql5SOJsGE/6mzfUk2k8iQFQtP6Buq7JlRew/Mx1uOOkIgyPT6xAUsSbyXqjqiDQl9k2S7iXtKaOs3Cs4aw=="], - "drizzle-orm": ["drizzle-orm@1.0.0-beta.2-b782ae1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-nD4C80uF1gFLA6Bl+n3Vl1TL5ofdFnIcKkDir7xxj5U5bfUlPtVBHtwjFGaUx57ib6yF6S8YT+/J0Zm/zjRBoQ=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.11-05230d9", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-y6i3AgHqeT5Eu/+7hsF1AWyPZQTxiBx1UOy4snEqljW7Fbpp/fO2zmWYFPvjngwOLq9prrbrn36AwCoTR+J2Bg=="], "duron": ["duron@workspace:packages/duron"], @@ -938,7 +1042,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.262", "", {}, "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ=="], - "elysia": ["elysia@1.4.16", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-KZtKN160/bdWVKg2hEgyoNXY8jRRquc+m6PboyisaLZL891I+Ufb7Ja6lDAD7vMQur8sLEWIcidZOzj5lWw9UA=="], + "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -954,8 +1058,6 @@ "esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -984,7 +1086,7 @@ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "exact-mirror": ["exact-mirror@0.2.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="], + "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], "examples": ["examples@workspace:packages/examples"], @@ -1020,6 +1122,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -1066,6 +1170,8 @@ "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], + "import-in-the-middle": ["import-in-the-middle@2.0.4", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-Al0kMpa0BqfvDnxjxGlab9vdQ0vTDs82TBKrD59X9jReUoPAzSGBb6vGDzMUMFBGyyDF03RpLT4oxGn6BpASzQ=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -1128,6 +1234,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonata": ["jsonata@2.1.0", "", {}, "sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], @@ -1158,6 +1266,10 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -1174,6 +1286,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], @@ -1188,6 +1302,8 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], @@ -1304,6 +1420,10 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "motion": ["motion@12.23.25", "", { "dependencies": { "framer-motion": "^12.23.25", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Fk5Y1kcgxYiTYOUjmwfXQAP7tP+iGqw/on1UID9WEL/6KpzxPr9jY2169OsjgZvXJdpraKXy0orkjaCVIl5fgQ=="], "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], @@ -1348,6 +1468,10 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "p-all": ["p-all@5.0.1", "", { "dependencies": { "p-map": "^6.0.0" } }, "sha512-LMT7WX9ZSaq3J1zjloApkIVmtz0ZdMFSIqbuiEa3txGYPLjUPOvgOPOx3nFjo+f37ZYL+1aY666I2SG7GVwLOA=="], + + "p-map": ["p-map@6.0.0", "", {}, "sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw=="], + "p-retry": ["p-retry@7.1.0", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-xL4PiFRQa/f9L9ZvR4/gUCRNus4N8YX80ku8kv9Jqz+ZokkiZLM0bcvX0gm1F3PDi9SPRsww1BDsTWgE6Y1GLQ=="], "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], @@ -1392,6 +1516,8 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "protobufjs": ["protobufjs@8.0.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], @@ -1418,6 +1544,8 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-resizable-panels": ["react-resizable-panels@4.4.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-dpM9oI6rGlAq7VYDeafSRA1JmkJv8aNuKySR+tZLQQLfaeqTnQLSM52EcoI/QdowzsjVUCk6jViKS0xHWITVRQ=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], @@ -1460,8 +1588,12 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "ret": ["ret@0.2.2", "", {}, "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ=="], @@ -1522,6 +1654,8 @@ "srvx": ["srvx@0.8.16", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-hmcGW4CgroeSmzgF1Ihwgl+Ths0JqAJ7HwjP2X7e3JzY7u4IydLMcdnlqGQiQGUswz+PO9oh/KtCpOISIvs9QQ=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -1620,6 +1754,8 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "usehooks-ts": ["usehooks-ts@3.1.1", "", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -1652,16 +1788,28 @@ "xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zocker": ["zocker@3.0.0", "", { "dependencies": { "@faker-js/faker": "^10.0.0", "randexp": "^0.5.3" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-xSJz2M9AELZKzgpuuZPbTQdMv9263p/zSHDIj81B58X3334Xl6iJ/C3u6OQiOlsrWAoJSJsveBcIdz1eEPL0EA=="], "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@grpc/proto-loader/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1716,12 +1864,22 @@ "chalk-template/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "duron-dashboard/@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "examples/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "fumadocs-ui/fumadocs-core": ["fumadocs-core@16.1.0", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.2", "@orama/orama": "^3.1.16", "@shikijs/rehype": "^3.15.0", "@shikijs/transformers": "^3.15.0", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.15.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.19.0", "@orama/core": "1.x.x", "@tanstack/react-router": "1.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0" }, "optionalPeers": ["@mixedbread/sdk", "@orama/core", "@tanstack/react-router", "@types/react", "algoliasearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku"] }, "sha512-5pbO2bOGc/xlb2yLQSy6Oag8mvD5CNf5HzQIG80HjZzLXYWEOHW8yovRKnWKRF9gAibn6WHnbssj3YPAlitV/A=="], "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -1750,6 +1908,8 @@ "serve-handler/path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="], + "shared-actions/@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + "solid-js/seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "solid-js/seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], @@ -1758,6 +1918,8 @@ "wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1766,6 +1928,12 @@ "chalk-template/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -1820,6 +1988,12 @@ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "duron-dashboard/@types/bun/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "examples/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + + "shared-actions/@types/bun/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -1872,8 +2046,14 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/packages/assets/complete-job.png b/packages/assets/complete-job.png deleted file mode 100644 index 25f4484..0000000 Binary files a/packages/assets/complete-job.png and /dev/null differ diff --git a/packages/assets/failed-job.png b/packages/assets/failed-job.png deleted file mode 100644 index 1a8f198..0000000 Binary files a/packages/assets/failed-job.png and /dev/null differ diff --git a/packages/assets/job-list.png b/packages/assets/job-list.png index 73abc22..0701125 100644 Binary files a/packages/assets/job-list.png and b/packages/assets/job-list.png differ diff --git a/packages/assets/job-output.png b/packages/assets/job-output.png new file mode 100644 index 0000000..13322ac Binary files /dev/null and b/packages/assets/job-output.png differ diff --git a/packages/assets/timeline-view.png b/packages/assets/timeline-view.png new file mode 100644 index 0000000..d7a1e9e Binary files /dev/null and b/packages/assets/timeline-view.png differ diff --git a/packages/docs/content/docs/actions.mdx b/packages/docs/content/docs/actions.mdx index bc303ad..ec07cc7 100644 --- a/packages/docs/content/docs/actions.mdx +++ b/packages/docs/content/docs/actions.mdx @@ -118,7 +118,7 @@ handler: async (ctx) => { ### `ctx.step` -A function to create and execute steps within the action. See [Steps](/docs/jobs-and-steps#steps) for more details. +A function to create and execute inline steps within the action. See [Steps](/docs/jobs-and-steps#steps) for more details. ```ts handler: async (ctx) => { @@ -128,6 +128,47 @@ handler: async (ctx) => { } ``` +### `ctx.run` + +A function to execute reusable step definitions created with `createStep()`. See [Reusable Step Definitions](/docs/jobs-and-steps#reusable-step-definitions) for more details. + +```ts +import { createStep } from 'duron' + +const sendEmailStep = createStep()({ + name: 'send-email', + input: z.object({ to: z.string().email(), body: z.string() }), + handler: async (ctx) => { + // ctx includes input, signal, var, logger, jobId, etc. + return { success: true } + }, +}) + +handler: async (ctx) => { + // Execute the reusable step + const result = await ctx.run(sendEmailStep, { + to: ctx.input.email, + body: 'Hello World', + }) +} +``` + +### `ctx.telemetry` + +Telemetry context for recording custom metrics and span data. Provides access to OpenTelemetry APIs. See [Telemetry](/docs/telemetry) for more details. + +```ts +handler: async (ctx) => { + // Record a custom metric + ctx.telemetry.recordMetric('custom.counter', 1) + + // Access the active span for attributes and events + const span = ctx.telemetry.getActiveSpan() + span.setAttribute('customKey', 'value') + span.addEvent('custom.event') +} +``` + ## Action Options ### `name` @@ -211,6 +252,28 @@ handler: async (ctx) => { } ``` +### `description` + +**Optional.** A function that generates a dynamic description for the job at creation time. The description is stored in the database and displayed in the dashboard, providing context about what the specific job instance is doing. + +```ts +{ + name: 'sendEmail', + description: async (ctx) => `Send email to ${ctx.input.to}`, +} +``` + +The function receives the same context as `groupKey` with `ctx.input` and `ctx.var`: + +```ts +{ + name: 'processOrder', + description: async (ctx) => { + return `Process order #${ctx.input.orderId} for user ${ctx.input.userId}` + }, +} +``` + ### `concurrency` **Optional.** Maximum number of jobs that can run concurrently for this action. Defaults to `100`. @@ -265,7 +328,7 @@ A function that determines the concurrency limit for a job's group. Defaults to #### `steps.concurrency` -Maximum number of steps that can run concurrently for this action. Defaults to `10`. +Maximum number of steps that can run concurrently for this action. Defaults to `100`. ```ts { @@ -332,6 +395,7 @@ const processOrder = defineAction<{ db: Database }>()({ groupKey: async (ctx) => `user-${ctx.input.userId}`, concurrency: async (ctx) => 5, }, + description: async (ctx) => `Process order ${ctx.input.orderId} for user ${ctx.input.userId}`, steps: { concurrency: 10, retry: { diff --git a/packages/docs/content/docs/adapters.mdx b/packages/docs/content/docs/adapters.mdx index d0f7b80..5e520d7 100644 --- a/packages/docs/content/docs/adapters.mdx +++ b/packages/docs/content/docs/adapters.mdx @@ -81,12 +81,14 @@ Stores job information. - `error` - JSONB error data (nullable) - `status` - Job status (`created`, `active`, `completed`, `failed`, `cancelled`) - `timeout_ms` - Timeout in milliseconds +- `expires_at` - Expiration timestamp (nullable) - `concurrency_limit` - Concurrency limit for the group +- `concurrency_step_limit` - Step concurrency limit for this job +- `client_id` - ID of the Duron instance that owns the job (nullable) - `created_at` - Timestamp - `started_at` - Timestamp (nullable) - `finished_at` - Timestamp (nullable) - `updated_at` - Timestamp -- `client_id` - ID of the Duron instance that owns the job ### `job_steps` Table @@ -94,18 +96,41 @@ Stores step information. - `id` - UUID primary key - `job_id` - Foreign key to jobs table +- `parent_step_id` - Parent step ID for nested steps (nullable) +- `parallel` - Whether this step runs in parallel with siblings (column name: `branch`) - `name` - Step name -- `input` - JSONB input data - `output` - JSONB output data (nullable) - `error` - JSONB error data (nullable) - `status` - Step status (`active`, `completed`, `failed`, `cancelled`) - `timeout_ms` - Timeout in milliseconds -- `delay_ms` - Delay in milliseconds (nullable) +- `expires_at` - Expiration timestamp (nullable) +- `retries_limit` - Maximum number of retries +- `retries_count` - Current retry count +- `delayed_ms` - Delay before next retry in milliseconds (nullable) +- `history_failed_attempts` - JSONB record of previous failed attempts - `created_at` - Timestamp -- `started_at` - Timestamp (nullable) +- `started_at` - Timestamp - `finished_at` - Timestamp (nullable) - `updated_at` - Timestamp -- `client_id` - ID of the Duron instance that owns the step + +### `spans` Table + +Stores OpenTelemetry spans for observability. Only used when `telemetry.local` is enabled. + +- `id` - Bigserial primary key +- `trace_id` - OpenTelemetry trace ID (32-char hex) +- `span_id` - OpenTelemetry span ID (16-char hex) +- `parent_span_id` - Parent span ID (nullable, null for root spans) +- `job_id` - Foreign key to jobs table (nullable) +- `step_id` - Foreign key to job_steps table (nullable) +- `name` - Span name +- `kind` - Span kind (0=INTERNAL, 1=SERVER, 2=CLIENT, 3=PRODUCER, 4=CONSUMER) +- `start_time_unix_nano` - Start time in nanoseconds since epoch +- `end_time_unix_nano` - End time in nanoseconds since epoch (nullable) +- `status_code` - Status code (0=UNSET, 1=OK, 2=ERROR) +- `status_message` - Status message (nullable) +- `attributes` - JSONB span attributes +- `events` - JSONB array of span events ### Migrations diff --git a/packages/docs/content/docs/client-api.mdx b/packages/docs/content/docs/client-api.mdx index bdee70f..7052062 100644 --- a/packages/docs/content/docs/client-api.mdx +++ b/packages/docs/content/docs/client-api.mdx @@ -160,11 +160,11 @@ const client = duron({ ### `processTimeout` -**Optional.** Timeout in milliseconds to wait for process ping responses. Defaults to `300000` (5 minutes). +**Optional.** Timeout in milliseconds to wait for process ping responses. Defaults to `5000` (5 seconds). ```ts { - processTimeout: 10 * 60 * 1000, // 10 minutes + processTimeout: 10 * 1000, // 10 seconds } ``` @@ -178,6 +178,25 @@ const client = duron({ } ``` +### `telemetry` + +**Optional.** Telemetry configuration for OpenTelemetry tracing. See [Telemetry](/docs/telemetry) for details. + +```ts +{ + telemetry: { + local: true, // Store spans in the database + serviceName: 'my-service', // OpenTelemetry service name (default: 'duron') + }, +} +``` + +**Options:** +- `local` - Enable local span storage (`boolean | { flushDelayMs?: number }`) +- `traceExporter` - Export spans to external systems (OTLP, Jaeger, etc.) +- `spanProcessors` - Add custom span processors +- `serviceName` - Service name for OpenTelemetry resource (default: `'duron'`) + ## Lifecycle Methods ### `start()` @@ -257,6 +276,92 @@ const newJobId = await client.retryJob(jobId) **Returns:** Promise resolving to the new job ID, or `null` if retry failed +### `runActionAndWait(actionName, input?, options?)` + +Run an action and wait for its completion. This is a convenience method that combines `runAction` and `waitForJob`. + +```ts +const result = await client.runActionAndWait('sendEmail', { + to: 'user@example.com', + subject: 'Hello', + body: 'Test', +}) + +console.log(result.status) // 'completed' | 'failed' | 'cancelled' +console.log(result.output) // Typed based on action's output schema +``` + +**Parameters:** +- `actionName` - Name of the action to run +- `input` - Input data for the action (validated against action's input schema) +- `options.signal` - Optional AbortSignal to cancel the operation. If aborted, the job will be cancelled and the promise will reject. +- `options.timeout` - Optional timeout in milliseconds. If the job doesn't complete within this time, the job will be cancelled and the promise will reject. + +**Returns:** Promise resolving to the job result with typed input and output + +**Example:** + +```ts +// With timeout +const result = await client.runActionAndWait('processOrder', { orderId: '123' }, { + timeout: 30000, // 30 seconds +}) + +// With abort signal +const controller = new AbortController() +const result = await client.runActionAndWait('processOrder', { orderId: '123' }, { + signal: controller.signal, +}) + +// Cancel the operation +controller.abort() +``` + +### `timeTravelJob(jobId, stepId)` + +Time travel a job to restart from a specific step. The job must be in completed, failed, or cancelled status. This resets the job and ancestor steps to active status, deletes subsequent steps, and preserves completed parallel siblings. + +```ts +const success = await client.timeTravelJob(jobId, stepId) +``` + +**Parameters:** +- `jobId` - The ID of the job to time travel +- `stepId` - The ID of the step to restart from + +**Returns:** Promise resolving to `true` if time travel succeeded, `false` otherwise + +### `deleteJob(jobId)` + +Delete a job by its ID. Active jobs cannot be deleted. + +```ts +const deleted = await client.deleteJob(jobId) +``` + +**Parameters:** +- `jobId` - The ID of the job to delete + +**Returns:** Promise resolving to `true` if deleted, `false` otherwise + +### `deleteJobs(options?)` + +Delete multiple jobs using the same filters as `getJobs`. Active jobs cannot be deleted and will be excluded from deletion. + +```ts +const deletedCount = await client.deleteJobs({ + filters: { + status: ['completed', 'failed'], + actionName: ['sendEmail'], + }, +}) +``` + +**Parameters:** +- `options` - Query options including filters (same as `getJobs`) + +**Returns:** Promise resolving to the number of jobs deleted + ## Query Methods ### `getJobById(jobId)` @@ -285,6 +390,7 @@ const result = await client.getJobs({ actionName: ['sendEmail'], groupKey: ['user-123'], clientId: ['worker-1'], + description: 'order', // Fuzzy match on description createdAt: [new Date('2024-01-01'), new Date('2024-12-31')], startedAt: [new Date('2024-01-01'), new Date('2024-12-31')], finishedAt: [new Date('2024-01-01'), new Date('2024-12-31')], @@ -311,6 +417,7 @@ const result = await client.getJobs({ - `actionName` - Array of action names - `groupKey` - Array of group keys - `clientId` - Array of client IDs +- `description` - Filter by job description (fuzzy match) - `createdAt` - Date range `[start, end]` - `startedAt` - Date range `[start, end]` - `finishedAt` - Date range `[start, end]` @@ -320,7 +427,7 @@ const result = await client.getJobs({ - `outputFilter` - JSONB filter on output data **Sort Options:** -- `field` - Field to sort by: `'createdAt'`, `'startedAt'`, `'finishedAt'`, `'updatedAt'`, `'status'`, `'actionName'` +- `field` - Field to sort by: `'createdAt'`, `'startedAt'`, `'finishedAt'`, `'updatedAt'`, `'status'`, `'actionName'`, `'description'` - `order` - Sort order: `'asc'` or `'desc'` **Returns:** Promise resolving to jobs result with pagination info @@ -444,8 +551,51 @@ const metadata = await client.getActionsMetadata() **Returns:** Promise resolving to action metadata array +### `getSpans(options)` + +Get spans for a job or step. Only available when `telemetry.local` is enabled. + +```ts +// Get spans for a job +const result = await client.getSpans({ + jobId: 'job-uuid', + filters: { + name: ['ai.'], // Filter by span name prefix + }, + sort: { field: 'startTimeUnixNano', order: 'asc' }, +}) + +// Get spans for a step +const result = await client.getSpans({ + stepId: 'step-uuid', +}) +``` + +**Parameters:** +- `options.jobId` - The ID of the job to get spans for +- `options.stepId` - The ID of the step to get spans for +- `options.filters.name` - Filter by span name (string or array) +- `options.filters.kind` - Filter by span kind +- `options.filters.traceId` - Filter by trace ID +- `options.filters.attributesFilter` - JSONB filter on attributes +- `options.sort` - Sort options with `field` and `order` + +**Returns:** Promise resolving to spans result + +**Throws:** Error if local telemetry is not enabled + ## Utility Methods +### `spansEnabled` + +Boolean property indicating if local spans are enabled. This is `true` when `telemetry.local` is configured. + +```ts +if (client.spansEnabled) { + const spans = await client.getSpans({ jobId }) +} +``` + ### `getConfig()` Get the current configuration of this Duron instance. @@ -458,8 +608,35 @@ const config = client.getConfig() ### `logger` -Access the logger instance. +Access the Pino logger instance. ```ts client.logger.info('Client started') ``` + +### `tracer` + +Access the OpenTelemetry tracer for creating custom spans. Always returns a tracer — it's a no-op tracer when no telemetry SDK is configured. + +```ts +const span = client.tracer.startSpan('custom-operation') +``` + +### `database` + +Access the database adapter instance. + +```ts +const adapter = client.database +``` + +### `flushTelemetry()` + +Force flush any pending telemetry data. Useful in tests or when you need to ensure spans are exported before querying. + +```ts +await client.flushTelemetry() + +// Now you can safely query spans +const { spans } = await client.getSpans({ jobId }) +``` diff --git a/packages/docs/content/docs/dashboard.mdx b/packages/docs/content/docs/dashboard.mdx new file mode 100644 index 0000000..60d9d85 --- /dev/null +++ b/packages/docs/content/docs/dashboard.mdx @@ -0,0 +1,325 @@ +--- +title: Dashboard +description: Monitor and manage Duron jobs with a beautiful React dashboard +icon: LayoutDashboard +--- + +Duron Dashboard is a beautiful React dashboard for monitoring and managing Duron jobs in real-time. It can be used as a React component library or as a standalone HTML application served by your backend. + +## Installation + +```bash +# Using bun +bun add duron-dashboard + +# Using npm +npm install duron-dashboard + +# Using pnpm +pnpm add duron-dashboard + +# Using yarn +yarn add duron-dashboard +``` + +## Usage + +### As an Inline HTML Application + +The most common way to use the dashboard is with the `getHTML` function, which generates a fully inlined HTML application with all assets bundled. + +```ts +import { serve } from 'bun' +import { duron } from 'duron' +import { postgresAdapter } from 'duron/adapters/postgres' +import { createServer } from 'duron/server' +import { getHTML } from 'duron-dashboard/get-html' + +// Create Duron client +const client = duron({ + database: postgresAdapter({ + connection: process.env.DATABASE_URL!, + }), + actions: { sendEmail }, +}) + +await client.start() + +// Create API server +const api = createServer({ + client, + prefix: '/api', +}) + +// Serve dashboard and API +serve({ + port: 3000, + routes: { + '/': async () => { + const html = await getHTML({ + url: 'http://localhost:3000/api', + enableLogin: false, + showLogo: true, + theme: 'system', + }) + return new Response(html, { + headers: { 'Content-Type': 'text/html' }, + }) + }, + '/api/*': api.fetch, + }, +}) + +console.log('Dashboard running at http://localhost:3000') +``` + +### As a React Component + +Import and use the `DuronDashboard` component in your React application: + +```tsx +import { DuronDashboard } from 'duron-dashboard' +import 'duron-dashboard/index.css' + +function App() { + return +} +``` + +## getHTML Options + +The `getHTML` function accepts the following options: + +### `url` + +**Required.** The base URL for the Duron API. + +```ts +{ + url: 'http://localhost:3000/api', +} +``` + +### `enableLogin` + +**Required.** Enable authentication flow (login/logout) in the dashboard. Set to `true` if your server has login configured. + +```ts +{ + enableLogin: true, +} +``` + +### `showLogo` + +**Required.** Controls whether the Duron logo is shown in the navbar. + +```ts +{ + showLogo: true, +} +``` + +### `theme` + +**Optional.** The theme to use for the dashboard. Defaults to `'system'`. + +- `'light'` - Always use light theme +- `'dark'` - Always use dark theme +- `'system'` - Use the system preference + +```ts +{ + theme: 'dark', +} +``` + +## Features + +### Job List + +The main dashboard view shows a paginated list of all jobs with: + +- **Status badges** - Visual indicators for job status (active, completed, failed, cancelled) +- **Description** - Dynamic job description with tooltip for long text (when defined on the action) +- **Action filtering** - Filter jobs by action name +- **Status filtering** - Filter by job status +- **Date filtering** - Filter by creation, start, or finish dates +- **Search** - Fuzzy search across job data (including description) +- **Sorting** - Sort by various fields in ascending or descending order + +### Job Details + +Click on any job to view detailed information: + +- **Input/Output** - View the job's input and output data as formatted JSON +- **Error details** - Inspect error information for failed jobs +- **Step list** - View all steps executed within the job +- **Timeline** - Visual timeline of job and step execution +- **Metadata** - Job ID, action name, group key, description, timestamps, etc. + +### Step Details + +Click on any step to view: + +- **Status** - Current step status +- **Output** - Step output data +- **Error** - Error information for failed steps +- **Retry history** - Previous failed attempts with error details +- **Timing** - Start time, finish time, duration + +### Job Actions + +From the job details view, you can: + +- **Cancel** - Cancel an active job +- **Retry** - Retry a failed job (creates a new job with the same input) +- **Time Travel** - Restart a completed/failed job from a specific step +- **Delete** - Delete a job (only for non-active jobs) + +### Action Runner + +Create and run new jobs directly from the dashboard: + +1. Click the "Create Job" button +2. Select an action from the dropdown +3. Edit the auto-generated mock input (based on the action's Zod schema) +4. Click "Run" to create the job + +### Spans Visualization + +When telemetry is enabled (`telemetry.local: true`), the dashboard shows a "Spans" button on job and step details. This displays: + +- OpenTelemetry spans associated with the job/step +- Custom metrics recorded via `ctx.telemetry.recordMetric()` +- Span events and attributes +- Hierarchical span relationships + +## Framework Examples + +### Elysia + +```ts +import { Elysia } from 'elysia' +import { createServer } from 'duron/server' +import { getHTML } from 'duron-dashboard/get-html' + +const api = createServer({ client }) + +const app = new Elysia() + .use(api) + .get('/', async () => { + const html = await getHTML({ + url: 'http://localhost:3000/api', + enableLogin: false, + showLogo: true, + }) + return new Response(html, { + headers: { 'Content-Type': 'text/html' }, + }) + }) + .listen(3000) +``` + +### Hono + +```ts +import { Hono } from 'hono' +import { getHTML } from 'duron-dashboard/get-html' + +const app = new Hono() + +app.get('/dashboard', async (c) => { + const html = await getHTML({ + url: 'https://api.example.com/api', + enableLogin: true, + showLogo: true, + }) + return c.html(html) +}) +``` + +### Express + +```ts +import express from 'express' +import { getHTML } from 'duron-dashboard/get-html' + +const app = express() + +app.get('/dashboard', async (req, res) => { + const html = await getHTML({ + url: 'https://api.example.com/api', + enableLogin: true, + showLogo: true, + }) + res.send(html) +}) +``` + +### Fastify + +```ts +import Fastify from 'fastify' +import { getHTML } from 'duron-dashboard/get-html' + +const app = Fastify() + +app.get('/dashboard', async (request, reply) => { + const html = await getHTML({ + url: 'https://api.example.com/api', + enableLogin: true, + showLogo: true, + }) + reply.type('text/html').send(html) +}) +``` + +## Authentication + +When your Duron server has login configured, set `enableLogin: true` in the dashboard options. Users will be prompted to log in when accessing the dashboard. + +```ts +// Server with authentication +const api = createServer({ + client, + login: { + onLogin: async ({ email, password }) => { + return email === 'admin@example.com' && password === 'secret' + }, + jwtSecret: process.env.JWT_SECRET!, + }, +}) + +// Dashboard with login enabled +const html = await getHTML({ + url: 'http://localhost:3000/api', + enableLogin: true, + showLogo: true, +}) +``` + +The dashboard handles: + +- Login form with email/password +- Token storage in localStorage +- Automatic token refresh +- Logout functionality + +## Responsive Design + +The dashboard is fully responsive and works on: + +- **Desktop** - Full-featured experience with side panels +- **Tablet** - Adapted layout with collapsible panels +- **Mobile** - Stack layout optimized for touch + +## Dark Mode + +The dashboard includes built-in dark mode support: + +- **System** (default) - Follows the operating system preference +- **Light** - Always use light theme +- **Dark** - Always use dark theme + +Users can toggle the theme using the theme button in the navbar. diff --git a/packages/docs/content/docs/error-handling.mdx b/packages/docs/content/docs/error-handling.mdx index 7333878..aa3bacc 100644 --- a/packages/docs/content/docs/error-handling.mdx +++ b/packages/docs/content/docs/error-handling.mdx @@ -276,7 +276,142 @@ Errors are automatically serialized and stored in the database. The serializatio - `name` - Error class name - `message` - Error message +- `code` - Error code (for Duron errors) +- `nonRetriable` - Whether the error prevents retries +- `metadata` - Context metadata (jobId, stepId, actionName, etc.) - `cause` - Underlying cause (if any) - `stack` - Stack trace (if available) This allows you to inspect errors later through the API or dashboard. + +## Built-in Error Types + +Duron provides several built-in error types for different scenarios. All Duron errors extend the base `DuronError` class. + +### UnhandledChildStepsError + +Thrown when a parent step completes with unhandled (non-awaited) child steps. This extends `NonRetriableError`. + +```ts +import { UnhandledChildStepsError } from 'duron' + +// This error is thrown automatically by Duron when: +await ctx.step('parent', async ({ step }) => { + step('child1', async () => { /* ... */ }) // Not awaited! + step('child2', async () => { /* ... */ }) // Not awaited! + return { done: true } // Throws UnhandledChildStepsError +}) + +// Always await all child steps: +await ctx.step('parent', async ({ step }) => { + await step('child1', async () => { /* ... */ }) + await step('child2', async () => { /* ... */ }) + return { done: true } +}) +``` + +### StepAlreadyExecutedError + +Thrown when attempting to execute a step that has already been executed (same step name within the same job). + +```ts +// This error is thrown automatically when you try to run the same step twice +await ctx.step('fetchUser', async () => { /* ... */ }) +await ctx.step('fetchUser', async () => { /* ... */ }) // Throws StepAlreadyExecutedError +``` + +### ActionTimeoutError + +Thrown when an action exceeds its configured timeout. This is a non-retriable error. + +```ts +const myAction = defineAction()({ + name: 'myAction', + expire: 5000, // 5 second timeout + handler: async (ctx) => { + // If this takes longer than 5 seconds, ActionTimeoutError is thrown + await ctx.step('slowOperation', async () => { + await sleep(10000) // Will timeout + }) + }, +}) +``` + +### StepTimeoutError + +Thrown when a step exceeds its configured timeout. Unlike `ActionTimeoutError`, this error is retriable by default. + +```ts +await ctx.step('fetchData', async () => { + // If this takes too long, StepTimeoutError is thrown + await fetch(url) +}, { + expire: 5000, // 5 second timeout for this step +}) +``` + +### ActionCancelError + +Thrown when an action is cancelled (e.g., via `client.cancelJob()`). This is a non-retriable error. + +```ts +// When you cancel a job, the action receives this error +await client.cancelJob(jobId) +``` + +## Error Utility Functions + +Duron exports utility functions for checking error types without using `instanceof`: + +```ts +import { + isDuronError, + isNonRetriableError, + isUnhandledChildStepsError, + isTimeoutError, + isCancelError, + serializeError, +} from 'duron/errors' + +try { + await ctx.step('operation', async () => { /* ... */ }) +} catch (error) { + if (isDuronError(error)) { + console.log('Duron error code:', error.code) + console.log('Metadata:', error.metadata) + } + + if (isNonRetriableError(error)) { + // Error will not be retried + } + + if (isTimeoutError(error)) { + // ActionTimeoutError or StepTimeoutError + } + + if (isCancelError(error)) { + // ActionCancelError + } + + if (isUnhandledChildStepsError(error)) { + console.log('Pending child steps:', error.pendingCount) + } + + // Serialize error for storage or logging + const serialized = serializeError(error) +} +``` + +### Error Codes + +Each Duron error has a `code` property for identification: + +| Error Type | Code | +|------------|------| +| `DuronError` (base) | `DURON_ERROR` | +| `StepAlreadyExecutedError` | `STEP_ALREADY_EXECUTED` | +| `NonRetriableError` | `NON_RETRIABLE` | +| `ActionTimeoutError` | `ACTION_TIMEOUT` | +| `StepTimeoutError` | `STEP_TIMEOUT` | +| `ActionCancelError` | `ACTION_CANCEL` | +| `UnhandledChildStepsError` | `UNHANDLED_CHILD_STEPS` | diff --git a/packages/docs/content/docs/getting-started.mdx b/packages/docs/content/docs/getting-started.mdx index 95f633c..0fedc4d 100644 --- a/packages/docs/content/docs/getting-started.mdx +++ b/packages/docs/content/docs/getting-started.mdx @@ -136,4 +136,5 @@ console.log(`Job created with ID: ${jobId}`) - Learn about [Actions](/docs/actions) - the building blocks of Duron - Understand [Jobs and Steps](/docs/jobs-and-steps) - how work is organized - Explore [Adapters](/docs/adapters) - database backends +- Add [Telemetry](/docs/telemetry) - observability for your job queue - Check out [Examples](/docs/examples) - real-world use cases diff --git a/packages/docs/content/docs/index.mdx b/packages/docs/content/docs/index.mdx index 577441e..38875c8 100644 --- a/packages/docs/content/docs/index.mdx +++ b/packages/docs/content/docs/index.mdx @@ -15,6 +15,26 @@ Duron is a powerful, type-safe job queue system built for Node.js and Bun.js. It - **Observable** - Track jobs and steps with detailed status information - **Simple** - Easy-to-use API with sensible defaults +## Dashboard + +Duron comes with a built-in dashboard for real-time job monitoring and management. + +import jobList from '@assets/job-list.png' +import jobOutput from '@assets/job-output.png' +import timelineView from '@assets/timeline-view.png' + +### Job List + +Job List + +### Job Output + +Job Output + +### Timeline View + +Timeline View + ## Quick Start ```ts diff --git a/packages/docs/content/docs/jobs-and-steps.mdx b/packages/docs/content/docs/jobs-and-steps.mdx index c0d82b4..4f74773 100644 --- a/packages/docs/content/docs/jobs-and-steps.mdx +++ b/packages/docs/content/docs/jobs-and-steps.mdx @@ -39,6 +39,7 @@ Each job has the following properties: - **`id`** - Unique identifier (UUID) - **`actionName`** - Name of the action - **`groupKey`** - Group key for concurrency control +- **`description`** - Dynamic description of the job (if defined on the action) - **`status`** - Current status - **`input`** - Input data passed to the action - **`output`** - Output data returned by the action (if completed) @@ -125,15 +126,298 @@ const processOrder = defineAction()({ The step handler receives a context object with: - **`signal`** - AbortSignal for cancellation support +- **`stepId`** - Unique identifier for the current step +- **`parentStepId`** - ID of the parent step (null for root steps) +- **`step`** - Function to create nested inline child steps +- **`run`** - Function to execute reusable step definitions +- **`telemetry`** - Telemetry context for metrics and tracing (see [Telemetry](/docs/telemetry)) ```ts -await ctx.step('fetchData', async ({ signal }) => { +await ctx.step('fetchData', async ({ signal, stepId, parentStepId, run }) => { + console.log(`Executing step ${stepId}, parent: ${parentStepId}`) + // Use signal for cancellation const response = await fetch(url, { signal }) + + // Call a step definition from within an inline step + await run(sendEmailStep, { to: 'user@example.com', body: 'Done!' }) + return response.json() }) ``` +### Nested Steps + +Steps can create child steps using the `step` function available in the step handler context. This is useful for breaking down complex operations into smaller, trackable units. + +```ts +const processOrder = defineAction()({ + name: 'processOrder', + input: z.object({ orderId: z.string() }), + handler: async (ctx) => { + const result = await ctx.step('process', async ({ step, stepId }) => { + console.log('Processing step:', stepId) + + // Create child steps - they inherit the parent's abort signal + const validation = await step('validate', async ({ parentStepId }) => { + // parentStepId links back to the 'process' step + console.log('Parent step:', parentStepId) + return { valid: true } + }) + + // Child steps can also be nested further + const payment = await step('charge', async ({ step: nestedStep }) => { + const auth = await nestedStep('authorize', async () => { + return { authCode: '123' } + }) + return { charged: true, authCode: auth.authCode } + }) + + return { success: validation.valid && payment.charged } + }) + + return result + }, +}) +``` + +#### Abort Signal Propagation + +Child steps automatically inherit the abort signal chain from their parent. When a parent step is cancelled or times out, all child steps receive the abort signal. + +```ts +await ctx.step('parent', async ({ step, signal }) => { + // If this step times out, all children are also aborted + const child1 = await step('child1', async ({ signal: childSignal }) => { + // childSignal is linked to the parent's signal + return await fetch(url, { signal: childSignal }) + }) + + return child1 +}) +``` + +#### Important: Await All Child Steps + +All child steps **must** be awaited before the parent step returns. If a parent step completes with unawaited children, Duron will: + +1. Abort all pending child steps +2. Wait for them to settle +3. Throw an `UnhandledChildStepsError` + +```ts +// ❌ Bad: Unawaited child steps +await ctx.step('parent', async ({ step }) => { + step('child1', async () => { /* ... */ }) // Not awaited! + step('child2', async () => { /* ... */ }) // Not awaited! + return { done: true } // Throws UnhandledChildStepsError +}) + +// ✅ Good: All children awaited +await ctx.step('parent', async ({ step }) => { + await step('child1', async () => { /* ... */ }) + await step('child2', async () => { /* ... */ }) + return { done: true } +}) + +// ✅ Good: Parallel execution with Promise.all +await ctx.step('parent', async ({ step }) => { + const [result1, result2] = await Promise.all([ + step('child1', async () => { /* ... */ }), + step('child2', async () => { /* ... */ }), + ]) + return { result1, result2 } +}) +``` + +#### Step Hierarchy in the Dashboard + +Nested steps are displayed hierarchically in the Duron Dashboard. Each step shows its parent relationship, making it easy to understand the execution flow of complex workflows. + +## Reusable Step Definitions + +For steps that are used across multiple actions or need to be called multiple times with different inputs, you can create **reusable step definitions** using `createStep()`. + +### Creating a Reusable Step + +Use `createStep()` to define a step with its own input schema, options, and handler: + +```ts +import { createStep } from 'duron' +import { z } from 'zod' + +const sendEmailStep = createStep()({ + name: 'send-email', + input: z.object({ + to: z.string().email(), + subject: z.string(), + body: z.string(), + }), + retry: { limit: 3 }, + expire: 60000, + handler: async (ctx) => { + // ctx includes all step context plus action-level properties + const result = await ctx.var.emailService.send({ + to: ctx.input.to, + subject: ctx.input.subject, + body: ctx.input.body, + }) + return { success: true, messageId: result.id } + }, +}) +``` + +### Step Definition Context + +The handler receives an extended context that includes: + +- **`input`** - The validated input data based on the step's input schema +- **`signal`** - AbortSignal for cancellation support +- **`stepId`** - Unique identifier for the current step +- **`parentStepId`** - ID of the parent step (null for root steps) +- **`step`** - Function to create nested inline child steps +- **`run`** - Function to execute other reusable step definitions +- **`telemetry`** - Telemetry context for metrics and tracing +- **`var`** - Variables from the action context +- **`logger`** - Pino logger instance +- **`jobId`** - The job ID this step belongs to + +### Executing Step Definitions + +Use `ctx.run()` to execute a step definition within an action handler: + +```ts +const processOrder = defineAction()({ + name: 'processOrder', + handler: async (ctx) => { + // Execute the reusable step + const emailResult = await ctx.run(sendEmailStep, { + to: ctx.input.email, + subject: 'Order Confirmation', + body: `Your order ${ctx.input.orderId} has been placed.`, + }) + + return { success: emailResult.success } + }, +}) +``` + +### Overriding Step Options + +You can override step options at call time: + +```ts +// Override expire and retry at call time +const result = await ctx.run( + sendEmailStep, + { to: 'user@example.com', subject: 'Hello', body: 'World' }, + { expire: 30000, retry: { limit: 5 } } +) +``` + +### Dynamic Step Names + +Use a function for the `name` property to generate unique step names based on input. This is essential when calling the same step definition multiple times within a single job. + +The name function receives a context object with: + +- **`input`** - The validated step input +- **`var`** - Variables from the action context +- **`jobId`** - The job ID this step belongs to +- **`parentStepId`** - The ID of the parent step (null for root steps) + +```ts +const sendEmailStep = createStep()({ + // Dynamic name based on input and context + name: (ctx) => `send-email-${ctx.input.to}`, + input: z.object({ + to: z.string().email(), + body: z.string(), + }), + handler: async (ctx) => { + // ... + }, +}) + +// Now you can call it multiple times with different inputs +const processOrder = defineAction()({ + name: 'processOrder', + handler: async (ctx) => { + // Each call creates a uniquely named step + await ctx.run(sendEmailStep, { to: 'customer@example.com', body: 'Confirmation' }) + await ctx.run(sendEmailStep, { to: 'admin@example.com', body: 'Notification' }) + return { success: true } + }, +}) +``` + +### Nested Steps in Step Definitions + +Step definitions can create nested child steps using `ctx.step()`: + +```ts +const processPaymentStep = createStep()({ + name: 'process-payment', + input: z.object({ amount: z.number(), currency: z.string() }), + handler: async (ctx) => { + // Create nested child steps + const auth = await ctx.step('authorize', async () => { + return await ctx.var.paymentGateway.authorize(ctx.input.amount) + }) + + const capture = await ctx.step('capture', async () => { + return await ctx.var.paymentGateway.capture(auth.transactionId) + }) + + return { success: true, transactionId: capture.id } + }, +}) +``` + +### Composing Step Definitions + +Step definitions can call other step definitions using `ctx.run()`. This enables powerful composition patterns where you can build complex workflows from reusable building blocks: + +```ts +// Inner step definition +const validateOrderStep = createStep()({ + name: (ctx) => `validate-order-${ctx.input.orderId}`, + input: z.object({ orderId: z.string() }), + handler: async (ctx) => { + const order = await ctx.var.db.getOrder(ctx.input.orderId) + return { valid: order.items.length > 0, total: order.total } + }, +}) + +// Outer step that calls inner step via ctx.run() +const processOrderStep = createStep()({ + name: (ctx) => `process-order-${ctx.input.orderId}`, + input: z.object({ orderId: z.string() }), + handler: async (ctx) => { + // Call another step definition from within this step definition + const validation = await ctx.run(validateOrderStep, { + orderId: ctx.input.orderId, + }) + + if (!validation.valid) { + throw new Error('Invalid order') + } + + return { processed: true, total: validation.total } + }, +}) + +// Use in an action +const orderAction = defineAction()({ + name: 'orderAction', + handler: async (ctx) => { + return ctx.run(processOrderStep, { orderId: ctx.input.orderId }) + }, +}) +``` + +This pattern supports arbitrary nesting depths. Each step definition call creates its own step in the database for tracking and observability. + ### Step Options You can configure steps with options: @@ -181,6 +465,34 @@ Retry configuration for the step. See [Retries](/docs/retries) for details. } ``` +#### `parallel` + +Whether this step runs independently from its siblings. Defaults to `false`. + +When a step is marked as `parallel: true`, it is treated as independent during time travel operations. If you time travel to a specific step, completed parallel siblings are preserved rather than deleted. This is useful for steps that don't depend on each other and can safely coexist. + +```ts +await ctx.step( + 'sendNotification', + async ({ signal }) => { + return await sendNotification({ signal }) + }, + { parallel: true } +) +``` + +You can also set `parallel` on reusable step definitions: + +```ts +const notifyStep = createStep()({ + name: 'notify', + parallel: true, + handler: async (ctx) => { + // This step is independent from siblings + }, +}) +``` + ### Step Lifecycle Steps go through the following states: @@ -196,16 +508,21 @@ Each step has the following properties: - **`id`** - Unique identifier (UUID) - **`jobId`** - ID of the parent job +- **`parentStepId`** - ID of the parent step (null for root steps) - **`name`** - Name of the step - **`status`** - Current status -- **`input`** - Input data (serialized) - **`output`** - Output data (if completed) - **`error`** - Error information (if failed) -- **`createdAt`** - Timestamp when step was created +- **`timeoutMs`** - Timeout in milliseconds +- **`expiresAt`** - Expiration timestamp +- **`retriesLimit`** - Maximum retry attempts +- **`retriesCount`** - Current retry count +- **`delayedMs`** - Delay before next retry (if retrying) +- **`historyFailedAttempts`** - Record of previous failed attempts - **`startedAt`** - Timestamp when step started - **`finishedAt`** - Timestamp when step finished +- **`createdAt`** - Timestamp when step was created - **`updatedAt`** - Timestamp when step was last updated -- **`clientId`** - ID of the Duron instance that owns the step ### Querying Steps @@ -226,7 +543,7 @@ const step = await client.getJobStepById(stepId) ### Step Concurrency -Steps within an action respect the `steps.concurrency` limit. By default, up to 10 steps can run concurrently per action. +Steps within an action respect the `steps.concurrency` limit. By default, up to 100 steps can run concurrently per action. ```ts { diff --git a/packages/docs/content/docs/meta.json b/packages/docs/content/docs/meta.json index 563fde5..6001395 100644 --- a/packages/docs/content/docs/meta.json +++ b/packages/docs/content/docs/meta.json @@ -6,7 +6,9 @@ "jobs-and-steps", "client-api", "server-api", + "dashboard", "adapters", + "telemetry", "retries", "error-handling", "multi-worker", diff --git a/packages/docs/content/docs/server-api.mdx b/packages/docs/content/docs/server-api.mdx index 0b5818c..b9a679b 100644 --- a/packages/docs/content/docs/server-api.mdx +++ b/packages/docs/content/docs/server-api.mdx @@ -37,7 +37,7 @@ const app = createServer({ return email === 'admin@example.com' && password === 'password' }, jwtSecret: process.env.JWT_SECRET || 'your-secret-key', - expirationTime: '24h', // Optional, defaults to '24h' + expirationTime: '1h', // Optional, defaults to '1h' }, }) @@ -112,17 +112,41 @@ app.listen(3000) #### `login.expirationTime` -**Optional.** Expiration time for the JWT token. Defaults to `'24h'`. Accepts any valid time string format (e.g., `'1h'`, `'7d'`, `'30m'`). +**Optional.** Expiration time for the access JWT token. Defaults to `'1h'`. Accepts any valid time string format (e.g., `'1h'`, `'7d'`, `'30m'`). ```ts { login: { jwtSecret: process.env.JWT_SECRET || 'your-secret-key', - expirationTime: '7d', // Token expires in 7 days + expirationTime: '30m', // Access token expires in 30 minutes }, } ``` +#### `login.refreshTokenExpirationTime` + +**Optional.** Expiration time for the refresh token. Defaults to `'7d'`. Accepts any valid time string format. + +```ts +{ + login: { + jwtSecret: process.env.JWT_SECRET || 'your-secret-key', + expirationTime: '1h', + refreshTokenExpirationTime: '30d', // Refresh token expires in 30 days + }, +} +``` + +### `spansEnabled` + +**Optional.** Enable spans endpoints (`/jobs/:id/spans`, `/steps/:id/spans`). Only works when client is configured with `telemetry.local` enabled. When `true`, enables the dashboard to show spans buttons. Defaults to auto-detected from `client.spansEnabled`. + +```ts +{ + spansEnabled: true, +} +``` + ## API Endpoints All endpoints use Zod for input and response validation. @@ -131,7 +155,7 @@ All endpoints use Zod for input and response validation. #### `POST /login` -Authenticate and receive a JWT token. +Authenticate and receive JWT tokens. **Request Body:** ```json @@ -144,7 +168,8 @@ Authenticate and receive a JWT token. **Response:** ```json { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` @@ -166,6 +191,7 @@ Get jobs with pagination, filtering, and sorting. - `fActionName` - Filter by action name (comma-separated or multiple params) - `fGroupKey` - Filter by group key (comma-separated or multiple params) - `fClientId` - Filter by client ID (comma-separated or multiple params) +- `fDescription` - Filter by job description (fuzzy match) - `fCreatedAt` - Filter by created date (ISO string or JSON array `[start, end]`) - `fStartedAt` - Filter by started date (ISO string or JSON array `[start, end]`) - `fFinishedAt` - Filter by finished date (ISO string or JSON array `[start, end]`) @@ -173,7 +199,7 @@ Get jobs with pagination, filtering, and sorting. - `fSearch` - Fuzzy search in job data - `fInputFilter` - JSONB filter on input (JSON string) - `fOutputFilter` - JSONB filter on output (JSON string) -- `sort` - Sort string format: `"field:asc,field:desc"` +- `sort` - Sort string format: `"field:asc,field:desc"` (fields: `createdAt`, `startedAt`, `finishedAt`, `status`, `actionName`, `expiresAt`, `duration`, `description`) **Example:** ``` @@ -187,6 +213,7 @@ GET /api/jobs?page=1&pageSize=20&fStatus=active,completed&sort=createdAt:desc { "id": "uuid", "actionName": "sendEmail", + "description": "Send email to user@example.com", "status": "completed", "input": { "to": "user@example.com" }, "output": { "success": true }, @@ -214,6 +241,7 @@ Get a job by its ID. { "id": "uuid", "actionName": "sendEmail", + "description": "Send email to user@example.com", "status": "completed", "input": { "to": "user@example.com" }, "output": { "success": true }, @@ -261,6 +289,56 @@ Retry a failed job. } ``` +#### `POST /jobs/:id/time-travel` + +Time travel a job to restart from a specific step. The job must be in completed, failed, or cancelled status. + +**Request Body:** +```json +{ + "stepId": "step-uuid" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Job uuid has been time traveled to step step-uuid" +} +``` + +#### `DELETE /jobs/:id` + +Delete a job by its ID. Active jobs cannot be deleted. + +**Response:** +```json +{ + "success": true, + "message": "Job uuid has been deleted" +} +``` + +**Status Codes:** +- `200` - Success +- `404` - Job not found or cannot be deleted (active jobs) + +#### `DELETE /jobs` + +Delete multiple jobs using the same query parameters as `GET /jobs`. Active jobs are excluded from deletion. + +**Query Parameters:** Same as `GET /jobs` + +**Response:** +```json +{ + "success": true, + "message": "Deleted 5 job(s)", + "deletedCount": 5 +} +``` + ### Steps #### `GET /jobs/:id/steps` @@ -329,6 +407,67 @@ Get step status and updatedAt timestamp. } ``` +### Spans + +Spans endpoints are only available when `spansEnabled` is true (auto-detected when `telemetry.local` is enabled on the client). + +#### `GET /jobs/:id/spans` + +Get spans for a job. + +**Query Parameters:** +- `fName` - Filter by span name (string or comma-separated) +- `fKind` - Filter by span kind (0=INTERNAL, 1=SERVER, 2=CLIENT, 3=PRODUCER, 4=CONSUMER) +- `fTraceId` - Filter by trace ID +- `fAttributesFilter` - JSONB filter on attributes (JSON string) +- `sort` - Sort string format: `"field:asc"` or `"field:desc"` + +**Response:** +```json +{ + "data": [ + { + "traceId": "abc123...", + "spanId": "def456...", + "parentSpanId": null, + "jobId": "job-uuid", + "stepId": "step-uuid", + "name": "step:fetchUser", + "kind": 0, + "startTimeUnixNano": "1704067200000000000", + "endTimeUnixNano": "1704067201000000000", + "statusCode": 1, + "statusMessage": null, + "attributes": { "duron.job.id": "job-uuid" }, + "events": [] + } + ], + "total": 1 +} +``` + +#### `GET /steps/:id/spans` + +Get spans for a step. + +**Query Parameters:** Same as `GET /jobs/:id/spans` + +**Response:** Same format as `GET /jobs/:id/spans` + +### Config + +#### `GET /config` + +Get server configuration. This endpoint does not require authentication. + +**Response:** +```json +{ + "spansEnabled": true, + "authEnabled": true +} +``` + ### Actions #### `GET /actions` diff --git a/packages/docs/content/docs/telemetry.mdx b/packages/docs/content/docs/telemetry.mdx new file mode 100644 index 0000000..6f2cb9a --- /dev/null +++ b/packages/docs/content/docs/telemetry.mdx @@ -0,0 +1,364 @@ +--- +title: Telemetry +description: Add observability to your job queue with OpenTelemetry tracing +icon: Activity +--- + +Duron provides built-in OpenTelemetry support for tracing job and step execution. You can store spans locally in the database, export to external systems, or both. + +## Configuration + +Configure telemetry when creating a Duron client: + +```ts +import { duron } from 'duron' +import { postgresAdapter } from 'duron/adapters/postgres' + +const client = duron({ + database: postgresAdapter({ + connection: process.env.DATABASE_URL, + }), + actions: { /* your actions */ }, + telemetry: { + local: true, // Store spans in the database + }, +}) +``` + +### Configuration Options + +| Option | Type | Description | +|--------|------|-------------| +| `local` | `boolean \| LocalTelemetryOptions` | Enable local span storage in the database | +| `traceExporter` | `SpanExporter` | Export spans to external systems (OTLP, Jaeger, etc.) | +| `spanProcessors` | `SpanProcessor[]` | Add custom span processors | +| `serviceName` | `string` | Service name for OpenTelemetry resource (default: `'duron'`) | + +### Local Storage + +Store spans in the Duron database for querying via the API or dashboard: + +```ts +// Simple: enable with defaults +telemetry: { local: true } + +// Custom flush delay (default: 5000ms) +telemetry: { local: { flushDelayMs: 10000 } } +``` + +### External Export + +Export spans to OpenTelemetry-compatible backends: + +```ts +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' + +telemetry: { + traceExporter: new OTLPTraceExporter({ + url: 'http://localhost:4318/v1/traces', + }), +} +``` + +### Combined + +Use both local storage and external export: + +```ts +telemetry: { + local: true, + traceExporter: new OTLPTraceExporter({ + url: 'http://localhost:4318/v1/traces', + }), +} +``` + +## Telemetry Context + +Both action and step handlers have access to `ctx.telemetry` which provides OpenTelemetry APIs: + +```ts +interface TelemetryContext { + // Get the active span for the current job/step (undefined if no span is active) + getActiveSpan(): Span | undefined + + // Get a tracer for creating custom spans + getTracer(name: string): Tracer + + // Start a new span as a child of the current job/step span + startSpan(name: string, options?: { attributes?: Record }): Span + + // Record a metric as a span event + recordMetric(name: string, value: number, attributes?: Record): void +} +``` + +### Recording Metrics + +Use `recordMetric()` to record numeric metrics as span events: + +```ts +const processOrder = defineAction()({ + name: 'process-order', + input: z.object({ orderId: z.string() }), + handler: async (ctx) => { + const startTime = Date.now() + + const result = await ctx.step('process', async ({ telemetry }) => { + // Record step-specific metrics + telemetry.recordMetric('items.count', 5) + telemetry.recordMetric('order.value', 99.99, { currency: 'USD' }) + + return { success: true } + }) + + // Record action-level metrics + ctx.telemetry.recordMetric('processing.duration_ms', Date.now() - startTime) + + return result + }, +}) +``` + +### Creating Custom Spans + +Use `getTracer()` to create custom spans. The tracer automatically links new spans to the current job/step trace: + +```ts +const result = await ctx.step('with-custom-span', async ({ telemetry }) => { + const tracer = telemetry.getTracer('my-service') + + return tracer.startActiveSpan('custom-operation', (span) => { + try { + span.setAttribute('custom.input', 'value') + // Do work... + span.setAttribute('custom.output', 'result') + return { success: true } + } finally { + span.end() + } + }) +}) +``` + +### Working with the Active Span + +Use `getActiveSpan()` to add attributes or events to the current span. It returns `undefined` if no span is active (e.g., when telemetry is not configured): + +```ts +const result = await ctx.step('annotated', async ({ telemetry }) => { + const span = telemetry.getActiveSpan() + + if (span) { + span.setAttribute('user.id', 'user-123') + span.addEvent('processing.started') + + // Do work... + + span.addEvent('processing.completed', { items: 5 }) + } + + return { success: true } +}) +``` + +### Integration with External Libraries + +Use `getTracer()` to get an OpenTelemetry tracer for external library integration. The tracer automatically links spans to the current job/step trace: + +```ts +import { generateText } from 'ai' +import { openai } from '@ai-sdk/openai' + +const aiAction = defineAction()({ + name: 'ai-generation', + input: z.object({ prompt: z.string() }), + handler: async (ctx) => { + const response = await ctx.step('generate', async ({ telemetry }) => { + // Pass the tracer to AI SDK - spans are automatically linked + const result = await generateText({ + prompt: ctx.input.prompt, + model: openai('gpt-4o-mini'), + experimental_telemetry: { + isEnabled: true, + tracer: telemetry.getTracer('ai'), + }, + }) + + return result.text + }) + + return { response } + }, +}) +``` + +The tracer returned by `getTracer()` is context-aware and automatically injects the parent span, so external libraries don't need to manually handle context propagation. + +### Using startSpan + +Use `startSpan()` as a shorthand to create a child span linked to the current job/step trace: + +```ts +const result = await ctx.step('with-child-span', async ({ telemetry }) => { + const span = telemetry.startSpan('custom-operation', { + attributes: { 'custom.input': 'value' }, + }) + + try { + // Do work... + span.addEvent('operation.complete') + return { success: true } + } catch (error) { + span.recordException(error) + throw error + } finally { + span.end() + } +}) +``` + +## Querying Spans + +When local telemetry is enabled, query spans via the client API: + +```ts +// Get spans for a specific job +const { spans, total } = await client.getSpans({ + jobId: 'job-id', + page: 1, + pageSize: 50, +}) + +// Get spans for a specific step +const { spans } = await client.getSpans({ + stepId: 'step-id', +}) + +// Filter by name pattern +const { spans } = await client.getSpans({ + jobId: 'job-id', + filters: { + name: 'ai.', // Matches spans starting with 'ai.' + }, +}) +``` + +### Span Structure + +Each span includes: + +```ts +interface Span { + traceId: string + spanId: string + parentSpanId: string | null + jobId: string | null + stepId: string | null + name: string + kind: SpanKind + startTimeUnixNano: bigint + endTimeUnixNano: bigint | null + statusCode: SpanStatusCode + statusMessage: string | null + attributes: Record + events: SpanEvent[] +} +``` + +## Automatic Tracing + +Duron automatically creates spans for: + +| Span Type | Name Format | Attributes | +|-----------|-------------|------------| +| Job | `job:{actionName}` | `duron.job.id`, `duron.action.name`, `duron.group.key` | +| Step | `step:{stepName}` | `duron.job.id`, `duron.step.id`, `duron.step.name`, `duron.step.parent_id` | + +Spans include: +- Start and end timestamps +- Duration +- Status (ok, error) +- Error details when applicable +- Parent-child relationships for nested steps + +## Best Practices + +### Use Descriptive Metric Names + +```ts +// Good: Namespaced and descriptive +telemetry.recordMetric('order.processing.duration_ms', duration) +telemetry.recordMetric('ai.tokens.total', tokens) + +// Bad: Generic and unclear +telemetry.recordMetric('time', duration) +telemetry.recordMetric('count', tokens) +``` + +### Add Context with Attributes + +```ts +telemetry.recordMetric('api.request.duration_ms', 250, { + endpoint: '/users', + method: 'GET', + statusCode: 200, +}) +``` + +### Track Costs and Resources + +```ts +// AI token usage +telemetry.recordMetric('ai.tokens.input', inputTokens) +telemetry.recordMetric('ai.tokens.output', outputTokens) +telemetry.recordMetric('ai.cost.usd', cost, { model: 'gpt-4o' }) +``` + +### Always End Custom Spans + +```ts +const tracer = telemetry.getTracer('my-service') +const span = tracer.startSpan('operation') +try { + // Do work... +} finally { + span.end() // Always end the span +} +``` + +## Installation + +For external OpenTelemetry export, install the required packages: + +```bash +# Core OpenTelemetry packages (included with duron) +# @opentelemetry/api and @opentelemetry/sdk-trace-node are peer dependencies + +# For OTLP export +bun add @opentelemetry/exporter-trace-otlp-http + +# For Jaeger +bun add @opentelemetry/exporter-jaeger +``` + +## Advanced: LocalSpanExporter + +If you need lower-level control over local span storage, you can use the `LocalSpanExporter` directly with a custom `SpanProcessor`: + +```ts +import { LocalSpanExporter } from 'duron/telemetry' + +const exporter = new LocalSpanExporter({ adapter: myAdapter }) +``` + +In most cases, use the `telemetry.local` option instead, which configures the `LocalSpanExporter` automatically. + +## Architecture Notes + +Duron's telemetry implementation: + +- **Does not globally register** the tracer provider, avoiding conflicts with other telemetry in your application +- Uses a **context-aware tracer wrapper** that automatically links spans to the correct trace hierarchy +- Stores spans with `duron.job.id` attribute for job association +- Queries spans by `trace_id` to include external library spans (like AI SDK) in job results diff --git a/packages/docs/src/styles/app.css b/packages/docs/src/styles/app.css index 21f0a2e..4ae0adf 100644 --- a/packages/docs/src/styles/app.css +++ b/packages/docs/src/styles/app.css @@ -3,59 +3,73 @@ @import 'fumadocs-ui/css/preset.css'; :root { - --background: oklch(95.78% 0.006 264.5); - --foreground: oklch(43.18% 0.043 279.3); - --muted: oklch(91.88% 0.007 268.5); - --muted-foreground: oklch(40.50% 0.023 265.7); - --popover: oklch(93.35% 0.009 264.5); - --popover-foreground: oklch(34.64% 0.031 279.7); - --card: oklch(94.22% 0.007 260.7); - --card-foreground: oklch(38.93% 0.038 278.5); - --border: oklch(91.88% 0.007 268.5); - --input: oklch(89.47% 0.008 271.3); - --primary: oklch(55.47% 0.250 297.0); - --primary-foreground: oklch(100.00% 0.000 89.9); - --secondary: oklch(77.13% 0.056 305.4); - --secondary-foreground: oklch(24.58% 0.045 303.3); - --accent: oklch(83.15% 0.024 264.4); - --accent-foreground: oklch(30.49% 0.031 263.9); - --destructive: oklch(48.29% 0.188 29.3); - --destructive-foreground: oklch(96.83% 0.014 17.4); - --ring: oklch(55.47% 0.250 297.0); - --chart-1: oklch(55.47% 0.250 297.0); - --chart-2: oklch(77.13% 0.056 305.4); - --chart-3: oklch(83.15% 0.024 264.4); - --chart-4: oklch(79.96% 0.050 305.2); - --chart-5: oklch(55.33% 0.256 296.4); - --radius: 0.5rem; + --radius: 0.65rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { - --background: oklch(32.91% 0.032 274.8); - --foreground: oklch(86.46% 0.051 273.1); - --muted: oklch(37.38% 0.023 275.0); - --muted-foreground: oklch(80.88% 0.016 273.8); - --popover: oklch(30.04% 0.030 273.6); - --popover-foreground: oklch(96.82% 0.012 276.1); - --card: oklch(30.89% 0.031 274.2); - --card-foreground: oklch(91.72% 0.031 271.7); - --border: oklch(38.52% 0.019 277.3); - --input: oklch(41.42% 0.019 273.5); - --primary: oklch(76.48% 0.111 311.7); - --primary-foreground: oklch(24.97% 0.089 309.1); - --secondary: oklch(34.18% 0.070 311.0); - --secondary-foreground: oklch(86.66% 0.035 312.4); - --accent: oklch(45.57% 0.050 274.1); - --accent-foreground: oklch(98.27% 0.003 286.4); - --destructive: oklch(64.41% 0.232 28.6); - --destructive-foreground: oklch(100.00% 0.000 89.9); - --ring: oklch(76.48% 0.111 311.7); - --chart-1: oklch(76.48% 0.111 311.7); - --chart-2: oklch(34.18% 0.070 311.0); - --chart-3: oklch(45.57% 0.050 274.1); - --chart-4: oklch(36.70% 0.078 310.9); - --chart-5: oklch(76.30% 0.117 312.0); - --radius: 0.5rem; + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } .logo-word { diff --git a/packages/duron-dashboard/build.ts b/packages/duron-dashboard/build.ts index 205f55f..123f294 100644 --- a/packages/duron-dashboard/build.ts +++ b/packages/duron-dashboard/build.ts @@ -46,6 +46,10 @@ const [indexResult, initResult] = await Promise.all([ format: 'esm', target: 'browser', sourcemap: 'linked', + minify: true, + define: { + 'process.env.NODE_ENV': '"production"', + }, external: [...Object.keys(pkg.dependencies), ...Object.keys(pkg.devDependencies)], }), Bun.build({ @@ -55,6 +59,9 @@ const [indexResult, initResult] = await Promise.all([ format: 'esm', minify: true, target: 'browser', + define: { + 'process.env.NODE_ENV': '"production"', + }, sourcemap: 'linked', }), ]) diff --git a/packages/duron-dashboard/package.json b/packages/duron-dashboard/package.json index d6d3e48..7fcf457 100644 --- a/packages/duron-dashboard/package.json +++ b/packages/duron-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "duron-dashboard", - "version": "0.2.0", + "version": "0.3.0-beta.13", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -28,7 +28,7 @@ ], "scripts": { "dev": "bun --hot src/dev.tsx", - "build": "bun run build.ts && bun run build:get-html", + "build": "NODE_ENV=production bun run build.ts && bun run build:get-html", "build:get-html": "tsc --project tsconfig.node.json", "prepublishOnly": "bun run build", "typecheck": "tsc --noEmit" @@ -41,6 +41,7 @@ "@maskito/core": "^4.0.1", "@maskito/kit": "^4.0.1", "@maskito/react": "^4.0.1", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -55,13 +56,13 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.10", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", - "@uiw/react-json-view": "2.0.0-alpha.37", + "@tanstack/react-virtual": "^3.13.18", "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "jsonata": "^2.1.0", "lucide-react": "^0.555.0", "motion": "^12.23.24", "nanoid": "^5.1.6", @@ -70,7 +71,9 @@ "react": "^19", "react-day-picker": "^9.11.2", "react-dom": "^19", + "react-resizable-panels": "^4.4.1", "tailwind-merge": "^3.4.0", + "usehooks-ts": "^3.1.1", "zod": "^4.1.12" }, "devDependencies": { diff --git a/packages/duron-dashboard/src/DuronDashboard.tsx b/packages/duron-dashboard/src/DuronDashboard.tsx index 705ed23..443704d 100644 --- a/packages/duron-dashboard/src/DuronDashboard.tsx +++ b/packages/duron-dashboard/src/DuronDashboard.tsx @@ -1,9 +1,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { NuqsAdapter } from 'nuqs/adapters/react' -import { ApiProvider } from './contexts/api-context' +import { ApiProvider, type CustomFetch } from './contexts/api-context' import { AuthProvider, useAuth } from './contexts/auth-context' -import { ThemeProvider } from './contexts/theme-context' +import { LayoutProvider } from './contexts/layout-context' +import { PollingProvider } from './contexts/polling-context' +import { SpansProvider } from './contexts/spans-context' +import { type ThemeOption, ThemeProvider } from './contexts/theme-context' import { Dashboard } from './views/dashboard' import Login from './views/login' @@ -19,9 +22,11 @@ const queryClient = new QueryClient({ interface AppContentProps { enableLogin?: boolean showLogo?: boolean + showThemeToggle?: boolean + className?: string } -function AppContent({ enableLogin = true, showLogo = true }: AppContentProps) { +function AppContent({ enableLogin = true, showLogo = true, showThemeToggle = true, className }: AppContentProps) { const { isAuthenticated } = useAuth() if (enableLogin && !isAuthenticated) { @@ -30,7 +35,12 @@ function AppContent({ enableLogin = true, showLogo = true }: AppContentProps) { return ( - + ) } @@ -48,17 +58,65 @@ export interface DuronDashboardProps { * Defaults to true. */ showLogo?: boolean + /** + * Controls whether the theme toggle button is shown in the navbar. + * Defaults to true. + */ + showThemeToggle?: boolean + /** + * The theme to use for the dashboard. + * - 'light': Always use light theme + * - 'dark': Always use dark theme + * - 'system': Use the system preference (default) + */ + theme?: ThemeOption + /** + * Custom fetch function to use for API requests. + * This allows you to intercept, modify, or wrap fetch calls. + * Defaults to the native fetch. + */ + customFetch?: CustomFetch + /** + * Custom className to apply to the root dashboard container. + * Merged with the default classes using clsx. + */ + className?: string + /** + * Polling interval in milliseconds for real-time updates. + * Defaults to 2000ms (2 seconds). + */ + pollingInterval?: number } -export function DuronDashboard({ url, enableLogin = false, showLogo = true }: DuronDashboardProps) { +export function DuronDashboard({ + url, + enableLogin = false, + showLogo = true, + showThemeToggle = true, + theme = 'system', + customFetch, + className, + pollingInterval, +}: DuronDashboardProps) { return ( - - - - - - + + + + + + + + + + + + ) diff --git a/packages/duron-dashboard/src/components/badge-status.tsx b/packages/duron-dashboard/src/components/badge-status.tsx index fdc23cc..572520f 100644 --- a/packages/duron-dashboard/src/components/badge-status.tsx +++ b/packages/duron-dashboard/src/components/badge-status.tsx @@ -11,27 +11,64 @@ const icons = { cancelled: Ban, } -const colors = { - created: 'bg-gray-100 text-gray-800 border-gray-800', - active: 'bg-blue-100 text-blue-800 border-blue-800', - completed: 'bg-green-100 text-green-800 border-green-800', - failed: 'bg-red-100 text-red-800 border-red-800', - cancelled: 'bg-yellow-100 text-yellow-800 border-yellow-800', -} +export const statusConfig = { + created: { + label: 'Created', + variant: 'outline', + badgeClassName: '', + iconClassName: '', + }, + active: { + label: 'Active', + variant: 'outline', + badgeClassName: + 'border-none bg-amber-600/10 text-amber-600 focus-visible:ring-amber-600/20 focus-visible:outline-none dark:bg-amber-400/10 dark:text-amber-400 dark:focus-visible:ring-amber-400/40 [a&]:hover:bg-amber-600/5 dark:[a&]:hover:bg-amber-400/5', + iconClassName: 'rounded-full bg-amber-600 dark:bg-amber-400', + }, + completed: { + label: 'Completed', + variant: 'outline', + badgeClassName: + 'border-none bg-green-600/10 text-green-600 focus-visible:ring-green-600/20 focus-visible:outline-none dark:bg-green-400/10 dark:text-green-400 dark:focus-visible:ring-green-400/40 [a&]:hover:bg-green-600/5 dark:[a&]:hover:bg-green-400/5', + iconClassName: 'rounded-full bg-green-600 dark:bg-green-400', + }, + failed: { + label: 'Failed', + variant: 'outline', + badgeClassName: + 'bg-destructive/10 [a&]:hover:bg-destructive/5 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive border-none focus-visible:outline-none', + iconClassName: 'bg-destructive rounded-full', + }, + cancelled: { + label: 'Cancelled', + variant: 'secondary', + badgeClassName: '', + iconClassName: '', + }, +} as const + +export type Status = keyof typeof statusConfig export function BadgeStatus({ status, justIcon = false }: { status: string; justIcon?: boolean }) { - const Icon = icons[status as keyof typeof icons] - const color = colors[status as keyof typeof colors] + const Icon = icons[status as Status] + const config = statusConfig[status as Status] + + if (!config) { + return {status.charAt(0).toUpperCase() + status.slice(1)} + } + if (justIcon) { return ( - {Icon && } + + {Icon && } + ) } return ( - - {Icon && } - {status.charAt(0).toUpperCase() + status.slice(1)} + + {Icon && } + {config.label} ) } diff --git a/packages/duron-dashboard/src/components/data-table/data-table-toolbar.tsx b/packages/duron-dashboard/src/components/data-table/data-table-toolbar.tsx index cbbbced..3ecf264 100644 --- a/packages/duron-dashboard/src/components/data-table/data-table-toolbar.tsx +++ b/packages/duron-dashboard/src/components/data-table/data-table-toolbar.tsx @@ -145,7 +145,7 @@ function DataTableToolbarFilter({ column }: DataTableToolbarFilterProps column.setFilterValue(event.target.value)} - className="h-8 w-40 lg:w-56" + className="h-8 w-full" /> ) diff --git a/packages/duron-dashboard/src/components/data-table/data-table.tsx b/packages/duron-dashboard/src/components/data-table/data-table.tsx index 367ea3a..462e5ea 100644 --- a/packages/duron-dashboard/src/components/data-table/data-table.tsx +++ b/packages/duron-dashboard/src/components/data-table/data-table.tsx @@ -1,68 +1,232 @@ -import { flexRender, type Table as TanstackTable } from '@tanstack/react-table' +import { flexRender, type Header, type Table as TanstackTable } from '@tanstack/react-table' import type * as React from 'react' import { DataTablePagination } from '@/components/data-table/data-table-pagination' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { getCommonPinningStyles } from '@/lib/data-table.ts' import { cn } from '@/lib/utils' +function ColumnResizer({ header }: { header: Header }) { + if (!header.column.getCanResize()) { + return null + } + + return ( + + + + + + {title} + +
+ +
+
+
+ ) } diff --git a/packages/duron-dashboard/src/components/spans-panel.tsx b/packages/duron-dashboard/src/components/spans-panel.tsx new file mode 100644 index 0000000..5910533 --- /dev/null +++ b/packages/duron-dashboard/src/components/spans-panel.tsx @@ -0,0 +1,408 @@ +'use client' + +import { useVirtualizer } from '@tanstack/react-virtual' +import jsonata from 'jsonata' +import { Activity, Clock, Code, Hash, Tag, X } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import { useDebounceValue } from 'usehooks-ts' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Textarea } from '@/components/ui/textarea' +import { type Span, useJobSpans, useStepSpans } from '@/hooks/use-job-spans' +import { formatMs } from '@/lib/duration' +import { formatDate } from '@/lib/format' +import { JsonView } from './json-view' + +const SpanKindLabels: Record = { + 0: 'INTERNAL', + 1: 'SERVER', + 2: 'CLIENT', + 3: 'PRODUCER', + 4: 'CONSUMER', +} + +const SpanStatusLabels: Record = { + 0: 'UNSET', + 1: 'OK', + 2: 'ERROR', +} + +interface SpanItemProps { + span: Span +} + +function nanosToDate(nanos: string | null): Date | null { + if (!nanos) return null + // Convert nanoseconds to milliseconds + const ms = Number(BigInt(nanos) / BigInt(1_000_000)) + return new Date(ms) +} + +function SpanItem({ span }: SpanItemProps) { + const durationNs = + span.endTimeUnixNano && span.startTimeUnixNano + ? BigInt(span.endTimeUnixNano) - BigInt(span.startTimeUnixNano) + : null + const durationMs = durationNs ? Number(durationNs / BigInt(1_000_000)) : null + const formattedDuration = durationMs !== null ? formatMs(durationMs) : null + const startTime = nanosToDate(span.startTimeUnixNano) + + return ( +
+
+
+ + {span.name} +
+
+ + {SpanKindLabels[span.kind] ?? 'UNKNOWN'} + + + {SpanStatusLabels[span.statusCode] ?? 'UNKNOWN'} + +
+
+ +
+ {formattedDuration !== null && ( +
+ + {formattedDuration} +
+ )} + {startTime && ( +
+ + {formatDate(startTime.toISOString())} +
+ )} +
+ + trace: {span.traceId.slice(0, 8)}... + +
+
+ + {span.statusMessage && ( +
{span.statusMessage}
+ )} + + {span.attributes && Object.keys(span.attributes).length > 0 && ( +
+
+ + Attributes +
+
+ +
+
+ )} + + {span.events && span.events.length > 0 && ( +
+
+ + Events ({span.events.length}) +
+
+ +
+
+ )} +
+ ) +} + +interface VirtualizedSpansListProps { + spans: Span[] +} + +function VirtualizedSpansList({ spans }: VirtualizedSpansListProps) { + const parentRef = useRef(null) + + const virtualizer = useVirtualizer({ + count: spans.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 120, + overscan: 5, + }) + + const virtualItems = virtualizer.getVirtualItems() + + return ( +
+
+ {virtualItems.map((virtualItem) => { + const span = spans[virtualItem.index]! + return ( +
+
+ +
+
+ ) + })} +
+
+ ) +} + +interface JsonataResult { + type: 'spans' | 'primitive' | 'error' | 'empty' + spans?: Span[] + primitiveValue?: unknown + error?: string +} + +function isSpanLike(item: unknown): item is Span { + return ( + typeof item === 'object' && + item !== null && + 'id' in item && + 'name' in item && + 'traceId' in item && + 'spanId' in item && + 'kind' in item + ) +} + +async function evaluateJsonata(expression: string, spans: Span[]): Promise { + if (!expression.trim()) { + return { type: 'empty' } + } + + try { + const compiled = jsonata(expression) + const result = await compiled.evaluate(spans) + + // Check if result is undefined/null + if (result === undefined || result === null) { + return { type: 'primitive', primitiveValue: result } + } + + // Check if result is an array + if (Array.isArray(result)) { + // Check if it looks like an array of spans + const isSpansArray = result.every(isSpanLike) + + if (isSpansArray) { + return { type: 'spans', spans: result } + } + + // It's an array but not spans - show as primitive + return { type: 'primitive', primitiveValue: result } + } + + // Check if it's a single span object + if (isSpanLike(result)) { + return { type: 'spans', spans: [result] } + } + + // It's a primitive value (string, number, boolean, object without span shape) + return { type: 'primitive', primitiveValue: result } + } catch (err) { + return { type: 'error', error: err instanceof Error ? err.message : 'Unknown error' } + } +} + +interface SpansModalProps { + open: boolean + onClose: () => void + title: string + spans: Span[] + total: number + isLoading: boolean + error: Error | null +} + +function SpansModal({ open, onClose, title, spans, total, isLoading, error }: SpansModalProps) { + const [query, setQuery] = useState('') + const [debouncedQuery] = useDebounceValue(query, 300) + const [jsonataResult, setJsonataResult] = useState({ type: 'empty' }) + + // Evaluate JSONata expression asynchronously + useEffect(() => { + let cancelled = false + + evaluateJsonata(debouncedQuery, spans).then((result) => { + if (!cancelled) { + setJsonataResult(result) + } + }) + + return () => { + cancelled = true + } + }, [spans, debouncedQuery]) + + // Determine which spans to display + const displaySpans = jsonataResult.type === 'spans' ? jsonataResult.spans! : spans + + return ( + !isOpen && onClose()}> + + + + + {title} + + + +
+ {/* JSONata Query Input */} +
+
+ + JSONata Query +
+