proschi 0.6.0 → 0.8.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/README.md +1 -1
- package/dist/cli.cjs +640 -218
- package/dist/render.cjs +159 -159
- package/dist/server.cjs +595 -219
- package/grammar/proschi.tmLanguage.json +1 -1
- package/package.json +2 -2
- package/schema/proschi-diagram.schema.json +138 -6
package/dist/cli.cjs
CHANGED
|
@@ -2237,8 +2237,8 @@ var require_resolve = __commonJS({
|
|
|
2237
2237
|
}
|
|
2238
2238
|
return count;
|
|
2239
2239
|
}
|
|
2240
|
-
function getFullPath(resolver, id = "",
|
|
2241
|
-
if (
|
|
2240
|
+
function getFullPath(resolver, id = "", normalize2) {
|
|
2241
|
+
if (normalize2 !== false)
|
|
2242
2242
|
id = normalizeId(id);
|
|
2243
2243
|
const p = resolver.parse(id);
|
|
2244
2244
|
return _getFullPath(resolver, p);
|
|
@@ -3833,7 +3833,7 @@ var require_fast_uri = __commonJS({
|
|
|
3833
3833
|
}
|
|
3834
3834
|
return decodedScheme;
|
|
3835
3835
|
}
|
|
3836
|
-
function
|
|
3836
|
+
function normalize2(uri, options) {
|
|
3837
3837
|
if (typeof uri === "string") {
|
|
3838
3838
|
uri = /** @type {T} */
|
|
3839
3839
|
normalizeString(uri, options);
|
|
@@ -4211,7 +4211,7 @@ var require_fast_uri = __commonJS({
|
|
|
4211
4211
|
}
|
|
4212
4212
|
var fastUri = {
|
|
4213
4213
|
SCHEMES,
|
|
4214
|
-
normalize,
|
|
4214
|
+
normalize: normalize2,
|
|
4215
4215
|
resolve: resolve4,
|
|
4216
4216
|
resolveComponent,
|
|
4217
4217
|
equal,
|
|
@@ -15201,125 +15201,237 @@ var componentCatalog = [
|
|
|
15201
15201
|
{ type: "shape", techStack: "Diamond", category: "Generic Shapes", searchTerms: "diamond rhombus shape decision" },
|
|
15202
15202
|
{ type: "shape", techStack: "Cylinder", category: "Generic Shapes", searchTerms: "cylinder shape storage" },
|
|
15203
15203
|
{ type: "shape", techStack: "Cloud", category: "Generic Shapes", searchTerms: "cloud shape" },
|
|
15204
|
-
{ type: "shape", techStack: "Actor", category: "Generic Shapes", searchTerms: "actor user person human shape" },
|
|
15205
|
-
{ type: "shape", techStack: "
|
|
15204
|
+
{ type: "shape", techStack: "Actor", category: "Generic Shapes", searchTerms: "actor user person human shape", aliases: ["User", "Client", "Customer", "Person"] },
|
|
15205
|
+
{ type: "shape", techStack: "Browser", category: "Generic Shapes", searchTerms: "browser web client user", aliases: ["Web Browser", "Web Client"] },
|
|
15206
|
+
{ 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"] },
|
|
15207
|
+
{ type: "shape", techStack: "Note", category: "Generic Shapes", searchTerms: "note document comment annotation", kind: "other" },
|
|
15208
|
+
// Generic components: one per kind, for a node whose product does not matter or is not listed.
|
|
15209
|
+
{ 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"] },
|
|
15210
|
+
{ type: "service", techStack: "Worker", category: "Generic Components", searchTerms: "worker background job consumer processor", aliases: ["Background Worker", "Job Worker", "Consumer", "Job Runner"] },
|
|
15211
|
+
{ type: "database", techStack: "Database", category: "Generic Components", searchTerms: "database relational sql db", aliases: ["DB", "RDBMS", "SQL Database", "Relational Database", "SQL"] },
|
|
15212
|
+
{ 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"] },
|
|
15213
|
+
{ type: "cache", techStack: "Cache", category: "Generic Components", searchTerms: "cache in-memory key value", aliases: ["In-Memory Cache", "Distributed Cache"] },
|
|
15214
|
+
{ 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"] },
|
|
15215
|
+
{ 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"] },
|
|
15216
|
+
{ type: "cdn", techStack: "CDN", category: "Generic Components", searchTerms: "cdn content delivery network edge cache", kind: "cdn", aliases: ["Content Delivery Network", "Edge Cache"] },
|
|
15217
|
+
{ 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"] },
|
|
15218
|
+
{ type: "cdn", techStack: "API Gateway", category: "Generic Components", searchTerms: "api gateway", kind: "gateway", aliases: ["Gateway", "Edge Gateway"] },
|
|
15219
|
+
{ type: "cdn", techStack: "DNS", category: "Generic Components", searchTerms: "dns domain name system", kind: "dns", aliases: ["Domain Name System", "DNS Server"] },
|
|
15220
|
+
{ type: "cdn", techStack: "WAF", category: "Generic Components", searchTerms: "waf web application firewall edge", kind: "edge", aliases: ["Web Application Firewall", "Firewall"] },
|
|
15221
|
+
{ type: "serverless", techStack: "Function", category: "Generic Components", searchTerms: "function serverless faas", aliases: ["Serverless Function", "Serverless", "FaaS", "Cloud Function"] },
|
|
15222
|
+
{ 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"] },
|
|
15223
|
+
{ 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"] },
|
|
15206
15224
|
// Services
|
|
15207
|
-
{ type: "service", techStack: "REST API", category: "Services", searchTerms: "rest api http service endpoint" },
|
|
15208
|
-
{ type: "service", techStack: "GraphQL", category: "Services", searchTerms: "graphql api service query" },
|
|
15209
|
-
{ type: "service", techStack: "gRPC", category: "Services", searchTerms: "grpc rpc service" },
|
|
15210
|
-
{ type: "service", techStack: "WebSocket", category: "Services", searchTerms: "websocket ws realtime service" },
|
|
15211
|
-
{ type: "service", techStack: "SOAP API", category: "Services", searchTerms: "soap api xml service" },
|
|
15225
|
+
{ type: "service", techStack: "REST API", category: "Services", searchTerms: "rest api http service endpoint", aliases: ["REST", "API", "HTTP API", "Web API", "REST Service"] },
|
|
15226
|
+
{ type: "service", techStack: "GraphQL", category: "Services", searchTerms: "graphql api service query", aliases: ["GraphQL API", "Apollo"] },
|
|
15227
|
+
{ type: "service", techStack: "gRPC", category: "Services", searchTerms: "grpc rpc service", aliases: ["gRPC Service", "RPC"] },
|
|
15228
|
+
{ type: "service", techStack: "WebSocket", category: "Services", searchTerms: "websocket ws realtime service", aliases: ["WebSockets", "WS", "Socket.IO", "WebSocket Server"] },
|
|
15229
|
+
{ type: "service", techStack: "SOAP API", category: "Services", searchTerms: "soap api xml service", aliases: ["SOAP"] },
|
|
15230
|
+
// Languages and frameworks: a service written in it.
|
|
15231
|
+
{ type: "service", techStack: "Spring Boot", category: "Languages & Frameworks", searchTerms: "spring boot java kotlin service", aliases: ["Spring", "Spring Framework", "Spring WebFlux"] },
|
|
15232
|
+
{ type: "service", techStack: "Kotlin", category: "Languages & Frameworks", searchTerms: "kotlin ktor jvm service", aliases: ["Ktor"] },
|
|
15233
|
+
{ type: "service", techStack: "Java", category: "Languages & Frameworks", searchTerms: "java jvm service", aliases: ["JVM", "Quarkus", "Micronaut"] },
|
|
15234
|
+
{ type: "service", techStack: "Go", category: "Languages & Frameworks", searchTerms: "go golang service", aliases: ["Golang"] },
|
|
15235
|
+
{ type: "service", techStack: "Node.js", category: "Languages & Frameworks", searchTerms: "node nodejs javascript typescript express nestjs service", aliases: ["Node", "Express", "Express.js", "NestJS", "TypeScript", "JavaScript"] },
|
|
15236
|
+
{ type: "service", techStack: "Python", category: "Languages & Frameworks", searchTerms: "python service" },
|
|
15237
|
+
{ type: "service", techStack: "Django", category: "Languages & Frameworks", searchTerms: "django python service" },
|
|
15238
|
+
{ type: "service", techStack: "FastAPI", category: "Languages & Frameworks", searchTerms: "fastapi python service" },
|
|
15239
|
+
{ type: "service", techStack: "Flask", category: "Languages & Frameworks", searchTerms: "flask python service" },
|
|
15240
|
+
{ type: "service", techStack: "Ruby on Rails", category: "Languages & Frameworks", searchTerms: "ruby on rails service", aliases: ["Rails", "Ruby"] },
|
|
15241
|
+
{ type: "service", techStack: ".NET", category: "Languages & Frameworks", searchTerms: "dotnet .net asp.net c# service", aliases: ["ASP.NET", "ASP.NET Core", "dotnet", "C#"] },
|
|
15242
|
+
{ type: "service", techStack: "PHP", category: "Languages & Frameworks", searchTerms: "php laravel symfony service", aliases: ["Laravel", "Symfony"] },
|
|
15243
|
+
{ type: "service", techStack: "Rust", category: "Languages & Frameworks", searchTerms: "rust axum actix service", aliases: ["Axum", "Actix"] },
|
|
15244
|
+
{ type: "service", techStack: "Elixir", category: "Languages & Frameworks", searchTerms: "elixir phoenix erlang service", aliases: ["Phoenix", "Erlang"] },
|
|
15245
|
+
// Containers and platforms
|
|
15246
|
+
{ type: "container", techStack: "Kubernetes", category: "Containers & Platforms", searchTerms: "kubernetes k8s container pod", aliases: ["K8s", "Kubernetes Pod", "Pod"] },
|
|
15247
|
+
{ type: "container", techStack: "Docker", category: "Containers & Platforms", searchTerms: "docker container", aliases: ["Container", "Docker Container"] },
|
|
15248
|
+
{ type: "compute", techStack: "VM", category: "Containers & Platforms", searchTerms: "vm virtual machine server instance", aliases: ["Virtual Machine", "Instance", "Bare Metal"] },
|
|
15249
|
+
{ type: "serverless", techStack: "Cloudflare Workers", category: "Containers & Platforms", searchTerms: "cloudflare workers edge functions serverless", aliases: ["Workers"] },
|
|
15212
15250
|
// AWS Serverless
|
|
15213
|
-
{ type: "serverless", techStack: "AWS Lambda", category: "AWS Services", searchTerms: "aws lambda serverless function" },
|
|
15214
|
-
{ type: "serverless", techStack: "AWS API Gateway", category: "AWS Services", searchTerms: "aws api gateway rest" },
|
|
15251
|
+
{ type: "serverless", techStack: "AWS Lambda", category: "AWS Services", searchTerms: "aws lambda serverless function", aliases: ["Lambda", "Amazon Lambda"] },
|
|
15252
|
+
{ type: "serverless", techStack: "AWS API Gateway", category: "AWS Services", searchTerms: "aws api gateway rest", kind: "gateway", aliases: ["Amazon API Gateway"] },
|
|
15215
15253
|
// AWS Compute
|
|
15216
|
-
{ type: "compute", techStack: "AWS EC2", category: "AWS Services", searchTerms: "aws ec2 compute instance vm" },
|
|
15217
|
-
{ type: "container", techStack: "AWS ECS", category: "AWS Services", searchTerms: "aws ecs container docker" },
|
|
15218
|
-
{ type: "container", techStack: "AWS EKS", category: "AWS Services", searchTerms: "aws eks kubernetes k8s" },
|
|
15219
|
-
{ type: "container", techStack: "AWS Fargate", category: "AWS Services", searchTerms: "aws fargate serverless container" },
|
|
15254
|
+
{ type: "compute", techStack: "AWS EC2", category: "AWS Services", searchTerms: "aws ec2 compute instance vm", aliases: ["EC2", "Amazon EC2"] },
|
|
15255
|
+
{ type: "container", techStack: "AWS ECS", category: "AWS Services", searchTerms: "aws ecs container docker", aliases: ["ECS", "Amazon ECS"] },
|
|
15256
|
+
{ type: "container", techStack: "AWS EKS", category: "AWS Services", searchTerms: "aws eks kubernetes k8s", aliases: ["EKS", "Amazon EKS"] },
|
|
15257
|
+
{ type: "container", techStack: "AWS Fargate", category: "AWS Services", searchTerms: "aws fargate serverless container", aliases: ["Fargate"] },
|
|
15220
15258
|
// AWS Storage
|
|
15221
|
-
{ type: "storage", techStack: "AWS S3", category: "AWS Services", searchTerms: "aws s3 storage bucket object" },
|
|
15222
|
-
{ type: "storage", techStack: "AWS EBS", category: "AWS Services", searchTerms: "aws ebs storage volume disk" },
|
|
15223
|
-
{ type: "storage", techStack: "AWS EFS", category: "AWS Services", searchTerms: "aws efs storage file system" },
|
|
15259
|
+
{ type: "storage", techStack: "AWS S3", category: "AWS Services", searchTerms: "aws s3 storage bucket object", aliases: ["S3", "Amazon S3", "S3 Bucket"] },
|
|
15260
|
+
{ type: "storage", techStack: "AWS EBS", category: "AWS Services", searchTerms: "aws ebs storage volume disk", aliases: ["EBS", "Amazon EBS"] },
|
|
15261
|
+
{ type: "storage", techStack: "AWS EFS", category: "AWS Services", searchTerms: "aws efs storage file system", aliases: ["EFS", "Amazon EFS"] },
|
|
15224
15262
|
// AWS Database
|
|
15225
|
-
{ type: "database", techStack: "AWS RDS", category: "AWS Services", searchTerms: "aws rds database relational sql" },
|
|
15226
|
-
{ type: "database", techStack: "AWS DynamoDB", category: "AWS Services", searchTerms: "aws dynamodb database nosql" },
|
|
15227
|
-
{ type: "cache", techStack: "AWS ElastiCache", category: "AWS Services", searchTerms: "aws elasticache redis memcached cache" },
|
|
15228
|
-
{ type: "database", techStack: "AWS Aurora", category: "AWS Services", searchTerms: "aws aurora database mysql postgresql" },
|
|
15263
|
+
{ type: "database", techStack: "AWS RDS", category: "AWS Services", searchTerms: "aws rds database relational sql", aliases: ["RDS", "Amazon RDS"] },
|
|
15264
|
+
{ type: "database", techStack: "AWS DynamoDB", category: "AWS Services", searchTerms: "aws dynamodb database nosql", profile: "nosql", aliases: ["Amazon DynamoDB", "Dynamo"] },
|
|
15265
|
+
{ type: "cache", techStack: "AWS ElastiCache", category: "AWS Services", searchTerms: "aws elasticache redis memcached cache", aliases: ["ElastiCache", "Amazon ElastiCache"] },
|
|
15266
|
+
{ type: "database", techStack: "AWS Aurora", category: "AWS Services", searchTerms: "aws aurora database mysql postgresql", aliases: ["Aurora", "Amazon Aurora", "Aurora PostgreSQL", "Aurora MySQL", "Aurora Serverless"] },
|
|
15267
|
+
{ type: "database", techStack: "AWS DocumentDB", category: "AWS Services", searchTerms: "aws documentdb mongodb document database", aliases: ["DocumentDB", "Amazon DocumentDB"] },
|
|
15268
|
+
{ type: "database", techStack: "AWS Keyspaces", category: "AWS Services", searchTerms: "aws keyspaces cassandra nosql", profile: "nosql", aliases: ["Keyspaces", "Amazon Keyspaces"] },
|
|
15269
|
+
{ type: "database", techStack: "AWS Neptune", category: "AWS Services", searchTerms: "aws neptune graph database", aliases: ["Neptune", "Amazon Neptune"] },
|
|
15270
|
+
{ type: "database", techStack: "AWS Redshift", category: "AWS Services", searchTerms: "aws redshift data warehouse analytics", kind: "analytics", aliases: ["Redshift", "Amazon Redshift"] },
|
|
15271
|
+
{ type: "database", techStack: "AWS Athena", category: "AWS Services", searchTerms: "aws athena query s3 analytics", kind: "analytics", aliases: ["Athena", "Amazon Athena"] },
|
|
15272
|
+
{ type: "database", techStack: "AWS OpenSearch", category: "AWS Services", searchTerms: "aws opensearch elasticsearch search", kind: "search", aliases: ["Amazon OpenSearch", "OpenSearch Service", "AWS Elasticsearch"] },
|
|
15229
15273
|
// AWS Messaging
|
|
15230
|
-
{ type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message" },
|
|
15231
|
-
{ type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub" },
|
|
15232
|
-
{ type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus" },
|
|
15233
|
-
{ type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data" },
|
|
15274
|
+
{ type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message", aliases: ["Amazon SQS", "Simple Queue Service"] },
|
|
15275
|
+
{ type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
|
|
15276
|
+
{ type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
|
|
15277
|
+
{ type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
|
|
15278
|
+
{ type: "queue", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
|
|
15234
15279
|
// AWS CDN/Network
|
|
15235
|
-
{ type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge" },
|
|
15236
|
-
{ type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain" },
|
|
15237
|
-
{ type: "cdn", techStack: "AWS Load Balancer", category: "AWS Services", searchTerms: "aws alb elb load balancer" },
|
|
15280
|
+
{ type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge", kind: "cdn", aliases: ["CloudFront", "Amazon CloudFront"] },
|
|
15281
|
+
{ type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain", kind: "dns", aliases: ["Route53", "Amazon Route 53"] },
|
|
15282
|
+
{ 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"] },
|
|
15283
|
+
{ type: "cdn", techStack: "AWS WAF", category: "AWS Services", searchTerms: "aws waf web application firewall", kind: "edge", aliases: ["Amazon WAF"] },
|
|
15284
|
+
{ type: "cdn", techStack: "AWS Global Accelerator", category: "AWS Services", searchTerms: "aws global accelerator anycast edge", kind: "edge", aliases: ["Global Accelerator"] },
|
|
15285
|
+
// AWS third-party style services
|
|
15286
|
+
{ type: "external", techStack: "AWS SES", category: "AWS Services", searchTerms: "aws ses email simple email service", aliases: ["SES", "Amazon SES", "Simple Email Service"] },
|
|
15287
|
+
{ type: "external", techStack: "AWS Cognito", category: "AWS Services", searchTerms: "aws cognito auth identity users", aliases: ["Cognito", "Amazon Cognito"] },
|
|
15238
15288
|
// GCP Serverless
|
|
15239
|
-
{ type: "serverless", techStack: "GCP Cloud Functions", category: "GCP Services", searchTerms: "gcp google cloud functions serverless" },
|
|
15240
|
-
{ type: "serverless", techStack: "GCP Cloud Run", category: "GCP Services", searchTerms: "gcp google cloud run serverless container" },
|
|
15241
|
-
{ type: "serverless", techStack: "GCP App Engine", category: "GCP Services", searchTerms: "gcp google app engine paas" },
|
|
15289
|
+
{ type: "serverless", techStack: "GCP Cloud Functions", category: "GCP Services", searchTerms: "gcp google cloud functions serverless", aliases: ["Cloud Functions", "Google Cloud Functions"] },
|
|
15290
|
+
{ type: "serverless", techStack: "GCP Cloud Run", category: "GCP Services", searchTerms: "gcp google cloud run serverless container", aliases: ["Cloud Run", "Google Cloud Run"] },
|
|
15291
|
+
{ type: "serverless", techStack: "GCP App Engine", category: "GCP Services", searchTerms: "gcp google app engine paas", aliases: ["App Engine", "Google App Engine"] },
|
|
15242
15292
|
// GCP Compute
|
|
15243
|
-
{ type: "compute", techStack: "GCP Compute Engine", category: "GCP Services", searchTerms: "gcp google compute engine vm instance" },
|
|
15244
|
-
{ type: "container", techStack: "GCP GKE", category: "GCP Services", searchTerms: "gcp google gke kubernetes k8s" },
|
|
15293
|
+
{ type: "compute", techStack: "GCP Compute Engine", category: "GCP Services", searchTerms: "gcp google compute engine vm instance", aliases: ["Compute Engine", "GCE", "Google Compute Engine"] },
|
|
15294
|
+
{ type: "container", techStack: "GCP GKE", category: "GCP Services", searchTerms: "gcp google gke kubernetes k8s", aliases: ["GKE", "Google Kubernetes Engine"] },
|
|
15245
15295
|
// GCP Storage
|
|
15246
|
-
{ type: "storage", techStack: "GCP Cloud Storage", category: "GCP Services", searchTerms: "gcp google cloud storage bucket object" },
|
|
15247
|
-
{ type: "storage", techStack: "GCP Persistent Disk", category: "GCP Services", searchTerms: "gcp google persistent disk storage" },
|
|
15296
|
+
{ type: "storage", techStack: "GCP Cloud Storage", category: "GCP Services", searchTerms: "gcp google cloud storage bucket object", aliases: ["GCS", "Cloud Storage", "Google Cloud Storage"] },
|
|
15297
|
+
{ type: "storage", techStack: "GCP Persistent Disk", category: "GCP Services", searchTerms: "gcp google persistent disk storage", aliases: ["Persistent Disk"] },
|
|
15248
15298
|
// GCP Database
|
|
15249
|
-
{ type: "database", techStack: "GCP Cloud SQL", category: "GCP Services", searchTerms: "gcp google cloud sql database" },
|
|
15250
|
-
{ type: "database", techStack: "GCP Firestore", category: "GCP Services", searchTerms: "gcp google firestore database nosql" },
|
|
15251
|
-
{ type: "database", techStack: "GCP Bigtable", category: "GCP Services", searchTerms: "gcp google bigtable database nosql" },
|
|
15252
|
-
{ type: "database", techStack: "GCP Spanner", category: "GCP Services", searchTerms: "gcp google spanner database sql" },
|
|
15253
|
-
{ type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics" },
|
|
15299
|
+
{ type: "database", techStack: "GCP Cloud SQL", category: "GCP Services", searchTerms: "gcp google cloud sql database", aliases: ["Cloud SQL", "Google Cloud SQL"] },
|
|
15300
|
+
{ type: "database", techStack: "GCP Firestore", category: "GCP Services", searchTerms: "gcp google firestore database nosql", profile: "nosqlStrong", aliases: ["Firestore", "Cloud Firestore", "Datastore"] },
|
|
15301
|
+
{ type: "database", techStack: "GCP Bigtable", category: "GCP Services", searchTerms: "gcp google bigtable database nosql", profile: "nosqlStrong", aliases: ["Bigtable", "Cloud Bigtable"] },
|
|
15302
|
+
{ type: "database", techStack: "GCP Spanner", category: "GCP Services", searchTerms: "gcp google spanner database sql", profile: "nosqlStrong", aliases: ["Spanner", "Cloud Spanner", "Google Spanner"] },
|
|
15303
|
+
{ type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics", kind: "analytics", aliases: ["BigQuery", "Google BigQuery"] },
|
|
15304
|
+
{ type: "cache", techStack: "GCP Memorystore", category: "GCP Services", searchTerms: "gcp google memorystore redis memcached cache", aliases: ["Memorystore"] },
|
|
15254
15305
|
// GCP Messaging
|
|
15255
|
-
{ type: "queue", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue" },
|
|
15306
|
+
{ 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"] },
|
|
15256
15307
|
// GCP CDN/Network
|
|
15257
|
-
{ type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge" },
|
|
15258
|
-
{ type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain" },
|
|
15259
|
-
{ type: "cdn", techStack: "GCP Load Balancing", category: "GCP Services", searchTerms: "gcp google load balancing" },
|
|
15308
|
+
{ type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge", kind: "cdn", aliases: ["Cloud CDN", "Google Cloud CDN"] },
|
|
15309
|
+
{ type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain", kind: "dns", aliases: ["Cloud DNS", "Google Cloud DNS"] },
|
|
15310
|
+
{ 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"] },
|
|
15311
|
+
{ type: "cdn", techStack: "GCP Apigee", category: "GCP Services", searchTerms: "gcp google apigee api gateway management", kind: "gateway", aliases: ["Apigee"] },
|
|
15260
15312
|
// Azure Serverless
|
|
15261
|
-
{ type: "serverless", techStack: "Azure Functions", category: "Azure Services", searchTerms: "azure functions serverless" },
|
|
15262
|
-
{ type: "serverless", techStack: "Azure Logic Apps", category: "Azure Services", searchTerms: "azure logic apps workflow" },
|
|
15263
|
-
{ type: "serverless", techStack: "Azure API Management", category: "Azure Services", searchTerms: "azure api management gateway" },
|
|
15313
|
+
{ type: "serverless", techStack: "Azure Functions", category: "Azure Services", searchTerms: "azure functions serverless", aliases: ["Azure Function"] },
|
|
15314
|
+
{ type: "serverless", techStack: "Azure Logic Apps", category: "Azure Services", searchTerms: "azure logic apps workflow", aliases: ["Logic Apps"] },
|
|
15315
|
+
{ type: "serverless", techStack: "Azure API Management", category: "Azure Services", searchTerms: "azure api management gateway", kind: "gateway", aliases: ["APIM", "Azure APIM"] },
|
|
15264
15316
|
// Azure Compute
|
|
15265
|
-
{ type: "compute", techStack: "Azure VM", category: "Azure Services", searchTerms: "azure vm virtual machine compute" },
|
|
15266
|
-
{ type: "container", techStack: "Azure AKS", category: "Azure Services", searchTerms: "azure aks kubernetes k8s" },
|
|
15267
|
-
{ type: "container", techStack: "Azure Container Instances", category: "Azure Services", searchTerms: "azure container instances aci" },
|
|
15317
|
+
{ type: "compute", techStack: "Azure VM", category: "Azure Services", searchTerms: "azure vm virtual machine compute", aliases: ["Azure Virtual Machine", "Azure Virtual Machines"] },
|
|
15318
|
+
{ type: "container", techStack: "Azure AKS", category: "Azure Services", searchTerms: "azure aks kubernetes k8s", aliases: ["AKS", "Azure Kubernetes Service"] },
|
|
15319
|
+
{ type: "container", techStack: "Azure Container Instances", category: "Azure Services", searchTerms: "azure container instances aci", aliases: ["ACI"] },
|
|
15320
|
+
{ type: "service", techStack: "Azure App Service", category: "Azure Services", searchTerms: "azure app service web apps paas", aliases: ["App Service", "Azure Web Apps"] },
|
|
15268
15321
|
// Azure Storage
|
|
15269
|
-
{ type: "storage", techStack: "Azure Blob Storage", category: "Azure Services", searchTerms: "azure blob storage object" },
|
|
15322
|
+
{ type: "storage", techStack: "Azure Blob Storage", category: "Azure Services", searchTerms: "azure blob storage object", aliases: ["Azure Blob", "Azure Storage"] },
|
|
15270
15323
|
{ type: "storage", techStack: "Azure Files", category: "Azure Services", searchTerms: "azure files storage file system" },
|
|
15271
|
-
{ type: "storage", techStack: "Azure Disk Storage", category: "Azure Services", searchTerms: "azure disk storage volume" },
|
|
15324
|
+
{ type: "storage", techStack: "Azure Disk Storage", category: "Azure Services", searchTerms: "azure disk storage volume", aliases: ["Azure Managed Disks"] },
|
|
15272
15325
|
// Azure Database
|
|
15273
|
-
{ type: "database", techStack: "Azure SQL", category: "Azure Services", searchTerms: "azure sql database" },
|
|
15274
|
-
{ type: "database", techStack: "Azure Cosmos DB", category: "Azure Services", searchTerms: "azure cosmos db database nosql" },
|
|
15275
|
-
{ type: "database", techStack: "Azure Database for PostgreSQL", category: "Azure Services", searchTerms: "azure postgresql database" },
|
|
15276
|
-
{ type: "database", techStack: "Azure Database for MySQL", category: "Azure Services", searchTerms: "azure mysql database" },
|
|
15277
|
-
{ type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache" },
|
|
15326
|
+
{ type: "database", techStack: "Azure SQL", category: "Azure Services", searchTerms: "azure sql database", aliases: ["Azure SQL Database"] },
|
|
15327
|
+
{ type: "database", techStack: "Azure Cosmos DB", category: "Azure Services", searchTerms: "azure cosmos db database nosql", profile: "nosql", aliases: ["Cosmos DB", "Cosmos"] },
|
|
15328
|
+
{ type: "database", techStack: "Azure Database for PostgreSQL", category: "Azure Services", searchTerms: "azure postgresql database", aliases: ["Azure PostgreSQL", "Azure Postgres"] },
|
|
15329
|
+
{ type: "database", techStack: "Azure Database for MySQL", category: "Azure Services", searchTerms: "azure mysql database", aliases: ["Azure MySQL"] },
|
|
15330
|
+
{ type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache", aliases: ["Azure Redis"] },
|
|
15331
|
+
{ type: "database", techStack: "Azure AI Search", category: "Azure Services", searchTerms: "azure ai cognitive search", kind: "search", aliases: ["Azure Cognitive Search", "Azure Search"] },
|
|
15278
15332
|
// Azure Messaging
|
|
15279
|
-
{ type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message" },
|
|
15280
|
-
{ type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream" },
|
|
15281
|
-
{ type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid" },
|
|
15282
|
-
{ type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message" },
|
|
15333
|
+
{ type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
|
|
15334
|
+
{ type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
|
|
15335
|
+
{ type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
|
|
15336
|
+
{ type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message", aliases: ["Azure Queue", "Azure Queues"] },
|
|
15283
15337
|
// Azure CDN/Network
|
|
15284
|
-
{ type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge" },
|
|
15285
|
-
{ type: "cdn", techStack: "Azure DNS", category: "Azure Services", searchTerms: "azure dns domain" },
|
|
15286
|
-
{ type: "cdn", techStack: "Azure Front Door", category: "Azure Services", searchTerms: "azure front door cdn" },
|
|
15338
|
+
{ type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge", kind: "cdn" },
|
|
15339
|
+
{ type: "cdn", techStack: "Azure DNS", category: "Azure Services", searchTerms: "azure dns domain", kind: "dns" },
|
|
15340
|
+
{ type: "cdn", techStack: "Azure Front Door", category: "Azure Services", searchTerms: "azure front door cdn", kind: "cdn", aliases: ["Front Door"] },
|
|
15341
|
+
{ type: "cdn", techStack: "Azure Load Balancer", category: "Azure Services", searchTerms: "azure load balancer", kind: "loadbalancer" },
|
|
15342
|
+
{ type: "cdn", techStack: "Azure Application Gateway", category: "Azure Services", searchTerms: "azure application gateway l7 load balancer", kind: "loadbalancer", aliases: ["Application Gateway", "Azure App Gateway"] },
|
|
15287
15343
|
// Traditional Databases
|
|
15288
|
-
{ type: "database", techStack: "PostgreSQL", category: "Databases", searchTerms: "postgresql postgres sql database" },
|
|
15289
|
-
{ type: "database", techStack: "MySQL", category: "Databases", searchTerms: "mysql sql database" },
|
|
15290
|
-
{ type: "database", techStack: "MongoDB", category: "Databases", searchTerms: "mongodb mongo nosql database" },
|
|
15291
|
-
{ type: "database", techStack: "Redis", category: "Databases", searchTerms: "redis cache database key-value" },
|
|
15292
|
-
{ type: "database", techStack: "DynamoDB", category: "Databases", searchTerms: "dynamodb nosql database aws" },
|
|
15293
|
-
{ type: "database", techStack: "Cassandra", category: "Databases", searchTerms: "cassandra nosql database" },
|
|
15294
|
-
{ type: "database", techStack: "CouchDB", category: "Databases", searchTerms: "couchdb nosql database" },
|
|
15295
|
-
{ type: "database", techStack: "Elasticsearch", category: "Databases", searchTerms: "elasticsearch search database" },
|
|
15296
|
-
{ type: "database", techStack: "Neo4j", category: "Databases", searchTerms: "neo4j graph database" },
|
|
15297
|
-
{ type: "database", techStack: "InfluxDB", category: "Databases", searchTerms: "influxdb timeseries database" },
|
|
15298
|
-
{ type: "database", techStack: "TimescaleDB", category: "Databases", searchTerms: "timescaledb timeseries postgresql database" },
|
|
15299
|
-
{ type: "database", techStack: "MariaDB", category: "Databases", searchTerms: "mariadb mysql sql database" },
|
|
15344
|
+
{ type: "database", techStack: "PostgreSQL", category: "Databases", searchTerms: "postgresql postgres sql database", aliases: ["Postgres", "PG", "PostGIS", "pgvector"] },
|
|
15345
|
+
{ type: "database", techStack: "MySQL", category: "Databases", searchTerms: "mysql sql database", aliases: ["Percona"] },
|
|
15346
|
+
{ type: "database", techStack: "MongoDB", category: "Databases", searchTerms: "mongodb mongo nosql database", profile: "nosqlStrong", aliases: ["Mongo", "MongoDB Atlas"] },
|
|
15347
|
+
{ type: "database", techStack: "Redis", category: "Databases", searchTerms: "redis cache database key-value", kind: "cache", aliases: ["Redis Cluster", "Redis Cache"] },
|
|
15348
|
+
{ type: "database", techStack: "DynamoDB", category: "Databases", searchTerms: "dynamodb nosql database aws", profile: "nosql" },
|
|
15349
|
+
{ type: "database", techStack: "Cassandra", category: "Databases", searchTerms: "cassandra nosql database", profile: "nosql", aliases: ["Apache Cassandra", "DataStax"] },
|
|
15350
|
+
{ type: "database", techStack: "CouchDB", category: "Databases", searchTerms: "couchdb nosql database", profile: "nosql", aliases: ["Apache CouchDB"] },
|
|
15351
|
+
{ type: "database", techStack: "Elasticsearch", category: "Databases", searchTerms: "elasticsearch search database", kind: "search", aliases: ["Elastic", "Elastic Search", "ES"] },
|
|
15352
|
+
{ type: "database", techStack: "Neo4j", category: "Databases", searchTerms: "neo4j graph database", aliases: ["Graph Database", "Graph DB"] },
|
|
15353
|
+
{ type: "database", techStack: "InfluxDB", category: "Databases", searchTerms: "influxdb timeseries database", kind: "analytics", aliases: ["Influx"] },
|
|
15354
|
+
{ type: "database", techStack: "TimescaleDB", category: "Databases", searchTerms: "timescaledb timeseries postgresql database", kind: "analytics", aliases: ["Timescale"] },
|
|
15355
|
+
{ type: "database", techStack: "MariaDB", category: "Databases", searchTerms: "mariadb mysql sql database", aliases: ["Maria"] },
|
|
15300
15356
|
{ type: "database", techStack: "SQLite", category: "Databases", searchTerms: "sqlite sql database" },
|
|
15301
|
-
{ type: "database", techStack: "Oracle", category: "Databases", searchTerms: "oracle sql database" },
|
|
15302
|
-
{ type: "database", techStack: "SQL Server", category: "Databases", searchTerms: "sql server mssql database microsoft" },
|
|
15357
|
+
{ type: "database", techStack: "Oracle", category: "Databases", searchTerms: "oracle sql database", aliases: ["Oracle DB", "Oracle Database"] },
|
|
15358
|
+
{ type: "database", techStack: "SQL Server", category: "Databases", searchTerms: "sql server mssql database microsoft", aliases: ["MSSQL", "MS SQL", "Microsoft SQL Server"] },
|
|
15359
|
+
{ type: "database", techStack: "CockroachDB", category: "Databases", searchTerms: "cockroachdb distributed sql database", profile: "nosqlStrong", aliases: ["Cockroach"] },
|
|
15360
|
+
{ type: "database", techStack: "YugabyteDB", category: "Databases", searchTerms: "yugabytedb distributed sql database", profile: "nosqlStrong", aliases: ["Yugabyte"] },
|
|
15361
|
+
{ type: "database", techStack: "TiDB", category: "Databases", searchTerms: "tidb distributed sql mysql database", profile: "nosqlStrong" },
|
|
15362
|
+
{ type: "database", techStack: "Vitess", category: "Databases", searchTerms: "vitess sharded mysql planetscale database", profile: "nosqlStrong", aliases: ["PlanetScale"] },
|
|
15363
|
+
{ type: "database", techStack: "ScyllaDB", category: "Databases", searchTerms: "scylladb cassandra nosql database", profile: "nosql", aliases: ["Scylla"] },
|
|
15364
|
+
{ type: "database", techStack: "Couchbase", category: "Databases", searchTerms: "couchbase nosql document database", profile: "nosql" },
|
|
15365
|
+
{ type: "database", techStack: "HBase", category: "Databases", searchTerms: "hbase hadoop wide column nosql database", profile: "nosqlStrong", aliases: ["Apache HBase"] },
|
|
15366
|
+
{ type: "database", techStack: "etcd", category: "Databases", searchTerms: "etcd key value consensus configuration" },
|
|
15367
|
+
{ type: "database", techStack: "ZooKeeper", category: "Databases", searchTerms: "zookeeper coordination consensus configuration", aliases: ["Apache ZooKeeper"] },
|
|
15303
15368
|
// Cache & In-Memory
|
|
15304
|
-
{ type: "cache", techStack: "Memcached", category: "Cache & In-Memory", searchTerms: "memcached cache memory" },
|
|
15369
|
+
{ type: "cache", techStack: "Memcached", category: "Cache & In-Memory", searchTerms: "memcached cache memory", aliases: ["Memcache"] },
|
|
15305
15370
|
{ type: "cache", techStack: "Hazelcast", category: "Cache & In-Memory", searchTerms: "hazelcast cache memory" },
|
|
15306
15371
|
{ type: "cache", techStack: "Aerospike", category: "Cache & In-Memory", searchTerms: "aerospike cache database" },
|
|
15372
|
+
{ type: "cache", techStack: "Valkey", category: "Cache & In-Memory", searchTerms: "valkey redis cache memory" },
|
|
15373
|
+
{ type: "cache", techStack: "KeyDB", category: "Cache & In-Memory", searchTerms: "keydb redis cache memory" },
|
|
15374
|
+
{ type: "cache", techStack: "Dragonfly", category: "Cache & In-Memory", searchTerms: "dragonfly dragonflydb redis cache memory", aliases: ["DragonflyDB"] },
|
|
15307
15375
|
// Message Queues
|
|
15308
|
-
{ type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream" },
|
|
15309
|
-
{ type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp" },
|
|
15376
|
+
{ type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
|
|
15377
|
+
{ type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp", aliases: ["Rabbit", "AMQP"] },
|
|
15310
15378
|
{ type: "queue", techStack: "SQS", category: "Message Queues", searchTerms: "sqs queue aws message" },
|
|
15311
|
-
{ type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message" },
|
|
15312
|
-
{ type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue" },
|
|
15313
|
-
{ type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream" },
|
|
15314
|
-
{ type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue" },
|
|
15315
|
-
{ type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue" },
|
|
15379
|
+
{ type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
|
|
15380
|
+
{ type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue", aliases: ["NATS JetStream", "JetStream"] },
|
|
15381
|
+
{ type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream", aliases: ["Pulsar"] },
|
|
15382
|
+
{ type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
|
|
15383
|
+
{ type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
|
|
15384
|
+
{ type: "queue", techStack: "Redpanda", category: "Message Queues", searchTerms: "redpanda kafka stream queue" },
|
|
15385
|
+
// Search
|
|
15386
|
+
{ type: "database", techStack: "OpenSearch", category: "Search", searchTerms: "opensearch elasticsearch search", kind: "search" },
|
|
15387
|
+
{ type: "database", techStack: "Solr", category: "Search", searchTerms: "solr lucene search", kind: "search", aliases: ["Apache Solr"] },
|
|
15388
|
+
{ type: "database", techStack: "Meilisearch", category: "Search", searchTerms: "meilisearch search", kind: "search" },
|
|
15389
|
+
{ type: "database", techStack: "Typesense", category: "Search", searchTerms: "typesense search", kind: "search" },
|
|
15390
|
+
{ type: "database", techStack: "Algolia", category: "Search", searchTerms: "algolia hosted search", kind: "search" },
|
|
15391
|
+
// Analytics
|
|
15392
|
+
{ type: "database", techStack: "Snowflake", category: "Analytics", searchTerms: "snowflake data warehouse analytics", kind: "analytics" },
|
|
15393
|
+
{ type: "database", techStack: "ClickHouse", category: "Analytics", searchTerms: "clickhouse olap analytics columnar", kind: "analytics" },
|
|
15394
|
+
{ type: "database", techStack: "Apache Druid", category: "Analytics", searchTerms: "druid olap analytics", kind: "analytics", aliases: ["Druid"] },
|
|
15395
|
+
{ type: "database", techStack: "Apache Pinot", category: "Analytics", searchTerms: "pinot olap analytics", kind: "analytics", aliases: ["Pinot"] },
|
|
15396
|
+
{ type: "database", techStack: "Databricks", category: "Analytics", searchTerms: "databricks lakehouse spark analytics", kind: "analytics" },
|
|
15397
|
+
{ type: "database", techStack: "Prometheus", category: "Analytics", searchTerms: "prometheus metrics time series monitoring", kind: "analytics" },
|
|
15398
|
+
// Storage
|
|
15399
|
+
{ type: "storage", techStack: "MinIO", category: "Storage", searchTerms: "minio s3 object storage" },
|
|
15400
|
+
{ type: "storage", techStack: "Cloudflare R2", category: "Storage", searchTerms: "cloudflare r2 object storage", aliases: ["R2"] },
|
|
15401
|
+
{ type: "storage", techStack: "Ceph", category: "Storage", searchTerms: "ceph object block storage" },
|
|
15402
|
+
{ type: "storage", techStack: "HDFS", category: "Storage", searchTerms: "hdfs hadoop file system storage", aliases: ["Hadoop", "Hadoop HDFS"] },
|
|
15403
|
+
// CDN, proxies and gateways
|
|
15404
|
+
{ type: "cdn", techStack: "Cloudflare", category: "CDN & Edge", searchTerms: "cloudflare cdn edge", kind: "cdn", aliases: ["Cloudflare CDN"] },
|
|
15405
|
+
{ type: "cdn", techStack: "Fastly", category: "CDN & Edge", searchTerms: "fastly cdn edge", kind: "cdn" },
|
|
15406
|
+
{ type: "cdn", techStack: "Akamai", category: "CDN & Edge", searchTerms: "akamai cdn edge", kind: "cdn" },
|
|
15407
|
+
{ type: "cdn", techStack: "nginx", category: "CDN & Edge", searchTerms: "nginx reverse proxy load balancer web server", kind: "loadbalancer", aliases: ["NGINX Plus", "OpenResty"] },
|
|
15408
|
+
{ type: "cdn", techStack: "Envoy", category: "CDN & Edge", searchTerms: "envoy proxy load balancer service mesh", kind: "loadbalancer", aliases: ["Envoy Proxy"] },
|
|
15409
|
+
{ type: "cdn", techStack: "HAProxy", category: "CDN & Edge", searchTerms: "haproxy load balancer proxy", kind: "loadbalancer" },
|
|
15410
|
+
{ type: "cdn", techStack: "Traefik", category: "CDN & Edge", searchTerms: "traefik reverse proxy load balancer", kind: "loadbalancer" },
|
|
15411
|
+
{ type: "cdn", techStack: "Kubernetes Ingress", category: "CDN & Edge", searchTerms: "kubernetes ingress controller load balancer", kind: "loadbalancer", aliases: ["Ingress", "Ingress Controller", "K8s Ingress"] },
|
|
15412
|
+
{ type: "cdn", techStack: "Kong", category: "CDN & Edge", searchTerms: "kong api gateway", kind: "gateway", aliases: ["Kong Gateway"] },
|
|
15413
|
+
{ type: "cdn", techStack: "Spring Cloud Gateway", category: "CDN & Edge", searchTerms: "spring cloud gateway api gateway", kind: "gateway" },
|
|
15316
15414
|
// External Systems
|
|
15317
|
-
{ type: "external", techStack: "Third Party API", category: "External Systems", searchTerms: "third party api external" },
|
|
15318
|
-
{ type: "external", techStack: "Payment Gateway", category: "External Systems", searchTerms: "payment gateway stripe paypal" },
|
|
15319
|
-
{ type: "external", techStack: "Auth Service", category: "External Systems", searchTerms: "auth authentication authorization" },
|
|
15320
|
-
{ type: "external", techStack: "Email Service", category: "External Systems", searchTerms: "email service sendgrid mailgun" },
|
|
15321
|
-
{ type: "external", techStack: "SMS Service", category: "External Systems", searchTerms: "sms service twilio" },
|
|
15322
|
-
{ type: "external", techStack: "
|
|
15415
|
+
{ 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"] },
|
|
15416
|
+
{ type: "external", techStack: "Payment Gateway", category: "External Systems", searchTerms: "payment gateway stripe paypal", aliases: ["Payment Provider", "Payments API", "Payment Processor", "PSP"] },
|
|
15417
|
+
{ type: "external", techStack: "Auth Service", category: "External Systems", searchTerms: "auth authentication authorization", aliases: ["Identity Provider", "IdP", "OAuth Provider", "SSO"] },
|
|
15418
|
+
{ type: "external", techStack: "Email Service", category: "External Systems", searchTerms: "email service sendgrid mailgun", aliases: ["Email", "Email Provider", "SMTP"] },
|
|
15419
|
+
{ type: "external", techStack: "SMS Service", category: "External Systems", searchTerms: "sms service twilio", aliases: ["SMS", "SMS Gateway", "SMS Provider"] },
|
|
15420
|
+
{ type: "external", techStack: "Push Service", category: "External Systems", searchTerms: "push notification service mobile", aliases: ["Push Notifications", "Push Notification Service", "Push Provider"] },
|
|
15421
|
+
{ type: "external", techStack: "Analytics Service", category: "External Systems", searchTerms: "analytics service google analytics", aliases: ["Google Analytics", "Segment", "Mixpanel", "Amplitude"] },
|
|
15422
|
+
{ type: "external", techStack: "Stripe", category: "External Systems", searchTerms: "stripe payments" },
|
|
15423
|
+
{ type: "external", techStack: "PayPal", category: "External Systems", searchTerms: "paypal payments" },
|
|
15424
|
+
{ type: "external", techStack: "Adyen", category: "External Systems", searchTerms: "adyen payments" },
|
|
15425
|
+
{ type: "external", techStack: "Braintree", category: "External Systems", searchTerms: "braintree payments" },
|
|
15426
|
+
{ type: "external", techStack: "Twilio", category: "External Systems", searchTerms: "twilio sms voice" },
|
|
15427
|
+
{ type: "external", techStack: "SendGrid", category: "External Systems", searchTerms: "sendgrid email" },
|
|
15428
|
+
{ type: "external", techStack: "Mailgun", category: "External Systems", searchTerms: "mailgun email" },
|
|
15429
|
+
{ type: "external", techStack: "Auth0", category: "External Systems", searchTerms: "auth0 identity login" },
|
|
15430
|
+
{ type: "external", techStack: "Okta", category: "External Systems", searchTerms: "okta identity sso" },
|
|
15431
|
+
{ type: "external", techStack: "Google Maps", category: "External Systems", searchTerms: "google maps geocoding routing", aliases: ["Google Maps API", "Maps API"] },
|
|
15432
|
+
{ type: "external", techStack: "APNs", category: "External Systems", searchTerms: "apns apple push notification", aliases: ["Apple Push Notification Service", "Apple Push"] },
|
|
15433
|
+
{ type: "external", techStack: "FCM", category: "External Systems", searchTerms: "fcm firebase cloud messaging push", aliases: ["Firebase Cloud Messaging", "Firebase Messaging"] },
|
|
15434
|
+
{ type: "external", techStack: "OpenAI", category: "External Systems", searchTerms: "openai llm ai api", aliases: ["OpenAI API", "LLM API", "LLM"] },
|
|
15323
15435
|
// Text & Annotations
|
|
15324
15436
|
{ type: "text", techStack: "Text Note", category: "Annotations", searchTerms: "text note annotation comment label" },
|
|
15325
15437
|
{ type: "text", techStack: "Sticky Note", category: "Annotations", searchTerms: "sticky note annotation comment" },
|
|
@@ -15330,6 +15442,54 @@ var componentCatalog = [
|
|
|
15330
15442
|
{ type: "group", techStack: "Security Zone", category: "Grouping", searchTerms: "group security zone boundary" },
|
|
15331
15443
|
{ type: "group", techStack: "Service Group", category: "Grouping", searchTerms: "group service container boundary" }
|
|
15332
15444
|
];
|
|
15445
|
+
var techKey = (name) => name.toLowerCase().replace(/[^a-z0-9#+]/g, "");
|
|
15446
|
+
var entries = componentCatalog;
|
|
15447
|
+
var byKey = /* @__PURE__ */ new Map();
|
|
15448
|
+
for (const c of entries) {
|
|
15449
|
+
for (const name of [c.techStack, ...c.aliases ?? []]) {
|
|
15450
|
+
const key = techKey(name);
|
|
15451
|
+
if (!byKey.has(key)) byKey.set(key, c);
|
|
15452
|
+
}
|
|
15453
|
+
}
|
|
15454
|
+
var VERSION = /[\s-]*v?\d+(\.(\d+|x))*\s*$/i;
|
|
15455
|
+
function findTech(name) {
|
|
15456
|
+
const exact = byKey.get(techKey(name));
|
|
15457
|
+
if (exact) return exact;
|
|
15458
|
+
const unversioned = name.replace(VERSION, "");
|
|
15459
|
+
return unversioned !== name && unversioned.trim() ? byKey.get(techKey(unversioned)) : void 0;
|
|
15460
|
+
}
|
|
15461
|
+
function editDistance(a, b) {
|
|
15462
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
15463
|
+
for (let i = 1; i <= a.length; i++) {
|
|
15464
|
+
for (let j = 1; j <= b.length; j++) {
|
|
15465
|
+
const cost2 = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
15466
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost2);
|
|
15467
|
+
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);
|
|
15468
|
+
}
|
|
15469
|
+
}
|
|
15470
|
+
return d[a.length][b.length];
|
|
15471
|
+
}
|
|
15472
|
+
function suggestTech(name, accept = (c) => c.type !== "group" && c.type !== "text" && c.techStack !== "Note") {
|
|
15473
|
+
const key = techKey(name);
|
|
15474
|
+
if (!key) return void 0;
|
|
15475
|
+
let best;
|
|
15476
|
+
for (const c of entries) {
|
|
15477
|
+
if (!accept(c)) continue;
|
|
15478
|
+
for (const candidate of [c.techStack, ...c.aliases ?? []]) {
|
|
15479
|
+
const d = editDistance(key, techKey(candidate));
|
|
15480
|
+
if (!best || d < best.d) best = { tech: c.techStack, d };
|
|
15481
|
+
}
|
|
15482
|
+
}
|
|
15483
|
+
if (best && best.d <= Math.max(1, Math.floor(key.length / 4))) return best.tech;
|
|
15484
|
+
const words = name.toLowerCase().split(/[^a-z0-9.#+]+/).filter(Boolean);
|
|
15485
|
+
for (let n = Math.min(3, words.length); n >= 1; n--) {
|
|
15486
|
+
for (let i = 0; i + n <= words.length; i++) {
|
|
15487
|
+
const found = byKey.get(techKey(words.slice(i, i + n).join(" ")));
|
|
15488
|
+
if (found && accept(found) && techKey(words.slice(i, i + n).join("")).length >= 3) return found.techStack;
|
|
15489
|
+
}
|
|
15490
|
+
}
|
|
15491
|
+
return void 0;
|
|
15492
|
+
}
|
|
15333
15493
|
|
|
15334
15494
|
// ../frontend/src/dsl/lexer.ts
|
|
15335
15495
|
var IDENT_START = /[A-Za-z_]/;
|
|
@@ -15540,26 +15700,7 @@ function endpointGroupKey(endpoint, all) {
|
|
|
15540
15700
|
var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
|
|
15541
15701
|
var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
|
|
15542
15702
|
var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
|
|
15543
|
-
var BY_TECH =
|
|
15544
|
-
"AWS CloudFront": "cdn",
|
|
15545
|
-
"Azure CDN": "cdn",
|
|
15546
|
-
"GCP Cloud CDN": "cdn",
|
|
15547
|
-
"Azure Front Door": "cdn",
|
|
15548
|
-
"AWS Load Balancer": "loadbalancer",
|
|
15549
|
-
"GCP Load Balancing": "loadbalancer",
|
|
15550
|
-
"AWS API Gateway": "gateway",
|
|
15551
|
-
"Azure API Management": "gateway",
|
|
15552
|
-
"AWS Route53": "dns",
|
|
15553
|
-
"Azure DNS": "dns",
|
|
15554
|
-
"GCP Cloud DNS": "dns",
|
|
15555
|
-
Redis: "cache",
|
|
15556
|
-
Elasticsearch: "search",
|
|
15557
|
-
"GCP BigQuery": "analytics",
|
|
15558
|
-
InfluxDB: "analytics",
|
|
15559
|
-
TimescaleDB: "analytics",
|
|
15560
|
-
// A note shape annotates the diagram; it is not a client.
|
|
15561
|
-
Note: "other"
|
|
15562
|
-
};
|
|
15703
|
+
var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
|
|
15563
15704
|
var BY_TYPE = {
|
|
15564
15705
|
shape: "client",
|
|
15565
15706
|
cdn: "edge",
|
|
@@ -15575,7 +15716,44 @@ var BY_TYPE = {
|
|
|
15575
15716
|
};
|
|
15576
15717
|
function kindOf(node) {
|
|
15577
15718
|
if (node.kind !== "component") return "other";
|
|
15578
|
-
return BY_TECH
|
|
15719
|
+
return node.inferredKind ?? BY_TECH.get(node.techStack) ?? BY_TYPE[node.type] ?? "other";
|
|
15720
|
+
}
|
|
15721
|
+
var TYPE_OF_KIND = {
|
|
15722
|
+
client: "shape",
|
|
15723
|
+
edge: "cdn",
|
|
15724
|
+
cdn: "cdn",
|
|
15725
|
+
loadbalancer: "cdn",
|
|
15726
|
+
gateway: "cdn",
|
|
15727
|
+
dns: "cdn",
|
|
15728
|
+
service: "service",
|
|
15729
|
+
function: "serverless",
|
|
15730
|
+
cache: "cache",
|
|
15731
|
+
database: "database",
|
|
15732
|
+
search: "database",
|
|
15733
|
+
analytics: "database",
|
|
15734
|
+
queue: "queue",
|
|
15735
|
+
storage: "storage",
|
|
15736
|
+
external: "external",
|
|
15737
|
+
other: "shape"
|
|
15738
|
+
};
|
|
15739
|
+
var KIND_WORDS = [
|
|
15740
|
+
[/\bsms\b|e-?mail|third.?party|3rd.?party|external|saas|webhook/, "external"],
|
|
15741
|
+
[/\bcdn\b|edge.?cache|content.?delivery/, "cdn"],
|
|
15742
|
+
[/\bdns\b/, "dns"],
|
|
15743
|
+
[/load.?balanc|\blb\b|\balb\b|\bnlb\b|proxy|ingress/, "loadbalancer"],
|
|
15744
|
+
[/gateway|\bapim?\b.*manage/, "gateway"],
|
|
15745
|
+
[/\bwaf\b|firewall/, "edge"],
|
|
15746
|
+
[/cache|memcache/, "cache"],
|
|
15747
|
+
[/queue|\bmq\b|mq$|broker|event.?stream|\bbus\b|topic|pub.?sub|event.?hub|kafka/, "queue"],
|
|
15748
|
+
[/search|lucene|\bindex\b/, "search"],
|
|
15749
|
+
[/warehouse|analytic|olap|data.?lake|lakehouse|time.?series|\btsdb\b/, "analytics"],
|
|
15750
|
+
[/bucket|blob|object.?stor|storage|\bfs\b|file.?system|\bnas\b|\bdisk\b|volume/, "storage"],
|
|
15751
|
+
[/\bdb\b|db$|database|\bsql\b|sql$|nosql|\bstore\b|ledger|key.?value|\bkv\b/, "database"],
|
|
15752
|
+
[/lambda|function|serverless|\bfaas\b/, "function"]
|
|
15753
|
+
];
|
|
15754
|
+
function kindFromName(tech) {
|
|
15755
|
+
const text = tech.toLowerCase();
|
|
15756
|
+
return KIND_WORDS.find(([words]) => words.test(text))?.[1];
|
|
15579
15757
|
}
|
|
15580
15758
|
function isKind(word) {
|
|
15581
15759
|
return KINDS.includes(word);
|
|
@@ -15697,12 +15875,31 @@ var UNIT_EXAMPLES = {
|
|
|
15697
15875
|
MBps: "a bandwidth, e.g. 500 MB/s or 1 GB/s",
|
|
15698
15876
|
"usd/GB": "a price per gigabyte, e.g. 0.05 usd/GB"
|
|
15699
15877
|
};
|
|
15700
|
-
var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth or
|
|
15878
|
+
var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth, egress or timeout";
|
|
15701
15879
|
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';
|
|
15702
15880
|
var ASSERTION_VERBS = "calls, every scenario calls, never calls, never waits for, writes, responds, starts at, has scenario or handles failure of";
|
|
15703
|
-
var
|
|
15704
|
-
|
|
15705
|
-
|
|
15881
|
+
var GENERIC_TECH = {
|
|
15882
|
+
client: "Actor",
|
|
15883
|
+
edge: "WAF",
|
|
15884
|
+
cdn: "CDN",
|
|
15885
|
+
loadbalancer: "Load Balancer",
|
|
15886
|
+
gateway: "API Gateway",
|
|
15887
|
+
dns: "DNS",
|
|
15888
|
+
service: "Service",
|
|
15889
|
+
function: "Function",
|
|
15890
|
+
cache: "Cache",
|
|
15891
|
+
database: "Database",
|
|
15892
|
+
search: "Search Engine",
|
|
15893
|
+
analytics: "Data Warehouse",
|
|
15894
|
+
queue: "Message Queue",
|
|
15895
|
+
storage: "Object Storage",
|
|
15896
|
+
external: "Third Party API",
|
|
15897
|
+
other: "Note"
|
|
15898
|
+
};
|
|
15899
|
+
function unknownTechMessage(tech, kind, suggestion) {
|
|
15900
|
+
const simulated = `simulated as a generic ${kind}${suggestion === GENERIC_TECH[kind] ? "" : `, like [${GENERIC_TECH[kind]}]`}`;
|
|
15901
|
+
return suggestion ? `Unknown tech stack '${tech}'. Did you mean '${suggestion}'? Until then it is ${simulated}` : `Unknown tech stack '${tech}'; ${simulated}`;
|
|
15902
|
+
}
|
|
15706
15903
|
function parse(source2, options = {}) {
|
|
15707
15904
|
return new Parser(source2, options).run();
|
|
15708
15905
|
}
|
|
@@ -15954,8 +16151,8 @@ var Parser = class {
|
|
|
15954
16151
|
if (tokens[i]?.kind === "string") name = tokens[i++].value;
|
|
15955
16152
|
if (tokens[i]?.kind === "tech") {
|
|
15956
16153
|
const tech = tokens[i++];
|
|
15957
|
-
const resolved =
|
|
15958
|
-
if (resolved
|
|
16154
|
+
const resolved = findTech(tech.value);
|
|
16155
|
+
if (resolved?.type === "group") techStack = resolved.techStack;
|
|
15959
16156
|
else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
|
|
15960
16157
|
}
|
|
15961
16158
|
let position;
|
|
@@ -16088,6 +16285,7 @@ var Parser = class {
|
|
|
16088
16285
|
const resolved = this.resolveTech(t, line);
|
|
16089
16286
|
node.type = resolved.type;
|
|
16090
16287
|
node.techStack = resolved.techStack;
|
|
16288
|
+
if (resolved.inferredKind) node.inferredKind = resolved.inferredKind;
|
|
16091
16289
|
node.kind = resolved.type === "text" ? "text" : resolved.type === "group" ? "group" : "component";
|
|
16092
16290
|
} else if (t.kind === "team") {
|
|
16093
16291
|
node.ownerTeam = t.value;
|
|
@@ -16442,12 +16640,13 @@ var Parser = class {
|
|
|
16442
16640
|
if (rps === void 0) return;
|
|
16443
16641
|
override.rps = rps;
|
|
16444
16642
|
i++;
|
|
16445
|
-
} else if (isWord(t, "latency") || isWord(t, "availability") || isWord(t, "cost")) {
|
|
16643
|
+
} else if (isWord(t, "latency") || isWord(t, "availability") || isWord(t, "cost") || isWord(t, "timeout")) {
|
|
16446
16644
|
part = t.value;
|
|
16447
|
-
const unit = part === "latency" ? "ms" : part === "availability" ? "%" : "usd/month";
|
|
16645
|
+
const unit = part === "latency" || part === "timeout" ? "ms" : part === "availability" ? "%" : "usd/month";
|
|
16448
16646
|
const value = this.quantityOf(tokens[i + 1], unit, t, line);
|
|
16449
16647
|
if (value === void 0) return;
|
|
16450
16648
|
if (part === "latency") override.latencyMs = value;
|
|
16649
|
+
else if (part === "timeout") override.timeoutMs = value;
|
|
16451
16650
|
else if (part === "availability") override.availability = value;
|
|
16452
16651
|
else override.costUsd = value;
|
|
16453
16652
|
i += 2;
|
|
@@ -16855,7 +17054,7 @@ var Parser = class {
|
|
|
16855
17054
|
parseSelectorAtom(tokens, i, line) {
|
|
16856
17055
|
const t = tokens[i];
|
|
16857
17056
|
if (t?.kind === "tech") {
|
|
16858
|
-
const tech =
|
|
17057
|
+
const tech = findTech(t.value)?.techStack ?? t.value;
|
|
16859
17058
|
return { selector: { tech }, next: i + 1, token: t, loc: this.locOf(t, line) };
|
|
16860
17059
|
}
|
|
16861
17060
|
if (isWord(t, "any")) {
|
|
@@ -16880,7 +17079,10 @@ var Parser = class {
|
|
|
16880
17079
|
/** Warns about a selector naming a tech stack that does not exist, and (once every file is read) a node that does not. */
|
|
16881
17080
|
checkSelectorLater({ atoms }) {
|
|
16882
17081
|
for (const { selector, token, loc } of atoms) {
|
|
16883
|
-
if ("tech" in selector && !
|
|
17082
|
+
if ("tech" in selector && !findTech(token.value)) {
|
|
17083
|
+
const suggestion = suggestTech(token.value);
|
|
17084
|
+
this.warning(`Unknown tech stack '${token.value}'${suggestion ? `. Did you mean '${suggestion}'?` : ""}`, loc);
|
|
17085
|
+
}
|
|
16884
17086
|
if ("node" in selector) {
|
|
16885
17087
|
this.deferred.push(() => {
|
|
16886
17088
|
if (!this.nodes.has(selector.node)) this.warning(`Unknown node '${selector.node}'`, loc);
|
|
@@ -17040,11 +17242,27 @@ var Parser = class {
|
|
|
17040
17242
|
where(first, later) {
|
|
17041
17243
|
return first.file === later.file ? `on line ${first.line}` : `in ${fileName(first.file ?? this.options.path ?? "the root document")} on line ${first.line}`;
|
|
17042
17244
|
}
|
|
17245
|
+
/**
|
|
17246
|
+
* A node's `[Tech]`: the catalog entry it names (name or alias), or for an
|
|
17247
|
+
* unknown tech the text as written, with a warning that names the closest
|
|
17248
|
+
* catalog tech. It is drawn and simulated as the kind its name suggests
|
|
17249
|
+
* (`TigerBeetle DB`: database), else the kind of that closest tech
|
|
17250
|
+
* (`Postgress`: database), else a service; never a free, infinitely fast
|
|
17251
|
+
* client.
|
|
17252
|
+
*/
|
|
17043
17253
|
resolveTech(token, line) {
|
|
17044
|
-
const
|
|
17045
|
-
if (
|
|
17046
|
-
|
|
17047
|
-
|
|
17254
|
+
const found = findTech(token.value);
|
|
17255
|
+
if (found) return { type: found.type, techStack: found.techStack };
|
|
17256
|
+
const text = token.value.trim();
|
|
17257
|
+
if (!text) {
|
|
17258
|
+
this.warning(`Empty tech stack; drawing a ${DEFAULT_TECH}`, this.locOf(token, line));
|
|
17259
|
+
return { type: "shape", techStack: DEFAULT_TECH };
|
|
17260
|
+
}
|
|
17261
|
+
const suggestion = suggestTech(text);
|
|
17262
|
+
const closest = suggestion && findTech(suggestion);
|
|
17263
|
+
const kind = kindFromName(text) ?? (closest ? kindOf({ kind: "component", type: closest.type, techStack: closest.techStack }) : void 0) ?? "service";
|
|
17264
|
+
this.warning(unknownTechMessage(text, kind, suggestion), this.locOf(token, line));
|
|
17265
|
+
return { type: TYPE_OF_KIND[kind], techStack: text, inferredKind: kind };
|
|
17048
17266
|
}
|
|
17049
17267
|
useCaseId(name) {
|
|
17050
17268
|
const base = slug(name) || "usecase";
|
|
@@ -17230,14 +17448,14 @@ function describe(token) {
|
|
|
17230
17448
|
|
|
17231
17449
|
// ../frontend/src/dsl/format.ts
|
|
17232
17450
|
function format(source2) {
|
|
17233
|
-
const
|
|
17451
|
+
const entries2 = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
|
|
17234
17452
|
const out = [];
|
|
17235
17453
|
let run2 = [];
|
|
17236
17454
|
const flush = () => {
|
|
17237
17455
|
if (run2.length) out.push(...renderRun(run2));
|
|
17238
17456
|
run2 = [];
|
|
17239
17457
|
};
|
|
17240
|
-
tidyBlankLines(
|
|
17458
|
+
tidyBlankLines(entries2).forEach((entry) => {
|
|
17241
17459
|
const last = run2[run2.length - 1];
|
|
17242
17460
|
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))
|
|
17243
17461
|
flush();
|
|
@@ -17253,19 +17471,19 @@ function format(source2) {
|
|
|
17253
17471
|
var INDENT = " ";
|
|
17254
17472
|
var KEYWORDS = /* @__PURE__ */ new Set(["title", "import", "group", "usecase", "par", "alt"]);
|
|
17255
17473
|
function classify(lines) {
|
|
17256
|
-
const
|
|
17474
|
+
const entries2 = [];
|
|
17257
17475
|
let depth = 0;
|
|
17258
17476
|
let section;
|
|
17259
17477
|
for (let i = 0; i < lines.length; i++) {
|
|
17260
17478
|
const raw = lines[i];
|
|
17261
17479
|
const original = raw.trim();
|
|
17262
17480
|
if (!original) {
|
|
17263
|
-
|
|
17481
|
+
entries2.push({ kind: "blank", depth, text: "" });
|
|
17264
17482
|
continue;
|
|
17265
17483
|
}
|
|
17266
17484
|
const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
|
|
17267
17485
|
if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
|
|
17268
|
-
|
|
17486
|
+
entries2.push({ kind: "line", depth, text: original });
|
|
17269
17487
|
continue;
|
|
17270
17488
|
}
|
|
17271
17489
|
const comment = commentOf(raw, tokens);
|
|
@@ -17274,7 +17492,7 @@ function classify(lines) {
|
|
|
17274
17492
|
const inSection = section !== void 0 && depth === 1 && first.kind !== "rbrace";
|
|
17275
17493
|
const cells = inSection ? sectionCells(section, raw, tokens) : void 0;
|
|
17276
17494
|
if (cells) {
|
|
17277
|
-
|
|
17495
|
+
entries2.push({ ...base, kind: "cells", section, cells });
|
|
17278
17496
|
continue;
|
|
17279
17497
|
}
|
|
17280
17498
|
if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
|
|
@@ -17289,7 +17507,7 @@ function classify(lines) {
|
|
|
17289
17507
|
}
|
|
17290
17508
|
while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
|
|
17291
17509
|
}
|
|
17292
|
-
|
|
17510
|
+
entries2.push(entry);
|
|
17293
17511
|
continue;
|
|
17294
17512
|
}
|
|
17295
17513
|
const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
|
|
@@ -17297,19 +17515,19 @@ function classify(lines) {
|
|
|
17297
17515
|
let k = 1;
|
|
17298
17516
|
const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
|
|
17299
17517
|
const cells2 = [first.value, take("string"), take("tech"), take("team"), join2(raw, tokens.slice(k))];
|
|
17300
|
-
|
|
17518
|
+
entries2.push({ ...base, kind: "node", cells: cells2 });
|
|
17301
17519
|
continue;
|
|
17302
17520
|
}
|
|
17303
17521
|
const closes = first.kind === "rbrace";
|
|
17304
17522
|
const opens = tokens[tokens.length - 1].kind === "lbrace";
|
|
17305
17523
|
const code = join2(raw, tokens);
|
|
17306
17524
|
const text = withComment(code, comment);
|
|
17307
|
-
|
|
17525
|
+
entries2.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
|
|
17308
17526
|
if (depth === 0) section = sectionOpenedBy(tokens);
|
|
17309
17527
|
for (const t of tokens) depth = Math.max(0, depth + (t.kind === "lbrace" ? 1 : t.kind === "rbrace" ? -1 : 0));
|
|
17310
17528
|
if (depth === 0) section = void 0;
|
|
17311
17529
|
}
|
|
17312
|
-
return
|
|
17530
|
+
return entries2;
|
|
17313
17531
|
}
|
|
17314
17532
|
function sectionOpenedBy(tokens) {
|
|
17315
17533
|
const [first, second] = tokens;
|
|
@@ -17343,9 +17561,9 @@ function sectionCells(section, raw, tokens) {
|
|
|
17343
17561
|
return void 0;
|
|
17344
17562
|
}
|
|
17345
17563
|
}
|
|
17346
|
-
function tidyBlankLines(
|
|
17564
|
+
function tidyBlankLines(entries2) {
|
|
17347
17565
|
const kept = [];
|
|
17348
|
-
for (const entry of
|
|
17566
|
+
for (const entry of entries2) {
|
|
17349
17567
|
if (entry.kind !== "blank") {
|
|
17350
17568
|
if (entry.kind === "line" && entry.closes) while (kept[kept.length - 1]?.kind === "blank") kept.pop();
|
|
17351
17569
|
kept.push(entry);
|
|
@@ -17577,6 +17795,8 @@ function accessIn(diagram) {
|
|
|
17577
17795
|
|
|
17578
17796
|
// ../frontend/src/sim/profiles.ts
|
|
17579
17797
|
var NEUTRAL = { rps: Infinity, latencyMs: 0, availability: 1, costUsd: 0, durable: false };
|
|
17798
|
+
var INTERNET_EGRESS_USD_PER_GB = 0.09;
|
|
17799
|
+
var CDN_EGRESS_USD_PER_GB = 0.02;
|
|
17580
17800
|
var RELATIONAL = {
|
|
17581
17801
|
rps: 2e4,
|
|
17582
17802
|
readRps: 2e4,
|
|
@@ -17590,8 +17810,9 @@ var RELATIONAL = {
|
|
|
17590
17810
|
};
|
|
17591
17811
|
var KIND_PROFILES = {
|
|
17592
17812
|
client: { ...NEUTRAL, bandwidthMBps: 10 },
|
|
17813
|
+
// A firewall or accelerator in front of everything else (WAF, Global Accelerator); `any edge` also selects the four below.
|
|
17593
17814
|
edge: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
|
|
17594
|
-
cdn: { rps: 2e5, latencyMs: 5, availability: 0.9999, costUsd: 100, durable: false, egressUsdPerGb:
|
|
17815
|
+
cdn: { rps: 2e5, latencyMs: 5, availability: 0.9999, costUsd: 100, durable: false, egressUsdPerGb: CDN_EGRESS_USD_PER_GB },
|
|
17595
17816
|
loadbalancer: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
|
|
17596
17817
|
gateway: { rps: 1e4, latencyMs: 10, availability: 0.9995, costUsd: 100, durable: false },
|
|
17597
17818
|
// Name resolution happens before the request and is cached: off the request path.
|
|
@@ -17603,23 +17824,16 @@ var KIND_PROFILES = {
|
|
|
17603
17824
|
search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
|
|
17604
17825
|
analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
|
|
17605
17826
|
queue: { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" },
|
|
17606
|
-
storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual"
|
|
17827
|
+
storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual" },
|
|
17607
17828
|
external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
|
|
17608
17829
|
other: NEUTRAL
|
|
17609
17830
|
};
|
|
17610
17831
|
var NOSQL = { rps: 2e4, latencyMs: 5, availability: 0.9999, costUsd: 500, durable: true, consistency: "eventual" };
|
|
17611
17832
|
var NOSQL_STRONG = { ...NOSQL, consistency: "strong" };
|
|
17612
|
-
var
|
|
17613
|
-
|
|
17614
|
-
"
|
|
17615
|
-
|
|
17616
|
-
CouchDB: NOSQL,
|
|
17617
|
-
"Azure Cosmos DB": NOSQL,
|
|
17618
|
-
MongoDB: NOSQL_STRONG,
|
|
17619
|
-
"GCP Bigtable": NOSQL_STRONG,
|
|
17620
|
-
"GCP Firestore": NOSQL_STRONG,
|
|
17621
|
-
"GCP Spanner": NOSQL_STRONG
|
|
17622
|
-
};
|
|
17833
|
+
var PROFILE_ROWS = { nosql: NOSQL, nosqlStrong: NOSQL_STRONG };
|
|
17834
|
+
var TECH_PROFILES = Object.fromEntries(
|
|
17835
|
+
componentCatalog.flatMap((c) => "profile" in c ? [[c.techStack, PROFILE_ROWS[c.profile]]] : [])
|
|
17836
|
+
);
|
|
17623
17837
|
var BANDWIDTH = {
|
|
17624
17838
|
client: 10,
|
|
17625
17839
|
edge: 1e3,
|
|
@@ -17630,9 +17844,10 @@ var BANDWIDTH = {
|
|
|
17630
17844
|
service: 200
|
|
17631
17845
|
};
|
|
17632
17846
|
var DEFAULT_BANDWIDTH_MBPS = 100;
|
|
17847
|
+
var CHARGES_EGRESS = (kind) => kind !== "client" && kind !== "external" && kind !== "dns" && kind !== "other";
|
|
17633
17848
|
function profileOf(node, override) {
|
|
17634
17849
|
const kind = kindOf(node);
|
|
17635
|
-
const base = TECH_PROFILES[node.techStack] ?? KIND_PROFILES[kind];
|
|
17850
|
+
const base = (Object.hasOwn(TECH_PROFILES, node.techStack) ? TECH_PROFILES[node.techStack] : void 0) ?? KIND_PROFILES[kind];
|
|
17636
17851
|
const baseRead = base.readRps ?? base.rps;
|
|
17637
17852
|
const baseWrite = base.writeRps ?? base.rps;
|
|
17638
17853
|
const readRps = override?.readRps ?? override?.rps ?? baseRead;
|
|
@@ -17651,7 +17866,8 @@ function profileOf(node, override) {
|
|
|
17651
17866
|
durable: override?.durable ?? base.durable,
|
|
17652
17867
|
...consistency && isDataStore(kind) ? { consistency } : {},
|
|
17653
17868
|
bandwidthMBps: override?.bandwidthMBps ?? base.bandwidthMBps ?? BANDWIDTH[kind] ?? DEFAULT_BANDWIDTH_MBPS,
|
|
17654
|
-
egressUsdPerGb: override?.egressUsdPerGb ?? base.egressUsdPerGb ?? 0
|
|
17869
|
+
egressUsdPerGb: override?.egressUsdPerGb ?? base.egressUsdPerGb ?? (CHARGES_EGRESS(kind) ? INTERNET_EGRESS_USD_PER_GB : 0),
|
|
17870
|
+
...override?.timeoutMs !== void 0 ? { timeoutMs: override.timeoutMs } : {}
|
|
17655
17871
|
};
|
|
17656
17872
|
}
|
|
17657
17873
|
function capacityOf(p, replicas) {
|
|
@@ -17804,10 +18020,11 @@ function formatUsd(usd) {
|
|
|
17804
18020
|
var DEFAULT_TIMEOUT_MS = 1e3;
|
|
17805
18021
|
var HOT = 0.7;
|
|
17806
18022
|
var QUEUE_CAP = 0.95;
|
|
18023
|
+
var FAILOVER_SHARE = 0.1;
|
|
18024
|
+
var SERVICE_SPREAD = 0.5;
|
|
17807
18025
|
var SECONDS_PER_MONTH = 2592e3;
|
|
17808
18026
|
var BYTES_PER_MB = 1e6;
|
|
17809
18027
|
var BYTES_PER_GB = 1e9;
|
|
17810
|
-
var PERCENTILE_FACTORS = { 50: 1, 90: 1.6, 95: 2, 99: 3, 99.9: 5 };
|
|
17811
18028
|
var PERCENTILE_KEYS = { 50: "p50", 90: "p90", 95: "p95", 99: "p99", 99.9: "p999" };
|
|
17812
18029
|
var PERCENTILES2 = Object.keys(PERCENTILE_KEYS).map(Number);
|
|
17813
18030
|
function components(diagram) {
|
|
@@ -17848,7 +18065,9 @@ function forEachStepRate(diagram, traffic, visit) {
|
|
|
17848
18065
|
const { rps, shares } = traffic.get(u.id) ?? { rps: 0, shares: [] };
|
|
17849
18066
|
u.scenarios.forEach((s, i) => {
|
|
17850
18067
|
const rate = rps * (shares[i] ?? 0);
|
|
17851
|
-
if (rate > 0)
|
|
18068
|
+
if (rate > 0) {
|
|
18069
|
+
for (const step of s.steps) if (!step.failed) visit(step, rate);
|
|
18070
|
+
}
|
|
17852
18071
|
});
|
|
17853
18072
|
}
|
|
17854
18073
|
}
|
|
@@ -17862,35 +18081,76 @@ function computeLoad(diagram, traffic) {
|
|
|
17862
18081
|
});
|
|
17863
18082
|
return load;
|
|
17864
18083
|
}
|
|
17865
|
-
var
|
|
17866
|
-
|
|
18084
|
+
var payloadDirection = (step, access) => access === "read" ? { from: step.toServiceId, to: step.fromServiceId } : { from: step.fromServiceId, to: step.toServiceId };
|
|
18085
|
+
var OUTSIDE = (kind) => kind === "client" || kind === "external";
|
|
18086
|
+
function computeEgressGb(diagram, traffic, kindOf2 = (id) => profileOfId(diagram, id)) {
|
|
17867
18087
|
const gb = /* @__PURE__ */ new Map();
|
|
17868
18088
|
const access = accessIn(diagram);
|
|
17869
18089
|
forEachStepRate(diagram, traffic, (step, rate) => {
|
|
17870
18090
|
if (!step.sizeBytes || step.sizeBytes <= 0) return;
|
|
17871
|
-
const
|
|
17872
|
-
|
|
18091
|
+
const { from, to } = payloadDirection(step, access(step));
|
|
18092
|
+
if (OUTSIDE(kindOf2(from)) || !OUTSIDE(kindOf2(to))) return;
|
|
18093
|
+
gb.set(from, (gb.get(from) ?? 0) + rate * multiplierOf(step) * step.sizeBytes * SECONDS_PER_MONTH / BYTES_PER_GB);
|
|
17873
18094
|
});
|
|
17874
18095
|
return gb;
|
|
17875
18096
|
}
|
|
18097
|
+
var BANDWIDTH_BOUND = (kind) => !["client", "external", "dns", "other", "storage", "cdn"].includes(kind);
|
|
18098
|
+
function computeBytesPerSecond(diagram, traffic) {
|
|
18099
|
+
const bytes = /* @__PURE__ */ new Map();
|
|
18100
|
+
forEachStepRate(diagram, traffic, (step, rate) => {
|
|
18101
|
+
if (!step.sizeBytes || step.sizeBytes <= 0) return;
|
|
18102
|
+
const b = rate * multiplierOf(step) * step.sizeBytes;
|
|
18103
|
+
for (const id of /* @__PURE__ */ new Set([step.fromServiceId, step.toServiceId])) bytes.set(id, (bytes.get(id) ?? 0) + b);
|
|
18104
|
+
});
|
|
18105
|
+
return bytes;
|
|
18106
|
+
}
|
|
18107
|
+
function profileOfId(diagram, id) {
|
|
18108
|
+
const node = diagram.nodes.find((n) => n.id === id);
|
|
18109
|
+
return node ? profileOf(node).kind : "client";
|
|
18110
|
+
}
|
|
17876
18111
|
function transferMs(step, bandwidthOf) {
|
|
17877
18112
|
if (!step.sizeBytes || step.sizeBytes <= 0) return 0;
|
|
17878
18113
|
const mbps = Math.min(bandwidthOf(step.fromServiceId), bandwidthOf(step.toServiceId));
|
|
17879
|
-
return mbps > 0 && Number.isFinite(mbps) ? step.sizeBytes / BYTES_PER_MB / mbps * 1e3 : 0;
|
|
18114
|
+
return mbps > 0 && Number.isFinite(mbps) ? multiplierOf(step) * step.sizeBytes / BYTES_PER_MB / mbps * 1e3 : 0;
|
|
17880
18115
|
}
|
|
17881
18116
|
var utilizationOf = (load, capacity) => Number.isFinite(capacity) && capacity > 0 ? load / capacity : 0;
|
|
17882
|
-
|
|
18117
|
+
function erlangC(servers, rho) {
|
|
18118
|
+
if (rho <= 0) return 0;
|
|
18119
|
+
if (rho >= 1) return 1;
|
|
18120
|
+
const c = Math.max(1, Math.round(servers));
|
|
18121
|
+
const a = c * rho;
|
|
18122
|
+
let b = 1;
|
|
18123
|
+
for (let k = 1; k <= c; k++) b = a * b / (k + a * b);
|
|
18124
|
+
return b / (1 - rho * (1 - b));
|
|
18125
|
+
}
|
|
18126
|
+
function hopModel(baseMs, rho, servers = 1) {
|
|
18127
|
+
const r = Math.min(Math.max(rho, 0), QUEUE_CAP);
|
|
18128
|
+
const c = Math.max(1, Math.round(servers));
|
|
18129
|
+
const waitProbability = erlangC(c, r);
|
|
18130
|
+
const meanMs = baseMs + waitProbability * baseMs / (c * (1 - r));
|
|
18131
|
+
const fixedMs = (1 - SERVICE_SPREAD) * baseMs;
|
|
18132
|
+
return { fixedMs, tailMs: meanMs - fixedMs, waitProbability };
|
|
18133
|
+
}
|
|
18134
|
+
var hopMean = (h) => h.fixedMs + h.tailMs;
|
|
18135
|
+
var tailFactor = (p) => -Math.log(1 - p);
|
|
17883
18136
|
var replicatedAvailability = (a, replicas) => 1 - (1 - a) ** replicas;
|
|
17884
|
-
|
|
18137
|
+
var primaryAvailability = (a, replicas) => replicas >= 2 ? 1 - (1 - a) * FAILOVER_SHARE : a;
|
|
18138
|
+
function usageOf(p, replicas, readLoadRps, writeLoadRps, bytesPerSecond = 0) {
|
|
17885
18139
|
const { readRps: readCapacityRps, writeRps: writeCapacityRps } = capacityOf(p, replicas);
|
|
17886
18140
|
const readUtilization = utilizationOf(readLoadRps, readCapacityRps);
|
|
17887
18141
|
const writeUtilization = utilizationOf(writeLoadRps, writeCapacityRps);
|
|
17888
|
-
const
|
|
18142
|
+
const requestUtilization = combinedUtilization(p, readUtilization, writeUtilization);
|
|
17889
18143
|
const loadRps = readLoadRps + writeLoadRps;
|
|
18144
|
+
const bound = BANDWIDTH_BOUND(p.kind);
|
|
18145
|
+
const bandwidthLoadMBps = bound ? bytesPerSecond / BYTES_PER_MB : 0;
|
|
18146
|
+
const bandwidthCapacityMBps = bound ? p.bandwidthMBps * replicas * p.shards : 0;
|
|
18147
|
+
const bandwidthUtilization = utilizationOf(bandwidthLoadMBps, bandwidthCapacityMBps);
|
|
18148
|
+
const utilization = Math.max(requestUtilization, bandwidthUtilization);
|
|
17890
18149
|
return {
|
|
17891
18150
|
loadRps,
|
|
17892
|
-
capacityRps: readCapacityRps === writeCapacityRps ||
|
|
18151
|
+
capacityRps: readCapacityRps === writeCapacityRps || requestUtilization === 0 ? readCapacityRps : loadRps / requestUtilization,
|
|
17893
18152
|
utilization,
|
|
18153
|
+
requestUtilization,
|
|
17894
18154
|
saturated: utilization >= 1,
|
|
17895
18155
|
readLoadRps,
|
|
17896
18156
|
writeLoadRps,
|
|
@@ -17898,27 +18158,68 @@ function usageOf(p, replicas, readLoadRps, writeLoadRps) {
|
|
|
17898
18158
|
writeCapacityRps,
|
|
17899
18159
|
readUtilization,
|
|
17900
18160
|
writeUtilization,
|
|
18161
|
+
bandwidthLoadMBps,
|
|
18162
|
+
bandwidthCapacityMBps,
|
|
18163
|
+
bandwidthUtilization,
|
|
17901
18164
|
writeScaling: p.writeScaling
|
|
17902
18165
|
};
|
|
17903
18166
|
}
|
|
18167
|
+
var bandwidthBound = (n) => n.bandwidthUtilization > n.requestUtilization;
|
|
18168
|
+
var serversOf = (n, replicas) => writeBound(n) ? 1 : replicas;
|
|
17904
18169
|
function usageText(n) {
|
|
17905
18170
|
const part = (load, capacity, rho) => `${formatRps(load)} of ${formatRps(capacity)}, ${formatPercent(rho)}`;
|
|
18171
|
+
if (bandwidthBound(n)) {
|
|
18172
|
+
const mb = (x) => `${Number(x.toPrecision(3)).toLocaleString("en-US")} MB/s`;
|
|
18173
|
+
return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
|
|
18174
|
+
}
|
|
17906
18175
|
if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
|
|
17907
18176
|
return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
17908
18177
|
}
|
|
17909
|
-
return part(n.loadRps, n.capacityRps, n.
|
|
18178
|
+
return part(n.loadRps, n.capacityRps, n.requestUtilization);
|
|
17910
18179
|
}
|
|
17911
18180
|
var writeBound = (n) => n.writeScaling === "shards" && n.writeUtilization >= n.readUtilization && n.writeLoadRps > 0;
|
|
17912
|
-
function
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
let
|
|
17917
|
-
for (const
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
18181
|
+
function pathMean(path) {
|
|
18182
|
+
return path.parts.reduce((sum, part) => sum + Math.max(0, ...part.map((i) => i.fixedMs + i.tailMs)), 0);
|
|
18183
|
+
}
|
|
18184
|
+
function pathAt(path, l) {
|
|
18185
|
+
let sum = 0;
|
|
18186
|
+
for (const part of path.parts) {
|
|
18187
|
+
let most = 0;
|
|
18188
|
+
for (const i of part) most = Math.max(most, i.fixedMs + i.tailMs * l);
|
|
18189
|
+
sum += most;
|
|
18190
|
+
}
|
|
18191
|
+
return sum;
|
|
18192
|
+
}
|
|
18193
|
+
var pathQuantile = (path, p) => pathAt(path, tailFactor(p));
|
|
18194
|
+
function pathCdf(path, t) {
|
|
18195
|
+
if (pathAt(path, 0) > t) return 0;
|
|
18196
|
+
let hi = 1;
|
|
18197
|
+
while (pathAt(path, hi) <= t) {
|
|
18198
|
+
hi *= 2;
|
|
18199
|
+
if (hi > 1e6) return 1;
|
|
18200
|
+
}
|
|
18201
|
+
let lo = 0;
|
|
18202
|
+
for (let i = 0; i < 50; i++) {
|
|
18203
|
+
const mid = (lo + hi) / 2;
|
|
18204
|
+
if (pathAt(path, mid) <= t) lo = mid;
|
|
18205
|
+
else hi = mid;
|
|
18206
|
+
}
|
|
18207
|
+
return 1 - Math.exp(-lo);
|
|
18208
|
+
}
|
|
18209
|
+
function mixtureQuantile(paths, q) {
|
|
18210
|
+
const live = paths.filter((s) => s.share > 0);
|
|
18211
|
+
if (live.length === 0) return 0;
|
|
18212
|
+
if (live.length === 1) return pathQuantile(live[0].path, q);
|
|
18213
|
+
const total = live.reduce((sum, s) => sum + s.share, 0);
|
|
18214
|
+
const cdf = (t) => live.reduce((sum, s) => sum + s.share * pathCdf(s.path, t), 0) / total;
|
|
18215
|
+
let lo = Math.min(...live.map((s) => pathQuantile(s.path, 0)));
|
|
18216
|
+
let hi = Math.max(...live.map((s) => pathQuantile(s.path, q)));
|
|
18217
|
+
for (let i = 0; i < 60 && hi - lo > 1e-9 * Math.max(1, hi); i++) {
|
|
18218
|
+
const mid = (lo + hi) / 2;
|
|
18219
|
+
if (cdf(mid) >= q) hi = mid;
|
|
18220
|
+
else lo = mid;
|
|
18221
|
+
}
|
|
18222
|
+
return hi;
|
|
17922
18223
|
}
|
|
17923
18224
|
function mainScenarioIndex(shares) {
|
|
17924
18225
|
let best = 0;
|
|
@@ -17935,20 +18236,27 @@ function profilesOf(diagram, warnings = []) {
|
|
|
17935
18236
|
return new Map(nodes.map((n) => [n.id, profileOf(n, overrides.get(n.id))]));
|
|
17936
18237
|
}
|
|
17937
18238
|
function analyze(diagram, options = {}) {
|
|
17938
|
-
const
|
|
18239
|
+
const defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17939
18240
|
const warnings = [];
|
|
17940
18241
|
const nodes = components(diagram);
|
|
17941
18242
|
const profiles = profilesOf(diagram, warnings);
|
|
18243
|
+
const kindOf2 = (id) => profiles.get(id)?.kind ?? "client";
|
|
17942
18244
|
const traffic = resolveTraffic(diagram, warnings);
|
|
17943
18245
|
const load = computeLoad(diagram, traffic);
|
|
17944
|
-
const egress = computeEgressGb(diagram, traffic);
|
|
18246
|
+
const egress = computeEgressGb(diagram, traffic, kindOf2);
|
|
18247
|
+
const bytes = computeBytesPerSecond(diagram, traffic);
|
|
18248
|
+
const hops = /* @__PURE__ */ new Map();
|
|
17945
18249
|
const nodeResults = nodes.map((n) => {
|
|
17946
18250
|
const p = profiles.get(n.id);
|
|
17947
|
-
const replicas = replicasOf(n);
|
|
18251
|
+
const replicas = Math.max(1, options.replicas?.get(n.id) ?? replicasOf(n));
|
|
17948
18252
|
const { read, write } = load.get(n.id) ?? { read: 0, write: 0 };
|
|
17949
|
-
const usage = usageOf(p, replicas, read, write);
|
|
18253
|
+
const usage = usageOf(p, replicas, read, write, bytes.get(n.id) ?? 0);
|
|
18254
|
+
const servers = serversOf(usage, replicas);
|
|
18255
|
+
const hop = hopModel(p.latencyMs, usage.utilization, servers);
|
|
18256
|
+
hops.set(n.id, hop);
|
|
17950
18257
|
const egressGbPerMonth = egress.get(n.id) ?? 0;
|
|
17951
18258
|
const egressUsd = egressGbPerMonth * p.egressUsdPerGb;
|
|
18259
|
+
const availability2 = replicatedAvailability(p.availability, replicas);
|
|
17952
18260
|
return {
|
|
17953
18261
|
id: n.id,
|
|
17954
18262
|
kind: p.kind,
|
|
@@ -17956,8 +18264,11 @@ function analyze(diagram, options = {}) {
|
|
|
17956
18264
|
shards: p.shards,
|
|
17957
18265
|
...usage,
|
|
17958
18266
|
...p.consistency ? { consistency: p.consistency } : {},
|
|
17959
|
-
|
|
17960
|
-
|
|
18267
|
+
servers,
|
|
18268
|
+
waitProbability: hop.waitProbability,
|
|
18269
|
+
latencyMs: hopMean(hop),
|
|
18270
|
+
availability: availability2,
|
|
18271
|
+
writeAvailability: p.writeScaling === "shards" ? primaryAvailability(p.availability, replicas) : availability2,
|
|
17961
18272
|
costUsd: replicas * p.shards * p.costUsd + egressUsd,
|
|
17962
18273
|
egressGbPerMonth,
|
|
17963
18274
|
egressUsd,
|
|
@@ -17965,28 +18276,49 @@ function analyze(diagram, options = {}) {
|
|
|
17965
18276
|
};
|
|
17966
18277
|
});
|
|
17967
18278
|
const nodeById = new Map(nodeResults.map((n) => [n.id, n]));
|
|
17968
|
-
const hopMs = (id) => nodeById.get(id)?.latencyMs ?? 0;
|
|
17969
|
-
const availabilityOf = (id) => nodeById.get(id)?.availability ?? 1;
|
|
17970
18279
|
const bandwidthOf = (id) => profiles.get(id)?.bandwidthMBps ?? Infinity;
|
|
18280
|
+
const timeoutOf = (id) => profiles.get(id)?.timeoutMs ?? defaultTimeoutMs;
|
|
17971
18281
|
for (const n of nodeResults) {
|
|
17972
18282
|
const usage = usageText(n);
|
|
17973
18283
|
if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
|
|
17974
18284
|
else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
|
|
17975
18285
|
}
|
|
18286
|
+
const access = accessIn(diagram);
|
|
18287
|
+
const pathOf = (hopsOfPath) => ({
|
|
18288
|
+
parts: hopsOfPath.map(
|
|
18289
|
+
(part) => part.map((h) => {
|
|
18290
|
+
if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
|
|
18291
|
+
const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
18292
|
+
return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
|
|
18293
|
+
})
|
|
18294
|
+
)
|
|
18295
|
+
});
|
|
17976
18296
|
const useCases = diagram.useCases.map((u) => {
|
|
17977
18297
|
const { rps, shares } = traffic.get(u.id);
|
|
17978
|
-
const
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
18298
|
+
const paths = u.scenarios.map((s) => pathOf(criticalPath(u, s)));
|
|
18299
|
+
const scenarios = u.scenarios.map((s, i) => ({
|
|
18300
|
+
id: s.id,
|
|
18301
|
+
name: s.name,
|
|
18302
|
+
share: shares[i] ?? 0,
|
|
18303
|
+
meanMs: pathMean(paths[i]),
|
|
18304
|
+
percentiles: Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], pathQuantile(paths[i], q / 100)]))
|
|
18305
|
+
}));
|
|
18306
|
+
const weighted = paths.map((path, i) => ({ share: shares[i] ?? 0, path }));
|
|
18307
|
+
const percentiles = Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], mixtureQuantile(weighted, q / 100)]));
|
|
18308
|
+
const tailScenario = Object.fromEntries(
|
|
18309
|
+
PERCENTILES2.map((q) => {
|
|
18310
|
+
const t = percentiles[PERCENTILE_KEYS[q]];
|
|
18311
|
+
const above = weighted.map((s) => s.share > 0 ? s.share * (1 - pathCdf(s.path, t * (1 - 1e-9))) : -1);
|
|
18312
|
+
return [PERCENTILE_KEYS[q], Math.max(0, above.indexOf(Math.max(...above)))];
|
|
18313
|
+
})
|
|
17987
18314
|
);
|
|
17988
18315
|
if (rps > 0 && u.scenarios.length === 0) warnings.push(`"${u.name}" has traffic but no steps`);
|
|
17989
|
-
|
|
18316
|
+
const writes = mainWrites(u, shares, (step) => access(step) === "write");
|
|
18317
|
+
const availabilityOf = (id) => {
|
|
18318
|
+
const n = nodeById.get(id);
|
|
18319
|
+
return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
|
|
18320
|
+
};
|
|
18321
|
+
return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
|
|
17990
18322
|
});
|
|
17991
18323
|
return {
|
|
17992
18324
|
nodes: nodeResults,
|
|
@@ -17997,8 +18329,10 @@ function analyze(diagram, options = {}) {
|
|
|
17997
18329
|
warnings
|
|
17998
18330
|
};
|
|
17999
18331
|
}
|
|
18000
|
-
function
|
|
18001
|
-
|
|
18332
|
+
function mainWrites(useCase, shares, isWrite) {
|
|
18333
|
+
if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
|
|
18334
|
+
const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
|
|
18335
|
+
return new Set(path.filter((h) => !h.failed && !h.async && isWrite(h.step)).map((h) => h.target));
|
|
18002
18336
|
}
|
|
18003
18337
|
function useCaseAvailability(useCase, shares, availabilityOf) {
|
|
18004
18338
|
if (useCase.scenarios.length === 0) return 1;
|
|
@@ -18025,9 +18359,9 @@ function singlePointsOfFailure(diagram) {
|
|
|
18025
18359
|
// ../frontend/src/sim/tests.ts
|
|
18026
18360
|
var pass = (message) => ({ passed: true, message });
|
|
18027
18361
|
var fail = (message, hint) => ({ passed: false, message, hint });
|
|
18028
|
-
function
|
|
18362
|
+
function makeRun(diagram, analysis) {
|
|
18029
18363
|
const access = accessIn(diagram);
|
|
18030
|
-
|
|
18364
|
+
return {
|
|
18031
18365
|
diagram,
|
|
18032
18366
|
analysis,
|
|
18033
18367
|
nodes: new Map(analysis.nodes.map((n) => [n.id, n])),
|
|
@@ -18036,6 +18370,9 @@ function runTests(diagram, analysis = analyze(diagram)) {
|
|
|
18036
18370
|
profiles: profilesOf(diagram),
|
|
18037
18371
|
isWrite: (step) => access(step) === "write"
|
|
18038
18372
|
};
|
|
18373
|
+
}
|
|
18374
|
+
function runTests(diagram, analysis = analyze(diagram)) {
|
|
18375
|
+
const run2 = makeRun(diagram, analysis);
|
|
18039
18376
|
const results = (diagram.requirements ?? []).map((r, i) => {
|
|
18040
18377
|
const { passed, message, hint } = requirement(run2, r);
|
|
18041
18378
|
return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
|
|
@@ -18127,7 +18464,7 @@ function latency(run2, u, q, maxMs) {
|
|
|
18127
18464
|
const value = `${key} of ${u.name} is ${formatMs(ms)}${result.rps > 0 ? "" : " with no traffic"}`;
|
|
18128
18465
|
const message = `${value} (limit ${formatMs(maxMs)})`;
|
|
18129
18466
|
if (ms < maxMs) return { ...pass(message), value };
|
|
18130
|
-
const index = Math.
|
|
18467
|
+
const index = Math.min(result.tailScenario[key] ?? 0, u.scenarios.length - 1);
|
|
18131
18468
|
const lead = u.scenarios.length > 1 ? `The "${u.scenarios[index].name}" path (${formatPercent(result.scenarios[index].share)} of traffic) sets ${key}. ` : "";
|
|
18132
18469
|
return { ...fail(message, lead + latencyHint(run2, u, u.scenarios[index])), value };
|
|
18133
18470
|
}
|
|
@@ -18146,6 +18483,10 @@ function scenarioLatency(run2, useCase, scenario, q, maxMs) {
|
|
|
18146
18483
|
}
|
|
18147
18484
|
function saturationHint(run2, n) {
|
|
18148
18485
|
const label = nodeLabel(run2.diagram, n.id);
|
|
18486
|
+
if (bandwidthBound(n)) {
|
|
18487
|
+
const replicas2 = Math.ceil(n.bandwidthUtilization * n.replicas / HOT);
|
|
18488
|
+
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)}`;
|
|
18489
|
+
}
|
|
18149
18490
|
if (writeBound(n)) {
|
|
18150
18491
|
const perShard = n.writeCapacityRps / n.shards;
|
|
18151
18492
|
const shards = Math.ceil(n.writeLoadRps / (perShard * HOT));
|
|
@@ -18157,11 +18498,12 @@ function saturationHint(run2, n) {
|
|
|
18157
18498
|
}
|
|
18158
18499
|
function latencyHint(run2, u, scenario) {
|
|
18159
18500
|
const hops = criticalPath(u, scenario).flat();
|
|
18160
|
-
const
|
|
18501
|
+
const timeout = (id) => run2.profiles.get(id)?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
18502
|
+
const cost2 = (h) => h.failed ? timeout(h.target) : run2.nodes.get(h.target)?.latencyMs ?? 0;
|
|
18161
18503
|
const slowest = hops.reduce((best, h) => !best || cost2(h) > cost2(best) ? h : best, void 0);
|
|
18162
18504
|
if (!slowest) return "Shorten the synchronous path of the request.";
|
|
18163
18505
|
const label = nodeLabel(run2.diagram, slowest.target);
|
|
18164
|
-
if (slowest.failed) return `It waits ${formatMs(
|
|
18506
|
+
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 }).`;
|
|
18165
18507
|
const sized = hops.find((h) => (h.step.sizeBytes ?? 0) > 0);
|
|
18166
18508
|
const node = run2.nodes.get(slowest.target);
|
|
18167
18509
|
if (node && node.utilization > HOT) return `${label} runs at ${formatPercent(node.utilization)} and queues requests; add replicas to it.`;
|
|
@@ -18175,8 +18517,11 @@ function availability(run2, u, minPercent) {
|
|
|
18175
18517
|
if (result.availability * 100 >= minPercent - 1e-9) return { ...pass(message), value };
|
|
18176
18518
|
const shares = run2.shares.get(u.id) ?? [];
|
|
18177
18519
|
const main = u.scenarios.length ? pathNodes(u, u.scenarios[mainScenarioIndex(shares)]) : [];
|
|
18178
|
-
const
|
|
18179
|
-
const
|
|
18520
|
+
const writes = mainWrites(u, shares, run2.isWrite);
|
|
18521
|
+
const effective = (n) => writes.has(n.id) ? n.writeAvailability : n.availability;
|
|
18522
|
+
const weakest = main.filter((id) => fallbacksFor(u, id).length === 0).map((id) => run2.nodes.get(id)).filter((n) => !!n && effective(n) < 1).sort((a, b) => effective(a) - effective(b))[0];
|
|
18523
|
+
const primaryBound = weakest && writes.has(weakest.id) && weakest.writeAvailability < weakest.availability;
|
|
18524
|
+
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(run2.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(run2.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`;
|
|
18180
18525
|
return { ...fail(message, hint), value };
|
|
18181
18526
|
}
|
|
18182
18527
|
function writeGap(run2, u, s, accept) {
|
|
@@ -18234,20 +18579,34 @@ function survive(run2, target) {
|
|
|
18234
18579
|
if (selected.length === 0) {
|
|
18235
18580
|
return target === "any" ? pass("No nodes to lose") : fail(`No node matches ${what}`, "Use a node id, [Tech] or any <kind>");
|
|
18236
18581
|
}
|
|
18582
|
+
const latencies = (run2.diagram.requirements ?? []).filter((r) => r.kind === "latency" && requirement(run2, r).passed);
|
|
18237
18583
|
const failures = [];
|
|
18238
18584
|
for (const node of selected) {
|
|
18239
18585
|
const n = run2.nodes.get(node.id);
|
|
18240
18586
|
const label = nodeLabel(run2.diagram, node.id);
|
|
18241
18587
|
const replicas = replicasOf(node);
|
|
18242
18588
|
if (replicas >= 2) {
|
|
18243
|
-
const
|
|
18589
|
+
const degraded = makeRun(run2.diagram, analyze(run2.diagram, { replicas: /* @__PURE__ */ new Map([[node.id, replicas - 1]]) }));
|
|
18590
|
+
const left = degraded.nodes.get(node.id);
|
|
18591
|
+
const lost = n.shards > 1 ? `one of the ${replicas} replicas of a ${node.id} shard` : `one of ${replicas} ${node.id} replicas`;
|
|
18244
18592
|
if (left.saturated) {
|
|
18593
|
+
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)}`;
|
|
18245
18594
|
failures.push(
|
|
18246
18595
|
fail(
|
|
18247
|
-
`Losing
|
|
18596
|
+
`Losing ${lost} ${leaves}${bandwidthBound(left) ? "" : ` (${formatPercent(left.utilization)})`}`,
|
|
18248
18597
|
writeBound(left) ? saturationHint(run2, { ...n, ...left }) : `Add a replica to ${label} (x${replicas + 1}) so the rest carry the load`
|
|
18249
18598
|
)
|
|
18250
18599
|
);
|
|
18600
|
+
continue;
|
|
18601
|
+
}
|
|
18602
|
+
const broken = latencies.map((r) => requirement(degraded, r)).find((c) => !c.passed);
|
|
18603
|
+
if (broken) {
|
|
18604
|
+
failures.push(
|
|
18605
|
+
fail(
|
|
18606
|
+
`Losing ${lost} breaks a latency limit: ${broken.message}`,
|
|
18607
|
+
`Add a replica to ${label} (x${replicas + 1}) so the rest keep it under ${formatPercent(HOT)}, or take load off it`
|
|
18608
|
+
)
|
|
18609
|
+
);
|
|
18251
18610
|
}
|
|
18252
18611
|
continue;
|
|
18253
18612
|
}
|
|
@@ -18509,16 +18868,16 @@ function startsAt(run2, a) {
|
|
|
18509
18868
|
const ids = idsOf(run2, a.target);
|
|
18510
18869
|
const what = selectorText(a.target);
|
|
18511
18870
|
if (ids.size === 0) return noMatch(a.target);
|
|
18512
|
-
const
|
|
18513
|
-
if (
|
|
18514
|
-
const wrong =
|
|
18871
|
+
const entries2 = u.scenarios.map((s) => s.steps[0]).filter((s) => !!s);
|
|
18872
|
+
if (entries2.length === 0) return fail(`${quote(u.name)} has no steps`, `Start "${u.name}" with a step sent by ${what}`);
|
|
18873
|
+
const wrong = entries2.find((s) => !ids.has(s.fromServiceId));
|
|
18515
18874
|
if (wrong) {
|
|
18516
18875
|
return fail(
|
|
18517
18876
|
`${quote(u.name)} starts at ${nodeLabel(run2.diagram, wrong.fromServiceId)}, not ${what}: ${stepText(wrong)}`,
|
|
18518
18877
|
`Make the first step of "${u.name}" a step sent by ${what}`
|
|
18519
18878
|
);
|
|
18520
18879
|
}
|
|
18521
|
-
return pass(`${quote(u.name)} starts at ${nodeLabel(run2.diagram,
|
|
18880
|
+
return pass(`${quote(u.name)} starts at ${nodeLabel(run2.diagram, entries2[0].fromServiceId)}`);
|
|
18522
18881
|
}
|
|
18523
18882
|
function statusMatches(status, wanted) {
|
|
18524
18883
|
if (status === void 0) return false;
|
|
@@ -18926,9 +19285,12 @@ var PROBLEM_MD = "problem.md";
|
|
|
18926
19285
|
var GIVEN = "given.proschi";
|
|
18927
19286
|
var STARTER = "starter.proschi";
|
|
18928
19287
|
var SOLUTION = "solution.proschi";
|
|
19288
|
+
var LESSON_MD = "lesson.md";
|
|
18929
19289
|
var WRONG_DIR = "wrong";
|
|
18930
19290
|
var REQUIRED = [PROBLEM_MD, GIVEN, STARTER, SOLUTION];
|
|
19291
|
+
var OPTIONAL = [LESSON_MD];
|
|
18931
19292
|
var ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
19293
|
+
var RESERVED_IDS = ["problem", "roadmap", "approach"];
|
|
18932
19294
|
var EXPECT_FAIL = /^#\s*expect-fail:\s*(.*?)\s*$/;
|
|
18933
19295
|
var ProblemFolderError = class extends Error {
|
|
18934
19296
|
folder;
|
|
@@ -18954,7 +19316,7 @@ function expectFailLines(source2) {
|
|
|
18954
19316
|
}
|
|
18955
19317
|
return names;
|
|
18956
19318
|
}
|
|
18957
|
-
var FIELDS = ["title", "summary", "difficulty", "tags", "order", "hints"];
|
|
19319
|
+
var FIELDS = ["title", "summary", "difficulty", "tags", "company", "order", "hints", "version"];
|
|
18958
19320
|
function readProblemMd(id, text) {
|
|
18959
19321
|
const fail2 = (message, line) => {
|
|
18960
19322
|
throw new ProblemFolderError(id, message, PROBLEM_MD, line);
|
|
@@ -18980,17 +19342,22 @@ function readProblemMd(id, text) {
|
|
|
18980
19342
|
};
|
|
18981
19343
|
const difficulty = str("difficulty");
|
|
18982
19344
|
if (!DIFFICULTIES.includes(difficulty)) fail2(`'difficulty' must be one of ${DIFFICULTIES.join(", ")}`);
|
|
19345
|
+
const company = data.company === void 0 ? void 0 : str("company");
|
|
18983
19346
|
const order = data.order;
|
|
18984
19347
|
if (order !== void 0 && typeof order !== "number") fail2("'order' must be a number");
|
|
19348
|
+
const version = data.version;
|
|
19349
|
+
if (version !== void 0 && !(typeof version === "number" && Number.isInteger(version) && version >= 1)) fail2("'version' must be a whole number from 1");
|
|
18985
19350
|
if (body === "") fail2("The statement (the Markdown after the front matter) is empty");
|
|
18986
19351
|
return {
|
|
18987
19352
|
title: str("title"),
|
|
18988
19353
|
summary: str("summary"),
|
|
18989
19354
|
difficulty,
|
|
18990
19355
|
tags: list("tags"),
|
|
19356
|
+
...company !== void 0 ? { company } : {},
|
|
18991
19357
|
...typeof order === "number" ? { order } : {},
|
|
18992
19358
|
statement: body,
|
|
18993
|
-
hints: list("hints")
|
|
19359
|
+
hints: list("hints"),
|
|
19360
|
+
...typeof version === "number" ? { version } : {}
|
|
18994
19361
|
};
|
|
18995
19362
|
}
|
|
18996
19363
|
function problemFromFiles(id, files) {
|
|
@@ -18998,11 +19365,12 @@ function problemFromFiles(id, files) {
|
|
|
18998
19365
|
throw new ProblemFolderError(id, message);
|
|
18999
19366
|
};
|
|
19000
19367
|
if (!ID.test(id)) fail2('The folder name is the problem id and must be lowercase words joined by "-", e.g. url-shortener');
|
|
19368
|
+
if (RESERVED_IDS.includes(id)) fail2(`"${id}" is taken by a practice page; reserved ids: ${RESERVED_IDS.join(", ")}`);
|
|
19001
19369
|
for (const name of REQUIRED) if (files[name] === void 0) fail2(`Missing ${name}`);
|
|
19002
19370
|
for (const name of Object.keys(files)) {
|
|
19003
19371
|
const wrong2 = name.startsWith(`${WRONG_DIR}/`) ? name.slice(WRONG_DIR.length + 1) : void 0;
|
|
19004
|
-
if (!REQUIRED.includes(name) && !(wrong2 && /^[a-z0-9][a-z0-9-]*\.proschi$/.test(wrong2))) {
|
|
19005
|
-
fail2(`Unexpected file ${name}: a problem folder holds ${REQUIRED.join(", ")} and ${WRONG_DIR}/<name>.proschi (lowercase, digits and "-")`);
|
|
19372
|
+
if (!REQUIRED.includes(name) && !OPTIONAL.includes(name) && !(wrong2 && /^[a-z0-9][a-z0-9-]*\.proschi$/.test(wrong2))) {
|
|
19373
|
+
fail2(`Unexpected file ${name}: a problem folder holds ${REQUIRED.join(", ")}, optionally ${OPTIONAL.join(", ")}, and ${WRONG_DIR}/<name>.proschi (lowercase, digits and "-")`);
|
|
19006
19374
|
}
|
|
19007
19375
|
}
|
|
19008
19376
|
const meta = readProblemMd(id, files[PROBLEM_MD]);
|
|
@@ -19013,10 +19381,63 @@ function problemFromFiles(id, files) {
|
|
|
19013
19381
|
given: files[GIVEN],
|
|
19014
19382
|
starter: files[STARTER],
|
|
19015
19383
|
solution: files[SOLUTION],
|
|
19384
|
+
...files[LESSON_MD] !== void 0 ? { lesson: files[LESSON_MD] } : {},
|
|
19016
19385
|
...wrong.length ? { wrong } : {}
|
|
19017
19386
|
};
|
|
19018
19387
|
}
|
|
19019
19388
|
|
|
19389
|
+
// ../frontend/src/practice/lesson.ts
|
|
19390
|
+
var LESSON_HEADINGS = [
|
|
19391
|
+
"What you'll learn",
|
|
19392
|
+
"The problem, explained",
|
|
19393
|
+
"Back-of-the-envelope",
|
|
19394
|
+
"Concepts",
|
|
19395
|
+
"Designing it step by step",
|
|
19396
|
+
"Common mistakes",
|
|
19397
|
+
"In the interview",
|
|
19398
|
+
"Further reading"
|
|
19399
|
+
];
|
|
19400
|
+
var normalize = (heading) => heading.replace(/[‘’]/g, "'").trim();
|
|
19401
|
+
function proseLines(source2) {
|
|
19402
|
+
const out = [];
|
|
19403
|
+
let fence = false;
|
|
19404
|
+
source2.replace(/\r\n?/g, "\n").split("\n").forEach((text, i) => {
|
|
19405
|
+
if (/^\s*```/.test(text)) fence = !fence;
|
|
19406
|
+
else if (!fence) out.push({ text, line: i + 1 });
|
|
19407
|
+
});
|
|
19408
|
+
return out;
|
|
19409
|
+
}
|
|
19410
|
+
function h2Headings(source2) {
|
|
19411
|
+
return proseLines(source2).map(({ text, line }) => ({ m: /^##\s+(.*?)\s*#*\s*$/.exec(text), line })).filter((h) => h.m !== null).map(({ m, line }) => ({ text: m[1], line }));
|
|
19412
|
+
}
|
|
19413
|
+
function lessonIssues(source2) {
|
|
19414
|
+
const issues = [];
|
|
19415
|
+
if (!source2.trim()) return [{ message: "The lesson is empty" }];
|
|
19416
|
+
const headings = h2Headings(source2);
|
|
19417
|
+
let last = -1;
|
|
19418
|
+
let lastName = "";
|
|
19419
|
+
for (const required of LESSON_HEADINGS) {
|
|
19420
|
+
const found = headings.find((h) => normalize(h.text) === required);
|
|
19421
|
+
if (!found) {
|
|
19422
|
+
issues.push({ message: `The lesson needs a "## ${required}" section (the sections are: ${LESSON_HEADINGS.join(", ")})` });
|
|
19423
|
+
continue;
|
|
19424
|
+
}
|
|
19425
|
+
const index = headings.indexOf(found);
|
|
19426
|
+
if (index < last) issues.push({ message: `"## ${required}" must come after "## ${lastName}"`, line: found.line });
|
|
19427
|
+
else {
|
|
19428
|
+
last = index;
|
|
19429
|
+
lastName = required;
|
|
19430
|
+
}
|
|
19431
|
+
}
|
|
19432
|
+
for (const { text, line } of proseLines(source2)) {
|
|
19433
|
+
const prose = text.replace(/`[^`]*`/g, "");
|
|
19434
|
+
for (const m of prose.matchAll(/\]\(([^)\s]*)[^)]*\)/g)) {
|
|
19435
|
+
if (!/^https?:\/\/\S+$/i.test(m[1])) issues.push({ message: `Links in a lesson must go to an http(s) address, not "${m[1]}"`, line });
|
|
19436
|
+
}
|
|
19437
|
+
}
|
|
19438
|
+
return issues;
|
|
19439
|
+
}
|
|
19440
|
+
|
|
19020
19441
|
// ../frontend/src/dsl/imports.ts
|
|
19021
19442
|
function joinImportPath(fromPath, importPath) {
|
|
19022
19443
|
const parts = importPath.startsWith("/") || !fromPath ? [] : fromPath.split("/").slice(0, -1);
|
|
@@ -19111,6 +19532,7 @@ function validateProblem(problem, engine = defaultEngine) {
|
|
|
19111
19532
|
if (!problem.statement.includes(`**${name}**`)) add(PROBLEM_MD, `The statement must name the use case **${name}** in bold`);
|
|
19112
19533
|
if (!problem.solution.includes(`usecase "${name}"`)) add(SOLUTION, `The solution must define usecase "${name}" from the traffic`);
|
|
19113
19534
|
}
|
|
19535
|
+
if (problem.lesson !== void 0) for (const issue of lessonIssues(problem.lesson)) add(LESSON_MD, issue.message, issue.line);
|
|
19114
19536
|
const importLine = problem.given.split("\n").findIndex((l) => /^\s*import /.test(l));
|
|
19115
19537
|
if (importLine >= 0) add(GIVEN, "The given must not import anything", importLine + 1);
|
|
19116
19538
|
report(GIVEN, parse(problem.given).diagnostics.filter((d) => d.severity === "error"));
|
|
@@ -19486,7 +19908,7 @@ ${PROBLEM_USAGE}`);
|
|
|
19486
19908
|
}
|
|
19487
19909
|
|
|
19488
19910
|
// src/cli.ts
|
|
19489
|
-
var
|
|
19911
|
+
var VERSION2 = true ? "0.8.0" : "dev";
|
|
19490
19912
|
var USAGE = `Usage:
|
|
19491
19913
|
proschi check [--strict] [--format text|github|json] [--openapi <node>=<spec>]... <file|dir>...
|
|
19492
19914
|
proschi parse <file>
|
|
@@ -19548,7 +19970,7 @@ function format2(results, style) {
|
|
|
19548
19970
|
function run(argv, out = console.log, err = console.error) {
|
|
19549
19971
|
const [command, ...rest] = argv;
|
|
19550
19972
|
if (command === "--version" || command === "-v") {
|
|
19551
|
-
out(
|
|
19973
|
+
out(VERSION2);
|
|
19552
19974
|
return 0;
|
|
19553
19975
|
}
|
|
19554
19976
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|