proschi 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -25299,125 +25299,237 @@ var componentCatalog = [
25299
25299
  { type: "shape", techStack: "Diamond", category: "Generic Shapes", searchTerms: "diamond rhombus shape decision" },
25300
25300
  { type: "shape", techStack: "Cylinder", category: "Generic Shapes", searchTerms: "cylinder shape storage" },
25301
25301
  { type: "shape", techStack: "Cloud", category: "Generic Shapes", searchTerms: "cloud shape" },
25302
- { type: "shape", techStack: "Actor", category: "Generic Shapes", searchTerms: "actor user person human shape" },
25303
- { type: "shape", techStack: "Note", category: "Generic Shapes", searchTerms: "note document comment annotation" },
25302
+ { type: "shape", techStack: "Actor", category: "Generic Shapes", searchTerms: "actor user person human shape", aliases: ["User", "Client", "Customer", "Person"] },
25303
+ { type: "shape", techStack: "Browser", category: "Generic Shapes", searchTerms: "browser web client user", aliases: ["Web Browser", "Web Client"] },
25304
+ { type: "shape", techStack: "Mobile App", category: "Generic Shapes", searchTerms: "mobile app ios android client user", aliases: ["Mobile", "Mobile Client", "iOS", "iOS App", "Android", "Android App"] },
25305
+ { type: "shape", techStack: "Note", category: "Generic Shapes", searchTerms: "note document comment annotation", kind: "other" },
25306
+ // Generic components: one per kind, for a node whose product does not matter or is not listed.
25307
+ { type: "service", techStack: "Service", category: "Generic Components", searchTerms: "service microservice backend server application", aliases: ["Microservice", "Backend", "Backend Service", "App Server", "Application Server", "Server", "API Server"] },
25308
+ { type: "service", techStack: "Worker", category: "Generic Components", searchTerms: "worker background job consumer processor", aliases: ["Background Worker", "Job Worker", "Consumer", "Job Runner"] },
25309
+ { type: "database", techStack: "Database", category: "Generic Components", searchTerms: "database relational sql db", aliases: ["DB", "RDBMS", "SQL Database", "Relational Database", "SQL"] },
25310
+ { type: "database", techStack: "NoSQL Database", category: "Generic Components", searchTerms: "nosql key value document wide column database", profile: "nosql", aliases: ["NoSQL", "NoSQL DB", "Key-Value Store", "KV Store", "Document Store", "Document Database", "Wide-Column Store"] },
25311
+ { type: "cache", techStack: "Cache", category: "Generic Components", searchTerms: "cache in-memory key value", aliases: ["In-Memory Cache", "Distributed Cache"] },
25312
+ { type: "queue", techStack: "Message Queue", category: "Generic Components", searchTerms: "queue message broker topic stream event bus", aliases: ["Queue", "Message Broker", "Broker", "Event Bus", "Topic", "Event Stream", "Stream"] },
25313
+ { type: "storage", techStack: "Object Storage", category: "Generic Components", searchTerms: "object storage blob bucket files", aliases: ["Object Store", "Blob Store", "Blob Storage", "Bucket", "Storage", "File Storage"] },
25314
+ { type: "cdn", techStack: "CDN", category: "Generic Components", searchTerms: "cdn content delivery network edge cache", kind: "cdn", aliases: ["Content Delivery Network", "Edge Cache"] },
25315
+ { type: "cdn", techStack: "Load Balancer", category: "Generic Components", searchTerms: "load balancer lb reverse proxy", kind: "loadbalancer", aliases: ["LB", "Reverse Proxy", "L7 Load Balancer", "L4 Load Balancer"] },
25316
+ { type: "cdn", techStack: "API Gateway", category: "Generic Components", searchTerms: "api gateway", kind: "gateway", aliases: ["Gateway", "Edge Gateway"] },
25317
+ { type: "cdn", techStack: "DNS", category: "Generic Components", searchTerms: "dns domain name system", kind: "dns", aliases: ["Domain Name System", "DNS Server"] },
25318
+ { type: "cdn", techStack: "WAF", category: "Generic Components", searchTerms: "waf web application firewall edge", kind: "edge", aliases: ["Web Application Firewall", "Firewall"] },
25319
+ { type: "serverless", techStack: "Function", category: "Generic Components", searchTerms: "function serverless faas", aliases: ["Serverless Function", "Serverless", "FaaS", "Cloud Function"] },
25320
+ { type: "database", techStack: "Search Engine", category: "Generic Components", searchTerms: "search engine index full text", kind: "search", aliases: ["Search", "Search Index", "Full-Text Search", "Search Service"] },
25321
+ { type: "database", techStack: "Data Warehouse", category: "Generic Components", searchTerms: "data warehouse lake olap analytics", kind: "analytics", aliases: ["Warehouse", "Data Lake", "OLAP", "Analytics DB", "Analytics Database", "Time Series DB", "Time Series Database", "TSDB"] },
25304
25322
  // Services
25305
- { type: "service", techStack: "REST API", category: "Services", searchTerms: "rest api http service endpoint" },
25306
- { type: "service", techStack: "GraphQL", category: "Services", searchTerms: "graphql api service query" },
25307
- { type: "service", techStack: "gRPC", category: "Services", searchTerms: "grpc rpc service" },
25308
- { type: "service", techStack: "WebSocket", category: "Services", searchTerms: "websocket ws realtime service" },
25309
- { type: "service", techStack: "SOAP API", category: "Services", searchTerms: "soap api xml service" },
25323
+ { type: "service", techStack: "REST API", category: "Services", searchTerms: "rest api http service endpoint", aliases: ["REST", "API", "HTTP API", "Web API", "REST Service"] },
25324
+ { type: "service", techStack: "GraphQL", category: "Services", searchTerms: "graphql api service query", aliases: ["GraphQL API", "Apollo"] },
25325
+ { type: "service", techStack: "gRPC", category: "Services", searchTerms: "grpc rpc service", aliases: ["gRPC Service", "RPC"] },
25326
+ { type: "service", techStack: "WebSocket", category: "Services", searchTerms: "websocket ws realtime service", aliases: ["WebSockets", "WS", "Socket.IO", "WebSocket Server"] },
25327
+ { type: "service", techStack: "SOAP API", category: "Services", searchTerms: "soap api xml service", aliases: ["SOAP"] },
25328
+ // Languages and frameworks: a service written in it.
25329
+ { type: "service", techStack: "Spring Boot", category: "Languages & Frameworks", searchTerms: "spring boot java kotlin service", aliases: ["Spring", "Spring Framework", "Spring WebFlux"] },
25330
+ { type: "service", techStack: "Kotlin", category: "Languages & Frameworks", searchTerms: "kotlin ktor jvm service", aliases: ["Ktor"] },
25331
+ { type: "service", techStack: "Java", category: "Languages & Frameworks", searchTerms: "java jvm service", aliases: ["JVM", "Quarkus", "Micronaut"] },
25332
+ { type: "service", techStack: "Go", category: "Languages & Frameworks", searchTerms: "go golang service", aliases: ["Golang"] },
25333
+ { type: "service", techStack: "Node.js", category: "Languages & Frameworks", searchTerms: "node nodejs javascript typescript express nestjs service", aliases: ["Node", "Express", "Express.js", "NestJS", "TypeScript", "JavaScript"] },
25334
+ { type: "service", techStack: "Python", category: "Languages & Frameworks", searchTerms: "python service" },
25335
+ { type: "service", techStack: "Django", category: "Languages & Frameworks", searchTerms: "django python service" },
25336
+ { type: "service", techStack: "FastAPI", category: "Languages & Frameworks", searchTerms: "fastapi python service" },
25337
+ { type: "service", techStack: "Flask", category: "Languages & Frameworks", searchTerms: "flask python service" },
25338
+ { type: "service", techStack: "Ruby on Rails", category: "Languages & Frameworks", searchTerms: "ruby on rails service", aliases: ["Rails", "Ruby"] },
25339
+ { type: "service", techStack: ".NET", category: "Languages & Frameworks", searchTerms: "dotnet .net asp.net c# service", aliases: ["ASP.NET", "ASP.NET Core", "dotnet", "C#"] },
25340
+ { type: "service", techStack: "PHP", category: "Languages & Frameworks", searchTerms: "php laravel symfony service", aliases: ["Laravel", "Symfony"] },
25341
+ { type: "service", techStack: "Rust", category: "Languages & Frameworks", searchTerms: "rust axum actix service", aliases: ["Axum", "Actix"] },
25342
+ { type: "service", techStack: "Elixir", category: "Languages & Frameworks", searchTerms: "elixir phoenix erlang service", aliases: ["Phoenix", "Erlang"] },
25343
+ // Containers and platforms
25344
+ { type: "container", techStack: "Kubernetes", category: "Containers & Platforms", searchTerms: "kubernetes k8s container pod", aliases: ["K8s", "Kubernetes Pod", "Pod"] },
25345
+ { type: "container", techStack: "Docker", category: "Containers & Platforms", searchTerms: "docker container", aliases: ["Container", "Docker Container"] },
25346
+ { type: "compute", techStack: "VM", category: "Containers & Platforms", searchTerms: "vm virtual machine server instance", aliases: ["Virtual Machine", "Instance", "Bare Metal"] },
25347
+ { type: "serverless", techStack: "Cloudflare Workers", category: "Containers & Platforms", searchTerms: "cloudflare workers edge functions serverless", aliases: ["Workers"] },
25310
25348
  // AWS Serverless
25311
- { type: "serverless", techStack: "AWS Lambda", category: "AWS Services", searchTerms: "aws lambda serverless function" },
25312
- { type: "serverless", techStack: "AWS API Gateway", category: "AWS Services", searchTerms: "aws api gateway rest" },
25349
+ { type: "serverless", techStack: "AWS Lambda", category: "AWS Services", searchTerms: "aws lambda serverless function", aliases: ["Lambda", "Amazon Lambda"] },
25350
+ { type: "serverless", techStack: "AWS API Gateway", category: "AWS Services", searchTerms: "aws api gateway rest", kind: "gateway", aliases: ["Amazon API Gateway"] },
25313
25351
  // AWS Compute
25314
- { type: "compute", techStack: "AWS EC2", category: "AWS Services", searchTerms: "aws ec2 compute instance vm" },
25315
- { type: "container", techStack: "AWS ECS", category: "AWS Services", searchTerms: "aws ecs container docker" },
25316
- { type: "container", techStack: "AWS EKS", category: "AWS Services", searchTerms: "aws eks kubernetes k8s" },
25317
- { type: "container", techStack: "AWS Fargate", category: "AWS Services", searchTerms: "aws fargate serverless container" },
25352
+ { type: "compute", techStack: "AWS EC2", category: "AWS Services", searchTerms: "aws ec2 compute instance vm", aliases: ["EC2", "Amazon EC2"] },
25353
+ { type: "container", techStack: "AWS ECS", category: "AWS Services", searchTerms: "aws ecs container docker", aliases: ["ECS", "Amazon ECS"] },
25354
+ { type: "container", techStack: "AWS EKS", category: "AWS Services", searchTerms: "aws eks kubernetes k8s", aliases: ["EKS", "Amazon EKS"] },
25355
+ { type: "container", techStack: "AWS Fargate", category: "AWS Services", searchTerms: "aws fargate serverless container", aliases: ["Fargate"] },
25318
25356
  // AWS Storage
25319
- { type: "storage", techStack: "AWS S3", category: "AWS Services", searchTerms: "aws s3 storage bucket object" },
25320
- { type: "storage", techStack: "AWS EBS", category: "AWS Services", searchTerms: "aws ebs storage volume disk" },
25321
- { type: "storage", techStack: "AWS EFS", category: "AWS Services", searchTerms: "aws efs storage file system" },
25357
+ { type: "storage", techStack: "AWS S3", category: "AWS Services", searchTerms: "aws s3 storage bucket object", aliases: ["S3", "Amazon S3", "S3 Bucket"] },
25358
+ { type: "storage", techStack: "AWS EBS", category: "AWS Services", searchTerms: "aws ebs storage volume disk", aliases: ["EBS", "Amazon EBS"] },
25359
+ { type: "storage", techStack: "AWS EFS", category: "AWS Services", searchTerms: "aws efs storage file system", aliases: ["EFS", "Amazon EFS"] },
25322
25360
  // AWS Database
25323
- { type: "database", techStack: "AWS RDS", category: "AWS Services", searchTerms: "aws rds database relational sql" },
25324
- { type: "database", techStack: "AWS DynamoDB", category: "AWS Services", searchTerms: "aws dynamodb database nosql" },
25325
- { type: "cache", techStack: "AWS ElastiCache", category: "AWS Services", searchTerms: "aws elasticache redis memcached cache" },
25326
- { type: "database", techStack: "AWS Aurora", category: "AWS Services", searchTerms: "aws aurora database mysql postgresql" },
25361
+ { type: "database", techStack: "AWS RDS", category: "AWS Services", searchTerms: "aws rds database relational sql", aliases: ["RDS", "Amazon RDS"] },
25362
+ { type: "database", techStack: "AWS DynamoDB", category: "AWS Services", searchTerms: "aws dynamodb database nosql", profile: "nosql", aliases: ["Amazon DynamoDB", "Dynamo"] },
25363
+ { type: "cache", techStack: "AWS ElastiCache", category: "AWS Services", searchTerms: "aws elasticache redis memcached cache", aliases: ["ElastiCache", "Amazon ElastiCache"] },
25364
+ { type: "database", techStack: "AWS Aurora", category: "AWS Services", searchTerms: "aws aurora database mysql postgresql", aliases: ["Aurora", "Amazon Aurora", "Aurora PostgreSQL", "Aurora MySQL", "Aurora Serverless"] },
25365
+ { type: "database", techStack: "AWS DocumentDB", category: "AWS Services", searchTerms: "aws documentdb mongodb document database", aliases: ["DocumentDB", "Amazon DocumentDB"] },
25366
+ { type: "database", techStack: "AWS Keyspaces", category: "AWS Services", searchTerms: "aws keyspaces cassandra nosql", profile: "nosql", aliases: ["Keyspaces", "Amazon Keyspaces"] },
25367
+ { type: "database", techStack: "AWS Neptune", category: "AWS Services", searchTerms: "aws neptune graph database", aliases: ["Neptune", "Amazon Neptune"] },
25368
+ { type: "database", techStack: "AWS Redshift", category: "AWS Services", searchTerms: "aws redshift data warehouse analytics", kind: "analytics", aliases: ["Redshift", "Amazon Redshift"] },
25369
+ { type: "database", techStack: "AWS Athena", category: "AWS Services", searchTerms: "aws athena query s3 analytics", kind: "analytics", aliases: ["Athena", "Amazon Athena"] },
25370
+ { type: "database", techStack: "AWS OpenSearch", category: "AWS Services", searchTerms: "aws opensearch elasticsearch search", kind: "search", aliases: ["Amazon OpenSearch", "OpenSearch Service", "AWS Elasticsearch"] },
25327
25371
  // AWS Messaging
25328
- { type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message" },
25329
- { type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub" },
25330
- { type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus" },
25331
- { type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data" },
25372
+ { type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message", aliases: ["Amazon SQS", "Simple Queue Service"] },
25373
+ { type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
25374
+ { type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
25375
+ { type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
25376
+ { type: "queue", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
25332
25377
  // AWS CDN/Network
25333
- { type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge" },
25334
- { type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain" },
25335
- { type: "cdn", techStack: "AWS Load Balancer", category: "AWS Services", searchTerms: "aws alb elb load balancer" },
25378
+ { type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge", kind: "cdn", aliases: ["CloudFront", "Amazon CloudFront"] },
25379
+ { type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain", kind: "dns", aliases: ["Route53", "Amazon Route 53"] },
25380
+ { type: "cdn", techStack: "AWS Load Balancer", category: "AWS Services", searchTerms: "aws alb elb nlb load balancer", kind: "loadbalancer", aliases: ["ALB", "NLB", "ELB", "AWS ALB", "AWS NLB", "AWS ELB", "Application Load Balancer", "Network Load Balancer", "Elastic Load Balancer", "Elastic Load Balancing"] },
25381
+ { type: "cdn", techStack: "AWS WAF", category: "AWS Services", searchTerms: "aws waf web application firewall", kind: "edge", aliases: ["Amazon WAF"] },
25382
+ { type: "cdn", techStack: "AWS Global Accelerator", category: "AWS Services", searchTerms: "aws global accelerator anycast edge", kind: "edge", aliases: ["Global Accelerator"] },
25383
+ // AWS third-party style services
25384
+ { type: "external", techStack: "AWS SES", category: "AWS Services", searchTerms: "aws ses email simple email service", aliases: ["SES", "Amazon SES", "Simple Email Service"] },
25385
+ { type: "external", techStack: "AWS Cognito", category: "AWS Services", searchTerms: "aws cognito auth identity users", aliases: ["Cognito", "Amazon Cognito"] },
25336
25386
  // GCP Serverless
25337
- { type: "serverless", techStack: "GCP Cloud Functions", category: "GCP Services", searchTerms: "gcp google cloud functions serverless" },
25338
- { type: "serverless", techStack: "GCP Cloud Run", category: "GCP Services", searchTerms: "gcp google cloud run serverless container" },
25339
- { type: "serverless", techStack: "GCP App Engine", category: "GCP Services", searchTerms: "gcp google app engine paas" },
25387
+ { type: "serverless", techStack: "GCP Cloud Functions", category: "GCP Services", searchTerms: "gcp google cloud functions serverless", aliases: ["Cloud Functions", "Google Cloud Functions"] },
25388
+ { type: "serverless", techStack: "GCP Cloud Run", category: "GCP Services", searchTerms: "gcp google cloud run serverless container", aliases: ["Cloud Run", "Google Cloud Run"] },
25389
+ { type: "serverless", techStack: "GCP App Engine", category: "GCP Services", searchTerms: "gcp google app engine paas", aliases: ["App Engine", "Google App Engine"] },
25340
25390
  // GCP Compute
25341
- { type: "compute", techStack: "GCP Compute Engine", category: "GCP Services", searchTerms: "gcp google compute engine vm instance" },
25342
- { type: "container", techStack: "GCP GKE", category: "GCP Services", searchTerms: "gcp google gke kubernetes k8s" },
25391
+ { type: "compute", techStack: "GCP Compute Engine", category: "GCP Services", searchTerms: "gcp google compute engine vm instance", aliases: ["Compute Engine", "GCE", "Google Compute Engine"] },
25392
+ { type: "container", techStack: "GCP GKE", category: "GCP Services", searchTerms: "gcp google gke kubernetes k8s", aliases: ["GKE", "Google Kubernetes Engine"] },
25343
25393
  // GCP Storage
25344
- { type: "storage", techStack: "GCP Cloud Storage", category: "GCP Services", searchTerms: "gcp google cloud storage bucket object" },
25345
- { type: "storage", techStack: "GCP Persistent Disk", category: "GCP Services", searchTerms: "gcp google persistent disk storage" },
25394
+ { type: "storage", techStack: "GCP Cloud Storage", category: "GCP Services", searchTerms: "gcp google cloud storage bucket object", aliases: ["GCS", "Cloud Storage", "Google Cloud Storage"] },
25395
+ { type: "storage", techStack: "GCP Persistent Disk", category: "GCP Services", searchTerms: "gcp google persistent disk storage", aliases: ["Persistent Disk"] },
25346
25396
  // GCP Database
25347
- { type: "database", techStack: "GCP Cloud SQL", category: "GCP Services", searchTerms: "gcp google cloud sql database" },
25348
- { type: "database", techStack: "GCP Firestore", category: "GCP Services", searchTerms: "gcp google firestore database nosql" },
25349
- { type: "database", techStack: "GCP Bigtable", category: "GCP Services", searchTerms: "gcp google bigtable database nosql" },
25350
- { type: "database", techStack: "GCP Spanner", category: "GCP Services", searchTerms: "gcp google spanner database sql" },
25351
- { type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics" },
25397
+ { type: "database", techStack: "GCP Cloud SQL", category: "GCP Services", searchTerms: "gcp google cloud sql database", aliases: ["Cloud SQL", "Google Cloud SQL"] },
25398
+ { type: "database", techStack: "GCP Firestore", category: "GCP Services", searchTerms: "gcp google firestore database nosql", profile: "nosqlStrong", aliases: ["Firestore", "Cloud Firestore", "Datastore"] },
25399
+ { type: "database", techStack: "GCP Bigtable", category: "GCP Services", searchTerms: "gcp google bigtable database nosql", profile: "nosqlStrong", aliases: ["Bigtable", "Cloud Bigtable"] },
25400
+ { type: "database", techStack: "GCP Spanner", category: "GCP Services", searchTerms: "gcp google spanner database sql", profile: "nosqlStrong", aliases: ["Spanner", "Cloud Spanner", "Google Spanner"] },
25401
+ { type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics", kind: "analytics", aliases: ["BigQuery", "Google BigQuery"] },
25402
+ { type: "cache", techStack: "GCP Memorystore", category: "GCP Services", searchTerms: "gcp google memorystore redis memcached cache", aliases: ["Memorystore"] },
25352
25403
  // GCP Messaging
25353
- { type: "queue", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue" },
25404
+ { type: "queue", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue", aliases: ["Pub/Sub", "Google Pub/Sub", "Cloud Pub/Sub"] },
25354
25405
  // GCP CDN/Network
25355
- { type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge" },
25356
- { type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain" },
25357
- { type: "cdn", techStack: "GCP Load Balancing", category: "GCP Services", searchTerms: "gcp google load balancing" },
25406
+ { type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge", kind: "cdn", aliases: ["Cloud CDN", "Google Cloud CDN"] },
25407
+ { type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain", kind: "dns", aliases: ["Cloud DNS", "Google Cloud DNS"] },
25408
+ { type: "cdn", techStack: "GCP Load Balancing", category: "GCP Services", searchTerms: "gcp google load balancing", kind: "loadbalancer", aliases: ["Cloud Load Balancing", "GCP Load Balancer", "Google Cloud Load Balancing"] },
25409
+ { type: "cdn", techStack: "GCP Apigee", category: "GCP Services", searchTerms: "gcp google apigee api gateway management", kind: "gateway", aliases: ["Apigee"] },
25358
25410
  // Azure Serverless
25359
- { type: "serverless", techStack: "Azure Functions", category: "Azure Services", searchTerms: "azure functions serverless" },
25360
- { type: "serverless", techStack: "Azure Logic Apps", category: "Azure Services", searchTerms: "azure logic apps workflow" },
25361
- { type: "serverless", techStack: "Azure API Management", category: "Azure Services", searchTerms: "azure api management gateway" },
25411
+ { type: "serverless", techStack: "Azure Functions", category: "Azure Services", searchTerms: "azure functions serverless", aliases: ["Azure Function"] },
25412
+ { type: "serverless", techStack: "Azure Logic Apps", category: "Azure Services", searchTerms: "azure logic apps workflow", aliases: ["Logic Apps"] },
25413
+ { type: "serverless", techStack: "Azure API Management", category: "Azure Services", searchTerms: "azure api management gateway", kind: "gateway", aliases: ["APIM", "Azure APIM"] },
25362
25414
  // Azure Compute
25363
- { type: "compute", techStack: "Azure VM", category: "Azure Services", searchTerms: "azure vm virtual machine compute" },
25364
- { type: "container", techStack: "Azure AKS", category: "Azure Services", searchTerms: "azure aks kubernetes k8s" },
25365
- { type: "container", techStack: "Azure Container Instances", category: "Azure Services", searchTerms: "azure container instances aci" },
25415
+ { type: "compute", techStack: "Azure VM", category: "Azure Services", searchTerms: "azure vm virtual machine compute", aliases: ["Azure Virtual Machine", "Azure Virtual Machines"] },
25416
+ { type: "container", techStack: "Azure AKS", category: "Azure Services", searchTerms: "azure aks kubernetes k8s", aliases: ["AKS", "Azure Kubernetes Service"] },
25417
+ { type: "container", techStack: "Azure Container Instances", category: "Azure Services", searchTerms: "azure container instances aci", aliases: ["ACI"] },
25418
+ { type: "service", techStack: "Azure App Service", category: "Azure Services", searchTerms: "azure app service web apps paas", aliases: ["App Service", "Azure Web Apps"] },
25366
25419
  // Azure Storage
25367
- { type: "storage", techStack: "Azure Blob Storage", category: "Azure Services", searchTerms: "azure blob storage object" },
25420
+ { type: "storage", techStack: "Azure Blob Storage", category: "Azure Services", searchTerms: "azure blob storage object", aliases: ["Azure Blob", "Azure Storage"] },
25368
25421
  { type: "storage", techStack: "Azure Files", category: "Azure Services", searchTerms: "azure files storage file system" },
25369
- { type: "storage", techStack: "Azure Disk Storage", category: "Azure Services", searchTerms: "azure disk storage volume" },
25422
+ { type: "storage", techStack: "Azure Disk Storage", category: "Azure Services", searchTerms: "azure disk storage volume", aliases: ["Azure Managed Disks"] },
25370
25423
  // Azure Database
25371
- { type: "database", techStack: "Azure SQL", category: "Azure Services", searchTerms: "azure sql database" },
25372
- { type: "database", techStack: "Azure Cosmos DB", category: "Azure Services", searchTerms: "azure cosmos db database nosql" },
25373
- { type: "database", techStack: "Azure Database for PostgreSQL", category: "Azure Services", searchTerms: "azure postgresql database" },
25374
- { type: "database", techStack: "Azure Database for MySQL", category: "Azure Services", searchTerms: "azure mysql database" },
25375
- { type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache" },
25424
+ { type: "database", techStack: "Azure SQL", category: "Azure Services", searchTerms: "azure sql database", aliases: ["Azure SQL Database"] },
25425
+ { type: "database", techStack: "Azure Cosmos DB", category: "Azure Services", searchTerms: "azure cosmos db database nosql", profile: "nosql", aliases: ["Cosmos DB", "Cosmos"] },
25426
+ { type: "database", techStack: "Azure Database for PostgreSQL", category: "Azure Services", searchTerms: "azure postgresql database", aliases: ["Azure PostgreSQL", "Azure Postgres"] },
25427
+ { type: "database", techStack: "Azure Database for MySQL", category: "Azure Services", searchTerms: "azure mysql database", aliases: ["Azure MySQL"] },
25428
+ { type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache", aliases: ["Azure Redis"] },
25429
+ { type: "database", techStack: "Azure AI Search", category: "Azure Services", searchTerms: "azure ai cognitive search", kind: "search", aliases: ["Azure Cognitive Search", "Azure Search"] },
25376
25430
  // Azure Messaging
25377
- { type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message" },
25378
- { type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream" },
25379
- { type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid" },
25380
- { type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message" },
25431
+ { type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
25432
+ { type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
25433
+ { type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
25434
+ { type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message", aliases: ["Azure Queue", "Azure Queues"] },
25381
25435
  // Azure CDN/Network
25382
- { type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge" },
25383
- { type: "cdn", techStack: "Azure DNS", category: "Azure Services", searchTerms: "azure dns domain" },
25384
- { type: "cdn", techStack: "Azure Front Door", category: "Azure Services", searchTerms: "azure front door cdn" },
25436
+ { type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge", kind: "cdn" },
25437
+ { type: "cdn", techStack: "Azure DNS", category: "Azure Services", searchTerms: "azure dns domain", kind: "dns" },
25438
+ { type: "cdn", techStack: "Azure Front Door", category: "Azure Services", searchTerms: "azure front door cdn", kind: "cdn", aliases: ["Front Door"] },
25439
+ { type: "cdn", techStack: "Azure Load Balancer", category: "Azure Services", searchTerms: "azure load balancer", kind: "loadbalancer" },
25440
+ { type: "cdn", techStack: "Azure Application Gateway", category: "Azure Services", searchTerms: "azure application gateway l7 load balancer", kind: "loadbalancer", aliases: ["Application Gateway", "Azure App Gateway"] },
25385
25441
  // Traditional Databases
25386
- { type: "database", techStack: "PostgreSQL", category: "Databases", searchTerms: "postgresql postgres sql database" },
25387
- { type: "database", techStack: "MySQL", category: "Databases", searchTerms: "mysql sql database" },
25388
- { type: "database", techStack: "MongoDB", category: "Databases", searchTerms: "mongodb mongo nosql database" },
25389
- { type: "database", techStack: "Redis", category: "Databases", searchTerms: "redis cache database key-value" },
25390
- { type: "database", techStack: "DynamoDB", category: "Databases", searchTerms: "dynamodb nosql database aws" },
25391
- { type: "database", techStack: "Cassandra", category: "Databases", searchTerms: "cassandra nosql database" },
25392
- { type: "database", techStack: "CouchDB", category: "Databases", searchTerms: "couchdb nosql database" },
25393
- { type: "database", techStack: "Elasticsearch", category: "Databases", searchTerms: "elasticsearch search database" },
25394
- { type: "database", techStack: "Neo4j", category: "Databases", searchTerms: "neo4j graph database" },
25395
- { type: "database", techStack: "InfluxDB", category: "Databases", searchTerms: "influxdb timeseries database" },
25396
- { type: "database", techStack: "TimescaleDB", category: "Databases", searchTerms: "timescaledb timeseries postgresql database" },
25397
- { type: "database", techStack: "MariaDB", category: "Databases", searchTerms: "mariadb mysql sql database" },
25442
+ { type: "database", techStack: "PostgreSQL", category: "Databases", searchTerms: "postgresql postgres sql database", aliases: ["Postgres", "PG", "PostGIS", "pgvector"] },
25443
+ { type: "database", techStack: "MySQL", category: "Databases", searchTerms: "mysql sql database", aliases: ["Percona"] },
25444
+ { type: "database", techStack: "MongoDB", category: "Databases", searchTerms: "mongodb mongo nosql database", profile: "nosqlStrong", aliases: ["Mongo", "MongoDB Atlas"] },
25445
+ { type: "database", techStack: "Redis", category: "Databases", searchTerms: "redis cache database key-value", kind: "cache", aliases: ["Redis Cluster", "Redis Cache"] },
25446
+ { type: "database", techStack: "DynamoDB", category: "Databases", searchTerms: "dynamodb nosql database aws", profile: "nosql" },
25447
+ { type: "database", techStack: "Cassandra", category: "Databases", searchTerms: "cassandra nosql database", profile: "nosql", aliases: ["Apache Cassandra", "DataStax"] },
25448
+ { type: "database", techStack: "CouchDB", category: "Databases", searchTerms: "couchdb nosql database", profile: "nosql", aliases: ["Apache CouchDB"] },
25449
+ { type: "database", techStack: "Elasticsearch", category: "Databases", searchTerms: "elasticsearch search database", kind: "search", aliases: ["Elastic", "Elastic Search", "ES"] },
25450
+ { type: "database", techStack: "Neo4j", category: "Databases", searchTerms: "neo4j graph database", aliases: ["Graph Database", "Graph DB"] },
25451
+ { type: "database", techStack: "InfluxDB", category: "Databases", searchTerms: "influxdb timeseries database", kind: "analytics", aliases: ["Influx"] },
25452
+ { type: "database", techStack: "TimescaleDB", category: "Databases", searchTerms: "timescaledb timeseries postgresql database", kind: "analytics", aliases: ["Timescale"] },
25453
+ { type: "database", techStack: "MariaDB", category: "Databases", searchTerms: "mariadb mysql sql database", aliases: ["Maria"] },
25398
25454
  { type: "database", techStack: "SQLite", category: "Databases", searchTerms: "sqlite sql database" },
25399
- { type: "database", techStack: "Oracle", category: "Databases", searchTerms: "oracle sql database" },
25400
- { type: "database", techStack: "SQL Server", category: "Databases", searchTerms: "sql server mssql database microsoft" },
25455
+ { type: "database", techStack: "Oracle", category: "Databases", searchTerms: "oracle sql database", aliases: ["Oracle DB", "Oracle Database"] },
25456
+ { type: "database", techStack: "SQL Server", category: "Databases", searchTerms: "sql server mssql database microsoft", aliases: ["MSSQL", "MS SQL", "Microsoft SQL Server"] },
25457
+ { type: "database", techStack: "CockroachDB", category: "Databases", searchTerms: "cockroachdb distributed sql database", profile: "nosqlStrong", aliases: ["Cockroach"] },
25458
+ { type: "database", techStack: "YugabyteDB", category: "Databases", searchTerms: "yugabytedb distributed sql database", profile: "nosqlStrong", aliases: ["Yugabyte"] },
25459
+ { type: "database", techStack: "TiDB", category: "Databases", searchTerms: "tidb distributed sql mysql database", profile: "nosqlStrong" },
25460
+ { type: "database", techStack: "Vitess", category: "Databases", searchTerms: "vitess sharded mysql planetscale database", profile: "nosqlStrong", aliases: ["PlanetScale"] },
25461
+ { type: "database", techStack: "ScyllaDB", category: "Databases", searchTerms: "scylladb cassandra nosql database", profile: "nosql", aliases: ["Scylla"] },
25462
+ { type: "database", techStack: "Couchbase", category: "Databases", searchTerms: "couchbase nosql document database", profile: "nosql" },
25463
+ { type: "database", techStack: "HBase", category: "Databases", searchTerms: "hbase hadoop wide column nosql database", profile: "nosqlStrong", aliases: ["Apache HBase"] },
25464
+ { type: "database", techStack: "etcd", category: "Databases", searchTerms: "etcd key value consensus configuration" },
25465
+ { type: "database", techStack: "ZooKeeper", category: "Databases", searchTerms: "zookeeper coordination consensus configuration", aliases: ["Apache ZooKeeper"] },
25401
25466
  // Cache & In-Memory
25402
- { type: "cache", techStack: "Memcached", category: "Cache & In-Memory", searchTerms: "memcached cache memory" },
25467
+ { type: "cache", techStack: "Memcached", category: "Cache & In-Memory", searchTerms: "memcached cache memory", aliases: ["Memcache"] },
25403
25468
  { type: "cache", techStack: "Hazelcast", category: "Cache & In-Memory", searchTerms: "hazelcast cache memory" },
25404
25469
  { type: "cache", techStack: "Aerospike", category: "Cache & In-Memory", searchTerms: "aerospike cache database" },
25470
+ { type: "cache", techStack: "Valkey", category: "Cache & In-Memory", searchTerms: "valkey redis cache memory" },
25471
+ { type: "cache", techStack: "KeyDB", category: "Cache & In-Memory", searchTerms: "keydb redis cache memory" },
25472
+ { type: "cache", techStack: "Dragonfly", category: "Cache & In-Memory", searchTerms: "dragonfly dragonflydb redis cache memory", aliases: ["DragonflyDB"] },
25405
25473
  // Message Queues
25406
- { type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream" },
25407
- { type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp" },
25474
+ { type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
25475
+ { type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp", aliases: ["Rabbit", "AMQP"] },
25408
25476
  { type: "queue", techStack: "SQS", category: "Message Queues", searchTerms: "sqs queue aws message" },
25409
- { type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message" },
25410
- { type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue" },
25411
- { type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream" },
25412
- { type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue" },
25413
- { type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue" },
25477
+ { type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
25478
+ { type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue", aliases: ["NATS JetStream", "JetStream"] },
25479
+ { type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream", aliases: ["Pulsar"] },
25480
+ { type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
25481
+ { type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
25482
+ { type: "queue", techStack: "Redpanda", category: "Message Queues", searchTerms: "redpanda kafka stream queue" },
25483
+ // Search
25484
+ { type: "database", techStack: "OpenSearch", category: "Search", searchTerms: "opensearch elasticsearch search", kind: "search" },
25485
+ { type: "database", techStack: "Solr", category: "Search", searchTerms: "solr lucene search", kind: "search", aliases: ["Apache Solr"] },
25486
+ { type: "database", techStack: "Meilisearch", category: "Search", searchTerms: "meilisearch search", kind: "search" },
25487
+ { type: "database", techStack: "Typesense", category: "Search", searchTerms: "typesense search", kind: "search" },
25488
+ { type: "database", techStack: "Algolia", category: "Search", searchTerms: "algolia hosted search", kind: "search" },
25489
+ // Analytics
25490
+ { type: "database", techStack: "Snowflake", category: "Analytics", searchTerms: "snowflake data warehouse analytics", kind: "analytics" },
25491
+ { type: "database", techStack: "ClickHouse", category: "Analytics", searchTerms: "clickhouse olap analytics columnar", kind: "analytics" },
25492
+ { type: "database", techStack: "Apache Druid", category: "Analytics", searchTerms: "druid olap analytics", kind: "analytics", aliases: ["Druid"] },
25493
+ { type: "database", techStack: "Apache Pinot", category: "Analytics", searchTerms: "pinot olap analytics", kind: "analytics", aliases: ["Pinot"] },
25494
+ { type: "database", techStack: "Databricks", category: "Analytics", searchTerms: "databricks lakehouse spark analytics", kind: "analytics" },
25495
+ { type: "database", techStack: "Prometheus", category: "Analytics", searchTerms: "prometheus metrics time series monitoring", kind: "analytics" },
25496
+ // Storage
25497
+ { type: "storage", techStack: "MinIO", category: "Storage", searchTerms: "minio s3 object storage" },
25498
+ { type: "storage", techStack: "Cloudflare R2", category: "Storage", searchTerms: "cloudflare r2 object storage", aliases: ["R2"] },
25499
+ { type: "storage", techStack: "Ceph", category: "Storage", searchTerms: "ceph object block storage" },
25500
+ { type: "storage", techStack: "HDFS", category: "Storage", searchTerms: "hdfs hadoop file system storage", aliases: ["Hadoop", "Hadoop HDFS"] },
25501
+ // CDN, proxies and gateways
25502
+ { type: "cdn", techStack: "Cloudflare", category: "CDN & Edge", searchTerms: "cloudflare cdn edge", kind: "cdn", aliases: ["Cloudflare CDN"] },
25503
+ { type: "cdn", techStack: "Fastly", category: "CDN & Edge", searchTerms: "fastly cdn edge", kind: "cdn" },
25504
+ { type: "cdn", techStack: "Akamai", category: "CDN & Edge", searchTerms: "akamai cdn edge", kind: "cdn" },
25505
+ { type: "cdn", techStack: "nginx", category: "CDN & Edge", searchTerms: "nginx reverse proxy load balancer web server", kind: "loadbalancer", aliases: ["NGINX Plus", "OpenResty"] },
25506
+ { type: "cdn", techStack: "Envoy", category: "CDN & Edge", searchTerms: "envoy proxy load balancer service mesh", kind: "loadbalancer", aliases: ["Envoy Proxy"] },
25507
+ { type: "cdn", techStack: "HAProxy", category: "CDN & Edge", searchTerms: "haproxy load balancer proxy", kind: "loadbalancer" },
25508
+ { type: "cdn", techStack: "Traefik", category: "CDN & Edge", searchTerms: "traefik reverse proxy load balancer", kind: "loadbalancer" },
25509
+ { type: "cdn", techStack: "Kubernetes Ingress", category: "CDN & Edge", searchTerms: "kubernetes ingress controller load balancer", kind: "loadbalancer", aliases: ["Ingress", "Ingress Controller", "K8s Ingress"] },
25510
+ { type: "cdn", techStack: "Kong", category: "CDN & Edge", searchTerms: "kong api gateway", kind: "gateway", aliases: ["Kong Gateway"] },
25511
+ { type: "cdn", techStack: "Spring Cloud Gateway", category: "CDN & Edge", searchTerms: "spring cloud gateway api gateway", kind: "gateway" },
25414
25512
  // External Systems
25415
- { type: "external", techStack: "Third Party API", category: "External Systems", searchTerms: "third party api external" },
25416
- { type: "external", techStack: "Payment Gateway", category: "External Systems", searchTerms: "payment gateway stripe paypal" },
25417
- { type: "external", techStack: "Auth Service", category: "External Systems", searchTerms: "auth authentication authorization" },
25418
- { type: "external", techStack: "Email Service", category: "External Systems", searchTerms: "email service sendgrid mailgun" },
25419
- { type: "external", techStack: "SMS Service", category: "External Systems", searchTerms: "sms service twilio" },
25420
- { type: "external", techStack: "Analytics Service", category: "External Systems", searchTerms: "analytics service google analytics" },
25513
+ { type: "external", techStack: "Third Party API", category: "External Systems", searchTerms: "third party api external", aliases: ["External API", "External Service", "Third-Party API", "3rd Party API", "Partner API", "SaaS", "Webhook"] },
25514
+ { type: "external", techStack: "Payment Gateway", category: "External Systems", searchTerms: "payment gateway stripe paypal", aliases: ["Payment Provider", "Payments API", "Payment Processor", "PSP"] },
25515
+ { type: "external", techStack: "Auth Service", category: "External Systems", searchTerms: "auth authentication authorization", aliases: ["Identity Provider", "IdP", "OAuth Provider", "SSO"] },
25516
+ { type: "external", techStack: "Email Service", category: "External Systems", searchTerms: "email service sendgrid mailgun", aliases: ["Email", "Email Provider", "SMTP"] },
25517
+ { type: "external", techStack: "SMS Service", category: "External Systems", searchTerms: "sms service twilio", aliases: ["SMS", "SMS Gateway", "SMS Provider"] },
25518
+ { type: "external", techStack: "Push Service", category: "External Systems", searchTerms: "push notification service mobile", aliases: ["Push Notifications", "Push Notification Service", "Push Provider"] },
25519
+ { type: "external", techStack: "Analytics Service", category: "External Systems", searchTerms: "analytics service google analytics", aliases: ["Google Analytics", "Segment", "Mixpanel", "Amplitude"] },
25520
+ { type: "external", techStack: "Stripe", category: "External Systems", searchTerms: "stripe payments" },
25521
+ { type: "external", techStack: "PayPal", category: "External Systems", searchTerms: "paypal payments" },
25522
+ { type: "external", techStack: "Adyen", category: "External Systems", searchTerms: "adyen payments" },
25523
+ { type: "external", techStack: "Braintree", category: "External Systems", searchTerms: "braintree payments" },
25524
+ { type: "external", techStack: "Twilio", category: "External Systems", searchTerms: "twilio sms voice" },
25525
+ { type: "external", techStack: "SendGrid", category: "External Systems", searchTerms: "sendgrid email" },
25526
+ { type: "external", techStack: "Mailgun", category: "External Systems", searchTerms: "mailgun email" },
25527
+ { type: "external", techStack: "Auth0", category: "External Systems", searchTerms: "auth0 identity login" },
25528
+ { type: "external", techStack: "Okta", category: "External Systems", searchTerms: "okta identity sso" },
25529
+ { type: "external", techStack: "Google Maps", category: "External Systems", searchTerms: "google maps geocoding routing", aliases: ["Google Maps API", "Maps API"] },
25530
+ { type: "external", techStack: "APNs", category: "External Systems", searchTerms: "apns apple push notification", aliases: ["Apple Push Notification Service", "Apple Push"] },
25531
+ { type: "external", techStack: "FCM", category: "External Systems", searchTerms: "fcm firebase cloud messaging push", aliases: ["Firebase Cloud Messaging", "Firebase Messaging"] },
25532
+ { type: "external", techStack: "OpenAI", category: "External Systems", searchTerms: "openai llm ai api", aliases: ["OpenAI API", "LLM API", "LLM"] },
25421
25533
  // Text & Annotations
25422
25534
  { type: "text", techStack: "Text Note", category: "Annotations", searchTerms: "text note annotation comment label" },
25423
25535
  { type: "text", techStack: "Sticky Note", category: "Annotations", searchTerms: "sticky note annotation comment" },
@@ -25428,6 +25540,54 @@ var componentCatalog = [
25428
25540
  { type: "group", techStack: "Security Zone", category: "Grouping", searchTerms: "group security zone boundary" },
25429
25541
  { type: "group", techStack: "Service Group", category: "Grouping", searchTerms: "group service container boundary" }
25430
25542
  ];
25543
+ var techKey = (name) => name.toLowerCase().replace(/[^a-z0-9#+]/g, "");
25544
+ var entries = componentCatalog;
25545
+ var byKey = /* @__PURE__ */ new Map();
25546
+ for (const c of entries) {
25547
+ for (const name of [c.techStack, ...c.aliases ?? []]) {
25548
+ const key = techKey(name);
25549
+ if (!byKey.has(key)) byKey.set(key, c);
25550
+ }
25551
+ }
25552
+ var VERSION = /[\s-]*v?\d+(\.(\d+|x))*\s*$/i;
25553
+ function findTech(name) {
25554
+ const exact = byKey.get(techKey(name));
25555
+ if (exact) return exact;
25556
+ const unversioned = name.replace(VERSION, "");
25557
+ return unversioned !== name && unversioned.trim() ? byKey.get(techKey(unversioned)) : void 0;
25558
+ }
25559
+ function editDistance(a, b) {
25560
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
25561
+ for (let i = 1; i <= a.length; i++) {
25562
+ for (let j = 1; j <= b.length; j++) {
25563
+ const cost2 = a[i - 1] === b[j - 1] ? 0 : 1;
25564
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost2);
25565
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
25566
+ }
25567
+ }
25568
+ return d[a.length][b.length];
25569
+ }
25570
+ function suggestTech(name, accept = (c) => c.type !== "group" && c.type !== "text" && c.techStack !== "Note") {
25571
+ const key = techKey(name);
25572
+ if (!key) return void 0;
25573
+ let best;
25574
+ for (const c of entries) {
25575
+ if (!accept(c)) continue;
25576
+ for (const candidate of [c.techStack, ...c.aliases ?? []]) {
25577
+ const d = editDistance(key, techKey(candidate));
25578
+ if (!best || d < best.d) best = { tech: c.techStack, d };
25579
+ }
25580
+ }
25581
+ if (best && best.d <= Math.max(1, Math.floor(key.length / 4))) return best.tech;
25582
+ const words = name.toLowerCase().split(/[^a-z0-9.#+]+/).filter(Boolean);
25583
+ for (let n = Math.min(3, words.length); n >= 1; n--) {
25584
+ for (let i = 0; i + n <= words.length; i++) {
25585
+ const found = byKey.get(techKey(words.slice(i, i + n).join(" ")));
25586
+ if (found && accept(found) && techKey(words.slice(i, i + n).join("")).length >= 3) return found.techStack;
25587
+ }
25588
+ }
25589
+ return void 0;
25590
+ }
25431
25591
 
25432
25592
  // ../frontend/src/dsl/lexer.ts
25433
25593
  var IDENT_START = /[A-Za-z_]/;
@@ -25577,22 +25737,30 @@ function stripTrailingComment(text) {
25577
25737
  }
25578
25738
  return text;
25579
25739
  }
25580
- function bracketDepth(text) {
25581
- let depth = 0;
25582
- let inString = false;
25583
- for (let i = 0; i < text.length; i++) {
25584
- const ch = text[i];
25585
- if (inString) {
25586
- if (ch === "\\") i++;
25587
- else if (ch === '"') inString = false;
25588
- continue;
25740
+ var BracketCounter = class {
25741
+ depth = 0;
25742
+ inString = false;
25743
+ escaped = false;
25744
+ /** Adds `text` and returns the depth so far. */
25745
+ feed(text) {
25746
+ for (let i = 0; i < text.length; i++) {
25747
+ const ch = text[i];
25748
+ if (this.escaped) {
25749
+ this.escaped = false;
25750
+ continue;
25751
+ }
25752
+ if (this.inString) {
25753
+ if (ch === "\\") this.escaped = true;
25754
+ else if (ch === '"') this.inString = false;
25755
+ continue;
25756
+ }
25757
+ if (ch === '"') this.inString = true;
25758
+ else if (ch === "{" || ch === "[") this.depth++;
25759
+ else if (ch === "}" || ch === "]") this.depth--;
25589
25760
  }
25590
- if (ch === '"') inString = true;
25591
- else if (ch === "{" || ch === "[") depth++;
25592
- else if (ch === "}" || ch === "]") depth--;
25761
+ return this.depth;
25593
25762
  }
25594
- return depth;
25595
- }
25763
+ };
25596
25764
 
25597
25765
  // ../frontend/src/dsl/paths.ts
25598
25766
  var PARAM2 = /^\{[^{}/]+\}$/;
@@ -25630,26 +25798,7 @@ function endpointGroupKey(endpoint, all) {
25630
25798
  var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
25631
25799
  var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
25632
25800
  var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
25633
- var BY_TECH = {
25634
- "AWS CloudFront": "cdn",
25635
- "Azure CDN": "cdn",
25636
- "GCP Cloud CDN": "cdn",
25637
- "Azure Front Door": "cdn",
25638
- "AWS Load Balancer": "loadbalancer",
25639
- "GCP Load Balancing": "loadbalancer",
25640
- "AWS API Gateway": "gateway",
25641
- "Azure API Management": "gateway",
25642
- "AWS Route53": "dns",
25643
- "Azure DNS": "dns",
25644
- "GCP Cloud DNS": "dns",
25645
- Redis: "cache",
25646
- Elasticsearch: "search",
25647
- "GCP BigQuery": "analytics",
25648
- InfluxDB: "analytics",
25649
- TimescaleDB: "analytics",
25650
- // A note shape annotates the diagram; it is not a client.
25651
- Note: "other"
25652
- };
25801
+ var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
25653
25802
  var BY_TYPE = {
25654
25803
  shape: "client",
25655
25804
  cdn: "edge",
@@ -25665,7 +25814,44 @@ var BY_TYPE = {
25665
25814
  };
25666
25815
  function kindOf(node) {
25667
25816
  if (node.kind !== "component") return "other";
25668
- return BY_TECH[node.techStack] ?? BY_TYPE[node.type] ?? "other";
25817
+ return node.inferredKind ?? BY_TECH.get(node.techStack) ?? BY_TYPE[node.type] ?? "other";
25818
+ }
25819
+ var TYPE_OF_KIND = {
25820
+ client: "shape",
25821
+ edge: "cdn",
25822
+ cdn: "cdn",
25823
+ loadbalancer: "cdn",
25824
+ gateway: "cdn",
25825
+ dns: "cdn",
25826
+ service: "service",
25827
+ function: "serverless",
25828
+ cache: "cache",
25829
+ database: "database",
25830
+ search: "database",
25831
+ analytics: "database",
25832
+ queue: "queue",
25833
+ storage: "storage",
25834
+ external: "external",
25835
+ other: "shape"
25836
+ };
25837
+ var KIND_WORDS = [
25838
+ [/\bsms\b|e-?mail|third.?party|3rd.?party|external|saas|webhook/, "external"],
25839
+ [/\bcdn\b|edge.?cache|content.?delivery/, "cdn"],
25840
+ [/\bdns\b/, "dns"],
25841
+ [/load.?balanc|\blb\b|\balb\b|\bnlb\b|proxy|ingress/, "loadbalancer"],
25842
+ [/gateway|\bapim?\b.*manage/, "gateway"],
25843
+ [/\bwaf\b|firewall/, "edge"],
25844
+ [/cache|memcache/, "cache"],
25845
+ [/queue|\bmq\b|mq$|broker|event.?stream|\bbus\b|topic|pub.?sub|event.?hub|kafka/, "queue"],
25846
+ [/search|lucene|\bindex\b/, "search"],
25847
+ [/warehouse|analytic|olap|data.?lake|lakehouse|time.?series|\btsdb\b/, "analytics"],
25848
+ [/bucket|blob|object.?stor|storage|\bfs\b|file.?system|\bnas\b|\bdisk\b|volume/, "storage"],
25849
+ [/\bdb\b|db$|database|\bsql\b|sql$|nosql|\bstore\b|ledger|key.?value|\bkv\b/, "database"],
25850
+ [/lambda|function|serverless|\bfaas\b/, "function"]
25851
+ ];
25852
+ function kindFromName(tech) {
25853
+ const text = tech.toLowerCase();
25854
+ return KIND_WORDS.find(([words]) => words.test(text))?.[1];
25669
25855
  }
25670
25856
  function isKind(word) {
25671
25857
  return KINDS.includes(word);
@@ -25775,6 +25961,7 @@ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
25775
25961
  var DEFAULT_TECH = "Rectangle";
25776
25962
  var DEFAULT_GROUP_TECH = "Logical Group";
25777
25963
  var MAX_SCENARIOS = 32;
25964
+ var MAX_ALT_DEPTH = 16;
25778
25965
  var REPLICAS = /^x(\d+)$/;
25779
25966
  var PERCENTILES = { p50: 50, p90: 90, p95: 95, p99: 99, p999: 99.9 };
25780
25967
  var FIELD_FLAGS = ["key", "index", "unique", "optional"];
@@ -25786,12 +25973,32 @@ var UNIT_EXAMPLES = {
25786
25973
  MBps: "a bandwidth, e.g. 500 MB/s or 1 GB/s",
25787
25974
  "usd/GB": "a price per gigabyte, e.g. 0.05 usd/GB"
25788
25975
  };
25789
- var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth or egress";
25976
+ var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth, egress or timeout";
25790
25977
  var ASSERTION_HELP = 'Expected an assertion: "Use case" calls <node>, "Use case" writes <node> before responding, <node> calls <node>, no path from <node> to <node>, <node> has replicas >= 2, \u2026';
25791
25978
  var ASSERTION_VERBS = "calls, every scenario calls, never calls, never waits for, writes, responds, starts at, has scenario or handles failure of";
25792
- var techByName = new Map(
25793
- componentCatalog.map((c) => [c.techStack.toLowerCase(), { type: c.type, techStack: c.techStack }])
25794
- );
25979
+ var GENERIC_TECH = {
25980
+ client: "Actor",
25981
+ edge: "WAF",
25982
+ cdn: "CDN",
25983
+ loadbalancer: "Load Balancer",
25984
+ gateway: "API Gateway",
25985
+ dns: "DNS",
25986
+ service: "Service",
25987
+ function: "Function",
25988
+ cache: "Cache",
25989
+ database: "Database",
25990
+ search: "Search Engine",
25991
+ analytics: "Data Warehouse",
25992
+ queue: "Message Queue",
25993
+ storage: "Object Storage",
25994
+ external: "Third Party API",
25995
+ other: "Note"
25996
+ };
25997
+ function unknownTechMessage(tech, kind, suggestion) {
25998
+ const simulated = `simulated as a generic ${kind}${suggestion === GENERIC_TECH[kind] ? "" : `, like [${GENERIC_TECH[kind]}]`}`;
25999
+ return suggestion ? `Unknown tech stack '${tech}'. Did you mean '${suggestion}'? Until then it is ${simulated}` : `Unknown tech stack '${tech}'; ${simulated}`;
26000
+ }
26001
+ var DID_YOU_MEAN = /^Unknown tech stack '(.*)'\. Did you mean '([^']+)'\?/;
25795
26002
  function parse(source2, options = {}) {
25796
26003
  return new Parser(source2, options).run();
25797
26004
  }
@@ -25802,8 +26009,12 @@ var Parser = class {
25802
26009
  file;
25803
26010
  stack = [];
25804
26011
  diagnostics = [];
26012
+ /** Warnings reported so far (see `warning`). */
26013
+ warned = /* @__PURE__ */ new Set();
25805
26014
  nodes = /* @__PURE__ */ new Map();
25806
26015
  edges = [];
26016
+ /** Connections so far per `from->to`, for the ids of repeated ones. */
26017
+ edgeCount = /* @__PURE__ */ new Map();
25807
26018
  useCases = [];
25808
26019
  references = [];
25809
26020
  imports = [];
@@ -25880,6 +26091,17 @@ var Parser = class {
25880
26091
  const loc = (t) => this.locOf(t, line);
25881
26092
  const top = this.top();
25882
26093
  if (top?.kind === "section") {
26094
+ if (top.skipped !== void 0) {
26095
+ const opens = tokens[tokens.length - 1].kind === "lbrace";
26096
+ if (first.kind !== "rbrace") top.nested = (top.nested ?? 0) + (opens ? 1 : 0);
26097
+ else if (top.nested) top.nested += opens ? 0 : -1;
26098
+ else {
26099
+ this.stack.pop();
26100
+ if (second?.kind === "ident" && second.value === "alt") this.parseAlt(tokens.slice(1), line);
26101
+ else if (second) this.error("Unexpected input after }", loc(second));
26102
+ }
26103
+ return index;
26104
+ }
25883
26105
  if (first.kind === "rbrace") {
25884
26106
  this.stack.pop();
25885
26107
  if (!top.discard) this.closeSection(top);
@@ -26028,8 +26250,8 @@ var Parser = class {
26028
26250
  if (tokens[i]?.kind === "string") name = tokens[i++].value;
26029
26251
  if (tokens[i]?.kind === "tech") {
26030
26252
  const tech = tokens[i++];
26031
- const resolved = this.resolveTech(tech, line);
26032
- if (resolved.type === "group") techStack = resolved.techStack;
26253
+ const resolved = findTech(tech.value);
26254
+ if (resolved?.type === "group") techStack = resolved.techStack;
26033
26255
  else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
26034
26256
  }
26035
26257
  let position;
@@ -26104,6 +26326,12 @@ var Parser = class {
26104
26326
  return;
26105
26327
  }
26106
26328
  this.expectEnd(tokens, i + 1, line);
26329
+ if (this.stack.filter((f) => f.kind === "alt").length >= MAX_ALT_DEPTH) {
26330
+ const loc2 = this.locOf(keyword, line);
26331
+ this.error(`alt blocks can nest at most ${MAX_ALT_DEPTH} deep; this one is skipped`, loc2);
26332
+ this.stack.push({ kind: "section", section: "test", discard: true, skipped: `alt '${nameToken.value}'`, loc: loc2 });
26333
+ return;
26334
+ }
26107
26335
  const owner = top.body;
26108
26336
  let set = owner.openAlt;
26109
26337
  if (!set) {
@@ -26156,6 +26384,7 @@ var Parser = class {
26156
26384
  const resolved = this.resolveTech(t, line);
26157
26385
  node.type = resolved.type;
26158
26386
  node.techStack = resolved.techStack;
26387
+ if (resolved.inferredKind) node.inferredKind = resolved.inferredKind;
26159
26388
  node.kind = resolved.type === "text" ? "text" : resolved.type === "group" ? "group" : "component";
26160
26389
  } else if (t.kind === "team") {
26161
26390
  node.ownerTeam = t.value;
@@ -26197,12 +26426,16 @@ var Parser = class {
26197
26426
  }
26198
26427
  let label = labelToken?.value ?? "";
26199
26428
  let lastLine = index;
26200
- if (bracketDepth(label) > 0) {
26201
- while (bracketDepth(label) > 0 && lastLine + 1 < this.lines.length) {
26429
+ const brackets = new BracketCounter();
26430
+ let depth = brackets.feed(label);
26431
+ if (depth > 0) {
26432
+ while (depth > 0 && lastLine + 1 < this.lines.length) {
26202
26433
  lastLine++;
26203
- label += "\n" + this.lines[lastLine];
26434
+ const more = "\n" + this.lines[lastLine];
26435
+ label += more;
26436
+ depth = brackets.feed(more);
26204
26437
  }
26205
- if (bracketDepth(label) > 0) this.error("Unclosed { or [ in payload", this.locOf(labelToken, line));
26438
+ if (depth > 0) this.error("Unclosed { or [ in payload", this.locOf(labelToken, line));
26206
26439
  label = label.trimEnd();
26207
26440
  }
26208
26441
  const loc = {
@@ -26237,7 +26470,8 @@ var Parser = class {
26237
26470
  this.error("'-x' marks a failed call and is only allowed in use case steps", this.locOf(arrowToken, line));
26238
26471
  } else {
26239
26472
  const base = `${from}->${to}`;
26240
- const count = this.edges.filter((e) => e.id === base || e.id.startsWith(`${base}#`)).length;
26473
+ const count = this.edgeCount.get(base) ?? 0;
26474
+ this.edgeCount.set(base, count + 1);
26241
26475
  this.edges.push({ id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc });
26242
26476
  }
26243
26477
  return lastLine;
@@ -26505,12 +26739,13 @@ var Parser = class {
26505
26739
  if (rps === void 0) return;
26506
26740
  override.rps = rps;
26507
26741
  i++;
26508
- } else if (isWord(t, "latency") || isWord(t, "availability") || isWord(t, "cost")) {
26742
+ } else if (isWord(t, "latency") || isWord(t, "availability") || isWord(t, "cost") || isWord(t, "timeout")) {
26509
26743
  part = t.value;
26510
- const unit = part === "latency" ? "ms" : part === "availability" ? "%" : "usd/month";
26744
+ const unit = part === "latency" || part === "timeout" ? "ms" : part === "availability" ? "%" : "usd/month";
26511
26745
  const value = this.quantityOf(tokens[i + 1], unit, t, line);
26512
26746
  if (value === void 0) return;
26513
26747
  if (part === "latency") override.latencyMs = value;
26748
+ else if (part === "timeout") override.timeoutMs = value;
26514
26749
  else if (part === "availability") override.availability = value;
26515
26750
  else override.costUsd = value;
26516
26751
  i += 2;
@@ -26918,7 +27153,7 @@ var Parser = class {
26918
27153
  parseSelectorAtom(tokens, i, line) {
26919
27154
  const t = tokens[i];
26920
27155
  if (t?.kind === "tech") {
26921
- const tech = techByName.get(t.value.toLowerCase())?.techStack ?? t.value;
27156
+ const tech = findTech(t.value)?.techStack ?? t.value;
26922
27157
  return { selector: { tech }, next: i + 1, token: t, loc: this.locOf(t, line) };
26923
27158
  }
26924
27159
  if (isWord(t, "any")) {
@@ -26943,7 +27178,10 @@ var Parser = class {
26943
27178
  /** Warns about a selector naming a tech stack that does not exist, and (once every file is read) a node that does not. */
26944
27179
  checkSelectorLater({ atoms }) {
26945
27180
  for (const { selector, token, loc } of atoms) {
26946
- if ("tech" in selector && !techByName.has(token.value.toLowerCase())) this.warning(`Unknown tech stack '${token.value}'`, loc);
27181
+ if ("tech" in selector && !findTech(token.value)) {
27182
+ const suggestion = suggestTech(token.value);
27183
+ this.warning(`Unknown tech stack '${token.value}'${suggestion ? `. Did you mean '${suggestion}'?` : ""}`, loc);
27184
+ }
26947
27185
  if ("node" in selector) {
26948
27186
  this.deferred.push(() => {
26949
27187
  if (!this.nodes.has(selector.node)) this.warning(`Unknown node '${selector.node}'`, loc);
@@ -27103,11 +27341,27 @@ var Parser = class {
27103
27341
  where(first, later) {
27104
27342
  return first.file === later.file ? `on line ${first.line}` : `in ${fileName(first.file ?? this.options.path ?? "the root document")} on line ${first.line}`;
27105
27343
  }
27344
+ /**
27345
+ * A node's `[Tech]`: the catalog entry it names (name or alias), or for an
27346
+ * unknown tech the text as written, with a warning that names the closest
27347
+ * catalog tech. It is drawn and simulated as the kind its name suggests
27348
+ * (`TigerBeetle DB`: database), else the kind of that closest tech
27349
+ * (`Postgress`: database), else a service; never a free, infinitely fast
27350
+ * client.
27351
+ */
27106
27352
  resolveTech(token, line) {
27107
- const resolved = techByName.get(token.value.toLowerCase());
27108
- if (resolved) return resolved;
27109
- this.warning(`Unknown tech stack '${token.value}'; drawing a ${DEFAULT_TECH}`, this.locOf(token, line));
27110
- return { type: "shape", techStack: DEFAULT_TECH };
27353
+ const found = findTech(token.value);
27354
+ if (found) return { type: found.type, techStack: found.techStack };
27355
+ const text = token.value.trim();
27356
+ if (!text) {
27357
+ this.warning(`Empty tech stack; drawing a ${DEFAULT_TECH}`, this.locOf(token, line));
27358
+ return { type: "shape", techStack: DEFAULT_TECH };
27359
+ }
27360
+ const suggestion = suggestTech(text);
27361
+ const closest = suggestion && findTech(suggestion);
27362
+ const kind = kindFromName(text) ?? (closest ? kindOf({ kind: "component", type: closest.type, techStack: closest.techStack }) : void 0) ?? "service";
27363
+ this.warning(unknownTechMessage(text, kind, suggestion), this.locOf(token, line));
27364
+ return { type: TYPE_OF_KIND[kind], techStack: text, inferredKind: kind };
27111
27365
  }
27112
27366
  useCaseId(name) {
27113
27367
  const base = slug(name) || "usecase";
@@ -27163,7 +27417,12 @@ var Parser = class {
27163
27417
  this.diagnostics.push({ severity: "error", message, ...loc });
27164
27418
  }
27165
27419
  warning(message, loc) {
27166
- if (this.diagnostics.some((d) => d.message === message && d.line === loc.line && d.col === loc.col && d.file === loc.file)) return;
27420
+ const key = `${loc.file ?? ""}
27421
+ ${loc.line}
27422
+ ${loc.col}
27423
+ ${message}`;
27424
+ if (this.warned.has(key)) return;
27425
+ this.warned.add(key);
27167
27426
  this.diagnostics.push({ severity: "warning", message, ...loc });
27168
27427
  }
27169
27428
  };
@@ -27196,6 +27455,7 @@ function isWord(token, word) {
27196
27455
  return token?.kind === "ident" && token.value === word;
27197
27456
  }
27198
27457
  function sectionName(frame) {
27458
+ if (frame.skipped) return frame.skipped;
27199
27459
  if (frame.entity) return `entity '${frame.entity.name}'`;
27200
27460
  if (frame.decision) return `decision '${frame.decision.title}'`;
27201
27461
  if (frame.test) return `test '${frame.test.name}'`;
@@ -27311,14 +27571,14 @@ ${text}
27311
27571
 
27312
27572
  // ../frontend/src/dsl/format.ts
27313
27573
  function format(source2) {
27314
- const entries = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
27574
+ const entries2 = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
27315
27575
  const out = [];
27316
27576
  let run = [];
27317
27577
  const flush = () => {
27318
27578
  if (run.length) out.push(...renderRun(run));
27319
27579
  run = [];
27320
27580
  };
27321
- tidyBlankLines(entries).forEach((entry) => {
27581
+ tidyBlankLines(entries2).forEach((entry) => {
27322
27582
  const last = run[run.length - 1];
27323
27583
  if (last && (entry.kind !== last.kind || entry.depth !== last.depth || last.kind === "connection" && last.continuation.length || last.kind === "cells" && entry.kind === "cells" && entry.section !== last.section))
27324
27584
  flush();
@@ -27334,19 +27594,19 @@ function format(source2) {
27334
27594
  var INDENT = " ";
27335
27595
  var KEYWORDS = /* @__PURE__ */ new Set(["title", "import", "group", "usecase", "par", "alt"]);
27336
27596
  function classify(lines) {
27337
- const entries = [];
27597
+ const entries2 = [];
27338
27598
  let depth = 0;
27339
27599
  let section;
27340
27600
  for (let i = 0; i < lines.length; i++) {
27341
27601
  const raw = lines[i];
27342
27602
  const original = raw.trim();
27343
27603
  if (!original) {
27344
- entries.push({ kind: "blank", depth, text: "" });
27604
+ entries2.push({ kind: "blank", depth, text: "" });
27345
27605
  continue;
27346
27606
  }
27347
27607
  const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
27348
27608
  if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
27349
- entries.push({ kind: "line", depth, text: original });
27609
+ entries2.push({ kind: "line", depth, text: original });
27350
27610
  continue;
27351
27611
  }
27352
27612
  const comment = commentOf(raw, tokens);
@@ -27355,22 +27615,22 @@ function classify(lines) {
27355
27615
  const inSection = section !== void 0 && depth === 1 && first.kind !== "rbrace";
27356
27616
  const cells = inSection ? sectionCells(section, raw, tokens) : void 0;
27357
27617
  if (cells) {
27358
- entries.push({ ...base, kind: "cells", section, cells });
27618
+ entries2.push({ ...base, kind: "cells", section, cells });
27359
27619
  continue;
27360
27620
  }
27361
27621
  if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
27362
27622
  const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, label: fourth?.value, continuation: [] };
27363
- if (fourth && bracketDepth(fourth.value) > 0) {
27364
- const delta = depth * INDENT.length - width(leadingSpace(raw));
27365
- let label = fourth.value;
27366
- while (bracketDepth(label) > 0 && i + 1 < lines.length) {
27623
+ const brackets = new BracketCounter();
27624
+ if (fourth && brackets.feed(fourth.value) > 0) {
27625
+ const delta = indent(depth).length - width(leadingSpace(raw));
27626
+ while (i + 1 < lines.length) {
27367
27627
  const next = lines[++i];
27368
- label += "\n" + next;
27369
27628
  entry.continuation.push(reindent(next, delta));
27629
+ if (brackets.feed("\n" + next) <= 0) break;
27370
27630
  }
27371
27631
  while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
27372
27632
  }
27373
- entries.push(entry);
27633
+ entries2.push(entry);
27374
27634
  continue;
27375
27635
  }
27376
27636
  const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
@@ -27378,19 +27638,19 @@ function classify(lines) {
27378
27638
  let k = 1;
27379
27639
  const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
27380
27640
  const cells2 = [first.value, take("string"), take("tech"), take("team"), join2(raw, tokens.slice(k))];
27381
- entries.push({ ...base, kind: "node", cells: cells2 });
27641
+ entries2.push({ ...base, kind: "node", cells: cells2 });
27382
27642
  continue;
27383
27643
  }
27384
27644
  const closes = first.kind === "rbrace";
27385
27645
  const opens = tokens[tokens.length - 1].kind === "lbrace";
27386
27646
  const code = join2(raw, tokens);
27387
27647
  const text = withComment(code, comment);
27388
- entries.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
27648
+ entries2.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
27389
27649
  if (depth === 0) section = sectionOpenedBy(tokens);
27390
27650
  for (const t of tokens) depth = Math.max(0, depth + (t.kind === "lbrace" ? 1 : t.kind === "rbrace" ? -1 : 0));
27391
27651
  if (depth === 0) section = void 0;
27392
27652
  }
27393
- return entries;
27653
+ return entries2;
27394
27654
  }
27395
27655
  function sectionOpenedBy(tokens) {
27396
27656
  const [first, second] = tokens;
@@ -27424,9 +27684,9 @@ function sectionCells(section, raw, tokens) {
27424
27684
  return void 0;
27425
27685
  }
27426
27686
  }
27427
- function tidyBlankLines(entries) {
27687
+ function tidyBlankLines(entries2) {
27428
27688
  const kept = [];
27429
- for (const entry of entries) {
27689
+ for (const entry of entries2) {
27430
27690
  if (entry.kind !== "blank") {
27431
27691
  if (entry.kind === "line" && entry.closes) while (kept[kept.length - 1]?.kind === "blank") kept.pop();
27432
27692
  kept.push(entry);
@@ -27507,7 +27767,8 @@ var source = (raw, t) => {
27507
27767
  var withComment = (code, comment) => comment ? code + " " + comment : code;
27508
27768
  var leadingSpace = (line) => /^[ \t]*/.exec(line)[0];
27509
27769
  var width = (space) => [...space].reduce((n, ch) => n + (ch === " " ? INDENT.length : 1), 0);
27510
- var indent = (depth) => INDENT.repeat(depth);
27770
+ var MAX_INDENT = 32;
27771
+ var indent = (depth) => INDENT.repeat(Math.min(depth, MAX_INDENT));
27511
27772
  var len = (s) => [...s].length;
27512
27773
  var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
27513
27774
 
@@ -27657,6 +27918,8 @@ function accessIn(diagram) {
27657
27918
 
27658
27919
  // ../frontend/src/sim/profiles.ts
27659
27920
  var NEUTRAL = { rps: Infinity, latencyMs: 0, availability: 1, costUsd: 0, durable: false };
27921
+ var INTERNET_EGRESS_USD_PER_GB = 0.09;
27922
+ var CDN_EGRESS_USD_PER_GB = 0.02;
27660
27923
  var RELATIONAL = {
27661
27924
  rps: 2e4,
27662
27925
  readRps: 2e4,
@@ -27670,8 +27933,9 @@ var RELATIONAL = {
27670
27933
  };
27671
27934
  var KIND_PROFILES = {
27672
27935
  client: { ...NEUTRAL, bandwidthMBps: 10 },
27936
+ // A firewall or accelerator in front of everything else (WAF, Global Accelerator); `any edge` also selects the four below.
27673
27937
  edge: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
27674
- cdn: { rps: 2e5, latencyMs: 5, availability: 0.9999, costUsd: 100, durable: false, egressUsdPerGb: 0.02 },
27938
+ cdn: { rps: 2e5, latencyMs: 5, availability: 0.9999, costUsd: 100, durable: false, egressUsdPerGb: CDN_EGRESS_USD_PER_GB },
27675
27939
  loadbalancer: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
27676
27940
  gateway: { rps: 1e4, latencyMs: 10, availability: 0.9995, costUsd: 100, durable: false },
27677
27941
  // Name resolution happens before the request and is cached: off the request path.
@@ -27683,23 +27947,16 @@ var KIND_PROFILES = {
27683
27947
  search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
27684
27948
  analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
27685
27949
  queue: { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" },
27686
- storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual", egressUsdPerGb: 0.09 },
27950
+ storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual" },
27687
27951
  external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
27688
27952
  other: NEUTRAL
27689
27953
  };
27690
27954
  var NOSQL = { rps: 2e4, latencyMs: 5, availability: 0.9999, costUsd: 500, durable: true, consistency: "eventual" };
27691
27955
  var NOSQL_STRONG = { ...NOSQL, consistency: "strong" };
27692
- var TECH_PROFILES = {
27693
- DynamoDB: NOSQL,
27694
- "AWS DynamoDB": NOSQL,
27695
- Cassandra: NOSQL,
27696
- CouchDB: NOSQL,
27697
- "Azure Cosmos DB": NOSQL,
27698
- MongoDB: NOSQL_STRONG,
27699
- "GCP Bigtable": NOSQL_STRONG,
27700
- "GCP Firestore": NOSQL_STRONG,
27701
- "GCP Spanner": NOSQL_STRONG
27702
- };
27956
+ var PROFILE_ROWS = { nosql: NOSQL, nosqlStrong: NOSQL_STRONG };
27957
+ var TECH_PROFILES = Object.fromEntries(
27958
+ componentCatalog.flatMap((c) => "profile" in c ? [[c.techStack, PROFILE_ROWS[c.profile]]] : [])
27959
+ );
27703
27960
  var BANDWIDTH = {
27704
27961
  client: 10,
27705
27962
  edge: 1e3,
@@ -27710,9 +27967,10 @@ var BANDWIDTH = {
27710
27967
  service: 200
27711
27968
  };
27712
27969
  var DEFAULT_BANDWIDTH_MBPS = 100;
27970
+ var CHARGES_EGRESS = (kind) => kind !== "client" && kind !== "external" && kind !== "dns" && kind !== "other";
27713
27971
  function profileOf(node, override) {
27714
27972
  const kind = kindOf(node);
27715
- const base = TECH_PROFILES[node.techStack] ?? KIND_PROFILES[kind];
27973
+ const base = (Object.hasOwn(TECH_PROFILES, node.techStack) ? TECH_PROFILES[node.techStack] : void 0) ?? KIND_PROFILES[kind];
27716
27974
  const baseRead = base.readRps ?? base.rps;
27717
27975
  const baseWrite = base.writeRps ?? base.rps;
27718
27976
  const readRps = override?.readRps ?? override?.rps ?? baseRead;
@@ -27731,7 +27989,8 @@ function profileOf(node, override) {
27731
27989
  durable: override?.durable ?? base.durable,
27732
27990
  ...consistency && isDataStore(kind) ? { consistency } : {},
27733
27991
  bandwidthMBps: override?.bandwidthMBps ?? base.bandwidthMBps ?? BANDWIDTH[kind] ?? DEFAULT_BANDWIDTH_MBPS,
27734
- egressUsdPerGb: override?.egressUsdPerGb ?? base.egressUsdPerGb ?? 0
27992
+ egressUsdPerGb: override?.egressUsdPerGb ?? base.egressUsdPerGb ?? (CHARGES_EGRESS(kind) ? INTERNET_EGRESS_USD_PER_GB : 0),
27993
+ ...override?.timeoutMs !== void 0 ? { timeoutMs: override.timeoutMs } : {}
27735
27994
  };
27736
27995
  }
27737
27996
  function capacityOf(p, replicas) {
@@ -27884,10 +28143,11 @@ function formatUsd(usd) {
27884
28143
  var DEFAULT_TIMEOUT_MS = 1e3;
27885
28144
  var HOT = 0.7;
27886
28145
  var QUEUE_CAP = 0.95;
28146
+ var FAILOVER_SHARE = 0.1;
28147
+ var SERVICE_SPREAD = 0.5;
27887
28148
  var SECONDS_PER_MONTH = 2592e3;
27888
28149
  var BYTES_PER_MB = 1e6;
27889
28150
  var BYTES_PER_GB = 1e9;
27890
- var PERCENTILE_FACTORS = { 50: 1, 90: 1.6, 95: 2, 99: 3, 99.9: 5 };
27891
28151
  var PERCENTILE_KEYS = { 50: "p50", 90: "p90", 95: "p95", 99: "p99", 99.9: "p999" };
27892
28152
  var PERCENTILES2 = Object.keys(PERCENTILE_KEYS).map(Number);
27893
28153
  function components(diagram) {
@@ -27928,7 +28188,9 @@ function forEachStepRate(diagram, traffic, visit) {
27928
28188
  const { rps, shares } = traffic.get(u.id) ?? { rps: 0, shares: [] };
27929
28189
  u.scenarios.forEach((s, i) => {
27930
28190
  const rate = rps * (shares[i] ?? 0);
27931
- if (rate > 0) for (const step of s.steps) visit(step, rate);
28191
+ if (rate > 0) {
28192
+ for (const step of s.steps) if (!step.failed) visit(step, rate);
28193
+ }
27932
28194
  });
27933
28195
  }
27934
28196
  }
@@ -27942,35 +28204,76 @@ function computeLoad(diagram, traffic) {
27942
28204
  });
27943
28205
  return load;
27944
28206
  }
27945
- var egressSource = (step, access) => access === "read" ? step.toServiceId : step.fromServiceId;
27946
- function computeEgressGb(diagram, traffic) {
28207
+ var payloadDirection = (step, access) => access === "read" ? { from: step.toServiceId, to: step.fromServiceId } : { from: step.fromServiceId, to: step.toServiceId };
28208
+ var OUTSIDE = (kind) => kind === "client" || kind === "external";
28209
+ function computeEgressGb(diagram, traffic, kindOf2 = (id) => profileOfId(diagram, id)) {
27947
28210
  const gb = /* @__PURE__ */ new Map();
27948
28211
  const access = accessIn(diagram);
27949
28212
  forEachStepRate(diagram, traffic, (step, rate) => {
27950
28213
  if (!step.sizeBytes || step.sizeBytes <= 0) return;
27951
- const source2 = egressSource(step, access(step));
27952
- gb.set(source2, (gb.get(source2) ?? 0) + rate * multiplierOf(step) * step.sizeBytes * SECONDS_PER_MONTH / BYTES_PER_GB);
28214
+ const { from, to } = payloadDirection(step, access(step));
28215
+ if (OUTSIDE(kindOf2(from)) || !OUTSIDE(kindOf2(to))) return;
28216
+ gb.set(from, (gb.get(from) ?? 0) + rate * multiplierOf(step) * step.sizeBytes * SECONDS_PER_MONTH / BYTES_PER_GB);
27953
28217
  });
27954
28218
  return gb;
27955
28219
  }
28220
+ var BANDWIDTH_BOUND = (kind) => !["client", "external", "dns", "other", "storage", "cdn"].includes(kind);
28221
+ function computeBytesPerSecond(diagram, traffic) {
28222
+ const bytes = /* @__PURE__ */ new Map();
28223
+ forEachStepRate(diagram, traffic, (step, rate) => {
28224
+ if (!step.sizeBytes || step.sizeBytes <= 0) return;
28225
+ const b = rate * multiplierOf(step) * step.sizeBytes;
28226
+ for (const id of /* @__PURE__ */ new Set([step.fromServiceId, step.toServiceId])) bytes.set(id, (bytes.get(id) ?? 0) + b);
28227
+ });
28228
+ return bytes;
28229
+ }
28230
+ function profileOfId(diagram, id) {
28231
+ const node = diagram.nodes.find((n) => n.id === id);
28232
+ return node ? profileOf(node).kind : "client";
28233
+ }
27956
28234
  function transferMs(step, bandwidthOf) {
27957
28235
  if (!step.sizeBytes || step.sizeBytes <= 0) return 0;
27958
28236
  const mbps = Math.min(bandwidthOf(step.fromServiceId), bandwidthOf(step.toServiceId));
27959
- return mbps > 0 && Number.isFinite(mbps) ? step.sizeBytes / BYTES_PER_MB / mbps * 1e3 : 0;
28237
+ return mbps > 0 && Number.isFinite(mbps) ? multiplierOf(step) * step.sizeBytes / BYTES_PER_MB / mbps * 1e3 : 0;
27960
28238
  }
27961
28239
  var utilizationOf = (load, capacity) => Number.isFinite(capacity) && capacity > 0 ? load / capacity : 0;
27962
- var hopLatency = (baseMs, utilization) => baseMs / (1 - Math.min(utilization, QUEUE_CAP));
28240
+ function erlangC(servers, rho) {
28241
+ if (rho <= 0) return 0;
28242
+ if (rho >= 1) return 1;
28243
+ const c = Math.max(1, Math.round(servers));
28244
+ const a = c * rho;
28245
+ let b = 1;
28246
+ for (let k = 1; k <= c; k++) b = a * b / (k + a * b);
28247
+ return b / (1 - rho * (1 - b));
28248
+ }
28249
+ function hopModel(baseMs, rho, servers = 1) {
28250
+ const r = Math.min(Math.max(rho, 0), QUEUE_CAP);
28251
+ const c = Math.max(1, Math.round(servers));
28252
+ const waitProbability = erlangC(c, r);
28253
+ const meanMs = baseMs + waitProbability * baseMs / (c * (1 - r));
28254
+ const fixedMs = (1 - SERVICE_SPREAD) * baseMs;
28255
+ return { fixedMs, tailMs: meanMs - fixedMs, waitProbability };
28256
+ }
28257
+ var hopMean = (h) => h.fixedMs + h.tailMs;
28258
+ var tailFactor = (p) => -Math.log(1 - p);
27963
28259
  var replicatedAvailability = (a, replicas) => 1 - (1 - a) ** replicas;
27964
- function usageOf(p, replicas, readLoadRps, writeLoadRps) {
28260
+ var primaryAvailability = (a, replicas) => replicas >= 2 ? 1 - (1 - a) * FAILOVER_SHARE : a;
28261
+ function usageOf(p, replicas, readLoadRps, writeLoadRps, bytesPerSecond = 0) {
27965
28262
  const { readRps: readCapacityRps, writeRps: writeCapacityRps } = capacityOf(p, replicas);
27966
28263
  const readUtilization = utilizationOf(readLoadRps, readCapacityRps);
27967
28264
  const writeUtilization = utilizationOf(writeLoadRps, writeCapacityRps);
27968
- const utilization = combinedUtilization(p, readUtilization, writeUtilization);
28265
+ const requestUtilization = combinedUtilization(p, readUtilization, writeUtilization);
27969
28266
  const loadRps = readLoadRps + writeLoadRps;
28267
+ const bound = BANDWIDTH_BOUND(p.kind);
28268
+ const bandwidthLoadMBps = bound ? bytesPerSecond / BYTES_PER_MB : 0;
28269
+ const bandwidthCapacityMBps = bound ? p.bandwidthMBps * replicas * p.shards : 0;
28270
+ const bandwidthUtilization = utilizationOf(bandwidthLoadMBps, bandwidthCapacityMBps);
28271
+ const utilization = Math.max(requestUtilization, bandwidthUtilization);
27970
28272
  return {
27971
28273
  loadRps,
27972
- capacityRps: readCapacityRps === writeCapacityRps || utilization === 0 ? readCapacityRps : loadRps / utilization,
28274
+ capacityRps: readCapacityRps === writeCapacityRps || requestUtilization === 0 ? readCapacityRps : loadRps / requestUtilization,
27973
28275
  utilization,
28276
+ requestUtilization,
27974
28277
  saturated: utilization >= 1,
27975
28278
  readLoadRps,
27976
28279
  writeLoadRps,
@@ -27978,27 +28281,68 @@ function usageOf(p, replicas, readLoadRps, writeLoadRps) {
27978
28281
  writeCapacityRps,
27979
28282
  readUtilization,
27980
28283
  writeUtilization,
28284
+ bandwidthLoadMBps,
28285
+ bandwidthCapacityMBps,
28286
+ bandwidthUtilization,
27981
28287
  writeScaling: p.writeScaling
27982
28288
  };
27983
28289
  }
28290
+ var bandwidthBound = (n) => n.bandwidthUtilization > n.requestUtilization;
28291
+ var serversOf = (n, replicas) => writeBound(n) ? 1 : replicas;
27984
28292
  function usageText(n) {
27985
28293
  const part = (load, capacity, rho) => `${formatRps(load)} of ${formatRps(capacity)}, ${formatPercent(rho)}`;
28294
+ if (bandwidthBound(n)) {
28295
+ const mb = (x) => `${Number(x.toPrecision(3)).toLocaleString("en-US")} MB/s`;
28296
+ return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
28297
+ }
27986
28298
  if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
27987
28299
  return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
27988
28300
  }
27989
- return part(n.loadRps, n.capacityRps, n.utilization);
28301
+ return part(n.loadRps, n.capacityRps, n.requestUtilization);
27990
28302
  }
27991
28303
  var writeBound = (n) => n.writeScaling === "shards" && n.writeUtilization >= n.readUtilization && n.writeLoadRps > 0;
27992
- function useCasePercentile(scenarios, q) {
27993
- const tail = 1 - q / 100 - 1e-9;
27994
- const heavy = scenarios.filter((s) => s.share >= tail && s.share > 0);
27995
- if (heavy.length) return Math.max(...heavy.map((s) => s.value));
27996
- let covered = 0;
27997
- for (const s of [...scenarios].sort((a, b) => b.value - a.value)) {
27998
- covered += s.share;
27999
- if (covered >= tail) return s.value;
28000
- }
28001
- return scenarios.length ? Math.max(...scenarios.map((s) => s.value)) : 0;
28304
+ function pathMean(path) {
28305
+ return path.parts.reduce((sum, part) => sum + Math.max(0, ...part.map((i) => i.fixedMs + i.tailMs)), 0);
28306
+ }
28307
+ function pathAt(path, l) {
28308
+ let sum = 0;
28309
+ for (const part of path.parts) {
28310
+ let most = 0;
28311
+ for (const i of part) most = Math.max(most, i.fixedMs + i.tailMs * l);
28312
+ sum += most;
28313
+ }
28314
+ return sum;
28315
+ }
28316
+ var pathQuantile = (path, p) => pathAt(path, tailFactor(p));
28317
+ function pathCdf(path, t) {
28318
+ if (pathAt(path, 0) > t) return 0;
28319
+ let hi = 1;
28320
+ while (pathAt(path, hi) <= t) {
28321
+ hi *= 2;
28322
+ if (hi > 1e6) return 1;
28323
+ }
28324
+ let lo = 0;
28325
+ for (let i = 0; i < 50; i++) {
28326
+ const mid = (lo + hi) / 2;
28327
+ if (pathAt(path, mid) <= t) lo = mid;
28328
+ else hi = mid;
28329
+ }
28330
+ return 1 - Math.exp(-lo);
28331
+ }
28332
+ function mixtureQuantile(paths, q) {
28333
+ const live = paths.filter((s) => s.share > 0);
28334
+ if (live.length === 0) return 0;
28335
+ if (live.length === 1) return pathQuantile(live[0].path, q);
28336
+ const total = live.reduce((sum, s) => sum + s.share, 0);
28337
+ const cdf = (t) => live.reduce((sum, s) => sum + s.share * pathCdf(s.path, t), 0) / total;
28338
+ let lo = Math.min(...live.map((s) => pathQuantile(s.path, 0)));
28339
+ let hi = Math.max(...live.map((s) => pathQuantile(s.path, q)));
28340
+ for (let i = 0; i < 60 && hi - lo > 1e-9 * Math.max(1, hi); i++) {
28341
+ const mid = (lo + hi) / 2;
28342
+ if (cdf(mid) >= q) hi = mid;
28343
+ else lo = mid;
28344
+ }
28345
+ return hi;
28002
28346
  }
28003
28347
  function mainScenarioIndex(shares) {
28004
28348
  let best = 0;
@@ -28015,20 +28359,27 @@ function profilesOf(diagram, warnings = []) {
28015
28359
  return new Map(nodes.map((n) => [n.id, profileOf(n, overrides.get(n.id))]));
28016
28360
  }
28017
28361
  function analyze(diagram, options = {}) {
28018
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28362
+ const defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28019
28363
  const warnings = [];
28020
28364
  const nodes = components(diagram);
28021
28365
  const profiles = profilesOf(diagram, warnings);
28366
+ const kindOf2 = (id) => profiles.get(id)?.kind ?? "client";
28022
28367
  const traffic = resolveTraffic(diagram, warnings);
28023
28368
  const load = computeLoad(diagram, traffic);
28024
- const egress = computeEgressGb(diagram, traffic);
28369
+ const egress = computeEgressGb(diagram, traffic, kindOf2);
28370
+ const bytes = computeBytesPerSecond(diagram, traffic);
28371
+ const hops = /* @__PURE__ */ new Map();
28025
28372
  const nodeResults = nodes.map((n) => {
28026
28373
  const p = profiles.get(n.id);
28027
- const replicas = replicasOf(n);
28374
+ const replicas = Math.max(1, options.replicas?.get(n.id) ?? replicasOf(n));
28028
28375
  const { read, write } = load.get(n.id) ?? { read: 0, write: 0 };
28029
- const usage = usageOf(p, replicas, read, write);
28376
+ const usage = usageOf(p, replicas, read, write, bytes.get(n.id) ?? 0);
28377
+ const servers = serversOf(usage, replicas);
28378
+ const hop = hopModel(p.latencyMs, usage.utilization, servers);
28379
+ hops.set(n.id, hop);
28030
28380
  const egressGbPerMonth = egress.get(n.id) ?? 0;
28031
28381
  const egressUsd = egressGbPerMonth * p.egressUsdPerGb;
28382
+ const availability2 = replicatedAvailability(p.availability, replicas);
28032
28383
  return {
28033
28384
  id: n.id,
28034
28385
  kind: p.kind,
@@ -28036,8 +28387,11 @@ function analyze(diagram, options = {}) {
28036
28387
  shards: p.shards,
28037
28388
  ...usage,
28038
28389
  ...p.consistency ? { consistency: p.consistency } : {},
28039
- latencyMs: hopLatency(p.latencyMs, usage.utilization),
28040
- availability: replicatedAvailability(p.availability, replicas),
28390
+ servers,
28391
+ waitProbability: hop.waitProbability,
28392
+ latencyMs: hopMean(hop),
28393
+ availability: availability2,
28394
+ writeAvailability: p.writeScaling === "shards" ? primaryAvailability(p.availability, replicas) : availability2,
28041
28395
  costUsd: replicas * p.shards * p.costUsd + egressUsd,
28042
28396
  egressGbPerMonth,
28043
28397
  egressUsd,
@@ -28045,28 +28399,49 @@ function analyze(diagram, options = {}) {
28045
28399
  };
28046
28400
  });
28047
28401
  const nodeById = new Map(nodeResults.map((n) => [n.id, n]));
28048
- const hopMs = (id) => nodeById.get(id)?.latencyMs ?? 0;
28049
- const availabilityOf = (id) => nodeById.get(id)?.availability ?? 1;
28050
28402
  const bandwidthOf = (id) => profiles.get(id)?.bandwidthMBps ?? Infinity;
28403
+ const timeoutOf = (id) => profiles.get(id)?.timeoutMs ?? defaultTimeoutMs;
28051
28404
  for (const n of nodeResults) {
28052
28405
  const usage = usageText(n);
28053
28406
  if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
28054
28407
  else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
28055
28408
  }
28409
+ const access = accessIn(diagram);
28410
+ const pathOf2 = (hopsOfPath) => ({
28411
+ parts: hopsOfPath.map(
28412
+ (part) => part.map((h) => {
28413
+ if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
28414
+ const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
28415
+ return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
28416
+ })
28417
+ )
28418
+ });
28056
28419
  const useCases = diagram.useCases.map((u) => {
28057
28420
  const { rps, shares } = traffic.get(u.id);
28058
- const scenarios = u.scenarios.map((s, i) => {
28059
- const meanMs = criticalPath(u, s).reduce(
28060
- (sum, part) => sum + Math.max(...part.map((hop) => hop.failed ? timeoutMs : hopMs(hop.target) + transferMs(hop.step, bandwidthOf))),
28061
- 0
28062
- );
28063
- return { id: s.id, name: s.name, share: shares[i] ?? 0, meanMs, percentiles: percentilesOf(meanMs) };
28064
- });
28065
- const percentiles = Object.fromEntries(
28066
- PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], useCasePercentile(scenarios.map((s) => ({ share: s.share, value: s.percentiles[PERCENTILE_KEYS[q]] })), q)])
28421
+ const paths = u.scenarios.map((s) => pathOf2(criticalPath(u, s)));
28422
+ const scenarios = u.scenarios.map((s, i) => ({
28423
+ id: s.id,
28424
+ name: s.name,
28425
+ share: shares[i] ?? 0,
28426
+ meanMs: pathMean(paths[i]),
28427
+ percentiles: Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], pathQuantile(paths[i], q / 100)]))
28428
+ }));
28429
+ const weighted = paths.map((path, i) => ({ share: shares[i] ?? 0, path }));
28430
+ const percentiles = Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], mixtureQuantile(weighted, q / 100)]));
28431
+ const tailScenario = Object.fromEntries(
28432
+ PERCENTILES2.map((q) => {
28433
+ const t = percentiles[PERCENTILE_KEYS[q]];
28434
+ const above = weighted.map((s) => s.share > 0 ? s.share * (1 - pathCdf(s.path, t * (1 - 1e-9))) : -1);
28435
+ return [PERCENTILE_KEYS[q], Math.max(0, above.indexOf(Math.max(...above)))];
28436
+ })
28067
28437
  );
28068
28438
  if (rps > 0 && u.scenarios.length === 0) warnings.push(`"${u.name}" has traffic but no steps`);
28069
- return { id: u.id, name: u.name, rps, scenarios, percentiles, availability: useCaseAvailability(u, shares, availabilityOf) };
28439
+ const writes = mainWrites(u, shares, (step) => access(step) === "write");
28440
+ const availabilityOf = (id) => {
28441
+ const n = nodeById.get(id);
28442
+ return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
28443
+ };
28444
+ return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
28070
28445
  });
28071
28446
  return {
28072
28447
  nodes: nodeResults,
@@ -28077,8 +28452,10 @@ function analyze(diagram, options = {}) {
28077
28452
  warnings
28078
28453
  };
28079
28454
  }
28080
- function percentilesOf(meanMs) {
28081
- return Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], meanMs * PERCENTILE_FACTORS[q]]));
28455
+ function mainWrites(useCase, shares, isWrite) {
28456
+ if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
28457
+ const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
28458
+ return new Set(path.filter((h) => !h.failed && !h.async && isWrite(h.step)).map((h) => h.target));
28082
28459
  }
28083
28460
  function useCaseAvailability(useCase, shares, availabilityOf) {
28084
28461
  if (useCase.scenarios.length === 0) return 1;
@@ -28105,9 +28482,9 @@ function singlePointsOfFailure(diagram) {
28105
28482
  // ../frontend/src/sim/tests.ts
28106
28483
  var pass = (message) => ({ passed: true, message });
28107
28484
  var fail = (message, hint) => ({ passed: false, message, hint });
28108
- function runTests(diagram, analysis = analyze(diagram)) {
28485
+ function makeRun(diagram, analysis) {
28109
28486
  const access = accessIn(diagram);
28110
- const run = {
28487
+ return {
28111
28488
  diagram,
28112
28489
  analysis,
28113
28490
  nodes: new Map(analysis.nodes.map((n) => [n.id, n])),
@@ -28116,6 +28493,9 @@ function runTests(diagram, analysis = analyze(diagram)) {
28116
28493
  profiles: profilesOf(diagram),
28117
28494
  isWrite: (step) => access(step) === "write"
28118
28495
  };
28496
+ }
28497
+ function runTests(diagram, analysis = analyze(diagram)) {
28498
+ const run = makeRun(diagram, analysis);
28119
28499
  const results = (diagram.requirements ?? []).map((r, i) => {
28120
28500
  const { passed, message, hint } = requirement(run, r);
28121
28501
  return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
@@ -28207,7 +28587,7 @@ function latency(run, u, q, maxMs) {
28207
28587
  const value = `${key} of ${u.name} is ${formatMs(ms)}${result.rps > 0 ? "" : " with no traffic"}`;
28208
28588
  const message = `${value} (limit ${formatMs(maxMs)})`;
28209
28589
  if (ms < maxMs) return { ...pass(message), value };
28210
- const index = Math.max(0, result.scenarios.findIndex((s) => s.percentiles[key] === ms));
28590
+ const index = Math.min(result.tailScenario[key] ?? 0, u.scenarios.length - 1);
28211
28591
  const lead = u.scenarios.length > 1 ? `The "${u.scenarios[index].name}" path (${formatPercent(result.scenarios[index].share)} of traffic) sets ${key}. ` : "";
28212
28592
  return { ...fail(message, lead + latencyHint(run, u, u.scenarios[index])), value };
28213
28593
  }
@@ -28226,6 +28606,10 @@ function scenarioLatency(run, useCase, scenario, q, maxMs) {
28226
28606
  }
28227
28607
  function saturationHint(run, n) {
28228
28608
  const label = nodeLabel(run.diagram, n.id);
28609
+ if (bandwidthBound(n)) {
28610
+ const replicas2 = Math.ceil(n.bandwidthUtilization * n.replicas / HOT);
28611
+ return `${label} moves more payload bytes than its network carries. Send the bytes around it (clients upload to and download from object storage or a CDN directly, with presigned URLs), or add replicas: x${replicas2} keeps its bandwidth under ${formatPercent(HOT)}`;
28612
+ }
28229
28613
  if (writeBound(n)) {
28230
28614
  const perShard = n.writeCapacityRps / n.shards;
28231
28615
  const shards = Math.ceil(n.writeLoadRps / (perShard * HOT));
@@ -28237,11 +28621,12 @@ function saturationHint(run, n) {
28237
28621
  }
28238
28622
  function latencyHint(run, u, scenario) {
28239
28623
  const hops = criticalPath(u, scenario).flat();
28240
- const cost2 = (h) => h.failed ? DEFAULT_TIMEOUT_MS : run.nodes.get(h.target)?.latencyMs ?? 0;
28624
+ const timeout = (id) => run.profiles.get(id)?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28625
+ const cost2 = (h) => h.failed ? timeout(h.target) : run.nodes.get(h.target)?.latencyMs ?? 0;
28241
28626
  const slowest = hops.reduce((best, h) => !best || cost2(h) > cost2(best) ? h : best, void 0);
28242
28627
  if (!slowest) return "Shorten the synchronous path of the request.";
28243
28628
  const label = nodeLabel(run.diagram, slowest.target);
28244
- if (slowest.failed) return `It waits ${formatMs(DEFAULT_TIMEOUT_MS)} for the failed call to ${label}; give that path a smaller share of traffic or avoid the call.`;
28629
+ if (slowest.failed) return `It waits ${formatMs(timeout(slowest.target))} for the failed call to ${label}; give that path a smaller share of traffic, avoid the call, or time out sooner (capacity { ${slowest.target} timeout 200ms }).`;
28245
28630
  const sized = hops.find((h) => (h.step.sizeBytes ?? 0) > 0);
28246
28631
  const node = run.nodes.get(slowest.target);
28247
28632
  if (node && node.utilization > HOT) return `${label} runs at ${formatPercent(node.utilization)} and queues requests; add replicas to it.`;
@@ -28255,8 +28640,11 @@ function availability(run, u, minPercent) {
28255
28640
  if (result.availability * 100 >= minPercent - 1e-9) return { ...pass(message), value };
28256
28641
  const shares = run.shares.get(u.id) ?? [];
28257
28642
  const main = u.scenarios.length ? pathNodes(u, u.scenarios[mainScenarioIndex(shares)]) : [];
28258
- const weakest = main.filter((id) => fallbacksFor(u, id).length === 0).map((id) => run.nodes.get(id)).filter((n) => !!n && n.availability < 1).sort((a, b) => a.availability - b.availability)[0];
28259
- const hint = weakest ? `The weakest link is ${nodeLabel(run.diagram, weakest.id)} at ${formatAvailability(weakest.availability)}: add a replica (x${weakest.replicas + 1}) or a fallback scenario that calls it with -x and still completes` : "Add replicas to the nodes on the main path, or fallback scenarios that complete without them";
28643
+ const writes = mainWrites(u, shares, run.isWrite);
28644
+ const effective = (n) => writes.has(n.id) ? n.writeAvailability : n.availability;
28645
+ const weakest = main.filter((id) => fallbacksFor(u, id).length === 0).map((id) => run.nodes.get(id)).filter((n) => !!n && effective(n) < 1).sort((a, b) => effective(a) - effective(b))[0];
28646
+ const primaryBound = weakest && writes.has(weakest.id) && weakest.writeAvailability < weakest.availability;
28647
+ const hint = !weakest ? "Add replicas to the nodes on the main path, or fallback scenarios that complete without them" : primaryBound ? `The weakest link is writing to ${nodeLabel(run.diagram, weakest.id)}: writes need its primary, ${formatAvailability(weakest.writeAvailability)} with failover${weakest.replicas < 2 ? " (none without a second replica; add one)" : ` (a replica takes over, but about ${formatPercent(FAILOVER_SHARE)} of each outage is lost)`}. Move the writes to a partitioned store, or add a fallback scenario that calls it with -x and still completes` : `The weakest link is ${nodeLabel(run.diagram, weakest.id)} at ${formatAvailability(effective(weakest))}: add a replica (x${weakest.replicas + 1}) or a fallback scenario that calls it with -x and still completes`;
28260
28648
  return { ...fail(message, hint), value };
28261
28649
  }
28262
28650
  function writeGap(run, u, s, accept) {
@@ -28314,20 +28702,34 @@ function survive(run, target) {
28314
28702
  if (selected.length === 0) {
28315
28703
  return target === "any" ? pass("No nodes to lose") : fail(`No node matches ${what}`, "Use a node id, [Tech] or any <kind>");
28316
28704
  }
28705
+ const latencies = (run.diagram.requirements ?? []).filter((r) => r.kind === "latency" && requirement(run, r).passed);
28317
28706
  const failures = [];
28318
28707
  for (const node of selected) {
28319
28708
  const n = run.nodes.get(node.id);
28320
28709
  const label = nodeLabel(run.diagram, node.id);
28321
28710
  const replicas = replicasOf(node);
28322
28711
  if (replicas >= 2) {
28323
- const left = usageOf(run.profiles.get(node.id), replicas - 1, n.readLoadRps, n.writeLoadRps);
28712
+ const degraded = makeRun(run.diagram, analyze(run.diagram, { replicas: /* @__PURE__ */ new Map([[node.id, replicas - 1]]) }));
28713
+ const left = degraded.nodes.get(node.id);
28714
+ const lost = n.shards > 1 ? `one of the ${replicas} replicas of a ${node.id} shard` : `one of ${replicas} ${node.id} replicas`;
28324
28715
  if (left.saturated) {
28716
+ const leaves = bandwidthBound(left) ? `leaves ${usageText(left)}` : n.shards > 1 ? `leaves that shard ${formatRps(left.capacityRps / n.shards)} for ${formatRps(n.loadRps / n.shards)}` : `leaves ${formatRps(left.capacityRps)} for ${formatRps(n.loadRps)}`;
28325
28717
  failures.push(
28326
28718
  fail(
28327
- `Losing one of ${replicas} ${node.id} replicas leaves ${formatRps(left.capacityRps)} for ${formatRps(n.loadRps)} (${formatPercent(left.utilization)})`,
28719
+ `Losing ${lost} ${leaves}${bandwidthBound(left) ? "" : ` (${formatPercent(left.utilization)})`}`,
28328
28720
  writeBound(left) ? saturationHint(run, { ...n, ...left }) : `Add a replica to ${label} (x${replicas + 1}) so the rest carry the load`
28329
28721
  )
28330
28722
  );
28723
+ continue;
28724
+ }
28725
+ const broken = latencies.map((r) => requirement(degraded, r)).find((c) => !c.passed);
28726
+ if (broken) {
28727
+ failures.push(
28728
+ fail(
28729
+ `Losing ${lost} breaks a latency limit: ${broken.message}`,
28730
+ `Add a replica to ${label} (x${replicas + 1}) so the rest keep it under ${formatPercent(HOT)}, or take load off it`
28731
+ )
28732
+ );
28331
28733
  }
28332
28734
  continue;
28333
28735
  }
@@ -28589,16 +28991,16 @@ function startsAt(run, a) {
28589
28991
  const ids = idsOf(run, a.target);
28590
28992
  const what = selectorText(a.target);
28591
28993
  if (ids.size === 0) return noMatch(a.target);
28592
- const entries = u.scenarios.map((s) => s.steps[0]).filter((s) => !!s);
28593
- if (entries.length === 0) return fail(`${quote(u.name)} has no steps`, `Start "${u.name}" with a step sent by ${what}`);
28594
- const wrong = entries.find((s) => !ids.has(s.fromServiceId));
28994
+ const entries2 = u.scenarios.map((s) => s.steps[0]).filter((s) => !!s);
28995
+ if (entries2.length === 0) return fail(`${quote(u.name)} has no steps`, `Start "${u.name}" with a step sent by ${what}`);
28996
+ const wrong = entries2.find((s) => !ids.has(s.fromServiceId));
28595
28997
  if (wrong) {
28596
28998
  return fail(
28597
28999
  `${quote(u.name)} starts at ${nodeLabel(run.diagram, wrong.fromServiceId)}, not ${what}: ${stepText(wrong)}`,
28598
29000
  `Make the first step of "${u.name}" a step sent by ${what}`
28599
29001
  );
28600
29002
  }
28601
- return pass(`${quote(u.name)} starts at ${nodeLabel(run.diagram, entries[0].fromServiceId)}`);
29003
+ return pass(`${quote(u.name)} starts at ${nodeLabel(run.diagram, entries2[0].fromServiceId)}`);
28602
29004
  }
28603
29005
  function statusMatches(status, wanted) {
28604
29006
  if (status === void 0) return false;
@@ -28801,13 +29203,17 @@ function complete(analysis, pos) {
28801
29203
  if (tech) {
28802
29204
  const closed = line[pos.character] === "]";
28803
29205
  const range2 = { start: { line: pos.line, character: pos.character - tech[1].length }, end: pos };
28804
- return componentCatalog.map((c) => ({
28805
- label: c.techStack,
28806
- kind: "tech",
28807
- detail: c.category,
28808
- insertText: closed ? c.techStack : `${c.techStack}]`,
28809
- range: range2
28810
- }));
29206
+ return componentCatalog.map((c) => {
29207
+ const aliases = "aliases" in c ? c.aliases : [];
29208
+ return {
29209
+ label: c.techStack,
29210
+ kind: "tech",
29211
+ detail: aliases.length ? `${c.category} \xB7 also ${aliases.slice(0, 3).join(", ")}` : c.category,
29212
+ insertText: closed ? c.techStack : `${c.techStack}]`,
29213
+ ...aliases.length ? { filterText: [c.techStack, ...aliases].join(" ") } : {},
29214
+ range: range2
29215
+ };
29216
+ });
28811
29217
  }
28812
29218
  if (inLabel(before2)) return [];
28813
29219
  const word = before2.match(/[A-Za-z_]\w*$/)?.[0] ?? "";
@@ -28948,7 +29354,18 @@ function sections(analysis) {
28948
29354
  }));
28949
29355
  }
28950
29356
  var MISSING_CONNECTION = /^No connection between '\w+' and '\w+' in the architecture; add '(\w+) -> (\w+)'$/;
28951
- function quickFix(analysis, message) {
29357
+ function quickFix(analysis, message, range) {
29358
+ const tech = message.match(DID_YOU_MEAN);
29359
+ if (tech && range) {
29360
+ const line = analysis.lines[range.start.line] ?? "";
29361
+ const open = line.indexOf("[", range.start.character);
29362
+ const close = open < 0 ? -1 : line.indexOf("]", open);
29363
+ if (open < 0 || close < 0) return null;
29364
+ return {
29365
+ title: `Change to '${tech[2]}'`,
29366
+ edit: { range: { start: { line: range.start.line, character: open + 1 }, end: { line: range.start.line, character: close } }, newText: tech[2] }
29367
+ };
29368
+ }
28952
29369
  const m = message.match(MISSING_CONNECTION);
28953
29370
  if (!m) return null;
28954
29371
  const [, from, to] = m;
@@ -28971,7 +29388,7 @@ function positionAt(text, offset) {
28971
29388
 
28972
29389
  // src/server.ts
28973
29390
  if (process.argv.includes("--version")) {
28974
- console.log(true ? "0.5.0" : "dev");
29391
+ console.log(true ? "0.7.0" : "dev");
28975
29392
  process.exit(0);
28976
29393
  }
28977
29394
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -29012,7 +29429,7 @@ connection.onInitialize(() => ({
29012
29429
  documentFormattingProvider: true,
29013
29430
  codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] }
29014
29431
  },
29015
- serverInfo: { name: "proschi-language-server", version: true ? "0.5.0" : "dev" }
29432
+ serverInfo: { name: "proschi-language-server", version: true ? "0.7.0" : "dev" }
29016
29433
  }));
29017
29434
  function validate(document) {
29018
29435
  const analysis = analysisOf(document);
@@ -29086,6 +29503,7 @@ connection.onCompletion(
29086
29503
  kind: COMPLETION_KIND[item.kind],
29087
29504
  detail: item.detail,
29088
29505
  textEdit: { range: item.range, newText: item.snippet ?? item.insertText ?? item.label },
29506
+ ...item.filterText ? { filterText: item.filterText } : {},
29089
29507
  insertTextFormat: item.snippet ? import_node.InsertTextFormat.Snippet : import_node.InsertTextFormat.PlainText
29090
29508
  }))
29091
29509
  )
@@ -29144,7 +29562,7 @@ connection.onCodeAction((params) => {
29144
29562
  const actions = [];
29145
29563
  for (const diagnostic of params.context.diagnostics) {
29146
29564
  if (diagnostic.source !== "proschi") continue;
29147
- const fix = typeof diagnostic.message === "string" && quickFix(analysis, diagnostic.message);
29565
+ const fix = typeof diagnostic.message === "string" && quickFix(analysis, diagnostic.message, diagnostic.range);
29148
29566
  if (!fix) continue;
29149
29567
  actions.push({
29150
29568
  title: fix.title,