proschi 0.6.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_]/;
@@ -25638,26 +25798,7 @@ function endpointGroupKey(endpoint, all) {
25638
25798
  var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
25639
25799
  var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
25640
25800
  var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
25641
- var BY_TECH = {
25642
- "AWS CloudFront": "cdn",
25643
- "Azure CDN": "cdn",
25644
- "GCP Cloud CDN": "cdn",
25645
- "Azure Front Door": "cdn",
25646
- "AWS Load Balancer": "loadbalancer",
25647
- "GCP Load Balancing": "loadbalancer",
25648
- "AWS API Gateway": "gateway",
25649
- "Azure API Management": "gateway",
25650
- "AWS Route53": "dns",
25651
- "Azure DNS": "dns",
25652
- "GCP Cloud DNS": "dns",
25653
- Redis: "cache",
25654
- Elasticsearch: "search",
25655
- "GCP BigQuery": "analytics",
25656
- InfluxDB: "analytics",
25657
- TimescaleDB: "analytics",
25658
- // A note shape annotates the diagram; it is not a client.
25659
- Note: "other"
25660
- };
25801
+ var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
25661
25802
  var BY_TYPE = {
25662
25803
  shape: "client",
25663
25804
  cdn: "edge",
@@ -25673,7 +25814,44 @@ var BY_TYPE = {
25673
25814
  };
25674
25815
  function kindOf(node) {
25675
25816
  if (node.kind !== "component") return "other";
25676
- 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];
25677
25855
  }
25678
25856
  function isKind(word) {
25679
25857
  return KINDS.includes(word);
@@ -25795,12 +25973,32 @@ var UNIT_EXAMPLES = {
25795
25973
  MBps: "a bandwidth, e.g. 500 MB/s or 1 GB/s",
25796
25974
  "usd/GB": "a price per gigabyte, e.g. 0.05 usd/GB"
25797
25975
  };
25798
- 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";
25799
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';
25800
25978
  var ASSERTION_VERBS = "calls, every scenario calls, never calls, never waits for, writes, responds, starts at, has scenario or handles failure of";
25801
- var techByName = new Map(
25802
- componentCatalog.map((c) => [c.techStack.toLowerCase(), { type: c.type, techStack: c.techStack }])
25803
- );
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 '([^']+)'\?/;
25804
26002
  function parse(source2, options = {}) {
25805
26003
  return new Parser(source2, options).run();
25806
26004
  }
@@ -26052,8 +26250,8 @@ var Parser = class {
26052
26250
  if (tokens[i]?.kind === "string") name = tokens[i++].value;
26053
26251
  if (tokens[i]?.kind === "tech") {
26054
26252
  const tech = tokens[i++];
26055
- const resolved = this.resolveTech(tech, line);
26056
- if (resolved.type === "group") techStack = resolved.techStack;
26253
+ const resolved = findTech(tech.value);
26254
+ if (resolved?.type === "group") techStack = resolved.techStack;
26057
26255
  else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
26058
26256
  }
26059
26257
  let position;
@@ -26186,6 +26384,7 @@ var Parser = class {
26186
26384
  const resolved = this.resolveTech(t, line);
26187
26385
  node.type = resolved.type;
26188
26386
  node.techStack = resolved.techStack;
26387
+ if (resolved.inferredKind) node.inferredKind = resolved.inferredKind;
26189
26388
  node.kind = resolved.type === "text" ? "text" : resolved.type === "group" ? "group" : "component";
26190
26389
  } else if (t.kind === "team") {
26191
26390
  node.ownerTeam = t.value;
@@ -26540,12 +26739,13 @@ var Parser = class {
26540
26739
  if (rps === void 0) return;
26541
26740
  override.rps = rps;
26542
26741
  i++;
26543
- } 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")) {
26544
26743
  part = t.value;
26545
- const unit = part === "latency" ? "ms" : part === "availability" ? "%" : "usd/month";
26744
+ const unit = part === "latency" || part === "timeout" ? "ms" : part === "availability" ? "%" : "usd/month";
26546
26745
  const value = this.quantityOf(tokens[i + 1], unit, t, line);
26547
26746
  if (value === void 0) return;
26548
26747
  if (part === "latency") override.latencyMs = value;
26748
+ else if (part === "timeout") override.timeoutMs = value;
26549
26749
  else if (part === "availability") override.availability = value;
26550
26750
  else override.costUsd = value;
26551
26751
  i += 2;
@@ -26953,7 +27153,7 @@ var Parser = class {
26953
27153
  parseSelectorAtom(tokens, i, line) {
26954
27154
  const t = tokens[i];
26955
27155
  if (t?.kind === "tech") {
26956
- const tech = techByName.get(t.value.toLowerCase())?.techStack ?? t.value;
27156
+ const tech = findTech(t.value)?.techStack ?? t.value;
26957
27157
  return { selector: { tech }, next: i + 1, token: t, loc: this.locOf(t, line) };
26958
27158
  }
26959
27159
  if (isWord(t, "any")) {
@@ -26978,7 +27178,10 @@ var Parser = class {
26978
27178
  /** Warns about a selector naming a tech stack that does not exist, and (once every file is read) a node that does not. */
26979
27179
  checkSelectorLater({ atoms }) {
26980
27180
  for (const { selector, token, loc } of atoms) {
26981
- 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
+ }
26982
27185
  if ("node" in selector) {
26983
27186
  this.deferred.push(() => {
26984
27187
  if (!this.nodes.has(selector.node)) this.warning(`Unknown node '${selector.node}'`, loc);
@@ -27138,11 +27341,27 @@ var Parser = class {
27138
27341
  where(first, later) {
27139
27342
  return first.file === later.file ? `on line ${first.line}` : `in ${fileName(first.file ?? this.options.path ?? "the root document")} on line ${first.line}`;
27140
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
+ */
27141
27352
  resolveTech(token, line) {
27142
- const resolved = techByName.get(token.value.toLowerCase());
27143
- if (resolved) return resolved;
27144
- this.warning(`Unknown tech stack '${token.value}'; drawing a ${DEFAULT_TECH}`, this.locOf(token, line));
27145
- 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 };
27146
27365
  }
27147
27366
  useCaseId(name) {
27148
27367
  const base = slug(name) || "usecase";
@@ -27352,14 +27571,14 @@ ${text}
27352
27571
 
27353
27572
  // ../frontend/src/dsl/format.ts
27354
27573
  function format(source2) {
27355
- 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));
27356
27575
  const out = [];
27357
27576
  let run = [];
27358
27577
  const flush = () => {
27359
27578
  if (run.length) out.push(...renderRun(run));
27360
27579
  run = [];
27361
27580
  };
27362
- tidyBlankLines(entries).forEach((entry) => {
27581
+ tidyBlankLines(entries2).forEach((entry) => {
27363
27582
  const last = run[run.length - 1];
27364
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))
27365
27584
  flush();
@@ -27375,19 +27594,19 @@ function format(source2) {
27375
27594
  var INDENT = " ";
27376
27595
  var KEYWORDS = /* @__PURE__ */ new Set(["title", "import", "group", "usecase", "par", "alt"]);
27377
27596
  function classify(lines) {
27378
- const entries = [];
27597
+ const entries2 = [];
27379
27598
  let depth = 0;
27380
27599
  let section;
27381
27600
  for (let i = 0; i < lines.length; i++) {
27382
27601
  const raw = lines[i];
27383
27602
  const original = raw.trim();
27384
27603
  if (!original) {
27385
- entries.push({ kind: "blank", depth, text: "" });
27604
+ entries2.push({ kind: "blank", depth, text: "" });
27386
27605
  continue;
27387
27606
  }
27388
27607
  const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
27389
27608
  if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
27390
- entries.push({ kind: "line", depth, text: original });
27609
+ entries2.push({ kind: "line", depth, text: original });
27391
27610
  continue;
27392
27611
  }
27393
27612
  const comment = commentOf(raw, tokens);
@@ -27396,7 +27615,7 @@ function classify(lines) {
27396
27615
  const inSection = section !== void 0 && depth === 1 && first.kind !== "rbrace";
27397
27616
  const cells = inSection ? sectionCells(section, raw, tokens) : void 0;
27398
27617
  if (cells) {
27399
- entries.push({ ...base, kind: "cells", section, cells });
27618
+ entries2.push({ ...base, kind: "cells", section, cells });
27400
27619
  continue;
27401
27620
  }
27402
27621
  if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
@@ -27411,7 +27630,7 @@ function classify(lines) {
27411
27630
  }
27412
27631
  while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
27413
27632
  }
27414
- entries.push(entry);
27633
+ entries2.push(entry);
27415
27634
  continue;
27416
27635
  }
27417
27636
  const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
@@ -27419,19 +27638,19 @@ function classify(lines) {
27419
27638
  let k = 1;
27420
27639
  const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
27421
27640
  const cells2 = [first.value, take("string"), take("tech"), take("team"), join2(raw, tokens.slice(k))];
27422
- entries.push({ ...base, kind: "node", cells: cells2 });
27641
+ entries2.push({ ...base, kind: "node", cells: cells2 });
27423
27642
  continue;
27424
27643
  }
27425
27644
  const closes = first.kind === "rbrace";
27426
27645
  const opens = tokens[tokens.length - 1].kind === "lbrace";
27427
27646
  const code = join2(raw, tokens);
27428
27647
  const text = withComment(code, comment);
27429
- 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 });
27430
27649
  if (depth === 0) section = sectionOpenedBy(tokens);
27431
27650
  for (const t of tokens) depth = Math.max(0, depth + (t.kind === "lbrace" ? 1 : t.kind === "rbrace" ? -1 : 0));
27432
27651
  if (depth === 0) section = void 0;
27433
27652
  }
27434
- return entries;
27653
+ return entries2;
27435
27654
  }
27436
27655
  function sectionOpenedBy(tokens) {
27437
27656
  const [first, second] = tokens;
@@ -27465,9 +27684,9 @@ function sectionCells(section, raw, tokens) {
27465
27684
  return void 0;
27466
27685
  }
27467
27686
  }
27468
- function tidyBlankLines(entries) {
27687
+ function tidyBlankLines(entries2) {
27469
27688
  const kept = [];
27470
- for (const entry of entries) {
27689
+ for (const entry of entries2) {
27471
27690
  if (entry.kind !== "blank") {
27472
27691
  if (entry.kind === "line" && entry.closes) while (kept[kept.length - 1]?.kind === "blank") kept.pop();
27473
27692
  kept.push(entry);
@@ -27699,6 +27918,8 @@ function accessIn(diagram) {
27699
27918
 
27700
27919
  // ../frontend/src/sim/profiles.ts
27701
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;
27702
27923
  var RELATIONAL = {
27703
27924
  rps: 2e4,
27704
27925
  readRps: 2e4,
@@ -27712,8 +27933,9 @@ var RELATIONAL = {
27712
27933
  };
27713
27934
  var KIND_PROFILES = {
27714
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.
27715
27937
  edge: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
27716
- 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 },
27717
27939
  loadbalancer: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
27718
27940
  gateway: { rps: 1e4, latencyMs: 10, availability: 0.9995, costUsd: 100, durable: false },
27719
27941
  // Name resolution happens before the request and is cached: off the request path.
@@ -27725,23 +27947,16 @@ var KIND_PROFILES = {
27725
27947
  search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
27726
27948
  analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
27727
27949
  queue: { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" },
27728
- 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" },
27729
27951
  external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
27730
27952
  other: NEUTRAL
27731
27953
  };
27732
27954
  var NOSQL = { rps: 2e4, latencyMs: 5, availability: 0.9999, costUsd: 500, durable: true, consistency: "eventual" };
27733
27955
  var NOSQL_STRONG = { ...NOSQL, consistency: "strong" };
27734
- var TECH_PROFILES = {
27735
- DynamoDB: NOSQL,
27736
- "AWS DynamoDB": NOSQL,
27737
- Cassandra: NOSQL,
27738
- CouchDB: NOSQL,
27739
- "Azure Cosmos DB": NOSQL,
27740
- MongoDB: NOSQL_STRONG,
27741
- "GCP Bigtable": NOSQL_STRONG,
27742
- "GCP Firestore": NOSQL_STRONG,
27743
- "GCP Spanner": NOSQL_STRONG
27744
- };
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
+ );
27745
27960
  var BANDWIDTH = {
27746
27961
  client: 10,
27747
27962
  edge: 1e3,
@@ -27752,9 +27967,10 @@ var BANDWIDTH = {
27752
27967
  service: 200
27753
27968
  };
27754
27969
  var DEFAULT_BANDWIDTH_MBPS = 100;
27970
+ var CHARGES_EGRESS = (kind) => kind !== "client" && kind !== "external" && kind !== "dns" && kind !== "other";
27755
27971
  function profileOf(node, override) {
27756
27972
  const kind = kindOf(node);
27757
- 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];
27758
27974
  const baseRead = base.readRps ?? base.rps;
27759
27975
  const baseWrite = base.writeRps ?? base.rps;
27760
27976
  const readRps = override?.readRps ?? override?.rps ?? baseRead;
@@ -27773,7 +27989,8 @@ function profileOf(node, override) {
27773
27989
  durable: override?.durable ?? base.durable,
27774
27990
  ...consistency && isDataStore(kind) ? { consistency } : {},
27775
27991
  bandwidthMBps: override?.bandwidthMBps ?? base.bandwidthMBps ?? BANDWIDTH[kind] ?? DEFAULT_BANDWIDTH_MBPS,
27776
- 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 } : {}
27777
27994
  };
27778
27995
  }
27779
27996
  function capacityOf(p, replicas) {
@@ -27926,10 +28143,11 @@ function formatUsd(usd) {
27926
28143
  var DEFAULT_TIMEOUT_MS = 1e3;
27927
28144
  var HOT = 0.7;
27928
28145
  var QUEUE_CAP = 0.95;
28146
+ var FAILOVER_SHARE = 0.1;
28147
+ var SERVICE_SPREAD = 0.5;
27929
28148
  var SECONDS_PER_MONTH = 2592e3;
27930
28149
  var BYTES_PER_MB = 1e6;
27931
28150
  var BYTES_PER_GB = 1e9;
27932
- var PERCENTILE_FACTORS = { 50: 1, 90: 1.6, 95: 2, 99: 3, 99.9: 5 };
27933
28151
  var PERCENTILE_KEYS = { 50: "p50", 90: "p90", 95: "p95", 99: "p99", 99.9: "p999" };
27934
28152
  var PERCENTILES2 = Object.keys(PERCENTILE_KEYS).map(Number);
27935
28153
  function components(diagram) {
@@ -27970,7 +28188,9 @@ function forEachStepRate(diagram, traffic, visit) {
27970
28188
  const { rps, shares } = traffic.get(u.id) ?? { rps: 0, shares: [] };
27971
28189
  u.scenarios.forEach((s, i) => {
27972
28190
  const rate = rps * (shares[i] ?? 0);
27973
- 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
+ }
27974
28194
  });
27975
28195
  }
27976
28196
  }
@@ -27984,35 +28204,76 @@ function computeLoad(diagram, traffic) {
27984
28204
  });
27985
28205
  return load;
27986
28206
  }
27987
- var egressSource = (step, access) => access === "read" ? step.toServiceId : step.fromServiceId;
27988
- 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)) {
27989
28210
  const gb = /* @__PURE__ */ new Map();
27990
28211
  const access = accessIn(diagram);
27991
28212
  forEachStepRate(diagram, traffic, (step, rate) => {
27992
28213
  if (!step.sizeBytes || step.sizeBytes <= 0) return;
27993
- const source2 = egressSource(step, access(step));
27994
- 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);
27995
28217
  });
27996
28218
  return gb;
27997
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
+ }
27998
28234
  function transferMs(step, bandwidthOf) {
27999
28235
  if (!step.sizeBytes || step.sizeBytes <= 0) return 0;
28000
28236
  const mbps = Math.min(bandwidthOf(step.fromServiceId), bandwidthOf(step.toServiceId));
28001
- 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;
28002
28238
  }
28003
28239
  var utilizationOf = (load, capacity) => Number.isFinite(capacity) && capacity > 0 ? load / capacity : 0;
28004
- 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);
28005
28259
  var replicatedAvailability = (a, replicas) => 1 - (1 - a) ** replicas;
28006
- 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) {
28007
28262
  const { readRps: readCapacityRps, writeRps: writeCapacityRps } = capacityOf(p, replicas);
28008
28263
  const readUtilization = utilizationOf(readLoadRps, readCapacityRps);
28009
28264
  const writeUtilization = utilizationOf(writeLoadRps, writeCapacityRps);
28010
- const utilization = combinedUtilization(p, readUtilization, writeUtilization);
28265
+ const requestUtilization = combinedUtilization(p, readUtilization, writeUtilization);
28011
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);
28012
28272
  return {
28013
28273
  loadRps,
28014
- capacityRps: readCapacityRps === writeCapacityRps || utilization === 0 ? readCapacityRps : loadRps / utilization,
28274
+ capacityRps: readCapacityRps === writeCapacityRps || requestUtilization === 0 ? readCapacityRps : loadRps / requestUtilization,
28015
28275
  utilization,
28276
+ requestUtilization,
28016
28277
  saturated: utilization >= 1,
28017
28278
  readLoadRps,
28018
28279
  writeLoadRps,
@@ -28020,27 +28281,68 @@ function usageOf(p, replicas, readLoadRps, writeLoadRps) {
28020
28281
  writeCapacityRps,
28021
28282
  readUtilization,
28022
28283
  writeUtilization,
28284
+ bandwidthLoadMBps,
28285
+ bandwidthCapacityMBps,
28286
+ bandwidthUtilization,
28023
28287
  writeScaling: p.writeScaling
28024
28288
  };
28025
28289
  }
28290
+ var bandwidthBound = (n) => n.bandwidthUtilization > n.requestUtilization;
28291
+ var serversOf = (n, replicas) => writeBound(n) ? 1 : replicas;
28026
28292
  function usageText(n) {
28027
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
+ }
28028
28298
  if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
28029
28299
  return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
28030
28300
  }
28031
- return part(n.loadRps, n.capacityRps, n.utilization);
28301
+ return part(n.loadRps, n.capacityRps, n.requestUtilization);
28032
28302
  }
28033
28303
  var writeBound = (n) => n.writeScaling === "shards" && n.writeUtilization >= n.readUtilization && n.writeLoadRps > 0;
28034
- function useCasePercentile(scenarios, q) {
28035
- const tail = 1 - q / 100 - 1e-9;
28036
- const heavy = scenarios.filter((s) => s.share >= tail && s.share > 0);
28037
- if (heavy.length) return Math.max(...heavy.map((s) => s.value));
28038
- let covered = 0;
28039
- for (const s of [...scenarios].sort((a, b) => b.value - a.value)) {
28040
- covered += s.share;
28041
- if (covered >= tail) return s.value;
28042
- }
28043
- 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;
28044
28346
  }
28045
28347
  function mainScenarioIndex(shares) {
28046
28348
  let best = 0;
@@ -28057,20 +28359,27 @@ function profilesOf(diagram, warnings = []) {
28057
28359
  return new Map(nodes.map((n) => [n.id, profileOf(n, overrides.get(n.id))]));
28058
28360
  }
28059
28361
  function analyze(diagram, options = {}) {
28060
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28362
+ const defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28061
28363
  const warnings = [];
28062
28364
  const nodes = components(diagram);
28063
28365
  const profiles = profilesOf(diagram, warnings);
28366
+ const kindOf2 = (id) => profiles.get(id)?.kind ?? "client";
28064
28367
  const traffic = resolveTraffic(diagram, warnings);
28065
28368
  const load = computeLoad(diagram, traffic);
28066
- const egress = computeEgressGb(diagram, traffic);
28369
+ const egress = computeEgressGb(diagram, traffic, kindOf2);
28370
+ const bytes = computeBytesPerSecond(diagram, traffic);
28371
+ const hops = /* @__PURE__ */ new Map();
28067
28372
  const nodeResults = nodes.map((n) => {
28068
28373
  const p = profiles.get(n.id);
28069
- const replicas = replicasOf(n);
28374
+ const replicas = Math.max(1, options.replicas?.get(n.id) ?? replicasOf(n));
28070
28375
  const { read, write } = load.get(n.id) ?? { read: 0, write: 0 };
28071
- 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);
28072
28380
  const egressGbPerMonth = egress.get(n.id) ?? 0;
28073
28381
  const egressUsd = egressGbPerMonth * p.egressUsdPerGb;
28382
+ const availability2 = replicatedAvailability(p.availability, replicas);
28074
28383
  return {
28075
28384
  id: n.id,
28076
28385
  kind: p.kind,
@@ -28078,8 +28387,11 @@ function analyze(diagram, options = {}) {
28078
28387
  shards: p.shards,
28079
28388
  ...usage,
28080
28389
  ...p.consistency ? { consistency: p.consistency } : {},
28081
- latencyMs: hopLatency(p.latencyMs, usage.utilization),
28082
- 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,
28083
28395
  costUsd: replicas * p.shards * p.costUsd + egressUsd,
28084
28396
  egressGbPerMonth,
28085
28397
  egressUsd,
@@ -28087,28 +28399,49 @@ function analyze(diagram, options = {}) {
28087
28399
  };
28088
28400
  });
28089
28401
  const nodeById = new Map(nodeResults.map((n) => [n.id, n]));
28090
- const hopMs = (id) => nodeById.get(id)?.latencyMs ?? 0;
28091
- const availabilityOf = (id) => nodeById.get(id)?.availability ?? 1;
28092
28402
  const bandwidthOf = (id) => profiles.get(id)?.bandwidthMBps ?? Infinity;
28403
+ const timeoutOf = (id) => profiles.get(id)?.timeoutMs ?? defaultTimeoutMs;
28093
28404
  for (const n of nodeResults) {
28094
28405
  const usage = usageText(n);
28095
28406
  if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
28096
28407
  else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
28097
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
+ });
28098
28419
  const useCases = diagram.useCases.map((u) => {
28099
28420
  const { rps, shares } = traffic.get(u.id);
28100
- const scenarios = u.scenarios.map((s, i) => {
28101
- const meanMs = criticalPath(u, s).reduce(
28102
- (sum, part) => sum + Math.max(...part.map((hop) => hop.failed ? timeoutMs : hopMs(hop.target) + transferMs(hop.step, bandwidthOf))),
28103
- 0
28104
- );
28105
- return { id: s.id, name: s.name, share: shares[i] ?? 0, meanMs, percentiles: percentilesOf(meanMs) };
28106
- });
28107
- const percentiles = Object.fromEntries(
28108
- 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
+ })
28109
28437
  );
28110
28438
  if (rps > 0 && u.scenarios.length === 0) warnings.push(`"${u.name}" has traffic but no steps`);
28111
- 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) };
28112
28445
  });
28113
28446
  return {
28114
28447
  nodes: nodeResults,
@@ -28119,8 +28452,10 @@ function analyze(diagram, options = {}) {
28119
28452
  warnings
28120
28453
  };
28121
28454
  }
28122
- function percentilesOf(meanMs) {
28123
- 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));
28124
28459
  }
28125
28460
  function useCaseAvailability(useCase, shares, availabilityOf) {
28126
28461
  if (useCase.scenarios.length === 0) return 1;
@@ -28147,9 +28482,9 @@ function singlePointsOfFailure(diagram) {
28147
28482
  // ../frontend/src/sim/tests.ts
28148
28483
  var pass = (message) => ({ passed: true, message });
28149
28484
  var fail = (message, hint) => ({ passed: false, message, hint });
28150
- function runTests(diagram, analysis = analyze(diagram)) {
28485
+ function makeRun(diagram, analysis) {
28151
28486
  const access = accessIn(diagram);
28152
- const run = {
28487
+ return {
28153
28488
  diagram,
28154
28489
  analysis,
28155
28490
  nodes: new Map(analysis.nodes.map((n) => [n.id, n])),
@@ -28158,6 +28493,9 @@ function runTests(diagram, analysis = analyze(diagram)) {
28158
28493
  profiles: profilesOf(diagram),
28159
28494
  isWrite: (step) => access(step) === "write"
28160
28495
  };
28496
+ }
28497
+ function runTests(diagram, analysis = analyze(diagram)) {
28498
+ const run = makeRun(diagram, analysis);
28161
28499
  const results = (diagram.requirements ?? []).map((r, i) => {
28162
28500
  const { passed, message, hint } = requirement(run, r);
28163
28501
  return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
@@ -28249,7 +28587,7 @@ function latency(run, u, q, maxMs) {
28249
28587
  const value = `${key} of ${u.name} is ${formatMs(ms)}${result.rps > 0 ? "" : " with no traffic"}`;
28250
28588
  const message = `${value} (limit ${formatMs(maxMs)})`;
28251
28589
  if (ms < maxMs) return { ...pass(message), value };
28252
- 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);
28253
28591
  const lead = u.scenarios.length > 1 ? `The "${u.scenarios[index].name}" path (${formatPercent(result.scenarios[index].share)} of traffic) sets ${key}. ` : "";
28254
28592
  return { ...fail(message, lead + latencyHint(run, u, u.scenarios[index])), value };
28255
28593
  }
@@ -28268,6 +28606,10 @@ function scenarioLatency(run, useCase, scenario, q, maxMs) {
28268
28606
  }
28269
28607
  function saturationHint(run, n) {
28270
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
+ }
28271
28613
  if (writeBound(n)) {
28272
28614
  const perShard = n.writeCapacityRps / n.shards;
28273
28615
  const shards = Math.ceil(n.writeLoadRps / (perShard * HOT));
@@ -28279,11 +28621,12 @@ function saturationHint(run, n) {
28279
28621
  }
28280
28622
  function latencyHint(run, u, scenario) {
28281
28623
  const hops = criticalPath(u, scenario).flat();
28282
- 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;
28283
28626
  const slowest = hops.reduce((best, h) => !best || cost2(h) > cost2(best) ? h : best, void 0);
28284
28627
  if (!slowest) return "Shorten the synchronous path of the request.";
28285
28628
  const label = nodeLabel(run.diagram, slowest.target);
28286
- 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 }).`;
28287
28630
  const sized = hops.find((h) => (h.step.sizeBytes ?? 0) > 0);
28288
28631
  const node = run.nodes.get(slowest.target);
28289
28632
  if (node && node.utilization > HOT) return `${label} runs at ${formatPercent(node.utilization)} and queues requests; add replicas to it.`;
@@ -28297,8 +28640,11 @@ function availability(run, u, minPercent) {
28297
28640
  if (result.availability * 100 >= minPercent - 1e-9) return { ...pass(message), value };
28298
28641
  const shares = run.shares.get(u.id) ?? [];
28299
28642
  const main = u.scenarios.length ? pathNodes(u, u.scenarios[mainScenarioIndex(shares)]) : [];
28300
- 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];
28301
- 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`;
28302
28648
  return { ...fail(message, hint), value };
28303
28649
  }
28304
28650
  function writeGap(run, u, s, accept) {
@@ -28356,20 +28702,34 @@ function survive(run, target) {
28356
28702
  if (selected.length === 0) {
28357
28703
  return target === "any" ? pass("No nodes to lose") : fail(`No node matches ${what}`, "Use a node id, [Tech] or any <kind>");
28358
28704
  }
28705
+ const latencies = (run.diagram.requirements ?? []).filter((r) => r.kind === "latency" && requirement(run, r).passed);
28359
28706
  const failures = [];
28360
28707
  for (const node of selected) {
28361
28708
  const n = run.nodes.get(node.id);
28362
28709
  const label = nodeLabel(run.diagram, node.id);
28363
28710
  const replicas = replicasOf(node);
28364
28711
  if (replicas >= 2) {
28365
- 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`;
28366
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)}`;
28367
28717
  failures.push(
28368
28718
  fail(
28369
- `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)})`}`,
28370
28720
  writeBound(left) ? saturationHint(run, { ...n, ...left }) : `Add a replica to ${label} (x${replicas + 1}) so the rest carry the load`
28371
28721
  )
28372
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
+ );
28373
28733
  }
28374
28734
  continue;
28375
28735
  }
@@ -28631,16 +28991,16 @@ function startsAt(run, a) {
28631
28991
  const ids = idsOf(run, a.target);
28632
28992
  const what = selectorText(a.target);
28633
28993
  if (ids.size === 0) return noMatch(a.target);
28634
- const entries = u.scenarios.map((s) => s.steps[0]).filter((s) => !!s);
28635
- if (entries.length === 0) return fail(`${quote(u.name)} has no steps`, `Start "${u.name}" with a step sent by ${what}`);
28636
- 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));
28637
28997
  if (wrong) {
28638
28998
  return fail(
28639
28999
  `${quote(u.name)} starts at ${nodeLabel(run.diagram, wrong.fromServiceId)}, not ${what}: ${stepText(wrong)}`,
28640
29000
  `Make the first step of "${u.name}" a step sent by ${what}`
28641
29001
  );
28642
29002
  }
28643
- 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)}`);
28644
29004
  }
28645
29005
  function statusMatches(status, wanted) {
28646
29006
  if (status === void 0) return false;
@@ -28843,13 +29203,17 @@ function complete(analysis, pos) {
28843
29203
  if (tech) {
28844
29204
  const closed = line[pos.character] === "]";
28845
29205
  const range2 = { start: { line: pos.line, character: pos.character - tech[1].length }, end: pos };
28846
- return componentCatalog.map((c) => ({
28847
- label: c.techStack,
28848
- kind: "tech",
28849
- detail: c.category,
28850
- insertText: closed ? c.techStack : `${c.techStack}]`,
28851
- range: range2
28852
- }));
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
+ });
28853
29217
  }
28854
29218
  if (inLabel(before2)) return [];
28855
29219
  const word = before2.match(/[A-Za-z_]\w*$/)?.[0] ?? "";
@@ -28990,7 +29354,18 @@ function sections(analysis) {
28990
29354
  }));
28991
29355
  }
28992
29356
  var MISSING_CONNECTION = /^No connection between '\w+' and '\w+' in the architecture; add '(\w+) -> (\w+)'$/;
28993
- 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
+ }
28994
29369
  const m = message.match(MISSING_CONNECTION);
28995
29370
  if (!m) return null;
28996
29371
  const [, from, to] = m;
@@ -29013,7 +29388,7 @@ function positionAt(text, offset) {
29013
29388
 
29014
29389
  // src/server.ts
29015
29390
  if (process.argv.includes("--version")) {
29016
- console.log(true ? "0.6.0" : "dev");
29391
+ console.log(true ? "0.7.0" : "dev");
29017
29392
  process.exit(0);
29018
29393
  }
29019
29394
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -29054,7 +29429,7 @@ connection.onInitialize(() => ({
29054
29429
  documentFormattingProvider: true,
29055
29430
  codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] }
29056
29431
  },
29057
- serverInfo: { name: "proschi-language-server", version: true ? "0.6.0" : "dev" }
29432
+ serverInfo: { name: "proschi-language-server", version: true ? "0.7.0" : "dev" }
29058
29433
  }));
29059
29434
  function validate(document) {
29060
29435
  const analysis = analysisOf(document);
@@ -29128,6 +29503,7 @@ connection.onCompletion(
29128
29503
  kind: COMPLETION_KIND[item.kind],
29129
29504
  detail: item.detail,
29130
29505
  textEdit: { range: item.range, newText: item.snippet ?? item.insertText ?? item.label },
29506
+ ...item.filterText ? { filterText: item.filterText } : {},
29131
29507
  insertTextFormat: item.snippet ? import_node.InsertTextFormat.Snippet : import_node.InsertTextFormat.PlainText
29132
29508
  }))
29133
29509
  )
@@ -29186,7 +29562,7 @@ connection.onCodeAction((params) => {
29186
29562
  const actions = [];
29187
29563
  for (const diagnostic of params.context.diagnostics) {
29188
29564
  if (diagnostic.source !== "proschi") continue;
29189
- const fix = typeof diagnostic.message === "string" && quickFix(analysis, diagnostic.message);
29565
+ const fix = typeof diagnostic.message === "string" && quickFix(analysis, diagnostic.message, diagnostic.range);
29190
29566
  if (!fix) continue;
29191
29567
  actions.push({
29192
29568
  title: fix.title,